@kokoa/clotho-editor 0.1.4 → 0.3.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-N47HF4VB.js';
2
+ export { addAppearance, addCameraFocus, addChapter, addCheckpoint, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, cameraControlAt, canRedo, canUndo, childIdsOf, clearCamera, configureAnimationRepository, configureApi, configureHost, createEditorPluginContext, createLayout, deleteCameraFocus, deleteChapter, deleteCheckpoint, deleteEffect, deleteElement, detachFromLayout, downloadAnimationJson, endTransient, findLayoutCollisions, garbageCollectAnimationAssets, getCamera, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, hasCamera, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, layoutIdsFor, markClean, mountEditorPlugins, moveCameraKeyframe, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeCameraKeyframe, removeCameraTrack, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCameraKeyframe, setCameraStrokeScaling, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueCheckpointId, uniqueEffectId, uniqueElementId, updateAppearance, updateCameraFocus, updateCanvas, updateChapter, updateCheckpoint, updateData, updateDuration, updateEffect, updateElementBase, updateLocales, updateMeta, updateResponsive, updateSettings, validateEditorPlugin } from './chunk-N47HF4VB.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,11 +485,13 @@ 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>
244
492
  <button type="button" id="studio-redo" class="studio-btn studio-btn-icon" aria-label="\uB2E4\uC2DC \uC2E4\uD589" title="\uB2E4\uC2DC \uC2E4\uD589 (\u2318\u21E7Z / \u2318Y)" disabled>\u21B7</button>
245
493
  <button type="button" id="studio-grid-toggle" class="studio-btn studio-btn-grid" aria-label="\uACA9\uC790 + \uC2A4\uB0C5" title="\uACA9\uC790 + \uC2A4\uB0C5 (G)" aria-pressed="false"><span class="studio-btn-grid-icon">\u229E</span><span class="studio-btn-grid-label" id="studio-grid-label">\uACA9\uC790 \uB054</span></button>
494
+ <button type="button" id="studio-camera-frame-toggle" class="studio-btn studio-btn-grid" aria-label="\uCE74\uBA54\uB77C \uC601\uC5ED \uD45C\uC2DC" title="\uCE94\uBC84\uC2A4\uC5D0 \uCE74\uBA54\uB77C\uAC00 \uBCF4\uC5EC\uC8FC\uB294 \uC601\uC5ED\uC744 \uD45C\uC2DC\uD569\uB2C8\uB2E4" aria-pressed="true"><span class="studio-btn-grid-icon">\u{1F3A5}</span><span class="studio-btn-grid-label" id="studio-camera-frame-label">\uCE74\uBA54\uB77C \uC601\uC5ED</span></button>
246
495
  <input type="text" id="studio-title" class="studio-title-input" placeholder="\uC81C\uBAA9" aria-label="\uC560\uB2C8\uBA54\uC774\uC158 \uC81C\uBAA9" disabled />
247
496
  <span id="studio-id-display" class="studio-id-display"></span>
248
497
  <span id="studio-status" class="studio-status" aria-live="polite">\uB300\uAE30 \uC911</span>
@@ -270,6 +519,7 @@ var SKELETON = `
270
519
  <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
520
  <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
521
  </div>
522
+ <div class="studio-plugin-panels" data-editor-plugin-slot="panel"></div>
273
523
  <div class="studio-tools-section">
274
524
  <div class="studio-tools-title">\uCE94\uBC84\uC2A4 \uD06C\uAE30</div>
275
525
  <div class="studio-canvas-size-row">
@@ -311,6 +561,7 @@ var SKELETON = `
311
561
  <div id="studio-props-content" class="studio-props-content">
312
562
  <p class="studio-props-empty">\uC694\uC18C \uB610\uB294 step \uC744 \uC120\uD0DD\uD558\uC138\uC694.</p>
313
563
  </div>
564
+ <div class="studio-plugin-inspectors" data-editor-plugin-slot="inspector"></div>
314
565
  </aside>
315
566
  </div>
316
567
 
@@ -426,24 +677,58 @@ function StudioMount({
426
677
  initialId,
427
678
  repository,
428
679
  editorTitle = "Clotho Editor",
429
- resolveImage
680
+ resolveImage,
681
+ importDocument,
682
+ plugins = EMPTY_EDITOR_PLUGINS,
683
+ resolvePluginPermissions = DENY_PLUGIN_PERMISSIONS,
684
+ templates = EMPTY_TEMPLATES,
685
+ story
430
686
  }) {
431
687
  const inited = useRef(false);
688
+ const mountedPlugins = useMemo(
689
+ () => [
690
+ ...templates.length > 0 ? [createTemplateEditorPlugin(templates)] : [],
691
+ ...story ? [createStoryEditorPlugin(story)] : [],
692
+ ...plugins
693
+ ],
694
+ [plugins, templates, story]
695
+ );
696
+ const permissionResolver = useMemo(
697
+ () => (plugin) => plugin.id === "dev.clotho.templates" || plugin.id === "dev.clotho.story" ? { ui: true, documentRead: true, documentWrite: true } : resolvePluginPermissions(plugin),
698
+ [resolvePluginPermissions]
699
+ );
432
700
  useEffect(() => {
433
701
  if (inited.current) return;
434
702
  inited.current = true;
435
703
  if (repository) configureAnimationRepository(repository);
436
704
  let disposed = false;
437
- void import('./main-NCPWDB2U.js').then(({ initStudio }) => {
705
+ let cleanup;
706
+ void import('./main-DC6D6RNQ.js').then(({ initStudio }) => {
438
707
  if (disposed) return;
439
- initStudio({ initialId, editorTitle, resolveImage });
708
+ cleanup = initStudio({
709
+ initialId,
710
+ editorTitle,
711
+ resolveImage,
712
+ importDocument,
713
+ plugins: mountedPlugins,
714
+ resolvePluginPermissions: permissionResolver
715
+ });
440
716
  });
441
717
  return () => {
442
718
  disposed = true;
719
+ cleanup?.();
443
720
  document.body.classList.remove("editor-active");
444
721
  document.documentElement.classList.remove("editor-active");
445
722
  };
446
- }, [editorTitle, initialId, repository, resolveImage]);
723
+ }, [
724
+ editorTitle,
725
+ importDocument,
726
+ initialId,
727
+ mountedPlugins,
728
+ repository,
729
+ resolveImage,
730
+ permissionResolver
731
+ ]);
447
732
  return /* @__PURE__ */ jsx("section", { className: "studio-shell w-full", "data-pagefind-ignore": "all", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: SKELETON } }) });
448
733
  }
449
734
  function browserStorage() {
@@ -531,7 +816,228 @@ function createLocalStorageRepository(options = {}) {
531
816
  }
532
817
  };
533
818
  }
819
+ function downloadReport(html) {
820
+ const url = URL.createObjectURL(new Blob([html], { type: "text/html" }));
821
+ const link = document.createElement("a");
822
+ link.href = url;
823
+ link.download = "clotho-animation-qa.html";
824
+ link.click();
825
+ URL.revokeObjectURL(url);
826
+ }
827
+ function createVisualRegressionPlugin() {
828
+ return {
829
+ manifest: {
830
+ id: "dev.clotho.visual-regression",
831
+ name: "Animation QA",
832
+ capabilities: ["editor"],
833
+ editor: { inspectors: ["animation-qa"] }
834
+ },
835
+ inspectors: {
836
+ "animation-qa": {
837
+ id: "animation-qa",
838
+ label: "Animation QA",
839
+ mount(container, context) {
840
+ container.classList.add("studio-qa-panel");
841
+ const title = document.createElement("strong");
842
+ title.textContent = "Animation QA";
843
+ const run = document.createElement("button");
844
+ run.type = "button";
845
+ run.className = "studio-btn";
846
+ run.textContent = "\uC804\uCCB4 frame \uAC80\uC0AC";
847
+ const status = document.createElement("p");
848
+ status.className = "studio-props-empty";
849
+ container.append(title, run, status);
850
+ run.addEventListener("click", () => {
851
+ const animation = context.getDocument();
852
+ const errors = [];
853
+ for (const time of animationSampleTimes(animation)) {
854
+ for (const element of animation.elements) {
855
+ if (!["rect", "circle", "image", "code"].includes(element.type))
856
+ continue;
857
+ try {
858
+ expectAnimation(animation).at(time).insideCanvas(element.id);
859
+ } catch (error) {
860
+ if (error instanceof AnimationAssertionError)
861
+ errors.push(error);
862
+ }
863
+ }
864
+ }
865
+ const snapshots = snapshotAnimationMatrix(animation);
866
+ if (errors.length === 0) {
867
+ status.textContent = `${snapshots.length}\uAC1C locale\xB7theme frame\uC744 \uAC80\uC0AC\uD588\uC2B5\uB2C8\uB2E4. \uBB38\uC81C\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.`;
868
+ status.removeAttribute("data-error");
869
+ } else {
870
+ status.textContent = `${errors.length}\uAC1C \uBB38\uC81C\uB97C \uBC1C\uACAC\uD588\uC2B5\uB2C8\uB2E4. HTML report\uB97C \uB0B4\uB824\uBC1B\uC2B5\uB2C8\uB2E4.`;
871
+ status.dataset.error = "true";
872
+ downloadReport(animationFailureReport(errors));
873
+ }
874
+ });
875
+ }
876
+ }
877
+ }
878
+ };
879
+ }
880
+ var RESPONSIVE_VIEWPORTS = [
881
+ { id: "compact", label: "\uBAA8\uBC14\uC77C", width: 375 },
882
+ { id: "regular", label: "\uBCF8\uBB38", width: 768 },
883
+ { id: "wide", label: "\uB113\uC740 \uD654\uBA74", width: 1280 }
884
+ ];
885
+ function addDefaultResponsiveVariants(document2) {
886
+ if (document2.responsive?.length) return document2;
887
+ return {
888
+ ...document2,
889
+ responsive: [
890
+ {
891
+ id: "compact",
892
+ minWidth: 0,
893
+ maxWidth: 479,
894
+ chapterListPosition: "bottom",
895
+ elementOverrides: {}
896
+ },
897
+ { id: "regular", minWidth: 480, maxWidth: 959, elementOverrides: {} },
898
+ { id: "wide", minWidth: 960, elementOverrides: {} }
899
+ ]
900
+ };
901
+ }
902
+ function createResponsiveInspectorPlugin() {
903
+ return {
904
+ manifest: {
905
+ id: "dev.clotho.responsive",
906
+ name: "Responsive Stage",
907
+ capabilities: ["editor"],
908
+ editor: { inspectors: ["responsive"] }
909
+ },
910
+ inspectors: {
911
+ responsive: {
912
+ id: "responsive",
913
+ label: "Responsive Stage",
914
+ mount(container, context) {
915
+ const render = () => {
916
+ const document2 = context.getDocument();
917
+ 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>`;
918
+ };
919
+ const click = (event) => {
920
+ if (!event.target.closest(
921
+ "[data-responsive-defaults]"
922
+ ))
923
+ return;
924
+ context.replaceDocument(
925
+ addDefaultResponsiveVariants(context.getDocument())
926
+ );
927
+ render();
928
+ };
929
+ container.addEventListener("click", click);
930
+ render();
931
+ return () => container.removeEventListener("click", click);
932
+ }
933
+ }
934
+ }
935
+ };
936
+ }
937
+ function profileAnimation(document2, sampleCount = 30, frameBudgetMs = 16.7) {
938
+ const plan = compileSceneDependencyPlan(document2);
939
+ const samples = [];
940
+ for (let index = 0; index < sampleCount; index += 1) {
941
+ const time = sampleCount === 1 ? 0 : document2.duration * index / (sampleCount - 1);
942
+ const started = performance.now();
943
+ buildScene(document2, time);
944
+ samples.push(performance.now() - started);
945
+ }
946
+ const totalMs = samples.reduce((sum, value) => sum + value, 0);
947
+ const maxMs = Math.max(0, ...samples);
948
+ return {
949
+ elementCount: plan.elementCount,
950
+ trackCount: plan.trackCount,
951
+ keyframeCount: plan.keyframeCount,
952
+ sampleCount,
953
+ totalMs,
954
+ averageMs: totalMs / Math.max(1, sampleCount),
955
+ maxMs,
956
+ overBudget: maxMs > frameBudgetMs
957
+ };
958
+ }
959
+ function createPerformanceProfilerPlugin() {
960
+ return {
961
+ manifest: {
962
+ id: "dev.clotho.performance",
963
+ name: "Scene Profiler",
964
+ capabilities: ["editor"],
965
+ editor: { inspectors: ["performance"] }
966
+ },
967
+ inspectors: {
968
+ performance: {
969
+ id: "performance",
970
+ label: "Scene Profiler",
971
+ mount(container, context) {
972
+ const run = () => {
973
+ const profile = profileAnimation(context.getDocument());
974
+ 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>`;
975
+ };
976
+ const click = (event) => {
977
+ if (event.target.closest("[data-run-profiler]"))
978
+ run();
979
+ };
980
+ container.addEventListener("click", click);
981
+ run();
982
+ return () => container.removeEventListener("click", click);
983
+ }
984
+ }
985
+ }
986
+ };
987
+ }
988
+ function createLinterPlugin() {
989
+ return {
990
+ manifest: {
991
+ id: "dev.clotho.linter",
992
+ name: "Clotho Linter",
993
+ capabilities: ["editor"],
994
+ editor: { inspectors: ["linter"] }
995
+ },
996
+ inspectors: {
997
+ linter: {
998
+ id: "linter",
999
+ label: "Clotho Linter",
1000
+ mount(container, context) {
1001
+ let selectedIndex = 0;
1002
+ const render = () => {
1003
+ const document2 = context.getDocument();
1004
+ const issues = lintDocument(document2);
1005
+ const selected = issues[selectedIndex] ?? issues[0];
1006
+ const fixed = autofixDocument(document2).document;
1007
+ 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>` : ""}`;
1008
+ };
1009
+ const click = (event) => {
1010
+ const target = event.target;
1011
+ if (target.closest("[data-lint-fix]")) {
1012
+ context.replaceDocument(
1013
+ autofixDocument(context.getDocument()).document
1014
+ );
1015
+ selectedIndex = 0;
1016
+ render();
1017
+ return;
1018
+ }
1019
+ const issue = target.closest("[data-lint-index]");
1020
+ if (!issue) return;
1021
+ selectedIndex = Number(issue.dataset.lintIndex);
1022
+ if (issue.dataset.elementId)
1023
+ context.setSelection({
1024
+ kind: "element",
1025
+ elementId: issue.dataset.elementId
1026
+ });
1027
+ render();
1028
+ };
1029
+ container.addEventListener("click", click);
1030
+ render();
1031
+ return () => container.removeEventListener("click", click);
1032
+ }
1033
+ }
1034
+ }
1035
+ };
1036
+ }
1037
+ function escapeHtml2(value) {
1038
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
1039
+ }
534
1040
 
535
- export { Studio, StudioMount, createLocalStorageRepository };
1041
+ export { RESPONSIVE_VIEWPORTS, Studio, StudioMount, addDefaultResponsiveVariants, appendStoryEdge, createLinterPlugin, createLocalStorageRepository, createPerformanceProfilerPlugin, createResponsiveInspectorPlugin, createStoryEditorPlugin, createTemplateEditorPlugin, createVisualRegressionPlugin, initialParameterValue, parameterInputValue, profileAnimation, replaceStoryNodeDocument };
536
1042
  //# sourceMappingURL=index.js.map
537
1043
  //# sourceMappingURL=index.js.map