@dfosco/storyboard-core 3.3.2 → 3.5.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 (51) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +14899 -11508
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/package.json +1 -1
  6. package/scaffold/toolbar.config.json +2 -2
  7. package/src/CanvasCreateMenu.svelte +1 -1
  8. package/src/CanvasZoomControl.svelte +105 -0
  9. package/src/CommandMenu.svelte +87 -25
  10. package/src/CoreUIBar.svelte +350 -347
  11. package/src/CreateMenuButton.svelte +6 -2
  12. package/src/InspectorPanel.svelte +123 -59
  13. package/src/SidePanel.svelte +1 -1
  14. package/src/ThemeMenuButton.svelte +35 -3
  15. package/src/commandActions.js +14 -0
  16. package/src/core-ui-colors.css +30 -2
  17. package/src/devtools.js +7 -1
  18. package/src/index.js +10 -1
  19. package/src/inspector/fiberWalker.js +49 -6
  20. package/src/inspector/highlighter.js +257 -33
  21. package/src/lib/components/ui/button/button.svelte +1 -1
  22. package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
  23. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
  24. package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
  25. package/src/lib/components/ui/trigger-button/trigger-button.svelte +31 -3
  26. package/src/modes.css +8 -0
  27. package/src/mountStoryboardCore.js +15 -1
  28. package/src/sidepanel.css +2 -2
  29. package/src/stores/themeStore.ts +66 -0
  30. package/src/svelte-plugin-ui/components/Viewfinder.svelte +16 -11
  31. package/src/toolRegistry.js +226 -0
  32. package/src/toolStateStore.js +180 -0
  33. package/src/toolStateStore.test.js +204 -0
  34. package/src/toolbarConfigStore.js +135 -0
  35. package/src/tools/handlers/canvasAddWidget.js +11 -0
  36. package/src/tools/handlers/canvasZoom.js +34 -0
  37. package/src/tools/handlers/comments.js +16 -0
  38. package/src/tools/handlers/create.js +39 -0
  39. package/src/tools/handlers/devtools.js +80 -0
  40. package/src/tools/handlers/docs.js +11 -0
  41. package/src/tools/handlers/featureFlags.js +21 -0
  42. package/src/tools/handlers/flows.js +62 -0
  43. package/src/tools/handlers/inspector.js +19 -0
  44. package/src/tools/handlers/theme.js +9 -0
  45. package/src/tools/registry.js +21 -0
  46. package/src/tools/surfaces/canvasToolbar.js +10 -0
  47. package/src/tools/surfaces/commandList.js +10 -0
  48. package/src/tools/surfaces/mainToolbar.js +11 -0
  49. package/src/tools/surfaces/registry.js +19 -0
  50. package/src/vite/server-plugin.js +36 -6
  51. package/toolbar.config.json +101 -48
@@ -33,11 +33,15 @@
33
33
 
34
34
  interface Props {
35
35
  features?: CreateMenuFeature[]
36
+ data?: { features?: CreateMenuFeature[] }
36
37
  config?: CreateMenuConfig
37
38
  tabindex?: number
38
39
  }
39
40
 
40
- let { features = [], config = { label: 'Create' }, tabindex }: Props = $props()
41
+ let { features: featuresProp = [], data, config = { label: 'Create' }, tabindex }: Props = $props()
42
+
43
+ // Support both direct `features` prop (legacy) and `data.features` (generic toolbar)
44
+ const features = $derived(featuresProp.length > 0 ? featuresProp : (data?.features || []))
41
45
 
42
46
  const menuWidth = $derived((config as any).menuWidth || null)
43
47
 
@@ -110,7 +114,7 @@
110
114
  <DropdownMenu.Separator />
111
115
  {:else if action.type === 'footer'}
112
116
  <DropdownMenu.Separator />
113
- <div class="px-2 py-1.5 text-xs text-muted-foreground">{action.label}</div>
117
+ <div class="px-2 py-1.5 text-xs text-muted-foreground flex flex-row items-baseline"><span class="inline-flex w-2 h-2 rounded-full mr-1.5" style="background: hsl(137, 66%, 30%)"></span>Only available in dev environment</div>
114
118
  {:else if action._feature}
115
119
  <DropdownMenu.Item onclick={() => showOverlay(action._feature.overlayId)}>
116
120
  {action.label || action._feature.label}
@@ -9,6 +9,7 @@
9
9
  import Icon from './svelte-plugin-ui/components/Icon.svelte'
10
10
  import { inspectElement, inspectElementChain } from './inspector/fiberWalker.js'
11
11
  import { createMouseMode } from './inspector/mouseMode.js'
12
+ import { getColors } from './inspector/highlighter.js'
12
13
 
13
14
  /** @type {{ name: string, props: object, source: { fileName: string, lineNumber: number, columnNumber?: number } | null, owner: string | null } | null} */
14
15
  let componentInfo = $state(null)
@@ -109,21 +110,23 @@
109
110
  return null
110
111
  }
111
112
 
113
+ const _isLocalDev = typeof window !== 'undefined' && window.__SB_LOCAL_DEV__ === true
114
+
112
115
  /**
113
116
  * Fetch source file content — uses dev middleware in dev, static JSON in prod.
114
- * Uses runtime detection (not import.meta.env.DEV) so this works in the
115
- * pre-compiled UI bundle where compile-time env vars are baked in.
116
117
  */
117
118
  async function fetchSourceContent(filePath) {
118
- // Try the dev middleware first works when Vite server plugin is active
119
- try {
120
- const res = await fetch(`/_storyboard/docs/source?path=${encodeURIComponent(filePath)}`)
121
- if (res.ok) {
122
- const json = await res.json()
123
- return json?.content || ''
124
- }
125
- } catch {}
126
- // Fall back to static inspector JSON for deployed/production builds
119
+ // In local dev, use the live middleware (reads from disk)
120
+ if (_isLocalDev) {
121
+ try {
122
+ const res = await fetch(`/_storyboard/docs/source?path=${encodeURIComponent(filePath)}`)
123
+ if (res.ok) {
124
+ const json = await res.json()
125
+ return json?.content || ''
126
+ }
127
+ } catch {}
128
+ }
129
+ // In production (or if dev middleware failed), use static build-time JSON
127
130
  const data = await loadStaticData()
128
131
  return data?.sources?.[filePath] || ''
129
132
  }
@@ -244,6 +247,9 @@
244
247
 
245
248
  let highlightedHtml = $state('')
246
249
 
250
+ // Code theme colors — refreshed on theme change events
251
+ let codeTheme = $state(getColors())
252
+
247
253
  /** @type {any} */
248
254
  let highlighter = null
249
255
 
@@ -254,6 +260,22 @@
254
260
  return highlighter
255
261
  }
256
262
 
263
+ /** Re-highlight source code with current theme (called on theme change). */
264
+ async function rehighlight() {
265
+ codeTheme = getColors()
266
+ if (!sourceCode || !sourcePath) return
267
+ try {
268
+ const hl = await getHighlighter()
269
+ highlightedHtml = hl.codeToHtml(sourceCode, {
270
+ lang: getLang(sourcePath),
271
+ theme: 'github-dark',
272
+ decorations: matchedLine > 0
273
+ ? [{ start: { line: matchedLine - 1, character: 0 }, end: { line: matchedLine - 1, character: Infinity }, properties: { class: 'highlighted-line' } }]
274
+ : [],
275
+ })
276
+ } catch { /* ignore */ }
277
+ }
278
+
257
279
  /**
258
280
  * Find the line number of a JSX component in source code by matching
259
281
  * the component name and its props against the source lines.
@@ -343,7 +365,7 @@
343
365
  lang: getLang(path),
344
366
  theme: 'github-dark',
345
367
  decorations: matchedLine > 0
346
- ? [{ start: { line: matchedLine - 1, character: 0 }, end: { line: matchedLine, character: 0 }, properties: { class: 'highlighted-line' } }]
368
+ ? [{ start: { line: matchedLine - 1, character: 0 }, end: { line: matchedLine - 1, character: Infinity }, properties: { class: 'highlighted-line' } }]
347
369
  : [],
348
370
  })
349
371
  } catch {
@@ -427,26 +449,31 @@
427
449
  onDeactivate: handleDeactivate,
428
450
  })
429
451
 
430
- // Pre-fetch file list and repo info FIRST (needed for source resolution)
431
- // Try dev middleware first, fall back to static JSON
452
+ // Re-highlight code when theme changes
453
+ document.addEventListener('storyboard:theme:changed', rehighlight)
454
+
455
+ // Pre-fetch file list and repo info
456
+ // In local dev, try dev middleware; in production, go straight to static JSON
432
457
  let filesLoaded = false
433
- try {
434
- const [filesRes, repoRes] = await Promise.all([
435
- fetch('/_storyboard/docs/files'),
436
- fetch('/_storyboard/docs/repo'),
437
- ])
438
- if (filesRes.ok) {
439
- const data = await filesRes.json()
440
- knownFiles = data.files || []
441
- filesLoaded = true
442
- }
443
- if (repoRes.ok) {
444
- repoInfo = await repoRes.json()
445
- }
446
- } catch {}
458
+ if (_isLocalDev) {
459
+ try {
460
+ const [filesRes, repoRes] = await Promise.all([
461
+ fetch('/_storyboard/docs/files'),
462
+ fetch('/_storyboard/docs/repo'),
463
+ ])
464
+ if (filesRes.ok) {
465
+ const data = await filesRes.json()
466
+ knownFiles = data.files || []
467
+ filesLoaded = true
468
+ }
469
+ if (repoRes.ok) {
470
+ repoInfo = await repoRes.json()
471
+ }
472
+ } catch {}
473
+ }
447
474
 
448
475
  if (!filesLoaded) {
449
- // Fall back to static build-time JSON
476
+ // Use static build-time JSON
450
477
  const data = await loadStaticData()
451
478
  if (data) {
452
479
  knownFiles = data.files || []
@@ -483,6 +510,7 @@
483
510
  mouseMode?.deactivate()
484
511
  mouseMode?.hideHighlight()
485
512
  setInspectParam(null)
513
+ document.removeEventListener('storyboard:theme:changed', rehighlight)
486
514
  })
487
515
  </script>
488
516
 
@@ -543,9 +571,12 @@
543
571
 
544
572
  <!-- Source code -->
545
573
  {#if sourcePath}
546
- <div class="border rounded-md overflow-hidden flex-1 min-h-0 flex flex-col inspector-code-block" style:border-color="var(--borderColor-default, var(--color-border, #d1d9e0))">
574
+ <div class="border rounded-md overflow-hidden flex-1 min-h-0 flex flex-col" style:background={codeTheme.bg} style:border-color={codeTheme.border}>
547
575
  <div
548
- class="flex items-center justify-between w-full px-3 py-1.5 text-xs font-semibold shrink-0 inspector-code-header"
576
+ class="flex items-center justify-between w-full px-3 py-1.5 text-xs font-semibold shrink-0"
577
+ style:background={codeTheme.headerBg}
578
+ style:color={codeTheme.headerFg}
579
+ style:border-bottom="1px solid {codeTheme.border}"
549
580
  >
550
581
  <span class="flex items-center gap-1.5 min-w-0">
551
582
  <Icon name="primer/file-code" size={12} />
@@ -557,6 +588,7 @@
557
588
  target="_blank"
558
589
  rel="noopener noreferrer"
559
590
  class="flex items-center gap-1 shrink-0 text-xs no-underline hover:underline inspector-mono inspector-code-link"
591
+ style:color={codeTheme.headerFg}
560
592
  >
561
593
  <Icon name="primer/mark-github" size={14} />
562
594
  <span>GitHub</span>
@@ -564,21 +596,21 @@
564
596
  {/if}
565
597
  </div>
566
598
 
567
- <div class="border-t flex-1 min-h-0 flex flex-col" style:border-color="#30363d">
599
+ <div class="border-t flex-1 min-h-0 flex flex-col" style:border-color={codeTheme.border}>
568
600
  {#if sourceLoading}
569
- <div class="px-3 py-4 text-xs text-center" style:color="#8b949e">
601
+ <div class="px-3 py-4 text-xs text-center" style:color={codeTheme.headerFg}>
570
602
  Loading source…
571
603
  </div>
572
604
  {:else if sourceCode}
573
- <div class="flex-1 min-h-0 overflow-y-auto source-scroll-container" bind:this={sourceContainer}>
605
+ <div class="flex-1 min-h-0 overflow-y-auto source-scroll-container" bind:this={sourceContainer} style:--inspector-line-num-color={codeTheme.comment} style:--inspector-line-hover={codeTheme.lineHighlight}>
574
606
  {#if highlightedHtml}
575
- <div class="shiki-wrapper">{@html highlightedHtml}</div>
607
+ <div class="code-wrapper line-numbers">{@html highlightedHtml}</div>
576
608
  {:else}
577
- <pre class="m-0 text-xs leading-relaxed inspector-mono source-pre" style:color="#c9d1d9">{sourceCode}</pre>
609
+ <pre class="m-0 text-xs leading-relaxed inspector-mono source-pre line-numbers" style:background={codeTheme.bg} style:color={codeTheme.fg}><code>{#each sourceCode.split('\n') as line, i}<span class="line{matchedLine > 0 && i + 1 === matchedLine ? ' highlighted-line' : ''}">{line}</span>{#if i < sourceCode.split('\n').length - 1}{'\n'}{/if}{/each}</code></pre>
578
610
  {/if}
579
611
  </div>
580
612
  {:else}
581
- <div class="px-3 py-4 text-xs text-center" style:color="#8b949e">
613
+ <div class="px-3 py-4 text-xs text-center" style:color={codeTheme.headerFg}>
582
614
  Unable to load source
583
615
  </div>
584
616
  {/if}
@@ -627,9 +659,54 @@
627
659
  .source-pre {
628
660
  background: transparent;
629
661
  tab-size: 2;
662
+ padding: 12px 0;
663
+ color: #c9d1d9;
664
+ overflow-x: auto;
665
+ }
666
+
667
+ .source-pre code {
668
+ font-family: inherit;
669
+ display: block;
670
+ }
671
+
672
+ .source-pre .line {
673
+ padding: 0 12px 0 0;
674
+ display: inline-block;
675
+ width: 100%;
676
+ min-height: 1.5em;
677
+ }
678
+
679
+ .source-pre .line:hover {
680
+ background: var(--inspector-line-hover, rgba(255, 255, 255, 0.04));
681
+ }
682
+
683
+ .source-pre :global(.highlighted-line) {
684
+ background: color-mix(in srgb, var(--color-purple, #7655a4) 20%, transparent);
685
+ border-left: 2px solid var(--color-purple, #7655a4);
686
+ padding-left: 10px;
630
687
  }
631
688
 
632
- .shiki-wrapper :global(pre) {
689
+ /* Line numbers via CSS counters — works for both highlight.js and plain-text */
690
+ .line-numbers :global(code) {
691
+ counter-reset: line;
692
+ }
693
+
694
+ .line-numbers :global(.line) {
695
+ padding-left: 0 !important;
696
+ }
697
+
698
+ .line-numbers :global(.line::before) {
699
+ counter-increment: line;
700
+ content: counter(line);
701
+ display: inline-block;
702
+ width: 3.5ch;
703
+ margin-right: 1.5ch;
704
+ text-align: right;
705
+ color: var(--inspector-line-num-color, #484f58);
706
+ user-select: none;
707
+ }
708
+
709
+ .code-wrapper :global(pre) {
633
710
  margin: 0;
634
711
  padding: 12px 0;
635
712
  font-size: 12px;
@@ -640,43 +717,30 @@
640
717
  overflow-x: auto;
641
718
  }
642
719
 
643
- .shiki-wrapper :global(code) {
720
+ .code-wrapper :global(code) {
644
721
  font-family: inherit;
645
722
  display: block;
646
723
  }
647
724
 
648
- .shiki-wrapper :global(.line) {
649
- padding: 0 12px;
725
+ .code-wrapper :global(.line) {
726
+ padding: 0 12px 0 0;
650
727
  display: inline-block;
651
728
  width: 100%;
652
729
  min-height: 1.5em;
653
730
  }
654
731
 
655
- .shiki-wrapper :global(.line:hover) {
656
- background: rgba(255, 255, 255, 0.04);
732
+ .code-wrapper :global(.line:hover) {
733
+ background: var(--inspector-line-hover, rgba(255, 255, 255, 0.04));
657
734
  }
658
735
 
659
- .shiki-wrapper :global(.highlighted-line) {
736
+ .code-wrapper :global(.highlighted-line) {
660
737
  background: color-mix(in srgb, var(--color-purple, #7655a4) 20%, transparent);
661
738
  border-left: 2px solid var(--color-purple, #7655a4);
739
+ padding-left: 10px;
662
740
  }
663
741
 
664
742
  /* Force dark chrome on the code block — independent of page theme */
665
- .inspector-code-block {
666
- background: #0d1117;
667
- border-color: #30363d !important;
668
- }
669
-
670
- .inspector-code-header {
671
- background: #161b22;
672
- color: #8b949e;
673
- border-bottom: 1px solid #30363d;
674
- }
675
-
676
- .inspector-code-link {
677
- color: #8b949e;
678
- }
679
743
  .inspector-code-link:hover {
680
- color: #c9d1d9;
744
+ text-decoration: underline;
681
745
  }
682
746
  </style>
@@ -320,7 +320,7 @@
320
320
  left: 0;
321
321
  right: 0;
322
322
  height: 3px;
323
- background: var(--mode-color, var(--borderColor-default, var(--color-border, #d0d7de)));
323
+ /* background: var(--mode-color, var(--borderColor-default, var(--color-border, #d0d7de))); */
324
324
  }
325
325
 
326
326
  /* Drag handle — side mode (left edge, vertical) */
@@ -1,15 +1,15 @@
1
1
  <!--
2
2
  ThemeMenuButton — toolbar dropdown for switching the app color scheme.
3
3
 
4
- Renders a radio group of theme options (System, Light, Dark, etc.)
5
- and persists the selection via the themeStore.
4
+ Renders a radio group of theme options (System, Light, Dark, etc.),
5
+ followed by a separator and "Theme settings" submenu with sync toggles.
6
6
  -->
7
7
 
8
8
  <script lang="ts">
9
9
  import { TriggerButton } from './lib/components/ui/trigger-button/index.js'
10
10
  import * as DropdownMenu from './lib/components/ui/dropdown-menu/index.js'
11
11
  import Icon from './svelte-plugin-ui/components/Icon.svelte'
12
- import { themeState, setTheme, THEMES, type ThemeValue } from './stores/themeStore.js'
12
+ import { themeState, setTheme, THEMES, type ThemeValue, themeSyncState, setThemeSyncTarget, type ThemeSyncTargets } from './stores/themeStore.js'
13
13
 
14
14
  interface Props {
15
15
  config?: {
@@ -30,6 +30,11 @@
30
30
  setTheme(value)
31
31
  menuOpen = false
32
32
  }
33
+
34
+ function handleSyncToggle(e: Event, target: keyof ThemeSyncTargets) {
35
+ e.preventDefault()
36
+ setThemeSyncTarget(target, !$themeSyncState[target])
37
+ }
33
38
  </script>
34
39
 
35
40
  <DropdownMenu.Root bind:open={menuOpen}>
@@ -62,5 +67,32 @@
62
67
  </DropdownMenu.RadioItem>
63
68
  {/each}
64
69
  </DropdownMenu.RadioGroup>
70
+
71
+ <DropdownMenu.Separator />
72
+
73
+ <DropdownMenu.Sub>
74
+ <DropdownMenu.SubTrigger>Theme settings</DropdownMenu.SubTrigger>
75
+ <DropdownMenu.SubContent class="min-w-[180px]">
76
+ <DropdownMenu.Label>Apply theme to</DropdownMenu.Label>
77
+ <DropdownMenu.CheckboxItem
78
+ checked={$themeSyncState.prototype}
79
+ onSelect={(e) => handleSyncToggle(e, 'prototype')}
80
+ >
81
+ Prototype
82
+ </DropdownMenu.CheckboxItem>
83
+ <DropdownMenu.CheckboxItem
84
+ checked={$themeSyncState.toolbar}
85
+ onSelect={(e) => handleSyncToggle(e, 'toolbar')}
86
+ >
87
+ Toolbar
88
+ </DropdownMenu.CheckboxItem>
89
+ <DropdownMenu.CheckboxItem
90
+ checked={$themeSyncState.codeBoxes}
91
+ onSelect={(e) => handleSyncToggle(e, 'codeBoxes')}
92
+ >
93
+ Code boxes
94
+ </DropdownMenu.CheckboxItem>
95
+ </DropdownMenu.SubContent>
96
+ </DropdownMenu.Sub>
65
97
  </DropdownMenu.Content>
66
98
  </DropdownMenu.Root>
@@ -192,6 +192,8 @@ export function getActionsForMode(mode) {
192
192
  label: a.label,
193
193
  type: a.type || 'default',
194
194
  url: a.url || null,
195
+ toolKey: a.toolKey || null,
196
+ localOnly: a.localOnly || false,
195
197
  handler,
196
198
  active,
197
199
  }
@@ -224,6 +226,18 @@ export function getActionChildren(id) {
224
226
  return handler.getChildren()
225
227
  }
226
228
 
229
+ /**
230
+ * Check if a handler provides dynamic children (getChildren).
231
+ * Used by CoreUIBar to distinguish action-menu tools (gate on children count)
232
+ * from custom-component menus (always visible, render their own content).
233
+ * @param {string} id
234
+ * @returns {boolean}
235
+ */
236
+ export function hasChildrenProvider(id) {
237
+ const handler = _handlers.get(id)
238
+ return !!handler?.getChildren
239
+ }
240
+
227
241
  // ---------------------------------------------------------------------------
228
242
  // Reactivity
229
243
  // ---------------------------------------------------------------------------
@@ -1,8 +1,10 @@
1
1
  /**
2
- * Core UI Colors — always light-mode.
2
+ * Core UI Colors — toolbar theme tokens.
3
3
  *
4
4
  * The CoreUIBar and its children (trigger buttons, dropdowns, menus)
5
- * are locked to light-mode colors regardless of the page's theme.
5
+ * default to light-mode colors. When the user enables toolbar theme sync,
6
+ * `data-sb-toolbar-theme` switches to the active theme and dark tokens
7
+ * are applied.
6
8
  *
7
9
  * These override the shadcn/Tailwind design tokens within the
8
10
  * [data-core-ui-bar] scope so that CSS custom-property dark-mode
@@ -16,6 +18,7 @@
16
18
  * classes) so its scoped CSS has full control over background/color.
17
19
  */
18
20
 
21
+ /* Default: light tokens */
19
22
  [data-core-ui-bar],
20
23
  [data-slot="dropdown-menu-content"],
21
24
  [data-slot="dropdown-menu-sub-content"] {
@@ -37,3 +40,28 @@
37
40
  --color-border: hsl(214.3 31.8% 91.4%);
38
41
  --color-input: hsl(214.3 31.8% 91.4%);
39
42
  }
43
+
44
+ /* Dark tokens — applied when toolbar syncs with a dark theme.
45
+ * Uses Primer-aligned colors (matching base.css --sb-* dark tokens)
46
+ * instead of generic shadcn dark values.
47
+ */
48
+ :root[data-sb-toolbar-theme^="dark"] [data-core-ui-bar],
49
+ :root[data-sb-toolbar-theme^="dark"] [data-slot="dropdown-menu-content"],
50
+ :root[data-sb-toolbar-theme^="dark"] [data-slot="dropdown-menu-sub-content"] {
51
+ color-scheme: dark;
52
+
53
+ --color-background: #161b22;
54
+ --color-foreground: #e6edf3;
55
+ --color-popover: #161b22;
56
+ --color-popover-foreground: #e6edf3;
57
+ --color-primary: #e6edf3;
58
+ --color-primary-foreground: #161b22;
59
+ --color-secondary: #21262d;
60
+ --color-secondary-foreground: #e6edf3;
61
+ --color-muted: #21262d;
62
+ --color-muted-foreground: #8b949e;
63
+ --color-accent: #21262d;
64
+ --color-accent-foreground: #e6edf3;
65
+ --color-border: #30363d;
66
+ --color-input: #30363d;
67
+ }
package/src/devtools.js CHANGED
@@ -22,6 +22,8 @@ let skipLink = null
22
22
  * @param {object} [options]
23
23
  * @param {HTMLElement} [options.container=document.body] - Where to mount
24
24
  * @param {string} [options.basePath='/'] - Base URL path
25
+ * @param {object} [options.toolbarConfig] - Merged toolbar config
26
+ * @param {Record<string, () => Promise<any>>} [options.customHandlers] - Custom tool handlers
25
27
  */
26
28
  export async function mountDevTools(options = {}) {
27
29
  const container = options.container || document.body
@@ -108,7 +110,11 @@ export async function mountDevTools(options = {}) {
108
110
 
109
111
  instance = mount(CoreUIBar, {
110
112
  target: wrapper,
111
- props: { basePath, toolbarConfig: options.toolbarConfig },
113
+ props: {
114
+ basePath,
115
+ toolbarConfig: options.toolbarConfig,
116
+ customHandlers: options.customHandlers,
117
+ },
112
118
  })
113
119
  }
114
120
 
package/src/index.js CHANGED
@@ -68,7 +68,7 @@ export { resolveSceneRoute, getSceneMeta } from './viewfinder.js'
68
68
  export { initFeatureFlags, getFlag, setFlag, toggleFlag, getAllFlags, resetFlags, getFlagKeys, syncFlagBodyClasses } from './featureFlags.js'
69
69
 
70
70
  // Command actions (config-driven command menu entries)
71
- export { initCommandActions, registerCommandAction, unregisterCommandAction, setDynamicActions, clearDynamicActions, getActionsForMode, executeAction, getActionChildren, subscribeToCommandActions, getCommandActionsSnapshot, setRoutingBasePath, isExcludedByRoute } from './commandActions.js'
71
+ export { initCommandActions, registerCommandAction, unregisterCommandAction, setDynamicActions, clearDynamicActions, getActionsForMode, executeAction, getActionChildren, hasChildrenProvider, subscribeToCommandActions, getCommandActionsSnapshot, setRoutingBasePath, isExcludedByRoute } from './commandActions.js'
72
72
 
73
73
  // Plugin configuration
74
74
  export { initPlugins, isPluginEnabled, getPluginsConfig } from './plugins.js'
@@ -76,5 +76,14 @@ export { initPlugins, isPluginEnabled, getPluginsConfig } from './plugins.js'
76
76
  // UI config (project-level chrome overrides)
77
77
  export { initUIConfig, isMenuHidden, getHiddenItems } from './uiConfig.js'
78
78
 
79
+ // Tool registry (declarative tool system)
80
+ export { initToolRegistry, registerToolModule, setToolComponent, setToolGuardResult, getToolComponent, getToolModule, getToolsForToolbar, getToolConfig, getAllToolConfigs, subscribeToToolRegistry, getToolRegistrySnapshot } from './toolRegistry.js'
81
+
82
+ // Toolbar config store (reactive layered overrides: core → custom → prototype → user)
83
+ export { initToolbarConfig, setPrototypeToolbarConfig, clearPrototypeToolbarConfig, getToolbarConfig, subscribeToToolbarConfig, getToolbarConfigSnapshot } from './toolbarConfigStore.js'
84
+
85
+ // Toolbar tool state management (runtime state for toolbar tools)
86
+ export { TOOL_STATES, initToolbarToolStates, setToolbarToolState, getToolbarToolState, isToolbarToolLocalOnly, subscribeToToolbarToolStates, getToolbarToolStatesSnapshot } from './toolStateStore.js'
87
+
79
88
  // Comments system
80
89
  export { initCommentsConfig, getCommentsConfig, isCommentsEnabled } from './comments/config.js'
@@ -50,6 +50,8 @@ function isUserComponent(fiber) {
50
50
  /**
51
51
  * Derive a human-readable name from a fiber's type.
52
52
  * Handles plain components, forwardRef, and memo wrappers.
53
+ * Skips minified names (single-char or generic) in favor of 'ForwardRef'/'Memo'
54
+ * so the caller can try walking up the tree for a better name.
53
55
  *
54
56
  * @param {object} fiber
55
57
  * @returns {string}
@@ -59,20 +61,43 @@ function getComponentName(fiber) {
59
61
  const t = fiber.type
60
62
  // Plain function/class
61
63
  if (typeof t === 'function') return t.displayName || t.name || 'Anonymous'
62
- // forwardRef
64
+ // forwardRef / memo wrapper objects
63
65
  if (typeof t === 'object' && t !== null) {
64
66
  if (t.displayName) return t.displayName
65
- if (typeof t.render === 'function') return t.render.displayName || t.render.name || 'ForwardRef'
66
- // memo
67
+ // forwardRef: { render: fn }
68
+ if (typeof t.render === 'function') {
69
+ const name = t.render.displayName || t.render.name
70
+ return isUsableName(name) ? name : 'ForwardRef'
71
+ }
72
+ // memo: { type: ... }
67
73
  if (t.type) {
68
74
  const inner = t.type
69
- if (typeof inner === 'function') return inner.displayName || inner.name || 'Memo'
70
- if (typeof inner === 'object' && inner.render) return inner.render.displayName || inner.render.name || 'Memo'
75
+ if (typeof inner === 'function') {
76
+ const name = inner.displayName || inner.name
77
+ return isUsableName(name) ? name : 'Memo'
78
+ }
79
+ // memo(forwardRef)
80
+ if (typeof inner === 'object' && inner.render) {
81
+ if (inner.displayName) return inner.displayName
82
+ const name = inner.render.displayName || inner.render.name
83
+ return isUsableName(name) ? name : 'ForwardRef'
84
+ }
71
85
  }
72
86
  }
73
87
  return 'Unknown'
74
88
  }
75
89
 
90
+ /**
91
+ * Check if a resolved component name is usable (not minified).
92
+ * Minified names are typically 1-2 chars or all lowercase short strings.
93
+ */
94
+ function isUsableName(name) {
95
+ if (!name) return false
96
+ // Single-char names (e, t, r, n) are almost certainly minified
97
+ if (name.length <= 2) return false
98
+ return true
99
+ }
100
+
76
101
  /**
77
102
  * Extract debug source info from a fiber (dev builds only).
78
103
  *
@@ -133,10 +158,28 @@ export function getComponentInfo(fiber) {
133
158
 
134
159
  if (!current) return null
135
160
 
161
+ let name = getComponentName(current)
162
+
163
+ // If we got a generic name (ForwardRef, Memo), try walking up
164
+ // to find the nearest ancestor with a real component name
165
+ if (name === 'ForwardRef' || name === 'Memo') {
166
+ let ancestor = current.return
167
+ while (ancestor) {
168
+ if (isUserComponent(ancestor)) {
169
+ const ancestorName = getComponentName(ancestor)
170
+ if (ancestorName !== 'ForwardRef' && ancestorName !== 'Memo' && ancestorName !== 'Unknown') {
171
+ name = ancestorName
172
+ break
173
+ }
174
+ }
175
+ ancestor = ancestor.return
176
+ }
177
+ }
178
+
136
179
  const ownerFiber = current._debugOwner ?? null
137
180
 
138
181
  return {
139
- name: getComponentName(current),
182
+ name,
140
183
  props: current.memoizedProps ?? {},
141
184
  source: getDebugSource(current),
142
185
  owner: ownerFiber ? getComponentName(ownerFiber) : null,