@kokoa/clotho-editor 0.1.1 → 0.1.4
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/README.md +79 -26
- package/dist/{chunk-UE32Q6U5.js → chunk-GMGB7NIL.js} +198 -103
- package/dist/chunk-GMGB7NIL.js.map +1 -0
- package/dist/clotho-editor.css +150 -18
- package/dist/index.d.ts +40 -2
- package/dist/index.js +129 -17
- package/dist/index.js.map +1 -1
- package/dist/{main-SWHHYTLM.js → main-NCPWDB2U.js} +2417 -1603
- package/dist/main-NCPWDB2U.js.map +1 -0
- package/docs/PORTING.md +16 -33
- package/package.json +10 -4
- package/dist/chunk-UE32Q6U5.js.map +0 -1
- package/dist/main-SWHHYTLM.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
|
|
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';
|
|
2
3
|
import { useState, useEffect, useRef } from 'react';
|
|
3
4
|
import { usePlayer, AnimationStage } from '@kokoa/clotho/react';
|
|
4
5
|
import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
|
|
6
|
+
import { animationDocumentSchema } from '@kokoa/clotho';
|
|
5
7
|
|
|
6
8
|
function elementsOf(def) {
|
|
7
9
|
return def.elements;
|
|
@@ -51,6 +53,7 @@ function Studio({ initial, onSave }) {
|
|
|
51
53
|
y: 100,
|
|
52
54
|
rotation: 0,
|
|
53
55
|
content: "New",
|
|
56
|
+
translations: {},
|
|
54
57
|
fontSize: 24,
|
|
55
58
|
color: "#4f46e5",
|
|
56
59
|
textAnchor: "start",
|
|
@@ -98,6 +101,15 @@ function Studio({ initial, onSave }) {
|
|
|
98
101
|
] }),
|
|
99
102
|
/* @__PURE__ */ jsxs("div", { className: "studio-preview", children: [
|
|
100
103
|
/* @__PURE__ */ jsxs("div", { className: "studio-preview-bar", children: [
|
|
104
|
+
/* @__PURE__ */ jsx(
|
|
105
|
+
"button",
|
|
106
|
+
{
|
|
107
|
+
type: "button",
|
|
108
|
+
className: "studio-btn",
|
|
109
|
+
onClick: () => downloadAnimationJson(def),
|
|
110
|
+
children: "JSON \uB0B4\uBCF4\uB0B4\uAE30"
|
|
111
|
+
}
|
|
112
|
+
),
|
|
101
113
|
/* @__PURE__ */ jsx(
|
|
102
114
|
"button",
|
|
103
115
|
{
|
|
@@ -224,7 +236,7 @@ var PRESET_HTML = CANVAS_PRESETS.map(
|
|
|
224
236
|
var SKELETON = `
|
|
225
237
|
<div id="studio-app" class="studio-app">
|
|
226
238
|
<header class="studio-header">
|
|
227
|
-
<h1 class="studio-header-title"
|
|
239
|
+
<h1 id="studio-editor-title" class="studio-header-title">Clotho Editor</h1>
|
|
228
240
|
<div class="studio-header-actions">
|
|
229
241
|
<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>
|
|
230
242
|
<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>
|
|
@@ -235,6 +247,10 @@ var SKELETON = `
|
|
|
235
247
|
<span id="studio-id-display" class="studio-id-display"></span>
|
|
236
248
|
<span id="studio-status" class="studio-status" aria-live="polite">\uB300\uAE30 \uC911</span>
|
|
237
249
|
<button type="button" id="studio-save" class="studio-btn studio-btn-primary" aria-label="\uC800\uC7A5" disabled>\u{1F4BE} \uC800\uC7A5 (\u2318S)</button>
|
|
250
|
+
<button type="button" id="studio-export-json" class="studio-btn" aria-label="\uD604\uC7AC \uC560\uB2C8\uBA54\uC774\uC158\uC744 JSON \uD30C\uC77C\uB85C \uB0B4\uBCF4\uB0B4\uAE30" disabled>\u2B07 JSON \uB0B4\uBCF4\uB0B4\uAE30</button>
|
|
251
|
+
<button type="button" id="studio-player-preview" class="studio-btn" aria-label="\uC2E4\uC81C \uC560\uB2C8\uBA54\uC774\uC158 \uCEF4\uD3EC\uB10C\uD2B8 \uBBF8\uB9AC\uBCF4\uAE30" aria-pressed="false">\u2197 \uC2E4\uC81C \uBBF8\uB9AC\uBCF4\uAE30</button>
|
|
252
|
+
<button type="button" id="studio-import-json" class="studio-btn" aria-label="JSON \uD30C\uC77C\uC5D0\uC11C \uC560\uB2C8\uBA54\uC774\uC158 \uAC00\uC838\uC624\uAE30">\u2B06 JSON \uAC00\uC838\uC624\uAE30</button>
|
|
253
|
+
<input type="file" id="studio-import-file" accept="application/json,.json" hidden />
|
|
238
254
|
<button type="button" id="studio-delete" class="studio-btn studio-btn-danger" aria-label="\uC0AD\uC81C" disabled>\u{1F5D1} \uC0AD\uC81C</button>
|
|
239
255
|
</div>
|
|
240
256
|
</header>
|
|
@@ -242,15 +258,16 @@ var SKELETON = `
|
|
|
242
258
|
<div class="studio-body">
|
|
243
259
|
<aside class="studio-tools" aria-label="\uC694\uC18C \uCD94\uAC00">
|
|
244
260
|
<div class="studio-tools-section">
|
|
245
|
-
<div class="studio-tools-title">\
|
|
246
|
-
<button type="button" class="studio-tool-btn" data-
|
|
247
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
248
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
249
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
250
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
251
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
252
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
253
|
-
<button type="button" class="studio-tool-btn" data-add-element="
|
|
261
|
+
<div class="studio-tools-title">\uB3C4\uAD6C</div>
|
|
262
|
+
<button type="button" class="studio-tool-btn is-active" data-studio-tool="select" aria-pressed="true" title="\uC120\uD0DD \xB7 \uC774\uB3D9 \xB7 \uD06C\uAE30 (V)"><span>\u2196 \uC120\uD0DD \xB7 \uC774\uB3D9 \xB7 \uD06C\uAE30</span><kbd>V</kbd></button>
|
|
263
|
+
<button type="button" class="studio-tool-btn" data-add-element="rect" title="\uC0AC\uAC01\uD615 (R)" aria-label="\uC0AC\uAC01\uD615 \uB3C4\uAD6C"><span>\u25A1 Rect</span><kbd>R</kbd></button>
|
|
264
|
+
<button type="button" class="studio-tool-btn" data-add-element="circle" title="\uC6D0 (O)" aria-label="\uC6D0 \uB3C4\uAD6C"><span>\u25CB Circle</span><kbd>O</kbd></button>
|
|
265
|
+
<button type="button" class="studio-tool-btn" data-add-element="line" title="\uC120 (L)" aria-label="\uC120 \uB3C4\uAD6C"><span>\uFF0F Line</span><kbd>L</kbd></button>
|
|
266
|
+
<button type="button" class="studio-tool-btn" data-add-element="arrow" title="\uD654\uC0B4\uD45C (A)" aria-label="\uD654\uC0B4\uD45C \uB3C4\uAD6C"><span>\u2197 Arrow</span><kbd>A</kbd></button>
|
|
267
|
+
<button type="button" class="studio-tool-btn" data-add-element="text" title="\uD14D\uC2A4\uD2B8 (T)" aria-label="\uD14D\uC2A4\uD2B8 \uB3C4\uAD6C"><span>T Text</span><kbd>T</kbd></button>
|
|
268
|
+
<button type="button" class="studio-tool-btn" data-add-element="image" title="\uC774\uBBF8\uC9C0 (I)" aria-label="\uC774\uBBF8\uC9C0 \uB3C4\uAD6C"><span>\u{1F5BC} Image</span><kbd>I</kbd></button>
|
|
269
|
+
<button type="button" class="studio-tool-btn" data-add-element="path" title="Path (B)" aria-label="Path \uB3C4\uAD6C"><span>\u270E Path</span><kbd>B</kbd></button>
|
|
270
|
+
<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>
|
|
254
271
|
<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>
|
|
255
272
|
</div>
|
|
256
273
|
<div class="studio-tools-section">
|
|
@@ -297,7 +314,7 @@ var SKELETON = `
|
|
|
297
314
|
</aside>
|
|
298
315
|
</div>
|
|
299
316
|
|
|
300
|
-
<div id="studio-timeline-resizer" class="studio-timeline-resizer" role="separator" aria-orientation="horizontal" aria-label="\uD0C0\uC784\uB77C\uC778 \uC601\uC5ED \uD06C\uAE30 \uC870\uC815"></div>
|
|
317
|
+
<div id="studio-timeline-resizer" class="studio-timeline-resizer" role="separator" aria-orientation="horizontal" aria-label="\uD0C0\uC784\uB77C\uC778 \uC601\uC5ED \uD06C\uAE30 \uC870\uC815" tabindex="0"><span class="studio-timeline-resizer-grip" aria-hidden="true"><i></i><i></i><i></i></span></div>
|
|
301
318
|
<footer class="studio-timeline-wrap">
|
|
302
319
|
<div class="studio-timeline-header">
|
|
303
320
|
<button type="button" id="studio-play" class="studio-btn">\u25B6 Play</button>
|
|
@@ -306,6 +323,8 @@ var SKELETON = `
|
|
|
306
323
|
<input type="range" id="studio-speed" min="0.25" max="3" step="0.25" value="1" />
|
|
307
324
|
<span id="studio-speed-value">1.00x</span>
|
|
308
325
|
</label>
|
|
326
|
+
<label class="studio-timeline-option"><input type="checkbox" id="studio-preview-loop" /> \uBB34\uD55C \uC7AC\uC0DD</label>
|
|
327
|
+
<button type="button" id="studio-detach-timeline" class="studio-btn" aria-pressed="false">\u25A3 \uD0C0\uC784\uB77C\uC778 \uBD84\uB9AC</button>
|
|
309
328
|
<span class="studio-timeline-spacer"></span>
|
|
310
329
|
<button type="button" id="studio-add-step" class="studio-btn">\uFF0B Chapter \uCD94\uAC00</button>
|
|
311
330
|
</div>
|
|
@@ -349,6 +368,10 @@ var SKELETON = `
|
|
|
349
368
|
<tr><th>\u2318 S</th><td>\uC800\uC7A5</td></tr>
|
|
350
369
|
<tr><th>\u2318 Z / \u2318 \u21E7 Z</th><td>Undo / Redo</td></tr>
|
|
351
370
|
<tr><th>\u2318 C / V / X</th><td>\uC694\uC18C \uBCF5\uC0AC / \uBD99\uC5EC\uB123\uAE30 / \uC798\uB77C\uB0B4\uAE30</td></tr>
|
|
371
|
+
<tr><th>V / R / O</th><td>\uC120\uD0DD / \uC0AC\uAC01\uD615 / \uC6D0 \uB3C4\uAD6C</td></tr>
|
|
372
|
+
<tr><th>L / A</th><td>\uC120 / \uD654\uC0B4\uD45C \uB3C4\uAD6C</td></tr>
|
|
373
|
+
<tr><th>T / I / B / Y</th><td>\uD14D\uC2A4\uD2B8 / \uC774\uBBF8\uC9C0 / Path / \uB2E4\uAC01\uD615 \uB3C4\uAD6C</td></tr>
|
|
374
|
+
<tr><th>\u2318\xB7Ctrl\xB7Shift + \uD074\uB9AD</th><td>\uC5EC\uB7EC \uC694\uC18C \uC120\uD0DD \uB610\uB294 \uC120\uD0DD \uD574\uC81C</td></tr>
|
|
352
375
|
<tr><th>Delete / Backspace</th><td>\uC120\uD0DD\uB41C \uC694\uC18C \uB610\uB294 step \uC0AD\uC81C</td></tr>
|
|
353
376
|
<tr><th>?</th><td>\uC774 \uD654\uBA74 \uC5F4\uAE30</td></tr>
|
|
354
377
|
<tr><th>Esc</th><td>\uC120\uD0DD \uD574\uC81C / \uB2E4\uC774\uC5BC\uB85C\uADF8 \uB2EB\uAE30</td></tr>
|
|
@@ -400,26 +423,115 @@ var SKELETON = `
|
|
|
400
423
|
</div>
|
|
401
424
|
`;
|
|
402
425
|
function StudioMount({
|
|
403
|
-
initialId
|
|
426
|
+
initialId,
|
|
427
|
+
repository,
|
|
428
|
+
editorTitle = "Clotho Editor",
|
|
429
|
+
resolveImage
|
|
404
430
|
}) {
|
|
405
431
|
const inited = useRef(false);
|
|
406
432
|
useEffect(() => {
|
|
407
433
|
if (inited.current) return;
|
|
408
434
|
inited.current = true;
|
|
435
|
+
if (repository) configureAnimationRepository(repository);
|
|
409
436
|
let disposed = false;
|
|
410
|
-
void import('./main-
|
|
437
|
+
void import('./main-NCPWDB2U.js').then(({ initStudio }) => {
|
|
411
438
|
if (disposed) return;
|
|
412
|
-
initStudio(
|
|
439
|
+
initStudio({ initialId, editorTitle, resolveImage });
|
|
413
440
|
});
|
|
414
441
|
return () => {
|
|
415
442
|
disposed = true;
|
|
416
443
|
document.body.classList.remove("editor-active");
|
|
417
444
|
document.documentElement.classList.remove("editor-active");
|
|
418
445
|
};
|
|
419
|
-
}, [initialId]);
|
|
446
|
+
}, [editorTitle, initialId, repository, resolveImage]);
|
|
420
447
|
return /* @__PURE__ */ jsx("section", { className: "studio-shell w-full", "data-pagefind-ignore": "all", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: SKELETON } }) });
|
|
421
448
|
}
|
|
449
|
+
function browserStorage() {
|
|
450
|
+
if (typeof localStorage === "undefined")
|
|
451
|
+
throw new Error("\uC774 \uC800\uC7A5\uC18C\uB294 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
|
|
452
|
+
return localStorage;
|
|
453
|
+
}
|
|
454
|
+
function createLocalStorageRepository(options = {}) {
|
|
455
|
+
const prefix = options.storageKey ?? "clotho-editor.animations";
|
|
456
|
+
const examples = new Map(
|
|
457
|
+
(options.examples ?? []).map((value) => {
|
|
458
|
+
const def = animationDocumentSchema.parse(value);
|
|
459
|
+
return [def.id, def];
|
|
460
|
+
})
|
|
461
|
+
);
|
|
462
|
+
const storage = () => options.storage ?? browserStorage();
|
|
463
|
+
const indexKey = `${prefix}:index`;
|
|
464
|
+
const documentKey = (id) => `${prefix}:document:${id}`;
|
|
465
|
+
const readIds = () => {
|
|
466
|
+
const value = storage().getItem(indexKey);
|
|
467
|
+
if (!value) return [];
|
|
468
|
+
try {
|
|
469
|
+
const parsed = JSON.parse(value);
|
|
470
|
+
return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
|
|
471
|
+
} catch {
|
|
472
|
+
return [];
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
const write = (def) => {
|
|
476
|
+
const parsed = animationDocumentSchema.parse(def);
|
|
477
|
+
const ids = readIds();
|
|
478
|
+
if (!ids.includes(parsed.id))
|
|
479
|
+
storage().setItem(indexKey, JSON.stringify([...ids, parsed.id]));
|
|
480
|
+
storage().setItem(documentKey(parsed.id), JSON.stringify(parsed));
|
|
481
|
+
return parsed;
|
|
482
|
+
};
|
|
483
|
+
const read = (id) => {
|
|
484
|
+
const value = storage().getItem(documentKey(id));
|
|
485
|
+
return value ? animationDocumentSchema.parse(JSON.parse(value)) : null;
|
|
486
|
+
};
|
|
487
|
+
return {
|
|
488
|
+
async list() {
|
|
489
|
+
const saved = readIds().map((id) => read(id)).filter((def) => def !== null);
|
|
490
|
+
const savedIds = new Set(saved.map((def) => def.id));
|
|
491
|
+
return [
|
|
492
|
+
...saved.map((def) => ({
|
|
493
|
+
id: def.id,
|
|
494
|
+
title: def.title || def.id,
|
|
495
|
+
description: def.description,
|
|
496
|
+
source: "saved"
|
|
497
|
+
})),
|
|
498
|
+
...[...examples.values()].filter((def) => !savedIds.has(def.id)).map((def) => ({
|
|
499
|
+
id: def.id,
|
|
500
|
+
title: def.title || def.id,
|
|
501
|
+
description: def.description,
|
|
502
|
+
source: "example"
|
|
503
|
+
}))
|
|
504
|
+
];
|
|
505
|
+
},
|
|
506
|
+
async load(id) {
|
|
507
|
+
const def = read(id) ?? examples.get(id);
|
|
508
|
+
if (!def) throw new Error(`'${id}' \uC560\uB2C8\uBA54\uC774\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
|
|
509
|
+
return structuredClone(def);
|
|
510
|
+
},
|
|
511
|
+
async create(id, title) {
|
|
512
|
+
if (read(id)) throw new Error(`'${id}' \uC560\uB2C8\uBA54\uC774\uC158\uC774 \uC774\uBBF8 \uC788\uC2B5\uB2C8\uB2E4.`);
|
|
513
|
+
return write(
|
|
514
|
+
animationDocumentSchema.parse({
|
|
515
|
+
clothoVersion: 1,
|
|
516
|
+
id,
|
|
517
|
+
title,
|
|
518
|
+
description: ""
|
|
519
|
+
})
|
|
520
|
+
);
|
|
521
|
+
},
|
|
522
|
+
async save(def) {
|
|
523
|
+
return write(def);
|
|
524
|
+
},
|
|
525
|
+
async delete(id) {
|
|
526
|
+
storage().removeItem(documentKey(id));
|
|
527
|
+
storage().setItem(
|
|
528
|
+
indexKey,
|
|
529
|
+
JSON.stringify(readIds().filter((value) => value !== id))
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
}
|
|
422
534
|
|
|
423
|
-
export { Studio, StudioMount };
|
|
535
|
+
export { Studio, StudioMount, createLocalStorageRepository };
|
|
424
536
|
//# sourceMappingURL=index.js.map
|
|
425
537
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/Studio.tsx","../src/StudioMount.tsx"],"names":["setDef","addElement","deleteElement","useEffect","jsx"],"mappings":"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","file":"index.js","sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { AnimationDocument } from \"@kokoa/clotho\";\nimport { AnimationStage, usePlayer } from \"@kokoa/clotho/react\";\n\ntype EditableElement = Record<string, unknown> & { id: string; type: string };\ntype ElementList = AnimationDocument[\"elements\"];\n\nexport interface StudioProps {\n initial: AnimationDocument;\n onSave: (def: AnimationDocument) => void | Promise<void>;\n}\n\nfunction elementsOf(def: AnimationDocument): EditableElement[] {\n return def.elements as unknown as EditableElement[];\n}\n\nexport function Studio({ initial, onSave }: StudioProps): React.JSX.Element {\n const [def, setDef] = useState<AnimationDocument>(initial);\n const [selectedId, setSelectedId] = useState<string | null>(\n elementsOf(initial)[0]?.id ?? null,\n );\n const [playing, setPlaying] = useState(false);\n const [time, setTime] = useState(0);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n\n // The clock for preview playback. It lives outside React (core/player), so the\n // editor subscribes rather than owning a rAF loop of its own.\n const { player, state: playerState } = usePlayer(def, { autoplay: false });\n\n useEffect(() => {\n if (playing) player.play();\n else {\n player.pause();\n player.seek(time);\n }\n }, [playing, player, time]);\n\n const elements = elementsOf(def);\n const selected = elements.find((e) => e.id === selectedId) ?? null;\n const scalarProps = selected\n ? Object.entries(selected).filter(\n ([key, value]) =>\n (typeof value === \"string\" || typeof value === \"number\") &&\n key !== \"id\" &&\n key !== \"type\",\n )\n : [];\n\n function setElements(next: EditableElement[]): void {\n setDef((current) => ({\n ...current,\n elements: next as unknown as ElementList,\n }));\n setSaved(false);\n }\n\n function updateElement(\n id: string,\n key: string,\n raw: string,\n numeric: boolean,\n ): void {\n setElements(\n elements.map((e) =>\n e.id === id ? { ...e, [key]: numeric ? Number(raw) : raw } : e,\n ),\n );\n }\n\n function addElement(): void {\n const id = `el-${Date.now().toString(36)}`;\n const el: EditableElement = {\n type: \"text\",\n id,\n x: 100,\n y: 100,\n rotation: 0,\n content: \"New\",\n fontSize: 24,\n color: \"#4f46e5\",\n textAnchor: \"start\",\n appearances: [{ start: 0, end: def.duration }],\n tracks: [],\n };\n setElements([...elements, el]);\n setSelectedId(id);\n }\n\n function deleteElement(id: string): void {\n setElements(elements.filter((e) => e.id !== id));\n if (selectedId === id) setSelectedId(null);\n }\n\n async function handleSave(): Promise<void> {\n setSaving(true);\n try {\n await onSave(def);\n setSaved(true);\n } finally {\n setSaving(false);\n }\n }\n\n return (\n <div className=\"studio\">\n <aside className=\"studio-panel studio-left\">\n <div className=\"studio-panel-head\">\n <span>요소 ({elements.length})</span>\n <button type=\"button\" className=\"studio-btn\" onClick={addElement}>\n + 추가\n </button>\n </div>\n <ul className=\"studio-el-list\">\n {elements.map((e) => (\n <li key={e.id}>\n <button\n type=\"button\"\n className={\n e.id === selectedId ? \"studio-el active\" : \"studio-el\"\n }\n onClick={() => setSelectedId(e.id)}\n >\n <span className=\"studio-el-type\">{e.type}</span>\n <span className=\"studio-el-id\">{e.id}</span>\n </button>\n </li>\n ))}\n </ul>\n </aside>\n\n <div className=\"studio-preview\">\n <div className=\"studio-preview-bar\">\n <button\n type=\"button\"\n className=\"studio-btn\"\n onClick={() => setPlaying((p) => !p)}\n >\n {playing ? \"⏸ 정지\" : \"▶ 재생\"}\n </button>\n <input\n type=\"range\"\n min={0}\n max={def.duration}\n step={50}\n value={time}\n disabled={playing}\n onChange={(e) => {\n setTime(Number(e.target.value));\n setPlaying(false);\n }}\n />\n <span className=\"studio-time\">\n {time} / {def.duration}ms\n </span>\n <button\n type=\"button\"\n className=\"studio-btn studio-save\"\n onClick={() => void handleSave()}\n disabled={saving}\n >\n {saving ? \"저장 중…\" : saved ? \"✓ 저장됨\" : \"저장\"}\n </button>\n </div>\n <div className=\"studio-canvas\">\n {/* One frame at a time: the editor's scrubber is the source of truth, and the\n player only supplies the clock while previewing. Same buildScene path as\n the published animation, so preview and production cannot diverge. */}\n <AnimationStage doc={def} time={playing ? playerState.time : time} />\n </div>\n </div>\n\n <aside className=\"studio-panel studio-right\">\n <div className=\"studio-panel-head\">메타</div>\n <label className=\"studio-field\">\n <span>제목</span>\n <input\n value={def.title}\n onChange={(e) => {\n setDef((d) => ({ ...d, title: e.target.value }));\n setSaved(false);\n }}\n />\n </label>\n <label className=\"studio-field\">\n <span>duration (ms)</span>\n <input\n type=\"number\"\n value={def.duration}\n onChange={(e) => {\n setDef((d) => ({ ...d, duration: Number(e.target.value) }));\n setSaved(false);\n }}\n />\n </label>\n <div className=\"studio-panel-head\">\n 속성 {selected ? `(${selected.type})` : \"\"}\n </div>\n {selected ? (\n <>\n {scalarProps.map(([key, value]) => (\n <label key={key} className=\"studio-field\">\n <span>{key}</span>\n <input\n value={String(value)}\n onChange={(e) =>\n updateElement(\n selected.id,\n key,\n e.target.value,\n typeof value === \"number\",\n )\n }\n />\n </label>\n ))}\n <button\n type=\"button\"\n className=\"studio-btn studio-del\"\n onClick={() => deleteElement(selected.id)}\n >\n 요소 삭제\n </button>\n </>\n ) : (\n <p className=\"studio-empty\">요소를 선택하세요.</p>\n )}\n </aside>\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\n// The stylesheet ships as a separate entry rather than an import, so a consumer that\n// only wants the headless pieces does not pull CSS into its bundle:\n// import \"@kokoa/clotho-editor/styles.css\";\n\nexport interface StudioMountProps {\n initialId?: string;\n}\n\nconst CANVAS_PRESETS: {\n id: string;\n label: string;\n sub: string;\n w: number;\n h: number;\n}[] = [\n { id: \"800x500\", label: \"8:5\", sub: \"800×500\", w: 32, h: 20 },\n { id: \"1280x720\", label: \"16:9\", sub: \"HD\", w: 32, h: 18 },\n { id: \"1024x768\", label: \"4:3\", sub: \"1024×768\", w: 28, h: 21 },\n { id: \"600x600\", label: \"1:1\", sub: \"정사각\", w: 22, h: 22 },\n { id: \"1200x400\", label: \"3:1\", sub: \"와이드\", w: 36, h: 12 },\n { id: \"375x812\", label: \"9:19\", sub: \"모바일\", w: 14, h: 30 },\n];\n\nconst PRESET_HTML = CANVAS_PRESETS.map(\n (p) => `\n <button type=\"button\" class=\"studio-preset-btn\" aria-label=\"캔버스 크기 ${p.id} (${p.sub})\" title=\"${p.id} · ${p.sub}\" data-canvas-preset=\"${p.id}\">\n <svg class=\"studio-preset-thumb\" viewBox=\"0 0 ${p.w + 2} ${p.h + 2}\" width=\"${p.w + 2}\" height=\"${p.h + 2}\" aria-hidden=\"true\">\n <rect x=\"1\" y=\"1\" width=\"${p.w}\" height=\"${p.h}\" rx=\"2\" />\n </svg>\n <span class=\"studio-preset-label\">${p.label}</span>\n <span class=\"studio-preset-sub\">${p.sub}</span>\n </button>`,\n).join(\"\");\n\nconst SKELETON = `\n<div id=\"studio-app\" class=\"studio-app\">\n <header class=\"studio-header\">\n <h1 class=\"studio-header-title\">🎬 애니메이션 스튜디오</h1>\n <div class=\"studio-header-actions\">\n <button type=\"button\" id=\"studio-open\" class=\"studio-btn\" aria-label=\"저장된 애니메이션 열기\">📁 열기</button>\n <button type=\"button\" id=\"studio-new\" class=\"studio-btn\" aria-label=\"새 애니메이션 만들기\">+ 새 애니메이션</button>\n <button type=\"button\" id=\"studio-undo\" class=\"studio-btn studio-btn-icon\" aria-label=\"실행 취소\" title=\"실행 취소 (⌘Z)\" disabled>↶</button>\n <button type=\"button\" id=\"studio-redo\" class=\"studio-btn studio-btn-icon\" aria-label=\"다시 실행\" title=\"다시 실행 (⌘⇧Z / ⌘Y)\" disabled>↷</button>\n <button type=\"button\" id=\"studio-grid-toggle\" class=\"studio-btn studio-btn-grid\" aria-label=\"격자 + 스냅\" title=\"격자 + 스냅 (G)\" aria-pressed=\"false\"><span class=\"studio-btn-grid-icon\">⊞</span><span class=\"studio-btn-grid-label\" id=\"studio-grid-label\">격자 끔</span></button>\n <input type=\"text\" id=\"studio-title\" class=\"studio-title-input\" placeholder=\"제목\" aria-label=\"애니메이션 제목\" disabled />\n <span id=\"studio-id-display\" class=\"studio-id-display\"></span>\n <span id=\"studio-status\" class=\"studio-status\" aria-live=\"polite\">대기 중</span>\n <button type=\"button\" id=\"studio-save\" class=\"studio-btn studio-btn-primary\" aria-label=\"저장\" disabled>💾 저장 (⌘S)</button>\n <button type=\"button\" id=\"studio-delete\" class=\"studio-btn studio-btn-danger\" aria-label=\"삭제\" disabled>🗑 삭제</button>\n </div>\n </header>\n\n <div class=\"studio-body\">\n <aside class=\"studio-tools\" aria-label=\"요소 추가\">\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">요소 추가</div>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"rect\" title=\"사각형\" aria-label=\"사각형 추가\">□ Rect</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"circle\" title=\"원\" aria-label=\"원 추가\">○ Circle</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"line\" title=\"선\" aria-label=\"선 추가\">/ Line</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"arrow\" title=\"화살표\" aria-label=\"화살표 추가\">↗ Arrow</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"text\" title=\"텍스트\" aria-label=\"텍스트 추가\">T Text</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"image\" title=\"이미지\" aria-label=\"이미지 추가\">🖼 Image</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"path\" title=\"SVG path\" aria-label=\"SVG path 추가\">✎ Path</button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"polygon\" title=\"다각형\" aria-label=\"다각형 추가\">⬢ Polygon</button>\n <button type=\"button\" class=\"studio-tool-btn\" id=\"studio-open-icons\" title=\"아이콘 라이브러리\" aria-label=\"아이콘 라이브러리 추가\">🎨 Icons</button>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">캔버스 크기</div>\n <div class=\"studio-canvas-size-row\">\n <input type=\"number\" id=\"studio-canvas-width\" class=\"studio-canvas-size-input\" min=\"100\" max=\"8000\" step=\"10\" placeholder=\"width\" />\n <span class=\"studio-canvas-size-x\">×</span>\n <input type=\"number\" id=\"studio-canvas-height\" class=\"studio-canvas-size-input\" min=\"100\" max=\"8000\" step=\"10\" placeholder=\"height\" />\n </div>\n <div class=\"studio-canvas-presets\">${PRESET_HTML}</div>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">이미지</div>\n <button type=\"button\" class=\"studio-tool-btn\" id=\"studio-image-upload\">📤 이미지 업로드</button>\n <input type=\"file\" id=\"studio-image-file\" accept=\"image/*\" hidden />\n <div class=\"studio-tools-hint\">이미지를 캔버스에 직접 드래그하거나 ⌘V 로 붙여넣어도 됩니다.</div>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">단축키</div>\n <button type=\"button\" class=\"studio-tool-btn\" id=\"studio-help\">⌨ 단축키 보기 (?)</button>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">요소 목록</div>\n <input type=\"text\" id=\"studio-element-search\" class=\"studio-element-search\" placeholder=\"🔍 검색 (name / label / id / type)\" autocomplete=\"off\" />\n <ul id=\"studio-element-list\" class=\"studio-element-list\"></ul>\n </div>\n </aside>\n\n <main class=\"studio-canvas-wrap\">\n <div class=\"studio-canvas-frame\">\n <svg id=\"studio-canvas\" class=\"studio-canvas\" xmlns=\"http://www.w3.org/2000/svg\"></svg>\n </div>\n <button type=\"button\" id=\"studio-floating-help\" class=\"studio-floating-help\" aria-label=\"단축키 보기\" title=\"단축키 보기 (? / Shift+/)\">?</button>\n <div id=\"studio-help-hint\" class=\"studio-help-hint\" hidden>\n <span>💡 단축키는 <kbd>?</kbd> 또는 우측 하단 버튼으로 확인하세요</span>\n <button type=\"button\" id=\"studio-help-hint-close\" aria-label=\"닫기\">✕</button>\n </div>\n </main>\n\n <aside class=\"studio-props\" aria-label=\"속성 패널\">\n <div id=\"studio-props-content\" class=\"studio-props-content\">\n <p class=\"studio-props-empty\">요소 또는 step 을 선택하세요.</p>\n </div>\n </aside>\n </div>\n\n <div id=\"studio-timeline-resizer\" class=\"studio-timeline-resizer\" role=\"separator\" aria-orientation=\"horizontal\" aria-label=\"타임라인 영역 크기 조정\"></div>\n <footer class=\"studio-timeline-wrap\">\n <div class=\"studio-timeline-header\">\n <button type=\"button\" id=\"studio-play\" class=\"studio-btn\">▶ Play</button>\n <button type=\"button\" id=\"studio-restart\" class=\"studio-btn\">⟲ Reset</button>\n <label class=\"studio-speed\">속도\n <input type=\"range\" id=\"studio-speed\" min=\"0.25\" max=\"3\" step=\"0.25\" value=\"1\" />\n <span id=\"studio-speed-value\">1.00x</span>\n </label>\n <span class=\"studio-timeline-spacer\"></span>\n <button type=\"button\" id=\"studio-add-step\" class=\"studio-btn\">+ Chapter 추가</button>\n </div>\n <div class=\"studio-timeline-tracks-wrap\">\n <div id=\"studio-timeline-tracks\" class=\"studio-timeline-tracks\"></div>\n </div>\n <div class=\"studio-element-tracks-wrap\">\n <div id=\"studio-element-tracks\" class=\"studio-element-tracks\"></div>\n </div>\n </footer>\n\n <dialog id=\"studio-library-dialog\" class=\"studio-dialog\">\n <div class=\"studio-dialog-header\">\n <h2>저장된 애니메이션</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close>✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <ul id=\"studio-library-list\" class=\"studio-library-list\"></ul>\n </div>\n </dialog>\n\n <dialog id=\"studio-icon-dialog\" class=\"studio-dialog\">\n <div class=\"studio-dialog-header\">\n <h2>🎨 아이콘 라이브러리</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-icon-dialog-close>✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <input type=\"search\" id=\"studio-icon-search\" placeholder=\"아이콘 검색…\" class=\"studio-icon-search-input\" />\n <div id=\"studio-icon-list\"></div>\n </div>\n </dialog>\n\n <dialog id=\"studio-help-dialog\" class=\"studio-dialog studio-dialog-small\">\n <div class=\"studio-dialog-header\">\n <h2>⌨ 단축키</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close>✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <table class=\"studio-help-table\">\n <tbody>\n <tr><th>⌘ S</th><td>저장</td></tr>\n <tr><th>⌘ Z / ⌘ ⇧ Z</th><td>Undo / Redo</td></tr>\n <tr><th>⌘ C / V / X</th><td>요소 복사 / 붙여넣기 / 잘라내기</td></tr>\n <tr><th>Delete / Backspace</th><td>선택된 요소 또는 step 삭제</td></tr>\n <tr><th>?</th><td>이 화면 열기</td></tr>\n <tr><th>Esc</th><td>선택 해제 / 다이얼로그 닫기</td></tr>\n <tr><th>드래그 (캔버스)</th><td>요소 이동</td></tr>\n <tr><th>드래그 (요소 위 dot)</th><td>화살표 연결</td></tr>\n <tr><th>드래그 (선/화살표 끝점)</th><td>끝점 재배치 + 다른 요소에 sticky</td></tr>\n <tr><th>드래그 (회전 핸들)</th><td>요소 회전</td></tr>\n <tr><th>드래그 (타임라인 바 우측)</th><td>step duration 조정</td></tr>\n <tr><th>이미지 드래그 in</th><td>이미지 업로드 + 배치</td></tr>\n </tbody>\n </table>\n </div>\n </dialog>\n\n <dialog id=\"studio-new-dialog\" class=\"studio-dialog studio-dialog-small\">\n <div class=\"studio-dialog-header\">\n <h2 id=\"studio-new-dialog-title\">새 애니메이션 만들기</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close aria-label=\"닫기\">✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <label class=\"studio-field\"><span>ID (영문 소문자 / 숫자 / - / _)</span>\n <input type=\"text\" id=\"studio-new-id\" placeholder=\"예: user-login-flow\" />\n </label>\n <label class=\"studio-field\"><span>제목</span>\n <input type=\"text\" id=\"studio-new-title\" placeholder=\"사용자 로그인 흐름\" />\n </label>\n <div id=\"studio-new-error\" class=\"studio-new-error\"></div>\n </div>\n <footer class=\"studio-dialog-footer\">\n <button type=\"button\" class=\"studio-btn\" data-studio-dialog-close>취소</button>\n <button type=\"button\" id=\"studio-new-create\" class=\"studio-btn studio-btn-primary\">만들기</button>\n </footer>\n </dialog>\n\n <dialog id=\"studio-palette-dialog\" class=\"studio-palette-dialog\">\n <input type=\"text\" id=\"studio-palette-input\" class=\"studio-palette-input\" placeholder=\"🔍 명령 또는 요소 검색…\" autocomplete=\"off\" spellcheck=\"false\" />\n <ul id=\"studio-palette-list\" class=\"studio-palette-list\"></ul>\n <div class=\"studio-palette-footer\">↑↓ 이동 · Enter 실행 · Esc 닫기</div>\n </dialog>\n\n <dialog id=\"studio-history-dialog\" class=\"studio-history-dialog\">\n <div class=\"studio-history-header\">\n <h2>📜 작업 이력</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close aria-label=\"닫기\">✕</button>\n </div>\n <ul id=\"studio-history-list\" class=\"studio-history-list\"></ul>\n <div class=\"studio-history-footer\">항목을 클릭하면 해당 시점으로 이동합니다 · ⌘Z / ⌘⇧Z</div>\n </dialog>\n</div>\n`;\n\nexport function StudioMount({\n initialId,\n}: StudioMountProps): React.JSX.Element {\n const inited = useRef(false);\n\n useEffect(() => {\n if (inited.current) return;\n inited.current = true;\n let disposed = false;\n void import(\"./legacy/main\").then(({ initStudio }) => {\n if (disposed) return;\n initStudio(initialId ? { initialId } : undefined);\n });\n return () => {\n disposed = true;\n document.body.classList.remove(\"editor-active\");\n document.documentElement.classList.remove(\"editor-active\");\n };\n }, [initialId]);\n\n return (\n <section className=\"studio-shell w-full\" data-pagefind-ignore=\"all\">\n <div dangerouslySetInnerHTML={{ __html: SKELETON }} />\n </section>\n );\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/Studio.tsx","../src/StudioMount.tsx","../src/repository.ts"],"names":["setDef","addElement","deleteElement","useEffect","jsx"],"mappings":";;;;;;;AAeA,SAAS,WAAW,GAAA,EAA2C;AAC7D,EAAA,OAAO,GAAA,CAAI,QAAA;AACb;AAEO,SAAS,MAAA,CAAO,EAAE,OAAA,EAAS,MAAA,EAAO,EAAmC;AAC1E,EAAA,MAAM,CAAC,GAAA,EAAKA,OAAM,CAAA,GAAI,SAA4B,OAAO,CAAA;AACzD,EAAA,MAAM,CAAC,UAAA,EAAY,aAAa,CAAA,GAAI,QAAA;AAAA,IAClC,UAAA,CAAW,OAAO,CAAA,CAAE,CAAC,GAAG,EAAA,IAAM;AAAA,GAChC;AACA,EAAA,MAAM,CAAC,OAAA,EAAS,UAAU,CAAA,GAAI,SAAS,KAAK,CAAA;AAC5C,EAAA,MAAM,CAAC,IAAA,EAAM,OAAO,CAAA,GAAI,SAAS,CAAC,CAAA;AAClC,EAAA,MAAM,CAAC,MAAA,EAAQ,SAAS,CAAA,GAAI,SAAS,KAAK,CAAA;AAC1C,EAAA,MAAM,CAAC,KAAA,EAAO,QAAQ,CAAA,GAAI,SAAS,KAAK,CAAA;AAIxC,EAAA,MAAM,EAAE,MAAA,EAAQ,KAAA,EAAO,WAAA,EAAY,GAAI,UAAU,GAAA,EAAK,EAAE,QAAA,EAAU,KAAA,EAAO,CAAA;AAEzE,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,IAAI,OAAA,SAAgB,IAAA,EAAK;AAAA,SACpB;AACH,MAAA,MAAA,CAAO,KAAA,EAAM;AACb,MAAA,MAAA,CAAO,KAAK,IAAI,CAAA;AAAA,IAClB;AAAA,EACF,CAAA,EAAG,CAAC,OAAA,EAAS,MAAA,EAAQ,IAAI,CAAC,CAAA;AAE1B,EAAA,MAAM,QAAA,GAAW,WAAW,GAAG,CAAA;AAC/B,EAAA,MAAM,QAAA,GAAW,SAAS,IAAA,CAAK,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,UAAU,CAAA,IAAK,IAAA;AAC9D,EAAA,MAAM,WAAA,GAAc,QAAA,GAChB,MAAA,CAAO,OAAA,CAAQ,QAAQ,CAAA,CAAE,MAAA;AAAA,IACvB,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,KAAA,CACT,OAAO,KAAA,KAAU,QAAA,IAAY,OAAO,KAAA,KAAU,QAAA,KAC/C,GAAA,KAAQ,QACR,GAAA,KAAQ;AAAA,MAEZ,EAAC;AAEL,EAAA,SAAS,YAAY,IAAA,EAA+B;AAClD,IAAAA,OAAAA,CAAO,CAAC,OAAA,MAAa;AAAA,MACnB,GAAG,OAAA;AAAA,MACH,QAAA,EAAU;AAAA,KACZ,CAAE,CAAA;AACF,IAAA,QAAA,CAAS,KAAK,CAAA;AAAA,EAChB;AAEA,EAAA,SAAS,aAAA,CACP,EAAA,EACA,GAAA,EACA,GAAA,EACA,OAAA,EACM;AACN,IAAA,WAAA;AAAA,MACE,QAAA,CAAS,GAAA;AAAA,QAAI,CAAC,CAAA,KACZ,CAAA,CAAE,EAAA,KAAO,EAAA,GAAK,EAAE,GAAG,CAAA,EAAG,CAAC,GAAG,GAAG,OAAA,GAAU,MAAA,CAAO,GAAG,CAAA,GAAI,KAAI,GAAI;AAAA;AAC/D,KACF;AAAA,EACF;AAEA,EAAA,SAASC,WAAAA,GAAmB;AAC1B,IAAA,MAAM,KAAK,CAAA,GAAA,EAAM,IAAA,CAAK,KAAI,CAAE,QAAA,CAAS,EAAE,CAAC,CAAA,CAAA;AACxC,IAAA,MAAM,EAAA,GAAsB;AAAA,MAC1B,IAAA,EAAM,MAAA;AAAA,MACN,EAAA;AAAA,MACA,CAAA,EAAG,GAAA;AAAA,MACH,CAAA,EAAG,GAAA;AAAA,MACH,QAAA,EAAU,CAAA;AAAA,MACV,OAAA,EAAS,KAAA;AAAA,MACT,cAAc,EAAC;AAAA,MACf,QAAA,EAAU,EAAA;AAAA,MACV,KAAA,EAAO,SAAA;AAAA,MACP,UAAA,EAAY,OAAA;AAAA,MACZ,WAAA,EAAa,CAAC,EAAE,KAAA,EAAO,GAAG,GAAA,EAAK,GAAA,CAAI,UAAU,CAAA;AAAA,MAC7C,QAAQ;AAAC,KACX;AACA,IAAA,WAAA,CAAY,CAAC,GAAG,QAAA,EAAU,EAAE,CAAC,CAAA;AAC7B,IAAA,aAAA,CAAc,EAAE,CAAA;AAAA,EAClB;AAEA,EAAA,SAASC,eAAc,EAAA,EAAkB;AACvC,IAAA,WAAA,CAAY,SAAS,MAAA,CAAO,CAAC,MAAM,CAAA,CAAE,EAAA,KAAO,EAAE,CAAC,CAAA;AAC/C,IAAA,IAAI,UAAA,KAAe,EAAA,EAAI,aAAA,CAAc,IAAI,CAAA;AAAA,EAC3C;AAEA,EAAA,eAAe,UAAA,GAA4B;AACzC,IAAA,SAAA,CAAU,IAAI,CAAA;AACd,IAAA,IAAI;AACF,MAAA,MAAM,OAAO,GAAG,CAAA;AAChB,MAAA,QAAA,CAAS,IAAI,CAAA;AAAA,IACf,CAAA,SAAE;AACA,MAAA,SAAA,CAAU,KAAK,CAAA;AAAA,IACjB;AAAA,EACF;AAEA,EAAA,uBACE,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,QAAA,EACb,QAAA,EAAA;AAAA,oBAAA,IAAA,CAAC,OAAA,EAAA,EAAM,WAAU,0BAAA,EACf,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,mBAAA,EACb,QAAA,EAAA;AAAA,wBAAA,IAAA,CAAC,MAAA,EAAA,EAAK,QAAA,EAAA;AAAA,UAAA,gBAAA;AAAA,UAAK,QAAA,CAAS,MAAA;AAAA,UAAO;AAAA,SAAA,EAAC,CAAA;AAAA,wBAC5B,GAAA,CAAC,YAAO,IAAA,EAAK,QAAA,EAAS,WAAU,YAAA,EAAa,OAAA,EAASD,aAAY,QAAA,EAAA,gBAAA,EAElE;AAAA,OAAA,EACF,CAAA;AAAA,sBACA,GAAA,CAAC,QAAG,SAAA,EAAU,gBAAA,EACX,mBAAS,GAAA,CAAI,CAAC,CAAA,qBACb,GAAA,CAAC,IAAA,EAAA,EACC,QAAA,kBAAA,IAAA;AAAA,QAAC,QAAA;AAAA,QAAA;AAAA,UACC,IAAA,EAAK,QAAA;AAAA,UACL,SAAA,EACE,CAAA,CAAE,EAAA,KAAO,UAAA,GAAa,kBAAA,GAAqB,WAAA;AAAA,UAE7C,OAAA,EAAS,MAAM,aAAA,CAAc,CAAA,CAAE,EAAE,CAAA;AAAA,UAEjC,QAAA,EAAA;AAAA,4BAAA,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,gBAAA,EAAkB,QAAA,EAAA,CAAA,CAAE,IAAA,EAAK,CAAA;AAAA,4BACzC,GAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,cAAA,EAAgB,YAAE,EAAA,EAAG;AAAA;AAAA;AAAA,OACvC,EAAA,EAVO,CAAA,CAAE,EAWX,CACD,CAAA,EACH;AAAA,KAAA,EACF,CAAA;AAAA,oBAEA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,gBAAA,EACb,QAAA,EAAA;AAAA,sBAAA,IAAA,CAAC,KAAA,EAAA,EAAI,WAAU,oBAAA,EACb,QAAA,EAAA;AAAA,wBAAA,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,QAAA;AAAA,YACL,SAAA,EAAU,YAAA;AAAA,YACV,OAAA,EAAS,MAAM,qBAAA,CAAsB,GAAG,CAAA;AAAA,YACzC,QAAA,EAAA;AAAA;AAAA,SAED;AAAA,wBACA,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,QAAA;AAAA,YACL,SAAA,EAAU,YAAA;AAAA,YACV,SAAS,MAAM,UAAA,CAAW,CAAC,CAAA,KAAM,CAAC,CAAC,CAAA;AAAA,YAElC,oBAAU,qBAAA,GAAS;AAAA;AAAA,SACtB;AAAA,wBACA,GAAA;AAAA,UAAC,OAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,OAAA;AAAA,YACL,GAAA,EAAK,CAAA;AAAA,YACL,KAAK,GAAA,CAAI,QAAA;AAAA,YACT,IAAA,EAAM,EAAA;AAAA,YACN,KAAA,EAAO,IAAA;AAAA,YACP,QAAA,EAAU,OAAA;AAAA,YACV,QAAA,EAAU,CAAC,CAAA,KAAM;AACf,cAAA,OAAA,CAAQ,MAAA,CAAO,CAAA,CAAE,MAAA,CAAO,KAAK,CAAC,CAAA;AAC9B,cAAA,UAAA,CAAW,KAAK,CAAA;AAAA,YAClB;AAAA;AAAA,SACF;AAAA,wBACA,IAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,aAAA,EACb,QAAA,EAAA;AAAA,UAAA,IAAA;AAAA,UAAK,KAAA;AAAA,UAAI,GAAA,CAAI,QAAA;AAAA,UAAS;AAAA,SAAA,EACzB,CAAA;AAAA,wBACA,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,QAAA;AAAA,YACL,SAAA,EAAU,wBAAA;AAAA,YACV,OAAA,EAAS,MAAM,KAAK,UAAA,EAAW;AAAA,YAC/B,QAAA,EAAU,MAAA;AAAA,YAET,QAAA,EAAA,MAAA,GAAS,2BAAA,GAAU,KAAA,GAAQ,2BAAA,GAAU;AAAA;AAAA;AACxC,OAAA,EACF,CAAA;AAAA,sBACA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,eAAA,EAIb,QAAA,kBAAA,GAAA,CAAC,cAAA,EAAA,EAAe,GAAA,EAAK,GAAA,EAAK,IAAA,EAAM,OAAA,GAAU,WAAA,CAAY,IAAA,GAAO,MAAM,CAAA,EACrE;AAAA,KAAA,EACF,CAAA;AAAA,oBAEA,IAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,2BAAA,EACf,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mBAAA,EAAoB,QAAA,EAAA,cAAA,EAAE,CAAA;AAAA,sBACrC,IAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,cAAA,EACf,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,UAAK,QAAA,EAAA,cAAA,EAAE,CAAA;AAAA,wBACR,GAAA;AAAA,UAAC,OAAA;AAAA,UAAA;AAAA,YACC,OAAO,GAAA,CAAI,KAAA;AAAA,YACX,QAAA,EAAU,CAAC,CAAA,KAAM;AACf,cAAAD,OAAAA,CAAO,CAAC,CAAA,MAAO,EAAE,GAAG,GAAG,KAAA,EAAO,CAAA,CAAE,MAAA,CAAO,KAAA,EAAM,CAAE,CAAA;AAC/C,cAAA,QAAA,CAAS,KAAK,CAAA;AAAA,YAChB;AAAA;AAAA;AACF,OAAA,EACF,CAAA;AAAA,sBACA,IAAA,CAAC,OAAA,EAAA,EAAM,SAAA,EAAU,cAAA,EACf,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,UAAK,QAAA,EAAA,eAAA,EAAa,CAAA;AAAA,wBACnB,GAAA;AAAA,UAAC,OAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,QAAA;AAAA,YACL,OAAO,GAAA,CAAI,QAAA;AAAA,YACX,QAAA,EAAU,CAAC,CAAA,KAAM;AACf,cAAAA,OAAAA,CAAO,CAAC,CAAA,MAAO,EAAE,GAAG,CAAA,EAAG,QAAA,EAAU,MAAA,CAAO,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA,EAAE,CAAE,CAAA;AAC1D,cAAA,QAAA,CAAS,KAAK,CAAA;AAAA,YAChB;AAAA;AAAA;AACF,OAAA,EACF,CAAA;AAAA,sBACA,IAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,mBAAA,EAAoB,QAAA,EAAA;AAAA,QAAA,eAAA;AAAA,QAC7B,QAAA,GAAW,CAAA,CAAA,EAAI,QAAA,CAAS,IAAI,CAAA,CAAA,CAAA,GAAM;AAAA,OAAA,EACxC,CAAA;AAAA,MACC,2BACC,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,QAAA,WAAA,CAAY,GAAA,CAAI,CAAC,CAAC,GAAA,EAAK,KAAK,CAAA,qBAC3B,IAAA,CAAC,OAAA,EAAA,EAAgB,SAAA,EAAU,cAAA,EACzB,QAAA,EAAA;AAAA,0BAAA,GAAA,CAAC,UAAM,QAAA,EAAA,GAAA,EAAI,CAAA;AAAA,0BACX,GAAA;AAAA,YAAC,OAAA;AAAA,YAAA;AAAA,cACC,KAAA,EAAO,OAAO,KAAK,CAAA;AAAA,cACnB,QAAA,EAAU,CAAC,CAAA,KACT,aAAA;AAAA,gBACE,QAAA,CAAS,EAAA;AAAA,gBACT,GAAA;AAAA,gBACA,EAAE,MAAA,CAAO,KAAA;AAAA,gBACT,OAAO,KAAA,KAAU;AAAA;AACnB;AAAA;AAEJ,SAAA,EAAA,EAZU,GAaZ,CACD,CAAA;AAAA,wBACD,GAAA;AAAA,UAAC,QAAA;AAAA,UAAA;AAAA,YACC,IAAA,EAAK,QAAA;AAAA,YACL,SAAA,EAAU,uBAAA;AAAA,YACV,OAAA,EAAS,MAAME,cAAAA,CAAc,QAAA,CAAS,EAAE,CAAA;AAAA,YACzC,QAAA,EAAA;AAAA;AAAA;AAED,OAAA,EACF,CAAA,mBAEA,GAAA,CAAC,GAAA,EAAA,EAAE,SAAA,EAAU,gBAAe,QAAA,EAAA,oDAAA,EAAU;AAAA,KAAA,EAE1C;AAAA,GAAA,EACF,CAAA;AAEJ;AChOA,IAAM,cAAA,GAMA;AAAA,EACJ,EAAE,EAAA,EAAI,SAAA,EAAW,KAAA,EAAO,KAAA,EAAO,KAAK,YAAA,EAAW,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAG;AAAA,EAC5D,EAAE,EAAA,EAAI,UAAA,EAAY,KAAA,EAAO,MAAA,EAAQ,KAAK,IAAA,EAAM,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAG;AAAA,EACzD,EAAE,EAAA,EAAI,UAAA,EAAY,KAAA,EAAO,KAAA,EAAO,KAAK,aAAA,EAAY,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAG;AAAA,EAC9D,EAAE,EAAA,EAAI,SAAA,EAAW,KAAA,EAAO,KAAA,EAAO,KAAK,oBAAA,EAAO,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAG;AAAA,EACxD,EAAE,EAAA,EAAI,UAAA,EAAY,KAAA,EAAO,KAAA,EAAO,KAAK,oBAAA,EAAO,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA,EAAG;AAAA,EACzD,EAAE,EAAA,EAAI,SAAA,EAAW,KAAA,EAAO,MAAA,EAAQ,KAAK,oBAAA,EAAO,CAAA,EAAG,EAAA,EAAI,CAAA,EAAG,EAAA;AACxD,CAAA;AAEA,IAAM,cAAc,cAAA,CAAe,GAAA;AAAA,EACjC,CAAC,CAAA,KAAM;AAAA,gGAAA,EACgE,CAAA,CAAE,EAAE,CAAA,EAAA,EAAK,CAAA,CAAE,GAAG,CAAA,UAAA,EAAa,CAAA,CAAE,EAAE,CAAA,MAAA,EAAM,CAAA,CAAE,GAAG,CAAA,sBAAA,EAAyB,EAAE,EAAE,CAAA;AAAA,oDAAA,EAC1F,CAAA,CAAE,CAAA,GAAI,CAAC,CAAA,CAAA,EAAI,EAAE,CAAA,GAAI,CAAC,CAAA,SAAA,EAAY,CAAA,CAAE,CAAA,GAAI,CAAC,CAAA,UAAA,EAAa,CAAA,CAAE,IAAI,CAAC,CAAA;AAAA,iCAAA,EAC5E,CAAA,CAAE,CAAC,CAAA,UAAA,EAAa,CAAA,CAAE,CAAC,CAAA;AAAA;AAAA,wCAAA,EAEZ,EAAE,KAAK,CAAA;AAAA,sCAAA,EACT,EAAE,GAAG,CAAA;AAAA,aAAA;AAE7C,CAAA,CAAE,KAAK,EAAE,CAAA;AAET,IAAM,QAAA,GAAW;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,2CAAA,EA4C4B,WAAW,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,CAAA;AAkJjD,SAAS,WAAA,CAAY;AAAA,EAC1B,SAAA;AAAA,EACA,UAAA;AAAA,EACA,WAAA,GAAc,eAAA;AAAA,EACd;AACF,CAAA,EAAwC;AACtC,EAAA,MAAM,MAAA,GAAS,OAAO,KAAK,CAAA;AAE3B,EAAAC,UAAU,MAAM;AACd,IAAA,IAAI,OAAO,OAAA,EAAS;AACpB,IAAA,MAAA,CAAO,OAAA,GAAU,IAAA;AACjB,IAAA,IAAI,UAAA,+BAAyC,UAAU,CAAA;AACvD,IAAA,IAAI,QAAA,GAAW,KAAA;AACf,IAAA,KAAK,OAAO,oBAAe,CAAA,CAAE,KAAK,CAAC,EAAE,YAAW,KAAM;AACpD,MAAA,IAAI,QAAA,EAAU;AACd,MAAA,UAAA,CAAW,EAAE,SAAA,EAAW,WAAA,EAAa,YAAA,EAAc,CAAA;AAAA,IACrD,CAAC,CAAA;AACD,IAAA,OAAO,MAAM;AACX,MAAA,QAAA,GAAW,IAAA;AACX,MAAA,QAAA,CAAS,IAAA,CAAK,SAAA,CAAU,MAAA,CAAO,eAAe,CAAA;AAC9C,MAAA,QAAA,CAAS,eAAA,CAAgB,SAAA,CAAU,MAAA,CAAO,eAAe,CAAA;AAAA,IAC3D,CAAA;AAAA,EACF,GAAG,CAAC,WAAA,EAAa,SAAA,EAAW,UAAA,EAAY,YAAY,CAAC,CAAA;AAErD,EAAA,uBACEC,GAAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,uBAAsB,sBAAA,EAAqB,KAAA,EAC5D,QAAA,kBAAAA,GAAAA,CAAC,SAAI,uBAAA,EAAyB,EAAE,MAAA,EAAQ,QAAA,IAAY,CAAA,EACtD,CAAA;AAEJ;AC9OA,SAAS,cAAA,GAA0B;AACjC,EAAA,IAAI,OAAO,YAAA,KAAiB,WAAA;AAC1B,IAAA,MAAM,IAAI,MAAM,gIAA4B,CAAA;AAC9C,EAAA,OAAO,YAAA;AACT;AAEO,SAAS,4BAAA,CACd,OAAA,GAAyC,EAAC,EACrB;AACrB,EAAA,MAAM,MAAA,GAAS,QAAQ,UAAA,IAAc,0BAAA;AACrC,EAAA,MAAM,WAAW,IAAI,GAAA;AAAA,IAAA,CAClB,QAAQ,QAAA,IAAY,EAAC,EAAG,GAAA,CAAI,CAAC,KAAA,KAAU;AACtC,MAAA,MAAM,GAAA,GAAM,uBAAA,CAAwB,KAAA,CAAM,KAAK,CAAA;AAC/C,MAAA,OAAO,CAAC,GAAA,CAAI,EAAA,EAAI,GAAG,CAAA;AAAA,IACrB,CAAC;AAAA,GACH;AACA,EAAA,MAAM,OAAA,GAAU,MACd,OAAA,CAAQ,OAAA,IAAW,cAAA,EAAe;AACpC,EAAA,MAAM,QAAA,GAAW,GAAG,MAAM,CAAA,MAAA,CAAA;AAC1B,EAAA,MAAM,cAAc,CAAC,EAAA,KAAuB,CAAA,EAAG,MAAM,aAAa,EAAE,CAAA,CAAA;AACpE,EAAA,MAAM,UAAU,MAAgB;AAC9B,IAAA,MAAM,KAAA,GAAQ,OAAA,EAAQ,CAAE,OAAA,CAAQ,QAAQ,CAAA;AACxC,IAAA,IAAI,CAAC,KAAA,EAAO,OAAO,EAAC;AACpB,IAAA,IAAI;AACF,MAAA,MAAM,MAAA,GAAkB,IAAA,CAAK,KAAA,CAAM,KAAK,CAAA;AACxC,MAAA,OAAO,KAAA,CAAM,OAAA,CAAQ,MAAM,CAAA,GACvB,MAAA,CAAO,MAAA,CAAO,CAAC,EAAA,KAAqB,OAAO,EAAA,KAAO,QAAQ,CAAA,GAC1D,EAAC;AAAA,IACP,CAAA,CAAA,MAAQ;AACN,MAAA,OAAO,EAAC;AAAA,IACV;AAAA,EACF,CAAA;AACA,EAAA,MAAM,KAAA,GAAQ,CAAC,GAAA,KAA8C;AAC3D,IAAA,MAAM,MAAA,GAAS,uBAAA,CAAwB,KAAA,CAAM,GAAG,CAAA;AAChD,IAAA,MAAM,MAAM,OAAA,EAAQ;AACpB,IAAA,IAAI,CAAC,GAAA,CAAI,QAAA,CAAS,MAAA,CAAO,EAAE,CAAA;AACzB,MAAA,OAAA,EAAQ,CAAE,OAAA,CAAQ,QAAA,EAAU,IAAA,CAAK,SAAA,CAAU,CAAC,GAAG,GAAA,EAAK,MAAA,CAAO,EAAE,CAAC,CAAC,CAAA;AACjE,IAAA,OAAA,EAAQ,CAAE,QAAQ,WAAA,CAAY,MAAA,CAAO,EAAE,CAAA,EAAG,IAAA,CAAK,SAAA,CAAU,MAAM,CAAC,CAAA;AAChE,IAAA,OAAO,MAAA;AAAA,EACT,CAAA;AACA,EAAA,MAAM,IAAA,GAAO,CAAC,EAAA,KAAyC;AACrD,IAAA,MAAM,QAAQ,OAAA,EAAQ,CAAE,OAAA,CAAQ,WAAA,CAAY,EAAE,CAAC,CAAA;AAC/C,IAAA,OAAO,QAAQ,uBAAA,CAAwB,KAAA,CAAM,KAAK,KAAA,CAAM,KAAK,CAAC,CAAA,GAAI,IAAA;AAAA,EACpE,CAAA;AAEA,EAAA,OAAO;AAAA,IACL,MAAM,IAAA,GAAO;AACX,MAAA,MAAM,KAAA,GAAQ,OAAA,EAAQ,CACnB,GAAA,CAAI,CAAC,EAAA,KAAO,IAAA,CAAK,EAAE,CAAC,CAAA,CACpB,MAAA,CAAO,CAAC,GAAA,KAAkC,QAAQ,IAAI,CAAA;AACzD,MAAA,MAAM,QAAA,GAAW,IAAI,GAAA,CAAI,KAAA,CAAM,IAAI,CAAC,GAAA,KAAQ,GAAA,CAAI,EAAE,CAAC,CAAA;AACnD,MAAA,OAAO;AAAA,QACL,GAAG,KAAA,CAAM,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,UACrB,IAAI,GAAA,CAAI,EAAA;AAAA,UACR,KAAA,EAAO,GAAA,CAAI,KAAA,IAAS,GAAA,CAAI,EAAA;AAAA,UACxB,aAAa,GAAA,CAAI,WAAA;AAAA,UACjB,MAAA,EAAQ;AAAA,SACV,CAAE,CAAA;AAAA,QACF,GAAG,CAAC,GAAG,SAAS,MAAA,EAAQ,EACrB,MAAA,CAAO,CAAC,QAAQ,CAAC,QAAA,CAAS,IAAI,GAAA,CAAI,EAAE,CAAC,CAAA,CACrC,GAAA,CAAI,CAAC,GAAA,MAAS;AAAA,UACb,IAAI,GAAA,CAAI,EAAA;AAAA,UACR,KAAA,EAAO,GAAA,CAAI,KAAA,IAAS,GAAA,CAAI,EAAA;AAAA,UACxB,aAAa,GAAA,CAAI,WAAA;AAAA,UACjB,MAAA,EAAQ;AAAA,SACV,CAAE;AAAA,OACN;AAAA,IACF,CAAA;AAAA,IACA,MAAM,KAAK,EAAA,EAAI;AACb,MAAA,MAAM,MAAM,IAAA,CAAK,EAAE,CAAA,IAAK,QAAA,CAAS,IAAI,EAAE,CAAA;AACvC,MAAA,IAAI,CAAC,GAAA,EAAK,MAAM,IAAI,KAAA,CAAM,CAAA,CAAA,EAAI,EAAE,CAAA,oFAAA,CAAqB,CAAA;AACrD,MAAA,OAAO,gBAAgB,GAAG,CAAA;AAAA,IAC5B,CAAA;AAAA,IACA,MAAM,MAAA,CAAO,EAAA,EAAI,KAAA,EAAO;AACtB,MAAA,IAAI,IAAA,CAAK,EAAE,CAAA,EAAG,MAAM,IAAI,KAAA,CAAM,CAAA,CAAA,EAAI,EAAE,CAAA,6EAAA,CAAmB,CAAA;AACvD,MAAA,OAAO,KAAA;AAAA,QACL,wBAAwB,KAAA,CAAM;AAAA,UAC5B,aAAA,EAAe,CAAA;AAAA,UACf,EAAA;AAAA,UACA,KAAA;AAAA,UACA,WAAA,EAAa;AAAA,SACd;AAAA,OACH;AAAA,IACF,CAAA;AAAA,IACA,MAAM,KAAK,GAAA,EAAK;AACd,MAAA,OAAO,MAAM,GAAG,CAAA;AAAA,IAClB,CAAA;AAAA,IACA,MAAM,OAAO,EAAA,EAAI;AACf,MAAA,OAAA,EAAQ,CAAE,UAAA,CAAW,WAAA,CAAY,EAAE,CAAC,CAAA;AACpC,MAAA,OAAA,EAAQ,CAAE,OAAA;AAAA,QACR,QAAA;AAAA,QACA,IAAA,CAAK,UAAU,OAAA,EAAQ,CAAE,OAAO,CAAC,KAAA,KAAU,KAAA,KAAU,EAAE,CAAC;AAAA,OAC1D;AAAA,IACF;AAAA,GACF;AACF","file":"index.js","sourcesContent":["\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport type { AnimationDocument } from \"@kokoa/clotho\";\nimport { AnimationStage, usePlayer } from \"@kokoa/clotho/react\";\nimport { downloadAnimationJson } from \"./export-json\";\n\ntype EditableElement = Record<string, unknown> & { id: string; type: string };\ntype ElementList = AnimationDocument[\"elements\"];\n\nexport interface StudioProps {\n initial: AnimationDocument;\n onSave: (def: AnimationDocument) => void | Promise<void>;\n}\n\nfunction elementsOf(def: AnimationDocument): EditableElement[] {\n return def.elements as unknown as EditableElement[];\n}\n\nexport function Studio({ initial, onSave }: StudioProps): React.JSX.Element {\n const [def, setDef] = useState<AnimationDocument>(initial);\n const [selectedId, setSelectedId] = useState<string | null>(\n elementsOf(initial)[0]?.id ?? null,\n );\n const [playing, setPlaying] = useState(false);\n const [time, setTime] = useState(0);\n const [saving, setSaving] = useState(false);\n const [saved, setSaved] = useState(false);\n\n // The clock for preview playback. It lives outside React (core/player), so the\n // editor subscribes rather than owning a rAF loop of its own.\n const { player, state: playerState } = usePlayer(def, { autoplay: false });\n\n useEffect(() => {\n if (playing) player.play();\n else {\n player.pause();\n player.seek(time);\n }\n }, [playing, player, time]);\n\n const elements = elementsOf(def);\n const selected = elements.find((e) => e.id === selectedId) ?? null;\n const scalarProps = selected\n ? Object.entries(selected).filter(\n ([key, value]) =>\n (typeof value === \"string\" || typeof value === \"number\") &&\n key !== \"id\" &&\n key !== \"type\",\n )\n : [];\n\n function setElements(next: EditableElement[]): void {\n setDef((current) => ({\n ...current,\n elements: next as unknown as ElementList,\n }));\n setSaved(false);\n }\n\n function updateElement(\n id: string,\n key: string,\n raw: string,\n numeric: boolean,\n ): void {\n setElements(\n elements.map((e) =>\n e.id === id ? { ...e, [key]: numeric ? Number(raw) : raw } : e,\n ),\n );\n }\n\n function addElement(): void {\n const id = `el-${Date.now().toString(36)}`;\n const el: EditableElement = {\n type: \"text\",\n id,\n x: 100,\n y: 100,\n rotation: 0,\n content: \"New\",\n translations: {},\n fontSize: 24,\n color: \"#4f46e5\",\n textAnchor: \"start\",\n appearances: [{ start: 0, end: def.duration }],\n tracks: [],\n };\n setElements([...elements, el]);\n setSelectedId(id);\n }\n\n function deleteElement(id: string): void {\n setElements(elements.filter((e) => e.id !== id));\n if (selectedId === id) setSelectedId(null);\n }\n\n async function handleSave(): Promise<void> {\n setSaving(true);\n try {\n await onSave(def);\n setSaved(true);\n } finally {\n setSaving(false);\n }\n }\n\n return (\n <div className=\"studio\">\n <aside className=\"studio-panel studio-left\">\n <div className=\"studio-panel-head\">\n <span>요소 ({elements.length})</span>\n <button type=\"button\" className=\"studio-btn\" onClick={addElement}>\n + 추가\n </button>\n </div>\n <ul className=\"studio-el-list\">\n {elements.map((e) => (\n <li key={e.id}>\n <button\n type=\"button\"\n className={\n e.id === selectedId ? \"studio-el active\" : \"studio-el\"\n }\n onClick={() => setSelectedId(e.id)}\n >\n <span className=\"studio-el-type\">{e.type}</span>\n <span className=\"studio-el-id\">{e.id}</span>\n </button>\n </li>\n ))}\n </ul>\n </aside>\n\n <div className=\"studio-preview\">\n <div className=\"studio-preview-bar\">\n <button\n type=\"button\"\n className=\"studio-btn\"\n onClick={() => downloadAnimationJson(def)}\n >\n JSON 내보내기\n </button>\n <button\n type=\"button\"\n className=\"studio-btn\"\n onClick={() => setPlaying((p) => !p)}\n >\n {playing ? \"⏸ 정지\" : \"▶ 재생\"}\n </button>\n <input\n type=\"range\"\n min={0}\n max={def.duration}\n step={50}\n value={time}\n disabled={playing}\n onChange={(e) => {\n setTime(Number(e.target.value));\n setPlaying(false);\n }}\n />\n <span className=\"studio-time\">\n {time} / {def.duration}ms\n </span>\n <button\n type=\"button\"\n className=\"studio-btn studio-save\"\n onClick={() => void handleSave()}\n disabled={saving}\n >\n {saving ? \"저장 중…\" : saved ? \"✓ 저장됨\" : \"저장\"}\n </button>\n </div>\n <div className=\"studio-canvas\">\n {/* One frame at a time: the editor's scrubber is the source of truth, and the\n player only supplies the clock while previewing. Same buildScene path as\n the published animation, so preview and production cannot diverge. */}\n <AnimationStage doc={def} time={playing ? playerState.time : time} />\n </div>\n </div>\n\n <aside className=\"studio-panel studio-right\">\n <div className=\"studio-panel-head\">메타</div>\n <label className=\"studio-field\">\n <span>제목</span>\n <input\n value={def.title}\n onChange={(e) => {\n setDef((d) => ({ ...d, title: e.target.value }));\n setSaved(false);\n }}\n />\n </label>\n <label className=\"studio-field\">\n <span>duration (ms)</span>\n <input\n type=\"number\"\n value={def.duration}\n onChange={(e) => {\n setDef((d) => ({ ...d, duration: Number(e.target.value) }));\n setSaved(false);\n }}\n />\n </label>\n <div className=\"studio-panel-head\">\n 속성 {selected ? `(${selected.type})` : \"\"}\n </div>\n {selected ? (\n <>\n {scalarProps.map(([key, value]) => (\n <label key={key} className=\"studio-field\">\n <span>{key}</span>\n <input\n value={String(value)}\n onChange={(e) =>\n updateElement(\n selected.id,\n key,\n e.target.value,\n typeof value === \"number\",\n )\n }\n />\n </label>\n ))}\n <button\n type=\"button\"\n className=\"studio-btn studio-del\"\n onClick={() => deleteElement(selected.id)}\n >\n 요소 삭제\n </button>\n </>\n ) : (\n <p className=\"studio-empty\">요소를 선택하세요.</p>\n )}\n </aside>\n </div>\n );\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport type { AnimationRepository } from \"./repository\";\nimport { configureAnimationRepository } from \"./legacy/api\";\nimport type { ImageUploadResolver } from \"./legacy/studio-image-upload\";\n// The stylesheet ships as a separate entry rather than an import, so a consumer that\n// only wants the headless pieces does not pull CSS into its bundle:\n// import \"@kokoa/clotho-editor/styles.css\";\n\nexport interface StudioMountProps {\n initialId?: string;\n repository?: AnimationRepository;\n editorTitle?: string;\n resolveImage?: ImageUploadResolver;\n}\n\nconst CANVAS_PRESETS: {\n id: string;\n label: string;\n sub: string;\n w: number;\n h: number;\n}[] = [\n { id: \"800x500\", label: \"8:5\", sub: \"800×500\", w: 32, h: 20 },\n { id: \"1280x720\", label: \"16:9\", sub: \"HD\", w: 32, h: 18 },\n { id: \"1024x768\", label: \"4:3\", sub: \"1024×768\", w: 28, h: 21 },\n { id: \"600x600\", label: \"1:1\", sub: \"정사각\", w: 22, h: 22 },\n { id: \"1200x400\", label: \"3:1\", sub: \"와이드\", w: 36, h: 12 },\n { id: \"375x812\", label: \"9:19\", sub: \"모바일\", w: 14, h: 30 },\n];\n\nconst PRESET_HTML = CANVAS_PRESETS.map(\n (p) => `\n <button type=\"button\" class=\"studio-preset-btn\" aria-label=\"캔버스 크기 ${p.id} (${p.sub})\" title=\"${p.id} · ${p.sub}\" data-canvas-preset=\"${p.id}\">\n <svg class=\"studio-preset-thumb\" viewBox=\"0 0 ${p.w + 2} ${p.h + 2}\" width=\"${p.w + 2}\" height=\"${p.h + 2}\" aria-hidden=\"true\">\n <rect x=\"1\" y=\"1\" width=\"${p.w}\" height=\"${p.h}\" rx=\"2\" />\n </svg>\n <span class=\"studio-preset-label\">${p.label}</span>\n <span class=\"studio-preset-sub\">${p.sub}</span>\n </button>`,\n).join(\"\");\n\nconst SKELETON = `\n<div id=\"studio-app\" class=\"studio-app\">\n <header class=\"studio-header\">\n <h1 id=\"studio-editor-title\" class=\"studio-header-title\">Clotho Editor</h1>\n <div class=\"studio-header-actions\">\n <button type=\"button\" id=\"studio-open\" class=\"studio-btn\" aria-label=\"저장된 애니메이션 열기\">📁 열기</button>\n <button type=\"button\" id=\"studio-new\" class=\"studio-btn\" aria-label=\"새 애니메이션 만들기\">+ 새 애니메이션</button>\n <button type=\"button\" id=\"studio-undo\" class=\"studio-btn studio-btn-icon\" aria-label=\"실행 취소\" title=\"실행 취소 (⌘Z)\" disabled>↶</button>\n <button type=\"button\" id=\"studio-redo\" class=\"studio-btn studio-btn-icon\" aria-label=\"다시 실행\" title=\"다시 실행 (⌘⇧Z / ⌘Y)\" disabled>↷</button>\n <button type=\"button\" id=\"studio-grid-toggle\" class=\"studio-btn studio-btn-grid\" aria-label=\"격자 + 스냅\" title=\"격자 + 스냅 (G)\" aria-pressed=\"false\"><span class=\"studio-btn-grid-icon\">⊞</span><span class=\"studio-btn-grid-label\" id=\"studio-grid-label\">격자 끔</span></button>\n <input type=\"text\" id=\"studio-title\" class=\"studio-title-input\" placeholder=\"제목\" aria-label=\"애니메이션 제목\" disabled />\n <span id=\"studio-id-display\" class=\"studio-id-display\"></span>\n <span id=\"studio-status\" class=\"studio-status\" aria-live=\"polite\">대기 중</span>\n <button type=\"button\" id=\"studio-save\" class=\"studio-btn studio-btn-primary\" aria-label=\"저장\" disabled>💾 저장 (⌘S)</button>\n <button type=\"button\" id=\"studio-export-json\" class=\"studio-btn\" aria-label=\"현재 애니메이션을 JSON 파일로 내보내기\" disabled>⬇ JSON 내보내기</button>\n <button type=\"button\" id=\"studio-player-preview\" class=\"studio-btn\" aria-label=\"실제 애니메이션 컴포넌트 미리보기\" aria-pressed=\"false\">↗ 실제 미리보기</button>\n <button type=\"button\" id=\"studio-import-json\" class=\"studio-btn\" aria-label=\"JSON 파일에서 애니메이션 가져오기\">⬆ JSON 가져오기</button>\n <input type=\"file\" id=\"studio-import-file\" accept=\"application/json,.json\" hidden />\n <button type=\"button\" id=\"studio-delete\" class=\"studio-btn studio-btn-danger\" aria-label=\"삭제\" disabled>🗑 삭제</button>\n </div>\n </header>\n\n <div class=\"studio-body\">\n <aside class=\"studio-tools\" aria-label=\"요소 추가\">\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">도구</div>\n <button type=\"button\" class=\"studio-tool-btn is-active\" data-studio-tool=\"select\" aria-pressed=\"true\" title=\"선택 · 이동 · 크기 (V)\"><span>↖ 선택 · 이동 · 크기</span><kbd>V</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"rect\" title=\"사각형 (R)\" aria-label=\"사각형 도구\"><span>□ Rect</span><kbd>R</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"circle\" title=\"원 (O)\" aria-label=\"원 도구\"><span>○ Circle</span><kbd>O</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"line\" title=\"선 (L)\" aria-label=\"선 도구\"><span>/ Line</span><kbd>L</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"arrow\" title=\"화살표 (A)\" aria-label=\"화살표 도구\"><span>↗ Arrow</span><kbd>A</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"text\" title=\"텍스트 (T)\" aria-label=\"텍스트 도구\"><span>T Text</span><kbd>T</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"image\" title=\"이미지 (I)\" aria-label=\"이미지 도구\"><span>🖼 Image</span><kbd>I</kbd></button>\n <button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"path\" title=\"Path (B)\" aria-label=\"Path 도구\"><span>✎ Path</span><kbd>B</kbd></button>\n <div class=\"studio-tool-with-option\"><button type=\"button\" class=\"studio-tool-btn\" data-add-element=\"polygon\" title=\"다각형 (Y)\" aria-label=\"다각형 도구\"><span>⬢ Polygon</span><kbd>Y</kbd></button><label>변 <input type=\"number\" id=\"studio-polygon-sides\" min=\"3\" max=\"24\" value=\"6\" aria-label=\"다각형의 변 개수\" /></label></div>\n <button type=\"button\" class=\"studio-tool-btn\" id=\"studio-open-icons\" title=\"아이콘 라이브러리\" aria-label=\"아이콘 라이브러리 추가\">🎨 Icons</button>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">캔버스 크기</div>\n <div class=\"studio-canvas-size-row\">\n <input type=\"number\" id=\"studio-canvas-width\" class=\"studio-canvas-size-input\" min=\"100\" max=\"8000\" step=\"10\" placeholder=\"width\" />\n <span class=\"studio-canvas-size-x\">×</span>\n <input type=\"number\" id=\"studio-canvas-height\" class=\"studio-canvas-size-input\" min=\"100\" max=\"8000\" step=\"10\" placeholder=\"height\" />\n </div>\n <div class=\"studio-canvas-presets\">${PRESET_HTML}</div>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">이미지</div>\n <button type=\"button\" class=\"studio-tool-btn\" id=\"studio-image-upload\">📤 이미지 업로드</button>\n <input type=\"file\" id=\"studio-image-file\" accept=\"image/*\" hidden />\n <div class=\"studio-tools-hint\">이미지를 캔버스에 직접 드래그하거나 ⌘V 로 붙여넣어도 됩니다.</div>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">단축키</div>\n <button type=\"button\" class=\"studio-tool-btn\" id=\"studio-help\">⌨ 단축키 보기 (?)</button>\n </div>\n <div class=\"studio-tools-section\">\n <div class=\"studio-tools-title\">요소 목록</div>\n <input type=\"text\" id=\"studio-element-search\" class=\"studio-element-search\" placeholder=\"🔍 검색 (name / label / id / type)\" autocomplete=\"off\" />\n <ul id=\"studio-element-list\" class=\"studio-element-list\"></ul>\n </div>\n </aside>\n\n <main class=\"studio-canvas-wrap\">\n <div class=\"studio-canvas-frame\">\n <svg id=\"studio-canvas\" class=\"studio-canvas\" xmlns=\"http://www.w3.org/2000/svg\"></svg>\n </div>\n <button type=\"button\" id=\"studio-floating-help\" class=\"studio-floating-help\" aria-label=\"단축키 보기\" title=\"단축키 보기 (? / Shift+/)\">?</button>\n <div id=\"studio-help-hint\" class=\"studio-help-hint\" hidden>\n <span>💡 단축키는 <kbd>?</kbd> 또는 우측 하단 버튼으로 확인하세요</span>\n <button type=\"button\" id=\"studio-help-hint-close\" aria-label=\"닫기\">✕</button>\n </div>\n </main>\n\n <aside class=\"studio-props\" aria-label=\"속성 패널\">\n <div id=\"studio-props-content\" class=\"studio-props-content\">\n <p class=\"studio-props-empty\">요소 또는 step 을 선택하세요.</p>\n </div>\n </aside>\n </div>\n\n <div id=\"studio-timeline-resizer\" class=\"studio-timeline-resizer\" role=\"separator\" aria-orientation=\"horizontal\" aria-label=\"타임라인 영역 크기 조정\" tabindex=\"0\"><span class=\"studio-timeline-resizer-grip\" aria-hidden=\"true\"><i></i><i></i><i></i></span></div>\n <footer class=\"studio-timeline-wrap\">\n <div class=\"studio-timeline-header\">\n <button type=\"button\" id=\"studio-play\" class=\"studio-btn\">▶ Play</button>\n <button type=\"button\" id=\"studio-restart\" class=\"studio-btn\">⟲ Reset</button>\n <label class=\"studio-speed\">속도\n <input type=\"range\" id=\"studio-speed\" min=\"0.25\" max=\"3\" step=\"0.25\" value=\"1\" />\n <span id=\"studio-speed-value\">1.00x</span>\n </label>\n <label class=\"studio-timeline-option\"><input type=\"checkbox\" id=\"studio-preview-loop\" /> 무한 재생</label>\n <button type=\"button\" id=\"studio-detach-timeline\" class=\"studio-btn\" aria-pressed=\"false\">▣ 타임라인 분리</button>\n <span class=\"studio-timeline-spacer\"></span>\n <button type=\"button\" id=\"studio-add-step\" class=\"studio-btn\">+ Chapter 추가</button>\n </div>\n <div class=\"studio-timeline-tracks-wrap\">\n <div id=\"studio-timeline-tracks\" class=\"studio-timeline-tracks\"></div>\n </div>\n <div class=\"studio-element-tracks-wrap\">\n <div id=\"studio-element-tracks\" class=\"studio-element-tracks\"></div>\n </div>\n </footer>\n\n <dialog id=\"studio-library-dialog\" class=\"studio-dialog\">\n <div class=\"studio-dialog-header\">\n <h2>저장된 애니메이션</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close>✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <ul id=\"studio-library-list\" class=\"studio-library-list\"></ul>\n </div>\n </dialog>\n\n <dialog id=\"studio-icon-dialog\" class=\"studio-dialog\">\n <div class=\"studio-dialog-header\">\n <h2>🎨 아이콘 라이브러리</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-icon-dialog-close>✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <input type=\"search\" id=\"studio-icon-search\" placeholder=\"아이콘 검색…\" class=\"studio-icon-search-input\" />\n <div id=\"studio-icon-list\"></div>\n </div>\n </dialog>\n\n <dialog id=\"studio-help-dialog\" class=\"studio-dialog studio-dialog-small\">\n <div class=\"studio-dialog-header\">\n <h2>⌨ 단축키</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close>✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <table class=\"studio-help-table\">\n <tbody>\n <tr><th>⌘ S</th><td>저장</td></tr>\n <tr><th>⌘ Z / ⌘ ⇧ Z</th><td>Undo / Redo</td></tr>\n <tr><th>⌘ C / V / X</th><td>요소 복사 / 붙여넣기 / 잘라내기</td></tr>\n <tr><th>V / R / O</th><td>선택 / 사각형 / 원 도구</td></tr>\n <tr><th>L / A</th><td>선 / 화살표 도구</td></tr>\n <tr><th>T / I / B / Y</th><td>텍스트 / 이미지 / Path / 다각형 도구</td></tr>\n <tr><th>⌘·Ctrl·Shift + 클릭</th><td>여러 요소 선택 또는 선택 해제</td></tr>\n <tr><th>Delete / Backspace</th><td>선택된 요소 또는 step 삭제</td></tr>\n <tr><th>?</th><td>이 화면 열기</td></tr>\n <tr><th>Esc</th><td>선택 해제 / 다이얼로그 닫기</td></tr>\n <tr><th>드래그 (캔버스)</th><td>요소 이동</td></tr>\n <tr><th>드래그 (요소 위 dot)</th><td>화살표 연결</td></tr>\n <tr><th>드래그 (선/화살표 끝점)</th><td>끝점 재배치 + 다른 요소에 sticky</td></tr>\n <tr><th>드래그 (회전 핸들)</th><td>요소 회전</td></tr>\n <tr><th>드래그 (타임라인 바 우측)</th><td>step duration 조정</td></tr>\n <tr><th>이미지 드래그 in</th><td>이미지 업로드 + 배치</td></tr>\n </tbody>\n </table>\n </div>\n </dialog>\n\n <dialog id=\"studio-new-dialog\" class=\"studio-dialog studio-dialog-small\">\n <div class=\"studio-dialog-header\">\n <h2 id=\"studio-new-dialog-title\">새 애니메이션 만들기</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close aria-label=\"닫기\">✕</button>\n </div>\n <div class=\"studio-dialog-body\">\n <label class=\"studio-field\"><span>ID (영문 소문자 / 숫자 / - / _)</span>\n <input type=\"text\" id=\"studio-new-id\" placeholder=\"예: user-login-flow\" />\n </label>\n <label class=\"studio-field\"><span>제목</span>\n <input type=\"text\" id=\"studio-new-title\" placeholder=\"사용자 로그인 흐름\" />\n </label>\n <div id=\"studio-new-error\" class=\"studio-new-error\"></div>\n </div>\n <footer class=\"studio-dialog-footer\">\n <button type=\"button\" class=\"studio-btn\" data-studio-dialog-close>취소</button>\n <button type=\"button\" id=\"studio-new-create\" class=\"studio-btn studio-btn-primary\">만들기</button>\n </footer>\n </dialog>\n\n <dialog id=\"studio-palette-dialog\" class=\"studio-palette-dialog\">\n <input type=\"text\" id=\"studio-palette-input\" class=\"studio-palette-input\" placeholder=\"🔍 명령 또는 요소 검색…\" autocomplete=\"off\" spellcheck=\"false\" />\n <ul id=\"studio-palette-list\" class=\"studio-palette-list\"></ul>\n <div class=\"studio-palette-footer\">↑↓ 이동 · Enter 실행 · Esc 닫기</div>\n </dialog>\n\n <dialog id=\"studio-history-dialog\" class=\"studio-history-dialog\">\n <div class=\"studio-history-header\">\n <h2>📜 작업 이력</h2>\n <button type=\"button\" class=\"studio-dialog-close\" data-studio-dialog-close aria-label=\"닫기\">✕</button>\n </div>\n <ul id=\"studio-history-list\" class=\"studio-history-list\"></ul>\n <div class=\"studio-history-footer\">항목을 클릭하면 해당 시점으로 이동합니다 · ⌘Z / ⌘⇧Z</div>\n </dialog>\n</div>\n`;\n\nexport function StudioMount({\n initialId,\n repository,\n editorTitle = \"Clotho Editor\",\n resolveImage,\n}: StudioMountProps): React.JSX.Element {\n const inited = useRef(false);\n\n useEffect(() => {\n if (inited.current) return;\n inited.current = true;\n if (repository) configureAnimationRepository(repository);\n let disposed = false;\n void import(\"./legacy/main\").then(({ initStudio }) => {\n if (disposed) return;\n initStudio({ initialId, editorTitle, resolveImage });\n });\n return () => {\n disposed = true;\n document.body.classList.remove(\"editor-active\");\n document.documentElement.classList.remove(\"editor-active\");\n };\n }, [editorTitle, initialId, repository, resolveImage]);\n\n return (\n <section className=\"studio-shell w-full\" data-pagefind-ignore=\"all\">\n <div dangerouslySetInnerHTML={{ __html: SKELETON }} />\n </section>\n );\n}\n","import { animationDocumentSchema, type AnimationDocument } from \"@kokoa/clotho\";\n\nexport interface AnimationSummary {\n id: string;\n title: string;\n description: string;\n updatedAt?: string;\n source?: \"saved\" | \"example\";\n}\n\nexport interface AnimationRepository {\n list(): Promise<AnimationSummary[]>;\n load(id: string): Promise<AnimationDocument>;\n create(id: string, title: string): Promise<AnimationDocument>;\n save(def: AnimationDocument): Promise<AnimationDocument>;\n delete(id: string): Promise<void>;\n}\n\nexport interface LocalStorageRepositoryOptions {\n storageKey?: string;\n examples?: readonly AnimationDocument[];\n storage?: Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\">;\n}\n\nfunction browserStorage(): Storage {\n if (typeof localStorage === \"undefined\")\n throw new Error(\"이 저장소는 브라우저에서만 사용할 수 있습니다.\");\n return localStorage;\n}\n\nexport function createLocalStorageRepository(\n options: LocalStorageRepositoryOptions = {},\n): AnimationRepository {\n const prefix = options.storageKey ?? \"clotho-editor.animations\";\n const examples = new Map(\n (options.examples ?? []).map((value) => {\n const def = animationDocumentSchema.parse(value);\n return [def.id, def] as const;\n }),\n );\n const storage = (): Pick<Storage, \"getItem\" | \"setItem\" | \"removeItem\"> =>\n options.storage ?? browserStorage();\n const indexKey = `${prefix}:index`;\n const documentKey = (id: string): string => `${prefix}:document:${id}`;\n const readIds = (): string[] => {\n const value = storage().getItem(indexKey);\n if (!value) return [];\n try {\n const parsed: unknown = JSON.parse(value);\n return Array.isArray(parsed)\n ? parsed.filter((id): id is string => typeof id === \"string\")\n : [];\n } catch {\n return [];\n }\n };\n const write = (def: AnimationDocument): AnimationDocument => {\n const parsed = animationDocumentSchema.parse(def);\n const ids = readIds();\n if (!ids.includes(parsed.id))\n storage().setItem(indexKey, JSON.stringify([...ids, parsed.id]));\n storage().setItem(documentKey(parsed.id), JSON.stringify(parsed));\n return parsed;\n };\n const read = (id: string): AnimationDocument | null => {\n const value = storage().getItem(documentKey(id));\n return value ? animationDocumentSchema.parse(JSON.parse(value)) : null;\n };\n\n return {\n async list() {\n const saved = readIds()\n .map((id) => read(id))\n .filter((def): def is AnimationDocument => def !== null);\n const savedIds = new Set(saved.map((def) => def.id));\n return [\n ...saved.map((def) => ({\n id: def.id,\n title: def.title || def.id,\n description: def.description,\n source: \"saved\" as const,\n })),\n ...[...examples.values()]\n .filter((def) => !savedIds.has(def.id))\n .map((def) => ({\n id: def.id,\n title: def.title || def.id,\n description: def.description,\n source: \"example\" as const,\n })),\n ];\n },\n async load(id) {\n const def = read(id) ?? examples.get(id);\n if (!def) throw new Error(`'${id}' 애니메이션을 찾을 수 없습니다.`);\n return structuredClone(def);\n },\n async create(id, title) {\n if (read(id)) throw new Error(`'${id}' 애니메이션이 이미 있습니다.`);\n return write(\n animationDocumentSchema.parse({\n clothoVersion: 1,\n id,\n title,\n description: \"\",\n }),\n );\n },\n async save(def) {\n return write(def);\n },\n async delete(id) {\n storage().removeItem(documentKey(id));\n storage().setItem(\n indexKey,\n JSON.stringify(readIds().filter((value) => value !== id)),\n );\n },\n };\n}\n"]}
|