@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.
@@ -1,5 +1,5 @@
1
- import { updateCanvas, getDef, subscribe, updateMeta, downloadAnimationJson, importAnimation, setDef, markClean, undo, redo, updateSettings, isDirty, canUndo, canRedo, isDraft, uniqueChapterId, getCurrentTime, addChapter, setSelection, uniqueElementId, registerExternalAsset, addElement, registerDataUriAsset, listAnimations, saveAnimation, deleteAnimation, duplicateAnimation, createAnimation, getSelection, getSelectedElementIds, deleteElement, deleteChapter, deleteEffect, updateChapter, isGroup, ungroupElement, groupElements, moveElementToEnd, moveElementToFront, reorderElement, loadAnimation, setDraft, getCurrentSnapshot, setElementValueAtTime, findContainingGroup, isElementSelected, updateElementBase, uniqueEffectId, addEffect, removeAppearance, removeTrack, removeTrackKeyframe, setTrackKeyframe, setCurrentTime, addAppearance, jumpBack, jumpForward, getHistory, promoteDraftToSaved, toggleSelectionFor, moveGroupBy, placeholderImageUrl, groupBbox, updateLocales, updateDuration, updateAppearance, updateEffect, childIdsOf } from './chunk-GMGB7NIL.js';
2
- import { activeAppearance, animationDocumentSchema, resolveAsset } from '@kokoa/clotho';
1
+ import { updateCanvas, getDef, subscribe, updateMeta, mountEditorPlugins, setSelection, getSelection, setDef, undo, redo, downloadAnimationJson, importAnimation, markClean, updateSettings, isDirty, isDraft, canUndo, canRedo, uniqueChapterId, getCurrentTime, addChapter, uniqueElementId, registerExternalAsset, addElement, registerDataUriAsset, listAnimations, setDraft, saveAnimation, deleteAnimation, duplicateAnimation, createAnimation, getSelectedElementIds, deleteElement, deleteChapter, deleteEffect, updateChapter, isGroup, ungroupElement, groupElements, moveElementToEnd, moveElementToFront, reorderElement, loadAnimation, getCurrentSnapshot, setElementValueAtTime, findContainingGroup, isElementSelected, updateElementBase, createLayout, detachFromLayout, addCheckpoint, uniqueCheckpointId, deleteCheckpoint, uniqueEffectId, addEffect, removeAppearance, removeTrack, removeTrackKeyframe, setTrackKeyframe, setCurrentTime, addAppearance, jumpBack, jumpForward, getHistory, promoteDraftToSaved, toggleSelectionFor, moveGroupBy, placeholderImageUrl, groupBbox, layoutIdsFor, findLayoutCollisions, updateLocales, updateData, updateResponsive, updateDuration, updateCheckpoint, updateAppearance, updateEffect, childIdsOf } from './chunk-FAI2G3XK.js';
2
+ import { activeAppearance, animationDocumentSchema, bindablePropertiesFor, resolveAsset, compileDataBindings } from '@kokoa/clotho';
3
3
 
4
4
  // src/legacy/grid.ts
5
5
  var STORAGE_KEY = "studio.grid";
@@ -646,6 +646,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
646
646
  rotation: 0,
647
647
  appearances: [],
648
648
  tracks: [],
649
+ bindings: [],
649
650
  x: cx - 60,
650
651
  y: cy - 30,
651
652
  width: 120,
@@ -665,6 +666,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
665
666
  rotation: 0,
666
667
  appearances: [],
667
668
  tracks: [],
669
+ bindings: [],
668
670
  cx,
669
671
  cy,
670
672
  r: 36,
@@ -682,6 +684,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
682
684
  rotation: 0,
683
685
  appearances: [],
684
686
  tracks: [],
687
+ bindings: [],
685
688
  x1: cx - 80,
686
689
  y1: cy,
687
690
  x2: cx + 80,
@@ -698,6 +701,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
698
701
  rotation: 0,
699
702
  appearances: [],
700
703
  tracks: [],
704
+ bindings: [],
701
705
  x1: cx - 100,
702
706
  y1: cy,
703
707
  x2: cx + 100,
@@ -718,10 +722,12 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
718
722
  rotation: 0,
719
723
  appearances: [],
720
724
  tracks: [],
725
+ bindings: [],
721
726
  x: cx,
722
727
  y: cy,
723
728
  content: id,
724
729
  translations: {},
730
+ references: {},
725
731
  fontSize: 18,
726
732
  fontWeight: 400,
727
733
  color: "#18181b",
@@ -734,6 +740,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
734
740
  rotation: 0,
735
741
  appearances: [],
736
742
  tracks: [],
743
+ bindings: [],
737
744
  x: cx - 50,
738
745
  y: cy - 50,
739
746
  width: 100,
@@ -749,6 +756,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
749
756
  rotation: 0,
750
757
  appearances: [],
751
758
  tracks: [],
759
+ bindings: [],
752
760
  x: cx - 40,
753
761
  y: cy - 40,
754
762
  d: "M 0 0 L 80 0 L 40 80 Z",
@@ -770,6 +778,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
770
778
  rotation: 0,
771
779
  appearances: [],
772
780
  tracks: [],
781
+ bindings: [],
773
782
  points: pts,
774
783
  fill: "#a5b4fc",
775
784
  stroke: "#6366f1",
@@ -1045,8 +1054,6 @@ function renderSelectionOutline(canvasEl2, elId, snap2, byId) {
1045
1054
  rect.classList.add("element-selected-outline");
1046
1055
  return rect;
1047
1056
  }
1048
-
1049
- // src/legacy/canvas-preview.ts
1050
1057
  var previewRoot = null;
1051
1058
  var previewHandle = null;
1052
1059
  var unsubscribePreview = null;
@@ -1075,7 +1082,7 @@ function showPreview(canvasEl2, def, options) {
1075
1082
  previewRoot.style.cssText = `position:absolute;left:0;top:0;width:${def.canvas.width}px;height:${def.canvas.height}px;background:${bg};z-index:2;overflow:hidden;`;
1076
1083
  parent.style.position = "relative";
1077
1084
  parent.appendChild(previewRoot);
1078
- void mountPreview(previewRoot, def, options);
1085
+ void mountPreview(previewRoot, compileDataBindings(def).document, options);
1079
1086
  }
1080
1087
  function hidePreview(canvasEl2) {
1081
1088
  if (canvasEl2) canvasEl2.style.visibility = "";
@@ -1824,6 +1831,7 @@ function onMouseUp(e) {
1824
1831
  rotation: 0,
1825
1832
  appearances: [],
1826
1833
  tracks: [],
1834
+ bindings: [],
1827
1835
  fromId: connectState.fromId,
1828
1836
  toId: elemId,
1829
1837
  fromAnchor: connectState.fromAnchor,
@@ -2708,6 +2716,41 @@ function parseLocales(value) {
2708
2716
  return true;
2709
2717
  });
2710
2718
  }
2719
+ function annotationTokens(...values) {
2720
+ const tokens = values.flatMap(
2721
+ (value) => [...value.matchAll(/\{([a-z][a-z0-9_-]*)\}/g)].map((match) => match[1])
2722
+ );
2723
+ return [...new Set(tokens)];
2724
+ }
2725
+ function annotationReferenceFields(keyPrefix, values, references = {}) {
2726
+ const tokens = annotationTokens(...values);
2727
+ if (tokens.length === 0) {
2728
+ return [
2729
+ '<p class="studio-props-empty studio-annotation-hint">\uBB38\uAD6C\uC5D0 {token}\uC744 \uC785\uB825\uD558\uBA74 \uC7A5\uBA74 \uC694\uC18C\uC640 \uC5F0\uACB0\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</p>'
2730
+ ];
2731
+ }
2732
+ return [
2733
+ '<p class="studio-props-empty studio-annotation-hint">\uB300\uC0C1 element id\uB97C \uC27C\uD45C\uB85C \uAD6C\uBD84\uD558\uC138\uC694. \uC5EC\uB7EC \uC694\uC18C\uB97C \uD568\uAED8 \uAC15\uC870\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.</p>',
2734
+ ...tokens.map((token) => {
2735
+ const target = references[token];
2736
+ const value = typeof target === "string" ? target : target?.join(", ") ?? "";
2737
+ return textField(
2738
+ `{${token}} \uB300\uC0C1`,
2739
+ `${keyPrefix}.reference.${token}`,
2740
+ value
2741
+ );
2742
+ })
2743
+ ];
2744
+ }
2745
+ function parseReferenceTargets(value) {
2746
+ const targets = [
2747
+ ...new Set(
2748
+ value.split(",").map((id) => id.trim()).filter(Boolean)
2749
+ )
2750
+ ];
2751
+ if (targets.length === 0) return void 0;
2752
+ return targets.length === 1 ? targets[0] : targets;
2753
+ }
2711
2754
  function initProperties(root) {
2712
2755
  panelEl = root;
2713
2756
  subscribe(render3);
@@ -2779,6 +2822,9 @@ function textField(label, key, value) {
2779
2822
  <input type="text" data-prop-key="${escapeHtml2(key)}" value="${escapeHtml2(value ?? "")}" />
2780
2823
  </label>`;
2781
2824
  }
2825
+ function textareaField(label, key, value) {
2826
+ return `<label class="studio-field"><span>${escapeHtml2(label)}</span><textarea rows="7" spellcheck="false" data-prop-key="${escapeHtml2(key)}">${escapeHtml2(value)}</textarea></label>`;
2827
+ }
2782
2828
  function numberField(label, key, value, step = 1) {
2783
2829
  return `<label class="studio-field">
2784
2830
  <span>${escapeHtml2(label)}</span>
@@ -2833,7 +2879,18 @@ function renderInner() {
2833
2879
  "canvas.background",
2834
2880
  "canvas.background",
2835
2881
  def.canvas.background
2836
- )
2882
+ ),
2883
+ textareaField(
2884
+ "\uC0D8\uD50C \uB370\uC774\uD130 (JSON)",
2885
+ "meta.data",
2886
+ JSON.stringify(def.data, null, 2)
2887
+ ),
2888
+ textareaField(
2889
+ "Responsive variants (JSON)",
2890
+ "meta.responsive",
2891
+ JSON.stringify(def.responsive ?? [], null, 2)
2892
+ ),
2893
+ `<p class="studio-props-empty">JSON Pointer\uB85C \uC694\uC18C \uC18D\uC131\uC5D0 \uC5F0\uACB0\uD569\uB2C8\uB2E4. \uC774 \uB370\uC774\uD130\uB294 \uBBF8\uB9AC\uBCF4\uAE30\uC640 \uB0B4\uBCF4\uB0B4\uAE30\uC5D0 \uD568\uAED8 \uC800\uC7A5\uB429\uB2C8\uB2E4.</p>`
2837
2894
  ].join("");
2838
2895
  const settingsHeader = `<span class="studio-props-header-title">\uC124\uC815</span>`;
2839
2896
  const settingsBody = [
@@ -2861,10 +2918,61 @@ function renderInner() {
2861
2918
  ]
2862
2919
  )
2863
2920
  ].join("");
2921
+ const checkpoints = def.checkpoints.map((checkpoint, index) => {
2922
+ const specific = checkpoint.interaction === "choice" ? textField(
2923
+ "\uC120\uD0DD\uC9C0 (value:label, \uC27C\uD45C \uAD6C\uBD84)",
2924
+ `checkpoint.${index}.options`,
2925
+ checkpoint.options.map(({ value, label }) => `${value}:${label}`).join(", ")
2926
+ ) : checkpoint.interaction === "select-element" ? textField(
2927
+ "\uC120\uD0DD \uAC00\uB2A5\uD55C element id",
2928
+ `checkpoint.${index}.elementIds`,
2929
+ checkpoint.elementIds.join(", ")
2930
+ ) : checkpoint.interaction === "number-input" ? [
2931
+ numberField(
2932
+ "\uCD5C\uC19F\uAC12",
2933
+ `checkpoint.${index}.min`,
2934
+ checkpoint.min
2935
+ ),
2936
+ numberField(
2937
+ "\uCD5C\uB313\uAC12",
2938
+ `checkpoint.${index}.max`,
2939
+ checkpoint.max
2940
+ ),
2941
+ numberField(
2942
+ "\uAC04\uACA9",
2943
+ `checkpoint.${index}.step`,
2944
+ checkpoint.step
2945
+ )
2946
+ ].join("") : "";
2947
+ const predicate = "predicate" in checkpoint && checkpoint.predicate?.type === "equals" ? textField(
2948
+ "\uC815\uB2F5 (equals)",
2949
+ `checkpoint.${index}.answer`,
2950
+ String(checkpoint.predicate.value)
2951
+ ) : "";
2952
+ return `<div class="studio-checkpoint-card" data-checkpoint-id="${escapeHtml2(checkpoint.id)}">
2953
+ <div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(checkpoint.id)}</span><button type="button" class="studio-btn studio-btn-danger" data-delete-checkpoint="${escapeHtml2(checkpoint.id)}">\uC0AD\uC81C</button></div>
2954
+ ${numberField("time (ms)", `checkpoint.${index}.time`, checkpoint.time, 50)}
2955
+ ${textField("\uC9C8\uBB38", `checkpoint.${index}.prompt`, checkpoint.prompt)}
2956
+ ${selectField(
2957
+ "\uC0C1\uD638\uC791\uC6A9",
2958
+ `checkpoint.${index}.interaction`,
2959
+ checkpoint.interaction,
2960
+ [
2961
+ { value: "continue", label: "\uACC4\uC18D" },
2962
+ { value: "choice", label: "\uC120\uD0DD\uC9C0" },
2963
+ { value: "select-element", label: "\uC694\uC18C \uC120\uD0DD" },
2964
+ { value: "number-input", label: "\uC22B\uC790 \uC785\uB825" }
2965
+ ]
2966
+ )}
2967
+ ${checkboxField("\uC751\uB2F5 \uD544\uC218", `checkpoint.${index}.required`, checkpoint.required)}
2968
+ ${specific}${predicate}
2969
+ </div>`;
2970
+ }).join("");
2864
2971
  panelEl.innerHTML = `
2865
2972
  ${timeHint}
2866
2973
  ${section("meta", metaHeader, metaBody)}
2867
2974
  ${section("settings", settingsHeader, settingsBody)}
2975
+ ${section("checkpoints", `<span class="studio-props-header-title">Checkpoint (${def.checkpoints.length})</span>`, `${checkpoints}<button type="button" class="studio-btn" data-add-checkpoint>\uFF0B \uD604\uC7AC \uC2DC\uAC04\uC5D0 checkpoint \uCD94\uAC00</button>`)}
2868
2976
  <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uBAA9\uCC28 (${def.chapters.length})</span></div>
2869
2977
  <button type="button" class="studio-btn" data-add-chapter>\uFF0B \uD604\uC7AC \uC2DC\uAC04\uC5D0 chapter \uCD94\uAC00</button>
2870
2978
  <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uD6A8\uACFC (${def.effects.length})</span></div>
@@ -2882,6 +2990,10 @@ function renderInner() {
2882
2990
  if (sel.kind === "elements") {
2883
2991
  const alignBtn = (kind, label, title) => `<button type="button" class="studio-btn studio-align-btn" data-align="${kind}" title="${escapeHtml2(title)}" aria-label="${escapeHtml2(title)}">${label}</button>`;
2884
2992
  const distributeDisabled = sel.elementIds.length < 3 ? "disabled" : "";
2993
+ const layoutIds = layoutIdsFor(sel.elementIds);
2994
+ const collisions = findLayoutCollisions(def).filter(
2995
+ ({ firstId, secondId }) => sel.elementIds.includes(firstId) || sel.elementIds.includes(secondId)
2996
+ );
2885
2997
  panelEl.innerHTML = `
2886
2998
  ${timeHint}
2887
2999
  <div class="studio-props-header"><span class="studio-props-header-title">\uB2E4\uC911 \uC120\uD0DD</span><span class="studio-props-header-type">${sel.elementIds.length} elements</span></div>
@@ -2905,6 +3017,16 @@ function renderInner() {
2905
3017
  <button type="button" class="studio-btn studio-align-btn" data-distribute="vertical" title="\uC138\uB85C \uADE0\uB4F1 \uBD84\uD3EC" ${distributeDisabled}>\u2195</button>
2906
3018
  </div>
2907
3019
  </div>
3020
+ <div class="studio-align-section studio-layout-section">
3021
+ <div class="studio-align-title">Constraint Layout</div>
3022
+ <div class="studio-align-row">
3023
+ <button type="button" class="studio-btn" data-create-layout="row">\uAC00\uB85C</button>
3024
+ <button type="button" class="studio-btn" data-create-layout="column">\uC138\uB85C</button>
3025
+ <button type="button" class="studio-btn" data-create-layout="grid">\uACA9\uC790</button>
3026
+ </div>
3027
+ ${layoutIds.length > 0 ? `<p class="studio-layout-status">\uC801\uC6A9 \uC911: ${layoutIds.map(escapeHtml2).join(", ")}</p><button type="button" class="studio-btn" data-detach-layout>\uD604\uC7AC \uC88C\uD45C\uB85C \uACE0\uC815</button>` : '<p class="studio-layout-status">\uC120\uD0DD\uD55C \uC694\uC18C\uC5D0 \uC801\uC6A9\uB41C layout\uC774 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}
3028
+ ${collisions.length > 0 ? `<p class="studio-layout-collision" role="alert">\uACB9\uCE68: ${collisions.map(({ firstId, secondId }) => `${escapeHtml2(firstId)} \u2194 ${escapeHtml2(secondId)}`).join(", ")}</p>` : ""}
3029
+ </div>
2908
3030
  <ul style="font-family:var(--font-mono);font-size:0.72rem;color:var(--color-fg-muted);padding-left:1rem;margin:0.6rem 0 0">
2909
3031
  ${sel.elementIds.map((id) => `<li>${escapeHtml2(id)}</li>`).join("")}
2910
3032
  </ul>
@@ -2926,12 +3048,14 @@ function renderInner() {
2926
3048
  setSelection({ kind: "none" });
2927
3049
  return;
2928
3050
  }
3051
+ const chapterReferences = ch.references;
2929
3052
  panelEl.innerHTML = `
2930
3053
  ${timeHint}
2931
3054
  <div class="studio-props-header"><span class="studio-props-header-title">${escapeHtml2(ch.id)}</span><span class="studio-props-header-type">chapter</span></div>
2932
3055
  ${numberField("time (ms)", "chapter.time", ch.time, 50)}
2933
3056
  ${textField("label", "chapter.label", ch.label)}
2934
3057
  ${textField("subtitle", "chapter.subtitle", ch.subtitle)}
3058
+ ${annotationReferenceFields("chapter", [ch.label, ch.subtitle], chapterReferences).join("")}
2935
3059
  <button type="button" class="studio-btn studio-btn-danger" data-delete-chapter style="margin-top:0.6rem">\u{1F5D1} chapter \uC0AD\uC81C</button>
2936
3060
  `;
2937
3061
  return;
@@ -2988,6 +3112,7 @@ function renderElementForm(def, el) {
2988
3112
  const baseFields = renderBaseFields(def, el);
2989
3113
  const appearances = renderAppearances(def, el);
2990
3114
  const tracks = renderTracks(el);
3115
+ const bindings = renderBindings(el);
2991
3116
  const baseHeader = `<span class="studio-props-header-title">${escapeHtml2(el.id)}</span><span class="studio-props-header-type">${escapeHtml2(el.type)}</span>`;
2992
3117
  const apHeader = `<span class="studio-props-header-title">\uCD9C\uD604 (Appearances)</span><button type="button" class="studio-btn studio-btn-small" data-add-appearance>\uFF0B</button>`;
2993
3118
  const tracksHeader = `<span class="studio-props-header-title">\uD0A4\uD504\uB808\uC784 \uD2B8\uB799 (${el.tracks.length})</span>`;
@@ -2996,12 +3121,45 @@ function renderElementForm(def, el) {
2996
3121
  ${section("el-base", baseHeader, baseFields)}
2997
3122
  ${section("el-appearances", apHeader, appearances)}
2998
3123
  ${section("el-tracks", tracksHeader, tracks)}
3124
+ ${section("el-bindings", `<span class="studio-props-header-title">\uB370\uC774\uD130 \uC5F0\uACB0 (${el.bindings.length})</span>`, bindings)}
2999
3125
  <div class="studio-props-empty" style="font-size:0.72rem;margin-top:0.5rem">
3000
3126
  base \uC18D\uC131\uC744 \uBCC0\uACBD\uD558\uBA74 \u2192 t=${getCurrentTime()} ms \uC5D0 keyframe \uCD94\uAC00<br/>
3001
3127
  \uD2B8\uB799\uC774 \uC5C6\uB294 \uC18D\uC131\uC740 base \uAC12\uC774 \uD56D\uC0C1 \uC0AC\uC6A9\uB428
3002
3128
  </div>
3003
3129
  `;
3004
3130
  }
3131
+ function renderBindings(el) {
3132
+ const properties = bindablePropertiesFor(el);
3133
+ const rows = el.bindings.map(
3134
+ (binding, index) => `<div class="studio-appearance-row">
3135
+ <div class="studio-appearance-row-head"><span class="studio-appearance-row-title">#${index + 1}</span><button type="button" class="studio-btn studio-btn-small studio-btn-danger" data-delete-binding="${index}">\u2715</button></div>
3136
+ ${selectField(
3137
+ "\uC18D\uC131",
3138
+ `binding.${index}.property`,
3139
+ binding.property,
3140
+ properties.map((value) => ({ value }))
3141
+ )}
3142
+ ${textField("JSON Pointer", `binding.${index}.pointer`, binding.pointer)}
3143
+ ${selectField(
3144
+ "\uD45C\uD604 \uBC29\uC2DD",
3145
+ `binding.${index}.formatter`,
3146
+ binding.formatter,
3147
+ [
3148
+ "identity",
3149
+ "string",
3150
+ "number",
3151
+ "fixed",
3152
+ "percent",
3153
+ "uppercase",
3154
+ "lowercase",
3155
+ "color"
3156
+ ].map((value) => ({ value }))
3157
+ )}
3158
+ ${textField("\uB300\uCCB4 \uAC12", `binding.${index}.fallback`, binding.fallback === void 0 ? "" : String(binding.fallback))}
3159
+ </div>`
3160
+ ).join("");
3161
+ return `${rows || '<p class="studio-props-empty">\uC5F0\uACB0\uB41C \uB370\uC774\uD130\uAC00 \uC5C6\uC2B5\uB2C8\uB2E4.</p>'}<button type="button" class="studio-btn" data-add-binding ${properties.length === 0 ? "disabled" : ""}>\uFF0B \uB370\uC774\uD130 \uC5F0\uACB0</button>`;
3162
+ }
3005
3163
  function renderBaseFields(def, el) {
3006
3164
  const numberFields = [];
3007
3165
  const colorFields = [];
@@ -3133,6 +3291,11 @@ function renderBaseFields(def, el) {
3133
3291
  `el.translation.${locale}`,
3134
3292
  localized.translations?.[locale] ?? ""
3135
3293
  )
3294
+ ),
3295
+ ...annotationReferenceFields(
3296
+ "el",
3297
+ [el.content, ...Object.values(localized.translations ?? {})],
3298
+ localized.references
3136
3299
  )
3137
3300
  ];
3138
3301
  })();
@@ -3208,7 +3371,7 @@ function onInput(e) {
3208
3371
  if (!key) return;
3209
3372
  if (target.type === "text" && e.isComposing) return;
3210
3373
  if (target.type === "text" && isComposingFallback) return;
3211
- if (target.type === "color") {
3374
+ if (target instanceof HTMLInputElement && target.type === "color") {
3212
3375
  const textInput = target.parentElement?.querySelector(
3213
3376
  `input[type="text"][data-prop-key="${CSS.escape(key)}"]`
3214
3377
  );
@@ -3216,8 +3379,10 @@ function onInput(e) {
3216
3379
  return;
3217
3380
  }
3218
3381
  let value = target.value;
3219
- if (target.type === "number") value = Number(target.value);
3220
- else if (target.type === "checkbox") value = target.checked;
3382
+ if (target instanceof HTMLInputElement && target.type === "number")
3383
+ value = Number(target.value);
3384
+ else if (target instanceof HTMLInputElement && target.type === "checkbox")
3385
+ value = target.checked;
3221
3386
  apply(key, value);
3222
3387
  }
3223
3388
  function onChange(e) {
@@ -3239,6 +3404,22 @@ function apply(key, value) {
3239
3404
  else if (key === "meta.locales") {
3240
3405
  const locales = parseLocales(String(value));
3241
3406
  if (locales.length > 0) updateLocales(locales);
3407
+ } else if (key === "meta.data") {
3408
+ try {
3409
+ const parsed = JSON.parse(String(value));
3410
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed))
3411
+ updateData(parsed);
3412
+ } catch {
3413
+ }
3414
+ } else if (key === "meta.responsive") {
3415
+ try {
3416
+ const parsed = JSON.parse(String(value));
3417
+ if (Array.isArray(parsed))
3418
+ updateResponsive(
3419
+ parsed
3420
+ );
3421
+ } catch {
3422
+ }
3242
3423
  } else if (key === "meta.duration") updateDuration(Number(value));
3243
3424
  else if (key === "canvas.width") updateCanvas({ width: Number(value) });
3244
3425
  else if (key === "canvas.height") updateCanvas({ height: Number(value) });
@@ -3255,7 +3436,56 @@ function apply(key, value) {
3255
3436
  updateSettings({
3256
3437
  chapterListPosition: String(value)
3257
3438
  });
3258
- else if (key.startsWith("el.") && sel.kind === "element") {
3439
+ else if (key.startsWith("checkpoint.")) {
3440
+ const [, indexText, property] = key.split(".");
3441
+ const checkpoint = def.checkpoints[Number(indexText)];
3442
+ if (!checkpoint || !property) return;
3443
+ if (property === "interaction") {
3444
+ const base = {
3445
+ id: checkpoint.id,
3446
+ time: checkpoint.time,
3447
+ prompt: checkpoint.prompt,
3448
+ required: checkpoint.required
3449
+ };
3450
+ const interaction = String(value);
3451
+ const replacement = interaction === "choice" ? {
3452
+ ...base,
3453
+ interaction,
3454
+ options: [{ value: "option", label: "\uC120\uD0DD\uC9C0" }]
3455
+ } : interaction === "select-element" ? {
3456
+ ...base,
3457
+ interaction,
3458
+ elementIds: [def.elements[0]?.id ?? "element"]
3459
+ } : interaction === "number-input" ? { ...base, interaction } : { ...base, interaction: "continue" };
3460
+ updateCheckpoint(checkpoint.id, replacement);
3461
+ } else if (property === "time")
3462
+ updateCheckpoint(checkpoint.id, { time: Number(value) });
3463
+ else if (property === "prompt")
3464
+ updateCheckpoint(checkpoint.id, { prompt: String(value) });
3465
+ else if (property === "required")
3466
+ updateCheckpoint(checkpoint.id, { required: Boolean(value) });
3467
+ else if (property === "options" && checkpoint.interaction === "choice") {
3468
+ const options = String(value).split(",").map((entry) => entry.trim()).filter(Boolean).map((entry) => {
3469
+ const [optionValue, ...label] = entry.split(":");
3470
+ return {
3471
+ value: optionValue || "option",
3472
+ label: label.join(":") || optionValue || "\uC120\uD0DD\uC9C0"
3473
+ };
3474
+ });
3475
+ if (options.length > 0) updateCheckpoint(checkpoint.id, { options });
3476
+ } else if (property === "elementIds" && checkpoint.interaction === "select-element") {
3477
+ const elementIds = String(value).split(",").map((id) => id.trim()).filter(Boolean);
3478
+ if (elementIds.length > 0)
3479
+ updateCheckpoint(checkpoint.id, { elementIds });
3480
+ } else if (["min", "max", "step"].includes(property) && checkpoint.interaction === "number-input") {
3481
+ updateCheckpoint(checkpoint.id, { [property]: Number(value) });
3482
+ } else if (property === "answer" && checkpoint.interaction !== "continue") {
3483
+ const answer = checkpoint.interaction === "number-input" ? Number(value) : String(value);
3484
+ updateCheckpoint(checkpoint.id, {
3485
+ predicate: { type: "equals", value: answer }
3486
+ });
3487
+ }
3488
+ } else if (key.startsWith("el.") && sel.kind === "element") {
3259
3489
  const prop = key.slice(3);
3260
3490
  const time = getCurrentTime();
3261
3491
  const el = def.elements.find((e) => e.id === sel.elementId);
@@ -3276,6 +3506,16 @@ function apply(key, value) {
3276
3506
  updateElementBase(sel.elementId, { translations });
3277
3507
  return;
3278
3508
  }
3509
+ if (prop.startsWith("reference.") && el.type === "text") {
3510
+ const token = prop.slice("reference.".length);
3511
+ const annotated = el;
3512
+ const references = { ...annotated.references ?? {} };
3513
+ const targets = parseReferenceTargets(String(value));
3514
+ if (targets) references[token] = targets;
3515
+ else delete references[token];
3516
+ updateElementBase(sel.elementId, { references });
3517
+ return;
3518
+ }
3279
3519
  if (prop === "polygonSides" && el.type === "polygon") {
3280
3520
  const points = el.points.trim().split(/\s+/).map((point) => point.split(",").map(Number)).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
3281
3521
  if (points.length < 3) return;
@@ -3300,6 +3540,18 @@ function apply(key, value) {
3300
3540
  } else {
3301
3541
  updateElementBase(sel.elementId, { [prop]: value });
3302
3542
  }
3543
+ } else if (key.startsWith("binding.") && sel.kind === "element") {
3544
+ const [, indexText, property] = key.split(".");
3545
+ const el = def.elements.find((item) => item.id === sel.elementId);
3546
+ const binding = el?.bindings[Number(indexText)];
3547
+ if (!el || !binding || !property) return;
3548
+ const bindings = el.bindings.map(
3549
+ (item, index) => index === Number(indexText) ? {
3550
+ ...item,
3551
+ [property]: property === "fallback" ? parseBindingFallback(String(value)) : String(value)
3552
+ } : item
3553
+ );
3554
+ updateElementBase(el.id, { bindings });
3303
3555
  } else if (key.startsWith("ap.") && sel.kind === "element") {
3304
3556
  const [, idxStr, prop] = key.split(".");
3305
3557
  const idx = Number(idxStr);
@@ -3317,6 +3569,16 @@ function apply(key, value) {
3317
3569
  updateChapter(sel.chapterId, { label: String(value) });
3318
3570
  } else if (key === "chapter.subtitle" && sel.kind === "chapter") {
3319
3571
  updateChapter(sel.chapterId, { subtitle: String(value) });
3572
+ } else if (key.startsWith("chapter.reference.") && sel.kind === "chapter") {
3573
+ const chapter = def.chapters.find(({ id }) => id === sel.chapterId);
3574
+ if (!chapter) return;
3575
+ const token = key.slice("chapter.reference.".length);
3576
+ const annotated = chapter;
3577
+ const references = { ...annotated.references ?? {} };
3578
+ const targets = parseReferenceTargets(String(value));
3579
+ if (targets) references[token] = targets;
3580
+ else delete references[token];
3581
+ updateChapter(sel.chapterId, { references });
3320
3582
  } else if (key.startsWith("effect.") && sel.kind === "effect") {
3321
3583
  const prop = key.slice(7);
3322
3584
  const patch = {};
@@ -3328,11 +3590,44 @@ function apply(key, value) {
3328
3590
  updateEffect(sel.effectId, patch);
3329
3591
  }
3330
3592
  }
3593
+ function parseBindingFallback(value) {
3594
+ if (value === "") return void 0;
3595
+ if (value === "true") return true;
3596
+ if (value === "false") return false;
3597
+ const number = Number(value);
3598
+ return Number.isFinite(number) && value.trim() !== "" ? number : value;
3599
+ }
3331
3600
  function onClick2(e) {
3332
3601
  const target = e.target;
3333
3602
  const def = getDef();
3334
3603
  if (!def) return;
3335
3604
  const sel = getSelection();
3605
+ if (target.closest("[data-add-binding]") && sel.kind === "element") {
3606
+ const el = def.elements.find((item) => item.id === sel.elementId);
3607
+ if (!el) return;
3608
+ const property = bindablePropertiesFor(el).find(
3609
+ (candidate) => !el.bindings.some((binding) => binding.property === candidate)
3610
+ );
3611
+ if (property)
3612
+ updateElementBase(el.id, {
3613
+ bindings: [
3614
+ ...el.bindings,
3615
+ { property, pointer: "", formatter: "identity" }
3616
+ ]
3617
+ });
3618
+ return;
3619
+ }
3620
+ const deleteBinding = target.closest("[data-delete-binding]");
3621
+ if (deleteBinding && sel.kind === "element") {
3622
+ const el = def.elements.find((item) => item.id === sel.elementId);
3623
+ if (el)
3624
+ updateElementBase(el.id, {
3625
+ bindings: el.bindings.filter(
3626
+ (_, index) => index !== Number(deleteBinding.dataset.deleteBinding)
3627
+ )
3628
+ });
3629
+ return;
3630
+ }
3336
3631
  const alignBtn = target.closest("[data-align]");
3337
3632
  if (alignBtn) {
3338
3633
  alignSelected(alignBtn.dataset.align);
@@ -3343,6 +3638,37 @@ function onClick2(e) {
3343
3638
  distributeSelected(distBtn.dataset.distribute);
3344
3639
  return;
3345
3640
  }
3641
+ const createLayoutButton = target.closest(
3642
+ "[data-create-layout]"
3643
+ );
3644
+ if (createLayoutButton && sel.kind === "elements") {
3645
+ createLayout(
3646
+ sel.elementIds,
3647
+ createLayoutButton.dataset.createLayout
3648
+ );
3649
+ return;
3650
+ }
3651
+ if (target.closest("[data-detach-layout]") && sel.kind === "elements") {
3652
+ detachFromLayout(sel.elementIds);
3653
+ return;
3654
+ }
3655
+ if (target.closest("[data-add-checkpoint]")) {
3656
+ addCheckpoint({
3657
+ id: uniqueCheckpointId(),
3658
+ time: getCurrentTime(),
3659
+ prompt: "\uACC4\uC18D \uC9C4\uD589\uD560\uAE4C\uC694?",
3660
+ required: true,
3661
+ interaction: "continue"
3662
+ });
3663
+ return;
3664
+ }
3665
+ const deleteCheckpointButton = target.closest(
3666
+ "[data-delete-checkpoint]"
3667
+ );
3668
+ if (deleteCheckpointButton?.dataset.deleteCheckpoint) {
3669
+ deleteCheckpoint(deleteCheckpointButton.dataset.deleteCheckpoint);
3670
+ return;
3671
+ }
3346
3672
  if (target.closest("[data-ungroup]") && sel.kind === "element") {
3347
3673
  ungroupElement(sel.elementId);
3348
3674
  return;
@@ -3362,7 +3688,8 @@ function onClick2(e) {
3362
3688
  id,
3363
3689
  time: getCurrentTime(),
3364
3690
  label: `Chapter ${id.split("-")[1]}`,
3365
- subtitle: ""
3691
+ subtitle: "",
3692
+ references: {}
3366
3693
  });
3367
3694
  return;
3368
3695
  }
@@ -3497,7 +3824,8 @@ function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
3497
3824
  id,
3498
3825
  time: newTime,
3499
3826
  label: `Chapter ${id.split("-")[1]}`,
3500
- subtitle: ""
3827
+ subtitle: "",
3828
+ references: {}
3501
3829
  });
3502
3830
  setSelection({ kind: "chapter", chapterId: id });
3503
3831
  });
@@ -15348,6 +15676,7 @@ var IconLibraryDialog = class _IconLibraryDialog {
15348
15676
  rotation: 0,
15349
15677
  appearances: [],
15350
15678
  tracks: [],
15679
+ bindings: [],
15351
15680
  x: cx - 32,
15352
15681
  y: cy - 32,
15353
15682
  width: 64,
@@ -15408,6 +15737,7 @@ async function uploadAndInsertImage(file, host) {
15408
15737
  rotation: 0,
15409
15738
  appearances: [],
15410
15739
  tracks: [],
15740
+ bindings: [],
15411
15741
  x: Math.round(cx - w / 2),
15412
15742
  y: Math.round(cy - h / 2),
15413
15743
  width: w,
@@ -15831,6 +16161,8 @@ var KIND_ICON = {
15831
16161
  effect: "\u2728",
15832
16162
  asset: "\u{1F5BC}",
15833
16163
  group: "\u2B1A",
16164
+ layout: "\u25A6",
16165
+ checkpoint: "\u25C6",
15834
16166
  other: "\xB7"
15835
16167
  };
15836
16168
  var dialogEl2 = null;
@@ -16766,7 +17098,8 @@ function initStudio(opts = {}) {
16766
17098
  const ui = queryUi();
16767
17099
  if (!ui) {
16768
17100
  console.error("[studio] missing required elements");
16769
- return;
17101
+ return () => {
17102
+ };
16770
17103
  }
16771
17104
  document.body.classList.add("editor-active");
16772
17105
  document.documentElement.classList.add("editor-active");
@@ -16856,6 +17189,18 @@ function initStudio(opts = {}) {
16856
17189
  ui.newBtn.addEventListener("click", () => openNewDialog(ui));
16857
17190
  initPalette();
16858
17191
  initHistoryPanel();
17192
+ if (!getDef()) startDraft();
17193
+ const pluginMount = mountEditorPlugins(
17194
+ ui.app,
17195
+ opts.plugins ?? [],
17196
+ opts.resolvePluginPermissions ?? (() => ({})),
17197
+ {
17198
+ getDocument: getDef,
17199
+ replaceDocument: setDef,
17200
+ getSelection,
17201
+ setSelection
17202
+ }
17203
+ );
16859
17204
  registerCommands([
16860
17205
  {
16861
17206
  id: "open",
@@ -16901,7 +17246,8 @@ function initStudio(opts = {}) {
16901
17246
  hint: "G",
16902
17247
  run: () => setGridEnabled(!isGridEnabled())
16903
17248
  },
16904
- { id: "help", label: "\u2328 \uB2E8\uCD95\uD0A4 \uBCF4\uAE30", hint: "? / Shift+/", run: openHelp }
17249
+ { id: "help", label: "\u2328 \uB2E8\uCD95\uD0A4 \uBCF4\uAE30", hint: "? / Shift+/", run: openHelp },
17250
+ ...pluginMount.commands
16905
17251
  ]);
16906
17252
  ui.saveBtn.addEventListener("click", () => void saveCurrent(ui));
16907
17253
  ui.exportBtn.addEventListener("click", () => {
@@ -16915,7 +17261,9 @@ function initStudio(opts = {}) {
16915
17261
  const file = ui.importFileInput.files?.[0];
16916
17262
  ui.importFileInput.value = "";
16917
17263
  if (!file) return;
16918
- void file.text().then((text) => animationDocumentSchema.parse(JSON.parse(text))).then((def) => importAnimation(def)).then((def) => {
17264
+ void file.text().then((text) => JSON.parse(text)).then(
17265
+ (input) => opts.importDocument ? opts.importDocument(input) : animationDocumentSchema.parse(input)
17266
+ ).then((def) => importAnimation(def)).then((def) => {
16919
17267
  setDef(def);
16920
17268
  markClean();
16921
17269
  setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.`, "ok");
@@ -16978,12 +17326,12 @@ function initStudio(opts = {}) {
16978
17326
  });
16979
17327
  if (opts.initialId) {
16980
17328
  void loadAnimation2(ui, opts.initialId);
16981
- } else if (!getDef()) {
16982
- startDraft();
17329
+ } else if (isDraft()) {
16983
17330
  setStatus(ui, "\uC784\uC2DC \uC791\uC5C5 (Draft), \uC800\uC7A5 \uC2DC ID/\uC81C\uBAA9 \uC785\uB825", "ok");
16984
17331
  } else {
16985
17332
  setStatus(ui, "\uC900\uBE44\uB428", "ok");
16986
17333
  }
17334
+ return () => pluginMount.dispose();
16987
17335
  }
16988
17336
  function reflectState(ui) {
16989
17337
  ui.undoBtn.disabled = !canUndo();
@@ -17024,5 +17372,5 @@ function reflectState(ui) {
17024
17372
  }
17025
17373
 
17026
17374
  export { getVisibleElementIds, initStudio };
17027
- //# sourceMappingURL=main-NCPWDB2U.js.map
17028
- //# sourceMappingURL=main-NCPWDB2U.js.map
17375
+ //# sourceMappingURL=main-SKM3P4X7.js.map
17376
+ //# sourceMappingURL=main-SKM3P4X7.js.map