@dfosco/storyboard-core 4.2.1 → 4.2.3

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 (57) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +7973 -8418
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/package.json +2 -12
  5. package/scaffold/AGENTS.md +432 -0
  6. package/scaffold/manifest.json +13 -8
  7. package/src/ActionMenuButton.jsx +1 -1
  8. package/src/AutosyncMenuButton.jsx +1 -1
  9. package/src/CanvasAgentsMenu.jsx +1 -1
  10. package/src/CanvasCreateMenu.jsx +1 -1
  11. package/src/CanvasSnap.jsx +1 -1
  12. package/src/CanvasZoomToFit.jsx +1 -1
  13. package/src/CommandMenu.jsx +2 -2
  14. package/src/CommandPalette.jsx +1 -1
  15. package/src/CommandPaletteTrigger.jsx +1 -1
  16. package/src/CommentsMenuButton.jsx +1 -1
  17. package/src/CoreUIBar.jsx +18 -2
  18. package/src/CreateMenuButton.jsx +1 -1
  19. package/src/HideChromeTrigger.jsx +1 -1
  20. package/src/{svelte-plugin-ui/components/Icon.jsx → Icon.jsx} +8 -10
  21. package/src/ThemeMenuButton.jsx +1 -1
  22. package/src/comments/ui/authModal.js +1 -1
  23. package/src/configSchema.js +2 -0
  24. package/src/configStore.js +1 -1
  25. package/src/devtools-consumer.js +2 -2
  26. package/src/index.js +3 -3
  27. package/src/loader.js +9 -2
  28. package/src/mountStoryboardCore.js +3 -3
  29. package/src/sidepanel.css +1 -1
  30. package/src/toolbarConfigStore.js +1 -1
  31. package/src/ui/design-modes.ts +4 -51
  32. package/src/ui/viewfinder.ts +4 -55
  33. package/src/ui-entry.js +5 -5
  34. package/src/vite/server-plugin.js +9 -0
  35. package/src/workshop/features/createFlow/index.js +1 -1
  36. package/src/workshop/features/createPrototype/index.js +1 -1
  37. package/src/workshop/features/registry-server.js +1 -1
  38. package/src/workshop/ui/mount.ts +3 -65
  39. package/scaffold/svelte.config.js +0 -1
  40. package/src/svelte-plugin-ui/__tests__/ModeSwitch.test.ts +0 -75
  41. package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +0 -126
  42. package/src/svelte-plugin-ui/__tests__/designModes.test.ts +0 -58
  43. package/src/svelte-plugin-ui/__tests__/modeStore.test.ts +0 -53
  44. package/src/svelte-plugin-ui/__tests__/mount.test.ts +0 -29
  45. package/src/svelte-plugin-ui/components/Icon.css +0 -11
  46. package/src/svelte-plugin-ui/components/ModeSwitch.css +0 -90
  47. package/src/svelte-plugin-ui/components/ModeSwitch.jsx +0 -47
  48. package/src/svelte-plugin-ui/components/ToolbarShell.css +0 -80
  49. package/src/svelte-plugin-ui/components/ToolbarShell.jsx +0 -84
  50. package/src/svelte-plugin-ui/components/Viewfinder.css +0 -412
  51. package/src/svelte-plugin-ui/components/Viewfinder.jsx +0 -513
  52. package/src/svelte-plugin-ui/index.ts +0 -20
  53. package/src/svelte-plugin-ui/mount.ts +0 -120
  54. package/src/svelte-plugin-ui/stores/modeStore.ts +0 -91
  55. package/src/svelte-plugin-ui/stores/toolStore.ts +0 -71
  56. package/src/svelte-plugin-ui/stores/types.ts +0 -55
  57. package/src/svelte-plugin-ui/styles/base.css +0 -69
@@ -1,126 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest'
2
- import { render, screen } from '@testing-library/react'
3
- import React from 'react'
4
- import {
5
- registerMode,
6
- initTools,
7
- setToolAction,
8
- setToolState,
9
- } from '@dfosco/storyboard-core'
10
- import { _resetModes } from '@test/modes'
11
- import ToolbarShell from '../components/ToolbarShell.jsx'
12
-
13
- afterEach(() => {
14
- _resetModes()
15
- const url = new URL(window.location.href)
16
- url.searchParams.delete('mode')
17
- window.history.replaceState(null, '', url.toString())
18
- })
19
-
20
- describe('ToolbarShell', () => {
21
- it('renders nothing when no tools are declared', () => {
22
- registerMode('prototype', { label: 'Navigate' })
23
- const { container } = render(React.createElement(ToolbarShell))
24
- expect(container.querySelector('[role="toolbar"]')).toBeNull()
25
- })
26
-
27
- it('renders tool buttons from the tool registry', () => {
28
- registerMode('prototype', { label: 'Navigate' })
29
- initTools({
30
- '*': [
31
- { id: 'zoom', label: 'Zoom', group: 'tools' },
32
- { id: 'pan', label: 'Pan', group: 'tools' },
33
- ],
34
- })
35
- setToolAction('zoom', () => {})
36
- setToolAction('pan', () => {})
37
-
38
- render(React.createElement(ToolbarShell))
39
-
40
- expect(screen.getByTitle('Zoom')).toBeInTheDocument()
41
- expect(screen.getByTitle('Pan')).toBeInTheDocument()
42
- })
43
-
44
- it('renders dev tools section', () => {
45
- registerMode('prototype', { label: 'Navigate' })
46
- initTools({
47
- '*': [{ id: 'debug', label: 'Debug', group: 'dev' }],
48
- })
49
- setToolAction('debug', () => {})
50
-
51
- render(React.createElement(ToolbarShell))
52
-
53
- expect(screen.getByTitle('Debug')).toBeInTheDocument()
54
- expect(screen.getByText('Dev')).toBeInTheDocument()
55
- })
56
-
57
- it('renders both tool groups', () => {
58
- registerMode('prototype', { label: 'Navigate' })
59
- initTools({
60
- '*': [
61
- { id: 'zoom', label: 'Zoom', group: 'tools' },
62
- { id: 'debug', label: 'Debug', group: 'dev' },
63
- ],
64
- })
65
- setToolAction('zoom', () => {})
66
- setToolAction('debug', () => {})
67
-
68
- render(React.createElement(ToolbarShell))
69
-
70
- const toolbars = screen.getAllByRole('toolbar')
71
- expect(toolbars).toHaveLength(2)
72
- expect(screen.getByText('Tools')).toBeInTheDocument()
73
- expect(screen.getByText('Dev')).toBeInTheDocument()
74
- })
75
-
76
- it('disables tools without an action', () => {
77
- registerMode('prototype', { label: 'Navigate' })
78
- initTools({
79
- '*': [{ id: 'no-action', label: 'No Action', group: 'tools' }],
80
- })
81
-
82
- render(React.createElement(ToolbarShell))
83
-
84
- const btn = screen.getByTitle('No Action')
85
- expect(btn).toBeDisabled()
86
- })
87
-
88
- it('disables tools with enabled: false state', () => {
89
- registerMode('prototype', { label: 'Navigate' })
90
- initTools({
91
- '*': [{ id: 'disabled-tool', label: 'Disabled', group: 'tools' }],
92
- })
93
- setToolAction('disabled-tool', () => {})
94
- setToolState('disabled-tool', { enabled: false })
95
-
96
- render(React.createElement(ToolbarShell))
97
-
98
- const btn = screen.getByTitle('Disabled')
99
- expect(btn).toBeDisabled()
100
- })
101
-
102
- it('hides tools with hidden: true state', () => {
103
- registerMode('prototype', { label: 'Navigate' })
104
- initTools({
105
- '*': [{ id: 'hidden-tool', label: 'Hidden', group: 'tools' }],
106
- })
107
- setToolState('hidden-tool', { hidden: true })
108
-
109
- const { container } = render(React.createElement(ToolbarShell))
110
-
111
- expect(container.querySelector('[role="toolbar"]')).toBeNull()
112
- })
113
-
114
- it('renders badge when present', () => {
115
- registerMode('prototype', { label: 'Navigate' })
116
- initTools({
117
- '*': [{ id: 'badged', label: 'Badged', group: 'tools' }],
118
- })
119
- setToolAction('badged', () => {})
120
- setToolState('badged', { badge: 5 })
121
-
122
- render(React.createElement(ToolbarShell))
123
-
124
- expect(screen.getByText('5')).toBeInTheDocument()
125
- })
126
- })
@@ -1,58 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest'
2
- import {
3
- registerMode,
4
- } from '@dfosco/storyboard-core'
5
- import { _resetModes } from '@test/modes'
6
- import { mountDesignModesUI, unmountDesignModesUI } from '../../ui/design-modes.js'
7
-
8
- afterEach(() => {
9
- unmountDesignModesUI()
10
- _resetModes()
11
- document.body.innerHTML = ''
12
- const url = new URL(window.location.href)
13
- url.searchParams.delete('mode')
14
- window.history.replaceState(null, '', url.toString())
15
- })
16
-
17
- describe('mountDesignModesUI', () => {
18
- it('mounts ModeSwitch and ToolbarShell into the target', () => {
19
- registerMode('prototype', { label: 'Navigate' })
20
- registerMode('inspect', { label: 'Develop' })
21
-
22
- mountDesignModesUI(document.body)
23
-
24
- const roots = document.querySelectorAll('.sb-plugin-root')
25
- expect(roots.length).toBe(2)
26
- })
27
-
28
- it('is idempotent — calling twice does not double-mount', () => {
29
- registerMode('prototype', { label: 'Navigate' })
30
- registerMode('inspect', { label: 'Develop' })
31
-
32
- mountDesignModesUI(document.body)
33
- mountDesignModesUI(document.body)
34
-
35
- const roots = document.querySelectorAll('.sb-plugin-root')
36
- expect(roots.length).toBe(2)
37
- })
38
-
39
- it('unmount removes all mounted components', () => {
40
- registerMode('prototype', { label: 'Navigate' })
41
- registerMode('inspect', { label: 'Develop' })
42
-
43
- const teardown = mountDesignModesUI(document.body)
44
- expect(document.querySelectorAll('.sb-plugin-root').length).toBe(2)
45
-
46
- teardown()
47
- expect(document.querySelectorAll('.sb-plugin-root').length).toBe(0)
48
- })
49
-
50
- it('defaults to document.body when no container given', () => {
51
- registerMode('prototype', { label: 'Navigate' })
52
- registerMode('inspect', { label: 'Develop' })
53
-
54
- mountDesignModesUI()
55
-
56
- expect(document.body.querySelectorAll('.sb-plugin-root').length).toBe(2)
57
- })
58
- })
@@ -1,53 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest'
2
- import { get } from 'svelte/store'
3
- import {
4
- registerMode,
5
- activateMode,
6
- } from '@dfosco/storyboard-core'
7
- import { _resetModes } from '@test/modes'
8
- import { modeState, switchMode } from '../stores/modeStore.js'
9
-
10
- afterEach(() => {
11
- _resetModes()
12
- const url = new URL(window.location.href)
13
- url.searchParams.delete('mode')
14
- window.history.replaceState(null, '', url.toString())
15
- })
16
-
17
- describe('modeState store', () => {
18
- it('returns default mode when no modes are registered', () => {
19
- const state = get(modeState)
20
- expect(state.mode).toBe('prototype')
21
- expect(state.modes).toEqual([])
22
- expect(state.currentModeConfig).toBeUndefined()
23
- })
24
-
25
- it('reflects registered modes', () => {
26
- registerMode('prototype', { label: 'Navigate' })
27
- registerMode('inspect', { label: 'Develop' })
28
-
29
- const state = get(modeState)
30
- expect(state.modes).toHaveLength(2)
31
- expect(state.modes[0]).toMatchObject({ name: 'prototype', label: 'Navigate' })
32
- expect(state.modes[1]).toMatchObject({ name: 'inspect', label: 'Develop' })
33
- })
34
-
35
- it('tracks the active mode', () => {
36
- registerMode('prototype', { label: 'Navigate' })
37
- registerMode('inspect', { label: 'Develop' })
38
-
39
- expect(get(modeState).mode).toBe('prototype')
40
-
41
- activateMode('inspect')
42
- expect(get(modeState).mode).toBe('inspect')
43
- expect(get(modeState).currentModeConfig?.name).toBe('inspect')
44
- })
45
-
46
- it('updates via switchMode helper', () => {
47
- registerMode('prototype', { label: 'Navigate' })
48
- registerMode('present', { label: 'Collaborate' })
49
-
50
- switchMode('present')
51
- expect(get(modeState).mode).toBe('present')
52
- })
53
- })
@@ -1,29 +0,0 @@
1
- import { describe, it, expect, afterEach } from 'vitest'
2
- import { mountSveltePlugin, _resetStyles } from '../mount.js'
3
-
4
- afterEach(() => {
5
- _resetStyles()
6
- document.body.innerHTML = ''
7
- })
8
-
9
- describe('mountSveltePlugin', () => {
10
- it('creates a wrapper element inside the target', () => {
11
- // We can't easily test with real Svelte components in unit tests
12
- // without the Svelte compiler, but we can test the mount utility
13
- // mechanics using a minimal mock.
14
-
15
- // Test style injection
16
- _resetStyles()
17
- expect(document.getElementById('sb-svelte-ui-styles')).toBeNull()
18
- })
19
-
20
- it('_resetStyles removes injected styles', () => {
21
- const link = document.createElement('link')
22
- link.id = 'sb-svelte-ui-styles'
23
- document.head.appendChild(link)
24
-
25
- expect(document.getElementById('sb-svelte-ui-styles')).not.toBeNull()
26
- _resetStyles()
27
- expect(document.getElementById('sb-svelte-ui-styles')).toBeNull()
28
- })
29
- })
@@ -1,11 +0,0 @@
1
- .storyboard-icon {
2
- display: inline-flex;
3
- align-items: center;
4
- vertical-align: middle;
5
- }
6
- .storyboard-icon:not(.stroke-icon) svg {
7
- fill: currentColor;
8
- }
9
- .storyboard-icon.stroke-icon svg {
10
- fill: none;
11
- }
@@ -1,90 +0,0 @@
1
- .sb-mode-switch {
2
- position: fixed;
3
- bottom: 20px;
4
- left: 50%;
5
- transform: translateX(-50%);
6
- z-index: 9999;
7
- display: flex;
8
- align-items: center;
9
- gap: 0;
10
- background: var(--bgColor-muted, #161b22);
11
- border: 1px solid var(--borderColor-default, #30363d);
12
- border-radius: 999px;
13
- padding: 4px;
14
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
15
- font-family: "Hubot Sans", -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
16
- }
17
-
18
- html.storyboard-mode-present .sb-mode-switch,
19
- html.storyboard-mode-plan .sb-mode-switch,
20
- html.storyboard-mode-inspect .sb-mode-switch {
21
- background: color-mix(in srgb, var(--sb--mode-color) 40%, black);
22
- transition: background 0.2s ease;
23
- }
24
-
25
- .sb-mode-btn {
26
- appearance: none;
27
- border: none;
28
- background: transparent;
29
- color: var(--fgColor-muted, #848d97);
30
- font-size: 13px;
31
- font-weight: 500;
32
- padding: 6px 14px;
33
- border-radius: 999px;
34
- cursor: pointer;
35
- transition: background 0.15s ease, color 0.15s ease;
36
- white-space: nowrap;
37
- line-height: 1;
38
- }
39
-
40
- .sb-mode-btn:hover {
41
- color: var(--fgColor-default, #e6edf3);
42
- background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.1));
43
- }
44
-
45
- .sb-mode-btn-active {
46
- background: var(--bgColor-accent-muted, rgba(56, 139, 253, 0.15));
47
- color: var(--fgColor-accent, #58a6ff);
48
- }
49
-
50
- .sb-mode-btn-active:hover {
51
- background: var(--bgColor-accent-muted, rgba(56, 139, 253, 0.2));
52
- color: var(--fgColor-accent, #58a6ff);
53
- }
54
-
55
- /* Mode-aware button colors */
56
- html.storyboard-mode-prototype .sb-mode-btn,
57
- html.storyboard-mode-present .sb-mode-btn,
58
- html.storyboard-mode-plan .sb-mode-btn,
59
- html.storyboard-mode-inspect .sb-mode-btn {
60
- color: rgba(255, 255, 255, 0.7);
61
- }
62
-
63
- html.storyboard-mode-prototype .sb-mode-btn:hover,
64
- html.storyboard-mode-present .sb-mode-btn:hover,
65
- html.storyboard-mode-plan .sb-mode-btn:hover,
66
- html.storyboard-mode-inspect .sb-mode-btn:hover {
67
- color: #fff;
68
- background: rgba(255, 255, 255, 0.1);
69
- }
70
-
71
- html.storyboard-mode-prototype .sb-mode-btn-active,
72
- html.storyboard-mode-present .sb-mode-btn-active,
73
- html.storyboard-mode-plan .sb-mode-btn-active,
74
- html.storyboard-mode-inspect .sb-mode-btn-active {
75
- background: rgba(255, 255, 255, 0.85);
76
- color: color-mix(in srgb, var(--sb--mode-color) 70%, black);
77
- }
78
-
79
- html.storyboard-mode-prototype .sb-mode-btn-active:hover,
80
- html.storyboard-mode-present .sb-mode-btn-active:hover,
81
- html.storyboard-mode-plan .sb-mode-btn-active:hover,
82
- html.storyboard-mode-inspect .sb-mode-btn-active:hover {
83
- background: rgba(255, 255, 255, 0.85);
84
- color: color-mix(in srgb, var(--sb--mode-color) 70%, black);
85
- }
86
-
87
- /* Hide when chrome is toggled off via ⌘ + . */
88
- html.storyboard-chrome-hidden .sb-mode-switch {
89
- display: none;
90
- }
@@ -1,47 +0,0 @@
1
- /**
2
- * ModeSwitch — segmented toggle for switching between design modes.
3
- *
4
- * Renders as a fixed pill at the bottom-center of the viewport.
5
- * Only visible when two or more modes are registered.
6
- */
7
-
8
- import { useSyncExternalStore } from 'react'
9
- import './ModeSwitch.css'
10
- import { modeState, switchMode } from '../stores/modeStore.js'
11
-
12
- function subscribeModeState(callback) {
13
- return modeState.subscribe(callback)
14
- }
15
-
16
- function getSnapshotModeState() {
17
- let current
18
- const unsub = modeState.subscribe((v) => { current = v })
19
- unsub()
20
- return current
21
- }
22
-
23
- export function ModeSwitch() {
24
- const state = useSyncExternalStore(subscribeModeState, getSnapshotModeState)
25
-
26
- if (!state?.switcherVisible || !state?.modes || state.modes.length < 2) {
27
- return null
28
- }
29
-
30
- return (
31
- <div className="sb-mode-switch" role="tablist" aria-label="Design mode">
32
- {state.modes.map((m) => (
33
- <button
34
- key={m.name}
35
- role="tab"
36
- aria-selected={state.mode === m.name}
37
- className={`sb-mode-btn${state.mode === m.name ? ' sb-mode-btn-active' : ''}`}
38
- onClick={() => switchMode(m.name)}
39
- >
40
- {m.label}
41
- </button>
42
- ))}
43
- </div>
44
- )
45
- }
46
-
47
- export default ModeSwitch
@@ -1,80 +0,0 @@
1
- .sb-toolbar-shell {
2
- position: fixed;
3
- right: 20px;
4
- bottom: 80px;
5
- z-index: 9998;
6
- display: flex;
7
- flex-direction: column;
8
- gap: 8px;
9
- align-items: flex-end;
10
- font-family: "Mona Sans", -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
11
- }
12
-
13
- .sb-toolbar {
14
- display: flex;
15
- flex-direction: column;
16
- gap: 2px;
17
- background: var(--bgColor-muted, #161b22);
18
- border: 1px solid var(--borderColor-default, #30363d);
19
- border-radius: 12px;
20
- padding: 4px;
21
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
22
- }
23
-
24
- .sb-tool-btn {
25
- appearance: none;
26
- border: none;
27
- background: transparent;
28
- color: var(--fgColor-muted, #848d97);
29
- font-size: 12px;
30
- font-weight: 500;
31
- padding: 6px 10px;
32
- border-radius: 8px;
33
- cursor: pointer;
34
- transition: background 0.15s ease, color 0.15s ease;
35
- white-space: nowrap;
36
- text-align: left;
37
- line-height: 1;
38
- display: flex;
39
- align-items: center;
40
- gap: 6px;
41
- }
42
-
43
- .sb-tool-btn:hover:not(:disabled) {
44
- color: var(--fgColor-default, #e6edf3);
45
- background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.1));
46
- }
47
-
48
- .sb-tool-btn:disabled {
49
- opacity: 0.4;
50
- cursor: default;
51
- }
52
-
53
- .sb-tool-btn-active {
54
- color: var(--fgColor-default, #e6edf3);
55
- background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.15));
56
- }
57
-
58
- .sb-tool-btn-busy {
59
- opacity: 0.6;
60
- }
61
-
62
- .sb-tool-badge {
63
- font-size: 10px;
64
- font-weight: 600;
65
- background: var(--bgColor-accent-muted, rgba(56, 139, 253, 0.15));
66
- color: var(--fgColor-accent, #58a6ff);
67
- padding: 1px 5px;
68
- border-radius: 10px;
69
- line-height: 1.2;
70
- }
71
-
72
- .sb-toolbar-label {
73
- font-size: 10px;
74
- font-weight: 600;
75
- text-transform: uppercase;
76
- letter-spacing: 0.05em;
77
- color: var(--fgColor-muted, #848d97);
78
- padding: 4px 10px 2px;
79
- opacity: 0.6;
80
- }
@@ -1,84 +0,0 @@
1
- /**
2
- * ToolbarShell — right-side toolbar container with two stacked groups:
3
- * 1. Mode-specific tools (group: 'tools')
4
- * 2. Developer tools (group: 'dev')
5
- *
6
- * Reads from the tool store, which sources from the declarative tool
7
- * registry (modes.config.json) + runtime state (setToolState/setToolAction).
8
- *
9
- * Fixed to the right side of the viewport, above the ModeSwitch.
10
- * Only renders when the current mode has visible tools.
11
- */
12
-
13
- import { useSyncExternalStore } from 'react'
14
- import './ToolbarShell.css'
15
- import { toolState } from '../stores/toolStore.js'
16
-
17
- function subscribeToolState(callback) {
18
- return toolState.subscribe(callback)
19
- }
20
-
21
- function getSnapshotToolState() {
22
- let current
23
- const unsub = toolState.subscribe((v) => { current = v })
24
- unsub()
25
- return current
26
- }
27
-
28
- function handleClick(tool) {
29
- if (tool.action && tool.state.enabled && !tool.state.busy) {
30
- tool.action()
31
- }
32
- }
33
-
34
- function ToolButton({ tool }) {
35
- return (
36
- <button
37
- className={
38
- 'sb-tool-btn' +
39
- (tool.state.active ? ' sb-tool-btn-active' : '') +
40
- (tool.state.busy ? ' sb-tool-btn-busy' : '')
41
- }
42
- onClick={() => handleClick(tool)}
43
- disabled={!tool.state.enabled || tool.state.busy || !tool.action}
44
- title={tool.label}
45
- >
46
- {tool.label}
47
- {tool.state.badge != null && (
48
- <span className="sb-tool-badge">{tool.state.badge}</span>
49
- )}
50
- </button>
51
- )
52
- }
53
-
54
- export function ToolbarShell() {
55
- const state = useSyncExternalStore(subscribeToolState, getSnapshotToolState)
56
-
57
- if (!state || (state.tools.length === 0 && state.devTools.length === 0)) {
58
- return null
59
- }
60
-
61
- return (
62
- <div className="sb-toolbar-shell">
63
- {state.tools.length > 0 && (
64
- <div className="sb-toolbar" role="toolbar" aria-label="Mode tools">
65
- <span className="sb-toolbar-label">Tools</span>
66
- {state.tools.map((tool) => (
67
- <ToolButton key={tool.id} tool={tool} />
68
- ))}
69
- </div>
70
- )}
71
-
72
- {state.devTools.length > 0 && (
73
- <div className="sb-toolbar" role="toolbar" aria-label="Developer tools">
74
- <span className="sb-toolbar-label">Dev</span>
75
- {state.devTools.map((tool) => (
76
- <ToolButton key={tool.id} tool={tool} />
77
- ))}
78
- </div>
79
- )}
80
- </div>
81
- )
82
- }
83
-
84
- export default ToolbarShell