@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
|
@@ -1,38 +1,154 @@
|
|
|
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
|
-
*
|
|
8
|
+
* Theme is configurable via toolbar.config.json's `highlighting` key:
|
|
9
|
+
* { "highlighting": { "dark": "github-dark-dimmed", "light": "github" } }
|
|
10
|
+
*
|
|
11
|
+
* Returns an adapter object matching the codeToHtml() call signature
|
|
12
|
+
* used by InspectorPanel.svelte.
|
|
11
13
|
*/
|
|
12
|
-
|
|
14
|
+
|
|
15
|
+
import hljs from 'highlight.js/lib/core'
|
|
16
|
+
import javascript from 'highlight.js/lib/languages/javascript'
|
|
17
|
+
import typescript from 'highlight.js/lib/languages/typescript'
|
|
18
|
+
import xml from 'highlight.js/lib/languages/xml'
|
|
19
|
+
import { getToolbarConfig } from '../toolbarConfigStore.js'
|
|
20
|
+
|
|
21
|
+
hljs.registerLanguage('javascript', javascript)
|
|
22
|
+
hljs.registerLanguage('typescript', typescript)
|
|
23
|
+
hljs.registerLanguage('xml', xml)
|
|
24
|
+
// Aliases for JSX/TSX — highlight.js uses xml for the JSX part,
|
|
25
|
+
// but we map these to typescript which handles JSX well enough
|
|
26
|
+
hljs.registerLanguage('jsx', javascript)
|
|
27
|
+
hljs.registerLanguage('tsx', typescript)
|
|
28
|
+
|
|
29
|
+
/** Map of highlight.js theme name → loaded CSS text (cached). */
|
|
30
|
+
const _loadedThemes = new Map()
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get the highlight.js theme name for the current context.
|
|
34
|
+
* Reads highlighting config (dark/light groups) from toolbar config.
|
|
35
|
+
* Uses theme sync targets to decide whether to follow global theme.
|
|
36
|
+
* When code boxes are not synced (default), always uses the dark theme.
|
|
37
|
+
*/
|
|
38
|
+
function getThemeName() {
|
|
39
|
+
const config = getToolbarConfig()
|
|
40
|
+
const highlighting = config?.highlighting || {}
|
|
41
|
+
const darkTheme = highlighting.dark || 'github-dark-dimmed'
|
|
42
|
+
const lightTheme = highlighting.light || 'github'
|
|
43
|
+
|
|
44
|
+
// Check if code boxes should follow the global theme
|
|
45
|
+
let codeBoxesSynced = false
|
|
46
|
+
if (typeof localStorage !== 'undefined') {
|
|
47
|
+
try {
|
|
48
|
+
const raw = localStorage.getItem('sb-theme-sync')
|
|
49
|
+
if (raw) codeBoxesSynced = JSON.parse(raw).codeBoxes === true
|
|
50
|
+
} catch { /* ignore malformed localStorage */ }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// When not synced, always use dark theme for code boxes
|
|
54
|
+
if (!codeBoxesSynced) return darkTheme
|
|
55
|
+
|
|
56
|
+
// When synced, follow the current resolved theme
|
|
57
|
+
const sbTheme = typeof document !== 'undefined'
|
|
58
|
+
? document.documentElement.getAttribute('data-sb-theme') || 'dark'
|
|
59
|
+
: 'dark'
|
|
60
|
+
|
|
61
|
+
return sbTheme.startsWith('dark') ? darkTheme : lightTheme
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Ensure a highlight.js theme CSS is loaded into the document.
|
|
66
|
+
* Dynamically imports the CSS file and injects a <style> tag.
|
|
67
|
+
*/
|
|
68
|
+
async function ensureThemeLoaded(themeName) {
|
|
69
|
+
if (_loadedThemes.has(themeName)) return
|
|
13
70
|
try {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
import('
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
])
|
|
25
|
-
|
|
26
|
-
if (!shikiCore || !oniguruma || !tsx || !jsx || !javascript || !typescript || !githubDark || !wasm) {
|
|
27
|
-
return null
|
|
71
|
+
// Dynamic import of CSS — Vite handles this
|
|
72
|
+
await import(/* @vite-ignore */ `highlight.js/styles/${themeName}.css`)
|
|
73
|
+
_loadedThemes.set(themeName, true)
|
|
74
|
+
} catch {
|
|
75
|
+
// Fallback: try github-dark-dimmed if the requested theme doesn't exist
|
|
76
|
+
if (themeName !== 'github-dark-dimmed') {
|
|
77
|
+
try {
|
|
78
|
+
await import('highlight.js/styles/github-dark-dimmed.css')
|
|
79
|
+
_loadedThemes.set(themeName, true)
|
|
80
|
+
} catch { /* ignore */ }
|
|
28
81
|
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
29
84
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Escape HTML entities in a string.
|
|
87
|
+
*/
|
|
88
|
+
function escapeHtml(str) {
|
|
89
|
+
return str
|
|
90
|
+
.replace(/&/g, '&')
|
|
91
|
+
.replace(/</g, '<')
|
|
92
|
+
.replace(/>/g, '>')
|
|
93
|
+
.replace(/"/g, '"')
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create the inspector highlighter.
|
|
98
|
+
* Returns an object with codeToHtml() matching the Shiki API shape.
|
|
99
|
+
*
|
|
100
|
+
* @returns {Promise<{ codeToHtml: (code: string, options: { lang?: string, theme?: string, decorations?: Array }) => string }>}
|
|
101
|
+
*/
|
|
102
|
+
export async function createInspectorHighlighter() {
|
|
103
|
+
const themeName = getThemeName()
|
|
104
|
+
await ensureThemeLoaded(themeName)
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
/**
|
|
108
|
+
* Highlight code and return HTML string.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} code - Source code to highlight
|
|
111
|
+
* @param {object} options
|
|
112
|
+
* @param {string} [options.lang] - Language identifier
|
|
113
|
+
* @param {string} [options.theme] - Ignored (theme comes from config)
|
|
114
|
+
* @param {Array<{ start: { line: number }, end: { line: number }, properties: { class: string } }>} [options.decorations]
|
|
115
|
+
* @returns {string} HTML string with highlighted code
|
|
116
|
+
*/
|
|
117
|
+
codeToHtml(code, options = {}) {
|
|
118
|
+
const lang = options.lang || 'javascript'
|
|
119
|
+
const decorations = options.decorations || []
|
|
120
|
+
|
|
121
|
+
// Ensure current theme is loaded (non-blocking, already cached after first call)
|
|
122
|
+
const currentTheme = getThemeName()
|
|
123
|
+
ensureThemeLoaded(currentTheme)
|
|
124
|
+
|
|
125
|
+
// Highlight the code
|
|
126
|
+
let highlighted
|
|
127
|
+
try {
|
|
128
|
+
highlighted = hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
|
|
129
|
+
} catch {
|
|
130
|
+
highlighted = escapeHtml(code)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Split into lines and wrap each in a <span class="line">
|
|
134
|
+
const lines = highlighted.split('\n')
|
|
135
|
+
// Build a set of highlighted line numbers (0-indexed from decorations)
|
|
136
|
+
const highlightedLines = new Set()
|
|
137
|
+
for (const dec of decorations) {
|
|
138
|
+
if (dec.start && dec.properties?.class) {
|
|
139
|
+
for (let i = dec.start.line; i <= (dec.end?.line ?? dec.start.line); i++) {
|
|
140
|
+
highlightedLines.add(i)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const wrappedLines = lines.map((line, i) => {
|
|
146
|
+
const classes = ['line']
|
|
147
|
+
if (highlightedLines.has(i)) classes.push('highlighted-line')
|
|
148
|
+
return `<span class="${classes.join(' ')}">${line}</span>`
|
|
149
|
+
}).join('\n')
|
|
150
|
+
|
|
151
|
+
return `<pre class="hljs"><code>${wrappedLines}</code></pre>`
|
|
152
|
+
},
|
|
37
153
|
}
|
|
38
154
|
}
|
|
@@ -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/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: false,
|
|
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'}
|