@dfosco/storyboard-react 4.0.0-beta.9 → 4.0.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 +6 -3
- package/src/AuthModal/AuthModal.jsx +134 -0
- package/src/AuthModal/AuthModal.module.css +221 -0
- package/src/BranchBar/BranchBar.jsx +56 -0
- package/src/BranchBar/BranchBar.module.css +230 -0
- package/src/BranchBar/useBranches.js +79 -0
- package/src/CommandPalette/CommandPalette.jsx +936 -0
- package/src/CommandPalette/CreateDialog.jsx +219 -0
- package/src/CommandPalette/command-palette.css +111 -0
- package/src/Icon.jsx +180 -0
- package/src/Viewfinder.jsx +1104 -57
- package/src/Viewfinder.module.css +1107 -149
- package/src/canvas/CanvasControls.jsx +51 -2
- package/src/canvas/CanvasControls.module.css +31 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +142 -19
- package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
- package/src/canvas/CanvasPage.jsx +807 -251
- package/src/canvas/CanvasPage.module.css +98 -50
- package/src/canvas/CanvasPage.multiselect.test.jsx +13 -11
- package/src/canvas/CanvasToolbar.jsx +2 -2
- package/src/canvas/MarqueeOverlay.jsx +20 -0
- package/src/canvas/PageSelector.jsx +239 -0
- package/src/canvas/PageSelector.module.css +165 -0
- package/src/canvas/PageSelector.test.jsx +104 -0
- package/src/canvas/canvasApi.js +22 -8
- package/src/canvas/canvasTheme.js +96 -52
- package/src/canvas/componentIsolate.jsx +33 -7
- package/src/canvas/useCanvas.js +9 -8
- package/src/canvas/useCanvas.test.js +4 -4
- package/src/canvas/useMarqueeSelect.js +187 -0
- package/src/canvas/useMarqueeSelect.test.js +78 -0
- package/src/canvas/widgets/CodePenEmbed.jsx +292 -0
- package/src/canvas/widgets/CodePenEmbed.module.css +161 -0
- package/src/canvas/widgets/ComponentWidget.jsx +42 -10
- package/src/canvas/widgets/ComponentWidget.module.css +6 -5
- package/src/canvas/widgets/FigmaEmbed.jsx +110 -24
- package/src/canvas/widgets/FigmaEmbed.module.css +21 -7
- package/src/canvas/widgets/LinkPreview.jsx +297 -11
- package/src/canvas/widgets/LinkPreview.module.css +386 -18
- package/src/canvas/widgets/LinkPreview.test.jsx +193 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +86 -5
- package/src/canvas/widgets/MarkdownBlock.module.css +64 -15
- package/src/canvas/widgets/PrototypeEmbed.jsx +96 -145
- package/src/canvas/widgets/PrototypeEmbed.module.css +74 -4
- package/src/canvas/widgets/StickyNote.module.css +5 -0
- package/src/canvas/widgets/StickyNote.test.jsx +9 -9
- package/src/canvas/widgets/StoryWidget.jsx +277 -0
- package/src/canvas/widgets/StoryWidget.module.css +211 -0
- package/src/canvas/widgets/WidgetChrome.jsx +76 -20
- package/src/canvas/widgets/WidgetChrome.module.css +2 -6
- package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
- package/src/canvas/widgets/codepenUrl.js +75 -0
- package/src/canvas/widgets/codepenUrl.test.js +76 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +235 -0
- package/src/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/canvas/widgets/embedTheme.js +138 -39
- package/src/canvas/widgets/githubUrl.js +82 -0
- package/src/canvas/widgets/githubUrl.test.js +74 -0
- package/src/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/canvas/widgets/index.js +4 -0
- package/src/canvas/widgets/pasteRules.js +295 -0
- package/src/canvas/widgets/pasteRules.test.js +474 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +259 -0
- package/src/canvas/widgets/widgetConfig.js +16 -5
- package/src/canvas/widgets/widgetConfig.test.js +34 -12
- package/src/context.jsx +145 -16
- package/src/hooks/useSceneData.js +4 -2
- package/src/hooks/useThemeState.js +61 -0
- package/src/hooks/useThemeState.test.js +66 -0
- package/src/index.js +10 -0
- package/src/story/StoryPage.jsx +117 -0
- package/src/story/StoryPage.module.css +18 -0
- package/src/vite/data-plugin.js +348 -66
- package/src/vite/data-plugin.test.js +405 -5
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useThemeState — React hook for the global storyboard theme.
|
|
3
|
+
*
|
|
4
|
+
* Subscribes to the core themeStore via useSyncExternalStore so React
|
|
5
|
+
* components re-render whenever the theme changes (user action, system
|
|
6
|
+
* preference toggle, or sync-target change).
|
|
7
|
+
*
|
|
8
|
+
* Returns { theme, resolved } where `theme` may be "system" and
|
|
9
|
+
* `resolved` is always a concrete Primer theme name.
|
|
10
|
+
*
|
|
11
|
+
* useThemeSyncTargets — React hook for which UI surfaces follow the theme.
|
|
12
|
+
* Returns { prototype, toolbar, codeBoxes, canvas }.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useSyncExternalStore } from 'react'
|
|
16
|
+
import {
|
|
17
|
+
themeState,
|
|
18
|
+
themeSyncState,
|
|
19
|
+
} from '@dfosco/storyboard-core'
|
|
20
|
+
|
|
21
|
+
// --- useThemeState ----------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function subscribeTheme(cb) {
|
|
24
|
+
return themeState.subscribe(cb)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let _themeSnapshot = null
|
|
28
|
+
themeState.subscribe((s) => { _themeSnapshot = s })
|
|
29
|
+
|
|
30
|
+
function getThemeSnapshot() {
|
|
31
|
+
return _themeSnapshot
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Subscribe to the global storyboard theme.
|
|
36
|
+
* @returns {{ theme: string, resolved: string }}
|
|
37
|
+
*/
|
|
38
|
+
export function useThemeState() {
|
|
39
|
+
return useSyncExternalStore(subscribeTheme, getThemeSnapshot, getThemeSnapshot)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// --- useThemeSyncTargets ----------------------------------------------------
|
|
43
|
+
|
|
44
|
+
function subscribeSyncTargets(cb) {
|
|
45
|
+
return themeSyncState.subscribe(cb)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let _syncSnapshot = null
|
|
49
|
+
themeSyncState.subscribe((s) => { _syncSnapshot = s })
|
|
50
|
+
|
|
51
|
+
function getSyncSnapshot() {
|
|
52
|
+
return _syncSnapshot
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Subscribe to which UI surfaces follow the global theme.
|
|
57
|
+
* @returns {{ prototype: boolean, toolbar: boolean, codeBoxes: boolean, canvas: boolean }}
|
|
58
|
+
*/
|
|
59
|
+
export function useThemeSyncTargets() {
|
|
60
|
+
return useSyncExternalStore(subscribeSyncTargets, getSyncSnapshot, getSyncSnapshot)
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { renderHook, act } from '@testing-library/react'
|
|
2
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
3
|
+
import { setTheme, setThemeSyncTarget } from '@dfosco/storyboard-core'
|
|
4
|
+
import { useThemeState, useThemeSyncTargets } from './useThemeState.js'
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
localStorage.clear()
|
|
8
|
+
// Reset to defaults
|
|
9
|
+
setTheme('system')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
describe('useThemeState', () => {
|
|
13
|
+
it('returns { theme, resolved }', () => {
|
|
14
|
+
const { result } = renderHook(() => useThemeState())
|
|
15
|
+
expect(result.current).toHaveProperty('theme')
|
|
16
|
+
expect(result.current).toHaveProperty('resolved')
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('defaults to system theme', () => {
|
|
20
|
+
const { result } = renderHook(() => useThemeState())
|
|
21
|
+
expect(result.current.theme).toBe('system')
|
|
22
|
+
// resolved should be 'light' or 'dark' depending on matchMedia mock
|
|
23
|
+
expect(['light', 'dark']).toContain(result.current.resolved)
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('updates when setTheme is called', () => {
|
|
27
|
+
const { result } = renderHook(() => useThemeState())
|
|
28
|
+
|
|
29
|
+
act(() => {
|
|
30
|
+
setTheme('dark_dimmed')
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
expect(result.current.theme).toBe('dark_dimmed')
|
|
34
|
+
expect(result.current.resolved).toBe('dark_dimmed')
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it('reverts to system when set back', () => {
|
|
38
|
+
const { result } = renderHook(() => useThemeState())
|
|
39
|
+
|
|
40
|
+
act(() => { setTheme('dark') })
|
|
41
|
+
expect(result.current.theme).toBe('dark')
|
|
42
|
+
|
|
43
|
+
act(() => { setTheme('system') })
|
|
44
|
+
expect(result.current.theme).toBe('system')
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
describe('useThemeSyncTargets', () => {
|
|
49
|
+
it('returns default sync targets', () => {
|
|
50
|
+
const { result } = renderHook(() => useThemeSyncTargets())
|
|
51
|
+
expect(result.current.prototype).toBe(true)
|
|
52
|
+
expect(result.current.toolbar).toBe(false)
|
|
53
|
+
expect(result.current.codeBoxes).toBe(true)
|
|
54
|
+
expect(result.current.canvas).toBe(true)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('updates when setThemeSyncTarget is called', () => {
|
|
58
|
+
const { result } = renderHook(() => useThemeSyncTargets())
|
|
59
|
+
|
|
60
|
+
act(() => {
|
|
61
|
+
setThemeSyncTarget('toolbar', true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
expect(result.current.toolbar).toBe(true)
|
|
65
|
+
})
|
|
66
|
+
})
|
package/src/index.js
CHANGED
|
@@ -24,6 +24,7 @@ export { useHideMode } from './hooks/useHideMode.js'
|
|
|
24
24
|
export { useUndoRedo } from './hooks/useUndoRedo.js'
|
|
25
25
|
export { useFeatureFlag } from './hooks/useFeatureFlag.js'
|
|
26
26
|
export { useMode } from './hooks/useMode.js'
|
|
27
|
+
export { useThemeState, useThemeSyncTargets } from './hooks/useThemeState.js'
|
|
27
28
|
|
|
28
29
|
// React Router integration
|
|
29
30
|
export { installHashPreserver } from './hashPreserver.js'
|
|
@@ -37,6 +38,15 @@ export { FormContext } from './context/FormContext.js'
|
|
|
37
38
|
// Viewfinder dashboard
|
|
38
39
|
export { default as Viewfinder } from './Viewfinder.jsx'
|
|
39
40
|
|
|
41
|
+
// Command Palette (includes BranchBar automatically)
|
|
42
|
+
export { default as StoryboardCommandPalette } from './CommandPalette/CommandPalette.jsx'
|
|
43
|
+
|
|
44
|
+
// Branch Bar (standalone, for consumers who don't use CommandPalette)
|
|
45
|
+
export { default as BranchBar } from './BranchBar/BranchBar.jsx'
|
|
46
|
+
|
|
47
|
+
// Auth Modal (standalone, for consumers who don't use CommandPalette)
|
|
48
|
+
export { default as AuthModal } from './AuthModal/AuthModal.jsx'
|
|
49
|
+
|
|
40
50
|
// Canvas
|
|
41
51
|
export { default as CanvasPage } from './canvas/CanvasPage.jsx'
|
|
42
52
|
export { useCanvas } from './canvas/useCanvas.js'
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoryPage — renders a .story.jsx module at its own route.
|
|
3
|
+
*
|
|
4
|
+
* Renders only the bare component(s) with no layout chrome.
|
|
5
|
+
* When ?export=ExportName is present, renders that single export.
|
|
6
|
+
* Without ?export, renders all named exports stacked.
|
|
7
|
+
*/
|
|
8
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
9
|
+
import { useLocation } from 'react-router-dom'
|
|
10
|
+
import { getStoryData } from '@dfosco/storyboard-core'
|
|
11
|
+
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
12
|
+
import styles from './StoryPage.module.css'
|
|
13
|
+
|
|
14
|
+
function StoryErrorFallback({ name, error }) {
|
|
15
|
+
return (
|
|
16
|
+
<div className={styles.error}>
|
|
17
|
+
<strong>{name}</strong>
|
|
18
|
+
<span>{String(error?.message || error)}</span>
|
|
19
|
+
</div>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default function StoryPage({ name }) {
|
|
24
|
+
const location = useLocation()
|
|
25
|
+
const searchParams = new URLSearchParams(location.search)
|
|
26
|
+
const exportFilter = searchParams.get('export')
|
|
27
|
+
const isEmbed = searchParams.has('_sb_embed')
|
|
28
|
+
|
|
29
|
+
const story = useMemo(() => getStoryData(name), [name])
|
|
30
|
+
const [exports, setExports] = useState(null)
|
|
31
|
+
const [error, setError] = useState(null)
|
|
32
|
+
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
if (!story?._storyImport) {
|
|
35
|
+
Promise.resolve().then(() => setError(`Story "${name}" not found or missing import`))
|
|
36
|
+
return
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let cancelled = false
|
|
40
|
+
story._storyImport()
|
|
41
|
+
.then((mod) => {
|
|
42
|
+
if (cancelled) return
|
|
43
|
+
const namedExports = {}
|
|
44
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
45
|
+
if (key !== 'default' && typeof value === 'function') {
|
|
46
|
+
namedExports[key] = value
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
setExports(namedExports)
|
|
50
|
+
setError(null)
|
|
51
|
+
})
|
|
52
|
+
.catch((err) => {
|
|
53
|
+
if (cancelled) return
|
|
54
|
+
setError(`Failed to load story "${name}": ${err.message || err}`)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
return () => { cancelled = true }
|
|
58
|
+
}, [name, story])
|
|
59
|
+
|
|
60
|
+
// Signal snapshot-ready after story renders in embed mode.
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (!isEmbed || !exports || window.parent === window) return
|
|
63
|
+
document.fonts.ready.then(() => {
|
|
64
|
+
requestAnimationFrame(() => requestAnimationFrame(() => {
|
|
65
|
+
window.__sbSnapshotReady?.()
|
|
66
|
+
}))
|
|
67
|
+
})
|
|
68
|
+
}, [isEmbed, exports])
|
|
69
|
+
|
|
70
|
+
if (error) {
|
|
71
|
+
return (
|
|
72
|
+
<StoryErrorFallback name={name} error={error} />
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!exports) {
|
|
77
|
+
if (isEmbed) return null
|
|
78
|
+
return (
|
|
79
|
+
<div className={styles.loading}>Loading story…</div>
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Single export mode
|
|
84
|
+
if (exportFilter) {
|
|
85
|
+
const Component = exports[exportFilter]
|
|
86
|
+
if (!Component) {
|
|
87
|
+
return (
|
|
88
|
+
<StoryErrorFallback
|
|
89
|
+
name={`${name}/${exportFilter}`}
|
|
90
|
+
error={`Export "${exportFilter}" not found in story "${name}"`}
|
|
91
|
+
/>
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<ThemeProvider colorMode="day">
|
|
97
|
+
<BaseStyles>
|
|
98
|
+
<Component />
|
|
99
|
+
</BaseStyles>
|
|
100
|
+
</ThemeProvider>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// All exports — render each component bare
|
|
105
|
+
const exportNames = Object.keys(exports)
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<ThemeProvider colorMode="day">
|
|
109
|
+
<BaseStyles>
|
|
110
|
+
{exportNames.map((exportName) => {
|
|
111
|
+
const Component = exports[exportName]
|
|
112
|
+
return <Component key={exportName} />
|
|
113
|
+
})}
|
|
114
|
+
</BaseStyles>
|
|
115
|
+
</ThemeProvider>
|
|
116
|
+
)
|
|
117
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
.loading {
|
|
2
|
+
display: flex;
|
|
3
|
+
align-items: center;
|
|
4
|
+
justify-content: center;
|
|
5
|
+
padding: 3rem;
|
|
6
|
+
color: var(--fgColor-muted, #656d76);
|
|
7
|
+
font-size: 0.875rem;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
.error {
|
|
11
|
+
display: flex;
|
|
12
|
+
flex-direction: column;
|
|
13
|
+
gap: 4px;
|
|
14
|
+
padding: 1rem;
|
|
15
|
+
color: var(--fgColor-danger, #cf222e);
|
|
16
|
+
font-size: 0.875rem;
|
|
17
|
+
line-height: 1.5;
|
|
18
|
+
}
|