@dfosco/storyboard-react 4.0.0-beta.8 → 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.
Files changed (75) hide show
  1. package/package.json +6 -3
  2. package/src/AuthModal/AuthModal.jsx +134 -0
  3. package/src/AuthModal/AuthModal.module.css +221 -0
  4. package/src/BranchBar/BranchBar.jsx +56 -0
  5. package/src/BranchBar/BranchBar.module.css +230 -0
  6. package/src/BranchBar/useBranches.js +79 -0
  7. package/src/CommandPalette/CommandPalette.jsx +936 -0
  8. package/src/CommandPalette/CreateDialog.jsx +219 -0
  9. package/src/CommandPalette/command-palette.css +111 -0
  10. package/src/Icon.jsx +180 -0
  11. package/src/Viewfinder.jsx +1104 -57
  12. package/src/Viewfinder.module.css +1107 -149
  13. package/src/canvas/CanvasControls.jsx +51 -2
  14. package/src/canvas/CanvasControls.module.css +31 -0
  15. package/src/canvas/CanvasPage.bridge.test.jsx +142 -19
  16. package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
  17. package/src/canvas/CanvasPage.jsx +807 -251
  18. package/src/canvas/CanvasPage.module.css +98 -50
  19. package/src/canvas/CanvasPage.multiselect.test.jsx +13 -11
  20. package/src/canvas/CanvasToolbar.jsx +2 -2
  21. package/src/canvas/MarqueeOverlay.jsx +20 -0
  22. package/src/canvas/PageSelector.jsx +239 -0
  23. package/src/canvas/PageSelector.module.css +165 -0
  24. package/src/canvas/PageSelector.test.jsx +104 -0
  25. package/src/canvas/canvasApi.js +22 -8
  26. package/src/canvas/canvasTheme.js +96 -52
  27. package/src/canvas/componentIsolate.jsx +33 -7
  28. package/src/canvas/useCanvas.js +9 -8
  29. package/src/canvas/useCanvas.test.js +4 -4
  30. package/src/canvas/useMarqueeSelect.js +187 -0
  31. package/src/canvas/useMarqueeSelect.test.js +78 -0
  32. package/src/canvas/widgets/CodePenEmbed.jsx +292 -0
  33. package/src/canvas/widgets/CodePenEmbed.module.css +161 -0
  34. package/src/canvas/widgets/ComponentWidget.jsx +42 -10
  35. package/src/canvas/widgets/ComponentWidget.module.css +6 -5
  36. package/src/canvas/widgets/FigmaEmbed.jsx +110 -24
  37. package/src/canvas/widgets/FigmaEmbed.module.css +21 -7
  38. package/src/canvas/widgets/LinkPreview.jsx +297 -11
  39. package/src/canvas/widgets/LinkPreview.module.css +386 -18
  40. package/src/canvas/widgets/LinkPreview.test.jsx +193 -0
  41. package/src/canvas/widgets/MarkdownBlock.jsx +86 -5
  42. package/src/canvas/widgets/MarkdownBlock.module.css +64 -15
  43. package/src/canvas/widgets/PrototypeEmbed.jsx +96 -145
  44. package/src/canvas/widgets/PrototypeEmbed.module.css +74 -4
  45. package/src/canvas/widgets/StickyNote.module.css +5 -0
  46. package/src/canvas/widgets/StickyNote.test.jsx +9 -9
  47. package/src/canvas/widgets/StoryWidget.jsx +277 -0
  48. package/src/canvas/widgets/StoryWidget.module.css +211 -0
  49. package/src/canvas/widgets/WidgetChrome.jsx +76 -20
  50. package/src/canvas/widgets/WidgetChrome.module.css +2 -6
  51. package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
  52. package/src/canvas/widgets/codepenUrl.js +75 -0
  53. package/src/canvas/widgets/codepenUrl.test.js +76 -0
  54. package/src/canvas/widgets/embedInteraction.test.jsx +235 -0
  55. package/src/canvas/widgets/embedOverlay.module.css +35 -0
  56. package/src/canvas/widgets/embedTheme.js +138 -39
  57. package/src/canvas/widgets/githubUrl.js +82 -0
  58. package/src/canvas/widgets/githubUrl.test.js +74 -0
  59. package/src/canvas/widgets/iframeDevLogs.js +49 -0
  60. package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
  61. package/src/canvas/widgets/index.js +4 -0
  62. package/src/canvas/widgets/pasteRules.js +295 -0
  63. package/src/canvas/widgets/pasteRules.test.js +474 -0
  64. package/src/canvas/widgets/snapshotDisplay.test.jsx +259 -0
  65. package/src/canvas/widgets/widgetConfig.js +16 -5
  66. package/src/canvas/widgets/widgetConfig.test.js +34 -12
  67. package/src/context.jsx +145 -16
  68. package/src/hooks/useSceneData.js +4 -2
  69. package/src/hooks/useThemeState.js +61 -0
  70. package/src/hooks/useThemeState.test.js +66 -0
  71. package/src/index.js +10 -0
  72. package/src/story/StoryPage.jsx +117 -0
  73. package/src/story/StoryPage.module.css +18 -0
  74. package/src/vite/data-plugin.js +348 -66
  75. 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
+ }