@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.
- package/assets/favicon.svg +12 -0
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +13907 -10816
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/package.json +138 -127
- package/scaffold/AGENTS.md +9 -7
- package/scaffold/README.md +18 -0
- package/scaffold/deploy.yml +21 -0
- package/scaffold/gitignore +8 -14
- package/scaffold/preview.yml +40 -0
- package/scaffold/{manifest.json → scaffold.config.json} +73 -3
- package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
- package/scaffold/skills/canvas/SKILL.md +1 -1
- package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
- package/scaffold/skills/ship/SKILL.md +2 -2
- package/scaffold/skills/worktree/SKILL.md +1 -1
- package/scaffold/src/_prototype.jsx +17 -0
- package/scaffold/src/_story.jsx +13 -0
- package/scaffold/src/library/README.md +34 -0
- package/scaffold/src/library/_app.jsx +53 -0
- package/scaffold/src/library/_app.module.css +8 -0
- package/scaffold/src/library/create.jsx +10 -0
- package/scaffold/src/library/index.jsx +14 -0
- package/scaffold/src/library/mount.jsx +52 -0
- package/scaffold/src/library/prototypes-entry.jsx +57 -0
- package/scaffold/src/library/routes.jsx +205 -0
- package/scaffold/src/library/viewfinder.jsx +13 -0
- package/scaffold/src/library/workspace.jsx +17 -0
- package/scripts/aggregate-artifacts.mjs +257 -0
- package/src/core/artifact/operations.js +19 -13
- package/src/core/autosync/server.test.js +1 -1
- package/src/core/canvas/githubEmbeds.js +12 -0
- package/src/core/canvas/githubEmbeds.test.js +71 -2
- package/src/core/canvas/hot-pool.js +10 -11
- package/src/core/canvas/materializer.js +39 -13
- package/src/core/canvas/server.js +324 -69
- package/src/core/canvas/server.test.js +365 -2
- package/src/core/canvas/terminal-server.js +144 -57
- package/src/core/canvas/undoRedo.js +174 -0
- package/src/core/canvas/undoRedo.test.js +250 -0
- package/src/core/cli/buildManifest.js +34 -0
- package/src/core/cli/canvasRedo.js +44 -0
- package/src/core/cli/canvasUndo.js +40 -0
- package/src/core/cli/create.js +116 -18
- package/src/core/cli/dev.js +23 -35
- package/src/core/cli/index.js +21 -0
- package/src/core/cli/schemas.js +2 -3
- package/src/core/cli/serverUrl.js +24 -0
- package/src/core/cli/setup.js +14 -25
- package/src/core/cli/terminal-welcome.js +12 -6
- package/src/core/cli/updateVersion.js +42 -2
- package/src/core/cli/workleaf.js +140 -0
- package/src/core/comments/api.js +1 -1
- package/src/core/comments/queries.js +3 -0
- package/src/core/comments/ui/CommentWindow.jsx +3 -2
- package/src/core/data/artifactIndex.js +235 -0
- package/src/core/data/artifactIndex.test.js +203 -0
- package/src/core/data/artifactManifest.js +290 -0
- package/src/core/data/artifactManifest.test.js +207 -0
- package/src/core/data/viewfinder.js +47 -7
- package/src/core/index.js +6 -1
- package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
- package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
- package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
- package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
- package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
- package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
- package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
- package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
- package/src/core/lib/components/ui/dialog/index.js +4 -34
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
- package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
- package/src/core/lib/components/ui/label/label.jsx +1 -2
- package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
- package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
- package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
- package/src/core/lib/components/ui/panel/panel.jsx +11 -2
- package/src/core/lib/components/ui/separator/separator.jsx +6 -4
- package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
- package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
- package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
- package/src/core/mountStoryboardCore.js +234 -74
- package/src/core/mountStoryboardCore.test.js +174 -0
- package/src/core/rename-watcher/watcher.js +329 -36
- package/src/core/rename-watcher/watcher.test.js +253 -0
- package/src/core/scaffold/fragments.js +287 -0
- package/src/core/scaffold/fragments.test.js +401 -0
- package/src/core/scaffold/index.js +291 -0
- package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
- package/src/core/scaffold/scaffold-integration.test.js +247 -0
- package/src/core/scaffold.js +4 -97
- package/src/core/stores/configSchema.js +41 -2
- package/src/core/stores/recentArtifacts.js +49 -2
- package/src/core/stores/recentArtifacts.test.js +59 -6
- package/src/core/stores/themeBootstrap.js +264 -0
- package/src/core/stores/themeBootstrap.test.js +211 -0
- package/src/core/stores/themeStore.test.js +154 -0
- package/src/core/stores/themeStore.ts +235 -220
- package/src/core/styles/generateThemesCss.js +63 -0
- package/src/core/styles/tailwind.css +12 -4
- package/src/core/styles/workspace-chrome.css +30 -0
- package/src/core/tools/handlers/paletteTheme.js +11 -8
- package/src/core/ui/ActionMenuButton.jsx +2 -1
- package/src/core/ui/CanvasCreateMenu.jsx +1 -19
- package/src/core/ui/CoreUIBar.jsx +10 -7
- package/src/core/ui/ThemeMenuButton.jsx +45 -30
- package/src/core/vite/server-plugin.js +36 -0
- package/src/core/workleaf/cleanup.js +102 -0
- package/src/core/workleaf/create.js +199 -0
- package/src/core/workleaf/delete.js +101 -0
- package/src/core/workleaf/list.js +57 -0
- package/src/core/workleaf/merge.js +238 -0
- package/src/core/workleaf/paths.js +146 -0
- package/src/core/workleaf/paths.test.js +230 -0
- package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +28 -21
- package/src/core/workshop/features/createFlow/server.js +5 -1
- package/src/core/workshop/features/createPrototype/server.js +5 -1
- package/src/core/workshop/features/templateIndex.js +28 -1
- package/src/core/worktree/port.js +48 -5
- package/src/core/worktree/port.test.js +42 -0
- package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
- package/src/internals/ArtifactForm/ArtifactForm.module.css +48 -66
- package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
- package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
- package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
- package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
- package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
- package/src/internals/CommandPalette/CommandPalette.jsx +25 -19
- package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
- package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
- package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
- package/src/internals/CommandPalette/createHelpers.js +30 -0
- package/src/internals/CreatePage/CreatePage.jsx +57 -0
- package/src/internals/CreatePage/CreatePage.module.css +34 -0
- package/src/internals/Dialog.jsx +51 -0
- package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +533 -0
- package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
- package/src/internals/Tooltip.jsx +39 -0
- package/src/internals/Viewfinder.jsx +196 -48
- package/src/internals/Viewfinder.module.css +72 -1
- package/src/internals/canvas/CanvasPage.jsx +267 -124
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
- package/src/internals/canvas/PageSelector.jsx +15 -1
- package/src/internals/canvas/PageSelector.module.css +14 -0
- package/src/internals/canvas/canvasApi.js +40 -5
- package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
- package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
- package/src/internals/canvas/isolation/prototypeRoutes.jsx +131 -0
- package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
- package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
- package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
- package/src/internals/canvas/useUndoRedo.js +75 -60
- package/src/internals/canvas/useUndoRedo.test.js +46 -178
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
- package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
- package/src/internals/canvas/widgets/LinkPreview.jsx +45 -1
- package/src/internals/canvas/widgets/LinkPreview.module.css +71 -0
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +69 -10
- package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +114 -0
- package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -4
- package/src/internals/canvas/widgets/StoryWidget.jsx +21 -38
- package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
- package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
- package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
- package/src/internals/canvas/widgets/embedTheme.js +2 -2
- package/src/internals/canvas/widgets/expandUtils.js +19 -19
- package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
- package/src/internals/context.jsx +22 -1
- package/src/internals/context.test.jsx +47 -0
- package/src/internals/hooks/useArtifactIndex.js +31 -0
- package/src/internals/hooks/useArtifactIndex.test.js +45 -0
- package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
- package/src/internals/index.js +6 -0
- package/src/internals/loadConsumerWrapper.js +60 -0
- package/src/internals/story/ComponentSetPage.jsx +75 -34
- package/src/internals/story/StoryPage.jsx +16 -25
- package/src/internals/vite/data-plugin.js +313 -148
- package/src/internals/vite/data-plugin.test.js +11 -11
- package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
- package/terminal.config.json +1 -1
- package/src/core/lib/components/ui/card/card-action.jsx +0 -11
- package/src/core/lib/components/ui/card/card-content.jsx +0 -11
- package/src/core/lib/components/ui/card/card-description.jsx +0 -11
- package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
- package/src/core/lib/components/ui/card/card-header.jsx +0 -19
- package/src/core/lib/components/ui/card/card-title.jsx +0 -11
- package/src/core/lib/components/ui/card/card.jsx +0 -17
- package/src/core/lib/components/ui/card/index.js +0 -25
- package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
- package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
- package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
- package/src/core/lib/components/ui/collapsible/index.js +0 -13
- package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
- package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
- package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
- package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
- package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
- package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
- package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
- package/src/core/lib/components/ui/popover/index.js +0 -28
- package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
- package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
- package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
- package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
- package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
- package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
- package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
- package/src/core/lib/components/ui/popover/popover.jsx +0 -4
- package/src/core/lib/components/ui/select/index.js +0 -37
- package/src/core/lib/components/ui/select/select-content.jsx +0 -30
- package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
- package/src/core/lib/components/ui/select/select-group.jsx +0 -15
- package/src/core/lib/components/ui/select/select-item.jsx +0 -26
- package/src/core/lib/components/ui/select/select-label.jsx +0 -11
- package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
- package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
- package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
- package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
- package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
- package/src/core/lib/components/ui/select/select.jsx +0 -4
- package/src/core/lib/components/ui/sheet/index.js +0 -34
- package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
- package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
- package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
- package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
- package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
- package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
- package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
- package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
- package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
- package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
- package/src/core/lib/components/ui/toggle/index.js +0 -8
- package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
- package/src/core/lib/components/ui/toggle-group/index.js +0 -10
- package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
- package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
- package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
- package/src/internals/canvas/componentIsolate.jsx +0 -144
- package/src/internals/canvas/componentSetIsolate.jsx +0 -300
- package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
- package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
- package/src/primer/ThemeSync.jsx +0 -73
- 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
|
+
}
|