From 66051dfa2610c8cc812184525ac6e7c0cc7135e8 Mon Sep 17 00:00:00 2001 From: Trang Doan Date: Thu, 2 Apr 2026 14:13:47 -0400 Subject: [PATCH 1/8] ENG-1565 Convert image to node via hover icon Add a "Convert to node" icon that appears on hover over embedded images in the Obsidian editor. Clicking it opens ModifyNodeModal to create a discourse node from the image. Also extracts shared image-to-node conversion logic into openConvertImageToNodeModal so both the file-menu and hover icon reuse the same handler. Co-Authored-By: Claude Opus 4.6 --- apps/obsidian/src/index.ts | 42 +- apps/obsidian/src/styles/style.css | 4553 +++++++++-------- apps/obsidian/src/utils/editorMenuUtils.ts | 52 +- .../obsidian/src/utils/imageEmbedHoverIcon.ts | 86 + 4 files changed, 2477 insertions(+), 2256 deletions(-) create mode 100644 apps/obsidian/src/utils/imageEmbedHoverIcon.ts diff --git a/apps/obsidian/src/index.ts b/apps/obsidian/src/index.ts index a120ebfaf..04be58716 100644 --- a/apps/obsidian/src/index.ts +++ b/apps/obsidian/src/index.ts @@ -14,8 +14,9 @@ import { Settings, VIEW_TYPE_DISCOURSE_CONTEXT } from "~/types"; import { addConvertSubmenu, isImageFile, - replaceImageEmbedInEditor, + openConvertImageToNodeModal, } from "~/utils/editorMenuUtils"; +import { createImageEmbedHoverExtension } from "~/utils/imageEmbedHoverIcon"; import { registerCommands } from "~/utils/registerCommands"; import { DiscourseContextView } from "~/components/DiscourseContextView"; import { VIEW_TYPE_TLDRAW_DG_PREVIEW, FRONTMATTER_KEY } from "~/constants"; @@ -159,41 +160,11 @@ export default class DiscourseGraphPlugin extends Plugin { label: "Convert into", nodeTypes: this.settings.nodeTypes, onClick: (nodeType) => { - new ModifyNodeModal(this.app, { - nodeTypes: this.settings.nodeTypes, + openConvertImageToNodeModal({ plugin: this, - initialTitle: "", + imageFile: file, initialNodeType: nodeType, - onSubmit: async ({ - nodeType: selectedType, - title, - selectedExistingNode, - }) => { - const targetFile = - selectedExistingNode ?? - (await createDiscourseNode({ - plugin: this, - nodeType: selectedType, - text: title, - })); - - if (!targetFile) return; - - const imageLink = this.app.metadataCache.fileToLinktext( - file, - targetFile.path, - ); - await this.app.vault.append( - targetFile, - `\n![[${imageLink}]]\n`, - ); - replaceImageEmbedInEditor({ - app: this.app, - imageFile: file, - targetFile, - }); - }, - }).open(); + }); }, }); return; @@ -300,6 +271,9 @@ export default class DiscourseGraphPlugin extends Plugin { // eslint-disable-next-line @typescript-eslint/no-unsafe-argument this.registerEditorExtension(nodeTagHotkeyExtension); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + this.registerEditorExtension(createImageEmbedHoverExtension(this)); } private createStyleElement() { diff --git a/apps/obsidian/src/styles/style.css b/apps/obsidian/src/styles/style.css index ab4c1ddce..96b1cd849 100644 --- a/apps/obsidian/src/styles/style.css +++ b/apps/obsidian/src/styles/style.css @@ -1,832 +1,835 @@ - - -/* We copy the styling from tldraw/tldraw.css here due to compilation failure */ +/* We copy the styling from tldraw/tldraw.css here due to compilation failure */ /* This file is created by the copy-css-files.mjs script in packages/tldraw. */ /* It combines @tldraw/editor's editor.css and tldraw's ui.css */ /* @tldraw/editor */ .tl-container { - width: 100%; - height: 100%; - font-size: 12px; - /* Spacing */ - --space-1: 2px; - --space-2: 4px; - --space-3: 8px; - --space-4: 12px; - --space-5: 16px; - --space-6: 20px; - --space-7: 28px; - --space-8: 32px; - --space-9: 64px; - --space-10: 72px; - /* Radius */ - --radius-0: 2px; - --radius-1: 4px; - --radius-2: 6px; - --radius-3: 9px; - --radius-4: 11px; - - /* Canvas z-index */ - --layer-canvas-hidden: -999999; - --layer-canvas-background: 100; - --layer-canvas-grid: 150; - --layer-watermark: 200; - --layer-canvas-shapes: 300; - --layer-canvas-overlays: 500; - --layer-canvas-blocker: 10000; - - /* Canvas overlays z-index */ - --layer-overlays-collaborator-scribble: 10; - --layer-overlays-collaborator-brush: 20; - --layer-overlays-collaborator-shape-indicator: 30; - --layer-overlays-user-scribble: 40; - --layer-overlays-user-brush: 50; - --layer-overlays-user-snapline: 90; - --layer-overlays-selection-fg: 100; - /* User handles need to be above selection edges / corners, matters for sticky note clone handles */ - --layer-overlays-user-handles: 105; - --layer-overlays-user-indicator-hint: 110; - --layer-overlays-custom: 115; - --layer-overlays-collaborator-cursor-hint: 120; - --layer-overlays-collaborator-cursor: 130; - - /* Text editor z-index */ - --layer-text-container: 1; - --layer-text-content: 3; - --layer-text-editor: 4; - - /* Error fallback z-index */ - --layer-error-overlay: 1; - --layer-error-canvas: 2; - --layer-error-canvas-after: 3; - --layer-error-content: 4; - - /* Misc */ - --tl-zoom: 1; - - /* Cursor SVGs */ - --tl-cursor-none: none; - --tl-cursor-default: - url("data:image/svg+xml,") - 12 8, - default; - --tl-cursor-pointer: - url("data:image/svg+xml,") - 14 10, - pointer; - --tl-cursor-cross: - url("data:image/svg+xml,") - 16 16, - crosshair; - --tl-cursor-move: - url("data:image/svg+xml,") - 16 16, - move; - --tl-cursor-grab: - url("data:image/svg+xml,") - 16 16, - grab; - --tl-cursor-grabbing: - url("data:image/svg+xml,") - 16 16, - grabbing; - --tl-cursor-text: - url("data:image/svg+xml,") - 4 10, - text; - --tl-cursor-zoom-in: - url("data:image/svg+xml,") - 16 16, - zoom-in; - --tl-cursor-zoom-out: - url("data:image/svg+xml,") - 16 16, - zoom-out; - - /* These cursor values get programmatically overridden */ - /* They're just here to help your editor autocomplete */ - --tl-cursor: var(--tl-cursor-default); - --tl-cursor-resize-edge: ew-resize; - --tl-cursor-resize-corner: nesw-resize; - --tl-cursor-ew-resize: ew-resize; - --tl-cursor-ns-resize: ns-resize; - --tl-cursor-nesw-resize: nesw-resize; - --tl-cursor-nwse-resize: nwse-resize; - --tl-cursor-rotate: pointer; - --tl-cursor-nwse-rotate: pointer; - --tl-cursor-nesw-rotate: pointer; - --tl-cursor-senw-rotate: pointer; - --tl-cursor-swne-rotate: pointer; - --tl-scale: calc(1 / var(--tl-zoom)); - /* fonts */ - --tl-font-draw: 'tldraw_draw', sans-serif; - --tl-font-sans: 'tldraw_sans', sans-serif; - --tl-font-serif: 'tldraw_serif', serif; - --tl-font-mono: 'tldraw_mono', monospace; - /* text outline */ - --a: calc(min(0.5, 1 / var(--tl-zoom)) * 2px); - --b: calc(min(0.5, 1 / var(--tl-zoom)) * -2px); - --tl-text-outline-reference: - 0 var(--b) 0 var(--color-background), 0 var(--a) 0 var(--color-background), - var(--b) var(--b) 0 var(--color-background), var(--a) var(--b) 0 var(--color-background), - var(--a) var(--a) 0 var(--color-background), var(--b) var(--a) 0 var(--color-background); - --tl-text-outline: var(--tl-text-outline-reference); - /* own properties */ - position: relative; - inset: 0px; - height: 100%; - width: 100%; - overflow: clip; - color: var(--color-text); + width: 100%; + height: 100%; + font-size: 12px; + /* Spacing */ + --space-1: 2px; + --space-2: 4px; + --space-3: 8px; + --space-4: 12px; + --space-5: 16px; + --space-6: 20px; + --space-7: 28px; + --space-8: 32px; + --space-9: 64px; + --space-10: 72px; + /* Radius */ + --radius-0: 2px; + --radius-1: 4px; + --radius-2: 6px; + --radius-3: 9px; + --radius-4: 11px; + + /* Canvas z-index */ + --layer-canvas-hidden: -999999; + --layer-canvas-background: 100; + --layer-canvas-grid: 150; + --layer-watermark: 200; + --layer-canvas-shapes: 300; + --layer-canvas-overlays: 500; + --layer-canvas-blocker: 10000; + + /* Canvas overlays z-index */ + --layer-overlays-collaborator-scribble: 10; + --layer-overlays-collaborator-brush: 20; + --layer-overlays-collaborator-shape-indicator: 30; + --layer-overlays-user-scribble: 40; + --layer-overlays-user-brush: 50; + --layer-overlays-user-snapline: 90; + --layer-overlays-selection-fg: 100; + /* User handles need to be above selection edges / corners, matters for sticky note clone handles */ + --layer-overlays-user-handles: 105; + --layer-overlays-user-indicator-hint: 110; + --layer-overlays-custom: 115; + --layer-overlays-collaborator-cursor-hint: 120; + --layer-overlays-collaborator-cursor: 130; + + /* Text editor z-index */ + --layer-text-container: 1; + --layer-text-content: 3; + --layer-text-editor: 4; + + /* Error fallback z-index */ + --layer-error-overlay: 1; + --layer-error-canvas: 2; + --layer-error-canvas-after: 3; + --layer-error-content: 4; + + /* Misc */ + --tl-zoom: 1; + + /* Cursor SVGs */ + --tl-cursor-none: none; + --tl-cursor-default: + url("data:image/svg+xml,") + 12 8, + default; + --tl-cursor-pointer: + url("data:image/svg+xml,") + 14 10, + pointer; + --tl-cursor-cross: + url("data:image/svg+xml,") + 16 16, + crosshair; + --tl-cursor-move: + url("data:image/svg+xml,") + 16 16, + move; + --tl-cursor-grab: + url("data:image/svg+xml,") + 16 16, + grab; + --tl-cursor-grabbing: + url("data:image/svg+xml,") + 16 16, + grabbing; + --tl-cursor-text: + url("data:image/svg+xml,") + 4 10, + text; + --tl-cursor-zoom-in: + url("data:image/svg+xml,") + 16 16, + zoom-in; + --tl-cursor-zoom-out: + url("data:image/svg+xml,") + 16 16, + zoom-out; + + /* These cursor values get programmatically overridden */ + /* They're just here to help your editor autocomplete */ + --tl-cursor: var(--tl-cursor-default); + --tl-cursor-resize-edge: ew-resize; + --tl-cursor-resize-corner: nesw-resize; + --tl-cursor-ew-resize: ew-resize; + --tl-cursor-ns-resize: ns-resize; + --tl-cursor-nesw-resize: nesw-resize; + --tl-cursor-nwse-resize: nwse-resize; + --tl-cursor-rotate: pointer; + --tl-cursor-nwse-rotate: pointer; + --tl-cursor-nesw-rotate: pointer; + --tl-cursor-senw-rotate: pointer; + --tl-cursor-swne-rotate: pointer; + --tl-scale: calc(1 / var(--tl-zoom)); + /* fonts */ + --tl-font-draw: "tldraw_draw", sans-serif; + --tl-font-sans: "tldraw_sans", sans-serif; + --tl-font-serif: "tldraw_serif", serif; + --tl-font-mono: "tldraw_mono", monospace; + /* text outline */ + --a: calc(min(0.5, 1 / var(--tl-zoom)) * 2px); + --b: calc(min(0.5, 1 / var(--tl-zoom)) * -2px); + --tl-text-outline-reference: + 0 var(--b) 0 var(--color-background), 0 var(--a) 0 var(--color-background), + var(--b) var(--b) 0 var(--color-background), + var(--a) var(--b) 0 var(--color-background), + var(--a) var(--a) 0 var(--color-background), + var(--b) var(--a) 0 var(--color-background); + --tl-text-outline: var(--tl-text-outline-reference); + /* own properties */ + position: relative; + inset: 0px; + height: 100%; + width: 100%; + overflow: clip; + color: var(--color-text); } .tl-theme__light { - /* Canvas */ - --color-snap: hsl(0, 76%, 60%); - --color-selection-fill: hsl(210, 100%, 56%, 24%); - --color-selection-stroke: hsl(214, 84%, 56%); - --color-background: hsl(210, 20%, 98%); - --color-brush-fill: hsl(0, 0%, 56%, 10.2%); - --color-brush-stroke: hsl(0, 0%, 56%, 25.1%); - --color-grid: hsl(0, 0%, 43%); - /* UI */ - --color-low: hsl(204, 16%, 94%); - --color-low-border: hsl(204, 16%, 92%); - --color-culled: hsl(204, 14%, 93%); - --color-muted-none: hsl(0, 0%, 0%, 0%); - --color-muted-0: hsl(0, 0%, 0%, 2%); - --color-muted-1: hsl(0, 0%, 0%, 10%); - --color-muted-2: hsl(0, 0%, 0%, 4.3%); - --color-hint: hsl(0, 0%, 0%, 5.5%); - --color-overlay: hsl(0, 0%, 0%, 20%); - --color-divider: hsl(0, 0%, 91%); - --color-panel: hsl(0, 0%, 99%); - --color-panel-contrast: hsl(0, 0%, 100%); - --color-panel-overlay: hsl(0, 0%, 100%, 82%); - --color-panel-transparent: hsla(0, 0%, 99%, 0%); - --color-selected: hsl(214, 84%, 56%); - --color-selected-contrast: hsl(0, 0%, 100%); - --color-focus: hsl(219, 65%, 50%); - /* Text */ - --color-text: hsl(0, 0%, 0%); - --color-text-0: hsl(0, 0%, 11%); - --color-text-1: hsl(0, 0%, 18%); - --color-text-3: hsl(220, 2%, 65%); - --color-text-shadow: hsl(0, 0%, 100%); - --color-text-highlight: hsl(52, 100%, 50%); - --color-text-highlight-p3: color(display-p3 0.972 0.8205 0.05); - /* Named */ - --color-primary: hsl(214, 84%, 56%); - --color-success: hsl(123, 46%, 34%); - --color-info: hsl(201, 98%, 41%); - --color-warning: hsl(27, 98%, 47%); - --color-danger: hsl(0, 90%, 43%); - --color-laser: hsl(0, 100%, 50%); - /* Shadows */ - --shadow-1: 0px 1px 2px hsl(0, 0%, 0%, 25%), 0px 1px 3px hsl(0, 0%, 0%, 9%); - --shadow-2: - 0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%), - 0px 2px 6px hsl(0, 0%, 0%, 0.1), inset 0px 0px 0px 1px var(--color-panel-contrast); - --shadow-3: - 0px 1px 2px hsl(0, 0%, 0%, 28%), 0px 2px 6px hsl(0, 0%, 0%, 14%), - inset 0px 0px 0px 1px var(--color-panel-contrast); - --shadow-4: - 0px 0px 3px hsl(0, 0%, 0%, 19%), 0px 5px 4px hsl(0, 0%, 0%, 16%), - 0px 2px 16px hsl(0, 0%, 0%, 6%), inset 0px 0px 0px 1px var(--color-panel-contrast); + /* Canvas */ + --color-snap: hsl(0, 76%, 60%); + --color-selection-fill: hsl(210, 100%, 56%, 24%); + --color-selection-stroke: hsl(214, 84%, 56%); + --color-background: hsl(210, 20%, 98%); + --color-brush-fill: hsl(0, 0%, 56%, 10.2%); + --color-brush-stroke: hsl(0, 0%, 56%, 25.1%); + --color-grid: hsl(0, 0%, 43%); + /* UI */ + --color-low: hsl(204, 16%, 94%); + --color-low-border: hsl(204, 16%, 92%); + --color-culled: hsl(204, 14%, 93%); + --color-muted-none: hsl(0, 0%, 0%, 0%); + --color-muted-0: hsl(0, 0%, 0%, 2%); + --color-muted-1: hsl(0, 0%, 0%, 10%); + --color-muted-2: hsl(0, 0%, 0%, 4.3%); + --color-hint: hsl(0, 0%, 0%, 5.5%); + --color-overlay: hsl(0, 0%, 0%, 20%); + --color-divider: hsl(0, 0%, 91%); + --color-panel: hsl(0, 0%, 99%); + --color-panel-contrast: hsl(0, 0%, 100%); + --color-panel-overlay: hsl(0, 0%, 100%, 82%); + --color-panel-transparent: hsla(0, 0%, 99%, 0%); + --color-selected: hsl(214, 84%, 56%); + --color-selected-contrast: hsl(0, 0%, 100%); + --color-focus: hsl(219, 65%, 50%); + /* Text */ + --color-text: hsl(0, 0%, 0%); + --color-text-0: hsl(0, 0%, 11%); + --color-text-1: hsl(0, 0%, 18%); + --color-text-3: hsl(220, 2%, 65%); + --color-text-shadow: hsl(0, 0%, 100%); + --color-text-highlight: hsl(52, 100%, 50%); + --color-text-highlight-p3: color(display-p3 0.972 0.8205 0.05); + /* Named */ + --color-primary: hsl(214, 84%, 56%); + --color-success: hsl(123, 46%, 34%); + --color-info: hsl(201, 98%, 41%); + --color-warning: hsl(27, 98%, 47%); + --color-danger: hsl(0, 90%, 43%); + --color-laser: hsl(0, 100%, 50%); + /* Shadows */ + --shadow-1: 0px 1px 2px hsl(0, 0%, 0%, 25%), 0px 1px 3px hsl(0, 0%, 0%, 9%); + --shadow-2: + 0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%), + 0px 2px 6px hsl(0, 0%, 0%, 0.1), + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-3: + 0px 1px 2px hsl(0, 0%, 0%, 28%), 0px 2px 6px hsl(0, 0%, 0%, 14%), + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-4: + 0px 0px 3px hsl(0, 0%, 0%, 19%), 0px 5px 4px hsl(0, 0%, 0%, 16%), + 0px 2px 16px hsl(0, 0%, 0%, 6%), + inset 0px 0px 0px 1px var(--color-panel-contrast); } .tl-theme__dark { - /* Canvas */ - --color-snap: hsl(0, 76%, 60%); - --color-selection-fill: hsl(209, 100%, 57%, 20%); - --color-selection-stroke: hsl(214, 84%, 56%); - --color-background: hsl(240, 5%, 6.5%); - --color-brush-fill: hsl(0, 0%, 71%, 5.1%); - --color-brush-stroke: hsl(0, 0%, 71%, 25.1%); - --color-grid: hsl(0, 0%, 40%); - /* UI */ - --color-low: hsl(260, 4.5%, 10.5%); - --color-low-border: hsl(207, 10%, 10%); - --color-culled: hsl(210, 11%, 19%); - --color-muted-none: hsl(0, 0%, 100%, 0%); - --color-muted-0: hsl(0, 0%, 100%, 2%); - --color-muted-1: hsl(0, 0%, 100%, 10%); - --color-muted-2: hsl(0, 0%, 100%, 5%); - --color-hint: hsl(0, 0%, 100%, 7%); - --color-overlay: hsl(0, 0%, 0%, 50%); - --color-divider: hsl(240, 9%, 22%); - --color-panel: hsl(235, 6.8%, 13.5%); - --color-panel-contrast: hsl(245, 12%, 23%); - --color-panel-overlay: hsl(210, 10%, 24%, 82%); - --color-panel-transparent: hsla(235, 6.8%, 13.5%, 0%); - --color-selected: hsl(217, 89%, 61%); - --color-selected-contrast: hsl(0, 0%, 100%); - --color-focus: hsl(217, 76%, 80%); - /* Text */ - --color-text: hsl(210, 17%, 98%); - --color-text-0: hsl(0, 9%, 94%); - --color-text-1: hsl(0, 0%, 85%); - --color-text-3: hsl(210, 6%, 45%); - --color-text-shadow: hsl(210, 13%, 18%); - --color-text-highlight: hsl(52, 100%, 41%); - --color-text-highlight-p3: color(display-p3 0.8078 0.6225 0.0312); - /* Named */ - --color-primary: hsl(214, 84%, 56%); - --color-success: hsl(123, 38%, 57%); - --color-info: hsl(199, 92%, 56%); - --color-warning: hsl(36, 100%, 57%); - --color-danger: hsl(0, 82%, 66%); - --color-laser: hsl(0, 100%, 50%); - /* Shadows */ - --shadow-1: - 0px 1px 2px hsl(0, 0%, 0%, 16.1%), 0px 1px 3px hsl(0, 0%, 0%, 22%), - inset 0px 0px 0px 1px var(--color-panel-contrast); - --shadow-2: - 0px 1px 3px hsl(0, 0%, 0%, 66.6%), 0px 2px 6px hsl(0, 0%, 0%, 33%), - inset 0px 0px 0px 1px var(--color-panel-contrast); - --shadow-3: - 0px 1px 3px hsl(0, 0%, 0%, 50%), 0px 2px 12px hsl(0, 0%, 0%, 50%), - inset 0px 0px 0px 1px var(--color-panel-contrast); + /* Canvas */ + --color-snap: hsl(0, 76%, 60%); + --color-selection-fill: hsl(209, 100%, 57%, 20%); + --color-selection-stroke: hsl(214, 84%, 56%); + --color-background: hsl(240, 5%, 6.5%); + --color-brush-fill: hsl(0, 0%, 71%, 5.1%); + --color-brush-stroke: hsl(0, 0%, 71%, 25.1%); + --color-grid: hsl(0, 0%, 40%); + /* UI */ + --color-low: hsl(260, 4.5%, 10.5%); + --color-low-border: hsl(207, 10%, 10%); + --color-culled: hsl(210, 11%, 19%); + --color-muted-none: hsl(0, 0%, 100%, 0%); + --color-muted-0: hsl(0, 0%, 100%, 2%); + --color-muted-1: hsl(0, 0%, 100%, 10%); + --color-muted-2: hsl(0, 0%, 100%, 5%); + --color-hint: hsl(0, 0%, 100%, 7%); + --color-overlay: hsl(0, 0%, 0%, 50%); + --color-divider: hsl(240, 9%, 22%); + --color-panel: hsl(235, 6.8%, 13.5%); + --color-panel-contrast: hsl(245, 12%, 23%); + --color-panel-overlay: hsl(210, 10%, 24%, 82%); + --color-panel-transparent: hsla(235, 6.8%, 13.5%, 0%); + --color-selected: hsl(217, 89%, 61%); + --color-selected-contrast: hsl(0, 0%, 100%); + --color-focus: hsl(217, 76%, 80%); + /* Text */ + --color-text: hsl(210, 17%, 98%); + --color-text-0: hsl(0, 9%, 94%); + --color-text-1: hsl(0, 0%, 85%); + --color-text-3: hsl(210, 6%, 45%); + --color-text-shadow: hsl(210, 13%, 18%); + --color-text-highlight: hsl(52, 100%, 41%); + --color-text-highlight-p3: color(display-p3 0.8078 0.6225 0.0312); + /* Named */ + --color-primary: hsl(214, 84%, 56%); + --color-success: hsl(123, 38%, 57%); + --color-info: hsl(199, 92%, 56%); + --color-warning: hsl(36, 100%, 57%); + --color-danger: hsl(0, 82%, 66%); + --color-laser: hsl(0, 100%, 50%); + /* Shadows */ + --shadow-1: + 0px 1px 2px hsl(0, 0%, 0%, 16.1%), 0px 1px 3px hsl(0, 0%, 0%, 22%), + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-2: + 0px 1px 3px hsl(0, 0%, 0%, 66.6%), 0px 2px 6px hsl(0, 0%, 0%, 33%), + inset 0px 0px 0px 1px var(--color-panel-contrast); + --shadow-3: + 0px 1px 3px hsl(0, 0%, 0%, 50%), 0px 2px 12px hsl(0, 0%, 0%, 50%), + inset 0px 0px 0px 1px var(--color-panel-contrast); } .tl-counter-scaled { - transform: scale(var(--tl-scale)); - transform-origin: top left; - width: calc(100% * var(--tl-zoom)); - height: calc(100% * var(--tl-zoom)); + transform: scale(var(--tl-scale)); + transform-origin: top left; + width: calc(100% * var(--tl-zoom)); + height: calc(100% * var(--tl-zoom)); } .tl-container, .tl-container * { - -webkit-touch-callout: none; - -webkit-tap-highlight-color: transparent; - scrollbar-highlight-color: transparent; - -webkit-user-select: none; - user-select: none; - box-sizing: border-box; - outline: none; + -webkit-touch-callout: none; + -webkit-tap-highlight-color: transparent; + scrollbar-highlight-color: transparent; + -webkit-user-select: none; + user-select: none; + box-sizing: border-box; + outline: none; } .tl-container a { - -webkit-touch-callout: initial; + -webkit-touch-callout: initial; } .tl-container__focused { - outline: 1px solid var(--color-low); + outline: 1px solid var(--color-low); } input, *[contenteditable], *[contenteditable] * { - user-select: text; + user-select: text; } /* --------------------- Canvas --------------------- */ .tl-canvas { - position: absolute; - inset: 0px; - height: 100%; - width: 100%; - color: var(--color-text); - cursor: var(--tl-cursor); - overflow: clip; - content-visibility: auto; - touch-action: none; - contain: strict; + position: absolute; + inset: 0px; + height: 100%; + width: 100%; + color: var(--color-text); + cursor: var(--tl-cursor); + overflow: clip; + content-visibility: auto; + touch-action: none; + contain: strict; } .tl-shapes { - position: relative; - z-index: var(--layer-canvas-shapes); + position: relative; + z-index: var(--layer-canvas-shapes); } .tl-overlays { - position: absolute; - top: 0px; - left: 0px; - height: 100%; - width: 100%; - contain: strict; - pointer-events: none; - z-index: var(--layer-canvas-overlays); + position: absolute; + top: 0px; + left: 0px; + height: 100%; + width: 100%; + contain: strict; + pointer-events: none; + z-index: var(--layer-canvas-overlays); } .tl-overlays__item { - position: absolute; - top: 0px; - left: 0px; - overflow: visible; - pointer-events: none; - transform-origin: top left; + position: absolute; + top: 0px; + left: 0px; + overflow: visible; + pointer-events: none; + transform-origin: top left; } .tl-svg-context { - position: absolute; - top: 0px; - left: 0px; - width: 100%; - height: 100%; - pointer-events: none; + position: absolute; + top: 0px; + left: 0px; + width: 100%; + height: 100%; + pointer-events: none; } /* ------------------- Background ------------------- */ .tl-background__wrapper { - z-index: var(--layer-canvas-background); - position: absolute; - inset: 0px; - height: 100%; - width: 100%; + z-index: var(--layer-canvas-background); + position: absolute; + inset: 0px; + height: 100%; + width: 100%; } .tl-background { - background-color: var(--color-background); - width: 100%; - height: 100%; + background-color: var(--color-background); + width: 100%; + height: 100%; } /* --------------------- Grid Layer --------------------- */ .tl-grid { - position: absolute; - inset: 0px; - width: 100%; - height: 100%; - touch-action: none; - pointer-events: none; - z-index: var(--layer-canvas-grid); - contain: strict; + position: absolute; + inset: 0px; + width: 100%; + height: 100%; + touch-action: none; + pointer-events: none; + z-index: var(--layer-canvas-grid); + contain: strict; } .tl-grid-dot { - fill: var(--color-grid); + fill: var(--color-grid); } /* --------------------- Layers --------------------- */ .tl-html-layer { - position: absolute; - top: 0px; - left: 0px; - width: 1px; - height: 1px; - contain: layout style size; + position: absolute; + top: 0px; + left: 0px; + width: 1px; + height: 1px; + contain: layout style size; } /* --------------- Overlay Stack --------------- */ /* back of the stack, behind user's stuff */ .tl-collaborator__scribble { - z-index: var(--layer-overlays-collaborator-scribble); + z-index: var(--layer-overlays-collaborator-scribble); } .tl-collaborator__brush { - z-index: var(--layer-overlays-collaborator-brush); + z-index: var(--layer-overlays-collaborator-brush); } .tl-collaborator__shape-indicator { - z-index: var(--layer-overlays-collaborator-shape-indicator); + z-index: var(--layer-overlays-collaborator-shape-indicator); } .tl-user-scribble { - z-index: var(--layer-overlays-user-scribble); + z-index: var(--layer-overlays-user-scribble); } .tl-user-brush { - z-index: var(--layer-overlays-user-brush); + z-index: var(--layer-overlays-user-brush); } .tl-user-handles { - z-index: var(--layer-overlays-user-handles); + z-index: var(--layer-overlays-user-handles); } .tl-user-snapline { - z-index: var(--layer-overlays-user-snapline); + z-index: var(--layer-overlays-user-snapline); } .tl-selection__fg { - pointer-events: none; - z-index: var(--layer-overlays-selection-fg); + pointer-events: none; + z-index: var(--layer-overlays-selection-fg); } .tl-user-indicator__hint { - z-index: var(--layer-overlays-user-indicator-hint); - stroke-width: calc(2.5px * var(--tl-scale)); + z-index: var(--layer-overlays-user-indicator-hint); + stroke-width: calc(2.5px * var(--tl-scale)); } .tl-custom-overlays { - z-index: var(--layer-overlays-custom); + z-index: var(--layer-overlays-custom); } /* behind collaborator cursor */ .tl-collaborator__cursor-hint { - z-index: var(--layer-overlays-collaborator-cursor-hint); + z-index: var(--layer-overlays-collaborator-cursor-hint); } .tl-collaborator__cursor { - z-index: var(--layer-overlays-collaborator-cursor); + z-index: var(--layer-overlays-collaborator-cursor); } .tl-cursor { - overflow: visible; + overflow: visible; } /* -------------- Selection foreground -------------- */ .tl-selection__bg { - position: absolute; - top: 0px; - left: 0px; - transform-origin: top left; - background-color: transparent; - pointer-events: all; + position: absolute; + top: 0px; + left: 0px; + transform-origin: top left; + background-color: transparent; + pointer-events: all; } .tl-selection__fg__outline { - fill: none; - pointer-events: none; - stroke: var(--color-selection-stroke); - stroke-width: calc(1.5px * var(--tl-scale)); + fill: none; + pointer-events: none; + stroke: var(--color-selection-stroke); + stroke-width: calc(1.5px * var(--tl-scale)); } .tl-corner-handle { - pointer-events: none; - stroke: var(--color-selection-stroke); - fill: var(--color-background); - stroke-width: calc(1.5px * var(--tl-scale)); + pointer-events: none; + stroke: var(--color-selection-stroke); + fill: var(--color-background); + stroke-width: calc(1.5px * var(--tl-scale)); } .tl-text-handle { - pointer-events: none; - fill: var(--color-selection-stroke); + pointer-events: none; + fill: var(--color-selection-stroke); } .tl-corner-crop-handle { - pointer-events: none; - fill: none; - stroke: var(--color-selection-stroke); + pointer-events: none; + fill: none; + stroke: var(--color-selection-stroke); } .tl-corner-crop-edge-handle { - pointer-events: none; - fill: none; - stroke: var(--color-selection-stroke); + pointer-events: none; + fill: none; + stroke: var(--color-selection-stroke); } .tl-mobile-rotate__bg { - pointer-events: all; - cursor: var(--tl-cursor-grab); + pointer-events: all; + cursor: var(--tl-cursor-grab); } .tl-mobile-rotate__fg { - pointer-events: none; - stroke: var(--color-selection-stroke); - fill: var(--color-background); - stroke-width: calc(1.5px * var(--tl-scale)); + pointer-events: none; + stroke: var(--color-selection-stroke); + fill: var(--color-background); + stroke-width: calc(1.5px * var(--tl-scale)); } .tl-transparent { - fill: transparent; - stroke: transparent; + fill: transparent; + stroke: transparent; } .tl-hidden { - opacity: 0; - pointer-events: none; + opacity: 0; + pointer-events: none; } /* -------------- Nametag / cursor chat ------------- */ .tl-nametag { - position: absolute; - top: 16px; - left: 13px; - width: fit-content; - height: fit-content; - max-width: 120px; - padding: 3px 6px; - white-space: nowrap; - position: absolute; - overflow: hidden; - text-overflow: ellipsis; - font-size: 12px; - font-family: var(--font-body); - border-radius: var(--radius-2); - color: var(--color-selected-contrast); + position: absolute; + top: 16px; + left: 13px; + width: fit-content; + height: fit-content; + max-width: 120px; + padding: 3px 6px; + white-space: nowrap; + position: absolute; + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + font-family: var(--font-body); + border-radius: var(--radius-2); + color: var(--color-selected-contrast); } .tl-nametag-title { - position: absolute; - top: -2px; - left: 13px; - width: fit-content; - height: fit-content; - padding: 0px 6px; - max-width: 120px; - white-space: nowrap; - position: absolute; - overflow: hidden; - text-overflow: ellipsis; - font-size: 12px; - font-family: var(--font-body); - text-shadow: var(--tl-text-outline); - color: var(--color-selected-contrast); + position: absolute; + top: -2px; + left: 13px; + width: fit-content; + height: fit-content; + padding: 0px 6px; + max-width: 120px; + white-space: nowrap; + position: absolute; + overflow: hidden; + text-overflow: ellipsis; + font-size: 12px; + font-family: var(--font-body); + text-shadow: var(--tl-text-outline); + color: var(--color-selected-contrast); } .tl-nametag-chat { - position: absolute; - top: 16px; - left: 13px; - width: fit-content; - height: fit-content; - color: var(--color-selected-contrast); - white-space: nowrap; - position: absolute; - padding: 3px 6px; - font-size: 12px; - font-family: var(--font-body); - opacity: 1; - border-radius: var(--radius-2); + position: absolute; + top: 16px; + left: 13px; + width: fit-content; + height: fit-content; + color: var(--color-selected-contrast); + white-space: nowrap; + position: absolute; + padding: 3px 6px; + font-size: 12px; + font-family: var(--font-body); + opacity: 1; + border-radius: var(--radius-2); } .tl-cursor-chat { - position: absolute; - color: var(--color-selected-contrast); - white-space: nowrap; - padding: 3px 6px; - font-size: 12px; - font-family: var(--font-body); - pointer-events: none; - z-index: var(--layer-cursor); - margin-top: 16px; - margin-left: 13px; - opacity: 1; - border: none; - user-select: text; - border-radius: var(--radius-2); + position: absolute; + color: var(--color-selected-contrast); + white-space: nowrap; + padding: 3px 6px; + font-size: 12px; + font-family: var(--font-body); + pointer-events: none; + z-index: var(--layer-cursor); + margin-top: 16px; + margin-left: 13px; + opacity: 1; + border: none; + user-select: text; + border-radius: var(--radius-2); } .tl-cursor-chat .tl-cursor-chat__bubble { - padding-right: 12px; + padding-right: 12px; } .tl-cursor-chat::selection { - background: var(--color-selected); - color: var(--color-selected-contrast); - text-shadow: none; + background: var(--color-selected); + color: var(--color-selected-contrast); + text-shadow: none; } .tl-cursor-chat::placeholder { - color: var(--color-selected-contrast); - opacity: 0.7; + color: var(--color-selected-contrast); + opacity: 0.7; } /* ---------------------- Text ---------------------- */ .tl-text-shape-label { - position: relative; - font-weight: normal; - min-width: 1px; - padding: 0px; - margin: 0px; - border: none; - width: fit-content; - height: fit-content; - font-variant: normal; - font-style: normal; - pointer-events: all; - white-space: pre-wrap; - overflow-wrap: break-word; - text-shadow: var(--tl-text-outline); + position: relative; + font-weight: normal; + min-width: 1px; + padding: 0px; + margin: 0px; + border: none; + width: fit-content; + height: fit-content; + font-variant: normal; + font-style: normal; + pointer-events: all; + white-space: pre-wrap; + overflow-wrap: break-word; + text-shadow: var(--tl-text-outline); } -.tl-text-wrapper[data-font='draw'] { - font-family: var(--tl-font-draw); +.tl-text-wrapper[data-font="draw"] { + font-family: var(--tl-font-draw); } -.tl-text-wrapper[data-font='sans'] { - font-family: var(--tl-font-sans); +.tl-text-wrapper[data-font="sans"] { + font-family: var(--tl-font-sans); } -.tl-text-wrapper[data-font='serif'] { - font-family: var(--tl-font-serif); +.tl-text-wrapper[data-font="serif"] { + font-family: var(--tl-font-serif); } -.tl-text-wrapper[data-font='mono'] { - font-family: var(--tl-font-mono); +.tl-text-wrapper[data-font="mono"] { + font-family: var(--tl-font-mono); } -.tl-text-wrapper[data-align='start'], -.tl-text-wrapper[data-align='start-legacy'] { - text-align: left; +.tl-text-wrapper[data-align="start"], +.tl-text-wrapper[data-align="start-legacy"] { + text-align: left; } -.tl-text-wrapper[data-align='middle'], -.tl-text-wrapper[data-align='middle-legacy'] { - text-align: center; +.tl-text-wrapper[data-align="middle"], +.tl-text-wrapper[data-align="middle-legacy"] { + text-align: center; } -.tl-text-wrapper[data-align='end'], -.tl-text-wrapper[data-align='end-legacy'] { - text-align: right; +.tl-text-wrapper[data-align="end"], +.tl-text-wrapper[data-align="end-legacy"] { + text-align: right; } -.tl-plain-text-wrapper[data-isediting='true'] .tl-text-content { - opacity: 0; +.tl-plain-text-wrapper[data-isediting="true"] .tl-text-content { + opacity: 0; } -.tl-rich-text-wrapper[data-isediting='true'] .tl-text-content { - display: none; +.tl-rich-text-wrapper[data-isediting="true"] .tl-text-content { + display: none; } .tl-text { - /* remove overflow from textarea on windows */ - margin: 0px; - padding: 0px; - - appearance: auto; - background: none; - border-image: none; - border: 0px; - caret-color: var(--color-text); - color: inherit; - column-count: initial !important; - display: inline-block; - font-family: inherit; - font-feature-settings: normal; - font-kerning: auto; - font-optical-sizing: auto; - font-size: inherit; - font-stretch: 100%; - font-style: inherit; - font-variant: inherit; - font-variation-settings: normal; - font-weight: inherit; - letter-spacing: inherit; - line-height: inherit; - outline: none; - overflow-wrap: break-word; - text-align: inherit; - text-indent: 0px; - text-rendering: auto; - text-shadow: inherit; - text-transform: none; - white-space: pre-wrap; - line-break: normal; - word-spacing: 0px; - word-wrap: break-word; - writing-mode: horizontal-tb !important; + /* remove overflow from textarea on windows */ + margin: 0px; + padding: 0px; + + appearance: auto; + background: none; + border-image: none; + border: 0px; + caret-color: var(--color-text); + color: inherit; + column-count: initial !important; + display: inline-block; + font-family: inherit; + font-feature-settings: normal; + font-kerning: auto; + font-optical-sizing: auto; + font-size: inherit; + font-stretch: 100%; + font-style: inherit; + font-variant: inherit; + font-variation-settings: normal; + font-weight: inherit; + letter-spacing: inherit; + line-height: inherit; + outline: none; + overflow-wrap: break-word; + text-align: inherit; + text-indent: 0px; + text-rendering: auto; + text-shadow: inherit; + text-transform: none; + white-space: pre-wrap; + line-break: normal; + word-spacing: 0px; + word-wrap: break-word; + writing-mode: horizontal-tb !important; } .tl-text-measure { - position: absolute; - z-index: var(--layer-canvas-hidden); - top: 0px; - left: 0px; - opacity: 0; - width: max-content; - box-sizing: border-box; - pointer-events: none; - white-space: pre-wrap; - word-wrap: break-word; - overflow-wrap: break-word; - resize: none; - border: none; - user-select: none; - contain: style paint; - visibility: hidden; - /* N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers") is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs. */ - unicode-bidi: plaintext; - -webkit-user-select: none; + position: absolute; + z-index: var(--layer-canvas-hidden); + top: 0px; + left: 0px; + opacity: 0; + width: max-content; + box-sizing: border-box; + pointer-events: none; + white-space: pre-wrap; + word-wrap: break-word; + overflow-wrap: break-word; + resize: none; + border: none; + user-select: none; + contain: style paint; + visibility: hidden; + /* N.B. This property, while discouraged ("intended for Document Type Definition (DTD) designers") is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs. */ + unicode-bidi: plaintext; + -webkit-user-select: none; } .tl-text-input, .tl-text-content { - position: absolute; - inset: 0px; - height: 100%; - width: 100%; - min-width: 1px; - min-height: 1px; - outline: none; + position: absolute; + inset: 0px; + height: 100%; + width: 100%; + min-width: 1px; + min-height: 1px; + outline: none; } .tl-text-content__wrapper { - position: relative; - width: fit-content; - height: fit-content; - display: flex; - align-items: center; - justify-content: center; - pointer-events: none; - min-height: auto; + position: relative; + width: fit-content; + height: fit-content; + display: flex; + align-items: center; + justify-content: center; + pointer-events: none; + min-height: auto; } .tl-text-content { - overflow: visible; - pointer-events: none; + overflow: visible; + pointer-events: none; } .tl-text-input { - resize: none; - user-select: all; - -webkit-user-select: text; - cursor: var(--tl-cursor-text); + resize: none; + user-select: all; + -webkit-user-select: text; + cursor: var(--tl-cursor-text); } .tl-text-input:not(.tl-rich-text) { - /* + /* * Note: this `overflow: hidden` is key for scrollbars to not show up * plaintext/