create-next-imagicma 0.1.14 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +2 -0
  2. package/package.json +1 -1
  3. package/template-hono/.env.example +6 -14
  4. package/template-hono/.imagicma/AGENTS.md +7 -0
  5. package/template-hono/AGENTS.md +98 -120
  6. package/template-hono/README.md +86 -118
  7. package/template-hono/client/index.html +1 -1
  8. package/template-hono/client/public/imagicma-picker-bridge.js +2 -6
  9. package/template-hono/client/public/imagicma-preview-feedback.js +0 -1
  10. package/template-hono/client/src/components/HelloClient.tsx +1 -1
  11. package/template-hono/client/src/globals.css +417 -1
  12. package/template-hono/client/src/lib/imagicma-preview-bridge.ts +1 -1
  13. package/template-hono/client/src/lib/imagicma-preview-picker.ts +28 -130
  14. package/template-hono/client/src/providers.tsx +1 -1
  15. package/template-hono/gitignore +2 -1
  16. package/template-hono/package.json +9 -9
  17. package/template-hono/pnpm-lock.yaml +646 -1173
  18. package/template-hono/server/app.ts +3 -2
  19. package/template-hono/server/controllers/greeting.controller.ts +22 -0
  20. package/template-hono/server/db/index.ts +129 -0
  21. package/template-hono/server/index.ts +3 -3
  22. package/template-hono/server/middlewares/error-handler.ts +8 -0
  23. package/template-hono/server/models/entities/message.entity.ts +13 -0
  24. package/template-hono/server/models/repositories/message.repository.ts +43 -0
  25. package/template-hono/server/models/services/greeting.service.ts +14 -0
  26. package/template-hono/server/models/types/message.ts +7 -0
  27. package/template-hono/server/routes/greeting.ts +1 -1
  28. package/template-hono/shared/constants/greeting.ts +1 -0
  29. package/template-hono/shared/contracts/routes.ts +14 -0
  30. package/template-hono/shared/routes.ts +1 -68
  31. package/template-hono/shared/schema.ts +5 -8
  32. package/template-hono/shared/types/greeting.ts +3 -0
  33. package/template-hono/tailwind.config.mjs +62 -111
  34. package/template-hono/tsconfig.json +3 -2
  35. package/template-hono/tsconfig.server.json +2 -0
  36. package/template-hono/vite.config.ts +4 -4
  37. package/template-hono/client/src/lib/ai-ui-stream.ts +0 -64
  38. package/template-hono/client/src/theme/default-theme.css +0 -482
  39. package/template-hono/drizzle.config.ts +0 -13
  40. package/template-hono/server/controllers/ai-chat-controller.ts +0 -49
  41. package/template-hono/server/controllers/greeting-controller.ts +0 -25
  42. package/template-hono/server/db/client.ts +0 -25
  43. package/template-hono/server/env.ts +0 -89
  44. package/template-hono/server/lib/ai-provider.ts +0 -74
  45. package/template-hono/server/lib/ai.ts +0 -205
  46. package/template-hono/server/middlewares/auth.ts +0 -69
  47. package/template-hono/server/middlewares/index.ts +0 -12
  48. package/template-hono/server/repositories/message-repository.ts +0 -13
  49. package/template-hono/server/routes/ai-chat.ts +0 -9
  50. package/template-hono/shared/schema/greeting.ts +0 -17
@@ -1,11 +1,427 @@
1
+ @import url("https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700;800&display=swap");
1
2
  @import "tailwindcss";
2
- @import "./theme/default-theme.css";
3
3
  @config "../../tailwind.config.mjs";
4
4
 
5
5
  .backdrop {
6
6
  display: none;
7
7
  }
8
8
 
9
+ :root {
10
+ /* Fonts */
11
+ --font-sans: "Poppins", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
12
+ "Noto Sans SC", "Helvetica Neue", sans-serif;
13
+ --font-display: "Poppins", "PingFang SC", "Hiragino Sans GB",
14
+ "Microsoft YaHei", sans-serif;
15
+ --font-serif: "Poppins", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei",
16
+ "Noto Sans SC", "Helvetica Neue", sans-serif;
17
+ --font-mono: Menlo, ui-monospace, SFMono-Regular, Monaco, Consolas,
18
+ "Liberation Mono", "Courier New", monospace;
19
+
20
+ /* Core theme tokens (HSL triples) */
21
+ --background: 0 0% 100%;
22
+ --foreground: 240 10% 3.9%;
23
+
24
+ --card: 0 0% 100%;
25
+ --card-foreground: 240 10% 3.9%;
26
+
27
+ --popover: 0 0% 100%;
28
+ --popover-foreground: 240 10% 3.9%;
29
+
30
+ --primary: 240 5.9% 10%;
31
+ --primary-foreground: 0 0% 98%;
32
+
33
+ --secondary: 240 4.8% 95.9%;
34
+ --secondary-foreground: 240 5.9% 10%;
35
+
36
+ --muted: 240 4.8% 95.9%;
37
+ --muted-foreground: 240 3.8% 46.1%;
38
+
39
+ --accent: 240 4.8% 95.9%;
40
+ --accent-foreground: 240 5.9% 10%;
41
+
42
+ --destructive: 0 84.2% 60.2%;
43
+ --destructive-foreground: 0 0% 98%;
44
+
45
+ --border: 240 5.9% 90%;
46
+ --input: 240 5.9% 90%;
47
+ --ring: 240 5.9% 10%;
48
+
49
+ --radius: 0.8rem;
50
+
51
+ /* Visual system tokens for showcase pages */
52
+ --surface-1: 224 40% 98%;
53
+ --surface-2: 220 35% 95%;
54
+ --brand-accent: 201 92% 56%;
55
+ --brand-success: 156 72% 40%;
56
+ --elevation-soft: 0 14px 34px rgba(15, 23, 42, 0.12);
57
+ --elevation-strong: 0 22px 56px rgba(15, 23, 42, 0.18);
58
+ --space-xs: 0.625rem;
59
+ --space-sm: 0.875rem;
60
+ --space-md: 1.125rem;
61
+ --space-lg: 1.625rem;
62
+
63
+ /* Extra tokens used by the migrated shadcn components */
64
+ --card-border: var(--border);
65
+ --popover-border: var(--border);
66
+ --sidebar: var(--background);
67
+ --sidebar-foreground: var(--foreground);
68
+ --sidebar-border: var(--border);
69
+ --sidebar-ring: var(--ring);
70
+
71
+ --chart-1: 220 70% 50%;
72
+ --chart-2: 160 60% 45%;
73
+ --chart-3: 30 80% 55%;
74
+ --chart-4: 280 65% 60%;
75
+ --chart-5: 340 75% 55%;
76
+
77
+ /* Border colors expected by template classnames */
78
+ --primary-border: hsl(var(--primary) / 0.18);
79
+ --secondary-border: hsl(var(--border));
80
+ --muted-border: hsl(var(--border));
81
+ --accent-border: hsl(var(--border));
82
+ --destructive-border: hsl(var(--destructive) / 0.25);
83
+
84
+ --button-outline: hsl(var(--border));
85
+ --badge-outline: hsl(var(--border));
86
+ }
87
+
88
+ .dark {
89
+ --background: 240 10% 3.9%;
90
+ --foreground: 0 0% 98%;
91
+
92
+ --card: 240 10% 3.9%;
93
+ --card-foreground: 0 0% 98%;
94
+
95
+ --popover: 240 10% 3.9%;
96
+ --popover-foreground: 0 0% 98%;
97
+
98
+ --primary: 0 0% 98%;
99
+ --primary-foreground: 240 5.9% 10%;
100
+
101
+ --secondary: 240 3.7% 15.9%;
102
+ --secondary-foreground: 0 0% 98%;
103
+
104
+ --muted: 240 3.7% 15.9%;
105
+ --muted-foreground: 240 5% 64.9%;
106
+
107
+ --accent: 240 3.7% 15.9%;
108
+ --accent-foreground: 0 0% 98%;
109
+
110
+ --destructive: 0 62.8% 30.6%;
111
+ --destructive-foreground: 0 0% 98%;
112
+
113
+ --border: 240 3.7% 15.9%;
114
+ --input: 240 3.7% 15.9%;
115
+ --ring: 240 4.9% 83.9%;
116
+
117
+ --card-border: var(--border);
118
+ --popover-border: var(--border);
119
+ --sidebar: var(--background);
120
+ --sidebar-foreground: var(--foreground);
121
+ --sidebar-border: var(--border);
122
+ --sidebar-ring: var(--ring);
123
+
124
+ --primary-border: hsl(var(--primary) / 0.15);
125
+ --secondary-border: hsl(var(--border));
126
+ --muted-border: hsl(var(--border));
127
+ --accent-border: hsl(var(--border));
128
+ --destructive-border: hsl(var(--destructive) / 0.35);
129
+
130
+ --button-outline: hsl(var(--border));
131
+ --badge-outline: hsl(var(--border));
132
+ }
133
+
134
+ /* Theme style presets (used by html[data-theme-style="<preset>"]) */
135
+ :root[data-theme-style="quadratic"] {
136
+ --background: 246 33% 96%;
137
+ --foreground: 233 24% 17%;
138
+
139
+ --card: 0 0% 100%;
140
+ --card-foreground: 233 24% 17%;
141
+ --popover: 0 0% 100%;
142
+ --popover-foreground: 233 24% 17%;
143
+
144
+ --primary: 258 84% 63%;
145
+ --primary-foreground: 0 0% 100%;
146
+ --secondary: 250 44% 92%;
147
+ --secondary-foreground: 233 24% 20%;
148
+ --muted: 246 36% 93%;
149
+ --muted-foreground: 230 12% 42%;
150
+ --accent: 257 62% 88%;
151
+ --accent-foreground: 234 24% 24%;
152
+
153
+ --border: 246 24% 86%;
154
+ --input: 246 24% 84%;
155
+ --ring: 258 84% 63%;
156
+
157
+ --chart-1: 258 78% 56%;
158
+ --chart-2: 201 84% 56%;
159
+ --chart-3: 172 65% 45%;
160
+ --chart-4: 338 74% 58%;
161
+ --chart-5: 39 84% 58%;
162
+ }
163
+
164
+ .dark[data-theme-style="quadratic"] {
165
+ --background: 233 22% 11%;
166
+ --foreground: 246 33% 96%;
167
+
168
+ --card: 232 20% 14%;
169
+ --card-foreground: 246 33% 96%;
170
+ --popover: 232 20% 14%;
171
+ --popover-foreground: 246 33% 96%;
172
+
173
+ --primary: 258 90% 70%;
174
+ --primary-foreground: 233 22% 11%;
175
+ --secondary: 235 16% 20%;
176
+ --secondary-foreground: 246 30% 94%;
177
+ --muted: 234 14% 18%;
178
+ --muted-foreground: 240 15% 76%;
179
+ --accent: 235 16% 20%;
180
+ --accent-foreground: 246 30% 94%;
181
+
182
+ --border: 234 14% 26%;
183
+ --input: 234 14% 26%;
184
+ --ring: 258 90% 70%;
185
+
186
+ --chart-1: 258 90% 70%;
187
+ --chart-2: 201 78% 68%;
188
+ --chart-3: 172 58% 62%;
189
+ --chart-4: 338 76% 72%;
190
+ --chart-5: 39 78% 70%;
191
+ }
192
+
193
+ :root[data-theme-style="nomad"] {
194
+ --background: 0 0% 94.1%;
195
+ --foreground: 0 0% 10.2%;
196
+
197
+ --card: 0 0% 98.8%;
198
+ --card-foreground: 0 0% 0%;
199
+ --popover: 45 25% 96.9%;
200
+ --popover-foreground: 0 0% 10.2%;
201
+
202
+ --primary: 341.9 85.1% 52.5%;
203
+ --primary-foreground: 0 0% 100%;
204
+ --secondary: 0 0% 76.9%;
205
+ --secondary-foreground: 0 0% 10.2%;
206
+ --muted: 0 0% 89%;
207
+ --muted-foreground: 0 0% 12.9%;
208
+ --accent: 0 0% 100%;
209
+ --accent-foreground: 0 0% 10.2%;
210
+
211
+ --border: 0 0% 91%;
212
+ --input: 0 0% 71%;
213
+ --ring: 0 0% 42%;
214
+
215
+ --chart-1: 203.9 88.3% 53.1%;
216
+ --chart-2: 159.8 100% 36.1%;
217
+ --chart-3: 42 92.8% 56.3%;
218
+ --chart-4: 147.1 78.5% 42%;
219
+ --chart-5: 341.5 75.2% 51%;
220
+ }
221
+
222
+ .dark[data-theme-style="nomad"] {
223
+ --background: 0 0% 10%;
224
+ --foreground: 0 0% 96%;
225
+
226
+ --card: 0 0% 13%;
227
+ --card-foreground: 0 0% 98%;
228
+ --popover: 0 0% 8%;
229
+ --popover-foreground: 0 0% 96%;
230
+
231
+ --primary: 341.9 85.1% 60%;
232
+ --primary-foreground: 0 0% 100%;
233
+ --secondary: 0 0% 26%;
234
+ --secondary-foreground: 0 0% 96%;
235
+ --muted: 0 0% 18%;
236
+ --muted-foreground: 0 0% 72%;
237
+ --accent: 0 0% 22%;
238
+ --accent-foreground: 0 0% 96%;
239
+
240
+ --border: 0 0% 24%;
241
+ --input: 0 0% 34%;
242
+ --ring: 0 0% 62%;
243
+
244
+ --chart-1: 203.9 88.3% 62%;
245
+ --chart-2: 159.8 100% 44%;
246
+ --chart-3: 42 92.8% 64%;
247
+ --chart-4: 147.1 78.5% 50%;
248
+ --chart-5: 341.5 75.2% 59%;
249
+ }
250
+
251
+ :root[data-theme-style="honey"] {
252
+ --background: 49 38.1% 91.8%;
253
+ --foreground: 40 52.9% 27.5%;
254
+
255
+ --card: 49 42% 95%;
256
+ --card-foreground: 40 52.9% 27.5%;
257
+ --popover: 49 42% 95%;
258
+ --popover-foreground: 40 52.9% 27.5%;
259
+
260
+ --primary: 40 72.8% 68.2%;
261
+ --primary-foreground: 40 52.9% 18%;
262
+ --secondary: 63 20.8% 79.2%;
263
+ --secondary-foreground: 40 52.9% 27.5%;
264
+ --muted: 49 30% 88%;
265
+ --muted-foreground: 40 24% 38%;
266
+ --accent: 63 20.8% 79.2%;
267
+ --accent-foreground: 40 52.9% 27.5%;
268
+
269
+ --border: 52 22% 74%;
270
+ --input: 52 22% 74%;
271
+ --ring: 40 72.8% 68.2%;
272
+
273
+ --chart-1: 40 72.8% 55%;
274
+ --chart-2: 63 30% 45%;
275
+ --chart-3: 173 25% 42%;
276
+ --chart-4: 18 56% 50%;
277
+ --chart-5: 240 16% 52%;
278
+ }
279
+
280
+ .dark[data-theme-style="honey"] {
281
+ --background: 36 38% 10%;
282
+ --foreground: 49 30% 88%;
283
+
284
+ --card: 34 30% 14%;
285
+ --card-foreground: 49 30% 88%;
286
+ --popover: 34 30% 14%;
287
+ --popover-foreground: 49 30% 88%;
288
+
289
+ --primary: 40 72.8% 68.2%;
290
+ --primary-foreground: 36 38% 10%;
291
+ --secondary: 38 20% 20%;
292
+ --secondary-foreground: 49 30% 88%;
293
+ --muted: 38 18% 18%;
294
+ --muted-foreground: 49 18% 70%;
295
+ --accent: 38 18% 18%;
296
+ --accent-foreground: 49 30% 88%;
297
+
298
+ --border: 38 18% 26%;
299
+ --input: 38 18% 26%;
300
+ --ring: 40 72.8% 68.2%;
301
+
302
+ --chart-1: 40 72.8% 68.2%;
303
+ --chart-2: 63 35% 58%;
304
+ --chart-3: 173 30% 56%;
305
+ --chart-4: 20 56% 64%;
306
+ --chart-5: 240 22% 70%;
307
+ }
308
+
309
+ :root[data-theme-style="zen-garden"] {
310
+ --background: 84 9.8% 90%;
311
+ --foreground: 225 18.2% 8.6%;
312
+
313
+ --card: 84 12% 94%;
314
+ --card-foreground: 225 18.2% 8.6%;
315
+ --popover: 84 12% 94%;
316
+ --popover-foreground: 225 18.2% 8.6%;
317
+
318
+ --primary: 173 24.7% 62%;
319
+ --primary-foreground: 225 18.2% 8.6%;
320
+ --secondary: 289 32.9% 72%;
321
+ --secondary-foreground: 225 18.2% 8.6%;
322
+ --muted: 84 10% 86%;
323
+ --muted-foreground: 225 10% 36%;
324
+ --accent: 289 32.9% 72%;
325
+ --accent-foreground: 225 18.2% 8.6%;
326
+
327
+ --border: 84 8% 78%;
328
+ --input: 84 8% 78%;
329
+ --ring: 173 24.7% 62%;
330
+
331
+ --chart-1: 173 24.7% 48%;
332
+ --chart-2: 289 32.9% 58%;
333
+ --chart-3: 40 68% 56%;
334
+ --chart-4: 220 26% 50%;
335
+ --chart-5: 0 55% 58%;
336
+ }
337
+
338
+ .dark[data-theme-style="zen-garden"] {
339
+ --background: 225 18.2% 8.6%;
340
+ --foreground: 84 9.8% 90%;
341
+
342
+ --card: 225 16% 12%;
343
+ --card-foreground: 84 9.8% 90%;
344
+ --popover: 225 16% 12%;
345
+ --popover-foreground: 84 9.8% 90%;
346
+
347
+ --primary: 173 30% 66%;
348
+ --primary-foreground: 225 18.2% 8.6%;
349
+ --secondary: 225 12% 18%;
350
+ --secondary-foreground: 84 9.8% 90%;
351
+ --muted: 225 10% 15%;
352
+ --muted-foreground: 84 8% 72%;
353
+ --accent: 225 10% 15%;
354
+ --accent-foreground: 84 9.8% 90%;
355
+
356
+ --border: 225 10% 24%;
357
+ --input: 225 10% 24%;
358
+ --ring: 173 30% 66%;
359
+
360
+ --chart-1: 173 30% 66%;
361
+ --chart-2: 289 36% 70%;
362
+ --chart-3: 40 68% 64%;
363
+ --chart-4: 220 30% 70%;
364
+ --chart-5: 355 65% 68%;
365
+ }
366
+
367
+ :root[data-theme-style="highlighter"] {
368
+ --background: 228 23.8% 95.9%;
369
+ --foreground: 223 18.9% 7.3%;
370
+
371
+ --card: 228 24% 98%;
372
+ --card-foreground: 223 18.9% 7.3%;
373
+ --popover: 228 24% 98%;
374
+ --popover-foreground: 223 18.9% 7.3%;
375
+
376
+ --primary: 97 70.5% 64.1%;
377
+ --primary-foreground: 223 18.9% 7.3%;
378
+ --secondary: 249 35.6% 88.4%;
379
+ --secondary-foreground: 223 18.9% 7.3%;
380
+ --muted: 228 20% 92%;
381
+ --muted-foreground: 223 10% 35%;
382
+ --accent: 249 35.6% 88.4%;
383
+ --accent-foreground: 223 18.9% 7.3%;
384
+
385
+ --border: 238 20% 84%;
386
+ --input: 238 20% 84%;
387
+ --ring: 97 70.5% 64.1%;
388
+
389
+ --chart-1: 97 70.5% 48%;
390
+ --chart-2: 249 45% 56%;
391
+ --chart-3: 202 72% 54%;
392
+ --chart-4: 339 78% 58%;
393
+ --chart-5: 40 78% 58%;
394
+ }
395
+
396
+ .dark[data-theme-style="highlighter"] {
397
+ --background: 223 18.9% 7.3%;
398
+ --foreground: 228 23.8% 95.9%;
399
+
400
+ --card: 223 16% 11%;
401
+ --card-foreground: 228 23.8% 95.9%;
402
+ --popover: 223 16% 11%;
403
+ --popover-foreground: 228 23.8% 95.9%;
404
+
405
+ --primary: 97 74% 66%;
406
+ --primary-foreground: 223 18.9% 7.3%;
407
+ --secondary: 223 12% 18%;
408
+ --secondary-foreground: 228 23.8% 95.9%;
409
+ --muted: 223 10% 15%;
410
+ --muted-foreground: 228 14% 72%;
411
+ --accent: 223 10% 15%;
412
+ --accent-foreground: 228 23.8% 95.9%;
413
+
414
+ --border: 223 10% 24%;
415
+ --input: 223 10% 24%;
416
+ --ring: 97 74% 66%;
417
+
418
+ --chart-1: 97 74% 66%;
419
+ --chart-2: 249 50% 72%;
420
+ --chart-3: 202 78% 70%;
421
+ --chart-4: 339 80% 70%;
422
+ --chart-5: 40 78% 68%;
423
+ }
424
+
9
425
  @layer base {
10
426
  * {
11
427
  border-color: hsl(var(--border));
@@ -1,4 +1,4 @@
1
- const PROD_PARENT_ORIGINS = new Set(["https://agentma.cn", "https://imagicma.cn", "https://nanocoda.com"]);
1
+ const PROD_PARENT_ORIGINS = new Set(["https://agentma.cn", "https://imagicma.cn"]);
2
2
  const LOCAL_PARENT_RE = /^https?:\/\/(localhost|127\.0\.0\.1)(:\d+)?$/i;
3
3
  const LOCAL_IMAGICMA_PARENT_RE = /^https?:\/\/([a-z0-9-]+\.)?local\.(agentma\.cn|imagicma\.cn)(:\d+)?$/i;
4
4
 
@@ -142,8 +142,6 @@ type PreviewPickerStateSyncPayload = {
142
142
  pageKey: string;
143
143
  overrides: PreviewOverridePageEntry | null;
144
144
  selectedNodeId: string | null;
145
- selectedTextEditable: boolean;
146
- draftTargetScope: PreviewDraftTargetScope;
147
145
  draft: PreviewDraftApplyPayload | null;
148
146
  drafts: PreviewDraftApplyPayload[];
149
147
  pendingSort: PreviewSortPreviewPayload | null;
@@ -169,19 +167,6 @@ type PickerMessage =
169
167
  sessionId: string | null;
170
168
  payload: PreviewPickerStateSyncPayload;
171
169
  }
172
- | {
173
- channel: typeof PREVIEW_PICKER_CHANNEL;
174
- version: typeof PREVIEW_PICKER_VERSION;
175
- type: "IMAGICMA_PICKER_DRAFT_PATCH";
176
- frameInstanceId: string;
177
- sessionId: string | null;
178
- payload?: {
179
- nodeId?: string;
180
- patch?: {
181
- textContent?: string;
182
- };
183
- };
184
- }
185
170
  | {
186
171
  channel: typeof PREVIEW_PICKER_CHANNEL;
187
172
  version: typeof PREVIEW_PICKER_VERSION;
@@ -208,8 +193,6 @@ type RuntimeState = {
208
193
  selectedSiblingBoxEls: HTMLDivElement[];
209
194
  draftStyleEl: HTMLStyleElement | null;
210
195
  persistedOverrides: PreviewOverridePageEntry | null;
211
- selectedTextEditable: boolean;
212
- draftTargetScope: PreviewDraftTargetScope;
213
196
  draftPayload: PreviewDraftApplyPayload | null;
214
197
  draftPayloads: PreviewDraftApplyPayload[];
215
198
  pendingSort: { groupKey: string; orderedSortKeys: readonly string[] } | null;
@@ -232,10 +215,6 @@ type RuntimeState = {
232
215
  throttledRecalculate: (() => void) | null;
233
216
  mutationObserver: MutationObserver | null;
234
217
  suppressMutationObserver: number;
235
- inlineEditingElement: HTMLElement | null;
236
- inlineEditingAbortController: AbortController | null;
237
- inlineEditingOriginalContentEditable: string | null;
238
- inlineEditingOriginalSpellcheck: boolean;
239
218
  };
240
219
 
241
220
  type SortableMetadata = {
@@ -524,6 +503,14 @@ function getElementNodeId(element: HTMLElement | null | undefined): string {
524
503
  return trimText(element?.getAttribute("data-imagicma-node-id"));
525
504
  }
526
505
 
506
+ function getRepeatItemScopeKey(element: HTMLElement | null | undefined): string | null {
507
+ const repeatRoot = element ? getRepeatItemRoot(element) : null;
508
+ const groupKey = trimText(repeatRoot?.getAttribute("data-imagicma-repeat-group"));
509
+ const itemKey = trimText(repeatRoot?.getAttribute("data-imagicma-sort-key"));
510
+ if (!groupKey || !itemKey) return null;
511
+ return `${groupKey}::${itemKey}`;
512
+ }
513
+
527
514
  function getIndexedElementsByNodeKey(state: RuntimeState, nodeKey: string): HTMLElement[] {
528
515
  const cached = state.nodeIdIndex.get(nodeKey)?.filter((element) => element.isConnected) ?? [];
529
516
  if (cached.length > 0) return cached;
@@ -535,13 +522,26 @@ function getIndexedElementsByNodeKey(state: RuntimeState, nodeKey: string): HTML
535
522
  );
536
523
  }
537
524
 
525
+ function isRepeatScopedNodeIdentity(peers: HTMLElement[]): boolean {
526
+ if (peers.length <= 1) return false;
527
+
528
+ const seenScopes = new Set<string>();
529
+ for (const peer of peers) {
530
+ const scopeKey = getRepeatItemScopeKey(peer);
531
+ if (!scopeKey || seenScopes.has(scopeKey)) {
532
+ return false;
533
+ }
534
+ seenScopes.add(scopeKey);
535
+ }
536
+
537
+ return seenScopes.size === peers.length;
538
+ }
539
+
538
540
  function getDirectSemanticSiblingItems(parent: HTMLElement | null): HTMLElement[] {
539
541
  if (!parent) return [];
540
542
 
541
543
  const items = Array.from(parent.children).filter((child): child is HTMLElement => {
542
- return child instanceof HTMLElement
543
- && Boolean(getElementNodeId(child))
544
- && trimText(child.getAttribute("data-imagicma-kind")) === "repeat-item";
544
+ return child instanceof HTMLElement && Boolean(getElementNodeId(child));
545
545
  });
546
546
 
547
547
  if (items.length < 2) return [];
@@ -559,10 +559,6 @@ function getSyntheticSortableParentId(parent: HTMLElement | null): string | null
559
559
  }
560
560
 
561
561
  function getSiblingSortableMetadata(element: HTMLElement): SortableMetadata | null {
562
- if (trimText(element.getAttribute("data-imagicma-kind")) !== "repeat-item") {
563
- return null;
564
- }
565
-
566
562
  const nodeId = getElementNodeId(element);
567
563
  if (!nodeId) return null;
568
564
 
@@ -1694,92 +1690,6 @@ function applyCurrentDraftToSelectedElement(state: RuntimeState) {
1694
1690
  });
1695
1691
  }
1696
1692
 
1697
- function disableInlineTextEditing(state: RuntimeState) {
1698
- const element = state.inlineEditingElement;
1699
- state.inlineEditingAbortController?.abort();
1700
- state.inlineEditingAbortController = null;
1701
-
1702
- if (element?.isConnected) {
1703
- if (state.inlineEditingOriginalContentEditable === null) {
1704
- element.removeAttribute("contenteditable");
1705
- } else {
1706
- element.setAttribute("contenteditable", state.inlineEditingOriginalContentEditable);
1707
- }
1708
- element.spellcheck = state.inlineEditingOriginalSpellcheck;
1709
- element.removeAttribute("data-imagicma-inline-editing");
1710
- }
1711
-
1712
- state.inlineEditingElement = null;
1713
- state.inlineEditingOriginalContentEditable = null;
1714
- state.inlineEditingOriginalSpellcheck = false;
1715
- }
1716
-
1717
- function moveCaretToEnd(element: HTMLElement) {
1718
- const selection = window.getSelection();
1719
- if (!selection) return;
1720
- const range = document.createRange();
1721
- range.selectNodeContents(element);
1722
- range.collapse(false);
1723
- selection.removeAllRanges();
1724
- selection.addRange(range);
1725
- }
1726
-
1727
- function shouldEnableInlineTextEditing(state: RuntimeState, element: HTMLElement | null): element is HTMLElement {
1728
- if (!state.enabled || !element || element !== state.selectedElement) return false;
1729
- if (state.draftTargetScope !== "single") return false;
1730
- if (!state.selectedTextEditable) return false;
1731
- if (!isSimpleTextElement(element)) return false;
1732
- return true;
1733
- }
1734
-
1735
- function enableInlineTextEditing(state: RuntimeState, element: HTMLElement) {
1736
- if (state.inlineEditingElement === element) return;
1737
- disableInlineTextEditing(state);
1738
-
1739
- state.inlineEditingElement = element;
1740
- state.inlineEditingOriginalContentEditable = element.getAttribute("contenteditable");
1741
- state.inlineEditingOriginalSpellcheck = element.spellcheck;
1742
-
1743
- const abortController = new AbortController();
1744
- state.inlineEditingAbortController = abortController;
1745
-
1746
- element.setAttribute("contenteditable", "plaintext-only");
1747
- element.setAttribute("data-imagicma-inline-editing", "true");
1748
- element.spellcheck = false;
1749
-
1750
- element.addEventListener("input", () => {
1751
- postToParent(state, {
1752
- channel: PREVIEW_PICKER_CHANNEL,
1753
- version: PREVIEW_PICKER_VERSION,
1754
- type: "IMAGICMA_PICKER_DRAFT_PATCH",
1755
- frameInstanceId: state.frameInstanceId,
1756
- sessionId: state.activeSessionId,
1757
- payload: {
1758
- nodeId: getElementNodeId(element),
1759
- patch: {
1760
- textContent: element.textContent || "",
1761
- },
1762
- },
1763
- });
1764
- }, { signal: abortController.signal });
1765
-
1766
- window.requestAnimationFrame(() => {
1767
- if (state.inlineEditingElement !== element) return;
1768
- if (document.activeElement !== element) {
1769
- element.focus({ preventScroll: true });
1770
- moveCaretToEnd(element);
1771
- }
1772
- });
1773
- }
1774
-
1775
- function syncInlineTextEditing(state: RuntimeState) {
1776
- if (shouldEnableInlineTextEditing(state, state.selectedElement)) {
1777
- enableInlineTextEditing(state, state.selectedElement);
1778
- return;
1779
- }
1780
- disableInlineTextEditing(state);
1781
- }
1782
-
1783
1693
  function applySortPreview(state: RuntimeState, items: HTMLElement[], orderedKeys: readonly string[]) {
1784
1694
  if (items.length < 2 || orderedKeys.length === 0) return;
1785
1695
 
@@ -1861,7 +1771,6 @@ function reapplyVisualState(state: RuntimeState) {
1861
1771
  applyTextOverrides(state, state.persistedOverrides, state.draftPayloads);
1862
1772
  applyCurrentDraftToSelectedElement(state);
1863
1773
  applySortOverrides(state, state.persistedOverrides, state.pendingSort);
1864
- syncInlineTextEditing(state);
1865
1774
 
1866
1775
  const hoverTarget = state.hoveredElement && state.hoveredElement !== state.selectedElement
1867
1776
  ? state.hoveredElement
@@ -1883,10 +1792,12 @@ function reapplyVisualState(state: RuntimeState) {
1883
1792
  }
1884
1793
  }
1885
1794
 
1886
- function isNodeSelectableElement(_state: RuntimeState, element: HTMLElement): boolean {
1795
+ function isNodeSelectableElement(state: RuntimeState, element: HTMLElement): boolean {
1887
1796
  const nodeId = getElementNodeId(element);
1888
1797
  if (!nodeId) return false;
1889
- return true;
1798
+
1799
+ const peers = getIndexedElementsByNodeKey(state, nodeId);
1800
+ return peers.length <= 1 || isRepeatScopedNodeIdentity(peers);
1890
1801
  }
1891
1802
 
1892
1803
  function isOverlayElement(state: RuntimeState, element: HTMLElement): boolean {
@@ -1968,8 +1879,6 @@ function createRuntimeState(): RuntimeState {
1968
1879
  selectedSiblingBoxEls: [],
1969
1880
  draftStyleEl: null,
1970
1881
  persistedOverrides: null,
1971
- selectedTextEditable: false,
1972
- draftTargetScope: "single",
1973
1882
  draftPayload: null,
1974
1883
  draftPayloads: [],
1975
1884
  pendingSort: null,
@@ -1988,10 +1897,6 @@ function createRuntimeState(): RuntimeState {
1988
1897
  throttledRecalculate: null,
1989
1898
  mutationObserver: null,
1990
1899
  suppressMutationObserver: 0,
1991
- inlineEditingElement: null,
1992
- inlineEditingAbortController: null,
1993
- inlineEditingOriginalContentEditable: null,
1994
- inlineEditingOriginalSpellcheck: false,
1995
1900
  };
1996
1901
  }
1997
1902
 
@@ -2199,8 +2104,6 @@ export function installPreviewPickerRuntime() {
2199
2104
  state.activeSessionId = event.data.sessionId;
2200
2105
  state.enabled = event.data.payload.mode === "picking";
2201
2106
  state.persistedOverrides = event.data.payload.overrides;
2202
- state.selectedTextEditable = event.data.payload.selectedTextEditable === true;
2203
- state.draftTargetScope = event.data.payload.draftTargetScope;
2204
2107
  state.draftPayload = event.data.payload.draft;
2205
2108
  state.draftPayloads = event.data.payload.drafts ?? (event.data.payload.draft ? [event.data.payload.draft] : []);
2206
2109
  state.pendingSort = event.data.payload.pendingSort
@@ -2218,8 +2121,6 @@ export function installPreviewPickerRuntime() {
2218
2121
  sessionId: state.activeSessionId,
2219
2122
  mode: event.data.payload.mode,
2220
2123
  selectedNodeId: event.data.payload.selectedNodeId,
2221
- selectedTextEditable: event.data.payload.selectedTextEditable,
2222
- draftTargetScope: event.data.payload.draftTargetScope,
2223
2124
  hasDraft: Boolean(event.data.payload.draft),
2224
2125
  hasPendingSort: Boolean(event.data.payload.pendingSort),
2225
2126
  pageKey: event.data.payload.pageKey,
@@ -2233,12 +2134,9 @@ export function installPreviewPickerRuntime() {
2233
2134
  state.activeSessionId = null;
2234
2135
  state.selectedElement = null;
2235
2136
  state.hoveredElement = null;
2236
- state.selectedTextEditable = false;
2237
- state.draftTargetScope = "single";
2238
2137
  state.draftPayload = null;
2239
2138
  state.draftPayloads = [];
2240
2139
  state.pendingSort = null;
2241
- disableInlineTextEditing(state);
2242
2140
  clearOverlay(state);
2243
2141
  applySortableAffordance(state, false);
2244
2142
  logPreviewPickerRuntime("stop-session", {
@@ -9,7 +9,7 @@ export function Providers({ children }: { children: React.ReactNode }) {
9
9
  const [queryClient] = React.useState(() => makeQueryClient());
10
10
 
11
11
  return (
12
- <ThemeProvider attribute="class" defaultTheme="light">
12
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem>
13
13
  <QueryClientProvider client={queryClient}>
14
14
  <TooltipProvider>
15
15
  <Toaster />