@dfosco/storyboard-react 4.2.0-beta.2 → 4.2.0-beta.20

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 (85) hide show
  1. package/package.json +9 -4
  2. package/src/AuthModal/AuthModal.jsx +6 -2
  3. package/src/BranchBar/BranchBar.jsx +20 -6
  4. package/src/BranchBar/BranchBar.module.css +13 -4
  5. package/src/BranchBar/useBranches.js +20 -6
  6. package/src/BranchBar/useBranches.test.js +68 -0
  7. package/src/CommandPalette/CommandPalette.jsx +478 -186
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +561 -191
  11. package/src/Viewfinder.module.css +434 -93
  12. package/src/Workspace.jsx +7 -0
  13. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  14. package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
  15. package/src/canvas/CanvasPage.jsx +738 -216
  16. package/src/canvas/CanvasPage.module.css +13 -15
  17. package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
  18. package/src/canvas/ConnectorLayer.jsx +121 -153
  19. package/src/canvas/ConnectorLayer.module.css +69 -0
  20. package/src/canvas/PageSelector.test.jsx +15 -6
  21. package/src/canvas/canvasApi.js +68 -2
  22. package/src/canvas/connectorGeometry.js +132 -0
  23. package/src/canvas/hotPoolDevLogs.js +25 -0
  24. package/src/canvas/useCanvas.js +1 -1
  25. package/src/canvas/useMarqueeSelect.js +30 -4
  26. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  27. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  28. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  29. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  30. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  31. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  32. package/src/canvas/widgets/ExpandedPane.jsx +472 -0
  33. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  34. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  35. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  38. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  39. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  40. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  41. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  42. package/src/canvas/widgets/LinkPreview.jsx +112 -4
  43. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  44. package/src/canvas/widgets/MarkdownBlock.jsx +164 -17
  45. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  46. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  47. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  48. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -38
  49. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  50. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  51. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  52. package/src/canvas/widgets/StoryWidget.jsx +72 -15
  53. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  54. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  55. package/src/canvas/widgets/TerminalWidget.jsx +496 -69
  56. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  57. package/src/canvas/widgets/TilesWidget.jsx +302 -0
  58. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  59. package/src/canvas/widgets/WidgetChrome.jsx +73 -153
  60. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  61. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  62. package/src/canvas/widgets/expandUtils.js +557 -0
  63. package/src/canvas/widgets/expandUtils.test.js +155 -0
  64. package/src/canvas/widgets/index.js +9 -0
  65. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  66. package/src/canvas/widgets/tilePool.js +23 -0
  67. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  68. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  70. package/src/canvas/widgets/tiles/leaf.png +0 -0
  71. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  73. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  75. package/src/canvas/widgets/widgetConfig.js +55 -4
  76. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  77. package/src/canvas/widgets/widgetProps.js +1 -0
  78. package/src/context.jsx +47 -19
  79. package/src/hooks/useConfig.js +14 -0
  80. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  81. package/src/index.js +8 -2
  82. package/src/story/ComponentSetPage.jsx +186 -0
  83. package/src/story/ComponentSetPage.module.css +121 -0
  84. package/src/story/StoryPage.jsx +32 -2
  85. package/src/vite/data-plugin.js +324 -30
package/package.json CHANGED
@@ -1,17 +1,22 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "4.2.0-beta.2",
3
+ "version": "4.2.0-beta.20",
4
4
  "type": "module",
5
5
  "dependencies": {
6
6
  "@base-ui/react": "^1.4.0",
7
- "@dfosco/storyboard-core": "4.2.0-beta.2",
8
- "@dfosco/tiny-canvas": "4.2.0-beta.2",
7
+ "@dfosco/storyboard-core": "4.2.0-beta.20",
8
+ "@dfosco/tiny-canvas": "4.2.0-beta.20",
9
9
  "@neodrag/react": "^2.3.1",
10
+ "@radix-ui/react-dialog": "^1.1.15",
11
+ "@radix-ui/react-visually-hidden": "^1.2.4",
12
+ "ansi-to-html": "^0.7.2",
13
+ "cmdk": "^1.1.1",
14
+ "feather-icons": "^4.29.2",
15
+ "ghostty-web": "^0.4.0",
10
16
  "glob": "^11.0.0",
11
17
  "jsonc-parser": "^3.3.1",
12
18
  "remark": "^15.0.1",
13
19
  "remark-gfm": "^4.0.1",
14
- "react-cmdk": "^1.3.9",
15
20
  "remark-html": "^16.0.1"
16
21
  },
17
22
  "license": "MIT",
@@ -43,8 +43,12 @@ export default function AuthModal() {
43
43
  try { localStorage.setItem(COMMENTS_TOKEN_KEY, trimmed) } catch { /* ignore */ }
44
44
 
45
45
  try {
46
- import('@dfosco/storyboard-core/comments').then(({ setToken }) => {
46
+ import('@dfosco/storyboard-core/comments').then(({ setToken, validateToken }) => {
47
47
  setToken(trimmed)
48
+ // Validate to cache user info (login + avatar), then notify Viewfinder
49
+ validateToken(trimmed)
50
+ .then(() => document.dispatchEvent(new CustomEvent('storyboard:auth-changed')))
51
+ .catch(() => document.dispatchEvent(new CustomEvent('storyboard:auth-changed')))
48
52
  }).catch(() => {})
49
53
  } catch { /* comments module may not be initialized */ }
50
54
 
@@ -64,7 +68,7 @@ export default function AuthModal() {
64
68
  <Dialog.Backdrop className={css.backdrop} />
65
69
  <div className={css.popupWrap}>
66
70
  <Dialog.Popup className={css.popup}>
67
- <Dialog.Title className={css.title}>Sign in for comments</Dialog.Title>
71
+ <Dialog.Title className={css.title}>Sign in with GitHub</Dialog.Title>
68
72
  <Dialog.Description className={css.desc}>
69
73
  Leave comments for other users to see and respond, and react to! Storyboard
70
74
  comments use Discussions as a back-end and require a GitHub PAT to be enabled.
@@ -1,15 +1,24 @@
1
1
  /**
2
- * BranchBar — dark top bar showing current branch on non-main routes.
2
+ * BranchBar — blue accent bar showing current branch and local dev status.
3
3
  *
4
- * Dev: shows branch name as a static label (use CLI to switch branches).
5
- * Prod: same label (dropdown switching deferred to ViewfinderNew).
4
+ * Dev: always visible (main or branch). Shows "Local development" label.
5
+ * Prod: shows on non-main branches only.
6
6
  */
7
7
  import { useState, useEffect, useMemo } from 'react'
8
8
  import { GitBranchIcon } from '@primer/octicons-react'
9
9
  import css from './BranchBar.module.css'
10
10
 
11
+ /** Check if we're in local dev mode (respects ?prodMode simulation). */
12
+ function checkLocalDev() {
13
+ if (typeof window === 'undefined') return false
14
+ if (new URLSearchParams(window.location.search).has('prodMode')) return false
15
+ return window.__SB_LOCAL_DEV__ === true
16
+ }
17
+
11
18
  export default function BranchBar({ basePath }) {
12
- const [hidden, setHidden] = useState(false)
19
+ const [hidden, setHidden] = useState(
20
+ () => typeof document !== 'undefined' && document.documentElement.classList.contains('storyboard-chrome-hidden')
21
+ )
13
22
 
14
23
  const isHiddenByParam = useMemo(() => {
15
24
  if (typeof window === 'undefined') return false
@@ -22,6 +31,7 @@ export default function BranchBar({ basePath }) {
22
31
  return m ? m[1] : 'main'
23
32
  }, [basePath])
24
33
 
34
+ const isLocalDev = checkLocalDev()
25
35
  const isOnBranch = currentBranch !== 'main'
26
36
 
27
37
  useEffect(() => {
@@ -32,7 +42,7 @@ export default function BranchBar({ basePath }) {
32
42
  return () => observer.disconnect()
33
43
  }, [])
34
44
 
35
- if (!isOnBranch || hidden || isHiddenByParam) return null
45
+ if ((!isOnBranch && !isLocalDev) || hidden || isHiddenByParam) return null
36
46
 
37
47
  function hideChrome() {
38
48
  window.dispatchEvent(new KeyboardEvent('keydown', {
@@ -42,10 +52,14 @@ export default function BranchBar({ basePath }) {
42
52
 
43
53
  return (
44
54
  <div className={css.bar} data-branch-bar>
45
- <div className={css.barInner}>
55
+ <div className={`${css.barInner}${isLocalDev ? '' : ` ${css.barProd}`}`}>
46
56
  <span className={css.barLabel}>
47
57
  <GitBranchIcon size={12} />
48
58
  <span className={css.barBranchName}>{currentBranch}</span>
59
+ {isLocalDev && <>
60
+ <span className={css.barSeparator}>·</span>
61
+ <span>Local development</span>
62
+ </>}
49
63
  </span>
50
64
  <div className={css.barActions}>
51
65
  <button className={css.barAction} onClick={hideChrome}>Hide</button>
@@ -16,12 +16,16 @@
16
16
  justify-content: center;
17
17
  gap: 8px;
18
18
  height: 32px;
19
- background: var(--bgColor-emphasis, #1a1a1a);
20
- color: var(--fgColor-onEmphasis, #ccc);
19
+ background: hsl(212, 92%, 45%);
20
+ color: #fff;
21
21
  padding: 4px 12px;
22
22
  position: relative;
23
23
  }
24
24
 
25
+ .barProd {
26
+ background: #1c2128;
27
+ }
28
+
25
29
  .barTrigger {
26
30
  display: flex;
27
31
  align-items: center;
@@ -68,6 +72,11 @@
68
72
  white-space: nowrap;
69
73
  }
70
74
 
75
+ .barSeparator {
76
+ opacity: 0.6;
77
+ margin: 0 2px;
78
+ }
79
+
71
80
  .barAction {
72
81
  background: none;
73
82
  border: none;
@@ -87,11 +96,11 @@
87
96
  }
88
97
 
89
98
  /* Push page content down */
90
- :global(html:has([data-branch-bar])) {
99
+ :global(html:has([data-branch-bar]):not(.storyboard-chrome-hidden)) {
91
100
  --sb-branch-bar-height: 32px;
92
101
  }
93
102
 
94
- :global(html:has([data-branch-bar]) body) {
103
+ :global(html:has([data-branch-bar]):not(.storyboard-chrome-hidden) body) {
95
104
  padding-top: 32px;
96
105
  }
97
106
 
@@ -15,27 +15,41 @@ export function useBranches(basePath) {
15
15
  })
16
16
 
17
17
  const [gitUser, setGitUser] = useState(null)
18
+ const branchBasePath = (basePath || '/').replace(/\/branch--[^/]*\/?$/, '/')
18
19
 
19
20
  useEffect(() => {
20
21
  const apiBase = (basePath || '/').replace(/\/$/, '')
22
+ const branchManifestUrl = `${branchBasePath.replace(/\/$/, '')}/branches.json`
21
23
 
22
24
  fetch(`${apiBase}/_storyboard/git-user`).then(r => r.ok ? r.json() : null)
23
25
  .then(data => { if (data?.name) setGitUser(data.name) })
24
26
  .catch(() => {})
25
27
 
26
- // Always fetch live branch list
28
+ // Keep dev behavior: prefer live branch list from server API.
29
+ // On static deployments (GitHub Pages), fallback to branches.json.
27
30
  fetch(`${apiBase}/_storyboard/worktrees`).then(r => r.ok ? r.json() : null)
28
- .then(data => { if (Array.isArray(data) && data.length > 0) setBranches(data) })
29
- .catch(() => {})
30
- }, [basePath])
31
+ .then(data => {
32
+ if (Array.isArray(data) && data.length > 0) {
33
+ setBranches(data)
34
+ return true
35
+ }
36
+ return false
37
+ })
38
+ .catch(() => false)
39
+ .then((hasLiveData) => {
40
+ if (hasLiveData || isLocalDev) return
41
+ return fetch(branchManifestUrl)
42
+ .then(r => r.ok ? r.json() : null)
43
+ .then(data => { if (Array.isArray(data) && data.length > 0) setBranches(data) })
44
+ .catch(() => {})
45
+ })
46
+ }, [basePath, branchBasePath])
31
47
 
32
48
  const currentBranch = useMemo(() => {
33
49
  const m = (basePath || '').match(/\/branch--([^/]+)\/?$/)
34
50
  return m ? m[1] : 'main'
35
51
  }, [basePath])
36
52
 
37
- const branchBasePath = (basePath || '/').replace(/\/branch--[^/]*\/$/, '/')
38
-
39
53
  return { branches, currentBranch, branchBasePath, gitUser }
40
54
  }
41
55
 
@@ -0,0 +1,68 @@
1
+ import { renderHook, waitFor } from '@testing-library/react'
2
+ import { useBranches } from './useBranches.js'
3
+
4
+ describe('useBranches', () => {
5
+ afterEach(() => {
6
+ delete window.__SB_BRANCHES__
7
+ vi.unstubAllGlobals()
8
+ })
9
+
10
+ it('loads branches from branches.json when worktrees API is unavailable on branch deploys', async () => {
11
+ const branchesFromManifest = [
12
+ { folder: 'branch--4.3.0-prompt-widget', branch: '4.3.0-prompt-widget' },
13
+ { folder: 'branch--main', branch: 'main' },
14
+ ]
15
+
16
+ const fetchMock = vi.fn((url) => {
17
+ if (url === '/storyboard/branch--4.3.0-prompt-widget/_storyboard/git-user') {
18
+ return Promise.resolve({ ok: false, json: async () => null })
19
+ }
20
+ if (url === '/storyboard/branch--4.3.0-prompt-widget/_storyboard/worktrees') {
21
+ return Promise.resolve({ ok: false, json: async () => null })
22
+ }
23
+ if (url === '/storyboard/branches.json') {
24
+ return Promise.resolve({ ok: true, json: async () => branchesFromManifest })
25
+ }
26
+ return Promise.reject(new Error(`Unexpected fetch URL: ${url}`))
27
+ })
28
+
29
+ vi.stubGlobal('fetch', fetchMock)
30
+
31
+ const { result } = renderHook(() => useBranches('/storyboard/branch--4.3.0-prompt-widget/'))
32
+
33
+ await waitFor(() => {
34
+ expect(result.current.branches).toEqual(branchesFromManifest)
35
+ })
36
+
37
+ expect(fetchMock).toHaveBeenCalledWith('/storyboard/branches.json')
38
+ })
39
+
40
+ it('resolves branches.json from repo root when basePath has no trailing slash', async () => {
41
+ const branchesFromManifest = [
42
+ { folder: 'branch--4.3.0-prompt-widget', branch: '4.3.0-prompt-widget' },
43
+ ]
44
+
45
+ const fetchMock = vi.fn((url) => {
46
+ if (url === '/storyboard/branch--4.3.0-prompt-widget/_storyboard/git-user') {
47
+ return Promise.resolve({ ok: false, json: async () => null })
48
+ }
49
+ if (url === '/storyboard/branch--4.3.0-prompt-widget/_storyboard/worktrees') {
50
+ return Promise.resolve({ ok: false, json: async () => null })
51
+ }
52
+ if (url === '/storyboard/branches.json') {
53
+ return Promise.resolve({ ok: true, json: async () => branchesFromManifest })
54
+ }
55
+ return Promise.reject(new Error(`Unexpected fetch URL: ${url}`))
56
+ })
57
+
58
+ vi.stubGlobal('fetch', fetchMock)
59
+
60
+ const { result } = renderHook(() => useBranches('/storyboard/branch--4.3.0-prompt-widget'))
61
+
62
+ await waitFor(() => {
63
+ expect(result.current.branches).toEqual(branchesFromManifest)
64
+ })
65
+
66
+ expect(fetchMock).toHaveBeenCalledWith('/storyboard/branches.json')
67
+ })
68
+ })