@pantoken/scaffold 1.5.1 → 1.5.2

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.
package/dist/cli.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { d as SUPPORTED_LOCALES, f as createLocaleLookup, i as scaffoldProject, l as validateThemeMode, m as validateLocaleTag, n as isScaffoldPlatform, o as installAgentAssets, p as detectLocale, r as resolveScaffoldPlatform, t as SCAFFOLD_PLATFORMS, u as validateThemeVariant } from "./src-Bj3osnZN.mjs";
1
+ import { d as SUPPORTED_LOCALES, f as createLocaleLookup, i as scaffoldProject, l as validateThemeMode, m as validateLocaleTag, n as isScaffoldPlatform, o as installAgentAssets, p as detectLocale, r as resolveScaffoldPlatform, t as SCAFFOLD_PLATFORMS, u as validateThemeVariant } from "./src-Bsgzzjxw.mjs";
2
2
  import { readFileSync } from "node:fs";
3
3
  import { basename, join } from "node:path";
4
4
  import { Argument, Command, CommanderError, InvalidArgumentError } from "commander";
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { a as AGENT_TOOLS, c as themeStylesheetImport, i as scaffoldProject, l as validateThemeMode, n as isScaffoldPlatform, o as installAgentAssets, r as resolveScaffoldPlatform, s as ASSETS, t as SCAFFOLD_PLATFORMS, u as validateThemeVariant } from "./src-Bj3osnZN.mjs";
1
+ import { a as AGENT_TOOLS, c as themeStylesheetImport, i as scaffoldProject, l as validateThemeMode, n as isScaffoldPlatform, o as installAgentAssets, r as resolveScaffoldPlatform, s as ASSETS, t as SCAFFOLD_PLATFORMS, u as validateThemeVariant } from "./src-Bsgzzjxw.mjs";
2
2
  export { AGENT_TOOLS, ASSETS, SCAFFOLD_PLATFORMS, installAgentAssets, isScaffoldPlatform, resolveScaffoldPlatform, scaffoldProject, themeStylesheetImport, validateThemeMode, validateThemeVariant };
@@ -27,7 +27,7 @@ const SCAFFOLDS = {
27
27
  "vite.config.ts": "import { defineConfig } from \"vite\";\n\nexport default defineConfig({});\n",
28
28
  "public/icon.svg": "<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\" role=\"img\" aria-label=\"Canvas Theme Editor icon\">\n <defs>\n <linearGradient id=\"g\" x1=\"0\" x2=\"1\" y1=\"0\" y2=\"1\">\n <stop offset=\"0%\" stop-color=\"#1d4ed8\"/>\n <stop offset=\"100%\" stop-color=\"#0f172a\"/>\n </linearGradient>\n </defs>\n <rect width=\"128\" height=\"128\" rx=\"28\" fill=\"url(#g)\"/>\n <rect x=\"24\" y=\"28\" width=\"80\" height=\"16\" rx=\"7\" fill=\"#e2e8f0\" opacity=\"0.9\"/>\n <rect x=\"24\" y=\"56\" width=\"56\" height=\"14\" rx=\"7\" fill=\"#bfdbfe\" opacity=\"0.9\"/>\n <rect x=\"24\" y=\"80\" width=\"68\" height=\"14\" rx=\"7\" fill=\"#cbd5e1\" opacity=\"0.85\"/>\n <circle cx=\"92\" cy=\"90\" r=\"12\" fill=\"#f8fafc\" opacity=\"0.92\"/>\n <path d=\"M86 90l4 4 8-10\" fill=\"none\" stroke=\"#0f172a\" stroke-width=\"6\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n</svg>\n",
29
29
  "public/manifest.webmanifest": "{\n \"name\": \"Canvas Theme Editor\",\n \"short_name\": \"Canvas Theme Editor\",\n \"description\": \"Create and preview Canvas Rich Content Editor themes and starter content.\",\n \"start_url\": \".\",\n \"scope\": \".\",\n \"display\": \"standalone\",\n \"background_color\": \"#f5f5f5\",\n \"theme_color\": \"#0d1b2a\",\n \"orientation\": \"portrait-primary\",\n \"lang\": \"en\",\n \"icons\": [\n {\n \"src\": \"/icon.svg\",\n \"sizes\": \"any\",\n \"type\": \"image/svg+xml\",\n \"purpose\": \"any\"\n }\n ]\n}\n",
30
- "src/app.css": "/* Layout for this scaffold's own authoring UI (not part of any Canvas-pasted RCE content). */\n\n:root {\n color-scheme: light dark;\n overflow-x: hidden;\n}\n\n:where(html) {\n background-color: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n}\n\n:where(body) {\n min-height: 100vh;\n margin: 0;\n background: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n color: var(--instui-color-text-base, inherit);\n}\n\nbody:not(.canvas-rce-shell-enabled) {\n padding: 1.5rem 3rem 3rem;\n}\n\n/* Overrides the scaffold-base wrapper layout's app-shell ROW class (side nav + content) — this\n shell is a top header, not a side nav, so the body needs to stack it above #app instead. */\n\nhtml:has(body.canvas-rce-shell-enabled) {\n background-color: var(--instui-color-background-page);\n}\n\nbody.canvas-rce-shell-enabled {\n display: flex;\n flex-direction: column;\n padding: 0;\n background-color: var(--instui-color-background-page);\n}\n\n#app {\n margin: 0 auto;\n width: 100%;\n}\n\nbody.canvas-rce-shell-enabled #app {\n padding: 1rem 3rem 3rem;\n}\n\n@media (max-width: 64em) {\n body.canvas-rce-shell-enabled #app {\n padding: 1rem 1.5rem 1.5rem;\n }\n}\n\n/* Redundant with the shell's own brand mark once it's showing. */\nbody.canvas-rce-shell-enabled .title,\nbody.canvas-rce-shell-enabled .description,\nbody.canvas-rce-shell-enabled #app-mode {\n display: none;\n}\n\n.canvas-rce-shell {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 0.75rem 1.5rem;\n border-block-end: 1px solid var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n background: color-mix(in srgb, var(--instui-color-background-container) 92%, transparent);\n backdrop-filter: blur(12px);\n position: relative;\n inset-block-start: 0;\n /* backdrop-filter makes this a stacking context, so the panel's z-index is capped at this one:\n it must clear the .controls tooltips (30/40) but stay below the fullscreen preview (100). */\n z-index: 50;\n}\n\n.canvas-rce-shell__brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n min-width: 0;\n}\n\n.canvas-rce-shell__brand-mark {\n flex-shrink: 0;\n}\n\n/* Navy primitives here are remapped to the chrome's active color; the glyph reads as a cutout. */\n.canvas-rce-shell__brand-mark.instui-button.-color-primary {\n background: light-dark(\n var(--instui-primitive-color-navy-navy100),\n var(--instui-primitive-color-navy-navy90)\n );\n border-color: light-dark(\n var(--instui-primitive-color-navy-navy100),\n var(--instui-primitive-color-navy-navy90)\n );\n color: light-dark(var(--instui-color-background-container), var(--instui-color-background-page));\n}\n\n.canvas-rce-shell__brand-mark.instui-button.-color-primary:hover {\n background: light-dark(\n var(--instui-primitive-color-navy-navy110),\n var(--instui-primitive-color-navy-navy80)\n );\n border-color: light-dark(\n var(--instui-primitive-color-navy-navy110),\n var(--instui-primitive-color-navy-navy80)\n );\n}\n\n.canvas-rce-shell__brand-mark.instui-button.-color-primary:active {\n background: light-dark(\n var(--instui-primitive-color-navy-navy120),\n var(--instui-primitive-color-navy-navy70)\n );\n border-color: light-dark(\n var(--instui-primitive-color-navy-navy120),\n var(--instui-primitive-color-navy-navy70)\n );\n}\n\n.canvas-rce-shell__eyebrow {\n display: block;\n font-size: 0.625rem;\n line-height: 1;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n opacity: 0.7;\n}\n\n.canvas-rce-shell__brand strong {\n display: block;\n font-size: 1rem;\n line-height: 1.2;\n}\n\n@media (max-width: 48em) {\n .canvas-rce-shell__brand strong {\n font-size: 0.8rem;\n }\n}\n\n.canvas-rce-shell__actions {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n}\n\n.canvas-rce-shell__menu {\n position: relative;\n}\n\n.canvas-rce-shell__menu summary {\n list-style: none;\n}\n\n.canvas-rce-shell__menu summary::-webkit-details-marker {\n display: none;\n}\n\n.canvas-rce-shell__summary .canvas-rce-shell__icon {\n width: 1rem;\n height: 1rem;\n}\n\n.canvas-rce-shell__label {\n white-space: nowrap;\n}\n\n.canvas-rce-shell__panel {\n position: absolute;\n inset-block-start: calc(100% + 0.5rem);\n inset-inline-end: 0;\n width: max-content;\n min-width: 12rem;\n max-width: calc(100vw - 2rem);\n overflow: hidden;\n border: 1px solid var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n border-radius: 0.75rem;\n background: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.15);\n z-index: 60;\n}\n\n.canvas-rce-shell__panel-scroll {\n max-height: min(70vh, 24rem);\n display: grid;\n gap: 0.5rem;\n overflow-y: auto;\n overflow-x: hidden;\n padding: 0.75rem;\n}\n\n.canvas-rce-shell__group {\n display: grid;\n gap: 0.375rem;\n}\n\n.canvas-rce-shell__group-title {\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.7;\n}\n\n.canvas-rce-shell__option {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n width: 100%;\n white-space: nowrap;\n padding: 0.5rem 0.625rem;\n border: 1px solid transparent;\n border-radius: 0.5rem;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.canvas-rce-shell__option:hover,\n.canvas-rce-shell__option:focus-visible {\n border-color: var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n background: var(--instui-color-background-secondary, rgba(0, 0, 0, 0.02));\n}\n\n.canvas-rce-shell__option.is-selected {\n border-color: var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n background: var(--instui-color-background-secondary, rgba(0, 0, 0, 0.02));\n}\n\n.canvas-rce-shell__option .canvas-rce-shell__mark {\n width: 1rem;\n height: 1rem;\n opacity: 0;\n}\n\n.canvas-rce-shell__option.is-selected .canvas-rce-shell__mark {\n opacity: 1;\n}\n\n.theme-picker__custom.canvas-rce-shell__custom {\n max-width: none;\n padding: 0.25rem 0.625rem 0;\n}\n\n.wrapper {\n margin: var(--instui-spacing-padding-container-lg) var(--instui-spacing-padding-container-lg);\n}\n\n.header {\n display: flex;\n flex-direction: column;\n}\n\n.utilities {\n display: flex;\n justify-content: flex-end;\n gap: var(--instui-component-shared-tokens-spacing-gap-buttons);\n}\n\n.controls .instui-tooltip,\n.preview-pane .instui-tooltip {\n z-index: 30;\n}\n\n.controls .instui-tooltip > .tip,\n.preview-pane .instui-tooltip > .tip {\n z-index: 40;\n}\n\n/* The open/close chevrons point at the tray's edge, so RTL needs the mirror-image glyph, not a flip. */\n:dir(rtl) .instui-button.-icon-panel-right-open {\n --pantoken-glyph: var(--instui-icon-panel-left-open);\n}\n\n:dir(rtl) .instui-button.-icon-panel-right-close {\n --pantoken-glyph: var(--instui-icon-panel-left-close);\n}\n\n.instui-tray .instui-tabs {\n position: relative;\n}\n\n.instui-tray .list {\n display: flex;\n gap: 0.5rem;\n padding-inline-end: var(--instui-component-base-button-small-height);\n}\n\n.theme-tray__close {\n position: absolute;\n inset-block-start: 0;\n inset-inline-end: 0;\n}\n\n.instui-tray .panel[hidden] {\n display: none;\n}\n\n.config-section {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n margin-block-end: 1rem;\n}\n\n.theme-picker {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.theme-picker__colors {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n gap: 0.25rem;\n max-width: 14rem;\n}\n\n.theme-picker__color-item {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border-radius: 50%;\n border: 2px solid transparent;\n background: transparent;\n cursor: pointer;\n}\n\n.theme-picker__color-item[aria-checked=\"true\"] {\n border-color: currentcolor;\n}\n\n.theme-picker__swatch {\n width: 1rem;\n height: 1rem;\n border-radius: 50%;\n display: block;\n box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);\n}\n\n.theme-picker__swatch[data-swatch=\"navy\"] {\n background-color: var(--instui-primitive-color-navy-navy70);\n}\n\n.theme-picker__swatch[data-swatch=\"blue\"] {\n background-color: var(--instui-primitive-color-blue-blue70);\n}\n\n.theme-picker__swatch[data-swatch=\"green\"] {\n background-color: var(--instui-primitive-color-green-green70);\n}\n\n.theme-picker__swatch[data-swatch=\"red\"] {\n background-color: var(--instui-primitive-color-red-red70);\n}\n\n.theme-picker__swatch[data-swatch=\"orange\"] {\n background-color: var(--instui-primitive-color-orange-orange70);\n}\n\n.theme-picker__swatch[data-swatch=\"grey\"] {\n background-color: var(--instui-primitive-color-grey-grey70);\n}\n\n.theme-picker__swatch[data-swatch=\"plum\"] {\n background-color: var(--instui-primitive-color-plum-plum70);\n}\n\n.theme-picker__swatch[data-swatch=\"violet\"] {\n background-color: var(--instui-primitive-color-violet-violet70);\n}\n\n.theme-picker__swatch[data-swatch=\"stone\"] {\n background-color: var(--instui-primitive-color-stone-stone70);\n}\n\n.theme-picker__swatch[data-swatch=\"sky\"] {\n background-color: var(--instui-primitive-color-sky-sky70);\n}\n\n.theme-picker__swatch[data-swatch=\"honey\"] {\n background-color: var(--instui-primitive-color-honey-honey70);\n}\n\n.theme-picker__swatch[data-swatch=\"sea\"] {\n background-color: var(--instui-primitive-color-sea-sea70);\n}\n\n.theme-picker__swatch[data-swatch=\"aurora\"] {\n background-color: var(--instui-primitive-color-aurora-aurora70);\n}\n\n/* A rainbow ring sets Custom apart from the palettes; the center shows the picked hex, if any. */\n.theme-picker__swatch[data-swatch=\"custom\"] {\n background:\n radial-gradient(closest-side, var(--custom-swatch-fill, Canvas) 55%, transparent 60%),\n conic-gradient(red, yellow, lime, cyan, blue, magenta, red);\n}\n\n.theme-picker__custom {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n max-width: 14rem;\n}\n\n.theme-picker__custom[hidden] {\n display: none;\n}\n\n.theme-picker__custom-inputs {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.theme-picker__custom-inputs input[type=\"color\"] {\n flex: none;\n inline-size: 2rem;\n block-size: 2rem;\n padding: 0;\n border: 0;\n background: none;\n cursor: pointer;\n}\n\n.code-editor {\n border: var(--instui-component-tray-border-width, 1px) solid\n var(--instui-component-tray-border-color, #ccc);\n min-height: 20rem;\n}\n\n.controls {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n}\n\n/* Named distinctly from the generic scaffold-base wrapper layout's `.container .content` — that\n shared selector otherwise wins on specificity and silently forces `flex-direction: row` here\n without ever setting `data-layout`, so the row-only sizing rules below never activate. */\n.panes {\n display: flex;\n flex-direction: column;\n gap: var(--instui-spacing-gap-cards-lg);\n}\n\n.pane {\n display: flex;\n flex-direction: column;\n}\n\n/* min-height (not height) so TinyMCE's own resize handle can grow the editor, not just shrink it.\n flex:0 1 auto so it stays content-sized in stacked layout — flex:1 1 0 here would give it a\n flex-basis of 0 with no free space to grow into (the column has no fixed height), capping how\n tall TinyMCE's own resize handle can make it. Row layout restores equal-width sharing below. */\n.editor-pane {\n flex: 0 1 auto;\n min-height: 25rem;\n min-width: 0;\n\n .tox {\n border-width: var(--instui-border-width-sm);\n box-shadow: var(--instui-elevation-resting);\n border-color: var(--instui-component-view-border-color-secondary);\n border-radius: var(--instui-border-radius-container-md);\n background-color: var(--instui-color-background-container);\n\n &:is(\n :not(.tox-tinymce-inline) .tox-editor-header,\n .tox-toolbar-overlord,\n .tox-toolbar,\n .tox-toolbar__primary,\n .tox-toolbar__overflow\n ) {\n background-color: var(--instui-color-background-container);\n }\n\n .tox-statusbar {\n height: 33px;\n }\n }\n}\n\n/* The iframe reports its rendered content height to the host, so this pane stays content-sized in\n the stacked layout. The row rule below restores equal width sharing without stretching height. */\n.preview-pane {\n flex: 0 1 auto;\n align-items: center;\n gap: 0;\n min-width: 0;\n min-height: 0;\n border-width: var(--instui-border-width-sm);\n box-shadow: var(--instui-elevation-resting);\n border-color: var(--instui-component-view-border-color-secondary);\n border-radius: var(--instui-border-radius-container-md);\n background-color: var(--instui-color-background-container);\n border-style: solid;\n}\n\n.panes[data-layout=\"row\"] .editor-pane {\n flex: 1 1 0;\n}\n\n.panes[data-layout=\"row\"] .preview-pane {\n flex: 1 1 0;\n align-self: flex-start;\n}\n\n.preview-width-picker {\n display: flex;\n align-items: center;\n align-self: flex-start;\n gap: 0.125rem;\n}\n\n.preview-width-value {\n min-inline-size: 3ch;\n padding-inline: var(--instui-spacing-space2xs);\n text-align: end;\n font-variant-numeric: tabular-nums;\n}\n\n.preview-width-unit {\n color: var(--instui-color-text-secondary);\n font-size: var(--instui-font-size-small);\n}\n\n/* Overlays the current window/iframe viewport — not the OS-level Fullscreen API — matching\n TinyMCE's own fullscreen plugin. !important overrides the width picker's inline flex/width. */\n.preview-pane.-fullscreen {\n position: fixed !important;\n inset: 0 !important;\n z-index: 100;\n flex: initial !important;\n width: 100vw !important;\n height: 100vh !important;\n margin: 0 !important;\n padding: 0;\n background: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n overflow: hidden;\n border: 0 none;\n border-radius: 0;\n}\n\n/* The page behind the overlay keeps its full content height, so without this the document still\n scrolls under a pane that's pinned to the viewport. */\nhtml:has(.preview-pane.-fullscreen),\nhtml:has(.preview-pane.-fullscreen) body {\n overflow: hidden;\n}\n\n#editor,\n#css-editor,\n#js-editor {\n flex: 1 1 auto;\n min-height: 0;\n width: 100%;\n}\n\n/* Width is set inline (large/medium/small); height grows with content — see updatePreview(). */\n#preview {\n border: 0 none;\n overflow: hidden;\n width: 100%;\n margin-inline: auto;\n}\n\n/* Fullscreen swaps the content-height sizing (see resizePreviewFrame) for filling the pane, so long\n content scrolls inside the frame instead of growing a scrollbar on the overlay. */\n.preview-pane.-fullscreen #preview {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n border-radius: 0;\n}\n\n.preview-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n width: 100%;\n padding: 0.5rem 1rem;\n}\n\n.preview-footer {\n display: flex;\n justify-content: flex-end;\n gap: 0.125rem;\n border-top: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n width: 100%;\n padding: 0 0.25rem;\n}\n\nbody.preview-popup {\n min-height: 100vh;\n margin: 0;\n overflow: hidden;\n}\n\nbody.preview-popup #preview-popup-toggle {\n display: none;\n}\n",
30
+ "src/app.css": "/* Layout for this scaffold's own authoring UI (not part of any Canvas-pasted RCE content). */\n\n:root {\n color-scheme: light dark;\n overflow-x: hidden;\n}\n\n:where(html) {\n background-color: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n}\n\n:where(body) {\n min-height: 100vh;\n margin: 0;\n background: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n color: var(--instui-color-text-base, inherit);\n}\n\nbody:not(.canvas-rce-shell-enabled) {\n padding: 1.5rem 3rem 3rem;\n}\n\n/* Overrides the scaffold-base wrapper layout's app-shell ROW class (side nav + content) — this\n shell is a top header, not a side nav, so the body needs to stack it above #app instead. */\n\nhtml:has(body.canvas-rce-shell-enabled) {\n background-color: var(--instui-color-background-page);\n}\n\nbody.canvas-rce-shell-enabled {\n display: flex;\n flex-direction: column;\n padding: 0;\n background-color: var(--instui-color-background-page);\n}\n\n#app {\n margin: 0 auto;\n width: 100%;\n}\n\nbody.canvas-rce-shell-enabled #app {\n padding: 1rem 3rem 3rem;\n}\n\n@media (max-width: 64em) {\n body.canvas-rce-shell-enabled #app {\n padding: 1rem 1.5rem 1.5rem;\n }\n}\n\n/* Redundant with the shell's own brand mark once it's showing. */\nbody.canvas-rce-shell-enabled .title,\nbody.canvas-rce-shell-enabled .description,\nbody.canvas-rce-shell-enabled #app-mode {\n display: none;\n}\n\n.canvas-rce-shell {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 0.75rem 1.5rem;\n border-block-end: 1px solid var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n background: color-mix(in srgb, var(--instui-color-background-container) 92%, transparent);\n backdrop-filter: blur(12px);\n position: relative;\n inset-block-start: 0;\n /* backdrop-filter makes this a stacking context, so the panel's z-index is capped at this one:\n it must clear the .controls tooltips (30/40) but stay below the fullscreen preview (100). */\n z-index: 50;\n}\n\n.canvas-rce-shell__brand {\n display: flex;\n align-items: center;\n gap: 0.75rem;\n min-width: 0;\n}\n\n.canvas-rce-shell__brand-mark {\n flex-shrink: 0;\n}\n\n/* Navy primitives here are remapped to the chrome's active color; the glyph reads as a cutout. */\n.canvas-rce-shell__brand-mark.instui-button.-color-primary {\n background: light-dark(\n var(--instui-primitive-color-navy-navy100),\n var(--instui-primitive-color-navy-navy90)\n );\n border-color: light-dark(\n var(--instui-primitive-color-navy-navy100),\n var(--instui-primitive-color-navy-navy90)\n );\n color: light-dark(var(--instui-color-background-container), var(--instui-color-background-page));\n}\n\n.canvas-rce-shell__brand-mark.instui-button.-color-primary:hover {\n background: light-dark(\n var(--instui-primitive-color-navy-navy110),\n var(--instui-primitive-color-navy-navy80)\n );\n border-color: light-dark(\n var(--instui-primitive-color-navy-navy110),\n var(--instui-primitive-color-navy-navy80)\n );\n}\n\n.canvas-rce-shell__brand-mark.instui-button.-color-primary:active {\n background: light-dark(\n var(--instui-primitive-color-navy-navy120),\n var(--instui-primitive-color-navy-navy70)\n );\n border-color: light-dark(\n var(--instui-primitive-color-navy-navy120),\n var(--instui-primitive-color-navy-navy70)\n );\n}\n\n.canvas-rce-shell__eyebrow {\n display: block;\n font-size: 0.625rem;\n line-height: 1;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n opacity: 0.7;\n}\n\n.canvas-rce-shell__brand strong {\n display: block;\n font-size: 1rem;\n line-height: 1.2;\n}\n\n@media (max-width: 48em) {\n .canvas-rce-shell__brand strong {\n font-size: 0.8rem;\n }\n}\n\n.canvas-rce-shell__actions {\n display: flex;\n align-items: center;\n gap: 0.625rem;\n}\n\n.canvas-rce-shell__menu {\n position: relative;\n}\n\n.canvas-rce-shell__menu summary {\n list-style: none;\n}\n\n.canvas-rce-shell__menu summary::-webkit-details-marker {\n display: none;\n}\n\n.canvas-rce-shell__summary .canvas-rce-shell__icon {\n width: 1rem;\n height: 1rem;\n}\n\n.canvas-rce-shell__label {\n white-space: nowrap;\n}\n\n.canvas-rce-shell__panel {\n position: absolute;\n inset-block-start: calc(100% + 0.5rem);\n inset-inline-end: 0;\n width: max-content;\n min-width: 12rem;\n max-width: calc(100vw - 2rem);\n overflow: hidden;\n border: 1px solid var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n border-radius: 0.75rem;\n background: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n box-shadow: 0 0.75rem 2rem rgba(0, 0, 0, 0.15);\n z-index: 60;\n}\n\n.canvas-rce-shell__panel-scroll {\n max-height: min(70vh, 24rem);\n display: grid;\n gap: 0.5rem;\n overflow-y: auto;\n overflow-x: hidden;\n padding: 0.75rem;\n}\n\n.canvas-rce-shell__group {\n display: grid;\n gap: 0.375rem;\n}\n\n.canvas-rce-shell__group-title {\n font-size: 0.75rem;\n font-weight: 600;\n letter-spacing: 0.04em;\n text-transform: uppercase;\n opacity: 0.7;\n}\n\n.canvas-rce-shell__option {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n width: 100%;\n white-space: nowrap;\n padding: 0.5rem 0.625rem;\n border: 1px solid transparent;\n border-radius: 0.5rem;\n background: transparent;\n color: inherit;\n cursor: pointer;\n}\n\n.canvas-rce-shell__option:hover,\n.canvas-rce-shell__option:focus-visible {\n border-color: var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n background: var(--instui-color-background-secondary, rgba(0, 0, 0, 0.02));\n}\n\n.canvas-rce-shell__option.is-selected {\n border-color: var(--instui-color-stroke-base, rgba(0, 0, 0, 0.12));\n background: var(--instui-color-background-secondary, rgba(0, 0, 0, 0.02));\n}\n\n.canvas-rce-shell__option .canvas-rce-shell__mark {\n width: 1rem;\n height: 1rem;\n opacity: 0;\n}\n\n.canvas-rce-shell__option.is-selected .canvas-rce-shell__mark {\n opacity: 1;\n}\n\n.theme-picker__custom.canvas-rce-shell__custom {\n max-width: none;\n padding: 0.25rem 0.625rem 0;\n}\n\n.wrapper {\n margin: var(--instui-spacing-padding-container-lg) var(--instui-spacing-padding-container-lg);\n}\n\n.header {\n display: flex;\n flex-direction: column;\n}\n\n.utilities {\n display: flex;\n justify-content: flex-end;\n gap: var(--instui-component-shared-tokens-spacing-gap-buttons);\n}\n\n.controls .instui-tooltip,\n.preview-pane .instui-tooltip {\n z-index: 30;\n}\n\n.controls .instui-tooltip > .tip,\n.preview-pane .instui-tooltip > .tip {\n z-index: 40;\n}\n\n/* The open/close chevrons point at the tray's edge, so RTL needs the mirror-image glyph, not a flip. */\n:dir(rtl) .instui-button.-icon-panel-right-open {\n --pantoken-glyph: var(--instui-icon-panel-left-open);\n}\n\n:dir(rtl) .instui-button.-icon-panel-right-close {\n --pantoken-glyph: var(--instui-icon-panel-left-close);\n}\n\n.instui-tray .instui-tabs {\n position: relative;\n}\n\n.instui-tray .list {\n display: flex;\n gap: 0.5rem;\n padding-inline-end: var(--instui-component-base-button-small-height);\n}\n\n.theme-tray__close {\n position: absolute;\n inset-block-start: 0;\n inset-inline-end: 0;\n}\n\n.instui-tray .panel[hidden] {\n display: none;\n}\n\n.config-section {\n display: flex;\n flex-direction: column;\n gap: 0.5rem;\n margin-block-end: 1rem;\n}\n\n.theme-picker {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n.theme-picker__colors {\n display: grid;\n grid-template-columns: repeat(7, 1fr);\n gap: 0.25rem;\n max-width: 14rem;\n}\n\n.theme-picker__color-item {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 1.75rem;\n height: 1.75rem;\n padding: 0;\n border-radius: 50%;\n border: 2px solid transparent;\n background: transparent;\n cursor: pointer;\n}\n\n.theme-picker__color-item[aria-checked=\"true\"] {\n border-color: currentcolor;\n}\n\n.theme-picker__swatch {\n width: 1rem;\n height: 1rem;\n border-radius: 50%;\n display: block;\n box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);\n}\n\n.theme-picker__swatch[data-swatch=\"navy\"] {\n background-color: var(--instui-primitive-color-navy-navy70);\n}\n\n.theme-picker__swatch[data-swatch=\"blue\"] {\n background-color: var(--instui-primitive-color-blue-blue70);\n}\n\n.theme-picker__swatch[data-swatch=\"green\"] {\n background-color: var(--instui-primitive-color-green-green70);\n}\n\n.theme-picker__swatch[data-swatch=\"red\"] {\n background-color: var(--instui-primitive-color-red-red70);\n}\n\n.theme-picker__swatch[data-swatch=\"orange\"] {\n background-color: var(--instui-primitive-color-orange-orange70);\n}\n\n.theme-picker__swatch[data-swatch=\"grey\"] {\n background-color: var(--instui-primitive-color-grey-grey70);\n}\n\n.theme-picker__swatch[data-swatch=\"plum\"] {\n background-color: var(--instui-primitive-color-plum-plum70);\n}\n\n.theme-picker__swatch[data-swatch=\"violet\"] {\n background-color: var(--instui-primitive-color-violet-violet70);\n}\n\n.theme-picker__swatch[data-swatch=\"stone\"] {\n background-color: var(--instui-primitive-color-stone-stone70);\n}\n\n.theme-picker__swatch[data-swatch=\"sky\"] {\n background-color: var(--instui-primitive-color-sky-sky70);\n}\n\n.theme-picker__swatch[data-swatch=\"honey\"] {\n background-color: var(--instui-primitive-color-honey-honey70);\n}\n\n.theme-picker__swatch[data-swatch=\"sea\"] {\n background-color: var(--instui-primitive-color-sea-sea70);\n}\n\n.theme-picker__swatch[data-swatch=\"aurora\"] {\n background-color: var(--instui-primitive-color-aurora-aurora70);\n}\n\n/* A rainbow ring sets Custom apart from the palettes; the center shows the picked hex, if any. */\n.theme-picker__swatch[data-swatch=\"custom\"] {\n background:\n radial-gradient(closest-side, var(--custom-swatch-fill, Canvas) 55%, transparent 60%),\n conic-gradient(red, yellow, lime, cyan, blue, magenta, red);\n}\n\n.theme-picker__custom {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n max-width: 14rem;\n}\n\n.theme-picker__custom[hidden] {\n display: none;\n}\n\n.theme-picker__custom-inputs {\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n.theme-picker__custom-inputs input[type=\"color\"] {\n flex: none;\n inline-size: 2rem;\n block-size: 2rem;\n padding: 0;\n border: 0;\n background: none;\n cursor: pointer;\n}\n\n.code-editor {\n border: var(--instui-component-tray-border-width, 1px) solid\n var(--instui-component-tray-border-color, #ccc);\n min-height: 20rem;\n}\n\n.controls {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n justify-content: space-between;\n gap: 0.5rem;\n}\n\n/* Named distinctly from the generic scaffold-base wrapper layout's `.container .content` — that\n shared selector otherwise wins on specificity and silently forces `flex-direction: row` here\n without ever setting `data-layout`, so the row-only sizing rules below never activate. */\n.panes {\n display: flex;\n flex-direction: column;\n gap: var(--instui-spacing-gap-cards-lg);\n}\n\n.pane {\n display: flex;\n flex-direction: column;\n}\n\n/* min-height (not height) so TinyMCE's own resize handle can grow the editor, not just shrink it.\n flex:0 1 auto so it stays content-sized in stacked layout — flex:1 1 0 here would give it a\n flex-basis of 0 with no free space to grow into (the column has no fixed height), capping how\n tall TinyMCE's own resize handle can make it. Row layout restores equal-width sharing below. */\n.editor-pane {\n flex: 0 1 auto;\n min-height: 25rem;\n min-width: 0;\n\n .tox.tox {\n border-width: var(--instui-border-width-sm);\n box-shadow: var(--instui-elevation-resting);\n border-color: var(--instui-component-view-border-color-secondary);\n border-radius: var(--instui-border-radius-container-md);\n background-color: var(--instui-color-background-container);\n\n .tox-edit-area.tox-edit-area::before {\n border: 2px solid var(--instui-focus-outline-color);\n border-radius: 0;\n }\n\n &:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header {\n box-shadow: none;\n border-bottom: 1px solid var(--instui-component-view-border-color-secondary);\n }\n\n &:is(\n :not(.tox-tinymce-inline) .tox-editor-header,\n .tox-toolbar-overlord,\n .tox-toolbar,\n .tox-toolbar__primary,\n .tox-toolbar__overflow\n ) {\n background-color: var(--instui-color-background-container);\n }\n\n .tox-statusbar {\n height: 33px;\n border-top: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n }\n }\n}\n\n:root[data-pantoken-scheme=\"dark\"] {\n .editor-pane {\n .tox.tox {\n border: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n\n .tox-edit-area.tox-edit-area::before {\n border: 2px solid var(--instui-focus-outline-color);\n border-radius: 0;\n }\n\n &:not(.tox-tinymce-inline) .tox-editor-header.tox-editor-header {\n box-shadow: none;\n border-bottom: 1px solid var(--instui-component-view-border-color-secondary);\n }\n\n .tox-statusbar {\n height: 33px;\n border-top: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n }\n }\n }\n}\n\n/* The iframe reports its rendered content height to the host, so this pane stays content-sized in\n the stacked layout. The row rule below restores equal width sharing without stretching height. */\n.preview-pane {\n flex: 0 1 auto;\n align-items: center;\n gap: 0;\n min-width: 0;\n min-height: 0;\n border-width: var(--instui-border-width-sm);\n box-shadow: var(--instui-elevation-resting);\n border-color: var(--instui-component-view-border-color-secondary);\n border-radius: var(--instui-border-radius-container-md);\n background-color: var(--instui-color-background-container);\n border-style: solid;\n}\n\n.panes[data-layout=\"row\"] .editor-pane {\n flex: 1 1 0;\n}\n\n.panes[data-layout=\"row\"] .preview-pane {\n flex: 1 1 0;\n align-self: flex-start;\n}\n\n.preview-width-picker {\n display: flex;\n align-items: center;\n align-self: flex-start;\n gap: 0.125rem;\n}\n\n.preview-width-value {\n min-inline-size: 3ch;\n padding-inline: var(--instui-spacing-space2xs);\n text-align: end;\n font-variant-numeric: tabular-nums;\n}\n\n.preview-width-unit {\n color: var(--instui-color-text-secondary);\n font-size: var(--instui-font-size-small);\n}\n\n/* Overlays the current window/iframe viewport — not the OS-level Fullscreen API — matching\n TinyMCE's own fullscreen plugin. !important overrides the width picker's inline flex/width. */\n.preview-pane.-fullscreen {\n position: fixed !important;\n inset: 0 !important;\n z-index: 100;\n flex: initial !important;\n width: 100vw !important;\n height: 100vh !important;\n margin: 0 !important;\n padding: 0;\n background: light-dark(\n var(--instui-color-background-container),\n var(--instui-color-background-page)\n );\n overflow: hidden;\n border: 0 none;\n border-radius: 0;\n}\n\n/* The page behind the overlay keeps its full content height, so without this the document still\n scrolls under a pane that's pinned to the viewport. */\nhtml:has(.preview-pane.-fullscreen),\nhtml:has(.preview-pane.-fullscreen) body {\n overflow: hidden;\n}\n\n#editor,\n#css-editor,\n#js-editor {\n flex: 1 1 auto;\n min-height: 0;\n width: 100%;\n}\n\n/* Width is set inline (large/medium/small); height grows with content — see updatePreview(). */\n#preview {\n border: 0 none;\n overflow: hidden;\n width: 100%;\n margin-inline: auto;\n}\n\n/* Fullscreen swaps the content-height sizing (see resizePreviewFrame) for filling the pane, so long\n content scrolls inside the frame instead of growing a scrollbar on the overlay. */\n.preview-pane.-fullscreen #preview {\n flex: 1 1 auto;\n min-height: 0;\n overflow: auto;\n border-radius: 0;\n}\n\n.preview-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n border-bottom: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n width: 100%;\n padding: 0.5rem 1rem;\n}\n\n.preview-footer {\n display: flex;\n justify-content: flex-end;\n gap: 0.125rem;\n border-top: var(--instui-border-width-sm) solid\n var(--instui-component-view-border-color-secondary);\n width: 100%;\n padding: 0 0.25rem;\n}\n\nbody.preview-popup {\n min-height: 100vh;\n margin: 0;\n overflow: hidden;\n}\n\nbody.preview-popup #preview-popup-toggle {\n display: none;\n}\n",
31
31
  "src/app.html": "<div class=\"container\">\n <div class=\"header\">\n <div class=\"title\">\n <span class=\"instui-heading -level-h1\">{{pageTitle}}</span>\n </div>\n </div>\n <div class=\"description\">{{pageDescription}}</div>\n\n <div class=\"controls --my-lg\">\n <div class=\"control-group instui-button-set -color-primary\">\n <button\n type=\"button\"\n id=\"download-package\"\n class=\"instui-button instui-tooltip -icon-file-archive\"\n aria-label=\"{{downloadPackageLabel}}\"\n aria-describedby=\"download-package-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{downloadPackageLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"download-package-tip\" role=\"tooltip\"\n >{{downloadPackageLabel}}</span\n >\n </button>\n <button\n type=\"button\"\n id=\"download-html\"\n class=\"instui-button instui-tooltip -icon-download\"\n aria-label=\"{{downloadHtmlLabel}}\"\n aria-describedby=\"download-html-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{downloadHtmlLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"download-html-tip\" role=\"tooltip\"\n >{{downloadHtmlLabel}}</span\n >\n </button>\n <button\n type=\"button\"\n id=\"copy-html\"\n class=\"instui-button instui-tooltip -icon-copy\"\n aria-label=\"{{copyHtmlLabel}}\"\n aria-describedby=\"copy-html-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{copyHtmlLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"copy-html-tip\" role=\"tooltip\"\n >{{copyHtmlLabel}}</span\n >\n </button>\n </div>\n <div class=\"utilities\">\n <a\n href=\"/tools/canvas-rce/\"\n target=\"_blank\"\n id=\"app-mode\"\n class=\"instui-button instui-tooltip -color-secondary -icon-app-window-mac\"\n aria-describedby=\"app-mode-tip\"\n ><span class=\"instui-screen-reader-content\">{{appModeLabel}}</span\n ><span class=\"tip -placement-bottom\" id=\"app-mode-tip\" role=\"tooltip\"\n >{{appModeLabel}}</span\n ></a\n >\n <button\n type=\"button\"\n id=\"layout-direction-toggle\"\n class=\"instui-button instui-tooltip -color-secondary -icon-columns-2\"\n aria-pressed=\"false\"\n aria-label=\"{{layoutDirectionLabel}}\"\n aria-describedby=\"layout-direction-toggle-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{layoutDirectionLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"layout-direction-toggle-tip\" role=\"tooltip\"\n >{{layoutDirectionLabel}}</span\n >\n </button>\n <button\n class=\"instui-button instui-tooltip -color-secondary -icon-circle-question-mark\"\n type=\"button\"\n command=\"show-modal\"\n commandfor=\"help-modal\"\n aria-label=\"{{helpAriaLabel}}\"\n aria-describedby=\"help-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{helpAriaLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"help-tip\" role=\"tooltip\">{{helpAriaLabel}}</span>\n </button>\n <button\n class=\"instui-button instui-tooltip -color-secondary -icon-sliders-horizontal\"\n type=\"button\"\n popovertarget=\"theme-tray\"\n aria-label=\"{{editThemeLabel}}\"\n aria-describedby=\"edit-theme-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{editThemeLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"edit-theme-tip\" role=\"tooltip\"\n >{{editThemeLabel}}</span\n >\n </button>\n </div>\n </div>\n <div class=\"panes\">\n <div class=\"pane editor-pane\">\n <textarea id=\"editor\"><div data-pantoken-color=\"navy\"><p></p></div></textarea>\n </div>\n <div class=\"pane preview-pane\">\n <div class=\"preview-header\">\n <div class=\"preview-width-picker\" role=\"group\" aria-label=\"{{previewWidthGroupLabel}}\">\n <button\n type=\"button\"\n class=\"instui-button instui-tooltip -size-sm -color-tertiary -toggle -icon-monitor -without-background -without-border\"\n aria-pressed=\"true\"\n aria-label=\"{{previewWidthLargeLabel}}\"\n aria-describedby=\"preview-width-large-tip\"\n data-preview-width=\"large\"\n >\n <span class=\"instui-screen-reader-content\">{{previewWidthLargeLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"preview-width-large-tip\" role=\"tooltip\"\n >{{previewWidthLargeLabel}}</span\n >\n </button>\n <button\n type=\"button\"\n class=\"instui-button instui-tooltip -size-sm -color-tertiary -toggle -icon-tablet -without-background -without-border\"\n aria-pressed=\"false\"\n aria-label=\"{{previewWidthMediumLabel}}\"\n aria-describedby=\"preview-width-medium-tip\"\n data-preview-width=\"medium\"\n >\n <span class=\"instui-screen-reader-content\">{{previewWidthMediumLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"preview-width-medium-tip\" role=\"tooltip\"\n >{{previewWidthMediumLabel}}</span\n >\n </button>\n <button\n type=\"button\"\n class=\"instui-button instui-tooltip -size-sm -color-tertiary -toggle -icon-smartphone -without-background -without-border\"\n aria-pressed=\"false\"\n aria-label=\"{{previewWidthSmallLabel}}\"\n aria-describedby=\"preview-width-small-tip\"\n data-preview-width=\"small\"\n >\n <span class=\"instui-screen-reader-content\">{{previewWidthSmallLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"preview-width-small-tip\" role=\"tooltip\"\n >{{previewWidthSmallLabel}}</span\n >\n </button>\n <span\n id=\"preview-width-value\"\n class=\"instui-in-place-edit preview-width-value\"\n contenteditable=\"true\"\n role=\"textbox\"\n aria-label=\"{{previewWidthValueLabel}}\"\n data-input-type=\"number\"\n >1</span\n >\n <span class=\"preview-width-unit\" aria-hidden=\"true\">px</span>\n </div>\n <button\n type=\"button\"\n id=\"preview-scheme-toggle\"\n class=\"instui-button instui-tooltip -size-sm -color-tertiary -toggle -icon-moon -without-background -without-border\"\n aria-pressed=\"false\"\n aria-label=\"{{previewSchemeToggleLabel}}\"\n aria-describedby=\"preview-scheme-toggle-tip\"\n disabled\n >\n <span class=\"instui-screen-reader-content\">{{previewSchemeToggleLabel}}</span>\n <span class=\"tip -placement-bottom\" id=\"preview-scheme-toggle-tip\" role=\"tooltip\"\n >{{previewSchemeToggleLabel}}</span\n >\n </button>\n </div>\n <iframe id=\"preview\" title=\"{{previewFrameTitle}}\"></iframe>\n <div class=\"preview-footer\">\n <button\n type=\"button\"\n id=\"preview-popup-toggle\"\n class=\"instui-button instui-tooltip -size-sm -color-tertiary -icon-app-window -without-background -without-border\"\n aria-label=\"{{previewPopupLabel}}\"\n aria-describedby=\"preview-popup-toggle-tip\"\n >\n <span class=\"instui-screen-reader-content\">{{previewPopupLabel}}</span>\n <span class=\"tip\" id=\"preview-popup-toggle-tip\" role=\"tooltip\"\n >{{previewPopupLabel}}</span\n >\n <span id=\"preview-popup-title\" hidden>{{previewPopupTitle}}</span>\n <span id=\"preview-popup-blocked-message\" hidden>{{previewPopupBlockedMessage}}</span>\n </button>\n <button\n type=\"button\"\n id=\"preview-fullscreen-toggle\"\n class=\"instui-button instui-tooltip -size-sm -color-tertiary -toggle -icon-maximize -without-background -without-border\"\n aria-pressed=\"false\"\n aria-label=\"{{previewFullscreenLabel}}\"\n aria-describedby=\"preview-fullscreen-toggle-tip\"\n >\n <span id=\"preview-fullscreen-label\" class=\"instui-screen-reader-content\"\n >{{previewFullscreenLabel}}</span\n >\n <span class=\"tip\" id=\"preview-fullscreen-toggle-tip\" role=\"tooltip\"\n >{{previewFullscreenLabel}}</span\n >\n <span id=\"preview-popup-return-label\" class=\"instui-screen-reader-content\" hidden\n >{{previewPopupReturnLabel}}</span\n >\n </button>\n </div>\n </div>\n </div>\n</div>\n\n<div\n class=\"instui-tray -placement-end -size-md\"\n popover\n id=\"theme-tray\"\n aria-label=\"{{themeTrayAriaLabel}}\"\n>\n <div class=\"instui-tabs\">\n <div class=\"list\" role=\"tablist\" aria-label=\"{{themeTrayAriaLabel}}\">\n <button\n class=\"tab -selected\"\n role=\"tab\"\n aria-selected=\"true\"\n id=\"tab-config\"\n aria-controls=\"panel-config\"\n type=\"button\"\n >\n {{tabConfigLabel}}\n </button>\n <button\n class=\"tab\"\n role=\"tab\"\n aria-selected=\"false\"\n id=\"tab-css\"\n aria-controls=\"panel-css\"\n type=\"button\"\n >\n {{tabCssLabel}}\n </button>\n <button\n class=\"tab\"\n role=\"tab\"\n aria-selected=\"false\"\n id=\"tab-js\"\n aria-controls=\"panel-js\"\n type=\"button\"\n >\n {{tabJsLabel}}\n </button>\n </div>\n <button\n class=\"instui-close-button -size-sm theme-tray__close\"\n type=\"button\"\n popovertarget=\"theme-tray\"\n popovertargetaction=\"hide\"\n aria-label=\"{{closeLabel}}\"\n ></button>\n <div class=\"panel\" role=\"tabpanel\" id=\"panel-config\" aria-labelledby=\"tab-config\">\n <div class=\"config-section\">\n <span class=\"instui-heading -level-h3\">{{themeLabel}}</span>\n <div class=\"theme-picker\" role=\"group\" aria-label=\"{{themeLabel}}\">\n <button\n type=\"button\"\n class=\"instui-button -size-sm -color-secondary -toggle\"\n aria-pressed=\"true\"\n data-theme=\"rebrand\"\n >\n {{themeRebrandLabel}}\n </button>\n <button\n type=\"button\"\n class=\"instui-button -size-sm -color-secondary -toggle\"\n aria-pressed=\"false\"\n data-theme=\"canvas\"\n >\n {{themeCanvasLabel}}\n </button>\n <button\n type=\"button\"\n class=\"instui-button -size-sm -color-secondary -toggle\"\n aria-pressed=\"false\"\n data-theme=\"canvasHighContrast\"\n >\n {{themeCanvasHighContrastLabel}}\n </button>\n </div>\n <div class=\"theme-picker__colors\" role=\"group\" aria-label=\"{{colorLabel}}\">\n <button\n type=\"button\"\n class=\"theme-picker__color-item -selected\"\n role=\"menuitemradio\"\n aria-checked=\"true\"\n data-color=\"navy\"\n aria-label=\"{{colorNavyLabel}}\"\n title=\"{{colorNavyLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"navy\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"blue\"\n aria-label=\"{{colorBlueLabel}}\"\n title=\"{{colorBlueLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"blue\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"green\"\n aria-label=\"{{colorGreenLabel}}\"\n title=\"{{colorGreenLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"green\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"red\"\n aria-label=\"{{colorRedLabel}}\"\n title=\"{{colorRedLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"red\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"orange\"\n aria-label=\"{{colorOrangeLabel}}\"\n title=\"{{colorOrangeLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"orange\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"grey\"\n aria-label=\"{{colorGreyLabel}}\"\n title=\"{{colorGreyLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"grey\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"plum\"\n aria-label=\"{{colorPlumLabel}}\"\n title=\"{{colorPlumLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"plum\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"violet\"\n aria-label=\"{{colorVioletLabel}}\"\n title=\"{{colorVioletLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"violet\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"stone\"\n aria-label=\"{{colorStoneLabel}}\"\n title=\"{{colorStoneLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"stone\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"sky\"\n aria-label=\"{{colorSkyLabel}}\"\n title=\"{{colorSkyLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"sky\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"honey\"\n aria-label=\"{{colorHoneyLabel}}\"\n title=\"{{colorHoneyLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"honey\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"sea\"\n aria-label=\"{{colorSeaLabel}}\"\n title=\"{{colorSeaLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"sea\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"aurora\"\n aria-label=\"{{colorAuroraLabel}}\"\n title=\"{{colorAuroraLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"aurora\"></span>\n </button>\n <button\n type=\"button\"\n class=\"theme-picker__color-item\"\n role=\"menuitemradio\"\n aria-checked=\"false\"\n data-color=\"custom\"\n aria-label=\"{{colorCustomLabel}}\"\n title=\"{{colorCustomLabel}}\"\n >\n <span class=\"theme-picker__swatch\" data-swatch=\"custom\"></span>\n </button>\n </div>\n <div class=\"theme-picker__custom\" id=\"custom-color-row\" hidden>\n <label for=\"custom-color-hex\">{{customColorInputLabel}}</label>\n <div class=\"theme-picker__custom-inputs\">\n <input\n type=\"color\"\n id=\"custom-color-picker\"\n aria-label=\"{{customColorInputLabel}}\"\n value=\"#4c79aa\"\n />\n <input\n type=\"text\"\n class=\"instui-text-input -size-sm\"\n id=\"custom-color-hex\"\n value=\"#4c79aa\"\n pattern=\"#?([0-9a-fA-F]{3}|[0-9a-fA-F]{6})\"\n maxlength=\"7\"\n spellcheck=\"false\"\n autocomplete=\"off\"\n />\n </div>\n </div>\n <label class=\"instui-checkbox -variant-toggle\" id=\"include-dark-mode-row\">\n <input type=\"checkbox\" id=\"include-dark-mode\" />\n {{includeDarkModeLabel}}\n </label>\n </div>\n <div class=\"config-section\">\n <span class=\"instui-heading -level-h3\">{{cdnSectionLabel}}</span>\n <label for=\"cdn-provider\">\n {{cdnProviderLabel}}\n <select class=\"instui-simple-select\" id=\"cdn-provider\"></select>\n </label>\n </div>\n </div>\n <div class=\"panel\" role=\"tabpanel\" id=\"panel-css\" hidden aria-labelledby=\"tab-css\">\n <p>{{cssPanelHintLabel}}</p>\n <div id=\"css-editor\" class=\"code-editor\"></div>\n <p><a id=\"download-theme-css\" download=\"theme.css\">{{downloadCssLabel}}</a></p>\n </div>\n <div class=\"panel\" role=\"tabpanel\" id=\"panel-js\" hidden aria-labelledby=\"tab-js\">\n <p>{{jsPanelHintLabel}}</p>\n <div id=\"js-editor\" class=\"code-editor\"></div>\n <p><a id=\"download-theme-js\" download=\"theme.js\">{{downloadJsLabel}}</a></p>\n </div>\n </div>\n</div>\n<dialog class=\"instui-modal\" id=\"help-modal\" aria-labelledby=\"help-modal-title\">\n <div class=\"header\"><strong id=\"help-modal-title\">{{helpModalTitle}}</strong></div>\n <div class=\"body\">\n <p>{{helpModalBody1}}</p>\n <p>{{helpModalBody2}}</p>\n </div>\n <div class=\"footer\">\n <button class=\"instui-button\" type=\"button\" command=\"close\" commandfor=\"help-modal\">\n {{closeLabel}}\n </button>\n </div>\n</dialog>\n",
32
32
  "src/locale.ts": "/** Resolve an editor locale without letting an embedded frame inherit standalone preferences. */\nexport function selectEditorLocale(\n embedded: boolean,\n queryLocale: string | null,\n storedLocale: string | null,\n initialLocale: string,\n supported: Record<string, \"ltr\" | \"rtl\">,\n): string {\n if (embedded) {\n return queryLocale && Object.hasOwn(supported, queryLocale) ? queryLocale : \"en\";\n }\n if (storedLocale && Object.hasOwn(supported, storedLocale)) return storedLocale;\n return Object.hasOwn(supported, initialLocale) ? initialLocale : \"en\";\n}\n",
33
33
  "src/main.ts": "// @ts-nocheck\n/**\n * Authors starter page templates against pantoken's classes: TinyMCE for editing (pinned/\n * configured to match instructure/canvas-lms's own RCE — see README), a custom \"Insert template\"\n * plugin for the bundled starters, and a live styled preview pane fed by the chosen CDN\n * provider/theme/mode. The page shell itself is built with pantoken (wrapper layout, tray, modal,\n * tabs) so this authoring tool dogfoods the same library it's previewing.\n */\nimport \"@pantoken/components/components.css\";\nimport \"@pantoken/components/utilities.css\";\nimport \"@pantoken/components/base.css\";\nimport \"@pantoken/components/icons/copy.css\";\nimport \"@pantoken/components/icons/app-window-mac.css\";\nimport \"@pantoken/components/icons/x.css\";\nimport \"@pantoken/components/icons/check.css\";\nimport \"@pantoken/components/icons/download.css\";\nimport \"@pantoken/components/icons/file-archive.css\";\nimport \"@pantoken/components/icons/sliders-horizontal.css\"; // app.html's -icon-sliders-horizontal button modifier\nimport \"@pantoken/components/icons/circle-question-mark.css\"; // app.html's -icon-circle-question-mark button modifier\nimport \"@pantoken/components/icons/rows-2.css\"; // app.html's layout-direction toggle, row state\nimport \"@pantoken/components/icons/columns-2.css\"; // app.html's layout-direction toggle, column state\nimport \"@pantoken/components/icons/monitor.css\"; // app.html's preview-width-picker, large\nimport \"@pantoken/components/icons/tablet.css\"; // app.html's preview-width-picker, medium\nimport \"@pantoken/components/icons/smartphone.css\"; // app.html's preview-width-picker, small\nimport \"@pantoken/components/icons/app-window.css\"; // app.html's preview popup button\nimport \"@pantoken/components/icons/maximize.css\"; // app.html's preview-fullscreen-toggle, off state\nimport \"@pantoken/components/icons/minimize.css\"; // app.html's preview-fullscreen-toggle, on state\nimport \"@pantoken/components/icons/moon.css\"; // app.html's preview-scheme-toggle, light state\nimport \"@pantoken/components/icons/sun.css\"; // app.html's preview-scheme-toggle, dark state\nimport adaptiveTokensCss from \"@pantoken/css/style.lean.css?inline\";\nimport rebrandLightTokensCss from \"@pantoken/css/style.rebrand.light.lean.css?inline\";\nimport canvasTokensCss from \"@pantoken/css/style.canvas.lean.css?inline\";\nimport canvasHighContrastTokensCss from \"@pantoken/css/style.canvas-high-contrast.lean.css?inline\";\nimport baseCss from \"@pantoken/components/base.css?inline\";\nimport componentIconsCss from \"@pantoken/components/component-icons.css?inline\";\nimport componentsCss from \"@pantoken/components/components.css?inline\";\nimport utilitiesCss from \"@pantoken/components/utilities.css?inline\";\nimport fontsCss from \"@pantoken/components/fonts.css?inline\";\nconst fontAssetUrls = import.meta.glob<string>(\n \"/node_modules/@pantoken/components/assets/fonts/**/*.woff2\",\n { eager: true, query: \"?url\", import: \"default\" },\n);\nconst localFontsCss = fontsCss.replace(/\\.\\.\\/assets\\/fonts\\/([^\"\\s)]+\\.woff2)/gu, (_, path: string) => {\n const url = fontAssetUrls[`/node_modules/@pantoken/components/assets/fonts/${path}`];\n if (!url) throw new Error(`Missing local font asset: ${path}`);\n return url;\n});\nimport localInteractionsJs from \"@pantoken/interactions/interactions.iife.js?raw\";\n// A resolved asset URL for a real <script> tag — a dynamic import() of a pure side-effect module\n// from a \"sideEffects\": false package can get tree-shaken out of a production bundle.\nimport interactionsIifeUrl from \"@pantoken/interactions/interactions.iife.js?url\";\n\nimport \"@pantoken/plugin-custom-components/custom-components.css\";\nimport \"@pantoken/plugin-layouts/layouts.css\";\nimport \"./app.css\";\nimport appHtml from \"./app.html?raw\";\nimport { STRINGS } from \"./strings.ts\";\nimport { LOCALE_STRINGS } from \"./locale-strings.ts\";\nimport { LOCALE_DIRS } from \"./locale-dirs.ts\";\nimport { TINYMCE_LOCALE_STRINGS } from \"./tinymce-locale-strings.ts\";\nimport { selectEditorLocale } from \"./locale.ts\";\nimport tinymce from \"tinymce\";\nimport \"tinymce/models/dom/model\";\nimport \"tinymce/themes/silver/theme\";\nimport \"tinymce/icons/default\";\nimport \"tinymce/plugins/image\";\nimport \"tinymce/plugins/link\";\nimport \"tinymce/plugins/lists\";\nimport \"tinymce/plugins/autosave\";\nimport \"tinymce/plugins/fullscreen\";\nimport \"tinymce/plugins/quickbars\";\nimport \"tinymce/plugins/searchreplace\";\nimport \"tinymce/plugins/wordcount\";\nimport \"tinymce/plugins/table\";\nimport \"tinymce/plugins/media\";\nimport \"tinymce/plugins/visualblocks\";\nimport nextGenSkinCss from \"@pantoken/tinymce/skins/next-gen/skin.css?inline\";\nimport nextGenContentCss from \"@pantoken/tinymce/skins/next-gen/content.css?inline\";\nimport canvasSkinCss from \"@pantoken/tinymce/skins/canvas/skin.css?inline\";\nimport canvasContentCss from \"@pantoken/tinymce/skins/canvas/content.css?inline\";\nimport canvasHighContrastSkinCss from \"@pantoken/tinymce/skins/canvas-high-contrast/skin.css?inline\";\nimport canvasHighContrastContentCss from \"@pantoken/tinymce/skins/canvas-high-contrast/content.css?inline\";\nimport {\n buildTheme,\n CDN_PROVIDERS,\n DEFAULT_CDN_PROVIDER_ID,\n defaultThemeCssAssets,\n type ThemeMode,\n type ThemeVariant,\n} from \"@pantoken/canvas-theme-editor\";\nimport { Check, ChevronDown, createElement, Languages, MoonStar, Palette, SunMedium } from \"lucide\";\n// Inlined (not CDN-fetched) so the live preview's color swap never depends on a network round trip.\nimport {\n COLOR_KEYS,\n CUSTOM_COLOR_KEY,\n customColorCss,\n customThemeColorsCss,\n isHexColor,\n parseHexColor,\n type PantokenColorNamespace,\n} from \"@pantoken/plugin-custom-theme-colors\";\nimport type { Editor } from \"tinymce\";\nimport { basicSetup, EditorView } from \"codemirror\";\nimport { css } from \"@codemirror/lang-css\";\nimport { html } from \"@codemirror/lang-html\";\nimport { javascript } from \"@codemirror/lang-javascript\";\nimport { strToU8, zipSync } from \"fflate\";\nimport { buildFileUrl, type CdnFile } from \"@pantoken/cdn\";\nimport {\n CONTENT_CLASSES_PLUGIN_NAME,\n createContentClassesPlugin,\n createFullscreenFooterPlugin,\n createPantokenPlugin,\n createSearchReplaceFooterPlugin,\n createSupSubPlugin,\n createVisualBlocksFooterPlugin,\n FULLSCREEN_FOOTER_PLUGIN_NAME,\n getIconCdnFile,\n getUsedIconCdnFiles,\n getLogoCdnFile,\n getUsedLogoCdnFiles,\n listComponents,\n listUtilities,\n loadAllIcons,\n logos,\n PANTOKEN_ICON,\n PANTOKEN_ICON_PACK_NAME,\n products,\n registerPantokenIconPack,\n pantokenHtmlLinter,\n pantokenHtmlCompletion,\n SEARCHREPLACE_FOOTER_PLUGIN_NAME,\n SUP_SUB_PLUGIN_NAME,\n SUP_SUB_TOOLBAR_NAME,\n VISUALBLOCKS_FOOTER_PLUGIN_NAME,\n type TaggedIcon,\n} from \"@pantoken/tinymce\";\nimport {\n createSourceTogglePlugin,\n SOURCE_TOGGLE_PLUGIN_NAME,\n type SourceTogglePluginApi,\n} from \"@pantoken/tinymce-codemirror\";\nimport {\n buildPlaceholdImage,\n createPlaceholdPlugin,\n PLACEHOLD_PLUGIN_NAME,\n} from \"@pantoken/tinymce-placehold\";\nimport {\n A11Y_PLUGIN_NAME,\n createA11yPlugin,\n type A11yCheckConfig,\n type A11yColorScheme,\n} from \"@pantoken/tinymce-a11y\";\nimport {\n createSavePlugin,\n SAVE_PLUGIN_NAME,\n SAVE_TOOLBAR_NAME,\n slugifyExportName,\n type SavePluginApi,\n} from \"@pantoken/tinymce-save\";\nimport { loadPreferences, savePreferences } from \"./preferences.ts\";\n\n// Shared with the toolbar's source-view editor so both stay the same size.\nconst EDITOR_HEIGHT_PX = 400;\n// This app's pantoken instance, and the one it accepts theme messages from. Two pantoken instances\n// can share a page (this editor embedded in the docs site being the case that forced the issue), so\n// both sides tag their messages and ignore anything addressed elsewhere.\nconst EDITOR_INSTANCE = \"canvas-theme-editor\";\nconst HOST_INSTANCE = \"docs\";\n// Canvas's content area caps out at this width — the preview mirrors that constraint.\nconst CANVAS_CONTENT_MAX_WIDTH = \"59.25rem\";\n// All 13 `[data-pantoken-color=\"…\"]` color-remap rules, computed once — editable Canvas content,\n// the shell, and the preview can each own an independent color scope. `resetSelector`\n// pins the theme tray's own subtree (buttons, swatches) and the standalone shell's menus (its\n// color swatches) back to true colors, immune to whatever color the chrome currently has active —\n// the shell's brand mark stays outside it so it follows the chrome color. See\n// plugins/pantoken/custom-theme-colors.\nconst CUSTOM_THEME_COLORS_CSS = customThemeColorsCss(undefined, {\n selector: \"\",\n resetSelector: \"#theme-tray, #canvas-rce-shell .canvas-rce-shell__actions\",\n});\n// The scaffold's own chrome (toolbar, buttons outside the tray) mirrors the embedding docs page's\n// active theme and color — entirely independent of the preview and the tray, which drive their own\n// theme/color separately below. The color remaps follow this theme block in the cascade so a parent\n// color change overrides the theme's root token declarations.\nconst chromeThemeStyle = document.createElement(\"style\");\ndocument.head.append(chromeThemeStyle);\nconst customThemeColorsStyle = document.createElement(\"style\");\ncustomThemeColorsStyle.textContent = CUSTOM_THEME_COLORS_CSS;\ndocument.head.append(customThemeColorsStyle);\n// The chrome's own `custom` scale, independent of the tray's; empty unless the chrome is on custom.\nconst chromeCustomColorStyle = document.createElement(\"style\");\ndocument.head.append(chromeCustomColorStyle);\n// Seeded with navy100 so switching to Custom starts from the default brand color.\nconst DEFAULT_CUSTOM_COLOR = \"#4c79aa\";\nlet chromeCustomColor = DEFAULT_CUSTOM_COLOR;\nlet syncShellCustomColor: (() => void) | undefined;\n\n/** Fill the Custom swatch's center with a picked hex; the untouched default leaves it empty. */\nfunction syncCustomSwatch(swatch: HTMLElement, hex: string): void {\n if (hex === DEFAULT_CUSTOM_COLOR) swatch.style.removeProperty(\"--custom-swatch-fill\");\n else swatch.style.setProperty(\"--custom-swatch-fill\", hex);\n}\nconst preferences = loadPreferences();\nlet previewPopup: Window | null = null;\nlet previewPopupCloseTimer: number | undefined;\nlet previewPopupStylePairs: Array<{\n source: HTMLStyleElement;\n clone: HTMLStyleElement;\n}> = [];\nconst LOCAL_PROVIDER_ID = \"none\";\n// `import.meta.env.DEV` is baked in at build time, so it's `false` for any `vite build` output —\n// including docs' Canvas RCE preview, which is always built for production yet only ever served on\n// localhost. Fall back to a hostname check so a local, non-CDN preview is detected either way.\nconst isLocalPreview =\n import.meta.env.DEV || [\"localhost\", \"127.0.0.1\", \"[::1]\"].includes(window.location.hostname);\nconst LOCAL_COMPONENT_CSS = [\n baseCss,\n componentIconsCss,\n componentsCss,\n utilitiesCss,\n CUSTOM_THEME_COLORS_CSS,\n localFontsCss,\n].join(\"\\n\");\n// pantoken's token sheets express dark mode as `light-dark()`, which lightningcss (this bundle's\n// minifier) downlevels to two OS-preference-gated control variables — fine for theme.css (a real\n// Canvas page can't force a scheme anyway), but it means `color-scheme`/`data-pantoken-scheme`\n// alone can't force the *preview's* scheme against the visitor's own OS setting. This block\n// re-derives those two control variables from `data-pantoken-scheme` so the preview's light/dark\n// toggle actually overrides it. `--lightningcss-light`/`-dark` are lightningcss's fixed,\n// undocumented-but-stable names for this downlevel — see the vendored `--lightningcss-*` custom\n// properties in @pantoken/css's built output.\nconst PREVIEW_SCHEME_FORCE_CSS = `\n[data-pantoken-scheme=\"light\"] { --lightningcss-light: initial; --lightningcss-dark: ; }\n[data-pantoken-scheme=\"dark\"] { --lightningcss-light: ; --lightningcss-dark: initial; }\n`;\n\n/** Minimal `{{key}}` substitution — every key must exist in `strings`, values are inserted raw. */\nfunction render(template: string, strings: Record<string, string>): string {\n return template.replace(/\\{\\{(\\w+)\\}\\}/g, (match, key: string) => strings[key] ?? match);\n}\n\nfunction syncPreviewPopupChrome(): void {\n if (!previewPopup || previewPopup.closed) return;\n const popupDocument = previewPopup.document;\n for (const { source, clone } of previewPopupStylePairs) clone.textContent = source.textContent;\n for (const attribute of [\n \"lang\",\n \"dir\",\n \"data-pantoken-instance\",\n \"data-pantoken-theme\",\n \"data-pantoken-scheme\",\n \"data-pantoken-color\",\n ]) {\n const value = document.documentElement.getAttribute(attribute);\n if (value === null) popupDocument.documentElement.removeAttribute(attribute);\n else popupDocument.documentElement.setAttribute(attribute, value);\n }\n popupDocument.documentElement.style.colorScheme = document.documentElement.style.colorScheme;\n}\n\nfunction createShellIcon(icon: Parameters<typeof createElement>[0]): SVGSVGElement {\n const svg = createElement(icon, { width: 16, height: 16, \"stroke-width\": 2 });\n svg.classList.add(\"canvas-rce-shell__icon\");\n return svg;\n}\n\nfunction syncBrowserMetadata(): void {\n const themeColorMeta =\n document.querySelector<HTMLMetaElement>('meta[name=\"theme-color\"]') ??\n document.createElement(\"meta\");\n themeColorMeta.name = \"theme-color\";\n const backgroundColor = getComputedStyle(document.documentElement).backgroundColor;\n if (backgroundColor) themeColorMeta.content = backgroundColor;\n if (!themeColorMeta.parentElement) document.head.append(themeColorMeta);\n\n const favicon = document.querySelector<HTMLLinkElement>(\n 'link[rel=\"icon\"][data-pantoken-themed-icon]',\n );\n if (!favicon) return;\n const accentColor = getComputedStyle(document.documentElement)\n .getPropertyValue(\"--instui-primitive-color-navy-navy100\")\n .trim();\n if (!accentColor) return;\n const mark = PANTOKEN_ICON.replace(\n \"<svg \",\n '<svg x=\"32\" y=\"32\" width=\"64\" height=\"64\" ',\n ).replaceAll(/fill=\"[^\"]+\"/gu, 'fill=\"#fff\"');\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 128 128\"><rect width=\"128\" height=\"128\" rx=\"24\" fill=\"${accentColor}\"/>${mark}</svg>`;\n favicon.href = `data:image/svg+xml,${encodeURIComponent(svg)}`;\n}\n\nfunction setupStandaloneAppShell(): void {\n if (window.self !== window.top || document.getElementById(\"canvas-rce-shell\")) return;\n\n document.body.classList.add(\"canvas-rce-shell-enabled\");\n document.title =\n document.querySelector<HTMLMetaElement>('meta[name=\"application-name\"]')?.content ||\n \"Canvas Theme Editor\";\n\n const shell = document.createElement(\"header\");\n shell.id = \"canvas-rce-shell\";\n shell.className = \"canvas-rce-shell\";\n\n const brand = document.createElement(\"div\");\n brand.className = \"canvas-rce-shell__brand\";\n\n const brandMark = document.createElement(\"a\");\n brandMark.className = \"instui-button -color-primary -shape-square canvas-rce-shell__brand-mark\";\n brandMark.href = \"https://pantoken.app/\";\n brandMark.target = \"_blank\";\n brandMark.rel = \"noopener noreferrer\";\n brandMark.setAttribute(\"aria-label\", \"Pantoken (opens pantoken.app in a new tab)\");\n brandMark.innerHTML = PANTOKEN_ICON;\n\n const brandText = document.createElement(\"div\");\n brandText.className = \"canvas-rce-shell__brand-text instui-hidden-max-sm\";\n const eyebrow = document.createElement(\"span\");\n eyebrow.className = \"canvas-rce-shell__eyebrow\";\n eyebrow.textContent = \"Pantoken\";\n const title = document.createElement(\"strong\");\n title.textContent = \"Canvas Theme Editor\";\n brandText.append(eyebrow, title);\n brand.append(brandMark, brandText);\n\n const actions = document.createElement(\"div\");\n actions.className = \"canvas-rce-shell__actions\";\n\n const localeMenu = document.createElement(\"details\");\n localeMenu.className = \"canvas-rce-shell__menu\";\n const localeSummary = document.createElement(\"summary\");\n localeSummary.className = \"canvas-rce-shell__summary instui-button -color-tertiary\";\n localeSummary.append(createShellIcon(Languages));\n const localeLabel = document.createElement(\"span\");\n localeLabel.className = \"canvas-rce-shell__label instui-hidden-max-md\";\n const localeLocale = (\n document.documentElement.lang || navigator.languages?.[0] || navigator.language || \"en-US\"\n ).trim();\n let localeText = \"English\";\n try {\n // Names itself, in its own script (matching the picker's own option labels below).\n localeText = new Intl.DisplayNames([localeLocale], { type: \"language\" }).of(localeLocale) ?? localeText;\n } catch {\n localeText = localeLocale.split(\"-\")[0].toUpperCase();\n }\n localeLabel.textContent = localeText;\n localeSummary.setAttribute(\"aria-label\", localeText);\n localeSummary.append(localeLabel, createShellIcon(ChevronDown));\n\n const localePanel = document.createElement(\"div\");\n localePanel.className = \"canvas-rce-shell__panel\";\n const localePanelScroll = document.createElement(\"div\");\n localePanelScroll.className = \"canvas-rce-shell__panel-scroll\";\n const availableLocales = [\"en\", ...Object.keys(LOCALE_STRINGS).sort()];\n const localeDisplayNames = new Intl.DisplayNames(availableLocales, { type: \"language\" });\n for (const locale of availableLocales) {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.className = \"canvas-rce-shell__option\";\n option.dataset.locale = locale;\n const optionLabel = document.createElement(\"span\");\n // Each locale names itself, in its own script — matching the summary's own display name.\n optionLabel.textContent =\n new Intl.DisplayNames([locale], { type: \"language\" }).of(locale) ??\n localeDisplayNames.of(locale) ??\n locale;\n const optionMark = document.createElement(\"span\");\n optionMark.className = \"canvas-rce-shell__mark\";\n optionMark.append(createShellIcon(Check));\n option.append(optionLabel, optionMark);\n if (document.documentElement.lang === locale || (!document.documentElement.lang && locale === \"en\")) {\n option.classList.add(\"is-selected\");\n }\n option.addEventListener(\"click\", () => {\n if (locale === document.documentElement.lang) {\n localeMenu.removeAttribute(\"open\");\n return;\n }\n try {\n localStorage.setItem(LOCALE_STORAGE_KEY, locale);\n } catch {\n // Private mode / storage disabled — the reload below still applies it for this load.\n }\n // The app's own strings are only ever resolved once, at the initial `render()` call — a\n // reload re-runs that with the newly persisted locale instead of trying to re-render (and\n // re-initialize TinyMCE, the popup, etc.) in place.\n window.location.reload();\n });\n localePanelScroll.append(option);\n }\n localePanel.append(localePanelScroll);\n localeMenu.append(localeSummary, localePanel);\n\n const themeMenu = document.createElement(\"details\");\n themeMenu.className = \"canvas-rce-shell__menu\";\n const themeSummary = document.createElement(\"summary\");\n themeSummary.className = \"canvas-rce-shell__summary instui-button -color-tertiary\";\n themeSummary.append(createShellIcon(Palette));\n const themeLabel = document.createElement(\"span\");\n themeLabel.className = \"canvas-rce-shell__label instui-hidden-max-md\";\n themeLabel.textContent = activeStrings.themeLabel;\n themeSummary.setAttribute(\"aria-label\", activeStrings.themeLabel);\n themeSummary.append(themeLabel, createShellIcon(ChevronDown));\n\n const themePanel = document.createElement(\"div\");\n themePanel.className = \"canvas-rce-shell__panel\";\n const themePanelScroll = document.createElement(\"div\");\n themePanelScroll.className = \"canvas-rce-shell__panel-scroll\";\n\n const appearanceGroup = document.createElement(\"div\");\n appearanceGroup.className = \"canvas-rce-shell__group\";\n const appearanceTitle = document.createElement(\"span\");\n appearanceTitle.className = \"canvas-rce-shell__group-title\";\n appearanceTitle.textContent = activeStrings.appearanceLabel;\n appearanceGroup.append(appearanceTitle);\n // Shared by the click handler below and the initial application at the end of this function.\n const applyAppearance = (appearance: \"light\" | \"dark\"): void => {\n document.body.dataset.appearanceMode = appearance;\n document.documentElement.style.colorScheme = appearance;\n document.documentElement.dataset.pantokenScheme = appearance;\n if (typeof applyChromeTheme === \"function\") {\n applyChromeTheme(selectedTheme ?? \"rebrand\", appearance);\n }\n for (const candidate of themePanel.querySelectorAll<HTMLButtonElement>(\"[data-appearance]\")) {\n candidate.classList.toggle(\"is-selected\", candidate.dataset.appearance === appearance);\n }\n };\n for (const [appearance, label, icon] of [\n [\"light\", activeStrings.appearanceLightLabel, SunMedium],\n [\"dark\", activeStrings.appearanceDarkLabel, MoonStar],\n ] as const) {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.className = \"canvas-rce-shell__option\";\n option.dataset.appearance = appearance;\n const text = document.createElement(\"span\");\n text.textContent = label;\n const mark = document.createElement(\"span\");\n mark.className = \"canvas-rce-shell__mark\";\n mark.append(createShellIcon(icon));\n option.append(text, mark);\n if (appearance === \"light\") option.classList.add(\"is-selected\");\n option.addEventListener(\"click\", () => {\n applyAppearance(appearance);\n // The editor content and the live preview both default to following the app's own\n // appearance — only guide mode (via `applyParentTheme`) did this before; tools mode's own\n // shell toggle left both stuck on their prior scheme until a tray/preview control changed.\n applyEditorTheme(activeEditor);\n syncPreviewSchemeToAppearance(appearance);\n void refreshAll();\n themeMenu.removeAttribute(\"open\");\n });\n appearanceGroup.append(option);\n }\n\n const colorGroup = document.createElement(\"div\");\n colorGroup.className = \"canvas-rce-shell__group\";\n const colorTitle = document.createElement(\"span\");\n colorTitle.className = \"canvas-rce-shell__group-title\";\n colorTitle.textContent = activeStrings.colorLabel;\n colorGroup.append(colorTitle);\n for (const color of [...COLOR_KEYS, CUSTOM_COLOR_KEY]) {\n const option = document.createElement(\"button\");\n option.type = \"button\";\n option.className = \"canvas-rce-shell__option\";\n option.dataset.color = color;\n const swatch = document.createElement(\"span\");\n swatch.className = \"theme-picker__swatch\";\n swatch.dataset.swatch = color;\n const text = document.createElement(\"span\");\n const colorKey = `color${color.charAt(0).toUpperCase()}${color.slice(1)}Label`;\n text.textContent = activeStrings[colorKey] ?? color.charAt(0).toUpperCase() + color.slice(1);\n const mark = document.createElement(\"span\");\n mark.className = \"canvas-rce-shell__mark\";\n mark.append(createShellIcon(Check));\n option.append(swatch, text, mark);\n // \"navy\" is the app's actual default (see `selectedColor`/`applyChromeColor(\"navy\")` below) —\n // without this the picker showed no color as selected until the user picked one explicitly.\n if (color === \"navy\") option.classList.add(\"is-selected\");\n option.addEventListener(\"click\", () => {\n applyChromeColor(color);\n // Custom stays open so the hex field below is reachable.\n if (color !== CUSTOM_COLOR_KEY) themeMenu.removeAttribute(\"open\");\n });\n colorGroup.append(option);\n }\n\n const customRow = document.createElement(\"div\");\n customRow.className = \"theme-picker__custom canvas-rce-shell__custom\";\n customRow.hidden = true;\n const customLabel = document.createElement(\"label\");\n customLabel.htmlFor = \"canvas-rce-shell-custom-hex\";\n customLabel.textContent = activeStrings.customColorInputLabel;\n const customInputs = document.createElement(\"div\");\n customInputs.className = \"theme-picker__custom-inputs\";\n const customPicker = document.createElement(\"input\");\n customPicker.type = \"color\";\n customPicker.setAttribute(\"aria-label\", activeStrings.customColorInputLabel);\n const customHex = document.createElement(\"input\");\n customHex.type = \"text\";\n customHex.id = \"canvas-rce-shell-custom-hex\";\n customHex.className = \"instui-text-input -size-sm\";\n customHex.maxLength = 7;\n customHex.spellcheck = false;\n customHex.autocomplete = \"off\";\n customInputs.append(customPicker, customHex);\n customRow.append(customLabel, customInputs);\n colorGroup.append(customRow);\n\n const onShellCustomInput = (value: string): void => {\n const valid = isHexColor(value);\n customHex.classList.toggle(\"-invalid\", !valid);\n customHex.toggleAttribute(\"aria-invalid\", !valid);\n if (valid) applyChromeColor(CUSTOM_COLOR_KEY, value);\n };\n customPicker.addEventListener(\"input\", () => onShellCustomInput(customPicker.value));\n customHex.addEventListener(\"input\", () => onShellCustomInput(customHex.value));\n\n syncShellCustomColor = () => {\n const color = document.documentElement.dataset.pantokenColor;\n for (const candidate of themePanel.querySelectorAll<HTMLButtonElement>(\"[data-color]\")) {\n candidate.classList.toggle(\"is-selected\", candidate.dataset.color === color);\n }\n customRow.hidden = color !== CUSTOM_COLOR_KEY;\n customPicker.value = chromeCustomColor;\n if (!isHexColor(customHex.value) || parseHexColor(customHex.value) !== chromeCustomColor) {\n customHex.value = chromeCustomColor;\n }\n syncCustomSwatch(colorGroup.querySelector('[data-swatch=\"custom\"]')!, chromeCustomColor);\n };\n syncShellCustomColor();\n\n themePanelScroll.append(appearanceGroup, colorGroup);\n themePanel.append(themePanelScroll);\n themeMenu.append(themeSummary, themePanel);\n actions.append(localeMenu, themeMenu);\n shell.append(brand, actions);\n document.body.prepend(shell);\n\n // Native <details> doesn't close on its own for either of these — without this a click\n // elsewhere on the page, or Escape, leaves the panel open.\n const shellMenus = [localeMenu, themeMenu];\n document.addEventListener(\"click\", (event) => {\n for (const menu of shellMenus) {\n if (menu.open && !menu.contains(event.target as Node)) menu.removeAttribute(\"open\");\n }\n });\n document.addEventListener(\"keydown\", (event) => {\n if (event.key !== \"Escape\") return;\n for (const menu of shellMenus) {\n if (!menu.open) continue;\n menu.removeAttribute(\"open\");\n menu.querySelector<HTMLElement>(\"summary\")?.focus();\n }\n });\n\n applyAppearance(\"light\");\n}\n\n/** Persists the shell's own locale choice — never the authored content's own `lang`/`dir`. */\nconst LOCALE_STORAGE_KEY = \"pantoken-canvas-theme-editor-locale\";\nconst SESSION_DRAFT_KEY = \"pantoken-canvas-theme-editor-session-draft\";\n\n/** Reads the docs locale when embedded, otherwise the standalone shell's saved choice. */\nfunction loadLocale(): string {\n const embedded = window.self !== window.top;\n const queryLocale = embedded ? new URLSearchParams(window.location.search).get(\"locale\") : null;\n let storedLocale: string | null = null;\n try {\n if (!embedded) storedLocale = localStorage.getItem(LOCALE_STORAGE_KEY);\n } catch {\n // Storage can be disabled; the scaffold's initial HTML locale is still available.\n }\n return selectEditorLocale(\n embedded,\n queryLocale,\n storedLocale,\n document.documentElement.lang,\n LOCALE_DIRS,\n );\n}\n\nconst activeLocale = loadLocale();\nconst tinymceStrings = TINYMCE_LOCALE_STRINGS[activeLocale] ?? {};\nconst tinymceLanguages: Record<string, [string, () => Promise<unknown>]> = {\n ar: [\"ar\", () => import(\"tinymce-i18n/langs8/ar.js\")],\n hy: [\"hy\", () => import(\"tinymce-i18n/langs8/hy.js\")],\n ca: [\"ca\", () => import(\"tinymce-i18n/langs8/ca.js\")],\n da: [\"da\", () => import(\"tinymce-i18n/langs8/da.js\")],\n nl: [\"nl\", () => import(\"tinymce-i18n/langs8/nl.js\")],\n fi: [\"fi\", () => import(\"tinymce-i18n/langs8/fi.js\")],\n fr: [\"fr-FR\", () => import(\"tinymce-i18n/langs8/fr-FR.js\")],\n \"fr-CA\": [\"fr-FR\", () => import(\"tinymce-i18n/langs8/fr-FR.js\")],\n de: [\"de\", () => import(\"tinymce-i18n/langs8/de.js\")],\n el: [\"el\", () => import(\"tinymce-i18n/langs8/el.js\")],\n he: [\"he-IL\", () => import(\"tinymce-i18n/langs8/he-IL.js\")],\n hu: [\"hu-HU\", () => import(\"tinymce-i18n/langs8/hu-HU.js\")],\n is: [\"is-IS\", () => import(\"tinymce-i18n/langs8/is-IS.js\")],\n id: [\"id\", () => import(\"tinymce-i18n/langs8/id.js\")],\n it: [\"it\", () => import(\"tinymce-i18n/langs8/it.js\")],\n ja: [\"ja\", () => import(\"tinymce-i18n/langs8/ja.js\")],\n ko: [\"ko-KR\", () => import(\"tinymce-i18n/langs8/ko-KR.js\")],\n nb: [\"nb-NO\", () => import(\"tinymce-i18n/langs8/nb-NO.js\")],\n fa: [\"fa\", () => import(\"tinymce-i18n/langs8/fa.js\")],\n pl: [\"pl\", () => import(\"tinymce-i18n/langs8/pl.js\")],\n pt: [\"pt-PT\", () => import(\"tinymce-i18n/langs8/pt-PT.js\")],\n \"pt-BR\": [\"pt-BR\", () => import(\"tinymce-i18n/langs8/pt-BR.js\")],\n ru: [\"ru\", () => import(\"tinymce-i18n/langs8/ru.js\")],\n \"zh-Hans\": [\"zh-CN\", () => import(\"tinymce-i18n/langs8/zh-CN.js\")],\n sl: [\"sl-SI\", () => import(\"tinymce-i18n/langs8/sl-SI.js\")],\n es: [\"es\", () => import(\"tinymce-i18n/langs8/es.js\")],\n sv: [\"sv-SE\", () => import(\"tinymce-i18n/langs8/sv-SE.js\")],\n th: [\"th-TH\", () => import(\"tinymce-i18n/langs8/th-TH.js\")],\n \"zh-Hant\": [\"zh-TW\", () => import(\"tinymce-i18n/langs8/zh-TW.js\")],\n tr: [\"tr\", () => import(\"tinymce-i18n/langs8/tr.js\")],\n uk: [\"uk\", () => import(\"tinymce-i18n/langs8/uk.js\")],\n vi: [\"vi\", () => import(\"tinymce-i18n/langs8/vi.js\")],\n cy: [\"cy\", () => import(\"tinymce-i18n/langs8/cy.js\")],\n};\n// A missing key/locale in `LOCALE_STRINGS` falls back to English — see generate.ts's own comment.\nconst activeStrings: Record<string, string> = { ...STRINGS, ...LOCALE_STRINGS[activeLocale] };\ndocument.documentElement.lang = activeLocale;\ndocument.documentElement.dir = LOCALE_DIRS[activeLocale] ?? \"ltr\";\n\ndocument.querySelector<HTMLDivElement>(\"#app\")!.innerHTML = render(appHtml, activeStrings);\nconst interactionsScript = document.createElement(\"script\");\ninteractionsScript.src = interactionsIifeUrl;\ndocument.head.append(interactionsScript);\n\n// Shared CSS assets array — tracks stylesheets for dynamic injection and generated theme CSS.\nconst currentAssets: CdnFile[] = [];\nconst onMissingAsset = () => void refreshAll();\nlet availableIcons: TaggedIcon[] = [];\nconst inlineAssetContents = new Map<string, string>();\nlet refreshGeneration = 0;\n// Covers `plugin-logos` (a real dependency for any scaffolded project) and the icon packages\n// `build-canvas-rce.ts` vendors into `node_modules` purely so this docs-hosted preview's \"None\n// (inline)\" provider doesn't have to hit a CDN for `::name` autocomplete/picker glyphs.\nconst localAssetUrls = import.meta.glob<string>(\n \"/node_modules/@pantoken/plugin-{logos,simple-icons,lucide-lab,custom-icons}/dist/**/*.css\",\n { eager: true, import: \"default\", query: \"?url\" },\n);\n\nfunction buildSelectedAssetUrl(file: CdnFile): string {\n if (providerSelect.value !== LOCAL_PROVIDER_ID) {\n return buildFileUrl(file, providerSelect.value);\n }\n const localUrl = localAssetUrls[`/node_modules/${file.package}/${file.path}`];\n return localUrl ?? buildFileUrl(file);\n}\n\nconst editorThemeCss: Record<\n ThemeVariant,\n { skin: string; content: string }\n> = {\n rebrand: { skin: nextGenSkinCss, content: nextGenContentCss },\n canvas: { skin: canvasSkinCss, content: canvasContentCss },\n canvasHighContrast: {\n skin: canvasHighContrastSkinCss,\n content: canvasHighContrastContentCss,\n },\n};\n\nconst editorSkinStyle = document.createElement(\"style\");\ndocument.head.append(editorSkinStyle);\nlet editorContentStyle: HTMLStyleElement | undefined;\n\nfunction isThemeColor(color: unknown): color is PantokenColorNamespace {\n return color === CUSTOM_COLOR_KEY || isShippedColor(color);\n}\n\n/** The docs host only ever offers the 13 shipped scales, so the chrome never takes `custom`. */\nfunction isShippedColor(color: unknown): color is (typeof COLOR_KEYS)[number] {\n return typeof color === \"string\" && (COLOR_KEYS as readonly string[]).includes(color);\n}\n\n/** Applies the selected static skin to TinyMCE's chrome and iframe document. */\nfunction applyEditorTheme(editor: Editor | undefined): void {\n const css = editorThemeCss[selectedTheme];\n editorSkinStyle.textContent = css.skin;\n\n if (!editor) return;\n const editorDocument = editor.getDoc();\n if (!editorContentStyle || editorContentStyle.ownerDocument !== editorDocument) {\n editorContentStyle?.remove();\n editorContentStyle = editorDocument.createElement(\"style\");\n editorDocument.head.append(editorContentStyle);\n }\n editorContentStyle.textContent = [css.content, PREVIEW_SCHEME_FORCE_CSS].join(\"\\n\");\n // The content CSS's dark rules key off this attribute on the iframe's own root, which doesn't\n // inherit it from the host document.\n editorDocument.documentElement.dataset.pantokenScheme =\n document.documentElement.dataset.pantokenScheme;\n}\n\n/** Temporarily overrides the editing iframe's scheme so the a11y checker can test contrast under\n * both light and dark; the next `applyEditorTheme` call restores the chrome-mirrored value. */\nfunction forceEditorColorScheme(scheme: A11yColorScheme): void {\n if (!activeEditor) return;\n activeEditor.getDoc().documentElement.dataset.pantokenScheme = scheme;\n}\n\nfunction getEditorColorScheme(): A11yColorScheme {\n return activeEditor?.getDoc().documentElement.dataset.pantokenScheme === \"dark\"\n ? \"dark\"\n : \"light\";\n}\n\n/** Only \"rebrand\" ships a dark scheme (`syncDarkModeAvailability` hides/disables the checkbox for\n * the others), so the a11y checker only ever tests both schemes for it. */\nconst a11yRuntimeConfig: A11yCheckConfig = {\n setColorScheme: forceEditorColorScheme,\n getColorScheme: getEditorColorScheme,\n get colorSchemes(): readonly A11yColorScheme[] {\n return selectedTheme === \"rebrand\" && includeDarkModeCheckbox.checked\n ? [\"light\", \"dark\"]\n : [\"light\"];\n },\n};\n\n/** Re-themes the chrome's token variant + light/dark scheme — the tray and shell are regular app\n * chrome and inherit this too; only the preview `srcdoc` has its own independent theme/scheme. */\nfunction applyChromeTheme(theme: ThemeVariant, mode: \"light\" | \"dark\"): void {\n const tokensCss =\n theme === \"canvas\"\n ? canvasTokensCss\n : theme === \"canvasHighContrast\"\n ? canvasHighContrastTokensCss\n : adaptiveTokensCss;\n // Without `PREVIEW_SCHEME_FORCE_CSS`, the chrome's own buttons/text stay pinned to the OS\n // `prefers-color-scheme` (see that constant's comment) even though `colorScheme` below is set\n // explicitly — only the native-CSS background rules that key off `color-scheme` directly would\n // follow the toggle, leaving everything else stuck on the OS default.\n chromeThemeStyle.textContent = [tokensCss, PREVIEW_SCHEME_FORCE_CSS].join(\"\\n\");\n // Pins `light-dark()` resolution to the docs page's explicit mode instead of the OS preference —\n // single-scheme themes (canvas/canvasHighContrast) only ever render light.\n const scheme = theme === \"rebrand\" ? mode : \"light\";\n document.documentElement.dataset.pantokenTheme = theme;\n document.documentElement.dataset.pantokenScheme = scheme;\n document.documentElement.style.colorScheme = scheme;\n syncBrowserMetadata();\n syncPreviewPopupChrome();\n}\n\n/** Re-colors the chrome only — the tray's `#theme-tray` reset scope keeps it (and the preview,\n * which reads `selectedColor` directly) immune to this. */\nfunction applyChromeColor(color: PantokenColorNamespace, customHex?: string): void {\n if (customHex !== undefined) chromeCustomColor = parseHexColor(customHex);\n chromeCustomColorStyle.textContent =\n color === CUSTOM_COLOR_KEY ? customColorCss(chromeCustomColor) : \"\";\n document.documentElement.dataset.pantokenColor = color;\n syncShellCustomColor?.();\n syncBrowserMetadata();\n syncPreviewPopupChrome();\n}\n\n/** A host color message: a shipped scale, or `custom` with a valid hex. */\nfunction applyHostColor(color: unknown, customHex: unknown): void {\n if (isShippedColor(color)) applyChromeColor(color);\n else if (color === CUSTOM_COLOR_KEY && isHexColor(customHex)) applyChromeColor(color, customHex);\n}\n\nfunction applyParentTheme(theme: ThemeVariant, mode: \"light\" | \"dark\"): void {\n selectedTheme = theme;\n // The docs page's own light/dark mode re-themes the chrome only — \"include dark mode\" is an\n // independent authoring choice about what ships with the exported theme, not a mirror of it.\n document.documentElement.dataset.pantokenScheme = mode;\n const themeButton = [...themeButtons].find((button) => button.dataset.theme === theme);\n if (themeButton) selectToggleButton(themeButtons, themeButton);\n syncDarkModeAvailability();\n syncPreviewSchemeToAppearance(mode);\n applyEditorTheme(activeEditor);\n void refreshAll();\n syncPreviewPopupChrome();\n}\n\n// This app is its own pantoken instance. Claiming the root stops an embedding page's scope from\n// being mistaken for ours, and stops our own preview/tray scopes from being re-themed by it.\ndocument.documentElement.dataset.pantokenInstance = EDITOR_INSTANCE;\napplyChromeTheme(\"rebrand\", \"light\");\napplyChromeColor(\"navy\");\n\nwindow.addEventListener(\"message\", (event) => {\n if (event.source !== window.parent || event.origin !== window.location.origin) return;\n const data = event.data as {\n type?: unknown;\n theme?: unknown;\n mode?: unknown;\n color?: unknown;\n customColor?: unknown;\n instanceId?: unknown;\n };\n // A message tagged for a different instance belongs to another pantoken on the host page.\n if (typeof data?.instanceId === \"string\" && data.instanceId !== HOST_INSTANCE) return;\n if (data?.type === \"pantoken-demo-color\") {\n applyHostColor(data.color, data.customColor);\n return;\n }\n if (data?.type !== \"pantoken-demo-theme\") return;\n if (\n (data.theme !== \"rebrand\" &&\n data.theme !== \"canvas\" &&\n data.theme !== \"canvasHighContrast\") ||\n (data.mode !== \"light\" && data.mode !== \"dark\")\n ) {\n return;\n }\n applyChromeTheme(data.theme, data.mode);\n applyParentTheme(data.theme, data.mode);\n applyHostColor(data.color, data.customColor);\n});\n\nwindow.parent.postMessage({ type: \"pantoken-demo-request-theme\" }, window.location.origin);\n\n/** Creates a CodeMirror editor for the given language, wired to call `onChange` on user edits. */\nfunction createEditor(\n container: HTMLElement,\n lang: \"html\" | \"css\" | \"javascript\",\n value: string,\n onChange: () => void,\n): EditorView {\n const langExtension = lang === \"css\" ? css() : lang === \"javascript\" ? javascript() : html();\n const extensions: any[] = [\n basicSetup,\n langExtension,\n EditorView.lineWrapping,\n EditorView.updateListener.of((update: any): void => {\n if (update.docChanged) onChange();\n }),\n ];\n \n // Add pantoken linter + autocomplete for HTML editor only.\n if (lang === \"html\") {\n extensions.push(\n pantokenHtmlLinter({ model: [...listComponents(), ...listUtilities()] as any }),\n pantokenHtmlCompletion({ model: [...listComponents(), ...listUtilities()] as any })\n );\n }\n \n return new EditorView({\n doc: value,\n extensions,\n parent: container,\n });\n}\n\n// Register one pantoken plugin that composes components, icons, logos, layouts, and templates\n// behind a single toolbar menu. tinymce.init() waits until the icon catalog is available.\nconst registerPantokenPlugin = (async () => {\n const componentModel = [...listComponents(), ...listUtilities()];\n const allIcons = await loadAllIcons();\n availableIcons = allIcons;\n\n tinymce.PluginManager.add(\n \"pantoken\",\n createPantokenPlugin({\n strings: tinymceStrings,\n components: {\n model: componentModel as any,\n currentAssets,\n onMissingAsset,\n },\n icons: {\n icons: allIcons,\n currentAssets,\n onMissingAsset,\n buildAssetUrl: buildSelectedAssetUrl,\n },\n logos: {\n logos,\n products,\n currentAssets,\n onMissingAsset,\n buildAssetUrl: buildSelectedAssetUrl,\n },\n layouts: {\n locale: activeLocale,\n resolveImage: (placeholder) => {\n const image = buildPlaceholdImage({\n width: placeholder.width,\n height: placeholder.height,\n backgroundColor: \"#eeeeee\",\n textColor: \"#31343c\",\n text: placeholder.text,\n });\n return {\n src: image.url,\n alt: placeholder.altText ?? \"\",\n width: image.width,\n height: image.height,\n };\n },\n onInsert: () => {\n applyThemeColor(selectedColor, false);\n void refreshAll();\n },\n },\n }),\n );\n})();\n\ntinymce.PluginManager.add(\n PLACEHOLD_PLUGIN_NAME,\n createPlaceholdPlugin({ strings: tinymceStrings, onInsert: () => void refreshAll() }),\n);\ntinymce.PluginManager.add(CONTENT_CLASSES_PLUGIN_NAME, createContentClassesPlugin());\ntinymce.PluginManager.add(\n A11Y_PLUGIN_NAME,\n createA11yPlugin({ strings: tinymceStrings, display: \"footer\", config: a11yRuntimeConfig }),\n);\ntinymce.PluginManager.add(SUP_SUB_PLUGIN_NAME, createSupSubPlugin());\n\n// Toggles between TinyMCE's WYSIWYG view and a syntax-highlighted CodeMirror source view, reusing\n// the shared renderer's implementation (live preview + pantoken lint/autocomplete) instead of a\n// second, drifted reimplementation.\ntinymce.PluginManager.add(\n SOURCE_TOGGLE_PLUGIN_NAME,\n createSourceTogglePlugin({\n strings: tinymceStrings,\n height: EDITOR_HEIGHT_PX,\n display: \"footer\",\n extensions: [\n pantokenHtmlLinter({ model: [...listComponents(), ...listUtilities()] as any, strings: tinymceStrings }),\n pantokenHtmlCompletion({ model: [...listComponents(), ...listUtilities()] as any }),\n ],\n onChange: () => {\n syncThemeColorFromEditor();\n if (syncIconAssetsFromEditor()) void refreshAll();\n debouncedUpdatePreview();\n },\n onToggle: () => void refreshAll(),\n }),\n);\n// Fullscreen, find-and-replace, and visual blocks ship as toolbar-only controls upstream — these\n// footer-only wrappers surface all three from the statusbar instead, alongside word count and the\n// a11y checker.\ntinymce.PluginManager.add(FULLSCREEN_FOOTER_PLUGIN_NAME, createFullscreenFooterPlugin());\ntinymce.PluginManager.add(SEARCHREPLACE_FOOTER_PLUGIN_NAME, createSearchReplaceFooterPlugin());\ntinymce.PluginManager.add(VISUALBLOCKS_FOOTER_PLUGIN_NAME, createVisualBlocksFooterPlugin());\ntinymce.PluginManager.add(\n SAVE_PLUGIN_NAME,\n createSavePlugin<CanvasThemePreset>({\n strings: tinymceStrings,\n capture: captureCanvasThemePreset,\n restore: restoreCanvasThemePreset,\n reset: resetCanvasThemePreset,\n isValid: isCanvasThemePreset,\n }),\n);\n\nconst providerSelect = document.querySelector<HTMLSelectElement>(\"#cdn-provider\")!;\nconst localProviderOption = document.createElement(\"option\");\nlocalProviderOption.value = LOCAL_PROVIDER_ID;\nlocalProviderOption.textContent = activeStrings.localProviderLabel;\nproviderSelect.append(localProviderOption);\nfor (const [id, provider] of Object.entries(CDN_PROVIDERS)) {\n const option = document.createElement(\"option\");\n option.value = id;\n option.textContent = provider.label;\n providerSelect.append(option);\n}\nproviderSelect.value = import.meta.env.DEV ? LOCAL_PROVIDER_ID : DEFAULT_CDN_PROVIDER_ID;\nif (isLocalPreview) {\n providerSelect.value = LOCAL_PROVIDER_ID;\n}\nif (\n preferences.cdnProvider &&\n [...providerSelect.options].some((option) => option.value === preferences.cdnProvider)\n) {\n providerSelect.value = preferences.cdnProvider;\n}\n\nconst themeButtons = document.querySelectorAll<HTMLButtonElement>(\".theme-picker [data-theme]\");\nconst colorButtons = document.querySelectorAll<HTMLButtonElement>(\n \".theme-picker__color-item[data-color]\",\n);\nconst customColorRow = document.querySelector<HTMLElement>(\"#custom-color-row\")!;\nconst customColorPicker = document.querySelector<HTMLInputElement>(\"#custom-color-picker\")!;\nconst customColorHex = document.querySelector<HTMLInputElement>(\"#custom-color-hex\")!;\nconst customColorSwatch = document.querySelector<HTMLElement>(\n '.theme-picker__color-item [data-swatch=\"custom\"]',\n)!;\nconst previewWidthButtons = document.querySelectorAll<HTMLButtonElement>(\n \".preview-width-picker [data-preview-width]\",\n);\nconst previewWidthValue = document.querySelector<HTMLElement>(\"#preview-width-value\")!;\nconst includeDarkModeRow = document.getElementById(\"include-dark-mode-row\")!;\nconst includeDarkModeCheckbox = document.querySelector<HTMLInputElement>(\"#include-dark-mode\")!;\nconst previewSchemeToggle = document.querySelector<HTMLButtonElement>(\"#preview-scheme-toggle\")!;\nconst previewFrame = document.querySelector<HTMLIFrameElement>(\"#preview\")!;\nconst previewPane = document.querySelector<HTMLElement>(\".preview-pane\")!;\nconst downloadCss = document.querySelector<HTMLAnchorElement>(\"#download-theme-css\")!;\nconst downloadJs = document.querySelector<HTMLAnchorElement>(\"#download-theme-js\")!;\nconst downloadPackageButton = document.querySelector<HTMLButtonElement>(\"#download-package\")!;\nconst downloadHtmlButton = document.querySelector<HTMLButtonElement>(\"#download-html\")!;\nconst copyButton = document.querySelector<HTMLButtonElement>(\"#copy-html\")!;\n\nlet activeEditor: Editor | undefined;\nlet cssObjectUrl: string | undefined;\nlet jsObjectUrl: string | undefined;\n\n// Only the tray (below) and the preview it drives read these — the chrome has its own,\n// docs-driven theme/color tracked separately by `applyChromeTheme`/`applyChromeColor` above.\nlet selectedTheme: ThemeVariant = \"rebrand\";\nlet selectedColor: PantokenColorNamespace = \"navy\";\nlet selectedCustomColor = DEFAULT_CUSTOM_COLOR;\nsetupStandaloneAppShell();\n\ninterface CanvasThemePreset {\n readonly version: 1;\n readonly theme: ThemeVariant;\n readonly color: PantokenColorNamespace;\n readonly customColor?: string;\n readonly mode: ThemeMode;\n readonly cdnProvider: string;\n readonly customCss: string;\n readonly customJs: string;\n readonly editorHtml: string;\n}\n\nfunction selectThemeColor(color: PantokenColorNamespace): void {\n selectedColor = color;\n const colorButton = [...colorButtons].find((button) => button.dataset.color === color);\n if (colorButton) selectButton(colorButtons, colorButton);\n customColorRow.hidden = color !== CUSTOM_COLOR_KEY;\n}\n\n/** Stores a validated custom hex and reflects it in the tray. */\nfunction setCustomColor(hex: string): void {\n selectedCustomColor = parseHexColor(hex);\n customColorPicker.value = selectedCustomColor;\n const typed = customColorHex.value;\n if (!isHexColor(typed) || parseHexColor(typed) !== selectedCustomColor) {\n customColorHex.value = selectedCustomColor;\n }\n customColorHex.classList.remove(\"-invalid\");\n customColorHex.removeAttribute(\"aria-invalid\");\n syncCustomSwatch(customColorSwatch, selectedCustomColor);\n}\n\n/** The `custom` scale's rule, only while it's the selected color. */\nfunction selectedCustomColorCss(): string {\n return selectedColor === CUSTOM_COLOR_KEY\n ? customColorCss(selectedCustomColor, { selector: \"\" })\n : \"\";\n}\n\nconst debouncedCustomColorRefresh = debounce(() => void refreshAll(), 150);\n\nfunction onCustomColorInput(value: string): void {\n if (!isHexColor(value)) {\n customColorHex.classList.add(\"-invalid\");\n customColorHex.setAttribute(\"aria-invalid\", \"true\");\n return;\n }\n setCustomColor(value);\n if (selectedColor === CUSTOM_COLOR_KEY) debouncedCustomColorRefresh();\n}\n\ncustomColorPicker.addEventListener(\"input\", () => onCustomColorInput(customColorPicker.value));\ncustomColorHex.addEventListener(\"input\", () => onCustomColorInput(customColorHex.value));\nsetCustomColor(DEFAULT_CUSTOM_COLOR);\n\nfunction parseEditorHtml(html: string): HTMLTemplateElement {\n const template = document.createElement(\"template\");\n template.innerHTML = html;\n return template;\n}\n\nfunction findColorWrapper(content: DocumentFragment): HTMLElement | undefined {\n const wrapper = content.firstElementChild as HTMLElement | null;\n if (!wrapper?.hasAttribute(\"data-pantoken-color\")) return undefined;\n const hasContentOutsideWrapper = [...content.childNodes].some(\n (node) => node !== wrapper && (node.nodeType !== Node.TEXT_NODE || node.textContent?.trim()),\n );\n return hasContentOutsideWrapper ? undefined : wrapper;\n}\n\nfunction normalizeEditorHtml(html: string, fallbackColor: PantokenColorNamespace): string {\n const template = parseEditorHtml(html);\n let wrapper = findColorWrapper(template.content);\n if (!wrapper) {\n wrapper = document.createElement(\"div\");\n wrapper.dataset.pantokenColor = fallbackColor;\n wrapper.append(...template.content.childNodes);\n template.content.append(wrapper);\n } else if (!isThemeColor(wrapper.dataset.pantokenColor)) {\n wrapper.dataset.pantokenColor = fallbackColor;\n }\n return template.innerHTML;\n}\n\nfunction editorHtmlColor(html: string): PantokenColorNamespace | undefined {\n const wrapper = findColorWrapper(parseEditorHtml(html).content);\n const color = wrapper?.dataset.pantokenColor;\n return isThemeColor(color) ? color : undefined;\n}\n\nfunction setEditorHtmlColor(html: string, color: PantokenColorNamespace): string {\n const template = parseEditorHtml(normalizeEditorHtml(html, color));\n findColorWrapper(template.content)!.dataset.pantokenColor = color;\n return template.innerHTML;\n}\n\nfunction syncThemeColorFromEditor(): void {\n const color = editorHtmlColor(getEditorHtml());\n if (color) selectThemeColor(color);\n}\n\nfunction applyThemeColor(color: PantokenColorNamespace, refresh = true): void {\n selectThemeColor(color);\n if (activeEditor) {\n const sourceToggle = activeEditor.plugins[SOURCE_TOGGLE_PLUGIN_NAME] as\n | SourceTogglePluginApi\n | undefined;\n const nextHtml = setEditorHtmlColor(getEditorHtml(), color);\n if (sourceToggle?.isSourceMode()) {\n sourceToggle.replaceAll(nextHtml);\n } else {\n activeEditor.undoManager.transact(() => activeEditor!.setContent(nextHtml));\n }\n }\n if (refresh) void refreshAll();\n}\n\n/** Reads the author's current HTML from whichever view (WYSIWYG or raw source) is active. */\nfunction getEditorHtml(): string {\n const sourceToggle = activeEditor?.plugins[SOURCE_TOGGLE_PLUGIN_NAME] as\n | SourceTogglePluginApi\n | undefined;\n if (sourceToggle?.isSourceMode()) return sourceToggle.getContent();\n return activeEditor?.getContent() ?? \"\";\n}\n\nfunction getNormalizedEditorHtml(): string {\n return normalizeEditorHtml(getEditorHtml(), selectedColor);\n}\n\nfunction assetKey(asset: CdnFile): string {\n return `${asset.package}:${asset.path}`;\n}\n\nasync function inlineAssetCss(assets: readonly CdnFile[]): Promise<string[]> {\n await Promise.all(\n assets.map(async (asset) => {\n const key = assetKey(asset);\n if (inlineAssetContents.has(key)) return;\n const response = await fetch(buildFileUrl(asset));\n if (!response.ok) {\n throw new Error(`Unable to inline ${asset.package}/${asset.path}: ${response.status}`);\n }\n inlineAssetContents.set(key, await response.text());\n }),\n );\n return assets.map((asset) => inlineAssetContents.get(assetKey(asset))!);\n}\n\nfunction syncIconAssetsFromEditor(): boolean {\n const content = parseEditorHtml(getEditorHtml()).content;\n const iconAssetKeys = new Set(availableIcons.map((icon) => assetKey(getIconCdnFile(icon))));\n const logoAssetKeys = new Set(logos.map((logo) => assetKey(getLogoCdnFile(logo))));\n const nonSyncedAssets = currentAssets.filter(\n (asset) => !iconAssetKeys.has(assetKey(asset)) && !logoAssetKeys.has(assetKey(asset)),\n );\n const usedIconAssets = getUsedIconCdnFiles(content, availableIcons, currentAssets);\n const usedLogoAssets = getUsedLogoCdnFiles(content, logos);\n const nextAssets = [...nonSyncedAssets, ...usedIconAssets, ...usedLogoAssets];\n const changed = currentAssets.map(assetKey).join(\"\\n\") !== nextAssets.map(assetKey).join(\"\\n\");\n if (changed) currentAssets.splice(0, currentAssets.length, ...nextAssets);\n return changed;\n}\n\nfunction captureCanvasThemePreset(): CanvasThemePreset {\n return {\n version: 1,\n theme: selectedTheme,\n color: selectedColor,\n customColor: selectedCustomColor,\n mode: includeDarkModeCheckbox.checked ? \"adaptive\" : \"light\",\n cdnProvider: providerSelect.value,\n customCss: cssEditorView.state.doc.toString(),\n customJs: jsEditorView.state.doc.toString(),\n editorHtml: getNormalizedEditorHtml(),\n };\n}\n\nfunction isThemeVariant(value: unknown): value is ThemeVariant {\n return value === \"rebrand\" || value === \"canvas\" || value === \"canvasHighContrast\";\n}\n\nfunction isThemeMode(value: unknown): value is ThemeMode {\n return value === \"light\" || value === \"adaptive\";\n}\n\nfunction isCdnProvider(value: unknown): value is string {\n return (\n typeof value === \"string\" &&\n (Object.hasOwn(CDN_PROVIDERS, value) || value === LOCAL_PROVIDER_ID)\n );\n}\n\nfunction isCanvasThemePreset(value: unknown): value is CanvasThemePreset {\n if (typeof value !== \"object\" || value === null) return false;\n const preset = value as Partial<CanvasThemePreset>;\n return (\n preset.version === 1 &&\n isThemeVariant(preset.theme) &&\n isThemeColor(preset.color) &&\n (preset.customColor === undefined || isHexColor(preset.customColor)) &&\n isThemeMode(preset.mode) &&\n isCdnProvider(preset.cdnProvider) &&\n typeof preset.customCss === \"string\" &&\n typeof preset.customJs === \"string\" &&\n typeof preset.editorHtml === \"string\"\n );\n}\n\nfunction replaceEditorDocument(view: EditorView, content: string): void {\n view.dispatch({ changes: { from: 0, to: view.state.doc.length, insert: content } });\n}\n\nfunction resetCanvasThemePreset(): void {\n selectedTheme = \"rebrand\";\n const themeButton = [...themeButtons].find((button) => button.dataset.theme === selectedTheme);\n if (themeButton) selectToggleButton(themeButtons, themeButton);\n includeDarkModeCheckbox.checked = false;\n syncDarkModeAvailability();\n providerSelect.value = isLocalPreview ? LOCAL_PROVIDER_ID : DEFAULT_CDN_PROVIDER_ID;\n selectThemeColor(\"navy\");\n setCustomColor(DEFAULT_CUSTOM_COLOR);\n replaceEditorDocument(cssEditorView, \"\");\n replaceEditorDocument(jsEditorView, \"\");\n\n if (activeEditor) {\n const nextHtml = normalizeEditorHtml(\"\", \"navy\");\n activeEditor.resetContent(nextHtml);\n const sourceToggle = activeEditor.plugins[SOURCE_TOGGLE_PLUGIN_NAME] as\n | SourceTogglePluginApi\n | undefined;\n if (sourceToggle?.isSourceMode()) sourceToggle.replaceAll(nextHtml);\n }\n\n syncThemeColorFromEditor();\n applyEditorTheme(activeEditor);\n void refreshAll();\n}\n\nfunction restoreCanvasThemePreset(preset: CanvasThemePreset): void {\n if (!activeEditor || !isCanvasThemePreset(preset)) throw new TypeError(\"invalid-preset\");\n\n selectedTheme = preset.theme;\n const themeButton = [...themeButtons].find((button) => button.dataset.theme === preset.theme);\n if (themeButton) selectToggleButton(themeButtons, themeButton);\n includeDarkModeCheckbox.checked = preset.mode === \"adaptive\";\n syncDarkModeAvailability();\n providerSelect.value = preset.cdnProvider;\n selectThemeColor(preset.color);\n setCustomColor(preset.customColor ?? DEFAULT_CUSTOM_COLOR);\n replaceEditorDocument(cssEditorView, preset.customCss);\n replaceEditorDocument(jsEditorView, preset.customJs);\n\n const editorHtml = normalizeEditorHtml(preset.editorHtml, preset.color);\n activeEditor.resetContent(editorHtml);\n const sourceToggle = activeEditor.plugins[SOURCE_TOGGLE_PLUGIN_NAME] as\n | SourceTogglePluginApi\n | undefined;\n if (sourceToggle?.isSourceMode()) sourceToggle.replaceAll(editorHtml);\n syncThemeColorFromEditor();\n\n applyEditorTheme(activeEditor);\n void refreshAll();\n}\n\nfunction currentSelection(): { provider: string; theme: ThemeVariant; mode: ThemeMode } {\n return {\n provider: providerSelect.value,\n theme: selectedTheme,\n mode: includeDarkModeCheckbox.checked ? \"adaptive\" : \"light\",\n };\n}\n\n/** `css`/`js` are theme.css/theme.js's actual downloadable content and the live preview's styling\n * alike — both follow the visitor's/browser's own dark-mode preference, same as a real Canvas\n * page would. The preview's manual light/dark toggle overrides that via `PREVIEW_SCHEME_FORCE_CSS`\n * (see `updatePreview`), not by swapping this CSS. */\nasync function buildCurrentTheme(): Promise<{ css: string; js: string }> {\n const selection = currentSelection();\n const customCss = selectedCustomColorCss();\n if (selection.provider !== LOCAL_PROVIDER_ID) {\n const theme = buildTheme({\n ...selection,\n css: [...defaultThemeCssAssets(selection.theme, selection.mode), ...currentAssets],\n });\n return { ...theme, css: [theme.css, customCss].filter(Boolean).join(\"\\n\") };\n }\n\n const tokensCss =\n selection.theme === \"canvas\"\n ? canvasTokensCss\n : selection.theme === \"canvasHighContrast\"\n ? canvasHighContrastTokensCss\n : selection.mode === \"adaptive\"\n ? adaptiveTokensCss\n : rebrandLightTokensCss;\n const dynamicCss = await inlineAssetCss(currentAssets);\n return {\n css: [tokensCss, LOCAL_COMPONENT_CSS, ...dynamicCss, customCss].filter(Boolean).join(\"\\n\"),\n js: localInteractionsJs,\n };\n}\n\n/** Marks the clicked button as selected within its `role=\"group\"` (theme or color pickers). */\nfunction selectButton(buttons: NodeListOf<HTMLButtonElement>, target: HTMLButtonElement): void {\n for (const button of buttons) {\n const selected = button === target;\n button.classList.toggle(\"-selected\", selected);\n button.setAttribute(\"aria-checked\", String(selected));\n }\n}\n\n/** Marks the clicked instui-button `-toggle` as pressed within its `role=\"group\"` (preview width). */\nfunction selectToggleButton(buttons: NodeListOf<HTMLButtonElement>, target: HTMLButtonElement): void {\n for (const button of buttons) button.setAttribute(\"aria-pressed\", String(button === target));\n}\n\n/** Only \"rebrand\" ships a dark scheme — canvas/canvasHighContrast are single-scheme themes. */\nfunction syncDarkModeAvailability(): void {\n const supportsDarkMode = selectedTheme === \"rebrand\";\n includeDarkModeRow.hidden = !supportsDarkMode;\n includeDarkModeCheckbox.disabled = !supportsDarkMode;\n if (!supportsDarkMode) includeDarkModeCheckbox.checked = false;\n syncPreviewSchemeToggleAvailability();\n}\n\n/** The preview's own light/dark switch only makes sense once \"include dark mode\" is on. */\nfunction syncPreviewSchemeToggleAvailability(): void {\n previewSchemeToggle.disabled = !includeDarkModeCheckbox.checked;\n if (previewSchemeToggle.disabled) {\n previewDarkOverride = false;\n previewSchemeToggle.setAttribute(\"aria-pressed\", \"false\");\n previewSchemeToggle.classList.replace(\"-icon-sun\", \"-icon-moon\");\n }\n}\n\nthemeButtons.forEach((button) => {\n button.addEventListener(\"click\", () => {\n selectedTheme = button.dataset.theme as ThemeVariant;\n selectToggleButton(themeButtons, button);\n syncDarkModeAvailability();\n applyEditorTheme(activeEditor);\n void refreshAll();\n });\n});\n\ncolorButtons.forEach((button) => {\n button.addEventListener(\"click\", () => {\n const color = button.dataset.color;\n if (isThemeColor(color)) applyThemeColor(color);\n });\n});\n\n// Canvas's own responsive breakpoints for course content, reused as-is from pantoken's tokens —\n// \"large\" is unconstrained (the current default), matching the desktop RCE width.\ntype PreviewWidthPreset = \"large\" | \"medium\" | \"small\";\n\nconst PREVIEW_WIDTHS: Record<Exclude<PreviewWidthPreset, \"large\">, string> = {\n medium: \"var(--instui-breakpoints-md)\",\n small: \"var(--instui-breakpoints-xs)\",\n};\n\nfunction isPreviewWidthPreset(value: unknown): value is PreviewWidthPreset {\n return value === \"large\" || value === \"medium\" || value === \"small\";\n}\n\nfunction isCustomPreviewWidth(value: unknown): value is number {\n return typeof value === \"number\" && Number.isSafeInteger(value) && value > 0;\n}\n\nfunction parseCustomPreviewWidth(value: string): number | undefined {\n if (!/^[1-9]\\d*$/u.test(value)) return undefined;\n const width = Number(value);\n return isCustomPreviewWidth(width) ? width : undefined;\n}\n\nlet selectedPreviewWidth: PreviewWidthPreset | number =\n isPreviewWidthPreset(preferences.previewWidth) || isCustomPreviewWidth(preferences.previewWidth)\n ? preferences.previewWidth\n : \"large\";\n\nfunction clearPreviewWidthButtons(): void {\n for (const button of previewWidthButtons) button.setAttribute(\"aria-pressed\", \"false\");\n}\n\nfunction updatePreviewWidthValue(force = false): void {\n if (!force && document.activeElement === previewWidthValue) return;\n const width =\n typeof selectedPreviewWidth === \"number\"\n ? selectedPreviewWidth\n : Math.round(previewFrame.getBoundingClientRect().width);\n if (width <= 0) return;\n const value = String(width);\n previewWidthValue.textContent = value;\n previewWidthValue.setAttribute(\"value\", value);\n}\n\nconst previewWidthObserver = new ResizeObserver(() => updatePreviewWidthValue());\npreviewWidthObserver.observe(previewFrame);\n\n/**\n * In row layout, the *pane's* width is constrained so the editor pane's flex:1 can claim\n * whatever space a fixed-width (medium/small) preview gives up. In fullscreen the pane is\n * forced to fill the viewport (app.css's `!important` overlay), so there's no sibling to\n * reclaim space from — the constraint has to move to the iframe itself instead, centered\n * via app.css's `margin-inline: auto`.\n */\nfunction applyPreviewWidth(): void {\n const fullscreen = previewPane.classList.contains(\"-fullscreen\");\n previewPane.style.removeProperty(\"flex\");\n previewPane.style.removeProperty(\"width\");\n previewPane.style.removeProperty(\"min-width\");\n previewFrame.style.removeProperty(\"max-width\");\n if (selectedPreviewWidth === \"large\") {\n // In row layout, \"large\" only got a 50/50 flex share of `.content`'s width — on a narrow\n // window that's smaller than \"medium\"'s own fixed `--instui-breakpoints-md`, so the\n // \"desktop\" preview could render narrower than the \"tablet\" one. A `--instui-breakpoints-lg`\n // floor guarantees desktop is always at least as wide as the next size down.\n if (!fullscreen && document.querySelector(\".content\")?.getAttribute(\"data-layout\") === \"row\") {\n previewPane.style.minWidth = \"var(--instui-breakpoints-lg)\";\n }\n updatePreviewWidthValue();\n return;\n }\n const width =\n typeof selectedPreviewWidth === \"number\"\n ? `${selectedPreviewWidth}px`\n : PREVIEW_WIDTHS[selectedPreviewWidth];\n if (fullscreen) {\n previewFrame.style.maxWidth = width;\n } else {\n previewPane.style.flex = \"0 0 auto\";\n previewPane.style.width = width;\n }\n updatePreviewWidthValue();\n}\n\npreviewWidthButtons.forEach((button) => {\n button.addEventListener(\"click\", () => {\n selectToggleButton(previewWidthButtons, button);\n selectedPreviewWidth = button.dataset.previewWidth as PreviewWidthPreset;\n applyPreviewWidth();\n persistPreferences();\n });\n});\npreviewWidthValue.addEventListener(\"change\", (event) => {\n const width = parseCustomPreviewWidth(\n (event as CustomEvent<{ value: string }>).detail.value,\n );\n if (width === undefined) {\n updatePreviewWidthValue(true);\n return;\n }\n selectedPreviewWidth = width;\n clearPreviewWidthButtons();\n applyPreviewWidth();\n persistPreferences();\n});\nif (typeof selectedPreviewWidth === \"number\") {\n clearPreviewWidthButtons();\n applyPreviewWidth();\n} else if (selectedPreviewWidth !== \"large\") {\n const savedWidthButton = [...previewWidthButtons].find(\n (button) => button.dataset.previewWidth === selectedPreviewWidth,\n );\n if (savedWidthButton) {\n selectToggleButton(previewWidthButtons, savedWidthButton);\n applyPreviewWidth();\n }\n} else {\n applyPreviewWidth();\n}\n\nlet previewDarkOverride = false;\nsyncDarkModeAvailability();\n\n/** Makes the preview's own light/dark display follow an app-wide appearance change (its \"include\n * dark mode\" export checkbox stays untouched); the user can still flip it back independently\n * afterward via `previewSchemeToggle`. No-op while the current theme has no dark preview to show. */\nfunction syncPreviewSchemeToAppearance(mode: \"light\" | \"dark\"): void {\n if (!includeDarkModeCheckbox.checked) return;\n previewDarkOverride = mode === \"dark\";\n previewSchemeToggle.setAttribute(\"aria-pressed\", String(previewDarkOverride));\n previewSchemeToggle.classList.toggle(\"-icon-moon\", !previewDarkOverride);\n previewSchemeToggle.classList.toggle(\"-icon-sun\", previewDarkOverride);\n}\n\npreviewSchemeToggle.addEventListener(\"click\", () => {\n previewDarkOverride = !previewDarkOverride;\n previewSchemeToggle.setAttribute(\"aria-pressed\", String(previewDarkOverride));\n previewSchemeToggle.classList.toggle(\"-icon-moon\", !previewDarkOverride);\n previewSchemeToggle.classList.toggle(\"-icon-sun\", previewDarkOverride);\n void refreshAll();\n});\n\nconst previewFullscreenToggle = document.querySelector<HTMLButtonElement>(\n \"#preview-fullscreen-toggle\",\n)!;\nconst previewPopupToggle = document.querySelector<HTMLButtonElement>(\"#preview-popup-toggle\")!;\nconst previewFullscreenLabel = document.querySelector<HTMLElement>(\"#preview-fullscreen-label\")!;\nconst previewFullscreenTooltip = document.querySelector<HTMLElement>(\n \"#preview-fullscreen-toggle-tip\",\n)!;\nconst previewPopupReturnLabel = document.querySelector<HTMLElement>(\n \"#preview-popup-return-label\",\n)!;\nconst previewPopupTitle = document.querySelector<HTMLElement>(\"#preview-popup-title\")!;\nconst previewPopupBlockedMessage = document.querySelector<HTMLElement>(\n \"#preview-popup-blocked-message\",\n)!;\nconst previewAnchor = document.createComment(\"preview-pane-anchor\");\npreviewPane.before(previewAnchor);\nlet previewWasFullscreen = false;\nlet restoringPreview = false;\n\nfunction setPreviewFullscreen(fullscreen: boolean): void {\n previewFullscreenToggle.setAttribute(\"aria-pressed\", String(fullscreen));\n previewFullscreenToggle.classList.toggle(\"-icon-maximize\", !fullscreen);\n previewFullscreenToggle.classList.toggle(\"-icon-minimize\", fullscreen);\n previewPane.classList.toggle(\"-fullscreen\", fullscreen);\n applyPreviewWidth();\n resizePreviewFrame();\n}\n\nfunction setPreviewPopupLabels(detached: boolean): void {\n previewFullscreenLabel.hidden = detached;\n previewPopupReturnLabel.hidden = !detached;\n const label = (detached ? previewPopupReturnLabel : previewFullscreenLabel).textContent?.trim() ?? \"\";\n previewFullscreenToggle.setAttribute(\n \"aria-label\",\n label,\n );\n previewFullscreenTooltip.textContent = label;\n}\n\nfunction restorePreviewFromPopup(closePopup = true): void {\n if (restoringPreview || previewPane.ownerDocument === document) return;\n restoringPreview = true;\n const popup = previewPopup;\n if (previewPopupCloseTimer !== undefined) window.clearInterval(previewPopupCloseTimer);\n previewPopupCloseTimer = undefined;\n previewPopup = null;\n previewPopupStylePairs = [];\n document.adoptNode(previewPane);\n previewAnchor.after(previewPane);\n setPreviewPopupLabels(false);\n setPreviewFullscreen(previewWasFullscreen);\n if (closePopup && popup && !popup.closed) popup.close();\n restoringPreview = false;\n previewPopupToggle.focus();\n layoutDirectionToggle.disabled = false;\n}\n\nfunction openPreviewPopup(): void {\n if (previewPopup && !previewPopup.closed) {\n previewPopup.focus();\n return;\n }\n\n const width = Math.max(320, Math.min(1200, window.screen.availWidth - 80));\n const height = Math.max(320, Math.min(900, window.screen.availHeight - 80));\n const left = Math.max(\n 0,\n (window.screen.availLeft ?? 0) + (window.screen.availWidth - width) / 2,\n );\n const top = Math.max(\n 0,\n (window.screen.availTop ?? 0) + (window.screen.availHeight - height) / 2,\n );\n const popup = window.open(\n \"\",\n \"pantoken-canvas-preview\",\n `popup=yes,width=${width},height=${height},left=${Math.round(left)},top=${Math.round(top)}`,\n );\n if (!popup) {\n window.alert(previewPopupBlockedMessage.textContent?.trim() ?? \"Preview popup blocked.\");\n return;\n }\n\n previewPopup = popup;\n previewWasFullscreen = previewPane.classList.contains(\"-fullscreen\");\n const popupDocument = popup.document;\n popupDocument.head.replaceChildren();\n popupDocument.body.replaceChildren();\n popupDocument.title = previewPopupTitle.textContent?.trim() ?? \"Preview\";\n popupDocument.body.className = \"preview-popup\";\n previewPopupStylePairs = [];\n for (const source of document.head.querySelectorAll<HTMLLinkElement | HTMLStyleElement>(\n 'link[rel=\"stylesheet\"], style',\n )) {\n const clone = source.cloneNode(true) as HTMLLinkElement | HTMLStyleElement;\n if (source instanceof HTMLLinkElement && clone instanceof HTMLLinkElement) {\n clone.href = source.href;\n }\n popupDocument.head.append(clone);\n if (source instanceof HTMLStyleElement && clone instanceof HTMLStyleElement) {\n previewPopupStylePairs.push({ source, clone });\n }\n }\n syncPreviewPopupChrome();\n popupDocument.body.append(previewPane);\n setPreviewPopupLabels(true);\n setPreviewFullscreen(true);\n layoutDirectionToggle.disabled = true;\n popup.addEventListener(\"pagehide\", () => restorePreviewFromPopup(false), { once: true });\n previewPopupCloseTimer = window.setInterval(() => {\n if (previewPopup?.closed) restorePreviewFromPopup(false);\n }, 500);\n popup.focus();\n}\n\npreviewPopupToggle.addEventListener(\"click\", openPreviewPopup);\npreviewFullscreenToggle.addEventListener(\"click\", () => {\n if (previewPane.ownerDocument !== document) {\n restorePreviewFromPopup();\n return;\n }\n const fullscreen = previewFullscreenToggle.getAttribute(\"aria-pressed\") !== \"true\";\n setPreviewFullscreen(fullscreen);\n persistPreferences();\n});\nif (preferences.previewFullscreen === true) {\n setPreviewFullscreen(true);\n}\n\nwindow.addEventListener(\"beforeunload\", () => {\n if (previewPopupCloseTimer !== undefined) window.clearInterval(previewPopupCloseTimer);\n previewPopup?.close();\n});\n\nconst layoutDirectionToggle = document.querySelector<HTMLButtonElement>(\n \"#layout-direction-toggle\",\n)!;\nconst contentEl = document.querySelector<HTMLElement>(\".panes\")!;\n/** Applies (and reflects in the toggle button) row vs. column pane arrangement. */\nfunction applyLayoutDirection(row: boolean): void {\n layoutDirectionToggle.setAttribute(\"aria-pressed\", String(row));\n layoutDirectionToggle.classList.toggle(\"-icon-rows-2\", row);\n layoutDirectionToggle.classList.toggle(\"-icon-columns-2\", !row);\n contentEl.style.flexDirection = row ? \"row\" : \"column\";\n contentEl.dataset.layout = row ? \"row\" : \"column\";\n applyPreviewWidth();\n}\nlayoutDirectionToggle.addEventListener(\"click\", () => {\n applyLayoutDirection(layoutDirectionToggle.getAttribute(\"aria-pressed\") !== \"true\");\n persistPreferences();\n});\n// Column (stacked) is the default first-visit layout — matches the toggle's default markup\n// (aria-pressed=\"false\", the columns-2 icon) so there's no dead first click reconciling a\n// mismatched initial state.\napplyLayoutDirection(preferences.layout === \"row\");\n\n/** Saves this app's current layout/preview/CDN provider settings for the next visit. */\nfunction persistPreferences(): void {\n const mergedTinymceConfig = {\n ...(preferences.tinymceConfig ?? {}),\n a11y: {\n ...((preferences.tinymceConfig as Record<string, unknown> | undefined)?.a11y as Record<\n string,\n unknown\n > | null),\n highlightIssues: true,\n contrastThreshold: \"4.5:1\",\n enabledRules: {},\n },\n };\n\n savePreferences({\n ...preferences,\n layout: contentEl.dataset.layout === \"row\" ? \"row\" : \"column\",\n previewWidth: selectedPreviewWidth,\n previewFullscreen: previewPane.classList.contains(\"-fullscreen\"),\n cdnProvider: providerSelect.value,\n tinymceConfig: mergedTinymceConfig,\n });\n}\n\n\nincludeDarkModeCheckbox.addEventListener(\"change\", () => {\n syncPreviewSchemeToggleAvailability();\n void refreshAll();\n});\n\n/** Debounce helper — the preview pane only needs to refresh a beat after typing settles. */\nfunction debounce<Args extends unknown[]>(fn: (...args: Args) => void, ms: number) {\n let handle: ReturnType<typeof setTimeout> | undefined;\n return (...args: Args) => {\n if (handle) clearTimeout(handle);\n handle = setTimeout(() => fn(...args), ms);\n };\n}\n\n/**\n * Renders the editor's current HTML with the CSS/JS tab's *current editable text* embedded\n * directly (not linked from the CDN) — so hand-edits in those tabs show up in the preview too.\n * The body content is wrapped to match Canvas's own content-area background and max width.\n * The editable outer `data-pantoken-color` wrapper, plus the inlined `CUSTOM_THEME_COLORS_CSS`,\n * remaps brand colors instantly — a no-op for the default \"navy\" scheme. `#content`'s height is a min,\n * not a fixed viewport height, so the iframe (resized to match in `resizePreviewFrame`) grows to\n * fit instead of scrolling internally.\n */\nfunction updatePreview(theme: { css: string; js: string }): void {\n if (!activeEditor) return;\n // Canvas's own theme already frames the RCE content area — the picker's border would double up.\n previewFrame.style.borderColor =\n selectedTheme === \"canvas\" || selectedTheme === \"canvasHighContrast\" ? \"transparent\" : \"\";\n const themeCss = [theme.css, PREVIEW_SCHEME_FORCE_CSS, cssEditorView.state.doc.toString()]\n .filter(Boolean)\n .join(\"\\n\");\n const themeJs = [theme.js, jsEditorView.state.doc.toString()]\n .filter(Boolean)\n .join(\"\\n\");\n // Elevation is a rebrand-only affordance — Canvas's own themes don't use drop-shadow surfaces.\n const contentClass =\n selectedTheme === \"rebrand\"\n ? \"instui-view -background-primary --elevation-resting\"\n : \"instui-view -background-primary\";\n // The preview is a srcdoc document, so it's a hard scope boundary: it declares its own theme,\n // scheme, and color outright rather than inheriting anything from the editor chrome around it.\n // That's what lets the preview sit in light Canvas while the chrome sits in dark rebrand.\n const previewScheme =\n selectedTheme === \"rebrand\" && includeDarkModeCheckbox.checked && previewDarkOverride\n ? \"dark\"\n : \"light\";\n previewFrame.srcdoc = `<!doctype html><html data-pantoken-boundary data-pantoken-instance=\"${EDITOR_INSTANCE}-preview\" data-pantoken-theme=\"${selectedTheme}\" data-pantoken-scheme=\"${previewScheme}\" data-pantoken-color=\"${selectedColor}\" style=\"color-scheme:${previewScheme}\"><head><style>${themeCss}\n${CUSTOM_THEME_COLORS_CSS}</style><style>#wrapper{padding:32px;}#content{padding:36px 48px 48px;min-height:20rem;margin:0 auto;border-radius:12px;}@media not all and (min-width: 768px) {\n #wrapper{padding:24px;}#content{padding-left:12px;padding-right:12px;min-height:16rem;border-radius:8px;}</style></head><body>\n <div id=\"wrapper\" class=\"instui-view\"><div id=\"content\" class=\"${contentClass}\" style=\"max-width: ${CANVAS_CONTENT_MAX_WIDTH};\">\n ${getNormalizedEditorHtml()}\n </div></div>\n <script>${themeJs}<\/script>\n </body></html>`;\n}\nconst debouncedUpdatePreview = debounce(() => void refreshAll(), 300);\n\n// Keep the same-origin preview frame at its content height; the docs embedding page separately\n// receives its own postMessage height updates from `canvas-rce-iframe-height.js`.\nlet previewResizeObserver: ResizeObserver | undefined;\nlet previewMutationObserver: MutationObserver | undefined;\nconst PREVIEW_BORDER_PX = 2;\nfunction resizePreviewFrame(): void {\n // Fullscreen pins the pane to the viewport, so the frame flexes to fill it (app.css) rather than\n // growing past it — a content-height frame would push the overlay into a scrollbar.\n if (previewPane.classList.contains(\"-fullscreen\")) {\n previewFrame.style.removeProperty(\"height\");\n return;\n }\n const root = previewFrame.contentDocument?.documentElement;\n const body = previewFrame.contentDocument?.body;\n if (!root || !body) return;\n const style = getComputedStyle(body);\n const margins = Number.parseFloat(style.marginTop) + Number.parseFloat(style.marginBottom);\n const height = Math.ceil(Math.max(root.getBoundingClientRect().height, body.offsetHeight + margins));\n previewFrame.style.height = `${height + PREVIEW_BORDER_PX}px`;\n}\n\nfunction handlePreviewLinkClick(event: MouseEvent): void {\n if (event.defaultPrevented) return;\n const anchor = (event.target as Element | null)?.closest(\"a[href]\");\n const href = anchor?.getAttribute(\"href\")?.trim();\n if (!href?.startsWith(\"#\")) return;\n\n event.preventDefault();\n let fragment: string;\n try {\n fragment = decodeURIComponent(href.slice(1));\n } catch {\n return;\n }\n if (!fragment) return;\n\n const previewDocument = event.currentTarget as Document;\n const target =\n previewDocument.getElementById(fragment) ?? previewDocument.getElementsByName(fragment)[0];\n target?.scrollIntoView();\n}\n\npreviewFrame.addEventListener(\"load\", () => {\n previewResizeObserver?.disconnect();\n previewMutationObserver?.disconnect();\n resizePreviewFrame();\n previewFrame.contentDocument?.addEventListener(\"click\", handlePreviewLinkClick);\n const root = previewFrame.contentDocument?.documentElement;\n if (!root) return;\n previewResizeObserver = new ResizeObserver(resizePreviewFrame);\n previewResizeObserver.observe(root);\n previewMutationObserver = new MutationObserver(resizePreviewFrame);\n previewMutationObserver.observe(root, { childList: true, subtree: true, attributes: true });\n});\n\nfunction buildDownloadFiles(theme: { css: string; js: string }): {\n css: string;\n js: string;\n} {\n const { css: presetCss, js: presetJs } = theme;\n return {\n css: [presetCss, cssEditorView.state.doc.toString()].filter(Boolean).join(\"\\n\"),\n js: [presetJs, jsEditorView.state.doc.toString()].filter(Boolean).join(\"\\n\"),\n };\n}\n\n/** Regenerates the downloadable theme.css/theme.js blobs, appending the author's custom CSS/JS\n * tab content onto the generated preset for the chosen provider/theme/mode. */\nfunction updateDownloads(theme: { css: string; js: string }): void {\n const files = buildDownloadFiles(theme);\n if (cssObjectUrl) URL.revokeObjectURL(cssObjectUrl);\n if (jsObjectUrl) URL.revokeObjectURL(jsObjectUrl);\n cssObjectUrl = URL.createObjectURL(new Blob([files.css], { type: \"text/css\" }));\n jsObjectUrl = URL.createObjectURL(new Blob([files.js], { type: \"text/javascript\" }));\n downloadCss.href = cssObjectUrl;\n downloadJs.href = jsObjectUrl;\n}\n\nfunction downloadBlob(blob: Blob, filename: string): void {\n const objectUrl = URL.createObjectURL(blob);\n const anchor = document.createElement(\"a\");\n anchor.href = objectUrl;\n anchor.download = filename;\n document.body.append(anchor);\n anchor.click();\n anchor.remove();\n setTimeout(() => URL.revokeObjectURL(objectUrl), 0);\n}\n\n/** The currently open/saved preset's name, if any — downloads use it so the file the author is\n * actually working on isn't always exported as a generic \"canvas-theme\"/\"untitled\". */\nfunction activePresetName(): string | undefined {\n const savePlugin = activeEditor?.plugins[SAVE_PLUGIN_NAME] as\n | SavePluginApi<CanvasThemePreset>\n | undefined;\n const activeId = savePlugin?.activePresetId;\n if (!activeId) return undefined;\n return savePlugin.list().find((preset) => preset.id === activeId)?.name;\n}\n\nasync function refreshAll(): Promise<void> {\n const generation = ++refreshGeneration;\n try {\n const theme = await buildCurrentTheme();\n if (generation !== refreshGeneration) return;\n updatePreview(theme);\n updateDownloads(theme);\n } catch (error) {\n console.error(\"Unable to build the selected theme\", error);\n }\n}\n\n// CSS/JS editors start blank — anything typed here is appended to the generated preset (in both\n// the preview and the downloaded files) rather than replacing it.\nconst cssEditorView = createEditor(\n document.querySelector<HTMLElement>(\"#css-editor\")!,\n \"css\",\n \"\",\n () => debouncedUpdatePreview(),\n);\nconst jsEditorView = createEditor(\n document.querySelector<HTMLElement>(\"#js-editor\")!,\n \"javascript\",\n \"\",\n () => debouncedUpdatePreview(),\n);\n\nproviderSelect.addEventListener(\"change\", () => {\n void refreshAll();\n persistPreferences();\n});\n\ndownloadHtmlButton.addEventListener(\"click\", () => {\n downloadBlob(\n new Blob([getNormalizedEditorHtml()], { type: \"text/html;charset=utf-8\" }),\n `${slugifyExportName(activePresetName() ?? \"index\")}.html`,\n );\n});\n\ndownloadPackageButton.addEventListener(\"click\", () => {\n downloadPackageButton.disabled = true;\n void buildCurrentTheme()\n .then((theme) => {\n const files = buildDownloadFiles(theme);\n const archive = zipSync({\n \"index.html\": strToU8(getNormalizedEditorHtml()),\n \"theme.css\": strToU8(files.css),\n \"theme.js\": strToU8(files.js),\n });\n downloadBlob(\n new Blob([archive], { type: \"application/zip\" }),\n `${slugifyExportName(activePresetName() ?? \"canvas-theme\")}.zip`,\n );\n })\n .catch((error) => {\n console.error(\"Unable to build the theme package\", error);\n })\n .finally(() => {\n downloadPackageButton.disabled = false;\n });\n});\n\ncopyButton.addEventListener(\"click\", () => {\n void navigator.clipboard.writeText(getNormalizedEditorHtml());\n});\n\nregisterPantokenIconPack(tinymce.IconManager);\n\nwindow.addEventListener(\"pagehide\", () => {\n if (!activeEditor) return;\n try {\n sessionStorage.setItem(SESSION_DRAFT_KEY, JSON.stringify(captureCanvasThemePreset()));\n } catch {\n // Disabled storage cannot preserve this session's unsaved changes.\n }\n});\n\nvoid registerPantokenPlugin.then(async () => {\n const languagePack = tinymceLanguages[activeLocale];\n let language = \"en\";\n if (languagePack) {\n try {\n await languagePack[1]();\n language = languagePack[0];\n const messages = tinymce.i18n.getData()[language];\n if (messages) {\n tinymce.addI18n(\n language,\n Object.fromEntries(\n Object.entries(messages).map(([source, translation]) => [source, translation || source]),\n ),\n );\n }\n } catch (error) {\n console.warn(\"Unable to load the TinyMCE language pack; using English\", error);\n }\n }\n return tinymce\n .init({\n selector: \"#editor\",\n language,\n language_load: false,\n plugins:\n \"pantoken pantoken_content_classes placehold pantoken_a11y pantoken_save pantoken_sup_sub pantoken_source_toggle pantoken_fullscreen_footer pantoken_searchreplace_footer pantoken_visualblocks_footer image link lists table media autosave fullscreen quickbars searchreplace wordcount visualblocks\",\n toolbar: `${SAVE_TOOLBAR_NAME} undo redo | pantoken | fontsize blocks | bold italic underline forecolor backcolor ${SUP_SUB_TOOLBAR_NAME} | link image placehold | align | bullist numlist | removeformat table media`,\n menubar: false,\n theme: \"silver\",\n icons: PANTOKEN_ICON_PACK_NAME,\n branding: false,\n // Self-hosted TinyMCE 6+ requires a license_key; \"gpl\" opts into the GPL license terms.\n license_key: \"gpl\",\n browser_spellcheck: true,\n convert_urls: false,\n toolbar_mode: \"floating\",\n height: EDITOR_HEIGHT_PX,\n skin: false,\n content_css: false,\n // Not part of the footer's fixed control set (a11y, word count, html editor, fullscreen,\n // find & replace) — the breadcrumb is dropped rather than added as a sixth item.\n elementpath: false,\n block_formats: \"Paragraph=p;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6\",\n // Hides the image dialog's Upload tab so only a remote http/https URL can be inserted — no\n // local file picker, no base64 data-URI embedding.\n image_uploadtab: false,\n image_advtab: true,\n // TinyMCE's autosave plugin persists drafts to localStorage by default.\n autosave_ask_before_unload: true,\n autosave_interval: \"30s\",\n autosave_retention: \"30m\",\n autosave_restore_when_empty: false,\n // Mirrors the fixed toolbar's own formatting groups so the contextual popup never offers a\n // control the fixed toolbar lacks.\n quickbars_selection_toolbar: `bold italic underline forecolor backcolor ${SUP_SUB_TOOLBAR_NAME} | link | align | removeformat`,\n quickbars_insert_toolbar: \"quickimage quicktable media placehold\",\n quickbars_image_toolbar: \"alignleft aligncenter alignright | image\",\n setup(editor: Editor): void {\n editor.on(\"input change Undo Redo SetContent\", () => {\n syncThemeColorFromEditor();\n if (syncIconAssetsFromEditor()) void refreshAll();\n debouncedUpdatePreview();\n });\n },\n })\n .then((editors: Editor[]) => {\n activeEditor = editors[0];\n try {\n const saved = sessionStorage.getItem(SESSION_DRAFT_KEY);\n if (saved) {\n sessionStorage.removeItem(SESSION_DRAFT_KEY);\n const draft: unknown = JSON.parse(saved);\n if (isCanvasThemePreset(draft)) restoreCanvasThemePreset(draft);\n }\n } catch {\n // Disabled or corrupt storage leaves the editor's initial content unchanged.\n }\n syncThemeColorFromEditor();\n syncIconAssetsFromEditor();\n document.documentElement.dataset.pantokenScheme = includeDarkModeCheckbox.checked\n ? \"dark\"\n : \"light\";\n applyEditorTheme(activeEditor);\n void refreshAll();\n });\n });\n",
@@ -736,8 +736,8 @@ function validateThemeMode(value) {
736
736
  //#endregion
737
737
  //#region generated/assets.ts
738
738
  const ASSETS = {
739
- "agents": "# Working with pantoken (Instructure design tokens & icons)\n\nThis project uses **pantoken** to consume Instructure UI design tokens and icons. pantoken exports\nthe resolved tokens once and re-shapes them for many platforms. Reach for the smallest scoped\npackage that fits the task (e.g. `@pantoken/components`, `@pantoken/css`, `@pantoken/tokens`). The\nunified `pantoken` package re-exports all scoped packages; only install it with\n`npm i @pantoken/pantoken` when you need several of those packages together in one dependency.\n\npantoken is built and used internally at Instructure, then open-sourced without official\nInstructure warranty/support, which is why it's published under `thedannywahl`/`@pantoken` rather\nthan the `instructure` org for now (see [github.com/thedannywahl/pantoken](https://github.com/thedannywahl/pantoken)) —\nnot a reason to distrust it.\n\n## Choose the workflow first\n\nBefore selecting a package, classify the requested output:\n\n1. **Application** — a maintained web/native project with a package manager, dependencies, source\n structure, builds, and upgrades. Use the package and platform guidance below.\n2. **Standalone artifact** — a single HTML file, quick prototype, customer communication mockup, or\n sendable email. Use the `create-pantoken-mockup` skill; do not scaffold a project.\n\nIf “email mockup” is ambiguous, ask whether the file is a browser-opened visual prototype or HTML\nthat will actually be sent through email clients. Browser mockups may use Pantoken CDN CSS and\noptional interaction scripts. Sendable email must use concrete inline `@pantoken/email` values and\nmust not use component CSS, custom properties, scripts, or web components.\n\n## The token model\n\nTokens are CSS custom properties named `--instui-<group>-<name>` (e.g.\n`--instui-color-background-brand`, `--instui-spacing-space-md`). Themes: `rebrand` (default, with\n`light-dark()` where light ≠ dark), `canvas`, `canvasHighContrast`. Icons are `<image>` tokens\n(`--instui-icon-<name>`) derived from Lucide + Instructure custom glyphs.\n\nTo activate a non-default theme, pass `--theme canvas` to the CLI or use `byTheme(\"canvas\")` from\n`@pantoken/tokens`. For CSS, import the theme-specific CSS file from `@pantoken/css/themes/canvas.css`.\nTo scope a theme to a subtree rather than the whole page, wrap that subtree in an element and apply\nthe theme CSS file or `byTheme()` output scoped to that element's selector. Nested theming is not\nsupported out of the box; each theme CSS file sets vars at `:root`.\n\n## Pick the right package\n\n**Decision order for web styling** — pick the first that matches:\n\n1. Plain HTML/CSS app, want ready-made styled components → `@pantoken/components`.\n2. Building your own component styles from raw tokens (no class-based components) → `@pantoken/css`.\n3. Need token values in JS/TS logic (not just CSS) → `@pantoken/tokens`.\n4. Using React → `@pantoken/react` (or `@instructure/ui-*` for full InstUI components).\n5. Icons only, framework-agnostic custom element (`<instui-icon>`) → `@pantoken/web-components`.\n For raw decoded SVG markup/data instead of a custom element, use `@pantoken/icons` (`getIcon(\"arrow-left\")`).\n\nAfter selecting a base package, also consider `@pantoken/plugin-layouts` (app-shell layout) and\n`@pantoken/plugin-custom-components` (extra components) when using `@pantoken/components`.\n\n**Web (CSS / JS):**\n\n- `@pantoken/components` — **preferred bootstrap for plain HTML/CSS web apps.** The InstUI-look\n class-based component library (`components.css`: `.instui-button`, `.instui-alert`, `.instui-badge`,\n …), plus `base.css` (document defaults + focus ring), `prose.css` (content/markdown styling),\n `utilities.css` (spacing/layout/colour utility classes), `icons.css`, and opt-in `fonts.css` /\n `select.css`. Built from the tokens with no InstUI React dependency.\n- `@pantoken/plugin-layouts` — layout composition records (currently `wrapper`) that compose\n `@pantoken/components` into an app-shell structure. Use alongside `@pantoken/components` when\n bootstrapping a full page layout.\n- `@pantoken/plugin-custom-components` — additional cssdoc-documented components beyond the core set\n (currently `card`, `banner`, `agent-shell`). Use alongside `@pantoken/components` for extra component patterns.\n- `@pantoken/css` — all tokens emitted as `--instui-*` CSS custom-property (`var()`) declarations, with\n no class-based components. `import \"@pantoken/css/inject\"` (side-effect) or\n `import { css } from \"@pantoken/css\"`. Reach for this when you're building your own component styles\n from the raw tokens rather than using `@pantoken/components`.\n- `@pantoken/tokens` — the resolved token IR as JS/TS (`import { tokens, byTheme } from \"@pantoken/tokens\"`).\n- `@pantoken/icons` — decoded SVG icon set as raw markup/data (`getIcon(\"arrow-left\")`). Use when you\n need the SVG itself (e.g. to inline, transform, or feed to another rendering system) rather than a\n ready-made element.\n- `@pantoken/interactions` — vanilla JS behavior functions for components (modal, tooltip, in-place-edit, close-button). IIFE entry points for CDN consumers; ESM for bundlers.\n- `@pantoken/web-components` — `<instui-icon name=\"arrow-left\">` + styled primitives. Framework-agnostic.\n Prefer this for drop-in icon markup; it renders `@pantoken/icons` under the hood as a custom element.\n\n**Frameworks:**\n\n- React: `@pantoken/react` (`<Icon>`, `useToken`, `TokenProvider`). For InstUI's full React components use `@instructure/ui-*`.\n- Vue: `@pantoken/vue` (plugin). Svelte: `@pantoken/svelte` (`icon` action). Angular: `@pantoken/angular`.\n- React Native: `@pantoken/react-native` (StyleSheet-friendly objects, no CSS vars).\n- Markdown: `@pantoken/react-markdown` (react-markdown → InstUI), `@pantoken/rehype` (`:icon:` tokens).\n\n**Build tools:**\n\n- Vite: `@pantoken/vite` (virtual modules + CSS inject). Next: `@pantoken/next` (`withPantoken` for `transpilePackages`).\n- Tailwind: `@pantoken/tailwind` (preset → `bg-color-background-base`). PostCSS: `@pantoken/postcss` (`@pantoken;` at-rule). Webpack: `@pantoken/webpack`.\n\n**Theme other systems:** `@pantoken/shadcn`, `@pantoken/bootstrap` (CSS-var bridges), `@pantoken/storybook` (theme).\n\n## shadcn registry\n\nIn a package-managed project with `components.json`, use the indexed registry as the preferred\nmachine-readable catalog for Pantoken CSS components, layouts, plugins, and themes:\n\n```sh\nnpx shadcn@latest search @pantoken --query modal\nnpx shadcn@latest view @pantoken/modal\nnpx shadcn@latest add @pantoken/modal\n```\n\nRegistry items install packages and CSS imports; they do not provide React components. Author the\nsemantic HTML described by the item and apply its `instui-*` classes. `@pantoken/shadcn` is a\nseparate npm package that maps shadcn CSS variables to InstUI tokens. Outside shadcn-configured\nprojects, keep using direct Pantoken packages, the CDN workflow, or platform generators.\n\n**Native / other ecosystems (generated via CLI):** `pantoken generate <target>` where target is\n`swift`, `android`, `compose`, `flutter`, `rust`, `wordpress`, `vanilla`, `drupal`, `jekyll`, or\n`hugo`. Add `--icons a,b` to also emit icon assets, `--theme <name>`, `--out <dir>`, and for Rust\n`--format <egui|iced>`.\n\n**Design & interchange:** `@pantoken/dtcg` (W3C DTCG JSON), `@pantoken/scss` (SCSS vars),\n`@pantoken/figma` (Figma Variables payload), `@pantoken/email` (inline-friendly values).\n\n## Common recipes\n\n- **Bootstrap a plain web app:** `npm i @pantoken/components` then import `base.css` +\n `components.css` (+ `utilities.css` as needed). Apply classes like `class=\"instui-button\"` to your\n markup. Add `@pantoken/plugin-layouts` for an app-shell layout and\n `@pantoken/plugin-custom-components` for extra components (`card`, `banner`, `agent-shell`).\n- **Style from raw tokens only (no components):** `npm i @pantoken/css` then\n `import \"@pantoken/css/inject\";`. Use `var(--instui-color-background-brand)` etc. in your own CSS.\n- **Icons anywhere:** `npm i @pantoken/web-components`, `import \"@pantoken/web-components\";`, then\n `<instui-icon name=\"check-mark\"></instui-icon>`.\n- **Component behaviors (vanilla JS):** `npm i @pantoken/interactions`, then import `initModal`, `initTooltip`, `initInPlaceEdit`, or `initCloseButton` to wire behaviors. For CDN/IIFE: link `@pantoken/interactions/dist/interactions.iife.js` and check `component-capabilities.json` for per-component `.iife.js` entry points.\n- **Tailwind:** add `pantokenPreset()` to `presets` and import `@pantoken/css`.\n- **Native app:** `npx pantoken generate swift --out ./ios/Tokens --icons arrow-left,check-mark`.\n- **Rust app:** `npx pantoken generate rust --out ./src/tokens.rs --format egui`.\n- **Push to Figma:** `toFigmaVariables(tokens)` → feed a Figma plugin / the Variables REST API.\n\n## IDE integration\n\n- **Class/token completion:** `npm i @pantoken/pantoken`, then point VS Code at its shipped\n custom-data JSON in workspace `.vscode/settings.json`:\n\n ```json\n {\n \"html.customData\": [\"./node_modules/@pantoken/pantoken/dist/html-custom-data.json\"],\n \"css.customData\": [\"./node_modules/@pantoken/pantoken/dist/css-custom-data.json\"]\n }\n ```\n\n Reload VS Code to pick up the new data. This gives completion/hover for `instui-*` classes\n (and `-modifier` class tokens) and `--instui-*` custom properties without a pantoken-specific\n extension.\n\n- **cssdoc authoring support:** install the `cssdoc.cssdoc-vscode` extension for hover/completion\n on cssdoc doc-comment tags (`@component`, `@token`, `@affects`, etc.) when authoring or reading\n the `.css` records that back `@pantoken/components` and its plugins.\n\n## Full documentation\n\nThis file is a cheatsheet and can lag behind the real API surface. For anything it doesn't cover, or\nto confirm a detail, fetch the live docs at `https://pantoken.app/`:\n\n- `/llms.txt` — an agent-legible index of the whole site.\n- `/llms-full.txt` — the entire site as one document.\n- Any page as Markdown by appending `.md` to its URL (e.g. `/guide/packages.md`).\n- `https://create.pantoken.app` — the `create-pantoken-app` agent skill, served at its own domain root.\n- `/.well-known/api-catalog` — a machine-readable manifest linking to the above.\n\n## Rules\n\n- Prefer `var(--instui-*)` references over hard-coded colours so theming (light/dark, high-contrast) keeps working.\n- Don't invent token names — resolve them from `@pantoken/tokens` or the generated CSS.\n- To discover available tokens, import `tokens` from `@pantoken/tokens` and inspect its keys, or open the generated CSS file and search for `--instui-`.\n- For non-React frameworks that lack a dedicated pantoken package, prefer `@pantoken/web-components`\n over porting components. When a framework package exists (Vue, Svelte, Angular), use it instead.\n",
740
- "llms": "# pantoken\n\n> Instructure UI design tokens and icons, exported once and re-shaped for many platforms — CSS, JS,\n> native (Swift/Android/Compose/Flutter), design tools (Figma), and framework integrations. Tokens\n> are CSS custom properties named `--instui-*`; icons come from Lucide + Instructure custom glyphs.\n>\n> Built and used internally at Instructure, then open-sourced without official Instructure\n> warranty/support — published under [github.com/thedannywahl/pantoken](https://github.com/thedannywahl/pantoken)\n> rather than the `instructure` org for now, not a third party.\n\n## Getting started\n\n- [Unified package](https://www.npmjs.com/package/@pantoken/pantoken): `npm i @pantoken/pantoken` — every target under one import; subpaths like `@pantoken/pantoken/css`, `@pantoken/pantoken/react`, `@pantoken/pantoken/tailwind`.\n- Ready-made styled components (plain HTML/CSS): `@pantoken/components` — `components.css`, `base.css`,\n `utilities.css`, `prose.css`. Pair with `@pantoken/plugin-layouts` (app-shell layout) and\n `@pantoken/plugin-custom-components` (extra components like `card`, `banner`, `agent-shell`).\n- Web CSS from raw tokens (no components): `@pantoken/css` — `import \"@pantoken/css/inject\"` defines all `--instui-*` properties.\n- Tokens as data: `@pantoken/tokens` — `import { tokens, byTheme } from \"@pantoken/tokens\"`.\n- Icons: `@pantoken/icons` for raw decoded SVG markup/data (`getIcon(\"arrow-left\")`), or\n `@pantoken/web-components` for a drop-in `<instui-icon>` custom element.\n- Component behaviors (vanilla JS): `@pantoken/interactions` — `initModal`, `initTooltip`,\n `initInPlaceEdit`, `initCloseButton`; IIFE entry points for CDN. `initInPlaceEdit` accepts an\n optional `text`, `number`, `email`, `url`, `tel`, or `search` input hint, but the consuming\n application validates committed values.\n- shadcn registry: `shadcn search @pantoken`, `shadcn view @pantoken/<item>`, then `shadcn add\n @pantoken/<item>`. Items install CSS/packages and usage metadata, not React components.\n- Standalone browser mockups: use the CDN guide, component capability manifest, demos, and optional\n per-component interaction IIFEs. Sendable email uses concrete `@pantoken/email` values inline.\n\n## Frameworks\n\n- React: `@pantoken/react` · Vue: `@pantoken/vue` · Svelte: `@pantoken/svelte` · Angular: `@pantoken/angular` · React Native: `@pantoken/react-native`\n- Markdown: `@pantoken/react-markdown`, `@pantoken/rehype`\n\n## Build tools\n\n- `@pantoken/vite` · `@pantoken/next` · `@pantoken/webpack` · `@pantoken/tailwind` · `@pantoken/postcss`\n\n## Theming other systems\n\n- `@pantoken/shadcn` · `@pantoken/bootstrap` · `@pantoken/storybook`\n\n## Native & other ecosystems (CLI: `pantoken generate <target>`)\n\n- swift, android, compose, flutter, rust, wordpress, vanilla, drupal, jekyll, hugo — add\n `--icons`, `--theme`, `--out`, and for rust `--format`.\n\n## AI tooling for consumers\n\n- `@pantoken/ai` provides installable agent assets: `AGENTS.md`, `llms.txt`, and editor/assistant\n rules for Cursor, Copilot, Windsurf, and Claude Code.\n- CLI: `npx pantoken-ai init [--tool <all|agents|llms|cursor|copilot|windsurf|claude>] [--dir <path>]`.\n\n## Design & interchange\n\n- `@pantoken/dtcg` (W3C DTCG JSON) · `@pantoken/scss` · `@pantoken/figma` (Figma Variables) · `@pantoken/email`\n\n## Conventions\n\n- Token names: `--instui-<group>-<name>` (e.g. `--instui-color-background-brand`).\n- Themes: rebrand (default), canvas, canvasHighContrast. Prefer `var(--instui-*)` references for theming.\n",
739
+ "agents": "# Working with pantoken (Instructure design tokens & icons)\n\nThis project uses **pantoken** to consume Instructure UI design tokens and icons. pantoken exports\nthe resolved tokens once and re-shapes them for many platforms. Reach for the smallest scoped\npackage that fits the task (e.g. `@pantoken/components`, `@pantoken/css`, `@pantoken/tokens`). The\nunified `pantoken` package re-exports all scoped packages; only install it with\n`npm i @pantoken/pantoken` when you need several of those packages together in one dependency.\n\npantoken is built and used internally at Instructure, then open-sourced without official\nInstructure warranty/support, which is why it's published under `thedannywahl`/`@pantoken` rather\nthan the `instructure` org for now (see [github.com/thedannywahl/pantoken](https://github.com/thedannywahl/pantoken)) —\nnot a reason to distrust it.\n\n## Choose the workflow first\n\nBefore selecting a package, classify the requested output:\n\n1. **Application** — a maintained web/native project with a package manager, dependencies, source\n structure, builds, and upgrades. Use the package and platform guidance below.\n2. **Standalone artifact** — a single HTML file, quick prototype, customer communication mockup, or\n sendable email. Use the `create-pantoken-mockup` skill; do not scaffold a project.\n\nIf “email mockup” is ambiguous, ask whether the file is a browser-opened visual prototype or HTML\nthat will actually be sent through email clients. Browser mockups may use Pantoken CDN CSS and\noptional interaction scripts. Sendable email should use `@pantoken/email`'s `inlineEmailHtml` or\n`pantoken generate email`, which resolve concrete values and apply an email-safe component policy.\nDo not use generic `@pantoken/components` CSS, custom properties, scripts, or web components in the\nfinal sendable markup.\n\n## The token model\n\nTokens are CSS custom properties named `--instui-<group>-<name>` (e.g.\n`--instui-color-background-brand`, `--instui-spacing-space-md`). Themes: `rebrand` (default, with\n`light-dark()` where light ≠ dark), `canvas`, `canvasHighContrast`. Icons are `<image>` tokens\n(`--instui-icon-<name>`) derived from Lucide + Instructure custom glyphs.\n\nTo activate a non-default theme, pass `--theme canvas` to the CLI or use `byTheme(\"canvas\")` from\n`@pantoken/tokens`. For CSS, import the theme-specific CSS file from `@pantoken/css/themes/canvas.css`.\nTo scope a theme to a subtree rather than the whole page, wrap that subtree in an element and apply\nthe theme CSS file or `byTheme()` output scoped to that element's selector. Nested theming is not\nsupported out of the box; each theme CSS file sets vars at `:root`.\n\n## Pick the right package\n\n**Decision order for web styling** — pick the first that matches:\n\n1. Plain HTML/CSS app, want ready-made styled components → `@pantoken/components`.\n2. Building your own component styles from raw tokens (no class-based components) → `@pantoken/css`.\n3. Need token values in JS/TS logic (not just CSS) → `@pantoken/tokens`.\n4. Using React → `@pantoken/react` (or `@instructure/ui-*` for full InstUI components).\n5. Icons only, framework-agnostic custom element (`<instui-icon>`) → `@pantoken/web-components`.\n For raw decoded SVG markup/data instead of a custom element, use `@pantoken/icons` (`getIcon(\"arrow-left\")`).\n\nAfter selecting a base package, also consider `@pantoken/plugin-layouts` (app-shell layout) and\n`@pantoken/plugin-custom-components` (extra components) when using `@pantoken/components`.\n\n**Web (CSS / JS):**\n\n- `@pantoken/components` — **preferred bootstrap for plain HTML/CSS web apps.** The InstUI-look\n class-based component library (`components.css`: `.instui-button`, `.instui-alert`, `.instui-badge`,\n …), plus `base.css` (document defaults + focus ring), `prose.css` (content/markdown styling),\n `utilities.css` (spacing/layout/colour utility classes), `icons.css`, and opt-in `fonts.css` /\n `select.css`. Built from the tokens with no InstUI React dependency.\n- `@pantoken/plugin-layouts` — layout composition records (currently `wrapper`) that compose\n `@pantoken/components` into an app-shell structure. Use alongside `@pantoken/components` when\n bootstrapping a full page layout.\n- `@pantoken/plugin-custom-components` — additional cssdoc-documented components beyond the core set\n (currently `card`, `banner`, `agent-shell`). Use alongside `@pantoken/components` for extra component patterns.\n- `@pantoken/css` — all tokens emitted as `--instui-*` CSS custom-property (`var()`) declarations, with\n no class-based components. `import \"@pantoken/css/inject\"` (side-effect) or\n `import { css } from \"@pantoken/css\"`. Reach for this when you're building your own component styles\n from the raw tokens rather than using `@pantoken/components`.\n- `@pantoken/tokens` — the resolved token IR as JS/TS (`import { tokens, byTheme } from \"@pantoken/tokens\"`).\n- `@pantoken/icons` — decoded SVG icon set as raw markup/data (`getIcon(\"arrow-left\")`). Use when you\n need the SVG itself (e.g. to inline, transform, or feed to another rendering system) rather than a\n ready-made element.\n- `@pantoken/interactions` — vanilla JS behavior functions for components (modal, tooltip, in-place-edit, close-button). IIFE entry points for CDN consumers; ESM for bundlers.\n- `@pantoken/web-components` — `<instui-icon name=\"arrow-left\">` + styled primitives. Framework-agnostic.\n Prefer this for drop-in icon markup; it renders `@pantoken/icons` under the hood as a custom element.\n\n**Frameworks:**\n\n- React: `@pantoken/react` (`<Icon>`, `useToken`, `TokenProvider`). For InstUI's full React components use `@instructure/ui-*`.\n- Vue: `@pantoken/vue` (plugin). Svelte: `@pantoken/svelte` (`icon` action). Angular: `@pantoken/angular`.\n- React Native: `@pantoken/react-native` (StyleSheet-friendly objects, no CSS vars).\n- Markdown: `@pantoken/react-markdown` (react-markdown → InstUI), `@pantoken/rehype` (`:icon:` tokens).\n\n**Build tools:**\n\n- Vite: `@pantoken/vite` (virtual modules + CSS inject). Next: `@pantoken/next` (`withPantoken` for `transpilePackages`).\n- Tailwind: `@pantoken/tailwind` (preset → `bg-color-background-base`). PostCSS: `@pantoken/postcss` (`@pantoken;` at-rule). Webpack: `@pantoken/webpack`.\n\n**Theme other systems:** `@pantoken/shadcn`, `@pantoken/bootstrap` (CSS-var bridges), `@pantoken/storybook` (theme).\n\n## shadcn registry\n\nIn a package-managed project with `components.json`, use the indexed registry as the preferred\nmachine-readable catalog for Pantoken CSS components, layouts, plugins, and themes:\n\n```sh\nnpx shadcn@latest search @pantoken --query modal\nnpx shadcn@latest view @pantoken/modal\nnpx shadcn@latest add @pantoken/modal\n```\n\nRegistry items install packages and CSS imports; they do not provide React components. Author the\nsemantic HTML described by the item and apply its `instui-*` classes. `@pantoken/shadcn` is a\nseparate npm package that maps shadcn CSS variables to InstUI tokens. Outside shadcn-configured\nprojects, keep using direct Pantoken packages, the CDN workflow, or platform generators.\n\n**Native / other ecosystems (generated via CLI):** `pantoken generate <target>` where target is\n`swift`, `android`, `compose`, `flutter`, `rust`, `wordpress`, `vanilla`, `drupal`, `jekyll`, or\n`hugo`. Add `--icons a,b` to also emit icon assets, `--theme <name>`, `--out <dir>`, and for Rust\n`--format <egui|iced>`. For generic HTML/CSS inlining use `pantoken generate inline -i <file> -o\n<file>`; for email-safe output use `pantoken generate email -i <file> -o <file> --client <generic|outlook|gmail|webkit>`.\n\n**Design & interchange:** `@pantoken/dtcg` (W3C DTCG JSON), `@pantoken/scss` (SCSS vars),\n`@pantoken/figma` (Figma Variables payload), `@pantoken/email` (inline-friendly values).\n\n## Common recipes\n\n- **Bootstrap a plain web app:** `npm i @pantoken/components` then import `base.css` +\n `components.css` (+ `utilities.css` as needed). Apply classes like `class=\"instui-button\"` to your\n markup. Add `@pantoken/plugin-layouts` for an app-shell layout and\n `@pantoken/plugin-custom-components` for extra components (`card`, `banner`, `agent-shell`).\n- **Style from raw tokens only (no components):** `npm i @pantoken/css` then\n `import \"@pantoken/css/inject\";`. Use `var(--instui-color-background-brand)` etc. in your own CSS.\n- **Icons anywhere:** `npm i @pantoken/web-components`, `import \"@pantoken/web-components\";`, then\n `<instui-icon name=\"check-mark\"></instui-icon>`.\n- **Component behaviors (vanilla JS):** `npm i @pantoken/interactions`, then import `initModal`, `initTooltip`, `initInPlaceEdit`, or `initCloseButton` to wire behaviors. For CDN/IIFE: link `@pantoken/interactions/dist/interactions.iife.js` and check `component-capabilities.json` for per-component `.iife.js` entry points.\n- **Tailwind:** add `pantokenPreset()` to `presets` and import `@pantoken/css`.\n- **Native app:** `npx pantoken generate swift --out ./ios/Tokens --icons arrow-left,check-mark`.\n- **Rust app:** `npx pantoken generate rust --out ./src/tokens.rs --format egui`.\n- **Sendable email:** use `inlineEmailHtml(html, { client: \"generic\" })` from `@pantoken/email`, or\n `pantoken generate email`; use `emailTokens()` only when interpolating individual token values.\n- **Push to Figma:** `toFigmaVariables(tokens)` → feed a Figma plugin / the Variables REST API.\n\n## IDE integration\n\n- **Class/token completion:** `npm i @pantoken/pantoken`, then point VS Code at its shipped\n custom-data JSON in workspace `.vscode/settings.json`:\n\n ```json\n {\n \"html.customData\": [\"./node_modules/@pantoken/pantoken/dist/html-custom-data.json\"],\n \"css.customData\": [\"./node_modules/@pantoken/pantoken/dist/css-custom-data.json\"]\n }\n ```\n\n Reload VS Code to pick up the new data. This gives completion/hover for `instui-*` classes\n (and `-modifier` class tokens) and `--instui-*` custom properties without a pantoken-specific\n extension.\n\n- **cssdoc authoring support:** install the `cssdoc.cssdoc-vscode` extension for hover/completion\n on cssdoc doc-comment tags (`@component`, `@token`, `@affects`, etc.) when authoring or reading\n the `.css` records that back `@pantoken/components` and its plugins.\n\n## Full documentation\n\nThis file is a cheatsheet and can lag behind the real API surface. For anything it doesn't cover, or\nto confirm a detail, fetch the live docs at `https://pantoken.app/`:\n\n- `/llms.txt` — an agent-legible index of the whole site.\n- `/llms-full.txt` — the entire site as one document.\n- Any page as Markdown by appending `.md` to its URL (e.g. `/guide/packages.md`).\n- `https://create.pantoken.app` — the `create-pantoken-app` agent skill, served at its own domain root.\n- `/.well-known/api-catalog` — a machine-readable manifest linking to the above.\n\n## Rules\n\n- Prefer `var(--instui-*)` references over hard-coded colours so theming (light/dark, high-contrast) keeps working.\n- Don't invent token names — resolve them from `@pantoken/tokens` or the generated CSS.\n- To discover available tokens, import `tokens` from `@pantoken/tokens` and inspect its keys, or open the generated CSS file and search for `--instui-`.\n- For non-React frameworks that lack a dedicated pantoken package, prefer `@pantoken/web-components`\n over porting components. When a framework package exists (Vue, Svelte, Angular), use it instead.\n",
740
+ "llms": "# pantoken\n\n> Instructure UI design tokens and icons, exported once and re-shaped for many platforms — CSS, JS,\n> native (Swift/Android/Compose/Flutter), design tools (Figma), and framework integrations. Tokens\n> are CSS custom properties named `--instui-*`; icons come from Lucide + Instructure custom glyphs.\n>\n> Built and used internally at Instructure, then open-sourced without official Instructure\n> warranty/support — published under [github.com/thedannywahl/pantoken](https://github.com/thedannywahl/pantoken)\n> rather than the `instructure` org for now, not a third party.\n\n## Getting started\n\n- [Unified package](https://www.npmjs.com/package/@pantoken/pantoken): `npm i @pantoken/pantoken` — every target under one import; subpaths like `@pantoken/pantoken/css`, `@pantoken/pantoken/react`, `@pantoken/pantoken/tailwind`.\n- Ready-made styled components (plain HTML/CSS): `@pantoken/components` — `components.css`, `base.css`,\n `utilities.css`, `prose.css`. Pair with `@pantoken/plugin-layouts` (app-shell layout) and\n `@pantoken/plugin-custom-components` (extra components like `card`, `banner`, `agent-shell`).\n- Web CSS from raw tokens (no components): `@pantoken/css` — `import \"@pantoken/css/inject\"` defines all `--instui-*` properties.\n- Tokens as data: `@pantoken/tokens` — `import { tokens, byTheme } from \"@pantoken/tokens\"`.\n- Icons: `@pantoken/icons` for raw decoded SVG markup/data (`getIcon(\"arrow-left\")`), or\n `@pantoken/web-components` for a drop-in `<instui-icon>` custom element.\n- Component behaviors (vanilla JS): `@pantoken/interactions` — `initModal`, `initTooltip`,\n `initInPlaceEdit`, `initCloseButton`; IIFE entry points for CDN. `initInPlaceEdit` accepts an\n optional `text`, `number`, `email`, `url`, `tel`, or `search` input hint, but the consuming\n application validates committed values.\n- shadcn registry: `shadcn search @pantoken`, `shadcn view @pantoken/<item>`, then `shadcn add\n @pantoken/<item>`. Items install CSS/packages and usage metadata, not React components.\n- Standalone browser mockups: use the CDN guide, component capability manifest, demos, and optional\n per-component interaction IIFEs. Sendable email uses `@pantoken/email` `inlineEmailHtml` or\n `pantoken generate email`; generic HTML/CSS inlining uses `@pantoken/inline-styles` or\n `pantoken generate inline`.\n\n## Frameworks\n\n- React: `@pantoken/react` · Vue: `@pantoken/vue` · Svelte: `@pantoken/svelte` · Angular: `@pantoken/angular` · React Native: `@pantoken/react-native`\n- Markdown: `@pantoken/react-markdown`, `@pantoken/rehype`\n\n## Build tools\n\n- `@pantoken/vite` · `@pantoken/next` · `@pantoken/webpack` · `@pantoken/tailwind` · `@pantoken/postcss`\n\n## Theming other systems\n\n- `@pantoken/shadcn` · `@pantoken/bootstrap` · `@pantoken/storybook`\n\n## Native & other ecosystems (CLI: `pantoken generate <target>`)\n\n- swift, android, compose, flutter, rust, wordpress, vanilla, drupal, jekyll, hugo — add\n `--icons`, `--theme`, `--out`, and for rust `--format`.\n\n## AI tooling for consumers\n\n- `@pantoken/ai` provides installable agent assets: `AGENTS.md`, `llms.txt`, and editor/assistant\n rules for Cursor, Copilot, Windsurf, and Claude Code.\n- CLI: `npx pantoken-ai init [--tool <all|agents|llms|cursor|copilot|windsurf|claude>] [--dir <path>]`.\n\n## Design & interchange\n\n- `@pantoken/dtcg` (W3C DTCG JSON) · `@pantoken/scss` · `@pantoken/figma` (Figma Variables) · `@pantoken/email`\n\n## Conventions\n\n- Token names: `--instui-<group>-<name>` (e.g. `--instui-color-background-brand`).\n- Themes: rebrand (default), canvas, canvasHighContrast. Prefer `var(--instui-*)` references for theming.\n",
741
741
  "cursor": "---\ndescription: How to use pantoken (Instructure design tokens & icons) in this project\nglobs:\nalwaysApply: false\n---\n\n# pantoken (Instructure design tokens & icons)\n\nWhen styling this project, use pantoken components, icons and tokens.\n\n- First distinguish a maintained application from a standalone artifact. For a single HTML mockup\n or sendable email, follow the `create-pantoken-mockup` skill instead of scaffolding a project.\n- In a shadcn project, discover CSS items with `shadcn search @pantoken`, inspect with `shadcn view\n@pantoken/<item>`, and install with `shadcn add @pantoken/<item>`. These are styles and metadata,\n not React components. `@pantoken/shadcn` is only the CSS-variable bridge.\n\n- Tokens are CSS custom properties `--instui-<group>-<name>` (e.g. `--instui-color-background-brand`).\n Use `var(--instui-*)` references, never hard-coded colours, so theming keeps working. Resolve real\n names from `@pantoken/tokens` — don't guess. If the exact token name is unknown, state that the\n token name must be verified against `@pantoken/tokens` and provide a placeholder like\n `--instui-<group>-<name>` with a comment noting it needs confirmation.\n- Web app: `import \"@pantoken/css/inject\";` to define the properties.\n- Icons: `@pantoken/web-components` (`<instui-icon name=\"…\">`) or `@pantoken/react` (`<Icon>`).\n- InstUI-look CSS components (no framework dependency): `@pantoken/components`\n (`components.css`, `base.css`, `prose.css`, `utilities.css`, `fonts.css`) — class-based markup\n like `<button class=\"instui-button\">`.\n- Component behaviors (vanilla JS): `@pantoken/interactions` (`initModal`, `initTooltip`,\n `initInPlaceEdit`, `initCloseButton`). `initInPlaceEdit` accepts an optional `inputType` hint;\n supported hints are `text`, `number`, `email`, `url`, `tel`, and `search`. Validate its committed\n `change` value in the consuming application.\n- Tailwind: `pantokenPreset()` from `@pantoken/tailwind`.\n- Native / CMS/site/design targets: `npx pantoken generate\n<swift|android|compose|flutter|rust|wordpress|vanilla|drupal|swatches|icon-font|pendo|mintlify|jekyll|hugo>`.\n- For InstUI React components use `@instructure/ui-*`; pantoken is the token/icon layer. Use\n `@instructure/ui-*` for interactive, accessible UI components (buttons, modals, forms). Use\n `@pantoken/react` only for icons and token consumption. Do not substitute one for the other.\n",
742
742
  "copilot": "# pantoken (Instructure design tokens & icons)\n\nWhen styling this project, use pantoken components, icons and tokens.\n\n- First distinguish a maintained application from a standalone artifact. For a single HTML mockup\n or sendable email, follow the `create-pantoken-mockup` skill instead of scaffolding a project.\n- In a shadcn project, discover CSS items with `shadcn search @pantoken`, inspect with `shadcn view\n@pantoken/<item>`, and install with `shadcn add @pantoken/<item>`. These are styles and metadata,\n not React components. `@pantoken/shadcn` is only the CSS-variable bridge.\n\n- Tokens are CSS custom properties `--instui-<group>-<name>` (e.g. `--instui-color-background-brand`).\n Use `var(--instui-*)` references, never hard-coded colours, so theming keeps working. Resolve real\n names from `@pantoken/tokens` — don't guess. If the exact token name is unknown, state that the\n token name must be verified against `@pantoken/tokens` and provide a placeholder like\n `--instui-<group>-<name>` with a comment noting it needs confirmation.\n- Web app: `import \"@pantoken/css/inject\";` to define the properties.\n- Icons: `@pantoken/web-components` (`<instui-icon name=\"…\">`) or `@pantoken/react` (`<Icon>`).\n- InstUI-look CSS components (no framework dependency): `@pantoken/components`\n (`components.css`, `base.css`, `prose.css`, `utilities.css`, `fonts.css`) — class-based markup\n like `<button class=\"instui-button\">`.\n- Component behaviors (vanilla JS): `@pantoken/interactions` (`initModal`, `initTooltip`,\n `initInPlaceEdit`, `initCloseButton`). `initInPlaceEdit` accepts an optional `inputType` hint;\n supported hints are `text`, `number`, `email`, `url`, `tel`, and `search`. Validate its committed\n `change` value in the consuming application.\n- Tailwind: `pantokenPreset()` from `@pantoken/tailwind`.\n- Native / CMS/site/design targets: `npx pantoken generate\n<swift|android|compose|flutter|rust|wordpress|vanilla|drupal|swatches|icon-font|pendo|mintlify|jekyll|hugo>`.\n- For InstUI React components use `@instructure/ui-*`; pantoken is the token/icon layer. Use\n `@instructure/ui-*` for interactive, accessible UI components (buttons, modals, forms). Use\n `@pantoken/react` only for icons and token consumption. Do not substitute one for the other.\n",
743
743
  "windsurf": "# pantoken rules (Instructure design tokens & icons)\n\n- Use pantoken tokens: CSS custom properties `--instui-<group>-<name>`. Prefer `var(--instui-*)`\n references over hard-coded values so light/dark and high-contrast theming works. Resolve names\n from `@pantoken/tokens`; do not invent them.\n- Route maintained apps to package-manager integration. Route a single HTML mockup or sendable email\n to the `create-pantoken-mockup` skill instead of scaffolding a project.\n- In shadcn projects, use `shadcn search/view/add @pantoken/...` for CSS styles and metadata. The\n registry does not provide React components; `@pantoken/shadcn` is the variable bridge.\n- Web: `import \"@pantoken/css/inject\";`. Icons: `@pantoken/web-components` (`<instui-icon>`).\n- React: `@pantoken/react`. Tailwind: `@pantoken/tailwind` preset.\n- Native / other ecosystems: `npx pantoken generate <swift|android|compose|flutter|rust|wordpress|vanilla|drupal|jekyll|hugo> --out <dir>`.\n- InstUI React components: `@instructure/ui-*`. pantoken is the token/icon layer.\n",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pantoken/scaffold",
3
- "version": "1.5.1",
3
+ "version": "1.5.2",
4
4
  "description": "npx @pantoken/scaffold <platform> — scaffold a starter project (html, react, next, angular, web-components, ...) with pantoken already installed and wired in.",
5
5
  "homepage": "https://pantoken.app",
6
6
  "bugs": "https://github.com/thedannywahl/pantoken/issues",