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.
- package/README.en.md +12 -2
- package/README.md +13 -3
- package/lib/client.js +1086 -133
- package/lib/client.js.map +1 -1
- package/lib/index.js +1425 -103
- package/lib/types/client/image-result-node.d.ts +11 -1
- package/lib/types/client/index.d.ts +5 -0
- package/lib/types/client/provider-pill.d.ts +1 -1
- package/lib/types/client/studio-style.d.ts +1 -1
- package/lib/types/client/studio-view.d.ts +6 -0
- package/lib/types/client/tl/css-supports-shim.d.ts +21 -0
- package/lib/types/client/tl/diagram-landing.d.ts +75 -0
- package/lib/types/client/tl/diagram-layout.d.ts +76 -0
- package/lib/types/client/tl/diagram-measure.d.ts +40 -0
- package/lib/types/client/tl/tl-brand-theme.d.ts +3 -0
- package/lib/types/client/tl/tl-brand-tokens.d.ts +24 -0
- package/lib/types/client/tl/tl-theme-css.d.ts +17 -0
- package/lib/types/config.d.ts +24 -0
- package/lib/types/diagram.d.ts +42 -0
- package/lib/types/openai-compatible.d.ts +15 -0
- package/lib/types/shared.d.ts +16 -1
- package/lib/types/subscription/blob.d.ts +23 -0
- package/lib/types/subscription/loopback.d.ts +11 -0
- package/lib/types/subscription/manager.d.ts +94 -0
- package/lib/types/subscription/oauth.d.ts +25 -0
- package/lib/types/subscription/subscription-route.d.ts +5 -0
- package/lib/types/subscription/vendors/antigravity.d.ts +66 -0
- package/lib/types/subscription/vendors/codex.d.ts +30 -0
- package/lib/types/subscription/vendors/grok.d.ts +32 -0
- package/lib/types/subscription.d.ts +42 -0
- package/lib/types/test-route.d.ts +9 -0
- 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, {
|
|
115945
|
-
|
|
115946
|
-
|
|
115947
|
-
|
|
115948
|
-
|
|
115949
|
-
|
|
115950
|
-
|
|
115951
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: [
|
|
116924
|
-
|
|
116925
|
-
|
|
116926
|
-
|
|
116927
|
-
|
|
116928
|
-
|
|
116929
|
-
|
|
116930
|
-
|
|
116931
|
-
|
|
116932
|
-
|
|
116933
|
-
|
|
116934
|
-
|
|
116935
|
-
|
|
116936
|
-
|
|
116937
|
-
|
|
116938
|
-
|
|
116939
|
-
|
|
116940
|
-
|
|
116941
|
-
|
|
116942
|
-
|
|
116943
|
-
|
|
116944
|
-
|
|
116945
|
-
|
|
116946
|
-
|
|
116947
|
-
|
|
116948
|
-
|
|
116949
|
-
|
|
116950
|
-
|
|
116951
|
-
|
|
116952
|
-
|
|
116953
|
-
|
|
116954
|
-
|
|
116955
|
-
|
|
116956
|
-
|
|
116957
|
-
|
|
116958
|
-
|
|
116959
|
-
|
|
116960
|
-
|
|
116961
|
-
|
|
116962
|
-
|
|
116963
|
-
|
|
116964
|
-
|
|
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
|
-
|
|
116969
|
-
|
|
116970
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
117026
|
-
|
|
117027
|
-
|
|
117028
|
-
|
|
117029
|
-
|
|
117030
|
-
|
|
117031
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
})
|
|
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{
|
|
120515
|
-
.dsh-ig-load-more{width:100%;height:32px;margin:
|
|
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:
|
|
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
|
-
|
|
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
|
|
121005
|
-
|
|
121006
|
-
|
|
121007
|
-
|
|
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
|
-
|
|
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
|
|
121213
|
-
.dsh-ig-pill-dot-neutral{background
|
|
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-
|
|
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
|