@dfosco/storyboard-core 3.3.1 → 3.4.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.
- package/dist/storyboard-ui.css +9 -1
- package/dist/storyboard-ui.js +14701 -11431
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +1 -1
- package/scaffold/toolbar.config.json +2 -2
- package/src/CanvasCreateMenu.svelte +1 -1
- package/src/CanvasZoomControl.svelte +105 -0
- package/src/CommandMenu.svelte +87 -25
- package/src/CoreUIBar.svelte +352 -346
- package/src/CreateMenuButton.svelte +6 -2
- package/src/InspectorPanel.svelte +87 -37
- package/src/SidePanel.svelte +1 -1
- package/src/ThemeMenuButton.svelte +35 -3
- package/src/commandActions.js +14 -0
- package/src/core-ui-colors.css +30 -2
- package/src/devtools.js +7 -1
- package/src/index.js +10 -1
- package/src/inspector/fiberWalker.js +49 -6
- package/src/inspector/highlighter.js +145 -29
- package/src/lib/components/ui/button/button.svelte +1 -1
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.svelte +1 -1
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.svelte +1 -1
- package/src/lib/components/ui/trigger-button/trigger-button.svelte +31 -3
- package/src/modes.css +8 -0
- package/src/mountStoryboardCore.js +15 -1
- package/src/stores/themeStore.ts +66 -0
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +16 -11
- package/src/toolRegistry.js +226 -0
- package/src/toolStateStore.js +180 -0
- package/src/toolStateStore.test.js +204 -0
- package/src/toolbarConfigStore.js +135 -0
- package/src/tools/handlers/canvasAddWidget.js +11 -0
- package/src/tools/handlers/canvasZoom.js +34 -0
- package/src/tools/handlers/comments.js +16 -0
- package/src/tools/handlers/create.js +39 -0
- package/src/tools/handlers/devtools.js +80 -0
- package/src/tools/handlers/docs.js +11 -0
- package/src/tools/handlers/featureFlags.js +21 -0
- package/src/tools/handlers/flows.js +59 -0
- package/src/tools/handlers/inspector.js +19 -0
- package/src/tools/handlers/theme.js +9 -0
- package/src/tools/registry.js +21 -0
- package/src/tools/surfaces/canvasToolbar.js +10 -0
- package/src/tools/surfaces/commandList.js +10 -0
- package/src/tools/surfaces/mainToolbar.js +11 -0
- package/src/tools/surfaces/registry.js +19 -0
- package/src/vite/server-plugin.js +54 -5
- package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +2 -2
- package/src/workshop/features/createPrototype/server.js +10 -15
- package/toolbar.config.json +107 -48
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Registry — config-driven state management for toolbar tools.
|
|
3
|
+
*
|
|
4
|
+
* Every tool is declared in toolbar.config.json under the `tools` key.
|
|
5
|
+
* Each tool specifies a `toolbar` target (main-toolbar, secondary-toolbar,
|
|
6
|
+
* command-list) and a `render` type (button, menu, sidepanel, submenu, link).
|
|
7
|
+
*
|
|
8
|
+
* Code modules register themselves via registerToolModule() to provide
|
|
9
|
+
* component, handler, setup, and guard functions.
|
|
10
|
+
*
|
|
11
|
+
* Framework-agnostic (zero npm dependencies).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// Internal state
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
/** @type {Record<string, object>} tool id → config from toolbar.config.json */
|
|
19
|
+
let _toolConfigs = {}
|
|
20
|
+
|
|
21
|
+
/** @type {Map<string, object>} tool id → code module { component?, handler?, setup?, guard? } */
|
|
22
|
+
const _modules = new Map()
|
|
23
|
+
|
|
24
|
+
/** @type {Map<string, any>} tool id → resolved component (after lazy loading) */
|
|
25
|
+
const _components = new Map()
|
|
26
|
+
|
|
27
|
+
/** @type {Map<string, boolean>} tool id → guard result */
|
|
28
|
+
const _guardResults = new Map()
|
|
29
|
+
|
|
30
|
+
/** @type {Set<Function>} */
|
|
31
|
+
const _listeners = new Set()
|
|
32
|
+
|
|
33
|
+
let _snapshotVersion = 0
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Initialization
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Seed the registry from toolbar config.
|
|
41
|
+
* Called once at app startup.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} config - The full toolbar config object
|
|
44
|
+
*/
|
|
45
|
+
export function initToolRegistry(config) {
|
|
46
|
+
if (config.tools) {
|
|
47
|
+
_toolConfigs = { ...config.tools }
|
|
48
|
+
}
|
|
49
|
+
_notify()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Module registration
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Register a code module for a declared tool.
|
|
58
|
+
*
|
|
59
|
+
* @param {string} id - Tool id (matches key in toolbar.config.json tools)
|
|
60
|
+
* @param {object} mod
|
|
61
|
+
* @param {Function} [mod.component] - () => import('./SomeComponent.svelte')
|
|
62
|
+
* @param {object|Function} [mod.handler] - Command action handler
|
|
63
|
+
* @param {Function} [mod.setup] - async (ctx) => void — called once at mount
|
|
64
|
+
* @param {Function} [mod.guard] - async (ctx) => boolean — return false to hide
|
|
65
|
+
*/
|
|
66
|
+
export function registerToolModule(id, mod) {
|
|
67
|
+
_modules.set(id, mod)
|
|
68
|
+
_notify()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Store a resolved component for a tool (after lazy loading).
|
|
73
|
+
*
|
|
74
|
+
* @param {string} id
|
|
75
|
+
* @param {any} component
|
|
76
|
+
*/
|
|
77
|
+
export function setToolComponent(id, component) {
|
|
78
|
+
_components.set(id, component)
|
|
79
|
+
_notify()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Store a guard result for a tool.
|
|
84
|
+
*
|
|
85
|
+
* @param {string} id
|
|
86
|
+
* @param {boolean} result
|
|
87
|
+
*/
|
|
88
|
+
export function setToolGuardResult(id, result) {
|
|
89
|
+
_guardResults.set(id, result)
|
|
90
|
+
_notify()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the resolved component for a tool.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} id
|
|
97
|
+
* @returns {any|null}
|
|
98
|
+
*/
|
|
99
|
+
export function getToolComponent(id) {
|
|
100
|
+
return _components.get(id) || null
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Get the code module for a tool.
|
|
105
|
+
*
|
|
106
|
+
* @param {string} id
|
|
107
|
+
* @returns {object|null}
|
|
108
|
+
*/
|
|
109
|
+
export function getToolModule(id) {
|
|
110
|
+
return _modules.get(id) || null
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
// Resolution
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get tools for a specific toolbar target, filtered by mode and visibility.
|
|
119
|
+
*
|
|
120
|
+
* @param {string} toolbar - "main-toolbar" | "secondary-toolbar" | "command-list"
|
|
121
|
+
* @param {string} mode - Current mode name
|
|
122
|
+
* @param {object} [options]
|
|
123
|
+
* @param {boolean} [options.isLocalDev] - Whether running in local dev
|
|
124
|
+
* @returns {Array<{ id: string, config: object, module: object|null, component: any|null }>}
|
|
125
|
+
*/
|
|
126
|
+
export function getToolsForToolbar(toolbar, mode, options = {}) {
|
|
127
|
+
const { isLocalDev = false } = options
|
|
128
|
+
const results = []
|
|
129
|
+
|
|
130
|
+
for (const [id, config] of Object.entries(_toolConfigs)) {
|
|
131
|
+
if (config.toolbar !== toolbar) continue
|
|
132
|
+
if (config.localOnly && !isLocalDev) continue
|
|
133
|
+
if (!isToolVisibleInMode(config, mode)) continue
|
|
134
|
+
|
|
135
|
+
// Check guard result if one was registered
|
|
136
|
+
if (_guardResults.has(id) && !_guardResults.get(id)) continue
|
|
137
|
+
|
|
138
|
+
results.push({
|
|
139
|
+
id,
|
|
140
|
+
config,
|
|
141
|
+
module: _modules.get(id) || null,
|
|
142
|
+
component: _components.get(id) || null,
|
|
143
|
+
})
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return results
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Get the config for a specific tool.
|
|
151
|
+
*
|
|
152
|
+
* @param {string} id
|
|
153
|
+
* @returns {object|null}
|
|
154
|
+
*/
|
|
155
|
+
export function getToolConfig(id) {
|
|
156
|
+
return _toolConfigs[id] || null
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get all tool configs.
|
|
161
|
+
*
|
|
162
|
+
* @returns {Record<string, object>}
|
|
163
|
+
*/
|
|
164
|
+
export function getAllToolConfigs() {
|
|
165
|
+
return { ..._toolConfigs }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if a tool is visible in a given mode.
|
|
170
|
+
*
|
|
171
|
+
* @param {object} config - Tool config
|
|
172
|
+
* @param {string} mode - Current mode name
|
|
173
|
+
* @returns {boolean}
|
|
174
|
+
*/
|
|
175
|
+
function isToolVisibleInMode(config, mode) {
|
|
176
|
+
const modes = config.modes
|
|
177
|
+
if (!modes) return true
|
|
178
|
+
return modes.includes('*') || modes.includes(mode)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Reactivity
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Subscribe to registry changes. Compatible with useSyncExternalStore.
|
|
187
|
+
*
|
|
188
|
+
* @param {Function} callback
|
|
189
|
+
* @returns {Function} Unsubscribe
|
|
190
|
+
*/
|
|
191
|
+
export function subscribeToToolRegistry(callback) {
|
|
192
|
+
_listeners.add(callback)
|
|
193
|
+
return () => _listeners.delete(callback)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Snapshot for useSyncExternalStore.
|
|
198
|
+
*
|
|
199
|
+
* @returns {string}
|
|
200
|
+
*/
|
|
201
|
+
export function getToolRegistrySnapshot() {
|
|
202
|
+
return String(_snapshotVersion)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function _notify() {
|
|
206
|
+
_snapshotVersion++
|
|
207
|
+
for (const cb of _listeners) {
|
|
208
|
+
try { cb() } catch (err) {
|
|
209
|
+
console.error('[storyboard] Error in tool registry subscriber:', err)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
// Test helpers
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
|
|
218
|
+
/** Reset all state. Only for tests. */
|
|
219
|
+
export function _resetToolRegistry() {
|
|
220
|
+
_toolConfigs = {}
|
|
221
|
+
_modules.clear()
|
|
222
|
+
_components.clear()
|
|
223
|
+
_guardResults.clear()
|
|
224
|
+
_listeners.clear()
|
|
225
|
+
_snapshotVersion = 0
|
|
226
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool State Store — runtime state management for toolbar tools.
|
|
3
|
+
*
|
|
4
|
+
* Each tool declared in toolbar.config.json can be in one of five states:
|
|
5
|
+
* active — Normal, clickable (default)
|
|
6
|
+
* inactive — Visible but unclickable, disabled-looking (also for errors)
|
|
7
|
+
* hidden — Invisible but shortcuts still work, tool is loaded
|
|
8
|
+
* dimmed — Visible but dimmed opacity, still clickable
|
|
9
|
+
* disabled — Completely removed, not loaded on FE
|
|
10
|
+
*
|
|
11
|
+
* Tools default to "active" unless:
|
|
12
|
+
* 1. Config declares a "state" property
|
|
13
|
+
* 2. The tool is localOnly and the environment is not local dev → "disabled"
|
|
14
|
+
*
|
|
15
|
+
* State can be changed at runtime by application code via setToolbarToolState().
|
|
16
|
+
* Framework-agnostic (zero npm dependencies).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Constants
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/** All valid tool states. */
|
|
24
|
+
export const TOOL_STATES = Object.freeze({
|
|
25
|
+
ACTIVE: 'active',
|
|
26
|
+
INACTIVE: 'inactive',
|
|
27
|
+
HIDDEN: 'hidden',
|
|
28
|
+
DIMMED: 'dimmed',
|
|
29
|
+
DISABLED: 'disabled',
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const VALID_STATES = new Set(Object.values(TOOL_STATES))
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Internal state
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** @type {Map<string, string>} tool id → state */
|
|
39
|
+
let _states = new Map()
|
|
40
|
+
|
|
41
|
+
/** @type {Map<string, boolean>} tool id → localOnly flag */
|
|
42
|
+
let _localOnlyFlags = new Map()
|
|
43
|
+
|
|
44
|
+
/** @type {Set<Function>} */
|
|
45
|
+
const _listeners = new Set()
|
|
46
|
+
|
|
47
|
+
let _snapshotVersion = 0
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Initialization
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Seed tool states from toolbar.config.json's `tools` object.
|
|
55
|
+
* Called once at app startup.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} toolsConfig - The `tools` object from toolbar.config.json
|
|
58
|
+
* (keys are tool IDs, values are tool config objects)
|
|
59
|
+
* @param {{ isLocalDev?: boolean }} [options]
|
|
60
|
+
*/
|
|
61
|
+
export function initToolbarToolStates(toolsConfig, options = {}) {
|
|
62
|
+
const { isLocalDev = false } = options
|
|
63
|
+
|
|
64
|
+
_states = new Map()
|
|
65
|
+
_localOnlyFlags = new Map()
|
|
66
|
+
|
|
67
|
+
if (!toolsConfig || typeof toolsConfig !== 'object') {
|
|
68
|
+
_notify()
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [id, tool] of Object.entries(toolsConfig)) {
|
|
73
|
+
const isLocalOnly = tool.localOnly === true
|
|
74
|
+
_localOnlyFlags.set(id, isLocalOnly)
|
|
75
|
+
|
|
76
|
+
if (isLocalOnly && !isLocalDev) {
|
|
77
|
+
_states.set(id, TOOL_STATES.DISABLED)
|
|
78
|
+
} else {
|
|
79
|
+
const state = tool.state || TOOL_STATES.ACTIVE
|
|
80
|
+
_states.set(id, state)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
_notify()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Runtime state changes
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Set state for a tool at runtime.
|
|
93
|
+
* Validates that the state is one of the 5 valid values.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} id Tool id
|
|
96
|
+
* @param {string} state One of: active, inactive, hidden, dimmed, disabled
|
|
97
|
+
*/
|
|
98
|
+
export function setToolbarToolState(id, state) {
|
|
99
|
+
if (!VALID_STATES.has(state)) {
|
|
100
|
+
console.warn(`[storyboard] Invalid tool state "${state}" for tool "${id}". Valid states: ${[...VALID_STATES].join(', ')}`)
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
_states.set(id, state)
|
|
104
|
+
_notify()
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Access
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get the current state for a tool.
|
|
113
|
+
* Returns "active" for unknown tool IDs (safe default).
|
|
114
|
+
*
|
|
115
|
+
* @param {string} id Tool id
|
|
116
|
+
* @returns {string}
|
|
117
|
+
*/
|
|
118
|
+
export function getToolbarToolState(id) {
|
|
119
|
+
return _states.get(id) || TOOL_STATES.ACTIVE
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Returns whether the tool was marked localOnly in config.
|
|
124
|
+
* Returns false for unknown IDs.
|
|
125
|
+
*
|
|
126
|
+
* @param {string} id Tool id
|
|
127
|
+
* @returns {boolean}
|
|
128
|
+
*/
|
|
129
|
+
export function isToolbarToolLocalOnly(id) {
|
|
130
|
+
return _localOnlyFlags.get(id) || false
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
// Reactivity
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Subscribe to tool state changes. Compatible with useSyncExternalStore.
|
|
139
|
+
*
|
|
140
|
+
* @param {Function} callback
|
|
141
|
+
* @returns {Function} Unsubscribe
|
|
142
|
+
*/
|
|
143
|
+
export function subscribeToToolbarToolStates(callback) {
|
|
144
|
+
_listeners.add(callback)
|
|
145
|
+
return () => _listeners.delete(callback)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Snapshot for useSyncExternalStore.
|
|
150
|
+
*
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function getToolbarToolStatesSnapshot() {
|
|
154
|
+
return String(_snapshotVersion)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Internal
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
|
|
161
|
+
function _notify() {
|
|
162
|
+
_snapshotVersion++
|
|
163
|
+
for (const cb of _listeners) {
|
|
164
|
+
try { cb() } catch (err) {
|
|
165
|
+
console.error('[storyboard] Error in tool state subscriber:', err)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Test helpers
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/** Reset all state. Only for tests. */
|
|
175
|
+
export function _resetToolbarToolStates() {
|
|
176
|
+
_states = new Map()
|
|
177
|
+
_localOnlyFlags = new Map()
|
|
178
|
+
_listeners.clear()
|
|
179
|
+
_snapshotVersion = 0
|
|
180
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOOL_STATES,
|
|
3
|
+
initToolbarToolStates,
|
|
4
|
+
setToolbarToolState,
|
|
5
|
+
getToolbarToolState,
|
|
6
|
+
isToolbarToolLocalOnly,
|
|
7
|
+
subscribeToToolbarToolStates,
|
|
8
|
+
getToolbarToolStatesSnapshot,
|
|
9
|
+
_resetToolbarToolStates,
|
|
10
|
+
} from './toolStateStore.js'
|
|
11
|
+
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
_resetToolbarToolStates()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
describe('toolStateStore', () => {
|
|
17
|
+
describe('TOOL_STATES', () => {
|
|
18
|
+
it('exports all 5 state constants', () => {
|
|
19
|
+
expect(TOOL_STATES.ACTIVE).toBe('active')
|
|
20
|
+
expect(TOOL_STATES.INACTIVE).toBe('inactive')
|
|
21
|
+
expect(TOOL_STATES.HIDDEN).toBe('hidden')
|
|
22
|
+
expect(TOOL_STATES.DIMMED).toBe('dimmed')
|
|
23
|
+
expect(TOOL_STATES.DISABLED).toBe('disabled')
|
|
24
|
+
expect(Object.keys(TOOL_STATES)).toHaveLength(5)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('getToolbarToolState', () => {
|
|
29
|
+
it('returns "active" for unknown tool IDs', () => {
|
|
30
|
+
expect(getToolbarToolState('nonexistent')).toBe('active')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns "active" after init with no state in config', () => {
|
|
34
|
+
initToolbarToolStates({ myTool: {} })
|
|
35
|
+
expect(getToolbarToolState('myTool')).toBe('active')
|
|
36
|
+
})
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
describe('initToolbarToolStates', () => {
|
|
40
|
+
it('seeds states from config', () => {
|
|
41
|
+
initToolbarToolStates({
|
|
42
|
+
inspector: { state: 'hidden' },
|
|
43
|
+
comments: { state: 'dimmed' },
|
|
44
|
+
})
|
|
45
|
+
expect(getToolbarToolState('inspector')).toBe('hidden')
|
|
46
|
+
expect(getToolbarToolState('comments')).toBe('dimmed')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('defaults to active when no state specified', () => {
|
|
50
|
+
initToolbarToolStates({ inspector: { render: 'panel' } })
|
|
51
|
+
expect(getToolbarToolState('inspector')).toBe('active')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('reads state from config when specified', () => {
|
|
55
|
+
initToolbarToolStates({ inspector: { state: 'inactive' } })
|
|
56
|
+
expect(getToolbarToolState('inspector')).toBe('inactive')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('localOnly + !isLocalDev → disabled (overrides config state)', () => {
|
|
60
|
+
initToolbarToolStates(
|
|
61
|
+
{ devTool: { localOnly: true, state: 'active' } },
|
|
62
|
+
{ isLocalDev: false },
|
|
63
|
+
)
|
|
64
|
+
expect(getToolbarToolState('devTool')).toBe('disabled')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('localOnly + isLocalDev → uses config state (active by default)', () => {
|
|
68
|
+
initToolbarToolStates(
|
|
69
|
+
{ devTool: { localOnly: true } },
|
|
70
|
+
{ isLocalDev: true },
|
|
71
|
+
)
|
|
72
|
+
expect(getToolbarToolState('devTool')).toBe('active')
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('handles empty config', () => {
|
|
76
|
+
initToolbarToolStates({})
|
|
77
|
+
expect(getToolbarToolState('anything')).toBe('active')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('replaces previous state on re-init', () => {
|
|
81
|
+
initToolbarToolStates({ inspector: { state: 'hidden' } })
|
|
82
|
+
expect(getToolbarToolState('inspector')).toBe('hidden')
|
|
83
|
+
|
|
84
|
+
initToolbarToolStates({ inspector: { state: 'dimmed' } })
|
|
85
|
+
expect(getToolbarToolState('inspector')).toBe('dimmed')
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
describe('setToolbarToolState', () => {
|
|
90
|
+
it('updates state for a known tool', () => {
|
|
91
|
+
initToolbarToolStates({ inspector: {} })
|
|
92
|
+
setToolbarToolState('inspector', 'hidden')
|
|
93
|
+
expect(getToolbarToolState('inspector')).toBe('hidden')
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('updates state for unknown tool (creates entry)', () => {
|
|
97
|
+
setToolbarToolState('newTool', 'dimmed')
|
|
98
|
+
expect(getToolbarToolState('newTool')).toBe('dimmed')
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('warns on invalid state value', () => {
|
|
102
|
+
const spy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
103
|
+
setToolbarToolState('inspector', 'bogus')
|
|
104
|
+
expect(spy).toHaveBeenCalledOnce()
|
|
105
|
+
expect(spy.mock.calls[0][0]).toContain('Invalid tool state')
|
|
106
|
+
spy.mockRestore()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('notifies subscribers on change', () => {
|
|
110
|
+
const cb = vi.fn()
|
|
111
|
+
subscribeToToolbarToolStates(cb)
|
|
112
|
+
initToolbarToolStates({ inspector: {} })
|
|
113
|
+
const callsBefore = cb.mock.calls.length
|
|
114
|
+
|
|
115
|
+
setToolbarToolState('inspector', 'inactive')
|
|
116
|
+
expect(cb).toHaveBeenCalledTimes(callsBefore + 1)
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('isToolbarToolLocalOnly', () => {
|
|
121
|
+
it('returns true for localOnly tools', () => {
|
|
122
|
+
initToolbarToolStates(
|
|
123
|
+
{ devTool: { localOnly: true } },
|
|
124
|
+
{ isLocalDev: true },
|
|
125
|
+
)
|
|
126
|
+
expect(isToolbarToolLocalOnly('devTool')).toBe(true)
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it('returns false for non-localOnly tools', () => {
|
|
130
|
+
initToolbarToolStates({ inspector: {} })
|
|
131
|
+
expect(isToolbarToolLocalOnly('inspector')).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('returns false for unknown tools', () => {
|
|
135
|
+
expect(isToolbarToolLocalOnly('nonexistent')).toBe(false)
|
|
136
|
+
})
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
describe('subscribeToToolbarToolStates', () => {
|
|
140
|
+
it('calls callback on state changes', () => {
|
|
141
|
+
const cb = vi.fn()
|
|
142
|
+
subscribeToToolbarToolStates(cb)
|
|
143
|
+
initToolbarToolStates({ inspector: {} })
|
|
144
|
+
expect(cb).toHaveBeenCalled()
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('returns working unsubscribe function', () => {
|
|
148
|
+
const cb = vi.fn()
|
|
149
|
+
const unsub = subscribeToToolbarToolStates(cb)
|
|
150
|
+
unsub()
|
|
151
|
+
|
|
152
|
+
initToolbarToolStates({ inspector: {} })
|
|
153
|
+
expect(cb).not.toHaveBeenCalled()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('supports multiple subscribers', () => {
|
|
157
|
+
const cb1 = vi.fn()
|
|
158
|
+
const cb2 = vi.fn()
|
|
159
|
+
subscribeToToolbarToolStates(cb1)
|
|
160
|
+
subscribeToToolbarToolStates(cb2)
|
|
161
|
+
|
|
162
|
+
initToolbarToolStates({ inspector: {} })
|
|
163
|
+
expect(cb1).toHaveBeenCalled()
|
|
164
|
+
expect(cb2).toHaveBeenCalled()
|
|
165
|
+
})
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
describe('getToolbarToolStatesSnapshot', () => {
|
|
169
|
+
it('returns string', () => {
|
|
170
|
+
expect(typeof getToolbarToolStatesSnapshot()).toBe('string')
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
it('changes on mutation', () => {
|
|
174
|
+
const before = getToolbarToolStatesSnapshot()
|
|
175
|
+
initToolbarToolStates({ inspector: {} })
|
|
176
|
+
const after = getToolbarToolStatesSnapshot()
|
|
177
|
+
expect(after).not.toBe(before)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('does not change without mutation', () => {
|
|
181
|
+
const a = getToolbarToolStatesSnapshot()
|
|
182
|
+
const b = getToolbarToolStatesSnapshot()
|
|
183
|
+
expect(a).toBe(b)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('_resetToolbarToolStates', () => {
|
|
188
|
+
it('clears all state', () => {
|
|
189
|
+
initToolbarToolStates({ inspector: { state: 'hidden', localOnly: true } }, { isLocalDev: true })
|
|
190
|
+
_resetToolbarToolStates()
|
|
191
|
+
expect(getToolbarToolState('inspector')).toBe('active')
|
|
192
|
+
expect(isToolbarToolLocalOnly('inspector')).toBe(false)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('clears all listeners', () => {
|
|
196
|
+
const cb = vi.fn()
|
|
197
|
+
subscribeToToolbarToolStates(cb)
|
|
198
|
+
_resetToolbarToolStates()
|
|
199
|
+
|
|
200
|
+
initToolbarToolStates({ inspector: {} })
|
|
201
|
+
expect(cb).not.toHaveBeenCalled()
|
|
202
|
+
})
|
|
203
|
+
})
|
|
204
|
+
})
|