create-imagine 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -0
  3. package/bin/create-imagine.js +407 -0
  4. package/package.json +19 -0
  5. package/templates/blank/README.md +96 -0
  6. package/templates/blank/gitignore +6 -0
  7. package/templates/blank/index.html +13 -0
  8. package/templates/blank/package.json +42 -0
  9. package/templates/blank/projects/example/figures/hello-world.tsx +45 -0
  10. package/templates/blank/projects/example/manifest.ts +44 -0
  11. package/templates/blank/projects/example/project.ts +16 -0
  12. package/templates/blank/projects/example/props.json +4 -0
  13. package/templates/blank/scripts/list.ts +46 -0
  14. package/templates/blank/scripts/projects.ts +43 -0
  15. package/templates/blank/scripts/render.ts +288 -0
  16. package/templates/blank/scripts/server.ts +117 -0
  17. package/templates/blank/src/core/__tests__/controls.test.ts +14 -0
  18. package/templates/blank/src/core/__tests__/manifest.test.ts +45 -0
  19. package/templates/blank/src/core/__tests__/props.test.ts +9 -0
  20. package/templates/blank/src/core/controls.ts +14 -0
  21. package/templates/blank/src/core/manifest.ts +134 -0
  22. package/templates/blank/src/core/props.ts +7 -0
  23. package/templates/blank/src/framework/Figure.tsx +34 -0
  24. package/templates/blank/src/framework/__tests__/sizing.test.ts +29 -0
  25. package/templates/blank/src/framework/charts/Axes.tsx +103 -0
  26. package/templates/blank/src/framework/charts/GridLines.tsx +59 -0
  27. package/templates/blank/src/framework/charts/Legend.tsx +31 -0
  28. package/templates/blank/src/framework/charts/Series.tsx +50 -0
  29. package/templates/blank/src/framework/charts/scales.ts +19 -0
  30. package/templates/blank/src/framework/diagrams/primitives.tsx +134 -0
  31. package/templates/blank/src/framework/layout/PanelGrid.tsx +60 -0
  32. package/templates/blank/src/framework/math/MathSvg.tsx +35 -0
  33. package/templates/blank/src/framework/math/mathjax.ts +64 -0
  34. package/templates/blank/src/framework/sizing.ts +28 -0
  35. package/templates/blank/src/framework/theme.ts +35 -0
  36. package/templates/blank/src/framework/types.ts +42 -0
  37. package/templates/blank/src/main.tsx +11 -0
  38. package/templates/blank/src/studio/StudioApp.tsx +130 -0
  39. package/templates/blank/src/studio/StudioRoot.tsx +14 -0
  40. package/templates/blank/src/studio/base64url.ts +8 -0
  41. package/templates/blank/src/studio/figureLoader.ts +30 -0
  42. package/templates/blank/src/studio/projectLoader.ts +40 -0
  43. package/templates/blank/src/studio/propsApi.ts +26 -0
  44. package/templates/blank/src/studio/routes/FigureView.tsx +365 -0
  45. package/templates/blank/src/studio/routes/ProjectHome.tsx +107 -0
  46. package/templates/blank/src/studio/routes/ProjectsHome.tsx +63 -0
  47. package/templates/blank/src/studio/routes/RenderView.tsx +123 -0
  48. package/templates/blank/src/studio/studio.css +540 -0
  49. package/templates/blank/src/studio/useProjectProps.ts +129 -0
  50. package/templates/blank/src/vite-env.d.ts +2 -0
  51. package/templates/blank/tsconfig.json +20 -0
  52. package/templates/blank/vite.config.ts +82 -0
  53. package/templates/blank/vitest.config.ts +8 -0
  54. package/templates/example/README.md +96 -0
  55. package/templates/example/gitignore +6 -0
  56. package/templates/example/index.html +13 -0
  57. package/templates/example/package.json +42 -0
  58. package/templates/example/projects/example/figures/ai-agent-architecture.tsx +133 -0
  59. package/templates/example/projects/example/figures/equation.tsx +29 -0
  60. package/templates/example/projects/example/figures/hello-world.tsx +45 -0
  61. package/templates/example/projects/example/figures/line-chart.tsx +80 -0
  62. package/templates/example/projects/example/figures/multi-panel.tsx +51 -0
  63. package/templates/example/projects/example/figures/pipeline-diagram.tsx +51 -0
  64. package/templates/example/projects/example/manifest.ts +161 -0
  65. package/templates/example/projects/example/project.ts +31 -0
  66. package/templates/example/projects/example/props.json +10 -0
  67. package/templates/example/public/projects/example/previews/ai-agent-architecture--default.png +0 -0
  68. package/templates/example/public/projects/example/previews/equation--default.png +0 -0
  69. package/templates/example/public/projects/example/previews/hello-world--default.png +0 -0
  70. package/templates/example/public/projects/example/previews/line-chart--default.png +0 -0
  71. package/templates/example/public/projects/example/previews/multi-panel--default.png +0 -0
  72. package/templates/example/public/projects/example/previews/pipeline-diagram--default.png +0 -0
  73. package/templates/example/scripts/list.ts +46 -0
  74. package/templates/example/scripts/projects.ts +43 -0
  75. package/templates/example/scripts/render.ts +288 -0
  76. package/templates/example/scripts/server.ts +117 -0
  77. package/templates/example/src/core/__tests__/controls.test.ts +14 -0
  78. package/templates/example/src/core/__tests__/manifest.test.ts +45 -0
  79. package/templates/example/src/core/__tests__/props.test.ts +9 -0
  80. package/templates/example/src/core/controls.ts +14 -0
  81. package/templates/example/src/core/manifest.ts +134 -0
  82. package/templates/example/src/core/props.ts +7 -0
  83. package/templates/example/src/framework/Figure.tsx +34 -0
  84. package/templates/example/src/framework/__tests__/sizing.test.ts +29 -0
  85. package/templates/example/src/framework/charts/Axes.tsx +103 -0
  86. package/templates/example/src/framework/charts/GridLines.tsx +59 -0
  87. package/templates/example/src/framework/charts/Legend.tsx +31 -0
  88. package/templates/example/src/framework/charts/Series.tsx +50 -0
  89. package/templates/example/src/framework/charts/scales.ts +19 -0
  90. package/templates/example/src/framework/diagrams/primitives.tsx +134 -0
  91. package/templates/example/src/framework/layout/PanelGrid.tsx +60 -0
  92. package/templates/example/src/framework/math/MathSvg.tsx +35 -0
  93. package/templates/example/src/framework/math/mathjax.ts +64 -0
  94. package/templates/example/src/framework/sizing.ts +28 -0
  95. package/templates/example/src/framework/theme.ts +35 -0
  96. package/templates/example/src/framework/types.ts +42 -0
  97. package/templates/example/src/main.tsx +11 -0
  98. package/templates/example/src/studio/StudioApp.tsx +130 -0
  99. package/templates/example/src/studio/StudioRoot.tsx +14 -0
  100. package/templates/example/src/studio/base64url.ts +8 -0
  101. package/templates/example/src/studio/figureLoader.ts +30 -0
  102. package/templates/example/src/studio/projectLoader.ts +40 -0
  103. package/templates/example/src/studio/propsApi.ts +26 -0
  104. package/templates/example/src/studio/routes/FigureView.tsx +365 -0
  105. package/templates/example/src/studio/routes/ProjectHome.tsx +107 -0
  106. package/templates/example/src/studio/routes/ProjectsHome.tsx +63 -0
  107. package/templates/example/src/studio/routes/RenderView.tsx +123 -0
  108. package/templates/example/src/studio/studio.css +540 -0
  109. package/templates/example/src/studio/useProjectProps.ts +129 -0
  110. package/templates/example/src/vite-env.d.ts +2 -0
  111. package/templates/example/tsconfig.json +20 -0
  112. package/templates/example/vite.config.ts +82 -0
  113. package/templates/example/vitest.config.ts +8 -0
@@ -0,0 +1,540 @@
1
+ :root {
2
+ color-scheme: light;
3
+ }
4
+
5
+ html,
6
+ body,
7
+ #root {
8
+ height: 100%;
9
+ }
10
+
11
+ body {
12
+ margin: 0;
13
+ font-family:
14
+ ui-sans-serif,
15
+ system-ui,
16
+ -apple-system,
17
+ BlinkMacSystemFont,
18
+ 'Segoe UI',
19
+ Roboto,
20
+ Arial,
21
+ 'Noto Sans',
22
+ 'Liberation Sans',
23
+ sans-serif;
24
+ color: #111827;
25
+ background: #f3f4f6;
26
+ }
27
+
28
+ * {
29
+ box-sizing: border-box;
30
+ }
31
+
32
+ a {
33
+ color: inherit;
34
+ }
35
+
36
+ .mono {
37
+ font-family:
38
+ ui-monospace,
39
+ SFMono-Regular,
40
+ Menlo,
41
+ Monaco,
42
+ Consolas,
43
+ 'Liberation Mono',
44
+ 'Courier New',
45
+ monospace;
46
+ }
47
+
48
+ .studio {
49
+ display: flex;
50
+ height: 100vh;
51
+ }
52
+
53
+ .sidebar {
54
+ width: 300px;
55
+ background: #ffffff;
56
+ border-right: 1px solid #e5e7eb;
57
+ display: flex;
58
+ flex-direction: column;
59
+ }
60
+
61
+ .sidebarHeader {
62
+ padding: 14px 14px 10px;
63
+ border-bottom: 1px solid #e5e7eb;
64
+ }
65
+
66
+ .sidebarSection {
67
+ padding: 12px 10px;
68
+ border-bottom: 1px solid #f3f4f6;
69
+ }
70
+
71
+ .sidebarSectionTitle {
72
+ padding: 0 6px 8px;
73
+ font-size: 12px;
74
+ color: #6b7280;
75
+ font-weight: 700;
76
+ text-transform: uppercase;
77
+ letter-spacing: 0.06em;
78
+ }
79
+
80
+ .sidebarHint {
81
+ padding: 8px 10px;
82
+ font-size: 12px;
83
+ color: #6b7280;
84
+ }
85
+
86
+ .sidebarError {
87
+ padding: 8px 10px;
88
+ font-size: 12px;
89
+ color: #991b1b;
90
+ }
91
+
92
+ .sidebarTitleRow {
93
+ display: flex;
94
+ align-items: center;
95
+ justify-content: space-between;
96
+ gap: 10px;
97
+ margin-bottom: 10px;
98
+ }
99
+
100
+ .sidebarTitle {
101
+ font-weight: 800;
102
+ letter-spacing: -0.02em;
103
+ }
104
+
105
+ .search {
106
+ width: 100%;
107
+ padding: 10px 10px;
108
+ border-radius: 10px;
109
+ border: 1px solid #e5e7eb;
110
+ outline: none;
111
+ }
112
+
113
+ .figureList {
114
+ padding: 6px;
115
+ overflow: auto;
116
+ flex: 1;
117
+ }
118
+
119
+ .projectList {
120
+ padding: 6px;
121
+ }
122
+
123
+ .projectItem {
124
+ display: block;
125
+ padding: 10px 10px;
126
+ border-radius: 10px;
127
+ text-decoration: none;
128
+ border: 1px solid transparent;
129
+ }
130
+
131
+ .projectItem:hover {
132
+ background: #f9fafb;
133
+ }
134
+
135
+ .projectItem.active {
136
+ background: #ecfeff;
137
+ border-color: #a5f3fc;
138
+ }
139
+
140
+ .projectItemTitle {
141
+ font-weight: 800;
142
+ margin-bottom: 4px;
143
+ }
144
+
145
+ .projectItemMeta {
146
+ font-size: 12px;
147
+ color: #4b5563;
148
+ display: flex;
149
+ gap: 8px;
150
+ align-items: center;
151
+ }
152
+
153
+ .figureItem {
154
+ display: block;
155
+ padding: 10px 10px;
156
+ border-radius: 10px;
157
+ text-decoration: none;
158
+ border: 1px solid transparent;
159
+ }
160
+
161
+ .figureItem:hover {
162
+ background: #f9fafb;
163
+ }
164
+
165
+ .figureItem.active {
166
+ background: #eef2ff;
167
+ border-color: #c7d2fe;
168
+ }
169
+
170
+ .figureItemTitle {
171
+ font-weight: 700;
172
+ margin-bottom: 4px;
173
+ }
174
+
175
+ .figureItemMeta {
176
+ font-size: 12px;
177
+ color: #4b5563;
178
+ display: flex;
179
+ gap: 8px;
180
+ align-items: center;
181
+ }
182
+
183
+ .dot {
184
+ opacity: 0.7;
185
+ }
186
+
187
+ .main {
188
+ flex: 1;
189
+ display: flex;
190
+ min-width: 0;
191
+ }
192
+
193
+ .page {
194
+ flex: 1;
195
+ display: flex;
196
+ flex-direction: column;
197
+ min-width: 0;
198
+ }
199
+
200
+ .pageHeader {
201
+ padding: 18px 18px;
202
+ border-bottom: 1px solid #e5e7eb;
203
+ background: #ffffff;
204
+ display: flex;
205
+ align-items: flex-start;
206
+ justify-content: space-between;
207
+ gap: 18px;
208
+ }
209
+
210
+ .pageTitle {
211
+ font-size: 16px;
212
+ font-weight: 800;
213
+ }
214
+
215
+ .pageSubtitle {
216
+ margin-top: 4px;
217
+ font-size: 13px;
218
+ color: #4b5563;
219
+ }
220
+
221
+ .cardGrid {
222
+ padding: 18px;
223
+ display: grid;
224
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
225
+ gap: 12px;
226
+ }
227
+
228
+ .card {
229
+ background: #ffffff;
230
+ border: 1px solid #e5e7eb;
231
+ border-radius: 14px;
232
+ padding: 14px;
233
+ text-decoration: none;
234
+ transition: transform 80ms ease, box-shadow 80ms ease;
235
+ }
236
+
237
+ .projectCard {
238
+ display: flex;
239
+ flex-direction: column;
240
+ gap: 4px;
241
+ }
242
+
243
+ .card:hover {
244
+ transform: translateY(-1px);
245
+ box-shadow: 0 6px 18px rgba(17, 24, 39, 0.08);
246
+ }
247
+
248
+ .cardTitle {
249
+ font-weight: 800;
250
+ margin-bottom: 6px;
251
+ }
252
+
253
+ .cardBody {
254
+ font-size: 13px;
255
+ color: #374151;
256
+ }
257
+
258
+ .cardMeta {
259
+ margin-top: 4px;
260
+ color: #4b5563;
261
+ }
262
+
263
+ .thumbRow {
264
+ display: flex;
265
+ gap: 8px;
266
+ margin-top: 10px;
267
+ }
268
+
269
+ .thumb {
270
+ width: 72px;
271
+ height: 48px;
272
+ object-fit: cover;
273
+ border-radius: 10px;
274
+ border: 1px solid #e5e7eb;
275
+ background: #f9fafb;
276
+ }
277
+
278
+ .section {
279
+ padding: 18px;
280
+ }
281
+
282
+ .sectionTitle {
283
+ font-size: 13px;
284
+ color: #374151;
285
+ font-weight: 800;
286
+ margin: 6px 0 12px;
287
+ }
288
+
289
+ .galleryGrid {
290
+ display: grid;
291
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
292
+ gap: 12px;
293
+ }
294
+
295
+ .galleryItem {
296
+ background: #ffffff;
297
+ border: 1px solid #e5e7eb;
298
+ border-radius: 14px;
299
+ overflow: hidden;
300
+ text-decoration: none;
301
+ transition: transform 80ms ease, box-shadow 80ms ease;
302
+ }
303
+
304
+ .galleryItem:hover {
305
+ transform: translateY(-1px);
306
+ box-shadow: 0 6px 18px rgba(17, 24, 39, 0.08);
307
+ }
308
+
309
+ .galleryImg {
310
+ width: 100%;
311
+ height: 160px;
312
+ object-fit: cover;
313
+ display: block;
314
+ background: #f9fafb;
315
+ }
316
+
317
+ .galleryCaption {
318
+ padding: 10px 12px;
319
+ font-size: 13px;
320
+ color: #374151;
321
+ font-weight: 700;
322
+ }
323
+
324
+ .figureHeader {
325
+ align-items: center;
326
+ }
327
+
328
+ .figureBody {
329
+ display: flex;
330
+ flex: 1;
331
+ min-height: 0;
332
+ }
333
+
334
+ .controlsPanel {
335
+ width: 360px;
336
+ background: #ffffff;
337
+ border-left: 1px solid #e5e7eb;
338
+ padding: 14px;
339
+ overflow: auto;
340
+ }
341
+
342
+ .controlsHeader {
343
+ display: flex;
344
+ align-items: center;
345
+ justify-content: space-between;
346
+ gap: 10px;
347
+ margin-bottom: 10px;
348
+ }
349
+
350
+ .controlsTitle {
351
+ font-weight: 900;
352
+ }
353
+
354
+ .controlsActions {
355
+ display: inline-flex;
356
+ gap: 8px;
357
+ }
358
+
359
+ .controlsStatus {
360
+ font-size: 12px;
361
+ color: #6b7280;
362
+ margin-bottom: 12px;
363
+ }
364
+
365
+ .statusOk {
366
+ color: #065f46;
367
+ font-weight: 700;
368
+ }
369
+
370
+ .statusErr {
371
+ color: #991b1b;
372
+ font-weight: 700;
373
+ margin-top: 6px;
374
+ }
375
+
376
+ .statusMuted {
377
+ color: #6b7280;
378
+ }
379
+
380
+ .controlsGrid {
381
+ display: flex;
382
+ flex-direction: column;
383
+ gap: 10px;
384
+ }
385
+
386
+ .control {
387
+ display: flex;
388
+ flex-direction: column;
389
+ gap: 6px;
390
+ }
391
+
392
+ .controlLabel {
393
+ font-size: 12px;
394
+ color: #4b5563;
395
+ font-weight: 700;
396
+ }
397
+
398
+ .controlCheckbox {
399
+ flex-direction: row;
400
+ align-items: center;
401
+ gap: 10px;
402
+ }
403
+
404
+ .textarea {
405
+ padding: 10px 10px;
406
+ border-radius: 10px;
407
+ border: 1px solid #e5e7eb;
408
+ background: #ffffff;
409
+ font-family:
410
+ ui-monospace,
411
+ SFMono-Regular,
412
+ Menlo,
413
+ Monaco,
414
+ Consolas,
415
+ 'Liberation Mono',
416
+ 'Courier New',
417
+ monospace;
418
+ font-size: 12px;
419
+ }
420
+
421
+ .controlsEmpty {
422
+ font-size: 13px;
423
+ color: #6b7280;
424
+ padding: 12px 0;
425
+ }
426
+
427
+ .figureHeaderLeft {
428
+ min-width: 0;
429
+ }
430
+
431
+ .figureHeaderRight {
432
+ display: flex;
433
+ flex-wrap: wrap;
434
+ align-items: flex-end;
435
+ gap: 10px;
436
+ }
437
+
438
+ .label {
439
+ display: flex;
440
+ flex-direction: column;
441
+ gap: 4px;
442
+ font-size: 12px;
443
+ color: #4b5563;
444
+ }
445
+
446
+ .input,
447
+ .select {
448
+ padding: 8px 10px;
449
+ border-radius: 10px;
450
+ border: 1px solid #e5e7eb;
451
+ background: #ffffff;
452
+ min-width: 110px;
453
+ }
454
+
455
+ .checkbox {
456
+ font-size: 12px;
457
+ color: #4b5563;
458
+ display: inline-flex;
459
+ align-items: center;
460
+ gap: 6px;
461
+ user-select: none;
462
+ }
463
+
464
+ .toolbarGroup {
465
+ display: inline-flex;
466
+ gap: 8px;
467
+ }
468
+
469
+ .btn {
470
+ display: inline-flex;
471
+ align-items: center;
472
+ justify-content: center;
473
+ padding: 9px 12px;
474
+ border-radius: 10px;
475
+ border: 1px solid #e5e7eb;
476
+ background: #ffffff;
477
+ font-size: 13px;
478
+ cursor: pointer;
479
+ text-decoration: none;
480
+ }
481
+
482
+ .btn:hover {
483
+ background: #f9fafb;
484
+ }
485
+
486
+ .btnSmall {
487
+ padding: 8px 10px;
488
+ font-size: 12px;
489
+ }
490
+
491
+ .previewSurface {
492
+ flex: 1;
493
+ overflow: auto;
494
+ padding: 18px;
495
+ min-width: 0;
496
+ }
497
+
498
+ .previewSurface.checker {
499
+ background-image: linear-gradient(45deg, #e5e7eb 25%, transparent 25%),
500
+ linear-gradient(-45deg, #e5e7eb 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e5e7eb 75%),
501
+ linear-gradient(-45deg, transparent 75%, #e5e7eb 75%);
502
+ background-size: 26px 26px;
503
+ background-position:
504
+ 0 0,
505
+ 0 13px,
506
+ 13px -13px,
507
+ -13px 0px;
508
+ }
509
+
510
+ .previewScale {
511
+ transform-origin: top left;
512
+ display: inline-block;
513
+ }
514
+
515
+ #figure-root {
516
+ box-shadow: 0 10px 34px rgba(17, 24, 39, 0.16);
517
+ }
518
+
519
+ .loading {
520
+ padding: 14px;
521
+ color: #4b5563;
522
+ }
523
+
524
+ .empty {
525
+ margin: 40px;
526
+ padding: 24px;
527
+ border-radius: 14px;
528
+ border: 1px solid #e5e7eb;
529
+ background: #ffffff;
530
+ }
531
+
532
+ .emptyTitle {
533
+ font-weight: 800;
534
+ margin-bottom: 6px;
535
+ }
536
+
537
+ .emptyBody {
538
+ color: #4b5563;
539
+ font-size: 13px;
540
+ }
@@ -0,0 +1,129 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { PropsFileV1 } from '../core/manifest';
3
+ import { emptyPropsFile, fetchPropsFile, savePropsFile } from './propsApi';
4
+
5
+ type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
6
+
7
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
8
+ return Boolean(value) && typeof value === 'object' && Object.getPrototypeOf(value) === Object.prototype;
9
+ }
10
+
11
+ export function useProjectProps(projectId: string) {
12
+ const [file, setFile] = useState<PropsFileV1>(() => emptyPropsFile());
13
+ const [readOnly, setReadOnly] = useState(false);
14
+ const [loadError, setLoadError] = useState<string | null>(null);
15
+ const [saveStatus, setSaveStatus] = useState<SaveStatus>('idle');
16
+ const [saveError, setSaveError] = useState<string | null>(null);
17
+ const saveTimer = useRef<number | null>(null);
18
+ const latestFile = useRef<PropsFileV1>(file);
19
+ latestFile.current = file;
20
+
21
+ useEffect(() => {
22
+ if (!projectId) return;
23
+ setLoadError(null);
24
+ setSaveStatus('idle');
25
+ setSaveError(null);
26
+ setReadOnly(false);
27
+
28
+ fetchPropsFile(projectId).then(
29
+ (f) => {
30
+ setFile(f);
31
+ setReadOnly(false);
32
+ },
33
+ (err) => {
34
+ setFile(emptyPropsFile());
35
+ setReadOnly(true);
36
+ setLoadError(String(err?.message ?? err));
37
+ }
38
+ );
39
+
40
+ return () => {
41
+ if (saveTimer.current) window.clearTimeout(saveTimer.current);
42
+ saveTimer.current = null;
43
+ };
44
+ }, [projectId]);
45
+
46
+ const queueSave = useCallback(
47
+ (next: PropsFileV1) => {
48
+ if (readOnly) return;
49
+ if (saveTimer.current) window.clearTimeout(saveTimer.current);
50
+
51
+ setSaveStatus('saving');
52
+ setSaveError(null);
53
+ saveTimer.current = window.setTimeout(() => {
54
+ saveTimer.current = null;
55
+ const toSave = next;
56
+ savePropsFile(projectId, toSave).then(
57
+ () => setSaveStatus('saved'),
58
+ (err) => {
59
+ setSaveStatus('error');
60
+ setSaveError(String(err?.message ?? err));
61
+ }
62
+ );
63
+ }, 350);
64
+ },
65
+ [projectId, readOnly]
66
+ );
67
+
68
+ const setVariantOverride = useCallback(
69
+ (figureId: string, variantId: string, key: string, value: unknown) => {
70
+ setFile((prev) => {
71
+ const overrides = { ...prev.overrides };
72
+ const fig = { ...(overrides[figureId] ?? {}) };
73
+ const variant = { ...(fig[variantId] ?? {}) };
74
+
75
+ if (value === undefined) delete variant[key];
76
+ else variant[key] = value;
77
+ fig[variantId] = variant;
78
+ overrides[figureId] = fig;
79
+
80
+ const next = { version: 1 as const, overrides };
81
+ queueSave(next);
82
+ return next;
83
+ });
84
+ },
85
+ [queueSave]
86
+ );
87
+
88
+ const resetVariantOverrides = useCallback(
89
+ (figureId: string, variantId: string) => {
90
+ setFile((prev) => {
91
+ const overrides = { ...prev.overrides };
92
+ const fig = { ...(overrides[figureId] ?? {}) };
93
+ delete fig[variantId];
94
+
95
+ if (!Object.keys(fig).length) delete overrides[figureId];
96
+ else overrides[figureId] = fig;
97
+
98
+ const next = { version: 1 as const, overrides };
99
+ queueSave(next);
100
+ return next;
101
+ });
102
+ },
103
+ [queueSave]
104
+ );
105
+
106
+ const getVariantOverrides = useCallback(
107
+ (figureId: string, variantId: string): Record<string, unknown> => {
108
+ const v = file.overrides[figureId]?.[variantId];
109
+ return isPlainObject(v) ? v : {};
110
+ },
111
+ [file.overrides]
112
+ );
113
+
114
+ const value = useMemo(
115
+ () => ({
116
+ file,
117
+ readOnly,
118
+ loadError,
119
+ saveStatus,
120
+ saveError,
121
+ setVariantOverride,
122
+ resetVariantOverrides,
123
+ getVariantOverrides
124
+ }),
125
+ [file, getVariantOverrides, loadError, readOnly, resetVariantOverrides, saveError, saveStatus, setVariantOverride]
126
+ );
127
+
128
+ return value;
129
+ }
@@ -0,0 +1,2 @@
1
+ /// <reference types="vite/client" />
2
+
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "baseUrl": ".",
8
+ "paths": {
9
+ "@/*": ["src/*"]
10
+ },
11
+ "jsx": "react-jsx",
12
+ "strict": true,
13
+ "skipLibCheck": true,
14
+ "noEmit": true,
15
+ "types": ["node", "vitest/globals"],
16
+ "resolveJsonModule": true,
17
+ "isolatedModules": true
18
+ },
19
+ "include": ["src", "scripts", "projects", "vite.config.ts", "vitest.config.ts"]
20
+ }