dsh-image-gen 0.6.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/README.en.md +12 -2
  2. package/README.md +13 -3
  3. package/lib/client.js +1086 -133
  4. package/lib/client.js.map +1 -1
  5. package/lib/index.js +1425 -103
  6. package/lib/types/client/image-result-node.d.ts +11 -1
  7. package/lib/types/client/index.d.ts +5 -0
  8. package/lib/types/client/provider-pill.d.ts +1 -1
  9. package/lib/types/client/studio-style.d.ts +1 -1
  10. package/lib/types/client/studio-view.d.ts +6 -0
  11. package/lib/types/client/tl/css-supports-shim.d.ts +21 -0
  12. package/lib/types/client/tl/diagram-landing.d.ts +75 -0
  13. package/lib/types/client/tl/diagram-layout.d.ts +76 -0
  14. package/lib/types/client/tl/diagram-measure.d.ts +40 -0
  15. package/lib/types/client/tl/tl-brand-theme.d.ts +3 -0
  16. package/lib/types/client/tl/tl-brand-tokens.d.ts +24 -0
  17. package/lib/types/client/tl/tl-theme-css.d.ts +17 -0
  18. package/lib/types/config.d.ts +24 -0
  19. package/lib/types/diagram.d.ts +42 -0
  20. package/lib/types/openai-compatible.d.ts +15 -0
  21. package/lib/types/shared.d.ts +16 -1
  22. package/lib/types/subscription/blob.d.ts +23 -0
  23. package/lib/types/subscription/loopback.d.ts +11 -0
  24. package/lib/types/subscription/manager.d.ts +94 -0
  25. package/lib/types/subscription/oauth.d.ts +25 -0
  26. package/lib/types/subscription/subscription-route.d.ts +5 -0
  27. package/lib/types/subscription/vendors/antigravity.d.ts +66 -0
  28. package/lib/types/subscription/vendors/codex.d.ts +30 -0
  29. package/lib/types/subscription/vendors/grok.d.ts +32 -0
  30. package/lib/types/subscription.d.ts +42 -0
  31. package/lib/types/test-route.d.ts +9 -0
  32. package/package.json +10 -8
package/lib/client.js CHANGED
@@ -48,6 +48,30 @@ window.__ModuleLoader__.load({
48
48
  let react_jsx_runtime = require("react/jsx-runtime");
49
49
  let react = require("react");
50
50
  react = __toESM(react, 1);
51
+ //#region lib/types/client/tl/css-supports-shim.js
52
+ /**
53
+ * CSS.supports shim for tldraw's module-scope environment probe (#40).
54
+ *
55
+ * @tldraw/editor's tlenv initialization runs at import time and calls
56
+ * CSS.supports("color", "color(display-p3 1 1 1)") guarded only by
57
+ * `typeof CSS !== "undefined"`. Environments that expose a `CSS` global
58
+ * without the `supports` method (embedded webviews with partial globals,
59
+ * browsers predating the API) therefore throw "CSS.supports is not a
60
+ * function" while DSH imports the client entry, which aborts the whole
61
+ * plugin load - the image tools and gallery die together with the canvas.
62
+ *
63
+ * This module must be the first import of the client entry so it evaluates
64
+ * before any tldraw code. It installs a conservative `supports()` returning
65
+ * false only when the method is genuinely missing; modern browsers are
66
+ * untouched (the probe already treats false as "no P3 / no backdrop filter",
67
+ * which only downgrades the canvas color space, not the plugin).
68
+ */
69
+ /** Install a fallback `supports` on a CSS-like object when the method is missing. */
70
+ function ensureCssSupports(cssLike) {
71
+ if (typeof cssLike.supports !== "function") cssLike.supports = () => false;
72
+ }
73
+ if (typeof CSS !== "undefined") ensureCssSupports(CSS);
74
+ //#endregion
51
75
  //#region lib/types/shared.js
52
76
  /** Browser route used by the generated-image card. */
53
77
  const IMAGE_ROUTE = "/plugins/dsh-image-gen/image";
@@ -63,6 +87,8 @@ window.__ModuleLoader__.load({
63
87
  const TEST_CONNECTION_ROUTE = "/plugins/dsh-image-gen/test";
64
88
  /** Browser route the workbench infinite canvas pushes live state through. */
65
89
  const CANVAS_STATE_ROUTE = "/plugins/dsh-image-gen/canvas-state";
90
+ const SUBSCRIPTION_LOGIN_ROUTE = "/plugins/dsh-image-gen/subscription-login";
91
+ const SUBSCRIPTION_STATUS_ROUTE = "/plugins/dsh-image-gen/subscription-status";
66
92
  /** Namespace persisted through DSH Settings. */
67
93
  const IMAGE_GENERATION_NAMESPACE = "image-generation";
68
94
  /** Supported providers. */
@@ -74,8 +100,25 @@ window.__ModuleLoader__.load({
74
100
  "dashscope",
75
101
  "xai",
76
102
  "zhipu",
77
- "comfyui"
103
+ "comfyui",
104
+ "chatgpt-sub",
105
+ "grok-sub",
106
+ "google-sub"
78
107
  ];
108
+ /**
109
+ * Providers that generate through a logged-in subscription account instead
110
+ * of an API key. They use no credential reference and never join the
111
+ * cloud/BYOK sets.
112
+ */
113
+ const SUBSCRIPTION_PROVIDERS = [
114
+ "chatgpt-sub",
115
+ "grok-sub",
116
+ "google-sub"
117
+ ];
118
+ /** True when the provider generates through a logged-in subscription account. */
119
+ function isSubscriptionProvider(provider) {
120
+ return SUBSCRIPTION_PROVIDERS.includes(provider);
121
+ }
79
122
  /** Providers supported by the first browser workbench release. */
80
123
  const CLOUD_IMAGE_PROVIDERS = [
81
124
  "google",
@@ -160,6 +203,12 @@ window.__ModuleLoader__.load({
160
203
  if (!existing.includes(candidate)) return candidate;
161
204
  }
162
205
  }
206
+ /** Default models for the subscription channels; fixed by the bridge protocol. */
207
+ const DEFAULT_SUBSCRIPTION_MODELS = {
208
+ "chatgpt-sub": "gpt-image-2.5-flare",
209
+ "grok-sub": "grok-imagine-image-2.0",
210
+ "google-sub": "gemini-3-pro-image"
211
+ };
163
212
  const DEFAULT_MODELS = {
164
213
  google: DEFAULT_GOOGLE_MODEL,
165
214
  openai: DEFAULT_OPENAI_MODEL,
@@ -168,7 +217,10 @@ window.__ModuleLoader__.load({
168
217
  dashscope: DEFAULT_DASHSCOPE_MODEL,
169
218
  xai: DEFAULT_XAI_MODEL,
170
219
  zhipu: DEFAULT_ZHIPU_MODEL,
171
- comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL
220
+ comfyui: DEFAULT_COMFYUI_WORKFLOW_LABEL,
221
+ "chatgpt-sub": DEFAULT_SUBSCRIPTION_MODELS["chatgpt-sub"],
222
+ "grok-sub": DEFAULT_SUBSCRIPTION_MODELS["grok-sub"],
223
+ "google-sub": DEFAULT_SUBSCRIPTION_MODELS["google-sub"]
172
224
  };
173
225
  const DEFAULT_BASE_URLS = {
174
226
  google: DEFAULT_GOOGLE_ENDPOINT,
@@ -178,7 +230,10 @@ window.__ModuleLoader__.load({
178
230
  dashscope: DEFAULT_DASHSCOPE_ENDPOINT,
179
231
  xai: DEFAULT_XAI_BASE_URL,
180
232
  zhipu: DEFAULT_ZHIPU_BASE_URL,
181
- comfyui: DEFAULT_COMFYUI_BASE_URL
233
+ comfyui: DEFAULT_COMFYUI_BASE_URL,
234
+ "chatgpt-sub": "",
235
+ "grok-sub": "",
236
+ "google-sub": ""
182
237
  };
183
238
  //#endregion
184
239
  //#region lib/types/comfyui-workflow.js
@@ -906,6 +961,28 @@ window.__ModuleLoader__.load({
906
961
  * This source code is licensed under the ISC license.
907
962
  * See the LICENSE file in the root directory of this source tree.
908
963
  */
964
+ const Info = createLucideIcon("info", [
965
+ ["circle", {
966
+ cx: "12",
967
+ cy: "12",
968
+ r: "10",
969
+ key: "1mglay"
970
+ }],
971
+ ["path", {
972
+ d: "M12 16v-4",
973
+ key: "1dtifu"
974
+ }],
975
+ ["path", {
976
+ d: "M12 8h.01",
977
+ key: "e9boi3"
978
+ }]
979
+ ]);
980
+ /**
981
+ * @license lucide-react v1.38.0 - ISC
982
+ *
983
+ * This source code is licensed under the ISC license.
984
+ * See the LICENSE file in the root directory of this source tree.
985
+ */
909
986
  const LoaderCircle = createLucideIcon("loader-circle", [["path", {
910
987
  d: "M21 12a9 9 0 1 1-6.219-8.56",
911
988
  key: "13zald"
@@ -115467,6 +115544,84 @@ img.ProseMirror-separator {
115467
115544
  //#region node_modules/.pnpm/tldraw@5.3.0_@floating-ui+d_6aac9d8fd21c97d7663c6dd43e72f388/node_modules/tldraw/dist-esm/index.mjs
115468
115545
  registerTldrawLibraryVersion("tldraw", "5.3.0", "esm");
115469
115546
  //#endregion
115547
+ //#region lib/types/client/tl/tl-brand-tokens.js
115548
+ /**
115549
+ * Single source of truth for the brand colors used across the tldraw surface.
115550
+ *
115551
+ * Consumed by tl-theme-css.ts (DOM chrome via CSS variables) and
115552
+ * tl-brand-theme.ts (canvas renderer via the runtime theme patch), so
115553
+ * re-tinting the surface is a one-file change instead of a hex hunt across
115554
+ * two files. Values mirror the workbench tokens in studio-style.ts
115555
+ * (`--ig-blue` and friends).
115556
+ */
115557
+ /** Brand blue, shared with studio-style.ts `--ig-blue`. */
115558
+ const BRAND_BLUE = "#2f64f5";
115559
+ /** Brand blue RGB components, for composing rgba() tints in one place. */
115560
+ const BRAND_BLUE_RGB = "47, 100, 245";
115561
+ /** Brand blue tuned for dark surfaces. */
115562
+ const BRAND_BLUE_DARK = "#5b83f8";
115563
+ /** Brand blue (dark) RGB components, for composing rgba() tints. */
115564
+ const BRAND_BLUE_DARK_RGB = "91, 131, 248";
115565
+ /** Cool ink from the workbench foreground family. */
115566
+ const BRAND_INK = "#182033";
115567
+ /** Flat canvas surface; also feeds export backgrounds, so it stays solid. */
115568
+ const CANVAS_BG = "#e6ebf5";
115569
+ /** Canvas gradient stops (top and bottom); the mid stop is CANVAS_BG. */
115570
+ const CANVAS_BG_TOP = "#eef2fa";
115571
+ const CANVAS_BG_BOTTOM = "#dfe6f4";
115572
+ //#endregion
115573
+ //#region lib/types/client/tl/tl-brand-theme.js
115574
+ /** Re-tint the palette's "blue" family to the brand blue. */
115575
+ function brandBluePalette(colors, dark) {
115576
+ const blue = colors.blue;
115577
+ if (typeof blue !== "object" || blue === null) return colors;
115578
+ const solid = dark ? BRAND_BLUE_DARK : BRAND_BLUE;
115579
+ return {
115580
+ ...colors,
115581
+ blue: {
115582
+ ...blue,
115583
+ solid,
115584
+ fill: solid,
115585
+ linedFill: dark ? blue.linedFill : "#5a82f7",
115586
+ frameHeadingStroke: dark ? blue.frameHeadingStroke : "#6f8ff5",
115587
+ frameHeadingFill: dark ? blue.frameHeadingFill : "#f7f9ff",
115588
+ frameStroke: dark ? blue.frameStroke : "#6f8ff5",
115589
+ frameFill: dark ? blue.frameFill : "#f7f9ff",
115590
+ noteFill: dark ? blue.noteFill : "#91a9ff",
115591
+ semi: dark ? blue.semi : "#e3e9fd",
115592
+ pattern: dark ? blue.pattern : "#5a82f7"
115593
+ }
115594
+ };
115595
+ }
115596
+ /** Re-tint the special (string) theme colors: selection, marquee, ink, surface. */
115597
+ function brandSpecials(colors, dark) {
115598
+ const lightOnly = dark ? {} : {
115599
+ text: BRAND_INK,
115600
+ background: CANVAS_BG,
115601
+ negativeSpace: CANVAS_BG
115602
+ };
115603
+ return {
115604
+ ...colors,
115605
+ ...lightOnly,
115606
+ selectionStroke: dark ? BRAND_BLUE_DARK : BRAND_BLUE,
115607
+ selectionFill: `rgba(${dark ? BRAND_BLUE_DARK_RGB : BRAND_BLUE_RGB}, ${dark ? "0.22" : "0.15"})`,
115608
+ brushFill: dark ? "rgba(148, 163, 184, 0.12)" : "rgba(100, 116, 139, 0.1)",
115609
+ brushStroke: dark ? "rgba(148, 163, 184, 0.3)" : "rgba(100, 116, 139, 0.28)"
115610
+ };
115611
+ }
115612
+ /** Apply the brand theme patch to a freshly mounted tldraw editor. */
115613
+ function applyBrandTheme(editor) {
115614
+ const base = editor.getTheme("default");
115615
+ if (base === void 0) return;
115616
+ editor.updateTheme({
115617
+ ...base,
115618
+ colors: {
115619
+ light: brandBluePalette(brandSpecials(base.colors.light, false), false),
115620
+ dark: brandBluePalette(brandSpecials(base.colors.dark, true), true)
115621
+ }
115622
+ });
115623
+ }
115624
+ //#endregion
115470
115625
  //#region lib/types/client/tl/tl-canvas-bridge.js
115471
115626
  const items = [];
115472
115627
  const listeners = /* @__PURE__ */ new Set();
@@ -115772,6 +115927,15 @@ img.ProseMirror-separator {
115772
115927
  const MAX_DISPLAY_SIDE = 380;
115773
115928
  const GRID_GAP = 40;
115774
115929
  const GRID_COLS = 3;
115930
+ /**
115931
+ * Dock the stock toolbar vertically along the left edge (Figma-style) instead
115932
+ * of tldraw's signature bottom-center pill. It is still the same
115933
+ * DefaultToolbar with the same tools and overflow handling — only the
115934
+ * orientation changes, which is what makes the whole composition read as our
115935
+ * own surface rather than stock tldraw. TL_THEME_CSS offsets it from the
115936
+ * screen edge like the other floating cards.
115937
+ */
115938
+ const StudioToolbar = () => (0, react_jsx_runtime.jsx)(DefaultToolbar, { orientation: "vertical" });
115775
115939
  function tlAssetIdFor(galleryId) {
115776
115940
  return `asset:ig-${galleryId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
115777
115941
  }
@@ -115941,16 +116105,21 @@ img.ProseMirror-separator {
115941
116105
  (0, react.useEffect)(() => subscribeTlLandings(processQueue), [processQueue]);
115942
116106
  return (0, react_jsx_runtime.jsx)("div", {
115943
116107
  className: "dsh-ig-tl-canvas",
115944
- children: (0, react_jsx_runtime.jsx)(Tldraw, { onMount: (editor) => {
115945
- editorRef.current = editor;
115946
- console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
115947
- processQueue();
115948
- const stopSync = startCanvasSync(editor);
115949
- return () => {
115950
- stopSync();
115951
- editorRef.current = null;
115952
- };
115953
- } })
116108
+ children: (0, react_jsx_runtime.jsx)(Tldraw, {
116109
+ components: { Toolbar: StudioToolbar },
116110
+ onMount: (editor) => {
116111
+ editorRef.current = editor;
116112
+ applyBrandTheme(editor);
116113
+ editor.updateInstanceState({ isGridMode: true });
116114
+ console.info(`[dsh-image-gen] tldraw mounted (instance ${editor.id})`);
116115
+ processQueue();
116116
+ const stopSync = startCanvasSync(editor);
116117
+ return () => {
116118
+ stopSync();
116119
+ editorRef.current = null;
116120
+ };
116121
+ }
116122
+ })
115954
116123
  });
115955
116124
  });
115956
116125
  //#endregion
@@ -116057,7 +116226,8 @@ img.ProseMirror-separator {
116057
116226
  collapseGenerate: "折叠生成面板",
116058
116227
  expandGenerate: "展开生成面板",
116059
116228
  generatePanel: "生成面板",
116060
- findInspiration: "找灵感"
116229
+ findInspiration: "找灵感",
116230
+ infiniteCanvasHint: "无限画布与侧边栏会话联动,才是完整功能。打开 DSH 右侧边栏的「图像工作台」即可解锁。"
116061
116231
  },
116062
116232
  en: {
116063
116233
  title: "Cloud Image Studio",
@@ -116158,10 +116328,13 @@ img.ProseMirror-separator {
116158
116328
  collapseGenerate: "Collapse generate panel",
116159
116329
  expandGenerate: "Expand generate panel",
116160
116330
  generatePanel: "Generate",
116161
- findInspiration: "Find inspiration"
116331
+ findInspiration: "Find inspiration",
116332
+ infiniteCanvasHint: "The infinite canvas is at its best when linked with the sidebar conversation. Open Image Studio from the DSH right sidebar to unlock the full experience."
116162
116333
  }
116163
116334
  };
116164
- const StudioView = ({ locale, workspace, initialPrompt, initialCanvasSurface, onInitialPromptApplied, onOpenInspiration }) => {
116335
+ /** localStorage flag remembering that the one-time infinite-canvas hint was dismissed. */
116336
+ const INFINITE_CANVAS_HINT_KEY = "dsh-ig-infinite-canvas-hint-dismissed";
116337
+ const StudioView = ({ locale, workspace, initialPrompt, initialCanvasSurface, showInfiniteCanvasHint, onInitialPromptApplied, onOpenInspiration }) => {
116165
116338
  const [lang, setLang] = (0, react.useState)(() => locale?.getSnapshot?.().active?.startsWith("en") ? "en" : "zh");
116166
116339
  const [config, setConfig] = (0, react.useState)(null);
116167
116340
  const [configLoading, setConfigLoading] = (0, react.useState)(true);
@@ -116169,8 +116342,9 @@ img.ProseMirror-separator {
116169
116342
  const [items, setItems] = (0, react.useState)([]);
116170
116343
  const [sidebarCollapsed, setSidebarCollapsed] = (0, react.useState)(false);
116171
116344
  /** Manual-only collapse for the right generate form; the form stays put in
116172
- * narrow seats because it is the primary generation surface. */
116173
- const [generateCollapsed, setGenerateCollapsed] = (0, react.useState)(false);
116345
+ * narrow seats because it is the primary generation surface. Starts folded
116346
+ * when the studio opens straight into the infinite canvas. */
116347
+ const [generateCollapsed, setGenerateCollapsed] = (0, react.useState)(initialCanvasSurface === "infinite");
116174
116348
  /** Workbench root, observed so a narrow column (right sidebar) can fold the recent rail. */
116175
116349
  const workbenchRootRef = (0, react.useRef)(null);
116176
116350
  /** Set once the user toggles the rail by hand; auto-folding then stands down for this mount. */
@@ -116203,11 +116377,29 @@ img.ProseMirror-separator {
116203
116377
  const dragStartRef = (0, react.useRef)(null);
116204
116378
  const hasDraggedRef = (0, react.useRef)(false);
116205
116379
  const [canvasSurface, setCanvasSurface] = (0, react.useState)(initialCanvasSurface ?? "preview");
116380
+ /** Generate-panel state from before the infinite canvas was opened, so the
116381
+ * panel is restored (not just forced open) when returning to preview. */
116382
+ const generatePanelBeforeInfiniteRef = (0, react.useRef)(false);
116206
116383
  /** Mirrors `canvasSurface` for async reads: a generation started on the
116207
116384
  * infinite canvas must land there even if the user flips back to preview
116208
116385
  * while the request is in flight. */
116209
116386
  const canvasSurfaceRef = (0, react.useRef)(canvasSurface);
116210
116387
  canvasSurfaceRef.current = canvasSurface;
116388
+ /** One-time infinite-canvas entry hint, dismissed per-install (localStorage)
116389
+ * so it educates once instead of nagging on every visit. */
116390
+ const [canvasHintDismissed, setCanvasHintDismissed] = (0, react.useState)(() => {
116391
+ try {
116392
+ return localStorage.getItem(INFINITE_CANVAS_HINT_KEY) === "true";
116393
+ } catch {
116394
+ return false;
116395
+ }
116396
+ });
116397
+ const dismissCanvasHint = (0, react.useCallback)(() => {
116398
+ setCanvasHintDismissed(true);
116399
+ try {
116400
+ localStorage.setItem(INFINITE_CANVAS_HINT_KEY, "true");
116401
+ } catch {}
116402
+ }, []);
116211
116403
  const [dragging, setDragging] = (0, react.useState)(false);
116212
116404
  const [lightboxOpen, setLightboxOpen] = (0, react.useState)(false);
116213
116405
  const [showDeleteModal, setShowDeleteModal] = (0, react.useState)(false);
@@ -116905,7 +117097,6 @@ img.ProseMirror-separator {
116905
117097
  }) : displayItems.map((item) => (0, react_jsx_runtime.jsx)(RecentItem, {
116906
117098
  item,
116907
117099
  active: selected?.id === item.id,
116908
- lang,
116909
117100
  onClick: () => selectItem(item)
116910
117101
  }, item.id)), items.length > visibleLimit && (0, react_jsx_runtime.jsx)("button", {
116911
117102
  type: "button",
@@ -116920,68 +117111,72 @@ img.ProseMirror-separator {
116920
117111
  children: [
116921
117112
  (0, react_jsx_runtime.jsxs)("div", {
116922
117113
  className: "dsh-ig-canvas-toolbar",
116923
- children: [(0, react_jsx_runtime.jsxs)("div", {
116924
- className: "dsh-ig-canvas-toolbar-left",
116925
- children: [sidebarCollapsed && (0, react_jsx_runtime.jsxs)("button", {
116926
- type: "button",
116927
- onClick: () => {
116928
- railToggledByUserRef.current = true;
116929
- setSidebarCollapsed(false);
116930
- },
116931
- title: t("expandSidebar"),
116932
- children: [(0, react_jsx_runtime.jsx)(PanelLeft, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("recent") })]
116933
- }), (0, react_jsx_runtime.jsxs)("div", {
116934
- className: "dsh-ig-status-badge-wrap",
116935
- children: [(0, react_jsx_runtime.jsxs)("div", {
116936
- className: "dsh-ig-status-badge",
116937
- children: [
116938
- (0, react_jsx_runtime.jsx)("span", { className: `dsh-ig-status-dot ${configuredCount > 0 ? "is-ready" : "is-muted"}` }),
116939
- (0, react_jsx_runtime.jsx)("span", {
116940
- className: "dsh-ig-status-text",
116941
- children: configuredCount > 0 ? t("configuredCount", { count: String(configuredCount) }) : t("unconfiguredStatus")
116942
- }),
116943
- (0, react_jsx_runtime.jsx)(ChevronDown, {
116944
- size: 11,
116945
- className: "dsh-ig-status-arrow"
116946
- })
116947
- ]
116948
- }), config !== null && (0, react_jsx_runtime.jsxs)("div", {
116949
- className: "dsh-ig-status-popover",
116950
- children: [(0, react_jsx_runtime.jsx)("div", {
116951
- className: "dsh-ig-status-popover-title",
116952
- children: t("providerStatus")
116953
- }), (0, react_jsx_runtime.jsx)("ul", {
116954
- className: "dsh-ig-status-list",
116955
- children: config.providers.map((p) => (0, react_jsx_runtime.jsxs)("li", {
116956
- className: p.configured ? "is-configured" : "is-missing",
116957
- children: [(0, react_jsx_runtime.jsxs)("div", {
116958
- className: "dsh-ig-status-item-name",
116959
- children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: p.model })]
116960
- }), (0, react_jsx_runtime.jsx)("span", {
116961
- className: "dsh-ig-status-item-tag",
116962
- children: p.configured ? t("configured") : t("unconfigured")
116963
- })]
116964
- }, p.provider))
117114
+ children: [
117115
+ (0, react_jsx_runtime.jsxs)("div", {
117116
+ className: "dsh-ig-canvas-toolbar-left",
117117
+ children: [sidebarCollapsed && (0, react_jsx_runtime.jsxs)("button", {
117118
+ type: "button",
117119
+ onClick: () => {
117120
+ railToggledByUserRef.current = true;
117121
+ setSidebarCollapsed(false);
117122
+ },
117123
+ title: t("expandSidebar"),
117124
+ children: [(0, react_jsx_runtime.jsx)(PanelLeft, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("recent") })]
117125
+ }), (0, react_jsx_runtime.jsxs)("div", {
117126
+ className: "dsh-ig-status-badge-wrap",
117127
+ children: [(0, react_jsx_runtime.jsxs)("div", {
117128
+ className: "dsh-ig-status-badge",
117129
+ children: [
117130
+ (0, react_jsx_runtime.jsx)("span", { className: `dsh-ig-status-dot ${configuredCount > 0 ? "is-ready" : "is-muted"}` }),
117131
+ (0, react_jsx_runtime.jsx)("span", {
117132
+ className: "dsh-ig-status-text",
117133
+ children: configuredCount > 0 ? t("configuredCount", { count: String(configuredCount) }) : t("unconfiguredStatus")
117134
+ }),
117135
+ (0, react_jsx_runtime.jsx)(ChevronDown, {
117136
+ size: 11,
117137
+ className: "dsh-ig-status-arrow"
117138
+ })
117139
+ ]
117140
+ }), config !== null && (0, react_jsx_runtime.jsxs)("div", {
117141
+ className: "dsh-ig-status-popover",
117142
+ children: [(0, react_jsx_runtime.jsx)("div", {
117143
+ className: "dsh-ig-status-popover-title",
117144
+ children: t("providerStatus")
117145
+ }), (0, react_jsx_runtime.jsx)("ul", {
117146
+ className: "dsh-ig-status-list",
117147
+ children: config.providers.map((p) => (0, react_jsx_runtime.jsxs)("li", {
117148
+ className: p.configured ? "is-configured" : "is-missing",
117149
+ children: [(0, react_jsx_runtime.jsxs)("div", {
117150
+ className: "dsh-ig-status-item-name",
117151
+ children: [(0, react_jsx_runtime.jsx)("strong", { children: p.label }), (0, react_jsx_runtime.jsx)("small", { children: p.model })]
117152
+ }), (0, react_jsx_runtime.jsx)("span", {
117153
+ className: "dsh-ig-status-item-tag",
117154
+ children: p.configured ? t("configured") : t("unconfigured")
117155
+ })]
117156
+ }, p.provider))
117157
+ })]
116965
117158
  })]
116966
117159
  })]
116967
- })]
116968
- }), (0, react_jsx_runtime.jsxs)("div", {
116969
- className: "dsh-ig-canvas-toolbar-right",
116970
- children: [
116971
- selected !== null && (0, react_jsx_runtime.jsxs)("button", {
116972
- type: "button",
116973
- onClick: startNew,
116974
- title: t("newGeneration"),
116975
- children: [(0, react_jsx_runtime.jsx)(Plus, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("newGeneration") })]
116976
- }),
116977
- (0, react_jsx_runtime.jsxs)("button", {
117160
+ }),
117161
+ (0, react_jsx_runtime.jsxs)("div", {
117162
+ className: "dsh-ig-canvas-toolbar-center",
117163
+ children: [(0, react_jsx_runtime.jsxs)("button", {
116978
117164
  type: "button",
116979
117165
  className: canvasSurface === "infinite" ? "is-active" : "",
116980
- onClick: () => setCanvasSurface((surface) => surface === "infinite" ? "preview" : "infinite"),
117166
+ onClick: () => {
117167
+ if (canvasSurface === "infinite") {
117168
+ setGenerateCollapsed(generatePanelBeforeInfiniteRef.current);
117169
+ setCanvasSurface("preview");
117170
+ return;
117171
+ }
117172
+ generatePanelBeforeInfiniteRef.current = generateCollapsed;
117173
+ setGenerateCollapsed(true);
117174
+ setCanvasSurface("infinite");
117175
+ },
116981
117176
  title: t("infiniteCanvas"),
116982
117177
  children: [(0, react_jsx_runtime.jsx)(Frame, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("infiniteCanvas") })]
116983
- }),
116984
- canvasSurface === "preview" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
117178
+ }), canvasSurface === "preview" && (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
117179
+ (0, react_jsx_runtime.jsx)("span", { className: "dsh-ig-toolbar-divider" }),
116985
117180
  (0, react_jsx_runtime.jsxs)("button", {
116986
117181
  type: "button",
116987
117182
  className: fit ? "is-active" : "",
@@ -117013,25 +117208,50 @@ img.ProseMirror-separator {
117013
117208
  },
117014
117209
  title: "Zoom in",
117015
117210
  children: (0, react_jsx_runtime.jsx)(ZoomIn, { size: 16 })
117211
+ })
117212
+ ] })]
117213
+ }),
117214
+ (0, react_jsx_runtime.jsxs)("div", {
117215
+ className: "dsh-ig-canvas-toolbar-right",
117216
+ children: [
117217
+ canvasSurface === "preview" && selected !== null && (0, react_jsx_runtime.jsxs)("button", {
117218
+ type: "button",
117219
+ onClick: startNew,
117220
+ title: t("newGeneration"),
117221
+ children: [(0, react_jsx_runtime.jsx)(Plus, { size: 13 }), (0, react_jsx_runtime.jsx)("span", { children: t("newGeneration") })]
117016
117222
  }),
117017
- (0, react_jsx_runtime.jsx)("button", {
117223
+ canvasSurface === "preview" && (0, react_jsx_runtime.jsx)("button", {
117018
117224
  type: "button",
117019
117225
  onClick: () => setLightboxOpen(true),
117020
117226
  title: t("fullscreen"),
117021
117227
  disabled: selected === null,
117022
117228
  children: (0, react_jsx_runtime.jsx)(Expand, { size: 16 })
117229
+ }),
117230
+ generateCollapsed && (0, react_jsx_runtime.jsxs)("button", {
117231
+ type: "button",
117232
+ onClick: () => {
117233
+ setGenerateCollapsed(false);
117234
+ },
117235
+ title: t("expandGenerate"),
117236
+ children: [(0, react_jsx_runtime.jsx)(PanelRight, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("generatePanel") })]
117023
117237
  })
117024
- ] }),
117025
- generateCollapsed && (0, react_jsx_runtime.jsxs)("button", {
117026
- type: "button",
117027
- onClick: () => {
117028
- setGenerateCollapsed(false);
117029
- },
117030
- title: t("expandGenerate"),
117031
- children: [(0, react_jsx_runtime.jsx)(PanelRight, { size: 14 }), (0, react_jsx_runtime.jsx)("span", { children: t("generatePanel") })]
117032
- })
117033
- ]
117034
- })]
117238
+ ]
117239
+ })
117240
+ ]
117241
+ }),
117242
+ canvasSurface === "infinite" && showInfiniteCanvasHint && !canvasHintDismissed && (0, react_jsx_runtime.jsxs)("div", {
117243
+ className: "dsh-ig-canvas-hint",
117244
+ children: [
117245
+ (0, react_jsx_runtime.jsx)(Info, { size: 14 }),
117246
+ (0, react_jsx_runtime.jsx)("span", { children: t("infiniteCanvasHint") }),
117247
+ (0, react_jsx_runtime.jsx)("button", {
117248
+ type: "button",
117249
+ onClick: dismissCanvasHint,
117250
+ title: t("close"),
117251
+ "aria-label": t("close"),
117252
+ children: (0, react_jsx_runtime.jsx)(X, { size: 13 })
117253
+ })
117254
+ ]
117035
117255
  }),
117036
117256
  canvasSurface === "infinite" ? (0, react_jsx_runtime.jsx)(StudioTlCanvas, {}) : (0, react_jsx_runtime.jsx)("div", {
117037
117257
  className: `dsh-ig-canvas ${isDragging ? "is-dragging" : ""}`,
@@ -117782,31 +118002,23 @@ img.ProseMirror-separator {
117782
118002
  }
117783
118003
  ];
117784
118004
  }
117785
- const RecentItem = ({ item, active, lang, onClick }) => {
118005
+ const RecentItem = ({ item, active, onClick }) => {
117786
118006
  const [setRef, inView] = useInView();
117787
118007
  const image = useAttachmentImage(item.attachment, inView);
117788
- return (0, react_jsx_runtime.jsxs)("button", {
118008
+ return (0, react_jsx_runtime.jsx)("button", {
117789
118009
  ref: setRef,
117790
118010
  type: "button",
117791
118011
  className: `dsh-ig-recent-item ${active ? "is-active" : ""}`,
117792
118012
  onClick,
117793
- children: [(0, react_jsx_runtime.jsx)("div", {
118013
+ title: item.prompt,
118014
+ children: (0, react_jsx_runtime.jsx)("div", {
117794
118015
  className: "dsh-ig-recent-thumb",
117795
118016
  children: image.url !== null ? (0, react_jsx_runtime.jsx)("img", {
117796
118017
  src: image.url,
117797
118018
  alt: "",
117798
118019
  loading: "lazy"
117799
118020
  }) : (0, react_jsx_runtime.jsx)(ImagePlus, { size: 18 })
117800
- }), (0, react_jsx_runtime.jsxs)("div", { children: [
117801
- (0, react_jsx_runtime.jsx)("strong", { children: item.prompt }),
117802
- (0, react_jsx_runtime.jsx)("span", { children: formatRelativeTime$1(item.createdAt, lang) }),
117803
- (0, react_jsx_runtime.jsxs)("small", { children: [
117804
- item.provider,
117805
- " · ",
117806
- item.model
117807
- ] }),
117808
- (0, react_jsx_runtime.jsx)("small", { children: item.attachment.width && item.attachment.height ? `${item.attachment.width} × ${item.attachment.height}` : "—" })
117809
- ] })]
118021
+ })
117810
118022
  });
117811
118023
  };
117812
118024
  function useInView() {
@@ -119975,6 +120187,7 @@ img.ProseMirror-separator {
119975
120187
  workspace: activeWorkspace,
119976
120188
  initialPrompt: studioDraft,
119977
120189
  initialCanvasSurface,
120190
+ showInfiniteCanvasHint: !inSidebar,
119978
120191
  onInitialPromptApplied: clearStudioDraft,
119979
120192
  onOpenInspiration: () => setActiveTab("inspiration")
119980
120193
  })
@@ -120510,17 +120723,23 @@ img.ProseMirror-separator {
120510
120723
  .dsh-ig-count-badge{font-size:11px;min-width:22px;text-align:center;padding:2px 6px;border-radius:999px;background:var(--ig-soft);color:#7b8498;font-weight:500}
120511
120724
  .dsh-ig-collapse-btn{width:28px;height:28px;display:inline-flex;align-items:center;justify-content:center;border:1px solid transparent;border-radius:6px;background:transparent;color:#717b90;cursor:pointer;transition:all .15s}
120512
120725
  .dsh-ig-collapse-btn:hover{border-color:var(--ig-line);background:var(--ig-soft);color:var(--foreground,#182033)}
120513
- .dsh-ig-recent-scroll{flex:1 1 0;height:0;padding:8px;overflow-x:hidden;overflow-y:auto;min-height:0}.dsh-ig-recent-empty{min-height:180px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;color:#929bad;text-align:center;font-size:12px}
120514
- .dsh-ig-recent-item{display:grid;grid-template-columns:70px minmax(0,1fr);gap:10px;width:100%;padding:8px;border:1px solid transparent;border-radius:9px;background:transparent;text-align:left;cursor:pointer;transition:background .16s,border-color .16s,transform .16s}.dsh-ig-recent-item:hover{background:var(--ig-soft);transform:translateY(-1px)}.dsh-ig-recent-item.is-active{border-color:#89a7ff;background:rgba(47,100,245,.075);box-shadow:inset 3px 0 0 var(--ig-blue)}.dsh-ig-recent-thumb{width:70px;height:70px;border-radius:7px;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#eef1f7;color:#929bad}.dsh-ig-recent-thumb img{width:100%;height:100%;object-fit:cover}.dsh-ig-recent-item>div:last-child{min-width:0;padding:1px 0}.dsh-ig-recent-item strong{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;font-size:12.5px;line-height:19px;font-weight:630}.dsh-ig-recent-item span,.dsh-ig-recent-item small{display:block;white-space:nowrap;text-overflow:ellipsis;overflow:hidden;color:#828b9e;font-size:10.5px;line-height:16px}
120515
- .dsh-ig-load-more{width:100%;height:32px;margin:8px 0;display:flex;align-items:center;justify-content:center;border:1px dashed var(--ig-line);border-radius:7px;background:transparent;font-size:11px;color:#6b758b;cursor:pointer;transition:background .15s,color .15s}.dsh-ig-load-more:hover{background:var(--ig-soft);color:var(--ig-blue);border-color:#a2b9f6}
120726
+ .dsh-ig-recent-scroll{flex:1 1 0;height:0;padding:8px;overflow-x:hidden;overflow-y:auto;min-height:0;display:grid;grid-template-columns:repeat(auto-fill,minmax(78px,1fr));gap:6px;align-content:start}.dsh-ig-recent-empty{min-height:180px;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:10px;color:#929bad;text-align:center;font-size:12px;grid-column:1/-1}
120727
+ .dsh-ig-recent-item{position:relative;width:100%;aspect-ratio:1;align-self:start;padding:0;border:1px solid transparent;border-radius:8px;background:transparent;cursor:pointer;transition:border-color .16s,box-shadow .16s}.dsh-ig-recent-item:hover{border-color:#b9ccfa;box-shadow:0 1px 4px rgba(30,42,76,.14)}.dsh-ig-recent-item.is-active{border-color:var(--ig-blue);box-shadow:0 0 0 2px rgba(47,100,245,.18)}.dsh-ig-recent-thumb{width:100%;height:100%;border-radius:7px;display:flex;align-items:center;justify-content:center;overflow:hidden;background:#eef1f7;color:#929bad}.dsh-ig-recent-thumb img{width:100%;height:100%;object-fit:cover}
120728
+ .dsh-ig-load-more{grid-column:1/-1;width:100%;height:32px;margin:4px 0 2px;display:flex;align-items:center;justify-content:center;border:1px dashed var(--ig-line);border-radius:7px;background:transparent;font-size:11px;color:#6b758b;cursor:pointer;transition:background .15s,color .15s}.dsh-ig-load-more:hover{background:var(--ig-soft);color:var(--ig-blue);border-color:#a2b9f6}
120516
120729
  .dsh-ig-mini-btn{height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:3px;border:1px solid var(--ig-line);border-radius:5px;background:var(--background,#fff);font-size:10.5px;font-weight:500;color:var(--foreground,#242f44);cursor:pointer;transition:all .15s}.dsh-ig-mini-btn:hover{border-color:#9bb4f5;color:var(--ig-blue);background:rgba(47,100,245,.06)}
120517
120730
  .dsh-ig-canvas-column{min-width:0;min-height:0;height:100%;overflow:hidden;display:flex;flex-direction:column;background:color-mix(in srgb,var(--background,#fff) 97%,#e8edf7)}
120518
- .dsh-ig-canvas-toolbar{height:46px;flex:0 0 46px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 14px;border-bottom:1px solid var(--ig-line)}
120519
- .dsh-ig-canvas-toolbar-left,.dsh-ig-canvas-toolbar-right{display:flex;align-items:center;gap:6px}
120731
+ .dsh-ig-canvas-toolbar{height:46px;flex:0 0 46px;display:grid;grid-template-columns:1fr auto 1fr;align-items:center;gap:8px;padding:0 14px;border-bottom:1px solid var(--ig-line)}
120732
+ .dsh-ig-canvas-toolbar-left,.dsh-ig-canvas-toolbar-right{display:flex;align-items:center;gap:6px;min-width:0}
120733
+ .dsh-ig-canvas-toolbar-left{justify-self:start}
120734
+ .dsh-ig-canvas-toolbar-right{justify-self:end}
120735
+ .dsh-ig-canvas-toolbar-center{display:flex;align-items:center;justify-self:center;gap:6px;min-width:0}
120736
+ .dsh-ig-toolbar-divider{width:1px;height:16px;background:var(--ig-line);flex-shrink:0;margin:0 2px}
120520
120737
  .dsh-ig-canvas-toolbar button{height:30px;min-width:30px;padding:0 9px;display:inline-flex;align-items:center;justify-content:center;gap:5px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);color:var(--foreground,#242f44);font-size:11.5px;font-weight:500;cursor:pointer;transition:border-color .15s,background .15s,color .15s}
120521
120738
  .dsh-ig-canvas-toolbar button:hover{border-color:#9bb4f5;background:rgba(47,100,245,.05);color:var(--ig-blue)}
120522
120739
  .dsh-ig-canvas-toolbar button.is-active{border-color:#8ba8f8;background:rgba(47,100,245,.08);color:var(--ig-blue);font-weight:550}
120740
+ .dsh-ig-canvas-toolbar-center button.is-active{border-color:var(--ig-blue);background:rgba(47,100,245,.09);color:var(--ig-blue);font-weight:600;box-shadow:inset 0 0 0 1px rgba(47,100,245,.28)}
120523
120741
  .dsh-ig-zoom-display{font-size:11px;min-width:38px;text-align:center;color:#64748b;user-select:none;font-variant-numeric:tabular-nums}
120742
+ .dsh-ig-canvas-hint{flex:0 0 auto;display:flex;align-items:flex-start;gap:8px;padding:7px 12px;border-bottom:1px solid rgba(47,100,245,.14);background:rgba(47,100,245,.055);color:#3d4a66;font-size:11.5px;line-height:17px}.dsh-ig-canvas-hint>svg:first-child{flex-shrink:0;color:var(--ig-blue);margin-top:1px}.dsh-ig-canvas-hint>span{flex:1 1 0;min-width:0}.dsh-ig-canvas-hint>button{flex-shrink:0;width:22px;height:22px;display:inline-flex;align-items:center;justify-content:center;border:0;border-radius:5px;background:transparent;color:#8a94a8;cursor:pointer;transition:background .15s,color .15s}.dsh-ig-canvas-hint>button:hover{background:rgba(47,100,245,.1);color:var(--ig-blue)}
120524
120743
  .dsh-ig-status-badge-wrap{position:relative;display:inline-flex;align-items:center}
120525
120744
  .dsh-ig-status-badge{height:30px;display:inline-flex;align-items:center;gap:7px;padding:0 10px;border:1px solid var(--ig-line);border-radius:7px;background:var(--background,#fff);cursor:pointer;user-select:none;transition:border-color .15s,background .15s,box-shadow .15s}
120526
120745
  .dsh-ig-status-badge:hover{border-color:#9bb4f5;background:rgba(47,100,245,.04)}
@@ -120639,6 +120858,166 @@ img.ProseMirror-separator {
120639
120858
  //#region lib/types/client/tl/tl-css.js
120640
120859
  /** Bundled tldraw styles (tldraw 5.3.0, tldraw SDK license; the "Made with tldraw" watermark is part of the licensed default UI). Generated from tldraw/tldraw.css - do not edit by hand. */
120641
120860
  const TL_CSS = "/* THIS CSS FILE IS GENERATED! DO NOT EDIT. OR EDIT. I'M A COMMENT NOT A COP */ \n/* This file is created by the copy-css-files.mjs script in packages/tldraw. */\n/* It combines @tldraw/editor's editor.css and tldraw's ui.css */\n\n/* @tldraw/editor */\n\n.tl-container {\n width: 100%;\n height: 100%;\n font-size: 12px;\n /* Spacing */\n --tl-space-1: 2px;\n --tl-space-2: 4px;\n --tl-space-3: 8px;\n --tl-space-4: 12px;\n --tl-space-5: 16px;\n --tl-space-6: 20px;\n --tl-space-7: 28px;\n --tl-space-8: 32px;\n --tl-space-9: 64px;\n --tl-space-10: 72px;\n /* Radius */\n --tl-radius-0: 2px;\n --tl-radius-1: 4px;\n --tl-radius-2: 6px;\n --tl-radius-3: 9px;\n --tl-radius-4: 11px;\n\n /*\n * Canvas z-index\n *\n * .tl-canvas has `contain: strict` which creates its own stacking context.\n * background, grid, shapes, overlays, and blocker are all children of .tl-canvas,\n * so their z-indices are relative to the canvas, not the container.\n *\n * collaborators, watermark, and canvas-in-front are siblings of .tl-canvas\n * (outside it), so they compete with the UI layer (.tlui-layout at z-index 300).\n * canvas-in-front must be below the UI layer (300) to sit between\n * the canvas and the UI; collaborator cursors sit above canvas content\n * (and layers that anchor to it, like comment pins) but below the\n * in-front content and the UI panels. The watermark (a licensing element)\n * sits above that content so it can't be occluded, but still below the\n * in-front content and the UI.\n */\n --tl-layer-canvas-hidden: -999999;\n --tl-layer-canvas-background: 100;\n --tl-layer-canvas-grid: 150;\n --tl-layer-collaborators: 245;\n --tl-layer-watermark: 248;\n --tl-layer-canvas-in-front: 250;\n --tl-layer-canvas-shapes: 300;\n --tl-layer-canvas-overlays: 500;\n --tl-layer-canvas-blocker: 10000;\n\n /* Canvas overlays z-index */\n --tl-layer-overlays-collaborator-scribble: 10;\n --tl-layer-overlays-collaborator-brush: 20;\n --tl-layer-overlays-collaborator-shape-indicator: 30;\n --tl-layer-overlays-user-scribble: 40;\n --tl-layer-overlays-user-brush: 50;\n --tl-layer-overlays-user-snapline: 90;\n --tl-layer-overlays-selection-fg: 100;\n /* User handles need to be above selection edges / corners, matters for sticky note clone handles */\n --tl-layer-overlays-user-handles: 105;\n --tl-layer-overlays-user-indicator-hint: 110;\n --tl-layer-overlays-collaborator-cursor-hint: 120;\n --tl-layer-overlays-collaborator-cursor: 130;\n\n /* Text editor z-index */\n --tl-layer-text-container: 1;\n --tl-layer-text-content: 3;\n --tl-layer-text-editor: 4;\n\n /* Error fallback z-index */\n --tl-layer-error-overlay: 1;\n --tl-layer-error-canvas: 2;\n --tl-layer-error-canvas-after: 3;\n --tl-layer-error-content: 4;\n\n /* Misc */\n --tl-zoom: 1;\n --tl-tab-size: 2;\n\n /* Cursor SVGs */\n --tl-cursor-none: none;\n --tl-cursor-default:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m12 24.4219v-16.015l11.591 11.619h-6.781l-.411.124z' fill='white'/><path d='m21.0845 25.0962-3.605 1.535-4.682-11.089 3.686-1.553z' fill='white'/><path d='m19.751 24.4155-1.844.774-3.1-7.374 1.841-.775z' fill='black'/><path d='m13 10.814v11.188l2.969-2.866.428-.139h4.768z' fill='black'/></g></svg>\")\n 12 8,\n default;\n --tl-cursor-pointer:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m13.3315 21.3799c-.284-.359-.629-1.093-1.243-1.984-.348-.504-1.211-1.453-1.468-1.935-.223-.426-.199-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.307.302.459.214.121-.071-.496-.187-1.343-.355-2.092-.128-.568-.159-.657-.281-1.093-.129-.464-.195-.789-.316-1.281-.084-.348-.235-1.059-.276-1.459-.057-.547-.087-1.439.264-1.849.275-.321.906-.418 1.297-.22.512.259.803 1.003.936 1.3.239.534.387 1.151.516 1.961.164 1.031.466 2.462.476 2.763.024-.369-.068-1.146-.004-1.5.058-.321.328-.694.666-.795.286-.085.621-.116.916-.055.313.064.643.288.766.499.362.624.369 1.899.384 1.831.086-.376.071-1.229.284-1.584.14-.234.497-.445.687-.479.294-.052.655-.068.964-.008.249.049.586.345.677.487.218.344.342 1.317.379 1.658.015.141.074-.392.293-.736.406-.639 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.012.828-.04 1.202-.031.4-.117 1.304-.242 1.742-.086.301-.371.978-.652 1.384 0 0-1.074 1.25-1.191 1.813-.118.562-.079.566-.102.965-.023.398.121.922.121.922s-.802.104-1.234.035c-.391-.063-.875-.841-1-1.079-.172-.328-.539-.265-.682-.023-.225.383-.709 1.07-1.051 1.113-.668.084-2.054.031-3.139.02 0 0 .185-1.011-.227-1.358-.305-.259-.83-.784-1.144-1.06z' fill='white'/><g stroke='black' stroke-linecap='round' stroke-width='.75'><path d='m13.3315 21.3799c-.284-.359-.629-1.093-1.243-1.984-.348-.504-1.211-1.453-1.468-1.935-.223-.426-.199-.617-.146-.97.094-.628.738-1.117 1.425-1.051.519.049.959.392 1.355.716.239.195.533.574.71.788.163.196.203.277.377.509.23.307.302.459.214.121-.071-.496-.187-1.343-.355-2.092-.128-.568-.159-.657-.281-1.093-.129-.464-.195-.789-.316-1.281-.084-.348-.235-1.059-.276-1.459-.057-.547-.087-1.439.264-1.849.275-.321.906-.418 1.297-.22.512.259.803 1.003.936 1.3.239.534.387 1.151.516 1.961.164 1.031.466 2.462.476 2.763.024-.369-.068-1.146-.004-1.5.058-.321.328-.694.666-.795.286-.085.621-.116.916-.055.313.064.643.288.766.499.362.624.369 1.899.384 1.831.086-.376.071-1.229.284-1.584.14-.234.497-.445.687-.479.294-.052.655-.068.964-.008.249.049.586.345.677.487.218.344.342 1.317.379 1.658.015.141.074-.392.293-.736.406-.639 1.843-.763 1.898.639.025.654.02.624.02 1.064 0 .517-.012.828-.04 1.202-.031.4-.117 1.304-.242 1.742-.086.301-.371.978-.652 1.384 0 0-1.074 1.25-1.191 1.813-.118.562-.079.566-.102.965-.023.398.121.922.121.922s-.802.104-1.234.035c-.391-.063-.875-.841-1-1.079-.172-.328-.539-.265-.682-.023-.225.383-.709 1.07-1.051 1.113-.668.084-2.054.031-3.139.02 0 0 .185-1.011-.227-1.358-.305-.259-.83-.784-1.144-1.06z' stroke-linejoin='round'/><path d='m21.5664 21.7344v-3.459'/><path d='m19.5508 21.7461-.016-3.473'/><path d='m17.5547 18.3047.021 3.426'/></g></g></svg>\")\n 14 10,\n pointer;\n --tl-cursor-cross:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m25 16h-6.01v-6h-2.98v6h-6.01v3h6.01v6h2.98v-6h6.01z' fill='white'/><path d='m23.9902 17.0103h-6v-6.01h-.98v6.01h-6v.98h6v6.01h.98v-6.01h6z' fill='%23231f1f'/></g></svg>\")\n 16 16,\n crosshair;\n --tl-cursor-comment:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g filter='url(%23shadow)'><path d='M5 19.5 L5 11 A8 8 0 0 1 13 3 A8 8 0 0 1 21 11 A8 8 0 0 1 13 19 Z' fill='white' stroke='black' stroke-width='1' stroke-linejoin='round'/></g></svg>\")\n 5 19,\n crosshair;\n --tl-cursor-move:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m19 14h1v1h-1zm1 6h-1v-1h1zm-5-5h-1v-1h1zm0 5h-1v-1h1zm2-10.987-7.985 7.988 5.222 5.221 2.763 2.763 7.984-7.985z' fill='white'/><g fill='black'><path d='m23.5664 16.9971-2.557-2.809v1.829h-4.009-4.001v-1.829l-2.571 2.809 2.572 2.808-.001-1.808h4.001 4.009l-.001 1.808z'/><path d='m17.9873 17h.013v-4.001l1.807.001-2.807-2.571-2.809 2.57h1.809v4.001h.008v4.002l-1.828-.001 2.807 2.577 2.805-2.576h-1.805z'/></g></g></svg>\")\n 16 16,\n move;\n --tl-cursor-grab:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m13.5557 17.5742c-.098-.375-.196-.847-.406-1.552-.167-.557-.342-.859-.47-1.233-.155-.455-.303-.721-.496-1.181-.139-.329-.364-1.048-.457-1.44-.119-.509.033-.924.244-1.206.253-.339.962-.49 1.357-.351.371.13.744.512.916.788.288.46.357.632.717 1.542.393.992.564 1.918.611 2.231l.085.452c-.001-.04-.043-1.122-.044-1.162-.035-1.029-.06-1.823-.038-2.939.002-.126.064-.587.084-.715.078-.5.305-.8.673-.979.412-.201.926-.215 1.401-.017.423.173.626.55.687 1.022.014.109.094.987.093 1.107-.013 1.025.006 1.641.015 2.174.004.231.003 1.625.017 1.469.061-.656.094-3.189.344-3.942.144-.433.405-.746.794-.929.431-.203 1.113-.07 1.404.243.285.305.446.692.482 1.153.032.405-.019.897-.02 1.245 0 .867-.021 1.324-.037 2.121-.001.038-.015.298.023.182.094-.28.188-.542.266-.745.049-.125.241-.614.359-.859.114-.234.211-.369.415-.688.2-.313.415-.448.668-.561.54-.235 1.109.112 1.301.591.086.215.009.713-.028 1.105-.061.647-.254 1.306-.352 1.648-.128.447-.274 1.235-.34 1.601-.072.394-.234 1.382-.359 1.82-.086.301-.371.978-.652 1.384 0 0-1.074 1.25-1.192 1.812-.117.563-.078.567-.101.965-.024.399.121.923.121.923s-.802.104-1.234.034c-.391-.062-.875-.841-1-1.078-.172-.328-.539-.265-.682-.023-.225.383-.709 1.07-1.051 1.113-.668.084-2.054.03-3.139.02 0 0 .185-1.011-.227-1.358-.305-.26-.83-.784-1.144-1.06l-.832-.921c-.284-.36-.629-1.093-1.243-1.985-.348-.504-1.027-1.085-1.284-1.579-.223-.425-.331-.954-.19-1.325.225-.594.675-.897 1.362-.832.519.05.848.206 1.238.537.225.19.573.534.75.748.163.195.203.276.377.509.23.307.302.459.214.121' fill='white'/><g stroke='black' stroke-linecap='round' stroke-width='.75'><path d='m13.5557 17.5742c-.098-.375-.196-.847-.406-1.552-.167-.557-.342-.859-.47-1.233-.155-.455-.303-.721-.496-1.181-.139-.329-.364-1.048-.457-1.44-.119-.509.033-.924.244-1.206.253-.339.962-.49 1.357-.351.371.13.744.512.916.788.288.46.357.632.717 1.542.393.992.564 1.918.611 2.231l.085.452c-.001-.04-.043-1.122-.044-1.162-.035-1.029-.06-1.823-.038-2.939.002-.126.064-.587.084-.715.078-.5.305-.8.673-.979.412-.201.926-.215 1.401-.017.423.173.626.55.687 1.022.014.109.094.987.093 1.107-.013 1.025.006 1.641.015 2.174.004.231.003 1.625.017 1.469.061-.656.094-3.189.344-3.942.144-.433.405-.746.794-.929.431-.203 1.113-.07 1.404.243.285.305.446.692.482 1.153.032.405-.019.897-.02 1.245 0 .867-.021 1.324-.037 2.121-.001.038-.015.298.023.182.094-.28.188-.542.266-.745.049-.125.241-.614.359-.859.114-.234.211-.369.415-.688.2-.313.415-.448.668-.561.54-.235 1.109.112 1.301.591.086.215.009.713-.028 1.105-.061.647-.254 1.306-.352 1.648-.128.447-.274 1.235-.34 1.601-.072.394-.234 1.382-.359 1.82-.086.301-.371.978-.652 1.384 0 0-1.074 1.25-1.192 1.812-.117.563-.078.567-.101.965-.024.399.121.923.121.923s-.802.104-1.234.034c-.391-.062-.875-.841-1-1.078-.172-.328-.539-.265-.682-.023-.225.383-.709 1.07-1.051 1.113-.668.084-2.054.03-3.139.02 0 0 .185-1.011-.227-1.358-.305-.26-.83-.784-1.144-1.06l-.832-.921c-.284-.36-.629-1.093-1.243-1.985-.348-.504-1.027-1.085-1.284-1.579-.223-.425-.331-.954-.19-1.325.225-.594.675-.897 1.362-.832.519.05.848.206 1.238.537.225.19.573.534.75.748.163.195.203.276.377.509.23.307.302.459.214.121' stroke-linejoin='round'/><path d='m20.5664 21.7344v-3.459'/><path d='m18.5508 21.7461-.016-3.473'/><path d='m16.5547 18.3047.021 3.426'/></g></g></svg>\")\n 16 16,\n grab;\n --tl-cursor-grabbing:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m13.5732 12.0361c.48-.178 1.427-.069 1.677.473.213.462.396 1.241.406 1.075.024-.369-.024-1.167.137-1.584.117-.304.347-.59.686-.691.285-.086.62-.116.916-.055.313.064.642.287.765.499.362.623.368 1.899.385 1.831.064-.272.07-1.229.283-1.584.141-.235.497-.445.687-.479.294-.052.656-.068.964-.008.249.049.586.344.677.487.219.344.342 1.316.379 1.658.016.141.074-.393.293-.736.406-.639 1.844-.763 1.898.639.026.654.02.624.02 1.064 0 .516-.012.828-.04 1.202-.03.399-.116 1.304-.241 1.742-.086.301-.371.978-.653 1.384 0 0-1.074 1.25-1.191 1.812-.117.563-.078.567-.102.965-.023.399.121.923.121.923s-.801.104-1.234.034c-.391-.062-.875-.84-1-1.078-.172-.328-.539-.265-.682-.023-.224.383-.709 1.07-1.05 1.113-.669.084-2.055.03-3.14.02 0 0 .185-1.011-.227-1.358-.305-.26-.83-.784-1.144-1.06l-.832-.921c-.283-.36-1.002-.929-1.243-1.985-.213-.936-.192-1.395.037-1.77.232-.381.67-.589.854-.625.208-.042.692-.039.875.062.223.123.313.159.488.391.23.307.312.456.213.121-.076-.262-.322-.595-.434-.97-.109-.361-.401-.943-.38-1.526.008-.221.103-.771.832-1.042' fill='white'/><g stroke='black' stroke-width='.75'><path d='m13.5732 12.0361c.48-.178 1.427-.069 1.677.473.213.462.396 1.241.406 1.075.024-.369-.024-1.167.137-1.584.117-.304.347-.59.686-.691.285-.086.62-.116.916-.055.313.064.642.287.765.499.362.623.368 1.899.385 1.831.064-.272.07-1.229.283-1.584.141-.235.497-.445.687-.479.294-.052.656-.068.964-.008.249.049.586.344.677.487.219.344.342 1.316.379 1.658.016.141.074-.393.293-.736.406-.639 1.844-.763 1.898.639.026.654.02.624.02 1.064 0 .516-.012.828-.04 1.202-.03.399-.116 1.304-.241 1.742-.086.301-.371.978-.653 1.384 0 0-1.074 1.25-1.191 1.812-.117.563-.078.567-.102.965-.023.399.121.923.121.923s-.801.104-1.234.034c-.391-.062-.875-.84-1-1.078-.172-.328-.539-.265-.682-.023-.224.383-.709 1.07-1.05 1.113-.669.084-2.055.03-3.14.02 0 0 .185-1.011-.227-1.358-.305-.26-.83-.784-1.144-1.06l-.832-.921c-.283-.36-1.002-.929-1.243-1.985-.213-.936-.192-1.395.037-1.77.232-.381.67-.589.854-.625.208-.042.692-.039.875.062.223.123.313.159.488.391.23.307.312.456.213.121-.076-.262-.322-.595-.434-.97-.109-.361-.401-.943-.38-1.526.008-.221.103-.771.832-1.042z' stroke-linejoin='round'/><path d='m20.5664 19.7344v-3.459' stroke-linecap='round'/><path d='m18.5508 19.7461-.016-3.473' stroke-linecap='round'/><path d='m16.5547 16.3047.021 3.426' stroke-linecap='round'/></g></g></svg>\")\n 16 16,\n grabbing;\n --tl-cursor-text:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path fill='white' d='M7.94 0a5.25 5.25 0 0 0-3.47 1.17A5.27 5.27 0 0 0 1 0H0v3h1c1.41 0 1.85.7 2 1v3.94H2v3h1v3c-.13.3-.57 1-2 1H0v3h1a5.27 5.27 0 0 0 3.47-1.17c.98.8 2.21 1.21 3.47 1.17h1v-3h-1c-1.41 0-1.85-.7-2-1v-3H7v-3H6V4c.13-.3.57-1 2-1h1V0H7.94z'/><path fill='black' d='M7.94 2V1a4 4 0 0 0-3.47 1.64A4 4 0 0 0 1 1v1c1.3-.17 2.56.6 3 1.84v5.1H3v1h1v4.16c-.45 1.24-1.7 2-3 1.84v1a4.05 4.05 0 0 0 3.47-1.63 4.05 4.05 0 0 0 3.47 1.63v-1A2.82 2.82 0 0 1 5 14.1V9.93h1v-1H5V3.85A2.81 2.81 0 0 1 7.94 2z'/></g></svg>\")\n 4 10,\n text;\n --tl-cursor-zoom-in:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m20.5 15c0 3.038-2.462 5.5-5.5 5.5s-5.5-2.462-5.5-5.5 2.462-5.5 5.5-5.5 5.5 2.462 5.5 5.5' fill='white'/><path d='m20.5 15c0 3.038-2.462 5.5-5.5 5.5s-5.5-2.462-5.5-5.5 2.462-5.5 5.5-5.5 5.5 2.462 5.5 5.5z' stroke='black'/><g fill='black'><path d='m18 14h-2v-2h-2v2h-2v1.98h2v2.02h2v-2.02h2z'/><path d='m23.5859 25 1.414-1.414-5.449-5.449-1.414 1.414z'/></g></g></svg>\")\n 16 16,\n zoom-in;\n --tl-cursor-zoom-out:\n url(\"data:image/svg+xml,<svg height='32' width='32' viewBox='0 0 32 32' xmlns='http://www.w3.org/2000/svg' style='color: black;'><defs><filter id='shadow' y='-40%' x='-40%' width='180px' height='180%' color-interpolation-filters='sRGB'><feDropShadow dx='1' dy='1' stdDeviation='1.2' flood-opacity='.5'/></filter></defs><g fill='none' transform='rotate(0 16 16)' filter='url(%23shadow)'><path d='m20.5 15c0 3.038-2.462 5.5-5.5 5.5s-5.5-2.462-5.5-5.5 2.462-5.5 5.5-5.5 5.5 2.462 5.5 5.5' fill='white'/><path d='m20.5 15c0 3.038-2.462 5.5-5.5 5.5s-5.5-2.462-5.5-5.5 2.462-5.5 5.5-5.5 5.5 2.462 5.5 5.5z' stroke='black'/><g fill='black'><path d='m18 16h-5.98v-1.98h5.98z'/><path d='m23.5859 25 1.414-1.414-5.449-5.449-1.414 1.414z'/></g></g></svg>\")\n 16 16,\n zoom-out;\n\n /* These cursor values get programmatically overridden */\n /* They're just here to help your editor autocomplete */\n --tl-cursor: var(--tl-cursor-default);\n --tl-cursor-resize-edge: ew-resize;\n --tl-cursor-resize-corner: nesw-resize;\n --tl-cursor-ew-resize: ew-resize;\n --tl-cursor-ns-resize: ns-resize;\n --tl-cursor-nesw-resize: nesw-resize;\n --tl-cursor-nwse-resize: nwse-resize;\n --tl-cursor-rotate: pointer;\n --tl-cursor-nwse-rotate: pointer;\n --tl-cursor-nesw-rotate: pointer;\n --tl-cursor-senw-rotate: pointer;\n --tl-cursor-swne-rotate: pointer;\n --tl-scale: calc(1 / var(--tl-zoom));\n /* fonts */\n --tl-font-draw: 'tldraw_draw', sans-serif;\n --tl-font-sans: 'tldraw_sans', sans-serif;\n --tl-font-serif: 'tldraw_serif', serif;\n --tl-font-mono: 'tldraw_mono', monospace;\n /* text outline */\n --tl-text-outline-a: calc(min(0.5, 1 / var(--tl-zoom)) * 2px);\n --tl-text-outline-b: calc(min(0.5, 1 / var(--tl-zoom)) * -2px);\n --tl-text-outline-reference:\n 0 var(--tl-text-outline-b) 0 var(--tl-color-background),\n 0 var(--tl-text-outline-a) 0 var(--tl-color-background),\n var(--tl-text-outline-b) var(--tl-text-outline-b) 0 var(--tl-color-background),\n var(--tl-text-outline-a) var(--tl-text-outline-b) 0 var(--tl-color-background),\n var(--tl-text-outline-a) var(--tl-text-outline-a) 0 var(--tl-color-background),\n var(--tl-text-outline-b) var(--tl-text-outline-a) 0 var(--tl-color-background);\n --tl-text-outline: var(--tl-text-outline-reference);\n /* own properties */\n position: relative;\n inset: 0px;\n height: 100%;\n width: 100%;\n overflow: clip;\n color: var(--tl-color-text);\n}\n\n.tl-theme__light {\n /* Canvas */\n --tl-color-selection-fill: hsl(210, 100%, 56%, 24%);\n --tl-color-selection-stroke: hsl(214, 84%, 56%);\n --tl-color-background: hsl(210, 20%, 98%);\n /* if you ever update --tl-color-background, update the hardcoded values in theme-init.js and globals.css\n they're there to make sure the background color matches the user's preference before the actual app loads*/\n --tl-color-grid: hsl(0, 0%, 43%);\n /* UI */\n --tl-color-low: hsl(204, 16%, 94%);\n --tl-color-low-border: hsl(204, 16%, 92%);\n --tl-color-muted-none: hsl(0, 0%, 0%, 0%);\n --tl-color-muted-0: hsl(0, 0%, 0%, 2%);\n --tl-color-muted-1: hsl(0, 0%, 0%, 10%);\n --tl-color-muted-2: hsl(0, 0%, 0%, 4.3%);\n --tl-color-hint: hsl(0, 0%, 0%, 5.5%);\n --tl-color-overlay: hsl(0, 0%, 0%, 20%);\n --tl-color-divider: hsl(0, 0%, 91%);\n --tl-color-panel: hsl(0, 0%, 99%);\n --tl-color-panel-contrast: hsl(0, 0%, 100%);\n --tl-color-panel-overlay: hsl(0, 0%, 100%, 82%);\n --tl-color-panel-transparent: hsla(0, 0%, 99%, 0%);\n --tl-color-selected: hsl(214, 84%, 56%);\n --tl-color-selected-contrast: hsl(0, 0%, 100%);\n --tl-color-focus: hsl(219, 65%, 50%);\n --tl-color-tooltip: hsla(200, 14%, 4%, 1);\n /* Text */\n --tl-color-text: hsl(0, 0%, 0%);\n --tl-color-text-0: hsl(0, 0%, 11%);\n --tl-color-text-1: hsl(0, 0%, 18%);\n --tl-color-text-3: hsl(204, 4%, 45%);\n --tl-color-text-disabled: hsl(204, 4%, 55%);\n --tl-color-text-shadow: hsl(0, 0%, 100%);\n --tl-color-text-highlight: hsl(52, 100%, 50%);\n --tl-color-text-highlight-p3: color(display-p3 0.972 0.8205 0.05);\n /* Named */\n --tl-color-primary: hsl(214, 84%, 56%);\n --tl-color-success: hsl(123, 46%, 34%);\n --tl-color-info: hsl(201, 98%, 41%);\n --tl-color-warning: hsl(27, 98%, 47%);\n --tl-color-danger: hsl(0, 90%, 43%);\n\n /* Shadows */\n --tl-shadow-1: 0px 1px 2px hsl(0, 0%, 0%, 25%), 0px 1px 3px hsl(0, 0%, 0%, 9%);\n --tl-shadow-2:\n 0px 0px 2px hsl(0, 0%, 0%, 16%), 0px 2px 3px hsl(0, 0%, 0%, 24%),\n 0px 2px 6px hsl(0, 0%, 0%, 0.1), inset 0px 0px 0px 1px var(--tl-color-panel-contrast);\n --tl-shadow-3:\n 0px 1px 2px hsl(0, 0%, 0%, 28%), 0px 2px 6px hsl(0, 0%, 0%, 14%),\n inset 0px 0px 0px 1px var(--tl-color-panel-contrast);\n --tl-shadow-4:\n 0px 0px 3px hsl(0, 0%, 0%, 19%), 0px 5px 4px hsl(0, 0%, 0%, 16%),\n 0px 2px 16px hsl(0, 0%, 0%, 6%), inset 0px 0px 0px 1px var(--tl-color-panel-contrast);\n}\n\n.tl-theme__dark {\n /* Canvas */\n --tl-color-selection-fill: hsl(209, 100%, 57%, 20%);\n --tl-color-selection-stroke: hsl(214, 84%, 56%);\n --tl-color-background: hsl(240, 5%, 6.5%);\n /* if you ever update --tl-color-background, update the hardcoded values in theme-init.js and globals.css\n they're there to make sure the background color matches the user's preference before the actual app loads*/\n --tl-color-grid: hsl(0, 0%, 40%);\n /* UI */\n --tl-color-low: hsl(260, 4.5%, 10.5%);\n --tl-color-low-border: hsl(207, 10%, 10%);\n --tl-color-muted-none: hsl(0, 0%, 100%, 0%);\n --tl-color-muted-0: hsl(0, 0%, 100%, 2%);\n --tl-color-muted-1: hsl(0, 0%, 100%, 10%);\n --tl-color-muted-2: hsl(0, 0%, 100%, 5%);\n --tl-color-hint: hsl(0, 0%, 100%, 7%);\n --tl-color-overlay: hsl(0, 0%, 0%, 50%);\n --tl-color-divider: hsl(240, 9%, 22%);\n --tl-color-panel: hsl(235, 6.8%, 13.5%);\n --tl-color-panel-contrast: hsl(245, 12%, 23%);\n --tl-color-panel-overlay: hsl(210, 10%, 24%, 82%);\n --tl-color-panel-transparent: hsla(235, 6.8%, 13.5%, 0%);\n --tl-color-selected: hsl(217, 89%, 61%);\n --tl-color-selected-contrast: hsl(0, 0%, 100%);\n --tl-color-focus: hsl(217, 76%, 80%);\n --tl-color-tooltip: hsla(0, 0%, 100%, 1);\n /* Text */\n --tl-color-text: hsl(210, 17%, 98%);\n --tl-color-text-0: hsl(0, 9%, 94%);\n --tl-color-text-1: hsl(0, 0%, 85%);\n --tl-color-text-3: hsl(204, 4%, 75%);\n --tl-color-text-disabled: hsl(204, 4%, 50%);\n --tl-color-text-shadow: hsl(210, 13%, 18%);\n --tl-color-text-highlight: hsl(52, 100%, 41%);\n --tl-color-text-highlight-p3: color(display-p3 0.8078 0.6225 0.0312);\n /* Named */\n --tl-color-primary: hsl(214, 84%, 56%);\n --tl-color-success: hsl(123, 38%, 57%);\n --tl-color-info: hsl(199, 92%, 56%);\n --tl-color-warning: hsl(36, 100%, 57%);\n --tl-color-danger: hsl(0, 82%, 66%);\n\n /* Shadows */\n --tl-shadow-1:\n 0px 1px 2px hsl(0, 0%, 0%, 16.1%), 0px 1px 3px hsl(0, 0%, 0%, 22%),\n inset 0px 0px 0px 1px var(--tl-color-panel-contrast);\n --tl-shadow-2:\n 0px 1px 3px hsl(0, 0%, 0%, 66.6%), 0px 2px 6px hsl(0, 0%, 0%, 33%),\n inset 0px 0px 0px 1px var(--tl-color-panel-contrast);\n --tl-shadow-3:\n 0px 1px 3px hsl(0, 0%, 0%, 50%), 0px 2px 12px hsl(0, 0%, 0%, 50%),\n inset 0px 0px 0px 1px var(--tl-color-panel-contrast);\n}\n\n.tl-counter-scaled {\n transform: scale(var(--tl-scale));\n transform-origin: top left;\n width: calc(100% * var(--tl-zoom));\n height: calc(100% * var(--tl-zoom));\n}\n\n.tl-container,\n.tl-container * {\n -webkit-touch-callout: none;\n -webkit-tap-highlight-color: transparent;\n scrollbar-highlight-color: transparent;\n -webkit-user-select: none;\n user-select: none;\n box-sizing: border-box;\n outline: none;\n}\n\n.tl-container a {\n -webkit-touch-callout: initial;\n}\n\n.tl-container__focused {\n outline: 1px solid var(--tl-color-low);\n}\n\n/* The host for <EditorPortal>, last among the container's children. It's a position in the DOM\n rather than a box: `display: contents` keeps what it holds positioned against the container. */\n.tl-portal-host {\n display: contents;\n}\n\ninput,\n*[contenteditable],\n*[contenteditable] * {\n user-select: text;\n}\n\n/* --------------------- Canvas --------------------- */\n\n.tl-canvas {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n color: var(--tl-color-text);\n cursor: var(--tl-cursor);\n overflow: clip;\n content-visibility: auto;\n touch-action: none;\n contain: strict;\n}\n\n.tl-canvas__in-front {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: var(--tl-layer-canvas-in-front);\n}\n\n.tl-shapes {\n position: relative;\n z-index: var(--tl-layer-canvas-shapes);\n}\n\n.tl-svg-context {\n position: absolute;\n top: 0px;\n left: 0px;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n/* ------------------- Background ------------------- */\n\n.tl-background__wrapper {\n z-index: var(--tl-layer-canvas-background);\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n}\n\n.tl-background {\n background-color: var(--tl-color-background);\n width: 100%;\n height: 100%;\n}\n\n/* --------------------- Grid Layer --------------------- */\n\n.tl-grid {\n position: absolute;\n inset: 0px;\n width: 100%;\n height: 100%;\n touch-action: none;\n pointer-events: none;\n z-index: var(--tl-layer-canvas-grid);\n contain: strict;\n}\n\n.tl-grid-dot {\n fill: var(--tl-color-grid);\n}\n\n/* --------------------- Layers --------------------- */\n\n.tl-html-layer {\n position: absolute;\n top: 0px;\n left: 0px;\n width: 1px;\n height: 1px;\n contain: layout style size;\n}\n\n/* The collaborator cursor layer: a sibling of .tl-canvas, above all canvas content, below the\n in-front layer (250) and the UI panels (300). Display-only — pointer events pass through. */\n.tl-collaborators {\n position: absolute;\n inset: 0px;\n z-index: var(--tl-layer-collaborators);\n pointer-events: none;\n overflow: clip;\n contain: layout style size;\n}\n\n.tl-collaborators__item {\n position: absolute;\n top: 0px;\n left: 0px;\n overflow: visible;\n pointer-events: none;\n transform-origin: top left;\n}\n\n.tl-cursor {\n overflow: visible;\n}\n\n/* --------------- Overlay Stack --------------- */\n\n/* -------------- Selection foreground -------------- */\n\n.tl-selection__bg {\n position: absolute;\n top: 0px;\n left: 0px;\n transform-origin: top left;\n background-color: transparent;\n pointer-events: all;\n}\n\n.tl-transparent {\n fill: transparent;\n stroke: transparent;\n}\n\n.tl-hidden {\n opacity: 0;\n pointer-events: none;\n}\n\n/* -------------- Nametag / cursor chat ------------- */\n\n.tl-nametag {\n position: absolute;\n top: 16px;\n left: 13px;\n width: fit-content;\n height: fit-content;\n max-width: 120px;\n padding: 3px 6px;\n white-space: nowrap;\n position: absolute;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 12px;\n border-radius: var(--tl-radius-2);\n color: var(--tl-color-selected-contrast);\n}\n\n.tl-nametag-title {\n position: absolute;\n top: -2px;\n left: 13px;\n width: fit-content;\n height: fit-content;\n padding: 0px 6px;\n max-width: 120px;\n white-space: nowrap;\n position: absolute;\n overflow: hidden;\n text-overflow: ellipsis;\n font-size: 12px;\n text-shadow: var(--tl-text-outline);\n color: var(--tl-color-selected-contrast);\n}\n\n.tl-nametag-chat {\n position: absolute;\n top: 16px;\n left: 13px;\n width: fit-content;\n height: fit-content;\n color: var(--tl-color-selected-contrast);\n white-space: nowrap;\n position: absolute;\n padding: 3px 6px;\n font-size: 12px;\n opacity: 1;\n border-radius: var(--tl-radius-2);\n}\n\n.tl-cursor-chat {\n position: absolute;\n color: var(--tl-color-selected-contrast);\n white-space: nowrap;\n padding: 3px 6px;\n font-size: 12px;\n pointer-events: none;\n z-index: var(--tl-layer-cursor);\n margin-top: 16px;\n margin-left: 13px;\n opacity: 1;\n border: none;\n user-select: text;\n border-radius: var(--tl-radius-2);\n}\n\n.tl-cursor-chat .tl-cursor-chat__bubble {\n padding-right: 12px;\n}\n\n.tl-cursor-chat::selection {\n background: var(--tl-color-selected);\n color: var(--tl-color-selected-contrast);\n text-shadow: none;\n}\n\n.tl-cursor-chat::placeholder {\n color: var(--tl-color-selected-contrast);\n opacity: 0.7;\n}\n\n/* ---------------------- Text ---------------------- */\n\n.tl-text-shape-label {\n position: relative;\n font-weight: normal;\n min-width: 1px;\n padding: 0px;\n margin: 0px;\n border: none;\n width: fit-content;\n height: fit-content;\n font-variant: normal;\n font-style: normal;\n pointer-events: all;\n white-space: pre-wrap;\n overflow-wrap: break-word;\n}\n\n.tl-plain-text-wrapper[data-isediting='true'] .tl-text-content {\n opacity: 0;\n}\n\n.tl-rich-text-wrapper[data-isediting='true'] .tl-text-content {\n display: none;\n}\n\n.tl-text {\n /* remove overflow from textarea on windows */\n margin: 0px;\n padding: 0px;\n\n appearance: auto;\n background: none;\n border-image: none;\n border: 0px;\n caret-color: var(--tl-color-text);\n color: inherit;\n column-count: initial !important;\n display: inline-block;\n font-family: inherit;\n font-feature-settings: normal;\n font-kerning: auto;\n font-optical-sizing: auto;\n font-size: inherit;\n font-stretch: 100%;\n font-style: inherit;\n font-variant: inherit;\n font-variation-settings: normal;\n font-weight: inherit;\n letter-spacing: inherit;\n line-height: inherit;\n outline: none;\n overflow-wrap: break-word;\n text-align: inherit;\n text-indent: 0px;\n text-rendering: auto;\n text-shadow: inherit;\n text-transform: none;\n white-space: pre-wrap;\n line-break: normal;\n word-spacing: 0px;\n word-wrap: break-word;\n writing-mode: horizontal-tb !important;\n}\n\n.tl-text-measure {\n z-index: var(--tl-layer-canvas-hidden);\n opacity: 0;\n visibility: hidden;\n\n /* pointer-events: all; */\n /* opacity: 1; */\n /* z-index: 99999; */\n\n position: absolute;\n top: 0px;\n left: 0px;\n width: max-content;\n box-sizing: border-box;\n pointer-events: none;\n white-space: pre-wrap;\n word-wrap: break-word;\n overflow-wrap: break-word;\n resize: none;\n border: none;\n user-select: none;\n contain: layout style paint;\n /* N.B. This property, while discouraged (\"intended for Document Type Definition (DTD) designers\") is necessary for ensuring correct mixed RTL/LTR behavior when exporting SVGs. */\n unicode-bidi: plaintext;\n -webkit-user-select: none;\n}\n\n.tl-text-input,\n.tl-text-content {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n min-width: 1px;\n min-height: 1px;\n outline: none;\n}\n\n.tl-text-content__wrapper {\n position: relative;\n width: fit-content;\n height: fit-content;\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n min-height: auto;\n}\n\n.tl-text-content {\n overflow: visible;\n pointer-events: none;\n}\n\n.tl-text-input {\n resize: none;\n user-select: all;\n -webkit-user-select: text;\n cursor: var(--tl-cursor-text);\n}\n\n.tl-text-input:not(.tl-rich-text) {\n /*\n * Note: this `overflow: hidden` is key for scrollbars to not show up\n * plaintext/<textarea> editors.\n */\n overflow: hidden;\n}\n\n.tl-text-input::selection {\n background: var(--tl-color-selected);\n color: var(--tl-color-selected-contrast);\n text-shadow: none;\n}\n\n/* Text label */\n\n.tl-text-label {\n display: flex;\n justify-content: center;\n align-items: center;\n color: var(--tl-color-text);\n line-height: inherit;\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n}\n\n.tl-text-label[data-hastext='false'][data-isediting='false'] > .tl-text-label__inner {\n width: 40px;\n height: 40px;\n}\n\n.tl-text-label[data-hastext='true'][data-isediting='false'] .tl-text-content {\n pointer-events: all;\n}\n\n.tl-text-label__inner > .tl-text-input.tl-rich-text {\n display: none;\n position: static;\n}\n\n.tl-text-wrapper[data-isediting='false'] .tl-text-input {\n opacity: 0;\n cursor: var(--tl-cursor-default);\n}\n\n.tl-rich-text[data-is-ready-for-editing='true'],\n.tl-text-wrapper[data-is-ready-for-editing='true'] .tl-text-input {\n cursor: var(--tl-cursor-text);\n}\n\n.tl-text-label[data-textwrap='true'] > .tl-text-label__inner {\n max-width: 100%;\n}\n\n.tl-text-label[data-isediting='true'] {\n background-color: transparent;\n min-height: auto;\n}\n\n.tl-text-wrapper .tl-text-content {\n pointer-events: all;\n z-index: var(--tl-layer-text-content);\n}\n\n.tl-text-label__inner > .tl-text-content {\n position: relative;\n top: 0px;\n left: 0px;\n padding: inherit;\n height: fit-content;\n width: fit-content;\n border-radius: var(--tl-radius-1);\n max-width: 100%;\n}\n\n.tl-text-label__inner > .tl-text-input {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n padding: inherit;\n}\n\n.tl-text-wrapper[data-isselected='true'] .tl-text-input {\n z-index: var(--tl-layer-text-editor);\n pointer-events: all;\n}\n\n/* This part of the rule helps preserve the occlusion rules for the shapes so we\n * don't click on shapes that are behind other shapes.\n * One extra nuance is we don't use this behavior for:\n * - arrows which have weird geometry and just gets in the way.\n * - draw/line shapes, because it feels restrictive to have them be 'in the way' of clicking on a textfield\n * - shapes that are not filled\n */\n.tl-canvas:is([data-iseditinganything='true'], [data-isselectinganything='true'])\n .tl-shape:not(\n [data-shape-type='arrow'],\n [data-shape-type='draw'],\n [data-shape-type='line'],\n [data-shape-type='highlight'],\n [data-shape-is-filled='false']\n ) {\n pointer-events: all;\n}\n\n.tl-rich-text .ProseMirror {\n word-wrap: break-word;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n\n /**\n * Note: ProseMirror disables this in https://github.com/ProseMirror/prosemirror-view/commit/6b3b2205e2f3029cb8e8e86c55a190a22491df31\n * However, that was from 8 years ago and the browser caret issue\n * it mentions seems to be fixed. So, we're re-enabling it.\n * We'll tell ProseMirror maybe to get rid of this on their end.\n *\n */\n -webkit-font-variant-ligatures: inherit;\n font-variant-ligatures: inherit;\n font-feature-settings: inherit;\n\n /**\n * N.B. This following CSS Rule comes standard with the tiptap editor.\n * Combined with the above rule that it supersedes, it allows for\n * the auto-linking to work in text. Say, when typing example.com\n * this helps it automatically turn that bit of text into a link.\n *\n * However, specifically, the break-spaces features seems to cause\n * rendering differences when going in-and-out of edit mode. For example,\n * the statically rendered text 'the rain in spain falls mainly on the plain'\n * in a note shape will render differently when going in-and-out of edit mode.\n *\n * So, this is commented out to help make both the autolinking work (which now\n * relies on the white-space: pre-wrap to work and to make the static/edit-mode\n * rendering consistent.\n * In the future, we might consider just making the static rendering just use\n * white-space: break-spaces to make it consistent with the edit mode if need be.\n *\n * Also, the amount of ink I've spilt in my career writing comments explaining\n * white-space in contenteditable is incredible.\n *\n /* white-space: break-spaces; */\n}\n\n.tl-rich-text {\n tab-size: var(--tl-tab-size, 2);\n}\n\n.tl-rich-text p {\n margin: 0;\n /* Depending on the extensions, <p> tags can be empty, without a <br />. */\n min-height: 1lh;\n}\n\n.tl-rich-text ul,\n.tl-rich-text ol {\n text-align: left;\n margin: 0;\n padding-left: 1.625ch;\n /* Some resets, like Tailwind, nix the list styling. */\n list-style: revert;\n}\n\n.tl-rich-text ol:has(> li:nth-child(10)) {\n padding-left: 2.625ch;\n}\n\n.tl-rich-text ol:has(> li:nth-child(100)) {\n padding-left: 3.625ch;\n}\n\n.tl-rich-text h1,\n.tl-rich-text h2,\n.tl-rich-text h3,\n.tl-rich-text h4,\n.tl-rich-text h5,\n.tl-rich-text h6 {\n margin-top: 5px;\n margin-bottom: 10px;\n /**\n * Headings render at a larger font-size than the body text (via the browser's\n * default h1–h6 sizing), so they can't inherit the container's resolved\n * whole-pixel line-height — that value is derived from the base font size and\n * would cram the taller lines together. Instead, scale line-height with each\n * heading's own font-size using tldraw's unitless line-height multiplier, which\n * the rich-text render/measure paths expose here. The 1.35 fallback mirrors the\n * default theme line-height (see defaultThemes.ts).\n */\n line-height: var(--tl-rich-text-heading-line-height, 1.35);\n}\n\n.tl-rich-text a {\n color: var(--tl-color-primary);\n text-decoration: underline;\n}\n\n.tl-rich-text[data-is-select-tool-active='false'] a {\n cursor: inherit;\n}\n\n.tl-rich-text code {\n font-family: var(--tl-font-mono);\n}\n\n.tl-rich-text mark {\n background-color: #fddd00;\n color: currentColor;\n border-radius: 2px;\n}\n\n.tl-theme__light .tl-rich-text mark {\n text-shadow: none;\n}\n\n.tl-theme__dark .tl-rich-text mark {\n background-color: var(--tl-color-text-highlight);\n color: currentColor;\n text-shadow: none;\n}\n\n@supports (color: color(display-p3 1 1 1)) {\n @media (color-gamut: p3) {\n .tl-container:not(.tl-theme__force-sRGB) .tl-rich-text mark {\n background-color: var(--tl-color-text-highlight-p3);\n }\n }\n}\n\n.tl-text-wrapper[data-isediting='true'] .tl-rich-text {\n display: block;\n}\n\n.tl-text__outline {\n text-shadow: var(--tl-text-outline);\n}\n\n.tl-text__no-outline {\n text-shadow: none;\n}\n\n/* --------------------- Loading -------------------- */\n\n.tl-loading {\n background-color: var(--tl-color-background);\n color: var(--tl-color-text-1);\n height: 100%;\n width: 100%;\n display: flex;\n flex-direction: column;\n justify-content: center;\n align-items: center;\n gap: var(--tl-space-2);\n font-size: 14px;\n font-weight: 500;\n opacity: 0;\n animation: tl-fade-in 0.2s ease-in-out forwards;\n animation-delay: 0.2s;\n position: absolute;\n inset: 0px;\n z-index: var(--tl-layer-canvas-blocker);\n}\n\n@keyframes tl-fade-in {\n 0% {\n opacity: 0;\n }\n 100% {\n opacity: 1;\n }\n}\n\n.tl-spinner {\n animation: tl-spin 1s linear infinite;\n}\n\n@keyframes tl-spin {\n 0% {\n transform: rotate(0deg);\n }\n 100% {\n transform: rotate(360deg);\n }\n}\n\n/* ---------------- Hyperlink Button ---------------- */\n\n.tl-hyperlink-button {\n background: none;\n margin: 0px;\n position: absolute;\n top: 0px;\n right: 0px;\n height: 44px;\n width: 44px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-size: 12px;\n font-weight: 400;\n color: var(--tl-color-text-1);\n padding: 13px;\n cursor: var(--tl-cursor-pointer);\n border: none;\n outline: none;\n pointer-events: all;\n z-index: 1;\n}\n\n.tl-hyperlink-button::after {\n content: '';\n z-index: -1;\n position: absolute;\n right: 6px;\n bottom: 6px;\n display: block;\n width: calc(100% - 12px);\n height: calc(100% - 12px);\n border-radius: var(--tl-radius-1);\n background-color: var(--tl-color-background);\n pointer-events: none;\n}\n\n.tl-hyperlink-button:focus-visible {\n color: var(--tl-color-selected);\n}\n\n.tl-hyperlink__icon {\n width: 15px;\n height: 15px;\n background-color: currentColor;\n pointer-events: none;\n}\n\n.tl-hyperlink-button__hidden {\n display: none;\n}\n\n/* ----------------- Shape indicator ---------------- */\n\n.tl-shape-indicator {\n transform-origin: top left;\n fill: none;\n stroke-width: calc(1.5px * var(--tl-scale));\n contain: size layout;\n}\n\n.tl-canvas-overlays {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: var(--tl-layer-canvas-overlays);\n}\n\n/* ---------------------- Shape --------------------- */\n\n.tl-shape {\n position: absolute;\n pointer-events: none;\n overflow: visible;\n transform-origin: top left;\n contain: size layout;\n}\n\n/* ---------------- Shape Containers ---------------- */\n\n.tl-svg-container {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n pointer-events: none;\n stroke-linecap: round;\n stroke-linejoin: round;\n transform-origin: top left;\n overflow: visible;\n}\n\n.tl-html-container {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n pointer-events: none;\n stroke-linecap: round;\n stroke-linejoin: round;\n /* content-visibility: auto; */\n transform-origin: top left;\n color: var(--tl-color-text-1);\n}\n\n.tl-content-slot {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n pointer-events: none;\n transform-origin: top left;\n}\n\n/* -------------------- Group shape ------------------ */\n\n.tl-group {\n stroke: var(--tl-color-text);\n stroke-width: calc(1px * var(--tl-scale));\n opacity: 0.5;\n}\n\n/* --------------------- Arrow shape -------------------- */\n\n.tl-shape[data-shape-type='arrow'] .tl-text-label {\n position: absolute;\n top: -1px;\n left: -1px;\n width: 2px;\n height: 2px;\n padding: 0px;\n display: flex;\n justify-content: center;\n align-items: center;\n text-align: center;\n color: var(--tl-color-text);\n}\n\n.tl-shape[data-shape-type='arrow'] .tl-text-label__inner {\n border-radius: var(--tl-radius-1);\n box-sizing: content-box;\n height: max-content;\n width: max-content;\n}\n\n.tl-shape[data-shape-type='arrow'] .tl-text {\n height: max-content;\n}\n\n/* ------------------- Bookmark shape ------------------- */\n\n.tl-bookmark__container {\n width: 100%;\n height: 100%;\n position: relative;\n border: 1px solid var(--tl-color-panel-contrast);\n background-color: var(--tl-color-panel);\n border-radius: var(--tl-radius-2);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.tl-bookmark__container--safariExport {\n border: 1px solid var(--tl-color-divider);\n}\n\n.tl-bookmark__image_container {\n flex: 1 1 100%;\n overflow: hidden;\n border-top-left-radius: var(--tl-radius-1);\n border-top-right-radius: var(--tl-radius-1);\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: flex-end;\n align-items: flex-start;\n box-shadow: inset 0px 0px 0px 1px var(--tl-color-divider);\n}\n\n.tl-bookmark__image_container > .tl-hyperlink-button::after {\n background-color: var(--tl-color-panel);\n}\n\n.tl-bookmark__placeholder {\n width: 100%;\n height: 100%;\n background-color: var(--tl-color-muted-2);\n border-bottom: 1px solid var(--tl-color-muted-2);\n}\n\n.tl-bookmark__image {\n width: 100%;\n height: 100%;\n object-fit: cover;\n object-position: center;\n border-bottom: 1px solid var(--tl-color-muted-2);\n}\n\n.tl-bookmark__copy_container {\n background-color: var(--tl-color-muted-0);\n padding: var(--tl-space-4);\n pointer-events: all;\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n flex: 1;\n}\n\n.tl-bookmark__copy_container:has(.tl-bookmark__link:only-child) {\n justify-content: center;\n}\n\n.tl-bookmark__heading,\n.tl-bookmark__description,\n.tl-bookmark__link {\n margin: 0px;\n width: 100%;\n font-family: inherit;\n}\n\n.tl-bookmark__heading {\n font-size: 16px;\n line-height: 1.6;\n font-weight: bold;\n padding-bottom: var(--tl-space-2);\n overflow: hidden;\n max-height: calc((16px * 1.6) * 2);\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n line-clamp: 2;\n text-overflow: ellipsis;\n display: -webkit-box;\n}\n\n.tl-bookmark__description {\n font-size: 12px;\n line-height: 1.5;\n overflow: hidden;\n max-height: calc((12px * 1.5) * 3);\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 3;\n line-clamp: 3;\n text-overflow: ellipsis;\n display: -webkit-box;\n color: var(--tl-color-text-1);\n margin: var(--tl-space-2) 0px;\n}\n\n.tl-bookmark__heading + .tl-bookmark__link,\n.tl-bookmark__description + .tl-bookmark__link {\n margin-top: var(--tl-space-3);\n}\n.tl-bookmark__link {\n font-size: 12px;\n pointer-events: all;\n display: flex;\n color: var(--tl-color-text-1);\n align-items: center;\n cursor: var(--tl-cursor-pointer);\n width: fit-content;\n max-width: 100%;\n text-decoration: none;\n}\n\n.tl-bookmark__link > span {\n flex-shrink: 0px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tl-bookmark__link > .tl-hyperlink__icon {\n margin-right: 8px;\n flex-shrink: 0;\n}\n\n.tl-bookmark__link > .tl-bookmark__favicon {\n margin-right: 8px;\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n}\n\n/* -------------- Image and video shape ------------- */\n\n.tl-image,\n.tl-video {\n object-fit: cover;\n background-size: cover;\n width: 100%;\n height: 100%;\n}\n\n.tl-video-container,\n.tl-image-container,\n.tl-embed-container {\n width: 100%;\n height: 100%;\n pointer-events: all;\n /* background-color: var(--tl-color-background); */\n\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.tl-image-container {\n position: relative;\n}\n\n.tl-image {\n position: absolute;\n inset: 0;\n}\n\n.tl-video.tl-video-is-fullscreen {\n object-fit: contain;\n background-size: contain;\n}\n\n/* -------------------- Note shape ------------------- */\n\n.tl-note__container {\n position: relative;\n width: 100%;\n height: 100%;\n pointer-events: all;\n opacity: 1;\n z-index: var(--tl-layer-text-container);\n border-radius: 1px;\n}\n\n.tl-note__container > .tl-text-label {\n color: currentColor;\n}\n\n.tl-note__attribution {\n position: absolute;\n bottom: calc(4px * var(--note-attribution-scale, 1));\n right: calc(8px * var(--note-attribution-scale, 1));\n font-family: var(--tl-font-sans);\n pointer-events: auto;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n max-width: 60%;\n z-index: 1;\n}\n\n/* ------------------- Frame shape ------------------- */\n\n.tl-frame__body {\n stroke-width: calc(1px * var(--tl-scale));\n}\n\n.tl-frame__creating {\n stroke: var(--tl-color-selected);\n fill: none;\n}\n\n.tl-frame-heading {\n --tl-frame-padding-x: 6px;\n --tl-frame-height: 24px;\n --tl-frame-minimum-width: 32px;\n --tl-frame-offset-width: 16px;\n display: flex;\n align-items: center;\n position: absolute;\n transform-origin: 0% 100%;\n overflow: hidden;\n max-width: 100%;\n min-width: var(--tl-frame-minimum-width);\n height: auto;\n font-size: 12px;\n padding-bottom: 4px;\n pointer-events: all;\n}\n\n.tl-frame-heading-hit-area {\n pointer-events: all;\n /* scale from bottom left corner so we can pin it to the top left corner of the frame */\n transform-origin: 0% 100%;\n display: flex;\n height: var(--tl-frame-height);\n width: 100%;\n align-items: center;\n border-radius: var(--tl-radius-1);\n}\n\n.tl-frame-label {\n pointer-events: all;\n overflow: hidden;\n text-overflow: ellipsis;\n padding: 0px var(--tl-frame-padding-x);\n border-radius: var(--tl-radius-1);\n position: relative;\n font-size: inherit;\n white-space: pre;\n}\n\n.tl-frame-label__editing {\n color: transparent;\n white-space: pre;\n width: auto;\n min-width: var(--tl-frame-minimum-width);\n height: 100%;\n overflow: visible;\n background-color: var(--tl-color-panel);\n border-color: var(--tl-color-selected);\n box-shadow: inset 0px 0px 0px 1.5px var(--tl-color-selected);\n}\n\n.tl-frame-name-input {\n position: absolute;\n border: none;\n background: none;\n outline: none;\n padding: 0px var(--tl-frame-padding-x);\n inset: 0px;\n height: 100%;\n width: 100%;\n font-size: inherit;\n font-family: inherit;\n font-weight: inherit;\n width: 100%;\n color: var(--tl-color-text-1);\n border-radius: var(--tl-radius-1);\n user-select: all;\n -webkit-user-select: text;\n white-space: pre;\n cursor: var(--tl-cursor-text);\n}\n\n/* ------------------- Embed Shape ------------------ */\n\n.tl-embed {\n border: none;\n border-radius: var(--tl-radius-2);\n}\n\n/* -------------- Shape error boundary -------------- */\n\n.tl-shape-error-boundary {\n width: 100%;\n height: 100%;\n background-color: var(--tl-color-muted-1);\n border-width: calc(1px * var(--tl-scale));\n border-color: var(--tl-color-muted-1);\n border-style: solid;\n border-radius: calc(var(--tl-radius-1) * var(--tl-scale));\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n text-align: left;\n position: relative;\n pointer-events: all;\n overflow: hidden;\n padding: var(--tl-space-2);\n}\n\n.tl-shape-error-boundary::before {\n transform: scale(var(--tl-scale));\n content: 'Error';\n font-size: 12px;\n font-family: inherit;\n color: var(--tl-color-text-0);\n}\n\n/* ----------------- Error boundary ----------------- */\n\n.tl-error-boundary {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n padding: var(--tl-space-4);\n background-color: var(--tl-color-background);\n color: var(--tl-color-text-1);\n position: absolute;\n}\n\n.tl-error-boundary__overlay {\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n z-index: var(--tl-layer-error-overlay);\n background-color: var(--tl-color-overlay);\n}\n\n.tl-error-boundary__content * {\n user-select: all;\n -webkit-user-select: text;\n pointer-events: all;\n}\n\n.tl-error-boundary__canvas {\n pointer-events: none;\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n z-index: var(--tl-layer-error-canvas);\n}\n\n/* some browsers seem to have some weird interactions between stacking contexts\nand pointer-events. this ::after pseudo element covers the canvas and prevents\nit from receiving any pointer events or affecting the cursor. */\n.tl-error-boundary__canvas::after {\n content: ' ';\n display: block;\n position: absolute;\n inset: 0px;\n height: 100%;\n width: 100%;\n z-index: var(--tl-layer-error-canvas-after);\n pointer-events: all;\n}\n\n.tl-error-boundary__content {\n width: fit-content;\n height: fit-content;\n max-width: 100%;\n width: 400px;\n max-height: 100%;\n background-color: var(--tl-color-panel);\n padding: 16px;\n border-radius: 16px;\n box-shadow: var(--tl-shadow-2);\n font-size: 14px;\n font-weight: 400;\n display: flex;\n flex-direction: column;\n overflow: auto;\n z-index: var(--tl-layer-error-content);\n gap: 12px;\n}\n\n.tl-error-boundary__content__expanded {\n width: 600px;\n}\n\n.tl-error-boundary__content h2 {\n font-size: 16px;\n margin: 0px;\n font-weight: 500;\n}\n\n.tl-error-boundary__content h4 {\n border: 1px solid var(--tl-color-low-border);\n margin: -6px 0 0 0;\n padding: var(--tl-space-5);\n border-radius: var(--tl-radius-2);\n font-weight: normal;\n}\n\n.tl-error-boundary__content p {\n line-height: 1.5;\n margin: 0px;\n}\n\n.tl-error-boundary__content pre {\n background-color: var(--tl-color-muted-2);\n margin-top: 0;\n padding: var(--tl-space-5);\n border-radius: var(--tl-radius-2);\n overflow: auto;\n font-size: 12px;\n max-height: 320px;\n}\n\n.tl-error-boundary__content button {\n background: none;\n border: none;\n font-family: inherit;\n font-size: 14px;\n font-weight: 500;\n padding: var(--tl-space-4);\n border-radius: var(--tl-radius-3);\n cursor: var(--tl-cursor-pointer);\n color: inherit;\n background-color: transparent;\n}\n\n.tl-error-boundary__content a {\n color: var(--tl-color-selected);\n font-weight: 500;\n text-decoration: none;\n}\n\n.tl-error-boundary__content__error {\n position: relative;\n margin: -6px 0 0 0;\n}\n\n.tl-error-boundary__content__error button {\n position: absolute;\n top: var(--tl-space-2);\n right: var(--tl-space-2);\n font-size: 12px;\n padding: var(--tl-space-2) var(--tl-space-3);\n background-color: var(--tl-color-panel);\n border-radius: var(--tl-radius-1);\n}\n\n.tl-error-boundary__content__actions {\n display: flex;\n justify-content: space-between;\n gap: var(--tl-space-4);\n margin: 0px;\n margin-left: -4px;\n}\n.tl-error-boundary__content__actions__group {\n display: flex;\n gap: var(--tl-space-4);\n}\n.tl-error-boundary__content .tl-error-boundary__reset {\n color: var(--tl-color-danger);\n}\n.tl-error-boundary__content .tl-error-boundary__refresh {\n background-color: var(--tl-color-primary);\n color: var(--tl-color-selected-contrast);\n}\n.tl-container__focused:not(.tl-container__no-focus-ring)\n .tlui-button.tl-error-boundary__refresh:focus-visible {\n border-radius: 8px;\n outline-offset: 0;\n}\n\n/* ---------------- Hit test blocker ---------------- */\n\n.tl-hit-test-blocker {\n position: absolute;\n z-index: var(--tl-layer-canvas-blocker);\n inset: 0px;\n width: 100%;\n height: 100%;\n pointer-events: all;\n}\n\n.tl-hit-test-blocker__hidden {\n display: none;\n}\n\n/* --------------------- Hovers --------------------- */\n\n@media (hover: hover) {\n .tl-handle__create:hover {\n opacity: 1;\n }\n\n .tl-handle__bg:hover {\n cursor: var(--tl-cursor-grab);\n fill: var(--tl-color-selection-fill);\n }\n\n .tl-bookmark__link:hover {\n color: var(--tl-color-selected);\n }\n\n .tl-hyperlink-button:hover {\n color: var(--tl-color-selected);\n }\n\n .tl-error-boundary__content button:hover {\n background-color: var(--tl-color-low);\n }\n .tl-error-boundary__content a:hover {\n color: var(--tl-color-text-1);\n }\n .tl-error-boundary__content .tl-error-boundary__refresh:hover {\n background-color: var(--tl-color-primary);\n opacity: 0.9;\n }\n\n /* These three rules help preserve clicking into specific points in text areas *while*\n * already in edit mode when jumping from shape to shape. */\n .tl-canvas[data-iseditinganything='true'] .tl-text-wrapper:hover .tl-text-input {\n z-index: var(--tl-layer-text-editor);\n pointer-events: all;\n }\n}\n\n/* @tldraw/ui */\n\n.tl-container {\n /*\n * UI z-index layers\n *\n * menu-click-capture and panels (.tlui-layout) participate in the\n * container-level stacking context alongside the canvas layers:\n * watermark (200) < canvas-in-front (250) < menu-click-capture (250) < panels (300)\n *\n * menus, toasts, header-footer, etc. are children of .tlui-layout\n * (which creates its own stacking context at z-index 300), so their\n * z-indices are relative to the layout, not the container.\n *\n * cursor is rendered inside canvas-in-front (stacking context at\n * z-index 250), so their z-indices are relative to that context.\n */\n --tl-layer-above: 1;\n --tl-layer-focused-input: 10;\n --tl-layer-menu-click-capture: 250;\n --tl-layer-panels: 300;\n --tl-layer-menus: 400;\n --tl-layer-toasts: 650;\n --tl-layer-cursor: 700;\n --tl-layer-header-footer: 999;\n}\n\n/* Button */\n\n.tlui-button {\n position: relative;\n height: 40px;\n min-width: 40px;\n padding: 0px 12px;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: transparent;\n border: transparent;\n color: currentColor;\n cursor: pointer;\n pointer-events: all;\n font-weight: inherit;\n font-family: inherit;\n line-height: inherit;\n text-rendering: optimizeLegibility;\n font-size: 12px;\n gap: 0px;\n color: var(--tl-color-text-1);\n z-index: 0;\n}\n\n.tlui-button:disabled,\n.tlui-button[data-disabled] {\n color: var(--tl-color-text-disabled);\n text-shadow: none;\n cursor: default;\n}\n\n.tlui-button:disabled .tlui-kbd,\n.tlui-button[data-disabled] .tlui-kbd {\n color: var(--tl-color-text-disabled);\n}\n\n.tlui-button > * {\n position: relative;\n z-index: var(--tl-layer-above);\n}\n\n.tlui-button__label {\n font-size: 12px;\n flex-grow: 2;\n text-align: start;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/*\n * We only want to show the focus ring if:\n * - the container is focused\n * - we're not using the mouse to interact (which is the .tl-container__no-focus-ring)\n */\n.tl-container__focused:not(.tl-container__no-focus-ring) .tlui-button:focus-visible {\n border-radius: 10px;\n outline: 2px solid var(--tl-color-focus);\n outline-offset: -5px;\n}\n.tl-container__focused:not(.tl-container__no-focus-ring) .tlui-button__tool:focus-visible {\n border-radius: 12px;\n}\n.tlui-slider__container:has(.tlui-slider__thumb:focus-visible) {\n border-radius: 10px;\n outline: 2px solid var(--tl-color-focus);\n outline-offset: -5px;\n}\n\n.tlui-button::after {\n display: block;\n content: '';\n position: absolute;\n inset: 4px;\n border-radius: var(--tl-radius-2);\n background: var(--tl-color-muted-2);\n opacity: 0;\n}\n\n.tlui-button__menu[data-highlighted]::after {\n opacity: 1;\n}\n\n.tlui-button[data-isactive='true']::after,\n.tlui-button[data-isactive='true']:not(:disabled, [data-disabled], :focus-visible):active:after {\n background: var(--tl-color-hint);\n opacity: 1;\n}\n\n.tlui-button[aria-expanded='true'][data-direction='left'] {\n background: linear-gradient(270deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n}\n\n@media (hover: hover) {\n .tlui-button[aria-expanded='true'][data-direction='left']:not(:hover)::after {\n background: linear-gradient(270deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n }\n\n .tlui-button:not(:disabled, [data-disabled]):hover {\n z-index: 1;\n }\n\n .tlui-button:not(:disabled, [data-disabled]):hover::after {\n opacity: 1;\n }\n}\n\n.tlui-button__icon + .tlui-button__label {\n margin-inline-start: var(--tl-space-2);\n}\n\n/* Low button */\n\n.tlui-button__low {\n border-radius: var(--tl-radius-3);\n background-color: var(--tl-color-low);\n}\n\n.tlui-button__low::after {\n background-color: var(--tl-color-muted-2);\n opacity: 0;\n}\n\n@media (hover: hover) {\n .tlui-button__low:hover::after {\n opacity: 1;\n }\n}\n\n/* Primary / danger buttons */\n\n.tlui-button__primary {\n color: var(--tl-color-primary);\n}\n\n.tlui-button__danger {\n color: var(--tl-color-danger);\n text-shadow: none;\n}\n\n@media (hover: hover) {\n .tlui-button__primary:not(:disabled, [data-disabled], :focus-visible):hover {\n color: var(--tl-color-primary);\n }\n\n .tlui-button__danger:not(:disabled, [data-disabled], :focus-visible):hover {\n color: var(--tl-color-danger);\n text-shadow: none;\n }\n}\n\n/* Menu button */\n\n.tlui-button__menu {\n height: 40px;\n min-height: 40px;\n width: 100%;\n gap: 8px;\n margin-top: -4px;\n}\n\n.tlui-button__menu::after {\n inset: 4px;\n border-radius: var(--tl-radius-2);\n}\n\n.tlui-button__menu > .tlui-icon + .tlui-button__label {\n margin-inline-start: 0px;\n}\n\n.tlui-button__menu:nth-child(1) {\n margin-top: 0px;\n}\n\n.tlui-button__menu:nth-last-child(1) {\n margin-bottom: 0px;\n}\n\n/* Menu checkbox button */\n\n.tlui-button__checkbox {\n padding-left: 8px;\n}\n\n.tlui-button__checkbox__indicator {\n width: 15px;\n height: 15px;\n}\n\n/* Tool lock button */\n\n.tlui-main-toolbar__lock-button {\n position: absolute;\n pointer-events: all;\n height: 40px;\n width: 40px;\n border-radius: var(--tl-radius-2);\n}\n\n.tlui-main-toolbar--horizontal .tlui-main-toolbar__lock-button {\n top: 4px;\n right: 0px;\n min-width: 0px;\n}\n\n.tlui-main-toolbar--vertical .tlui-main-toolbar__lock-button {\n bottom: 0px;\n min-height: 0px;\n}\n\n.tlui-main-toolbar__lock-button::after {\n top: 4px;\n left: 8px;\n inset: 4px;\n}\n\n/* Tool button */\n\n.tlui-button__tool {\n position: relative;\n height: 48px;\n width: 48px;\n}\n\n.tlui-button__tool::after {\n inset: 4px;\n border-radius: 8px;\n}\n\n.tlui-button__tool[aria-pressed='true'] {\n color: var(--tl-color-selected-contrast);\n}\n\n.tlui-button__tool[aria-pressed='true']:not(:disabled, [data-disabled], :focus-visible):active {\n color: var(--tl-color-selected-contrast);\n}\n\n.tlui-button__tool[aria-pressed='true']:not(:disabled, [data-disabled])::after {\n background: var(--tl-color-selected);\n opacity: 1;\n}\n\n.tlui-layout__mobile .tlui-main-toolbar--horizontal .tlui-button__tool {\n height: 48px;\n width: 43px;\n}\n\n.tlui-layout__mobile .tlui-main-toolbar--horizontal .tlui-button__tool > .tlui-icon {\n height: 16px;\n width: 16px;\n}\n\n/* Row layout */\n\n.tlui-row {\n display: flex;\n flex-direction: row;\n padding: 0 2px;\n}\n.tlui-row > * {\n margin-left: -2px;\n margin-right: -2px;\n}\n\n/* Column layout */\n\n.tlui-column {\n display: flex;\n flex-direction: column;\n padding: 2px 0;\n}\n.tlui-column > * {\n margin-top: -2px;\n margin-bottom: -2px;\n}\n\n/* Grid layout */\n\n.tlui-grid {\n display: grid;\n grid-template-columns: repeat(4, 1fr);\n grid-auto-flow: row;\n overflow: hidden;\n padding: 2px;\n}\n.tlui-grid > * {\n margin: -2px;\n}\n\n/* Zoom button */\n\n.tlui-zoom-menu__button {\n width: 60px;\n min-width: 60px;\n text-align: center;\n}\n\n.tlui-zoom-menu__button[data-state='open']::after {\n background: linear-gradient(0deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n}\n\n/* --------------------- Layout --------------------- */\n\n.tlui-layout {\n position: relative;\n display: grid;\n grid-template-columns: 1fr;\n grid-template-rows: minmax(0px, 1fr) auto;\n grid-auto-rows: auto;\n height: 100%;\n max-height: 100%;\n overflow: clip;\n pointer-events: none;\n user-select: none;\n contain: strict;\n z-index: var(--tl-layer-panels);\n transform: translate3d(0, 0, 0);\n --tl-sab: env(safe-area-inset-bottom);\n font-weight: 500;\n line-height: 1.6;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n font-smooth: antialiased;\n text-rendering: optimizeLegibility;\n}\n\n.tlui-layout__top {\n grid-column: 1;\n grid-row: 1;\n display: flex;\n min-width: 0px;\n justify-content: space-between;\n}\n\n.tlui-layout__top__left {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n justify-content: flex-start;\n width: 100%;\n height: 100%;\n flex: 0 1 0;\n}\n\n.tlui-layout__top__right {\n display: flex;\n flex-direction: column;\n align-items: flex-end;\n justify-content: flex-start;\n height: 100%;\n flex: 0 0 auto;\n min-width: 0px;\n}\n\n.tlui-scrollable,\n.tlui-scrollable * {\n pointer-events: all;\n touch-action: auto;\n overscroll-behavior: none;\n}\n\n/* ----------------- Helper Buttons ---------------- */\n\n.tlui-helper-buttons {\n position: relative;\n display: flex;\n flex-direction: column;\n justify-content: flex-start;\n align-items: flex-start;\n width: min-content;\n gap: var(--tl-space-3);\n margin: var(--tl-space-2) var(--tl-space-3);\n white-space: nowrap;\n pointer-events: none;\n z-index: var(--tl-layer-panels);\n}\n\n/* ---------------------- Icon ---------------------- */\n\n.tlui-icon {\n flex-shrink: 0;\n width: 18px;\n height: 18px;\n background-color: currentColor;\n}\n\n.tlui-icon__placeholder {\n flex-shrink: 0;\n width: 18px;\n height: 18px;\n background-color: transparent;\n}\n\n.tlui-icon__small {\n width: 15px;\n height: 15px;\n}\n\n.tlui-icon__tiny {\n width: 12px;\n height: 12px;\n}\n\n/* --------------------- Slider --------------------- */\n\n.tlui-slider__container {\n width: 100%;\n padding: 0px var(--tl-space-4);\n}\n\n.tlui-slider {\n position: relative;\n display: flex;\n align-items: center;\n user-select: none;\n touch-action: none;\n width: 100%;\n}\n\n.tlui-slider__track {\n position: relative;\n flex-grow: 1;\n height: 44px;\n cursor: pointer;\n}\n\n.tlui-slider__track::after {\n display: block;\n position: absolute;\n top: calc(50% - 2px);\n content: '';\n height: 3px;\n width: 100%;\n background-color: var(--tl-color-muted-1);\n border-radius: 14px;\n}\n\n.tlui-slider__range {\n position: absolute;\n top: calc(50% - 2px);\n left: 0px;\n height: 3px;\n background-color: var(--tl-color-selected);\n border-radius: 14px;\n}\n\n.tlui-slider__thumb {\n all: unset;\n cursor: grab;\n display: block;\n width: 18px;\n height: 18px;\n position: relative;\n top: -1px;\n background-color: var(--tl-color-panel);\n border-radius: 999px;\n box-shadow: inset 0px 0px 0px 2px var(--tl-color-text-1);\n}\n\n.tlui-slider__thumb:active {\n cursor: grabbing;\n box-shadow:\n inset 0px 0px 0px 2px var(--tl-color-text-1),\n var(--tl-shadow-1);\n}\n\n/* ---------------------- Input --------------------- */\n\n.tlui-input {\n background: none;\n margin: 0px;\n position: relative;\n z-index: var(--tl-layer-above);\n height: 40px;\n max-height: 40px;\n display: flex;\n align-items: center;\n justify-content: center;\n font-family: inherit;\n font-size: 12px;\n font-weight: inherit;\n color: var(--tl-color-text-1);\n padding: var(--tl-space-4);\n padding-left: 0px;\n border: none;\n outline: none;\n text-overflow: ellipsis;\n width: 100%;\n user-select: all;\n text-rendering: optimizeLegibility;\n -webkit-user-select: auto !important;\n}\n\n.tlui-input::placeholder {\n color: var(--tl-color-text-3);\n}\n\n.tlui-input__wrapper {\n width: 100%;\n height: 44px;\n display: flex;\n align-items: center;\n gap: var(--tl-space-4);\n color: var(--tl-color-text);\n}\n\n.tlui-input__wrapper > .tlui-icon {\n flex-shrink: 0;\n}\n\n/* The label is a fixed bit of chrome next to the input; it should never be squeezed. Without this,\n an inherited `overflow-wrap: anywhere` (e.g. from .tlui-dialog__body) drops its min-content width\n to a single character and short labels wrap mid-word, e.g. \"URL\" breaking after \"UR\". */\n.tlui-input__wrapper > label {\n flex-shrink: 0;\n overflow-wrap: normal;\n}\n\n/* If mobile use 16px as font size */\n/* On iOS, font size under 16px in an input will make the page zoom into the input 🤦‍♂️ */\n/* https://css-tricks.com/16px-or-larger-text-prevents-ios-form-zoom/ */\n@supports (-webkit-touch-callout: none) {\n /* CSS specific to iOS devices */\n .tlui-input {\n font-size: 16px;\n }\n}\n\n/* --------------------- Select --------------------- */\n\n/* Trigger extends .tlui-button with space-between layout */\n.tlui-select__trigger {\n justify-content: space-between;\n gap: var(--tl-space-3);\n}\n\n.tlui-select__trigger[data-state='open']::after {\n opacity: 1;\n}\n\n.tlui-select__value {\n display: flex;\n align-items: center;\n gap: var(--tl-space-2);\n}\n\n.tlui-select__chevron {\n flex-shrink: 0;\n color: var(--tl-color-text-2);\n}\n\n/* Content extends .tlui-menu with Radix-aware sizing */\n.tlui-select__content {\n min-width: var(--radix-select-trigger-width);\n max-height: var(--radix-select-content-available-height);\n}\n\n.tlui-select__viewport {\n padding: 4px 0;\n}\n\n/* Item extends .tlui-button__checkbox with left-aligned text */\n.tlui-select__item {\n justify-content: flex-start;\n}\n\n.tlui-select__item-indicator {\n visibility: hidden;\n}\n\n.tlui-select__item[data-state='checked'] .tlui-select__item-indicator {\n visibility: visible;\n}\n\n/* ----------------------- Kbd ---------------------- */\n\n.tlui-kbd {\n font-family: inherit;\n font-size: 11px;\n line-height: 11px;\n display: grid;\n justify-items: center;\n grid-auto-flow: column;\n grid-template-columns: auto;\n grid-auto-columns: minmax(1em, auto);\n align-self: bottom;\n color: currentColor;\n margin-inline-start: var(--tl-space-4);\n}\n\n.tlui-kbd > span {\n width: 100%;\n text-align: center;\n display: inline;\n margin: 0px;\n padding: 2px;\n border-radius: 2px;\n}\n\n.tlui-kbd > span:last-child {\n padding-inline-end: 0;\n}\n\n.tlui-kbd:not(:last-child) {\n margin-inline-end: var(--tl-space-2);\n}\n\n/* Focus Mode Button */\n\n.tlui-focus-button {\n z-index: var(--tl-layer-panels);\n pointer-events: all;\n}\n\n/* ---------------------- Menu ---------------------- */\n\n.tlui-menu:empty {\n display: none;\n}\n\n.tlui-menu {\n z-index: var(--tl-layer-menus);\n height: fit-content;\n width: fit-content;\n border-radius: var(--tl-radius-3);\n pointer-events: all;\n touch-action: auto;\n overflow-y: auto;\n overscroll-behavior: none;\n background-color: var(--tl-color-panel);\n box-shadow: var(--tl-shadow-3);\n}\n\n@media (max-height: 600px) {\n .tlui-menu {\n max-height: 70vh;\n }\n}\n\n.tlui-menu::-webkit-scrollbar {\n display: none;\n}\n\n/* Menu groups */\n\n.tlui-menu__group {\n width: 100%;\n}\n\n.tlui-menu__group:empty {\n display: none;\n}\n\n.tlui-menu__group {\n border-bottom: 1px solid var(--tl-color-divider);\n}\n.tlui-menu__group:nth-last-of-type(1) {\n border-bottom: none;\n}\n\n/* Submenu triggers */\n\n.tlui-menu__submenu__trigger {\n --gradient-angle: 90deg;\n}\n\n.tlui-menu__submenu__trigger[data-direction='left'] {\n --gradient-angle: 270deg;\n}\n\n.tlui-menu__submenu__trigger[data-state='open']::after {\n opacity: 1;\n background: linear-gradient(\n var(--gradient-angle),\n rgba(144, 144, 144, 0) 0%,\n var(--tl-color-muted-2) 100%\n );\n}\n\n@media (hover: hover) {\n .tlui-menu__submenu__trigger:is(:hover, [data-state='open'])::after {\n opacity: 1;\n background: linear-gradient(\n var(--gradient-angle),\n rgba(144, 144, 144, 0) 0%,\n var(--tl-color-muted-2) 100%\n );\n }\n}\n\n/* Menu Sizes */\n\n.tlui-menu[data-size='large'] > .tlui-menu__group {\n min-width: initial;\n}\n\n.tlui-menu[data-size='medium'] > .tlui-menu__group {\n min-width: 144px;\n}\n\n.tlui-menu[data-size='small'] > .tlui-menu__group {\n min-width: 96px;\n}\n\n.tlui-menu[data-size='tiny'] > .tlui-menu__group {\n min-width: 0px;\n}\n\n.tlui-menu-click-capture {\n position: fixed;\n inset: 0;\n z-index: var(--tl-layer-menu-click-capture);\n}\n\n/* --------------------- Popover -------------------- */\n\n.tlui-popover {\n position: relative;\n display: flex;\n align-content: stretch;\n}\n\n.tlui-popover__content {\n position: relative;\n max-height: calc(var(--radix-popover-content-available-height) - 8px);\n margin: 0px;\n border: none;\n border-radius: var(--tl-radius-3);\n background-color: var(--tl-color-panel);\n box-shadow: var(--tl-shadow-3);\n z-index: var(--tl-layer-menus);\n overflow: hidden;\n overflow-y: auto;\n touch-action: auto;\n overscroll-behavior: none;\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.tlui-hover-card__content {\n position: relative;\n margin: 0px;\n border: none;\n border-radius: var(--tl-radius-3);\n background-color: var(--tl-color-panel);\n box-shadow: var(--tl-shadow-3);\n z-index: var(--tl-layer-menus);\n}\n\n/* -------------------- Menu Zone ------------------- */\n\n.tlui-menu-zone {\n position: relative;\n z-index: var(--tl-layer-panels);\n width: fit-content;\n border-right: 2px solid var(--tl-color-background);\n border-bottom: 2px solid var(--tl-color-background);\n border-bottom-right-radius: var(--tl-radius-4);\n background-color: var(--tl-color-low);\n}\n\n.tlui-menu-zone *[data-state='open']::after {\n background: linear-gradient(180deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n}\n\n@media (hover: hover) {\n .tlui-menu-zone *[data-state='open']:not(:hover)::after {\n background: linear-gradient(180deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n }\n}\n\n/* ------------------- Page Select ------------------ */\n\n.tlui-page-menu__wrapper {\n display: flex;\n flex-direction: column;\n width: 220px;\n}\n\n.tlui-page-menu__trigger {\n width: auto;\n}\n\n.tlui-page-menu__trigger[data-state='open']::after {\n background: linear-gradient(180deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n}\n\n.tlui-page-menu__list {\n flex: 0 0 auto;\n overflow-x: hidden;\n overflow-y: auto;\n scrollbar-width: none;\n -ms-overflow-style: none;\n}\n\n.tlui-page-menu__list::-webkit-scrollbar {\n display: none;\n}\n\n.tlui-page-menu__list__content {\n position: relative;\n overflow: hidden;\n}\n\n.tlui-page-menu__resize-handle {\n flex: 0 0 auto;\n position: relative;\n height: 1px;\n cursor: ns-resize;\n touch-action: none;\n background: var(--tl-color-divider);\n transition: background-color ease-in-out 150ms 80ms;\n z-index: 999;\n}\n\n/* Hit test area */\n.tlui-page-menu__resize-handle::after {\n content: '';\n position: absolute;\n inset: -4px 0;\n background: none;\n z-index: 2000;\n pointer-events: all;\n}\n\n/* Thick blue indicator */\n.tlui-page-menu__resize-handle::before {\n content: '';\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n height: 1.5px;\n background: transparent;\n pointer-events: none;\n z-index: 2001;\n}\n\n.tlui-page-menu__resize-handle[data-resizing='true']::before {\n background: var(--tl-color-selected);\n transition: none;\n}\n\n.tlui-page-menu__create-button {\n flex: 0 0 auto;\n margin: 0;\n}\n\n.tlui-page-menu__item__button {\n flex: 1 1 auto;\n min-width: 0px;\n}\n\n.tlui-page-menu__item__button::after,\n.tlui-page-menu__item__drag-handle::after {\n content: none;\n}\n\n.tlui-page-menu__item__button > .tlui-button__label {\n padding-inline-end: calc(40px - 12px);\n}\n\n.tlui-page-menu__item {\n position: absolute;\n top: 0px;\n inset-inline-start: 0px;\n width: 100%;\n height: 40px;\n display: flex;\n flex-direction: row;\n align-items: center;\n overflow: hidden;\n transition: transform 0.08s ease-out;\n}\n\n.tlui-page-menu__item::before {\n content: '';\n position: absolute;\n inset: 4px;\n border-radius: var(--tl-radius-2);\n background: var(--tl-color-muted-2);\n opacity: 0;\n pointer-events: none;\n}\n\n.tlui-page-menu__item > * {\n position: relative;\n z-index: var(--tl-layer-above);\n}\n\n.tlui-page-menu__item[data-iscurrent='true']:not([data-editing='true'])::before {\n background: var(--tl-color-hint);\n opacity: 1;\n}\n\n.tlui-page-menu__item__title {\n flex: 1;\n height: 40px;\n position: relative;\n}\n\n.tlui-page-menu__item__title::before {\n content: '';\n position: absolute;\n inset: 4px;\n border-radius: var(--tl-radius-2);\n border: 1px solid var(--tl-color-primary);\n pointer-events: none;\n}\n\n.tlui-page-menu__item__title > .tlui-input__wrapper {\n height: 100%;\n}\n\n.tlui-page-menu__item[data-dragging='true'],\n.tlui-page-menu__item[data-dragging='true'] .tlui-page-menu__item__button,\n.tlui-page-menu__item[data-dragging='true'] .tlui-page-menu__item__drag-handle {\n cursor: grabbing;\n transition: none;\n}\n\n.tlui-page-menu__item[data-dragging='true'] {\n background: var(--tl-color-panel);\n opacity: 1;\n}\n\n.tlui-page-menu__item__drag-handle {\n flex: 0 0 32px;\n min-width: 32px;\n padding: 0px;\n color: var(--tl-color-text-3);\n cursor: grab;\n touch-action: none;\n}\n\n.tlui-page-menu__item__drag-handle > .tlui-icon {\n position: relative;\n inset-inline-start: 1px;\n}\n\n.tlui-page-menu__item__drag-handle + .tlui-page-menu__item__button {\n padding-inline-start: 0px;\n}\n\n.tlui-page-menu__item__input {\n margin-inline-start: 12px;\n}\n\n.tlui-page-menu__item__submenu {\n position: absolute;\n inset-inline-end: 0px;\n top: 0px;\n height: 100%;\n z-index: 10;\n}\n\n.tlui-page-menu__item__submenu > .tlui-button {\n opacity: 0;\n color: var(--tl-color-text-3);\n}\n\n.tlui-page-menu__item__submenu > .tlui-button::after {\n content: none;\n}\n\n.tlui-page-menu__item:has(.tlui-button:focus-visible)\n > .tlui-page-menu__item__submenu\n > .tlui-button,\n.tlui-page-menu__item__submenu > .tlui-button[data-state='open'],\n.tlui-page-menu__item__submenu > .tlui-button:focus-visible {\n opacity: 1;\n}\n\n/* Hide the kebab while any row is being dragged: :hover sticks during pointer */\n/* capture, which would otherwise leave the kebab visible mid-drag. */\n.tlui-page-menu__list__content[data-dragging='true']\n .tlui-page-menu__item\n > .tlui-page-menu__item__submenu\n > .tlui-button {\n opacity: 0;\n}\n\n.tl-container[data-coarse='true'] .tlui-page-menu__item__submenu > .tlui-button {\n opacity: 1;\n}\n\n.tl-container[data-coarse='true'] .tlui-page-menu__item__button > .tlui-button__label {\n padding-inline-end: calc(40px - 12px);\n}\n\n@media (hover: hover) {\n .tlui-page-menu__resize-handle:hover::before {\n background: var(--tl-color-selected);\n height: 2px;\n transition: background-color 150ms 80ms;\n }\n\n .tlui-page-menu__item:not([data-editing='true']):hover::before {\n opacity: 1;\n }\n\n .tlui-page-menu__item:hover > .tlui-page-menu__item__submenu > .tlui-button {\n opacity: 1;\n }\n\n .tlui-page-menu__item__button > .tlui-button__label {\n padding-inline-end: 0px;\n }\n\n .tlui-page-menu__item:hover > .tlui-page-menu__item__button > .tlui-button__label {\n padding-inline-end: calc(40px - 12px);\n }\n\n .tlui-page-menu__item__submenu > .tlui-button:hover {\n color: var(--tl-color-text);\n }\n}\n/* -------------- Skip to main content -------------- */\n\n.tl-skip-to-main-content {\n position: fixed;\n top: 48px;\n left: -9999px;\n padding: 8px 16px;\n z-index: var(--tl-layer-toasts);\n}\n\n.tl-skip-to-main-content:focus {\n left: 8px;\n}\n\n/*\n * focusing skip-to-main-content hide top ui buttons to make sure we avoid\n * z-index clashes on hover (see issue #8328)\n */\n.tl-skip-to-main-content:focus + .tlui-layout__top .tlui-helper-buttons {\n display: none;\n}\n\n/* ---------------- Offline indicator --------------- */\n\n.tlui-offline-indicator {\n display: flex;\n color: var(--tl-color-background);\n background-color: var(--tl-color-text);\n border: none;\n width: 32px;\n height: 32px;\n align-items: center;\n justify-content: center;\n border-radius: 99px;\n cursor: default;\n opacity: 0;\n animation: tl-fade-in;\n animation-duration: 0.12s;\n animation-delay: 2s;\n animation-fill-mode: forwards;\n}\n\n/* ------------------- Style panel ------------------ */\n\n.tlui-style-panel__wrapper {\n box-shadow: var(--tl-shadow-2);\n border-radius: var(--tl-radius-3);\n pointer-events: all;\n background-color: var(--tl-color-panel);\n height: fit-content;\n max-height: 100%;\n margin: 8px;\n margin-top: 4px;\n touch-action: auto;\n overscroll-behavior: none;\n overflow-y: auto;\n overflow-x: hidden;\n color: var(--tl-color-text);\n}\n/* if the style panel is the only child (ie no share menu), increase the margin */\n.tlui-style-panel__wrapper:only-child {\n margin-top: 8px;\n}\n\n.tlui-style-panel {\n position: relative;\n z-index: var(--tl-layer-panels);\n pointer-events: all;\n width: 148px;\n max-width: 148px;\n}\n\n.tlui-style-panel[data-enhanced-a11y-mode='true'] .tlui-button[data-isactive='true'] {\n border-radius: 10px;\n outline: 2px solid var(--tl-color-text);\n outline-offset: -5px;\n}\n\n.tlui-style-panel::-webkit-scrollbar {\n display: none;\n}\n\n.tlui-style-panel .tlui-button.select {\n width: 100%;\n}\n\n.tlui-style-panel__section {\n display: flex;\n position: relative;\n flex-direction: column;\n}\n\n/*\nadd a border to the bottom of all but the last section. we have to handle empty sections too, which\nare hidden and shouldn't be counted\n*/\n.tlui-style-panel__section:not(:nth-last-child(-n + 1 of .tlui-style-panel__section:not(:empty))) {\n border-bottom: 1px solid var(--tl-color-divider);\n}\n/*\nif a section ends with a slider and we're adding a border, we need some extra space for visual\nbalance. we need to handle empty sections as above. is this the most complex css selector in all of\ntldraw? probably.\n*/\n.tlui-style-panel__section:has(.tlui-slider__container:last-child):not(\n :nth-last-child(-n + 1 of .tlui-style-panel__section:not(:empty))\n ) {\n margin-bottom: 7px;\n}\n\n.tlui-style-panel__section:empty {\n display: none;\n}\n\n/*\n * This is used in a couple places, like Align and Vertical Align.\n * It's because we have a toolbar with a Toggle Group but then an adjacent button\n * next to it that opens a popup.\n */\n.tlui-style-panel__section .tlui-toolbar:has(.tlui-toolbar) {\n flex-wrap: wrap;\n}\n\n.tlui-style-panel__section .tlui-toolbar:has(.tlui-toolbar) .tlui-style-panel__subheading {\n margin-left: -2px;\n}\n\n.tlui-style-panel__section__common:not(:only-child) {\n margin-bottom: 7px;\n border-bottom: 1px solid var(--tl-color-divider);\n}\n\n.tlui-toolbar-toggle-group-item,\n.tlui-style-panel__dropdown-picker > .tlui-button__icon {\n width: 40px;\n padding: 0;\n}\n\n.tlui-style-panel__dropdown-picker:only-child {\n flex: 1;\n}\n\n.tlui-style-panel__double-select-picker {\n display: flex;\n grid-template-columns: 1fr auto;\n align-items: center;\n padding-left: var(--tl-space-4);\n color: var(--tl-color-text-1);\n font-size: 12px;\n}\n\n.tlui-style-panel__double-select-picker-label {\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n flex-grow: 2;\n max-width: 100%;\n}\n\n.tlui-style-panel .tlui-button[data-state='open']::after {\n opacity: 1;\n background: linear-gradient(270deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n}\n\n@media (hover: hover) {\n .tlui-style-panel .tlui-button[aria-expanded='true'] {\n background: none;\n }\n .tlui-style-panel .tlui-button[data-state='open']:not(:hover)::after {\n opacity: 1;\n background: linear-gradient(270deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n }\n}\n\n/* Accessibility subheadings */\n\n.tlui-style-panel__section .tlui-style-panel__subheading,\n.tlui-style-panel__section__common .tlui-style-panel__subheading,\n.tlui-style-panel__subheading + .tlui-slider__container {\n margin: 0;\n padding: var(--tl-space-2) var(--tl-space-3) 0px var(--tl-space-4);\n font-size: 12px;\n font-weight: inherit;\n line-height: inherit;\n}\n\n.tlui-style-panel .tlui-style-panel__subheading:nth-of-type(1) {\n padding-top: var(--tl-space-3);\n}\n\n.tlui-style-panel__subheading + .tlui-slider__container {\n padding-top: 0px;\n}\n\n/* --------------------- Bottom --------------------- */\n\n.tlui-layout__bottom {\n grid-row: 2;\n width: 100%;\n min-width: 0px;\n}\n\n.tlui-layout__bottom__main {\n width: 100%;\n display: flex;\n align-items: flex-end;\n justify-content: center;\n}\n\n/* ------------------- Navigation ------------------- */\n\n.tlui-navigation-panel {\n display: flex;\n width: min-content;\n flex-direction: column;\n z-index: var(--tl-layer-panels);\n pointer-events: all;\n position: absolute;\n left: 0px;\n}\n\n.tl-container[dir='rtl'] .tlui-navigation-panel {\n left: auto;\n right: 0px;\n}\n\n.tlui-navigation-panel::before {\n content: '';\n display: block;\n position: absolute;\n z-index: -1;\n inset: -2px -2px 0px 0px;\n border-radius: 0;\n border-top: 2px solid var(--tl-color-background);\n border-right: 2px solid var(--tl-color-background);\n border-top-right-radius: var(--tl-radius-4);\n background-color: var(--tl-color-low);\n}\n\n.tl-container[dir='rtl'] .tlui-navigation-panel::before {\n inset: -2px 0px 0px -2px;\n border-right: 0;\n border-left: 2px solid var(--tl-color-background);\n border-top-right-radius: 0;\n border-top-left-radius: var(--tl-radius-4);\n}\n\n.tlui-navigation-panel[data-a11y='true']::before {\n display: none;\n}\n\n/* Minimap */\n\n.tlui-minimap {\n width: 100%;\n height: 96px;\n min-height: 96px;\n overflow: hidden;\n padding: var(--tl-space-3);\n padding-top: 0px;\n}\n\n.tlui-minimap__canvas {\n position: relative;\n width: 100%;\n height: 100%;\n}\n\n/* --------------------- Toolbar -------------------- */\n\n/* Wide container */\n.tlui-main-toolbar {\n grid-column: 1 / span 3;\n grid-row: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n flex-grow: 2;\n}\n\n.tlui-main-toolbar--horizontal {\n padding-bottom: calc(var(--tl-space-3) + var(--tl-sab));\n max-width: 100%;\n}\n\n.tlui-main-toolbar--vertical {\n position: absolute;\n left: 0;\n top: 90px; /* height of page menu + 'back to content' button */\n bottom: 140px; /* height of expanded mini-map */\n padding-left: calc(var(--tl-space-3) + var(--tl-sab));\n}\n\n[data-breakpoints-below*='6'] .tlui-main-toolbar--vertical {\n bottom: 90px; /* collapsed mini-map, but same as top to keep things looking even */\n}\n\n/* Centered Content */\n.tlui-main-toolbar__inner {\n position: relative;\n width: fit-content;\n display: flex;\n gap: var(--tl-space-3);\n align-items: flex-start;\n}\n\n.tlui-main-toolbar--vertical .tlui-main-toolbar__inner {\n flex-direction: column;\n}\n\n.tlui-main-toolbar--horizontal .tlui-main-toolbar__left {\n width: fit-content;\n}\n.tlui-main-toolbar--vertical .tlui-main-toolbar__left {\n display: flex;\n height: fit-content;\n}\n\n/* Row of controls + lock button */\n.tlui-main-toolbar__extras {\n position: relative;\n z-index: var(--tl-layer-above);\n pointer-events: none;\n align-self: stretch;\n}\n\n.tlui-main-toolbar__extras:empty {\n display: none;\n}\n\n.tlui-main-toolbar--horizontal .tlui-main-toolbar__extras {\n height: 48px;\n top: 6px;\n}\n\n.tlui-main-toolbar--vertical .tlui-main-toolbar__extras {\n width: 48px;\n order: 1;\n}\n\n.tlui-main-toolbar__extras__controls {\n position: relative;\n z-index: var(--tl-layer-above);\n background-color: var(--tl-color-low);\n border: 2px solid var(--tl-color-background);\n pointer-events: all;\n}\n\n.tlui-main-toolbar--horizontal .tlui-main-toolbar__extras__controls {\n border-top-left-radius: var(--tl-radius-4);\n border-top-right-radius: var(--tl-radius-4);\n margin-left: 8px;\n margin-right: 0px;\n width: fit-content;\n}\n\n.tlui-main-toolbar--vertical .tlui-main-toolbar__extras__controls {\n border-top-right-radius: var(--tl-radius-4);\n border-bottom-right-radius: var(--tl-radius-4);\n margin-top: 8px;\n margin-left: -2px;\n margin-bottom: 0px;\n width: fit-content;\n}\n\n.tlui-main-toolbar__tools {\n border-radius: var(--tl-radius-4);\n z-index: var(--tl-layer-panels);\n pointer-events: all;\n position: relative;\n background: var(--tl-color-panel);\n box-shadow: var(--tl-shadow-2);\n contain: layout style paint;\n}\n\n.tlui-main-toolbar--horizontal .tlui-main-toolbar__mobile-style-panel {\n align-self: flex-end;\n}\n.tlui-main-toolbar--vertical .tlui-main-toolbar__mobile-style-panel {\n align-self: flex-start;\n}\n\n.tlui-main-toolbar--horizontal .tlui-main-toolbar__overflow {\n width: 40px;\n margin-left: 2px;\n}\n.tlui-main-toolbar--vertical .tlui-main-toolbar__overflow {\n height: 40px;\n margin-top: 2px;\n}\n\n.tlui-layout__mobile .tlui-main-toolbar--horizontal .tlui-main-toolbar__overflow {\n width: 32px;\n padding: 0px;\n}\n\n.tlui-main-toolbar--horizontal *[data-state='open']::after {\n background: linear-gradient(0deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n}\n.tlui-main-toolbar--vertical *[data-state='open']::after {\n background: linear-gradient(90deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n}\n\n.tlui-main-toolbar__overflow-content {\n touch-action: none;\n}\n\n.tlui-main-toolbar__tools [data-toolbar-visible='false'],\n.tlui-main-toolbar__overflow-content [data-toolbar-visible='false'] {\n display: none;\n}\n\n.tlui-main-toolbar__group:empty {\n display: none;\n}\n.tlui-row.tlui-main-toolbar__group:not(:nth-last-child(-n + 1 of [data-toolbar-visible='true'])) {\n border-right: 1px solid var(--tl-color-divider);\n margin-right: 2px;\n}\n.tlui-column.tlui-main-toolbar__group:not(\n :nth-last-child(-n + 1 of [data-toolbar-visible='true'])\n ) {\n border-bottom: 1px solid var(--tl-color-divider);\n margin-bottom: 2px;\n}\n.tlui-grid.tlui-main-toolbar__group {\n grid-column: 1 / span 4;\n}\n.tlui-grid.tlui-main-toolbar__group:not(:nth-last-child(-n + 1 of [data-toolbar-visible='true'])) {\n border-bottom: 1px solid var(--tl-color-divider);\n margin-bottom: 2px;\n}\n\n@media (hover: hover) {\n .tlui-main-toolbar--horizontal *[data-state='open']:not(:hover)::after {\n background: linear-gradient(0deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n }\n .tlui-main-toolbar--vertical *[data-state='open']:not(:hover)::after {\n background: linear-gradient(90deg, rgba(144, 144, 144, 0) 0%, var(--tl-color-muted-2) 100%);\n opacity: 1;\n }\n}\n\n/* ------------------- Tooltip -------------------- */\n\n.tlui-tooltip {\n font-size: 12px;\n padding: 2px 8px;\n border-radius: 4px;\n background-color: var(--tl-color-tooltip);\n box-shadow: none;\n color: var(--tl-color-text-shadow);\n max-width: 400px;\n width: fit-content;\n text-align: center;\n will-change: transform, opacity;\n z-index: 2;\n}\n\n.tlui-tooltip__arrow {\n fill: var(--tl-color-tooltip);\n will-change: opacity;\n}\n\n[data-radix-popper-content-wrapper]:has(.tlui-tooltip) {\n z-index: var(--tl-layer-toasts) !important;\n pointer-events: none;\n}\n\n/* ------------------- Debug panel ------------------ */\n\n.tlui-debug-panel {\n background-color: var(--tl-color-low);\n width: 100%;\n display: grid;\n align-items: center;\n grid-template-columns: 1fr auto auto auto;\n justify-content: space-between;\n padding-inline-start: var(--tl-space-4);\n border-top: 1px solid var(--tl-color-background);\n font-size: 12px;\n color: var(--tl-color-text-1);\n z-index: var(--tl-layer-panels);\n pointer-events: all;\n}\n\n.tlui-debug-panel__current-state {\n overflow: hidden;\n white-space: nowrap;\n text-overflow: ellipsis;\n}\n\n.tlui-debug-panel__fps {\n margin-right: 8px;\n}\n\n.tlui-debug-panel__fps__slow {\n font-weight: bold;\n color: var(--tl-color-danger);\n}\n\n.tlui-a11y-audit {\n border-collapse: collapse;\n}\n\n.tlui-a11y-audit th,\n.tlui-a11y-audit td {\n padding: 8px;\n border: 1px solid var(--tl-color-low-border);\n}\n\n/* --------------------- Toasts --------------------- */\n\n.tlui-toast__viewport {\n position: absolute;\n inset: 0px;\n margin: 0px;\n display: flex;\n align-items: flex-end;\n justify-content: flex-end;\n flex-direction: column;\n gap: var(--tl-space-3);\n pointer-events: none;\n padding: 0px var(--tl-space-3) 64px 0px;\n z-index: var(--tl-layer-toasts);\n}\n\n.tl-container[dir='rtl'] .tlui-toast__viewport {\n padding: 0px 0px 64px var(--tl-space-3);\n}\n\n.tlui-toast__viewport > * {\n pointer-events: all;\n}\n\n.tlui-toast__icon {\n padding-top: 11px;\n padding-inline-start: var(--tl-space-4);\n color: var(--tl-color-text-1);\n}\n\n.tlui-toast__container {\n min-width: 200px;\n display: flex;\n flex-direction: row;\n background-color: var(--tl-color-panel);\n box-shadow: var(--tl-shadow-2);\n border-radius: var(--tl-radius-3);\n font-size: 12px;\n}\n\n.tlui-toast__container[data-severity='success'] .tlui-icon {\n color: var(--tl-color-success);\n}\n\n.tlui-toast__container[data-severity='info'] .tlui-icon {\n color: var(--tl-color-info);\n}\n\n.tlui-toast__container[data-severity='warning'] .tlui-icon {\n color: var(--tl-color-warning);\n}\n\n.tlui-toast__container[data-severity='error'] .tlui-icon {\n color: var(--tl-color-danger);\n}\n\n.tlui-toast__main {\n flex-grow: 2;\n max-width: 280px;\n min-width: 0;\n}\n\n.tlui-toast__content {\n padding: var(--tl-space-4);\n display: flex;\n line-height: 1.4;\n flex-direction: column;\n gap: var(--tl-space-3);\n min-width: 0;\n}\n\n.tlui-toast__main[data-actions='true'] .tlui-toast__content {\n padding-bottom: var(--tl-space-2);\n}\n\n.tlui-toast__title {\n font-weight: bold;\n color: var(--tl-color-text-1);\n /* this makes the default toast look better */\n line-height: 16px;\n overflow-wrap: anywhere;\n}\n\n.tlui-toast__description {\n color: var(--tl-color-text-1);\n padding: var(--tl-space-3);\n margin: 0px;\n padding: 0px;\n overflow-wrap: anywhere;\n}\n\n.tlui-toast__icon + .tlui-toast__main > .tlui-toast__actions {\n padding-left: 0px;\n}\n\n.tlui-toast__actions {\n display: flex;\n flex-direction: row;\n justify-content: flex-start;\n margin-left: 0;\n}\n\n.tlui-toast__close {\n align-self: flex-end;\n flex-shrink: 0;\n}\n\n@media (prefers-reduced-motion: no-preference) {\n .tlui-toast__container[data-state='open'] {\n animation: tlui-slide-in 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86);\n }\n\n .tlui-toast__container[data-state='closed'] {\n animation: tlui-fade-out 100ms ease-in;\n }\n\n .tlui-toast__container[data-swipe='move'] {\n transform: translateX(var(--radix-toast-swipe-move-x));\n }\n\n .tlui-toast__container[data-swipe='cancel'] {\n transform: translateX(0);\n transition: transform 200ms ease-out;\n }\n\n .tlui-toast__container[data-swipe='end'] {\n animation: tlui-slide-out 100ms ease-out;\n }\n\n .tl-container[dir='rtl'] .tlui-toast__container[data-state='open'] {\n animation: tlui-slide-in-rtl 200ms cubic-bezier(0.785, 0.135, 0.15, 0.86);\n }\n\n .tl-container[dir='rtl'] .tlui-toast__container[data-state='closed'] {\n animation: tlui-fade-out 100ms ease-in;\n }\n\n .tl-container[dir='rtl'] .tlui-toast__container[data-swipe='end'] {\n animation: tlui-slide-out-rtl 100ms ease-out;\n }\n}\n\n/* ---------------- Dialog ---------------- */\n\n.tlui-dialog__overlay {\n position: absolute;\n top: 0px;\n left: 0px;\n width: 100%;\n height: 100%;\n z-index: var(--tl-layer-canvas-overlays);\n background-color: var(--tl-color-overlay);\n animation: tl-fade-in 0.12s ease-out;\n}\n\n.tlui-dialog__positioner {\n position: absolute;\n top: 0px;\n left: 0px;\n width: 100%;\n height: 100%;\n z-index: var(--tl-layer-canvas-overlays);\n pointer-events: all;\n display: grid;\n place-items: center;\n overflow-y: auto;\n padding: 0px var(--tl-space-3);\n}\n\n.tlui-dialog__content {\n display: flex;\n flex-direction: column;\n position: relative;\n cursor: default;\n background-color: var(--tl-color-panel);\n box-shadow: var(--tl-shadow-3);\n border-radius: var(--tl-radius-3);\n font-size: 12px;\n overflow: hidden;\n min-width: 300px;\n max-width: 100%;\n max-height: 80%;\n}\n\n.tlui-dialog__header {\n position: relative;\n display: flex;\n align-items: center;\n flex: 0;\n z-index: var(--tl-layer-header-footer);\n padding-inline-start: var(--tl-space-4);\n color: var(--tl-color-text);\n height: 40px;\n}\n\n.tlui-dialog__header__title {\n flex: 1;\n font-weight: inherit;\n font-size: 12px;\n margin: 0px;\n color: var(--tl-color-text-1);\n}\n\n.tlui-dialog__header__close {\n justify-self: flex-end;\n}\n\n.tlui-dialog__body {\n padding: var(--tl-space-4) var(--tl-space-4);\n flex: 0 1;\n overflow-y: auto;\n overflow-x: hidden;\n color: var(--tl-color-text-1);\n user-select: all;\n -webkit-user-select: text;\n overflow-wrap: anywhere;\n}\n.tlui-dialog__body a {\n color: var(--tl-color-selected);\n}\n\n.tlui-dialog__body ul,\n.tlui-dialog__body ol {\n padding-left: 16px;\n display: flex;\n flex-direction: column;\n gap: var(--tl-space-2);\n}\n\n.tlui-dialog__footer {\n position: relative;\n min-height: 12px;\n z-index: var(--tl-layer-header-footer);\n}\n\n.tlui-dialog__footer__actions {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n}\n\n.tlui-dialog__footer__actions > .tlui-button:nth-last-child(n + 2) {\n margin-right: -4px;\n}\n\n/* --------------------- Dialogs -------------------- */\n\n/* Edit Link Dialog */\n\n.tlui-edit-link-dialog {\n display: flex;\n flex-direction: column;\n gap: var(--tl-space-4);\n color: var(--tl-color-text);\n}\n\n.tlui-edit-link-dialog__input {\n background-color: var(--tl-color-muted-2);\n flex-grow: 2;\n border-radius: var(--tl-radius-2);\n padding: 0px var(--tl-space-4);\n}\n\n/* Embed Dialog */\n\n.tlui-embed__spacer {\n flex-grow: 2;\n min-height: 0px;\n margin-left: calc(-1 * var(--tl-space-4));\n margin-top: calc(-1 * var(--tl-space-4));\n pointer-events: none;\n}\n\n.tlui-embed-dialog__list {\n display: flex;\n flex-direction: column;\n padding: 0px var(--tl-space-3) var(--tl-space-4) var(--tl-space-3);\n}\n\n.tlui-embed-dialog__item__image {\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n background-size: contain;\n background-repeat: no-repeat;\n background-position: center center;\n background-color: var(--tl-color-selected-contrast);\n border-radius: var(--tl-radius-1);\n}\n\n.tlui-embed-dialog__enter {\n display: flex;\n flex-direction: column;\n gap: var(--tl-space-4);\n color: var(--tl-color-text-1);\n}\n\n.tlui-embed-dialog__input {\n background-color: var(--tl-color-muted-2);\n flex-grow: 2;\n border-radius: var(--tl-radius-2);\n padding: 0px var(--tl-space-4);\n}\n\n.tlui-embed-dialog__warning {\n color: var(--tl-color-danger);\n text-shadow: none;\n}\n\n.tlui-embed-dialog__instruction__link {\n display: flex;\n gap: var(--tl-space-1);\n margin-top: var(--tl-space-4);\n}\n\n.tlui-embed-dialog__enter a {\n color: var(--tl-color-text-1);\n}\n\n/* --------------- Keyboard shortcuts --------------- */\n\n.tlui-shortcuts-dialog__header {\n border-bottom: 1px solid var(--tl-color-divider);\n}\n\n.tlui-shortcuts-dialog__body {\n position: relative;\n columns: 3;\n column-gap: var(--tl-space-9);\n pointer-events: all;\n touch-action: auto;\n\n /* Terrible fix to allow firefox users to scroll the dialog */\n overflow-x: auto;\n\n scrollbar-width: none;\n}\n\n.tlui-shortcuts-dialog__body::-webkit-scrollbar {\n display: none;\n}\n\n.tlui-shortcuts-dialog__body__tablet {\n columns: 2;\n}\n\n.tlui-shortcuts-dialog__body__mobile {\n columns: 1;\n}\n\n.tlui-shortcuts-dialog__group {\n break-inside: avoid-column;\n padding-bottom: var(--tl-space-6);\n}\n\n.tlui-shortcuts-dialog__group__title {\n font-size: inherit;\n font-weight: inherit;\n margin: 0px;\n color: var(--tl-color-text-3);\n height: 32px;\n display: flex;\n align-items: center;\n}\n\n.tlui-shortcuts-dialog__group__content {\n display: flex;\n flex-direction: column;\n color: var(--tl-color-text-1);\n}\n\n.tlui-shortcuts-dialog__key-pair {\n display: flex;\n gap: var(--tl-space-4);\n align-items: center;\n justify-content: space-between;\n height: 32px;\n}\n\n.tlui-shortcuts-dialog__key-pair__key {\n flex: 1;\n font-size: 12px;\n}\n\n/* ------------------ Language menu ----------------- */\n\n.tlui-language-menu {\n max-height: 500px;\n}\n\n.tlui-language-menu::after {\n content: '';\n display: block;\n position: absolute;\n bottom: 0px;\n left: 0px;\n right: 0px;\n height: 24px;\n background: linear-gradient(\n to bottom,\n var(--tl-color-panel-transparent) 0%,\n var(--tl-color-panel) 90%,\n var(--tl-color-panel) 100%\n );\n border-bottom-left-radius: var(--tl-radius-3);\n border-bottom-right-radius: var(--tl-radius-3);\n pointer-events: none;\n}\n\n/* ------------------ Actions menu ------------------ */\n\n.tlui-actions-menu {\n max-height: calc(100vh - 150px);\n}\n\n/* -------------------- Help menu ------------------- */\n\n.tlui-help-menu {\n pointer-events: all;\n position: absolute;\n bottom: var(--tl-space-2);\n right: var(--tl-space-2);\n z-index: var(--tl-layer-panels);\n border: 2px solid var(--tl-color-background);\n border-radius: 100%;\n}\n\n/* ------------------- Da share zone ------------------ */\n\n.tlui-share-zone {\n padding: 0px 0px 0px 0px;\n display: flex;\n flex-direction: row;\n justify-content: flex-end;\n z-index: var(--tl-layer-panels);\n align-items: center;\n padding-top: 2px;\n padding-right: 4px;\n}\n\n/* ------------------- People Menu ------------------- */\n\n.tlui-people-menu__avatars-button {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n background: none;\n border: none;\n cursor: pointer;\n pointer-events: all;\n border-radius: var(--tl-radius-1);\n padding-right: 1px;\n height: 100%;\n}\n\n.tlui-people-menu__avatars {\n display: flex;\n flex-direction: row;\n}\n\n.tlui-people-menu__avatar {\n height: 24px;\n width: 24px;\n border: 2px solid var(--tl-color-background);\n background-color: var(--tl-color-low);\n border-radius: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n position: relative;\n font-size: 10px;\n font-weight: bold;\n text-align: center;\n color: var(--tl-color-selected-contrast);\n z-index: 2;\n}\n\n.tlui-people-menu__avatar:nth-of-type(n + 2) {\n margin-inline-start: -12px;\n}\n\n.tlui-people-menu__avatars-button[data-state='open'] {\n opacity: 1;\n}\n\n@media (hover: hover) {\n .tlui-people-menu__avatars-button:hover .tlui-people-menu__avatar {\n border-color: var(--tl-color-low);\n }\n}\n\n.tlui-people-menu__more {\n min-width: 0px;\n font-size: 11px;\n font-weight: 600;\n color: var(--tl-color-text-1);\n font-family: inherit;\n padding: 0px 4px;\n}\n.tlui-people-menu__more::after {\n border-radius: var(--tl-radius-2);\n inset: 0px;\n}\n\n.tlui-people-menu__wrapper {\n position: relative;\n display: flex;\n flex-direction: column;\n width: 220px;\n height: fit-content;\n max-height: 50vh;\n}\n\n.tlui-people-menu__section {\n position: relative;\n touch-action: auto;\n flex-direction: column;\n max-height: 100%;\n overflow-x: hidden;\n overflow-y: auto;\n touch-action: auto;\n}\n\n.tlui-people-menu__section:first-child,\n.tlui-people-menu__section:last-child {\n flex-shrink: 0;\n}\n\n.tlui-people-menu__section:not(:last-child) {\n border-bottom: 1px solid var(--tl-color-divider);\n}\n\n.tlui-people-menu__user {\n display: flex;\n justify-content: flex-start;\n align-items: center;\n}\n\n.tlui-people-menu__user__color {\n flex-shrink: 0;\n}\n\n.tlui-people-menu__user__name {\n text-align: start;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n color: var(--tl-color-text-1);\n max-width: 100%;\n flex-grow: 1;\n flex-shrink: 100;\n}\n\n.tlui-people-menu__user__label {\n text-align: start;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 12px;\n color: var(--tl-color-text-3);\n flex-grow: 100;\n flex-shrink: 0;\n margin-inline-start: 4px;\n}\n\n.tlui-people-menu__user__input {\n flex-grow: 2;\n height: 100%;\n padding: 0px;\n margin: 0px;\n}\n\n.tlui-people-menu__user > .tlui-input__wrapper {\n width: auto;\n display: flex;\n align-items: auto;\n flex-grow: 2;\n gap: 8px;\n height: 100%;\n padding: 0px;\n}\n\n.tlui-people-menu__item {\n position: relative;\n}\n\n.tlui-people-menu__item:last-of-type .tlui-button__menu {\n margin-bottom: 0;\n}\n\n.tlui-people-menu__item__button {\n padding: 0 11px;\n overflow: hidden;\n}\n\n.tlui-people-menu__item > .tlui-button__menu {\n width: auto;\n display: flex;\n align-items: auto;\n justify-content: flex-start;\n flex-grow: 2;\n gap: 11px;\n}\n\n.tlui-people-menu__name {\n text-align: start;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n}\n\n.tlui-people-menu__item__follow {\n position: absolute;\n top: 0px;\n inset-inline-end: 0px;\n max-width: 40px;\n flex-shrink: 0;\n}\n\n.tlui-people-menu__item[data-follow='true'],\n.tlui-people-menu__item:has(.tlui-button:focus-visible) {\n padding-inline-end: 36px;\n}\n\n.tlui-people-menu__item[data-follow='true'] .tlui-people-menu__item__follow,\n.tlui-people-menu__item:has(.tlui-button:focus-visible) .tlui-people-menu__item__follow {\n opacity: 1;\n}\n\n@media (hover: hover) {\n .tlui-people-menu__item__follow {\n opacity: 0;\n }\n\n .tlui-people-menu__item:hover {\n padding-right: 36px;\n }\n .tlui-people-menu__item:hover .tlui-people-menu__item__follow {\n opacity: 1;\n }\n}\n\n/* --------------- Following indicator -------------- */\n\n.tlui-following-indicator {\n display: block;\n position: absolute;\n inset: 0px;\n border-width: 2px;\n border-style: solid;\n pointer-events: none;\n}\n\n/* --------------- Contextual toolbar --------------- */\n\n.tlui-contextual-toolbar {\n position: absolute;\n}\n\n/**\n * N.B. We hide the toolbar while mousing down, so no need to turn these off.\n */\n.tlui-contextual-toolbar,\n.tlui-contextual-toolbar * {\n pointer-events: all;\n}\n\n.tlui-contextual-toolbar [data-isactive='true']::after {\n background-color: var(--tl-color-muted-2);\n opacity: 1;\n}\n\n.tlui-contextual-toolbar {\n opacity: 0;\n transition: opacity 0.08s ease-in-out;\n}\n\n.tlui-contextual-toolbar,\n.tlui-contextual-toolbar * {\n pointer-events: none;\n}\n\n.tlui-contextual-toolbar[data-visible='true'] {\n opacity: 1;\n z-index: var(--tl-layer-menus);\n}\n\n.tlui-contextual-toolbar[data-interactive='true'],\n.tlui-contextual-toolbar[data-interactive='true'] * {\n pointer-events: all;\n}\n\n.tlui-rich-text__toolbar-link-input {\n margin-left: 12px;\n /*\n * Nice touch tweak: keep the link editor toolbar the same as the default toolbar.\n * This is so the toolbar size stays stable going in and out of the link editor.\n */\n width: 148px;\n}\n\n.tlui-media__toolbar-alt-text-input {\n margin-left: 12px;\n /*\n * Nice touch tweak: keep the link editor toolbar the same as the default toolbar.\n * This is so the toolbar size stays stable going in and out of the alt text editor.\n */\n min-width: 200px;\n}\n\n.tlui-contextual-toolbar .tlui-input__wrapper {\n height: 40px;\n}\n\n.tlui-image__toolbar .tlui-slider__container {\n width: 125px;\n}\n\n.tlui-image__toolbar .tlui-slider {\n height: 100%;\n}\n\n.tlui-image__toolbar .tlui-slider__track {\n height: 32px;\n}\n\n.tlui-image__toolbar .tlui-slider__thumb {\n width: 14px;\n height: 14px;\n}\n\n/* ------------------- Animations ------------------- */\n@keyframes tlui-fade-out {\n 0% {\n opacity: 1;\n }\n 100% {\n opacity: 0;\n }\n}\n\n@keyframes tlui-slide-in {\n from {\n transform: translateX(calc(100% + var(--tl-space-3)));\n }\n to {\n transform: translateX(0px);\n }\n}\n\n@keyframes tlui-slide-out {\n from {\n transform: translateX(var(--radix-toast-swipe-end-x));\n }\n to {\n transform: translateX(calc(100% + var(--tl-space-3)));\n }\n}\n\n@keyframes tlui-slide-in-rtl {\n from {\n transform: translateX(calc(-100% - var(--tl-space-3)));\n }\n to {\n transform: translateX(0px);\n }\n}\n\n@keyframes tlui-slide-out-rtl {\n from {\n transform: translateX(var(--radix-toast-swipe-end-x));\n }\n to {\n transform: translateX(calc(-100% - var(--tl-space-3)));\n }\n}\n\n";
120861
+ //#endregion
120862
+ //#region lib/types/client/tl/tl-theme-css.js
120863
+ /**
120864
+ * DSH image-gen brand skin for the tldraw surface.
120865
+ *
120866
+ * Injected AFTER the bundled tldraw styles (tl-css.ts): it only re-declares
120867
+ * tldraw's own CSS variables and restyles tldraw's own chrome classes, scoped
120868
+ * to our canvas container, so upgrading tldraw never requires edits here. This
120869
+ * layer owns the UI chrome (panels, toolbars, hover/active feedback, grid
120870
+ * dots); colors painted onto the canvas itself (shape palette, selection
120871
+ * handles, marquee) come from the runtime theme patch in tl-brand-theme.ts.
120872
+ * Dark chrome keeps stock tldraw colors: the plugin lives in DSH's light shell.
120873
+ *
120874
+ * Beyond re-tinting, this pass reshapes tldraw's signature chrome so the
120875
+ * surface no longer reads as "stock tldraw": the grey corner strips (top-left
120876
+ * menu zone, bottom-left navigation) become floating white cards with hairline
120877
+ * borders, matching the workbench card language.
120878
+ */
120879
+ /** Hairline border used on every floating chrome card. */
120880
+ const CARD_BORDER = "#d9e2f2";
120881
+ /** Shadow used on every floating chrome card (workbench blue-grey family). */
120882
+ const CARD_SHADOW = "0px 1px 2px rgba(30, 42, 76, 0.06), 0px 4px 12px rgba(30, 42, 76, 0.08)";
120883
+ const TL_THEME_CSS = `
120884
+ /* DSH image-gen skin for the tldraw surface (light chrome only). */
120885
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light {
120886
+ /* Canvas surface: a clearly cool blue-grey board, distinguishable from the
120887
+ pure-white floating cards at a glance. Same token feeds the runtime
120888
+ theme patch (CANVAS_BG in tl-brand-theme.ts). */
120889
+ --tl-color-background: ${CANVAS_BG};
120890
+ --tl-color-grid: rgba(88, 110, 158, 0.5);
120891
+
120892
+ /* Brand accents: selection outlines, active tool, chips, focus rings. */
120893
+ --tl-color-selected: ${BRAND_BLUE};
120894
+ --tl-color-primary: ${BRAND_BLUE};
120895
+ --tl-color-focus: ${BRAND_BLUE};
120896
+ --tl-color-selection-stroke: ${BRAND_BLUE};
120897
+ --tl-color-selection-fill: rgba(${BRAND_BLUE_RGB}, 0.14);
120898
+
120899
+ /* Interaction feedback: hover chips and active-tool highlight are tinted
120900
+ brand blue instead of tldraw's neutral grey-black. */
120901
+ --tl-color-hint: rgba(${BRAND_BLUE_RGB}, 0.14);
120902
+ --tl-color-muted-1: rgba(${BRAND_BLUE_RGB}, 0.14);
120903
+ --tl-color-muted-2: rgba(${BRAND_BLUE_RGB}, 0.07);
120904
+
120905
+ /* Chrome: panels, borders, and text aligned with the workbench tokens. */
120906
+ --tl-color-panel: #ffffff;
120907
+ --tl-color-panel-contrast: #ffffff;
120908
+ --tl-color-low: #ffffff;
120909
+ --tl-color-low-border: ${CARD_BORDER};
120910
+ --tl-color-divider: #dfe6f3;
120911
+ --tl-color-text: ${BRAND_INK};
120912
+ --tl-color-text-0: ${BRAND_INK};
120913
+ --tl-color-text-1: #242f44;
120914
+ --tl-color-text-3: #6b7690;
120915
+
120916
+ /* Shadows: the workbench's cold blue-grey instead of tldraw's black. */
120917
+ --tl-shadow-1: 0px 1px 2px rgba(30, 42, 76, 0.24), 0px 1px 3px rgba(30, 42, 76, 0.08);
120918
+ --tl-shadow-2:
120919
+ 0px 0px 2px rgba(30, 42, 76, 0.14), 0px 2px 3px rgba(30, 42, 76, 0.2),
120920
+ 0px 2px 6px rgba(30, 42, 76, 0.08), inset 0px 0px 0px 1px var(--tl-color-panel-contrast);
120921
+ --tl-shadow-3:
120922
+ 0px 1px 2px rgba(30, 42, 76, 0.24), 0px 2px 6px rgba(30, 42, 76, 0.12),
120923
+ inset 0px 0px 0px 1px var(--tl-color-panel-contrast);
120924
+ --tl-shadow-4:
120925
+ 0px 0px 3px rgba(30, 42, 76, 0.16), 0px 5px 4px rgba(30, 42, 76, 0.13),
120926
+ 0px 2px 16px rgba(30, 42, 76, 0.05), inset 0px 0px 0px 1px var(--tl-color-panel-contrast);
120927
+
120928
+ /* Noticeably rounder chrome than tldraw's stock 4/6/9/11px. */
120929
+ --tl-radius-1: 6px;
120930
+ --tl-radius-2: 10px;
120931
+ --tl-radius-3: 13px;
120932
+ --tl-radius-4: 16px;
120933
+ }
120934
+
120935
+ /* ---------------------------------------------------------------
120936
+ * De-tldraw pass: reshape tldraw's signature corner strips and
120937
+ * floating chrome into the workbench's card language.
120938
+ * ------------------------------------------------------------- */
120939
+
120940
+ /* Canvas backdrop: subtle vertical gradient (light at top, a touch deeper at
120941
+ the bottom) for an airy, lit-from-above feel. Painted on the UI background
120942
+ layer ONLY — --tl-color-background stays a flat color because tldraw reuses
120943
+ it for export backgrounds and edge highlight lines, which must stay clean. */
120944
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tl-background {
120945
+ background-image: linear-gradient(180deg, ${CANVAS_BG_TOP} 0%, ${CANVAS_BG} 60%, ${CANVAS_BG_BOTTOM} 100%);
120946
+ }
120947
+
120948
+ /* Top-left menu strip -> floating white card (Page menu + quick actions). */
120949
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-menu-zone {
120950
+ margin: 10px;
120951
+ background-color: #ffffff;
120952
+ border: 1px solid ${CARD_BORDER};
120953
+ border-radius: 12px;
120954
+ box-shadow: ${CARD_SHADOW};
120955
+ }
120956
+
120957
+ /* Bottom-left navigation strip -> floating white card (zoom, pages, minimap).
120958
+ Offsets detach it from the screen corner; the ::before paints the card. */
120959
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-navigation-panel {
120960
+ left: 12px;
120961
+ bottom: 12px;
120962
+ }
120963
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light[dir='rtl'] .tlui-navigation-panel {
120964
+ left: auto;
120965
+ right: 12px;
120966
+ }
120967
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-navigation-panel::before {
120968
+ inset: -6px -6px -6px -6px;
120969
+ background-color: #ffffff;
120970
+ border: 1px solid ${CARD_BORDER};
120971
+ border-radius: 12px;
120972
+ box-shadow: ${CARD_SHADOW};
120973
+ }
120974
+
120975
+ /* Bottom toolbar pill -> bordered card so it matches the other cards. */
120976
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-main-toolbar__tools {
120977
+ border: 1px solid ${CARD_BORDER};
120978
+ }
120979
+
120980
+ /* Vertical left rail (Figma-style, set via components.Toolbar): float it off
120981
+ the screen edge like the other cards. Stock offsets (top 90px / bottom
120982
+ 140px) already clear the top-left menu card and bottom-left minimap card. */
120983
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-main-toolbar--vertical {
120984
+ left: 12px;
120985
+ padding-left: 0;
120986
+ }
120987
+
120988
+ /* Compact rail buttons (40px instead of stock 48px) for an app-like density
120989
+ that differs from tldraw's chunky mobile-sized chrome. */
120990
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-main-toolbar--vertical .tlui-button__tool {
120991
+ width: 40px;
120992
+ height: 40px;
120993
+ }
120994
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-main-toolbar--vertical .tlui-button__tool::after {
120995
+ inset: 3px;
120996
+ border-radius: 9px;
120997
+ }
120998
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-main-toolbar--vertical .tlui-main-toolbar__extras {
120999
+ width: 40px;
121000
+ }
121001
+
121002
+ /* Right style panel -> bordered card. */
121003
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-style-panel__wrapper {
121004
+ border: 1px solid ${CARD_BORDER};
121005
+ }
121006
+
121007
+ /* Dropdown menus and popovers -> hairline border for definition. */
121008
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-menu,
121009
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-popover__content {
121010
+ border: 1px solid ${CARD_BORDER};
121011
+ }
121012
+
121013
+ /* Active menu chips (e.g. style panel selections): brand tint instead of grey. */
121014
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-button[data-isactive='true'] {
121015
+ color: ${BRAND_BLUE};
121016
+ }
121017
+ .dsh-ig-tl-canvas .tl-container.tl-theme__light .tlui-button[data-isactive='true']::after {
121018
+ background: rgba(${BRAND_BLUE_RGB}, 0.14);
121019
+ }
121020
+ `;
120642
121021
  //#endregion
120643
121022
  //#region lib/types/client/inspiration-style.js
120644
121023
  const INSPIRATION_STYLE = `
@@ -120684,6 +121063,14 @@ img.ProseMirror-separator {
120684
121063
  kind: IMAGE_RESULT_NODE_KIND,
120685
121064
  target: "chat",
120686
121065
  match: (event) => {
121066
+ if (event.type === "tool/ptc-dispatch") {
121067
+ const subCallId = event.data.subCallId;
121068
+ if (typeof subCallId !== "string" || subCallId === "") return null;
121069
+ return imageResultFromPtcDispatch(event) === void 0 ? null : {
121070
+ id: `ptc:${subCallId}`,
121071
+ role: "start"
121072
+ };
121073
+ }
120687
121074
  const turn = eventTurn(event);
120688
121075
  if (turn === void 0) return null;
120689
121076
  if (event.type === "turn/start") return {
@@ -120705,6 +121092,14 @@ img.ProseMirror-separator {
120705
121092
  return null;
120706
121093
  },
120707
121094
  start: (_context, match) => {
121095
+ if (match.event.type === "tool/ptc-dispatch") {
121096
+ const result = imageResultFromPtcDispatch(match.event);
121097
+ if (result === void 0) throw new Error("dsh-image-result start requires a plugin image dispatch");
121098
+ return { results: [{
121099
+ ...result,
121100
+ seq: match.event.seq
121101
+ }] };
121102
+ }
120708
121103
  const turn = eventTurn(match.event);
120709
121104
  if (match.event.type !== "turn/start" || turn === void 0) throw new Error("dsh-image-result start requires turn/start");
120710
121105
  return {
@@ -120721,17 +121116,14 @@ img.ProseMirror-separator {
120721
121116
  ...context.state,
120722
121117
  answerSeq: match.event.seq
120723
121118
  };
121119
+ if (match.event.type === "tool/ptc-dispatch") {
121120
+ const result = imageResultFromPtcDispatch(match.event);
121121
+ return result === void 0 ? context.state : appendResult(context.state, result, match.event.seq);
121122
+ }
120724
121123
  if (match.event.type !== "tool/result") return context.state;
120725
121124
  const result = imageResultFromMeta(match.event.data.meta);
120726
121125
  if (result === void 0) return context.state;
120727
- if (context.state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return context.state;
120728
- return {
120729
- ...context.state,
120730
- results: [...context.state.results, {
120731
- ...result,
120732
- seq: match.event.seq
120733
- }]
120734
- };
121126
+ return appendResult(context.state, result, match.event.seq);
120735
121127
  },
120736
121128
  buildViewNode: (context) => {
120737
121129
  const state = context.state;
@@ -120747,13 +121139,24 @@ img.ProseMirror-separator {
120747
121139
  location,
120748
121140
  visibility: "visible",
120749
121141
  data: {
120750
- turn: state.turn,
121142
+ ...state.turn !== void 0 ? { turn: state.turn } : {},
120751
121143
  results: state.results.map(({ seq: _seq, ...result }) => result)
120752
121144
  }
120753
121145
  };
120754
121146
  }
120755
121147
  };
120756
121148
  }
121149
+ /** Append a parsed result unless its attachment is already collected. */
121150
+ function appendResult(state, result, seq) {
121151
+ if (state.results.some((candidate) => candidate.attachment.attachmentId === result.attachment.attachmentId)) return state;
121152
+ return {
121153
+ ...state,
121154
+ results: [...state.results, {
121155
+ ...result,
121156
+ seq
121157
+ }]
121158
+ };
121159
+ }
120757
121160
  function compactTranscript(options) {
120758
121161
  try {
120759
121162
  return options.isCompactTranscript?.() ?? true;
@@ -120781,6 +121184,60 @@ img.ProseMirror-separator {
120781
121184
  const turn = event.data.turn;
120782
121185
  return typeof turn === "number" && Number.isSafeInteger(turn) && turn >= 0 ? turn : void 0;
120783
121186
  }
121187
+ /** Plugin tools whose PTC (run_code) dispatch results are image results. */
121188
+ const PTC_IMAGE_TOOL_NAMES = /* @__PURE__ */ new Set(["generate_image", "edit_image"]);
121189
+ /**
121190
+ * Fixed summary text rendered by the tools' imageOutput(). PTC dispatch events
121191
+ * carry no presentationMeta, so provider/model/output are recovered from this
121192
+ * template rather than parsed out of natural language.
121193
+ */
121194
+ const PTC_SUMMARY_PATTERN = /^(?:Generated|Edited) one image with ([^/\s]+)\/(\S+) \((.+)\)\. Attachment ID: /;
121195
+ const PTC_SAVED_TO_PATTERN = / It was also saved to the workspace as (.+)\. (?:(?:Saving it to the workspace failed)|(?:It has no local file path)|(?:Respond to the user))/;
121196
+ /**
121197
+ * Parse a plugin image result from a tool/ptc-dispatch event (#38). PTC
121198
+ * run_code sub-calls carry neither a turn nor presentationMeta, so the result
121199
+ * is rebuilt from the dispatch payload itself: the image content block for the
121200
+ * attachment, arguments for the prompt (and edit source ids), and the tool's
121201
+ * fixed-format summary text for provider/model/output. Returns undefined for
121202
+ * anything that is not a successful image result from our own image tools.
121203
+ */
121204
+ function imageResultFromPtcDispatch(event) {
121205
+ if (event.type !== "tool/ptc-dispatch") return void 0;
121206
+ const data = event.data;
121207
+ if (data.isError === true) return void 0;
121208
+ if (typeof data.name !== "string" || !PTC_IMAGE_TOOL_NAMES.has(data.name)) return void 0;
121209
+ const content = Array.isArray(data.content) ? data.content : [];
121210
+ let attachment;
121211
+ let summaryText = "";
121212
+ for (const block of content) {
121213
+ const candidate = record(block);
121214
+ if (candidate === void 0) continue;
121215
+ if (candidate.type === "image" && attachment === void 0) attachment = imageAttachment(candidate.attachment);
121216
+ else if (candidate.type === "text" && summaryText === "" && typeof candidate.text === "string") summaryText = candidate.text;
121217
+ }
121218
+ if (attachment === void 0) return void 0;
121219
+ const summary = PTC_SUMMARY_PATTERN.exec(summaryText);
121220
+ const savedTo = PTC_SAVED_TO_PATTERN.exec(summaryText)?.[1];
121221
+ const args = record(data.arguments);
121222
+ const sourceIds = sourceAttachmentIds(args);
121223
+ return {
121224
+ attachment,
121225
+ prompt: stringValue(args?.prompt, "Generated Image"),
121226
+ provider: summary?.[1] ?? "",
121227
+ model: summary?.[2] ?? "",
121228
+ output: summary?.[3] ?? "",
121229
+ ...savedTo !== void 0 ? { savedTo } : {},
121230
+ ...sourceIds !== void 0 ? { sourceAttachmentIds: sourceIds } : {}
121231
+ };
121232
+ }
121233
+ /** Collect edit_image source attachment ids (single or list form) from dispatch arguments. */
121234
+ function sourceAttachmentIds(args) {
121235
+ if (args === void 0) return void 0;
121236
+ const single = typeof args.source_attachment_id === "string" ? [args.source_attachment_id] : void 0;
121237
+ const list = Array.isArray(args.source_attachment_ids) ? args.source_attachment_ids.filter((id) => typeof id === "string") : void 0;
121238
+ const ids = single ?? list;
121239
+ return ids !== void 0 && ids.length > 0 ? ids : void 0;
121240
+ }
120784
121241
  function imageAttachment(value) {
120785
121242
  const candidate = record(value);
120786
121243
  if (candidate === void 0 || typeof candidate.attachmentId !== "string" || !isImageMediaType(candidate.mediaType) || !positiveInteger(candidate.bytes) || !positiveInteger(candidate.width) || !positiveInteger(candidate.height)) return void 0;
@@ -120896,6 +121353,10 @@ img.ProseMirror-separator {
120896
121353
  keyMissing: "未配置 Key",
120897
121354
  noKeyNeeded: "无需 Key",
120898
121355
  noWorkflow: "未导入工作流",
121356
+ subNoKey: "订阅生图",
121357
+ subSignedIn: "已登录",
121358
+ subSignedOut: "未登录",
121359
+ subStatusUnknown: "状态未知",
120899
121360
  readOnly: "图像设置为只读,无法在此切换",
120900
121361
  switchFailed: "切换失败"
120901
121362
  },
@@ -120906,6 +121367,10 @@ img.ProseMirror-separator {
120906
121367
  keyMissing: "No key",
120907
121368
  noKeyNeeded: "No key needed",
120908
121369
  noWorkflow: "No workflow",
121370
+ subNoKey: "Subscription",
121371
+ subSignedIn: "Signed in",
121372
+ subSignedOut: "Signed out",
121373
+ subStatusUnknown: "Unknown",
120909
121374
  readOnly: "Image settings are read-only; switch them in the config source",
120910
121375
  switchFailed: "Switch failed"
120911
121376
  }
@@ -120919,8 +121384,25 @@ img.ProseMirror-separator {
120919
121384
  dashscope: "DashScope",
120920
121385
  xai: "Grok",
120921
121386
  zhipu: "智谱 GLM",
120922
- comfyui: "ComfyUI"
120923
- };
121387
+ comfyui: "ComfyUI",
121388
+ "chatgpt-sub": "ChatGPT 订阅",
121389
+ "grok-sub": "Grok 订阅",
121390
+ "google-sub": "Google 订阅"
121391
+ };
121392
+ /** Compute the fixed-position anchor from the pill button: open upward when
121393
+ * the chat pane leaves room above the composer, drop down otherwise. */
121394
+ function anchorOf(button) {
121395
+ const rect = button?.getBoundingClientRect();
121396
+ if (rect === void 0) return void 0;
121397
+ if (rect.top >= 332) return {
121398
+ left: rect.left,
121399
+ bottom: window.innerHeight - rect.top + 6
121400
+ };
121401
+ return {
121402
+ left: rect.left,
121403
+ top: rect.bottom + 6
121404
+ };
121405
+ }
120924
121406
  /** Whether the user opted into the composer pill; hidden (not merely inert) when off. */
120925
121407
  function pillVisible(value) {
120926
121408
  return value?.showProviderPill === true;
@@ -120931,6 +121413,7 @@ img.ProseMirror-separator {
120931
121413
  const workflow = activeComfyUIWorkflow(value ?? {});
120932
121414
  return workflow === void 0 ? "" : workflow.name;
120933
121415
  }
121416
+ if (SUBSCRIPTION_PROVIDERS.includes(provider)) return DEFAULT_MODELS[provider];
120934
121417
  const stored = provider === "google" ? value?.googleModel : provider === "openai" ? value?.openaiModel : provider === "openai-compat" ? value?.openaiCompatModel : provider === "seedream" ? value?.seedreamModel : provider === "dashscope" ? value?.dashscopeModel : provider === "xai" ? value?.xaiModel : value?.zhipuModel;
120935
121418
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
120936
121419
  }
@@ -120951,6 +121434,11 @@ img.ProseMirror-separator {
120951
121434
  zhipu: "unknown"
120952
121435
  });
120953
121436
  const [keyTick, setKeyTick] = (0, react.useState)(0);
121437
+ const [subDots, setSubDots] = (0, react.useState)(() => ({
121438
+ "chatgpt-sub": "unknown",
121439
+ "grok-sub": "unknown",
121440
+ "google-sub": "unknown"
121441
+ }));
120954
121442
  const buttonRef = (0, react.useRef)(null);
120955
121443
  const [menuStyle, setMenuStyle] = (0, react.useState)(null);
120956
121444
  (0, react.useEffect)(() => props.scope.subscribe(() => {
@@ -120993,6 +121481,31 @@ img.ProseMirror-separator {
120993
121481
  active = false;
120994
121482
  };
120995
121483
  }, [props.credentials, keyTick]);
121484
+ (0, react.useEffect)(() => {
121485
+ if (!open) return;
121486
+ let active = true;
121487
+ const probe = async () => {
121488
+ try {
121489
+ const response = await fetch(SUBSCRIPTION_STATUS_ROUTE, {
121490
+ method: "POST",
121491
+ cache: "no-store"
121492
+ });
121493
+ if (!active || !response.ok) return;
121494
+ const payload = await response.json();
121495
+ if (!active || payload.statuses === void 0) return;
121496
+ const next = {};
121497
+ for (const provider of SUBSCRIPTION_PROVIDERS) {
121498
+ const row = payload.statuses[provider];
121499
+ next[provider] = row?.state === "logged-in" ? "logged-in" : row?.state === "logged-out" ? "logged-out" : "unknown";
121500
+ }
121501
+ setSubDots(next);
121502
+ } catch {}
121503
+ };
121504
+ probe();
121505
+ return () => {
121506
+ active = false;
121507
+ };
121508
+ }, [open, keyTick]);
120996
121509
  const t = (keyName) => (lang === "en" ? PILL_DICT.en : PILL_DICT.zh)[keyName];
120997
121510
  const current = snapshot.value?.provider ?? "google";
120998
121511
  const writable = snapshot.writable;
@@ -121001,11 +121514,15 @@ img.ProseMirror-separator {
121001
121514
  setMenuStyle(null);
121002
121515
  return;
121003
121516
  }
121004
- const rect = buttonRef.current?.getBoundingClientRect();
121005
- if (rect !== void 0) setMenuStyle({
121006
- left: rect.left,
121007
- bottom: window.innerHeight - rect.top + 6
121008
- });
121517
+ const anchor = () => {
121518
+ const next = anchorOf(buttonRef.current);
121519
+ if (next !== void 0) setMenuStyle(next);
121520
+ };
121521
+ anchor();
121522
+ window.addEventListener("resize", anchor);
121523
+ return () => {
121524
+ window.removeEventListener("resize", anchor);
121525
+ };
121009
121526
  }, [open]);
121010
121527
  (0, react.useEffect)(() => {
121011
121528
  if (!open) return;
@@ -121016,8 +121533,15 @@ img.ProseMirror-separator {
121016
121533
  const onKeyDown = (event) => {
121017
121534
  if (event.key === "Escape") setOpen(false);
121018
121535
  };
121019
- const onReflow = () => {
121020
- setOpen(false);
121536
+ const onReflow = (event) => {
121537
+ if (event.target instanceof Element && event.target.closest(".dsh-ig-pill-menu") !== null) return;
121538
+ const rect = buttonRef.current?.getBoundingClientRect();
121539
+ if (rect !== void 0 && (rect.bottom < 0 || rect.top > window.innerHeight)) {
121540
+ setOpen(false);
121541
+ return;
121542
+ }
121543
+ const next = anchorOf(buttonRef.current);
121544
+ if (next !== void 0) setMenuStyle(next);
121021
121545
  };
121022
121546
  document.addEventListener("mousedown", onPointerDown, true);
121023
121547
  document.addEventListener("keydown", onKeyDown, true);
@@ -121047,8 +121571,12 @@ img.ProseMirror-separator {
121047
121571
  setPending(void 0);
121048
121572
  });
121049
121573
  };
121574
+ /** Plan A dots: ComfyUI is always fixed blue (theme-independent); each
121575
+ * subscription reflects its login state (green in, gray out); cloud rows
121576
+ * reflect their API-key state as before. */
121050
121577
  const dotClassOf = (provider) => {
121051
121578
  if (provider === "comfyui") return "dsh-ig-pill-dot-neutral";
121579
+ if (isSubscriptionProvider(provider)) return subDots[provider] === "logged-in" ? "dsh-ig-pill-dot-ok" : "dsh-ig-pill-dot-missing";
121052
121580
  const dot = dots[provider];
121053
121581
  return dot === "configured" ? "dsh-ig-pill-dot-ok" : dot === "missing" ? "dsh-ig-pill-dot-missing" : "dsh-ig-pill-dot-neutral";
121054
121582
  };
@@ -121057,6 +121585,11 @@ img.ProseMirror-separator {
121057
121585
  const workflow = pillModelOf(provider, snapshot.value);
121058
121586
  return workflow.length > 0 ? `${t("noKeyNeeded")} · ${workflow}` : `${t("noKeyNeeded")} · ${t("noWorkflow")}`;
121059
121587
  }
121588
+ if (isSubscriptionProvider(provider)) {
121589
+ const state = subDots[provider];
121590
+ const stateText = state === "logged-in" ? t("subSignedIn") : state === "logged-out" ? t("subSignedOut") : t("subStatusUnknown");
121591
+ return `${t("subNoKey")} · ${stateText} · ${pillModelOf(provider, snapshot.value)}`;
121592
+ }
121060
121593
  const dot = dots[provider];
121061
121594
  const model = pillModelOf(provider, snapshot.value);
121062
121595
  if (dot === "configured") return `${t("keyConfigured")} · ${model}`;
@@ -121209,8 +121742,8 @@ img.ProseMirror-separator {
121209
121742
  .dsh-ig-pill-option-active{background:var(--dsw-alias-brand-primary-soft,rgba(76,120,255,.10))}
121210
121743
  .dsh-ig-pill-dot{flex:none;width:7px;height:7px;border-radius:50%}
121211
121744
  .dsh-ig-pill-dot-ok{background:#22c55e}
121212
- .dsh-ig-pill-dot-missing{background:var(--dsw-alias-border-l2,#d1d5db)}
121213
- .dsh-ig-pill-dot-neutral{background:var(--dsw-alias-brand-primary,#4c78ff)}
121745
+ .dsh-ig-pill-dot-missing{background:#9ca3af}
121746
+ .dsh-ig-pill-dot-neutral{background:#4c78ff}
121214
121747
  .dsh-ig-pill-option-text{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
121215
121748
  .dsh-ig-pill-option-name{font-size:12.5px;font-weight:500;color:var(--dsw-alias-label-primary,#111827);line-height:1.2}
121216
121749
  .dsh-ig-pill-option-sub{font-size:11px;color:var(--dsw-alias-label-tertiary,#6b7280);line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -121219,7 +121752,7 @@ img.ProseMirror-separator {
121219
121752
  `;
121220
121753
  //#endregion
121221
121754
  //#region lib/types/client/index.js
121222
- console.info(`[dsh-image-gen] canvas bundle 2026-09-12T10:11:10.729Z`);
121755
+ console.info(`[dsh-image-gen] canvas bundle 2026-09-14T00:33:35.649Z`);
121223
121756
  /** Right-sidebar tab-type registration id (DSH 0.1.5 `sidebarRightTabs`). */
121224
121757
  const SIDEBAR_STUDIO_TAB_ID = "dsh-image-gen/studio";
121225
121758
  /** The tab kind: a page type opened by kind, recognizing no resource address. */
@@ -121239,6 +121772,27 @@ img.ProseMirror-separator {
121239
121772
  providerXAI: "xAI Grok Imagine",
121240
121773
  providerZhipu: "智谱 GLM-Image",
121241
121774
  providerComfyUI: "本地 ComfyUI",
121775
+ providerChatGPTSub: "ChatGPT 订阅",
121776
+ providerGrokSub: "Grok 订阅",
121777
+ providerGoogleSub: "Google 订阅",
121778
+ subBadgeLoggedIn: "已登录",
121779
+ subBadgeLoggedOut: "未登录",
121780
+ subBadgeUnknown: "状态未知",
121781
+ subSectionTitle: "订阅生图",
121782
+ subHint: "通过已登录的订阅账号生图,无需 API Key。登录在你的授权下进行,不会修改任何 API Key 或默认 Provider。",
121783
+ subAccountLabel: "账号",
121784
+ subAccountEmail: "已登录:{email}",
121785
+ subAccountNone: "未登录",
121786
+ subLogin: "登录",
121787
+ subLoggingIn: "正在登录…",
121788
+ subLogout: "退出登录",
121789
+ subLoggingOut: "正在退出…",
121790
+ subWaitingLogin: "已打开授权页面,请在浏览器中完成登录…",
121791
+ subLoginOk: "登录成功",
121792
+ subModelLabel: "订阅模型",
121793
+ subModelHint: "由订阅通道固定,不可更改。",
121794
+ subEditUnsupported: "订阅通道暂不支持图生图(编辑图片),仅支持文字生图。",
121795
+ subDefaultHint: "订阅 Provider 只能生成,不能编辑图片;需要编辑时请切换到 API Key Provider。",
121242
121796
  apiKeyLabel: "{provider} API Key",
121243
121797
  apiKeyPlaceholder: "留空即可保留已配置的 Key",
121244
121798
  apiKeyHint: "安全保存为 {key};页面不会读回明文。",
@@ -121266,6 +121820,14 @@ img.ProseMirror-separator {
121266
121820
  endpointHintGoogle: "Google 官方地址或反代端点(全路径)。",
121267
121821
  endpointHintOpenAI: "官方 api.openai.com 的 /v1 地址;中转站请使用下方「OpenAI 兼容」行。",
121268
121822
  endpointHintOpenAICompat: "中转站/自建服务的 OpenAI 兼容 /v1 地址(必填),例如 https://your-relay.example.com/v1。",
121823
+ editFormat: "图生图请求形态",
121824
+ editFormatMultipart: "标准 multipart(OpenAI 官方)",
121825
+ editFormatJsonImageUrlArray: "JSON images 数组(商汤等中转)",
121826
+ editFormatHint: "文生图不受影响。仅当中转站的图生图接口使用自有 JSON 契约(如商汤 SenseNova)时才需要切换。",
121827
+ editExtra: "附加 JSON 字段",
121828
+ editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
121829
+ editExtraHint: "仅 JSON 形态生效;会合并到请求体末尾,可覆盖默认字段。留空表示不附加。",
121830
+ editExtraInvalid: "附加 JSON 字段必须是合法的 JSON 对象。",
121269
121831
  endpointHintSeedream: "火山方舟兼容的 /api/v3 地址。",
121270
121832
  endpointHintDashScope: "阿里云百炼 DashScope 官方接口地址。",
121271
121833
  endpointHintXAI: "xAI 官方 api.x.ai 的 /v1 地址。",
@@ -121339,6 +121901,27 @@ img.ProseMirror-separator {
121339
121901
  providerXAI: "xAI Grok Imagine",
121340
121902
  providerZhipu: "Zhipu GLM-Image",
121341
121903
  providerComfyUI: "Local ComfyUI",
121904
+ providerChatGPTSub: "ChatGPT Subscription",
121905
+ providerGrokSub: "Grok Subscription",
121906
+ providerGoogleSub: "Google Subscription",
121907
+ subBadgeLoggedIn: "Signed in",
121908
+ subBadgeLoggedOut: "Signed out",
121909
+ subBadgeUnknown: "Unknown",
121910
+ subSectionTitle: "Subscription generation",
121911
+ subHint: "Generates through a logged-in subscription account; no API key needed. Signing in never changes any API key or the default provider.",
121912
+ subAccountLabel: "Account",
121913
+ subAccountEmail: "Signed in: {email}",
121914
+ subAccountNone: "Not signed in",
121915
+ subLogin: "Sign in",
121916
+ subLoggingIn: "Signing in…",
121917
+ subLogout: "Sign out",
121918
+ subLoggingOut: "Signing out…",
121919
+ subWaitingLogin: "Authorization page opened; complete sign-in in your browser…",
121920
+ subLoginOk: "Signed in",
121921
+ subModelLabel: "Subscription model",
121922
+ subModelHint: "Fixed by the subscription channel; not changeable.",
121923
+ subEditUnsupported: "The subscription channel does not support image editing yet; text-to-image only.",
121924
+ subDefaultHint: "Subscription providers can only generate; switch to an API-key provider for editing.",
121342
121925
  apiKeyLabel: "{provider} API Key",
121343
121926
  apiKeyPlaceholder: "Leave empty to keep configured key",
121344
121927
  apiKeyHint: "Securely saved as {key}; never read back in plaintext.",
@@ -121366,6 +121949,14 @@ img.ProseMirror-separator {
121366
121949
  endpointHintGoogle: "Official Google endpoint or reverse proxy (full path).",
121367
121950
  endpointHintOpenAI: "Official api.openai.com /v1 base URL; for relays use the \"OpenAI-compatible\" row below.",
121368
121951
  endpointHintOpenAICompat: "OpenAI-compatible /v1 base URL of your relay or self-hosted service (required), e.g. https://your-relay.example.com/v1.",
121952
+ editFormat: "Edit request format",
121953
+ editFormatMultipart: "Standard multipart (official OpenAI)",
121954
+ editFormatJsonImageUrlArray: "JSON images array (SenseNova etc.)",
121955
+ editFormatHint: "Text-to-image is unaffected. Only switch when the relay runs image edits on its own JSON contract (e.g. SenseNova); size is pinned to \"auto\" in this format (the only value SenseNova's edits endpoint accepts) and can be overridden via extra fields.",
121956
+ editExtra: "Extra JSON fields",
121957
+ editExtraPlaceholder: "{\"watermark\": false, \"prompt_extend\": true}",
121958
+ editExtraHint: "Only used with the JSON format; merged into the request body last and may override defaults. Leave empty for none.",
121959
+ editExtraInvalid: "Extra JSON fields must be a valid JSON object.",
121369
121960
  endpointHintSeedream: "Volcengine Ark compatible /api/v3 base URL.",
121370
121961
  endpointHintDashScope: "Official Aliyun DashScope endpoint.",
121371
121962
  endpointHintXAI: "Official xAI api.x.ai /v1 base URL.",
@@ -121442,6 +122033,7 @@ img.ProseMirror-separator {
121442
122033
  .dsh-ig-label{font-size:13px;font-weight:500;color:var(--dsw-alias-label-primary,inherit)}
121443
122034
  .dsh-ig-input{box-sizing:border-box;width:100%;padding:8px 12px;font-size:13px;border:1px solid var(--dsw-alias-border-l2,#d7dbe0);border-radius:8px;background:var(--dsw-alias-bg-layer-3,transparent);color:inherit;outline:none;transition:border-color .15s}
121444
122035
  .dsh-ig-input:focus{border-color:var(--dsw-alias-brand-primary,#4c78ff)}
122036
+ .dsh-ig-textarea{resize:vertical;min-height:56px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;line-height:1.5}
121445
122037
  .dsh-ig-input-group{display:flex;gap:8px;align-items:center}
121446
122038
  .dsh-ig-file-row{display:flex;align-items:center;gap:10px;min-width:0}
121447
122039
  .dsh-ig-file-input{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;clip-path:inset(50%)}
@@ -121730,7 +122322,7 @@ img.ProseMirror-separator {
121730
122322
  ctx.effect(() => {
121731
122323
  const style = document.createElement("style");
121732
122324
  style.dataset.plugin = "dsh-image-gen";
121733
- style.textContent = `${STYLE}\n${STUDIO_STYLE}\n${INSPIRATION_STYLE}\n${PROVIDER_PILL_STYLE}\n${TL_CSS}`;
122325
+ style.textContent = `${STYLE}\n${STUDIO_STYLE}\n${INSPIRATION_STYLE}\n${PROVIDER_PILL_STYLE}\n${TL_CSS}\n${TL_THEME_CSS}`;
121734
122326
  document.head.appendChild(style);
121735
122327
  return () => {
121736
122328
  style.remove();
@@ -121937,7 +122529,9 @@ img.ProseMirror-separator {
121937
122529
  messageIsError: false,
121938
122530
  workflows: [],
121939
122531
  activeWorkflow: "",
121940
- timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3
122532
+ timeoutSeconds: DEFAULT_COMFYUI_TIMEOUT_MS / 1e3,
122533
+ editFormat: "multipart",
122534
+ editExtraText: ""
121941
122535
  };
121942
122536
  }
121943
122537
  /** Settings field each cloud provider persists its model under. */
@@ -121982,6 +122576,15 @@ img.ProseMirror-separator {
121982
122576
  function modelPullSupported(_provider) {
121983
122577
  return true;
121984
122578
  }
122579
+ /** Serialize the extra-fields record into editor text; empty record becomes ''. */
122580
+ function editExtraTextOf(value) {
122581
+ if (value === void 0 || Object.keys(value).length === 0) return "";
122582
+ try {
122583
+ return JSON.stringify(value);
122584
+ } catch {
122585
+ return "";
122586
+ }
122587
+ }
121985
122588
  /** Build one row per provider from persisted settings, including ComfyUI extras. */
121986
122589
  function rowsFromSettings(value) {
121987
122590
  const rows = {};
@@ -121993,6 +122596,10 @@ img.ProseMirror-separator {
121993
122596
  workflows: resolveComfyUIWorkflows(value ?? {}),
121994
122597
  activeWorkflow: activeComfyUIWorkflow(value ?? {})?.name ?? "",
121995
122598
  timeoutSeconds: Math.max(1, Math.round((value?.comfyuiTimeoutMs ?? 3e5) / 1e3))
122599
+ } : {},
122600
+ ...provider === "openai-compat" ? {
122601
+ editFormat: value?.openaiCompatEditFormat === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart",
122602
+ editExtraText: editExtraTextOf(value?.openaiCompatEditExtra)
121996
122603
  } : {}
121997
122604
  };
121998
122605
  return rows;
@@ -122014,6 +122621,14 @@ img.ProseMirror-separator {
122014
122621
  const [workspaceMessageIsError, setWorkspaceMessageIsError] = (0, react.useState)(false);
122015
122622
  const [rows, setRows] = (0, react.useState)(() => rowsFromSettings(props.scope.getSnapshot().value));
122016
122623
  const [keyTick, setKeyTick] = (0, react.useState)(0);
122624
+ const [subStatus, setSubStatus] = (0, react.useState)(() => ({
122625
+ "chatgpt-sub": { state: "unknown" },
122626
+ "grok-sub": { state: "unknown" },
122627
+ "google-sub": { state: "unknown" }
122628
+ }));
122629
+ const [subTick, setSubTick] = (0, react.useState)(0);
122630
+ const [subBusy, setSubBusy] = (0, react.useState)({});
122631
+ const [subPending, setSubPending] = (0, react.useState)([]);
122017
122632
  (0, react.useEffect)(() => props.scope.subscribe(() => {
122018
122633
  setSnapshot(props.scope.getSnapshot());
122019
122634
  }), [props.scope]);
@@ -122026,6 +122641,143 @@ img.ProseMirror-separator {
122026
122641
  (0, react.useEffect)(() => credentialEvents?.listen(() => {
122027
122642
  setKeyTick((tick) => tick + 1);
122028
122643
  }), [credentialEvents]);
122644
+ const pending = subPending;
122645
+ (0, react.useEffect)(() => {
122646
+ if (!open) return void 0;
122647
+ let active = true;
122648
+ const probe = async () => {
122649
+ try {
122650
+ const response = await fetch(SUBSCRIPTION_STATUS_ROUTE, {
122651
+ method: "POST",
122652
+ cache: "no-store"
122653
+ });
122654
+ if (!response.ok) return;
122655
+ const payload = await response.json();
122656
+ if (!active || payload.statuses === void 0) return;
122657
+ const next = {
122658
+ "chatgpt-sub": { state: "unknown" },
122659
+ "grok-sub": { state: "unknown" },
122660
+ "google-sub": { state: "unknown" }
122661
+ };
122662
+ for (const provider of SUBSCRIPTION_PROVIDERS) {
122663
+ const row = payload.statuses[provider];
122664
+ if (row?.state === "logged-in") next[provider] = {
122665
+ state: "logged-in",
122666
+ ...typeof row.email === "string" ? { email: row.email } : {}
122667
+ };
122668
+ else if (row?.state === "logged-out") next[provider] = { state: "logged-out" };
122669
+ }
122670
+ setSubStatus(next);
122671
+ const landed = pending.filter((entry) => next[entry.provider].state === "logged-in");
122672
+ if (landed.length > 0) {
122673
+ setSubPending((current) => current.filter((entry) => next[entry.provider].state !== "logged-in"));
122674
+ for (const entry of landed) updateRow(entry.provider, {
122675
+ message: t("subLoginOk"),
122676
+ messageIsError: false
122677
+ });
122678
+ }
122679
+ const WINDOW_MS = 63e4;
122680
+ if (pending.filter((entry) => Date.now() - entry.startedAt > WINDOW_MS).length > 0) setSubPending((current) => current.filter((entry) => Date.now() - entry.startedAt <= WINDOW_MS));
122681
+ } catch {}
122682
+ };
122683
+ probe();
122684
+ let timer;
122685
+ if (pending.length > 0) timer = setInterval(() => {
122686
+ probe();
122687
+ }, 2e3);
122688
+ const onFocus = () => {
122689
+ probe();
122690
+ };
122691
+ window.addEventListener("focus", onFocus);
122692
+ return () => {
122693
+ active = false;
122694
+ if (timer !== void 0) clearInterval(timer);
122695
+ window.removeEventListener("focus", onFocus);
122696
+ };
122697
+ }, [
122698
+ open,
122699
+ subTick,
122700
+ pending
122701
+ ]);
122702
+ /** Start a subscription login: the host registers PKCE and the loopback
122703
+ * catch server, then the browser opens the vendor's authorize URL. The
122704
+ * provider joins the pending set so the badge polls until login lands. */
122705
+ const subscriptionLogin = async (provider) => {
122706
+ updateRow(provider, {
122707
+ message: "",
122708
+ messageIsError: false
122709
+ });
122710
+ setSubBusy((current) => ({
122711
+ ...current,
122712
+ [provider]: "login"
122713
+ }));
122714
+ try {
122715
+ const response = await fetch(SUBSCRIPTION_LOGIN_ROUTE, {
122716
+ method: "POST",
122717
+ headers: { "content-type": "application/json" },
122718
+ body: JSON.stringify({ provider })
122719
+ });
122720
+ const payload = await response.json().catch(() => ({}));
122721
+ if (!response.ok || payload.ok !== true || typeof payload.url !== "string") {
122722
+ const detail = typeof payload.error === "string" ? payload.error : payload.error?.message;
122723
+ throw new Error(detail ?? `HTTP ${String(response.status)}`);
122724
+ }
122725
+ setSubPending((current) => [...current.filter((entry) => entry.provider !== provider), {
122726
+ provider,
122727
+ startedAt: Date.now()
122728
+ }]);
122729
+ window.open(payload.url, "_blank", "noopener");
122730
+ updateRow(provider, {
122731
+ message: t("subWaitingLogin"),
122732
+ messageIsError: false
122733
+ });
122734
+ } catch (cause) {
122735
+ updateRow(provider, {
122736
+ message: cause instanceof Error ? cause.message : String(cause),
122737
+ messageIsError: true
122738
+ });
122739
+ } finally {
122740
+ setSubBusy((current) => {
122741
+ const next = { ...current };
122742
+ delete next[provider];
122743
+ return next;
122744
+ });
122745
+ }
122746
+ };
122747
+ /** Sign out one subscription account; nothing else changes. */
122748
+ const subscriptionLogout = async (provider) => {
122749
+ setSubBusy((current) => ({
122750
+ ...current,
122751
+ [provider]: "logout"
122752
+ }));
122753
+ try {
122754
+ const response = await fetch(SUBSCRIPTION_LOGIN_ROUTE, {
122755
+ method: "POST",
122756
+ headers: { "content-type": "application/json" },
122757
+ body: JSON.stringify({
122758
+ provider,
122759
+ action: "logout"
122760
+ })
122761
+ });
122762
+ if (!response.ok) throw new Error(`HTTP ${String(response.status)}`);
122763
+ updateRow(provider, {
122764
+ message: "",
122765
+ messageIsError: false
122766
+ });
122767
+ } catch (cause) {
122768
+ updateRow(provider, {
122769
+ message: cause instanceof Error ? cause.message : String(cause),
122770
+ messageIsError: true
122771
+ });
122772
+ } finally {
122773
+ setSubBusy((current) => {
122774
+ const next = { ...current };
122775
+ delete next[provider];
122776
+ return next;
122777
+ });
122778
+ setSubTick((tick) => tick + 1);
122779
+ }
122780
+ };
122029
122781
  const t = (keyName, params) => {
122030
122782
  let text = (lang === "en" ? DICT.en : DICT.zh)[keyName] || DICT.zh[keyName] || keyName;
122031
122783
  if (params) for (const [k, v] of Object.entries(params)) text = text.replace(`{${k}}`, v);
@@ -122039,7 +122791,10 @@ img.ProseMirror-separator {
122039
122791
  dashscope: t("providerDashScope"),
122040
122792
  xai: t("providerXAI"),
122041
122793
  zhipu: t("providerZhipu"),
122042
- comfyui: t("providerComfyUI")
122794
+ comfyui: t("providerComfyUI"),
122795
+ "chatgpt-sub": t("providerChatGPTSub"),
122796
+ "grok-sub": t("providerGrokSub"),
122797
+ "google-sub": t("providerGoogleSub")
122043
122798
  };
122044
122799
  (0, react.useEffect)(() => {
122045
122800
  const value = snapshot.value;
@@ -122166,9 +122921,28 @@ img.ProseMirror-separator {
122166
122921
  await props.scope.set("comfyuiWorkflowJson", activeEntry === void 0 ? "" : activeEntry.json);
122167
122922
  await props.scope.set("comfyuiWorkflowName", activeEntry === void 0 ? "" : activeEntry.name);
122168
122923
  await props.scope.set("comfyuiTimeoutMs", Math.max(1, Math.round(row.timeoutSeconds)) * 1e3);
122169
- } else {
122924
+ } else if (isSubscriptionProvider(provider)) await Promise.resolve();
122925
+ else {
122926
+ let editExtra = {};
122927
+ if (provider === "openai-compat") {
122928
+ const extraText = row.editExtraText.trim();
122929
+ if (extraText.length > 0) {
122930
+ let parsed;
122931
+ try {
122932
+ parsed = JSON.parse(extraText);
122933
+ } catch {
122934
+ throw new Error(t("editExtraInvalid"));
122935
+ }
122936
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) throw new Error(t("editExtraInvalid"));
122937
+ editExtra = parsed;
122938
+ }
122939
+ }
122170
122940
  await props.scope.set(modelFieldOf(provider), row.model);
122171
122941
  await props.scope.set(baseURLFieldOf(provider), row.baseURL);
122942
+ if (provider === "openai-compat") {
122943
+ await props.scope.set("openaiCompatEditFormat", row.editFormat);
122944
+ await props.scope.set("openaiCompatEditExtra", editExtra);
122945
+ }
122172
122946
  if (row.keyInput.trim().length > 0) {
122173
122947
  const keyRef = cloudCredentialRef(provider);
122174
122948
  if (keyRef === void 0) throw new Error(t("comfyuiNoKey"));
@@ -122312,6 +123086,21 @@ img.ProseMirror-separator {
122312
123086
  text: t("comfyuiNoKey"),
122313
123087
  className: "dsh-ig-badge dsh-ig-badge-neutral"
122314
123088
  };
123089
+ if (isSubscriptionProvider(provider)) {
123090
+ const status = subStatus[provider].state;
123091
+ if (status === "logged-in") return {
123092
+ text: t("subBadgeLoggedIn"),
123093
+ className: "dsh-ig-badge dsh-ig-badge-ok"
123094
+ };
123095
+ if (status === "logged-out") return {
123096
+ text: t("subBadgeLoggedOut"),
123097
+ className: "dsh-ig-badge dsh-ig-badge-missing"
123098
+ };
123099
+ return {
123100
+ text: t("subBadgeUnknown"),
123101
+ className: "dsh-ig-badge dsh-ig-badge-neutral"
123102
+ };
123103
+ }
122315
123104
  const status = rows[provider].keyStatus;
122316
123105
  if (status === "checking") return {
122317
123106
  text: t("badgeChecking"),
@@ -122581,6 +123370,57 @@ img.ProseMirror-separator {
122581
123370
  }) : null
122582
123371
  ]
122583
123372
  }),
123373
+ provider === "openai-compat" ? (0, react_jsx_runtime.jsxs)("label", {
123374
+ className: "dsh-ig-field",
123375
+ children: [
123376
+ (0, react_jsx_runtime.jsx)("span", {
123377
+ className: "dsh-ig-label",
123378
+ children: t("editFormat")
123379
+ }),
123380
+ (0, react_jsx_runtime.jsxs)("select", {
123381
+ className: "dsh-ig-input",
123382
+ value: row.editFormat,
123383
+ onChange: (event) => {
123384
+ updateRow(provider, { editFormat: event.target.value === "jsonImageUrlArray" ? "jsonImageUrlArray" : "multipart" });
123385
+ },
123386
+ disabled: !snapshot.writable,
123387
+ children: [(0, react_jsx_runtime.jsx)("option", {
123388
+ value: "multipart",
123389
+ children: t("editFormatMultipart")
123390
+ }), (0, react_jsx_runtime.jsx)("option", {
123391
+ value: "jsonImageUrlArray",
123392
+ children: t("editFormatJsonImageUrlArray")
123393
+ })]
123394
+ }),
123395
+ (0, react_jsx_runtime.jsx)("span", {
123396
+ className: "dsh-ig-hint",
123397
+ children: t("editFormatHint")
123398
+ })
123399
+ ]
123400
+ }) : null,
123401
+ provider === "openai-compat" && row.editFormat === "jsonImageUrlArray" ? (0, react_jsx_runtime.jsxs)("label", {
123402
+ className: "dsh-ig-field",
123403
+ children: [
123404
+ (0, react_jsx_runtime.jsx)("span", {
123405
+ className: "dsh-ig-label",
123406
+ children: t("editExtra")
123407
+ }),
123408
+ (0, react_jsx_runtime.jsx)("textarea", {
123409
+ className: "dsh-ig-input dsh-ig-textarea",
123410
+ value: row.editExtraText,
123411
+ onChange: (event) => {
123412
+ updateRow(provider, { editExtraText: event.target.value });
123413
+ },
123414
+ placeholder: t("editExtraPlaceholder"),
123415
+ rows: 2,
123416
+ disabled: !snapshot.writable
123417
+ }),
123418
+ (0, react_jsx_runtime.jsx)("span", {
123419
+ className: "dsh-ig-hint",
123420
+ children: t("editExtraHint")
123421
+ })
123422
+ ]
123423
+ }) : null,
122584
123424
  (0, react_jsx_runtime.jsxs)("div", {
122585
123425
  className: "dsh-ig-row-actions",
122586
123426
  children: [(0, react_jsx_runtime.jsx)("p", {
@@ -122798,6 +123638,119 @@ img.ProseMirror-separator {
122798
123638
  })
122799
123639
  });
122800
123640
  };
123641
+ /** Subscription rows: account login/logout, fixed model, edit limitation. */
123642
+ const renderSubscriptionBody = (provider) => {
123643
+ const row = rows[provider];
123644
+ const status = subStatus[provider];
123645
+ const busy = subBusy[provider];
123646
+ return (0, react_jsx_runtime.jsx)("div", {
123647
+ className: "dsh-ig-provider-body",
123648
+ children: (0, react_jsx_runtime.jsxs)("form", {
123649
+ onSubmit: (event) => {
123650
+ event.preventDefault();
123651
+ saveProviderRow(provider);
123652
+ },
123653
+ children: [
123654
+ (0, react_jsx_runtime.jsxs)("div", {
123655
+ className: "dsh-ig-field",
123656
+ children: [(0, react_jsx_runtime.jsx)("span", {
123657
+ className: "dsh-ig-label",
123658
+ children: t("subSectionTitle")
123659
+ }), (0, react_jsx_runtime.jsx)("p", {
123660
+ className: "dsh-ig-hint",
123661
+ children: t("subHint")
123662
+ })]
123663
+ }),
123664
+ (0, react_jsx_runtime.jsxs)("div", {
123665
+ className: "dsh-ig-field",
123666
+ children: [
123667
+ (0, react_jsx_runtime.jsx)("span", {
123668
+ className: "dsh-ig-label",
123669
+ children: t("subAccountLabel")
123670
+ }),
123671
+ (0, react_jsx_runtime.jsx)("p", {
123672
+ className: "dsh-ig-hint",
123673
+ children: status.state === "logged-in" ? t("subAccountEmail", { email: status.email ?? "" }) : status.state === "logged-out" ? t("subAccountNone") : t("subBadgeUnknown")
123674
+ }),
123675
+ (0, react_jsx_runtime.jsx)("span", {
123676
+ className: "dsh-ig-row-buttons",
123677
+ children: status.state === "logged-in" ? (0, react_jsx_runtime.jsx)("button", {
123678
+ type: "button",
123679
+ className: "dsh-ig-btn-secondary",
123680
+ disabled: busy !== void 0,
123681
+ onClick: () => {
123682
+ subscriptionLogout(provider);
123683
+ },
123684
+ children: busy === "logout" ? t("subLoggingOut") : t("subLogout")
123685
+ }) : (0, react_jsx_runtime.jsx)("button", {
123686
+ type: "button",
123687
+ className: "dsh-ig-save",
123688
+ disabled: busy !== void 0,
123689
+ onClick: () => {
123690
+ subscriptionLogin(provider);
123691
+ setSubTick((tick) => tick + 1);
123692
+ },
123693
+ children: busy === "login" ? t("subLoggingIn") : t("subLogin")
123694
+ })
123695
+ })
123696
+ ]
123697
+ }),
123698
+ (0, react_jsx_runtime.jsxs)("label", {
123699
+ className: "dsh-ig-field",
123700
+ children: [
123701
+ (0, react_jsx_runtime.jsx)("span", {
123702
+ className: "dsh-ig-label",
123703
+ children: t("subModelLabel")
123704
+ }),
123705
+ (0, react_jsx_runtime.jsx)("input", {
123706
+ className: "dsh-ig-input",
123707
+ type: "text",
123708
+ value: row.model,
123709
+ readOnly: true,
123710
+ disabled: true
123711
+ }),
123712
+ (0, react_jsx_runtime.jsx)("span", {
123713
+ className: "dsh-ig-hint",
123714
+ children: t("subModelHint")
123715
+ })
123716
+ ]
123717
+ }),
123718
+ (0, react_jsx_runtime.jsx)("div", {
123719
+ className: "dsh-ig-field",
123720
+ children: (0, react_jsx_runtime.jsx)("span", {
123721
+ className: "dsh-ig-label",
123722
+ children: t("subEditUnsupported")
123723
+ })
123724
+ }),
123725
+ (0, react_jsx_runtime.jsxs)("div", {
123726
+ className: "dsh-ig-row-actions",
123727
+ children: [(0, react_jsx_runtime.jsx)("p", {
123728
+ className: `dsh-ig-status${row.messageIsError ? " dsh-ig-status-error" : ""}`,
123729
+ role: "status",
123730
+ children: row.message || testResultText(row.testResult)
123731
+ }), (0, react_jsx_runtime.jsxs)("span", {
123732
+ className: "dsh-ig-row-buttons",
123733
+ children: [(0, react_jsx_runtime.jsx)("button", {
123734
+ type: "button",
123735
+ className: "dsh-ig-btn-secondary",
123736
+ disabled: row.testing,
123737
+ onClick: () => {
123738
+ testConnection(provider);
123739
+ setSubTick((tick) => tick + 1);
123740
+ },
123741
+ children: row.testing ? t("testing") : t("testConnection")
123742
+ }), (0, react_jsx_runtime.jsx)("button", {
123743
+ className: "dsh-ig-save",
123744
+ type: "submit",
123745
+ disabled: row.saving || !snapshot.writable,
123746
+ children: row.saving ? t("saving") : t("save")
123747
+ })]
123748
+ })]
123749
+ })
123750
+ ]
123751
+ })
123752
+ });
123753
+ };
122801
123754
  const renderProviderRow = (provider) => {
122802
123755
  const row = rows[provider];
122803
123756
  const badge = badgeOf(provider);
@@ -122839,7 +123792,7 @@ img.ProseMirror-separator {
122839
123792
  })
122840
123793
  })
122841
123794
  ]
122842
- }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : renderCloudBody(provider) : null]
123795
+ }), row.expanded ? provider === "comfyui" ? renderComfyUIBody() : isSubscriptionProvider(provider) ? renderSubscriptionBody(provider) : renderCloudBody(provider) : null]
122843
123796
  }, provider);
122844
123797
  };
122845
123798
  return (0, react_jsx_runtime.jsxs)("li", {
@@ -123578,11 +124531,11 @@ img.ProseMirror-separator {
123578
124531
  };
123579
124532
  }
123580
124533
  function modelOf(provider, value) {
123581
- const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : value?.[CLOUD_MODEL_FIELDS[provider]];
124534
+ const stored = provider === "comfyui" ? activeComfyUIWorkflow(value ?? {})?.name : isSubscriptionProvider(provider) ? DEFAULT_MODELS[provider] : value?.[CLOUD_MODEL_FIELDS[provider]];
123582
124535
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_MODELS[provider];
123583
124536
  }
123584
124537
  function baseURLOf(provider, value) {
123585
- const stored = provider === "comfyui" ? value?.comfyuiBaseURL : value?.[CLOUD_URL_FIELDS[provider]];
124538
+ const stored = provider === "comfyui" ? value?.comfyuiBaseURL : isSubscriptionProvider(provider) ? "" : value?.[CLOUD_URL_FIELDS[provider]];
123586
124539
  return typeof stored === "string" && stored.length > 0 ? stored : DEFAULT_BASE_URLS[provider];
123587
124540
  }
123588
124541
  //#endregion