mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
Drawing board on theme dependent palette (#9750)
* Drawing board colors palette now depends on UI color theme Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com> * Desktop app for windows bug fixes. Formatting and style fixes. Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com> * Desktop app bug fix. Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com> * Theme unsubscribing fix. Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com> * Formatting. Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com> * Typo fix. Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com> --------- Signed-off-by: Denis Gladkiy <denis.gladkiy@hardcoreeng.com>
This commit is contained in:
+12
-1
@@ -1,3 +1,5 @@
|
||||
const SVELTE_MOCKS_PATH = '<rootDir>/../packages/presentation/src/__mocks__'
|
||||
|
||||
module.exports = {
|
||||
projects: [
|
||||
{
|
||||
@@ -10,7 +12,16 @@ module.exports = {
|
||||
displayName: 'jsdom',
|
||||
testEnvironment: 'jsdom',
|
||||
preset: 'ts-jest',
|
||||
testMatch: ['<rootDir>/src/__test__/ui/**/*.test.ts']
|
||||
testMatch: ['<rootDir>/src/__test__/ui/**/*.test.ts'],
|
||||
moduleNameMapper: {
|
||||
'^@hcengineering/platform-rig/profiles/ui/svelte$': `${SVELTE_MOCKS_PATH}/svelte-runtime.ts`,
|
||||
'^svelte/store$': `${SVELTE_MOCKS_PATH}/svelte-store.ts`,
|
||||
'^svelte/transition$': `${SVELTE_MOCKS_PATH}/svelte-transition.ts`,
|
||||
'^svelte/animate$': `${SVELTE_MOCKS_PATH}/svelte-animate.ts`,
|
||||
'^svelte$': `${SVELTE_MOCKS_PATH}/svelte.ts`,
|
||||
'\\.svelte$': `${SVELTE_MOCKS_PATH}/svelte-component.ts`
|
||||
},
|
||||
setupFilesAfterEnv: [`${SVELTE_MOCKS_PATH}/setup.ts`]
|
||||
}
|
||||
],
|
||||
roots: ["./src", "./tests"],
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
--huly-history-box-left-indent: 0;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
[data-theme="theme-dark"] {
|
||||
--bg-secondary: #323233;
|
||||
--bg-tertiary: #252526;
|
||||
--bg-hover: #2a2d2e;
|
||||
@@ -125,7 +125,7 @@
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .desktop-app-dropdown-menu {
|
||||
[data-theme="theme-dark"] .desktop-app-dropdown-menu {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ import { setupTitleBarMenu } from './titleBarMenu'
|
||||
import { defineScreenShare, defineGetDisplayMedia } from './screenShare'
|
||||
import { CommandLogout, CommandSelectWorkspace, CommandOpenSettings, CommandOpenInbox, CommandOpenPlanner, CommandOpenOffice, CommandOpenApplication, LaunchApplication, NotificationParams } from './types'
|
||||
import { ipcMainExposed } from './typesUtils'
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { themeStore, ThemeVariant } from '@hcengineering/theme'
|
||||
import type { Application } from '@hcengineering/workbench'
|
||||
import { isAllowedToRole } from '@hcengineering/workbench-resources'
|
||||
|
||||
@@ -50,15 +50,14 @@ window.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
themeStore.subscribe((themeOptions) => {
|
||||
if (themeOptions != null) {
|
||||
const isDarkTheme = themeOptions.dark
|
||||
menuBar.setTheme(isDarkTheme ? 'dark' : 'light')
|
||||
menuBar.setTheme(themeOptions.variant)
|
||||
}
|
||||
})
|
||||
|
||||
void ipcMain.isOsUsingDarkTheme().then((isDarkTheme) => {
|
||||
menuBar.setTheme(isDarkTheme ? 'dark' : 'light')
|
||||
menuBar.setTheme(isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light)
|
||||
}).catch(() => {
|
||||
menuBar.setTheme('light') // fallback
|
||||
menuBar.setTheme(ThemeVariant.Light) // fallback
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+473
-473
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
@echo off
|
||||
for /f %%i in ('node ../common/scripts/show_version.js') do set MODEL_VERSION=%%i
|
||||
for /f %%i in ('node ../common/scripts/show_tag.js') do set VERSION=%%i
|
||||
set NODE_ENV=development
|
||||
electron --no-sandbox --trace-warnings ./
|
||||
@@ -0,0 +1,11 @@
|
||||
$MODEL_VERSION = node ../common/scripts/show_version.js | Out-String
|
||||
$MODEL_VERSION = $MODEL_VERSION.Trim()
|
||||
|
||||
$VERSION = node ../common/scripts/show_tag.js | Out-String
|
||||
$VERSION = $VERSION.Trim()
|
||||
|
||||
# Set environment variable
|
||||
$env:NODE_ENV = "development"
|
||||
|
||||
# Run Electron
|
||||
electron --no-sandbox ./
|
||||
@@ -1,6 +1,8 @@
|
||||
const SVELTE_MOCKS_PATH = '<rootDir>/src/__mocks__'
|
||||
|
||||
module.exports = {
|
||||
projects: [
|
||||
// Default configuration for most tests (node environment)
|
||||
// default configuration for most tests (node environment)
|
||||
{
|
||||
displayName: 'node',
|
||||
preset: 'ts-jest',
|
||||
@@ -8,12 +10,26 @@ module.exports = {
|
||||
testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
|
||||
testPathIgnorePatterns: ['.*drawing\\.test\\.ts$']
|
||||
},
|
||||
// Configuration for drawing tests (jsdom environment)
|
||||
// configuration for drawing tests (jsdom environment)
|
||||
{
|
||||
displayName: 'jsdom',
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
testMatch: ['**/drawing.test.ts']
|
||||
testMatch: ['**/drawing.test.ts'],
|
||||
moduleNameMapper: {
|
||||
'^@hcengineering/platform-rig/profiles/ui/svelte$': `${SVELTE_MOCKS_PATH}/svelte-runtime.ts`,
|
||||
'^svelte/store$': `${SVELTE_MOCKS_PATH}/svelte-store.ts`,
|
||||
'^svelte/transition$': `${SVELTE_MOCKS_PATH}/svelte-transition.ts`,
|
||||
'^svelte/animate$': `${SVELTE_MOCKS_PATH}/svelte-animate.ts`,
|
||||
'^svelte$': `${SVELTE_MOCKS_PATH}/svelte.ts`,
|
||||
'\\.svelte$': `${SVELTE_MOCKS_PATH}/svelte-component.ts`
|
||||
},
|
||||
// setup files to run before tests
|
||||
setupFilesAfterEnv: [`${SVELTE_MOCKS_PATH}/setup.ts`],
|
||||
// allow transformation of ES modules in case we encounter them
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(svelte|@sveltejs|@testing-library)/)'
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Nástroj guma",
|
||||
"PanTool": "Nástroj posun",
|
||||
"TextTool": "Nástroj text",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Spravovat barevné předvolby"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Radiergummi-Werkzeug",
|
||||
"PanTool": "Verschieben-Werkzeug",
|
||||
"TextTool": "Text-Werkzeug",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Farbpresets verwalten"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Eraser tool",
|
||||
"PanTool": "Pan tool",
|
||||
"TextTool": "Text tool",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Manage color presets"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Herramienta borrador",
|
||||
"PanTool": "Herramienta mover",
|
||||
"TextTool": "Herramienta texto",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Gestionar preajustes de color"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Outil gomme",
|
||||
"PanTool": "Outil déplacement",
|
||||
"TextTool": "Outil texte",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Gérer les préréglages de couleur"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Strumento gomma",
|
||||
"PanTool": "Strumento sposta",
|
||||
"TextTool": "Strumento testo",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Gestisci i preset di colore"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "消しゴムツール",
|
||||
"PanTool": "パンツール",
|
||||
"TextTool": "テキストツール",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "カラープリセットを管理"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Ferramenta borracha",
|
||||
"PanTool": "Ferramenta mover",
|
||||
"TextTool": "Ferramenta texto",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Gerenciar predefinições de cor"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "Инструмент ластик",
|
||||
"PanTool": "Инструмент перемещения",
|
||||
"TextTool": "Инструмент текст",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "Управление цветовыми пресетами"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -53,7 +53,6 @@
|
||||
"EraserTool": "橡皮擦工具",
|
||||
"PanTool": "移动工具",
|
||||
"TextTool": "文字工具",
|
||||
"ColorTooltip": "{color}",
|
||||
"PaletteManagementMenu": "管理颜色预设"
|
||||
},
|
||||
"status": {
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
"@hcengineering/uploader": "^0.6.0",
|
||||
"@hcengineering/view": "^0.6.13",
|
||||
"@hcengineering/emoji": "^0.6.0",
|
||||
"@hcengineering/theme": "^0.6.5",
|
||||
"fast-equals": "^5.2.2",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"svelte": "^4.2.19",
|
||||
|
||||
@@ -15,9 +15,10 @@
|
||||
|
||||
import '@testing-library/jest-dom'
|
||||
|
||||
import { makeCommandUid, drawing } from '../drawing'
|
||||
import type { DrawTextCmd, CommandUid, DrawingTool, DrawingProps, DrawingCmd } from '../drawing'
|
||||
import { makeCanvasPoint } from '../drawingUtils'
|
||||
import { makeCommandUid, type CommandUid, type DrawTextCmd, type DrawingCmd } from '../drawingCommand'
|
||||
import { ThemeAwareColor, type ColorsList } from '../drawingColors'
|
||||
import { drawing, type DrawingTool, type DrawingProps } from '../drawing'
|
||||
import { type ColorMetaNameOrHex, makeCanvasPoint } from '../drawingUtils'
|
||||
|
||||
const fakeCanvasContext = {
|
||||
clearRect: jest.fn(),
|
||||
@@ -120,7 +121,10 @@ describe('drawing module tests', () => {
|
||||
|
||||
it('create a drawing board', () => {
|
||||
const drawingBoard = drawing(drawingPlugInPoint, {
|
||||
colorsList: [['alpha', new ThemeAwareColor('red', 'yellow')]],
|
||||
readonly: false,
|
||||
getCurrentTheme: () => 'theme-light',
|
||||
subscribeOnThemeChange: () => {},
|
||||
imageWidth: 40,
|
||||
imageHeight: 40,
|
||||
commands: []
|
||||
@@ -130,7 +134,7 @@ describe('drawing module tests', () => {
|
||||
})
|
||||
|
||||
describe('text editing', () => {
|
||||
const DefaultPenColor: string = 'red'
|
||||
const DefaultPenColor: ColorMetaNameOrHex = 'red' as ColorMetaNameOrHex
|
||||
const DefaultTool: DrawingTool = 'pen'
|
||||
const EmptyCommandUid = '' as CommandUid
|
||||
const DefaultDrawingBoardWidth = 200
|
||||
@@ -147,7 +151,7 @@ describe('drawing module tests', () => {
|
||||
pos: makeCanvasPoint(10, 10),
|
||||
fontSize: 12,
|
||||
fontFace: '"IBM Plex Sans"',
|
||||
color: 'green',
|
||||
color: 'green' as ColorMetaNameOrHex,
|
||||
...overrides
|
||||
}
|
||||
return { textCommandUid, textCommand }
|
||||
@@ -159,7 +163,11 @@ describe('drawing module tests', () => {
|
||||
): { drawingBoard: ReturnType<typeof drawing>, initialState: DrawingProps } => {
|
||||
const commands = existingTextCommand === undefined ? [] : [existingTextCommand]
|
||||
|
||||
const initialState = {
|
||||
const colorsList: ColorsList = [['alpha', new ThemeAwareColor('red', 'yellow')]]
|
||||
const initialState: any = {
|
||||
colorsList,
|
||||
getCurrentTheme: () => 'theme-light',
|
||||
subscribeOnThemeChange: () => {},
|
||||
readonly: false,
|
||||
imageWidth: DefaultDrawingBoardWidth,
|
||||
imageHeight: DefaultDrawingBoardHeight,
|
||||
@@ -222,7 +230,7 @@ describe('drawing module tests', () => {
|
||||
const { drawingBoard, initialState: boardState } = createDrawingBoard(undefined, { cmdAdded: commandAddedSpy })
|
||||
|
||||
{
|
||||
const colorToSet = 'blue'
|
||||
const colorToSet = 'blue' as ColorMetaNameOrHex
|
||||
// simulating: user selected text tool
|
||||
drawingBoard.update?.({ ...boardState, tool: 'text' })
|
||||
// simulating: user selected a color
|
||||
@@ -235,12 +243,12 @@ describe('drawing module tests', () => {
|
||||
|
||||
// change color to something else
|
||||
{
|
||||
const colorToSet = 'blue'
|
||||
const colorToSet = 'blue' as ColorMetaNameOrHex
|
||||
drawingBoard.update?.({ ...boardState, tool: 'text', penColor: colorToSet, changingCmdId: EmptyCommandUid })
|
||||
expect(getTextEditorColor(drawingPlugInPoint)).toBe(colorToSet)
|
||||
}
|
||||
|
||||
const lastSetColor = 'red'
|
||||
const lastSetColor = 'red' as ColorMetaNameOrHex
|
||||
drawingBoard.update?.({ ...boardState, tool: 'text', penColor: lastSetColor, changingCmdId: EmptyCommandUid })
|
||||
expect(getTextEditorColor(drawingPlugInPoint)).toBe(lastSetColor)
|
||||
|
||||
@@ -250,7 +258,12 @@ describe('drawing module tests', () => {
|
||||
// new tool selection - editor should have been closed
|
||||
drawingBoard.update?.({ ...boardState, tool: 'pen', penColor: lastSetColor, changingCmdId: EmptyCommandUid })
|
||||
// changing color for the pen
|
||||
drawingBoard.update?.({ ...boardState, tool: 'pen', penColor: 'magenta', changingCmdId: undefined })
|
||||
drawingBoard.update?.({
|
||||
...boardState,
|
||||
tool: 'pen',
|
||||
penColor: 'magenta' as ColorMetaNameOrHex,
|
||||
changingCmdId: undefined
|
||||
})
|
||||
|
||||
setTextEditorText(drawingPlugInPoint, setTextContent + setTextContent)
|
||||
|
||||
@@ -342,7 +355,7 @@ describe('drawing module tests', () => {
|
||||
const newText = 'New Text'
|
||||
setTextEditorText(drawingPlugInPoint, newText)
|
||||
|
||||
const newColor = 'yellow'
|
||||
const newColor = 'yellow' as ColorMetaNameOrHex
|
||||
drawingBoard.update?.({ ...boardState, tool: 'text', changingCmdId: textCommandUid, penColor: newColor })
|
||||
|
||||
// commit text editing
|
||||
@@ -453,7 +466,10 @@ describe('drawing module tests', () => {
|
||||
const pointerMovedSpy = jest.fn()
|
||||
|
||||
drawing(drawingPlugInPoint, {
|
||||
colorsList: [['alpha', new ThemeAwareColor('red', 'yellow')]],
|
||||
readonly: false,
|
||||
getCurrentTheme: () => 'theme-light',
|
||||
subscribeOnThemeChange: () => {},
|
||||
imageWidth: 400,
|
||||
imageHeight: 300,
|
||||
commands: [],
|
||||
@@ -485,13 +501,16 @@ describe('drawing module tests', () => {
|
||||
const backgroundImageWidth = 400
|
||||
const backgroundImageHeight = 300
|
||||
drawing(drawingPlugInPoint, {
|
||||
colorsList: [['alpha', new ThemeAwareColor('red', 'yellow')]],
|
||||
readonly: false,
|
||||
getCurrentTheme: () => 'theme-light',
|
||||
subscribeOnThemeChange: () => {},
|
||||
autoSize: false,
|
||||
imageWidth: backgroundImageWidth,
|
||||
imageHeight: backgroundImageHeight,
|
||||
commands: [],
|
||||
tool: 'pen',
|
||||
penColor: 'red',
|
||||
penColor: 'red' as ColorMetaNameOrHex,
|
||||
cmdAdded: commandAddedSpy,
|
||||
pointerMoved: pointerMovedSpy
|
||||
})
|
||||
|
||||
@@ -14,9 +14,9 @@
|
||||
//
|
||||
|
||||
import { DrawingCommandsProcessor, UndoRedoAvailability } from '../drawingCommandsProcessor'
|
||||
import { makeCommandUid, type DrawingCmd, type DrawTextCmd, type DrawLineCmd } from '../drawing'
|
||||
import { makeCommandUid, type DrawingCmd, type DrawTextCmd, type DrawLineCmd } from '../drawingCommand'
|
||||
import { type Array as YArray, Doc as YDoc } from 'yjs'
|
||||
import { makeCanvasPoint } from '../drawingUtils'
|
||||
import { type ColorMetaNameOrHex, makeCanvasPoint } from '../drawingUtils'
|
||||
|
||||
const makeTextCommand = (overrides: Partial<DrawTextCmd> = {}): DrawTextCmd => ({
|
||||
id: makeCommandUid(),
|
||||
@@ -25,7 +25,7 @@ const makeTextCommand = (overrides: Partial<DrawTextCmd> = {}): DrawTextCmd => (
|
||||
pos: makeCanvasPoint(13, 17),
|
||||
fontSize: 11,
|
||||
fontFace: 'Arial',
|
||||
color: 'red',
|
||||
color: 'red' as ColorMetaNameOrHex,
|
||||
...overrides
|
||||
})
|
||||
|
||||
@@ -34,7 +34,7 @@ const makeLineCommand = (overrides: Partial<DrawLineCmd> = {}): DrawLineCmd => (
|
||||
type: 'line',
|
||||
lineWidth: 3,
|
||||
erasing: false,
|
||||
penColor: 'blue',
|
||||
penColor: 'blue' as ColorMetaNameOrHex,
|
||||
points: [makeCanvasPoint(1, 3), makeCanvasPoint(11, 13)],
|
||||
...overrides
|
||||
})
|
||||
@@ -163,13 +163,13 @@ describe('DrawingCommandsProcessor Tests', () => {
|
||||
it('existing command change', () => {
|
||||
const originalCommand = makeTextCommand({
|
||||
text: 'Original',
|
||||
color: 'red'
|
||||
color: 'red' as ColorMetaNameOrHex
|
||||
})
|
||||
|
||||
const changedCommand: DrawTextCmd = {
|
||||
...originalCommand,
|
||||
text: 'Changed',
|
||||
color: 'blue'
|
||||
color: 'blue' as ColorMetaNameOrHex
|
||||
}
|
||||
|
||||
systemUnderTest.addCommand(originalCommand)
|
||||
@@ -189,19 +189,19 @@ describe('DrawingCommandsProcessor Tests', () => {
|
||||
it('order preservation', () => {
|
||||
const first = makeTextCommand({
|
||||
text: 'First',
|
||||
color: 'red'
|
||||
color: 'red' as ColorMetaNameOrHex
|
||||
})
|
||||
|
||||
const second = makeTextCommand({
|
||||
text: 'Second',
|
||||
pos: makeCanvasPoint(20, 20),
|
||||
color: 'blue'
|
||||
color: 'blue' as ColorMetaNameOrHex
|
||||
})
|
||||
|
||||
const third = makeTextCommand({
|
||||
text: 'Third',
|
||||
pos: makeCanvasPoint(30, 30),
|
||||
color: 'green'
|
||||
color: 'green' as ColorMetaNameOrHex
|
||||
})
|
||||
|
||||
systemUnderTest.addCommand(first)
|
||||
@@ -391,7 +391,7 @@ describe('DrawingCommandsProcessor Tests', () => {
|
||||
const changedTextCommand: DrawTextCmd = {
|
||||
...textCommand,
|
||||
text: 'Changed Text',
|
||||
color: 'green'
|
||||
color: 'green' as ColorMetaNameOrHex
|
||||
}
|
||||
systemUnderTest.changeCommand(changedTextCommand)
|
||||
expect(systemUnderTest.snapshot()).toEqual([changedTextCommand, lineCommand])
|
||||
@@ -419,7 +419,7 @@ describe('DrawingCommandsProcessor Tests', () => {
|
||||
const cmd = makeTextCommand({
|
||||
text: `Command ${i}`,
|
||||
pos: makeCanvasPoint(i * 10, i * 10),
|
||||
color: 'black'
|
||||
color: 'black' as ColorMetaNameOrHex
|
||||
})
|
||||
commands.push(cmd)
|
||||
systemUnderTest.addCommand(cmd)
|
||||
|
||||
@@ -13,7 +13,83 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { rescaleToFitAspectRatio, scalePoint, offsetPoint, offsetInParent, type Point } from '../drawingUtils'
|
||||
import { ThemeAwareColor, type ColorsList, DrawingBoardColoringSetup, metaColorNameToHex } from '../drawingColors'
|
||||
import {
|
||||
rescaleToFitAspectRatio,
|
||||
scalePoint,
|
||||
offsetPoint,
|
||||
offsetInParent,
|
||||
type Point,
|
||||
type ColorMetaName,
|
||||
type ColorMetaNameOrHex
|
||||
} from '../drawingUtils'
|
||||
import { ThemeVariant, type ThemeVariantType } from '@hcengineering/theme'
|
||||
|
||||
jest.mock('@hcengineering/theme', () => ({
|
||||
ThemeVariant: {
|
||||
Dark: 'dark',
|
||||
Light: 'light'
|
||||
}
|
||||
}))
|
||||
|
||||
const StubPlatformColors: Record<string, { light: any, dark: any }> = {
|
||||
Firework: {
|
||||
light: {
|
||||
name: 'Firework',
|
||||
color: '#D15045',
|
||||
title: '#C03B2F',
|
||||
icon: '#D15045',
|
||||
number: '#C03B2F',
|
||||
background: '#C03B2F'
|
||||
},
|
||||
dark: {
|
||||
name: 'Firework',
|
||||
color: '#D15045',
|
||||
title: '#FFFFFF',
|
||||
icon: '#D15045',
|
||||
number: '#FFFFFF',
|
||||
background: '#C03B2F'
|
||||
}
|
||||
},
|
||||
Sky: {
|
||||
light: {
|
||||
name: 'Sky',
|
||||
color: '#4CA6EE',
|
||||
title: '#1F90EA',
|
||||
icon: '#4CA6EE',
|
||||
number: '#1F90EA',
|
||||
background: '#1F90EA'
|
||||
},
|
||||
dark: { name: 'Sky', color: '#4CA6EE', title: '#FFFFFF', icon: '#4CA6EE', number: '#FFFFFF', background: '#1F90EA' }
|
||||
},
|
||||
Grass: {
|
||||
light: {
|
||||
name: 'Grass',
|
||||
color: '#83AF12',
|
||||
title: '#60810E',
|
||||
icon: '#83AF12',
|
||||
number: '#60810E',
|
||||
background: '#60810E'
|
||||
},
|
||||
dark: {
|
||||
name: 'Grass',
|
||||
color: '#83AF12',
|
||||
title: '#FFFFFF',
|
||||
icon: '#83AF12',
|
||||
number: '#FFFFFF',
|
||||
background: '#83AF12'
|
||||
}
|
||||
}
|
||||
}
|
||||
jest.mock('@hcengineering/ui', () => ({
|
||||
getPlatformColorByName: jest.fn().mockImplementation((name: string, darkTheme: boolean) => {
|
||||
const colorDefinition = StubPlatformColors[name]
|
||||
if (colorDefinition == null) {
|
||||
return undefined
|
||||
}
|
||||
return darkTheme ? colorDefinition.dark : colorDefinition.light
|
||||
})
|
||||
}))
|
||||
|
||||
describe('drawingUtils module tests', () => {
|
||||
describe('scalePoint', () => {
|
||||
@@ -210,4 +286,128 @@ describe('drawingUtils module tests', () => {
|
||||
expect(result).toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('ThemeAwareColor', () => {
|
||||
interface ThemeTestCase {
|
||||
theme: ThemeVariantType
|
||||
expected: string
|
||||
}
|
||||
const themeUnknownColorsTestCases: ThemeTestCase[] = [
|
||||
{ theme: ThemeVariant.Dark, expected: 'DarkColor' },
|
||||
{ theme: ThemeVariant.Light, expected: 'LightColor' }
|
||||
]
|
||||
it.each(themeUnknownColorsTestCases)("materialize unknown color for '$theme' theme", ({ theme, expected }) => {
|
||||
const systemUnderTest = new ThemeAwareColor('DarkColor', 'LightColor')
|
||||
const result = systemUnderTest.materialize(theme)
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
const themeKnownColorsTestCases: ThemeTestCase[] = [
|
||||
{ theme: ThemeVariant.Dark, expected: StubPlatformColors.Firework.dark.color },
|
||||
{ theme: ThemeVariant.Light, expected: StubPlatformColors.Firework.light.color }
|
||||
]
|
||||
it.each(themeKnownColorsTestCases)("materialize known color for '$theme' theme", ({ theme, expected }) => {
|
||||
const systemUnderTest = new ThemeAwareColor(
|
||||
StubPlatformColors.Firework.dark.name,
|
||||
StubPlatformColors.Firework.light.name
|
||||
)
|
||||
const result = systemUnderTest.materialize(theme)
|
||||
expect(result).toBe(expected)
|
||||
})
|
||||
|
||||
it('materialize hex colors', () => {
|
||||
const expectedDarkColor = '#000000'
|
||||
const expectedLightColor = '#FFFFFF'
|
||||
const systemUnderTest = new ThemeAwareColor(expectedDarkColor, expectedLightColor)
|
||||
|
||||
expect(systemUnderTest.materialize(ThemeVariant.Dark)).toBe(expectedDarkColor)
|
||||
expect(systemUnderTest.materialize(ThemeVariant.Light)).toBe(expectedLightColor)
|
||||
})
|
||||
})
|
||||
|
||||
describe('DrawingBoardColoringSetup', () => {
|
||||
const stubColorsList: ColorsList = [
|
||||
['alpha', new ThemeAwareColor('#000000', '#FFFFFF')],
|
||||
['beta', new ThemeAwareColor('#FF0000', '#00FF00')],
|
||||
['gamma', new ThemeAwareColor('#0000FF', '#FFFF00')]
|
||||
]
|
||||
|
||||
it('construction', () => {
|
||||
const systemUnderTest = new DrawingBoardColoringSetup(stubColorsList)
|
||||
expect(systemUnderTest.allColors).toBe(stubColorsList)
|
||||
})
|
||||
|
||||
it('colorByName, known color', () => {
|
||||
const systemUnderTest = new DrawingBoardColoringSetup(stubColorsList)
|
||||
|
||||
const actualColor = systemUnderTest.colorByName('alpha')
|
||||
|
||||
const expectedColor = stubColorsList[0][1]
|
||||
expect(actualColor).toBe(expectedColor)
|
||||
})
|
||||
|
||||
it('colorByName, unknown color', () => {
|
||||
const systemUnderTest = new DrawingBoardColoringSetup(stubColorsList)
|
||||
|
||||
const actualColor = systemUnderTest.colorByName('unknown' as ColorMetaName)
|
||||
|
||||
expect(actualColor).toBeUndefined()
|
||||
})
|
||||
|
||||
it('construction with empty list', () => {
|
||||
const systemUnderTest = new DrawingBoardColoringSetup([])
|
||||
|
||||
expect(systemUnderTest.allColors).toEqual([])
|
||||
expect(systemUnderTest.colorByName('alpha')).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('metaColorNameToHex', () => {
|
||||
const AlphaDarkColor = '#000000'
|
||||
const AlphaLightColor = '#FFFFFF'
|
||||
const testColorsList: ColorsList = [
|
||||
['alpha', new ThemeAwareColor(AlphaDarkColor, AlphaLightColor)],
|
||||
['beta', new ThemeAwareColor('Firework', 'Sky')],
|
||||
['gamma', new ThemeAwareColor('Grass', 'Grass')]
|
||||
]
|
||||
const colorsSetupStub = new DrawingBoardColoringSetup(testColorsList)
|
||||
|
||||
it('hex color, 7 characters', () => {
|
||||
const hexColor = '#FF5733' as ColorMetaNameOrHex
|
||||
const actualColor = metaColorNameToHex(hexColor, ThemeVariant.Dark, colorsSetupStub)
|
||||
expect(actualColor).toBe(hexColor)
|
||||
})
|
||||
|
||||
it('hex color, 4 characters', () => {
|
||||
const hexColor = '#F53' as ColorMetaNameOrHex
|
||||
const actualColor = metaColorNameToHex(hexColor, ThemeVariant.Dark, colorsSetupStub)
|
||||
expect(actualColor).toBe(hexColor)
|
||||
})
|
||||
|
||||
it('meta color name, dark theme', () => {
|
||||
const colorName = 'alpha' as ColorMetaNameOrHex
|
||||
const actualColor = metaColorNameToHex(colorName, ThemeVariant.Dark, colorsSetupStub)
|
||||
expect(actualColor).toBe(AlphaDarkColor)
|
||||
})
|
||||
|
||||
it('meta color name, light theme', () => {
|
||||
const colorName = 'alpha' as ColorMetaNameOrHex
|
||||
const actualColor = metaColorNameToHex(colorName, ThemeVariant.Light, colorsSetupStub)
|
||||
expect(actualColor).toBe(AlphaLightColor)
|
||||
})
|
||||
|
||||
it('unknown color name', () => {
|
||||
const unknownColor = 'unknown' as ColorMetaNameOrHex
|
||||
const actualColor = metaColorNameToHex(unknownColor, ThemeVariant.Dark, colorsSetupStub)
|
||||
expect(actualColor).toBe(unknownColor)
|
||||
})
|
||||
|
||||
it('should handle platform colors with same name for both themes', () => {
|
||||
const gammaColor = 'gamma' as ColorMetaNameOrHex
|
||||
const darkResult = metaColorNameToHex(gammaColor, ThemeVariant.Dark, colorsSetupStub)
|
||||
const lightResult = metaColorNameToHex(gammaColor, ThemeVariant.Light, colorsSetupStub)
|
||||
|
||||
expect(darkResult).toBe('#83AF12')
|
||||
expect(lightResult).toBe('#83AF12')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
# Svelte Testing Strategy
|
||||
|
||||
This project uses an enhanced mocking approach for Svelte components and stores in Jest tests, inspired by Svelte Testing Library patterns but adapted for our current Jest setup.
|
||||
|
||||
## Current Approach: Enhanced Svelte Mocking
|
||||
|
||||
### What We Have
|
||||
- **Robust Svelte Store Mocks**: Our `svelte-store.js` mock provides functional implementations of `writable`, `derived`, and `readable` stores with testing helpers
|
||||
- **Complete Svelte Ecosystem Coverage**: Mocks for `svelte`, `svelte/transition`, `svelte/animate`, and `.svelte` components
|
||||
- **Jest Integration**: Works seamlessly with our existing Jest configuration without requiring ESM mode
|
||||
|
||||
### Benefits
|
||||
- ✅ **Simple Setup**: No complex ESM configuration required
|
||||
- ✅ **Fast Tests**: Lightweight mocks don't slow down test execution
|
||||
- ✅ **Focused Testing**: Tests logic rather than UI components
|
||||
- ✅ **Backward Compatible**: Works with existing Jest setup
|
||||
|
||||
### When This Approach Works Best
|
||||
- Testing business logic that uses Svelte stores
|
||||
- Testing utility functions that depend on Svelte ecosystem
|
||||
- Integration tests that need to mock Svelte dependencies
|
||||
- When you don't need to test actual Svelte component rendering
|
||||
|
||||
## Future: Migration to Svelte Testing Library
|
||||
|
||||
### When to Consider Migration
|
||||
Consider migrating to full Svelte Testing Library when you need to:
|
||||
- **Test Svelte Components**: Render and interact with actual `.svelte` components
|
||||
- **Test User Interactions**: Simulate clicks, form inputs, etc. on Svelte components
|
||||
- **Test Component Props**: Verify how components respond to different prop values
|
||||
- **Test Component Events**: Verify custom event dispatching
|
||||
|
||||
### Migration Steps (Future)
|
||||
If you need full Svelte Testing Library support:
|
||||
|
||||
1. **Install Dependencies**:
|
||||
```bash
|
||||
npm install --save-dev @testing-library/svelte svelte-jester jest-environment-jsdom
|
||||
```
|
||||
|
||||
2. **Update Jest Configuration** to ESM mode:
|
||||
```javascript
|
||||
export default {
|
||||
transform: {
|
||||
'^.+\\.svelte(\\.(js|ts))?$': 'svelte-jester',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!@testing-library/svelte/)',
|
||||
],
|
||||
moduleFileExtensions: ['js', 'svelte', 'ts'],
|
||||
extensionsToTreatAsEsm: ['.svelte'],
|
||||
testEnvironment: 'jsdom',
|
||||
setupFilesAfterEnv: ['<rootDir>/jest-setup.js'],
|
||||
}
|
||||
```
|
||||
|
||||
3. **Update Package.json** scripts:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"test": "npx --node-options=\"--experimental-vm-modules\" jest src"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example Test with Svelte Testing Library
|
||||
```javascript
|
||||
import { render, screen, fireEvent } from '@testing-library/svelte'
|
||||
import MyComponent from './MyComponent.svelte'
|
||||
|
||||
test('renders and handles click', async () => {
|
||||
render(MyComponent, { props: { title: 'Hello' } })
|
||||
|
||||
const button = screen.getByRole('button')
|
||||
await fireEvent.click(button)
|
||||
|
||||
expect(screen.getByText('Clicked!')).toBeInTheDocument()
|
||||
})
|
||||
```
|
||||
|
||||
## Current Mock Files
|
||||
|
||||
### `__mocks__/svelte-store.js`
|
||||
- Functional implementations of Svelte stores
|
||||
- Testing helpers like `_getValue()` and `get()`
|
||||
- Proper subscriber management
|
||||
|
||||
### `__mocks__/svelte.js`
|
||||
- Core Svelte lifecycle functions (`onMount`, `onDestroy`)
|
||||
- Utility functions (`tick`, `createEventDispatcher`)
|
||||
|
||||
### `__mocks__/svelte-component.js`
|
||||
- Mock for `.svelte` files
|
||||
- Returns simple DOM element for component imports
|
||||
|
||||
### `__mocks__/svelte-transition.js` & `__mocks__/svelte-animate.js`
|
||||
- Animation and transition function mocks
|
||||
- Return basic CSS transformation objects
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Use Store Testing Helpers**: Leverage `_getValue()` for easy store value assertions
|
||||
2. **Focus on Logic**: Test business logic, not UI rendering details
|
||||
3. **Mock Appropriately**: Only mock what you need to avoid test complexity
|
||||
4. **Consider Migration**: Plan for Svelte Testing Library when component testing becomes necessary
|
||||
|
||||
## Example Usage
|
||||
|
||||
```javascript
|
||||
import { get } from '../__mocks__/svelte-store.js'
|
||||
|
||||
test('store behavior', () => {
|
||||
const store = writable(0)
|
||||
store.set(5)
|
||||
|
||||
expect(get(store)).toBe(5)
|
||||
expect(store._getSubscriberCount()).toBe(0)
|
||||
})
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// TypeScript setup for Jest test environment
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
getComputedStyle: (element: Element, pseudoElt?: string | null) => CSSStyleDeclaration
|
||||
}
|
||||
}
|
||||
|
||||
// Mock getComputedStyle
|
||||
Object.defineProperty(window, 'getComputedStyle', {
|
||||
value: (): Partial<CSSStyleDeclaration> => ({
|
||||
getPropertyValue: (): string => ''
|
||||
})
|
||||
})
|
||||
|
||||
// Canvas API mock interface
|
||||
interface MockCanvasRenderingContext2D {
|
||||
clearRect: jest.MockedFunction<() => void>
|
||||
beginPath: jest.MockedFunction<() => void>
|
||||
moveTo: jest.MockedFunction<() => void>
|
||||
lineTo: jest.MockedFunction<() => void>
|
||||
stroke: jest.MockedFunction<() => void>
|
||||
fill: jest.MockedFunction<() => void>
|
||||
strokeText: jest.MockedFunction<() => void>
|
||||
fillText: jest.MockedFunction<() => void>
|
||||
arc: jest.MockedFunction<() => void>
|
||||
save: jest.MockedFunction<() => void>
|
||||
restore: jest.MockedFunction<() => void>
|
||||
translate: jest.MockedFunction<() => void>
|
||||
scale: jest.MockedFunction<() => void>
|
||||
rotate: jest.MockedFunction<() => void>
|
||||
setTransform: jest.MockedFunction<() => void>
|
||||
drawImage: jest.MockedFunction<() => void>
|
||||
createImageData: jest.MockedFunction<() => void>
|
||||
getImageData: jest.MockedFunction<() => void>
|
||||
putImageData: jest.MockedFunction<() => void>
|
||||
measureText: jest.MockedFunction<() => { width: number }>
|
||||
}
|
||||
|
||||
// Canvas API mock if needed
|
||||
Object.defineProperty(HTMLCanvasElement.prototype, 'getContext', {
|
||||
value: (): MockCanvasRenderingContext2D => ({
|
||||
clearRect: jest.fn(),
|
||||
beginPath: jest.fn(),
|
||||
moveTo: jest.fn(),
|
||||
lineTo: jest.fn(),
|
||||
stroke: jest.fn(),
|
||||
fill: jest.fn(),
|
||||
strokeText: jest.fn(),
|
||||
fillText: jest.fn(),
|
||||
arc: jest.fn(),
|
||||
save: jest.fn(),
|
||||
restore: jest.fn(),
|
||||
translate: jest.fn(),
|
||||
scale: jest.fn(),
|
||||
rotate: jest.fn(),
|
||||
setTransform: jest.fn(),
|
||||
drawImage: jest.fn(),
|
||||
createImageData: jest.fn(),
|
||||
getImageData: jest.fn(),
|
||||
putImageData: jest.fn(),
|
||||
measureText: jest.fn().mockReturnValue({ width: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
export {}
|
||||
@@ -0,0 +1,39 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// TypeScript mock for svelte/animate
|
||||
|
||||
export type EasingFunction = (t: number) => number
|
||||
|
||||
export interface AnimationConfig {
|
||||
delay?: number
|
||||
duration?: number | ((len: number) => number)
|
||||
easing?: EasingFunction
|
||||
css?: (t: number, u: number) => string
|
||||
tick?: (t: number, u: number) => void
|
||||
}
|
||||
|
||||
export interface FlipParams {
|
||||
delay?: number
|
||||
duration?: number | ((len: number) => number)
|
||||
easing?: EasingFunction
|
||||
}
|
||||
|
||||
export const flip = (params: FlipParams = {}): AnimationConfig => ({
|
||||
delay: params.delay ?? 0,
|
||||
duration: params.duration ?? ((d: number) => Math.sqrt(d) * 120),
|
||||
easing: params.easing ?? ((t: number) => t),
|
||||
css: (t: number, u: number) => `transform: translate(${u * t}px, ${u * t}px)`
|
||||
})
|
||||
@@ -0,0 +1,18 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// TypeScript mock for .svelte components
|
||||
|
||||
export default 'div' // mock as a simple div element
|
||||
@@ -0,0 +1,22 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// TypeScript mock for @hcengineering/platform-rig/profiles/ui/svelte
|
||||
// This is used in Jest tests to avoid loading the actual Svelte runtime
|
||||
// which uses ES modules that Jest doesn't handle well by default
|
||||
|
||||
// This file is intentionally empty as the import is just for TypeScript definitions
|
||||
// and doesn't need any runtime behavior in tests
|
||||
export default {}
|
||||
@@ -0,0 +1,137 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// Enhanced TypeScript mock for svelte/store inspired by Svelte Testing Library patterns
|
||||
// This provides more accurate Jest-compatible implementations of Svelte stores
|
||||
|
||||
export type Subscriber<T> = (value: T) => void
|
||||
export type Unsubscriber = () => void
|
||||
export type Updater<T> = (value: T) => T
|
||||
export type StartStopNotifier<T> = (set: (value: T) => void) => Unsubscriber | undefined
|
||||
|
||||
export interface Readable<T> {
|
||||
subscribe: (run: Subscriber<T>, invalidate?: any) => Unsubscriber
|
||||
_getValue?: () => T
|
||||
}
|
||||
|
||||
export interface Writable<T> extends Readable<T> {
|
||||
set: (value: T) => void
|
||||
update: (updater: Updater<T>) => void
|
||||
_getSubscriberCount?: () => number
|
||||
}
|
||||
|
||||
export interface Derived<T> extends Readable<T> {
|
||||
_getValue?: () => T
|
||||
}
|
||||
|
||||
export const writable = <T>(initialValue: T): Writable<T> => {
|
||||
let value = initialValue
|
||||
const subscribers = new Set<Subscriber<T>>()
|
||||
|
||||
const store: Writable<T> = {
|
||||
subscribe: (callback: Subscriber<T>) => {
|
||||
subscribers.add(callback)
|
||||
callback(value)
|
||||
return () => subscribers.delete(callback)
|
||||
},
|
||||
set: (newValue: T) => {
|
||||
value = newValue
|
||||
subscribers.forEach((callback) => {
|
||||
callback(value)
|
||||
})
|
||||
},
|
||||
update: (updater: Updater<T>) => {
|
||||
value = updater(value)
|
||||
subscribers.forEach((callback) => {
|
||||
callback(value)
|
||||
})
|
||||
},
|
||||
_getValue: () => value,
|
||||
_getSubscriberCount: () => subscribers.size
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const derived = <T>(
|
||||
stores: Readable<any> | Array<Readable<any>>,
|
||||
fn: (values: any) => T,
|
||||
initialValue?: T
|
||||
): Derived<T> => {
|
||||
const storeArray = Array.isArray(stores) ? stores : [stores]
|
||||
|
||||
let computedValue: T
|
||||
try {
|
||||
// provide reasonable defaults for testing
|
||||
const values = storeArray.map((store) => {
|
||||
if (store != null && typeof store.subscribe === 'function') {
|
||||
return store._getValue != null ? store._getValue() : []
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
computedValue = Array.isArray(stores) ? fn(values) : fn(values[0])
|
||||
} catch (e) {
|
||||
computedValue = initialValue ?? ([] as any)
|
||||
}
|
||||
|
||||
const store: Derived<T> = {
|
||||
subscribe: (callback: Subscriber<T>) => {
|
||||
callback(computedValue)
|
||||
return () => {}
|
||||
},
|
||||
_getValue: () => computedValue
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const readable = <T>(initialValue: T, startStopNotifier?: StartStopNotifier<T>): Readable<T> => {
|
||||
const store: Readable<T> = {
|
||||
subscribe: (callback: Subscriber<T>) => {
|
||||
callback(initialValue)
|
||||
|
||||
if (typeof startStopNotifier === 'function') {
|
||||
const stop = startStopNotifier((newValue: T) => {
|
||||
callback(newValue)
|
||||
})
|
||||
|
||||
return () => {
|
||||
if (typeof stop === 'function') {
|
||||
stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return () => {}
|
||||
},
|
||||
_getValue: () => initialValue
|
||||
}
|
||||
|
||||
return store
|
||||
}
|
||||
|
||||
export const get = <T>(store: Readable<T>): T => {
|
||||
if (store?._getValue != null) {
|
||||
return store._getValue()
|
||||
}
|
||||
|
||||
let value: T | undefined
|
||||
const unsubscribe = store.subscribe((v: T) => {
|
||||
value = v
|
||||
})
|
||||
unsubscribe()
|
||||
return value as T
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// TypeScript mock for svelte/transition
|
||||
|
||||
export type EasingFunction = (t: number) => number
|
||||
|
||||
export interface TransitionConfig {
|
||||
delay?: number
|
||||
duration?: number | ((from: number, to: number) => number)
|
||||
easing?: EasingFunction
|
||||
css?: (t: number, u: number) => string
|
||||
tick?: (t: number, u: number) => void
|
||||
}
|
||||
|
||||
export interface FadeParams {
|
||||
delay?: number
|
||||
duration?: number
|
||||
easing?: EasingFunction
|
||||
}
|
||||
|
||||
export interface FlyParams extends FadeParams {
|
||||
x?: number
|
||||
y?: number
|
||||
opacity?: number
|
||||
}
|
||||
|
||||
export interface SlideParams extends FadeParams {
|
||||
axis?: 'x' | 'y'
|
||||
}
|
||||
|
||||
export interface ScaleParams extends FadeParams {
|
||||
start?: number
|
||||
opacity?: number
|
||||
}
|
||||
|
||||
export interface DrawParams extends FadeParams {
|
||||
speed?: number
|
||||
}
|
||||
|
||||
export const fade = (params: FadeParams = {}): TransitionConfig => ({
|
||||
delay: params.delay ?? 0,
|
||||
duration: params.duration ?? 400,
|
||||
easing: params.easing ?? ((t: number) => t),
|
||||
css: (t: number, u: number) => `opacity: ${t}`
|
||||
})
|
||||
|
||||
export const fly = (params: FlyParams = {}): TransitionConfig => ({
|
||||
delay: params.delay ?? 0,
|
||||
duration: params.duration ?? 400,
|
||||
easing: params.easing ?? ((t: number) => t),
|
||||
css: (t: number, u: number) =>
|
||||
`transform: translate(${u * (params.x ?? 20)}px, ${u * (params.y ?? 0)}px); opacity: ${t * (params.opacity ?? 1)}`
|
||||
})
|
||||
|
||||
export const slide = (params: SlideParams = {}): TransitionConfig => ({
|
||||
delay: params.delay ?? 0,
|
||||
duration: params.duration ?? 400,
|
||||
easing: params.easing ?? ((t: number) => t),
|
||||
css: (t: number, u: number) => {
|
||||
const property = params.axis === 'x' ? 'width' : 'height'
|
||||
return `${property}: ${t * 100}%`
|
||||
}
|
||||
})
|
||||
|
||||
export const scale = (params: ScaleParams = {}): TransitionConfig => ({
|
||||
delay: params.delay ?? 0,
|
||||
duration: params.duration ?? 400,
|
||||
easing: params.easing ?? ((t: number) => t),
|
||||
css: (t: number, u: number) => `transform: scale(${t * (params.start ?? 1)}); opacity: ${t * (params.opacity ?? 1)}`
|
||||
})
|
||||
|
||||
export const draw = (params: DrawParams = {}): TransitionConfig => ({
|
||||
delay: params.delay ?? 0,
|
||||
duration: params.duration ?? 800,
|
||||
easing: params.easing ?? ((t: number) => t),
|
||||
css: (t: number) => `stroke-dasharray: ${t * (params.speed ?? 100)}`
|
||||
})
|
||||
|
||||
export const crossfade = (): {
|
||||
fallback: typeof fade
|
||||
send: typeof fade
|
||||
receive: typeof fade
|
||||
} => {
|
||||
return {
|
||||
fallback: fade,
|
||||
send: fade,
|
||||
receive: fade
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
// TypeScript mock for svelte
|
||||
|
||||
export type MountCallback = () => undefined | (() => void)
|
||||
export type DestroyCallback = () => void
|
||||
export type EventDispatcher<T extends Record<string, any> = any> = <K extends keyof T>(
|
||||
type: K,
|
||||
detail?: T[K]
|
||||
) => boolean
|
||||
|
||||
export const onMount = (fn: MountCallback): undefined | (() => void) => {
|
||||
if (typeof fn === 'function') {
|
||||
return fn()
|
||||
}
|
||||
}
|
||||
|
||||
export const onDestroy = (fn: DestroyCallback): DestroyCallback => {
|
||||
return fn
|
||||
}
|
||||
|
||||
export const tick = async (): Promise<void> => {
|
||||
await Promise.resolve()
|
||||
}
|
||||
|
||||
export const createEventDispatcher = <T extends Record<string, any> = any>(): EventDispatcher<T> => {
|
||||
return <K extends keyof T>(eventType: K, detail?: T[K]): boolean => {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -16,17 +16,14 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import { resizeObserver } from '@hcengineering/ui'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import {
|
||||
CommandUid,
|
||||
drawing,
|
||||
type DrawingCmd,
|
||||
type DrawingData,
|
||||
type DrawingTool,
|
||||
type DrawTextCmd
|
||||
} from '../drawing'
|
||||
import { drawing, type DrawingData, type DrawingTool } from '../drawing'
|
||||
import DrawingBoardToolbar from './DrawingBoardToolbar.svelte'
|
||||
import { DrawingCommandsProcessor } from '../drawingCommandsProcessor'
|
||||
import { Doc as YDoc } from 'yjs'
|
||||
import { ColorMetaNameOrHex } from '../drawingUtils'
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { ColorsList, ThemeAwareColor } from '../drawingColors'
|
||||
import { DrawingCmd, CommandUid, DrawTextCmd } from '../drawingCommand'
|
||||
|
||||
export let active = false
|
||||
export let readonly = true
|
||||
@@ -36,7 +33,7 @@
|
||||
export let createDrawing: (data: any) => Promise<void>
|
||||
|
||||
let tool: DrawingTool
|
||||
let penColor: string
|
||||
let penColor: ColorMetaNameOrHex
|
||||
let penWidth: number
|
||||
let eraserWidth: number
|
||||
let fontSize: number
|
||||
@@ -52,6 +49,21 @@
|
||||
let disableUndo: boolean = false
|
||||
let disableRedo: boolean = false
|
||||
|
||||
const themeChangeUnsubscribe: Array<() => void> = []
|
||||
|
||||
const DrawingColorPalette: ColorsList = [
|
||||
['alpha', new ThemeAwareColor('#000', '#000')],
|
||||
['beta', new ThemeAwareColor('#FFF', '#FFF')],
|
||||
['gamma', new ThemeAwareColor('Fuchsia', 'Fuchsia')],
|
||||
['delta', new ThemeAwareColor('Houseplant', 'Houseplant')],
|
||||
['epsilon', new ThemeAwareColor('Sky', 'Sky')],
|
||||
['zeta', new ThemeAwareColor('Turquoise', 'Turquoise')],
|
||||
['eta', new ThemeAwareColor('Pink', 'Pink')],
|
||||
['theta', new ThemeAwareColor('Cloud', 'Cloud')],
|
||||
['iota', new ThemeAwareColor('#FFC114', '#FFC114')],
|
||||
['kappa', new ThemeAwareColor('Mauve', 'Mauve')]
|
||||
]
|
||||
|
||||
const document: YDoc = new YDoc()
|
||||
const undoableCommands = document.getArray<DrawingCmd>('drawing-commands')
|
||||
const commandProcessor = new DrawingCommandsProcessor(document, undoableCommands)
|
||||
@@ -185,7 +197,13 @@
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
themeChangeUnsubscribe.forEach((unsubscribe) => {
|
||||
unsubscribe()
|
||||
})
|
||||
themeChangeUnsubscribe.length = 0
|
||||
|
||||
undoableCommands.unobserve(onSavedCommandsChanged)
|
||||
|
||||
saveDrawing()
|
||||
})
|
||||
</script>
|
||||
@@ -200,6 +218,11 @@
|
||||
}}
|
||||
use:drawing={{
|
||||
autoSize: imageWidth === undefined || imageHeight === undefined,
|
||||
colorsList: DrawingColorPalette,
|
||||
getCurrentTheme: () => $themeStore.variant,
|
||||
subscribeOnThemeChange: (callback) => {
|
||||
themeChangeUnsubscribe.push(themeStore.subscribe(callback))
|
||||
},
|
||||
readonly,
|
||||
imageWidth,
|
||||
imageHeight,
|
||||
@@ -225,6 +248,7 @@
|
||||
{#if !readonly}
|
||||
<DrawingBoardToolbar
|
||||
placeInside={toolbarInside}
|
||||
colorsList={DrawingColorPalette}
|
||||
{cmdEditor}
|
||||
bind:toolbar
|
||||
bind:tool
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { ColorMetaNameOrHex } from '../drawingUtils'
|
||||
import { DrawingBoardColoringSetup, metaColorNameToHex } from '../drawingColors'
|
||||
|
||||
export let color: ColorMetaNameOrHex = 'alpha'
|
||||
export let palette: DrawingBoardColoringSetup
|
||||
</script>
|
||||
|
||||
<div class="colorIcon" style:background={metaColorNameToHex(color, $themeStore.variant, palette)} />
|
||||
|
||||
<style lang="scss">
|
||||
.colorIcon {
|
||||
width: 10rem;
|
||||
height: 1rem;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0px 0px 0.15rem 0px var(--theme-button-contrast-enabled);
|
||||
}
|
||||
</style>
|
||||
@@ -27,6 +27,15 @@
|
||||
showPopup
|
||||
} from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import IconEraser from './icons/Eraser.svelte'
|
||||
import IconMove from './icons/Move.svelte'
|
||||
import IconText from './icons/Text.svelte'
|
||||
import { DrawingTool } from '../drawing'
|
||||
import presentation from '../plugin'
|
||||
import { ColorMetaName, ColorMetaNameOrHex } from '../drawingUtils'
|
||||
import DrawingBoardToolbarColorIcon from './DrawingBoardToolbarColorIcon.svelte'
|
||||
import DrawingBoardColorSelectorIcon from './DrawingBoardColorSelectorIcon.svelte'
|
||||
import { ColorsList, DrawingBoardColoringSetup } from '../drawingColors'
|
||||
|
||||
interface DrawingBoardToolbarEvents {
|
||||
undo: undefined
|
||||
@@ -34,17 +43,11 @@
|
||||
clear: undefined
|
||||
}
|
||||
|
||||
import IconEraser from './icons/Eraser.svelte'
|
||||
import IconMove from './icons/Move.svelte'
|
||||
import IconText from './icons/Text.svelte'
|
||||
import { DrawingTool } from '../drawing'
|
||||
import presentation from '../plugin'
|
||||
|
||||
const dispatch = createEventDispatcher<DrawingBoardToolbarEvents>()
|
||||
const maxColors = 8
|
||||
const minColors = 0
|
||||
const defaultColor = '#0000ff'
|
||||
const defaultColors = ['#ff0000', '#00ff00', '#0000ff', '#ffffff', '#000000']
|
||||
const defaultColor: ColorMetaName = 'alpha'
|
||||
const defaultColors: Array<ColorMetaName> = ['alpha', 'gamma', 'delta', 'epsilon']
|
||||
const storageKey = {
|
||||
color: 'drawingBoard.color',
|
||||
colors: 'drawingBoard.colors',
|
||||
@@ -54,7 +57,7 @@
|
||||
}
|
||||
|
||||
export let tool: DrawingTool = 'pen'
|
||||
export let penColor: string
|
||||
export let penColor: ColorMetaNameOrHex
|
||||
export let penWidth: number
|
||||
export let eraserWidth: number
|
||||
export let fontSize: number
|
||||
@@ -64,22 +67,23 @@
|
||||
export let cmdEditor: HTMLDivElement | undefined
|
||||
export let disableUndo: boolean = false
|
||||
export let disableRedo: boolean = false
|
||||
export let colorsList: ColorsList
|
||||
|
||||
let colorSelector: HTMLInputElement
|
||||
let colorsPalette: string[] = defaultColors
|
||||
const availableColors = new DrawingBoardColoringSetup(colorsList)
|
||||
let userSelectedPalette: ColorMetaNameOrHex[] = defaultColors
|
||||
|
||||
type PaletteCommandId = 'add-color' | 'remove-color' | 'reset-colors'
|
||||
|
||||
function showPaletteManagementMenu (ev: MouseEvent): void {
|
||||
const items: Array<Omit<SelectPopupValueType, 'id'> & { id: PaletteCommandId }> = []
|
||||
if (colorsPalette.length < maxColors) {
|
||||
if (userSelectedPalette.length < maxColors) {
|
||||
items.push({
|
||||
id: 'add-color',
|
||||
label: presentation.string.ColorAdd,
|
||||
icon: IconAdd
|
||||
})
|
||||
}
|
||||
if (colorsPalette.length > minColors) {
|
||||
if (userSelectedPalette.length > minColors) {
|
||||
items.push({
|
||||
id: 'remove-color',
|
||||
label: presentation.string.ColorRemove,
|
||||
@@ -91,26 +95,34 @@
|
||||
label: presentation.string.ColorReset,
|
||||
icon: IconRedo
|
||||
})
|
||||
|
||||
showPopup(SelectPopup, { value: items }, eventToHTMLElement(ev), (id: PaletteCommandId | undefined) => {
|
||||
switch (id) {
|
||||
case 'add-color': {
|
||||
if (colorSelector !== undefined) {
|
||||
colorSelector.value = penColor
|
||||
colorSelector.showPicker()
|
||||
}
|
||||
const colorsRange: Array<SelectPopupValueType> = colorsList.map((color, index) => ({
|
||||
id: index,
|
||||
icon: DrawingBoardColorSelectorIcon,
|
||||
iconProps: { color: color[0], palette: availableColors }
|
||||
}))
|
||||
showPopup(SelectPopup, { value: colorsRange }, eventToHTMLElement(ev), (id) => {
|
||||
if (id != null) {
|
||||
penColor = colorsList[id][0]
|
||||
addColorPreset()
|
||||
}
|
||||
})
|
||||
break
|
||||
}
|
||||
case 'remove-color': {
|
||||
colorsPalette = colorsPalette.filter((c: string) => c !== penColor)
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(colorsPalette))
|
||||
selectColor(colorsPalette[0])
|
||||
userSelectedPalette = userSelectedPalette.filter((c: string) => c !== penColor)
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(userSelectedPalette))
|
||||
selectColor(userSelectedPalette[0])
|
||||
focusEditor()
|
||||
break
|
||||
}
|
||||
case 'reset-colors': {
|
||||
colorsPalette = defaultColors
|
||||
userSelectedPalette = defaultColors
|
||||
localStorage.removeItem(storageKey.colors)
|
||||
selectColor(colorsPalette[0])
|
||||
selectColor(userSelectedPalette[0])
|
||||
focusEditor()
|
||||
break
|
||||
}
|
||||
@@ -127,15 +139,14 @@
|
||||
}
|
||||
|
||||
function addColorPreset (): void {
|
||||
penColor = penColor.toLowerCase()
|
||||
if (!colorsPalette.includes(penColor)) {
|
||||
colorsPalette = [...colorsPalette, penColor]
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(colorsPalette))
|
||||
if (!userSelectedPalette.includes(penColor)) {
|
||||
userSelectedPalette = [...userSelectedPalette, penColor]
|
||||
localStorage.setItem(storageKey.colors, JSON.stringify(userSelectedPalette))
|
||||
}
|
||||
focusEditor()
|
||||
}
|
||||
|
||||
function selectColor (color: string): void {
|
||||
function selectColor (color: ColorMetaNameOrHex): void {
|
||||
penColor = color ?? defaultColor
|
||||
localStorage.setItem(storageKey.color, penColor)
|
||||
}
|
||||
@@ -143,13 +154,13 @@
|
||||
onMount(() => {
|
||||
try {
|
||||
const savedColors = localStorage.getItem(storageKey.colors)
|
||||
colorsPalette = savedColors !== null ? JSON.parse(savedColors.toLowerCase()) : defaultColors
|
||||
userSelectedPalette = savedColors !== null ? JSON.parse(savedColors.toLowerCase()) : defaultColors
|
||||
} catch {
|
||||
colorsPalette = defaultColors
|
||||
userSelectedPalette = defaultColors
|
||||
}
|
||||
penColor = (localStorage.getItem(storageKey.color) ?? penColor ?? defaultColor).toLowerCase()
|
||||
if (!colorsPalette.includes(penColor)) {
|
||||
penColor = colorsPalette[0] ?? defaultColor
|
||||
penColor = (localStorage.getItem(storageKey.color) ?? penColor ?? defaultColor) as ColorMetaNameOrHex
|
||||
if (!userSelectedPalette.includes(penColor)) {
|
||||
penColor = userSelectedPalette[0] ?? defaultColor
|
||||
}
|
||||
penWidth = parseInt(localStorage.getItem(storageKey.penWidth) ?? '4')
|
||||
eraserWidth = parseInt(localStorage.getItem(storageKey.eraserWidth) ?? '50')
|
||||
@@ -288,12 +299,11 @@
|
||||
/>
|
||||
<div class="divider buttons-divider" />
|
||||
{/if}
|
||||
{#each colorsPalette as color}
|
||||
{#each userSelectedPalette as color}
|
||||
<Button
|
||||
kind="icon"
|
||||
noFocus
|
||||
selected={penColor === color}
|
||||
showTooltip={{ label: presentation.string.ColorTooltip, props: { color } }}
|
||||
on:click={() => {
|
||||
if (tool === 'erase') {
|
||||
tool = 'pen'
|
||||
@@ -302,17 +312,10 @@
|
||||
focusEditor()
|
||||
}}
|
||||
>
|
||||
<div slot="content" class="colorIcon" style:background={color} />
|
||||
<DrawingBoardToolbarColorIcon {color} palette={availableColors} slot="content" />
|
||||
</Button>
|
||||
{/each}
|
||||
<div>
|
||||
<input
|
||||
type="color"
|
||||
class="colorSelector"
|
||||
bind:this={colorSelector}
|
||||
bind:value={penColor}
|
||||
on:change={addColorPreset}
|
||||
/>
|
||||
<Button
|
||||
kind="icon"
|
||||
icon={IconMoreH}
|
||||
@@ -355,12 +358,6 @@
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.colorSelector {
|
||||
position: absolute;
|
||||
width: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.widthSelector {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
<!--
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { themeStore } from '@hcengineering/theme'
|
||||
import { ColorMetaNameOrHex } from '../drawingUtils'
|
||||
import { DrawingBoardColoringSetup, metaColorNameToHex } from '../drawingColors'
|
||||
|
||||
export let color: ColorMetaNameOrHex = 'alpha'
|
||||
export let palette: DrawingBoardColoringSetup
|
||||
</script>
|
||||
|
||||
<div class="colorIcon" style:background={metaColorNameToHex(color, $themeStore.variant, palette)} />
|
||||
|
||||
<style lang="scss">
|
||||
.colorIcon {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
border-radius: 50%;
|
||||
margin: -0.15rem;
|
||||
box-shadow: 0px 0px 0.15rem 0px var(--theme-button-contrast-enabled);
|
||||
}
|
||||
</style>
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { generateId } from '@hcengineering/core'
|
||||
import { type ThemeVariantType } from '@hcengineering/theme'
|
||||
import {
|
||||
type CanvasPoint,
|
||||
easeInOutCubic,
|
||||
@@ -28,8 +28,11 @@ import {
|
||||
type MouseScaledPoint,
|
||||
makeMouseScaledPoint,
|
||||
makeNodePoint,
|
||||
offsetCanvasPoint
|
||||
offsetCanvasPoint,
|
||||
type ColorMetaNameOrHex
|
||||
} from './drawingUtils'
|
||||
import { type DrawingCmd, type CommandUid, type DrawTextCmd, type DrawLineCmd, makeCommandUid } from './drawingCommand'
|
||||
import { type ColorsList, DrawingBoardColoringSetup, metaColorNameToHex } from './drawingColors'
|
||||
|
||||
export interface DrawingData {
|
||||
content?: string
|
||||
@@ -37,13 +40,16 @@ export interface DrawingData {
|
||||
|
||||
export interface DrawingProps {
|
||||
readonly: boolean
|
||||
colorsList: ColorsList
|
||||
getCurrentTheme: () => ThemeVariantType
|
||||
subscribeOnThemeChange: (callback: () => void) => void
|
||||
autoSize?: boolean
|
||||
imageWidth?: number
|
||||
imageHeight?: number
|
||||
commands?: DrawingCmd[]
|
||||
offset?: Point
|
||||
tool?: DrawingTool
|
||||
penColor?: string
|
||||
penColor?: ColorMetaNameOrHex
|
||||
penWidth?: number
|
||||
eraserWidth?: number
|
||||
fontSize?: number
|
||||
@@ -63,36 +69,10 @@ export interface DrawingProps {
|
||||
panned?: (offset: Point) => void
|
||||
}
|
||||
|
||||
export type CommandUid = string & { readonly __brand: 'CommandUid' }
|
||||
|
||||
export interface DrawingCmd {
|
||||
id: CommandUid
|
||||
type: 'line' | 'text'
|
||||
}
|
||||
|
||||
export interface DrawTextCmd extends DrawingCmd {
|
||||
text: string
|
||||
pos: CanvasPoint
|
||||
fontSize: number
|
||||
fontFace: string
|
||||
color: string
|
||||
}
|
||||
|
||||
export interface DrawLineCmd extends DrawingCmd {
|
||||
lineWidth: number
|
||||
erasing: boolean
|
||||
penColor: string
|
||||
points: CanvasPoint[]
|
||||
}
|
||||
|
||||
export type DrawingTool = 'pen' | 'erase' | 'pan' | 'text'
|
||||
|
||||
const maxTextLength = 500
|
||||
|
||||
export const makeCommandUid = (): CommandUid => {
|
||||
return (crypto?.randomUUID?.() ?? generateId()) as CommandUid
|
||||
}
|
||||
|
||||
const crossSvg = `<svg height="8" width="8" viewBox="0 0 16 16" fill="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="m1.29 2.71 5.3 5.29-5.3 5.29c-.92.92.49 2.34 1.41 1.41l5.3-5.29 5.29 5.3c.92.92 2.34-.49 1.41-1.41l-5.29-5.3 5.3-5.29c.92-.93-.49-2.34-1.42-1.42l-5.29 5.3-5.29-5.3c-.93-.92-2.34.49-1.42 1.42z"/>
|
||||
</svg>`
|
||||
@@ -102,7 +82,7 @@ type PointStatus = 'last-point' | 'intermediate-point'
|
||||
class DrawState {
|
||||
on = false
|
||||
tool: DrawingTool = 'pen'
|
||||
penColor = 'blue'
|
||||
penColor: ColorMetaNameOrHex = 'alpha'
|
||||
penWidth = 4
|
||||
eraserWidth = 30
|
||||
minLineLength = 6
|
||||
@@ -113,11 +93,11 @@ class DrawState {
|
||||
points: CanvasPoint[] = []
|
||||
scale: Point = { x: 1, y: 1 }
|
||||
cssTransformScale: Point = { x: 1, y: 1 }
|
||||
ctx: CanvasRenderingContext2D
|
||||
|
||||
constructor (ctx: CanvasRenderingContext2D) {
|
||||
this.ctx = ctx
|
||||
}
|
||||
constructor (
|
||||
readonly ctx: CanvasRenderingContext2D,
|
||||
readonly colors: DrawingBoardColoringSetup
|
||||
) {}
|
||||
|
||||
cursorWidth = (): number => {
|
||||
return Math.max(8, this.tool === 'erase' ? this.eraserWidth : this.penWidth)
|
||||
@@ -153,7 +133,7 @@ class DrawState {
|
||||
this.ctx.translate(this.offset.x + this.center.x, this.offset.y + this.center.y)
|
||||
}
|
||||
|
||||
drawLine = (point: MouseScaledPoint, status: PointStatus): void => {
|
||||
drawLine = (point: MouseScaledPoint, status: PointStatus, currentTheme: ThemeVariantType): void => {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (status === 'intermediate-point' || this.points.length <= 1) {
|
||||
this.addPoint(point)
|
||||
@@ -164,7 +144,7 @@ class DrawState {
|
||||
this.translateCtx()
|
||||
this.ctx.beginPath()
|
||||
this.ctx.lineCap = 'round'
|
||||
this.ctx.strokeStyle = this.penColor
|
||||
this.ctx.strokeStyle = metaColorNameToHex(this.penColor, currentTheme, this.colors)
|
||||
this.ctx.lineWidth = erasing ? this.eraserWidth : this.penWidth
|
||||
this.ctx.globalCompositeOperation = erasing ? 'destination-out' : 'source-over'
|
||||
if (this.points.length === 1) {
|
||||
@@ -179,20 +159,20 @@ class DrawState {
|
||||
})
|
||||
}
|
||||
|
||||
drawCommand = (cmd: DrawingCmd): void => {
|
||||
drawCommand = (cmd: DrawingCmd, currentTheme: ThemeVariantType): void => {
|
||||
if (cmd.type === 'text') {
|
||||
this.drawTextCommand(cmd as DrawTextCmd)
|
||||
this.drawTextCommand(cmd as DrawTextCmd, currentTheme)
|
||||
} else {
|
||||
this.drawLineCommand(cmd as DrawLineCmd)
|
||||
this.drawLineCommand(cmd as DrawLineCmd, currentTheme)
|
||||
}
|
||||
}
|
||||
|
||||
drawLineCommand = (cmd: DrawLineCmd): void => {
|
||||
drawLineCommand = (cmd: DrawLineCmd, currentTheme: ThemeVariantType): void => {
|
||||
this.ctx.save()
|
||||
this.translateCtx()
|
||||
this.ctx.beginPath()
|
||||
this.ctx.lineCap = 'round'
|
||||
this.ctx.strokeStyle = cmd.penColor
|
||||
this.ctx.strokeStyle = metaColorNameToHex(cmd.penColor, currentTheme, this.colors)
|
||||
this.ctx.lineWidth = cmd.lineWidth
|
||||
this.ctx.globalCompositeOperation = cmd.erasing ? 'destination-out' : 'source-over'
|
||||
if (cmd.points.length === 1) {
|
||||
@@ -207,12 +187,12 @@ class DrawState {
|
||||
this.ctx.restore()
|
||||
}
|
||||
|
||||
drawTextCommand = (cmd: DrawTextCmd): void => {
|
||||
drawTextCommand = (cmd: DrawTextCmd, currentTheme: ThemeVariantType): void => {
|
||||
const p = { ...cmd.pos }
|
||||
this.ctx.save()
|
||||
this.translateCtx()
|
||||
this.ctx.font = `${cmd.fontSize}px ${cmd.fontFace}`
|
||||
this.ctx.fillStyle = cmd.color
|
||||
this.ctx.fillStyle = metaColorNameToHex(cmd.color, currentTheme, this.colors)
|
||||
this.ctx.textBaseline = 'top'
|
||||
const lines = cmd.text.split('\n').map((l) => l.trim())
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
@@ -318,7 +298,8 @@ export function drawing (
|
||||
let personCursorPos: CanvasPoint = makeCanvasPoint(0, 0)
|
||||
let isPersonCursorAnimating = false
|
||||
|
||||
const draw = new DrawState(ctx)
|
||||
const colorsSetup = new DrawingBoardColoringSetup(props.colorsList)
|
||||
const draw = new DrawState(ctx, colorsSetup)
|
||||
draw.tool = props.tool ?? draw.tool
|
||||
draw.penColor = props.penColor ?? draw.penColor
|
||||
draw.penWidth = props.penWidth ?? draw.penWidth
|
||||
@@ -353,6 +334,11 @@ export function drawing (
|
||||
|
||||
replayCommands(currentCommands)
|
||||
|
||||
props.subscribeOnThemeChange(() => {
|
||||
updateToolCursor()
|
||||
replayCommands(currentCommands)
|
||||
})
|
||||
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === canvas) {
|
||||
@@ -509,7 +495,7 @@ export function drawing (
|
||||
|
||||
if (draw.on) {
|
||||
if (Math.hypot(prevPos.x - scaledPoint.x, prevPos.y - scaledPoint.y) >= draw.minLineLength) {
|
||||
draw.drawLine(scaledPoint, 'intermediate-point')
|
||||
draw.drawLine(scaledPoint, 'intermediate-point', props.getCurrentTheme())
|
||||
prevPos = scaledPoint
|
||||
}
|
||||
}
|
||||
@@ -540,7 +526,7 @@ export function drawing (
|
||||
const scaledPoint = rescaleWithCss(p)
|
||||
if (draw.on) {
|
||||
if (draw.isDrawingTool()) {
|
||||
draw.drawLine(scaledPoint, 'last-point')
|
||||
draw.drawLine(scaledPoint, 'last-point', props.getCurrentTheme())
|
||||
storeLineCommand()
|
||||
} else if (draw.tool === 'pan') {
|
||||
props.panned?.(draw.offset)
|
||||
@@ -816,7 +802,7 @@ export function drawing (
|
||||
|
||||
function updateLiveTextBox (): void {
|
||||
if (liveTextBox !== undefined) {
|
||||
liveTextBox.editor.style.color = draw.penColor
|
||||
liveTextBox.editor.style.color = metaColorNameToHex(draw.penColor, props.getCurrentTheme(), colorsSetup)
|
||||
liveTextBox.editor.style.lineHeight = `${draw.fontSize / draw.lineScale()}px`
|
||||
liveTextBox.editor.style.fontSize = `${draw.fontSize / draw.lineScale()}px`
|
||||
liveTextBox.editor.style.fontFamily = draw.fontFace
|
||||
@@ -891,7 +877,9 @@ export function drawing (
|
||||
toolCursor.style.visibility = 'visible'
|
||||
const erasing = draw.tool === 'erase'
|
||||
const w = draw.cursorWidth()
|
||||
toolCursor.style.background = erasing ? 'none' : draw.penColor
|
||||
toolCursor.style.background = erasing
|
||||
? 'none'
|
||||
: metaColorNameToHex(draw.penColor, props.getCurrentTheme(), colorsSetup)
|
||||
toolCursor.style.boxShadow = erasing
|
||||
? '0px 0px 1px 1px white inset, 0px 0px 2px 1px black'
|
||||
: '0px 0px 3px 0px var(--theme-button-contrast-enabled)'
|
||||
@@ -996,9 +984,11 @@ export function drawing (
|
||||
*/
|
||||
draw.ctx.clearRect(0, 0, canvas.width, canvas.height)
|
||||
|
||||
const currentTheme: ThemeVariantType = props.getCurrentTheme()
|
||||
|
||||
traverseCommands(drawing, (command) => {
|
||||
if (command.id === undefined || liveTextBox?.cmdId !== command.id) {
|
||||
draw.drawCommand(command)
|
||||
draw.drawCommand(command, currentTheme)
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { ThemeVariant, type ThemeVariantType } from '@hcengineering/theme'
|
||||
import { getPlatformColorByName } from '@hcengineering/ui'
|
||||
import { type ColorMetaName, type ColorMetaNameOrHex } from './drawingUtils'
|
||||
|
||||
export class ThemeAwareColor {
|
||||
constructor (
|
||||
private readonly darkThemeName: string,
|
||||
private readonly lightThemeName: string
|
||||
) {}
|
||||
|
||||
materialize (target: ThemeVariantType): string {
|
||||
const darkTheme = target === ThemeVariant.Dark
|
||||
const platformColorName = darkTheme ? this.darkThemeName : this.lightThemeName
|
||||
const colorDefinition = getPlatformColorByName(platformColorName, darkTheme)
|
||||
if (colorDefinition == null) {
|
||||
return platformColorName
|
||||
}
|
||||
return colorDefinition.color
|
||||
}
|
||||
}
|
||||
|
||||
export type ColorsList = Array<[ColorMetaName, ThemeAwareColor]>
|
||||
|
||||
export class DrawingBoardColoringSetup {
|
||||
private readonly _colorByName: Map<ColorMetaName, ThemeAwareColor>
|
||||
constructor (private readonly _allColors: ColorsList) {
|
||||
this._colorByName = new Map<ColorMetaName, ThemeAwareColor>(_allColors)
|
||||
}
|
||||
|
||||
get allColors (): ColorsList {
|
||||
return this._allColors
|
||||
}
|
||||
|
||||
colorByName (name: ColorMetaName): ThemeAwareColor | undefined {
|
||||
return this._colorByName.get(name)
|
||||
}
|
||||
}
|
||||
|
||||
export function metaColorNameToHex (
|
||||
color: ColorMetaNameOrHex,
|
||||
theme: ThemeVariantType,
|
||||
setup: DrawingBoardColoringSetup
|
||||
): string {
|
||||
const backend = color as string
|
||||
if (backend.startsWith('#') && (backend.length === 7 || backend.length === 4)) {
|
||||
return backend
|
||||
}
|
||||
return setup.colorByName(color as ColorMetaName)?.materialize(theme) ?? backend
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { generateId } from '@hcengineering/core'
|
||||
import { type ColorMetaNameOrHex, type CanvasPoint } from './drawingUtils'
|
||||
|
||||
export type CommandUid = string & { readonly __brand: 'CommandUid' }
|
||||
|
||||
export interface DrawingCmd {
|
||||
id: CommandUid
|
||||
type: 'line' | 'text'
|
||||
}
|
||||
|
||||
export interface DrawTextCmd extends DrawingCmd {
|
||||
text: string
|
||||
pos: CanvasPoint
|
||||
fontSize: number
|
||||
fontFace: string
|
||||
color: ColorMetaNameOrHex
|
||||
}
|
||||
|
||||
export interface DrawLineCmd extends DrawingCmd {
|
||||
lineWidth: number
|
||||
erasing: boolean
|
||||
penColor: ColorMetaNameOrHex
|
||||
points: CanvasPoint[]
|
||||
}
|
||||
|
||||
export const makeCommandUid = (): CommandUid => {
|
||||
return (crypto?.randomUUID?.() ?? generateId()) as CommandUid
|
||||
}
|
||||
@@ -13,7 +13,7 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { makeCommandUid, type CommandUid, type DrawingCmd } from './drawing'
|
||||
import { makeCommandUid, type CommandUid, type DrawingCmd } from './drawingCommand'
|
||||
import { type Array as YArray, type Doc as YDoc, UndoManager as YUndoManager } from 'yjs'
|
||||
|
||||
export class UndoRedoAvailability {
|
||||
|
||||
@@ -13,6 +13,38 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export type ColorMetaName =
|
||||
| 'alpha'
|
||||
| 'beta'
|
||||
| 'gamma'
|
||||
| 'delta'
|
||||
| 'epsilon'
|
||||
| 'zeta'
|
||||
| 'eta'
|
||||
| 'theta'
|
||||
| 'iota'
|
||||
| 'kappa'
|
||||
// | 'lambda'
|
||||
// | 'mu'
|
||||
// | 'nu'
|
||||
// | 'xi'
|
||||
// | 'omicron'
|
||||
// | 'pi'
|
||||
// | 'rho'
|
||||
// | 'sigma'
|
||||
// | 'tau'
|
||||
// | 'upsilon'
|
||||
// | 'phi'
|
||||
// | 'chi'
|
||||
// | 'psi'
|
||||
// | 'omega'
|
||||
|
||||
/*
|
||||
We need to be backward compatible with user data, that already contains arbitrarily colors.
|
||||
First version of the drawing board allowed selection of any RGB color.
|
||||
*/
|
||||
export type ColorMetaNameOrHex = (string & { readonly __brand: 'ColorMetaNameOrHex' }) | ColorMetaName
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
|
||||
@@ -73,6 +73,8 @@ export * from './sound'
|
||||
export * from './stats'
|
||||
export * from './drawing'
|
||||
export * from './drawingUtils'
|
||||
export * from './drawingColors'
|
||||
export * from './drawingCommand'
|
||||
export * from './drawingCommandsProcessor'
|
||||
export * from './link-preview'
|
||||
export * from './communication'
|
||||
|
||||
@@ -154,7 +154,6 @@ export default plugin(presentationId, {
|
||||
EraserTool: '' as IntlString,
|
||||
PanTool: '' as IntlString,
|
||||
TextTool: '' as IntlString,
|
||||
ColorTooltip: '' as IntlString,
|
||||
PaletteManagementMenu: '' as IntlString
|
||||
},
|
||||
extension: {
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import platform, { loadPluginStrings, setMetadata } from '@hcengineering/platform'
|
||||
import { onMount, setContext } from 'svelte'
|
||||
import { writable } from 'svelte/store'
|
||||
import { ThemeVariant } from './variants'
|
||||
import {
|
||||
ThemeOptions,
|
||||
getCurrentFontSize,
|
||||
@@ -35,7 +36,7 @@
|
||||
themeOptions.set(new ThemeOptions(currentFont === 'normal-font' ? 16 : 14, isThemeDark(theme), language))
|
||||
}
|
||||
|
||||
const getRealTheme = (theme: string): string => (isThemeDark(theme) ? 'theme-dark' : 'theme-light')
|
||||
const getRealTheme = (theme: string): string => (isThemeDark(theme) ? ThemeVariant.Dark : ThemeVariant.Light)
|
||||
const setRootColors = (theme: string, set = true) => {
|
||||
currentTheme.set(theme)
|
||||
if (set) {
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
import { Analytics } from '@hcengineering/analytics'
|
||||
import '@hcengineering/platform-rig/profiles/ui/svelte'
|
||||
import { derived, writable } from 'svelte/store'
|
||||
import { ThemeVariant, type ThemeVariantType } from './variants'
|
||||
|
||||
export { default as Theme } from './Theme.svelte'
|
||||
export { default as InvertedTheme } from './InvertedTheme.svelte'
|
||||
export { ThemeVariant, type ThemeVariantType } from './variants'
|
||||
|
||||
/**
|
||||
* @public
|
||||
@@ -62,11 +64,14 @@ export const getCurrentLanguage = (): string => {
|
||||
}
|
||||
|
||||
export class ThemeOptions {
|
||||
readonly variant: ThemeVariantType
|
||||
constructor (
|
||||
readonly fontSize: number,
|
||||
readonly dark: boolean,
|
||||
readonly language: string
|
||||
) {}
|
||||
) {
|
||||
this.variant = dark ? ThemeVariant.Dark : ThemeVariant.Light
|
||||
}
|
||||
}
|
||||
export const themeStore = writable<ThemeOptions>()
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
export const ThemeVariant = {
|
||||
Light: 'theme-light',
|
||||
Dark: 'theme-dark'
|
||||
} as const
|
||||
|
||||
export type ThemeVariantType = (typeof ThemeVariant)[keyof typeof ThemeVariant]
|
||||
@@ -0,0 +1,57 @@
|
||||
//
|
||||
// Copyright © 2025 Hardcore Engineering Inc.
|
||||
//
|
||||
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
//
|
||||
|
||||
import { getPlatformColorByName, getPlatformAvatarColorByName, avatarWhiteColors, avatarDarkColors } from '../colors'
|
||||
|
||||
describe('colors module tests', () => {
|
||||
describe('getPlatformColorByName', () => {
|
||||
it('get existing color, light theme', () => {
|
||||
const result = getPlatformColorByName('Firework', false)
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.name).toBe('Firework')
|
||||
expect(result?.title).toBe('#C03B2F')
|
||||
})
|
||||
|
||||
it('get existing color, dark theme', () => {
|
||||
const result = getPlatformColorByName('Firework', true)
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result?.name).toBe('Firework')
|
||||
expect(result?.title).toBe('#FFFFFF')
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ theme: 'light', darkTheme: false },
|
||||
{ theme: 'dark', darkTheme: true }
|
||||
])('get non-existent color ($theme theme)', ({ darkTheme }) => {
|
||||
const result = getPlatformColorByName('NonExistentColor', darkTheme)
|
||||
expect(result).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPlatformAvatarColorByName', () => {
|
||||
it.each([
|
||||
{ theme: 'light', darkTheme: false, expectedPalette: avatarWhiteColors },
|
||||
{ theme: 'dark', darkTheme: true, expectedPalette: avatarDarkColors }
|
||||
])('get non-existent color ($theme theme)', ({ darkTheme, expectedPalette }) => {
|
||||
const result = getPlatformAvatarColorByName('NonExistentAvatarColor', darkTheme)
|
||||
const firstColorFromPalette = expectedPalette[0]
|
||||
|
||||
expect(result).toBeDefined()
|
||||
expect(result).toEqual(firstColorFromPalette)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -74,7 +74,7 @@ export enum PaletteColorIndexes {
|
||||
Firework,
|
||||
Watermelon,
|
||||
Pink,
|
||||
Fuschia,
|
||||
Fuchsia,
|
||||
Lavander,
|
||||
Mauve,
|
||||
Heather,
|
||||
@@ -104,7 +104,7 @@ export const whitePalette = Object.freeze<ColorDefinition[]>([
|
||||
define('Firework', 'D15045', 'D15045', 'C03B2F', 'C03B2F', 'C03B2F', [60, 20]),
|
||||
define('Watermelon', 'DB877D', 'DB877D', 'D2685B', 'D2685B', 'D2685B', [60, 20]),
|
||||
define('Pink', 'EF86AA', 'EF86AA', 'E9588A', 'E9588A', 'E9588A', [60, 20]),
|
||||
define('Fuschia', 'EB5181', 'EB5181', 'E62360', 'E62360', 'E62360', [60, 20]),
|
||||
define('Fuchsia', 'EB5181', 'EB5181', 'E62360', 'E62360', 'E62360', [60, 20]),
|
||||
define('Lavander', 'DC85F5', 'DC85F5', 'CE55F1', 'CE55F1', 'CE55F1', [60, 20]),
|
||||
define('Mauve', '925CB1', '925CB1', '784794', '784794', '784794', [60, 20]),
|
||||
define('Heather', '7B86C6', '7B86C6', '5866B7', '5866B7', '5866B7', [60, 20]),
|
||||
@@ -134,7 +134,7 @@ export const darkPalette = Object.freeze<ColorDefinition[]>([
|
||||
define('Firework', 'D15045', 'D15045', 'FFFFFF', 'FFFFFF', 'C03B2F', [60, 15, 0], true),
|
||||
define('Watermelon', 'DB877D', 'DB877D', 'FFFFFF', 'FFFFFF', 'D2685B', [60, 15, 0], true),
|
||||
define('Pink', 'EF86AA', 'EF86AA', 'FFFFFF', 'FFFFFF', 'E9588A', [60, 15, 0], true),
|
||||
define('Fuschia', 'EB5181', 'EB5181', 'FFFFFF', 'FFFFFF', 'E62360', [60, 15, 0], true),
|
||||
define('Fuchsia', 'EB5181', 'EB5181', 'FFFFFF', 'FFFFFF', 'E62360', [60, 15, 0], true),
|
||||
define('Lavander', 'DC85F5', 'DC85F5', 'FFFFFF', 'FFFFFF', 'CE55F1', [60, 15, 0], true),
|
||||
define('Mauve', '925CB1', '925CB1', 'FFFFFF', 'FFFFFF', '784794', [60, 15, 0], true),
|
||||
define('Heather', '7B86C6', '7B86C6', 'FFFFFF', 'FFFFFF', '5866B7', [60, 15, 0], true),
|
||||
@@ -272,8 +272,45 @@ export function getPlatformAvatarColorForTextDef (text: string, darkTheme: boole
|
||||
* @public
|
||||
*/
|
||||
export function getPlatformAvatarColorByName (name: string, darkTheme: boolean): ColorDefinition {
|
||||
const palette = darkTheme ? avatarDarkColors : avatarWhiteColors
|
||||
return palette.find((col) => col.name === name) ?? palette[0]
|
||||
const defaultIndex = 0
|
||||
return getColorByName(name, darkTheme, avatarWhiteColors, avatarDarkColors, defaultIndex)
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export function getPlatformColorByName (name: string, darkTheme: boolean): ColorDefinition | undefined {
|
||||
const defaultIndex: number | undefined = undefined
|
||||
return getColorByName(name, darkTheme, whitePalette, darkPalette, defaultIndex)
|
||||
}
|
||||
|
||||
function getColorByName (
|
||||
name: string,
|
||||
darkTheme: boolean,
|
||||
paletteLight: readonly ColorDefinition[],
|
||||
paletteDark: readonly ColorDefinition[],
|
||||
defaultIndex: number
|
||||
): ColorDefinition
|
||||
function getColorByName (
|
||||
name: string,
|
||||
darkTheme: boolean,
|
||||
paletteLight: readonly ColorDefinition[],
|
||||
paletteDark: readonly ColorDefinition[],
|
||||
defaultIndex: undefined
|
||||
): ColorDefinition | undefined
|
||||
function getColorByName (
|
||||
name: string,
|
||||
darkTheme: boolean,
|
||||
paletteLight: readonly ColorDefinition[],
|
||||
paletteDark: readonly ColorDefinition[],
|
||||
defaultIndex: number | undefined
|
||||
): ColorDefinition | undefined {
|
||||
const targetPalette = darkTheme ? paletteDark : paletteLight
|
||||
const found = targetPalette.find((col) => col.name === name)
|
||||
if (found != null) {
|
||||
return found
|
||||
}
|
||||
return defaultIndex != null ? targetPalette[defaultIndex] : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -22,13 +22,17 @@
|
||||
drawing,
|
||||
CommandUid,
|
||||
Point,
|
||||
DrawingCommandsProcessor
|
||||
DrawingCommandsProcessor,
|
||||
ThemeAwareColor,
|
||||
ColorsList,
|
||||
ColorMetaNameOrHex
|
||||
} from '@hcengineering/presentation'
|
||||
import presence from '@hcengineering/presence'
|
||||
import { getResource } from '@hcengineering/platform'
|
||||
import { Loading, Component } from '@hcengineering/ui'
|
||||
import { Loading, Component, themeStore } from '@hcengineering/ui'
|
||||
import { onMount, onDestroy } from 'svelte'
|
||||
import { Array as YArray, Map as YMap, Doc as YDoc } from 'yjs'
|
||||
import { get } from 'svelte/store'
|
||||
|
||||
export let boardId: string
|
||||
export let document: YDoc
|
||||
@@ -43,7 +47,7 @@
|
||||
export let fullSize = false
|
||||
|
||||
let tool: DrawingTool
|
||||
let penColor: string
|
||||
let penColor: ColorMetaNameOrHex
|
||||
let penWidth: number
|
||||
let eraserWidth: number
|
||||
let fontSize: number
|
||||
@@ -61,6 +65,21 @@
|
||||
let getFollowee: (() => Promise<Person | undefined>) | undefined
|
||||
let panning = false
|
||||
let followee: Person | undefined
|
||||
|
||||
const themeChangeUnsubscribe: Array<() => void> = []
|
||||
|
||||
const DrawingColorPalette: ColorsList = [
|
||||
['alpha', new ThemeAwareColor('#FFF', '#000')],
|
||||
['gamma', new ThemeAwareColor('Fuchsia', 'Fuchsia')],
|
||||
['delta', new ThemeAwareColor('Houseplant', 'Houseplant')],
|
||||
['epsilon', new ThemeAwareColor('Sky', 'Waterway')],
|
||||
['zeta', new ThemeAwareColor('Turquoise', 'Ocean')],
|
||||
['eta', new ThemeAwareColor('Pink', 'Firework')],
|
||||
['theta', new ThemeAwareColor('Cloud', 'Porpoise')],
|
||||
['iota', new ThemeAwareColor('#705201', '#FFC114')],
|
||||
['kappa', new ThemeAwareColor('Lavander', 'Mauve')]
|
||||
]
|
||||
|
||||
const dataTopicOffset = 'drawing-board-offset'
|
||||
const dataTopicCursor = 'drawing-board-cursor'
|
||||
|
||||
@@ -190,6 +209,11 @@
|
||||
})
|
||||
|
||||
onDestroy(() => {
|
||||
themeChangeUnsubscribe.forEach((unsubscribe) => {
|
||||
unsubscribe()
|
||||
})
|
||||
themeChangeUnsubscribe.length = 0
|
||||
|
||||
savedCmds.unobserve(onSavedCommandsChanged)
|
||||
|
||||
getResource(presence.function.FolloweeDataUnsubscribe)
|
||||
@@ -223,7 +247,12 @@
|
||||
style:flex-grow={resizeable ? undefined : '1'}
|
||||
style:height={resizeable ? `${height}px` : undefined}
|
||||
use:drawing={{
|
||||
colorsList: DrawingColorPalette,
|
||||
readonly,
|
||||
getCurrentTheme: () => $themeStore.variant,
|
||||
subscribeOnThemeChange: (callback) => {
|
||||
themeChangeUnsubscribe.push(themeStore.subscribe(callback))
|
||||
},
|
||||
autoSize: true,
|
||||
commands: model,
|
||||
offset,
|
||||
@@ -271,6 +300,7 @@
|
||||
<DrawingBoardToolbar
|
||||
placeInside={true}
|
||||
showPanTool={true}
|
||||
colorsList={DrawingColorPalette}
|
||||
{cmdEditor}
|
||||
{disableUndo}
|
||||
{disableRedo}
|
||||
|
||||
Reference in New Issue
Block a user