@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.
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +14899 -11508
- 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 +350 -347
- package/src/CreateMenuButton.svelte +6 -2
- package/src/InspectorPanel.svelte +123 -59
- 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 +257 -33
- 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/sidepanel.css +2 -2
- 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 +62 -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 +36 -6
- package/toolbar.config.json +101 -48
|
@@ -1,43 +1,267 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Lightweight
|
|
2
|
+
* Lightweight highlight.js highlighter for the inspector panel.
|
|
3
3
|
*
|
|
4
|
-
* Uses
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Uses highlight.js/core with only the languages the inspector needs
|
|
5
|
+
* (javascript, typescript, xml for JSX), producing small bundles with
|
|
6
|
+
* no WASM dependencies.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Returns null when shiki is unavailable.
|
|
8
|
+
* Theme is resolved at render time via toolbar.config.json's
|
|
9
|
+
* `highlighting` key and the theme sync settings. Colors are applied
|
|
10
|
+
* as inline styles — no global CSS injection, no theme conflicts.
|
|
12
11
|
*/
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
import hljs from 'highlight.js/lib/core'
|
|
14
|
+
import javascript from 'highlight.js/lib/languages/javascript'
|
|
15
|
+
import typescript from 'highlight.js/lib/languages/typescript'
|
|
16
|
+
import xml from 'highlight.js/lib/languages/xml'
|
|
17
|
+
import { getToolbarConfig } from '../toolbarConfigStore.js'
|
|
16
18
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
19
|
+
hljs.registerLanguage('javascript', javascript)
|
|
20
|
+
hljs.registerLanguage('typescript', typescript)
|
|
21
|
+
hljs.registerLanguage('xml', xml)
|
|
22
|
+
hljs.registerLanguage('jsx', javascript)
|
|
23
|
+
hljs.registerLanguage('tsx', typescript)
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Color palettes — inline styles, no external CSS needed
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const THEMES = {
|
|
30
|
+
'github-dark-dimmed': {
|
|
31
|
+
bg: '#22272e',
|
|
32
|
+
fg: '#adbac7',
|
|
33
|
+
headerBg: '#2d333b',
|
|
34
|
+
headerFg: '#768390',
|
|
35
|
+
border: '#373e47',
|
|
36
|
+
lineHighlight: 'rgba(99, 110, 123, 0.15)',
|
|
37
|
+
linkHover: '#adbac7',
|
|
38
|
+
keyword: '#f47067',
|
|
39
|
+
string: '#96d0ff',
|
|
40
|
+
number: '#6cb6ff',
|
|
41
|
+
comment: '#768390',
|
|
42
|
+
function: '#dcbdfb',
|
|
43
|
+
title: '#dcbdfb',
|
|
44
|
+
built_in: '#6cb6ff',
|
|
45
|
+
literal: '#6cb6ff',
|
|
46
|
+
type: '#6cb6ff',
|
|
47
|
+
attr: '#6cb6ff',
|
|
48
|
+
tag: '#8ddb8c',
|
|
49
|
+
name: '#8ddb8c',
|
|
50
|
+
attribute: '#6cb6ff',
|
|
51
|
+
variable: '#f69d50',
|
|
52
|
+
'template-variable': '#f69d50',
|
|
53
|
+
params: '#adbac7',
|
|
54
|
+
meta: '#768390',
|
|
55
|
+
regexp: '#96d0ff',
|
|
56
|
+
symbol: '#6cb6ff',
|
|
57
|
+
operator: '#adbac7',
|
|
58
|
+
punctuation: '#adbac7',
|
|
59
|
+
selector: '#8ddb8c',
|
|
60
|
+
property: '#6cb6ff',
|
|
61
|
+
},
|
|
62
|
+
'github-dark': {
|
|
63
|
+
bg: '#0d1117',
|
|
64
|
+
fg: '#e6edf3',
|
|
65
|
+
headerBg: '#161b22',
|
|
66
|
+
headerFg: '#8b949e',
|
|
67
|
+
border: '#30363d',
|
|
68
|
+
lineHighlight: 'rgba(110, 118, 129, 0.15)',
|
|
69
|
+
linkHover: '#c9d1d9',
|
|
70
|
+
keyword: '#ff7b72',
|
|
71
|
+
string: '#a5d6ff',
|
|
72
|
+
number: '#79c0ff',
|
|
73
|
+
comment: '#8b949e',
|
|
74
|
+
function: '#d2a8ff',
|
|
75
|
+
title: '#d2a8ff',
|
|
76
|
+
built_in: '#79c0ff',
|
|
77
|
+
literal: '#79c0ff',
|
|
78
|
+
type: '#79c0ff',
|
|
79
|
+
attr: '#79c0ff',
|
|
80
|
+
tag: '#7ee787',
|
|
81
|
+
name: '#7ee787',
|
|
82
|
+
attribute: '#79c0ff',
|
|
83
|
+
variable: '#ffa657',
|
|
84
|
+
'template-variable': '#ffa657',
|
|
85
|
+
params: '#e6edf3',
|
|
86
|
+
meta: '#8b949e',
|
|
87
|
+
regexp: '#a5d6ff',
|
|
88
|
+
symbol: '#79c0ff',
|
|
89
|
+
operator: '#e6edf3',
|
|
90
|
+
punctuation: '#e6edf3',
|
|
91
|
+
selector: '#7ee787',
|
|
92
|
+
property: '#79c0ff',
|
|
93
|
+
},
|
|
94
|
+
github: {
|
|
95
|
+
bg: '#ffffff',
|
|
96
|
+
fg: '#1f2328',
|
|
97
|
+
headerBg: '#f6f8fa',
|
|
98
|
+
headerFg: '#656d76',
|
|
99
|
+
border: '#d1d9e0',
|
|
100
|
+
lineHighlight: 'rgba(234, 179, 8, 0.12)',
|
|
101
|
+
linkHover: '#1f2328',
|
|
102
|
+
keyword: '#cf222e',
|
|
103
|
+
string: '#0a3069',
|
|
104
|
+
number: '#0550ae',
|
|
105
|
+
comment: '#6e7781',
|
|
106
|
+
function: '#8250df',
|
|
107
|
+
title: '#8250df',
|
|
108
|
+
built_in: '#0550ae',
|
|
109
|
+
literal: '#0550ae',
|
|
110
|
+
type: '#0550ae',
|
|
111
|
+
attr: '#0550ae',
|
|
112
|
+
tag: '#116329',
|
|
113
|
+
name: '#116329',
|
|
114
|
+
attribute: '#0550ae',
|
|
115
|
+
variable: '#953800',
|
|
116
|
+
'template-variable': '#953800',
|
|
117
|
+
params: '#1f2328',
|
|
118
|
+
meta: '#6e7781',
|
|
119
|
+
regexp: '#0a3069',
|
|
120
|
+
symbol: '#0550ae',
|
|
121
|
+
operator: '#1f2328',
|
|
122
|
+
punctuation: '#1f2328',
|
|
123
|
+
selector: '#116329',
|
|
124
|
+
property: '#0550ae',
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Theme resolution
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Resolve the current theme ID based on page theme and config.
|
|
134
|
+
* Always follows the page theme (data-sb-theme attribute).
|
|
135
|
+
*/
|
|
136
|
+
function normalizeThemeId(requested, mode) {
|
|
137
|
+
const fallback = mode === 'light' ? 'github' : 'github-dark-dimmed'
|
|
138
|
+
if (!requested || typeof requested !== 'string') return fallback
|
|
139
|
+
if (THEMES[requested]) return requested
|
|
140
|
+
|
|
141
|
+
const key = requested.trim().toLowerCase().replace(/[\s_]+/g, '-')
|
|
142
|
+
const aliases = {
|
|
143
|
+
github: 'github',
|
|
144
|
+
'github-light': 'github',
|
|
145
|
+
light: 'github',
|
|
146
|
+
'night-owl-light': 'github',
|
|
147
|
+
'github-dark': 'github-dark',
|
|
148
|
+
dark: 'github-dark-dimmed',
|
|
149
|
+
'dark-dimmed': 'github-dark-dimmed',
|
|
150
|
+
'github-dark-dimmed': 'github-dark-dimmed',
|
|
151
|
+
'night-owl': 'github-dark-dimmed',
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const resolved = aliases[key]
|
|
155
|
+
if (resolved && THEMES[resolved]) return resolved
|
|
156
|
+
return fallback
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Resolve the current theme ID based on page theme and config.
|
|
161
|
+
* Always follows the page theme (data-sb-theme attribute).
|
|
162
|
+
*/
|
|
163
|
+
function resolveThemeId() {
|
|
164
|
+
const config = getToolbarConfig()
|
|
165
|
+
const highlighting = config?.highlighting || {}
|
|
166
|
+
const darkTheme = normalizeThemeId(highlighting.dark, 'dark')
|
|
167
|
+
const lightTheme = normalizeThemeId(highlighting.light, 'light')
|
|
168
|
+
|
|
169
|
+
const sbTheme = typeof document !== 'undefined'
|
|
170
|
+
? document.documentElement.getAttribute('data-sb-theme') || 'dark'
|
|
171
|
+
: 'dark'
|
|
172
|
+
|
|
173
|
+
return sbTheme.startsWith('dark') ? darkTheme : lightTheme
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get the color palette for the current theme.
|
|
178
|
+
* Falls back to github-dark-dimmed for unknown theme names.
|
|
179
|
+
* Exported so InspectorPanel can use it for header/container colors.
|
|
180
|
+
*/
|
|
181
|
+
export function getColors() {
|
|
182
|
+
const id = resolveThemeId()
|
|
183
|
+
return THEMES[id] || THEMES['github-dark-dimmed']
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// HTML generation
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
function escapeHtml(str) {
|
|
191
|
+
return str
|
|
192
|
+
.replace(/&/g, '&')
|
|
193
|
+
.replace(/</g, '<')
|
|
194
|
+
.replace(/>/g, '>')
|
|
195
|
+
.replace(/"/g, '"')
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Convert highlight.js class-based spans to inline-styled spans.
|
|
200
|
+
* highlight.js emits `<span class="hljs-keyword">` etc.
|
|
201
|
+
* We replace each with `<span style="color:...">` using the palette.
|
|
202
|
+
*/
|
|
203
|
+
function applyInlineColors(html, colors) {
|
|
204
|
+
return html.replace(
|
|
205
|
+
/<span class="hljs-([^"]+)">/g,
|
|
206
|
+
(_, cls) => {
|
|
207
|
+
const color = colors[cls] || colors.fg
|
|
208
|
+
return `<span style="color:${color}">`
|
|
33
209
|
}
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
// Public API
|
|
215
|
+
// ---------------------------------------------------------------------------
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Create the inspector highlighter.
|
|
219
|
+
* Returns an object with codeToHtml() matching the Shiki-compatible API.
|
|
220
|
+
*/
|
|
221
|
+
export async function createInspectorHighlighter() {
|
|
222
|
+
return {
|
|
223
|
+
/**
|
|
224
|
+
* Highlight code and return HTML string with inline styles.
|
|
225
|
+
*
|
|
226
|
+
* @param {string} code - Source code to highlight
|
|
227
|
+
* @param {object} options
|
|
228
|
+
* @param {string} [options.lang] - Language identifier
|
|
229
|
+
* @param {string} [options.theme] - Ignored (theme resolved from config)
|
|
230
|
+
* @param {Array<{ start: { line: number }, end: { line: number }, properties: { class: string } }>} [options.decorations]
|
|
231
|
+
* @returns {string} HTML string with highlighted code
|
|
232
|
+
*/
|
|
233
|
+
codeToHtml(code, options = {}) {
|
|
234
|
+
const lang = options.lang || 'javascript'
|
|
235
|
+
const decorations = options.decorations || []
|
|
236
|
+
const colors = getColors()
|
|
237
|
+
|
|
238
|
+
let highlighted
|
|
239
|
+
try {
|
|
240
|
+
highlighted = hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
|
|
241
|
+
} catch {
|
|
242
|
+
highlighted = escapeHtml(code)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Convert class-based spans to inline styles
|
|
246
|
+
highlighted = applyInlineColors(highlighted, colors)
|
|
247
|
+
|
|
248
|
+
const lines = highlighted.split('\n')
|
|
249
|
+
const highlightedLines = new Set()
|
|
250
|
+
for (const dec of decorations) {
|
|
251
|
+
if (dec.start && dec.properties?.class) {
|
|
252
|
+
for (let i = dec.start.line; i <= (dec.end?.line ?? dec.start.line); i++) {
|
|
253
|
+
highlightedLines.add(i)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const wrappedLines = lines.map((line, i) => {
|
|
259
|
+
const classes = ['line']
|
|
260
|
+
if (highlightedLines.has(i)) classes.push('highlighted-line')
|
|
261
|
+
return `<span class="${classes.join(' ')}">${line}</span>`
|
|
262
|
+
}).join('\n')
|
|
34
263
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
langs: [tsx.default, jsx.default, javascript.default, typescript.default],
|
|
38
|
-
engine: oniguruma.createOnigurumaEngine(wasm),
|
|
39
|
-
})
|
|
40
|
-
} catch {
|
|
41
|
-
return null
|
|
264
|
+
return `<pre style="background:${colors.bg};color:${colors.fg};margin:0;padding:0;overflow-x:auto"><code>${wrappedLines}</code></pre>`
|
|
265
|
+
},
|
|
42
266
|
}
|
|
43
267
|
}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
{sideOffset}
|
|
20
20
|
{align}
|
|
21
21
|
class={cn(
|
|
22
|
-
"font-sans data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 w-(--bits-dropdown-menu-anchor-width) data-closed:overflow-hidden z-[10000] bg-popover border-3 border-
|
|
22
|
+
"font-sans data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 w-(--bits-dropdown-menu-anchor-width) data-closed:overflow-hidden z-[10000] bg-popover border-3 border-border text-popover-foreground fill-popover-foreground min-w-40 rounded-xl p-2 shadow-xl duration-100 overflow-x-hidden overflow-y-auto",
|
|
23
23
|
className
|
|
24
24
|
)}
|
|
25
25
|
{...restProps}
|
|
@@ -12,6 +12,6 @@
|
|
|
12
12
|
<DropdownMenuPrimitive.SubContent
|
|
13
13
|
bind:ref
|
|
14
14
|
data-slot="dropdown-menu-sub-content"
|
|
15
|
-
class={cn("font-sans data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[10000] bg-popover border-3 border-
|
|
15
|
+
class={cn("font-sans data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-[10000] bg-popover border-3 border-border text-popover-foreground fill-popover-foreground min-w-[96px] rounded-xl p-2 shadow-xl duration-100 w-auto", className)}
|
|
16
16
|
{...restProps}
|
|
17
17
|
/>
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
data-slot="dropdown-menu-sub-trigger"
|
|
18
18
|
data-inset={inset}
|
|
19
19
|
class={cn(
|
|
20
|
-
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-
|
|
20
|
+
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-1.5 rounded-md px-2.5 py-2 text-sm data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 flex cursor-default items-center outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
21
21
|
className
|
|
22
22
|
)}
|
|
23
23
|
{...restProps}
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
class: className,
|
|
32
32
|
wrapperClass = "",
|
|
33
33
|
active = false,
|
|
34
|
+
inactive = false,
|
|
35
|
+
dimmed = false,
|
|
36
|
+
localOnly = false,
|
|
34
37
|
size = "icon-2xl",
|
|
35
38
|
children,
|
|
36
39
|
...restProps
|
|
@@ -41,13 +44,21 @@
|
|
|
41
44
|
);
|
|
42
45
|
</script>
|
|
43
46
|
|
|
44
|
-
<span
|
|
47
|
+
<span
|
|
48
|
+
data-trigger-button
|
|
49
|
+
data-active={active || undefined}
|
|
50
|
+
data-inactive={inactive || undefined}
|
|
51
|
+
data-dimmed={dimmed || undefined}
|
|
52
|
+
data-local-only={localOnly || undefined}
|
|
53
|
+
style:--sb-trigger-border-width={borderWidth}
|
|
54
|
+
>
|
|
45
55
|
<Button
|
|
46
56
|
variant="trigger"
|
|
47
57
|
{size}
|
|
58
|
+
disabled={inactive}
|
|
48
59
|
wrapperClass={cn(
|
|
49
60
|
"smooth-corners [--smooth-corners:4] hover:rotate-2 focus-visible:rotate-2 transition-transform",
|
|
50
|
-
active && "rotate-2",
|
|
61
|
+
active && !inactive && "rotate-2",
|
|
51
62
|
wrapperClass
|
|
52
63
|
)}
|
|
53
64
|
class={cn(
|
|
@@ -62,7 +73,8 @@
|
|
|
62
73
|
|
|
63
74
|
<style>
|
|
64
75
|
[data-trigger-button] {
|
|
65
|
-
display:
|
|
76
|
+
display: inline-flex;
|
|
77
|
+
position: relative;
|
|
66
78
|
}
|
|
67
79
|
[data-trigger-button] :global([data-slot="button-wrapper"]) {
|
|
68
80
|
--sc-border-color: var(--trigger-border, var(--color-slate-400));
|
|
@@ -82,4 +94,20 @@
|
|
|
82
94
|
[data-trigger-button][data-active] :global([data-slot="button"]) {
|
|
83
95
|
background-color: var(--trigger-bg-hover, var(--color-slate-300));
|
|
84
96
|
}
|
|
97
|
+
|
|
98
|
+
/* Inactive: disabled-looking, no interaction */
|
|
99
|
+
[data-trigger-button][data-inactive] {
|
|
100
|
+
opacity: 0.45;
|
|
101
|
+
pointer-events: none;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/* Dimmed: reduced visibility, interactive on hover/focus */
|
|
105
|
+
[data-trigger-button][data-dimmed] {
|
|
106
|
+
opacity: 0.3;
|
|
107
|
+
transition: opacity 200ms;
|
|
108
|
+
}
|
|
109
|
+
[data-trigger-button][data-dimmed]:hover,
|
|
110
|
+
[data-trigger-button][data-dimmed]:focus-within {
|
|
111
|
+
opacity: 1;
|
|
112
|
+
}
|
|
85
113
|
</style>
|
package/src/modes.css
CHANGED
|
@@ -87,4 +87,12 @@ html.storyboard-mode-inspect {
|
|
|
87
87
|
--trigger-bg-hover: color-mix(in srgb, var(--mode-color) 20%, white);
|
|
88
88
|
--trigger-text: color-mix(in srgb, var(--mode-color) 85%, black);
|
|
89
89
|
--trigger-border: color-mix(in srgb, var(--mode-color) 45%, white);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Dark trigger tokens — applied when toolbar follows a dark theme */
|
|
93
|
+
:root[data-sb-toolbar-theme^="dark"] {
|
|
94
|
+
--trigger-bg: #21262d;
|
|
95
|
+
--trigger-bg-hover: #30363d;
|
|
96
|
+
--trigger-text: #e6edf3;
|
|
97
|
+
--trigger-border: #30363d;
|
|
90
98
|
}
|
|
@@ -17,6 +17,7 @@ import { initCommentsConfig, isCommentsEnabled } from './comments/config.js'
|
|
|
17
17
|
import { initFeatureFlags } from './featureFlags.js'
|
|
18
18
|
import { initPlugins } from './plugins.js'
|
|
19
19
|
import { initUIConfig } from './uiConfig.js'
|
|
20
|
+
import { initToolbarConfig } from './toolbarConfigStore.js'
|
|
20
21
|
|
|
21
22
|
let _mounted = false
|
|
22
23
|
|
|
@@ -85,12 +86,14 @@ async function injectUIStyles() {
|
|
|
85
86
|
* @param {object} [options={}]
|
|
86
87
|
* @param {string} [options.basePath='/'] - Base URL path (e.g. import.meta.env.BASE_URL)
|
|
87
88
|
* @param {HTMLElement} [options.container=document.body] - Where to mount devtools
|
|
89
|
+
* @param {Record<string, () => Promise<any>>} [options.handlers={}] - Custom tool handlers (key → lazy loader)
|
|
88
90
|
*/
|
|
89
91
|
export async function mountStoryboardCore(config = {}, options = {}) {
|
|
90
92
|
if (_mounted) return
|
|
91
93
|
_mounted = true
|
|
92
94
|
|
|
93
95
|
const basePath = options.basePath || '/'
|
|
96
|
+
const customHandlers = options.handlers || {}
|
|
94
97
|
|
|
95
98
|
// Apply saved theme to DOM immediately — before Svelte/React mount
|
|
96
99
|
applyEarlyTheme()
|
|
@@ -130,9 +133,16 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
130
133
|
? deepMerge(defaultConfig, config.toolbar)
|
|
131
134
|
: { ...defaultConfig }
|
|
132
135
|
|
|
133
|
-
// Inject repository URL from storyboard.config.json into the
|
|
136
|
+
// Inject repository URL from storyboard.config.json into the toolbar config
|
|
134
137
|
if (config.repository?.owner && config.repository?.name) {
|
|
135
138
|
const repoUrl = `https://github.com/${config.repository.owner}/${config.repository.name}`
|
|
139
|
+
|
|
140
|
+
// New tools schema
|
|
141
|
+
if (toolbarConfig.tools?.repository) {
|
|
142
|
+
toolbarConfig.tools.repository.url = repoUrl
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Legacy menus schema
|
|
136
146
|
const commandMenu = toolbarConfig.menus?.command
|
|
137
147
|
if (commandMenu?.actions) {
|
|
138
148
|
const repoAction = commandMenu.actions.find(a => a.id === 'core/repository')
|
|
@@ -140,6 +150,9 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
140
150
|
}
|
|
141
151
|
}
|
|
142
152
|
|
|
153
|
+
// Seed the reactive toolbar config store (core → custom merge)
|
|
154
|
+
initToolbarConfig(toolbarConfig)
|
|
155
|
+
|
|
143
156
|
// Skip all UI mounting when loaded inside a prototype embed iframe
|
|
144
157
|
const isEmbed = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('_sb_embed')
|
|
145
158
|
if (isEmbed) return
|
|
@@ -155,6 +168,7 @@ export async function mountStoryboardCore(config = {}, options = {}) {
|
|
|
155
168
|
container: options.container,
|
|
156
169
|
basePath,
|
|
157
170
|
toolbarConfig,
|
|
171
|
+
customHandlers,
|
|
158
172
|
})
|
|
159
173
|
|
|
160
174
|
// Mount comments system if configured
|
package/src/sidepanel.css
CHANGED
|
@@ -34,7 +34,7 @@ html:not(.sb-sidepanel-open) > body > #root {
|
|
|
34
34
|
|
|
35
35
|
/* Push the CoreUIBar (fixed bottom-right) — side mode */
|
|
36
36
|
html.sb-sidepanel-open:not(.sb-sidepanel-bottom) [data-core-ui-bar] {
|
|
37
|
-
right: calc(var(--sidepanel-width) + 24px);
|
|
37
|
+
right: calc(var(--sidepanel-width) + 24px) !important;
|
|
38
38
|
transition: right 0.25s ease;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -76,7 +76,7 @@ html.sb-sidepanel-open.sb-sidepanel-bottom.storyboard-mode-inspect > body > #roo
|
|
|
76
76
|
|
|
77
77
|
/* Push the CoreUIBar up — bottom mode */
|
|
78
78
|
html.sb-sidepanel-open.sb-sidepanel-bottom [data-core-ui-bar] {
|
|
79
|
-
bottom: calc(var(--sidepanel-height) + 24px);
|
|
79
|
+
bottom: calc(var(--sidepanel-height) + 24px) !important;
|
|
80
80
|
transition: bottom 0.25s ease;
|
|
81
81
|
}
|
|
82
82
|
|
package/src/stores/themeStore.ts
CHANGED
|
@@ -95,6 +95,10 @@ function _applyToDOM(theme: ThemeValue, resolved: string): void {
|
|
|
95
95
|
// Internal attribute
|
|
96
96
|
el.setAttribute('data-sb-theme', resolved)
|
|
97
97
|
|
|
98
|
+
// Toolbar theme — follows global theme when synced, stays light otherwise
|
|
99
|
+
const toolbarTheme = _syncTargets.toolbar ? resolved : 'light'
|
|
100
|
+
el.setAttribute('data-sb-toolbar-theme', toolbarTheme)
|
|
101
|
+
|
|
98
102
|
// Primer CSS attributes — these drive @primer/react ThemeProvider and
|
|
99
103
|
// Primer CSS custom-property layers without needing React state updates.
|
|
100
104
|
if (theme === 'system') {
|
|
@@ -163,6 +167,68 @@ if (typeof window !== 'undefined') {
|
|
|
163
167
|
})
|
|
164
168
|
}
|
|
165
169
|
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Theme sync targets
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
/** Which parts of the UI follow the global theme */
|
|
175
|
+
export interface ThemeSyncTargets {
|
|
176
|
+
prototype: boolean
|
|
177
|
+
toolbar: boolean
|
|
178
|
+
codeBoxes: boolean
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const SYNC_STORAGE_KEY = 'sb-theme-sync'
|
|
182
|
+
|
|
183
|
+
const DEFAULT_SYNC: ThemeSyncTargets = {
|
|
184
|
+
prototype: true,
|
|
185
|
+
toolbar: false,
|
|
186
|
+
codeBoxes: true,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function readStoredSync(): ThemeSyncTargets {
|
|
190
|
+
if (typeof localStorage === 'undefined') return { ...DEFAULT_SYNC }
|
|
191
|
+
try {
|
|
192
|
+
const raw = localStorage.getItem(SYNC_STORAGE_KEY)
|
|
193
|
+
if (!raw) return { ...DEFAULT_SYNC }
|
|
194
|
+
return { ...DEFAULT_SYNC, ...JSON.parse(raw) }
|
|
195
|
+
} catch {
|
|
196
|
+
return { ...DEFAULT_SYNC }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
let _syncTargets: ThemeSyncTargets = readStoredSync()
|
|
201
|
+
const _syncStore = writable<ThemeSyncTargets>(_syncTargets)
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get the current theme sync targets.
|
|
205
|
+
*/
|
|
206
|
+
export function getThemeSyncTargets(): ThemeSyncTargets {
|
|
207
|
+
return { ..._syncTargets }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Set a theme sync target. Persists to localStorage.
|
|
212
|
+
*/
|
|
213
|
+
export function setThemeSyncTarget(target: keyof ThemeSyncTargets, value: boolean): void {
|
|
214
|
+
_syncTargets = { ..._syncTargets, [target]: value }
|
|
215
|
+
_syncStore.set(_syncTargets)
|
|
216
|
+
|
|
217
|
+
if (typeof localStorage !== 'undefined') {
|
|
218
|
+
localStorage.setItem(SYNC_STORAGE_KEY, JSON.stringify(_syncTargets))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Re-apply DOM attributes so toolbar/codebox can react
|
|
222
|
+
const state = snapshot(_current)
|
|
223
|
+
_applyToDOM(_current, state.resolved)
|
|
224
|
+
_dispatchEvent(_current, state.resolved)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Readable Svelte store for sync target state.
|
|
229
|
+
*/
|
|
230
|
+
export const themeSyncState: Readable<ThemeSyncTargets> = { subscribe: _syncStore.subscribe }
|
|
231
|
+
|
|
166
232
|
// ---------------------------------------------------------------------------
|
|
167
233
|
// Boot — apply the stored theme immediately on import
|
|
168
234
|
// ---------------------------------------------------------------------------
|
|
@@ -446,17 +446,22 @@
|
|
|
446
446
|
{/snippet}
|
|
447
447
|
|
|
448
448
|
{#snippet canvasEntry(canvas)}
|
|
449
|
-
<
|
|
450
|
-
<
|
|
451
|
-
<
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
449
|
+
<section class="protoGroup">
|
|
450
|
+
<a class="listItem" href={canvas.route}>
|
|
451
|
+
<div class="cardBody">
|
|
452
|
+
<p class="protoName">
|
|
453
|
+
<span class="protoIcon">{canvas.icon || '🎨'}</span>
|
|
454
|
+
{canvas.name}
|
|
455
|
+
</p>
|
|
456
|
+
{#if canvas.description}
|
|
457
|
+
<p class="protoDesc">{canvas.description}</p>
|
|
458
|
+
{/if}
|
|
459
|
+
{#if canvas.widgetCount > 0}
|
|
460
|
+
<p class="flowDesc">{canvas.widgetCount} widget{canvas.widgetCount === 1 ? '' : 's'}</p>
|
|
461
|
+
{/if}
|
|
462
|
+
</div>
|
|
463
|
+
</a>
|
|
464
|
+
</section>
|
|
460
465
|
{/snippet}
|
|
461
466
|
|
|
462
467
|
{#if viewMode === 'prototypes'}
|