@dfosco/storyboard-react 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/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "3.7.0",
3
+ "version": "3.8.0",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "3.7.0",
6
+ "@dfosco/storyboard-core": "3.8.0",
7
7
  "@dfosco/tiny-canvas": "^1.1.0",
8
8
  "@neodrag/react": "^2.3.1",
9
9
  "glob": "^11.0.0",
@@ -1,5 +1,6 @@
1
1
  import { fireEvent, render, screen, waitFor } from '@testing-library/react'
2
2
  import CanvasPage from './CanvasPage.jsx'
3
+ import { getCanvasPrimerAttrs, getCanvasThemeVars } from './canvasTheme.js'
3
4
  import { updateCanvas } from './canvasApi.js'
4
5
 
5
6
  vi.mock('@dfosco/tiny-canvas', () => ({
@@ -34,7 +35,13 @@ const mockCanvas = {
34
35
  }
35
36
 
36
37
  vi.mock('./useCanvas.js', () => ({
37
- useCanvas: () => ({ canvas: mockCanvas, jsxExports: null, loading: false }),
38
+ useCanvas: () => ({
39
+ canvas: mockCanvas,
40
+ jsxExports: {
41
+ PrimaryButtons: () => <div data-testid="jsx-widget-content">jsx widget</div>,
42
+ },
43
+ loading: false,
44
+ }),
38
45
  }))
39
46
 
40
47
  vi.mock('./widgets/index.js', () => ({
@@ -138,3 +145,61 @@ describe('CanvasPage canvas bridge', () => {
138
145
  })
139
146
  })
140
147
  })
148
+
149
+ describe('getCanvasThemeVars', () => {
150
+ it('returns a distinct dark-dimmed background token', () => {
151
+ expect(getCanvasThemeVars('light')['--sb--canvas-bg']).toBe('#f6f8fa')
152
+ expect(getCanvasThemeVars('light')['--tc-bg-muted']).toBe('#f6f8fa')
153
+ expect(getCanvasThemeVars('dark')['--sb--canvas-bg']).toBe('#161b22')
154
+ expect(getCanvasThemeVars('dark')['--bgColor-muted']).toBe('#161b22')
155
+ expect(getCanvasThemeVars('dark')['--tc-bg-muted']).toBe('#161b22')
156
+ expect(getCanvasThemeVars('dark_dimmed')['--sb--canvas-bg']).toBe('#22272e')
157
+ expect(getCanvasThemeVars('dark_dimmed')['--bgColor-muted']).toBe('#22272e')
158
+ expect(getCanvasThemeVars('dark_dimmed')['--tc-bg-muted']).toBe('#22272e')
159
+ expect(getCanvasThemeVars('dark_dimmed')['--tc-dot-color']).toBe('rgba(205, 217, 229, 0.22)')
160
+ expect(getCanvasThemeVars('dark_dimmed')['--overlay-backdrop-bgColor']).toBe('rgba(205, 217, 229, 0.22)')
161
+ })
162
+ })
163
+
164
+ describe('getCanvasPrimerAttrs', () => {
165
+ it('maps canvas theme to local Primer mode attrs', () => {
166
+ expect(getCanvasPrimerAttrs('light')).toEqual({
167
+ 'data-color-mode': 'light',
168
+ 'data-dark-theme': 'dark',
169
+ 'data-light-theme': 'light',
170
+ })
171
+ expect(getCanvasPrimerAttrs('dark')).toEqual({
172
+ 'data-color-mode': 'dark',
173
+ 'data-dark-theme': 'dark',
174
+ 'data-light-theme': 'light',
175
+ })
176
+ expect(getCanvasPrimerAttrs('dark_dimmed')).toEqual({
177
+ 'data-color-mode': 'dark',
178
+ 'data-dark-theme': 'dark_dimmed',
179
+ 'data-light-theme': 'light',
180
+ })
181
+ })
182
+ })
183
+
184
+ describe('canvas target fallback', () => {
185
+ it('stays light when canvas target is unchecked even if stale canvas attribute is dark', () => {
186
+ localStorage.setItem('sb-theme-sync', JSON.stringify({
187
+ prototype: true,
188
+ toolbar: true,
189
+ codeBoxes: true,
190
+ canvas: false,
191
+ }))
192
+ localStorage.setItem('sb-color-scheme', 'dark')
193
+ document.documentElement.setAttribute('data-sb-canvas-theme', 'dark')
194
+
195
+ render(<CanvasPage name="design-overview" />)
196
+
197
+ const scroll = document.querySelector('[data-storyboard-canvas-scroll]')
198
+ const jsxWidget = document.getElementById('jsx-PrimaryButtons')
199
+ expect(scroll?.style.getPropertyValue('--sb--canvas-bg')).toBe('#f6f8fa')
200
+ expect(scroll?.style.getPropertyValue('--tc-bg-muted')).toBe('#f6f8fa')
201
+ expect(scroll?.getAttribute('data-color-mode')).toBe('light')
202
+ expect(jsxWidget?.getAttribute('data-color-mode')).toBe('light')
203
+ expect(jsxWidget?.style.getPropertyValue('--bgColor-default')).toBe('#ffffff')
204
+ })
205
+ })
@@ -3,6 +3,7 @@ import { Canvas } from '@dfosco/tiny-canvas'
3
3
  import '@dfosco/tiny-canvas/style.css'
4
4
  import { useCanvas } from './useCanvas.js'
5
5
  import { shouldPreventCanvasTextSelection } from './textSelection.js'
6
+ import { getCanvasThemeVars, getCanvasPrimerAttrs } from './canvasTheme.js'
6
7
  import { getWidgetComponent } from './widgets/index.js'
7
8
  import { schemas, getDefaults } from './widgets/widgetProps.js'
8
9
  import ComponentWidget from './widgets/ComponentWidget.jsx'
@@ -14,6 +15,30 @@ const ZOOM_MAX = 200
14
15
 
15
16
  const CANVAS_BRIDGE_STATE_KEY = '__storyboardCanvasBridgeState'
16
17
 
18
+ function getToolbarColorMode(theme) {
19
+ return String(theme || 'light').startsWith('dark') ? 'dark' : 'light'
20
+ }
21
+
22
+ function resolveCanvasThemeFromStorage() {
23
+ if (typeof localStorage === 'undefined') return 'light'
24
+ let sync = { prototype: true, toolbar: false, codeBoxes: true, canvas: false }
25
+ try {
26
+ const rawSync = localStorage.getItem('sb-theme-sync')
27
+ if (rawSync) sync = { ...sync, ...JSON.parse(rawSync) }
28
+ } catch {
29
+ // Ignore malformed sync settings
30
+ }
31
+
32
+ if (!sync.canvas) return 'light'
33
+
34
+ const attrTheme = document.documentElement.getAttribute('data-sb-canvas-theme')
35
+ if (attrTheme) return attrTheme
36
+
37
+ const stored = localStorage.getItem('sb-color-scheme') || 'system'
38
+ if (stored !== 'system') return stored
39
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
40
+ }
41
+
17
42
  /**
18
43
  * Debounce helper — returns a function that delays invocation.
19
44
  */
@@ -72,6 +97,7 @@ export default function CanvasPage({ name }) {
72
97
  const [canvasTitle, setCanvasTitle] = useState(canvas?.title || name)
73
98
  const titleInputRef = useRef(null)
74
99
  const [localSources, setLocalSources] = useState(canvas?.sources ?? [])
100
+ const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
75
101
 
76
102
  if (canvas !== trackedCanvas) {
77
103
  setTrackedCanvas(canvas)
@@ -223,6 +249,17 @@ export default function CanvasPage({ name }) {
223
249
  return () => document.removeEventListener('storyboard:canvas:set-zoom', handleZoom)
224
250
  }, [])
225
251
 
252
+ // Canvas background should follow toolbar theme target.
253
+ useEffect(() => {
254
+ function readMode() {
255
+ setCanvasTheme(resolveCanvasThemeFromStorage())
256
+ }
257
+
258
+ readMode()
259
+ document.addEventListener('storyboard:theme:changed', readMode)
260
+ return () => document.removeEventListener('storyboard:theme:changed', readMode)
261
+ }, [])
262
+
226
263
  // Broadcast zoom level to CoreUIBar whenever it changes
227
264
  useEffect(() => {
228
265
  window[CANVAS_BRIDGE_STATE_KEY] = { active: true, name, zoom }
@@ -405,9 +442,14 @@ export default function CanvasPage({ name }) {
405
442
  dotted: canvas.dotted ?? false,
406
443
  grid: canvas.grid ?? false,
407
444
  gridSize: canvas.gridSize ?? 18,
408
- colorMode: canvas.colorMode ?? 'auto',
445
+ colorMode: canvas.colorMode === 'auto'
446
+ ? getToolbarColorMode(canvasTheme)
447
+ : (canvas.colorMode ?? 'auto'),
409
448
  }
410
449
 
450
+ const canvasThemeVars = getCanvasThemeVars(canvasTheme)
451
+ const canvasPrimerAttrs = getCanvasPrimerAttrs(canvasTheme)
452
+
411
453
  // Merge JSX-sourced widgets (from .canvas.jsx) and JSON widgets
412
454
  const allChildren = []
413
455
 
@@ -427,6 +469,8 @@ export default function CanvasPage({ name }) {
427
469
  id={`jsx-${exportName}`}
428
470
  data-tc-x={sourcePosition.x}
429
471
  data-tc-y={sourcePosition.y}
472
+ {...canvasPrimerAttrs}
473
+ style={canvasThemeVars}
430
474
  >
431
475
  <ComponentWidget component={Component} />
432
476
  </div>
@@ -442,6 +486,8 @@ export default function CanvasPage({ name }) {
442
486
  id={widget.id}
443
487
  data-tc-x={widget?.position?.x ?? 0}
444
488
  data-tc-y={widget?.position?.y ?? 0}
489
+ {...canvasPrimerAttrs}
490
+ style={canvasThemeVars}
445
491
  onClick={(e) => {
446
492
  e.stopPropagation()
447
493
  setSelectedWidgetId(widget.id)
@@ -476,13 +522,19 @@ export default function CanvasPage({ name }) {
476
522
  <div
477
523
  ref={scrollRef}
478
524
  data-storyboard-canvas-scroll
525
+ data-sb-canvas-theme={canvasTheme}
526
+ {...canvasPrimerAttrs}
479
527
  className={styles.canvasScroll}
480
- style={spaceHeld ? { cursor: panningActive ? 'grabbing' : 'grab' } : undefined}
528
+ style={{
529
+ ...canvasThemeVars,
530
+ ...(spaceHeld ? { cursor: panningActive ? 'grabbing' : 'grab' } : {}),
531
+ }}
481
532
  onClick={() => setSelectedWidgetId(null)}
482
533
  onMouseDown={handlePanStart}
483
534
  >
484
535
  <div
485
536
  data-storyboard-canvas-zoom
537
+ data-sb-canvas-theme={canvasTheme}
486
538
  className={styles.canvasZoom}
487
539
  style={{
488
540
  transform: `scale(${scale})`,
@@ -18,12 +18,12 @@
18
18
  width: 100vw;
19
19
  height: 100vh;
20
20
  overflow: auto;
21
- background-color: var(--bgColor-muted, #f6f8fa);
21
+ background-color: var(--sb--canvas-bg, var(--bgColor-muted, #f6f8fa));
22
22
  }
23
23
 
24
24
  @media (prefers-color-scheme: dark) {
25
25
  .canvasScroll {
26
- background-color: var(--bgColor-muted, #161b22);
26
+ background-color: var(--sb--canvas-bg, var(--bgColor-muted, #161b22));
27
27
  }
28
28
  }
29
29
 
@@ -0,0 +1,74 @@
1
+ export function getCanvasPrimerAttrs(theme) {
2
+ if (String(theme || 'light') === 'dark_dimmed') {
3
+ return {
4
+ 'data-color-mode': 'dark',
5
+ 'data-dark-theme': 'dark_dimmed',
6
+ 'data-light-theme': 'light',
7
+ }
8
+ }
9
+ if (String(theme || 'light').startsWith('dark')) {
10
+ return {
11
+ 'data-color-mode': 'dark',
12
+ 'data-dark-theme': 'dark',
13
+ 'data-light-theme': 'light',
14
+ }
15
+ }
16
+ return {
17
+ 'data-color-mode': 'light',
18
+ 'data-dark-theme': 'dark',
19
+ 'data-light-theme': 'light',
20
+ }
21
+ }
22
+
23
+ export function getCanvasThemeVars(theme) {
24
+ const value = String(theme || 'light')
25
+ if (value === 'dark_dimmed') {
26
+ return {
27
+ '--sb--canvas-bg': '#22272e',
28
+ '--bgColor-default': '#22272e',
29
+ '--bgColor-muted': '#22272e',
30
+ '--bgColor-neutral-muted': 'rgba(99, 110, 123, 0.3)',
31
+ '--bgColor-accent-emphasis': '#316dca',
32
+ '--tc-bg-muted': '#22272e',
33
+ '--tc-dot-color': 'rgba(205, 217, 229, 0.22)',
34
+ '--overlay-backdrop-bgColor': 'rgba(205, 217, 229, 0.22)',
35
+ '--fgColor-muted': '#768390',
36
+ '--fgColor-default': '#adbac7',
37
+ '--fgColor-onEmphasis': '#ffffff',
38
+ '--borderColor-default': '#444c56',
39
+ '--borderColor-muted': '#545d68',
40
+ }
41
+ }
42
+ if (value.startsWith('dark')) {
43
+ return {
44
+ '--sb--canvas-bg': '#161b22',
45
+ '--bgColor-default': '#161b22',
46
+ '--bgColor-muted': '#161b22',
47
+ '--bgColor-neutral-muted': 'rgba(110, 118, 129, 0.2)',
48
+ '--bgColor-accent-emphasis': '#2f81f7',
49
+ '--tc-bg-muted': '#161b22',
50
+ '--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
51
+ '--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
52
+ '--fgColor-muted': '#8b949e',
53
+ '--fgColor-default': '#e6edf3',
54
+ '--fgColor-onEmphasis': '#ffffff',
55
+ '--borderColor-default': '#30363d',
56
+ '--borderColor-muted': '#30363d',
57
+ }
58
+ }
59
+ return {
60
+ '--sb--canvas-bg': '#f6f8fa',
61
+ '--bgColor-default': '#ffffff',
62
+ '--tc-bg-muted': '#f6f8fa',
63
+ '--tc-dot-color': 'rgba(0, 0, 0, 0.08)',
64
+ '--overlay-backdrop-bgColor': 'rgba(0, 0, 0, 0.08)',
65
+ '--bgColor-muted': '#f6f8fa',
66
+ '--bgColor-neutral-muted': '#eaeef2',
67
+ '--bgColor-accent-emphasis': '#2f81f7',
68
+ '--fgColor-muted': '#656d76',
69
+ '--fgColor-default': '#1f2328',
70
+ '--fgColor-onEmphasis': '#ffffff',
71
+ '--borderColor-default': '#d1d9e0',
72
+ '--borderColor-muted': '#d8dee4',
73
+ }
74
+ }
@@ -1,6 +1,11 @@
1
1
  .block {
2
2
  min-height: 80px;
3
- background: var(--bgColor-default, #ffffff);
3
+ --sb--markdown-bg: var(--bgColor-default, #ffffff);
4
+ --sb--markdown-fg: var(--fgColor-default, #1f2328);
5
+ --sb--markdown-muted: var(--fgColor-muted, #656d76);
6
+ --sb--markdown-accent: var(--bgColor-accent-emphasis, #2f81f7);
7
+ background: var(--sb--markdown-bg);
8
+ color: var(--sb--markdown-fg);
4
9
  font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
5
10
  }
6
11
 
@@ -8,11 +13,15 @@
8
13
  padding: 16px 20px;
9
14
  font-size: 14px;
10
15
  line-height: 1.6;
11
- color: var(--fgColor-default, #1f2328);
16
+ color: var(--sb--markdown-fg);
12
17
  cursor: text;
13
18
  min-height: 60px;
14
19
  }
15
20
 
21
+ .preview :global(*) {
22
+ color: inherit;
23
+ }
24
+
16
25
  .preview h1 {
17
26
  font-size: 20px;
18
27
  font-weight: 700;
@@ -56,7 +65,7 @@
56
65
  }
57
66
 
58
67
  .preview :global(.placeholder) {
59
- color: var(--fgColor-muted, #656d76);
68
+ color: var(--sb--markdown-muted);
60
69
  font-style: italic;
61
70
  }
62
71
 
@@ -69,10 +78,10 @@
69
78
  padding: 16px 20px;
70
79
  border: none;
71
80
  outline: none;
72
- background: var(--bgColor-default, #ffffff);
81
+ background: var(--sb--markdown-bg);
73
82
  font-family: ui-monospace, SFMono-Regular, monospace;
74
83
  font-size: 13px;
75
84
  line-height: 1.5;
76
- color: var(--fgColor-default, #1f2328);
85
+ color: var(--sb--markdown-fg);
77
86
  resize: none;
78
87
  }
@@ -2,6 +2,7 @@ import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
2
2
  import { buildPrototypeIndex } from '@dfosco/storyboard-core'
3
3
  import WidgetWrapper from './WidgetWrapper.jsx'
4
4
  import { readProp, prototypeEmbedSchema } from './widgetProps.js'
5
+ import { getEmbedChromeVars } from './embedTheme.js'
5
6
  import styles from './PrototypeEmbed.module.css'
6
7
 
7
8
  function formatName(name) {
@@ -10,6 +11,23 @@ function formatName(name) {
10
11
  .replace(/\b\w/g, (c) => c.toUpperCase())
11
12
  }
12
13
 
14
+ function resolveCanvasThemeFromStorage() {
15
+ if (typeof localStorage === 'undefined') return 'light'
16
+ let sync = { prototype: true, toolbar: false, codeBoxes: true, canvas: false }
17
+ try {
18
+ const rawSync = localStorage.getItem('sb-theme-sync')
19
+ if (rawSync) sync = { ...sync, ...JSON.parse(rawSync) }
20
+ } catch {
21
+ // Ignore malformed sync settings
22
+ }
23
+ if (!sync.canvas) return 'light'
24
+ const attrTheme = document.documentElement.getAttribute('data-sb-canvas-theme')
25
+ if (attrTheme) return attrTheme
26
+ const stored = localStorage.getItem('sb-color-scheme') || 'system'
27
+ if (stored !== 'system') return stored
28
+ return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
29
+ }
30
+
13
31
  export default function PrototypeEmbed({ props, onUpdate }) {
14
32
  const src = readProp(props, 'src', prototypeEmbedSchema)
15
33
  const width = readProp(props, 'width', prototypeEmbedSchema)
@@ -19,17 +37,21 @@ export default function PrototypeEmbed({ props, onUpdate }) {
19
37
 
20
38
  const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
21
39
  const rawSrc = src ? `${basePath}${src}` : ''
22
- const iframeSrc = rawSrc ? `${rawSrc}${rawSrc.includes('?') ? '&' : '?'}_sb_embed` : ''
23
40
 
24
41
  const scale = zoom / 100
25
42
 
26
43
  const [editing, setEditing] = useState(false)
27
44
  const [interactive, setInteractive] = useState(false)
28
45
  const [filter, setFilter] = useState('')
46
+ const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
29
47
  const inputRef = useRef(null)
30
48
  const filterRef = useRef(null)
31
49
  const embedRef = useRef(null)
32
50
 
51
+ const iframeSrc = rawSrc
52
+ ? `${rawSrc}${rawSrc.includes('?') ? '&' : '?'}_sb_embed&_sb_theme_target=prototype&_sb_canvas_theme=${canvasTheme}`
53
+ : ''
54
+
33
55
  // Build prototype index for the picker
34
56
  const prototypeIndex = useMemo(() => {
35
57
  try {
@@ -132,6 +154,17 @@ export default function PrototypeEmbed({ props, onUpdate }) {
132
154
  return () => document.removeEventListener('pointerdown', handlePointerDown)
133
155
  }, [interactive])
134
156
 
157
+ useEffect(() => {
158
+ function readToolbarTheme() {
159
+ setCanvasTheme(resolveCanvasThemeFromStorage())
160
+ }
161
+ readToolbarTheme()
162
+ document.addEventListener('storyboard:theme:changed', readToolbarTheme)
163
+ return () => document.removeEventListener('storyboard:theme:changed', readToolbarTheme)
164
+ }, [])
165
+
166
+ const chromeVars = useMemo(() => getEmbedChromeVars(canvasTheme), [canvasTheme])
167
+
135
168
  const enterInteractive = useCallback(() => setInteractive(true), [])
136
169
 
137
170
  function handlePickRoute(route) {
@@ -158,7 +191,7 @@ export default function PrototypeEmbed({ props, onUpdate }) {
158
191
  <div
159
192
  ref={embedRef}
160
193
  className={styles.embed}
161
- style={{ width, height }}
194
+ style={{ width, height, ...chromeVars }}
162
195
  >
163
196
  {editing ? (
164
197
  <div
@@ -52,10 +52,9 @@
52
52
  align-items: center;
53
53
  justify-content: center;
54
54
  border-radius: 6px;
55
- background: rgba(255, 255, 255, 0.92);
56
- backdrop-filter: blur(12px);
57
- -webkit-backdrop-filter: blur(12px);
58
- border: 1px solid rgba(0, 0, 0, 0.12);
55
+ background: var(--bgColor-default, rgba(255, 255, 255, 0.92));
56
+ border: 1px solid var(--borderColor-default, rgba(0, 0, 0, 0.12));
57
+ color: var(--fgColor-default, #1f2328);
59
58
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.15);
60
59
  font-size: 14px;
61
60
  opacity: 0;
@@ -68,18 +67,7 @@
68
67
  }
69
68
 
70
69
  .editBtn:hover {
71
- background: rgba(255, 255, 255, 0.98);
72
- }
73
-
74
- @media (prefers-color-scheme: dark) {
75
- .editBtn {
76
- background: rgba(22, 27, 34, 0.88);
77
- border-color: rgba(255, 255, 255, 0.1);
78
- }
79
-
80
- .editBtn:hover {
81
- background: rgba(30, 37, 46, 0.95);
82
- }
70
+ background: var(--bgColor-muted, rgba(255, 255, 255, 0.98));
83
71
  }
84
72
 
85
73
  .urlForm {
@@ -0,0 +1,10 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { getEmbedChromeVars } from './embedTheme.js'
3
+
4
+ describe('getEmbedChromeVars', () => {
5
+ it('follows toolbar theme variants for embed edit chrome', () => {
6
+ expect(getEmbedChromeVars('light')['--bgColor-default']).toBe('#ffffff')
7
+ expect(getEmbedChromeVars('dark')['--bgColor-default']).toBe('#161b22')
8
+ expect(getEmbedChromeVars('dark_dimmed')['--bgColor-default']).toBe('#22272e')
9
+ })
10
+ })
@@ -14,8 +14,7 @@
14
14
  position: relative;
15
15
  }
16
16
 
17
- :global(html[data-color-mode='dark']) .sticky,
18
- :global(html[data-sb-theme^='dark']) .sticky {
17
+ :global([data-sb-canvas-theme^='dark']) .sticky {
19
18
  background: color-mix(in srgb, var(--sticky-bg) 30%, #0d1117 70%);
20
19
  border-color: color-mix(in srgb, var(--sticky-bg) 55%, #f0f6fc 18%);
21
20
  box-shadow: 2px 3px 10px rgba(0, 0, 0, 0.35);
@@ -33,8 +32,7 @@
33
32
  min-height: 60px;
34
33
  }
35
34
 
36
- :global(html[data-color-mode='dark']) .text,
37
- :global(html[data-sb-theme^='dark']) .text {
35
+ :global([data-sb-canvas-theme^='dark']) .text {
38
36
  color: color-mix(in srgb, var(--sticky-bg) 30%, #f0f6fc 70%);
39
37
  }
40
38
 
@@ -59,8 +57,7 @@
59
57
  resize: none;
60
58
  }
61
59
 
62
- :global(html[data-color-mode='dark']) .textarea,
63
- :global(html[data-sb-theme^='dark']) .textarea {
60
+ :global([data-sb-canvas-theme^='dark']) .textarea {
64
61
  color: color-mix(in srgb, var(--sticky-bg) 26%, #f0f6fc 74%);
65
62
  }
66
63
 
@@ -99,8 +96,7 @@
99
96
  z-index: 10;
100
97
  }
101
98
 
102
- :global(html[data-color-mode='dark']) .pickerPopup,
103
- :global(html[data-sb-theme^='dark']) .pickerPopup {
99
+ :global([data-sb-canvas-theme^='dark']) .pickerPopup {
104
100
  background: var(--bgColor-muted, #161b22);
105
101
  box-shadow:
106
102
  0 0 0 1px rgba(255, 255, 255, 0.08),
@@ -0,0 +1,49 @@
1
+ export function getEmbedChromeVars(theme) {
2
+ const value = String(theme || 'light')
3
+ if (value === 'dark_dimmed') {
4
+ return {
5
+ '--bgColor-default': '#22272e',
6
+ '--bgColor-muted': '#2d333b',
7
+ '--bgColor-neutral-muted': 'rgba(99, 110, 123, 0.3)',
8
+ '--fgColor-default': '#adbac7',
9
+ '--fgColor-muted': '#768390',
10
+ '--fgColor-onEmphasis': '#ffffff',
11
+ '--borderColor-default': '#444c56',
12
+ '--borderColor-muted': '#545d68',
13
+ '--bgColor-accent-emphasis': '#316dca',
14
+ '--trigger-bg': '#2d333b',
15
+ '--trigger-bg-hover': '#373e47',
16
+ '--trigger-border': '#444c56',
17
+ }
18
+ }
19
+ if (value.startsWith('dark')) {
20
+ return {
21
+ '--bgColor-default': '#161b22',
22
+ '--bgColor-muted': '#21262d',
23
+ '--bgColor-neutral-muted': 'rgba(110, 118, 129, 0.2)',
24
+ '--fgColor-default': '#e6edf3',
25
+ '--fgColor-muted': '#8b949e',
26
+ '--fgColor-onEmphasis': '#ffffff',
27
+ '--borderColor-default': '#30363d',
28
+ '--borderColor-muted': '#30363d',
29
+ '--bgColor-accent-emphasis': '#2f81f7',
30
+ '--trigger-bg': '#21262d',
31
+ '--trigger-bg-hover': '#30363d',
32
+ '--trigger-border': '#30363d',
33
+ }
34
+ }
35
+ return {
36
+ '--bgColor-default': '#ffffff',
37
+ '--bgColor-muted': '#f6f8fa',
38
+ '--bgColor-neutral-muted': '#eaeef2',
39
+ '--fgColor-default': '#1f2328',
40
+ '--fgColor-muted': '#656d76',
41
+ '--fgColor-onEmphasis': '#ffffff',
42
+ '--borderColor-default': '#d0d7de',
43
+ '--borderColor-muted': '#d8dee4',
44
+ '--bgColor-accent-emphasis': '#2f81f7',
45
+ '--trigger-bg': '#f6f8fa',
46
+ '--trigger-bg-hover': '#eaeef2',
47
+ '--trigger-border': '#d0d7de',
48
+ }
49
+ }
package/src/context.jsx CHANGED
@@ -73,6 +73,9 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
73
73
  if (canvasName) return null
74
74
  const requested = sceneParam || flowName || sceneName
75
75
  if (requested) {
76
+ // Allow fully-scoped flow names from URLs/widgets without re-prefixing
77
+ // (e.g. "Proto/flow" should not become "Proto/Proto/flow").
78
+ if (requested.includes('/')) return requested
76
79
  return resolveFlowName(prototypeName, requested)
77
80
  }
78
81
  // 1. Page-specific flow (e.g., Example/Forms)
@@ -83,8 +86,14 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
83
86
  const protoFlow = resolveFlowName(prototypeName, prototypeName)
84
87
  if (flowExists(protoFlow)) return protoFlow
85
88
  }
86
- // 3. Global default
87
- return 'default'
89
+ // 3. Prototype-scoped default (e.g. Example/default)
90
+ if (prototypeName) {
91
+ const scopedDefault = resolveFlowName(prototypeName, 'default')
92
+ if (flowExists(scopedDefault)) return scopedDefault
93
+ }
94
+ // 4. Global default — or null if no flow exists at all
95
+ if (flowExists('default')) return 'default'
96
+ return null
88
97
  }, [canvasName, sceneParam, flowName, sceneName, prototypeName, pageFlow])
89
98
 
90
99
  // Auto-install body class sync (sb-key--value classes on <body>)
@@ -106,9 +115,10 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
106
115
  return () => cleanup?.()
107
116
  }, [])
108
117
 
109
- // Skip flow loading for canvas pages
118
+ // Skip flow loading for canvas pages and flow-less pages
110
119
  const { data, error } = useMemo(() => {
111
120
  if (canvasName) return { data: null, error: null }
121
+ if (!activeFlowName) return { data: {}, error: null }
112
122
  try {
113
123
  let flowData = loadFlow(activeFlowName)
114
124