@kokoa/clotho-editor 0.1.0 → 0.1.3

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.
@@ -4,6 +4,27 @@ body.editor-active {
4
4
  overflow: hidden;
5
5
  }
6
6
 
7
+ body.studio-timeline-popout {
8
+ min-width: 0;
9
+ margin: 0;
10
+ background: var(--color-surface-elevated, #f8fafc);
11
+ color: var(--color-fg, #0f172a);
12
+ }
13
+
14
+ body.studio-player-popout {
15
+ min-width: 0;
16
+ margin: 0;
17
+ padding: clamp(1rem, 3vw, 2.5rem);
18
+ box-sizing: border-box;
19
+ background: var(--color-bg, #f1f5f9);
20
+ color: var(--color-fg, #0f172a);
21
+ }
22
+
23
+ body.studio-player-popout #clotho-player-preview {
24
+ width: min(100%, 1200px);
25
+ margin: 0 auto;
26
+ }
27
+
7
28
  @layer components {
8
29
  .studio-shell {
9
30
  height: calc(100vh - 80px);
@@ -18,10 +39,18 @@ body.editor-active {
18
39
  height: 100%;
19
40
  flex: 1;
20
41
  min-height: 0;
21
- border: 1px solid var(--color-border);
42
+ border: 1px solid var(--color-border, #d1d5db);
22
43
  border-radius: 0.5rem;
23
44
  background-color: var(--color-surface);
24
45
  overflow: hidden;
46
+ box-shadow: 0 8px 30px rgb(15 23 42 / 10%);
47
+ }
48
+ .studio-app.is-timeline-detached {
49
+ grid-template-rows: auto minmax(220px, 1fr) 0 0;
50
+ }
51
+ .studio-app.is-timeline-detached .studio-timeline-resizer,
52
+ .studio-app.is-timeline-detached .studio-timeline-wrap {
53
+ display: none;
25
54
  }
26
55
  .studio-timeline-resizer {
27
56
  height: 6px;
@@ -33,25 +62,37 @@ body.editor-active {
33
62
  transition: background-color 100ms ease-out;
34
63
  }
35
64
  .studio-timeline-resizer::before {
36
- content: "";
65
+ content: none;
66
+ }
67
+ .studio-timeline-resizer-grip {
37
68
  position: absolute;
38
69
  left: 50%;
39
70
  top: 50%;
40
- width: 28px;
41
- height: 2px;
71
+ display: grid;
72
+ gap: 2px;
73
+ width: 42px;
74
+ padding: 4px 7px;
42
75
  transform: translate(-50%, -50%);
43
- background-color: var(--color-fg-muted);
44
- border-radius: 1px;
45
- opacity: 0.5;
76
+ border: 1px solid var(--color-border, #9ca3af);
77
+ border-radius: 999px;
78
+ background: var(--color-surface-elevated, #f8fafc);
79
+ box-shadow: 0 1px 4px rgb(15 23 42 / 20%);
80
+ }
81
+ .studio-timeline-resizer-grip i {
82
+ display: block;
83
+ height: 1px;
84
+ background: var(--color-fg-muted, #64748b);
46
85
  }
47
86
  .studio-timeline-resizer:hover,
48
87
  .studio-timeline-resizer.is-dragging {
49
88
  background-color: var(--color-accent);
50
89
  }
51
- .studio-timeline-resizer:hover::before,
52
- .studio-timeline-resizer.is-dragging::before {
53
- background-color: white;
54
- opacity: 1;
90
+ .studio-timeline-resizer:hover .studio-timeline-resizer-grip,
91
+ .studio-timeline-resizer.is-dragging .studio-timeline-resizer-grip,
92
+ .studio-timeline-resizer:focus-visible .studio-timeline-resizer-grip {
93
+ border-color: var(--color-accent, #4f46e5);
94
+ box-shadow: 0 0 0 3px
95
+ color-mix(in srgb, var(--color-accent, #4f46e5) 20%, transparent);
55
96
  }
56
97
  .studio-header {
57
98
  display: flex;
@@ -277,7 +318,9 @@ body.editor-active {
277
318
  letter-spacing: 0.05em;
278
319
  }
279
320
  .studio-tool-btn {
280
- display: block;
321
+ display: flex;
322
+ align-items: center;
323
+ gap: 0.35rem;
281
324
  width: 100%;
282
325
  text-align: left;
283
326
  padding: 0.35rem 0.5rem;
@@ -289,10 +332,62 @@ body.editor-active {
289
332
  cursor: pointer;
290
333
  font-size: 0.78rem;
291
334
  }
335
+ .studio-tool-btn kbd {
336
+ margin-left: auto;
337
+ min-width: 1.15rem;
338
+ padding: 0.05rem 0.22rem;
339
+ border: 1px solid var(--color-border);
340
+ border-radius: 0.22rem;
341
+ background: var(--color-surface-elevated);
342
+ color: var(--color-fg-muted);
343
+ font-family: var(--font-mono);
344
+ font-size: 0.6rem;
345
+ line-height: 1.15;
346
+ text-align: center;
347
+ }
348
+ .studio-tool-btn.is-active kbd {
349
+ border-color: color-mix(in srgb, currentColor 35%, transparent);
350
+ background: color-mix(in srgb, currentColor 10%, transparent);
351
+ color: inherit;
352
+ }
353
+ .studio-tool-with-option {
354
+ display: grid;
355
+ grid-template-columns: 1fr auto;
356
+ gap: 0.35rem;
357
+ align-items: center;
358
+ }
359
+ .studio-tool-with-option label {
360
+ display: inline-flex;
361
+ align-items: center;
362
+ gap: 0.2rem;
363
+ font-size: 0.68rem;
364
+ color: var(--color-fg-muted);
365
+ }
366
+ .studio-tool-with-option input {
367
+ width: 2.8rem;
368
+ padding: 0.25rem;
369
+ border: 1px solid var(--color-border);
370
+ border-radius: 0.25rem;
371
+ background: var(--color-surface);
372
+ color: var(--color-fg);
373
+ }
292
374
  .studio-tool-btn:hover {
293
375
  border-color: var(--color-accent);
294
376
  color: var(--color-accent);
295
377
  }
378
+ .studio-tool-btn.is-active {
379
+ border-color: var(--color-accent);
380
+ background: color-mix(
381
+ in srgb,
382
+ var(--color-accent) 16%,
383
+ var(--color-surface)
384
+ );
385
+ color: var(--color-accent);
386
+ box-shadow: inset 3px 0 0 var(--color-accent);
387
+ }
388
+ body:not([data-studio-tool="select"]) .studio-canvas {
389
+ cursor: crosshair;
390
+ }
296
391
  .studio-element-search {
297
392
  width: 100%;
298
393
  box-sizing: border-box;
@@ -340,6 +435,18 @@ body.editor-active {
340
435
  align-items: center;
341
436
  gap: 0.25rem;
342
437
  }
438
+ .studio-element-list-item {
439
+ padding-left: calc(0.35rem + var(--studio-tree-depth, 0) * 0.9rem);
440
+ }
441
+ .studio-element-list-item.is-group {
442
+ background: color-mix(
443
+ in srgb,
444
+ var(--color-accent) 9%,
445
+ var(--color-surface)
446
+ );
447
+ border-left: 3px solid var(--color-accent);
448
+ font-weight: 650;
449
+ }
343
450
  .studio-element-list-item:hover {
344
451
  background-color: var(--color-surface);
345
452
  }
@@ -726,6 +833,11 @@ body.editor-active {
726
833
  gap: 0.4rem;
727
834
  min-height: 0;
728
835
  }
836
+ body.studio-timeline-popout > .studio-timeline-wrap {
837
+ width: 100vw;
838
+ height: 100vh;
839
+ box-sizing: border-box;
840
+ }
729
841
  .studio-timeline-tracks-wrap {
730
842
  overflow-x: auto;
731
843
  overflow-y: hidden;
@@ -755,6 +867,13 @@ body.editor-active {
755
867
  .studio-speed input[type="range"] {
756
868
  width: 100px;
757
869
  }
870
+ .studio-timeline-option {
871
+ display: inline-flex;
872
+ align-items: center;
873
+ gap: 0.3rem;
874
+ font-size: 0.75rem;
875
+ color: var(--color-fg-muted);
876
+ }
758
877
  .studio-timeline-tracks {
759
878
  display: block;
760
879
  padding-bottom: 0.25rem;
@@ -785,6 +904,13 @@ body.editor-active {
785
904
  text-overflow: ellipsis;
786
905
  white-space: nowrap;
787
906
  }
907
+ .studio-tl-element-label {
908
+ padding-left: calc(0.5rem + var(--studio-tree-depth, 0) * 0.8rem);
909
+ }
910
+ .studio-tl-element-row.is-group .studio-tl-gutter {
911
+ color: var(--color-accent);
912
+ font-weight: 700;
913
+ }
788
914
  .studio-tl-gutter-header {
789
915
  font-weight: 700;
790
916
  color: var(--color-accent);
@@ -1094,11 +1220,12 @@ body.editor-active {
1094
1220
  width: min(640px, 92vw);
1095
1221
  max-height: 80vh;
1096
1222
  padding: 0;
1097
- border: 1px solid var(--color-border);
1223
+ border: 1px solid var(--color-border, #d1d5db);
1098
1224
  border-radius: 0.5rem;
1099
- background-color: var(--color-surface);
1100
- color: var(--color-fg);
1225
+ background-color: var(--color-surface, #ffffff);
1226
+ color: var(--color-fg, #111827);
1101
1227
  overflow: hidden;
1228
+ box-shadow: 0 24px 64px rgb(0 0 0 / 35%);
1102
1229
  }
1103
1230
  .studio-dialog[open] {
1104
1231
  display: flex;
@@ -1111,8 +1238,8 @@ body.editor-active {
1111
1238
  width: min(420px, 92vw);
1112
1239
  }
1113
1240
  .studio-dialog::backdrop {
1114
- background-color: rgba(0, 0, 0, 0.6);
1115
- backdrop-filter: blur(4px);
1241
+ background-color: rgb(15 23 42 / 55%);
1242
+ backdrop-filter: blur(2px);
1116
1243
  }
1117
1244
  .studio-dialog-header {
1118
1245
  display: flex;
@@ -1120,7 +1247,7 @@ body.editor-active {
1120
1247
  align-items: center;
1121
1248
  padding: 0.6rem 0.85rem;
1122
1249
  border-bottom: 1px solid var(--color-border);
1123
- background-color: var(--color-surface-elevated);
1250
+ background-color: var(--color-surface-elevated, #f8fafc);
1124
1251
  }
1125
1252
  .studio-dialog-header h2 {
1126
1253
  margin: 0;
package/dist/index.d.ts CHANGED
@@ -6,10 +6,45 @@ interface StudioProps {
6
6
  }
7
7
  declare function Studio({ initial, onSave }: StudioProps): React.JSX.Element;
8
8
 
9
+ interface AnimationSummary {
10
+ id: string;
11
+ title: string;
12
+ description: string;
13
+ updatedAt?: string;
14
+ source?: "saved" | "example";
15
+ }
16
+ interface AnimationRepository {
17
+ list(): Promise<AnimationSummary[]>;
18
+ load(id: string): Promise<AnimationDocument>;
19
+ create(id: string, title: string): Promise<AnimationDocument>;
20
+ save(def: AnimationDocument): Promise<AnimationDocument>;
21
+ delete(id: string): Promise<void>;
22
+ }
23
+ interface LocalStorageRepositoryOptions {
24
+ storageKey?: string;
25
+ examples?: readonly AnimationDocument[];
26
+ storage?: Pick<Storage, "getItem" | "setItem" | "removeItem">;
27
+ }
28
+ declare function createLocalStorageRepository(options?: LocalStorageRepositoryOptions): AnimationRepository;
29
+
30
+ type ImageUploadResolver = (file: File) => Promise<string> | string;
31
+
9
32
  interface StudioMountProps {
10
33
  initialId?: string;
34
+ repository?: AnimationRepository;
35
+ editorTitle?: string;
36
+ resolveImage?: ImageUploadResolver;
11
37
  }
12
- declare function StudioMount({ initialId, }: StudioMountProps): React.JSX.Element;
38
+ declare function StudioMount({ initialId, repository, editorTitle, resolveImage, }: StudioMountProps): React.JSX.Element;
39
+
40
+ /** 검증된 애니메이션 문서를 읽기 쉬운 JSON으로 변환한다. */
41
+ declare function animationDocumentToJson(def: AnimationDocument): string;
42
+ /** 어떤 image 요소에서도 참조하지 않는 asset을 제거한 새 문서를 반환한다. */
43
+ declare function garbageCollectAnimationAssets(def: AnimationDocument): AnimationDocument;
44
+ /** 문서 ID를 운영체제에서 안전하게 사용할 수 있는 파일 이름으로 바꾼다. */
45
+ declare function animationDocumentFileName(def: AnimationDocument): string;
46
+ /** 현재 문서를 브라우저에서 JSON 파일로 내려받는다. */
47
+ declare function downloadAnimationJson(def: AnimationDocument): void;
13
48
 
14
49
  type Selection = {
15
50
  kind: "none";
@@ -127,6 +162,7 @@ declare function configureApi(options: {
127
162
  }): void;
128
163
  /** The base currently in use, for hosts that need to build matching links. */
129
164
  declare function apiBaseUrl(): string;
165
+ declare function configureAnimationRepository(next: AnimationRepository): void;
130
166
 
131
167
  interface HostOptions {
132
168
  /** Image used when inserting a placeholder image element. */
@@ -135,4 +171,4 @@ interface HostOptions {
135
171
  declare function configureHost(options: HostOptions): void;
136
172
  declare function placeholderImageUrl(): string;
137
173
 
138
- export { type HistoryEntry, type HistoryKind, type HostOptions, type Selection, Studio, StudioMount, type StudioMountProps, type StudioProps, addAppearance, addChapter, addEffect, addElement, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureApi, configureHost, deleteChapter, deleteEffect, deleteElement, endTransient, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, markClean, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateMeta, updateSettings };
174
+ export { type AnimationRepository, type AnimationSummary, type HistoryEntry, type HistoryKind, type HostOptions, type ImageUploadResolver, type LocalStorageRepositoryOptions, type Selection, Studio, StudioMount, type StudioMountProps, type StudioProps, addAppearance, addChapter, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, createLocalStorageRepository, deleteChapter, deleteEffect, deleteElement, downloadAnimationJson, endTransient, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, markClean, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateMeta, updateSettings };
package/dist/index.js CHANGED
@@ -1,7 +1,9 @@
1
- export { addAppearance, addChapter, addEffect, addElement, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureApi, configureHost, deleteChapter, deleteEffect, deleteElement, endTransient, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, markClean, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateMeta, updateSettings } from './chunk-UE32Q6U5.js';
1
+ import { downloadAnimationJson, configureAnimationRepository } from './chunk-Z2K5HFHI.js';
2
+ export { addAppearance, addChapter, addEffect, addElement, animationDocumentFileName, animationDocumentToJson, apiBaseUrl, beginTransient, canRedo, canUndo, childIdsOf, configureAnimationRepository, configureApi, configureHost, deleteChapter, deleteEffect, deleteElement, downloadAnimationJson, endTransient, garbageCollectAnimationAssets, getCurrentSnapshot, getCurrentTime, getDef, getHistory, getSelectedElementIds, getSelection, groupElements, isDirty, isDraft, isElementSelected, isGroup, jumpBack, jumpForward, markClean, moveElementToEnd, moveElementToFront, placeholderImageUrl, promoteDraftToSaved, redo, registerDataUriAsset, registerExternalAsset, registerInlineAsset, removeAppearance, removeTrack, removeTrackKeyframe, reorderElement, resetHistory, setCurrentTime, setDef, setDraft, setElementValueAtTime, setSelection, setTrackKeyframe, subscribe, toggleSelectionFor, undo, ungroupElement, uniqueChapterId, uniqueEffectId, uniqueElementId, updateAppearance, updateCanvas, updateChapter, updateDuration, updateEffect, updateElementBase, updateMeta, updateSettings } from './chunk-Z2K5HFHI.js';
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;
@@ -98,6 +100,15 @@ function Studio({ initial, onSave }) {
98
100
  ] }),
99
101
  /* @__PURE__ */ jsxs("div", { className: "studio-preview", children: [
100
102
  /* @__PURE__ */ jsxs("div", { className: "studio-preview-bar", children: [
103
+ /* @__PURE__ */ jsx(
104
+ "button",
105
+ {
106
+ type: "button",
107
+ className: "studio-btn",
108
+ onClick: () => downloadAnimationJson(def),
109
+ children: "JSON \uB0B4\uBCF4\uB0B4\uAE30"
110
+ }
111
+ ),
101
112
  /* @__PURE__ */ jsx(
102
113
  "button",
103
114
  {
@@ -224,7 +235,7 @@ var PRESET_HTML = CANVAS_PRESETS.map(
224
235
  var SKELETON = `
225
236
  <div id="studio-app" class="studio-app">
226
237
  <header class="studio-header">
227
- <h1 class="studio-header-title">\u{1F3AC} \uC560\uB2C8\uBA54\uC774\uC158 \uC2A4\uD29C\uB514\uC624</h1>
238
+ <h1 id="studio-editor-title" class="studio-header-title">Clotho Editor</h1>
228
239
  <div class="studio-header-actions">
229
240
  <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
241
  <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 +246,10 @@ var SKELETON = `
235
246
  <span id="studio-id-display" class="studio-id-display"></span>
236
247
  <span id="studio-status" class="studio-status" aria-live="polite">\uB300\uAE30 \uC911</span>
237
248
  <button type="button" id="studio-save" class="studio-btn studio-btn-primary" aria-label="\uC800\uC7A5" disabled>\u{1F4BE} \uC800\uC7A5 (\u2318S)</button>
249
+ <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>
250
+ <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>
251
+ <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>
252
+ <input type="file" id="studio-import-file" accept="application/json,.json" hidden />
238
253
  <button type="button" id="studio-delete" class="studio-btn studio-btn-danger" aria-label="\uC0AD\uC81C" disabled>\u{1F5D1} \uC0AD\uC81C</button>
239
254
  </div>
240
255
  </header>
@@ -242,15 +257,16 @@ var SKELETON = `
242
257
  <div class="studio-body">
243
258
  <aside class="studio-tools" aria-label="\uC694\uC18C \uCD94\uAC00">
244
259
  <div class="studio-tools-section">
245
- <div class="studio-tools-title">\uC694\uC18C \uCD94\uAC00</div>
246
- <button type="button" class="studio-tool-btn" data-add-element="rect" title="\uC0AC\uAC01\uD615" aria-label="\uC0AC\uAC01\uD615 \uCD94\uAC00">\u25A1 Rect</button>
247
- <button type="button" class="studio-tool-btn" data-add-element="circle" title="\uC6D0" aria-label="\uC6D0 \uCD94\uAC00">\u25CB Circle</button>
248
- <button type="button" class="studio-tool-btn" data-add-element="line" title="\uC120" aria-label="\uC120 \uCD94\uAC00">\uFF0F Line</button>
249
- <button type="button" class="studio-tool-btn" data-add-element="arrow" title="\uD654\uC0B4\uD45C" aria-label="\uD654\uC0B4\uD45C \uCD94\uAC00">\u2197 Arrow</button>
250
- <button type="button" class="studio-tool-btn" data-add-element="text" title="\uD14D\uC2A4\uD2B8" aria-label="\uD14D\uC2A4\uD2B8 \uCD94\uAC00">T Text</button>
251
- <button type="button" class="studio-tool-btn" data-add-element="image" title="\uC774\uBBF8\uC9C0" aria-label="\uC774\uBBF8\uC9C0 \uCD94\uAC00">\u{1F5BC} Image</button>
252
- <button type="button" class="studio-tool-btn" data-add-element="path" title="SVG path" aria-label="SVG path \uCD94\uAC00">\u270E Path</button>
253
- <button type="button" class="studio-tool-btn" data-add-element="polygon" title="\uB2E4\uAC01\uD615" aria-label="\uB2E4\uAC01\uD615 \uCD94\uAC00">\u2B22 Polygon</button>
260
+ <div class="studio-tools-title">\uB3C4\uAD6C</div>
261
+ <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>
262
+ <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>
263
+ <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>
264
+ <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>
265
+ <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>
266
+ <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>
267
+ <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>
268
+ <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>
269
+ <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
270
  <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
271
  </div>
256
272
  <div class="studio-tools-section">
@@ -297,7 +313,7 @@ var SKELETON = `
297
313
  </aside>
298
314
  </div>
299
315
 
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>
316
+ <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
317
  <footer class="studio-timeline-wrap">
302
318
  <div class="studio-timeline-header">
303
319
  <button type="button" id="studio-play" class="studio-btn">\u25B6 Play</button>
@@ -306,6 +322,8 @@ var SKELETON = `
306
322
  <input type="range" id="studio-speed" min="0.25" max="3" step="0.25" value="1" />
307
323
  <span id="studio-speed-value">1.00x</span>
308
324
  </label>
325
+ <label class="studio-timeline-option"><input type="checkbox" id="studio-preview-loop" /> \uBB34\uD55C \uC7AC\uC0DD</label>
326
+ <button type="button" id="studio-detach-timeline" class="studio-btn" aria-pressed="false">\u25A3 \uD0C0\uC784\uB77C\uC778 \uBD84\uB9AC</button>
309
327
  <span class="studio-timeline-spacer"></span>
310
328
  <button type="button" id="studio-add-step" class="studio-btn">\uFF0B Chapter \uCD94\uAC00</button>
311
329
  </div>
@@ -349,6 +367,10 @@ var SKELETON = `
349
367
  <tr><th>\u2318 S</th><td>\uC800\uC7A5</td></tr>
350
368
  <tr><th>\u2318 Z / \u2318 \u21E7 Z</th><td>Undo / Redo</td></tr>
351
369
  <tr><th>\u2318 C / V / X</th><td>\uC694\uC18C \uBCF5\uC0AC / \uBD99\uC5EC\uB123\uAE30 / \uC798\uB77C\uB0B4\uAE30</td></tr>
370
+ <tr><th>V / R / O</th><td>\uC120\uD0DD / \uC0AC\uAC01\uD615 / \uC6D0 \uB3C4\uAD6C</td></tr>
371
+ <tr><th>L / A</th><td>\uC120 / \uD654\uC0B4\uD45C \uB3C4\uAD6C</td></tr>
372
+ <tr><th>T / I / B / Y</th><td>\uD14D\uC2A4\uD2B8 / \uC774\uBBF8\uC9C0 / Path / \uB2E4\uAC01\uD615 \uB3C4\uAD6C</td></tr>
373
+ <tr><th>\u2318\xB7Ctrl\xB7Shift + \uD074\uB9AD</th><td>\uC5EC\uB7EC \uC694\uC18C \uC120\uD0DD \uB610\uB294 \uC120\uD0DD \uD574\uC81C</td></tr>
352
374
  <tr><th>Delete / Backspace</th><td>\uC120\uD0DD\uB41C \uC694\uC18C \uB610\uB294 step \uC0AD\uC81C</td></tr>
353
375
  <tr><th>?</th><td>\uC774 \uD654\uBA74 \uC5F4\uAE30</td></tr>
354
376
  <tr><th>Esc</th><td>\uC120\uD0DD \uD574\uC81C / \uB2E4\uC774\uC5BC\uB85C\uADF8 \uB2EB\uAE30</td></tr>
@@ -400,26 +422,115 @@ var SKELETON = `
400
422
  </div>
401
423
  `;
402
424
  function StudioMount({
403
- initialId
425
+ initialId,
426
+ repository,
427
+ editorTitle = "Clotho Editor",
428
+ resolveImage
404
429
  }) {
405
430
  const inited = useRef(false);
406
431
  useEffect(() => {
407
432
  if (inited.current) return;
408
433
  inited.current = true;
434
+ if (repository) configureAnimationRepository(repository);
409
435
  let disposed = false;
410
- void import('./main-SWHHYTLM.js').then(({ initStudio }) => {
436
+ void import('./main-5YXLTYPQ.js').then(({ initStudio }) => {
411
437
  if (disposed) return;
412
- initStudio(initialId ? { initialId } : void 0);
438
+ initStudio({ initialId, editorTitle, resolveImage });
413
439
  });
414
440
  return () => {
415
441
  disposed = true;
416
442
  document.body.classList.remove("editor-active");
417
443
  document.documentElement.classList.remove("editor-active");
418
444
  };
419
- }, [initialId]);
445
+ }, [editorTitle, initialId, repository, resolveImage]);
420
446
  return /* @__PURE__ */ jsx("section", { className: "studio-shell w-full", "data-pagefind-ignore": "all", children: /* @__PURE__ */ jsx("div", { dangerouslySetInnerHTML: { __html: SKELETON } }) });
421
447
  }
448
+ function browserStorage() {
449
+ if (typeof localStorage === "undefined")
450
+ throw new Error("\uC774 \uC800\uC7A5\uC18C\uB294 \uBE0C\uB77C\uC6B0\uC800\uC5D0\uC11C\uB9CC \uC0AC\uC6A9\uD560 \uC218 \uC788\uC2B5\uB2C8\uB2E4.");
451
+ return localStorage;
452
+ }
453
+ function createLocalStorageRepository(options = {}) {
454
+ const prefix = options.storageKey ?? "clotho-editor.animations";
455
+ const examples = new Map(
456
+ (options.examples ?? []).map((value) => {
457
+ const def = animationDocumentSchema.parse(value);
458
+ return [def.id, def];
459
+ })
460
+ );
461
+ const storage = () => options.storage ?? browserStorage();
462
+ const indexKey = `${prefix}:index`;
463
+ const documentKey = (id) => `${prefix}:document:${id}`;
464
+ const readIds = () => {
465
+ const value = storage().getItem(indexKey);
466
+ if (!value) return [];
467
+ try {
468
+ const parsed = JSON.parse(value);
469
+ return Array.isArray(parsed) ? parsed.filter((id) => typeof id === "string") : [];
470
+ } catch {
471
+ return [];
472
+ }
473
+ };
474
+ const write = (def) => {
475
+ const parsed = animationDocumentSchema.parse(def);
476
+ const ids = readIds();
477
+ if (!ids.includes(parsed.id))
478
+ storage().setItem(indexKey, JSON.stringify([...ids, parsed.id]));
479
+ storage().setItem(documentKey(parsed.id), JSON.stringify(parsed));
480
+ return parsed;
481
+ };
482
+ const read = (id) => {
483
+ const value = storage().getItem(documentKey(id));
484
+ return value ? animationDocumentSchema.parse(JSON.parse(value)) : null;
485
+ };
486
+ return {
487
+ async list() {
488
+ const saved = readIds().map((id) => read(id)).filter((def) => def !== null);
489
+ const savedIds = new Set(saved.map((def) => def.id));
490
+ return [
491
+ ...saved.map((def) => ({
492
+ id: def.id,
493
+ title: def.title || def.id,
494
+ description: def.description,
495
+ source: "saved"
496
+ })),
497
+ ...[...examples.values()].filter((def) => !savedIds.has(def.id)).map((def) => ({
498
+ id: def.id,
499
+ title: def.title || def.id,
500
+ description: def.description,
501
+ source: "example"
502
+ }))
503
+ ];
504
+ },
505
+ async load(id) {
506
+ const def = read(id) ?? examples.get(id);
507
+ if (!def) throw new Error(`'${id}' \uC560\uB2C8\uBA54\uC774\uC158\uC744 \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.`);
508
+ return structuredClone(def);
509
+ },
510
+ async create(id, title) {
511
+ if (read(id)) throw new Error(`'${id}' \uC560\uB2C8\uBA54\uC774\uC158\uC774 \uC774\uBBF8 \uC788\uC2B5\uB2C8\uB2E4.`);
512
+ return write(
513
+ animationDocumentSchema.parse({
514
+ clothoVersion: 1,
515
+ id,
516
+ title,
517
+ description: ""
518
+ })
519
+ );
520
+ },
521
+ async save(def) {
522
+ return write(def);
523
+ },
524
+ async delete(id) {
525
+ storage().removeItem(documentKey(id));
526
+ storage().setItem(
527
+ indexKey,
528
+ JSON.stringify(readIds().filter((value) => value !== id))
529
+ );
530
+ }
531
+ };
532
+ }
422
533
 
423
- export { Studio, StudioMount };
534
+ export { Studio, StudioMount, createLocalStorageRepository };
424
535
  //# sourceMappingURL=index.js.map
425
536
  //# sourceMappingURL=index.js.map