@dfosco/storyboard 0.7.0 → 0.8.1

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 (258) hide show
  1. package/assets/favicon.svg +12 -0
  2. package/dist/storyboard-ui.css +1 -1
  3. package/dist/storyboard-ui.js +13907 -10816
  4. package/dist/storyboard-ui.js.map +1 -1
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +138 -127
  7. package/scaffold/AGENTS.md +9 -7
  8. package/scaffold/README.md +18 -0
  9. package/scaffold/deploy.yml +21 -0
  10. package/scaffold/gitignore +8 -14
  11. package/scaffold/preview.yml +40 -0
  12. package/scaffold/{manifest.json → scaffold.config.json} +73 -3
  13. package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
  14. package/scaffold/skills/canvas/SKILL.md +1 -1
  15. package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
  16. package/scaffold/skills/ship/SKILL.md +2 -2
  17. package/scaffold/skills/worktree/SKILL.md +1 -1
  18. package/scaffold/src/_prototype.jsx +17 -0
  19. package/scaffold/src/_story.jsx +13 -0
  20. package/scaffold/src/library/README.md +34 -0
  21. package/scaffold/src/library/_app.jsx +53 -0
  22. package/scaffold/src/library/_app.module.css +8 -0
  23. package/scaffold/src/library/create.jsx +10 -0
  24. package/scaffold/src/library/index.jsx +14 -0
  25. package/scaffold/src/library/mount.jsx +52 -0
  26. package/scaffold/src/library/prototypes-entry.jsx +57 -0
  27. package/scaffold/src/library/routes.jsx +205 -0
  28. package/scaffold/src/library/viewfinder.jsx +13 -0
  29. package/scaffold/src/library/workspace.jsx +17 -0
  30. package/scripts/aggregate-artifacts.mjs +257 -0
  31. package/src/core/artifact/operations.js +19 -13
  32. package/src/core/autosync/server.test.js +1 -1
  33. package/src/core/canvas/githubEmbeds.js +12 -0
  34. package/src/core/canvas/githubEmbeds.test.js +71 -2
  35. package/src/core/canvas/hot-pool.js +10 -11
  36. package/src/core/canvas/materializer.js +39 -13
  37. package/src/core/canvas/server.js +324 -69
  38. package/src/core/canvas/server.test.js +365 -2
  39. package/src/core/canvas/terminal-server.js +144 -57
  40. package/src/core/canvas/undoRedo.js +174 -0
  41. package/src/core/canvas/undoRedo.test.js +250 -0
  42. package/src/core/cli/buildManifest.js +34 -0
  43. package/src/core/cli/canvasRedo.js +44 -0
  44. package/src/core/cli/canvasUndo.js +40 -0
  45. package/src/core/cli/create.js +116 -18
  46. package/src/core/cli/dev.js +23 -35
  47. package/src/core/cli/index.js +21 -0
  48. package/src/core/cli/schemas.js +2 -3
  49. package/src/core/cli/serverUrl.js +24 -0
  50. package/src/core/cli/setup.js +14 -25
  51. package/src/core/cli/terminal-welcome.js +12 -6
  52. package/src/core/cli/updateVersion.js +42 -2
  53. package/src/core/cli/workleaf.js +140 -0
  54. package/src/core/comments/api.js +1 -1
  55. package/src/core/comments/queries.js +3 -0
  56. package/src/core/comments/ui/CommentWindow.jsx +3 -2
  57. package/src/core/data/artifactIndex.js +235 -0
  58. package/src/core/data/artifactIndex.test.js +203 -0
  59. package/src/core/data/artifactManifest.js +290 -0
  60. package/src/core/data/artifactManifest.test.js +207 -0
  61. package/src/core/data/viewfinder.js +47 -7
  62. package/src/core/index.js +6 -1
  63. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
  64. package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
  65. package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
  66. package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
  67. package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
  68. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
  69. package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
  70. package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
  71. package/src/core/lib/components/ui/dialog/index.js +4 -34
  72. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
  73. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
  74. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
  75. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
  76. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
  77. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
  78. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
  79. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
  80. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
  81. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
  82. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
  83. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
  84. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
  85. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
  86. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
  87. package/src/core/lib/components/ui/label/label.jsx +1 -2
  88. package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
  89. package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
  90. package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
  91. package/src/core/lib/components/ui/panel/panel.jsx +11 -2
  92. package/src/core/lib/components/ui/separator/separator.jsx +6 -4
  93. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
  94. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
  95. package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
  96. package/src/core/mountStoryboardCore.js +234 -74
  97. package/src/core/mountStoryboardCore.test.js +174 -0
  98. package/src/core/rename-watcher/watcher.js +329 -36
  99. package/src/core/rename-watcher/watcher.test.js +253 -0
  100. package/src/core/scaffold/fragments.js +287 -0
  101. package/src/core/scaffold/fragments.test.js +401 -0
  102. package/src/core/scaffold/index.js +291 -0
  103. package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
  104. package/src/core/scaffold/scaffold-integration.test.js +247 -0
  105. package/src/core/scaffold.js +4 -97
  106. package/src/core/stores/configSchema.js +41 -2
  107. package/src/core/stores/recentArtifacts.js +49 -2
  108. package/src/core/stores/recentArtifacts.test.js +59 -6
  109. package/src/core/stores/themeBootstrap.js +264 -0
  110. package/src/core/stores/themeBootstrap.test.js +211 -0
  111. package/src/core/stores/themeStore.test.js +154 -0
  112. package/src/core/stores/themeStore.ts +235 -220
  113. package/src/core/styles/generateThemesCss.js +63 -0
  114. package/src/core/styles/tailwind.css +12 -4
  115. package/src/core/styles/workspace-chrome.css +30 -0
  116. package/src/core/tools/handlers/paletteTheme.js +11 -8
  117. package/src/core/ui/ActionMenuButton.jsx +2 -1
  118. package/src/core/ui/CanvasCreateMenu.jsx +1 -19
  119. package/src/core/ui/CoreUIBar.jsx +10 -7
  120. package/src/core/ui/ThemeMenuButton.jsx +45 -30
  121. package/src/core/vite/server-plugin.js +36 -0
  122. package/src/core/workleaf/cleanup.js +102 -0
  123. package/src/core/workleaf/create.js +199 -0
  124. package/src/core/workleaf/delete.js +101 -0
  125. package/src/core/workleaf/list.js +57 -0
  126. package/src/core/workleaf/merge.js +238 -0
  127. package/src/core/workleaf/paths.js +146 -0
  128. package/src/core/workleaf/paths.test.js +230 -0
  129. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +28 -21
  130. package/src/core/workshop/features/createFlow/server.js +5 -1
  131. package/src/core/workshop/features/createPrototype/server.js +5 -1
  132. package/src/core/workshop/features/templateIndex.js +28 -1
  133. package/src/core/worktree/port.js +48 -5
  134. package/src/core/worktree/port.test.js +42 -0
  135. package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
  136. package/src/internals/ArtifactForm/ArtifactForm.module.css +48 -66
  137. package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
  138. package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
  139. package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
  140. package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
  141. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
  142. package/src/internals/CommandPalette/CommandPalette.jsx +25 -19
  143. package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
  144. package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
  145. package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
  146. package/src/internals/CommandPalette/createHelpers.js +30 -0
  147. package/src/internals/CreatePage/CreatePage.jsx +57 -0
  148. package/src/internals/CreatePage/CreatePage.module.css +34 -0
  149. package/src/internals/Dialog.jsx +51 -0
  150. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +533 -0
  151. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
  152. package/src/internals/Tooltip.jsx +39 -0
  153. package/src/internals/Viewfinder.jsx +196 -48
  154. package/src/internals/Viewfinder.module.css +72 -1
  155. package/src/internals/canvas/CanvasPage.jsx +267 -124
  156. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
  157. package/src/internals/canvas/PageSelector.jsx +15 -1
  158. package/src/internals/canvas/PageSelector.module.css +14 -0
  159. package/src/internals/canvas/canvasApi.js +40 -5
  160. package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
  161. package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
  162. package/src/internals/canvas/isolation/prototypeRoutes.jsx +131 -0
  163. package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
  164. package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
  165. package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
  166. package/src/internals/canvas/useUndoRedo.js +75 -60
  167. package/src/internals/canvas/useUndoRedo.test.js +46 -178
  168. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
  169. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
  170. package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
  171. package/src/internals/canvas/widgets/LinkPreview.jsx +45 -1
  172. package/src/internals/canvas/widgets/LinkPreview.module.css +71 -0
  173. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +69 -10
  174. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +114 -0
  175. package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -4
  176. package/src/internals/canvas/widgets/StoryWidget.jsx +21 -38
  177. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
  178. package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
  179. package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
  180. package/src/internals/canvas/widgets/embedTheme.js +2 -2
  181. package/src/internals/canvas/widgets/expandUtils.js +19 -19
  182. package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
  183. package/src/internals/context.jsx +22 -1
  184. package/src/internals/context.test.jsx +47 -0
  185. package/src/internals/hooks/useArtifactIndex.js +31 -0
  186. package/src/internals/hooks/useArtifactIndex.test.js +45 -0
  187. package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
  188. package/src/internals/index.js +6 -0
  189. package/src/internals/loadConsumerWrapper.js +60 -0
  190. package/src/internals/story/ComponentSetPage.jsx +75 -34
  191. package/src/internals/story/StoryPage.jsx +16 -25
  192. package/src/internals/vite/data-plugin.js +313 -148
  193. package/src/internals/vite/data-plugin.test.js +11 -11
  194. package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
  195. package/terminal.config.json +1 -1
  196. package/src/core/lib/components/ui/card/card-action.jsx +0 -11
  197. package/src/core/lib/components/ui/card/card-content.jsx +0 -11
  198. package/src/core/lib/components/ui/card/card-description.jsx +0 -11
  199. package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
  200. package/src/core/lib/components/ui/card/card-header.jsx +0 -19
  201. package/src/core/lib/components/ui/card/card-title.jsx +0 -11
  202. package/src/core/lib/components/ui/card/card.jsx +0 -17
  203. package/src/core/lib/components/ui/card/index.js +0 -25
  204. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
  205. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
  206. package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
  207. package/src/core/lib/components/ui/collapsible/index.js +0 -13
  208. package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
  209. package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
  210. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
  211. package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
  212. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
  213. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
  214. package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
  215. package/src/core/lib/components/ui/popover/index.js +0 -28
  216. package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
  217. package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
  218. package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
  219. package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
  220. package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
  221. package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
  222. package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
  223. package/src/core/lib/components/ui/popover/popover.jsx +0 -4
  224. package/src/core/lib/components/ui/select/index.js +0 -37
  225. package/src/core/lib/components/ui/select/select-content.jsx +0 -30
  226. package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
  227. package/src/core/lib/components/ui/select/select-group.jsx +0 -15
  228. package/src/core/lib/components/ui/select/select-item.jsx +0 -26
  229. package/src/core/lib/components/ui/select/select-label.jsx +0 -11
  230. package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
  231. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
  232. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
  233. package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
  234. package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
  235. package/src/core/lib/components/ui/select/select.jsx +0 -4
  236. package/src/core/lib/components/ui/sheet/index.js +0 -34
  237. package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
  238. package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
  239. package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
  240. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
  241. package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
  242. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
  243. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
  244. package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
  245. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
  246. package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
  247. package/src/core/lib/components/ui/toggle/index.js +0 -8
  248. package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
  249. package/src/core/lib/components/ui/toggle-group/index.js +0 -10
  250. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
  251. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
  252. package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
  253. package/src/internals/canvas/componentIsolate.jsx +0 -144
  254. package/src/internals/canvas/componentSetIsolate.jsx +0 -300
  255. package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
  256. package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
  257. package/src/primer/ThemeSync.jsx +0 -73
  258. package/src/primer/index.js +0 -1
@@ -0,0 +1,533 @@
1
+ /**
2
+ * SimpleWorkspace — minimal "old index"-style homescreen for /
3
+ *
4
+ * Renders only in **dev mode**. In prod (or when ?prodMode is on), delegates
5
+ * to the full <Workspace /> so deployed environments keep the existing UX.
6
+ *
7
+ * Defaults to showing artifacts created by the current git user — solo / small
8
+ * teams see a focused list instead of every artifact in the repo. The "Show all"
9
+ * button at the bottom expands to the full set without leaving the simplified view.
10
+ *
11
+ * Filter rules for "mine":
12
+ * - item.gitAuthor === git user.name (the file's git creator), OR
13
+ * - item.author (string or array) contains the GitHub login (case-insensitive,
14
+ * normalized — strips leading @, trims, splits comma-separated strings).
15
+ *
16
+ * Note: "edited by you" is not tracked per file — only creator data is available.
17
+ * If we add `lastAuthor` later, extend `isMine()` to include it.
18
+ *
19
+ * Draft / private artifacts (e.g. canvases under `drafts/` or folders marked
20
+ * `_isPrivate`) are NOT filtered out — they render with an EyeClosedIcon badge
21
+ * to match the full Workspace's behaviour, so the user can still reach their
22
+ * own work-in-progress from the simplified homepage.
23
+ */
24
+ import { useState, useEffect, useMemo, useCallback } from 'react'
25
+ import {
26
+ buildPrototypeIndex,
27
+ getCustomerModeConfig,
28
+ } from '../../core/index.js'
29
+ import { isProdMode } from '../../core/utils/prodMode.js'
30
+ import {
31
+ mergeArtifactIndexIntoPrototypeIndex,
32
+ buildBranchArtifactHref,
33
+ } from '../../core/data/artifactIndex.js'
34
+ import { ChevronRightIcon, EyeClosedIcon, PlusIcon } from '@primer/octicons-react'
35
+ import { Menu } from '@base-ui/react/menu'
36
+ import { useBranches } from '../BranchBar/useBranches.js'
37
+ import { useArtifactIndex } from '../hooks/useArtifactIndex.js'
38
+ import Workspace, { CreateMenu } from '../Viewfinder.jsx'
39
+ import Icon from '../Icon.jsx'
40
+ import css from './SimpleWorkspace.module.css'
41
+
42
+ /* ─── Helpers ─── */
43
+
44
+ function withBase(basePath, route) {
45
+ const normalizedRoute = route.startsWith('/') ? route : `/${route}`
46
+ const normalizedBase = (basePath || '/').replace(/\/+$/, '')
47
+ if (!normalizedBase || normalizedBase === '/') return normalizedRoute
48
+ return `${normalizedBase}${normalizedRoute}`.replace(/\/+/g, '/')
49
+ }
50
+
51
+ function normalizeAuthorList(author) {
52
+ if (!author) return []
53
+ const raw = Array.isArray(author) ? author : String(author).split(',')
54
+ return raw
55
+ .map(a => String(a).trim().replace(/^@/, '').toLowerCase())
56
+ .filter(Boolean)
57
+ }
58
+
59
+ /**
60
+ * Returns true when an item was created by the current user, or explicitly
61
+ * lists them as a metadata author.
62
+ */
63
+ function isMine(item, gitName, ghLogin) {
64
+ if (gitName && item.gitAuthor && item.gitAuthor === gitName) return true
65
+ if (ghLogin) {
66
+ const login = ghLogin.toLowerCase()
67
+ const authors = normalizeAuthorList(item.author)
68
+ if (authors.includes(login)) return true
69
+ }
70
+ return false
71
+ }
72
+
73
+ /* ─── Current user (name + login) ─── */
74
+
75
+ const USER_CACHE_KEY = 'sb:simple-workspace:git-user-v1'
76
+
77
+ function readCachedUser() {
78
+ if (typeof window === 'undefined') return null
79
+ try {
80
+ const raw = window.sessionStorage?.getItem(USER_CACHE_KEY)
81
+ if (!raw) return null
82
+ const parsed = JSON.parse(raw)
83
+ if (parsed && typeof parsed === 'object') {
84
+ return { name: parsed.name || null, login: parsed.login || null }
85
+ }
86
+ } catch {
87
+ // Ignore storage / parse errors — fall through to null.
88
+ }
89
+ return null
90
+ }
91
+
92
+ function writeCachedUser(user) {
93
+ if (typeof window === 'undefined') return
94
+ try {
95
+ window.sessionStorage?.setItem(USER_CACHE_KEY, JSON.stringify(user))
96
+ } catch {
97
+ // Ignore quota / privacy-mode write failures.
98
+ }
99
+ }
100
+
101
+ /**
102
+ * Reads the current git user. Synchronously hydrates from sessionStorage on
103
+ * mount so subsequent loads in the same tab never flash unfiltered content.
104
+ * Exposes a `loaded` flag so the consumer can suppress rendering until we know
105
+ * whether the "mine" filter should apply — otherwise the page paints with all
106
+ * items, then snaps to "mine only" once the fetch resolves.
107
+ */
108
+ function useCurrentUser(basePath) {
109
+ const cached = useMemo(() => readCachedUser(), [])
110
+ const [user, setUser] = useState(cached || { name: null, login: null })
111
+ const [loaded, setLoaded] = useState(!!cached)
112
+ useEffect(() => {
113
+ const apiBase = (basePath || '/').replace(/\/$/, '')
114
+ let cancelled = false
115
+ fetch(`${apiBase}/_storyboard/git-user`)
116
+ .then(r => (r.ok ? r.json() : null))
117
+ .then(data => {
118
+ if (cancelled) return
119
+ if (data) {
120
+ const next = { name: data.name || null, login: data.login || null }
121
+ setUser(next)
122
+ writeCachedUser(next)
123
+ }
124
+ setLoaded(true)
125
+ })
126
+ .catch(() => { if (!cancelled) setLoaded(true) })
127
+ return () => { cancelled = true }
128
+ }, [basePath])
129
+ return { ...user, loaded }
130
+ }
131
+
132
+ /* ─── Avatars ─── */
133
+
134
+ function AvatarStack({ authors }) {
135
+ if (!authors || authors.length === 0) return null
136
+ const list = Array.isArray(authors) ? authors : [authors]
137
+ return (
138
+ <div className={css.avatarStack}>
139
+ {list.map(username => (
140
+ <img
141
+ key={username}
142
+ className={css.avatarImg}
143
+ src={`https://github.com/${username}.png?size=48`}
144
+ alt={username}
145
+ width={20}
146
+ height={20}
147
+ loading="lazy"
148
+ onError={(e) => { e.target.style.display = 'none' }}
149
+ />
150
+ ))}
151
+ </div>
152
+ )
153
+ }
154
+
155
+ /* ─── Cards ─── */
156
+
157
+ function FolderCard({ folder }) {
158
+ const isPrivate = !!folder.isPrivate
159
+ return (
160
+ <div className={`${css.folderCard}${isPrivate ? ' ' + css.folderCardPrivate : ''}`}>
161
+ <span className={css.folderIcon} aria-hidden="true">
162
+ <Icon name="folder" size={18} />
163
+ </span>
164
+ <span className={css.folderName}>{folder.dirName.replace(/\.folder$/, '').toUpperCase()}</span>
165
+ {isPrivate && (
166
+ <span
167
+ className={css.privateBadge}
168
+ aria-label="Draft folder — only visible to you"
169
+ title="Draft folder — only visible to you"
170
+ >
171
+ <EyeClosedIcon size={12} />
172
+ </span>
173
+ )}
174
+ {folder.description && <span className={css.folderDesc}>{folder.description}</span>}
175
+ </div>
176
+ )
177
+ }
178
+
179
+ function ArtifactRow({ item, basePath, branchBasePath }) {
180
+ const isBranchOnly = !!item.isBranchOnly
181
+ const isExternal = item.isExternal
182
+ const branchOnlyHref = isBranchOnly
183
+ ? isExternal
184
+ ? item.externalUrl
185
+ : buildBranchArtifactHref(branchBasePath || basePath, item.homeBranch?.folder, item.homeBranch?.route || item.route)
186
+ : null
187
+ const href = isBranchOnly
188
+ ? branchOnlyHref || '#'
189
+ : isExternal
190
+ ? item.externalUrl
191
+ : item.route ? withBase(basePath, item.route) : '#'
192
+
193
+ const linkProps = isBranchOnly || isExternal
194
+ ? { href, target: '_blank', rel: 'noopener noreferrer' }
195
+ : { href }
196
+
197
+ const authorList = Array.isArray(item.author)
198
+ ? item.author
199
+ : item.author
200
+ ? [item.author]
201
+ : item.gitAuthor ? [item.gitAuthor] : []
202
+
203
+ const isPrivate = !!item.isPrivate
204
+ const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Item'
205
+ const cardClass = `${css.card}${isPrivate ? ' ' + css.cardPrivate : ''}`
206
+
207
+ return (
208
+ <a className={cardClass} {...linkProps}>
209
+ <div className={css.cardBody}>
210
+ <div className={css.cardTitleRow}>
211
+ <span className={css.cardTitle}>{item.name}</span>
212
+ {isPrivate && (
213
+ <span
214
+ className={css.privateBadge}
215
+ aria-label={`Draft ${typeLabel.toLowerCase()} — only visible to you`}
216
+ title={`Draft ${typeLabel.toLowerCase()} — only visible to you`}
217
+ >
218
+ <EyeClosedIcon size={12} />
219
+ </span>
220
+ )}
221
+ <ChevronRightIcon size={16} className={css.cardChevron} />
222
+ </div>
223
+ {item.description && (
224
+ <div className={css.cardDescription}>{item.description}</div>
225
+ )}
226
+ {authorList.length > 0 && (
227
+ <div className={css.cardFooter}>
228
+ <AvatarStack authors={authorList} />
229
+ <span className={css.cardAuthorList}>{authorList.join(', ')}</span>
230
+ </div>
231
+ )}
232
+ </div>
233
+ </a>
234
+ )
235
+ }
236
+
237
+ /* ─── Tabs ─── */
238
+
239
+ const TABS = [
240
+ { id: 'prototypes', label: 'Prototypes' },
241
+ { id: 'canvases', label: 'Canvas' },
242
+ ]
243
+
244
+ /* ─── Impl ─── */
245
+
246
+ function SimpleWorkspaceImpl({
247
+ pageModules = {},
248
+ basePath,
249
+ title = 'Storyboard',
250
+ subtitle,
251
+ }) {
252
+ const ghUser = useCurrentUser(basePath)
253
+ const { currentBranch, branchBasePath } = useBranches(basePath)
254
+ const { artifactIndex } = useArtifactIndex(basePath)
255
+
256
+ // Re-render on canvas/story HMR events so the list stays in sync with disk.
257
+ const [hmrTick, setHmrTick] = useState(0)
258
+ useEffect(() => {
259
+ const bump = () => setHmrTick(t => t + 1)
260
+ document.addEventListener('storyboard:canvas-index-changed', bump)
261
+ document.addEventListener('storyboard:story-index-changed', bump)
262
+ return () => {
263
+ document.removeEventListener('storyboard:canvas-index-changed', bump)
264
+ document.removeEventListener('storyboard:story-index-changed', bump)
265
+ }
266
+ }, [])
267
+
268
+ const knownRoutes = useMemo(() =>
269
+ Object.keys(pageModules)
270
+ .map(p => p.replace('/src/prototypes/', '').replace('.jsx', ''))
271
+ .filter(n => !n.startsWith('_') && n !== 'index' && n !== 'workspace' && n !== 'viewfinder' && n !== 'create'),
272
+ [pageModules],
273
+ )
274
+
275
+ // hmrTick is an intentional re-render trigger from HMR events
276
+ // eslint-disable-next-line react-hooks/exhaustive-deps
277
+ const localIndex = useMemo(() => buildPrototypeIndex(knownRoutes), [knownRoutes, hmrTick])
278
+ const protoIndex = useMemo(
279
+ () => mergeArtifactIndexIntoPrototypeIndex(localIndex, artifactIndex, currentBranch),
280
+ [localIndex, artifactIndex, currentBranch],
281
+ )
282
+
283
+ // Flatten prototypes + canvases (preserve folder membership) into a single list.
284
+ const allItems = useMemo(() => {
285
+ const items = []
286
+
287
+ const addProto = (proto) => {
288
+ const defaultFlow = proto.flows?.find(f => f.meta?.default === true)
289
+ const route = proto.route
290
+ ?? (defaultFlow?.route
291
+ ?? (proto.flows?.length > 0 ? proto.flows[0].route : `/${proto.dirName}`))
292
+ items.push({
293
+ id: proto.id || `proto:${proto.dirName}`,
294
+ name: proto.name,
295
+ type: 'prototype',
296
+ author: proto.author,
297
+ gitAuthor: proto.gitAuthor,
298
+ lastModified: proto.lastModified,
299
+ route,
300
+ isExternal: proto.isExternal,
301
+ externalUrl: proto.externalUrl,
302
+ folder: proto.folder,
303
+ description: proto.description,
304
+ isPrivate: !!proto.isPrivate,
305
+ isBranchOnly: !!proto.isBranchOnly,
306
+ homeBranch: proto.homeBranch || null,
307
+ })
308
+ }
309
+
310
+ const addCanvas = (canvas) => {
311
+ items.push({
312
+ id: canvas.id || `canvas:${canvas.dirName}`,
313
+ name: canvas.name,
314
+ type: 'canvas',
315
+ author: canvas.author,
316
+ gitAuthor: canvas.gitAuthor,
317
+ lastModified: canvas.lastModified || null,
318
+ route: canvas.route,
319
+ isExternal: false,
320
+ externalUrl: null,
321
+ folder: canvas.folder,
322
+ description: canvas.description,
323
+ isPrivate: !!canvas.isPrivate,
324
+ isBranchOnly: !!canvas.isBranchOnly,
325
+ homeBranch: canvas.homeBranch || null,
326
+ })
327
+ }
328
+
329
+ for (const proto of protoIndex.prototypes || []) addProto(proto)
330
+ for (const folder of protoIndex.folders || []) {
331
+ for (const proto of folder.prototypes || []) addProto(proto)
332
+ for (const canvas of folder.canvases || []) addCanvas(canvas)
333
+ }
334
+ for (const canvas of protoIndex.canvases || []) addCanvas(canvas)
335
+
336
+ return items
337
+ }, [protoIndex])
338
+
339
+ // Tab + "Show all" state
340
+ const [activeTab, setActiveTab] = useState('prototypes')
341
+ const [showAll, setShowAll] = useState(false)
342
+ const [showCreate, setShowCreate] = useState(false)
343
+
344
+ const canFilter = !!(ghUser.name || ghUser.login)
345
+
346
+ // Filter by tab + (optionally) "mine".
347
+ // Draft / private items (e.g. canvases under `drafts/`) are kept — they render
348
+ // with an EyeClosedIcon badge so the user can still see and reach them.
349
+ const visibleItems = useMemo(() => {
350
+ const tabType = activeTab === 'prototypes' ? 'prototype' : 'canvas'
351
+ let pool = allItems.filter(i => i.type === tabType)
352
+ if (!showAll && canFilter) {
353
+ pool = pool.filter(i => isMine(i, ghUser.name, ghUser.login))
354
+ }
355
+ return pool
356
+ }, [allItems, activeTab, showAll, canFilter, ghUser.name, ghUser.login])
357
+
358
+ // Folders visible only if they contain at least one visible item of the active tab type.
359
+ // Private folders (e.g. `drafts/`) are kept and rendered with a draft badge.
360
+ const visibleFolders = useMemo(() => {
361
+ const folderDirs = new Set(visibleItems.map(i => i.folder).filter(Boolean))
362
+ return (protoIndex.folders || []).filter(f => folderDirs.has(f.dirName))
363
+ }, [protoIndex, visibleItems])
364
+
365
+ // Counts for the "Show all" button label
366
+ const allTabCount = useMemo(() => {
367
+ const tabType = activeTab === 'prototypes' ? 'prototype' : 'canvas'
368
+ return allItems.filter(i => i.type === tabType).length
369
+ }, [allItems, activeTab])
370
+ const mineTabCount = useMemo(() => {
371
+ if (!canFilter) return allTabCount
372
+ const tabType = activeTab === 'prototypes' ? 'prototype' : 'canvas'
373
+ return allItems.filter(
374
+ i => i.type === tabType && isMine(i, ghUser.name, ghUser.login),
375
+ ).length
376
+ }, [allItems, activeTab, canFilter, ghUser.name, ghUser.login, allTabCount])
377
+ const hiddenCount = Math.max(0, allTabCount - mineTabCount)
378
+
379
+ // Sort items by lastModified desc (newest first)
380
+ const sortedItems = useMemo(() => {
381
+ return [...visibleItems].sort((a, b) => {
382
+ const aTime = a.lastModified ? new Date(a.lastModified).getTime() : 0
383
+ const bTime = b.lastModified ? new Date(b.lastModified).getTime() : 0
384
+ return bTime - aTime
385
+ })
386
+ }, [visibleItems])
387
+
388
+ // Group sorted items by folder for rendering
389
+ const grouped = useMemo(() => {
390
+ const byFolder = new Map()
391
+ const ungrouped = []
392
+ for (const item of sortedItems) {
393
+ if (item.folder) {
394
+ if (!byFolder.has(item.folder)) byFolder.set(item.folder, [])
395
+ byFolder.get(item.folder).push(item)
396
+ } else {
397
+ ungrouped.push(item)
398
+ }
399
+ }
400
+ return { byFolder, ungrouped }
401
+ }, [sortedItems])
402
+
403
+ const handleToggleShowAll = useCallback(() => setShowAll(s => !s), [])
404
+
405
+ return (
406
+ <div className={css.page}>
407
+ <div className={css.inner}>
408
+ {/* Header */}
409
+ <header className={css.header}>
410
+ <div className={css.headerTop}>
411
+ <div>
412
+ <h1 className={css.title}>{title}</h1>
413
+ {subtitle && <p className={css.subtitle}>{subtitle}</p>}
414
+ </div>
415
+ <Menu.Root open={showCreate} onOpenChange={setShowCreate}>
416
+ <Menu.Trigger className={css.createBtn}>
417
+ <PlusIcon size={14} /> Create
418
+ </Menu.Trigger>
419
+ <Menu.Portal>
420
+ <Menu.Positioner className={css.createDropdownPositioner} side="bottom" align="end" sideOffset={4}>
421
+ <Menu.Popup className={css.createDropdown}>
422
+ <CreateMenu onClose={() => setShowCreate(false)} basePath={basePath} />
423
+ </Menu.Popup>
424
+ </Menu.Positioner>
425
+ </Menu.Portal>
426
+ </Menu.Root>
427
+ </div>
428
+
429
+ <div className={css.tabs} role="tablist">
430
+ {TABS.map(tab => (
431
+ <button
432
+ key={tab.id}
433
+ role="tab"
434
+ aria-selected={activeTab === tab.id}
435
+ className={activeTab === tab.id ? css.tabActive : css.tab}
436
+ onClick={() => setActiveTab(tab.id)}
437
+ type="button"
438
+ >
439
+ {tab.label}
440
+ </button>
441
+ ))}
442
+ </div>
443
+ </header>
444
+
445
+ {/* Cards */}
446
+ <div className={css.list}>
447
+ {!ghUser.loaded ? (
448
+ // Suppress paint until we know whether to filter to "mine".
449
+ // Otherwise the page flashes the full list, then snaps to "mine only"
450
+ // once the git-user fetch resolves.
451
+ null
452
+ ) : visibleFolders.length === 0 && sortedItems.length === 0 ? (
453
+ <div className={css.empty}>
454
+ {canFilter && !showAll
455
+ ? `No ${activeTab === 'prototypes' ? 'prototypes' : 'canvases'} created by you yet.`
456
+ : `No ${activeTab === 'prototypes' ? 'prototypes' : 'canvases'} found.`}
457
+ </div>
458
+ ) : (
459
+ <>
460
+ {/* Ungrouped items first (no folder) */}
461
+ {grouped.ungrouped.map(item => (
462
+ <ArtifactRow
463
+ key={item.id}
464
+ item={item}
465
+ basePath={basePath}
466
+ branchBasePath={branchBasePath}
467
+ />
468
+ ))}
469
+
470
+ {/* Then each folder as a section: header + its items */}
471
+ {visibleFolders.map(folder => {
472
+ const folderItems = grouped.byFolder.get(folder.dirName) || []
473
+ return (
474
+ <section key={folder.dirName} className={css.folderSection}>
475
+ <FolderCard folder={folder} />
476
+ <div className={css.folderItems}>
477
+ {folderItems.map(item => (
478
+ <ArtifactRow
479
+ key={item.id}
480
+ item={item}
481
+ basePath={basePath}
482
+ branchBasePath={branchBasePath}
483
+ />
484
+ ))}
485
+ </div>
486
+ </section>
487
+ )
488
+ })}
489
+ </>
490
+ )}
491
+ </div>
492
+
493
+ {/* Footer actions */}
494
+ <div className={css.footer}>
495
+ {ghUser.loaded && canFilter && hiddenCount > 0 && (
496
+ <button
497
+ type="button"
498
+ className={css.expandBtn}
499
+ onClick={handleToggleShowAll}
500
+ >
501
+ {showAll
502
+ ? `Show only my ${activeTab === 'prototypes' ? 'prototypes' : 'canvases'}`
503
+ : `Show all ${activeTab === 'prototypes' ? 'prototypes' : 'canvases'} (${hiddenCount} more)`}
504
+ </button>
505
+ )}
506
+ <a className={css.expandBtn} href={withBase(basePath, '/workspace')}>
507
+ See full workspace
508
+ </a>
509
+ </div>
510
+ </div>
511
+ </div>
512
+ )
513
+ }
514
+
515
+ /* ─── Public wrapper ─── */
516
+
517
+ /**
518
+ * SimpleWorkspace — a minimal dev-mode homescreen.
519
+ *
520
+ * In prod (or with ?prodMode), delegates to the full <Workspace />.
521
+ * Respects customerMode.hideHomepage like Workspace does.
522
+ */
523
+ export default function SimpleWorkspace(props) {
524
+ const cm = getCustomerModeConfig()
525
+ if (cm.enabled && cm.hideHomepage) return null
526
+ if (isProdMode()) return <Workspace {...props} />
527
+ // SSR / non-browser fallback: render full Workspace so static prerenders
528
+ // produce the expected output.
529
+ if (typeof window === 'undefined') return <Workspace {...props} />
530
+ // Local dev (window.__SB_LOCAL_DEV__ true and no ?prodMode)
531
+ if (window.__SB_LOCAL_DEV__ !== true) return <Workspace {...props} />
532
+ return <SimpleWorkspaceImpl {...props} />
533
+ }