@kokoa/clotho-editor 0.1.4 → 0.2.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.
package/dist/index.js CHANGED
@@ -1,9 +1,11 @@
1
- import { downloadAnimationJson, configureAnimationRepository } from './chunk-GMGB7NIL.js';
2
- export { addAppearance, addChapter, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, deleteChapter, deleteEffect, deleteElement, downloadAnimationJson, endTransient, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, markClean, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateSettings } from './chunk-GMGB7NIL.js';
3
- import { useState, useEffect, useRef } from 'react';
1
+ import { downloadAnimationJson, configureAnimationRepository } from './chunk-FAI2G3XK.js';
2
+ export { addAppearance, addChapter, addCheckpoint, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, createEditorPluginContext, createLayout, deleteChapter, deleteCheckpoint, deleteEffect, deleteElement, detachFromLayout, downloadAnimationJson, endTransient, findLayoutCollisions, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, layoutIdsFor, markClean, mountEditorPlugins, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueCheckpointId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateCheckpoint, updateData, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateResponsive, updateSettings, validateEditorPlugin } from './chunk-FAI2G3XK.js';
3
+ import { useState, useEffect, useRef, useMemo } from 'react';
4
4
  import { usePlayer, AnimationStage } from '@kokoa/clotho/react';
5
5
  import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
6
- import { animationDocumentSchema } from '@kokoa/clotho';
6
+ import { defineStory, animationDocumentSchema, compileSceneDependencyPlan, buildScene, lintDocument, autofixDocument, compileResponsiveStage } from '@kokoa/clotho';
7
+ import { animationSampleTimes, expectAnimation, snapshotAnimationMatrix, animationFailureReport, AnimationAssertionError } from '@kokoa/clotho/testing';
8
+ import { renderDocumentToSvg } from '@kokoa/clotho/svg';
7
9
 
8
10
  function elementsOf(def) {
9
11
  return def.elements;
@@ -54,6 +56,7 @@ function Studio({ initial, onSave }) {
54
56
  rotation: 0,
55
57
  content: "New",
56
58
  translations: {},
59
+ references: {},
57
60
  fontSize: 24,
58
61
  color: "#4f46e5",
59
62
  textAnchor: "start",
@@ -215,6 +218,250 @@ function Studio({ initial, onSave }) {
215
218
  ] })
216
219
  ] });
217
220
  }
221
+
222
+ // src/template-editor-plugin.ts
223
+ function initialParameterValue(schema) {
224
+ if (schema.default !== void 0) return structuredClone(schema.default);
225
+ if (schema.type === "string") return "";
226
+ if (schema.type === "number") return schema.min ?? 0;
227
+ if (schema.type === "boolean") return false;
228
+ if (schema.type === "enum") return schema.values[0];
229
+ if (schema.type === "array") return [];
230
+ return Object.fromEntries(
231
+ Object.entries(schema.properties).map(([key, child]) => [
232
+ key,
233
+ initialParameterValue(child)
234
+ ])
235
+ );
236
+ }
237
+ function parameterInputValue(schema, raw) {
238
+ if (schema.type === "boolean") return Boolean(raw);
239
+ if (schema.type === "number") return Number(raw);
240
+ if (schema.type === "array" || schema.type === "object")
241
+ return JSON.parse(String(raw));
242
+ return String(raw);
243
+ }
244
+ function downloadReference(reference) {
245
+ const blob = new Blob([JSON.stringify(reference, null, 2)], {
246
+ type: "application/json"
247
+ });
248
+ const url = URL.createObjectURL(blob);
249
+ const anchor = document.createElement("a");
250
+ anchor.href = url;
251
+ anchor.download = `${reference.templateId.replace(/[^a-z0-9._-]/gi, "-")}.template.json`;
252
+ anchor.click();
253
+ URL.revokeObjectURL(url);
254
+ }
255
+ function field(key, schema, value, onChange) {
256
+ const label = document.createElement("label");
257
+ label.className = "studio-field studio-template-field";
258
+ const title = document.createElement("span");
259
+ title.textContent = key;
260
+ if (schema.description) title.title = schema.description;
261
+ label.append(title);
262
+ let control;
263
+ if (schema.type === "enum") {
264
+ control = document.createElement("select");
265
+ for (const option of schema.values) {
266
+ const element = document.createElement("option");
267
+ element.value = option;
268
+ element.textContent = option;
269
+ control.append(element);
270
+ }
271
+ control.value = String(value);
272
+ } else if (schema.type === "array" || schema.type === "object") {
273
+ control = document.createElement("textarea");
274
+ control.rows = 3;
275
+ control.value = JSON.stringify(value, null, 2);
276
+ } else {
277
+ control = document.createElement("input");
278
+ control.type = schema.type === "boolean" ? "checkbox" : schema.type;
279
+ if (schema.type === "boolean") control.checked = Boolean(value);
280
+ else control.value = String(value);
281
+ if (schema.type === "number") {
282
+ if (schema.min !== void 0) control.min = String(schema.min);
283
+ if (schema.max !== void 0) control.max = String(schema.max);
284
+ control.step = schema.integer ? "1" : "any";
285
+ }
286
+ if (schema.type === "string") {
287
+ if (schema.minLength !== void 0) control.minLength = schema.minLength;
288
+ if (schema.maxLength !== void 0) control.maxLength = schema.maxLength;
289
+ if (schema.pattern !== void 0) control.pattern = schema.pattern;
290
+ }
291
+ }
292
+ control.dataset.templateParameter = key;
293
+ const update = () => {
294
+ try {
295
+ const raw = control instanceof HTMLInputElement && control.type === "checkbox" ? control.checked : control.value;
296
+ onChange(parameterInputValue(schema, raw));
297
+ control.setCustomValidity("");
298
+ } catch {
299
+ control.setCustomValidity("\uC62C\uBC14\uB978 JSON\uC744 \uC785\uB825\uD558\uC138\uC694.");
300
+ }
301
+ };
302
+ control.addEventListener(
303
+ schema.type === "enum" || schema.type === "boolean" ? "change" : "input",
304
+ update
305
+ );
306
+ label.append(control);
307
+ return label;
308
+ }
309
+ function createTemplateEditorPlugin(templates) {
310
+ return {
311
+ manifest: {
312
+ id: "dev.clotho.templates",
313
+ name: "Template parameters",
314
+ capabilities: ["editor"],
315
+ editor: { panels: ["parameters"] }
316
+ },
317
+ panels: {
318
+ parameters: {
319
+ id: "parameters",
320
+ label: "Template parameters",
321
+ mount(container, context) {
322
+ if (templates.length === 0) return;
323
+ container.classList.add(
324
+ "studio-tools-section",
325
+ "studio-template-panel"
326
+ );
327
+ const heading = document.createElement("div");
328
+ heading.className = "studio-tools-title";
329
+ heading.textContent = "Template";
330
+ const select = document.createElement("select");
331
+ select.className = "studio-template-select";
332
+ for (const template of templates) {
333
+ const option = document.createElement("option");
334
+ option.value = template.id;
335
+ option.textContent = template.id;
336
+ select.append(option);
337
+ }
338
+ const form = document.createElement("div");
339
+ const status = document.createElement("p");
340
+ status.className = "studio-props-empty studio-template-status";
341
+ const actions = document.createElement("div");
342
+ actions.className = "studio-align-row";
343
+ const standalone = document.createElement("button");
344
+ standalone.className = "studio-btn";
345
+ standalone.textContent = "Standalone JSON";
346
+ const reference = document.createElement("button");
347
+ reference.className = "studio-btn";
348
+ reference.textContent = "Template \uCC38\uC870";
349
+ actions.append(standalone, reference);
350
+ container.append(heading, select, form, status, actions);
351
+ let active = templates[0];
352
+ let values = {};
353
+ let previewDocument = context.getDocument();
354
+ const rebuild = () => {
355
+ try {
356
+ previewDocument = active.instantiate(values);
357
+ context.replaceDocument(previewDocument);
358
+ status.textContent = "parameter\uAC00 \uBBF8\uB9AC\uBCF4\uAE30\uC5D0 \uBC18\uC601\uB418\uC5C8\uC2B5\uB2C8\uB2E4.";
359
+ status.removeAttribute("data-error");
360
+ } catch (error) {
361
+ const issues = error.issues;
362
+ status.textContent = issues ? issues.map(({ path, message }) => `${path}: ${message}`).join(" \xB7 ") : String(error);
363
+ status.dataset.error = "true";
364
+ }
365
+ };
366
+ const render = () => {
367
+ values = Object.fromEntries(
368
+ Object.entries(active.parameters).map(([key, schema]) => [
369
+ key,
370
+ initialParameterValue(schema)
371
+ ])
372
+ );
373
+ form.replaceChildren(
374
+ ...Object.entries(active.parameters).map(
375
+ ([key, schema]) => field(key, schema, values[key], (value) => {
376
+ values[key] = value;
377
+ rebuild();
378
+ })
379
+ )
380
+ );
381
+ rebuild();
382
+ };
383
+ select.addEventListener("change", () => {
384
+ active = templates.find(({ id }) => id === select.value) ?? templates[0];
385
+ render();
386
+ });
387
+ standalone.addEventListener(
388
+ "click",
389
+ () => downloadAnimationJson(previewDocument)
390
+ );
391
+ reference.addEventListener(
392
+ "click",
393
+ () => downloadReference(active.reference(values))
394
+ );
395
+ render();
396
+ }
397
+ }
398
+ }
399
+ };
400
+ }
401
+ function replaceStoryNodeDocument(manifest, nodeId, document2) {
402
+ return defineStory({
403
+ ...manifest,
404
+ nodes: manifest.nodes.map(
405
+ (node) => node.id === nodeId ? { ...node, document: document2 } : node
406
+ )
407
+ });
408
+ }
409
+ function appendStoryEdge(manifest, edge) {
410
+ return defineStory({ ...manifest, edges: [...manifest.edges, edge] });
411
+ }
412
+ function createStoryEditorPlugin(options) {
413
+ let manifest = structuredClone(options.manifest);
414
+ let activeNode = manifest.initialNode;
415
+ return {
416
+ manifest: {
417
+ id: "dev.clotho.story",
418
+ name: "Story Graph",
419
+ capabilities: ["editor"],
420
+ editor: { panels: ["story-graph"] }
421
+ },
422
+ panels: {
423
+ "story-graph": {
424
+ id: "story-graph",
425
+ label: "Story Graph",
426
+ mount(container, context) {
427
+ const render = () => {
428
+ container.innerHTML = `<div class="studio-tools-section"><div class="studio-tools-title">Story Graph</div><div class="studio-story-nodes">${manifest.nodes.map((node) => `<button type="button" class="studio-btn${node.id === activeNode ? " is-active" : ""}" data-story-node="${escapeAttribute(node.id)}">${escapeHtml(node.title || node.id)}</button>`).join("")}</div><div class="studio-tools-hint">${manifest.edges.map((edge) => `${escapeHtml(edge.from)} \u2192 ${escapeHtml(edge.to)}${edge.label ? ` \xB7 ${escapeHtml(edge.label)}` : ""}`).join("<br>") || "\uC5F0\uACB0\uC774 \uC5C6\uC2B5\uB2C8\uB2E4."}</div></div>`;
429
+ };
430
+ const click = (event) => {
431
+ const button = event.target.closest(
432
+ "[data-story-node]"
433
+ );
434
+ const nextId = button?.dataset.storyNode;
435
+ if (!nextId || nextId === activeNode) return;
436
+ manifest = replaceStoryNodeDocument(
437
+ manifest,
438
+ activeNode,
439
+ context.getDocument()
440
+ );
441
+ const node = manifest.nodes.find(({ id }) => id === nextId);
442
+ if (!node) return;
443
+ activeNode = nextId;
444
+ context.replaceDocument(node.document);
445
+ options.onChange?.(structuredClone(manifest));
446
+ render();
447
+ };
448
+ container.addEventListener("click", click);
449
+ render();
450
+ return () => container.removeEventListener("click", click);
451
+ }
452
+ }
453
+ }
454
+ };
455
+ }
456
+ function escapeHtml(value) {
457
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
458
+ }
459
+ function escapeAttribute(value) {
460
+ return escapeHtml(value).replaceAll('"', "&quot;");
461
+ }
462
+ var EMPTY_EDITOR_PLUGINS = [];
463
+ var EMPTY_TEMPLATES = [];
464
+ var DENY_PLUGIN_PERMISSIONS = () => ({});
218
465
  var CANVAS_PRESETS = [
219
466
  { id: "800x500", label: "8:5", sub: "800\xD7500", w: 32, h: 20 },
220
467
  { id: "1280x720", label: "16:9", sub: "HD", w: 32, h: 18 },
@@ -238,6 +485,7 @@ var SKELETON = `
238
485
  <header class="studio-header">
239
486
  <h1 id="studio-editor-title" class="studio-header-title">Clotho Editor</h1>
240
487
  <div class="studio-header-actions">
488
+ <span class="studio-plugin-toolbar" data-editor-plugin-slot="toolbar"></span>
241
489
  <button type="button" id="studio-open" class="studio-btn" aria-label="\uC800\uC7A5\uB41C \uC560\uB2C8\uBA54\uC774\uC158 \uC5F4\uAE30">\u{1F4C1} \uC5F4\uAE30</button>
242
490
  <button type="button" id="studio-new" class="studio-btn" aria-label="\uC0C8 \uC560\uB2C8\uBA54\uC774\uC158 \uB9CC\uB4E4\uAE30">\uFF0B \uC0C8 \uC560\uB2C8\uBA54\uC774\uC158</button>
243
491
  <button type="button" id="studio-undo" class="studio-btn studio-btn-icon" aria-label="\uC2E4\uD589 \uCDE8\uC18C" title="\uC2E4\uD589 \uCDE8\uC18C (\u2318Z)" disabled>\u21B6</button>
@@ -270,6 +518,7 @@ var SKELETON = `
270
518
  <div class="studio-tool-with-option"><button type="button" class="studio-tool-btn" data-add-element="polygon" title="\uB2E4\uAC01\uD615 (Y)" aria-label="\uB2E4\uAC01\uD615 \uB3C4\uAD6C"><span>\u2B22 Polygon</span><kbd>Y</kbd></button><label>\uBCC0 <input type="number" id="studio-polygon-sides" min="3" max="24" value="6" aria-label="\uB2E4\uAC01\uD615\uC758 \uBCC0 \uAC1C\uC218" /></label></div>
271
519
  <button type="button" class="studio-tool-btn" id="studio-open-icons" title="\uC544\uC774\uCF58 \uB77C\uC774\uBE0C\uB7EC\uB9AC" aria-label="\uC544\uC774\uCF58 \uB77C\uC774\uBE0C\uB7EC\uB9AC \uCD94\uAC00">\u{1F3A8} Icons</button>
272
520
  </div>
521
+ <div class="studio-plugin-panels" data-editor-plugin-slot="panel"></div>
273
522
  <div class="studio-tools-section">
274
523
  <div class="studio-tools-title">\uCE94\uBC84\uC2A4 \uD06C\uAE30</div>
275
524
  <div class="studio-canvas-size-row">
@@ -311,6 +560,7 @@ var SKELETON = `
311
560
  <div id="studio-props-content" class="studio-props-content">
312
561
  <p class="studio-props-empty">\uC694\uC18C \uB610\uB294 step \uC744 \uC120\uD0DD\uD558\uC138\uC694.</p>
313
562
  </div>
563
+ <div class="studio-plugin-inspectors" data-editor-plugin-slot="inspector"></div>
314
564
  </aside>
315
565
  </div>
316
566
 
@@ -426,24 +676,58 @@ function StudioMount({
426
676
  initialId,
427
677
  repository,
428
678
  editorTitle = "Clotho Editor",
429
- resolveImage
679
+ resolveImage,
680
+ importDocument,
681
+ plugins = EMPTY_EDITOR_PLUGINS,
682
+ resolvePluginPermissions = DENY_PLUGIN_PERMISSIONS,
683
+ templates = EMPTY_TEMPLATES,
684
+ story
430
685
  }) {
431
686
  const inited = useRef(false);
687
+ const mountedPlugins = useMemo(
688
+ () => [
689
+ ...templates.length > 0 ? [createTemplateEditorPlugin(templates)] : [],
690
+ ...story ? [createStoryEditorPlugin(story)] : [],
691
+ ...plugins
692
+ ],
693
+ [plugins, templates, story]
694
+ );
695
+ const permissionResolver = useMemo(
696
+ () => (plugin) => plugin.id === "dev.clotho.templates" || plugin.id === "dev.clotho.story" ? { ui: true, documentRead: true, documentWrite: true } : resolvePluginPermissions(plugin),
697
+ [resolvePluginPermissions]
698
+ );
432
699
  useEffect(() => {
433
700
  if (inited.current) return;
434
701
  inited.current = true;
435
702
  if (repository) configureAnimationRepository(repository);
436
703
  let disposed = false;
437
- void import('./main-NCPWDB2U.js').then(({ initStudio }) => {
704
+ let cleanup;
705
+ void import('./main-SKM3P4X7.js').then(({ initStudio }) => {
438
706
  if (disposed) return;
439
- initStudio({ initialId, editorTitle, resolveImage });
707
+ cleanup = initStudio({
708
+ initialId,
709
+ editorTitle,
710
+ resolveImage,
711
+ importDocument,
712
+ plugins: mountedPlugins,
713
+ resolvePluginPermissions: permissionResolver
714
+ });
440
715
  });
441
716
  return () => {
442
717
  disposed = true;
718
+ cleanup?.();
443
719
  document.body.classList.remove("editor-active");
444
720
  document.documentElement.classList.remove("editor-active");
445
721
  };
446
- }, [editorTitle, initialId, repository, resolveImage]);
722
+ }, [
723
+ editorTitle,
724
+ importDocument,
725
+ initialId,
726
+ mountedPlugins,
727
+ repository,
728
+ resolveImage,
729
+ permissionResolver
730
+ ]);
447
731
  return /* @__PURE__ */ jsx("section", { className: "studio-shell w-full", "data-pagefind-ignore": "all", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: SKELETON } }) });
448
732
  }
449
733
  function browserStorage() {
@@ -531,7 +815,228 @@ function createLocalStorageRepository(options = {}) {
531
815
  }
532
816
  };
533
817
  }
818
+ function downloadReport(html) {
819
+ const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
820
+ const link = document.createElement("a");
821
+ link.href = url;
822
+ link.download = "clotho-animation-qa.html";
823
+ link.click();
824
+ URL.revokeObjectURL(url);
825
+ }
826
+ function createVisualRegressionPlugin() {
827
+ return {
828
+ manifest: {
829
+ id: "dev.clotho.visual-regression",
830
+ name: "Animation QA",
831
+ capabilities: ["editor"],
832
+ editor: { inspectors: ["animation-qa"] }
833
+ },
834
+ inspectors: {
835
+ "animation-qa": {
836
+ id: "animation-qa",
837
+ label: "Animation QA",
838
+ mount(container, context) {
839
+ container.classList.add("studio-qa-panel");
840
+ const title = document.createElement("strong");
841
+ title.textContent = "Animation QA";
842
+ const run = document.createElement("button");
843
+ run.type = "button";
844
+ run.className = "studio-btn";
845
+ run.textContent = "\uC804\uCCB4 frame \uAC80\uC0AC";
846
+ const status = document.createElement("p");
847
+ status.className = "studio-props-empty";
848
+ container.append(title, run, status);
849
+ run.addEventListener("click", () => {
850
+ const animation = context.getDocument();
851
+ const errors = [];
852
+ for (const time of animationSampleTimes(animation)) {
853
+ for (const element of animation.elements) {
854
+ if (!["rect", "circle", "image", "code"].includes(element.type))
855
+ continue;
856
+ try {
857
+ expectAnimation(animation).at(time).insideCanvas(element.id);
858
+ } catch (error) {
859
+ if (error instanceof AnimationAssertionError)
860
+ errors.push(error);
861
+ }
862
+ }
863
+ }
864
+ const snapshots = snapshotAnimationMatrix(animation);
865
+ if (errors.length === 0) {
866
+ status.textContent = `${snapshots.length}\uAC1C locale\xB7theme frame\uC744 \uAC80\uC0AC\uD588\uC2B5\uB2C8\uB2E4. \uBB38\uC81C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`;
867
+ status.removeAttribute("data-error");
868
+ } else {
869
+ status.textContent = `${errors.length}\uAC1C \uBB38\uC81C\uB97C \uBC1C\uACAC\uD588\uC2B5\uB2C8\uB2E4. HTML report\uB97C \uB0B4\uB824\uBC1B\uC2B5\uB2C8\uB2E4.`;
870
+ status.dataset.error = "true";
871
+ downloadReport(animationFailureReport(errors));
872
+ }
873
+ });
874
+ }
875
+ }
876
+ }
877
+ };
878
+ }
879
+ var RESPONSIVE_VIEWPORTS = [
880
+ { id: "compact", label: "\uBAA8\uBC14\uC77C", width: 375 },
881
+ { id: "regular", label: "\uBCF8\uBB38", width: 768 },
882
+ { id: "wide", label: "\uB113\uC740 \uD654\uBA74", width: 1280 }
883
+ ];
884
+ function addDefaultResponsiveVariants(document2) {
885
+ if (document2.responsive?.length) return document2;
886
+ return {
887
+ ...document2,
888
+ responsive: [
889
+ {
890
+ id: "compact",
891
+ minWidth: 0,
892
+ maxWidth: 479,
893
+ chapterListPosition: "bottom",
894
+ elementOverrides: {}
895
+ },
896
+ { id: "regular", minWidth: 480, maxWidth: 959, elementOverrides: {} },
897
+ { id: "wide", minWidth: 960, elementOverrides: {} }
898
+ ]
899
+ };
900
+ }
901
+ function createResponsiveInspectorPlugin() {
902
+ return {
903
+ manifest: {
904
+ id: "dev.clotho.responsive",
905
+ name: "Responsive Stage",
906
+ capabilities: ["editor"],
907
+ editor: { inspectors: ["responsive"] }
908
+ },
909
+ inspectors: {
910
+ responsive: {
911
+ id: "responsive",
912
+ label: "Responsive Stage",
913
+ mount(container, context) {
914
+ const render = () => {
915
+ const document2 = context.getDocument();
916
+ container.innerHTML = `<div class="studio-props-header"><span class="studio-props-header-title">Responsive Stage</span></div><button type="button" class="studio-btn" data-responsive-defaults>\uAE30\uBCF8 breakpoint \uB9CC\uB4E4\uAE30</button><div class="studio-responsive-grid">${RESPONSIVE_VIEWPORTS.map((viewport) => `<figure><figcaption>${viewport.label} \xB7 ${viewport.width}px</figcaption>${renderDocumentToSvg(compileResponsiveStage(document2, viewport.width), 0)}</figure>`).join("")}</div>`;
917
+ };
918
+ const click = (event) => {
919
+ if (!event.target.closest(
920
+ "[data-responsive-defaults]"
921
+ ))
922
+ return;
923
+ context.replaceDocument(
924
+ addDefaultResponsiveVariants(context.getDocument())
925
+ );
926
+ render();
927
+ };
928
+ container.addEventListener("click", click);
929
+ render();
930
+ return () => container.removeEventListener("click", click);
931
+ }
932
+ }
933
+ }
934
+ };
935
+ }
936
+ function profileAnimation(document2, sampleCount = 30, frameBudgetMs = 16.7) {
937
+ const plan = compileSceneDependencyPlan(document2);
938
+ const samples = [];
939
+ for (let index = 0; index < sampleCount; index += 1) {
940
+ const time = sampleCount === 1 ? 0 : document2.duration * index / (sampleCount - 1);
941
+ const started = performance.now();
942
+ buildScene(document2, time);
943
+ samples.push(performance.now() - started);
944
+ }
945
+ const totalMs = samples.reduce((sum, value) => sum + value, 0);
946
+ const maxMs = Math.max(0, ...samples);
947
+ return {
948
+ elementCount: plan.elementCount,
949
+ trackCount: plan.trackCount,
950
+ keyframeCount: plan.keyframeCount,
951
+ sampleCount,
952
+ totalMs,
953
+ averageMs: totalMs / Math.max(1, sampleCount),
954
+ maxMs,
955
+ overBudget: maxMs > frameBudgetMs
956
+ };
957
+ }
958
+ function createPerformanceProfilerPlugin() {
959
+ return {
960
+ manifest: {
961
+ id: "dev.clotho.performance",
962
+ name: "Scene Profiler",
963
+ capabilities: ["editor"],
964
+ editor: { inspectors: ["performance"] }
965
+ },
966
+ inspectors: {
967
+ performance: {
968
+ id: "performance",
969
+ label: "Scene Profiler",
970
+ mount(container, context) {
971
+ const run = () => {
972
+ const profile = profileAnimation(context.getDocument());
973
+ container.innerHTML = `<div class="studio-props-header"><span class="studio-props-header-title">Scene Profiler</span></div><dl class="studio-profile"><div><dt>Elements</dt><dd>${profile.elementCount}</dd></div><div><dt>Tracks / keyframes</dt><dd>${profile.trackCount} / ${profile.keyframeCount}</dd></div><div><dt>\uD3C9\uADE0 / \uCD5C\uB300</dt><dd>${profile.averageMs.toFixed(2)} / ${profile.maxMs.toFixed(2)} ms</dd></div></dl><p class="studio-template-status" data-error="${profile.overBudget}">${profile.overBudget ? "16.7ms frame budget\uC744 \uB118\uC5C8\uC2B5\uB2C8\uB2E4." : "60fps frame budget \uC548\uC5D0 \uC788\uC2B5\uB2C8\uB2E4."}</p><button type="button" class="studio-btn" data-run-profiler>\uB2E4\uC2DC \uCE21\uC815</button>`;
974
+ };
975
+ const click = (event) => {
976
+ if (event.target.closest("[data-run-profiler]"))
977
+ run();
978
+ };
979
+ container.addEventListener("click", click);
980
+ run();
981
+ return () => container.removeEventListener("click", click);
982
+ }
983
+ }
984
+ }
985
+ };
986
+ }
987
+ function createLinterPlugin() {
988
+ return {
989
+ manifest: {
990
+ id: "dev.clotho.linter",
991
+ name: "Clotho Linter",
992
+ capabilities: ["editor"],
993
+ editor: { inspectors: ["linter"] }
994
+ },
995
+ inspectors: {
996
+ linter: {
997
+ id: "linter",
998
+ label: "Clotho Linter",
999
+ mount(container, context) {
1000
+ let selectedIndex = 0;
1001
+ const render = () => {
1002
+ const document2 = context.getDocument();
1003
+ const issues = lintDocument(document2);
1004
+ const selected = issues[selectedIndex] ?? issues[0];
1005
+ const fixed = autofixDocument(document2).document;
1006
+ container.innerHTML = `<div class="studio-props-header"><span class="studio-props-header-title">Clotho Linter (${issues.length})</span><button type="button" class="studio-btn" data-lint-fix ${issues.some(({ fixable }) => fixable) ? "" : "disabled"}>\uC548\uC804\uD55C \uC218\uC815 \uC801\uC6A9</button></div><ol class="studio-lint-list">${issues.map((issue, index) => `<li><button type="button" data-lint-index="${index}" data-element-id="${issue.elementId ?? ""}"><strong>${escapeHtml2(issue.ruleId)}</strong><span>${escapeHtml2(issue.message)}</span>${issue.fixable ? "<em>\uC790\uB3D9 \uC218\uC815</em>" : ""}</button></li>`).join("") || "<li>\uBB38\uC81C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</li>"}</ol>${selected ? `<div class="studio-lint-compare"><figure><figcaption>\uD604\uC7AC</figcaption>${renderDocumentToSvg(document2, selected.time ?? 0)}</figure><figure><figcaption>\uC218\uC815 \uD6C4</figcaption>${renderDocumentToSvg(fixed, selected.time ?? 0)}</figure></div>` : ""}`;
1007
+ };
1008
+ const click = (event) => {
1009
+ const target = event.target;
1010
+ if (target.closest("[data-lint-fix]")) {
1011
+ context.replaceDocument(
1012
+ autofixDocument(context.getDocument()).document
1013
+ );
1014
+ selectedIndex = 0;
1015
+ render();
1016
+ return;
1017
+ }
1018
+ const issue = target.closest("[data-lint-index]");
1019
+ if (!issue) return;
1020
+ selectedIndex = Number(issue.dataset.lintIndex);
1021
+ if (issue.dataset.elementId)
1022
+ context.setSelection({
1023
+ kind: "element",
1024
+ elementId: issue.dataset.elementId
1025
+ });
1026
+ render();
1027
+ };
1028
+ container.addEventListener("click", click);
1029
+ render();
1030
+ return () => container.removeEventListener("click", click);
1031
+ }
1032
+ }
1033
+ }
1034
+ };
1035
+ }
1036
+ function escapeHtml2(value) {
1037
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1038
+ }
534
1039
 
535
- export { Studio, StudioMount, createLocalStorageRepository };
1040
+ export { RESPONSIVE_VIEWPORTS, Studio, StudioMount, addDefaultResponsiveVariants, appendStoryEdge, createLinterPlugin, createLocalStorageRepository, createPerformanceProfilerPlugin, createResponsiveInspectorPlugin, createStoryEditorPlugin, createTemplateEditorPlugin, createVisualRegressionPlugin, initialParameterValue, parameterInputValue, profileAnimation, replaceStoryNodeDocument };
536
1041
  //# sourceMappingURL=index.js.map
537
1042
  //# sourceMappingURL=index.js.map