@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.
@@ -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, updateDuration, updateAppearance, updateEffect, childIdsOf } from './chunk-Z2K5HFHI.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,9 +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,
729
+ translations: {},
730
+ references: {},
724
731
  fontSize: 18,
725
732
  fontWeight: 400,
726
733
  color: "#18181b",
@@ -733,6 +740,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
733
740
  rotation: 0,
734
741
  appearances: [],
735
742
  tracks: [],
743
+ bindings: [],
736
744
  x: cx - 50,
737
745
  y: cy - 50,
738
746
  width: 100,
@@ -748,6 +756,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
748
756
  rotation: 0,
749
757
  appearances: [],
750
758
  tracks: [],
759
+ bindings: [],
751
760
  x: cx - 40,
752
761
  y: cy - 40,
753
762
  d: "M 0 0 L 80 0 L 40 80 Z",
@@ -769,6 +778,7 @@ function makeDefaultElement(type, id, cx, cy, polygonSides = 6) {
769
778
  rotation: 0,
770
779
  appearances: [],
771
780
  tracks: [],
781
+ bindings: [],
772
782
  points: pts,
773
783
  fill: "#a5b4fc",
774
784
  stroke: "#6366f1",
@@ -1044,8 +1054,6 @@ function renderSelectionOutline(canvasEl2, elId, snap2, byId) {
1044
1054
  rect.classList.add("element-selected-outline");
1045
1055
  return rect;
1046
1056
  }
1047
-
1048
- // src/legacy/canvas-preview.ts
1049
1057
  var previewRoot = null;
1050
1058
  var previewHandle = null;
1051
1059
  var unsubscribePreview = null;
@@ -1074,7 +1082,7 @@ function showPreview(canvasEl2, def, options) {
1074
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;`;
1075
1083
  parent.style.position = "relative";
1076
1084
  parent.appendChild(previewRoot);
1077
- void mountPreview(previewRoot, def, options);
1085
+ void mountPreview(previewRoot, compileDataBindings(def).document, options);
1078
1086
  }
1079
1087
  function hidePreview(canvasEl2) {
1080
1088
  if (canvasEl2) canvasEl2.style.visibility = "";
@@ -1823,6 +1831,7 @@ function onMouseUp(e) {
1823
1831
  rotation: 0,
1824
1832
  appearances: [],
1825
1833
  tracks: [],
1834
+ bindings: [],
1826
1835
  fromId: connectState.fromId,
1827
1836
  toId: elemId,
1828
1837
  fromAnchor: connectState.fromAnchor,
@@ -2697,6 +2706,51 @@ function distributeSelected(kind) {
2697
2706
  var panelEl = null;
2698
2707
  var closedSections = /* @__PURE__ */ new Set();
2699
2708
  var isComposingFallback = false;
2709
+ var DEFAULT_LOCALES = ["ko", "en"];
2710
+ function parseLocales(value) {
2711
+ const seen = /* @__PURE__ */ new Set();
2712
+ return value.split(",").map((locale) => locale.trim()).filter((locale) => {
2713
+ const key = locale.toLowerCase();
2714
+ if (!locale || seen.has(key)) return false;
2715
+ seen.add(key);
2716
+ return true;
2717
+ });
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
+ }
2700
2754
  function initProperties(root) {
2701
2755
  panelEl = root;
2702
2756
  subscribe(render3);
@@ -2768,6 +2822,9 @@ function textField(label, key, value) {
2768
2822
  <input type="text" data-prop-key="${escapeHtml2(key)}" value="${escapeHtml2(value ?? "")}" />
2769
2823
  </label>`;
2770
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
+ }
2771
2828
  function numberField(label, key, value, step = 1) {
2772
2829
  return `<label class="studio-field">
2773
2830
  <span>${escapeHtml2(label)}</span>
@@ -2810,6 +2867,11 @@ function renderInner() {
2810
2867
  const metaBody = [
2811
2868
  textField("title", "meta.title", def.title),
2812
2869
  textField("description", "meta.description", def.description),
2870
+ textField(
2871
+ "\uBB38\uC11C \uC5B8\uC5B4 (\uC27C\uD45C\uB85C \uAD6C\uBD84)",
2872
+ "meta.locales",
2873
+ (def.locales ?? DEFAULT_LOCALES).join(", ")
2874
+ ),
2813
2875
  numberField("duration (ms)", "meta.duration", def.duration, 100),
2814
2876
  numberField("canvas.width", "canvas.width", def.canvas.width),
2815
2877
  numberField("canvas.height", "canvas.height", def.canvas.height),
@@ -2817,7 +2879,18 @@ function renderInner() {
2817
2879
  "canvas.background",
2818
2880
  "canvas.background",
2819
2881
  def.canvas.background
2820
- )
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>`
2821
2894
  ].join("");
2822
2895
  const settingsHeader = `<span class="studio-props-header-title">\uC124\uC815</span>`;
2823
2896
  const settingsBody = [
@@ -2845,10 +2918,61 @@ function renderInner() {
2845
2918
  ]
2846
2919
  )
2847
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("");
2848
2971
  panelEl.innerHTML = `
2849
2972
  ${timeHint}
2850
2973
  ${section("meta", metaHeader, metaBody)}
2851
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>`)}
2852
2976
  <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uBAA9\uCC28 (${def.chapters.length})</span></div>
2853
2977
  <button type="button" class="studio-btn" data-add-chapter>\uFF0B \uD604\uC7AC \uC2DC\uAC04\uC5D0 chapter \uCD94\uAC00</button>
2854
2978
  <div class="studio-props-header" style="margin-top:0.6rem"><span class="studio-props-header-title">\uD6A8\uACFC (${def.effects.length})</span></div>
@@ -2866,6 +2990,10 @@ function renderInner() {
2866
2990
  if (sel.kind === "elements") {
2867
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>`;
2868
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
+ );
2869
2997
  panelEl.innerHTML = `
2870
2998
  ${timeHint}
2871
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>
@@ -2889,6 +3017,16 @@ function renderInner() {
2889
3017
  <button type="button" class="studio-btn studio-align-btn" data-distribute="vertical" title="\uC138\uB85C \uADE0\uB4F1 \uBD84\uD3EC" ${distributeDisabled}>\u2195</button>
2890
3018
  </div>
2891
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>
2892
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">
2893
3031
  ${sel.elementIds.map((id) => `<li>${escapeHtml2(id)}</li>`).join("")}
2894
3032
  </ul>
@@ -2910,12 +3048,14 @@ function renderInner() {
2910
3048
  setSelection({ kind: "none" });
2911
3049
  return;
2912
3050
  }
3051
+ const chapterReferences = ch.references;
2913
3052
  panelEl.innerHTML = `
2914
3053
  ${timeHint}
2915
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>
2916
3055
  ${numberField("time (ms)", "chapter.time", ch.time, 50)}
2917
3056
  ${textField("label", "chapter.label", ch.label)}
2918
3057
  ${textField("subtitle", "chapter.subtitle", ch.subtitle)}
3058
+ ${annotationReferenceFields("chapter", [ch.label, ch.subtitle], chapterReferences).join("")}
2919
3059
  <button type="button" class="studio-btn studio-btn-danger" data-delete-chapter style="margin-top:0.6rem">\u{1F5D1} chapter \uC0AD\uC81C</button>
2920
3060
  `;
2921
3061
  return;
@@ -2969,9 +3109,10 @@ function renderElementForm(def, el) {
2969
3109
  `;
2970
3110
  return;
2971
3111
  }
2972
- const baseFields = renderBaseFields(el);
3112
+ const baseFields = renderBaseFields(def, el);
2973
3113
  const appearances = renderAppearances(def, el);
2974
3114
  const tracks = renderTracks(el);
3115
+ const bindings = renderBindings(el);
2975
3116
  const baseHeader = `<span class="studio-props-header-title">${escapeHtml2(el.id)}</span><span class="studio-props-header-type">${escapeHtml2(el.type)}</span>`;
2976
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>`;
2977
3118
  const tracksHeader = `<span class="studio-props-header-title">\uD0A4\uD504\uB808\uC784 \uD2B8\uB799 (${el.tracks.length})</span>`;
@@ -2980,13 +3121,46 @@ function renderElementForm(def, el) {
2980
3121
  ${section("el-base", baseHeader, baseFields)}
2981
3122
  ${section("el-appearances", apHeader, appearances)}
2982
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)}
2983
3125
  <div class="studio-props-empty" style="font-size:0.72rem;margin-top:0.5rem">
2984
3126
  base \uC18D\uC131\uC744 \uBCC0\uACBD\uD558\uBA74 \u2192 t=${getCurrentTime()} ms \uC5D0 keyframe \uCD94\uAC00<br/>
2985
3127
  \uD2B8\uB799\uC774 \uC5C6\uB294 \uC18D\uC131\uC740 base \uAC12\uC774 \uD56D\uC0C1 \uC0AC\uC6A9\uB428
2986
3128
  </div>
2987
3129
  `;
2988
3130
  }
2989
- function renderBaseFields(el) {
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
+ }
3163
+ function renderBaseFields(def, el) {
2990
3164
  const numberFields = [];
2991
3165
  const colorFields = [];
2992
3166
  const textFields = [];
@@ -3099,8 +3273,35 @@ function renderBaseFields(el) {
3099
3273
  value: e.rotation,
3100
3274
  step: 5
3101
3275
  });
3276
+ const localizedTextFields = (() => {
3277
+ if (el.type !== "text") return [];
3278
+ const localized = el;
3279
+ const documentLocales = def.locales ?? DEFAULT_LOCALES;
3280
+ const locales = localized.locales ?? documentLocales;
3281
+ return [
3282
+ textField(
3283
+ "\uC774 \uC694\uC18C\uC758 \uC5B8\uC5B4 (\uBE44\uC6B0\uBA74 \uBB38\uC11C \uC124\uC815 \uC0AC\uC6A9)",
3284
+ "el.locales",
3285
+ localized.locales?.join(", ") ?? ""
3286
+ ),
3287
+ `<p class="studio-props-empty studio-i18n-hint">\uAE30\uBCF8 \uBB38\uAD6C\uB294 content\uC785\uB2C8\uB2E4. \uBC88\uC5ED\uC774 \uC5C6\uAC70\uB098 \uD604\uC7AC \uC5B8\uC5B4\uC640 \uC77C\uCE58\uD558\uC9C0 \uC54A\uC73C\uBA74 \uAE30\uBCF8 \uBB38\uAD6C\uB97C \uD45C\uC2DC\uD569\uB2C8\uB2E4.</p>`,
3288
+ ...locales.map(
3289
+ (locale) => textField(
3290
+ `${locale} \uBC88\uC5ED`,
3291
+ `el.translation.${locale}`,
3292
+ localized.translations?.[locale] ?? ""
3293
+ )
3294
+ ),
3295
+ ...annotationReferenceFields(
3296
+ "el",
3297
+ [el.content, ...Object.values(localized.translations ?? {})],
3298
+ localized.references
3299
+ )
3300
+ ];
3301
+ })();
3102
3302
  const html = [
3103
3303
  ...textFields.map((f) => textField(f.label, `el.${f.key}`, f.value)),
3304
+ ...localizedTextFields,
3104
3305
  ...numberFields.map(
3105
3306
  (f) => numberField(f.label, `el.${f.key}`, f.value, f.step)
3106
3307
  ),
@@ -3170,7 +3371,7 @@ function onInput(e) {
3170
3371
  if (!key) return;
3171
3372
  if (target.type === "text" && e.isComposing) return;
3172
3373
  if (target.type === "text" && isComposingFallback) return;
3173
- if (target.type === "color") {
3374
+ if (target instanceof HTMLInputElement && target.type === "color") {
3174
3375
  const textInput = target.parentElement?.querySelector(
3175
3376
  `input[type="text"][data-prop-key="${CSS.escape(key)}"]`
3176
3377
  );
@@ -3178,8 +3379,10 @@ function onInput(e) {
3178
3379
  return;
3179
3380
  }
3180
3381
  let value = target.value;
3181
- if (target.type === "number") value = Number(target.value);
3182
- 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;
3183
3386
  apply(key, value);
3184
3387
  }
3185
3388
  function onChange(e) {
@@ -3198,7 +3401,26 @@ function apply(key, value) {
3198
3401
  if (key === "meta.title") updateMeta({ title: String(value) });
3199
3402
  else if (key === "meta.description")
3200
3403
  updateMeta({ description: String(value) });
3201
- else if (key === "meta.duration") updateDuration(Number(value));
3404
+ else if (key === "meta.locales") {
3405
+ const locales = parseLocales(String(value));
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
+ }
3423
+ } else if (key === "meta.duration") updateDuration(Number(value));
3202
3424
  else if (key === "canvas.width") updateCanvas({ width: Number(value) });
3203
3425
  else if (key === "canvas.height") updateCanvas({ height: Number(value) });
3204
3426
  else if (key === "canvas.background")
@@ -3214,11 +3436,86 @@ function apply(key, value) {
3214
3436
  updateSettings({
3215
3437
  chapterListPosition: String(value)
3216
3438
  });
3217
- 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") {
3218
3489
  const prop = key.slice(3);
3219
3490
  const time = getCurrentTime();
3220
3491
  const el = def.elements.find((e) => e.id === sel.elementId);
3221
3492
  if (!el) return;
3493
+ if (prop === "locales" && el.type === "text") {
3494
+ const locales = parseLocales(String(value));
3495
+ updateElementBase(sel.elementId, {
3496
+ locales: locales.length > 0 ? locales : void 0
3497
+ });
3498
+ return;
3499
+ }
3500
+ if (prop.startsWith("translation.") && el.type === "text") {
3501
+ const locale = prop.slice("translation.".length);
3502
+ const localized = el;
3503
+ const translations = { ...localized.translations ?? {} };
3504
+ if (String(value)) translations[locale] = String(value);
3505
+ else delete translations[locale];
3506
+ updateElementBase(sel.elementId, { translations });
3507
+ return;
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
+ }
3222
3519
  if (prop === "polygonSides" && el.type === "polygon") {
3223
3520
  const points = el.points.trim().split(/\s+/).map((point) => point.split(",").map(Number)).filter(([x, y]) => Number.isFinite(x) && Number.isFinite(y));
3224
3521
  if (points.length < 3) return;
@@ -3243,6 +3540,18 @@ function apply(key, value) {
3243
3540
  } else {
3244
3541
  updateElementBase(sel.elementId, { [prop]: value });
3245
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 });
3246
3555
  } else if (key.startsWith("ap.") && sel.kind === "element") {
3247
3556
  const [, idxStr, prop] = key.split(".");
3248
3557
  const idx = Number(idxStr);
@@ -3260,6 +3569,16 @@ function apply(key, value) {
3260
3569
  updateChapter(sel.chapterId, { label: String(value) });
3261
3570
  } else if (key === "chapter.subtitle" && sel.kind === "chapter") {
3262
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 });
3263
3582
  } else if (key.startsWith("effect.") && sel.kind === "effect") {
3264
3583
  const prop = key.slice(7);
3265
3584
  const patch = {};
@@ -3271,11 +3590,44 @@ function apply(key, value) {
3271
3590
  updateEffect(sel.effectId, patch);
3272
3591
  }
3273
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
+ }
3274
3600
  function onClick2(e) {
3275
3601
  const target = e.target;
3276
3602
  const def = getDef();
3277
3603
  if (!def) return;
3278
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
+ }
3279
3631
  const alignBtn = target.closest("[data-align]");
3280
3632
  if (alignBtn) {
3281
3633
  alignSelected(alignBtn.dataset.align);
@@ -3286,6 +3638,37 @@ function onClick2(e) {
3286
3638
  distributeSelected(distBtn.dataset.distribute);
3287
3639
  return;
3288
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
+ }
3289
3672
  if (target.closest("[data-ungroup]") && sel.kind === "element") {
3290
3673
  ungroupElement(sel.elementId);
3291
3674
  return;
@@ -3305,7 +3688,8 @@ function onClick2(e) {
3305
3688
  id,
3306
3689
  time: getCurrentTime(),
3307
3690
  label: `Chapter ${id.split("-")[1]}`,
3308
- subtitle: ""
3691
+ subtitle: "",
3692
+ references: {}
3309
3693
  });
3310
3694
  return;
3311
3695
  }
@@ -3440,7 +3824,8 @@ function initTimeline(tracksRoot, addChapterBtn, elementTracks) {
3440
3824
  id,
3441
3825
  time: newTime,
3442
3826
  label: `Chapter ${id.split("-")[1]}`,
3443
- subtitle: ""
3827
+ subtitle: "",
3828
+ references: {}
3444
3829
  });
3445
3830
  setSelection({ kind: "chapter", chapterId: id });
3446
3831
  });
@@ -15291,6 +15676,7 @@ var IconLibraryDialog = class _IconLibraryDialog {
15291
15676
  rotation: 0,
15292
15677
  appearances: [],
15293
15678
  tracks: [],
15679
+ bindings: [],
15294
15680
  x: cx - 32,
15295
15681
  y: cy - 32,
15296
15682
  width: 64,
@@ -15351,6 +15737,7 @@ async function uploadAndInsertImage(file, host) {
15351
15737
  rotation: 0,
15352
15738
  appearances: [],
15353
15739
  tracks: [],
15740
+ bindings: [],
15354
15741
  x: Math.round(cx - w / 2),
15355
15742
  y: Math.round(cy - h / 2),
15356
15743
  width: w,
@@ -15774,6 +16161,8 @@ var KIND_ICON = {
15774
16161
  effect: "\u2728",
15775
16162
  asset: "\u{1F5BC}",
15776
16163
  group: "\u2B1A",
16164
+ layout: "\u25A6",
16165
+ checkpoint: "\u25C6",
15777
16166
  other: "\xB7"
15778
16167
  };
15779
16168
  var dialogEl2 = null;
@@ -16709,7 +17098,8 @@ function initStudio(opts = {}) {
16709
17098
  const ui = queryUi();
16710
17099
  if (!ui) {
16711
17100
  console.error("[studio] missing required elements");
16712
- return;
17101
+ return () => {
17102
+ };
16713
17103
  }
16714
17104
  document.body.classList.add("editor-active");
16715
17105
  document.documentElement.classList.add("editor-active");
@@ -16799,6 +17189,18 @@ function initStudio(opts = {}) {
16799
17189
  ui.newBtn.addEventListener("click", () => openNewDialog(ui));
16800
17190
  initPalette();
16801
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
+ );
16802
17204
  registerCommands([
16803
17205
  {
16804
17206
  id: "open",
@@ -16844,7 +17246,8 @@ function initStudio(opts = {}) {
16844
17246
  hint: "G",
16845
17247
  run: () => setGridEnabled(!isGridEnabled())
16846
17248
  },
16847
- { 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
16848
17251
  ]);
16849
17252
  ui.saveBtn.addEventListener("click", () => void saveCurrent(ui));
16850
17253
  ui.exportBtn.addEventListener("click", () => {
@@ -16858,7 +17261,9 @@ function initStudio(opts = {}) {
16858
17261
  const file = ui.importFileInput.files?.[0];
16859
17262
  ui.importFileInput.value = "";
16860
17263
  if (!file) return;
16861
- 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) => {
16862
17267
  setDef(def);
16863
17268
  markClean();
16864
17269
  setStatus(ui, `${def.id}.json \uD30C\uC77C\uC744 \uAC00\uC838\uC654\uC2B5\uB2C8\uB2E4.`, "ok");
@@ -16921,12 +17326,12 @@ function initStudio(opts = {}) {
16921
17326
  });
16922
17327
  if (opts.initialId) {
16923
17328
  void loadAnimation2(ui, opts.initialId);
16924
- } else if (!getDef()) {
16925
- startDraft();
17329
+ } else if (isDraft()) {
16926
17330
  setStatus(ui, "\uC784\uC2DC \uC791\uC5C5 (Draft), \uC800\uC7A5 \uC2DC ID/\uC81C\uBAA9 \uC785\uB825", "ok");
16927
17331
  } else {
16928
17332
  setStatus(ui, "\uC900\uBE44\uB428", "ok");
16929
17333
  }
17334
+ return () => pluginMount.dispose();
16930
17335
  }
16931
17336
  function reflectState(ui) {
16932
17337
  ui.undoBtn.disabled = !canUndo();
@@ -16967,5 +17372,5 @@ function reflectState(ui) {
16967
17372
  }
16968
17373
 
16969
17374
  export { getVisibleElementIds, initStudio };
16970
- //# sourceMappingURL=main-5YXLTYPQ.js.map
16971
- //# sourceMappingURL=main-5YXLTYPQ.js.map
17375
+ //# sourceMappingURL=main-SKM3P4X7.js.map
17376
+ //# sourceMappingURL=main-SKM3P4X7.js.map