@ikenga/contract 0.7.0 → 0.9.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 (58) hide show
  1. package/README.md +56 -11
  2. package/dist/canvas/Canvas.d.ts +7 -0
  3. package/dist/canvas/Canvas.d.ts.map +1 -0
  4. package/dist/canvas/Canvas.js +115 -0
  5. package/dist/canvas/Canvas.js.map +1 -0
  6. package/dist/canvas/canvas.css +579 -0
  7. package/dist/canvas/index.d.ts +7 -0
  8. package/dist/canvas/index.d.ts.map +1 -0
  9. package/dist/canvas/index.js +4 -0
  10. package/dist/canvas/index.js.map +1 -0
  11. package/dist/canvas/types.d.ts +45 -0
  12. package/dist/canvas/types.d.ts.map +1 -0
  13. package/dist/canvas/types.js +2 -0
  14. package/dist/canvas/types.js.map +1 -0
  15. package/dist/canvas/use-drag-snap.d.ts +33 -0
  16. package/dist/canvas/use-drag-snap.d.ts.map +1 -0
  17. package/dist/canvas/use-drag-snap.js +73 -0
  18. package/dist/canvas/use-drag-snap.js.map +1 -0
  19. package/dist/canvas/use-pan-zoom.d.ts +32 -0
  20. package/dist/canvas/use-pan-zoom.d.ts.map +1 -0
  21. package/dist/canvas/use-pan-zoom.js +161 -0
  22. package/dist/canvas/use-pan-zoom.js.map +1 -0
  23. package/dist/engine.d.ts +574 -0
  24. package/dist/engine.d.ts.map +1 -0
  25. package/dist/engine.js +85 -0
  26. package/dist/engine.js.map +1 -0
  27. package/dist/host-verbs.d.ts +194 -0
  28. package/dist/host-verbs.d.ts.map +1 -0
  29. package/dist/host-verbs.js +15 -0
  30. package/dist/host-verbs.js.map +1 -0
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +1 -0
  34. package/dist/index.js.map +1 -1
  35. package/dist/manifest.d.ts +376 -19
  36. package/dist/manifest.d.ts.map +1 -1
  37. package/dist/manifest.js +95 -4
  38. package/dist/manifest.js.map +1 -1
  39. package/dist/registry.d.ts +364 -36
  40. package/dist/registry.d.ts.map +1 -1
  41. package/dist/registry.js +9 -0
  42. package/dist/registry.js.map +1 -1
  43. package/dist/scopes.js +1 -1
  44. package/dist/scopes.js.map +1 -1
  45. package/package.json +36 -10
  46. package/schemas/registry/index-v1.json +11 -0
  47. package/src/canvas/Canvas.tsx +161 -0
  48. package/src/canvas/canvas.css +579 -0
  49. package/src/canvas/index.ts +14 -0
  50. package/src/canvas/types.ts +48 -0
  51. package/src/canvas/use-drag-snap.ts +107 -0
  52. package/src/canvas/use-pan-zoom.ts +211 -0
  53. package/src/host-verbs.ts +207 -0
  54. package/src/index.ts +1 -0
  55. package/src/manifest.test.ts +97 -0
  56. package/src/manifest.ts +109 -4
  57. package/src/registry.ts +9 -0
  58. package/src/scopes.ts +1 -1
@@ -0,0 +1,579 @@
1
+ /* Ikenga <Canvas> primitive — free-form, pan/zoom/grid-snap widget surface.
2
+ *
3
+ * Extracted from the shell home page (design source:
4
+ * design/shell/concepts/03-screens/16-home.html). Owns the canvas chrome
5
+ * (surface, grain, stage, item wrapper, top bar, palette) plus the default
6
+ * item-content styling shared by the home widgets. Theme-aware via the host
7
+ * document's data-mode attribute — no @media prefers-color-scheme here.
8
+ *
9
+ * Tokens (var(--fg), var(--border), …) resolve against whatever stylesheet
10
+ * the host document loads. Inside the shell that's the global styles.css;
11
+ * inside a Vite/iframe pkg the consumer must import @ikenga/tokens at entry
12
+ * (Round 8 / G21) so these custom properties resolve in the sandbox.
13
+ *
14
+ * Structural selectors are prefixed `.ikenga-canvas*` so they cannot collide
15
+ * with shell-global styles; inner content selectors (.w-*, .qrow, .pad-meta,
16
+ * the greeting/finance helpers) are scoped under `.ikenga-canvas-item` /
17
+ * `.ikenga-canvas-stage` for the same reason.
18
+ */
19
+
20
+ .ikenga-canvas {
21
+ position: absolute;
22
+ inset: 0;
23
+ overflow: hidden;
24
+ background: radial-gradient(900px 600px at 50% 40%, var(--primary-soft) 0%, var(--bg-base) 70%);
25
+ }
26
+
27
+ /* Wood-grain dot pattern, pans with the stage. */
28
+ .ikenga-canvas-grain {
29
+ position: absolute;
30
+ inset: 0;
31
+ background-image: radial-gradient(circle, hsla(28, 14%, 22%, 0.55) 1px, transparent 1px);
32
+ background-size: 28px 28px;
33
+ opacity: 0.38;
34
+ pointer-events: none;
35
+ transition: opacity 180ms ease;
36
+ }
37
+ .ikenga-canvas[data-mode-edit="true"] .ikenga-canvas-grain {
38
+ opacity: 0.65;
39
+ }
40
+ [data-mode="light"] .ikenga-canvas-grain {
41
+ background-image: radial-gradient(circle, hsla(28, 14%, 40%, 0.3) 1px, transparent 1px);
42
+ opacity: 0.35;
43
+ }
44
+
45
+ /* Stage holds items, translated/scaled as a whole on pan/autofit. */
46
+ .ikenga-canvas-stage {
47
+ position: absolute;
48
+ inset: 0;
49
+ transform-origin: 0 0;
50
+ transition: transform 260ms cubic-bezier(0.2, 0.6, 0.2, 1);
51
+ will-change: transform;
52
+ }
53
+ .ikenga-canvas-stage.is-dragging {
54
+ transition: none;
55
+ }
56
+
57
+ /* Item geometry. Canvas injects left/top/width/height (or min-height) inline
58
+ onto the element renderItem returns, so the consumer's own card/greeting
59
+ element stays the positioned box — DOM shape is identical to pre-extraction.
60
+ The renderItem element must carry `position: absolute` itself; the home
61
+ widget/greeting rules below provide it. */
62
+
63
+ /* Widget card */
64
+ .home-widget {
65
+ position: absolute;
66
+ background: var(--bg-surface);
67
+ border: 1px solid var(--border);
68
+ border-radius: 12px;
69
+ box-shadow:
70
+ 0 1px 2px rgba(0, 0, 0, 0.55),
71
+ 0 4px 12px -2px rgba(0, 0, 0, 0.45);
72
+ overflow: hidden;
73
+ display: flex;
74
+ flex-direction: column;
75
+ transition:
76
+ border-color 120ms ease,
77
+ box-shadow 120ms ease;
78
+ }
79
+ [data-mode="light"] .home-widget {
80
+ box-shadow:
81
+ 0 1px 2px rgba(28, 18, 8, 0.08),
82
+ 0 4px 12px -2px rgba(28, 18, 8, 0.1);
83
+ }
84
+ .home-widget:hover {
85
+ border-color: var(--border-soft);
86
+ }
87
+ .home-widget[data-patina="warm"] {
88
+ box-shadow:
89
+ 0 4px 12px -2px rgba(0, 0, 0, 0.55),
90
+ 0 0 0 1px hsla(42, 78%, 54%, 0.14),
91
+ 0 0 22px 2px hsla(20, 50%, 34%, 0.12);
92
+ }
93
+ [data-mode="light"] .home-widget[data-patina="warm"] {
94
+ box-shadow:
95
+ 0 4px 12px -2px rgba(28, 18, 8, 0.1),
96
+ 0 0 0 1px hsla(42, 78%, 42%, 0.18),
97
+ 0 0 22px 2px hsla(20, 50%, 70%, 0.14);
98
+ }
99
+
100
+ /* Naked greeting — sits on the canvas like text on paper, no card. */
101
+ .home-greeting {
102
+ position: absolute;
103
+ padding: 4px 8px;
104
+ transition:
105
+ outline-color 120ms ease,
106
+ background 120ms ease;
107
+ outline: 1px dashed transparent;
108
+ outline-offset: 6px;
109
+ border-radius: 4px;
110
+ }
111
+ .ikenga-canvas[data-mode-edit="true"] .home-greeting {
112
+ outline-color: var(--border);
113
+ cursor: grab;
114
+ }
115
+ .ikenga-canvas[data-mode-edit="true"] .home-greeting:hover {
116
+ outline-color: var(--achievement, hsl(42, 78%, 54%));
117
+ background: hsla(42, 78%, 54%, 0.04);
118
+ }
119
+ .home-greeting.is-selected {
120
+ outline-color: var(--achievement, hsl(42, 78%, 54%)) !important;
121
+ }
122
+ .home-greeting h1 {
123
+ font-family: var(--font-display, ui-serif);
124
+ font-weight: 500;
125
+ font-size: 36px;
126
+ line-height: 1.04;
127
+ letter-spacing: -0.014em;
128
+ margin: 0;
129
+ color: var(--fg);
130
+ }
131
+ .home-greeting h1 em {
132
+ font-style: italic;
133
+ font-weight: 400;
134
+ color: var(--achievement, hsl(42, 78%, 54%));
135
+ }
136
+ .home-greeting .gloss {
137
+ margin-top: 6px;
138
+ display: flex;
139
+ flex-wrap: wrap;
140
+ align-items: baseline;
141
+ gap: 8px;
142
+ font-family: var(--font-mono, ui-monospace);
143
+ font-size: 11px;
144
+ letter-spacing: 0.04em;
145
+ color: var(--fg-faint);
146
+ }
147
+ .home-greeting .gloss .english {
148
+ font-family: var(--font-display, ui-serif);
149
+ font-style: italic;
150
+ font-size: 13px;
151
+ letter-spacing: 0;
152
+ color: var(--fg-muted);
153
+ }
154
+ .home-greeting .gloss .sep {
155
+ opacity: 0.55;
156
+ }
157
+ .home-greeting .shape {
158
+ margin-top: 14px;
159
+ font-size: 14px;
160
+ color: var(--fg-muted);
161
+ line-height: 1.55;
162
+ max-width: 36em;
163
+ }
164
+ .home-greeting .quote {
165
+ margin-top: 18px;
166
+ padding-top: 14px;
167
+ border-top: 1px solid var(--border-soft);
168
+ max-width: 40em;
169
+ }
170
+ .home-greeting .quote-body {
171
+ font-family: var(--font-display, ui-serif);
172
+ font-style: italic;
173
+ font-size: 14px;
174
+ line-height: 1.55;
175
+ color: var(--fg);
176
+ }
177
+ .home-greeting .quote-gloss {
178
+ font-style: normal;
179
+ font-size: 13px;
180
+ color: var(--fg-faint);
181
+ margin-left: 6px;
182
+ }
183
+ .home-greeting .quote-gloss::before {
184
+ content: "— ";
185
+ }
186
+ .home-greeting .quote-attr {
187
+ margin-top: 6px;
188
+ font-family: var(--font-mono, ui-monospace);
189
+ font-size: 10.5px;
190
+ letter-spacing: 0.04em;
191
+ color: var(--fg-faint);
192
+ }
193
+
194
+ .home-widget .w-head {
195
+ display: flex;
196
+ align-items: center;
197
+ gap: 8px;
198
+ padding: 12px 16px;
199
+ border-bottom: 1px solid var(--border-soft);
200
+ background: linear-gradient(180deg, hsla(28, 14%, 14%, 0.4) 0%, transparent 100%);
201
+ }
202
+ [data-mode="light"] .home-widget .w-head {
203
+ background: linear-gradient(180deg, hsla(28, 14%, 86%, 0.45) 0%, transparent 100%);
204
+ }
205
+ .home-widget .w-icon {
206
+ width: 22px;
207
+ height: 22px;
208
+ border-radius: 5px;
209
+ background: var(--bg-raised);
210
+ display: grid;
211
+ place-items: center;
212
+ color: var(--fg-muted);
213
+ }
214
+ .home-widget .w-icon svg {
215
+ width: 14px;
216
+ height: 14px;
217
+ }
218
+ .home-widget .w-title {
219
+ font-family: var(--font-display, ui-serif);
220
+ font-weight: 500;
221
+ font-size: 14px;
222
+ color: var(--fg);
223
+ line-height: 1;
224
+ }
225
+ .home-widget .w-tag {
226
+ font-family: var(--font-mono, ui-monospace);
227
+ font-size: 10px;
228
+ letter-spacing: 0.08em;
229
+ text-transform: uppercase;
230
+ color: var(--fg-faint);
231
+ margin-left: auto;
232
+ }
233
+ .home-widget[data-patina="warm"] .w-tag {
234
+ color: var(--achievement, hsl(42, 78%, 54%));
235
+ }
236
+ .home-widget .w-body {
237
+ flex: 1;
238
+ padding: 12px 16px;
239
+ font-size: 13px;
240
+ color: var(--fg-muted);
241
+ line-height: 1.5;
242
+ overflow: auto;
243
+ }
244
+
245
+ .home-widget .w-row {
246
+ display: flex;
247
+ align-items: center;
248
+ gap: 12px;
249
+ padding: 7px 0;
250
+ border-bottom: 1px solid var(--border-soft);
251
+ }
252
+ .home-widget .w-row:last-child {
253
+ border-bottom: 0;
254
+ }
255
+ .home-widget .w-dot {
256
+ width: 7px;
257
+ height: 7px;
258
+ border-radius: 50%;
259
+ background: var(--fg-faint);
260
+ flex-shrink: 0;
261
+ }
262
+ .home-widget .w-dot.live {
263
+ background: var(--live, hsl(150, 48%, 52%));
264
+ box-shadow: 0 0 0 3px hsla(150, 48%, 52%, 0.16);
265
+ }
266
+ .home-widget .w-dot.warm {
267
+ background: var(--achievement, hsl(42, 78%, 54%));
268
+ }
269
+ .home-widget .w-dot.danger {
270
+ background: var(--danger);
271
+ }
272
+ .home-widget .w-dot.agent {
273
+ background: var(--agent, hsl(265, 52%, 64%));
274
+ }
275
+ .home-widget .w-label {
276
+ flex: 1;
277
+ color: var(--fg);
278
+ font-size: 13px;
279
+ min-width: 0;
280
+ }
281
+ .home-widget .w-meta {
282
+ font-family: var(--font-mono, ui-monospace);
283
+ font-size: 10.5px;
284
+ letter-spacing: 0.04em;
285
+ color: var(--fg-faint);
286
+ flex-shrink: 0;
287
+ }
288
+ .home-widget .w-agent-tag {
289
+ color: var(--fg-faint);
290
+ font-family: var(--font-mono, ui-monospace);
291
+ font-size: 10.5px;
292
+ letter-spacing: 0.04em;
293
+ margin-right: 8px;
294
+ }
295
+
296
+ /* Edit-mode adornments. */
297
+ .ikenga-canvas[data-mode-edit="true"] .home-widget {
298
+ border-style: dashed;
299
+ border-color: var(--border-soft);
300
+ cursor: grab;
301
+ }
302
+ .ikenga-canvas[data-mode-edit="true"] .home-widget:hover {
303
+ border-color: var(--achievement, hsl(42, 78%, 54%));
304
+ box-shadow:
305
+ 0 4px 12px -2px rgba(0, 0, 0, 0.55),
306
+ 0 0 0 1px hsla(42, 78%, 54%, 0.18);
307
+ }
308
+ .ikenga-canvas[data-mode-edit="true"] .home-widget.is-selected {
309
+ border-color: var(--achievement, hsl(42, 78%, 54%));
310
+ border-style: solid;
311
+ box-shadow:
312
+ 0 12px 28px -8px rgba(0, 0, 0, 0.6),
313
+ 0 0 0 1px var(--achievement, hsl(42, 78%, 54%));
314
+ }
315
+
316
+ /* Top bar — floats over the canvas. */
317
+ .ikenga-canvas-bar {
318
+ position: absolute;
319
+ top: 0;
320
+ left: 0;
321
+ right: 0;
322
+ height: 44px;
323
+ display: flex;
324
+ align-items: center;
325
+ gap: 12px;
326
+ padding: 0 16px;
327
+ background: linear-gradient(180deg, var(--bg-sunken) 0%, transparent 100%);
328
+ z-index: 5;
329
+ pointer-events: none;
330
+ }
331
+ .ikenga-canvas-bar > * {
332
+ pointer-events: auto;
333
+ }
334
+ .ikenga-canvas-bar .crumb {
335
+ font-family: var(--font-mono, ui-monospace);
336
+ font-size: 11px;
337
+ letter-spacing: 0.08em;
338
+ text-transform: uppercase;
339
+ color: var(--fg-faint);
340
+ }
341
+ .ikenga-canvas-bar .crumb b {
342
+ color: var(--fg-muted);
343
+ font-weight: 500;
344
+ }
345
+ .ikenga-canvas-bar .hint {
346
+ font-family: var(--font-mono, ui-monospace);
347
+ font-size: 11px;
348
+ color: var(--fg-faint);
349
+ letter-spacing: 0.04em;
350
+ margin-left: auto;
351
+ }
352
+ .ikenga-canvas-bar .hint .kbd {
353
+ display: inline-block;
354
+ padding: 1px 5px;
355
+ background: var(--bg-surface);
356
+ border: 1px solid var(--border-soft);
357
+ border-radius: 3px;
358
+ color: var(--fg-muted);
359
+ font-family: var(--font-mono, ui-monospace);
360
+ font-size: 10px;
361
+ }
362
+ .ikenga-canvas-bar .btn {
363
+ height: 26px;
364
+ padding: 0 12px;
365
+ border: 1px solid var(--border);
366
+ background: var(--bg-surface);
367
+ color: var(--fg-muted);
368
+ font-family: var(--font-mono, ui-monospace);
369
+ font-size: 11px;
370
+ letter-spacing: 0.04em;
371
+ border-radius: 5px;
372
+ display: inline-flex;
373
+ align-items: center;
374
+ gap: 6px;
375
+ cursor: pointer;
376
+ transition:
377
+ border-color 120ms ease,
378
+ color 120ms ease,
379
+ background 120ms ease;
380
+ }
381
+ .ikenga-canvas-bar .btn:hover {
382
+ color: var(--fg);
383
+ border-color: var(--border-soft);
384
+ }
385
+ .ikenga-canvas-bar .btn.is-primary {
386
+ background: hsla(42, 78%, 54%, 0.14);
387
+ border-color: hsla(42, 78%, 54%, 0.14);
388
+ color: var(--achievement, hsl(42, 78%, 54%));
389
+ }
390
+ .ikenga-canvas-bar .btn svg {
391
+ width: 12px;
392
+ height: 12px;
393
+ }
394
+
395
+ /* Palette slide-in. */
396
+ .home-palette {
397
+ position: absolute;
398
+ top: 44px;
399
+ right: 0;
400
+ bottom: 0;
401
+ width: 320px;
402
+ background: var(--bg-sunken);
403
+ border-left: 1px solid var(--border-soft);
404
+ display: none;
405
+ flex-direction: column;
406
+ z-index: 7;
407
+ box-shadow: -16px 0 24px -12px rgba(0, 0, 0, 0.5);
408
+ overflow-y: auto;
409
+ }
410
+ [data-mode="light"] .home-palette {
411
+ box-shadow: -16px 0 24px -12px rgba(28, 18, 8, 0.14);
412
+ }
413
+ .ikenga-canvas[data-mode-edit="true"] .home-palette {
414
+ display: flex;
415
+ }
416
+ .home-palette .head {
417
+ padding: 16px 16px 12px;
418
+ border-bottom: 1px solid var(--border-soft);
419
+ }
420
+ .home-palette .head .ptag {
421
+ font-family: var(--font-mono, ui-monospace);
422
+ font-size: 10.5px;
423
+ letter-spacing: 0.08em;
424
+ text-transform: uppercase;
425
+ color: var(--achievement, hsl(42, 78%, 54%));
426
+ margin-bottom: 4px;
427
+ }
428
+ .home-palette .head h3 {
429
+ margin: 0;
430
+ font-family: var(--font-display, ui-serif);
431
+ font-weight: 500;
432
+ font-size: 16px;
433
+ color: var(--fg);
434
+ }
435
+ .home-palette .head p {
436
+ margin: 4px 0 0;
437
+ font-size: 12px;
438
+ color: var(--fg-muted);
439
+ line-height: 1.5;
440
+ }
441
+ .home-palette .section {
442
+ padding: 12px 16px 16px;
443
+ }
444
+ .home-palette .section + .section {
445
+ border-top: 1px solid var(--border-soft);
446
+ }
447
+ .home-palette .section h4 {
448
+ margin: 0 0 8px;
449
+ font-family: var(--font-mono, ui-monospace);
450
+ font-size: 10px;
451
+ letter-spacing: 0.08em;
452
+ text-transform: uppercase;
453
+ color: var(--fg-faint);
454
+ font-weight: 500;
455
+ }
456
+ .home-palette .list {
457
+ display: grid;
458
+ gap: 6px;
459
+ }
460
+ .home-palette .item {
461
+ display: flex;
462
+ align-items: center;
463
+ gap: 12px;
464
+ padding: 8px 12px;
465
+ background: var(--bg-surface);
466
+ border: 1px solid var(--border-soft);
467
+ border-radius: 5px;
468
+ cursor: grab;
469
+ transition:
470
+ border-color 120ms ease,
471
+ transform 120ms ease;
472
+ }
473
+ .home-palette .item:hover {
474
+ border-color: var(--achievement, hsl(42, 78%, 54%));
475
+ transform: translateX(-2px);
476
+ }
477
+ .home-palette .item .pic {
478
+ width: 24px;
479
+ height: 24px;
480
+ background: var(--bg-raised);
481
+ border-radius: 3px;
482
+ display: grid;
483
+ place-items: center;
484
+ color: var(--fg-muted);
485
+ }
486
+ .home-palette .item .pic svg {
487
+ width: 13px;
488
+ height: 13px;
489
+ }
490
+ .home-palette .item .title {
491
+ font-size: 13px;
492
+ color: var(--fg);
493
+ }
494
+ .home-palette .item .meta {
495
+ margin-left: auto;
496
+ font-family: var(--font-mono, ui-monospace);
497
+ font-size: 10px;
498
+ letter-spacing: 0.04em;
499
+ color: var(--fg-faint);
500
+ }
501
+ .home-palette .item.is-placed {
502
+ opacity: 0.45;
503
+ cursor: default;
504
+ }
505
+ .home-palette .item.is-placed:hover {
506
+ border-color: var(--border-soft);
507
+ transform: none;
508
+ }
509
+
510
+ /* Quick-actions widget — distinct row shape with kbd column. */
511
+ .home-widget .qrow {
512
+ display: grid;
513
+ grid-template-columns: 1fr auto auto;
514
+ gap: 12px;
515
+ align-items: center;
516
+ padding: 7px 0;
517
+ border-bottom: 1px solid var(--border-soft);
518
+ }
519
+ .home-widget .qrow:last-child {
520
+ border-bottom: 0;
521
+ }
522
+ .home-widget .qrow > span:first-child {
523
+ color: var(--fg);
524
+ font-size: 13px;
525
+ }
526
+ .home-widget .qrow .qmeta {
527
+ color: var(--fg-faint);
528
+ font-family: var(--font-mono, ui-monospace);
529
+ font-size: 10.5px;
530
+ }
531
+ .home-widget .qrow .kbd {
532
+ display: inline-block;
533
+ padding: 2px 6px;
534
+ background: var(--bg-raised);
535
+ border: 1px solid var(--border);
536
+ border-radius: 3px;
537
+ color: var(--fg-muted);
538
+ font-family: var(--font-mono, ui-monospace);
539
+ font-size: 10px;
540
+ }
541
+
542
+ /* Scratchpad — pull-quote feel. */
543
+ .home-widget.w-pad .w-body {
544
+ font-family: var(--font-display, ui-serif);
545
+ font-style: italic;
546
+ color: var(--fg);
547
+ font-size: 14px;
548
+ line-height: 1.6;
549
+ }
550
+ .home-widget.w-pad .pad-meta {
551
+ margin-top: 12px;
552
+ font-style: normal;
553
+ font-family: var(--font-mono, ui-monospace);
554
+ font-size: 10.5px;
555
+ letter-spacing: 0.04em;
556
+ color: var(--fg-faint);
557
+ }
558
+
559
+ /* Finance — single big figure + spark. */
560
+ .home-widget.w-finance .figure {
561
+ font-family: var(--font-display, ui-serif);
562
+ font-weight: 500;
563
+ font-size: 36px;
564
+ color: var(--fg);
565
+ letter-spacing: -0.018em;
566
+ line-height: 1;
567
+ margin: 8px 0 12px;
568
+ }
569
+ .home-widget.w-finance .figure em {
570
+ font-style: normal;
571
+ color: var(--achievement, hsl(42, 78%, 54%));
572
+ font-size: 18px;
573
+ margin-left: 8px;
574
+ letter-spacing: 0;
575
+ }
576
+ .home-widget.w-finance .spark {
577
+ height: 32px;
578
+ margin-top: 4px;
579
+ }
@@ -0,0 +1,7 @@
1
+ export type { ItemId, Placement, Viewport, ItemRenderState, CanvasProps, CanvasHandle, } from './types.js';
2
+ export { Canvas } from './Canvas.js';
3
+ export { usePanZoom } from './use-pan-zoom.js';
4
+ export { useDragSnap } from './use-drag-snap.js';
5
+ export type { UsePanZoom, UsePanZoomArgs } from './use-pan-zoom.js';
6
+ export type { DragState, UseDragSnap, UseDragSnapArgs } from './use-drag-snap.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/canvas/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,MAAM,EACN,SAAS,EACT,QAAQ,EACR,eAAe,EACf,WAAW,EACX,YAAY,GACb,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,YAAY,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACpE,YAAY,EAAE,SAAS,EAAE,WAAW,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export { Canvas } from './Canvas.js';
2
+ export { usePanZoom } from './use-pan-zoom.js';
3
+ export { useDragSnap } from './use-drag-snap.js';
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/canvas/index.ts"],"names":[],"mappings":"AASA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC"}
@@ -0,0 +1,45 @@
1
+ import type { ReactNode } from 'react';
2
+ export type ItemId = string & {
3
+ readonly __brand: 'ItemId';
4
+ };
5
+ export interface Placement {
6
+ x: number;
7
+ y: number;
8
+ w: number;
9
+ h: number;
10
+ }
11
+ export interface Viewport {
12
+ x: number;
13
+ y: number;
14
+ scale: number;
15
+ }
16
+ export interface ItemRenderState {
17
+ id: ItemId;
18
+ kind: string;
19
+ placement: Placement;
20
+ isSelected: boolean;
21
+ isEditMode: boolean;
22
+ }
23
+ export interface CanvasProps<T> {
24
+ items: T[];
25
+ itemId: (item: T) => ItemId;
26
+ itemKind: (item: T) => string;
27
+ layout: Record<ItemId, Placement>;
28
+ viewport: Viewport;
29
+ editMode: boolean;
30
+ selectedId?: ItemId | null;
31
+ gridSnap?: number;
32
+ renderItem: (item: T, state: ItemRenderState) => ReactNode;
33
+ onLayoutChange?: (layout: Record<ItemId, Placement>) => void;
34
+ onViewportChange?: (viewport: Viewport) => void;
35
+ onEditModeChange?: (editMode: boolean) => void;
36
+ onSelectionChange?: (selectedId: ItemId | null) => void;
37
+ autoFitOnResize?: boolean;
38
+ className?: string;
39
+ /** Consumer chrome (top bar, palette) rendered inside the canvas surface. */
40
+ children?: ReactNode;
41
+ }
42
+ export interface CanvasHandle {
43
+ autoFit(animate?: boolean): void;
44
+ }
45
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/canvas/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;AAE7D,MAAM,WAAW,SAAS;IACxB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,SAAS,CAAC;IACrB,UAAU,EAAE,OAAO,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,WAAW,WAAW,CAAC,CAAC;IAC5B,KAAK,EAAE,CAAC,EAAE,CAAC;IACX,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC5B,QAAQ,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,CAAC;IAC9B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,QAAQ,EAAE,QAAQ,CAAC;IACnB,QAAQ,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,eAAe,KAAK,SAAS,CAAC;IAC3D,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,IAAI,CAAC;IAC7D,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAC;IAChD,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/C,iBAAiB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACxD,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,CAAC,OAAO,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAClC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/canvas/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,33 @@
1
+ import type { ItemId, Placement } from './types.js';
2
+ export interface DragState {
3
+ id: ItemId;
4
+ startMouse: {
5
+ x: number;
6
+ y: number;
7
+ };
8
+ startPos: {
9
+ x: number;
10
+ y: number;
11
+ };
12
+ }
13
+ export interface UseDragSnapArgs {
14
+ /** Controlled layout (id → placement). Read at gesture start; never mutated. */
15
+ layout: Record<ItemId, Placement>;
16
+ /** Snap lattice in canvas units. Home passes 12, studio passes 24. */
17
+ gridSnap: number;
18
+ /** Current viewport scale — drag delta is divided by this. */
19
+ scale: number;
20
+ /** Emits the moved layout so the parent can persist / re-render. */
21
+ onLayoutChange?: (layout: Record<ItemId, Placement>) => void;
22
+ /** Stage element whose `.is-dragging` class suppresses the pan transition. */
23
+ stageRef: React.RefObject<HTMLDivElement | null>;
24
+ }
25
+ export interface UseDragSnap {
26
+ dragState: React.MutableRefObject<DragState | null>;
27
+ /** Begin dragging `id` from a mousedown at screen (clientX, clientY). */
28
+ beginDrag: (id: ItemId, clientX: number, clientY: number) => void;
29
+ /** Is an item drag in flight? */
30
+ isDragging: () => boolean;
31
+ }
32
+ export declare function useDragSnap(args: UseDragSnapArgs): UseDragSnap;
33
+ //# sourceMappingURL=use-drag-snap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-drag-snap.d.ts","sourceRoot":"","sources":["../../src/canvas/use-drag-snap.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEpD,MAAM,WAAW,SAAS;IACxB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACrC,QAAQ,EAAE;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CACpC;AAED,MAAM,WAAW,eAAe;IAC9B,gFAAgF;IAChF,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;IAClC,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,KAAK,IAAI,CAAC;IAC7D,8EAA8E;IAC9E,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC,cAAc,GAAG,IAAI,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,WAAW;IAC1B,SAAS,EAAE,KAAK,CAAC,gBAAgB,CAAC,SAAS,GAAG,IAAI,CAAC,CAAC;IACpD,yEAAyE;IACzE,SAAS,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClE,iCAAiC;IACjC,UAAU,EAAE,MAAM,OAAO,CAAC;CAC3B;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,CA6D9D"}