@toolr/ui-design 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 (112) hide show
  1. package/README.md +63 -0
  2. package/components/content/info-panel-primitives.tsx +297 -0
  3. package/components/diagrams/diagram-utils.tsx +908 -0
  4. package/components/hooks/use-click-outside.ts +27 -0
  5. package/components/hooks/use-dropdown-max-height.ts +20 -0
  6. package/components/hooks/use-navigation-history.ts +94 -0
  7. package/components/lib/ai-tools.tsx +44 -0
  8. package/components/lib/cn.ts +6 -0
  9. package/components/lib/form-colors.ts +32 -0
  10. package/components/lib/theme-engine.ts +97 -0
  11. package/components/lib/toolr-brand.tsx +31 -0
  12. package/components/sections/ai-tools-paths/index.ts +37 -0
  13. package/components/sections/ai-tools-paths/tools-paths-panel.tsx +212 -0
  14. package/components/sections/ai-tools-paths/types.ts +111 -0
  15. package/components/sections/ai-tools-paths/use-tools-paths.ts +159 -0
  16. package/components/sections/captured-issues/captured-issues-panel.tsx +214 -0
  17. package/components/sections/captured-issues/index.ts +38 -0
  18. package/components/sections/captured-issues/types.ts +113 -0
  19. package/components/sections/captured-issues/use-captured-issues.ts +111 -0
  20. package/components/sections/golden-snapshots/file-diff-viewer.tsx +420 -0
  21. package/components/sections/golden-snapshots/golden-sync-panel.tsx +223 -0
  22. package/components/sections/golden-snapshots/index.ts +145 -0
  23. package/components/sections/golden-snapshots/snapshot-manager.tsx +200 -0
  24. package/components/sections/golden-snapshots/status-overview.tsx +305 -0
  25. package/components/sections/golden-snapshots/types.ts +288 -0
  26. package/components/sections/golden-snapshots/use-golden-sync.ts +477 -0
  27. package/components/sections/golden-snapshots/version-manager.tsx +186 -0
  28. package/components/sections/prompt-editor/file-type-tabbed-prompt-editor.tsx +210 -0
  29. package/components/sections/prompt-editor/index.ts +121 -0
  30. package/components/sections/prompt-editor/simulator-prompt-editor.tsx +276 -0
  31. package/components/sections/prompt-editor/tabbed-prompt-editor.tsx +514 -0
  32. package/components/sections/prompt-editor/types.ts +101 -0
  33. package/components/sections/prompt-editor/use-prompt-editor.ts +131 -0
  34. package/components/sections/report-bug/error-logger.ts +392 -0
  35. package/components/sections/report-bug/index.ts +59 -0
  36. package/components/sections/report-bug/issue-reporter-api.ts +83 -0
  37. package/components/sections/report-bug/report-bug-form.tsx +282 -0
  38. package/components/sections/report-bug/screenshot-uploader.tsx +228 -0
  39. package/components/sections/report-bug/use-report-bug.ts +170 -0
  40. package/components/sections/snapshot-browser/index.ts +53 -0
  41. package/components/sections/snapshot-browser/snapshot-browser-panel.tsx +147 -0
  42. package/components/sections/snapshot-browser/snapshot-tree.tsx +451 -0
  43. package/components/sections/snapshot-browser/types.ts +106 -0
  44. package/components/sections/snapshot-browser/use-snapshot-browser.ts +125 -0
  45. package/components/sections/snippets-editor/index.ts +31 -0
  46. package/components/sections/snippets-editor/snippets-editor.tsx +381 -0
  47. package/components/sections/snippets-editor/types.ts +48 -0
  48. package/components/sections/snippets-editor/use-snippets-editor.ts +217 -0
  49. package/components/ui/action-dialog.tsx +309 -0
  50. package/components/ui/ai-action-button.tsx +137 -0
  51. package/components/ui/ai-execution-action-buttons.tsx +106 -0
  52. package/components/ui/badge.tsx +67 -0
  53. package/components/ui/bottom-panel-header.tsx +240 -0
  54. package/components/ui/breadcrumb.tsx +168 -0
  55. package/components/ui/checkbox.tsx +102 -0
  56. package/components/ui/collapsible-section.tsx +100 -0
  57. package/components/ui/confirm-badge.tsx +71 -0
  58. package/components/ui/detail-section.tsx +67 -0
  59. package/components/ui/detail-view-wrapper.tsx +55 -0
  60. package/components/ui/editor-placeholder-card.tsx +197 -0
  61. package/components/ui/editor-toolbar.tsx +123 -0
  62. package/components/ui/execution-details-panel.tsx +93 -0
  63. package/components/ui/extension-list-card.tsx +105 -0
  64. package/components/ui/file-structure-section.tsx +373 -0
  65. package/components/ui/file-tree.tsx +171 -0
  66. package/components/ui/files-panel.tsx +251 -0
  67. package/components/ui/filter-dropdown.tsx +173 -0
  68. package/components/ui/form-actions.tsx +127 -0
  69. package/components/ui/frontmatter-form-header.tsx +80 -0
  70. package/components/ui/icon-button.tsx +388 -0
  71. package/components/ui/input.tsx +211 -0
  72. package/components/ui/label.tsx +159 -0
  73. package/components/ui/layout-tab-bar.tsx +289 -0
  74. package/components/ui/modal.tsx +194 -0
  75. package/components/ui/nav-card.tsx +81 -0
  76. package/components/ui/navigation-bar.tsx +285 -0
  77. package/components/ui/number-input.tsx +165 -0
  78. package/components/ui/registry-browser.tsx +261 -0
  79. package/components/ui/registry-card.tsx +710 -0
  80. package/components/ui/registry-detail.tsx +224 -0
  81. package/components/ui/resizable-textarea.tsx +290 -0
  82. package/components/ui/scope-badge.tsx +67 -0
  83. package/components/ui/segmented-toggle.tsx +133 -0
  84. package/components/ui/select.tsx +172 -0
  85. package/components/ui/selection-grid.tsx +313 -0
  86. package/components/ui/setting-row.tsx +97 -0
  87. package/components/ui/snapshot-card.tsx +107 -0
  88. package/components/ui/snippets-panel.tsx +161 -0
  89. package/components/ui/sort-dropdown.tsx +109 -0
  90. package/components/ui/status-card.tsx +96 -0
  91. package/components/ui/tab-bar.tsx +340 -0
  92. package/components/ui/toggle.tsx +142 -0
  93. package/components/ui/tooltip.tsx +326 -0
  94. package/dist/content.d.ts +110 -0
  95. package/dist/content.js +195 -0
  96. package/dist/diagrams.d.ts +371 -0
  97. package/dist/diagrams.js +702 -0
  98. package/dist/index.d.ts +2714 -0
  99. package/dist/index.js +11220 -0
  100. package/dist/preset.d.ts +24 -0
  101. package/dist/preset.js +17 -0
  102. package/dist/tokens/tokens/primitives.css +45 -0
  103. package/dist/tokens/tokens/semantic.css +46 -0
  104. package/dist/tokens/tokens/theme.css +11 -0
  105. package/dist/tokens/tokens/tokens.json +65 -0
  106. package/index.ts +123 -0
  107. package/package.json +63 -0
  108. package/tailwind-preset.ts +22 -0
  109. package/tokens/primitives.css +45 -0
  110. package/tokens/semantic.css +46 -0
  111. package/tokens/theme.css +11 -0
  112. package/tokens/tokens.json +65 -0
@@ -0,0 +1,908 @@
1
+ /* eslint-disable react-refresh/only-export-components */
2
+ /**
3
+ * Shared Motion + Visx utilities for diagram components.
4
+ *
5
+ * Replaces the per-file CSS keyframe animations, `useDelayedMount`, and
6
+ * `DiagramWrapper` boilerplate with declarative Motion variants.
7
+ *
8
+ * Usage:
9
+ * import { DiagramWrapper, stagger, item, dashItem, MAnimatedPath } from './diagram-utils'
10
+ *
11
+ * <DiagramWrapper>
12
+ * <svg ...>
13
+ * <motion.g variants={stagger()}>
14
+ * <motion.g variants={item}>...</motion.g>
15
+ * <motion.g variants={item}>...</motion.g>
16
+ * </motion.g>
17
+ * </svg>
18
+ * </DiagramWrapper>
19
+ */
20
+
21
+ import type { ReactNode } from 'react'
22
+ import { motion, type Variants } from 'motion/react'
23
+
24
+ // ============================================================================
25
+ // Design Tokens – single source of truth for all diagram dimensions & styles
26
+ // ============================================================================
27
+
28
+ /** Standard viewBox width for all diagrams. */
29
+ const VB_W = 420
30
+
31
+ /** ViewBox height tiers. Pick the smallest that fits your content. */
32
+ const VB_H = { compact: 200, standard: 240, tall: 280, extended: 340, large: 420, xlarge: 520, xxlarge: 580 } as const
33
+
34
+ /** Pre-built viewBox strings: `"0 0 420 <height>"` */
35
+ export const viewBox = {
36
+ compact: `0 0 ${VB_W} ${VB_H.compact}`,
37
+ standard: `0 0 ${VB_W} ${VB_H.standard}`,
38
+ tall: `0 0 ${VB_W} ${VB_H.tall}`,
39
+ extended: `0 0 ${VB_W} ${VB_H.extended}`,
40
+ large: `0 0 ${VB_W} ${VB_H.large}`,
41
+ xlarge: `0 0 ${VB_W} ${VB_H.xlarge}`,
42
+ xxlarge: `0 0 ${VB_W} ${VB_H.xxlarge}`,
43
+ } as const
44
+
45
+ /** Font size tiers (SVG units). 1 SVG unit ≈ 3px at max-width 1260px (3×420). */
46
+ export const font = {
47
+ icon: 10, // 30px at 3× scale – decorative icon-text inside circles
48
+ primary: 5.33, // 16px at 3× scale
49
+ secondary: 4.67, // 14px at 3× scale
50
+ small: 4, // 12px at 3× scale
51
+ micro: 3, // 9px at 3× scale – constrained layouts (multi-line in tiny boxes)
52
+ } as const
53
+
54
+ /** Standard box dimensions [width, height] by role. */
55
+ export const box = {
56
+ nodeMain: { w: 120, h: 55 },
57
+ nodeCard: { w: 95, h: 50 },
58
+ nodeSmall: { w: 80, h: 32 },
59
+ infoBox: { w: 360, h: 28 },
60
+ codeBlock: { w: 360, h: 28 },
61
+ } as const
62
+
63
+ /** Spacing constants (SVG units). */
64
+ export const spacing = {
65
+ margin: 20,
66
+ gapSm: 10,
67
+ gapMd: 20,
68
+ gapLg: 35,
69
+ centerX: 210,
70
+ titleY: 18,
71
+ } as const
72
+
73
+ /** Border radius tiers. */
74
+ export const radius = { lg: 8, md: 6, sm: 4 } as const
75
+
76
+ /** Stroke width tiers. */
77
+ export const stroke = {
78
+ bold: 2,
79
+ normal: 1.5,
80
+ thin: 1,
81
+ hairline: 0.5,
82
+ } as const
83
+
84
+ /** Semantic text colors. */
85
+ export const color = {
86
+ textBright: '#e5e7eb',
87
+ textMedium: '#9ca3af',
88
+ textDim: '#6b7280',
89
+ bgDark: '#1f2937',
90
+ bgDarker: '#0f172a',
91
+ border: '#374151',
92
+ } as const
93
+
94
+ /** Dark tint backgrounds for colored overlays (use with fillOpacity). */
95
+ export const tint = {
96
+ red: '#7f1d1d',
97
+ emerald: '#064e3b',
98
+ blue: '#1e3a5f',
99
+ } as const
100
+
101
+ /** Accent palette – pick by semantic intent, not by hue. */
102
+ export const accent = {
103
+ cyan: '#22d3ee',
104
+ teal: '#2dd4bf',
105
+ blue: '#60a5fa',
106
+ purple: '#a78bfa',
107
+ violet: '#c084fc',
108
+ green: '#34d399',
109
+ lime: '#4ade80',
110
+ yellow: '#fbbf24',
111
+ orange: '#fb923c',
112
+ red: '#ef4444',
113
+ redLight: '#f87171',
114
+ pink: '#f472b6',
115
+ pinkLight: '#fda4af',
116
+ indigo: '#a5b4fc',
117
+ emerald: '#10b981',
118
+ amber: '#f59e0b',
119
+ fuchsia: '#d946ef',
120
+ rose: '#fb7185',
121
+ sky: '#38bdf8',
122
+ } as const
123
+
124
+ /**
125
+ * Convenience bundle for imports:
126
+ * import { D } from './diagram-utils'
127
+ * <rect rx={D.radius.md} strokeWidth={D.stroke.normal} />
128
+ */
129
+ export const D = {
130
+ viewBox,
131
+ font,
132
+ box,
133
+ spacing,
134
+ radius,
135
+ stroke,
136
+ color,
137
+ tint,
138
+ accent,
139
+ } as const
140
+
141
+ // ============================================================================
142
+ // DiagramWrapper – fade-in container (replaces per-file DiagramWrapper + <style>)
143
+ // ============================================================================
144
+
145
+ export function DiagramWrapper({ children }: { children: ReactNode; sourceUrl?: string }) {
146
+ return (
147
+ <motion.div
148
+ className="relative flex flex-col h-full"
149
+ initial={{ opacity: 0 }}
150
+ animate={{ opacity: 1 }}
151
+ transition={{ duration: 0.3, ease: 'easeOut' }}
152
+ >
153
+ <div className="flex-1 max-w-[1260px] mx-auto w-full">
154
+ {children}
155
+ </div>
156
+ </motion.div>
157
+ )
158
+ }
159
+
160
+ // ============================================================================
161
+ // Variants – declarative stagger & item animations
162
+ // ============================================================================
163
+
164
+ /**
165
+ * Container variant that staggers children.
166
+ * @param staggerDelay – seconds between each child (default 0.05)
167
+ * @param delayStart – initial delay before first child (default 0)
168
+ */
169
+ export function stagger(staggerDelay = 0.05, delayStart = 0): Variants {
170
+ return {
171
+ hidden: {},
172
+ visible: {
173
+ transition: {
174
+ delayChildren: delayStart,
175
+ staggerChildren: staggerDelay,
176
+ },
177
+ },
178
+ }
179
+ }
180
+
181
+ /** Standard fade-in-up item variant (replaces all FADE_IN_UP_* constants). */
182
+ export const item: Variants = {
183
+ hidden: { opacity: 0, y: 8 },
184
+ visible: {
185
+ opacity: 1,
186
+ y: 0,
187
+ transition: { duration: 0.35, ease: 'easeOut' },
188
+ },
189
+ }
190
+
191
+ /** Slide-in from left variant. */
192
+ export const slideInItem: Variants = {
193
+ hidden: { opacity: 0, x: -10 },
194
+ visible: {
195
+ opacity: 1,
196
+ x: 0,
197
+ transition: { duration: 0.35, ease: 'easeOut' },
198
+ },
199
+ }
200
+
201
+ /** Scale-in item variant (good for circles / icons). */
202
+ export const scaleItem: Variants = {
203
+ hidden: { opacity: 0, scale: 0.85 },
204
+ visible: {
205
+ opacity: 1,
206
+ scale: 1,
207
+ transition: { duration: 0.3, ease: 'easeOut' },
208
+ },
209
+ }
210
+
211
+ // ============================================================================
212
+ // Animated SVG dash path – replaces manual <animate> elements
213
+ // ============================================================================
214
+
215
+ /**
216
+ * An SVG <path> with an infinite animated dash offset.
217
+ * Replaces:
218
+ * <path strokeDasharray="4 3">
219
+ * <animate attributeName="stroke-dashoffset" values="0;-14" dur="1.2s" ... />
220
+ * </path>
221
+ */
222
+ export function AnimatedDashPath({
223
+ d,
224
+ stroke,
225
+ strokeWidth = 1.5,
226
+ dashArray = '4 3',
227
+ opacity = 0.6,
228
+ duration = 1.2,
229
+ delay = 0,
230
+ }: {
231
+ d: string
232
+ stroke: string
233
+ strokeWidth?: number
234
+ dashArray?: string
235
+ opacity?: number
236
+ duration?: number
237
+ delay?: number
238
+ }) {
239
+ return (
240
+ <motion.path
241
+ d={d}
242
+ stroke={stroke}
243
+ strokeWidth={strokeWidth}
244
+ strokeDasharray={dashArray}
245
+ opacity={opacity}
246
+ fill="none"
247
+ initial={{ strokeDashoffset: 0 }}
248
+ animate={{ strokeDashoffset: -14 }}
249
+ transition={{
250
+ duration,
251
+ delay,
252
+ ease: 'linear',
253
+ repeat: Infinity,
254
+ }}
255
+ />
256
+ )
257
+ }
258
+
259
+ // ============================================================================
260
+ // Reusable Diagram Primitives
261
+ // ============================================================================
262
+
263
+ /**
264
+ * Standard diagram title text rendered at top center.
265
+ * Every diagram should have exactly one of these.
266
+ */
267
+ export function DiagramTitle({ children, y = spacing.titleY }: { children: string; y?: number }) {
268
+ return (
269
+ <text
270
+ x={spacing.centerX}
271
+ y={y}
272
+ fill={color.textDim}
273
+ fontSize={font.primary}
274
+ textAnchor="middle"
275
+ fontWeight="600"
276
+ letterSpacing="0.5"
277
+ >
278
+ {children}
279
+ </text>
280
+ )
281
+ }
282
+
283
+ /**
284
+ * A rounded rectangle node with a heading and optional body text.
285
+ * Renders a filled rect with border, centered heading, and up to two body lines.
286
+ *
287
+ * Use explicit `headingY` / `bodyY` / `body2Y` to match hand-positioned layouts.
288
+ * When omitted, text positions are calculated from the box dimensions.
289
+ */
290
+ export function DiagramNode({
291
+ x,
292
+ y,
293
+ width,
294
+ height,
295
+ heading,
296
+ body,
297
+ body2,
298
+ accentColor,
299
+ rx = radius.md,
300
+ fill: bgFill = color.bgDark,
301
+ strokeWidth: sw = stroke.normal,
302
+ headingWeight = '600',
303
+ headingColor,
304
+ bodyColor = color.textDim,
305
+ headingY: explicitHeadingY,
306
+ bodyY: explicitBodyY,
307
+ body2Y: explicitBody2Y,
308
+ }: {
309
+ x: number
310
+ y: number
311
+ width: number
312
+ height: number
313
+ heading: string
314
+ body?: string
315
+ body2?: string
316
+ accentColor: string
317
+ rx?: number
318
+ fill?: string
319
+ strokeWidth?: number
320
+ headingWeight?: string
321
+ headingColor?: string
322
+ bodyColor?: string
323
+ headingY?: number
324
+ bodyY?: number
325
+ body2Y?: number
326
+ }) {
327
+ const cx = x + width / 2
328
+ const hasBody2 = body2 != null
329
+ const hasBody = body != null
330
+ const headingY = explicitHeadingY ?? (hasBody ? y + height * 0.38 : y + height / 2 + font.primary / 3)
331
+ const bodyY = explicitBodyY ?? (hasBody2 ? y + height * 0.6 : y + height * 0.7)
332
+ const body2Y = explicitBody2Y ?? y + height * 0.82
333
+
334
+ return (
335
+ <>
336
+ <rect
337
+ x={x}
338
+ y={y}
339
+ width={width}
340
+ height={height}
341
+ rx={rx}
342
+ fill={bgFill}
343
+ stroke={accentColor}
344
+ strokeWidth={sw}
345
+ />
346
+ <text
347
+ x={cx}
348
+ y={headingY}
349
+ fill={headingColor ?? accentColor}
350
+ fontSize={font.primary}
351
+ textAnchor="middle"
352
+ fontWeight={headingWeight}
353
+ >
354
+ {heading}
355
+ </text>
356
+ {hasBody && (
357
+ <text x={cx} y={bodyY} fill={bodyColor} fontSize={font.secondary} textAnchor="middle">
358
+ {body}
359
+ </text>
360
+ )}
361
+ {hasBody2 && (
362
+ <text x={cx} y={body2Y} fill={bodyColor} fontSize={font.small} textAnchor="middle">
363
+ {body2}
364
+ </text>
365
+ )}
366
+ </>
367
+ )
368
+ }
369
+
370
+ /**
371
+ * Full-width info strip at the bottom of a diagram.
372
+ * Renders a rect with one or more centered text lines inside.
373
+ */
374
+ export function DiagramInfoBox({
375
+ y,
376
+ height = box.infoBox.h,
377
+ children,
378
+ borderColor = color.border,
379
+ fill: bgFill = color.bgDark,
380
+ rx = radius.md,
381
+ }: {
382
+ y: number
383
+ height?: number
384
+ children: ReactNode
385
+ borderColor?: string
386
+ fill?: string
387
+ rx?: number
388
+ }) {
389
+ return (
390
+ <>
391
+ <rect
392
+ x="30"
393
+ y={y}
394
+ width={box.infoBox.w}
395
+ height={height}
396
+ rx={rx}
397
+ fill={bgFill}
398
+ stroke={borderColor}
399
+ strokeWidth={stroke.thin}
400
+ />
401
+ {children}
402
+ </>
403
+ )
404
+ }
405
+
406
+ /**
407
+ * Full-width code display box with a dark background.
408
+ * Children should be <text> elements with monospace font.
409
+ */
410
+ export function DiagramCodeBlock({
411
+ x = 30,
412
+ y,
413
+ width = box.codeBlock.w,
414
+ height = box.codeBlock.h,
415
+ children,
416
+ borderColor = color.border,
417
+ rx = radius.sm,
418
+ }: {
419
+ x?: number
420
+ y: number
421
+ width?: number
422
+ height?: number
423
+ children?: ReactNode
424
+ borderColor?: string
425
+ rx?: number
426
+ }) {
427
+ return (
428
+ <>
429
+ <rect
430
+ x={x}
431
+ y={y}
432
+ width={width}
433
+ height={height}
434
+ rx={rx}
435
+ fill={color.bgDarker}
436
+ stroke={borderColor}
437
+ strokeWidth={stroke.thin}
438
+ />
439
+ {children}
440
+ </>
441
+ )
442
+ }
443
+
444
+ /**
445
+ * Small text badge/pill with tinted background.
446
+ */
447
+ export function DiagramBadge({
448
+ x,
449
+ y,
450
+ width,
451
+ height = 14,
452
+ label,
453
+ accentColor,
454
+ fillOpacity = 0.1,
455
+ rx = radius.sm,
456
+ }: {
457
+ x: number
458
+ y: number
459
+ width: number
460
+ height?: number
461
+ label: string
462
+ accentColor: string
463
+ fillOpacity?: number
464
+ rx?: number
465
+ }) {
466
+ return (
467
+ <>
468
+ <rect
469
+ x={x}
470
+ y={y}
471
+ width={width}
472
+ height={height}
473
+ rx={rx}
474
+ fill={accentColor}
475
+ fillOpacity={fillOpacity}
476
+ stroke={accentColor}
477
+ strokeWidth={stroke.hairline}
478
+ />
479
+ <text
480
+ x={x + width / 2}
481
+ y={y + height / 2 + font.small / 3}
482
+ fill={accentColor}
483
+ fontSize={font.small}
484
+ textAnchor="middle"
485
+ >
486
+ {label}
487
+ </text>
488
+ </>
489
+ )
490
+ }
491
+
492
+ // ============================================================================
493
+ // StepMarker – numbered circle with optional connector line
494
+ // ============================================================================
495
+
496
+ export function StepMarker({
497
+ cx,
498
+ cy,
499
+ n,
500
+ accentColor,
501
+ connectorToY,
502
+ r = 10,
503
+ }: {
504
+ cx: number
505
+ cy: number
506
+ n: number
507
+ accentColor: string
508
+ connectorToY?: number
509
+ r?: number
510
+ }) {
511
+ return (
512
+ <>
513
+ <circle
514
+ cx={cx}
515
+ cy={cy}
516
+ r={r}
517
+ fill={accentColor}
518
+ fillOpacity={0.15}
519
+ stroke={accentColor}
520
+ strokeWidth={stroke.thin}
521
+ />
522
+ <text
523
+ x={cx}
524
+ y={cy + font.primary / 3}
525
+ fill={accentColor}
526
+ fontSize={font.primary}
527
+ textAnchor="middle"
528
+ fontWeight="600"
529
+ >
530
+ {n}
531
+ </text>
532
+ {connectorToY != null && (
533
+ <line
534
+ x1={cx}
535
+ y1={cy + r + 2}
536
+ x2={cx}
537
+ y2={connectorToY}
538
+ stroke={color.border}
539
+ strokeWidth={stroke.thin}
540
+ strokeDasharray="3 3"
541
+ />
542
+ )}
543
+ </>
544
+ )
545
+ }
546
+
547
+ // ============================================================================
548
+ // ArrowMarker – reusable SVG <marker> for ArrowPath
549
+ // ============================================================================
550
+
551
+ export function ArrowMarker({
552
+ id,
553
+ color: fill,
554
+ opacity = 0.7,
555
+ }: {
556
+ id: string
557
+ color: string
558
+ opacity?: number
559
+ }) {
560
+ return (
561
+ <marker
562
+ id={id}
563
+ markerWidth="5"
564
+ markerHeight="4"
565
+ refX="4"
566
+ refY="2"
567
+ orient="auto"
568
+ >
569
+ <path d="M 0 0 L 5 2 L 0 4 Z" fill={fill} fillOpacity={opacity} />
570
+ </marker>
571
+ )
572
+ }
573
+
574
+ // ============================================================================
575
+ // ArrowPath – line with arrowhead marker
576
+ // ============================================================================
577
+
578
+ export function ArrowPath({
579
+ d,
580
+ color: strokeColor,
581
+ dashed = false,
582
+ strokeWidth: sw = stroke.normal,
583
+ markerEnd,
584
+ opacity: op = 1,
585
+ }: {
586
+ d: string
587
+ color: string
588
+ dashed?: boolean
589
+ strokeWidth?: number
590
+ markerEnd?: string
591
+ opacity?: number
592
+ }) {
593
+ return (
594
+ <path
595
+ d={d}
596
+ stroke={strokeColor}
597
+ strokeWidth={sw}
598
+ fill="none"
599
+ opacity={op}
600
+ {...(dashed ? { strokeDasharray: '4 3' } : {})}
601
+ {...(markerEnd ? { markerEnd: `url(#${markerEnd})` } : {})}
602
+ />
603
+ )
604
+ }
605
+
606
+ // ============================================================================
607
+ // LayerBar – horizontal bar with colored left accent stripe
608
+ // ============================================================================
609
+
610
+ export function LayerBar({
611
+ x,
612
+ y,
613
+ width,
614
+ label,
615
+ detail,
616
+ accentColor,
617
+ opacity: op = 1,
618
+ height = 30,
619
+ }: {
620
+ x: number
621
+ y: number
622
+ width: number
623
+ label: string
624
+ detail?: string
625
+ accentColor: string
626
+ opacity?: number
627
+ height?: number
628
+ }) {
629
+ return (
630
+ <>
631
+ <rect
632
+ x={x}
633
+ y={y}
634
+ width={width}
635
+ height={height}
636
+ rx={radius.md}
637
+ fill={color.bgDark}
638
+ stroke={accentColor}
639
+ strokeWidth={stroke.normal}
640
+ opacity={op}
641
+ />
642
+ <rect
643
+ x={x}
644
+ y={y}
645
+ width={5}
646
+ height={height}
647
+ rx={2}
648
+ fill={accentColor}
649
+ opacity={op}
650
+ />
651
+ <text
652
+ x={x + 15}
653
+ y={y + (detail ? height * 0.42 : height / 2 + font.primary / 3)}
654
+ fill={accentColor}
655
+ fontSize={font.primary}
656
+ fontWeight="600"
657
+ opacity={op}
658
+ >
659
+ {label}
660
+ </text>
661
+ {detail && (
662
+ <text
663
+ x={x + 15}
664
+ y={y + height * 0.78}
665
+ fill={color.textDim}
666
+ fontSize={font.small}
667
+ opacity={op}
668
+ >
669
+ {detail}
670
+ </text>
671
+ )}
672
+ </>
673
+ )
674
+ }
675
+
676
+ // ============================================================================
677
+ // GridCard – card with icon circle, label, description, code hint
678
+ // ============================================================================
679
+
680
+ export function GridCard({
681
+ x,
682
+ y,
683
+ width,
684
+ icon,
685
+ label,
686
+ desc,
687
+ codeHint,
688
+ accentColor,
689
+ }: {
690
+ x: number
691
+ y: number
692
+ width: number
693
+ icon: string
694
+ label: string
695
+ desc?: string
696
+ codeHint?: string
697
+ accentColor: string
698
+ }) {
699
+ const height = codeHint ? 48 : 38
700
+ return (
701
+ <>
702
+ <rect
703
+ x={x}
704
+ y={y}
705
+ width={width}
706
+ height={height}
707
+ rx={radius.md}
708
+ fill={color.bgDark}
709
+ stroke={accentColor}
710
+ strokeWidth={stroke.normal}
711
+ />
712
+ <circle
713
+ cx={x + 22}
714
+ cy={y + 18}
715
+ r={10}
716
+ fill={accentColor}
717
+ fillOpacity={0.15}
718
+ stroke={accentColor}
719
+ strokeWidth={stroke.hairline}
720
+ />
721
+ <text
722
+ x={x + 22}
723
+ y={y + 22}
724
+ fill={accentColor}
725
+ fontSize={font.primary}
726
+ textAnchor="middle"
727
+ fontWeight="600"
728
+ >
729
+ {icon}
730
+ </text>
731
+ <text
732
+ x={x + 42}
733
+ y={y + 16}
734
+ fill={color.textBright}
735
+ fontSize={font.primary}
736
+ fontWeight="500"
737
+ >
738
+ {label}
739
+ </text>
740
+ {desc && (
741
+ <text x={x + 42} y={y + 28} fill={color.textDim} fontSize={font.small}>
742
+ {desc}
743
+ </text>
744
+ )}
745
+ {codeHint && (
746
+ <>
747
+ <rect
748
+ x={x + 8}
749
+ y={y + 34}
750
+ width={width - 16}
751
+ height={10}
752
+ rx={2}
753
+ fill={color.bgDarker}
754
+ />
755
+ <text
756
+ x={x + width / 2}
757
+ y={y + 42}
758
+ fill={accentColor}
759
+ fontSize={font.small}
760
+ textAnchor="middle"
761
+ fontFamily="monospace"
762
+ opacity={0.7}
763
+ >
764
+ {codeHint}
765
+ </text>
766
+ </>
767
+ )}
768
+ </>
769
+ )
770
+ }
771
+
772
+ // ============================================================================
773
+ // ActorNode – circle with icon text and external label
774
+ // ============================================================================
775
+
776
+ export function ActorNode({
777
+ cx,
778
+ cy,
779
+ r = 12,
780
+ label,
781
+ labelPos = 'below',
782
+ accentColor,
783
+ }: {
784
+ cx: number
785
+ cy: number
786
+ r?: number
787
+ label: string
788
+ labelPos?: 'above' | 'below' | 'left' | 'right'
789
+ accentColor: string
790
+ }) {
791
+ const labelOffset = r + 8
792
+ const labelProps = {
793
+ above: { x: cx, y: cy - labelOffset, textAnchor: 'middle' as const },
794
+ below: { x: cx, y: cy + labelOffset, textAnchor: 'middle' as const },
795
+ left: { x: cx - labelOffset, y: cy + font.small / 3, textAnchor: 'end' as const },
796
+ right: { x: cx + labelOffset, y: cy + font.small / 3, textAnchor: 'start' as const },
797
+ }[labelPos]
798
+
799
+ return (
800
+ <>
801
+ <circle
802
+ cx={cx}
803
+ cy={cy}
804
+ r={r}
805
+ fill={accentColor}
806
+ fillOpacity={0.15}
807
+ stroke={accentColor}
808
+ strokeWidth={stroke.normal}
809
+ />
810
+ <text
811
+ {...labelProps}
812
+ fill={accentColor}
813
+ fontSize={font.small}
814
+ fontWeight="500"
815
+ >
816
+ {label}
817
+ </text>
818
+ </>
819
+ )
820
+ }
821
+
822
+ // ============================================================================
823
+ // KeyValueRow – monospace key-value pair for code displays
824
+ // ============================================================================
825
+
826
+ export function KeyValueRow({
827
+ x,
828
+ y,
829
+ field,
830
+ value,
831
+ fieldColor,
832
+ }: {
833
+ x: number
834
+ y: number
835
+ field: string
836
+ value: string
837
+ fieldColor: string
838
+ }) {
839
+ return (
840
+ <>
841
+ <text
842
+ x={x}
843
+ y={y}
844
+ fill={fieldColor}
845
+ fontSize={font.secondary}
846
+ fontFamily="monospace"
847
+ >
848
+ {field}
849
+ </text>
850
+ <text
851
+ x={x + field.length * 4.2}
852
+ y={y}
853
+ fill={color.textDim}
854
+ fontSize={font.secondary}
855
+ fontFamily="monospace"
856
+ >
857
+ {value}
858
+ </text>
859
+ </>
860
+ )
861
+ }
862
+
863
+ // ============================================================================
864
+ // WarningIndicator – triangle icon for warnings
865
+ // ============================================================================
866
+
867
+ export function WarningIndicator({
868
+ cx,
869
+ cy,
870
+ color: fillColor,
871
+ size = 10,
872
+ }: {
873
+ cx: number
874
+ cy: number
875
+ color: string
876
+ size?: number
877
+ }) {
878
+ const half = size / 2
879
+ const top = cy - half
880
+ const bottom = cy + half
881
+ return (
882
+ <>
883
+ <path
884
+ d={`M ${cx} ${top} L ${cx + half} ${bottom} L ${cx - half} ${bottom} Z`}
885
+ fill="none"
886
+ stroke={fillColor}
887
+ strokeWidth={stroke.normal}
888
+ />
889
+ <text
890
+ x={cx}
891
+ y={bottom - 2}
892
+ fill={fillColor}
893
+ fontSize={font.primary}
894
+ textAnchor="middle"
895
+ fontWeight="700"
896
+ >
897
+ !
898
+ </text>
899
+ </>
900
+ )
901
+ }
902
+
903
+ // ============================================================================
904
+ // Re-exports for convenience
905
+ // ============================================================================
906
+
907
+ export { motion }
908
+ export type { Variants }