@groeponline/pi-wishcraft 1.1.0 → 1.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 (64) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +16 -7
  3. package/ROADMAP.md +41 -260
  4. package/docs/commands.md +6 -2
  5. package/docs/configuration.md +21 -0
  6. package/docs/design/accessibility.md +71 -0
  7. package/docs/design/deck-layout.md +69 -0
  8. package/docs/design/motion-gallery.md +80 -0
  9. package/docs/design/motion-system.md +119 -0
  10. package/docs/design/presets.md +196 -0
  11. package/docs/design/regression-testing.md +54 -0
  12. package/docs/design/responsive.md +53 -0
  13. package/docs/design/signal.md +68 -0
  14. package/docs/design/theme-contract.md +132 -0
  15. package/docs/design/vnext-overview.md +55 -0
  16. package/docs/design/vnext-release-plan.md +212 -0
  17. package/docs/index.md +19 -1
  18. package/docs/segments.md +1 -1
  19. package/package.json +2 -1
  20. package/src/config/appearance.ts +230 -0
  21. package/src/config/parse.ts +41 -0
  22. package/src/config/presets.ts +184 -0
  23. package/src/config/structural-presets.ts +555 -0
  24. package/src/config/tokens.ts +154 -0
  25. package/src/config/types.ts +210 -2
  26. package/src/extension/commands/commands.ts +36 -12
  27. package/src/extension/commands/powerline-completions.ts +4 -0
  28. package/src/extension/commands/queue-commands.ts +6 -0
  29. package/src/extension/core/segment-context.ts +7 -1
  30. package/src/extension/core/state.ts +14 -0
  31. package/src/extension/core/types.ts +7 -0
  32. package/src/extension/session/session-lifecycle.ts +20 -0
  33. package/src/extension/settings/appearance-write.ts +135 -0
  34. package/src/extension/settings/wishcraft-config-items.ts +119 -0
  35. package/src/extension/settings/wishcraft-config.ts +28 -114
  36. package/src/extension/skills/skill-manager.ts +5 -0
  37. package/src/extension/ui/deck/component.ts +358 -0
  38. package/src/extension/ui/deck/index.ts +34 -0
  39. package/src/extension/ui/deck/render.ts +260 -0
  40. package/src/extension/ui/deck/route-bodies.ts +209 -0
  41. package/src/extension/ui/deck/routes.ts +37 -0
  42. package/src/extension/ui/deck/session-snapshot.ts +104 -0
  43. package/src/extension/ui/deck/types.ts +92 -0
  44. package/src/extension/ui/powerline-menu-view.ts +19 -15
  45. package/src/extension/ui/signal-layout.ts +77 -0
  46. package/src/extension/ui/status-line-renderers.ts +20 -2
  47. package/src/motion/accessibility.ts +59 -0
  48. package/src/motion/catalog-extra.ts +429 -0
  49. package/src/motion/catalog.ts +336 -0
  50. package/src/motion/composer.ts +147 -0
  51. package/src/motion/frames.ts +84 -0
  52. package/src/motion/gallery.ts +77 -0
  53. package/src/motion/index.ts +78 -0
  54. package/src/motion/policy.ts +128 -0
  55. package/src/motion/scheduler.ts +159 -0
  56. package/src/motion/types.ts +132 -0
  57. package/src/render/timer.ts +1 -0
  58. package/src/signal/controller.ts +123 -0
  59. package/src/signal/integration.ts +42 -0
  60. package/src/signal/render.ts +178 -0
  61. package/src/theme/detect.ts +48 -0
  62. package/src/theme/tokens/index.ts +9 -0
  63. package/src/theme/tokens/mapping.ts +62 -0
  64. package/src/theme/tokens/types.ts +39 -0
@@ -0,0 +1,555 @@
1
+ /**
2
+ * structural-presets.ts
3
+ * ---------------------------------------------------------------------------
4
+ * Ten signature structural presets for Wishcraft vNext. Each preset is a full
5
+ * design system: tokens, chrome, signal grammar, motion refs, deck chrome,
6
+ * welcome screen, and glyph pairs (Nerd + ASCII).
7
+ * ---------------------------------------------------------------------------
8
+ */
9
+
10
+ import type {
11
+ ChromeSpec,
12
+ DeckSpec,
13
+ GlyphSet,
14
+ MotionRef,
15
+ SignalSpec,
16
+ StructuralPresetDef,
17
+ StructuralPresetName,
18
+ WelcomeSpec,
19
+ WishcraftTokens,
20
+ } from "./types.ts";
21
+ import { STRUCTURAL_PRESET_NAMES } from "./types.ts";
22
+
23
+ export { STRUCTURAL_PRESET_NAMES };
24
+ import type { MotionEvent } from "../motion/types.ts";
25
+ import { defaultMotionFor } from "../motion/catalog.ts";
26
+
27
+ function tokens(partial: Partial<WishcraftTokens>): Partial<WishcraftTokens> {
28
+ return partial;
29
+ }
30
+
31
+ function motion(
32
+ signature: MotionRef,
33
+ overrides?: Partial<Record<MotionEvent, MotionRef>>,
34
+ ): Partial<Record<MotionEvent, MotionRef>> {
35
+ const base: Partial<Record<MotionEvent, MotionRef>> = {
36
+ idle: "wisp",
37
+ thinking: signature,
38
+ streaming: signature,
39
+ "tool.start": signature,
40
+ "tool.end": "rune-bloom",
41
+ "idea.capture": "rune-bloom",
42
+ "skill.insert": "rune-bloom",
43
+ "policy.deny": "rune-bloom",
44
+ repair: signature,
45
+ compact: "bar",
46
+ success: "rune-bloom",
47
+ warning: "rune-bloom",
48
+ error: "rune-bloom",
49
+ };
50
+ return { ...base, ...overrides };
51
+ }
52
+
53
+ const ROUNDED_CHROME: ChromeSpec = {
54
+ frame: "rounded",
55
+ corners: { tl: "╭", tr: "╮", bl: "╰", br: "╯" },
56
+ dividers: { horizontal: "─", vertical: "│", cross: "┼" },
57
+ density: "medium",
58
+ };
59
+
60
+ const MINIMAL_CHROME: ChromeSpec = {
61
+ frame: "minimal",
62
+ corners: { tl: " ", tr: " ", bl: " ", br: " " },
63
+ dividers: { horizontal: "─", vertical: " ", cross: " " },
64
+ density: "spacious",
65
+ };
66
+
67
+ const BORDERLESS_CHROME: ChromeSpec = {
68
+ frame: "borderless",
69
+ corners: { tl: "", tr: "", bl: "", br: "" },
70
+ dividers: { horizontal: "─", vertical: " ", cross: " " },
71
+ density: "spacious",
72
+ };
73
+
74
+ const DEFAULT_DECK: DeckSpec = {
75
+ navigation: "tabs",
76
+ panelStyle: "framed",
77
+ activityStyle: "pulse",
78
+ };
79
+
80
+ const DEFAULT_WELCOME: WelcomeSpec = {
81
+ lantern: true,
82
+ ambient: true,
83
+ };
84
+
85
+ export const STRUCTURAL_PRESETS: Record<StructuralPresetName, StructuralPresetDef> = {
86
+ lanternwake: {
87
+ name: "lanternwake",
88
+ displayName: "Lanternwake",
89
+ description: "Warm amber embers in a dark room — Wishcraft's signature lantern identity.",
90
+ tokens: tokens({
91
+ surface: "#0f172a",
92
+ surfaceRaised: "#1e293b",
93
+ text: "#f8fafc",
94
+ textMuted: "#94a3b8",
95
+ primary: "#f59e0b",
96
+ secondary: "#d97706",
97
+ accent: "#ea580c",
98
+ success: "#22c55e",
99
+ warning: "#fbbf24",
100
+ error: "#ef4444",
101
+ focus: "#fbbf24",
102
+ selection: "#334155",
103
+ motionDim: "#78350f",
104
+ motionHot: "#fbbf24",
105
+ motionTrail: "#92400e",
106
+ }),
107
+ chrome: ROUNDED_CHROME,
108
+ signal: {
109
+ layout: "fluid",
110
+ separators: { left: "━╾", right: "╼━", subLeft: "✦", subRight: "✦" },
111
+ caps: { leftOpen: "◇", leftClose: "◇", rightOpen: "✦", rightClose: "✦" },
112
+ animation: "ember-relay",
113
+ },
114
+ motion: motion("ember-relay"),
115
+ deck: DEFAULT_DECK,
116
+ welcome: { lantern: true, ambient: true, motionId: "ember-relay" },
117
+ glyphs: {
118
+ mode: "auto",
119
+ model: "◇",
120
+ modelAscii: "*",
121
+ segment: "✦",
122
+ segmentAscii: "*",
123
+ },
124
+ },
125
+
126
+ threadbound: {
127
+ name: "threadbound",
128
+ displayName: "Threadbound",
129
+ description: "Woven craft aesthetic with indigo thread and travelling stitch knots.",
130
+ tokens: tokens({
131
+ surface: "#18181b",
132
+ surfaceRaised: "#27272a",
133
+ text: "#fafafa",
134
+ textMuted: "#a1a1aa",
135
+ primary: "#6366f1",
136
+ secondary: "#818cf8",
137
+ accent: "#ec4899",
138
+ success: "#34d399",
139
+ warning: "#fbbf24",
140
+ error: "#f87171",
141
+ focus: "#a5b4fc",
142
+ selection: "#3f3f46",
143
+ motionDim: "#312e81",
144
+ motionHot: "#ec4899",
145
+ motionTrail: "#4c1d95",
146
+ }),
147
+ chrome: {
148
+ frame: "minimal",
149
+ corners: { tl: "┌", tr: "┐", bl: "└", br: "┘" },
150
+ dividers: { horizontal: "─", vertical: "│", cross: "┼" },
151
+ density: "compact",
152
+ },
153
+ signal: {
154
+ layout: "woven",
155
+ separators: { left: "──╼", right: "╾──", subLeft: "·", subRight: "·" },
156
+ caps: { leftOpen: "╼", leftClose: "╾", rightOpen: "◆", rightClose: "◆" },
157
+ animation: "stitch-travel",
158
+ },
159
+ motion: motion("stitch-travel"),
160
+ deck: { ...DEFAULT_DECK, panelStyle: "inset" },
161
+ welcome: { lantern: false, ambient: true, motionId: "stitch-travel" },
162
+ glyphs: {
163
+ mode: "auto",
164
+ model: "╼",
165
+ modelAscii: "-",
166
+ segment: "·",
167
+ segmentAscii: ".",
168
+ },
169
+ },
170
+
171
+ scryglass: {
172
+ name: "scryglass",
173
+ displayName: "Scryglass",
174
+ description: "Refractive glass capsules with prism cyan and lens violet.",
175
+ tokens: tokens({
176
+ surface: "#090d16",
177
+ surfaceRaised: "#111827",
178
+ text: "#e0f2fe",
179
+ textMuted: "#67e8f9",
180
+ primary: "#06b6d4",
181
+ secondary: "#22d3ee",
182
+ accent: "#8b5cf6",
183
+ success: "#2dd4bf",
184
+ warning: "#fbbf24",
185
+ error: "#fb7185",
186
+ focus: "#67e8f9",
187
+ selection: "#1e3a5f",
188
+ motionDim: "#164e63",
189
+ motionHot: "#22d3ee",
190
+ motionTrail: "#312e81",
191
+ }),
192
+ chrome: {
193
+ frame: "rounded",
194
+ corners: { tl: "╭", tr: "╮", bl: "╰", br: "╯" },
195
+ dividers: { horizontal: "─", vertical: "│", cross: "┼" },
196
+ density: "medium",
197
+ },
198
+ signal: {
199
+ layout: "capsule",
200
+ separators: { left: "────", right: "────" },
201
+ caps: { leftOpen: "╭", leftClose: "╮", rightOpen: "╰", rightClose: "╯" },
202
+ animation: "refraction-sweep",
203
+ },
204
+ motion: motion("refraction-sweep"),
205
+ deck: DEFAULT_DECK,
206
+ welcome: { lantern: false, ambient: true, motionId: "refraction-sweep" },
207
+ glyphs: {
208
+ mode: "auto",
209
+ model: "◈",
210
+ modelAscii: "O",
211
+ segment: "◇",
212
+ segmentAscii: "o",
213
+ },
214
+ },
215
+
216
+ runebloom: {
217
+ name: "runebloom",
218
+ displayName: "Runebloom",
219
+ description: "Organic alchemical sigils with sparse anchors and event bloom.",
220
+ tokens: tokens({
221
+ surface: "#0c140f",
222
+ surfaceRaised: "#14261a",
223
+ text: "#ecfdf5",
224
+ textMuted: "#6ee7b7",
225
+ primary: "#10b981",
226
+ secondary: "#34d399",
227
+ accent: "#eab308",
228
+ success: "#22c55e",
229
+ warning: "#f59e0b",
230
+ error: "#ef4444",
231
+ focus: "#fde047",
232
+ selection: "#14532d",
233
+ motionDim: "#064e3b",
234
+ motionHot: "#eab308",
235
+ motionTrail: "#166534",
236
+ }),
237
+ chrome: MINIMAL_CHROME,
238
+ signal: {
239
+ layout: "sparse",
240
+ separators: { left: " · ", right: " · " },
241
+ caps: { leftOpen: "◇", leftClose: "◇" },
242
+ animation: "rune-bloom",
243
+ },
244
+ motion: motion("rune-bloom", { thinking: "rune-bloom", streaming: "rune-bloom" }),
245
+ deck: { ...DEFAULT_DECK, activityStyle: "none" },
246
+ welcome: { lantern: true, ambient: false, motionId: "rune-bloom" },
247
+ glyphs: {
248
+ mode: "auto",
249
+ model: "◇",
250
+ modelAscii: "+",
251
+ segment: "·",
252
+ segmentAscii: ".",
253
+ },
254
+ },
255
+
256
+ moonwell: {
257
+ name: "moonwell",
258
+ displayName: "Moonwell",
259
+ description: "Nocturnal sanctuary with silver arcs and orbital breathe.",
260
+ tokens: tokens({
261
+ surface: "#020617",
262
+ surfaceRaised: "#0f172a",
263
+ text: "#e2e8f0",
264
+ textMuted: "#94a3b8",
265
+ primary: "#94a3b8",
266
+ secondary: "#cbd5e1",
267
+ accent: "#38bdf8",
268
+ success: "#4ade80",
269
+ warning: "#fbbf24",
270
+ error: "#f87171",
271
+ focus: "#7dd3fc",
272
+ selection: "#1e293b",
273
+ motionDim: "#1e3a5f",
274
+ motionHot: "#38bdf8",
275
+ motionTrail: "#334155",
276
+ }),
277
+ chrome: ROUNDED_CHROME,
278
+ signal: {
279
+ layout: "arc",
280
+ separators: { left: "───", right: "───", subLeft: "◜", subRight: "◟" },
281
+ caps: { leftOpen: "◜", leftClose: "◝", rightOpen: "◞", rightClose: "◟" },
282
+ animation: "lunar-breathe",
283
+ },
284
+ motion: motion("lunar-breathe"),
285
+ deck: DEFAULT_DECK,
286
+ welcome: { lantern: true, ambient: true, motionId: "lunar-breathe" },
287
+ glyphs: {
288
+ mode: "auto",
289
+ model: "◌",
290
+ modelAscii: "O",
291
+ segment: "◦",
292
+ segmentAscii: "o",
293
+ },
294
+ },
295
+
296
+ hexforge: {
297
+ name: "hexforge",
298
+ displayName: "Hexforge",
299
+ description: "Industrial forge with block hexagons and heat propagation.",
300
+ tokens: tokens({
301
+ surface: "#1c1917",
302
+ surfaceRaised: "#292524",
303
+ text: "#fafaf9",
304
+ textMuted: "#a8a29e",
305
+ primary: "#f97316",
306
+ secondary: "#fb923c",
307
+ accent: "#ef4444",
308
+ success: "#84cc16",
309
+ warning: "#fbbf24",
310
+ error: "#dc2626",
311
+ focus: "#fdba74",
312
+ selection: "#44403c",
313
+ motionDim: "#7c2d12",
314
+ motionHot: "#ef4444",
315
+ motionTrail: "#9a3412",
316
+ }),
317
+ chrome: {
318
+ frame: "square",
319
+ corners: { tl: "┏", tr: "┓", bl: "┗", br: "┛" },
320
+ dividers: { horizontal: "━", vertical: "┃", cross: "╋" },
321
+ density: "compact",
322
+ },
323
+ signal: {
324
+ layout: "block",
325
+ separators: { left: "▰▰", right: "▰▰" },
326
+ caps: { leftOpen: "⬡", leftClose: "⬡", rightOpen: "◆", rightClose: "◆" },
327
+ animation: "hex-relay",
328
+ },
329
+ motion: motion("hex-relay"),
330
+ deck: { navigation: "rail", panelStyle: "framed", activityStyle: "bar" },
331
+ welcome: { lantern: false, ambient: false, motionId: "hex-relay" },
332
+ glyphs: {
333
+ mode: "auto",
334
+ model: "⬡",
335
+ modelAscii: "#",
336
+ segment: "█",
337
+ segmentAscii: "#",
338
+ },
339
+ },
340
+
341
+ vellum: {
342
+ name: "vellum",
343
+ displayName: "Vellum",
344
+ description: "Editorial grimoire with parchment ink and writing reveal.",
345
+ tokens: tokens({
346
+ surface: "#1a1815",
347
+ surfaceRaised: "#292524",
348
+ text: "#faf5eb",
349
+ textMuted: "#d6d3d1",
350
+ primary: "#d97706",
351
+ secondary: "#b45309",
352
+ accent: "#a16207",
353
+ success: "#65a30d",
354
+ warning: "#ca8a04",
355
+ error: "#b91c1c",
356
+ focus: "#fbbf24",
357
+ selection: "#44403c",
358
+ motionDim: "#57534e",
359
+ motionHot: "#d97706",
360
+ motionTrail: "#78716c",
361
+ }),
362
+ chrome: BORDERLESS_CHROME,
363
+ signal: {
364
+ layout: "editorial",
365
+ separators: { left: "╾", right: "╼" },
366
+ caps: {},
367
+ animation: "writing-reveal",
368
+ },
369
+ motion: motion("writing-reveal"),
370
+ deck: { navigation: "minimal", panelStyle: "borderless", activityStyle: "none" },
371
+ welcome: { lantern: true, ambient: false, motionId: "writing-reveal" },
372
+ glyphs: {
373
+ mode: "auto",
374
+ model: "",
375
+ modelAscii: "",
376
+ segment: "·",
377
+ segmentAscii: ".",
378
+ },
379
+ },
380
+
381
+ wisp: {
382
+ name: "wisp",
383
+ displayName: "Wisp",
384
+ description: "Ethereal minimal whitespace with phase drift ambient motion.",
385
+ tokens: tokens({
386
+ surface: "#0b0f19",
387
+ surfaceRaised: "#111827",
388
+ text: "#e2e8f0",
389
+ textMuted: "#94a3b8",
390
+ primary: "#cbd5e1",
391
+ secondary: "#94a3b8",
392
+ accent: "#38bdf8",
393
+ success: "#4ade80",
394
+ warning: "#fbbf24",
395
+ error: "#f87171",
396
+ focus: "#7dd3fc",
397
+ selection: "#1e293b",
398
+ motionDim: "#1e293b",
399
+ motionHot: "#38bdf8",
400
+ motionTrail: "#334155",
401
+ }),
402
+ chrome: MINIMAL_CHROME,
403
+ signal: {
404
+ layout: "sparse",
405
+ separators: { left: " ", right: " · " },
406
+ caps: { leftOpen: "◌", leftClose: "◌" },
407
+ animation: "wisp",
408
+ },
409
+ motion: motion("wisp", {
410
+ thinking: "wisp",
411
+ streaming: "wisp",
412
+ "tool.start": "wisp",
413
+ }),
414
+ deck: { navigation: "minimal", panelStyle: "borderless", activityStyle: "none" },
415
+ welcome: { lantern: false, ambient: true, motionId: "wisp" },
416
+ glyphs: {
417
+ mode: "auto",
418
+ model: "◌",
419
+ modelAscii: "o",
420
+ segment: "·",
421
+ segmentAscii: ".",
422
+ },
423
+ },
424
+
425
+ starweave: {
426
+ name: "starweave",
427
+ displayName: "Starweave",
428
+ description: "Celestial cartography with constellation nodes and path traversal.",
429
+ tokens: tokens({
430
+ surface: "#050515",
431
+ surfaceRaised: "#0f0a1e",
432
+ text: "#ede9fe",
433
+ textMuted: "#c4b5fd",
434
+ primary: "#a855f7",
435
+ secondary: "#c084fc",
436
+ accent: "#38bdf8",
437
+ success: "#34d399",
438
+ warning: "#fbbf24",
439
+ error: "#fb7185",
440
+ focus: "#e879f9",
441
+ selection: "#2e1065",
442
+ motionDim: "#4c1d95",
443
+ motionHot: "#38bdf8",
444
+ motionTrail: "#6b21a8",
445
+ }),
446
+ chrome: ROUNDED_CHROME,
447
+ signal: {
448
+ layout: "constellation",
449
+ separators: { left: "───", right: "───", subLeft: "✦", subRight: "✦" },
450
+ caps: { leftOpen: "✦", leftClose: "✦", rightOpen: "◆", rightClose: "◆" },
451
+ animation: "path-traverse",
452
+ },
453
+ motion: motion("path-traverse"),
454
+ deck: DEFAULT_DECK,
455
+ welcome: { lantern: false, ambient: true, motionId: "path-traverse" },
456
+ glyphs: {
457
+ mode: "auto",
458
+ model: "✦",
459
+ modelAscii: "*",
460
+ segment: "◆",
461
+ segmentAscii: "*",
462
+ },
463
+ },
464
+
465
+ crucible: {
466
+ name: "crucible",
467
+ displayName: "Crucible",
468
+ description: "Alchemical fluid cells with liquid level rise on activity.",
469
+ tokens: tokens({
470
+ surface: "#110b11",
471
+ surfaceRaised: "#1f1520",
472
+ text: "#fce7f3",
473
+ textMuted: "#f9a8d4",
474
+ primary: "#ec4899",
475
+ secondary: "#f472b6",
476
+ accent: "#f43f5e",
477
+ success: "#4ade80",
478
+ warning: "#fbbf24",
479
+ error: "#e11d48",
480
+ focus: "#fb7185",
481
+ selection: "#4a044e",
482
+ motionDim: "#831843",
483
+ motionHot: "#f43f5e",
484
+ motionTrail: "#9d174d",
485
+ }),
486
+ chrome: {
487
+ frame: "double",
488
+ corners: { tl: "╔", tr: "╗", bl: "╚", br: "╝" },
489
+ dividers: { horizontal: "═", vertical: "║", cross: "╬" },
490
+ density: "medium",
491
+ },
492
+ signal: {
493
+ layout: "cell",
494
+ separators: { left: "]", right: "[" },
495
+ caps: { leftOpen: "[", leftClose: "]", rightOpen: "[", rightClose: "]" },
496
+ animation: "liquid-rise",
497
+ },
498
+ motion: motion("liquid-rise"),
499
+ deck: { ...DEFAULT_DECK, activityStyle: "bar" },
500
+ welcome: { lantern: false, ambient: true, motionId: "liquid-rise" },
501
+ glyphs: {
502
+ mode: "auto",
503
+ model: "▓",
504
+ modelAscii: "=",
505
+ segment: "░",
506
+ segmentAscii: ".",
507
+ },
508
+ },
509
+ };
510
+
511
+ export function isStructuralPresetName(name: string): name is StructuralPresetName {
512
+ return (STRUCTURAL_PRESET_NAMES as readonly string[]).includes(name);
513
+ }
514
+
515
+ export function getStructuralPreset(name: StructuralPresetName): StructuralPresetDef {
516
+ return STRUCTURAL_PRESETS[name];
517
+ }
518
+
519
+ /** Display name for a stored base, or the raw value when it is not structural. */
520
+ export function appearanceDisplayName(name: string): string {
521
+ return isStructuralPresetName(name) ? getStructuralPreset(name).displayName : name;
522
+ }
523
+
524
+ /** Signature motion id for a structural preset (streaming / thinking lane). */
525
+ export function signatureMotionFor(name: StructuralPresetName): MotionRef {
526
+ return STRUCTURAL_PRESETS[name].signal.animation;
527
+ }
528
+
529
+ /** Fill missing motion events with catalog defaults. */
530
+ export function completeMotionMap(
531
+ partial: Partial<Record<MotionEvent, MotionRef>>,
532
+ ): Partial<Record<MotionEvent, MotionRef>> {
533
+ const complete: Partial<Record<MotionEvent, MotionRef>> = { ...partial };
534
+ const events: MotionEvent[] = [
535
+ "idle",
536
+ "thinking",
537
+ "streaming",
538
+ "tool.start",
539
+ "tool.end",
540
+ "idea.capture",
541
+ "skill.insert",
542
+ "policy.deny",
543
+ "repair",
544
+ "compact",
545
+ "success",
546
+ "warning",
547
+ "error",
548
+ ];
549
+ for (const event of events) {
550
+ if (complete[event] === undefined) {
551
+ complete[event] = defaultMotionFor(event);
552
+ }
553
+ }
554
+ return complete;
555
+ }
@@ -0,0 +1,154 @@
1
+ /**
2
+ * tokens.ts
3
+ * ---------------------------------------------------------------------------
4
+ * Semantic design tokens for Wishcraft vNext.
5
+ *
6
+ * The existing ColorScheme is segment-shaped: `model`, `gitDirty`, `context`,
7
+ * `cost`. That ties colors to concrete segments, so a new surface (Deck, Signal
8
+ * lanes, motion trails) has nowhere to get its color from except a fresh hex
9
+ * literal. Tokens invert that: a preset fills abstract roles, and the legacy
10
+ * semantic colors are derived from them.
11
+ *
12
+ * Existing presets keep working unchanged. `DEFAULT_TOKENS` is chosen so that
13
+ * `colorSchemeFromTokens(DEFAULT_TOKENS)` equals `getDefaultColors()`, and any
14
+ * explicit `colors` a preset already declares still wins over the derived value.
15
+ * ---------------------------------------------------------------------------
16
+ */
17
+
18
+ import type {
19
+ ColorScheme,
20
+ ColorValue,
21
+ PresetDef,
22
+ SemanticColor,
23
+ TokenRole,
24
+ WishcraftTokens,
25
+ } from "./types.ts";
26
+
27
+ export type { TokenRole, WishcraftTokens };
28
+
29
+ /**
30
+ * Which token each legacy semantic color reads from.
31
+ *
32
+ * `thinking*` is deliberately absent: those four are Pi's own thinking-level
33
+ * colors, not part of the Wishcraft palette, so they pass through untouched.
34
+ */
35
+ export const SEMANTIC_TOKEN_ROLE: Record<
36
+ Exclude<SemanticColor, "thinking" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium">,
37
+ TokenRole
38
+ > = {
39
+ model: "primary",
40
+ shellMode: "accent",
41
+ path: "secondary",
42
+ gitDirty: "warning",
43
+ gitClean: "success",
44
+ context: "motionDim",
45
+ contextWarn: "warning",
46
+ contextError: "error",
47
+ cost: "text",
48
+ tokens: "textMuted",
49
+ queue: "accent",
50
+ separator: "motionDim",
51
+ border: "surfaceRaised",
52
+ };
53
+
54
+ /** Pi thinking-level colors, passed through rather than tokenised. */
55
+ export const THINKING_COLORS: Pick<
56
+ Required<ColorScheme>,
57
+ "thinking" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium"
58
+ > = {
59
+ thinking: "thinkingOff",
60
+ thinkingMinimal: "thinkingMinimal",
61
+ thinkingLow: "thinkingLow",
62
+ thinkingMedium: "thinkingMedium",
63
+ };
64
+
65
+ /**
66
+ * Token values that reproduce today's default color scheme exactly, so
67
+ * switching a preset to tokens is not a visual change.
68
+ */
69
+ export const DEFAULT_TOKENS: WishcraftTokens = {
70
+ surface: "background" as ColorValue,
71
+ surfaceRaised: "borderMuted",
72
+ text: "text",
73
+ textMuted: "muted",
74
+ primary: "#d787af",
75
+ secondary: "#00afaf",
76
+ accent: "accent",
77
+ success: "success",
78
+ warning: "warning",
79
+ error: "error",
80
+ focus: "borderAccent",
81
+ selection: "borderMuted",
82
+ motionDim: "dim",
83
+ motionHot: "accent",
84
+ motionTrail: "muted",
85
+ };
86
+
87
+ /** Derive the full legacy color scheme from a token set. */
88
+ export function colorSchemeFromTokens(tokens: WishcraftTokens): Required<ColorScheme> {
89
+ const derived = {} as Required<ColorScheme>;
90
+ for (const [semantic, role] of Object.entries(SEMANTIC_TOKEN_ROLE) as Array<
91
+ [keyof typeof SEMANTIC_TOKEN_ROLE, TokenRole]
92
+ >) {
93
+ derived[semantic] = tokens[role];
94
+ }
95
+ return { ...derived, ...THINKING_COLORS };
96
+ }
97
+
98
+ /**
99
+ * Colors for a preset that declares tokens. Explicit `colors` entries win, so a
100
+ * preset can tokenise its palette and still pin one segment by hand.
101
+ */
102
+ export function mergeTokenColors(
103
+ tokens: WishcraftTokens,
104
+ overrides?: ColorScheme,
105
+ ): Required<ColorScheme> {
106
+ const derived = colorSchemeFromTokens(tokens);
107
+ if (!overrides) return derived;
108
+
109
+ const merged = { ...derived };
110
+ for (const key of Object.keys(overrides) as SemanticColor[]) {
111
+ const value = overrides[key];
112
+ if (value !== undefined) merged[key] = value;
113
+ }
114
+ return merged;
115
+ }
116
+
117
+ /**
118
+ * Colors a preset should render with.
119
+ *
120
+ * Presets that declare tokens get their scheme derived from them; presets that
121
+ * predate tokens keep their explicit `colors` (or the default scheme) untouched.
122
+ */
123
+ export function presetColorScheme(
124
+ preset: Pick<PresetDef, "colors" | "tokens">,
125
+ defaults: () => Required<ColorScheme>,
126
+ ): ColorScheme {
127
+ if (preset.tokens) {
128
+ return mergeTokenColors(resolveTokens(preset.tokens), preset.colors);
129
+ }
130
+ return preset.colors ?? defaults();
131
+ }
132
+
133
+ /** Read one token, falling back to the default palette. */
134
+ export function token(
135
+ role: TokenRole,
136
+ tokens?: Partial<WishcraftTokens>,
137
+ ): ColorValue {
138
+ return tokens?.[role] ?? DEFAULT_TOKENS[role];
139
+ }
140
+
141
+ /** Fill the gaps in a partial token set. */
142
+ export function resolveTokens(partial?: Partial<WishcraftTokens>): WishcraftTokens {
143
+ if (!partial) return { ...DEFAULT_TOKENS };
144
+ return { ...DEFAULT_TOKENS, ...stripUndefined(partial) };
145
+ }
146
+
147
+ function stripUndefined(partial: Partial<WishcraftTokens>): Partial<WishcraftTokens> {
148
+ const clean: Partial<WishcraftTokens> = {};
149
+ for (const key of Object.keys(partial) as TokenRole[]) {
150
+ const value = partial[key];
151
+ if (value !== undefined) clean[key] = value;
152
+ }
153
+ return clean;
154
+ }