@kokoa/clotho-editor 0.1.3 → 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-Z2K5HFHI.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, updateMeta, updateSettings } from './chunk-Z2K5HFHI.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;
@@ -53,6 +55,8 @@ function Studio({ initial, onSave }) {
53
55
  y: 100,
54
56
  rotation: 0,
55
57
  content: "New",
58
+ translations: {},
59
+ references: {},
56
60
  fontSize: 24,
57
61
  color: "#4f46e5",
58
62
  textAnchor: "start",
@@ -214,6 +218,250 @@ function Studio({ initial, onSave }) {
214
218
  ] })
215
219
  ] });
216
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 = () => ({});
217
465
  var CANVAS_PRESETS = [
218
466
  { id: "800x500", label: "8:5", sub: "800\xD7500", w: 32, h: 20 },
219
467
  { id: "1280x720", label: "16:9", sub: "HD", w: 32, h: 18 },
@@ -237,6 +485,7 @@ var SKELETON = `
237
485
  <header class="studio-header">
238
486
  <h1 id="studio-editor-title" class="studio-header-title">Clotho Editor</h1>
239
487
  <div class="studio-header-actions">
488
+ <span class="studio-plugin-toolbar" data-editor-plugin-slot="toolbar"></span>
240
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>
241
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>
242
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>
@@ -269,6 +518,7 @@ var SKELETON = `
269
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>
270
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>
271
520
  </div>
521
+ <div class="studio-plugin-panels" data-editor-plugin-slot="panel"></div>
272
522
  <div class="studio-tools-section">
273
523
  <div class="studio-tools-title">\uCE94\uBC84\uC2A4 \uD06C\uAE30</div>
274
524
  <div class="studio-canvas-size-row">
@@ -310,6 +560,7 @@ var SKELETON = `
310
560
  <div id="studio-props-content" class="studio-props-content">
311
561
  <p class="studio-props-empty">\uC694\uC18C \uB610\uB294 step \uC744 \uC120\uD0DD\uD558\uC138\uC694.</p>
312
562
  </div>
563
+ <div class="studio-plugin-inspectors" data-editor-plugin-slot="inspector"></div>
313
564
  </aside>
314
565
  </div>
315
566
 
@@ -425,24 +676,58 @@ function StudioMount({
425
676
  initialId,
426
677
  repository,
427
678
  editorTitle = "Clotho Editor",
428
- resolveImage
679
+ resolveImage,
680
+ importDocument,
681
+ plugins = EMPTY_EDITOR_PLUGINS,
682
+ resolvePluginPermissions = DENY_PLUGIN_PERMISSIONS,
683
+ templates = EMPTY_TEMPLATES,
684
+ story
429
685
  }) {
430
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
+ );
431
699
  useEffect(() => {
432
700
  if (inited.current) return;
433
701
  inited.current = true;
434
702
  if (repository) configureAnimationRepository(repository);
435
703
  let disposed = false;
436
- void import('./main-5YXLTYPQ.js').then(({ initStudio }) => {
704
+ let cleanup;
705
+ void import('./main-SKM3P4X7.js').then(({ initStudio }) => {
437
706
  if (disposed) return;
438
- initStudio({ initialId, editorTitle, resolveImage });
707
+ cleanup = initStudio({
708
+ initialId,
709
+ editorTitle,
710
+ resolveImage,
711
+ importDocument,
712
+ plugins: mountedPlugins,
713
+ resolvePluginPermissions: permissionResolver
714
+ });
439
715
  });
440
716
  return () => {
441
717
  disposed = true;
718
+ cleanup?.();
442
719
  document.body.classList.remove("editor-active");
443
720
  document.documentElement.classList.remove("editor-active");
444
721
  };
445
- }, [editorTitle, initialId, repository, resolveImage]);
722
+ }, [
723
+ editorTitle,
724
+ importDocument,
725
+ initialId,
726
+ mountedPlugins,
727
+ repository,
728
+ resolveImage,
729
+ permissionResolver
730
+ ]);
446
731
  return /* @__PURE__ */ jsx("section", { className: "studio-shell w-full", "data-pagefind-ignore": "all", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: SKELETON } }) });
447
732
  }
448
733
  function browserStorage() {
@@ -530,7 +815,228 @@ function createLocalStorageRepository(options = {}) {
530
815
  }
531
816
  };
532
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
+ }
533
1039
 
534
- export { Studio, StudioMount, createLocalStorageRepository };
1040
+ export { RESPONSIVE_VIEWPORTS, Studio, StudioMount, addDefaultResponsiveVariants, appendStoryEdge, createLinterPlugin, createLocalStorageRepository, createPerformanceProfilerPlugin, createResponsiveInspectorPlugin, createStoryEditorPlugin, createTemplateEditorPlugin, createVisualRegressionPlugin, initialParameterValue, parameterInputValue, profileAnimation, replaceStoryNodeDocument };
535
1041
  //# sourceMappingURL=index.js.map
536
1042
  //# sourceMappingURL=index.js.map