@oh-just-another/diagram 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +68 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/bindings.d.ts +73 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +77 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/index.d.ts +18 -0
  9. package/dist/index.d.ts.map +1 -0
  10. package/dist/index.js +20 -0
  11. package/dist/index.js.map +1 -0
  12. package/dist/oja-diagram-element.d.ts +42 -0
  13. package/dist/oja-diagram-element.d.ts.map +1 -0
  14. package/dist/oja-diagram-element.js +202 -0
  15. package/dist/oja-diagram-element.js.map +1 -0
  16. package/dist/oja-diagram.global.js +1846 -0
  17. package/dist/oja-diagram.global.js.map +7 -0
  18. package/dist/render-worker.js +5 -0
  19. package/dist/render-worker.js.map +7 -0
  20. package/dist/styles.generated.d.ts +2 -0
  21. package/dist/styles.generated.d.ts.map +1 -0
  22. package/dist/styles.generated.js +4 -0
  23. package/dist/styles.generated.js.map +1 -0
  24. package/fonts/LICENSE +25 -0
  25. package/fonts/PTSerif-Bold.woff2 +0 -0
  26. package/fonts/PTSerif-BoldItalic.woff2 +0 -0
  27. package/fonts/PTSerif-Italic.woff2 +0 -0
  28. package/fonts/PTSerif-Regular.woff2 +0 -0
  29. package/fonts/Roboto-Bold.woff2 +0 -0
  30. package/fonts/Roboto-BoldItalic.woff2 +0 -0
  31. package/fonts/Roboto-Italic.woff2 +0 -0
  32. package/fonts/Roboto-Regular.woff2 +0 -0
  33. package/fonts/RobotoMono-Bold.woff2 +0 -0
  34. package/fonts/RobotoMono-BoldItalic.woff2 +0 -0
  35. package/fonts/RobotoMono-Italic.woff2 +0 -0
  36. package/fonts/RobotoMono-Regular.woff2 +0 -0
  37. package/package.json +72 -0
  38. package/wasm/rasterizer.wasm +0 -0
  39. package/wasm/text_shaper.wasm +0 -0
@@ -0,0 +1,2 @@
1
+ export declare const REACT_UI_STYLES = "/**\n * Diagram UI stylesheet. Imported once by the host:\n *\n * import \"@oh-just-another/react-ui/styles.css\";\n *\n * Provides:\n * \u2022 CSS variables for theme (`:root` + `[data-theme=\"dark\"]`)\n * \u2022 Utility classes prefixed `du-` (\"diagram ui\") so they don't\n * collide with host's own styles.\n * \u2022 Per-component blocks (.du-icon-button, .du-top-bar, ...).\n *\n * Hosts can override any variable in their own stylesheet for\n * theming \u2014 every component reads colours / sizes through these\n * variables, never hard-coded.\n *\n * Colour values mirror `@oh-just-another/tokens` \u2014 CSS can't import\n * TypeScript, so values are hand-copied with comments pointing\n * back to the token name. Keep this in sync when you change a\n * value in `packages/tokens/src/colors.ts`.\n */\n\n:root {\n /* --- Surfaces (from UI_SURFACE.light) --- */\n --du-canvas-bg: #f5f5f5; /* UI_SURFACE.light.canvas */\n --du-ui-bg: rgba(255, 255, 255, 0.95); /* UI_SURFACE.light.bg */\n --du-ui-bg-solid: #ffffff; /* UI_SURFACE.light.bgSolid */\n --du-ui-border: rgba(0, 0, 0, 0.08); /* UI_SURFACE.light.border */\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04);\n\n /* --- Text (from UI_SURFACE.light) --- */\n --du-text: #1a1a1a; /* UI_SURFACE.light.text */\n --du-text-muted: #6b6b6b; /* UI_SURFACE.light.textMuted */\n --du-text-on-accent: #ffffff;\n\n /* --- Accent (from UI_ACCENT.light \u2014 Radix iris) --- */\n --du-accent: #5b5bd6; /* UI_ACCENT.light.accent (iris.iris9) */\n --du-accent-hover: #5151cd; /* UI_ACCENT.light.accentHover (iris.iris10) */\n --du-danger: #e54d2e; /* UI_ACCENT.light.danger (tomato.tomato9) */\n\n /* --- Selected (tonal, modern-style) --- */\n /* Active toolbar / icon buttons get a *muted* tonal fill so they\n read as \"this state is selected\" without hijacking the eye like\n a saturated accent fill would. The icon switches to the accent\n hue, the background stays close to the surface. */\n --du-selected-bg: #e6e7ff; /* UI_ACCENT.light.selectedBg (iris.iris4) */\n --du-selected-fg: #5753c6; /* UI_ACCENT.light.selectedFg (iris.iris11) */\n\n /* --- Sizing --- */\n --du-radius: 10px;\n --du-radius-sm: 6px;\n --du-radius-lg: 14px;\n --du-button-size: 36px;\n --du-button-size-sm: 32px;\n --du-icon-size: 18px;\n --du-gap: 8px;\n --du-gap-sm: 4px;\n --du-pad: 8px;\n --du-pad-sm: 4px;\n --du-hover-overlay: rgba(0, 0, 0, 0.05);\n --du-bar-inset: 12px;\n --du-z-canvas: 1;\n --du-z-ui: 100;\n --du-z-modal: 1000;\n}\n\n/**\n * Theme resolution priority (highest wins):\n * [data-theme=\"light\"] \u2014 host forced light.\n * [data-theme=\"dark\"] \u2014 host forced dark.\n * @media prefers-color-scheme: dark \u2192 :root \u2014 OS-derived dark when\n * no data-theme is present.\n * :root \u2014 light defaults.\n *\n * Hosts set `data-theme` via the Diagram component's `theme` prop\n * (\"light\" / \"dark\" / \"system\" \u2014 \"system\" omits the attribute so\n * the media query takes over).\n */\n@media (prefers-color-scheme: dark) {\n :root {\n /* All values mirror UI_SURFACE.dark + UI_ACCENT.dark from\n `@oh-just-another/tokens`. */\n --du-canvas-bg: #121212; /* UI_SURFACE.dark.canvas */\n --du-ui-bg: rgba(35, 35, 35, 0.95); /* UI_SURFACE.dark.bg */\n --du-ui-bg-solid: #252525; /* UI_SURFACE.dark.bgSolid */\n --du-ui-border: rgba(255, 255, 255, 0.08); /* UI_SURFACE.dark.border */\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3);\n --du-text: #e8e8e8; /* UI_SURFACE.dark.text */\n --du-text-muted: #9a9a9a; /* UI_SURFACE.dark.textMuted */\n --du-accent: #5b5bd6; /* UI_ACCENT.dark.accent (irisDark.iris9) */\n --du-accent-hover: #6e6ade; /* UI_ACCENT.dark.accentHover (irisDark.iris10) */\n --du-hover-overlay: rgba(255, 255, 255, 0.06); /* UI_SURFACE.dark.hoverOverlay */\n --du-selected-bg: #262a65; /* UI_ACCENT.dark.selectedBg (irisDark.iris4) */\n --du-selected-fg: #b1a9ff; /* UI_ACCENT.dark.selectedFg (irisDark.iris11) */\n }\n}\n\n[data-theme=\"light\"] {\n /* Same as :root \u2014 explicit override so a host that toggles\n `data-theme` between \"light\" / \"dark\" / unset still picks\n up the light palette under all conditions. Mirrors\n UI_SURFACE.light + UI_ACCENT.light. */\n --du-canvas-bg: #f5f5f5;\n --du-ui-bg: rgba(255, 255, 255, 0.95);\n --du-ui-bg-solid: #ffffff;\n --du-ui-border: rgba(0, 0, 0, 0.08);\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04);\n --du-text: #1a1a1a;\n --du-text-muted: #6b6b6b;\n --du-accent: #5b5bd6; /* iris.iris9 */\n --du-accent-hover: #5151cd; /* iris.iris10 */\n --du-hover-overlay: rgba(0, 0, 0, 0.05);\n --du-selected-bg: #e6e7ff; /* iris.iris4 */\n --du-selected-fg: #5753c6; /* iris.iris11 */\n}\n\n/* ------------------------------------------------------------------ */\n/* Legacy variable aliases \u2014 kept so the older components (MainMenu, */\n/* ContextMenu, Modal, HelpDialog) that read `var(--panel)` / */\n/* `var(--text)` / `var(--button-bg)` etc. still pick up the active */\n/* theme without being rewritten to the `--du-*` namespace. */\n/* */\n/* Hosts can still override the legacy names individually \u2014 these */\n/* are defaults that *forward* to the unified theme variables. */\n/* */\n/* Declared at every theme scope (not just `:root`) on purpose: a */\n/* `var()` inside a custom property resolves on the element where the */\n/* property is declared. If these lived on `:root` alone, the alias */\n/* would bake in `:root`'s `--du-*` (e.g. the OS `prefers-color-scheme` */\n/* dark value) and inherit that frozen colour past a `[data-theme]` */\n/* override \u2014 so a light app would still get a dark context menu. */\n/* Re-declaring per scope lets each alias re-resolve against the */\n/* scoped `--du-*`, including the themed portal wrapper menus mount */\n/* into. */\n/* ------------------------------------------------------------------ */\n:root,\n[data-theme=\"light\"],\n[data-theme=\"dark\"] {\n --bg: var(--du-canvas-bg);\n --panel: var(--du-ui-bg-solid);\n --surface: var(--du-ui-bg-solid);\n --text: var(--du-text);\n --muted: var(--du-text-muted);\n --border: var(--du-ui-border);\n --button-bg: var(--du-ui-bg-solid);\n --button-hover-bg: var(--du-ui-bg);\n --accent: var(--du-accent);\n --menu-bg: var(--du-ui-bg-solid);\n --menu-text: var(--du-text);\n --menu-border: var(--du-ui-border);\n --menu-divider: var(--du-ui-border);\n /* Remaining legacy names the chrome reads \u2014 defined here (forwarding to the\n theme-aware `--du-*` set) so the editor is fully themed from this stylesheet\n alone, without a host-provided token block. */\n --divider: var(--du-ui-border);\n --text-strong: var(--du-text);\n --faint: var(--du-text-muted);\n --toolbar-bg: var(--du-ui-bg-solid);\n --cursor-bg: var(--du-selected-bg);\n --code-bg: var(--du-ui-bg-solid);\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n}\n\n[data-theme=\"dark\"] {\n /* Mirrors UI_SURFACE.dark + UI_ACCENT.dark from `@oh-just-another/tokens`. */\n --du-canvas-bg: #121212;\n --du-ui-bg: rgba(35, 35, 35, 0.95);\n --du-ui-bg-solid: #252525;\n --du-ui-border: rgba(255, 255, 255, 0.08);\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3);\n --du-text: #e8e8e8;\n --du-text-muted: #9a9a9a;\n --du-accent: #5b5bd6; /* irisDark.iris9 */\n --du-accent-hover: #6e6ade; /* irisDark.iris10 */\n --du-hover-overlay: rgba(255, 255, 255, 0.06);\n --du-selected-bg: #262a65; /* irisDark.iris4 */\n --du-selected-fg: #b1a9ff; /* irisDark.iris11 */\n}\n\n/* ------------------------------------------------------------------ */\n/* Canvas surface focus ring. The surface is `tabIndex=0` so keyboard */\n/* users can land on it, but it also takes focus on a pointer press */\n/* (so shortcuts work right after a click). Gate the ring on */\n/* `:focus-visible` so it shows ONLY for keyboard focus \u2014 a mouse click */\n/* never draws an input-like outline. Class+pseudo specificity (0,2,0) */\n/* beats a host's `*:focus { outline: none }`. */\n/* ------------------------------------------------------------------ */\n.du-canvas-surface {\n outline: none;\n}\n.du-canvas-surface:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: -2px;\n}\n\n/* ------------------------------------------------------------------ */\n/* UI layer \u2014 absolute overlay above the canvas surface. */\n/* pointer-events: none on the wrapper, auto on children, so the */\n/* canvas keeps receiving pointer events anywhere the UI isn't drawn. */\n/* ------------------------------------------------------------------ */\n.du-ui-layer {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: var(--du-z-ui);\n}\n\n.du-ui-layer > * {\n pointer-events: auto;\n}\n\n/* ------------------------------------------------------------------ */\n/* Bars \u2014 top / bottom 3-zone layout (left / center / right). */\n/* ------------------------------------------------------------------ */\n.du-bar {\n position: absolute;\n left: var(--du-bar-inset);\n right: var(--du-bar-inset);\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--du-gap);\n pointer-events: none;\n}\n\n.du-bar > * {\n pointer-events: auto;\n}\n\n.du-bar-top {\n top: var(--du-bar-inset);\n}\n\n.du-bar-bottom {\n bottom: var(--du-bar-inset);\n}\n\n.du-bar-zone {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap);\n}\n\n.du-bar-zone-center {\n margin: 0 auto;\n}\n\n/* ------------------------------------------------------------------ */\n/* Icon button \u2014 square clickable. Two visual modes: */\n/* 1. Standalone (outside `du-button-group`): full pill with */\n/* surface, border, shadow. */\n/* 2. Inside `du-button-group`: transparent surface, subtle radius, */\n/* lets the group's pill chrome carry the framing. */\n/* ------------------------------------------------------------------ */\n.du-icon-button {\n width: var(--du-button-size);\n height: var(--du-button-size);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--du-ui-bg);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n cursor: pointer;\n font-size: 14px;\n line-height: 1;\n box-shadow: var(--du-ui-shadow);\n transition:\n background 0.12s ease,\n color 0.12s ease,\n transform 0.05s ease;\n padding: 0;\n outline: none;\n user-select: none;\n}\n\n.du-icon-button:hover:not(:disabled) {\n background: var(--du-ui-bg-solid);\n}\n\n.du-icon-button:active:not(:disabled) {\n transform: scale(0.96);\n}\n\n.du-icon-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.du-icon-button:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 2px;\n}\n\n.du-icon-button[aria-pressed=\"true\"],\n.du-icon-button.is-active {\n background: var(--du-selected-bg);\n color: var(--du-selected-fg);\n border-color: var(--du-selected-bg);\n}\n\n.du-icon-button[aria-pressed=\"true\"]:hover:not(:disabled),\n.du-icon-button.is-active:hover:not(:disabled) {\n /* Slight tint shift on hover \u2014 keeps the \"selected\" cue but\n acknowledges the cursor. Same hue family, marginally darker. */\n background: color-mix(in srgb, var(--du-selected-bg) 90%, var(--du-text) 10%);\n}\n\n.du-icon-button-sm {\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n font-size: 12px;\n}\n\n/**\n * Flat modifier \u2014 strips the standalone pill chrome (background,\n * border, shadow) and adopts the same look as an IconButton sitting\n * inside a `du-button-group`. Use this when a button has to live\n * inside a container that the descendant selector can't reach (e.g.\n * the MainMenu trigger, which is wrapped in a positioning <div> so\n * its dropdown can be `position: absolute`).\n */\n.du-icon-button-flat {\n background: transparent;\n border: none;\n box-shadow: none;\n border-radius: var(--du-radius-sm);\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n.du-icon-button-flat:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n/* ------------------------------------------------------------------ */\n/* Button group \u2014 pill that wraps one or more IconButtons. Group */\n/* carries the surface, border, shadow; the buttons inside drop */\n/* their own chrome so the row reads as one cohesive control. */\n/* Padding + gap match standard's tool bar feel. */\n/* ------------------------------------------------------------------ */\n.du-button-group {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap-sm);\n background: var(--du-ui-bg);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n padding: var(--du-pad-sm);\n /* No `overflow: hidden` \u2014 a child popup (MainMenu dropdown) must\n be able to render outside the group's bounds. */\n}\n\n/* Vertical variant \u2014 the left creation dock. Stacks buttons in a\n column; dividers (rendered inline by <Toolbar>) become horizontal. */\n.du-button-group-vertical {\n flex-direction: column;\n align-items: stretch;\n}\n\n.du-button-group > .du-icon-button {\n border: none;\n box-shadow: none;\n border-radius: var(--du-radius-sm);\n background: transparent;\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n/* Vertical dock buttons are square + centred (icon-only tools). */\n.du-button-group-vertical > .du-icon-button {\n width: var(--du-button-size-sm) !important;\n min-width: var(--du-button-size-sm) !important;\n padding: 0 !important;\n}\n\n/* ------------------------------------------------------------------ */\n/* Mobile / coarse-pointer chrome (keep the 640px breakpoint in sync */\n/* with MOBILE_MAX_WIDTH_PX in constants.ts). */\n/* ------------------------------------------------------------------ */\n@media (pointer: coarse), (max-width: 640px) {\n /* Bars clear the notch / home indicator + rounded corners. */\n .du-bar-top {\n top: calc(var(--du-bar-inset) + env(safe-area-inset-top, 0px));\n }\n .du-bar-bottom {\n bottom: calc(var(--du-bar-inset) + env(safe-area-inset-bottom, 0px));\n }\n .du-bar {\n left: calc(var(--du-bar-inset) + env(safe-area-inset-left, 0px));\n right: calc(var(--du-bar-inset) + env(safe-area-inset-right, 0px));\n }\n\n /* Vertical creation dock: \u226544px touch targets; scroll inside the dock\n when a short screen can't fit every tool. */\n .du-button-group-vertical {\n max-height: calc(\n 100vh - 2 * var(--du-bar-inset) - env(safe-area-inset-top, 0px) -\n env(safe-area-inset-bottom, 0px)\n );\n overflow-y: auto;\n scrollbar-width: none;\n }\n .du-button-group-vertical::-webkit-scrollbar {\n display: none;\n }\n .du-button-group-vertical > .du-icon-button {\n width: 44px !important;\n min-width: 44px !important;\n height: 44px !important;\n }\n}\n\n.du-button-group > .du-icon-button:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n.du-button-group > .du-icon-button[aria-pressed=\"true\"],\n.du-button-group > .du-icon-button.is-active {\n background: var(--du-selected-bg);\n color: var(--du-selected-fg);\n}\n\n.du-button-group > .du-icon-button[aria-pressed=\"true\"]:hover:not(:disabled),\n.du-button-group > .du-icon-button.is-active:hover:not(:disabled) {\n background: color-mix(in srgb, var(--du-selected-bg) 90%, var(--du-text) 10%);\n}\n\n/* ------------------------------------------------------------------ */\n/* Floating side panel \u2014 used by Library / SelectedShapeActions. */\n/* Fixed-width, fixed-height-with-scroll, sits flush against the */\n/* corresponding side of the canvas with a `du-bar-inset` margin. */\n/* ------------------------------------------------------------------ */\n.du-side-panel {\n position: absolute;\n top: calc(var(--du-bar-inset) + var(--du-button-size) + var(--du-gap));\n bottom: calc(var(--du-bar-inset) + var(--du-button-size) + var(--du-gap));\n width: 240px;\n background: var(--du-ui-bg);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.du-side-panel-left {\n left: var(--du-bar-inset);\n}\n\n.du-side-panel-right {\n right: var(--du-bar-inset);\n}\n\n/* Sheet mode \u2014 fills the parent BottomSheet; the sheet provides the\n surface, so drop the floating-pill position / border / shadow. */\n.du-side-panel.du-side-panel-sheet {\n position: static;\n top: auto;\n bottom: auto;\n left: auto;\n right: auto;\n width: 100%;\n height: 100%;\n border: none;\n border-radius: 0;\n box-shadow: none;\n background: transparent;\n}\n\n.du-side-panel-header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--du-gap-sm);\n padding: var(--du-pad-sm) var(--du-pad-sm) var(--du-pad-sm) var(--du-pad);\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--du-text-muted);\n}\n\n/* Hide the header when it's empty (no action buttons configured). */\n.du-side-panel-header:empty {\n display: none;\n}\n\n/* Auto-height variant \u2014 content hugs its natural height instead\n of stretching from top to bottom of the canvas area. Used by\n the Properties panel so it shrinks to whatever the inspector\n sections need. The Library panel keeps the default full-height\n so its long grid can scroll. */\n.du-side-panel-auto-height {\n bottom: auto;\n max-height: calc(100% - 2 * var(--du-bar-inset) - 2 * var(--du-button-size) - 2 * var(--du-gap));\n}\n\n.du-side-panel-body {\n flex: 1;\n overflow-y: auto;\n padding: var(--du-pad);\n}\n\n/* Variant for hosts that lay out their own grid / list inside the\n panel body (e.g. <Palette>) and would otherwise have to subtract\n the body's padding from their own paddings. Keeps overflow + flex\n behaviour. */\n.du-side-panel-body-flush {\n padding: 0;\n}\n\n/* ------------------------------------------------------------------ */\n/* Palette (template library content) \u2014 sectioned grid of icon tiles. */\n/* No per-tile borders, no per-section dividers \u2014 visual structure */\n/* comes from spacing alone so the panel reads as one calm surface. */\n/* ------------------------------------------------------------------ */\n.du-palette {\n display: flex;\n flex-direction: column;\n min-height: 0;\n color: var(--du-text);\n width: 100%;\n}\n\n.du-palette-body {\n flex: 1 1 auto;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n}\n\n.du-palette-section {\n padding: 0 var(--du-pad) var(--du-pad-sm);\n}\n\n.du-palette-category {\n width: 100%;\n background: transparent;\n color: var(--du-text-muted);\n border: none;\n padding: 10px 0 6px;\n font: inherit;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--du-gap-sm);\n}\n\n.du-palette-category-caret {\n display: inline-flex;\n align-items: center;\n opacity: 0.6;\n}\n\n.du-palette-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: var(--du-gap-sm);\n align-content: start;\n padding: 0 var(--du-pad) var(--du-pad-sm);\n}\n\n/* `.du-palette-section > .du-palette-grid` already gets padding via\n .du-palette-section; remove the duplicate to avoid double margin. */\n.du-palette-section > .du-palette-grid {\n padding: 0;\n}\n\n/* Compact list layout (modern-style templates panel) \u2014 single column of\n icon + name rows. Denser than the grid; the label reads left-aligned\n beside a small icon. */\n.du-palette-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 0 var(--du-pad) var(--du-pad-sm);\n}\n.du-palette-section > .du-palette-list {\n padding: 0;\n}\n.du-palette-list .du-palette-item {\n flex-direction: row;\n align-items: center;\n justify-content: flex-start;\n aspect-ratio: auto;\n gap: 8px;\n padding: 5px 8px;\n text-align: left;\n font-size: 12px;\n line-height: 1.2;\n}\n.du-palette-list .du-palette-item-icon {\n width: 20px !important;\n height: 20px !important;\n flex: 0 0 auto;\n}\n.du-palette-list .du-palette-item-label {\n flex: 1 1 auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.du-palette-empty {\n padding: 24px 12px;\n text-align: center;\n font-size: 12px;\n color: var(--du-text-muted);\n}\n\n.du-palette-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n aspect-ratio: 1;\n padding: 6px 4px;\n background: transparent;\n color: var(--du-text);\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: grab;\n text-align: center;\n font-size: 10px;\n line-height: 1.1;\n transition: background 0.12s ease;\n}\n\n.du-palette-item:hover {\n background: var(--du-hover-overlay);\n}\n\n.du-palette-item:active {\n cursor: grabbing;\n}\n\n.du-palette-item-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--du-text);\n}\n\n.du-palette-item-icon svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.du-palette-item-label {\n /* Two-line clamp so long names don't push the tile out of square. */\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ------------------------------------------------------------------ */\n/* Property panel \u2014 modern-style sectioned inspector. */\n/* Sections are stacked top-to-bottom; each one has a tiny uppercase */\n/* label, then a row of controls (color swatches, segmented buttons, */\n/* slider). No card chrome \u2014 visual structure comes from spacing. */\n/* ------------------------------------------------------------------ */\n.du-prop-panel {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 4px 0;\n color: var(--du-text);\n font-size: 12px;\n}\n\n.du-prop-section {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.du-prop-section-label {\n margin: 0;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--du-text-muted);\n}\n\n.du-prop-row {\n display: flex;\n align-items: center;\n gap: var(--du-gap-sm);\n flex-wrap: wrap;\n}\n\n.du-prop-row-label {\n font-size: 11px;\n color: var(--du-text-muted);\n min-width: 48px;\n}\n\n.du-prop-empty {\n font-style: italic;\n color: var(--du-text-muted);\n font-size: 12px;\n}\n\n/* modern-style flat segmented control inside the panel:\n no card chrome on the ButtonGroup wrapper. The buttons still\n read as a row through their tonal `is-active` fill + hover. */\n.du-prop-panel .du-button-group {\n background: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n gap: 2px;\n}\n\n/* Tighter active state inside the panel (smaller buttons, same\n tonal pair). Keeps the rest of the toolbar unaffected. */\n.du-prop-panel .du-button-group > .du-icon-button {\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n/* Checkbox row \u2014 small native input + label. Used by Corners\n \"Auto radius\" toggle. */\n.du-prop-checkbox {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--du-text);\n user-select: none;\n}\n\n.du-prop-checkbox input[type=\"checkbox\"] {\n width: 14px;\n height: 14px;\n margin: 0;\n accent-color: var(--du-selected-fg);\n cursor: pointer;\n}\n\n/* ------------------------------------------------------------------ */\n/* Colour swatch grid \u2014 modern-style fixed 26 \u00D7 26 cell with a */\n/* 24 \u00D7 24 fill that grows on hover to fill the cell. Selected cells */\n/* get a 2 px tonal ring on the OUTER cell so the ring lands beside */\n/* the colour, not over it. */\n/* ------------------------------------------------------------------ */\n.du-swatch-grid {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 4px;\n max-width: 232px; /* 8 cells (26 \u00D7 8 = 208) + 7 gaps \u00D7 4 = ~232 */\n}\n\n.du-swatch {\n position: relative;\n width: 26px;\n height: 26px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* Reserve the ring space so width never changes on selection. */\n box-shadow: inset 0 0 0 0 transparent;\n transition: box-shadow 0.12s ease;\n /* Suppress the UA tap-highlight on iOS so the cell looks clean\n while the user drags through swatches. */\n -webkit-tap-highlight-color: transparent;\n}\n\n.du-swatch:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 2px var(--du-accent);\n}\n\n.du-swatch[aria-checked=\"true\"] {\n box-shadow: inset 0 0 0 2px var(--du-selected-fg);\n}\n\n.du-swatch-fill {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: var(--du-radius-sm);\n border: 1px solid var(--du-ui-border);\n box-sizing: border-box;\n transition:\n width 0.12s ease,\n height 0.12s ease,\n border-color 0.12s ease;\n}\n\n.du-swatch:hover .du-swatch-fill,\n.du-swatch:focus-visible .du-swatch-fill {\n width: 26px;\n height: 26px;\n}\n\n/* Clear (\u00D7) + custom-picker (+) cells use the same chrome as a\n regular swatch but with a neutral fill + glyph slot. */\n.du-swatch-fill-blank {\n background: var(--du-ui-bg-solid);\n color: var(--du-text-muted);\n}\n\n.du-swatch-custom .du-swatch-fill {\n background: var(--du-ui-bg-solid);\n color: var(--du-text-muted);\n}\n\n.du-swatch-color-input {\n position: absolute;\n inset: 0;\n opacity: 0;\n cursor: pointer;\n border: none;\n padding: 0;\n}\n\n/* ------------------------------------------------------------------ */\n/* Slider \u2014 themed native <input type=range> + value badge. */\n/* ------------------------------------------------------------------ */\n.du-slider {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n}\n\n.du-slider-track {\n flex: 1;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n height: 18px;\n margin: 0;\n padding: 0;\n cursor: pointer;\n}\n\n.du-slider-track::-webkit-slider-runnable-track {\n height: 4px;\n background: var(--du-selected-bg);\n border-radius: 2px;\n}\n\n.du-slider-track::-moz-range-track {\n height: 4px;\n background: var(--du-selected-bg);\n border-radius: 2px;\n border: none;\n}\n\n.du-slider-track::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 14px;\n height: 14px;\n background: var(--du-selected-fg);\n border-radius: 50%;\n border: 2px solid var(--du-ui-bg-solid);\n margin-top: -5px;\n cursor: grab;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.du-slider-track::-moz-range-thumb {\n width: 14px;\n height: 14px;\n background: var(--du-selected-fg);\n border-radius: 50%;\n border: 2px solid var(--du-ui-bg-solid);\n cursor: grab;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.du-slider-track:focus {\n outline: none;\n}\n\n.du-slider-track:focus-visible::-webkit-slider-thumb {\n outline: 2px solid var(--du-accent);\n outline-offset: 2px;\n}\n\n.du-slider-value {\n min-width: 36px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n font-size: 11px;\n color: var(--du-text-muted);\n}\n\n/* ------------------------------------------------------------------ */\n/* Number input \u2014 small themed `<input type=number>`. */\n/* ------------------------------------------------------------------ */\n.du-number-input {\n width: 64px;\n height: 24px;\n box-sizing: border-box;\n background: var(--du-ui-bg-solid);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius-sm);\n padding: 0 6px;\n font: inherit;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n outline: none;\n}\n\n.du-number-input:focus {\n border-color: var(--du-accent);\n}\n\n/* Search input row \u2014 sits between the header and the body. */\n.du-side-panel-search {\n position: relative;\n padding: var(--du-pad-sm) var(--du-pad);\n border-bottom: 1px solid var(--du-ui-border);\n}\n\n.du-side-panel-search input[type=\"search\"] {\n width: 100%;\n height: 28px;\n box-sizing: border-box;\n background: var(--du-ui-bg-solid);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius-sm);\n padding: 0 8px 0 26px;\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.du-side-panel-search input[type=\"search\"]::placeholder {\n color: var(--du-text-muted);\n}\n\n.du-side-panel-search input[type=\"search\"]:focus {\n border-color: var(--du-accent);\n}\n\n.du-side-panel-search-icon {\n position: absolute;\n left: calc(var(--du-pad) + 8px);\n top: 50%;\n transform: translateY(-50%);\n color: var(--du-text-muted);\n pointer-events: none;\n}\n\n/* ------------------------------------------------------------------ */\n/* Toast for reset-to-content visibility hint. */\n/* ------------------------------------------------------------------ */\n.du-pill-button {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap-sm);\n padding: 0 12px;\n height: var(--du-button-size);\n background: var(--du-ui-bg);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: 999px;\n box-shadow: var(--du-ui-shadow);\n cursor: pointer;\n font-size: 13px;\n line-height: 1;\n white-space: nowrap;\n transition:\n background 0.15s ease,\n transform 0.05s ease;\n}\n\n.du-pill-button:hover {\n background: var(--du-ui-bg-solid);\n}\n\n.du-pill-button:active {\n transform: scale(0.96);\n}\n\n/* ------------------------------------------------------------------ */\n/* Tooltip \u2014 singleton popup rendered via portal by <TooltipProvider>.*/\n/* Pointer-events-none so it never blocks hover on neighbour buttons. */\n/* ------------------------------------------------------------------ */\n.du-tooltip {\n background: var(--du-text);\n color: var(--du-ui-bg-solid);\n padding: 4px 8px;\n border-radius: var(--du-radius-sm);\n font-size: 12px;\n line-height: 1.4;\n white-space: nowrap;\n box-shadow: var(--du-ui-shadow);\n font-family: inherit;\n max-width: 260px;\n}\n\n/* ------------------------------------------------------------------ */\n/* Help dialog \u2014 modern-style hotkey cheatsheet. */\n/* Each section is an \"island\" with bordered rows; each key is its */\n/* own pill `<kbd>` rendered with the same tonal selected fill as */\n/* active toolbar buttons so the visual language stays consistent. */\n/* ------------------------------------------------------------------ */\n.du-help-islands {\n display: grid;\n gap: 24px;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n}\n\n.du-help-island {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.du-help-island-title {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--du-text);\n letter-spacing: 0.2px;\n}\n\n.du-help-island-rows {\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n overflow: hidden;\n}\n\n.du-help-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 8px 12px;\n font-size: 13px;\n color: var(--du-text);\n border-bottom: 1px solid var(--du-ui-border);\n}\n\n.du-help-row:last-child {\n border-bottom: none;\n}\n\n.du-help-row-label {\n /* Label may wrap on narrow viewports; keys stay on a single line. */\n min-width: 0;\n}\n\n.du-help-keys {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.du-help-keys-separator {\n font-size: 11px;\n color: var(--du-text-muted);\n padding: 0 2px;\n}\n\n.du-help-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n background: var(--du-selected-bg);\n color: var(--du-selected-fg);\n border-radius: var(--du-radius-sm);\n font-family: inherit;\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n}\n\n.du-help-platform-badge {\n font-size: 11px;\n color: var(--du-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* ------------------------------------------------------------------ */\n/* Selection floating panel \u2014 modern-style horizontal toolbar that */\n/* hovers above the selection bbox. Position is set inline via */\n/* `transform:translate(...)` by floating-ui \u2014 these rules cover only */\n/* the visual chrome (pill background, shadow, animation, layout of */\n/* inline controls + their popover triggers). */\n/* ------------------------------------------------------------------ */\n/* Fade-in is handled inline via the `positioned` state in\n <SelectionFloatingPanel> \u2014 a CSS animation here would override the\n inline `transform: translate(...)` floating-ui writes, causing\n the panel to flash at the (0, 0) origin before snapping into\n place. */\n\n.du-sel-panel {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap-sm);\n background: var(--du-ui-bg-solid);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n padding: var(--du-pad-sm);\n font-size: 12px;\n color: var(--du-text);\n max-width: calc(100vw - 32px);\n /* Allow horizontal scroll within the pill when too many controls\n are visible; `shift` middleware clamps the outer position but\n a panel wider than viewport still needs internal overflow. */\n overflow-x: auto;\n overflow-y: visible;\n scrollbar-width: thin;\n}\n\n/* ---- Mobile bottom-sheet property panel (modern-style) ---- */\n/* Fixed dock at the bottom edge; transparent container so only the\n panel itself catches touches. Honours the home-indicator safe area. */\n.du-sel-panel-dock {\n position: fixed;\n left: 0;\n right: 0;\n /* Sit ABOVE the bottom bar (zoom / help) + the home-indicator safe area\n so the two don't overlap. Mirrors the side-panel top offset. */\n bottom: calc(\n var(--du-bar-inset) + var(--du-button-size) + var(--du-gap) + env(safe-area-inset-bottom, 0px)\n );\n z-index: 1500;\n display: flex;\n justify-content: center;\n padding: 0 8px;\n pointer-events: none;\n}\n\n/* Full-width sheet: a primary row + (when \u22EE is tapped) a wrapped\n overflow grid below. Overrides the floating pill's row/scroll chrome. */\n.du-sel-panel-mobile {\n pointer-events: auto;\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n max-width: 560px;\n overflow: visible;\n gap: 6px;\n padding: 8px;\n}\n.du-sel-mobile-row {\n display: flex;\n align-items: center;\n gap: var(--du-gap-sm);\n width: 100%;\n}\n/* Primary controls scroll horizontally if they don't fit; the \u22EE button\n stays pinned to the right, always reachable. */\n.du-sel-mobile-primary {\n display: flex;\n align-items: center;\n gap: var(--du-gap-sm);\n flex: 1 1 auto;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.du-sel-mobile-primary::-webkit-scrollbar {\n display: none;\n}\n.du-sel-mobile-expand {\n flex: 0 0 auto;\n}\n.du-sel-mobile-overflow {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--du-gap-sm);\n border-top: 1px solid var(--du-ui-border);\n padding-top: 8px;\n}\n/* \u226544px touch targets on the mobile panel (Apple HIG). Standalone icon\n buttons + color triggers grow; grouped segmented buttons stay a touch\n smaller so wide controls (font-size S/M/L/XL) still fit one row. */\n.du-sel-panel-mobile .du-sel-icon-button,\n.du-sel-panel-mobile .du-sel-color-trigger {\n width: 44px;\n height: 44px;\n}\n.du-sel-panel-mobile .du-button-group > .du-icon-button {\n width: 38px;\n height: 38px;\n}\n\n/* Flat segmented controls inside the floating panel \u2014 no group\n pill chrome (the outer panel already provides it). Reuses the\n button-group rules from PropertyPanel but with tighter gaps. */\n.du-sel-panel .du-button-group {\n background: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n gap: 2px;\n}\n\n.du-sel-panel .du-button-group > .du-icon-button {\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n/* Color swatch trigger \u2014 28\u00D728 with 20\u00D720 colored square inside. */\n.du-sel-color-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n}\n\n.du-sel-color-trigger:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n.du-sel-color-trigger:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 1px;\n}\n\n.du-sel-color-swatch {\n display: inline-block;\n width: 18px;\n height: 18px;\n border-radius: 4px;\n border: 1px solid var(--du-ui-border);\n box-sizing: border-box;\n}\n\n/* Plain icon button used for corners / etc. Same chrome as\n color-trigger but icon inside. */\n.du-sel-icon-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n color: var(--du-text);\n}\n\n.du-sel-icon-button:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n/* Pressed / on state for toggle buttons (Bold / Italic / Underline /\n Strikethrough in the text style popover). Without this the computed\n `is-active` class had no visual, so toggles never reflected the\n selection's current style. */\n.du-sel-icon-button.is-active {\n background: var(--du-accent-soft, rgba(80, 130, 255, 0.22));\n color: var(--du-accent, #5082ff);\n}\n\n.du-sel-icon-button:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 1px;\n}\n\n/* Text-label button (opacity badge \"75%\"). */\n.du-sel-text-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: var(--du-button-size-sm);\n min-width: var(--du-button-size-sm);\n padding: 0 8px;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n color: var(--du-text);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n}\n\n.du-sel-text-button:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n.du-sel-text-button:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 1px;\n}\n\n/* Visual divider between inline groups (z-order, actions). */\n.du-sel-divider {\n display: inline-block;\n width: 1px;\n height: 18px;\n background: var(--du-ui-border);\n margin: 0 2px;\n}\n\n/* ------------------------------------------------------------------ */\n/* Popover \u2014 generic floating panel used by color picker / corners / */\n/* opacity slider inside the selection floating panel. */\n/* ------------------------------------------------------------------ */\n.du-popover {\n background: var(--du-ui-bg-solid);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n padding: var(--du-pad);\n font-size: 12px;\n color: var(--du-text);\n min-width: 120px;\n max-width: 320px;\n}\n\n.du-sel-popover-section {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.du-sel-popover-label {\n margin: 0;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--du-text-muted);\n}\n\n/* Vertical list row inside a selection popover (e.g. font-family\n picker). One full-width clickable row; `is-active` marks the\n current value. */\n.du-sel-menu-row {\n all: unset;\n display: block;\n width: 100%;\n box-sizing: border-box;\n padding: 6px 10px;\n border-radius: 4px;\n font-size: 13px;\n color: var(--du-text, #ddd);\n cursor: pointer;\n}\n\n.du-sel-menu-row:hover {\n background: var(--du-ui-hover, #2a2a2a);\n}\n\n.du-sel-menu-row.is-active {\n background: var(--du-accent-soft, rgba(80, 130, 255, 0.2));\n}\n\n/* URL field inside the element-link popover. */\n.du-sel-link-input {\n width: 220px;\n max-width: 60vw;\n box-sizing: border-box;\n padding: 6px 8px;\n font-size: 13px;\n color: var(--du-text, #ddd);\n background: var(--du-ui-bg, #1a1a1a);\n border: 1px solid var(--du-ui-border, #333);\n border-radius: 4px;\n outline: none;\n}\n\n.du-sel-link-input:focus-visible {\n outline: 2px solid var(--du-accent, #5082ff);\n outline-offset: -1px;\n}\n\n/* Hover link popup (chip above a linked element). */\n.du-link-popup {\n z-index: 1400;\n margin-top: -6px;\n pointer-events: auto;\n}\n\n.du-link-popup-url {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 280px;\n padding: 4px 8px;\n font:\n 12px system-ui,\n -apple-system,\n Segoe UI,\n Roboto,\n sans-serif;\n color: var(--du-accent, #5082ff);\n text-decoration: none;\n background: var(--menu-bg, #1a1a1a);\n border: 1px solid var(--menu-border, #333);\n border-radius: 6px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n cursor: pointer;\n}\n\n.du-link-popup-url:hover {\n background: var(--du-ui-hover, #2a2a2a);\n}\n\n.du-link-popup-text {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 240px;\n}\n\n/* Arrowhead picker popover (per-end). */\n.du-arrowhead-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 2px;\n margin-top: 4px;\n}\n.du-arrowhead-option {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 30px;\n height: 26px;\n padding: 2px;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n color: var(--du-fg, #1c1c1e);\n}\n.du-arrowhead-option:hover {\n background: var(--du-hover, #f0f0f3);\n}\n.du-arrowhead-option--active {\n border-color: var(--du-accent, #5082ff);\n}\n.du-arrowhead-none {\n font-size: 13px;\n opacity: 0.6;\n}\n.du-arrowhead-erd-label {\n font-size: 9px;\n line-height: 1.05;\n text-align: center;\n}\n.du-arrowhead-erd-toggle {\n margin-top: 6px;\n width: 100%;\n text-align: left;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 11px;\n color: var(--du-muted, #6b6b70);\n padding: 2px 0;\n}\n.du-arrowhead-erd-toggle:hover {\n color: var(--du-fg, #1c1c1e);\n}\n\n/* Inline link caption editor. */\n.du-link-caption-input {\n min-width: 60px;\n max-width: 220px;\n padding: 2px 6px;\n font:\n 12px system-ui,\n sans-serif;\n text-align: center;\n color: var(--du-fg, #222);\n background: var(--du-panel-bg, #fff);\n border: 1px solid var(--du-accent, #5082ff);\n border-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);\n outline: none;\n}\n\n/* Mini shape-picker shown after a link is dropped on empty canvas. */\n.du-link-drop-menu {\n display: flex;\n gap: 2px;\n padding: 4px;\n background: var(--du-panel-bg, #fff);\n border: 1px solid var(--du-border, #e3e3e6);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);\n}\n.du-link-drop-item {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n padding: 4px;\n border: none;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n color: var(--du-fg, #1c1c1e);\n}\n.du-link-drop-item:hover {\n background: var(--du-hover, #f0f0f3);\n}\n.du-link-drop-item svg {\n width: 100%;\n height: 100%;\n}\n\n.du-sel-menu-row:focus-visible {\n outline: 2px solid var(--du-accent, #5082ff);\n outline-offset: -2px;\n}\n";
2
+ //# sourceMappingURL=styles.generated.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.generated.d.ts","sourceRoot":"","sources":["../src/styles.generated.ts"],"names":[],"mappings":"AAEA,eAAO,MAAM,eAAe,8r4CAA453C,CAAC"}
@@ -0,0 +1,4 @@
1
+ // Generated by scripts/gen-styles.mjs — do not edit.
2
+ // The react-ui stylesheet, inlined for shadow-root adoption.
3
+ export const REACT_UI_STYLES = "/**\n * Diagram UI stylesheet. Imported once by the host:\n *\n * import \"@oh-just-another/react-ui/styles.css\";\n *\n * Provides:\n * • CSS variables for theme (`:root` + `[data-theme=\"dark\"]`)\n * • Utility classes prefixed `du-` (\"diagram ui\") so they don't\n * collide with host's own styles.\n * • Per-component blocks (.du-icon-button, .du-top-bar, ...).\n *\n * Hosts can override any variable in their own stylesheet for\n * theming — every component reads colours / sizes through these\n * variables, never hard-coded.\n *\n * Colour values mirror `@oh-just-another/tokens` — CSS can't import\n * TypeScript, so values are hand-copied with comments pointing\n * back to the token name. Keep this in sync when you change a\n * value in `packages/tokens/src/colors.ts`.\n */\n\n:root {\n /* --- Surfaces (from UI_SURFACE.light) --- */\n --du-canvas-bg: #f5f5f5; /* UI_SURFACE.light.canvas */\n --du-ui-bg: rgba(255, 255, 255, 0.95); /* UI_SURFACE.light.bg */\n --du-ui-bg-solid: #ffffff; /* UI_SURFACE.light.bgSolid */\n --du-ui-border: rgba(0, 0, 0, 0.08); /* UI_SURFACE.light.border */\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04);\n\n /* --- Text (from UI_SURFACE.light) --- */\n --du-text: #1a1a1a; /* UI_SURFACE.light.text */\n --du-text-muted: #6b6b6b; /* UI_SURFACE.light.textMuted */\n --du-text-on-accent: #ffffff;\n\n /* --- Accent (from UI_ACCENT.light — Radix iris) --- */\n --du-accent: #5b5bd6; /* UI_ACCENT.light.accent (iris.iris9) */\n --du-accent-hover: #5151cd; /* UI_ACCENT.light.accentHover (iris.iris10) */\n --du-danger: #e54d2e; /* UI_ACCENT.light.danger (tomato.tomato9) */\n\n /* --- Selected (tonal, modern-style) --- */\n /* Active toolbar / icon buttons get a *muted* tonal fill so they\n read as \"this state is selected\" without hijacking the eye like\n a saturated accent fill would. The icon switches to the accent\n hue, the background stays close to the surface. */\n --du-selected-bg: #e6e7ff; /* UI_ACCENT.light.selectedBg (iris.iris4) */\n --du-selected-fg: #5753c6; /* UI_ACCENT.light.selectedFg (iris.iris11) */\n\n /* --- Sizing --- */\n --du-radius: 10px;\n --du-radius-sm: 6px;\n --du-radius-lg: 14px;\n --du-button-size: 36px;\n --du-button-size-sm: 32px;\n --du-icon-size: 18px;\n --du-gap: 8px;\n --du-gap-sm: 4px;\n --du-pad: 8px;\n --du-pad-sm: 4px;\n --du-hover-overlay: rgba(0, 0, 0, 0.05);\n --du-bar-inset: 12px;\n --du-z-canvas: 1;\n --du-z-ui: 100;\n --du-z-modal: 1000;\n}\n\n/**\n * Theme resolution priority (highest wins):\n * [data-theme=\"light\"] — host forced light.\n * [data-theme=\"dark\"] — host forced dark.\n * @media prefers-color-scheme: dark → :root — OS-derived dark when\n * no data-theme is present.\n * :root — light defaults.\n *\n * Hosts set `data-theme` via the Diagram component's `theme` prop\n * (\"light\" / \"dark\" / \"system\" — \"system\" omits the attribute so\n * the media query takes over).\n */\n@media (prefers-color-scheme: dark) {\n :root {\n /* All values mirror UI_SURFACE.dark + UI_ACCENT.dark from\n `@oh-just-another/tokens`. */\n --du-canvas-bg: #121212; /* UI_SURFACE.dark.canvas */\n --du-ui-bg: rgba(35, 35, 35, 0.95); /* UI_SURFACE.dark.bg */\n --du-ui-bg-solid: #252525; /* UI_SURFACE.dark.bgSolid */\n --du-ui-border: rgba(255, 255, 255, 0.08); /* UI_SURFACE.dark.border */\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3);\n --du-text: #e8e8e8; /* UI_SURFACE.dark.text */\n --du-text-muted: #9a9a9a; /* UI_SURFACE.dark.textMuted */\n --du-accent: #5b5bd6; /* UI_ACCENT.dark.accent (irisDark.iris9) */\n --du-accent-hover: #6e6ade; /* UI_ACCENT.dark.accentHover (irisDark.iris10) */\n --du-hover-overlay: rgba(255, 255, 255, 0.06); /* UI_SURFACE.dark.hoverOverlay */\n --du-selected-bg: #262a65; /* UI_ACCENT.dark.selectedBg (irisDark.iris4) */\n --du-selected-fg: #b1a9ff; /* UI_ACCENT.dark.selectedFg (irisDark.iris11) */\n }\n}\n\n[data-theme=\"light\"] {\n /* Same as :root — explicit override so a host that toggles\n `data-theme` between \"light\" / \"dark\" / unset still picks\n up the light palette under all conditions. Mirrors\n UI_SURFACE.light + UI_ACCENT.light. */\n --du-canvas-bg: #f5f5f5;\n --du-ui-bg: rgba(255, 255, 255, 0.95);\n --du-ui-bg-solid: #ffffff;\n --du-ui-border: rgba(0, 0, 0, 0.08);\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.08), 0 4px 12px rgba(0, 0, 0, 0.04);\n --du-text: #1a1a1a;\n --du-text-muted: #6b6b6b;\n --du-accent: #5b5bd6; /* iris.iris9 */\n --du-accent-hover: #5151cd; /* iris.iris10 */\n --du-hover-overlay: rgba(0, 0, 0, 0.05);\n --du-selected-bg: #e6e7ff; /* iris.iris4 */\n --du-selected-fg: #5753c6; /* iris.iris11 */\n}\n\n/* ------------------------------------------------------------------ */\n/* Legacy variable aliases — kept so the older components (MainMenu, */\n/* ContextMenu, Modal, HelpDialog) that read `var(--panel)` / */\n/* `var(--text)` / `var(--button-bg)` etc. still pick up the active */\n/* theme without being rewritten to the `--du-*` namespace. */\n/* */\n/* Hosts can still override the legacy names individually — these */\n/* are defaults that *forward* to the unified theme variables. */\n/* */\n/* Declared at every theme scope (not just `:root`) on purpose: a */\n/* `var()` inside a custom property resolves on the element where the */\n/* property is declared. If these lived on `:root` alone, the alias */\n/* would bake in `:root`'s `--du-*` (e.g. the OS `prefers-color-scheme` */\n/* dark value) and inherit that frozen colour past a `[data-theme]` */\n/* override — so a light app would still get a dark context menu. */\n/* Re-declaring per scope lets each alias re-resolve against the */\n/* scoped `--du-*`, including the themed portal wrapper menus mount */\n/* into. */\n/* ------------------------------------------------------------------ */\n:root,\n[data-theme=\"light\"],\n[data-theme=\"dark\"] {\n --bg: var(--du-canvas-bg);\n --panel: var(--du-ui-bg-solid);\n --surface: var(--du-ui-bg-solid);\n --text: var(--du-text);\n --muted: var(--du-text-muted);\n --border: var(--du-ui-border);\n --button-bg: var(--du-ui-bg-solid);\n --button-hover-bg: var(--du-ui-bg);\n --accent: var(--du-accent);\n --menu-bg: var(--du-ui-bg-solid);\n --menu-text: var(--du-text);\n --menu-border: var(--du-ui-border);\n --menu-divider: var(--du-ui-border);\n /* Remaining legacy names the chrome reads — defined here (forwarding to the\n theme-aware `--du-*` set) so the editor is fully themed from this stylesheet\n alone, without a host-provided token block. */\n --divider: var(--du-ui-border);\n --text-strong: var(--du-text);\n --faint: var(--du-text-muted);\n --toolbar-bg: var(--du-ui-bg-solid);\n --cursor-bg: var(--du-selected-bg);\n --code-bg: var(--du-ui-bg-solid);\n --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace;\n}\n\n[data-theme=\"dark\"] {\n /* Mirrors UI_SURFACE.dark + UI_ACCENT.dark from `@oh-just-another/tokens`. */\n --du-canvas-bg: #121212;\n --du-ui-bg: rgba(35, 35, 35, 0.95);\n --du-ui-bg-solid: #252525;\n --du-ui-border: rgba(255, 255, 255, 0.08);\n --du-ui-shadow: 0 1px 4px rgba(0, 0, 0, 0.4), 0 4px 12px rgba(0, 0, 0, 0.3);\n --du-text: #e8e8e8;\n --du-text-muted: #9a9a9a;\n --du-accent: #5b5bd6; /* irisDark.iris9 */\n --du-accent-hover: #6e6ade; /* irisDark.iris10 */\n --du-hover-overlay: rgba(255, 255, 255, 0.06);\n --du-selected-bg: #262a65; /* irisDark.iris4 */\n --du-selected-fg: #b1a9ff; /* irisDark.iris11 */\n}\n\n/* ------------------------------------------------------------------ */\n/* Canvas surface focus ring. The surface is `tabIndex=0` so keyboard */\n/* users can land on it, but it also takes focus on a pointer press */\n/* (so shortcuts work right after a click). Gate the ring on */\n/* `:focus-visible` so it shows ONLY for keyboard focus — a mouse click */\n/* never draws an input-like outline. Class+pseudo specificity (0,2,0) */\n/* beats a host's `*:focus { outline: none }`. */\n/* ------------------------------------------------------------------ */\n.du-canvas-surface {\n outline: none;\n}\n.du-canvas-surface:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: -2px;\n}\n\n/* ------------------------------------------------------------------ */\n/* UI layer — absolute overlay above the canvas surface. */\n/* pointer-events: none on the wrapper, auto on children, so the */\n/* canvas keeps receiving pointer events anywhere the UI isn't drawn. */\n/* ------------------------------------------------------------------ */\n.du-ui-layer {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: var(--du-z-ui);\n}\n\n.du-ui-layer > * {\n pointer-events: auto;\n}\n\n/* ------------------------------------------------------------------ */\n/* Bars — top / bottom 3-zone layout (left / center / right). */\n/* ------------------------------------------------------------------ */\n.du-bar {\n position: absolute;\n left: var(--du-bar-inset);\n right: var(--du-bar-inset);\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: var(--du-gap);\n pointer-events: none;\n}\n\n.du-bar > * {\n pointer-events: auto;\n}\n\n.du-bar-top {\n top: var(--du-bar-inset);\n}\n\n.du-bar-bottom {\n bottom: var(--du-bar-inset);\n}\n\n.du-bar-zone {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap);\n}\n\n.du-bar-zone-center {\n margin: 0 auto;\n}\n\n/* ------------------------------------------------------------------ */\n/* Icon button — square clickable. Two visual modes: */\n/* 1. Standalone (outside `du-button-group`): full pill with */\n/* surface, border, shadow. */\n/* 2. Inside `du-button-group`: transparent surface, subtle radius, */\n/* lets the group's pill chrome carry the framing. */\n/* ------------------------------------------------------------------ */\n.du-icon-button {\n width: var(--du-button-size);\n height: var(--du-button-size);\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: var(--du-ui-bg);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n cursor: pointer;\n font-size: 14px;\n line-height: 1;\n box-shadow: var(--du-ui-shadow);\n transition:\n background 0.12s ease,\n color 0.12s ease,\n transform 0.05s ease;\n padding: 0;\n outline: none;\n user-select: none;\n}\n\n.du-icon-button:hover:not(:disabled) {\n background: var(--du-ui-bg-solid);\n}\n\n.du-icon-button:active:not(:disabled) {\n transform: scale(0.96);\n}\n\n.du-icon-button:disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.du-icon-button:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 2px;\n}\n\n.du-icon-button[aria-pressed=\"true\"],\n.du-icon-button.is-active {\n background: var(--du-selected-bg);\n color: var(--du-selected-fg);\n border-color: var(--du-selected-bg);\n}\n\n.du-icon-button[aria-pressed=\"true\"]:hover:not(:disabled),\n.du-icon-button.is-active:hover:not(:disabled) {\n /* Slight tint shift on hover — keeps the \"selected\" cue but\n acknowledges the cursor. Same hue family, marginally darker. */\n background: color-mix(in srgb, var(--du-selected-bg) 90%, var(--du-text) 10%);\n}\n\n.du-icon-button-sm {\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n font-size: 12px;\n}\n\n/**\n * Flat modifier — strips the standalone pill chrome (background,\n * border, shadow) and adopts the same look as an IconButton sitting\n * inside a `du-button-group`. Use this when a button has to live\n * inside a container that the descendant selector can't reach (e.g.\n * the MainMenu trigger, which is wrapped in a positioning <div> so\n * its dropdown can be `position: absolute`).\n */\n.du-icon-button-flat {\n background: transparent;\n border: none;\n box-shadow: none;\n border-radius: var(--du-radius-sm);\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n.du-icon-button-flat:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n/* ------------------------------------------------------------------ */\n/* Button group — pill that wraps one or more IconButtons. Group */\n/* carries the surface, border, shadow; the buttons inside drop */\n/* their own chrome so the row reads as one cohesive control. */\n/* Padding + gap match standard's tool bar feel. */\n/* ------------------------------------------------------------------ */\n.du-button-group {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap-sm);\n background: var(--du-ui-bg);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n padding: var(--du-pad-sm);\n /* No `overflow: hidden` — a child popup (MainMenu dropdown) must\n be able to render outside the group's bounds. */\n}\n\n/* Vertical variant — the left creation dock. Stacks buttons in a\n column; dividers (rendered inline by <Toolbar>) become horizontal. */\n.du-button-group-vertical {\n flex-direction: column;\n align-items: stretch;\n}\n\n.du-button-group > .du-icon-button {\n border: none;\n box-shadow: none;\n border-radius: var(--du-radius-sm);\n background: transparent;\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n/* Vertical dock buttons are square + centred (icon-only tools). */\n.du-button-group-vertical > .du-icon-button {\n width: var(--du-button-size-sm) !important;\n min-width: var(--du-button-size-sm) !important;\n padding: 0 !important;\n}\n\n/* ------------------------------------------------------------------ */\n/* Mobile / coarse-pointer chrome (keep the 640px breakpoint in sync */\n/* with MOBILE_MAX_WIDTH_PX in constants.ts). */\n/* ------------------------------------------------------------------ */\n@media (pointer: coarse), (max-width: 640px) {\n /* Bars clear the notch / home indicator + rounded corners. */\n .du-bar-top {\n top: calc(var(--du-bar-inset) + env(safe-area-inset-top, 0px));\n }\n .du-bar-bottom {\n bottom: calc(var(--du-bar-inset) + env(safe-area-inset-bottom, 0px));\n }\n .du-bar {\n left: calc(var(--du-bar-inset) + env(safe-area-inset-left, 0px));\n right: calc(var(--du-bar-inset) + env(safe-area-inset-right, 0px));\n }\n\n /* Vertical creation dock: ≥44px touch targets; scroll inside the dock\n when a short screen can't fit every tool. */\n .du-button-group-vertical {\n max-height: calc(\n 100vh - 2 * var(--du-bar-inset) - env(safe-area-inset-top, 0px) -\n env(safe-area-inset-bottom, 0px)\n );\n overflow-y: auto;\n scrollbar-width: none;\n }\n .du-button-group-vertical::-webkit-scrollbar {\n display: none;\n }\n .du-button-group-vertical > .du-icon-button {\n width: 44px !important;\n min-width: 44px !important;\n height: 44px !important;\n }\n}\n\n.du-button-group > .du-icon-button:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n.du-button-group > .du-icon-button[aria-pressed=\"true\"],\n.du-button-group > .du-icon-button.is-active {\n background: var(--du-selected-bg);\n color: var(--du-selected-fg);\n}\n\n.du-button-group > .du-icon-button[aria-pressed=\"true\"]:hover:not(:disabled),\n.du-button-group > .du-icon-button.is-active:hover:not(:disabled) {\n background: color-mix(in srgb, var(--du-selected-bg) 90%, var(--du-text) 10%);\n}\n\n/* ------------------------------------------------------------------ */\n/* Floating side panel — used by Library / SelectedShapeActions. */\n/* Fixed-width, fixed-height-with-scroll, sits flush against the */\n/* corresponding side of the canvas with a `du-bar-inset` margin. */\n/* ------------------------------------------------------------------ */\n.du-side-panel {\n position: absolute;\n top: calc(var(--du-bar-inset) + var(--du-button-size) + var(--du-gap));\n bottom: calc(var(--du-bar-inset) + var(--du-button-size) + var(--du-gap));\n width: 240px;\n background: var(--du-ui-bg);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.du-side-panel-left {\n left: var(--du-bar-inset);\n}\n\n.du-side-panel-right {\n right: var(--du-bar-inset);\n}\n\n/* Sheet mode — fills the parent BottomSheet; the sheet provides the\n surface, so drop the floating-pill position / border / shadow. */\n.du-side-panel.du-side-panel-sheet {\n position: static;\n top: auto;\n bottom: auto;\n left: auto;\n right: auto;\n width: 100%;\n height: 100%;\n border: none;\n border-radius: 0;\n box-shadow: none;\n background: transparent;\n}\n\n.du-side-panel-header {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: var(--du-gap-sm);\n padding: var(--du-pad-sm) var(--du-pad-sm) var(--du-pad-sm) var(--du-pad);\n font-size: 12px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--du-text-muted);\n}\n\n/* Hide the header when it's empty (no action buttons configured). */\n.du-side-panel-header:empty {\n display: none;\n}\n\n/* Auto-height variant — content hugs its natural height instead\n of stretching from top to bottom of the canvas area. Used by\n the Properties panel so it shrinks to whatever the inspector\n sections need. The Library panel keeps the default full-height\n so its long grid can scroll. */\n.du-side-panel-auto-height {\n bottom: auto;\n max-height: calc(100% - 2 * var(--du-bar-inset) - 2 * var(--du-button-size) - 2 * var(--du-gap));\n}\n\n.du-side-panel-body {\n flex: 1;\n overflow-y: auto;\n padding: var(--du-pad);\n}\n\n/* Variant for hosts that lay out their own grid / list inside the\n panel body (e.g. <Palette>) and would otherwise have to subtract\n the body's padding from their own paddings. Keeps overflow + flex\n behaviour. */\n.du-side-panel-body-flush {\n padding: 0;\n}\n\n/* ------------------------------------------------------------------ */\n/* Palette (template library content) — sectioned grid of icon tiles. */\n/* No per-tile borders, no per-section dividers — visual structure */\n/* comes from spacing alone so the panel reads as one calm surface. */\n/* ------------------------------------------------------------------ */\n.du-palette {\n display: flex;\n flex-direction: column;\n min-height: 0;\n color: var(--du-text);\n width: 100%;\n}\n\n.du-palette-body {\n flex: 1 1 auto;\n overflow-y: auto;\n display: flex;\n flex-direction: column;\n}\n\n.du-palette-section {\n padding: 0 var(--du-pad) var(--du-pad-sm);\n}\n\n.du-palette-category {\n width: 100%;\n background: transparent;\n color: var(--du-text-muted);\n border: none;\n padding: 10px 0 6px;\n font: inherit;\n font-size: 11px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n text-align: left;\n cursor: pointer;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: var(--du-gap-sm);\n}\n\n.du-palette-category-caret {\n display: inline-flex;\n align-items: center;\n opacity: 0.6;\n}\n\n.du-palette-grid {\n display: grid;\n grid-template-columns: repeat(2, 1fr);\n gap: var(--du-gap-sm);\n align-content: start;\n padding: 0 var(--du-pad) var(--du-pad-sm);\n}\n\n/* `.du-palette-section > .du-palette-grid` already gets padding via\n .du-palette-section; remove the duplicate to avoid double margin. */\n.du-palette-section > .du-palette-grid {\n padding: 0;\n}\n\n/* Compact list layout (modern-style templates panel) — single column of\n icon + name rows. Denser than the grid; the label reads left-aligned\n beside a small icon. */\n.du-palette-list {\n display: flex;\n flex-direction: column;\n gap: 2px;\n padding: 0 var(--du-pad) var(--du-pad-sm);\n}\n.du-palette-section > .du-palette-list {\n padding: 0;\n}\n.du-palette-list .du-palette-item {\n flex-direction: row;\n align-items: center;\n justify-content: flex-start;\n aspect-ratio: auto;\n gap: 8px;\n padding: 5px 8px;\n text-align: left;\n font-size: 12px;\n line-height: 1.2;\n}\n.du-palette-list .du-palette-item-icon {\n width: 20px !important;\n height: 20px !important;\n flex: 0 0 auto;\n}\n.du-palette-list .du-palette-item-label {\n flex: 1 1 auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.du-palette-empty {\n padding: 24px 12px;\n text-align: center;\n font-size: 12px;\n color: var(--du-text-muted);\n}\n\n.du-palette-item {\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 4px;\n aspect-ratio: 1;\n padding: 6px 4px;\n background: transparent;\n color: var(--du-text);\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: grab;\n text-align: center;\n font-size: 10px;\n line-height: 1.1;\n transition: background 0.12s ease;\n}\n\n.du-palette-item:hover {\n background: var(--du-hover-overlay);\n}\n\n.du-palette-item:active {\n cursor: grabbing;\n}\n\n.du-palette-item-icon {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n color: var(--du-text);\n}\n\n.du-palette-item-icon svg {\n width: 100%;\n height: 100%;\n display: block;\n}\n\n.du-palette-item-label {\n /* Two-line clamp so long names don't push the tile out of square. */\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n/* ------------------------------------------------------------------ */\n/* Property panel — modern-style sectioned inspector. */\n/* Sections are stacked top-to-bottom; each one has a tiny uppercase */\n/* label, then a row of controls (color swatches, segmented buttons, */\n/* slider). No card chrome — visual structure comes from spacing. */\n/* ------------------------------------------------------------------ */\n.du-prop-panel {\n display: flex;\n flex-direction: column;\n gap: 16px;\n padding: 4px 0;\n color: var(--du-text);\n font-size: 12px;\n}\n\n.du-prop-section {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.du-prop-section-label {\n margin: 0;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--du-text-muted);\n}\n\n.du-prop-row {\n display: flex;\n align-items: center;\n gap: var(--du-gap-sm);\n flex-wrap: wrap;\n}\n\n.du-prop-row-label {\n font-size: 11px;\n color: var(--du-text-muted);\n min-width: 48px;\n}\n\n.du-prop-empty {\n font-style: italic;\n color: var(--du-text-muted);\n font-size: 12px;\n}\n\n/* modern-style flat segmented control inside the panel:\n no card chrome on the ButtonGroup wrapper. The buttons still\n read as a row through their tonal `is-active` fill + hover. */\n.du-prop-panel .du-button-group {\n background: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n gap: 2px;\n}\n\n/* Tighter active state inside the panel (smaller buttons, same\n tonal pair). Keeps the rest of the toolbar unaffected. */\n.du-prop-panel .du-button-group > .du-icon-button {\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n/* Checkbox row — small native input + label. Used by Corners\n \"Auto radius\" toggle. */\n.du-prop-checkbox {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n cursor: pointer;\n font-size: 12px;\n color: var(--du-text);\n user-select: none;\n}\n\n.du-prop-checkbox input[type=\"checkbox\"] {\n width: 14px;\n height: 14px;\n margin: 0;\n accent-color: var(--du-selected-fg);\n cursor: pointer;\n}\n\n/* ------------------------------------------------------------------ */\n/* Colour swatch grid — modern-style fixed 26 × 26 cell with a */\n/* 24 × 24 fill that grows on hover to fill the cell. Selected cells */\n/* get a 2 px tonal ring on the OUTER cell so the ring lands beside */\n/* the colour, not over it. */\n/* ------------------------------------------------------------------ */\n.du-swatch-grid {\n display: inline-flex;\n flex-wrap: wrap;\n gap: 4px;\n max-width: 232px; /* 8 cells (26 × 8 = 208) + 7 gaps × 4 = ~232 */\n}\n\n.du-swatch {\n position: relative;\n width: 26px;\n height: 26px;\n padding: 0;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n /* Reserve the ring space so width never changes on selection. */\n box-shadow: inset 0 0 0 0 transparent;\n transition: box-shadow 0.12s ease;\n /* Suppress the UA tap-highlight on iOS so the cell looks clean\n while the user drags through swatches. */\n -webkit-tap-highlight-color: transparent;\n}\n\n.du-swatch:focus-visible {\n outline: none;\n box-shadow: inset 0 0 0 2px var(--du-accent);\n}\n\n.du-swatch[aria-checked=\"true\"] {\n box-shadow: inset 0 0 0 2px var(--du-selected-fg);\n}\n\n.du-swatch-fill {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 24px;\n height: 24px;\n border-radius: var(--du-radius-sm);\n border: 1px solid var(--du-ui-border);\n box-sizing: border-box;\n transition:\n width 0.12s ease,\n height 0.12s ease,\n border-color 0.12s ease;\n}\n\n.du-swatch:hover .du-swatch-fill,\n.du-swatch:focus-visible .du-swatch-fill {\n width: 26px;\n height: 26px;\n}\n\n/* Clear (×) + custom-picker (+) cells use the same chrome as a\n regular swatch but with a neutral fill + glyph slot. */\n.du-swatch-fill-blank {\n background: var(--du-ui-bg-solid);\n color: var(--du-text-muted);\n}\n\n.du-swatch-custom .du-swatch-fill {\n background: var(--du-ui-bg-solid);\n color: var(--du-text-muted);\n}\n\n.du-swatch-color-input {\n position: absolute;\n inset: 0;\n opacity: 0;\n cursor: pointer;\n border: none;\n padding: 0;\n}\n\n/* ------------------------------------------------------------------ */\n/* Slider — themed native <input type=range> + value badge. */\n/* ------------------------------------------------------------------ */\n.du-slider {\n display: flex;\n align-items: center;\n gap: 8px;\n width: 100%;\n}\n\n.du-slider-track {\n flex: 1;\n -webkit-appearance: none;\n appearance: none;\n background: transparent;\n height: 18px;\n margin: 0;\n padding: 0;\n cursor: pointer;\n}\n\n.du-slider-track::-webkit-slider-runnable-track {\n height: 4px;\n background: var(--du-selected-bg);\n border-radius: 2px;\n}\n\n.du-slider-track::-moz-range-track {\n height: 4px;\n background: var(--du-selected-bg);\n border-radius: 2px;\n border: none;\n}\n\n.du-slider-track::-webkit-slider-thumb {\n -webkit-appearance: none;\n appearance: none;\n width: 14px;\n height: 14px;\n background: var(--du-selected-fg);\n border-radius: 50%;\n border: 2px solid var(--du-ui-bg-solid);\n margin-top: -5px;\n cursor: grab;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.du-slider-track::-moz-range-thumb {\n width: 14px;\n height: 14px;\n background: var(--du-selected-fg);\n border-radius: 50%;\n border: 2px solid var(--du-ui-bg-solid);\n cursor: grab;\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);\n}\n\n.du-slider-track:focus {\n outline: none;\n}\n\n.du-slider-track:focus-visible::-webkit-slider-thumb {\n outline: 2px solid var(--du-accent);\n outline-offset: 2px;\n}\n\n.du-slider-value {\n min-width: 36px;\n text-align: right;\n font-variant-numeric: tabular-nums;\n font-size: 11px;\n color: var(--du-text-muted);\n}\n\n/* ------------------------------------------------------------------ */\n/* Number input — small themed `<input type=number>`. */\n/* ------------------------------------------------------------------ */\n.du-number-input {\n width: 64px;\n height: 24px;\n box-sizing: border-box;\n background: var(--du-ui-bg-solid);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius-sm);\n padding: 0 6px;\n font: inherit;\n font-size: 11px;\n font-variant-numeric: tabular-nums;\n outline: none;\n}\n\n.du-number-input:focus {\n border-color: var(--du-accent);\n}\n\n/* Search input row — sits between the header and the body. */\n.du-side-panel-search {\n position: relative;\n padding: var(--du-pad-sm) var(--du-pad);\n border-bottom: 1px solid var(--du-ui-border);\n}\n\n.du-side-panel-search input[type=\"search\"] {\n width: 100%;\n height: 28px;\n box-sizing: border-box;\n background: var(--du-ui-bg-solid);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius-sm);\n padding: 0 8px 0 26px;\n font: inherit;\n font-size: 12px;\n outline: none;\n}\n\n.du-side-panel-search input[type=\"search\"]::placeholder {\n color: var(--du-text-muted);\n}\n\n.du-side-panel-search input[type=\"search\"]:focus {\n border-color: var(--du-accent);\n}\n\n.du-side-panel-search-icon {\n position: absolute;\n left: calc(var(--du-pad) + 8px);\n top: 50%;\n transform: translateY(-50%);\n color: var(--du-text-muted);\n pointer-events: none;\n}\n\n/* ------------------------------------------------------------------ */\n/* Toast for reset-to-content visibility hint. */\n/* ------------------------------------------------------------------ */\n.du-pill-button {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap-sm);\n padding: 0 12px;\n height: var(--du-button-size);\n background: var(--du-ui-bg);\n color: var(--du-text);\n border: 1px solid var(--du-ui-border);\n border-radius: 999px;\n box-shadow: var(--du-ui-shadow);\n cursor: pointer;\n font-size: 13px;\n line-height: 1;\n white-space: nowrap;\n transition:\n background 0.15s ease,\n transform 0.05s ease;\n}\n\n.du-pill-button:hover {\n background: var(--du-ui-bg-solid);\n}\n\n.du-pill-button:active {\n transform: scale(0.96);\n}\n\n/* ------------------------------------------------------------------ */\n/* Tooltip — singleton popup rendered via portal by <TooltipProvider>.*/\n/* Pointer-events-none so it never blocks hover on neighbour buttons. */\n/* ------------------------------------------------------------------ */\n.du-tooltip {\n background: var(--du-text);\n color: var(--du-ui-bg-solid);\n padding: 4px 8px;\n border-radius: var(--du-radius-sm);\n font-size: 12px;\n line-height: 1.4;\n white-space: nowrap;\n box-shadow: var(--du-ui-shadow);\n font-family: inherit;\n max-width: 260px;\n}\n\n/* ------------------------------------------------------------------ */\n/* Help dialog — modern-style hotkey cheatsheet. */\n/* Each section is an \"island\" with bordered rows; each key is its */\n/* own pill `<kbd>` rendered with the same tonal selected fill as */\n/* active toolbar buttons so the visual language stays consistent. */\n/* ------------------------------------------------------------------ */\n.du-help-islands {\n display: grid;\n gap: 24px;\n grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));\n}\n\n.du-help-island {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.du-help-island-title {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--du-text);\n letter-spacing: 0.2px;\n}\n\n.du-help-island-rows {\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n overflow: hidden;\n}\n\n.du-help-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 8px 12px;\n font-size: 13px;\n color: var(--du-text);\n border-bottom: 1px solid var(--du-ui-border);\n}\n\n.du-help-row:last-child {\n border-bottom: none;\n}\n\n.du-help-row-label {\n /* Label may wrap on narrow viewports; keys stay on a single line. */\n min-width: 0;\n}\n\n.du-help-keys {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.du-help-keys-separator {\n font-size: 11px;\n color: var(--du-text-muted);\n padding: 0 2px;\n}\n\n.du-help-key {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 22px;\n height: 22px;\n padding: 0 6px;\n background: var(--du-selected-bg);\n color: var(--du-selected-fg);\n border-radius: var(--du-radius-sm);\n font-family: inherit;\n font-size: 11px;\n font-weight: 600;\n line-height: 1;\n white-space: nowrap;\n}\n\n.du-help-platform-badge {\n font-size: 11px;\n color: var(--du-text-muted);\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n/* ------------------------------------------------------------------ */\n/* Selection floating panel — modern-style horizontal toolbar that */\n/* hovers above the selection bbox. Position is set inline via */\n/* `transform:translate(...)` by floating-ui — these rules cover only */\n/* the visual chrome (pill background, shadow, animation, layout of */\n/* inline controls + their popover triggers). */\n/* ------------------------------------------------------------------ */\n/* Fade-in is handled inline via the `positioned` state in\n <SelectionFloatingPanel> — a CSS animation here would override the\n inline `transform: translate(...)` floating-ui writes, causing\n the panel to flash at the (0, 0) origin before snapping into\n place. */\n\n.du-sel-panel {\n display: inline-flex;\n align-items: center;\n gap: var(--du-gap-sm);\n background: var(--du-ui-bg-solid);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n padding: var(--du-pad-sm);\n font-size: 12px;\n color: var(--du-text);\n max-width: calc(100vw - 32px);\n /* Allow horizontal scroll within the pill when too many controls\n are visible; `shift` middleware clamps the outer position but\n a panel wider than viewport still needs internal overflow. */\n overflow-x: auto;\n overflow-y: visible;\n scrollbar-width: thin;\n}\n\n/* ---- Mobile bottom-sheet property panel (modern-style) ---- */\n/* Fixed dock at the bottom edge; transparent container so only the\n panel itself catches touches. Honours the home-indicator safe area. */\n.du-sel-panel-dock {\n position: fixed;\n left: 0;\n right: 0;\n /* Sit ABOVE the bottom bar (zoom / help) + the home-indicator safe area\n so the two don't overlap. Mirrors the side-panel top offset. */\n bottom: calc(\n var(--du-bar-inset) + var(--du-button-size) + var(--du-gap) + env(safe-area-inset-bottom, 0px)\n );\n z-index: 1500;\n display: flex;\n justify-content: center;\n padding: 0 8px;\n pointer-events: none;\n}\n\n/* Full-width sheet: a primary row + (when ⋮ is tapped) a wrapped\n overflow grid below. Overrides the floating pill's row/scroll chrome. */\n.du-sel-panel-mobile {\n pointer-events: auto;\n flex-direction: column;\n align-items: stretch;\n width: 100%;\n max-width: 560px;\n overflow: visible;\n gap: 6px;\n padding: 8px;\n}\n.du-sel-mobile-row {\n display: flex;\n align-items: center;\n gap: var(--du-gap-sm);\n width: 100%;\n}\n/* Primary controls scroll horizontally if they don't fit; the ⋮ button\n stays pinned to the right, always reachable. */\n.du-sel-mobile-primary {\n display: flex;\n align-items: center;\n gap: var(--du-gap-sm);\n flex: 1 1 auto;\n min-width: 0;\n overflow-x: auto;\n scrollbar-width: none;\n}\n.du-sel-mobile-primary::-webkit-scrollbar {\n display: none;\n}\n.du-sel-mobile-expand {\n flex: 0 0 auto;\n}\n.du-sel-mobile-overflow {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: var(--du-gap-sm);\n border-top: 1px solid var(--du-ui-border);\n padding-top: 8px;\n}\n/* ≥44px touch targets on the mobile panel (Apple HIG). Standalone icon\n buttons + color triggers grow; grouped segmented buttons stay a touch\n smaller so wide controls (font-size S/M/L/XL) still fit one row. */\n.du-sel-panel-mobile .du-sel-icon-button,\n.du-sel-panel-mobile .du-sel-color-trigger {\n width: 44px;\n height: 44px;\n}\n.du-sel-panel-mobile .du-button-group > .du-icon-button {\n width: 38px;\n height: 38px;\n}\n\n/* Flat segmented controls inside the floating panel — no group\n pill chrome (the outer panel already provides it). Reuses the\n button-group rules from PropertyPanel but with tighter gaps. */\n.du-sel-panel .du-button-group {\n background: transparent;\n border: none;\n box-shadow: none;\n padding: 0;\n gap: 2px;\n}\n\n.du-sel-panel .du-button-group > .du-icon-button {\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n}\n\n/* Color swatch trigger — 28×28 with 20×20 colored square inside. */\n.du-sel-color-trigger {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n}\n\n.du-sel-color-trigger:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n.du-sel-color-trigger:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 1px;\n}\n\n.du-sel-color-swatch {\n display: inline-block;\n width: 18px;\n height: 18px;\n border-radius: 4px;\n border: 1px solid var(--du-ui-border);\n box-sizing: border-box;\n}\n\n/* Plain icon button used for corners / etc. Same chrome as\n color-trigger but icon inside. */\n.du-sel-icon-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: var(--du-button-size-sm);\n height: var(--du-button-size-sm);\n padding: 0;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n color: var(--du-text);\n}\n\n.du-sel-icon-button:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n/* Pressed / on state for toggle buttons (Bold / Italic / Underline /\n Strikethrough in the text style popover). Without this the computed\n `is-active` class had no visual, so toggles never reflected the\n selection's current style. */\n.du-sel-icon-button.is-active {\n background: var(--du-accent-soft, rgba(80, 130, 255, 0.22));\n color: var(--du-accent, #5082ff);\n}\n\n.du-sel-icon-button:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 1px;\n}\n\n/* Text-label button (opacity badge \"75%\"). */\n.du-sel-text-button {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: var(--du-button-size-sm);\n min-width: var(--du-button-size-sm);\n padding: 0 8px;\n background: transparent;\n border: none;\n border-radius: var(--du-radius-sm);\n cursor: pointer;\n color: var(--du-text);\n font-size: 12px;\n font-variant-numeric: tabular-nums;\n}\n\n.du-sel-text-button:hover:not(:disabled) {\n background: var(--du-hover-overlay);\n}\n\n.du-sel-text-button:focus-visible {\n outline: 2px solid var(--du-accent);\n outline-offset: 1px;\n}\n\n/* Visual divider between inline groups (z-order, actions). */\n.du-sel-divider {\n display: inline-block;\n width: 1px;\n height: 18px;\n background: var(--du-ui-border);\n margin: 0 2px;\n}\n\n/* ------------------------------------------------------------------ */\n/* Popover — generic floating panel used by color picker / corners / */\n/* opacity slider inside the selection floating panel. */\n/* ------------------------------------------------------------------ */\n.du-popover {\n background: var(--du-ui-bg-solid);\n border: 1px solid var(--du-ui-border);\n border-radius: var(--du-radius);\n box-shadow: var(--du-ui-shadow);\n padding: var(--du-pad);\n font-size: 12px;\n color: var(--du-text);\n min-width: 120px;\n max-width: 320px;\n}\n\n.du-sel-popover-section {\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 0;\n}\n\n.du-sel-popover-label {\n margin: 0;\n font-size: 10px;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n color: var(--du-text-muted);\n}\n\n/* Vertical list row inside a selection popover (e.g. font-family\n picker). One full-width clickable row; `is-active` marks the\n current value. */\n.du-sel-menu-row {\n all: unset;\n display: block;\n width: 100%;\n box-sizing: border-box;\n padding: 6px 10px;\n border-radius: 4px;\n font-size: 13px;\n color: var(--du-text, #ddd);\n cursor: pointer;\n}\n\n.du-sel-menu-row:hover {\n background: var(--du-ui-hover, #2a2a2a);\n}\n\n.du-sel-menu-row.is-active {\n background: var(--du-accent-soft, rgba(80, 130, 255, 0.2));\n}\n\n/* URL field inside the element-link popover. */\n.du-sel-link-input {\n width: 220px;\n max-width: 60vw;\n box-sizing: border-box;\n padding: 6px 8px;\n font-size: 13px;\n color: var(--du-text, #ddd);\n background: var(--du-ui-bg, #1a1a1a);\n border: 1px solid var(--du-ui-border, #333);\n border-radius: 4px;\n outline: none;\n}\n\n.du-sel-link-input:focus-visible {\n outline: 2px solid var(--du-accent, #5082ff);\n outline-offset: -1px;\n}\n\n/* Hover link popup (chip above a linked element). */\n.du-link-popup {\n z-index: 1400;\n margin-top: -6px;\n pointer-events: auto;\n}\n\n.du-link-popup-url {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n max-width: 280px;\n padding: 4px 8px;\n font:\n 12px system-ui,\n -apple-system,\n Segoe UI,\n Roboto,\n sans-serif;\n color: var(--du-accent, #5082ff);\n text-decoration: none;\n background: var(--menu-bg, #1a1a1a);\n border: 1px solid var(--menu-border, #333);\n border-radius: 6px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);\n cursor: pointer;\n}\n\n.du-link-popup-url:hover {\n background: var(--du-ui-hover, #2a2a2a);\n}\n\n.du-link-popup-text {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n max-width: 240px;\n}\n\n/* Arrowhead picker popover (per-end). */\n.du-arrowhead-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n gap: 2px;\n margin-top: 4px;\n}\n.du-arrowhead-option {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 30px;\n height: 26px;\n padding: 2px;\n border: 1px solid transparent;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n color: var(--du-fg, #1c1c1e);\n}\n.du-arrowhead-option:hover {\n background: var(--du-hover, #f0f0f3);\n}\n.du-arrowhead-option--active {\n border-color: var(--du-accent, #5082ff);\n}\n.du-arrowhead-none {\n font-size: 13px;\n opacity: 0.6;\n}\n.du-arrowhead-erd-label {\n font-size: 9px;\n line-height: 1.05;\n text-align: center;\n}\n.du-arrowhead-erd-toggle {\n margin-top: 6px;\n width: 100%;\n text-align: left;\n border: none;\n background: transparent;\n cursor: pointer;\n font-size: 11px;\n color: var(--du-muted, #6b6b70);\n padding: 2px 0;\n}\n.du-arrowhead-erd-toggle:hover {\n color: var(--du-fg, #1c1c1e);\n}\n\n/* Inline link caption editor. */\n.du-link-caption-input {\n min-width: 60px;\n max-width: 220px;\n padding: 2px 6px;\n font:\n 12px system-ui,\n sans-serif;\n text-align: center;\n color: var(--du-fg, #222);\n background: var(--du-panel-bg, #fff);\n border: 1px solid var(--du-accent, #5082ff);\n border-radius: 4px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.18);\n outline: none;\n}\n\n/* Mini shape-picker shown after a link is dropped on empty canvas. */\n.du-link-drop-menu {\n display: flex;\n gap: 2px;\n padding: 4px;\n background: var(--du-panel-bg, #fff);\n border: 1px solid var(--du-border, #e3e3e6);\n border-radius: 8px;\n box-shadow: 0 6px 20px rgba(0, 0, 0, 0.18);\n}\n.du-link-drop-item {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 30px;\n height: 30px;\n padding: 4px;\n border: none;\n border-radius: 6px;\n background: transparent;\n cursor: pointer;\n color: var(--du-fg, #1c1c1e);\n}\n.du-link-drop-item:hover {\n background: var(--du-hover, #f0f0f3);\n}\n.du-link-drop-item svg {\n width: 100%;\n height: 100%;\n}\n\n.du-sel-menu-row:focus-visible {\n outline: 2px solid var(--du-accent, #5082ff);\n outline-offset: -2px;\n}\n";
4
+ //# sourceMappingURL=styles.generated.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"styles.generated.js","sourceRoot":"","sources":["../src/styles.generated.ts"],"names":[],"mappings":"AAAA,qDAAqD;AACrD,6DAA6D;AAC7D,MAAM,CAAC,MAAM,eAAe,GAAG,y53CAAy53C,CAAC"}
package/fonts/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Embedded fonts — licensing
2
+ ===========================
3
+
4
+ text_shaper.wasm embeds a 3-family × 4-style matrix (12 faces). Font id
5
+ = family base (0 sans / 4 serif / 8 mono) + (bold?1:0) + (italic?2:0).
6
+
7
+ Sans + Mono — Apache License 2.0
8
+ (http://www.apache.org/licenses/LICENSE-2.0):
9
+ - Roboto-Regular / -Bold / -Italic / -BoldItalic.ttf
10
+ © Google (Christian Robertson)
11
+ - RobotoMono-Regular / -Bold / -Italic / -BoldItalic.ttf
12
+ © The Roboto Mono Project Authors
13
+
14
+ Serif — SIL Open Font License 1.1 (https://scripts.sil.org/OFL):
15
+ - PTSerif-Regular / -Bold / -Italic / -BoldItalic.ttf
16
+ © ParaType Inc. (PT Serif)
17
+
18
+ Both licenses permit embedding and redistribution (including inside a
19
+ compiled WebAssembly binary) provided the license + attribution are
20
+ retained — which this file does.
21
+
22
+ Sources:
23
+ - https://github.com/googlefonts/roboto
24
+ - https://github.com/googlefonts/RobotoMono
25
+ - https://github.com/google/fonts/tree/main/ofl/ptserif
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@oh-just-another/diagram",
3
+ "version": "0.1.0",
4
+ "description": "Framework-neutral custom element (<oja-diagram>) wrapping the diagram editor — drop into any framework or plain HTML.",
5
+ "license": "MIT",
6
+ "author": "Rustam Garifulin <rustam@n69.in>",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/oh-just-another/diagram.git",
10
+ "directory": "packages/diagram"
11
+ },
12
+ "homepage": "https://github.com/oh-just-another/diagram#readme",
13
+ "bugs": "https://github.com/oh-just-another/diagram/issues",
14
+ "keywords": [
15
+ "diagram",
16
+ "canvas",
17
+ "editor",
18
+ "web-component",
19
+ "custom-element"
20
+ ],
21
+ "type": "module",
22
+ "main": "./dist/index.js",
23
+ "module": "./dist/index.js",
24
+ "types": "./dist/index.d.ts",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./global": "./dist/oja-diagram.global.js"
31
+ },
32
+ "files": [
33
+ "dist",
34
+ "wasm",
35
+ "fonts",
36
+ "README.md",
37
+ "CHANGELOG.md"
38
+ ],
39
+ "sideEffects": [
40
+ "./dist/index.js",
41
+ "./dist/oja-diagram.global.js"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "react": "19.2.6",
48
+ "react-dom": "19.2.6",
49
+ "@oh-just-another/editor": "0.59.0",
50
+ "@oh-just-another/react-ui": "0.58.0",
51
+ "@oh-just-another/scene": "0.59.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/react": "19.2.15",
55
+ "@types/react-dom": "19.2.3",
56
+ "@vitejs/plugin-react": "4.7.0",
57
+ "esbuild": "0.25.12",
58
+ "jsdom": "25.0.1",
59
+ "vite": "6.4.3"
60
+ },
61
+ "scripts": {
62
+ "gen:styles": "node scripts/gen-styles.mjs",
63
+ "dev": "vite",
64
+ "build": "pnpm gen:styles && tsc -b tsconfig.build.json && pnpm build:cdn",
65
+ "build:cdn": "node scripts/build-cdn.mjs",
66
+ "typecheck": "pnpm gen:styles && tsc --noEmit",
67
+ "test": "vitest run",
68
+ "test:watch": "vitest",
69
+ "lint": "eslint src",
70
+ "clean": "rm -rf dist wasm fonts *.tsbuildinfo src/styles.generated.ts"
71
+ }
72
+ }
Binary file
Binary file