@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 +2 -2
- package/src/canvas/CanvasPage.bridge.test.jsx +66 -1
- package/src/canvas/CanvasPage.jsx +54 -2
- package/src/canvas/CanvasPage.module.css +2 -2
- package/src/canvas/canvasTheme.js +74 -0
- package/src/canvas/widgets/MarkdownBlock.module.css +14 -5
- package/src/canvas/widgets/PrototypeEmbed.jsx +35 -2
- package/src/canvas/widgets/PrototypeEmbed.module.css +4 -16
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +10 -0
- package/src/canvas/widgets/StickyNote.module.css +4 -8
- package/src/canvas/widgets/embedTheme.js +49 -0
- package/src/context.jsx +13 -3
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "3.
|
|
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: () => ({
|
|
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
|
|
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={
|
|
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
|
-
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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
|
-
|
|
57
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
87
|
-
|
|
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
|
|