@dfosco/storyboard-core 3.7.0 → 3.8.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 +4537 -4499
- package/dist/storyboard-ui.js.map +1 -1
- package/package.json +1 -1
- package/src/ThemeMenuButton.svelte +38 -6
- package/src/canvas/materializer.js +57 -8
- package/src/canvas/materializer.test.js +16 -0
- package/src/core-ui-colors.css +46 -0
- package/src/mountStoryboardCore.js +35 -5
- package/src/stores/themeStore.ts +6 -0
- package/toolbar.config.json +2 -2
package/package.json
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
let { config = {}, tabindex = -1 }: Props = $props()
|
|
26
26
|
|
|
27
27
|
let menuOpen = $state(false)
|
|
28
|
+
let canvasActive = $state(false)
|
|
28
29
|
|
|
29
30
|
function handleSelect(value: ThemeValue) {
|
|
30
31
|
setTheme(value)
|
|
@@ -35,6 +36,28 @@
|
|
|
35
36
|
e.preventDefault()
|
|
36
37
|
setThemeSyncTarget(target, !$themeSyncState[target])
|
|
37
38
|
}
|
|
39
|
+
|
|
40
|
+
$effect(() => {
|
|
41
|
+
function handleCanvasMounted() {
|
|
42
|
+
canvasActive = true
|
|
43
|
+
}
|
|
44
|
+
function handleCanvasUnmounted() {
|
|
45
|
+
canvasActive = false
|
|
46
|
+
}
|
|
47
|
+
document.addEventListener('storyboard:canvas:mounted', handleCanvasMounted)
|
|
48
|
+
document.addEventListener('storyboard:canvas:unmounted', handleCanvasUnmounted)
|
|
49
|
+
|
|
50
|
+
const state = (window as any).__storyboardCanvasBridgeState
|
|
51
|
+
canvasActive = state?.active === true
|
|
52
|
+
if (!canvasActive) {
|
|
53
|
+
document.dispatchEvent(new CustomEvent('storyboard:canvas:status-request'))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
document.removeEventListener('storyboard:canvas:mounted', handleCanvasMounted)
|
|
58
|
+
document.removeEventListener('storyboard:canvas:unmounted', handleCanvasUnmounted)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
38
61
|
</script>
|
|
39
62
|
|
|
40
63
|
<DropdownMenu.Root bind:open={menuOpen}>
|
|
@@ -74,12 +97,21 @@
|
|
|
74
97
|
<DropdownMenu.SubTrigger>Theme settings</DropdownMenu.SubTrigger>
|
|
75
98
|
<DropdownMenu.SubContent class="min-w-[180px]">
|
|
76
99
|
<DropdownMenu.Label>Apply theme to</DropdownMenu.Label>
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
{#if canvasActive}
|
|
101
|
+
<DropdownMenu.CheckboxItem
|
|
102
|
+
checked={$themeSyncState.canvas}
|
|
103
|
+
onSelect={(e) => handleSyncToggle(e, 'canvas')}
|
|
104
|
+
>
|
|
105
|
+
Canvas
|
|
106
|
+
</DropdownMenu.CheckboxItem>
|
|
107
|
+
{:else}
|
|
108
|
+
<DropdownMenu.CheckboxItem
|
|
109
|
+
checked={$themeSyncState.prototype}
|
|
110
|
+
onSelect={(e) => handleSyncToggle(e, 'prototype')}
|
|
111
|
+
>
|
|
112
|
+
Prototype
|
|
113
|
+
</DropdownMenu.CheckboxItem>
|
|
114
|
+
{/if}
|
|
83
115
|
<DropdownMenu.CheckboxItem
|
|
84
116
|
checked={$themeSyncState.toolbar}
|
|
85
117
|
onSelect={(e) => handleSyncToggle(e, 'toolbar')}
|
|
@@ -16,21 +16,70 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* Split a text blob into top-level JSON object snippets.
|
|
20
|
+
* Supports strict JSONL and accidentally concatenated objects.
|
|
21
21
|
*
|
|
22
|
-
* @param {string} text
|
|
22
|
+
* @param {string} text
|
|
23
|
+
* @returns {string[]}
|
|
24
|
+
*/
|
|
25
|
+
function splitJsonObjects(text) {
|
|
26
|
+
const chunks = []
|
|
27
|
+
let start = -1
|
|
28
|
+
let depth = 0
|
|
29
|
+
let inString = false
|
|
30
|
+
let escaped = false
|
|
31
|
+
|
|
32
|
+
for (let i = 0; i < text.length; i++) {
|
|
33
|
+
const ch = text[i]
|
|
34
|
+
|
|
35
|
+
if (inString) {
|
|
36
|
+
if (escaped) {
|
|
37
|
+
escaped = false
|
|
38
|
+
} else if (ch === '\\') {
|
|
39
|
+
escaped = true
|
|
40
|
+
} else if (ch === '"') {
|
|
41
|
+
inString = false
|
|
42
|
+
}
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (ch === '"') {
|
|
47
|
+
inString = true
|
|
48
|
+
continue
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (ch === '{') {
|
|
52
|
+
if (depth === 0) start = i
|
|
53
|
+
depth++
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (ch === '}') {
|
|
58
|
+
if (depth > 0) depth--
|
|
59
|
+
if (depth === 0 && start >= 0) {
|
|
60
|
+
chunks.push(text.slice(start, i + 1))
|
|
61
|
+
start = -1
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return chunks
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse canvas event text into an array of event objects.
|
|
71
|
+
* Blank lines and malformed JSON snippets are skipped.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} text - Raw canvas event file contents
|
|
23
74
|
* @returns {object[]} Parsed event objects
|
|
24
75
|
*/
|
|
25
76
|
export function parseCanvasJsonl(text) {
|
|
26
77
|
const events = []
|
|
27
|
-
for (const
|
|
28
|
-
const trimmed = line.trim()
|
|
29
|
-
if (!trimmed) continue
|
|
78
|
+
for (const snippet of splitJsonObjects(text || '')) {
|
|
30
79
|
try {
|
|
31
|
-
events.push(JSON.parse(
|
|
80
|
+
events.push(JSON.parse(snippet))
|
|
32
81
|
} catch {
|
|
33
|
-
// Skip malformed
|
|
82
|
+
// Skip malformed snippets
|
|
34
83
|
}
|
|
35
84
|
}
|
|
36
85
|
return events
|
|
@@ -22,6 +22,22 @@ describe('parseCanvasJsonl', () => {
|
|
|
22
22
|
expect(events).toHaveLength(2)
|
|
23
23
|
})
|
|
24
24
|
|
|
25
|
+
it('parses concatenated JSON objects on a single line', () => {
|
|
26
|
+
const text = '{"event":"canvas_created","title":"Test"}{"event":"source_updated","sources":[]}'
|
|
27
|
+
const events = parseCanvasJsonl(text)
|
|
28
|
+
expect(events).toHaveLength(2)
|
|
29
|
+
expect(events[0].event).toBe('canvas_created')
|
|
30
|
+
expect(events[1].event).toBe('source_updated')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('handles braces inside JSON strings', () => {
|
|
34
|
+
const text = '{"event":"canvas_created","title":"A {title}"}{"event":"widget_added","widget":{"id":"w1","props":{"text":"x}"}}}'
|
|
35
|
+
const events = parseCanvasJsonl(text)
|
|
36
|
+
expect(events).toHaveLength(2)
|
|
37
|
+
expect(events[0].title).toBe('A {title}')
|
|
38
|
+
expect(events[1].widget.props.text).toBe('x}')
|
|
39
|
+
})
|
|
40
|
+
|
|
25
41
|
it('returns empty array for empty input', () => {
|
|
26
42
|
expect(parseCanvasJsonl('')).toEqual([])
|
|
27
43
|
expect(parseCanvasJsonl('\n\n')).toEqual([])
|
package/src/core-ui-colors.css
CHANGED
|
@@ -20,11 +20,27 @@
|
|
|
20
20
|
|
|
21
21
|
/* Default: light tokens */
|
|
22
22
|
[data-core-ui-bar],
|
|
23
|
+
[data-sidepanel],
|
|
23
24
|
[data-slot="dropdown-menu-content"],
|
|
24
25
|
[data-slot="dropdown-menu-sub-content"] {
|
|
25
26
|
color-scheme: light;
|
|
26
27
|
|
|
27
28
|
/* shadcn / Tailwind semantic tokens — pinned to light values */
|
|
29
|
+
--color-background: hsl(0 0% 100%);
|
|
30
|
+
--color-foreground: hsl(222.2 84% 4.9%);
|
|
31
|
+
--color-popover: hsl(0 0% 100%);
|
|
32
|
+
--color-popover-foreground: hsl(222.2 84% 4.9%);
|
|
33
|
+
--color-primary: hsl(222.2 47.4% 11.2%);
|
|
34
|
+
--color-primary-foreground: hsl(210 40% 98%);
|
|
35
|
+
--color-secondary: hsl(210 40% 96.1%);
|
|
36
|
+
--color-secondary-foreground: hsl(222.2 47.4% 11.2%);
|
|
37
|
+
--color-muted: hsl(210 40% 96.1%);
|
|
38
|
+
--color-muted-foreground: hsl(215.4 16.3% 46.9%);
|
|
39
|
+
--color-accent: hsl(210 40% 96.1%);
|
|
40
|
+
--color-accent-foreground: hsl(222.2 47.4% 11.2%);
|
|
41
|
+
--color-border: hsl(214.3 31.8% 91.4%);
|
|
42
|
+
--color-input: hsl(214.3 31.8% 91.4%);
|
|
43
|
+
|
|
28
44
|
--sb--color-background: hsl(0 0% 100%);
|
|
29
45
|
--sb--color-foreground: hsl(222.2 84% 4.9%);
|
|
30
46
|
--sb--color-popover: hsl(0 0% 100%);
|
|
@@ -39,6 +55,13 @@
|
|
|
39
55
|
--sb--color-accent-foreground: hsl(222.2 47.4% 11.2%);
|
|
40
56
|
--sb--color-border: hsl(214.3 31.8% 91.4%);
|
|
41
57
|
--sb--color-input: hsl(214.3 31.8% 91.4%);
|
|
58
|
+
|
|
59
|
+
/* Primer token aliases used in side panel + inspector chrome */
|
|
60
|
+
--bgColor-default: #ffffff;
|
|
61
|
+
--fgColor-default: #1f2328;
|
|
62
|
+
--fgColor-muted: #656d76;
|
|
63
|
+
--borderColor-default: #d1d9e0;
|
|
64
|
+
--bgColor-neutral-muted: rgba(175, 184, 193, 0.2);
|
|
42
65
|
}
|
|
43
66
|
|
|
44
67
|
/* Dark tokens — applied when toolbar syncs with a dark theme.
|
|
@@ -46,10 +69,26 @@
|
|
|
46
69
|
* instead of generic shadcn dark values.
|
|
47
70
|
*/
|
|
48
71
|
:root[data-sb-toolbar-theme^="dark"] [data-core-ui-bar],
|
|
72
|
+
:root[data-sb-toolbar-theme^="dark"] [data-sidepanel],
|
|
49
73
|
:root[data-sb-toolbar-theme^="dark"] [data-slot="dropdown-menu-content"],
|
|
50
74
|
:root[data-sb-toolbar-theme^="dark"] [data-slot="dropdown-menu-sub-content"] {
|
|
51
75
|
color-scheme: dark;
|
|
52
76
|
|
|
77
|
+
--color-background: #161b22;
|
|
78
|
+
--color-foreground: #e6edf3;
|
|
79
|
+
--color-popover: #161b22;
|
|
80
|
+
--color-popover-foreground: #e6edf3;
|
|
81
|
+
--color-primary: #e6edf3;
|
|
82
|
+
--color-primary-foreground: #161b22;
|
|
83
|
+
--color-secondary: #21262d;
|
|
84
|
+
--color-secondary-foreground: #e6edf3;
|
|
85
|
+
--color-muted: #21262d;
|
|
86
|
+
--color-muted-foreground: #8b949e;
|
|
87
|
+
--color-accent: #21262d;
|
|
88
|
+
--color-accent-foreground: #e6edf3;
|
|
89
|
+
--color-border: #30363d;
|
|
90
|
+
--color-input: #30363d;
|
|
91
|
+
|
|
53
92
|
--sb--color-background: #161b22;
|
|
54
93
|
--sb--color-foreground: #e6edf3;
|
|
55
94
|
--sb--color-popover: #161b22;
|
|
@@ -64,4 +103,11 @@
|
|
|
64
103
|
--sb--color-accent-foreground: #e6edf3;
|
|
65
104
|
--sb--color-border: #30363d;
|
|
66
105
|
--sb--color-input: #30363d;
|
|
106
|
+
|
|
107
|
+
/* Primer token aliases used in side panel + inspector chrome */
|
|
108
|
+
--bgColor-default: #161b22;
|
|
109
|
+
--fgColor-default: #e6edf3;
|
|
110
|
+
--fgColor-muted: #8b949e;
|
|
111
|
+
--borderColor-default: #30363d;
|
|
112
|
+
--bgColor-neutral-muted: rgba(110, 118, 129, 0.2);
|
|
67
113
|
}
|
|
@@ -33,8 +33,23 @@ function applyEarlyTheme() {
|
|
|
33
33
|
typeof localStorage !== 'undefined'
|
|
34
34
|
? localStorage.getItem('sb-color-scheme')
|
|
35
35
|
: null
|
|
36
|
+
const storedSync =
|
|
37
|
+
typeof localStorage !== 'undefined'
|
|
38
|
+
? localStorage.getItem('sb-theme-sync')
|
|
39
|
+
: null
|
|
40
|
+
let syncTargets = { prototype: true, toolbar: false, codeBoxes: true, canvas: false }
|
|
41
|
+
if (storedSync) {
|
|
42
|
+
try {
|
|
43
|
+
syncTargets = { ...syncTargets, ...JSON.parse(storedSync) }
|
|
44
|
+
} catch {
|
|
45
|
+
// Ignore malformed persisted sync settings and use defaults.
|
|
46
|
+
}
|
|
47
|
+
}
|
|
36
48
|
const theme = stored || 'system'
|
|
37
49
|
const el = document.documentElement
|
|
50
|
+
const searchParams =
|
|
51
|
+
typeof window !== 'undefined' ? new URLSearchParams(window.location.search) : null
|
|
52
|
+
const forcedTarget = searchParams?.get('_sb_theme_target')
|
|
38
53
|
|
|
39
54
|
// Resolve "system" to an actual theme for data-sb-theme
|
|
40
55
|
let resolved = theme
|
|
@@ -46,19 +61,34 @@ function applyEarlyTheme() {
|
|
|
46
61
|
: 'light'
|
|
47
62
|
}
|
|
48
63
|
|
|
49
|
-
|
|
64
|
+
const forcePrototype = forcedTarget === 'prototype'
|
|
65
|
+
const forceToolbar = forcedTarget === 'toolbar'
|
|
50
66
|
|
|
51
|
-
|
|
67
|
+
const prototypeTheme = forcePrototype
|
|
68
|
+
? resolved
|
|
69
|
+
: (syncTargets.prototype ? resolved : 'light')
|
|
70
|
+
const toolbarTheme = forceToolbar
|
|
71
|
+
? resolved
|
|
72
|
+
: (syncTargets.toolbar ? resolved : 'light')
|
|
73
|
+
const codeTheme = syncTargets.codeBoxes ? resolved : 'light'
|
|
74
|
+
const canvasTheme = syncTargets.canvas ? resolved : 'light'
|
|
75
|
+
|
|
76
|
+
el.setAttribute('data-sb-theme', prototypeTheme)
|
|
77
|
+
el.setAttribute('data-sb-toolbar-theme', toolbarTheme)
|
|
78
|
+
el.setAttribute('data-sb-code-theme', codeTheme)
|
|
79
|
+
el.setAttribute('data-sb-canvas-theme', canvasTheme)
|
|
80
|
+
|
|
81
|
+
if (theme === 'system' && syncTargets.prototype) {
|
|
52
82
|
el.setAttribute('data-color-mode', 'auto')
|
|
53
83
|
el.setAttribute('data-light-theme', 'light')
|
|
54
84
|
el.setAttribute('data-dark-theme', 'dark')
|
|
55
|
-
} else if (
|
|
85
|
+
} else if (prototypeTheme.startsWith('dark')) {
|
|
56
86
|
el.setAttribute('data-color-mode', 'dark')
|
|
57
|
-
el.setAttribute('data-dark-theme',
|
|
87
|
+
el.setAttribute('data-dark-theme', prototypeTheme)
|
|
58
88
|
el.setAttribute('data-light-theme', 'light')
|
|
59
89
|
} else {
|
|
60
90
|
el.setAttribute('data-color-mode', 'light')
|
|
61
|
-
el.setAttribute('data-light-theme',
|
|
91
|
+
el.setAttribute('data-light-theme', prototypeTheme)
|
|
62
92
|
el.setAttribute('data-dark-theme', 'dark')
|
|
63
93
|
}
|
|
64
94
|
}
|
package/src/stores/themeStore.ts
CHANGED
|
@@ -96,6 +96,7 @@ function _applyToDOM(theme: ThemeValue, resolved: string): void {
|
|
|
96
96
|
const prototypeTheme = _syncTargets.prototype ? resolved : 'light'
|
|
97
97
|
const toolbarTheme = _syncTargets.toolbar ? resolved : 'light'
|
|
98
98
|
const codeTheme = _syncTargets.codeBoxes ? resolved : 'light'
|
|
99
|
+
const canvasTheme = _syncTargets.canvas ? resolved : 'light'
|
|
99
100
|
|
|
100
101
|
// Internal attributes
|
|
101
102
|
el.setAttribute('data-sb-theme', prototypeTheme)
|
|
@@ -103,6 +104,7 @@ function _applyToDOM(theme: ThemeValue, resolved: string): void {
|
|
|
103
104
|
|
|
104
105
|
// Toolbar theme — follows global theme when synced, stays light otherwise
|
|
105
106
|
el.setAttribute('data-sb-toolbar-theme', toolbarTheme)
|
|
107
|
+
el.setAttribute('data-sb-canvas-theme', canvasTheme)
|
|
106
108
|
|
|
107
109
|
// Primer CSS attributes — these drive @primer/react ThemeProvider and
|
|
108
110
|
// Primer CSS custom-property layers without needing React state updates.
|
|
@@ -127,6 +129,7 @@ function _dispatchEvent(theme: ThemeValue, resolved: string): void {
|
|
|
127
129
|
const prototypeResolved = _syncTargets.prototype ? resolved : 'light'
|
|
128
130
|
const toolbarResolved = _syncTargets.toolbar ? resolved : 'light'
|
|
129
131
|
const codeResolved = _syncTargets.codeBoxes ? resolved : 'light'
|
|
132
|
+
const canvasResolved = _syncTargets.canvas ? resolved : 'light'
|
|
130
133
|
|
|
131
134
|
document.dispatchEvent(
|
|
132
135
|
new CustomEvent('storyboard:theme:changed', {
|
|
@@ -137,6 +140,7 @@ function _dispatchEvent(theme: ThemeValue, resolved: string): void {
|
|
|
137
140
|
prototypeResolved,
|
|
138
141
|
toolbarResolved,
|
|
139
142
|
codeResolved,
|
|
143
|
+
canvasResolved,
|
|
140
144
|
},
|
|
141
145
|
}),
|
|
142
146
|
)
|
|
@@ -193,6 +197,7 @@ export interface ThemeSyncTargets {
|
|
|
193
197
|
prototype: boolean
|
|
194
198
|
toolbar: boolean
|
|
195
199
|
codeBoxes: boolean
|
|
200
|
+
canvas: boolean
|
|
196
201
|
}
|
|
197
202
|
|
|
198
203
|
const SYNC_STORAGE_KEY = 'sb-theme-sync'
|
|
@@ -201,6 +206,7 @@ const DEFAULT_SYNC: ThemeSyncTargets = {
|
|
|
201
206
|
prototype: true,
|
|
202
207
|
toolbar: false,
|
|
203
208
|
codeBoxes: true,
|
|
209
|
+
canvas: false,
|
|
204
210
|
}
|
|
205
211
|
|
|
206
212
|
function readStoredSync(): ThemeSyncTargets {
|
package/toolbar.config.json
CHANGED
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"render": "menu",
|
|
29
29
|
"surface": "main-toolbar",
|
|
30
30
|
"modes": ["*"],
|
|
31
|
-
"menuWidth": "
|
|
31
|
+
"menuWidth": "280px",
|
|
32
32
|
"handler": "core:flows"
|
|
33
33
|
},
|
|
34
34
|
"theme": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"surface": "main-toolbar",
|
|
41
41
|
"handler": "core:theme",
|
|
42
42
|
"modes": ["*"],
|
|
43
|
-
"menuWidth": "
|
|
43
|
+
"menuWidth": "240px"
|
|
44
44
|
},
|
|
45
45
|
"comments": {
|
|
46
46
|
"ariaLabel": "Comments",
|