@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.
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +7973 -8418
- package/dist/storyboard-ui.js.map +1 -1
- package/package.json +2 -12
- package/scaffold/AGENTS.md +432 -0
- package/scaffold/manifest.json +13 -8
- package/src/ActionMenuButton.jsx +1 -1
- package/src/AutosyncMenuButton.jsx +1 -1
- package/src/CanvasAgentsMenu.jsx +1 -1
- package/src/CanvasCreateMenu.jsx +1 -1
- package/src/CanvasSnap.jsx +1 -1
- package/src/CanvasZoomToFit.jsx +1 -1
- package/src/CommandMenu.jsx +2 -2
- package/src/CommandPalette.jsx +1 -1
- package/src/CommandPaletteTrigger.jsx +1 -1
- package/src/CommentsMenuButton.jsx +1 -1
- package/src/CoreUIBar.jsx +18 -2
- package/src/CreateMenuButton.jsx +1 -1
- package/src/HideChromeTrigger.jsx +1 -1
- package/src/{svelte-plugin-ui/components/Icon.jsx → Icon.jsx} +8 -10
- package/src/ThemeMenuButton.jsx +1 -1
- package/src/comments/ui/authModal.js +1 -1
- package/src/configSchema.js +2 -0
- package/src/configStore.js +1 -1
- package/src/devtools-consumer.js +2 -2
- package/src/index.js +3 -3
- package/src/loader.js +9 -2
- package/src/mountStoryboardCore.js +3 -3
- package/src/sidepanel.css +1 -1
- package/src/toolbarConfigStore.js +1 -1
- package/src/ui/design-modes.ts +4 -51
- package/src/ui/viewfinder.ts +4 -55
- package/src/ui-entry.js +5 -5
- package/src/vite/server-plugin.js +9 -0
- package/src/workshop/features/createFlow/index.js +1 -1
- package/src/workshop/features/createPrototype/index.js +1 -1
- package/src/workshop/features/registry-server.js +1 -1
- package/src/workshop/ui/mount.ts +3 -65
- package/scaffold/svelte.config.js +0 -1
- package/src/svelte-plugin-ui/__tests__/ModeSwitch.test.ts +0 -75
- package/src/svelte-plugin-ui/__tests__/ToolbarShell.test.ts +0 -126
- package/src/svelte-plugin-ui/__tests__/designModes.test.ts +0 -58
- package/src/svelte-plugin-ui/__tests__/modeStore.test.ts +0 -53
- package/src/svelte-plugin-ui/__tests__/mount.test.ts +0 -29
- package/src/svelte-plugin-ui/components/Icon.css +0 -11
- package/src/svelte-plugin-ui/components/ModeSwitch.css +0 -90
- package/src/svelte-plugin-ui/components/ModeSwitch.jsx +0 -47
- package/src/svelte-plugin-ui/components/ToolbarShell.css +0 -80
- package/src/svelte-plugin-ui/components/ToolbarShell.jsx +0 -84
- package/src/svelte-plugin-ui/components/Viewfinder.css +0 -412
- package/src/svelte-plugin-ui/components/Viewfinder.jsx +0 -513
- package/src/svelte-plugin-ui/index.ts +0 -20
- package/src/svelte-plugin-ui/mount.ts +0 -120
- package/src/svelte-plugin-ui/stores/modeStore.ts +0 -91
- package/src/svelte-plugin-ui/stores/toolStore.ts +0 -71
- package/src/svelte-plugin-ui/stores/types.ts +0 -55
- 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,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
|