customforge 0.1.0-alpha.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/CHANGELOG.md +79 -24
  2. package/LICENSES/NunitoSans-OFL.txt +93 -0
  3. package/LICENSES/plain-mug-CC-BY-4.0.txt +11 -0
  4. package/README.md +592 -290
  5. package/README.zh-CN.md +589 -290
  6. package/dist/NunitoSans-Variable.ttf +0 -0
  7. package/dist/ProductCustomizer-4ytA68eg.js +2350 -0
  8. package/dist/ProductCustomizer-4ytA68eg.js.map +1 -0
  9. package/dist/bridge/TextureBridge.d.ts +1 -1
  10. package/dist/core/api.d.ts +94 -0
  11. package/dist/core/api.d.ts.map +1 -0
  12. package/dist/core/config.d.ts +3 -12
  13. package/dist/core/config.d.ts.map +1 -1
  14. package/dist/core/design.d.ts.map +1 -1
  15. package/dist/core/types.d.ts +217 -8
  16. package/dist/core/types.d.ts.map +1 -1
  17. package/dist/core/uv.d.ts +8 -0
  18. package/dist/core/uv.d.ts.map +1 -0
  19. package/dist/cup_decal_small_margins.glb +0 -0
  20. package/dist/customizer/ProductCustomizer.d.ts +174 -7
  21. package/dist/customizer/ProductCustomizer.d.ts.map +1 -1
  22. package/dist/editor/DesignEditor.d.ts +224 -14
  23. package/dist/editor/DesignEditor.d.ts.map +1 -1
  24. package/dist/editor/DesignHistory.d.ts +27 -0
  25. package/dist/editor/DesignHistory.d.ts.map +1 -0
  26. package/dist/editor/uvBounds.d.ts +15 -0
  27. package/dist/editor/uvBounds.d.ts.map +1 -0
  28. package/dist/index.d.ts +4 -2
  29. package/dist/index.d.ts.map +1 -1
  30. package/dist/index.js +1 -1108
  31. package/dist/index.js.map +1 -1
  32. package/dist/style.css +2123 -11
  33. package/dist/viewer/ProductViewer.d.ts +37 -6
  34. package/dist/viewer/ProductViewer.d.ts.map +1 -1
  35. package/dist/viewer/uvLayout.d.ts +11 -0
  36. package/dist/viewer/uvLayout.d.ts.map +1 -0
  37. package/dist/workbench/CustomForgeWorkbench.d.ts +205 -0
  38. package/dist/workbench/CustomForgeWorkbench.d.ts.map +1 -0
  39. package/dist/workbench/assets/catalog.d.ts +8 -0
  40. package/dist/workbench/assets/catalog.d.ts.map +1 -0
  41. package/dist/workbench/config.d.ts +96 -0
  42. package/dist/workbench/config.d.ts.map +1 -0
  43. package/dist/workbench/icons.d.ts +4 -0
  44. package/dist/workbench/icons.d.ts.map +1 -0
  45. package/dist/workbench/index.d.ts +25 -0
  46. package/dist/workbench/index.d.ts.map +1 -0
  47. package/dist/workbench/template.d.ts +3 -0
  48. package/dist/workbench/template.d.ts.map +1 -0
  49. package/dist/workbench/types.d.ts +542 -0
  50. package/dist/workbench/types.d.ts.map +1 -0
  51. package/dist/workbench.js +3052 -0
  52. package/dist/workbench.js.map +1 -0
  53. package/package.json +77 -71
@@ -0,0 +1,3052 @@
1
+ import { n as resolveElement, t as ProductCustomizer } from "./ProductCustomizer-4ytA68eg.js";
2
+ //#endregion
3
+ //#region src/workbench/assets/catalog.ts
4
+ /**
5
+ * Workbench 随包提供的图片素材目录
6
+ *
7
+ * 新素材文件应分别放入 backgrounds 或 elements 目录,并在此处使用 import 后注册
8
+ */
9
+ var builtInAssetLibrary = {
10
+ backgrounds: [],
11
+ elements: [{
12
+ id: "customforge-logo",
13
+ name: "CustomForge logo",
14
+ url: "data:image/png;base64,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",
15
+ alt: "CustomForge logo"
16
+ }]
17
+ };
18
+ //#endregion
19
+ //#region src/workbench/config.ts
20
+ var defaultFeatures = {
21
+ addText: true,
22
+ textFormatting: true,
23
+ addImage: true,
24
+ undoRedo: true,
25
+ deleteSelection: true,
26
+ saveDesign: true,
27
+ loadDesign: true,
28
+ loadRemoteProduct: true,
29
+ resetView: true,
30
+ reorderObjects: true,
31
+ toggleObjectVisibility: true,
32
+ lockObjects: true,
33
+ renameObjects: true,
34
+ presetBackgrounds: true,
35
+ presetElements: true
36
+ };
37
+ var defaultLayout = {
38
+ header: true,
39
+ editorHeader: true,
40
+ viewerHeader: true,
41
+ toolbar: true,
42
+ layers: true,
43
+ status: true
44
+ };
45
+ var defaultLabels = {
46
+ workbench: "Product customization studio",
47
+ editorTitle: "Design surface",
48
+ viewerTitle: "Product preview",
49
+ editorMode: "2D",
50
+ viewerMode: "3D",
51
+ editorCanvas: "UV texture editor",
52
+ viewerCanvas: "Interactive 3D product preview",
53
+ layers: "Layers",
54
+ noObjects: "No design objects",
55
+ addText: "Text",
56
+ textFormatting: "Text formatting",
57
+ moreTextOptions: "More text options",
58
+ editText: "Edit text",
59
+ fontFamily: "Font",
60
+ fontSize: "Font size",
61
+ bold: "Bold",
62
+ italic: "Italic",
63
+ underline: "Underline",
64
+ alignLeft: "Align left",
65
+ alignCenter: "Align center",
66
+ alignRight: "Align right",
67
+ textBackground: "Text highlight",
68
+ lineHeight: "Line height",
69
+ letterSpacing: "Letter spacing",
70
+ addImage: "Image",
71
+ setImageAsBackground: "Set as background",
72
+ undo: "Undo",
73
+ redo: "Redo",
74
+ saveDesign: "Save design JSON",
75
+ loadDesign: "Open design JSON",
76
+ deleteSelection: "Delete selected object",
77
+ loadProduct: "Load product",
78
+ resetView: "Reset 3D view",
79
+ textObject: "Text",
80
+ imageObject: "Image",
81
+ backgroundObject: "Background",
82
+ showLayer: "Show layer",
83
+ hideLayer: "Hide layer",
84
+ lockLayer: "Lock layer",
85
+ unlockLayer: "Unlock layer",
86
+ moveLayerForward: "Move layer forward",
87
+ moveLayerBackward: "Move layer backward",
88
+ deleteLayer: "Delete layer",
89
+ textDialogEyebrow: "Typography",
90
+ textDialogTitle: "Add text",
91
+ textInputLabel: "Text",
92
+ textInputPlaceholder: "Make it yours",
93
+ defaultText: "Edit this text",
94
+ textRequired: "Enter text",
95
+ textPresets: "Style",
96
+ textColor: "Color",
97
+ colorBlack: "Black",
98
+ colorSlate: "Slate",
99
+ colorGray: "Gray",
100
+ colorWhite: "White",
101
+ colorRed: "Red",
102
+ colorOrange: "Orange",
103
+ colorYellow: "Yellow",
104
+ colorGreen: "Green",
105
+ colorTeal: "Teal",
106
+ colorCyan: "Cyan",
107
+ colorBlue: "Blue",
108
+ colorPurple: "Purple",
109
+ colorPink: "Pink",
110
+ colorBrown: "Brown",
111
+ mixedValue: "—",
112
+ addTextConfirm: "Add text",
113
+ imageDialogEyebrow: "Artwork",
114
+ imageDialogTitle: "Add image",
115
+ uploadImageTab: "Upload",
116
+ backgroundsTab: "Backgrounds",
117
+ elementsTab: "Elements",
118
+ chooseImage: "Choose image",
119
+ noImageSelected: "No image selected",
120
+ noPresetAssets: "No preset assets",
121
+ addImageConfirm: "Add image",
122
+ close: "Close",
123
+ cancel: "Cancel",
124
+ productDialogEyebrow: "Product source",
125
+ productDialogTitle: "Load product",
126
+ productSourceMethod: "Model source",
127
+ productFileSource: "Upload GLB",
128
+ productUrlSource: "From URL",
129
+ modelFile: "GLB file",
130
+ chooseModelFile: "Choose GLB file",
131
+ noModelFileSelected: "No file selected",
132
+ invalidModelFile: "Choose a .glb file",
133
+ modelFileRequired: "Choose a GLB file",
134
+ modelUrl: "GLB or GLTF URL",
135
+ modelUrlPlaceholder: "https://example.com/product.glb",
136
+ modelUrlRequired: "Enter a model URL",
137
+ invalidModelUrl: "Enter a valid model URL",
138
+ advancedProductOptions: "Advanced options",
139
+ textureUrl: "Base artwork URL (optional)",
140
+ textureUrlPlaceholder: "https://example.com/artwork.png",
141
+ invalidTextureUrl: "Enter a valid artwork URL",
142
+ textureUrlHint: "Placed beneath all editable objects",
143
+ surfaceMesh: "Printable mesh name",
144
+ surfaceMeshRequired: "Enter a printable mesh name",
145
+ surfaceMeshHint: "The model mesh that receives the design; defaults to PrintArea",
146
+ flipTexture: "Flip texture vertically",
147
+ flipTextureHint: "Only enable this when the design appears upside down",
148
+ useDemo: "Use built-in demo",
149
+ addingImage: "Adding image",
150
+ loadingDesign: "Loading design",
151
+ loadingProduct: "Loading product...",
152
+ undoing: "Undoing",
153
+ redoing: "Redoing",
154
+ starting: "Starting",
155
+ productReady: "Product ready",
156
+ designSaved: "Design saved",
157
+ textAdded: "Text added",
158
+ imageAdded: "Image added",
159
+ designLoaded: "Design loaded",
160
+ undoComplete: "Undo complete",
161
+ redoComplete: "Redo complete",
162
+ objectCountOne: "{count} object",
163
+ objectCountMany: "{count} objects",
164
+ designFilename: "customforge-design.json"
165
+ };
166
+ var defaultTheme = {
167
+ ink: "#18181b",
168
+ muted: "#71717a",
169
+ border: "#e4e4e7",
170
+ surface: "#ffffff",
171
+ surfaceMuted: "#fafafa",
172
+ stage: "#f4f4f5",
173
+ accent: "#268a4b",
174
+ accentHover: "#1f7640",
175
+ accentContrast: "#ffffff",
176
+ danger: "#b42318",
177
+ fontFamily: "\"Nunito Sans\", \"Avenir Next\", \"Segoe UI Variable\", \"Segoe UI\", ui-sans-serif, system-ui, sans-serif",
178
+ controlRadius: "4px"
179
+ };
180
+ var defaultTextPresets = [
181
+ {
182
+ id: "modern-bold",
183
+ name: "Modern bold",
184
+ previewText: "CustomForge",
185
+ fontFamily: "Arial",
186
+ fontSize: 22,
187
+ width: 220,
188
+ color: "#17191c"
189
+ },
190
+ {
191
+ id: "editorial-serif",
192
+ name: "Editorial serif",
193
+ previewText: "Made for you",
194
+ fontFamily: "Georgia",
195
+ fontSize: 64,
196
+ width: 480,
197
+ color: "#1f2933"
198
+ },
199
+ {
200
+ id: "clean-label",
201
+ name: "Clean label",
202
+ previewText: "CUSTOM EDITION",
203
+ fontFamily: "Trebuchet MS",
204
+ fontSize: 52,
205
+ width: 420,
206
+ color: "#0b66c3"
207
+ },
208
+ {
209
+ id: "signature",
210
+ name: "Signature",
211
+ previewText: "Yours truly",
212
+ fontFamily: "Brush Script MT",
213
+ fontSize: 76,
214
+ width: 440,
215
+ color: "#7a3e2f"
216
+ }
217
+ ];
218
+ var defaultFontFamilies = [
219
+ "Arial",
220
+ "Georgia",
221
+ "Trebuchet MS",
222
+ "Verdana",
223
+ "Times New Roman",
224
+ "Courier New",
225
+ "Brush Script MT",
226
+ "Nunito Sans"
227
+ ].map((fontFamily) => ({
228
+ value: fontFamily,
229
+ label: fontFamily
230
+ }));
231
+ function defaultErrorFormatter(error) {
232
+ return error instanceof Error ? error.message : String(error);
233
+ }
234
+ function positiveInteger(value, fallback, name) {
235
+ const resolved = value ?? fallback;
236
+ if (!Number.isInteger(resolved) || resolved < 1) throw new RangeError(`${name} must be a positive integer`);
237
+ return resolved;
238
+ }
239
+ function uniqueAssets(assets, name) {
240
+ const ids = /* @__PURE__ */ new Set();
241
+ return assets.map((asset) => {
242
+ const id = asset.id.trim();
243
+ const label = asset.name.trim();
244
+ const url = asset.url.trim();
245
+ if (!id || !label || !url) throw new TypeError(`${name} assets require id, name, and url`);
246
+ if (ids.has(id)) throw new TypeError(`${name} asset id is duplicated: ${id}`);
247
+ ids.add(id);
248
+ return {
249
+ ...asset,
250
+ id,
251
+ name: label,
252
+ url
253
+ };
254
+ });
255
+ }
256
+ function uniqueTextPresets(presets) {
257
+ const ids = /* @__PURE__ */ new Set();
258
+ return presets.map((preset) => {
259
+ const id = preset.id.trim();
260
+ const name = preset.name.trim();
261
+ if (!id || !name || !preset.fontFamily.trim()) throw new TypeError("Text presets require id, name, and fontFamily");
262
+ if (ids.has(id)) throw new TypeError(`Text preset id is duplicated: ${id}`);
263
+ if (preset.fontSize <= 0 || preset.width <= 0) throw new RangeError(`Text preset ${id} requires positive fontSize and width`);
264
+ ids.add(id);
265
+ return {
266
+ ...preset,
267
+ id,
268
+ name,
269
+ fontFamily: preset.fontFamily.trim()
270
+ };
271
+ });
272
+ }
273
+ function normalizeClassNames(value) {
274
+ return [...new Set(value?.trim().split(/\s+/).filter(Boolean) ?? [])];
275
+ }
276
+ var extensionPlacements = /* @__PURE__ */ new Set([
277
+ "globalActions",
278
+ "editorToolbar",
279
+ "selectionToolbar",
280
+ "layerActions"
281
+ ]);
282
+ var extensionVariants = /* @__PURE__ */ new Set([
283
+ "primary",
284
+ "secondary",
285
+ "plain",
286
+ "danger"
287
+ ]);
288
+ function normalizeExtensionPredicate(value, fallback, field) {
289
+ if (value === void 0) return fallback;
290
+ if (typeof value !== "boolean" && typeof value !== "function") throw new TypeError(`Extension ${field} must be a boolean or function`);
291
+ return value;
292
+ }
293
+ /**
294
+ * 校验并补全单个 Workbench 扩展按钮配置
295
+ *
296
+ * @param extension 外部扩展按钮配置
297
+ * @returns 可直接渲染的内部配置
298
+ */
299
+ function normalizeWorkbenchExtension(extension) {
300
+ const id = extension.id.trim();
301
+ const label = extension.label.trim();
302
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(id)) throw new TypeError("Extension id must start with an alphanumeric character and contain only letters, numbers, dots, underscores, or hyphens");
303
+ if (!extensionPlacements.has(extension.placement)) throw new TypeError(`Extension ${id} has an invalid placement`);
304
+ if (!label) throw new TypeError(`Extension ${id} requires a label`);
305
+ if (typeof extension.onClick !== "function") throw new TypeError(`Extension ${id} requires an onClick function`);
306
+ const iconUrl = extension.iconUrl?.trim() || void 0;
307
+ const showLabel = extension.showLabel ?? (extension.placement !== "layerActions" || iconUrl === void 0);
308
+ if (!showLabel && !iconUrl) throw new TypeError(`Extension ${id} must provide iconUrl when showLabel is false`);
309
+ const variant = extension.variant ?? (extension.placement === "globalActions" ? "secondary" : "plain");
310
+ if (!extensionVariants.has(variant)) throw new TypeError(`Extension ${id} has an invalid variant`);
311
+ const order = extension.order ?? 0;
312
+ if (!Number.isFinite(order)) throw new TypeError(`Extension ${id} order must be a finite number`);
313
+ return {
314
+ id,
315
+ placement: extension.placement,
316
+ label,
317
+ iconUrl,
318
+ showLabel,
319
+ variant,
320
+ order,
321
+ classNames: normalizeClassNames(extension.className),
322
+ visible: normalizeExtensionPredicate(extension.visible, true, "visible"),
323
+ disabled: normalizeExtensionPredicate(extension.disabled, false, "disabled"),
324
+ onClick: extension.onClick
325
+ };
326
+ }
327
+ /**
328
+ * 校验 Workbench 扩展按钮集合并拒绝重复 ID
329
+ *
330
+ * @param extensions 外部扩展按钮集合
331
+ * @returns 保持声明顺序的完整内部配置
332
+ */
333
+ function normalizeWorkbenchExtensions(extensions) {
334
+ const ids = /* @__PURE__ */ new Set();
335
+ return extensions.map((extension) => {
336
+ const normalized = normalizeWorkbenchExtension(extension);
337
+ if (ids.has(normalized.id)) throw new TypeError(`Extension id is duplicated: ${normalized.id}`);
338
+ ids.add(normalized.id);
339
+ return normalized;
340
+ });
341
+ }
342
+ function uniqueFontFamilies(fonts) {
343
+ const values = /* @__PURE__ */ new Set();
344
+ return fonts.map((font) => {
345
+ const value = font.value.trim();
346
+ const label = font.label.trim();
347
+ if (!value || !label) throw new TypeError("Font families require value and label");
348
+ if (values.has(value)) throw new TypeError(`Font family value is duplicated: ${value}`);
349
+ values.add(value);
350
+ return {
351
+ value,
352
+ label
353
+ };
354
+ });
355
+ }
356
+ function normalizeBranding(branding) {
357
+ return {
358
+ logoUrl: branding?.logoUrl?.trim() || "data:image/png;base64,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",
359
+ logoAlt: branding?.logoAlt?.trim() || "CustomForge",
360
+ showLogo: branding?.showLogo ?? true,
361
+ title: branding?.title ?? "CustomForge",
362
+ showTitle: branding?.showTitle ?? true,
363
+ subtitle: branding?.subtitle ?? "Product customization studio",
364
+ showSubtitle: branding?.showSubtitle ?? true
365
+ };
366
+ }
367
+ /**
368
+ * 补全并校验 Workbench 默认配置
369
+ *
370
+ * @param options 外部 Workbench 配置
371
+ * @returns 不包含挂载容器的完整内部配置
372
+ */
373
+ function normalizeWorkbenchOptions(options) {
374
+ return {
375
+ classNames: normalizeClassNames(options.className),
376
+ editorWidth: positiveInteger(options.editorWidth, 1024, "editorWidth"),
377
+ editorHeight: positiveInteger(options.editorHeight, 512, "editorHeight"),
378
+ historyLimit: positiveInteger(options.historyLimit, 50, "historyLimit"),
379
+ product: options.product,
380
+ appearance: options.appearance,
381
+ features: {
382
+ ...defaultFeatures,
383
+ ...options.features
384
+ },
385
+ layout: {
386
+ ...defaultLayout,
387
+ ...options.layout
388
+ },
389
+ branding: normalizeBranding(options.branding),
390
+ labels: {
391
+ ...defaultLabels,
392
+ ...options.labels
393
+ },
394
+ theme: {
395
+ ...defaultTheme,
396
+ ...options.theme
397
+ },
398
+ icons: {
399
+ enabled: options.icons?.enabled ?? true,
400
+ sources: { ...options.icons?.sources }
401
+ },
402
+ textPresets: uniqueTextPresets(options.textPresets ?? defaultTextPresets),
403
+ fontFamilies: uniqueFontFamilies(options.fontFamilies ?? defaultFontFamilies),
404
+ backgrounds: uniqueAssets(options.assets?.backgrounds ?? builtInAssetLibrary.backgrounds, "Background"),
405
+ elements: uniqueAssets(options.assets?.elements ?? builtInAssetLibrary.elements, "Element"),
406
+ extensions: normalizeWorkbenchExtensions(options.extensions ?? []),
407
+ formatError: options.formatError ?? defaultErrorFormatter
408
+ };
409
+ }
410
+ //#endregion
411
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/defaultAttributes.mjs
412
+ /**
413
+ * @license lucide v1.34.0 - ISC
414
+ *
415
+ * This source code is licensed under the ISC license.
416
+ * See the LICENSE file in the root directory of this source tree.
417
+ */
418
+ var defaultAttributes = {
419
+ xmlns: "http://www.w3.org/2000/svg",
420
+ width: 24,
421
+ height: 24,
422
+ viewBox: "0 0 24 24",
423
+ fill: "none",
424
+ stroke: "currentColor",
425
+ "stroke-width": 2,
426
+ "stroke-linecap": "round",
427
+ "stroke-linejoin": "round"
428
+ };
429
+ //#endregion
430
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/createElement.mjs
431
+ /**
432
+ * @license lucide v1.34.0 - ISC
433
+ *
434
+ * This source code is licensed under the ISC license.
435
+ * See the LICENSE file in the root directory of this source tree.
436
+ */
437
+ var createSVGElement = ([tag, attrs, children]) => {
438
+ const element = document.createElementNS("http://www.w3.org/2000/svg", tag);
439
+ Object.keys(attrs).forEach((name) => {
440
+ element.setAttribute(name, String(attrs[name]));
441
+ });
442
+ if (children?.length) children.forEach((child) => {
443
+ const childElement = createSVGElement(child);
444
+ element.appendChild(childElement);
445
+ });
446
+ return element;
447
+ };
448
+ var createElement = (iconNode, customAttrs = {}) => {
449
+ return createSVGElement([
450
+ "svg",
451
+ {
452
+ ...defaultAttributes,
453
+ ...customAttrs
454
+ },
455
+ iconNode
456
+ ]);
457
+ };
458
+ //#endregion
459
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/shared/src/utils/hasA11yProp.mjs
460
+ /**
461
+ * @license lucide v1.34.0 - ISC
462
+ *
463
+ * This source code is licensed under the ISC license.
464
+ * See the LICENSE file in the root directory of this source tree.
465
+ */
466
+ var hasA11yProp = (props) => {
467
+ for (const prop in props) if (prop.startsWith("aria-") || prop === "role" || prop === "title") return true;
468
+ return false;
469
+ };
470
+ //#endregion
471
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/shared/src/utils/mergeClasses.mjs
472
+ /**
473
+ * @license lucide v1.34.0 - ISC
474
+ *
475
+ * This source code is licensed under the ISC license.
476
+ * See the LICENSE file in the root directory of this source tree.
477
+ */
478
+ var mergeClasses = (...classes) => classes.filter((className, index, array) => {
479
+ return Boolean(className) && className.trim() !== "" && array.indexOf(className) === index;
480
+ }).join(" ").trim();
481
+ //#endregion
482
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/shared/src/utils/toCamelCase.mjs
483
+ /**
484
+ * @license lucide v1.34.0 - ISC
485
+ *
486
+ * This source code is licensed under the ISC license.
487
+ * See the LICENSE file in the root directory of this source tree.
488
+ */
489
+ var toCamelCase = (string) => string.replace(/^([A-Z])|[\s-_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase());
490
+ //#endregion
491
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/shared/src/utils/toPascalCase.mjs
492
+ /**
493
+ * @license lucide v1.34.0 - ISC
494
+ *
495
+ * This source code is licensed under the ISC license.
496
+ * See the LICENSE file in the root directory of this source tree.
497
+ */
498
+ var toPascalCase = (string) => {
499
+ const camelCase = toCamelCase(string);
500
+ return camelCase.charAt(0).toUpperCase() + camelCase.slice(1);
501
+ };
502
+ //#endregion
503
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/replaceElement.mjs
504
+ /**
505
+ * @license lucide v1.34.0 - ISC
506
+ *
507
+ * This source code is licensed under the ISC license.
508
+ * See the LICENSE file in the root directory of this source tree.
509
+ */
510
+ var getAttrs = (element) => Array.from(element.attributes).reduce((attrs, attr) => {
511
+ attrs[attr.name] = attr.value;
512
+ return attrs;
513
+ }, {});
514
+ var getClassNames = (attrs) => {
515
+ if (typeof attrs === "string") return attrs;
516
+ if (!attrs || !attrs.class) return "";
517
+ if (attrs.class && typeof attrs.class === "string") return attrs.class.split(" ");
518
+ if (attrs.class && Array.isArray(attrs.class)) return attrs.class;
519
+ return "";
520
+ };
521
+ var replaceElement = (element, { nameAttr, icons, attrs }) => {
522
+ const iconName = element.getAttribute(nameAttr);
523
+ if (iconName == null) return;
524
+ const iconNode = icons[toPascalCase(iconName)];
525
+ if (!iconNode) return console.warn(`${element.outerHTML} icon name was not found in the provided icons object.`);
526
+ const elementAttrs = getAttrs(element);
527
+ const ariaProps = hasA11yProp(elementAttrs) ? {} : { "aria-hidden": "true" };
528
+ const iconAttrs = {
529
+ ...defaultAttributes,
530
+ "data-lucide": iconName,
531
+ ...ariaProps,
532
+ ...attrs,
533
+ ...elementAttrs
534
+ };
535
+ const elementClassNames = getClassNames(elementAttrs);
536
+ const className = getClassNames(attrs);
537
+ const classNames = mergeClasses("lucide", `lucide-${iconName}`, ...elementClassNames, ...className);
538
+ if (classNames) Object.assign(iconAttrs, { class: classNames });
539
+ const svgElement = createElement(iconNode, iconAttrs);
540
+ return element.parentNode?.replaceChild(svgElement, element);
541
+ };
542
+ //#endregion
543
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/arrow-down.mjs
544
+ /**
545
+ * @license lucide v1.34.0 - ISC
546
+ *
547
+ * This source code is licensed under the ISC license.
548
+ * See the LICENSE file in the root directory of this source tree.
549
+ */
550
+ var ArrowDown = [["path", { d: "M12 5v14" }], ["path", { d: "m19 12-7 7-7-7" }]];
551
+ //#endregion
552
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/arrow-up.mjs
553
+ /**
554
+ * @license lucide v1.34.0 - ISC
555
+ *
556
+ * This source code is licensed under the ISC license.
557
+ * See the LICENSE file in the root directory of this source tree.
558
+ */
559
+ var ArrowUp = [["path", { d: "m5 12 7-7 7 7" }], ["path", { d: "M12 19V5" }]];
560
+ //#endregion
561
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/bold.mjs
562
+ /**
563
+ * @license lucide v1.34.0 - ISC
564
+ *
565
+ * This source code is licensed under the ISC license.
566
+ * See the LICENSE file in the root directory of this source tree.
567
+ */
568
+ var Bold = [["path", { d: "M6 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8" }]];
569
+ //#endregion
570
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/chevron-down.mjs
571
+ /**
572
+ * @license lucide v1.34.0 - ISC
573
+ *
574
+ * This source code is licensed under the ISC license.
575
+ * See the LICENSE file in the root directory of this source tree.
576
+ */
577
+ var ChevronDown = [["path", { d: "m6 9 6 6 6-6" }]];
578
+ //#endregion
579
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/ellipsis.mjs
580
+ /**
581
+ * @license lucide v1.34.0 - ISC
582
+ *
583
+ * This source code is licensed under the ISC license.
584
+ * See the LICENSE file in the root directory of this source tree.
585
+ */
586
+ var Ellipsis = [
587
+ ["circle", {
588
+ cx: "12",
589
+ cy: "12",
590
+ r: "1"
591
+ }],
592
+ ["circle", {
593
+ cx: "19",
594
+ cy: "12",
595
+ r: "1"
596
+ }],
597
+ ["circle", {
598
+ cx: "5",
599
+ cy: "12",
600
+ r: "1"
601
+ }]
602
+ ];
603
+ //#endregion
604
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/eye-off.mjs
605
+ /**
606
+ * @license lucide v1.34.0 - ISC
607
+ *
608
+ * This source code is licensed under the ISC license.
609
+ * See the LICENSE file in the root directory of this source tree.
610
+ */
611
+ var EyeOff = [
612
+ ["path", { d: "M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" }],
613
+ ["path", { d: "M14.084 14.158a3 3 0 0 1-4.242-4.242" }],
614
+ ["path", { d: "M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" }],
615
+ ["path", { d: "m2 2 20 20" }]
616
+ ];
617
+ //#endregion
618
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/eye.mjs
619
+ /**
620
+ * @license lucide v1.34.0 - ISC
621
+ *
622
+ * This source code is licensed under the ISC license.
623
+ * See the LICENSE file in the root directory of this source tree.
624
+ */
625
+ var Eye = [["path", { d: "M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" }], ["circle", {
626
+ cx: "12",
627
+ cy: "12",
628
+ r: "3"
629
+ }]];
630
+ //#endregion
631
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/folder-open.mjs
632
+ /**
633
+ * @license lucide v1.34.0 - ISC
634
+ *
635
+ * This source code is licensed under the ISC license.
636
+ * See the LICENSE file in the root directory of this source tree.
637
+ */
638
+ var FolderOpen = [["path", { d: "m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" }]];
639
+ //#endregion
640
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/highlighter.mjs
641
+ /**
642
+ * @license lucide v1.34.0 - ISC
643
+ *
644
+ * This source code is licensed under the ISC license.
645
+ * See the LICENSE file in the root directory of this source tree.
646
+ */
647
+ var Highlighter = [["path", { d: "m9 11-6 6v3h9l3-3" }], ["path", { d: "m22 12-4.6 4.6a2 2 0 0 1-2.8 0l-5.2-5.2a2 2 0 0 1 0-2.8L14 4" }]];
648
+ //#endregion
649
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/image-plus.mjs
650
+ /**
651
+ * @license lucide v1.34.0 - ISC
652
+ *
653
+ * This source code is licensed under the ISC license.
654
+ * See the LICENSE file in the root directory of this source tree.
655
+ */
656
+ var ImagePlus = [
657
+ ["path", { d: "M16 5h6" }],
658
+ ["path", { d: "M19 2v6" }],
659
+ ["path", { d: "M21 11.5V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h7.5" }],
660
+ ["path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" }],
661
+ ["circle", {
662
+ cx: "9",
663
+ cy: "9",
664
+ r: "2"
665
+ }]
666
+ ];
667
+ //#endregion
668
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/image.mjs
669
+ /**
670
+ * @license lucide v1.34.0 - ISC
671
+ *
672
+ * This source code is licensed under the ISC license.
673
+ * See the LICENSE file in the root directory of this source tree.
674
+ */
675
+ var Image = [
676
+ ["rect", {
677
+ width: "18",
678
+ height: "18",
679
+ x: "3",
680
+ y: "3",
681
+ rx: "2",
682
+ ry: "2"
683
+ }],
684
+ ["circle", {
685
+ cx: "9",
686
+ cy: "9",
687
+ r: "2"
688
+ }],
689
+ ["path", { d: "m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" }]
690
+ ];
691
+ //#endregion
692
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/italic.mjs
693
+ /**
694
+ * @license lucide v1.34.0 - ISC
695
+ *
696
+ * This source code is licensed under the ISC license.
697
+ * See the LICENSE file in the root directory of this source tree.
698
+ */
699
+ var Italic = [
700
+ ["line", {
701
+ x1: "19",
702
+ x2: "10",
703
+ y1: "4",
704
+ y2: "4"
705
+ }],
706
+ ["line", {
707
+ x1: "14",
708
+ x2: "5",
709
+ y1: "20",
710
+ y2: "20"
711
+ }],
712
+ ["line", {
713
+ x1: "15",
714
+ x2: "9",
715
+ y1: "4",
716
+ y2: "20"
717
+ }]
718
+ ];
719
+ //#endregion
720
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/layers.mjs
721
+ /**
722
+ * @license lucide v1.34.0 - ISC
723
+ *
724
+ * This source code is licensed under the ISC license.
725
+ * See the LICENSE file in the root directory of this source tree.
726
+ */
727
+ var Layers = [
728
+ ["path", { d: "M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z" }],
729
+ ["path", { d: "M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" }],
730
+ ["path", { d: "M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" }]
731
+ ];
732
+ //#endregion
733
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/link-2.mjs
734
+ /**
735
+ * @license lucide v1.34.0 - ISC
736
+ *
737
+ * This source code is licensed under the ISC license.
738
+ * See the LICENSE file in the root directory of this source tree.
739
+ */
740
+ var Link2 = [
741
+ ["path", { d: "M9 17H7A5 5 0 0 1 7 7h2" }],
742
+ ["path", { d: "M15 7h2a5 5 0 1 1 0 10h-2" }],
743
+ ["line", {
744
+ x1: "8",
745
+ x2: "16",
746
+ y1: "12",
747
+ y2: "12"
748
+ }]
749
+ ];
750
+ //#endregion
751
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/lock-open.mjs
752
+ /**
753
+ * @license lucide v1.34.0 - ISC
754
+ *
755
+ * This source code is licensed under the ISC license.
756
+ * See the LICENSE file in the root directory of this source tree.
757
+ */
758
+ var LockOpen = [["rect", {
759
+ width: "18",
760
+ height: "11",
761
+ x: "3",
762
+ y: "11",
763
+ rx: "2",
764
+ ry: "2"
765
+ }], ["path", { d: "M7 11V7a5 5 0 0 1 9.9-1" }]];
766
+ //#endregion
767
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/lock.mjs
768
+ /**
769
+ * @license lucide v1.34.0 - ISC
770
+ *
771
+ * This source code is licensed under the ISC license.
772
+ * See the LICENSE file in the root directory of this source tree.
773
+ */
774
+ var Lock = [["rect", {
775
+ width: "18",
776
+ height: "11",
777
+ x: "3",
778
+ y: "11",
779
+ rx: "2",
780
+ ry: "2"
781
+ }], ["path", { d: "M7 11V7a5 5 0 0 1 10 0v4" }]];
782
+ //#endregion
783
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/palette.mjs
784
+ /**
785
+ * @license lucide v1.34.0 - ISC
786
+ *
787
+ * This source code is licensed under the ISC license.
788
+ * See the LICENSE file in the root directory of this source tree.
789
+ */
790
+ var Palette = [
791
+ ["path", { d: "M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" }],
792
+ ["circle", {
793
+ cx: "13.5",
794
+ cy: "6.5",
795
+ r: ".5",
796
+ fill: "currentColor"
797
+ }],
798
+ ["circle", {
799
+ cx: "17.5",
800
+ cy: "10.5",
801
+ r: ".5",
802
+ fill: "currentColor"
803
+ }],
804
+ ["circle", {
805
+ cx: "6.5",
806
+ cy: "12.5",
807
+ r: ".5",
808
+ fill: "currentColor"
809
+ }],
810
+ ["circle", {
811
+ cx: "8.5",
812
+ cy: "7.5",
813
+ r: ".5",
814
+ fill: "currentColor"
815
+ }]
816
+ ];
817
+ //#endregion
818
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/redo-2.mjs
819
+ /**
820
+ * @license lucide v1.34.0 - ISC
821
+ *
822
+ * This source code is licensed under the ISC license.
823
+ * See the LICENSE file in the root directory of this source tree.
824
+ */
825
+ var Redo2 = [["path", { d: "m15 14 5-5-5-5" }], ["path", { d: "M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" }]];
826
+ //#endregion
827
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/rotate-ccw.mjs
828
+ /**
829
+ * @license lucide v1.34.0 - ISC
830
+ *
831
+ * This source code is licensed under the ISC license.
832
+ * See the LICENSE file in the root directory of this source tree.
833
+ */
834
+ var RotateCcw = [["path", { d: "M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" }], ["path", { d: "M3 3v5h5" }]];
835
+ //#endregion
836
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/rows-3.mjs
837
+ /**
838
+ * @license lucide v1.34.0 - ISC
839
+ *
840
+ * This source code is licensed under the ISC license.
841
+ * See the LICENSE file in the root directory of this source tree.
842
+ */
843
+ var Rows3 = [
844
+ ["rect", {
845
+ width: "18",
846
+ height: "18",
847
+ x: "3",
848
+ y: "3",
849
+ rx: "2"
850
+ }],
851
+ ["path", { d: "M21 9H3" }],
852
+ ["path", { d: "M21 15H3" }]
853
+ ];
854
+ //#endregion
855
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/save.mjs
856
+ /**
857
+ * @license lucide v1.34.0 - ISC
858
+ *
859
+ * This source code is licensed under the ISC license.
860
+ * See the LICENSE file in the root directory of this source tree.
861
+ */
862
+ var Save = [
863
+ ["path", { d: "M15.2 3a2 2 0 0 1 1.4.6l3.8 3.8a2 2 0 0 1 .6 1.4V19a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2z" }],
864
+ ["path", { d: "M17 21v-7a1 1 0 0 0-1-1H8a1 1 0 0 0-1 1v7" }],
865
+ ["path", { d: "M7 3v4a1 1 0 0 0 1 1h7" }]
866
+ ];
867
+ //#endregion
868
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/stretch-horizontal.mjs
869
+ /**
870
+ * @license lucide v1.34.0 - ISC
871
+ *
872
+ * This source code is licensed under the ISC license.
873
+ * See the LICENSE file in the root directory of this source tree.
874
+ */
875
+ var StretchHorizontal = [["rect", {
876
+ width: "20",
877
+ height: "6",
878
+ x: "2",
879
+ y: "4",
880
+ rx: "2"
881
+ }], ["rect", {
882
+ width: "20",
883
+ height: "6",
884
+ x: "2",
885
+ y: "14",
886
+ rx: "2"
887
+ }]];
888
+ //#endregion
889
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-align-center.mjs
890
+ /**
891
+ * @license lucide v1.34.0 - ISC
892
+ *
893
+ * This source code is licensed under the ISC license.
894
+ * See the LICENSE file in the root directory of this source tree.
895
+ */
896
+ var TextAlignCenter = [
897
+ ["path", { d: "M21 5H3" }],
898
+ ["path", { d: "M17 12H7" }],
899
+ ["path", { d: "M19 19H5" }]
900
+ ];
901
+ //#endregion
902
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-align-end.mjs
903
+ /**
904
+ * @license lucide v1.34.0 - ISC
905
+ *
906
+ * This source code is licensed under the ISC license.
907
+ * See the LICENSE file in the root directory of this source tree.
908
+ */
909
+ var TextAlignEnd = [
910
+ ["path", { d: "M21 5H3" }],
911
+ ["path", { d: "M21 12H9" }],
912
+ ["path", { d: "M21 19H7" }]
913
+ ];
914
+ //#endregion
915
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-align-start.mjs
916
+ /**
917
+ * @license lucide v1.34.0 - ISC
918
+ *
919
+ * This source code is licensed under the ISC license.
920
+ * See the LICENSE file in the root directory of this source tree.
921
+ */
922
+ var TextAlignStart = [
923
+ ["path", { d: "M21 5H3" }],
924
+ ["path", { d: "M15 12H3" }],
925
+ ["path", { d: "M17 19H3" }]
926
+ ];
927
+ //#endregion
928
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/text-cursor-input.mjs
929
+ /**
930
+ * @license lucide v1.34.0 - ISC
931
+ *
932
+ * This source code is licensed under the ISC license.
933
+ * See the LICENSE file in the root directory of this source tree.
934
+ */
935
+ var TextCursorInput = [
936
+ ["path", { d: "M12 20h-1a2 2 0 0 1-2-2 2 2 0 0 1-2 2H6" }],
937
+ ["path", { d: "M13 8h7a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2h-7" }],
938
+ ["path", { d: "M5 16H4a2 2 0 0 1-2-2v-4a2 2 0 0 1 2-2h1" }],
939
+ ["path", { d: "M6 4h1a2 2 0 0 1 2 2 2 2 0 0 1 2-2h1" }],
940
+ ["path", { d: "M9 6v12" }]
941
+ ];
942
+ //#endregion
943
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/trash-2.mjs
944
+ /**
945
+ * @license lucide v1.34.0 - ISC
946
+ *
947
+ * This source code is licensed under the ISC license.
948
+ * See the LICENSE file in the root directory of this source tree.
949
+ */
950
+ var Trash2 = [
951
+ ["path", { d: "M10 11v6" }],
952
+ ["path", { d: "M14 11v6" }],
953
+ ["path", { d: "M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" }],
954
+ ["path", { d: "M3 6h18" }],
955
+ ["path", { d: "M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" }]
956
+ ];
957
+ //#endregion
958
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/type.mjs
959
+ /**
960
+ * @license lucide v1.34.0 - ISC
961
+ *
962
+ * This source code is licensed under the ISC license.
963
+ * See the LICENSE file in the root directory of this source tree.
964
+ */
965
+ var Type = [
966
+ ["path", { d: "M12 4v16" }],
967
+ ["path", { d: "M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" }],
968
+ ["path", { d: "M9 20h6" }]
969
+ ];
970
+ //#endregion
971
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/underline.mjs
972
+ /**
973
+ * @license lucide v1.34.0 - ISC
974
+ *
975
+ * This source code is licensed under the ISC license.
976
+ * See the LICENSE file in the root directory of this source tree.
977
+ */
978
+ var Underline = [["path", { d: "M6 4v6a6 6 0 0 0 12 0V4" }], ["line", {
979
+ x1: "4",
980
+ x2: "20",
981
+ y1: "20",
982
+ y2: "20"
983
+ }]];
984
+ //#endregion
985
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/undo-2.mjs
986
+ /**
987
+ * @license lucide v1.34.0 - ISC
988
+ *
989
+ * This source code is licensed under the ISC license.
990
+ * See the LICENSE file in the root directory of this source tree.
991
+ */
992
+ var Undo2 = [["path", { d: "M9 14 4 9l5-5" }], ["path", { d: "M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" }]];
993
+ //#endregion
994
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/upload.mjs
995
+ /**
996
+ * @license lucide v1.34.0 - ISC
997
+ *
998
+ * This source code is licensed under the ISC license.
999
+ * See the LICENSE file in the root directory of this source tree.
1000
+ */
1001
+ var Upload = [
1002
+ ["path", { d: "M12 3v12" }],
1003
+ ["path", { d: "m17 8-5-5-5 5" }],
1004
+ ["path", { d: "M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" }]
1005
+ ];
1006
+ //#endregion
1007
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/icons/x.mjs
1008
+ /**
1009
+ * @license lucide v1.34.0 - ISC
1010
+ *
1011
+ * This source code is licensed under the ISC license.
1012
+ * See the LICENSE file in the root directory of this source tree.
1013
+ */
1014
+ var X = [["path", { d: "M18 6 6 18" }], ["path", { d: "m6 6 12 12" }]];
1015
+ //#endregion
1016
+ //#region node_modules/.pnpm/lucide@1.34.0/node_modules/lucide/dist/esm/lucide.mjs
1017
+ /**
1018
+ * @license lucide v1.34.0 - ISC
1019
+ *
1020
+ * This source code is licensed under the ISC license.
1021
+ * See the LICENSE file in the root directory of this source tree.
1022
+ */
1023
+ var createIcons = ({ icons = {}, nameAttr = "data-lucide", attrs = {}, root = document, inTemplates } = {}) => {
1024
+ if (!Object.values(icons).length) throw new Error("Please provide an icons object.\nIf you want to use all the icons you can import it like:\n `import { createIcons, icons } from 'lucide';\nlucide.createIcons({icons});`");
1025
+ if (typeof root === "undefined") throw new Error("`createIcons()` only works in a browser environment.");
1026
+ Array.from(root.querySelectorAll(`[${nameAttr}]`)).forEach((element) => replaceElement(element, {
1027
+ nameAttr,
1028
+ icons,
1029
+ attrs
1030
+ }));
1031
+ if (inTemplates) Array.from(root.querySelectorAll("template")).forEach((template) => createIcons({
1032
+ icons,
1033
+ nameAttr,
1034
+ attrs,
1035
+ root: template.content,
1036
+ inTemplates
1037
+ }));
1038
+ if (nameAttr === "data-lucide") {
1039
+ const deprecatedElements = root.querySelectorAll("[icon-name]");
1040
+ if (deprecatedElements.length > 0) {
1041
+ console.warn("[Lucide] Some icons were found with the now deprecated icon-name attribute. These will still be replaced for backwards compatibility, but will no longer be supported in v1.0 and you should switch to data-lucide");
1042
+ Array.from(deprecatedElements).forEach((element) => replaceElement(element, {
1043
+ nameAttr: "icon-name",
1044
+ icons,
1045
+ attrs
1046
+ }));
1047
+ }
1048
+ }
1049
+ };
1050
+ //#endregion
1051
+ //#region src/workbench/icons.ts
1052
+ /** 渲染 Workbench 内置图标并应用调用方覆盖 */
1053
+ function renderWorkbenchIcons(root, configuration) {
1054
+ root.dataset.icons = configuration.enabled ? "visible" : "hidden";
1055
+ createIcons({
1056
+ nameAttr: "data-customforge-icon",
1057
+ icons: {
1058
+ AlignCenter: TextAlignCenter,
1059
+ AlignLeft: TextAlignStart,
1060
+ AlignRight: TextAlignEnd,
1061
+ ArrowDown,
1062
+ ArrowUp,
1063
+ Bold,
1064
+ ChevronDown,
1065
+ Ellipsis,
1066
+ Eye,
1067
+ EyeOff,
1068
+ FolderOpen,
1069
+ Image,
1070
+ ImagePlus,
1071
+ Highlighter,
1072
+ Italic,
1073
+ Layers3: Layers,
1074
+ Link2,
1075
+ Lock,
1076
+ LockOpen,
1077
+ Palette,
1078
+ Redo2,
1079
+ Rows3,
1080
+ RotateCcw,
1081
+ Save,
1082
+ StretchHorizontal,
1083
+ TextCursorInput,
1084
+ Trash2,
1085
+ Type,
1086
+ Undo2,
1087
+ Underline,
1088
+ Upload,
1089
+ X
1090
+ },
1091
+ attrs: {
1092
+ width: 18,
1093
+ height: 18,
1094
+ "stroke-width": 1.8,
1095
+ "aria-hidden": "true"
1096
+ }
1097
+ });
1098
+ root.querySelectorAll("[data-icon-slot]").forEach((element) => {
1099
+ const name = element.dataset.iconSlot;
1100
+ const source = configuration.sources[name];
1101
+ const control = element.closest("button");
1102
+ if (!configuration.enabled || source === null) {
1103
+ element.hidden = true;
1104
+ if (source === null) control?.classList.add("customforge-workbench__icon-missing");
1105
+ return;
1106
+ }
1107
+ control?.classList.remove("customforge-workbench__icon-missing");
1108
+ if (!source) {
1109
+ element.hidden = false;
1110
+ return;
1111
+ }
1112
+ if (element instanceof HTMLImageElement && element.getAttribute("src") === source) {
1113
+ element.hidden = false;
1114
+ return;
1115
+ }
1116
+ const image = document.createElement("img");
1117
+ image.className = "customforge-workbench__custom-icon";
1118
+ image.dataset.iconSlot = name;
1119
+ image.src = source;
1120
+ image.alt = "";
1121
+ image.setAttribute("aria-hidden", "true");
1122
+ element.replaceWith(image);
1123
+ });
1124
+ }
1125
+ //#endregion
1126
+ //#region src/workbench/template.ts
1127
+ /** 创建 Workbench 使用的静态 DOM 结构 */
1128
+ function createWorkbenchElement() {
1129
+ const root = document.createElement("main");
1130
+ root.className = "customforge-workbench";
1131
+ root.innerHTML = `
1132
+ <header class="customforge-workbench__topbar" data-layout="header">
1133
+ <div class="customforge-workbench__brand" data-role="brand">
1134
+ <img class="customforge-workbench__brand-logo" data-role="brand-logo" alt="">
1135
+ <span class="customforge-workbench__brand-copy" data-role="brand-copy">
1136
+ <strong data-role="brand-title"></strong>
1137
+ <span data-role="brand-subtitle"></span>
1138
+ </span>
1139
+ </div>
1140
+ <div class="customforge-workbench__global-actions">
1141
+ <div class="customforge-workbench__extension-slot" data-extension-slot="globalActions"></div>
1142
+ <button class="customforge-workbench__button customforge-workbench__button--secondary" data-action="load-remote" data-feature="loadRemoteProduct" data-control-label="loadProduct" type="button">
1143
+ <i data-customforge-icon="upload" data-icon-slot="loadProduct"></i>
1144
+ <span data-label="loadProduct"></span>
1145
+ </button>
1146
+ </div>
1147
+ </header>
1148
+
1149
+ <section class="customforge-workbench__workspace">
1150
+ <section class="customforge-workbench__panel customforge-workbench__panel--editor" data-region-label="editorTitle">
1151
+ <header class="customforge-workbench__panelbar" data-layout="editorHeader">
1152
+ <div class="customforge-workbench__panel-title">
1153
+ <span class="customforge-workbench__panel-kicker" data-label="editorMode"></span>
1154
+ <h1 data-label="editorTitle"></h1>
1155
+ </div>
1156
+ <span class="customforge-workbench__resolution" data-role="resolution"></span>
1157
+ </header>
1158
+
1159
+ <div class="customforge-workbench__toolbar" data-layout="toolbar" data-region-label="workbench" role="toolbar">
1160
+ <div class="customforge-workbench__tool-group" data-tool-group="history">
1161
+ <button class="customforge-workbench__icon-button" data-action="undo" data-feature="undoRedo" data-control-label="undo" type="button" disabled>
1162
+ <i data-customforge-icon="undo-2" data-icon-slot="undo"></i>
1163
+ <span class="customforge-workbench__icon-label" data-label="undo"></span>
1164
+ </button>
1165
+ <button class="customforge-workbench__icon-button" data-action="redo" data-feature="undoRedo" data-control-label="redo" type="button" disabled>
1166
+ <i data-customforge-icon="redo-2" data-icon-slot="redo"></i>
1167
+ <span class="customforge-workbench__icon-label" data-label="redo"></span>
1168
+ </button>
1169
+ </div>
1170
+ <span class="customforge-workbench__separator" data-tool-group="history-separator" aria-hidden="true"></span>
1171
+ <div class="customforge-workbench__tool-group" data-tool-group="insert">
1172
+ <button class="customforge-workbench__tool-button" data-action="open-text-dialog" data-feature="addText" data-control-label="addText" type="button">
1173
+ <i data-customforge-icon="type" data-icon-slot="addText"></i>
1174
+ <span data-label="addText"></span>
1175
+ </button>
1176
+ <button class="customforge-workbench__tool-button" data-action="open-image-dialog" data-feature="addImage" data-control-label="addImage" type="button">
1177
+ <i data-customforge-icon="image-plus" data-icon-slot="addImage"></i>
1178
+ <span data-label="addImage"></span>
1179
+ </button>
1180
+ </div>
1181
+ <div class="customforge-workbench__extension-slot" data-extension-slot="editorToolbar"></div>
1182
+ <span class="customforge-workbench__separator" data-tool-group="document-separator" aria-hidden="true"></span>
1183
+ <div class="customforge-workbench__tool-group" data-tool-group="document">
1184
+ <button class="customforge-workbench__icon-button" data-action="save-design" data-feature="saveDesign" data-control-label="saveDesign" type="button">
1185
+ <i data-customforge-icon="save" data-icon-slot="saveDesign"></i>
1186
+ <span class="customforge-workbench__icon-label" data-label="saveDesign"></span>
1187
+ </button>
1188
+ <button class="customforge-workbench__icon-button" data-action="load-design" data-feature="loadDesign" data-control-label="loadDesign" type="button">
1189
+ <i data-customforge-icon="folder-open" data-icon-slot="loadDesign"></i>
1190
+ <span class="customforge-workbench__icon-label" data-label="loadDesign"></span>
1191
+ </button>
1192
+ </div>
1193
+ <span class="customforge-workbench__separator" data-tool-group="selection-separator" aria-hidden="true"></span>
1194
+ <button class="customforge-workbench__icon-button customforge-workbench__icon-button--danger" data-action="delete-selection" data-feature="deleteSelection" data-control-label="deleteSelection" type="button" disabled>
1195
+ <i data-customforge-icon="trash-2" data-icon-slot="deleteSelection"></i>
1196
+ <span class="customforge-workbench__icon-label" data-label="deleteSelection"></span>
1197
+ </button>
1198
+ <div class="customforge-workbench__extension-slot" data-extension-slot="selectionToolbar"></div>
1199
+
1200
+ <span class="customforge-workbench__separator" data-tool-group="layers-separator" aria-hidden="true"></span>
1201
+ <button class="customforge-workbench__tool-button customforge-workbench__layers-toggle" data-action="toggle-layers" data-control-label="layers" data-role="layers-toggle" type="button" aria-expanded="false">
1202
+ <i data-customforge-icon="layers-3" data-icon-slot="layers"></i>
1203
+ <span data-label="layers"></span>
1204
+ <span class="customforge-workbench__layer-count" data-role="layer-count" aria-hidden="true">0</span>
1205
+ </button>
1206
+
1207
+ <div class="customforge-workbench__text-toolbar" data-role="text-toolbar" data-region-label="textFormatting" role="group" hidden>
1208
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1209
+ <select class="customforge-workbench__font-select" data-role="text-font-family" data-control-label="fontFamily"></select>
1210
+ <label class="customforge-workbench__format-number" data-control-label="fontSize">
1211
+ <span class="customforge-workbench__icon-label" data-label="fontSize"></span>
1212
+ <input data-role="text-font-size" data-control-label="fontSize" type="number" min="8" max="360" step="1" inputmode="numeric">
1213
+ </label>
1214
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1215
+ <div class="customforge-workbench__format-group" role="group">
1216
+ <button class="customforge-workbench__format-button" data-text-style="bold" data-control-label="bold" type="button" aria-pressed="false">
1217
+ <i data-customforge-icon="bold" data-icon-slot="bold"></i>
1218
+ <span class="customforge-workbench__icon-label" data-label="bold"></span>
1219
+ </button>
1220
+ <button class="customforge-workbench__format-button" data-text-style="italic" data-control-label="italic" type="button" aria-pressed="false">
1221
+ <i data-customforge-icon="italic" data-icon-slot="italic"></i>
1222
+ <span class="customforge-workbench__icon-label" data-label="italic"></span>
1223
+ </button>
1224
+ <button class="customforge-workbench__format-button" data-text-style="underline" data-control-label="underline" type="button" aria-pressed="false">
1225
+ <i data-customforge-icon="underline" data-icon-slot="underline"></i>
1226
+ <span class="customforge-workbench__icon-label" data-label="underline"></span>
1227
+ </button>
1228
+ </div>
1229
+ <details class="customforge-workbench__text-options" data-role="text-options">
1230
+ <summary class="customforge-workbench__format-button" data-control-label="moreTextOptions">
1231
+ <i data-customforge-icon="ellipsis" data-icon-slot="moreTextOptions"></i>
1232
+ <span class="customforge-workbench__icon-label" data-label="moreTextOptions"></span>
1233
+ </summary>
1234
+ <div class="customforge-workbench__text-options-panel">
1235
+ <div class="customforge-workbench__text-options-row">
1236
+ <button class="customforge-workbench__format-button" data-action="edit-text" data-control-label="editText" type="button">
1237
+ <i data-customforge-icon="text-cursor-input" data-icon-slot="editText"></i>
1238
+ <span class="customforge-workbench__icon-label" data-label="editText"></span>
1239
+ </button>
1240
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1241
+ <div class="customforge-workbench__format-group" role="group">
1242
+ <button class="customforge-workbench__format-button" data-text-align="left" data-control-label="alignLeft" type="button" aria-pressed="false">
1243
+ <i data-customforge-icon="align-left" data-icon-slot="alignLeft"></i>
1244
+ <span class="customforge-workbench__icon-label" data-label="alignLeft"></span>
1245
+ </button>
1246
+ <button class="customforge-workbench__format-button" data-text-align="center" data-control-label="alignCenter" type="button" aria-pressed="false">
1247
+ <i data-customforge-icon="align-center" data-icon-slot="alignCenter"></i>
1248
+ <span class="customforge-workbench__icon-label" data-label="alignCenter"></span>
1249
+ </button>
1250
+ <button class="customforge-workbench__format-button" data-text-align="right" data-control-label="alignRight" type="button" aria-pressed="false">
1251
+ <i data-customforge-icon="align-right" data-icon-slot="alignRight"></i>
1252
+ <span class="customforge-workbench__icon-label" data-label="alignRight"></span>
1253
+ </button>
1254
+ </div>
1255
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1256
+ <label class="customforge-workbench__format-color" data-control-label="textColor">
1257
+ <i data-customforge-icon="palette" data-icon-slot="textColor"></i>
1258
+ <input data-role="text-format-color" data-control-label="textColor" type="color" value="#172126">
1259
+ </label>
1260
+ <div class="customforge-workbench__format-group" role="group">
1261
+ <button class="customforge-workbench__format-button" data-action="toggle-text-background" data-control-label="textBackground" type="button" aria-pressed="false">
1262
+ <i data-customforge-icon="highlighter" data-icon-slot="textBackground"></i>
1263
+ <span class="customforge-workbench__icon-label" data-label="textBackground"></span>
1264
+ </button>
1265
+ <input class="customforge-workbench__background-color" data-role="text-background-color" data-control-label="textBackground" type="color" value="#fff2a8">
1266
+ </div>
1267
+ </div>
1268
+ <div class="customforge-workbench__text-color-palette" data-role="text-color-palette" data-region-label="textColor" role="group"></div>
1269
+ <div class="customforge-workbench__text-options-row customforge-workbench__text-options-row--metrics">
1270
+ <label class="customforge-workbench__format-metric" data-control-label="lineHeight">
1271
+ <i data-customforge-icon="rows-3" data-icon-slot="lineHeight"></i>
1272
+ <input data-role="text-line-height" data-control-label="lineHeight" type="number" min="0.5" max="5" step="0.1" inputmode="decimal">
1273
+ </label>
1274
+ <label class="customforge-workbench__format-metric" data-control-label="letterSpacing">
1275
+ <i data-customforge-icon="stretch-horizontal" data-icon-slot="letterSpacing"></i>
1276
+ <input data-role="text-letter-spacing" data-control-label="letterSpacing" type="number" min="-500" max="2000" step="10" inputmode="numeric">
1277
+ </label>
1278
+ </div>
1279
+ </div>
1280
+ </details>
1281
+ </div>
1282
+
1283
+ <div class="customforge-workbench__image-toolbar" data-role="image-toolbar" data-region-label="setImageAsBackground" role="group" hidden>
1284
+ <span class="customforge-workbench__format-separator" aria-hidden="true"></span>
1285
+ <button class="customforge-workbench__tool-button" data-action="set-image-as-background" data-control-label="setImageAsBackground" type="button">
1286
+ <i data-customforge-icon="stretch-horizontal" data-icon-slot="setImageAsBackground"></i>
1287
+ <span data-label="setImageAsBackground"></span>
1288
+ </button>
1289
+ </div>
1290
+ </div>
1291
+
1292
+ <div class="customforge-workbench__editor-body">
1293
+ <div class="customforge-workbench__editor-stage" data-role="editor-host"></div>
1294
+ <aside class="customforge-workbench__layers" data-layout="layers" data-region-label="layers">
1295
+ <header class="customforge-workbench__layers-header">
1296
+ <span>
1297
+ <i data-customforge-icon="layers-3" data-icon-slot="layers"></i>
1298
+ <span data-label="layers"></span>
1299
+ </span>
1300
+ </header>
1301
+ <ol class="customforge-workbench__layer-list" data-role="layer-list"></ol>
1302
+ </aside>
1303
+ </div>
1304
+ </section>
1305
+
1306
+ <section class="customforge-workbench__panel customforge-workbench__panel--viewer" data-region-label="viewerTitle">
1307
+ <header class="customforge-workbench__panelbar" data-layout="viewerHeader">
1308
+ <div class="customforge-workbench__panel-title">
1309
+ <span class="customforge-workbench__panel-kicker" data-label="viewerMode"></span>
1310
+ <h2 data-label="viewerTitle"></h2>
1311
+ </div>
1312
+ <button class="customforge-workbench__icon-button" data-action="reset-view" data-feature="resetView" data-control-label="resetView" type="button">
1313
+ <i data-customforge-icon="rotate-ccw" data-icon-slot="resetView"></i>
1314
+ <span class="customforge-workbench__icon-label" data-label="resetView"></span>
1315
+ </button>
1316
+ </header>
1317
+ <div class="customforge-workbench__viewer-stage">
1318
+ <div class="customforge-workbench__viewer-host" data-role="viewer-host"></div>
1319
+ <div class="customforge-workbench__status" data-layout="status">
1320
+ <span class="customforge-workbench__status-indicator" aria-hidden="true"></span>
1321
+ <span data-role="status-label" role="status" data-label="starting"></span>
1322
+ </div>
1323
+ </div>
1324
+ </section>
1325
+ </section>
1326
+
1327
+ <input data-role="image-input" type="file" accept="image/png,image/jpeg,image/webp" hidden>
1328
+ <input data-role="design-input" type="file" accept="application/json,.json" hidden>
1329
+
1330
+ <dialog class="customforge-workbench__dialog customforge-workbench__dialog--text" data-role="text-dialog">
1331
+ <form data-role="text-form">
1332
+ <header class="customforge-workbench__dialog-header">
1333
+ <div>
1334
+ <span class="customforge-workbench__eyebrow" data-label="textDialogEyebrow"></span>
1335
+ <h2 data-label="textDialogTitle"></h2>
1336
+ </div>
1337
+ <button class="customforge-workbench__icon-button" data-action="close-text-dialog" data-control-label="close" type="button">
1338
+ <i data-customforge-icon="x" data-icon-slot="close"></i>
1339
+ <span class="customforge-workbench__icon-label" data-label="close"></span>
1340
+ </button>
1341
+ </header>
1342
+ <div class="customforge-workbench__dialog-body">
1343
+ <label class="customforge-workbench__field customforge-workbench__field--prominent">
1344
+ <span data-label="textInputLabel"></span>
1345
+ <input data-role="text-value" type="text" maxlength="160" data-placeholder-label="textInputPlaceholder" required>
1346
+ </label>
1347
+ <fieldset class="customforge-workbench__fieldset">
1348
+ <legend data-label="textPresets"></legend>
1349
+ <div class="customforge-workbench__preset-grid customforge-workbench__preset-grid--text" data-role="text-presets"></div>
1350
+ </fieldset>
1351
+ <label class="customforge-workbench__field customforge-workbench__color-field">
1352
+ <span data-label="textColor"></span>
1353
+ <input data-role="text-color" type="color" value="#17191c">
1354
+ </label>
1355
+ </div>
1356
+ <footer class="customforge-workbench__dialog-actions">
1357
+ <button class="customforge-workbench__button customforge-workbench__button--secondary" data-action="close-text-dialog" data-label="cancel" type="button"></button>
1358
+ <button class="customforge-workbench__button customforge-workbench__button--primary" data-action="submit-text" data-label="addTextConfirm" type="submit"></button>
1359
+ </footer>
1360
+ </form>
1361
+ </dialog>
1362
+
1363
+ <dialog class="customforge-workbench__dialog customforge-workbench__dialog--image" data-role="image-dialog">
1364
+ <form data-role="image-form">
1365
+ <header class="customforge-workbench__dialog-header">
1366
+ <div>
1367
+ <span class="customforge-workbench__eyebrow" data-label="imageDialogEyebrow"></span>
1368
+ <h2 data-label="imageDialogTitle"></h2>
1369
+ </div>
1370
+ <button class="customforge-workbench__icon-button" data-action="close-image-dialog" data-control-label="close" type="button">
1371
+ <i data-customforge-icon="x" data-icon-slot="close"></i>
1372
+ <span class="customforge-workbench__icon-label" data-label="close"></span>
1373
+ </button>
1374
+ </header>
1375
+ <div class="customforge-workbench__dialog-body">
1376
+ <div class="customforge-workbench__tabs" role="tablist">
1377
+ <button data-image-tab="upload" data-label="uploadImageTab" type="button" role="tab"></button>
1378
+ <button data-image-tab="backgrounds" data-feature="presetBackgrounds" data-label="backgroundsTab" type="button" role="tab"></button>
1379
+ <button data-image-tab="elements" data-feature="presetElements" data-label="elementsTab" type="button" role="tab"></button>
1380
+ </div>
1381
+ <section class="customforge-workbench__tab-panel" data-image-panel="upload" role="tabpanel">
1382
+ <button class="customforge-workbench__upload-field" data-action="choose-image" type="button">
1383
+ <span class="customforge-workbench__upload-icon"><i data-customforge-icon="upload" data-icon-slot="upload"></i></span>
1384
+ <strong data-label="chooseImage"></strong>
1385
+ <span data-role="image-file-name" data-label="noImageSelected"></span>
1386
+ <img data-role="image-preview" alt="" hidden>
1387
+ </button>
1388
+ </section>
1389
+ <section class="customforge-workbench__tab-panel" data-image-panel="backgrounds" role="tabpanel" hidden>
1390
+ <div class="customforge-workbench__preset-grid customforge-workbench__preset-grid--assets" data-role="background-presets"></div>
1391
+ </section>
1392
+ <section class="customforge-workbench__tab-panel" data-image-panel="elements" role="tabpanel" hidden>
1393
+ <div class="customforge-workbench__preset-grid customforge-workbench__preset-grid--assets" data-role="element-presets"></div>
1394
+ </section>
1395
+ </div>
1396
+ <footer class="customforge-workbench__dialog-actions">
1397
+ <button class="customforge-workbench__button customforge-workbench__button--secondary" data-action="close-image-dialog" data-label="cancel" type="button"></button>
1398
+ <button class="customforge-workbench__button customforge-workbench__button--primary" data-action="submit-image" data-label="addImageConfirm" type="submit" disabled></button>
1399
+ </footer>
1400
+ </form>
1401
+ </dialog>
1402
+
1403
+ <dialog class="customforge-workbench__dialog" data-role="product-dialog">
1404
+ <form data-role="remote-form">
1405
+ <header class="customforge-workbench__dialog-header">
1406
+ <div>
1407
+ <span class="customforge-workbench__eyebrow" data-label="productDialogEyebrow"></span>
1408
+ <h2 data-label="productDialogTitle"></h2>
1409
+ </div>
1410
+ <button class="customforge-workbench__icon-button" data-action="close-product-dialog" data-control-label="close" type="button">
1411
+ <i data-customforge-icon="x" data-icon-slot="close"></i>
1412
+ <span class="customforge-workbench__icon-label" data-label="close"></span>
1413
+ </button>
1414
+ </header>
1415
+ <div class="customforge-workbench__dialog-body customforge-workbench__product-form">
1416
+ <div class="customforge-workbench__product-source-switch" data-region-label="productSourceMethod" role="group">
1417
+ <button data-product-source="file" data-label="productFileSource" type="button" aria-pressed="true"></button>
1418
+ <button data-product-source="url" data-label="productUrlSource" type="button" aria-pressed="false"></button>
1419
+ </div>
1420
+
1421
+ <section data-product-source-panel="file">
1422
+ <label class="customforge-workbench__field">
1423
+ <span data-label="modelFile"></span>
1424
+ <span class="customforge-workbench__product-file-picker">
1425
+ <input data-role="model-file" name="modelFile" type="file" accept=".glb,model/gltf-binary" required>
1426
+ <span class="customforge-workbench__product-file-command" data-label="chooseModelFile"></span>
1427
+ <small data-role="model-file-name" data-label="noModelFileSelected"></small>
1428
+ </span>
1429
+ </label>
1430
+ </section>
1431
+
1432
+ <section data-product-source-panel="url" hidden>
1433
+ <label class="customforge-workbench__field">
1434
+ <span data-label="modelUrl"></span>
1435
+ <div class="customforge-workbench__input-shell">
1436
+ <i data-customforge-icon="link-2" data-icon-slot="loadProduct"></i>
1437
+ <input data-role="model-url" name="modelUrl" type="url" data-placeholder-label="modelUrlPlaceholder" disabled>
1438
+ </div>
1439
+ </label>
1440
+ </section>
1441
+
1442
+ <details class="customforge-workbench__product-options">
1443
+ <summary>
1444
+ <span data-label="advancedProductOptions"></span>
1445
+ <i data-customforge-icon="chevron-down"></i>
1446
+ </summary>
1447
+ <div class="customforge-workbench__product-options-fields">
1448
+ <label class="customforge-workbench__field">
1449
+ <span data-label="textureUrl"></span>
1450
+ <div class="customforge-workbench__input-shell">
1451
+ <i data-customforge-icon="image-plus" data-icon-slot="addImage"></i>
1452
+ <input data-role="texture-url" name="textureUrl" type="url" data-placeholder-label="textureUrlPlaceholder">
1453
+ </div>
1454
+ <small data-label="textureUrlHint"></small>
1455
+ </label>
1456
+ <label class="customforge-workbench__field">
1457
+ <span data-label="surfaceMesh"></span>
1458
+ <input data-role="mesh-name" name="meshName" type="text" value="PrintArea" required>
1459
+ <small data-label="surfaceMeshHint"></small>
1460
+ </label>
1461
+ <label class="customforge-workbench__check-field">
1462
+ <input data-role="flip-texture" name="flipTexture" type="checkbox">
1463
+ <span>
1464
+ <strong data-label="flipTexture"></strong>
1465
+ <small data-label="flipTextureHint"></small>
1466
+ </span>
1467
+ </label>
1468
+ </div>
1469
+ </details>
1470
+ </div>
1471
+ <footer class="customforge-workbench__dialog-actions">
1472
+ <button class="customforge-workbench__button customforge-workbench__button--secondary" data-action="use-demo" data-label="useDemo" type="button"></button>
1473
+ <button class="customforge-workbench__button customforge-workbench__button--primary" data-action="submit-product" data-control-label="loadProduct" type="submit">
1474
+ <i data-customforge-icon="upload" data-icon-slot="upload"></i>
1475
+ <span data-label="loadProduct"></span>
1476
+ </button>
1477
+ </footer>
1478
+ </form>
1479
+ </dialog>
1480
+ `;
1481
+ return root;
1482
+ }
1483
+ //#endregion
1484
+ //#region src/workbench/CustomForgeWorkbench.ts
1485
+ var DIALOG_CLOSE_DELAY_MS = 180;
1486
+ var EXTENSION_SLOTS = [
1487
+ "globalActions",
1488
+ "editorToolbar",
1489
+ "selectionToolbar"
1490
+ ];
1491
+ var TEXT_COLOR_PRESETS = [
1492
+ {
1493
+ label: "colorBlack",
1494
+ value: "#17191c"
1495
+ },
1496
+ {
1497
+ label: "colorSlate",
1498
+ value: "#475467"
1499
+ },
1500
+ {
1501
+ label: "colorGray",
1502
+ value: "#98a2b3"
1503
+ },
1504
+ {
1505
+ label: "colorWhite",
1506
+ value: "#ffffff"
1507
+ },
1508
+ {
1509
+ label: "colorRed",
1510
+ value: "#d92d20"
1511
+ },
1512
+ {
1513
+ label: "colorOrange",
1514
+ value: "#f79009"
1515
+ },
1516
+ {
1517
+ label: "colorYellow",
1518
+ value: "#fdb022"
1519
+ },
1520
+ {
1521
+ label: "colorGreen",
1522
+ value: "#12b76a"
1523
+ },
1524
+ {
1525
+ label: "colorTeal",
1526
+ value: "#0e9384"
1527
+ },
1528
+ {
1529
+ label: "colorCyan",
1530
+ value: "#06aed4"
1531
+ },
1532
+ {
1533
+ label: "colorBlue",
1534
+ value: "#0875c9"
1535
+ },
1536
+ {
1537
+ label: "colorPurple",
1538
+ value: "#7f56d9"
1539
+ },
1540
+ {
1541
+ label: "colorPink",
1542
+ value: "#c11574"
1543
+ },
1544
+ {
1545
+ label: "colorBrown",
1546
+ value: "#854a0e"
1547
+ }
1548
+ ];
1549
+ function requiredElement(root, selector) {
1550
+ const element = root.querySelector(selector);
1551
+ if (!element) throw new Error(`Workbench element was not found: ${selector}`);
1552
+ return element;
1553
+ }
1554
+ function downloadJson(filename, value) {
1555
+ const blob = new Blob([JSON.stringify(value, null, 2)], { type: "application/json" });
1556
+ const url = URL.createObjectURL(blob);
1557
+ const anchor = document.createElement("a");
1558
+ anchor.download = filename;
1559
+ anchor.href = url;
1560
+ anchor.click();
1561
+ window.setTimeout(() => URL.revokeObjectURL(url), 0);
1562
+ }
1563
+ function applyLabels(element, labels) {
1564
+ element.setAttribute("aria-label", labels.workbench);
1565
+ element.querySelectorAll("[data-label]").forEach((target) => {
1566
+ target.textContent = labels[target.dataset.label];
1567
+ });
1568
+ element.querySelectorAll("[data-placeholder-label]").forEach((target) => {
1569
+ target.placeholder = labels[target.dataset.placeholderLabel];
1570
+ });
1571
+ element.querySelectorAll("[data-control-label]").forEach((target) => {
1572
+ const key = target.dataset.controlLabel;
1573
+ const label = labels[key] || String(key);
1574
+ target.title = label;
1575
+ target.setAttribute("aria-label", label);
1576
+ });
1577
+ element.querySelectorAll("[data-region-label]").forEach((target) => {
1578
+ const key = target.dataset.regionLabel;
1579
+ target.setAttribute("aria-label", labels[key] || String(key));
1580
+ });
1581
+ }
1582
+ function applyBranding(element, options) {
1583
+ const logo = requiredElement(element, "[data-role=\"brand-logo\"]");
1584
+ const title = requiredElement(element, "[data-role=\"brand-title\"]");
1585
+ const subtitle = requiredElement(element, "[data-role=\"brand-subtitle\"]");
1586
+ const copy = requiredElement(element, "[data-role=\"brand-copy\"]");
1587
+ const brand = requiredElement(element, "[data-role=\"brand\"]");
1588
+ logo.src = options.logoUrl;
1589
+ logo.alt = options.logoAlt;
1590
+ logo.hidden = !options.showLogo;
1591
+ title.textContent = options.title;
1592
+ title.hidden = !options.showTitle || options.title.length === 0;
1593
+ subtitle.textContent = options.subtitle;
1594
+ subtitle.hidden = !options.showSubtitle || options.subtitle.length === 0;
1595
+ copy.hidden = title.hidden && subtitle.hidden;
1596
+ brand.hidden = logo.hidden && copy.hidden;
1597
+ brand.setAttribute("aria-label", options.title || options.logoAlt);
1598
+ }
1599
+ function applyTheme(element, theme) {
1600
+ element.style.setProperty("--cfw-ink", theme.ink);
1601
+ element.style.setProperty("--cfw-muted", theme.muted);
1602
+ element.style.setProperty("--cfw-line", theme.border);
1603
+ element.style.setProperty("--cfw-surface", theme.surface);
1604
+ element.style.setProperty("--cfw-surface-muted", theme.surfaceMuted);
1605
+ element.style.setProperty("--cfw-stage", theme.stage);
1606
+ element.style.setProperty("--cfw-accent", theme.accent);
1607
+ element.style.setProperty("--cfw-accent-hover", theme.accentHover);
1608
+ element.style.setProperty("--cfw-accent-contrast", theme.accentContrast);
1609
+ element.style.setProperty("--cfw-danger", theme.danger);
1610
+ element.style.setProperty("--cfw-font-family", theme.fontFamily);
1611
+ element.style.setProperty("--cfw-control-radius", theme.controlRadius);
1612
+ }
1613
+ function formatCount(labels, count) {
1614
+ return (count === 1 ? labels.objectCountOne : labels.objectCountMany).replace("{count}", String(count));
1615
+ }
1616
+ /**
1617
+ * 提供可配置默认界面的产品定制工作台
1618
+ *
1619
+ * 功能和布局开关只控制 Workbench 自带界面
1620
+ * 底层 ProductCustomizer 仍通过 customizer 属性提供完整公共 API
1621
+ * 不再使用实例时必须调用 destroy
1622
+ */
1623
+ var CustomForgeWorkbench = class CustomForgeWorkbench {
1624
+ /** Workbench 使用的底层无界面定制器 */
1625
+ customizer;
1626
+ /** Workbench 根元素 */
1627
+ element;
1628
+ host;
1629
+ abortController = new AbortController();
1630
+ dialogCloseTimers = /* @__PURE__ */ new Map();
1631
+ unsubscribe = [];
1632
+ features;
1633
+ layout;
1634
+ theme;
1635
+ labels;
1636
+ icons;
1637
+ textPresets;
1638
+ fontFamilies;
1639
+ backgrounds;
1640
+ elements;
1641
+ extensions = /* @__PURE__ */ new Map();
1642
+ pendingExtensions = /* @__PURE__ */ new Map();
1643
+ formatError;
1644
+ layerList;
1645
+ deleteButton;
1646
+ undoButton;
1647
+ redoButton;
1648
+ layersToggle;
1649
+ statusLabel;
1650
+ imageInput;
1651
+ designInput;
1652
+ loadDesignButton;
1653
+ textDialog;
1654
+ textForm;
1655
+ textToolbar;
1656
+ imageToolbar;
1657
+ textOptions;
1658
+ textFontFamily;
1659
+ textFontSize;
1660
+ textColor;
1661
+ textBackgroundColor;
1662
+ textLineHeight;
1663
+ textLetterSpacing;
1664
+ imageDialog;
1665
+ imageForm;
1666
+ imageSubmitButton;
1667
+ productDialog;
1668
+ remoteForm;
1669
+ submitProductButton;
1670
+ modelFileInput;
1671
+ modelUrlInput;
1672
+ selectedObjectIds = /* @__PURE__ */ new Set();
1673
+ selectedTextPresetId;
1674
+ selectedImage;
1675
+ activeImageTab = "upload";
1676
+ productSourceMode = "file";
1677
+ activeProductObjectUrl;
1678
+ layersExpanded = false;
1679
+ layerSignature = "";
1680
+ draggedObjectId;
1681
+ destroyed = false;
1682
+ constructor(host, element, customizer, options) {
1683
+ this.host = host;
1684
+ this.element = element;
1685
+ this.customizer = customizer;
1686
+ this.features = options.features;
1687
+ this.layout = options.layout;
1688
+ this.theme = options.theme;
1689
+ this.labels = options.labels;
1690
+ this.icons = options.icons;
1691
+ this.textPresets = options.textPresets;
1692
+ this.fontFamilies = options.fontFamilies;
1693
+ this.backgrounds = options.backgrounds;
1694
+ this.elements = options.elements;
1695
+ options.extensions.forEach((extension) => {
1696
+ this.extensions.set(extension.id, extension);
1697
+ });
1698
+ this.formatError = options.formatError;
1699
+ this.selectedTextPresetId = this.textPresets[0]?.id;
1700
+ this.layerList = requiredElement(element, "[data-role=\"layer-list\"]");
1701
+ this.deleteButton = this.action("delete-selection");
1702
+ this.undoButton = this.action("undo");
1703
+ this.redoButton = this.action("redo");
1704
+ this.layersToggle = this.action("toggle-layers");
1705
+ this.statusLabel = requiredElement(element, "[data-role=\"status-label\"]");
1706
+ this.imageInput = requiredElement(element, "[data-role=\"image-input\"]");
1707
+ this.designInput = requiredElement(element, "[data-role=\"design-input\"]");
1708
+ this.loadDesignButton = this.action("load-design");
1709
+ this.textDialog = requiredElement(element, "[data-role=\"text-dialog\"]");
1710
+ this.textForm = requiredElement(element, "[data-role=\"text-form\"]");
1711
+ this.textToolbar = requiredElement(element, "[data-role=\"text-toolbar\"]");
1712
+ this.imageToolbar = requiredElement(element, "[data-role=\"image-toolbar\"]");
1713
+ this.textOptions = requiredElement(element, "[data-role=\"text-options\"]");
1714
+ this.textFontFamily = requiredElement(element, "[data-role=\"text-font-family\"]");
1715
+ this.textFontSize = requiredElement(element, "[data-role=\"text-font-size\"]");
1716
+ this.textColor = requiredElement(element, "[data-role=\"text-format-color\"]");
1717
+ this.textBackgroundColor = requiredElement(element, "[data-role=\"text-background-color\"]");
1718
+ this.textLineHeight = requiredElement(element, "[data-role=\"text-line-height\"]");
1719
+ this.textLetterSpacing = requiredElement(element, "[data-role=\"text-letter-spacing\"]");
1720
+ this.imageDialog = requiredElement(element, "[data-role=\"image-dialog\"]");
1721
+ this.imageForm = requiredElement(element, "[data-role=\"image-form\"]");
1722
+ this.imageSubmitButton = this.action("submit-image");
1723
+ this.productDialog = requiredElement(element, "[data-role=\"product-dialog\"]");
1724
+ this.remoteForm = requiredElement(element, "[data-role=\"remote-form\"]");
1725
+ this.submitProductButton = this.action("submit-product");
1726
+ this.modelFileInput = requiredElement(element, "[data-role=\"model-file\"]");
1727
+ this.modelUrlInput = requiredElement(element, "[data-role=\"model-url\"]");
1728
+ this.renderTextFontOptions();
1729
+ this.renderTextPresets();
1730
+ this.renderTextColorPresets();
1731
+ this.renderAssetPresets();
1732
+ this.renderExtensionSlots();
1733
+ this.bindEvents();
1734
+ this.applyVisibility();
1735
+ this.setImageTab("upload");
1736
+ this.setProductSourceMode("file");
1737
+ this.updateHistoryButtons();
1738
+ this.renderLayers(true);
1739
+ this.updateTextToolbar();
1740
+ this.updateImageToolbar();
1741
+ this.setStatus(this.labels.productReady);
1742
+ }
1743
+ /**
1744
+ * 创建 Workbench 并完成底层定制器初始化
1745
+ *
1746
+ * @param options 挂载容器、产品资源、功能、外观和素材配置
1747
+ * @returns 初始化完成的 Workbench
1748
+ * @throws 挂载容器无效或产品初始化失败时清理已创建界面并继续抛出错误
1749
+ */
1750
+ static async create(options) {
1751
+ const host = resolveElement(options.container, "Workbench");
1752
+ const normalized = normalizeWorkbenchOptions(options);
1753
+ const element = createWorkbenchElement();
1754
+ if (normalized.classNames.length > 0) element.classList.add(...normalized.classNames);
1755
+ applyLabels(element, normalized.labels);
1756
+ applyBranding(element, normalized.branding);
1757
+ applyTheme(element, normalized.theme);
1758
+ requiredElement(element, "[data-role=\"resolution\"]").textContent = `${normalized.editorWidth} x ${normalized.editorHeight}`;
1759
+ host.replaceChildren(element);
1760
+ renderWorkbenchIcons(element, normalized.icons);
1761
+ try {
1762
+ const customizer = await ProductCustomizer.create({
1763
+ editor: requiredElement(element, "[data-role=\"editor-host\"]"),
1764
+ viewer: requiredElement(element, "[data-role=\"viewer-host\"]"),
1765
+ editorWidth: normalized.editorWidth,
1766
+ editorHeight: normalized.editorHeight,
1767
+ historyLimit: normalized.historyLimit,
1768
+ product: normalized.product,
1769
+ appearance: normalized.appearance,
1770
+ editorAriaLabel: normalized.labels.editorCanvas,
1771
+ viewerAriaLabel: normalized.labels.viewerCanvas,
1772
+ defaultText: normalized.labels.defaultText,
1773
+ textObjectName: normalized.labels.textObject,
1774
+ imageObjectName: normalized.labels.imageObject,
1775
+ backgroundObjectName: normalized.labels.backgroundObject
1776
+ });
1777
+ return new CustomForgeWorkbench(host, element, customizer, normalized);
1778
+ } catch (error) {
1779
+ host.replaceChildren();
1780
+ throw error;
1781
+ }
1782
+ }
1783
+ /** 返回当前默认功能控件开关的独立快照 */
1784
+ getFeatures() {
1785
+ return { ...this.features };
1786
+ }
1787
+ /**
1788
+ * 在运行时显示或隐藏某项内置功能控件
1789
+ *
1790
+ * 该方法不限制通过 customizer 属性直接调用对应核心 API
1791
+ *
1792
+ * @param feature 功能名称
1793
+ * @param enabled 是否显示并启用对应控件
1794
+ */
1795
+ setFeature(feature, enabled) {
1796
+ this.features[feature] = enabled;
1797
+ if (feature === "loadRemoteProduct" && !enabled && this.productDialog.open) this.closeDialog(this.productDialog);
1798
+ if (feature === "addText" && !enabled && this.textDialog.open) this.closeDialog(this.textDialog);
1799
+ if (feature === "addImage" && !enabled && this.imageDialog.open) this.closeDialog(this.imageDialog);
1800
+ if (feature === "presetBackgrounds" && this.activeImageTab === "backgrounds" || feature === "presetElements" && this.activeImageTab === "elements") this.setImageTab("upload");
1801
+ this.applyVisibility();
1802
+ this.renderLayers(true);
1803
+ this.updateTextToolbar();
1804
+ this.updateImageToolbar();
1805
+ }
1806
+ /** 返回当前默认布局区域开关的独立快照 */
1807
+ getLayout() {
1808
+ return { ...this.layout };
1809
+ }
1810
+ /**
1811
+ * 在运行时显示或隐藏 Workbench 布局区域
1812
+ *
1813
+ * @param section 布局区域名称
1814
+ * @param visible 是否显示该区域
1815
+ */
1816
+ setLayout(section, visible) {
1817
+ this.layout[section] = visible;
1818
+ this.applyVisibility();
1819
+ this.updateTextToolbar();
1820
+ this.updateImageToolbar();
1821
+ }
1822
+ /** 返回当前完整主题的独立快照 */
1823
+ getTheme() {
1824
+ return { ...this.theme };
1825
+ }
1826
+ /**
1827
+ * 合并并立即应用当前 Workbench 实例的主题变量
1828
+ *
1829
+ * @param theme 要覆盖的主题字段
1830
+ * @returns 合并后的完整主题独立快照
1831
+ */
1832
+ setTheme(theme) {
1833
+ this.theme = {
1834
+ ...this.theme,
1835
+ ...theme
1836
+ };
1837
+ applyTheme(this.element, this.theme);
1838
+ this.refreshExtensions();
1839
+ return this.getTheme();
1840
+ }
1841
+ /** 返回当前已注册扩展按钮的独立配置快照 */
1842
+ getExtensions() {
1843
+ return [...this.extensions.values()].map(({ classNames, ...extension }) => ({
1844
+ ...extension,
1845
+ className: classNames.length > 0 ? classNames.join(" ") : void 0
1846
+ }));
1847
+ }
1848
+ /**
1849
+ * 注册扩展按钮并立即渲染到声明位置
1850
+ *
1851
+ * @param extension 扩展按钮配置
1852
+ * @returns 仅在该注册仍为当前同 ID 项时将其移除的清理函数
1853
+ * @throws ID、位置、文案、图标或回调配置无效以及 ID 重复时抛出错误
1854
+ */
1855
+ registerExtension(extension) {
1856
+ const normalized = normalizeWorkbenchExtension(extension);
1857
+ if (this.extensions.has(normalized.id)) throw new TypeError(`Extension id is duplicated: ${normalized.id}`);
1858
+ this.extensions.set(normalized.id, normalized);
1859
+ this.renderRegisteredExtensions();
1860
+ return () => {
1861
+ if (this.extensions.get(normalized.id) === normalized) this.removeExtension(normalized.id);
1862
+ };
1863
+ }
1864
+ /** 按稳定 ID 移除扩展按钮并返回是否存在该注册 */
1865
+ removeExtension(id) {
1866
+ const normalizedId = id.trim();
1867
+ const extension = this.extensions.get(normalizedId);
1868
+ if (!extension) return false;
1869
+ this.extensions.delete(normalizedId);
1870
+ for (const [key, pendingExtension] of this.pendingExtensions) if (pendingExtension === extension) this.pendingExtensions.delete(key);
1871
+ this.renderRegisteredExtensions();
1872
+ return true;
1873
+ }
1874
+ /** 使用最新核心状态重新计算所有扩展按钮 */
1875
+ refreshExtensions() {
1876
+ if (this.destroyed) return;
1877
+ const state = this.customizer.getState();
1878
+ const objectsById = new Map(state.objects.map((object) => [object.id, object]));
1879
+ const selection = state.selectedObjectIds.map((id) => objectsById.get(id)).filter((object) => object !== void 0);
1880
+ this.element.querySelectorAll("[data-extension-id]").forEach((button) => {
1881
+ const extensionId = button.dataset.extensionId;
1882
+ const extension = extensionId ? this.extensions.get(extensionId) : void 0;
1883
+ if (!extension) {
1884
+ button.remove();
1885
+ return;
1886
+ }
1887
+ const layerId = button.dataset.extensionLayerId;
1888
+ const layer = layerId ? objectsById.get(layerId) : void 0;
1889
+ const context = {
1890
+ workbench: this,
1891
+ customizer: this.customizer,
1892
+ state,
1893
+ selection,
1894
+ layer
1895
+ };
1896
+ const visible = (extension.placement !== "selectionToolbar" || selection.length > 0) && (extension.placement !== "layerActions" || layer !== void 0) && this.resolveExtensionPredicate(extension.visible, context, false);
1897
+ const executionKey = this.extensionExecutionKey(extension.id, layer?.id);
1898
+ const pending = this.pendingExtensions.get(executionKey) === extension;
1899
+ button.hidden = !visible;
1900
+ button.disabled = !visible || pending || this.resolveExtensionPredicate(extension.disabled, context, true);
1901
+ button.dataset.loading = String(pending);
1902
+ button.setAttribute("aria-busy", String(pending));
1903
+ });
1904
+ EXTENSION_SLOTS.forEach((placement) => {
1905
+ const slot = this.extensionSlot(placement);
1906
+ slot.hidden = ![...slot.querySelectorAll("button")].some((button) => !button.hidden);
1907
+ });
1908
+ this.updateLayerActionWidths();
1909
+ this.updateRegionVisibility();
1910
+ }
1911
+ /**
1912
+ * 更新三维预览区域内的运行状态
1913
+ *
1914
+ * @param message 状态文字
1915
+ * @param mode 状态样式,默认为 ready
1916
+ */
1917
+ setStatus(message, mode = "ready") {
1918
+ this.statusLabel.textContent = message;
1919
+ this.statusLabel.title = message;
1920
+ this.element.dataset.status = mode;
1921
+ }
1922
+ /** 释放 DOM 事件、核心实例和 Workbench 挂载内容 */
1923
+ destroy() {
1924
+ if (this.destroyed) return;
1925
+ this.destroyed = true;
1926
+ this.releaseImageSelection();
1927
+ this.dialogCloseTimers.forEach((timer) => window.clearTimeout(timer));
1928
+ this.dialogCloseTimers.clear();
1929
+ this.abortController.abort();
1930
+ this.pendingExtensions.clear();
1931
+ this.unsubscribe.splice(0).forEach((stop) => stop());
1932
+ this.customizer.destroy();
1933
+ this.replaceActiveProductObjectUrl();
1934
+ this.host.replaceChildren();
1935
+ }
1936
+ bindEvents() {
1937
+ const signal = this.abortController.signal;
1938
+ this.unsubscribe.push(this.customizer.on("change", ({ objectCount }) => {
1939
+ this.updateObjectCount(objectCount);
1940
+ this.renderLayers();
1941
+ this.updateTextToolbar();
1942
+ this.updateImageToolbar();
1943
+ this.refreshExtensions();
1944
+ }), this.customizer.on("selectionchange", ({ objectIds }) => {
1945
+ this.selectedObjectIds = new Set(objectIds);
1946
+ this.deleteButton.disabled = objectIds.length === 0;
1947
+ this.renderLayers();
1948
+ this.updateTextToolbar();
1949
+ this.updateImageToolbar();
1950
+ this.refreshExtensions();
1951
+ }), this.customizer.on("historychange", () => {
1952
+ this.updateHistoryButtons();
1953
+ this.refreshExtensions();
1954
+ }), this.customizer.on("viewchange", () => this.refreshExtensions()), this.customizer.on("status", ({ message }) => {
1955
+ const localized = this.localizeStatus(message);
1956
+ if (localized) this.setStatus(localized);
1957
+ }), this.customizer.on("error", ({ error }) => this.setStatus(this.formatError(error), "error")));
1958
+ this.element.addEventListener("click", (event) => this.handleRootClick(event), { signal });
1959
+ this.element.addEventListener("keydown", (event) => {
1960
+ this.handleImageTabShortcut(event);
1961
+ this.handleHistoryShortcut(event);
1962
+ }, { signal });
1963
+ document.addEventListener("pointerdown", (event) => {
1964
+ const target = event.target;
1965
+ if (target instanceof Node && !this.textOptions.contains(target)) this.textOptions.open = false;
1966
+ }, { signal });
1967
+ document.addEventListener("keydown", (event) => {
1968
+ if (event.key === "Escape") this.textOptions.open = false;
1969
+ }, { signal });
1970
+ this.textForm.addEventListener("submit", (event) => this.addTextFromDialog(event), { signal });
1971
+ this.imageForm.addEventListener("submit", (event) => void this.addImageFromDialog(event), { signal });
1972
+ this.remoteForm.addEventListener("submit", (event) => void this.loadProductFromDialog(event), { signal });
1973
+ requiredElement(this.element, "[data-role=\"text-value\"]").addEventListener("input", (event) => {
1974
+ event.currentTarget.setCustomValidity("");
1975
+ this.updateTextPreviews();
1976
+ }, { signal });
1977
+ requiredElement(this.element, "[data-role=\"text-color\"]").addEventListener("input", () => this.updateTextPreviews(), { signal });
1978
+ this.textFontFamily.addEventListener("change", () => {
1979
+ if (this.textFontFamily.value) this.updateSelectedText({ fontFamily: this.textFontFamily.value });
1980
+ }, { signal });
1981
+ this.textFontSize.addEventListener("change", () => {
1982
+ const value = this.textFontSize.valueAsNumber;
1983
+ if (Number.isFinite(value)) this.updateSelectedText({ fontSize: value });
1984
+ }, { signal });
1985
+ const updateTextColor = () => {
1986
+ this.updateSelectedText({ color: this.textColor.value });
1987
+ };
1988
+ this.textColor.addEventListener("input", updateTextColor, { signal });
1989
+ this.textColor.addEventListener("change", updateTextColor, { signal });
1990
+ const updateTextBackgroundColor = () => {
1991
+ this.updateSelectedText({ backgroundColor: this.textBackgroundColor.value });
1992
+ };
1993
+ this.textBackgroundColor.addEventListener("input", updateTextBackgroundColor, { signal });
1994
+ this.textBackgroundColor.addEventListener("change", updateTextBackgroundColor, { signal });
1995
+ this.textLineHeight.addEventListener("change", () => {
1996
+ const value = this.textLineHeight.valueAsNumber;
1997
+ if (Number.isFinite(value)) this.updateSelectedText({ lineHeight: value });
1998
+ }, { signal });
1999
+ this.textLetterSpacing.addEventListener("change", () => {
2000
+ const value = this.textLetterSpacing.valueAsNumber;
2001
+ if (Number.isFinite(value)) this.updateSelectedText({ charSpacing: value });
2002
+ }, { signal });
2003
+ this.imageInput.addEventListener("change", () => this.selectUploadedImage(), { signal });
2004
+ this.modelFileInput.addEventListener("change", () => this.updateSelectedProductFile(), { signal });
2005
+ this.modelUrlInput.addEventListener("input", () => this.modelUrlInput.setCustomValidity(""), { signal });
2006
+ for (const selector of ["[data-role=\"texture-url\"]", "[data-role=\"mesh-name\"]"]) requiredElement(this.element, selector).addEventListener("input", (event) => {
2007
+ event.currentTarget.setCustomValidity("");
2008
+ }, { signal });
2009
+ this.designInput.addEventListener("change", () => void this.loadSelectedDesign(), { signal });
2010
+ this.imageDialog.addEventListener("close", () => this.resetImageSelection(), { signal });
2011
+ for (const dialog of [
2012
+ this.textDialog,
2013
+ this.imageDialog,
2014
+ this.productDialog
2015
+ ]) {
2016
+ dialog.addEventListener("cancel", (event) => {
2017
+ event.preventDefault();
2018
+ this.closeDialog(dialog);
2019
+ }, { signal });
2020
+ dialog.addEventListener("close", () => this.clearDialogClose(dialog), { signal });
2021
+ }
2022
+ this.layerList.addEventListener("dblclick", (event) => this.handleLayerRename(event), { signal });
2023
+ this.layerList.addEventListener("dragstart", (event) => this.handleLayerDragStart(event), { signal });
2024
+ this.layerList.addEventListener("dragover", (event) => this.handleLayerDragOver(event), { signal });
2025
+ this.layerList.addEventListener("drop", (event) => this.handleLayerDrop(event), { signal });
2026
+ this.layerList.addEventListener("dragend", () => this.clearLayerDragState(), { signal });
2027
+ window.addEventListener("beforeunload", () => this.destroy(), {
2028
+ once: true,
2029
+ signal
2030
+ });
2031
+ }
2032
+ handleRootClick(event) {
2033
+ const target = event.target;
2034
+ const extension = target.closest("[data-extension-id]");
2035
+ if (extension && !extension.disabled && !extension.hidden) {
2036
+ this.runExtension(extension, event);
2037
+ return;
2038
+ }
2039
+ const textColor = target.closest("[data-text-color]");
2040
+ if (textColor?.dataset.textColor) {
2041
+ this.setColorControl(this.textColor, textColor.dataset.textColor);
2042
+ this.updateSelectedText({ color: textColor.dataset.textColor });
2043
+ return;
2044
+ }
2045
+ const textStyle = target.closest("[data-text-style]");
2046
+ if (textStyle?.dataset.textStyle) {
2047
+ this.toggleSelectedTextStyle(textStyle.dataset.textStyle);
2048
+ return;
2049
+ }
2050
+ const textAlign = target.closest("[data-text-align]");
2051
+ if (textAlign?.dataset.textAlign) {
2052
+ this.updateSelectedText({ textAlign: textAlign.dataset.textAlign });
2053
+ return;
2054
+ }
2055
+ const layerAction = target.closest("[data-layer-action]");
2056
+ if (layerAction) {
2057
+ this.handleLayerAction(layerAction);
2058
+ return;
2059
+ }
2060
+ const textPreset = target.closest("[data-text-preset-id]");
2061
+ if (textPreset?.dataset.textPresetId) {
2062
+ this.selectTextPreset(textPreset.dataset.textPresetId);
2063
+ return;
2064
+ }
2065
+ const asset = target.closest("[data-asset-id]");
2066
+ if (asset?.dataset.assetId && asset.dataset.assetRole) {
2067
+ this.selectPresetAsset(asset.dataset.assetId, asset.dataset.assetRole);
2068
+ return;
2069
+ }
2070
+ const tab = target.closest("[data-image-tab]");
2071
+ if (tab?.dataset.imageTab) {
2072
+ this.setImageTab(tab.dataset.imageTab);
2073
+ return;
2074
+ }
2075
+ const productSource = target.closest("[data-product-source]");
2076
+ if (productSource?.dataset.productSource) {
2077
+ this.setProductSourceMode(productSource.dataset.productSource);
2078
+ return;
2079
+ }
2080
+ switch (target.closest("[data-action]")?.dataset.action) {
2081
+ case "open-text-dialog":
2082
+ this.openTextDialog();
2083
+ break;
2084
+ case "edit-text": {
2085
+ const selected = this.selectedTextObjects();
2086
+ if (selected?.length === 1) this.customizer.editText(selected[0].id);
2087
+ break;
2088
+ }
2089
+ case "toggle-text-background":
2090
+ this.toggleSelectedTextBackground();
2091
+ break;
2092
+ case "open-image-dialog":
2093
+ this.openImageDialog();
2094
+ break;
2095
+ case "set-image-as-background":
2096
+ this.setSelectedImageAsBackground();
2097
+ break;
2098
+ case "undo":
2099
+ this.runHistory("undo");
2100
+ break;
2101
+ case "redo":
2102
+ this.runHistory("redo");
2103
+ break;
2104
+ case "save-design":
2105
+ this.saveDesign();
2106
+ break;
2107
+ case "load-design":
2108
+ this.designInput.click();
2109
+ break;
2110
+ case "delete-selection":
2111
+ this.customizer.deleteSelected();
2112
+ break;
2113
+ case "toggle-layers":
2114
+ this.toggleLayers();
2115
+ break;
2116
+ case "reset-view":
2117
+ this.customizer.resetView();
2118
+ break;
2119
+ case "load-remote":
2120
+ this.showDialog(this.productDialog);
2121
+ break;
2122
+ case "close-text-dialog":
2123
+ this.closeDialog(this.textDialog);
2124
+ break;
2125
+ case "close-image-dialog":
2126
+ this.closeDialog(this.imageDialog);
2127
+ break;
2128
+ case "close-product-dialog":
2129
+ this.closeDialog(this.productDialog);
2130
+ break;
2131
+ case "choose-image":
2132
+ this.imageInput.click();
2133
+ break;
2134
+ case "use-demo": this.loadDemoProduct();
2135
+ }
2136
+ }
2137
+ handleHistoryShortcut(event) {
2138
+ if (!this.features.undoRedo || !event.ctrlKey && !event.metaKey || event.altKey) return;
2139
+ const target = event.target;
2140
+ if (target instanceof HTMLInputElement || target instanceof HTMLTextAreaElement || target instanceof HTMLSelectElement || target.isContentEditable) return;
2141
+ const key = event.key.toLowerCase();
2142
+ if (key === "z" && !event.shiftKey) {
2143
+ event.preventDefault();
2144
+ this.runHistory("undo");
2145
+ } else if (key === "z" && event.shiftKey || key === "y") {
2146
+ event.preventDefault();
2147
+ this.runHistory("redo");
2148
+ }
2149
+ }
2150
+ handleImageTabShortcut(event) {
2151
+ const current = event.target.closest("[data-image-tab]");
2152
+ if (!current || ![
2153
+ "ArrowLeft",
2154
+ "ArrowRight",
2155
+ "Home",
2156
+ "End"
2157
+ ].includes(event.key)) return;
2158
+ const tabs = Array.from(this.element.querySelectorAll("[data-image-tab]")).filter((tab) => !tab.hidden);
2159
+ const currentIndex = tabs.indexOf(current);
2160
+ if (currentIndex < 0 || tabs.length === 0) return;
2161
+ event.preventDefault();
2162
+ const next = tabs[event.key === "Home" ? 0 : event.key === "End" ? tabs.length - 1 : (currentIndex + (event.key === "ArrowRight" ? 1 : -1) + tabs.length) % tabs.length];
2163
+ if (next?.dataset.imageTab) {
2164
+ this.setImageTab(next.dataset.imageTab);
2165
+ next.focus();
2166
+ }
2167
+ }
2168
+ extensionSlot(placement) {
2169
+ return requiredElement(this.element, `[data-extension-slot="${placement}"]`);
2170
+ }
2171
+ extensionsAt(placement) {
2172
+ return [...this.extensions.values()].filter((extension) => extension.placement === placement).sort((left, right) => left.order - right.order);
2173
+ }
2174
+ renderExtensionSlots() {
2175
+ EXTENSION_SLOTS.forEach((placement) => {
2176
+ this.extensionSlot(placement).replaceChildren(...this.extensionsAt(placement).map((extension) => this.createExtensionButton(extension)));
2177
+ });
2178
+ }
2179
+ renderRegisteredExtensions() {
2180
+ this.renderExtensionSlots();
2181
+ this.renderLayers(true);
2182
+ }
2183
+ createExtensionButton(extension, layerId) {
2184
+ const button = document.createElement("button");
2185
+ button.type = "button";
2186
+ button.classList.add("customforge-workbench__extension-button", `customforge-workbench__extension-button--${extension.placement}`, `customforge-workbench__extension-button--${extension.variant}`, ...extension.classNames);
2187
+ if (extension.placement === "globalActions") button.classList.add("customforge-workbench__button");
2188
+ else if (extension.placement === "layerActions") button.classList.add("customforge-workbench__layer-action");
2189
+ else button.classList.add("customforge-workbench__tool-button");
2190
+ button.dataset.extensionId = extension.id;
2191
+ button.dataset.extensionPlacement = extension.placement;
2192
+ button.dataset.hasIcon = String(Boolean(extension.iconUrl && this.icons.enabled));
2193
+ if (layerId) button.dataset.extensionLayerId = layerId;
2194
+ button.title = extension.label;
2195
+ button.setAttribute("aria-label", extension.label);
2196
+ const showLabel = extension.showLabel || !this.icons.enabled;
2197
+ button.dataset.labelVisible = String(showLabel);
2198
+ if (extension.iconUrl && this.icons.enabled) {
2199
+ const icon = document.createElement("img");
2200
+ icon.className = "customforge-workbench__extension-icon";
2201
+ icon.src = extension.iconUrl;
2202
+ icon.alt = "";
2203
+ icon.setAttribute("aria-hidden", "true");
2204
+ button.append(icon);
2205
+ }
2206
+ const label = document.createElement("span");
2207
+ label.className = showLabel ? "customforge-workbench__extension-label" : "customforge-workbench__icon-label";
2208
+ label.textContent = extension.label;
2209
+ button.append(label);
2210
+ return button;
2211
+ }
2212
+ resolveExtensionPredicate(predicate, context, fallback) {
2213
+ try {
2214
+ return typeof predicate === "function" ? Boolean(predicate(context)) : predicate;
2215
+ } catch (error) {
2216
+ this.setStatus(this.formatError(error), "error");
2217
+ return fallback;
2218
+ }
2219
+ }
2220
+ extensionExecutionKey(id, layerId) {
2221
+ return layerId ? `${id}:${layerId}` : id;
2222
+ }
2223
+ updateLayerActionWidths() {
2224
+ this.layerList.querySelectorAll("[data-object-id]").forEach((item) => {
2225
+ const actions = item.querySelector(".customforge-workbench__layer-actions");
2226
+ if (!actions) return;
2227
+ const width = [...actions.children].filter((child) => child instanceof HTMLElement && !child.hidden).reduce((total, child) => total + child.getBoundingClientRect().width, 0);
2228
+ const visibleActionCount = [...actions.children].filter((child) => child instanceof HTMLElement && !child.hidden).length;
2229
+ item.style.setProperty("--cfw-layer-action-width", `${Math.ceil(width) + Math.max(visibleActionCount - 1, 0) + 7}px`);
2230
+ });
2231
+ }
2232
+ async runExtension(button, event) {
2233
+ const extensionId = button.dataset.extensionId;
2234
+ const extension = extensionId ? this.extensions.get(extensionId) : void 0;
2235
+ if (!extension || this.destroyed) return;
2236
+ const state = this.customizer.getState();
2237
+ const objectsById = new Map(state.objects.map((object) => [object.id, object]));
2238
+ const selection = state.selectedObjectIds.map((id) => objectsById.get(id)).filter((object) => object !== void 0);
2239
+ const layerId = button.dataset.extensionLayerId;
2240
+ const layer = layerId ? objectsById.get(layerId) : void 0;
2241
+ const context = {
2242
+ workbench: this,
2243
+ customizer: this.customizer,
2244
+ state,
2245
+ selection,
2246
+ layer
2247
+ };
2248
+ if (extension.placement === "selectionToolbar" && selection.length === 0 || extension.placement === "layerActions" && !layer || !this.resolveExtensionPredicate(extension.visible, context, false) || this.resolveExtensionPredicate(extension.disabled, context, true)) {
2249
+ this.refreshExtensions();
2250
+ return;
2251
+ }
2252
+ const executionKey = this.extensionExecutionKey(extension.id, layer?.id);
2253
+ if (this.pendingExtensions.get(executionKey) === extension) return;
2254
+ this.pendingExtensions.set(executionKey, extension);
2255
+ this.refreshExtensions();
2256
+ const actionContext = {
2257
+ ...context,
2258
+ event,
2259
+ anchor: button,
2260
+ signal: this.abortController.signal
2261
+ };
2262
+ try {
2263
+ await extension.onClick(actionContext);
2264
+ } catch (error) {
2265
+ if (!this.destroyed && !this.abortController.signal.aborted) this.setStatus(this.formatError(error), "error");
2266
+ } finally {
2267
+ if (this.pendingExtensions.get(executionKey) === extension) this.pendingExtensions.delete(executionKey);
2268
+ if (!this.destroyed) this.refreshExtensions();
2269
+ }
2270
+ }
2271
+ action(name) {
2272
+ return requiredElement(this.element, `[data-action="${name}"]`);
2273
+ }
2274
+ showDialog(dialog) {
2275
+ this.clearDialogClose(dialog);
2276
+ if (!dialog.open) dialog.showModal();
2277
+ }
2278
+ closeDialog(dialog) {
2279
+ if (!dialog.open || dialog.dataset.closing === "true") return;
2280
+ if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
2281
+ dialog.close();
2282
+ return;
2283
+ }
2284
+ dialog.dataset.closing = "true";
2285
+ const timer = window.setTimeout(() => {
2286
+ this.dialogCloseTimers.delete(dialog);
2287
+ delete dialog.dataset.closing;
2288
+ if (dialog.open) dialog.close();
2289
+ }, DIALOG_CLOSE_DELAY_MS);
2290
+ this.dialogCloseTimers.set(dialog, timer);
2291
+ }
2292
+ clearDialogClose(dialog) {
2293
+ const timer = this.dialogCloseTimers.get(dialog);
2294
+ if (timer !== void 0) {
2295
+ window.clearTimeout(timer);
2296
+ this.dialogCloseTimers.delete(dialog);
2297
+ }
2298
+ delete dialog.dataset.closing;
2299
+ }
2300
+ applyVisibility() {
2301
+ this.element.querySelectorAll("[data-feature]").forEach((element) => {
2302
+ const feature = element.dataset.feature;
2303
+ element.hidden = !this.features[feature];
2304
+ });
2305
+ this.element.querySelectorAll("[data-layout]").forEach((element) => {
2306
+ const section = element.dataset.layout;
2307
+ element.hidden = !this.layout[section];
2308
+ });
2309
+ const historyVisible = this.features.undoRedo;
2310
+ const insertVisible = this.features.addText || this.features.addImage;
2311
+ const documentVisible = this.features.saveDesign || this.features.loadDesign;
2312
+ const selectionVisible = this.features.deleteSelection;
2313
+ const layersVisible = this.layout.layers;
2314
+ this.toolGroup("history").hidden = !historyVisible;
2315
+ this.toolGroup("insert").hidden = !insertVisible;
2316
+ this.toolGroup("document").hidden = !documentVisible;
2317
+ this.toolGroup("history-separator").hidden = !historyVisible || !insertVisible && !documentVisible && !selectionVisible && !layersVisible;
2318
+ this.toolGroup("document-separator").hidden = !insertVisible || !documentVisible && !selectionVisible && !layersVisible;
2319
+ this.toolGroup("selection-separator").hidden = !selectionVisible || !historyVisible && !insertVisible && !documentVisible;
2320
+ this.toolGroup("layers-separator").hidden = !layersVisible || !historyVisible && !insertVisible && !documentVisible && !selectionVisible;
2321
+ this.element.dataset.toolbar = this.layout.toolbar ? "visible" : "hidden";
2322
+ this.element.dataset.layers = this.layout.layers ? "visible" : "hidden";
2323
+ this.element.dataset.layersExpanded = this.layout.layers && this.layersExpanded ? "true" : "false";
2324
+ this.layersToggle.hidden = !this.layout.layers;
2325
+ this.layersToggle.setAttribute("aria-expanded", String(this.layout.layers && this.layersExpanded));
2326
+ renderWorkbenchIcons(this.element, this.icons);
2327
+ this.updateTextToolbar();
2328
+ this.updateImageToolbar();
2329
+ this.refreshExtensions();
2330
+ }
2331
+ updateRegionVisibility() {
2332
+ const hasBuiltInToolbarControls = this.features.undoRedo || this.features.addText || this.features.addImage || this.features.saveDesign || this.features.loadDesign || this.features.deleteSelection || this.layout.layers;
2333
+ const hasToolbarExtensions = !this.extensionSlot("editorToolbar").hidden || !this.extensionSlot("selectionToolbar").hidden;
2334
+ const toolbar = requiredElement(this.element, "[data-layout=\"toolbar\"]");
2335
+ toolbar.hidden = !this.layout.toolbar || !hasBuiltInToolbarControls && !hasToolbarExtensions;
2336
+ const brandHidden = requiredElement(this.element, "[data-role=\"brand\"]").hidden;
2337
+ const hasGlobalActions = this.features.loadRemoteProduct || !this.extensionSlot("globalActions").hidden;
2338
+ const topbar = requiredElement(this.element, "[data-layout=\"header\"]");
2339
+ topbar.hidden = !this.layout.header || brandHidden && !hasGlobalActions;
2340
+ }
2341
+ toolGroup(name) {
2342
+ return requiredElement(this.element, `[data-tool-group="${name}"]`);
2343
+ }
2344
+ updateObjectCount(objectCount) {
2345
+ const formattedCount = formatCount(this.labels, objectCount);
2346
+ requiredElement(this.element, "[data-role=\"layer-count\"]").textContent = String(objectCount);
2347
+ const layersLabel = `${this.labels.layers}: ${formattedCount}`;
2348
+ this.layersToggle.title = layersLabel;
2349
+ this.layersToggle.setAttribute("aria-label", layersLabel);
2350
+ }
2351
+ toggleLayers() {
2352
+ if (!this.layout.layers) return;
2353
+ this.layersExpanded = !this.layersExpanded;
2354
+ this.element.dataset.layersExpanded = String(this.layersExpanded);
2355
+ this.layersToggle.setAttribute("aria-expanded", String(this.layersExpanded));
2356
+ this.refreshExtensions();
2357
+ }
2358
+ updateHistoryButtons() {
2359
+ this.undoButton.disabled = !this.customizer.canUndo();
2360
+ this.redoButton.disabled = !this.customizer.canRedo();
2361
+ }
2362
+ localizeStatus(message) {
2363
+ switch (message) {
2364
+ case "Loading design": return this.labels.loadingDesign;
2365
+ case "Design loaded": return this.labels.designLoaded;
2366
+ case "Loading product": return this.labels.loadingProduct;
2367
+ case "Remote product ready":
2368
+ case "Demo product ready": return this.labels.productReady;
2369
+ case "Undo complete": return this.labels.undoComplete;
2370
+ case "Redo complete": return this.labels.redoComplete;
2371
+ default: return;
2372
+ }
2373
+ }
2374
+ async runHistory(direction) {
2375
+ if (direction === "undo" && !this.customizer.canUndo() || direction === "redo" && !this.customizer.canRedo()) return;
2376
+ this.undoButton.disabled = true;
2377
+ this.redoButton.disabled = true;
2378
+ this.setStatus(direction === "undo" ? this.labels.undoing : this.labels.redoing, "busy");
2379
+ try {
2380
+ if (direction === "undo" ? await this.customizer.undo() : await this.customizer.redo()) this.setStatus(direction === "undo" ? this.labels.undoComplete : this.labels.redoComplete);
2381
+ } catch (error) {
2382
+ this.setStatus(this.formatError(error), "error");
2383
+ } finally {
2384
+ this.updateHistoryButtons();
2385
+ }
2386
+ }
2387
+ saveDesign() {
2388
+ try {
2389
+ downloadJson(this.labels.designFilename, this.customizer.saveDesign());
2390
+ this.setStatus(this.labels.designSaved);
2391
+ } catch (error) {
2392
+ this.setStatus(this.formatError(error), "error");
2393
+ }
2394
+ }
2395
+ renderTextFontOptions() {
2396
+ const fonts = new Map(this.fontFamilies.map((font) => [font.value, font.label]));
2397
+ this.textPresets.forEach((preset) => {
2398
+ if (!fonts.has(preset.fontFamily)) fonts.set(preset.fontFamily, preset.fontFamily);
2399
+ });
2400
+ const mixed = document.createElement("option");
2401
+ mixed.value = "";
2402
+ mixed.textContent = this.labels.mixedValue;
2403
+ mixed.disabled = true;
2404
+ this.textFontFamily.replaceChildren(mixed, ...Array.from(fonts).map(([fontFamily, label]) => {
2405
+ const option = document.createElement("option");
2406
+ option.value = fontFamily;
2407
+ option.textContent = label;
2408
+ option.style.fontFamily = fontFamily;
2409
+ return option;
2410
+ }));
2411
+ }
2412
+ ensureTextFontOption(fontFamily) {
2413
+ if (Array.from(this.textFontFamily.options).some((option) => option.value === fontFamily)) return;
2414
+ const option = document.createElement("option");
2415
+ option.value = fontFamily;
2416
+ option.textContent = fontFamily;
2417
+ option.style.fontFamily = fontFamily;
2418
+ this.textFontFamily.append(option);
2419
+ }
2420
+ selectedTextObjects() {
2421
+ if (this.selectedObjectIds.size === 0) return;
2422
+ const selected = this.customizer.getObjects().filter((object) => this.selectedObjectIds.has(object.id));
2423
+ if (selected.length !== this.selectedObjectIds.size || selected.some((object) => object.type !== "text")) return;
2424
+ return selected;
2425
+ }
2426
+ selectedImageObject() {
2427
+ if (this.selectedObjectIds.size !== 1) return;
2428
+ const [selectedId] = this.selectedObjectIds;
2429
+ const object = this.customizer.getObjects().find((entry) => entry.id === selectedId);
2430
+ return object?.type === "image" && object.role !== "background" ? object : void 0;
2431
+ }
2432
+ updateImageToolbar() {
2433
+ const object = this.selectedImageObject();
2434
+ const visible = this.layout.toolbar && this.features.addImage && object !== void 0;
2435
+ this.imageToolbar.hidden = !visible;
2436
+ this.action("set-image-as-background").disabled = !visible || Boolean(object?.locked);
2437
+ }
2438
+ setSelectedImageAsBackground() {
2439
+ const object = this.selectedImageObject();
2440
+ if (!object || object.locked) return;
2441
+ this.customizer.setImageAsBackground(object.id);
2442
+ }
2443
+ commonTextValue(objects, read) {
2444
+ const first = read(objects[0]);
2445
+ return objects.every((object) => Object.is(read(object), first)) ? first : void 0;
2446
+ }
2447
+ updateTextToolbar() {
2448
+ const objects = this.selectedTextObjects();
2449
+ const visible = this.layout.toolbar && this.features.textFormatting && objects !== void 0 && objects.length > 0;
2450
+ this.textToolbar.hidden = !visible;
2451
+ if (!visible || !objects) {
2452
+ this.textOptions.open = false;
2453
+ return;
2454
+ }
2455
+ const locked = objects.some((object) => object.locked ?? false);
2456
+ this.textToolbar.querySelectorAll("input, select, button").forEach((control) => {
2457
+ control.disabled = locked;
2458
+ });
2459
+ this.action("edit-text").disabled = locked || objects.length !== 1;
2460
+ const fontFamily = this.commonTextValue(objects, (object) => object.fontFamily);
2461
+ if (fontFamily) this.ensureTextFontOption(fontFamily);
2462
+ this.textFontFamily.value = fontFamily ?? "";
2463
+ this.setNumberControl(this.textFontSize, this.commonTextValue(objects, (object) => object.fontSize));
2464
+ this.setNumberControl(this.textLineHeight, this.commonTextValue(objects, (object) => object.lineHeight ?? 1.16));
2465
+ this.setNumberControl(this.textLetterSpacing, this.commonTextValue(objects, (object) => object.charSpacing ?? 0));
2466
+ const color = this.commonTextValue(objects, (object) => object.color);
2467
+ if (color) this.setColorControl(this.textColor, color);
2468
+ this.updateTextColorPresetState(color);
2469
+ const backgroundColor = this.commonTextValue(objects, (object) => object.backgroundColor ?? "");
2470
+ if (backgroundColor) this.setColorControl(this.textBackgroundColor, backgroundColor);
2471
+ this.setPressedState("[data-text-style=\"bold\"]", objects.map((object) => {
2472
+ const weight = object.fontWeight ?? 700;
2473
+ return weight === "bold" || typeof weight === "number" && weight >= 600;
2474
+ }));
2475
+ this.setPressedState("[data-text-style=\"italic\"]", objects.map((object) => (object.fontStyle ?? "normal") === "italic"));
2476
+ this.setPressedState("[data-text-style=\"underline\"]", objects.map((object) => object.underline ?? false));
2477
+ this.setPressedState("[data-action=\"toggle-text-background\"]", objects.map((object) => Boolean(object.backgroundColor)));
2478
+ for (const alignment of [
2479
+ "left",
2480
+ "center",
2481
+ "right"
2482
+ ]) this.setPressedState(`[data-text-align="${alignment}"]`, objects.map((object) => (object.textAlign ?? "center") === alignment));
2483
+ }
2484
+ setNumberControl(input, value) {
2485
+ input.value = value === void 0 ? "" : String(value);
2486
+ }
2487
+ setColorControl(input, value) {
2488
+ const shortHex = /^#([\da-f])([\da-f])([\da-f])$/i.exec(value);
2489
+ let normalizedValue;
2490
+ if (shortHex) normalizedValue = `#${shortHex[1]}${shortHex[1]}${shortHex[2]}${shortHex[2]}${shortHex[3]}${shortHex[3]}`;
2491
+ else if (/^#[\da-f]{6}$/i.test(value)) normalizedValue = value;
2492
+ if (normalizedValue !== void 0 && input.value.toLowerCase() !== normalizedValue.toLowerCase()) input.value = normalizedValue;
2493
+ }
2494
+ renderTextColorPresets() {
2495
+ const palette = requiredElement(this.textToolbar, "[data-role=\"text-color-palette\"]");
2496
+ const buttons = TEXT_COLOR_PRESETS.map((preset) => {
2497
+ const button = document.createElement("button");
2498
+ const label = `${this.labels.textColor}: ${this.labels[preset.label]}`;
2499
+ button.className = "customforge-workbench__text-color-swatch";
2500
+ button.type = "button";
2501
+ button.dataset.textColor = preset.value;
2502
+ button.style.setProperty("--cfw-text-swatch", preset.value);
2503
+ button.title = label;
2504
+ button.setAttribute("aria-label", label);
2505
+ button.setAttribute("aria-pressed", "false");
2506
+ return button;
2507
+ });
2508
+ palette.replaceChildren(...buttons);
2509
+ }
2510
+ updateTextColorPresetState(color) {
2511
+ this.textToolbar.querySelectorAll("[data-text-color]").forEach((button) => {
2512
+ button.setAttribute("aria-pressed", String(color !== void 0 && button.dataset.textColor?.toLowerCase() === color.toLowerCase()));
2513
+ });
2514
+ }
2515
+ setPressedState(selector, states) {
2516
+ const button = requiredElement(this.textToolbar, selector);
2517
+ const pressed = states.every(Boolean) ? "true" : states.some(Boolean) ? "mixed" : "false";
2518
+ button.setAttribute("aria-pressed", pressed);
2519
+ }
2520
+ updateSelectedText(options) {
2521
+ const objects = this.selectedTextObjects();
2522
+ if (!objects) return;
2523
+ try {
2524
+ objects.forEach((object) => this.customizer.updateText(object.id, options));
2525
+ this.updateTextToolbar();
2526
+ } catch (error) {
2527
+ this.setStatus(this.formatError(error), "error");
2528
+ }
2529
+ }
2530
+ toggleSelectedTextStyle(style) {
2531
+ const objects = this.selectedTextObjects();
2532
+ if (!objects) return;
2533
+ if (style === "bold") {
2534
+ const enabled = objects.every((object) => {
2535
+ const weight = object.fontWeight ?? 700;
2536
+ return weight === "bold" || typeof weight === "number" && weight >= 600;
2537
+ });
2538
+ this.updateSelectedText({ fontWeight: enabled ? "normal" : "bold" });
2539
+ } else if (style === "italic") {
2540
+ const enabled = objects.every((object) => (object.fontStyle ?? "normal") === "italic");
2541
+ this.updateSelectedText({ fontStyle: enabled ? "normal" : "italic" });
2542
+ } else {
2543
+ const enabled = objects.every((object) => object.underline ?? false);
2544
+ this.updateSelectedText({ underline: !enabled });
2545
+ }
2546
+ }
2547
+ toggleSelectedTextBackground() {
2548
+ const objects = this.selectedTextObjects();
2549
+ if (!objects) return;
2550
+ const enabled = objects.every((object) => Boolean(object.backgroundColor));
2551
+ this.updateSelectedText({ backgroundColor: enabled ? null : this.textBackgroundColor.value });
2552
+ }
2553
+ renderLayers(force = false) {
2554
+ const objects = this.customizer.getObjects();
2555
+ const signature = JSON.stringify({
2556
+ layers: objects.map((object) => [
2557
+ object.id,
2558
+ object.name,
2559
+ object.visible ?? true,
2560
+ object.locked ?? false,
2561
+ object.type === "image" ? object.role : void 0
2562
+ ]),
2563
+ selected: [...this.selectedObjectIds]
2564
+ });
2565
+ if (!force && signature === this.layerSignature) {
2566
+ this.updateObjectCount(objects.length);
2567
+ return;
2568
+ }
2569
+ this.layerSignature = signature;
2570
+ const backgroundCount = objects.filter((object) => object.type === "image" && object.role === "background").length;
2571
+ const items = objects.map((object, index) => this.createLayerItem(object, index, objects.length, backgroundCount)).reverse();
2572
+ if (items.length === 0) {
2573
+ const empty = document.createElement("li");
2574
+ empty.className = "customforge-workbench__layer-empty";
2575
+ empty.textContent = this.labels.noObjects;
2576
+ items.push(empty);
2577
+ }
2578
+ this.layerList.replaceChildren(...items);
2579
+ this.updateObjectCount(objects.length);
2580
+ this.applyVisibility();
2581
+ }
2582
+ createLayerItem(object, index, objectCount, backgroundCount) {
2583
+ const isBackground = object.type === "image" && object.role === "background";
2584
+ const typeLabel = object.type === "text" ? this.labels.textObject : isBackground ? this.labels.backgroundObject : this.labels.imageObject;
2585
+ const fallbackName = object.type === "text" ? object.text || this.labels.textObject : typeLabel;
2586
+ const item = document.createElement("li");
2587
+ item.className = "customforge-workbench__layer-item";
2588
+ item.dataset.objectId = object.id;
2589
+ item.dataset.visible = String(object.visible ?? true);
2590
+ item.dataset.locked = String(object.locked ?? false);
2591
+ item.dataset.selected = String(this.selectedObjectIds.has(object.id));
2592
+ item.dataset.role = isBackground ? "background" : "element";
2593
+ item.draggable = this.features.reorderObjects && !isBackground;
2594
+ const selectButton = document.createElement("button");
2595
+ selectButton.className = "customforge-workbench__layer-main";
2596
+ selectButton.type = "button";
2597
+ selectButton.dataset.layerAction = "select";
2598
+ selectButton.title = object.name || fallbackName;
2599
+ selectButton.setAttribute("aria-label", object.name || fallbackName);
2600
+ const icon = document.createElement("i");
2601
+ icon.dataset.customforgeIcon = object.type === "text" ? "type" : "image";
2602
+ icon.dataset.iconSlot = object.type === "text" ? "textObject" : isBackground ? "backgroundObject" : "imageObject";
2603
+ const copy = document.createElement("span");
2604
+ copy.className = "customforge-workbench__layer-copy";
2605
+ const name = document.createElement("strong");
2606
+ name.textContent = object.name || fallbackName;
2607
+ const type = document.createElement("span");
2608
+ type.textContent = typeLabel;
2609
+ copy.append(name, type);
2610
+ selectButton.append(icon, copy);
2611
+ const actions = document.createElement("span");
2612
+ actions.className = "customforge-workbench__layer-actions";
2613
+ actions.append(this.createLayerButton("visibility", object.visible === false ? "eye-off" : "eye", object.visible === false ? "showLayer" : "hideLayer", object.visible === false ? this.labels.showLayer : this.labels.hideLayer, "toggleObjectVisibility"), this.createLayerButton("lock", object.locked ? "lock" : "lock-open", object.locked ? "unlock" : "lock", object.locked ? this.labels.unlockLayer : this.labels.lockLayer, "lockObjects"), this.createLayerButton("forward", "arrow-up", "moveForward", this.labels.moveLayerForward, "reorderObjects", isBackground || index === objectCount - 1), this.createLayerButton("backward", "arrow-down", "moveBackward", this.labels.moveLayerBackward, "reorderObjects", isBackground || index <= backgroundCount), this.createLayerButton("remove", "trash-2", "deleteSelection", this.labels.deleteLayer, "deleteSelection"));
2614
+ actions.append(...this.extensionsAt("layerActions").map((extension) => this.createExtensionButton(extension, object.id)));
2615
+ item.append(selectButton, actions);
2616
+ return item;
2617
+ }
2618
+ createLayerButton(action, iconName, iconSlot, title, feature, disabled = false) {
2619
+ const button = document.createElement("button");
2620
+ button.className = "customforge-workbench__layer-action";
2621
+ button.type = "button";
2622
+ button.dataset.layerAction = action;
2623
+ button.dataset.feature = feature;
2624
+ button.title = title;
2625
+ button.setAttribute("aria-label", title);
2626
+ button.disabled = disabled;
2627
+ const icon = document.createElement("i");
2628
+ icon.dataset.customforgeIcon = iconName;
2629
+ icon.dataset.iconSlot = iconSlot;
2630
+ const label = document.createElement("span");
2631
+ label.className = "customforge-workbench__icon-label";
2632
+ label.textContent = title;
2633
+ button.append(icon, label);
2634
+ return button;
2635
+ }
2636
+ handleLayerAction(button) {
2637
+ const id = button.closest("[data-object-id]")?.dataset.objectId;
2638
+ if (!id) return;
2639
+ const object = this.customizer.getObjects().find((entry) => entry.id === id);
2640
+ if (!object) return;
2641
+ switch (button.dataset.layerAction) {
2642
+ case "select":
2643
+ this.customizer.selectObject(id);
2644
+ break;
2645
+ case "visibility":
2646
+ this.customizer.setObjectVisibility(id, object.visible === false);
2647
+ break;
2648
+ case "lock":
2649
+ this.customizer.setObjectLocked(id, !object.locked);
2650
+ break;
2651
+ case "forward":
2652
+ this.moveLayerBy(id, 1);
2653
+ break;
2654
+ case "backward":
2655
+ this.moveLayerBy(id, -1);
2656
+ break;
2657
+ case "remove": this.customizer.removeObject(id);
2658
+ }
2659
+ }
2660
+ handleLayerRename(event) {
2661
+ if (!this.features.renameObjects) return;
2662
+ const button = event.target.closest("[data-layer-action=\"select\"]");
2663
+ const id = (button?.closest("[data-object-id]"))?.dataset.objectId;
2664
+ const object = id ? this.customizer.getObjects().find((entry) => entry.id === id) : void 0;
2665
+ if (!button || !id || !object) return;
2666
+ const input = document.createElement("input");
2667
+ input.className = "customforge-workbench__layer-name-input";
2668
+ input.value = object.name || (object.type === "text" ? object.text : object.role === "background" ? this.labels.backgroundObject : this.labels.imageObject);
2669
+ input.maxLength = 80;
2670
+ button.replaceWith(input);
2671
+ input.focus();
2672
+ input.select();
2673
+ let completed = false;
2674
+ const finish = (save) => {
2675
+ if (completed) return;
2676
+ completed = true;
2677
+ if (save) this.customizer.renameObject(id, input.value);
2678
+ this.renderLayers(true);
2679
+ };
2680
+ input.addEventListener("blur", () => finish(true), { once: true });
2681
+ input.addEventListener("keydown", (keyboardEvent) => {
2682
+ if (keyboardEvent.key === "Enter") {
2683
+ keyboardEvent.preventDefault();
2684
+ finish(true);
2685
+ } else if (keyboardEvent.key === "Escape") {
2686
+ keyboardEvent.preventDefault();
2687
+ finish(false);
2688
+ }
2689
+ });
2690
+ }
2691
+ moveLayerBy(id, offset) {
2692
+ const currentIndex = this.customizer.getObjects().findIndex((object) => object.id === id);
2693
+ if (currentIndex >= 0 && this.customizer.moveObject(id, currentIndex + offset)) this.renderLayers(true);
2694
+ }
2695
+ handleLayerDragStart(event) {
2696
+ const item = event.target.closest("[data-object-id]");
2697
+ if (!this.features.reorderObjects || item?.dataset.role === "background") {
2698
+ event.preventDefault();
2699
+ return;
2700
+ }
2701
+ this.draggedObjectId = item?.dataset.objectId;
2702
+ if (!this.draggedObjectId) {
2703
+ event.preventDefault();
2704
+ return;
2705
+ }
2706
+ item?.classList.add("is-dragging");
2707
+ event.dataTransfer?.setData("text/plain", this.draggedObjectId);
2708
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
2709
+ }
2710
+ handleLayerDragOver(event) {
2711
+ if (!this.draggedObjectId) return;
2712
+ event.preventDefault();
2713
+ this.layerList.querySelectorAll(".is-drag-target").forEach((item) => item.classList.remove("is-drag-target"));
2714
+ event.target.closest("[data-object-id]")?.classList.add("is-drag-target");
2715
+ }
2716
+ handleLayerDrop(event) {
2717
+ event.preventDefault();
2718
+ const targetId = event.target.closest("[data-object-id]")?.dataset.objectId;
2719
+ const sourceId = this.draggedObjectId;
2720
+ if (!sourceId || !targetId || sourceId === targetId) {
2721
+ this.clearLayerDragState();
2722
+ return;
2723
+ }
2724
+ const targetIndex = this.customizer.getObjects().findIndex((object) => object.id === targetId);
2725
+ if (targetIndex >= 0) this.customizer.moveObject(sourceId, targetIndex);
2726
+ this.clearLayerDragState();
2727
+ this.renderLayers(true);
2728
+ }
2729
+ clearLayerDragState() {
2730
+ this.draggedObjectId = void 0;
2731
+ this.layerList.querySelectorAll(".is-dragging, .is-drag-target").forEach((item) => item.classList.remove("is-dragging", "is-drag-target"));
2732
+ }
2733
+ renderTextPresets() {
2734
+ const container = requiredElement(this.element, "[data-role=\"text-presets\"]");
2735
+ const items = this.textPresets.map((preset) => {
2736
+ const button = document.createElement("button");
2737
+ button.className = "customforge-workbench__text-preset";
2738
+ button.type = "button";
2739
+ button.dataset.textPresetId = preset.id;
2740
+ button.dataset.selected = String(preset.id === this.selectedTextPresetId);
2741
+ const preview = document.createElement("span");
2742
+ preview.className = "customforge-workbench__text-preview";
2743
+ preview.dataset.previewFallback = preset.previewText ?? preset.name;
2744
+ preview.textContent = preset.previewText ?? preset.name;
2745
+ preview.style.fontFamily = preset.fontFamily;
2746
+ preview.style.color = preset.color;
2747
+ const name = document.createElement("strong");
2748
+ name.textContent = preset.name;
2749
+ button.append(preview, name);
2750
+ return button;
2751
+ });
2752
+ container.replaceChildren(...items);
2753
+ const fieldset = container.closest("fieldset");
2754
+ if (fieldset) fieldset.hidden = items.length === 0;
2755
+ }
2756
+ openTextDialog() {
2757
+ const input = requiredElement(this.element, "[data-role=\"text-value\"]");
2758
+ input.value = this.labels.textInputPlaceholder;
2759
+ this.selectedTextPresetId = this.textPresets[0]?.id;
2760
+ if (this.selectedTextPresetId) this.selectTextPreset(this.selectedTextPresetId);
2761
+ this.updateTextPreviews();
2762
+ this.showDialog(this.textDialog);
2763
+ input.focus();
2764
+ input.select();
2765
+ }
2766
+ selectTextPreset(id) {
2767
+ const preset = this.textPresets.find((entry) => entry.id === id);
2768
+ if (!preset) return;
2769
+ this.selectedTextPresetId = id;
2770
+ requiredElement(this.element, "[data-role=\"text-color\"]").value = preset.color;
2771
+ this.element.querySelectorAll("[data-text-preset-id]").forEach((button) => {
2772
+ button.dataset.selected = String(button.dataset.textPresetId === id);
2773
+ });
2774
+ this.updateTextPreviews();
2775
+ }
2776
+ updateTextPreviews() {
2777
+ const text = requiredElement(this.element, "[data-role=\"text-value\"]").value;
2778
+ const color = requiredElement(this.element, "[data-role=\"text-color\"]").value;
2779
+ this.element.querySelectorAll(".customforge-workbench__text-preview").forEach((preview) => {
2780
+ const presetId = preview.closest("[data-text-preset-id]")?.dataset.textPresetId;
2781
+ const preset = this.textPresets.find((entry) => entry.id === presetId);
2782
+ preview.textContent = text || preview.dataset.previewFallback || "";
2783
+ preview.style.color = presetId === this.selectedTextPresetId ? color : preset?.color ?? "";
2784
+ });
2785
+ }
2786
+ addTextFromDialog(event) {
2787
+ event.preventDefault();
2788
+ const input = requiredElement(this.element, "[data-role=\"text-value\"]");
2789
+ const text = input.value.trim();
2790
+ input.setCustomValidity(text ? "" : this.labels.textRequired);
2791
+ if (!this.textForm.reportValidity()) return;
2792
+ const color = requiredElement(this.element, "[data-role=\"text-color\"]").value;
2793
+ const preset = this.textPresets.find((entry) => entry.id === this.selectedTextPresetId);
2794
+ this.customizer.addText({
2795
+ text,
2796
+ name: text.slice(0, 48),
2797
+ color,
2798
+ ...preset ? {
2799
+ fontFamily: preset.fontFamily,
2800
+ fontSize: preset.fontSize,
2801
+ width: preset.width
2802
+ } : {}
2803
+ });
2804
+ this.closeDialog(this.textDialog);
2805
+ this.setStatus(this.labels.textAdded);
2806
+ }
2807
+ renderAssetPresets() {
2808
+ this.renderAssetGroup("background-presets", this.backgrounds, "background");
2809
+ this.renderAssetGroup("element-presets", this.elements, "element");
2810
+ }
2811
+ renderAssetGroup(roleName, assets, role) {
2812
+ const container = requiredElement(this.element, `[data-role="${roleName}"]`);
2813
+ if (assets.length === 0) {
2814
+ const empty = document.createElement("p");
2815
+ empty.className = "customforge-workbench__preset-empty";
2816
+ empty.textContent = this.labels.noPresetAssets;
2817
+ container.replaceChildren(empty);
2818
+ return;
2819
+ }
2820
+ container.replaceChildren(...assets.map((asset) => {
2821
+ const button = document.createElement("button");
2822
+ button.className = "customforge-workbench__asset-preset";
2823
+ button.type = "button";
2824
+ button.dataset.assetId = asset.id;
2825
+ button.dataset.assetRole = role;
2826
+ button.dataset.selected = "false";
2827
+ const image = document.createElement("img");
2828
+ image.src = asset.thumbnailUrl ?? asset.url;
2829
+ image.alt = asset.alt ?? asset.name;
2830
+ image.loading = "lazy";
2831
+ const name = document.createElement("strong");
2832
+ name.textContent = asset.name;
2833
+ button.append(image, name);
2834
+ return button;
2835
+ }));
2836
+ }
2837
+ openImageDialog() {
2838
+ this.resetImageSelection();
2839
+ this.setImageTab("upload");
2840
+ this.showDialog(this.imageDialog);
2841
+ }
2842
+ setImageTab(tab) {
2843
+ const next = tab === "backgrounds" && !this.features.presetBackgrounds ? "upload" : tab === "elements" && !this.features.presetElements ? "upload" : tab;
2844
+ if (this.activeImageTab !== next && this.selectedImage) this.resetImageSelection();
2845
+ this.activeImageTab = next;
2846
+ this.element.querySelectorAll("[data-image-tab]").forEach((button) => {
2847
+ const selected = button.dataset.imageTab === next;
2848
+ button.dataset.selected = String(selected);
2849
+ button.setAttribute("aria-selected", String(selected));
2850
+ button.tabIndex = selected ? 0 : -1;
2851
+ });
2852
+ this.element.querySelectorAll("[data-image-panel]").forEach((panel) => {
2853
+ panel.hidden = panel.dataset.imagePanel !== next;
2854
+ });
2855
+ }
2856
+ selectUploadedImage() {
2857
+ const file = this.imageInput.files?.[0];
2858
+ if (!file) return;
2859
+ this.releaseImageSelection();
2860
+ const objectUrl = URL.createObjectURL(file);
2861
+ this.selectedImage = {
2862
+ src: objectUrl,
2863
+ name: file.name.replace(/\.[^.]+$/, "") || this.labels.imageObject,
2864
+ role: "element",
2865
+ objectUrl
2866
+ };
2867
+ requiredElement(this.element, "[data-role=\"image-file-name\"]").textContent = file.name;
2868
+ const preview = requiredElement(this.element, "[data-role=\"image-preview\"]");
2869
+ preview.src = objectUrl;
2870
+ preview.alt = file.name;
2871
+ preview.hidden = false;
2872
+ this.imageSubmitButton.disabled = false;
2873
+ }
2874
+ selectPresetAsset(id, role) {
2875
+ const asset = (role === "background" ? this.backgrounds : this.elements).find((entry) => entry.id === id);
2876
+ if (!asset) return;
2877
+ this.releaseImageSelection();
2878
+ this.selectedImage = {
2879
+ src: asset.url,
2880
+ name: asset.name,
2881
+ role
2882
+ };
2883
+ this.element.querySelectorAll("[data-asset-id]").forEach((button) => {
2884
+ button.dataset.selected = String(button.dataset.assetId === id && button.dataset.assetRole === role);
2885
+ });
2886
+ this.imageSubmitButton.disabled = false;
2887
+ }
2888
+ async addImageFromDialog(event) {
2889
+ event.preventDefault();
2890
+ const selection = this.selectedImage;
2891
+ if (!selection) return;
2892
+ this.imageSubmitButton.disabled = true;
2893
+ this.setStatus(this.labels.addingImage, "busy");
2894
+ try {
2895
+ await this.customizer.addImage({
2896
+ src: selection.src,
2897
+ name: selection.name,
2898
+ role: selection.role
2899
+ });
2900
+ this.closeDialog(this.imageDialog);
2901
+ this.setStatus(this.labels.imageAdded);
2902
+ } catch (error) {
2903
+ this.setStatus(this.formatError(error), "error");
2904
+ this.imageSubmitButton.disabled = false;
2905
+ }
2906
+ }
2907
+ resetImageSelection() {
2908
+ this.releaseImageSelection();
2909
+ this.selectedImage = void 0;
2910
+ this.imageInput.value = "";
2911
+ requiredElement(this.element, "[data-role=\"image-file-name\"]").textContent = this.labels.noImageSelected;
2912
+ const preview = requiredElement(this.element, "[data-role=\"image-preview\"]");
2913
+ preview.removeAttribute("src");
2914
+ preview.alt = "";
2915
+ preview.hidden = true;
2916
+ this.element.querySelectorAll("[data-asset-id]").forEach((button) => {
2917
+ button.dataset.selected = "false";
2918
+ });
2919
+ this.imageSubmitButton.disabled = true;
2920
+ }
2921
+ releaseImageSelection() {
2922
+ if (this.selectedImage?.objectUrl) URL.revokeObjectURL(this.selectedImage.objectUrl);
2923
+ }
2924
+ async loadSelectedDesign() {
2925
+ const file = this.designInput.files?.[0];
2926
+ if (!file) return;
2927
+ this.loadDesignButton.disabled = true;
2928
+ this.setStatus(this.labels.loadingDesign, "busy");
2929
+ try {
2930
+ await this.customizer.loadDesign(JSON.parse(await file.text()));
2931
+ this.setStatus(this.labels.designLoaded);
2932
+ } catch (error) {
2933
+ this.setStatus(this.formatError(error), "error");
2934
+ } finally {
2935
+ this.loadDesignButton.disabled = false;
2936
+ this.designInput.value = "";
2937
+ }
2938
+ }
2939
+ setProductSourceMode(mode) {
2940
+ this.productSourceMode = mode;
2941
+ this.element.querySelectorAll("[data-product-source]").forEach((button) => {
2942
+ const selected = button.dataset.productSource === mode;
2943
+ button.dataset.selected = String(selected);
2944
+ button.setAttribute("aria-pressed", String(selected));
2945
+ });
2946
+ this.element.querySelectorAll("[data-product-source-panel]").forEach((panel) => {
2947
+ panel.hidden = panel.dataset.productSourcePanel !== mode;
2948
+ });
2949
+ this.modelFileInput.disabled = mode !== "file";
2950
+ this.modelFileInput.required = mode === "file";
2951
+ this.modelUrlInput.disabled = mode !== "url";
2952
+ this.modelUrlInput.required = mode === "url";
2953
+ if (mode === "file") this.updateSelectedProductFile();
2954
+ }
2955
+ updateSelectedProductFile() {
2956
+ const file = this.modelFileInput.files?.[0];
2957
+ const valid = !file || file.name.toLowerCase().endsWith(".glb");
2958
+ const validationMessage = this.productSourceMode !== "file" ? "" : !file ? this.labels.modelFileRequired : valid ? "" : this.labels.invalidModelFile;
2959
+ this.modelFileInput.setCustomValidity(validationMessage);
2960
+ requiredElement(this.element, "[data-role=\"model-file-name\"]").textContent = file?.name ?? this.labels.noModelFileSelected;
2961
+ }
2962
+ validateProductForm() {
2963
+ this.updateSelectedProductFile();
2964
+ this.modelUrlInput.setCustomValidity("");
2965
+ if (this.productSourceMode === "url") this.modelUrlInput.setCustomValidity(!this.modelUrlInput.value.trim() ? this.labels.modelUrlRequired : this.modelUrlInput.validity.typeMismatch ? this.labels.invalidModelUrl : "");
2966
+ const textureUrl = requiredElement(this.element, "[data-role=\"texture-url\"]");
2967
+ textureUrl.setCustomValidity("");
2968
+ if (textureUrl.value.trim() && textureUrl.validity.typeMismatch) textureUrl.setCustomValidity(this.labels.invalidTextureUrl);
2969
+ const surfaceMesh = requiredElement(this.element, "[data-role=\"mesh-name\"]");
2970
+ surfaceMesh.setCustomValidity(surfaceMesh.value.trim() ? "" : this.labels.surfaceMeshRequired);
2971
+ }
2972
+ setProductLoading(loading) {
2973
+ const label = requiredElement(this.submitProductButton, "[data-label=\"loadProduct\"]");
2974
+ this.submitProductButton.disabled = loading;
2975
+ this.submitProductButton.dataset.loading = String(loading);
2976
+ this.submitProductButton.setAttribute("aria-busy", String(loading));
2977
+ label.textContent = loading ? this.labels.loadingProduct : this.labels.loadProduct;
2978
+ }
2979
+ replaceActiveProductObjectUrl(nextUrl) {
2980
+ if (this.activeProductObjectUrl && this.activeProductObjectUrl !== nextUrl) URL.revokeObjectURL(this.activeProductObjectUrl);
2981
+ this.activeProductObjectUrl = nextUrl;
2982
+ }
2983
+ async loadDemoProduct() {
2984
+ this.setProductLoading(true);
2985
+ this.setStatus(this.labels.loadingProduct, "busy");
2986
+ try {
2987
+ await this.customizer.loadProduct({});
2988
+ this.replaceActiveProductObjectUrl();
2989
+ this.closeDialog(this.productDialog);
2990
+ this.setStatus(this.labels.productReady);
2991
+ } catch (error) {
2992
+ this.setStatus(this.formatError(error), "error");
2993
+ } finally {
2994
+ this.setProductLoading(false);
2995
+ }
2996
+ }
2997
+ async loadProductFromDialog(event) {
2998
+ event.preventDefault();
2999
+ this.validateProductForm();
3000
+ if (!this.remoteForm.reportValidity()) return;
3001
+ const file = this.productSourceMode === "file" ? this.modelFileInput.files?.[0] : void 0;
3002
+ let pendingObjectUrl;
3003
+ this.setProductLoading(true);
3004
+ this.setStatus(this.labels.loadingProduct, "busy");
3005
+ try {
3006
+ pendingObjectUrl = file ? URL.createObjectURL(file) : void 0;
3007
+ await this.customizer.loadProduct({
3008
+ modelUrl: pendingObjectUrl ?? this.modelUrlInput.value,
3009
+ textureUrl: requiredElement(this.element, "[data-role=\"texture-url\"]").value,
3010
+ surfaceMesh: requiredElement(this.element, "[data-role=\"mesh-name\"]").value,
3011
+ textureFlipY: requiredElement(this.element, "[data-role=\"flip-texture\"]").checked
3012
+ });
3013
+ this.replaceActiveProductObjectUrl(pendingObjectUrl);
3014
+ pendingObjectUrl = void 0;
3015
+ this.closeDialog(this.productDialog);
3016
+ this.setStatus(this.labels.productReady);
3017
+ } catch (error) {
3018
+ this.setStatus(this.formatError(error), "error");
3019
+ } finally {
3020
+ if (pendingObjectUrl) URL.revokeObjectURL(pendingObjectUrl);
3021
+ this.setProductLoading(false);
3022
+ }
3023
+ }
3024
+ };
3025
+ //#endregion
3026
+ //#region src/workbench/index.ts
3027
+ /**
3028
+ * 创建带默认界面的产品定制工作台
3029
+ *
3030
+ * 使用 `features` 控制具体功能控件,使用 `layout` 控制顶部栏、工具栏、图层面板等区域
3031
+ * 不再使用返回的实例时必须调用 `destroy()`
3032
+ *
3033
+ * @param options 挂载容器、产品资源、功能和布局开关
3034
+ * @returns 初始化完成的 Workbench
3035
+ * @throws 挂载容器无效、模型加载失败或目标 Mesh 不存在时抛出错误
3036
+ *
3037
+ * @example
3038
+ * ```ts
3039
+ * const workbench = await createWorkbench({
3040
+ * container: '#app',
3041
+ * features: { addText: false },
3042
+ * layout: { header: false },
3043
+ * })
3044
+ * ```
3045
+ */
3046
+ async function createWorkbench(options) {
3047
+ return CustomForgeWorkbench.create(options);
3048
+ }
3049
+ //#endregion
3050
+ export { CustomForgeWorkbench, createWorkbench };
3051
+
3052
+ //# sourceMappingURL=workbench.js.map