@dfosco/storyboard 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/storyboard-ui.css +1 -1
- package/dist/storyboard-ui.js +14128 -10953
- package/dist/storyboard-ui.js.map +1 -1
- package/dist/tailwind.css +1 -1
- package/dist/tiny-canvas.css +1 -1
- package/package.json +138 -128
- 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 +41 -0
- package/scaffold/src/library/_app.module.css +2 -0
- package/scaffold/src/library/create.jsx +10 -0
- package/scaffold/src/library/index.jsx +14 -0
- package/scaffold/src/library/mount.jsx +51 -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/canvas/style.css +16 -16
- package/src/core/artifact/operations.js +54 -15
- package/src/core/artifact/schemas/canvas.schema.json +7 -1
- package/src/core/autosync/server.test.js +1 -1
- package/src/core/canvas/hot-pool.js +21 -33
- package/src/core/canvas/materializer.js +39 -13
- package/src/core/canvas/selectedWidgets.js +1 -1
- package/src/core/canvas/server.js +376 -100
- package/src/core/canvas/server.test.js +472 -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/artifact.js +2 -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 +151 -23
- package/src/core/cli/dev.js +23 -35
- package/src/core/cli/index.js +21 -0
- package/src/core/cli/schemas.js +8 -4
- 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/comments/ui/comment-layout.css +14 -14
- 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 +24 -1
- package/src/core/inspector/highlighter.js +1 -1
- package/src/core/inspector/mouseMode.js +2 -2
- 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/modes/modes.css +4 -4
- package/src/core/mountStoryboardCore.js +287 -83
- package/src/core/mountStoryboardCore.test.js +198 -0
- package/src/core/rename-watcher/config.json +1 -1
- package/src/core/rename-watcher/watcher.js +330 -37
- 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/canvasInteractionStore.js +120 -0
- package/src/core/stores/canvasInteractionStore.test.js +126 -0
- package/src/core/stores/configSchema.js +64 -8
- package/src/core/stores/configSchema.test.js +3 -0
- package/src/core/stores/customerModeConfig.js +24 -3
- package/src/core/stores/customerModeConfig.test.js +46 -0
- package/src/core/stores/presentationStore.js +118 -0
- package/src/core/stores/presentationStore.test.js +114 -0
- 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/stores/uiConfig.js +10 -2
- package/src/core/styles/generateThemesCss.js +63 -0
- package/src/core/styles/tailwind.css +150 -94
- package/src/core/styles/workspace-chrome.css +13 -0
- package/src/core/tools/handlers/paletteTheme.js +11 -8
- package/src/core/ui/ActionMenuButton.jsx +2 -1
- package/src/core/ui/AutosyncMenuButton.css +7 -7
- package/src/core/ui/BranchSelect.module.css +5 -5
- package/src/core/ui/CanvasCreateMenu.jsx +1 -19
- package/src/core/ui/ChromeSlot.jsx +92 -0
- package/src/core/ui/ChromeSlot.test.jsx +140 -0
- package/src/core/ui/CoreUIBar.css +5 -5
- package/src/core/ui/CoreUIBar.jsx +170 -142
- package/src/core/ui/InspectorPanel.css +5 -5
- package/src/core/ui/InspectorPanel.jsx +9 -9
- package/src/core/ui/PwaInstallBanner.css +5 -5
- package/src/core/ui/PwaInstallBanner.jsx +5 -5
- package/src/core/ui/ThemeMenuButton.jsx +45 -30
- package/src/core/ui/core-ui-colors.css +28 -66
- package/src/core/ui/sidepanel.css +12 -12
- package/src/core/vite/server-plugin.js +13 -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 +130 -34
- 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 +51 -69
- package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
- package/src/internals/AuthModal/AuthModal.module.css +34 -34
- package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
- package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
- package/src/internals/BranchBar/BranchBar.module.css +4 -4
- package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
- package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
- package/src/internals/CommandPalette/CommandPalette.jsx +41 -29
- package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
- package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
- package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
- package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
- package/src/internals/CommandPalette/command-palette.css +14 -14
- 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/FlowError.module.css +5 -5
- package/src/internals/PrototypeErrorBoundary.module.css +15 -15
- package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +534 -0
- package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
- package/src/internals/Tooltip.jsx +39 -0
- package/src/internals/Viewfinder.jsx +229 -64
- package/src/internals/Viewfinder.module.css +253 -182
- package/src/internals/canvas/CanvasControls.module.css +11 -11
- package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
- package/src/internals/canvas/CanvasPage.jsx +358 -217
- package/src/internals/canvas/CanvasPage.module.css +20 -19
- package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
- package/src/internals/canvas/CanvasToolbar.module.css +11 -11
- package/src/internals/canvas/ConnectorLayer.jsx +39 -2
- package/src/internals/canvas/ConnectorLayer.module.css +10 -10
- package/src/internals/canvas/PageSelector.jsx +15 -1
- package/src/internals/canvas/PageSelector.module.css +41 -27
- package/src/internals/canvas/canvasApi.js +40 -5
- package/src/internals/canvas/canvasTheme.js +22 -111
- 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 +140 -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/CodePenEmbed.jsx +16 -36
- package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
- package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
- package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
- package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
- package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
- package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
- package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
- package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
- package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
- package/src/internals/canvas/widgets/LinkPreview.module.css +79 -80
- package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
- package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
- package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
- package/src/internals/canvas/widgets/PrototypeEmbed.jsx +92 -17
- package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
- package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +118 -4
- package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
- package/src/internals/canvas/widgets/StorySetWidget.jsx +53 -25
- package/src/internals/canvas/widgets/StoryWidget.jsx +71 -56
- package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
- package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
- package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
- package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
- package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
- package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
- package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
- package/src/internals/canvas/widgets/WidgetChrome.module.css +76 -61
- package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
- package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
- package/src/internals/canvas/widgets/embedTheme.js +12 -104
- package/src/internals/canvas/widgets/expandUtils.js +19 -19
- package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
- package/src/internals/canvas/widgets/widgetConfig.js +5 -5
- 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/ComponentSetPage.module.css +14 -14
- package/src/internals/story/StoryPage.jsx +16 -25
- package/src/internals/story/StoryPage.module.css +2 -2
- package/src/internals/vite/data-plugin.js +317 -149
- 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/WebGLContextPool.jsx +0 -291
- package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
- package/src/internals/canvas/componentIsolate.jsx +0 -144
- package/src/internals/canvas/componentSetIsolate.jsx +0 -300
- package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
- package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
- package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
- package/src/internals/canvas/widgets/StorySetWidget.module.css +0 -89
- package/src/primer/ThemeSync.jsx +0 -73
- package/src/primer/index.js +0 -1
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
initCanvasInteraction,
|
|
5
|
+
setCanvasInteraction,
|
|
6
|
+
getCanvasInteraction,
|
|
7
|
+
subscribeToCanvasInteraction,
|
|
8
|
+
getCanvasInteractionSnapshot,
|
|
9
|
+
_resetCanvasInteraction,
|
|
10
|
+
} from './canvasInteractionStore.js'
|
|
11
|
+
|
|
12
|
+
describe('canvasInteractionStore', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
_resetCanvasInteraction()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('starts with defaults: scrollAxis=both, zoomGestures=true', () => {
|
|
18
|
+
expect(getCanvasInteraction()).toEqual({ scrollAxis: 'both', zoomGestures: true })
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('initCanvasInteraction seeds from config', () => {
|
|
22
|
+
initCanvasInteraction({ scrollAxis: 'vertical', zoomGestures: false })
|
|
23
|
+
expect(getCanvasInteraction()).toEqual({ scrollAxis: 'vertical', zoomGestures: false })
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('initCanvasInteraction with no args / empty / invalid resets to defaults', () => {
|
|
27
|
+
initCanvasInteraction({ scrollAxis: 'vertical', zoomGestures: false })
|
|
28
|
+
initCanvasInteraction()
|
|
29
|
+
expect(getCanvasInteraction()).toEqual({ scrollAxis: 'both', zoomGestures: true })
|
|
30
|
+
|
|
31
|
+
initCanvasInteraction({ scrollAxis: 'vertical', zoomGestures: false })
|
|
32
|
+
initCanvasInteraction({})
|
|
33
|
+
expect(getCanvasInteraction()).toEqual({ scrollAxis: 'both', zoomGestures: true })
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it('initCanvasInteraction ignores invalid scrollAxis values', () => {
|
|
37
|
+
initCanvasInteraction({ scrollAxis: 'sideways' })
|
|
38
|
+
expect(getCanvasInteraction().scrollAxis).toBe('both')
|
|
39
|
+
|
|
40
|
+
initCanvasInteraction({ scrollAxis: null })
|
|
41
|
+
expect(getCanvasInteraction().scrollAxis).toBe('both')
|
|
42
|
+
|
|
43
|
+
initCanvasInteraction({ scrollAxis: 42 })
|
|
44
|
+
expect(getCanvasInteraction().scrollAxis).toBe('both')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('initCanvasInteraction ignores non-boolean zoomGestures', () => {
|
|
48
|
+
initCanvasInteraction({ zoomGestures: 'yes' })
|
|
49
|
+
expect(getCanvasInteraction().zoomGestures).toBe(true)
|
|
50
|
+
|
|
51
|
+
initCanvasInteraction({ zoomGestures: 1 })
|
|
52
|
+
expect(getCanvasInteraction().zoomGestures).toBe(true)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('accepts all four valid axis values', () => {
|
|
56
|
+
for (const axis of ['both', 'vertical', 'horizontal', 'none']) {
|
|
57
|
+
initCanvasInteraction({ scrollAxis: axis })
|
|
58
|
+
expect(getCanvasInteraction().scrollAxis).toBe(axis)
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('setCanvasInteraction patches partial state without resetting other keys', () => {
|
|
63
|
+
initCanvasInteraction({ scrollAxis: 'vertical', zoomGestures: false })
|
|
64
|
+
|
|
65
|
+
setCanvasInteraction({ zoomGestures: true })
|
|
66
|
+
expect(getCanvasInteraction()).toEqual({ scrollAxis: 'vertical', zoomGestures: true })
|
|
67
|
+
|
|
68
|
+
setCanvasInteraction({ scrollAxis: 'horizontal' })
|
|
69
|
+
expect(getCanvasInteraction()).toEqual({ scrollAxis: 'horizontal', zoomGestures: true })
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('setCanvasInteraction with no-op patch does not notify subscribers', () => {
|
|
73
|
+
const cb = vi.fn()
|
|
74
|
+
subscribeToCanvasInteraction(cb)
|
|
75
|
+
// Reset count: subscribers fire on initial reset_+ subscribe etc.
|
|
76
|
+
cb.mockClear()
|
|
77
|
+
|
|
78
|
+
setCanvasInteraction({}) // no keys
|
|
79
|
+
setCanvasInteraction({ scrollAxis: 'invalid' }) // ignored
|
|
80
|
+
setCanvasInteraction({ scrollAxis: 'both' }) // same as default
|
|
81
|
+
setCanvasInteraction({ zoomGestures: true }) // same as default
|
|
82
|
+
expect(cb).not.toHaveBeenCalled()
|
|
83
|
+
|
|
84
|
+
setCanvasInteraction({ zoomGestures: false }) // real change
|
|
85
|
+
expect(cb).toHaveBeenCalledTimes(1)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('subscribers are notified on init and meaningful set; unsubscribe stops further notifications', () => {
|
|
89
|
+
const cb = vi.fn()
|
|
90
|
+
const unsub = subscribeToCanvasInteraction(cb)
|
|
91
|
+
|
|
92
|
+
initCanvasInteraction({ scrollAxis: 'vertical' })
|
|
93
|
+
setCanvasInteraction({ zoomGestures: false })
|
|
94
|
+
|
|
95
|
+
expect(cb).toHaveBeenCalledTimes(2)
|
|
96
|
+
unsub()
|
|
97
|
+
|
|
98
|
+
setCanvasInteraction({ scrollAxis: 'horizontal' })
|
|
99
|
+
expect(cb).toHaveBeenCalledTimes(2)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('snapshot version changes on every meaningful mutation', () => {
|
|
103
|
+
const v0 = getCanvasInteractionSnapshot()
|
|
104
|
+
initCanvasInteraction({ scrollAxis: 'vertical' })
|
|
105
|
+
const v1 = getCanvasInteractionSnapshot()
|
|
106
|
+
expect(v1).not.toBe(v0)
|
|
107
|
+
|
|
108
|
+
setCanvasInteraction({ zoomGestures: false })
|
|
109
|
+
const v2 = getCanvasInteractionSnapshot()
|
|
110
|
+
expect(v2).not.toBe(v1)
|
|
111
|
+
})
|
|
112
|
+
|
|
113
|
+
it('subscriber errors are isolated', () => {
|
|
114
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
115
|
+
const goodCb = vi.fn()
|
|
116
|
+
|
|
117
|
+
subscribeToCanvasInteraction(() => { throw new Error('boom') })
|
|
118
|
+
subscribeToCanvasInteraction(goodCb)
|
|
119
|
+
|
|
120
|
+
setCanvasInteraction({ zoomGestures: false })
|
|
121
|
+
|
|
122
|
+
expect(goodCb).toHaveBeenCalled()
|
|
123
|
+
expect(errorSpy).toHaveBeenCalled()
|
|
124
|
+
errorSpy.mockRestore()
|
|
125
|
+
})
|
|
126
|
+
})
|
|
@@ -42,7 +42,6 @@
|
|
|
42
42
|
* @property {Record<string, CanvasAgentConfig>} [agents] — per-agent overrides
|
|
43
43
|
* @property {CanvasZoomConfig} [zoom] — zoom min/max/step settings
|
|
44
44
|
* @property {CanvasMessagingConfig} [messaging] — hub messaging settings
|
|
45
|
-
* @property {boolean} [inlineStories] — render StoryWidget inline (no iframe). Default false.
|
|
46
45
|
*/
|
|
47
46
|
|
|
48
47
|
/**
|
|
@@ -79,7 +78,7 @@
|
|
|
79
78
|
* @property {number} [default_max_pool_size] — default surge cap per pool (default: 3)
|
|
80
79
|
* @property {boolean} [load_balancer] — enable auto-scaling (default: true)
|
|
81
80
|
* @property {number} [load_balancer_cooldown_mins] — minutes idle before scale-down (default: 10)
|
|
82
|
-
* @property {Record<string, { pool_size?: number, max_pool_size?: number
|
|
81
|
+
* @property {Record<string, { pool_size?: number, max_pool_size?: number }>} [pools] — per-pool overrides (terminal, prompt, copilot, claude, codex).
|
|
83
82
|
*/
|
|
84
83
|
|
|
85
84
|
/**
|
|
@@ -121,10 +120,30 @@
|
|
|
121
120
|
|
|
122
121
|
/**
|
|
123
122
|
* @typedef {object} CustomerModeConfig
|
|
124
|
-
* @property {boolean} enabled
|
|
125
|
-
* @property {boolean} hideChrome
|
|
126
|
-
* @property {boolean} hideHomepage
|
|
127
|
-
* @property {string} protoHomepage
|
|
123
|
+
* @property {boolean} enabled — master toggle for customer mode
|
|
124
|
+
* @property {boolean} hideChrome — hides all toolbars (except canvas tools), branchbar, cmd+k, cmd+.
|
|
125
|
+
* @property {boolean} hideHomepage — removes the storyboard homepage (leaves empty page)
|
|
126
|
+
* @property {string} protoHomepage — internal /path that replaces homepage; redirects from / and /workspace
|
|
127
|
+
* @property {string} canvasHomepage — canvas id (e.g. "landing") that replaces homepage. Mutually exclusive with `protoHomepage` — when both are set, this wins.
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @typedef {object} ThemeDefinition
|
|
132
|
+
* @property {string} label — display name shown in theme switchers
|
|
133
|
+
* @property {Record<string,string>} [attrs] — companion data attributes written onto each surface element when this theme is active (e.g. Primer's `data-color-mode`, `data-light-theme`). Storyboard core writes no companion attrs by default; consumers (Primer-using repos, etc.) add them via their own storyboard.config.json.
|
|
134
|
+
*/
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* @typedef {object} SurfaceDefinition
|
|
138
|
+
* @property {string} label — display label shown in the "Apply theme to" sub-menu
|
|
139
|
+
* @property {boolean} sync — default value for whether this surface follows the global theme (true) or stays light (false). Persisted per-user in localStorage.
|
|
140
|
+
*/
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* @typedef {object} ThemesConfig
|
|
144
|
+
* @property {Record<string, ThemeDefinition>} themes — registry of available themes. Theme IDs are the values written to `data-sb-<surface>-theme`. Storyboard core ships only `light` and `dark`; consumers add more (e.g. `dark_dimmed`, `light_colorblind`).
|
|
145
|
+
* @property {Record<string, SurfaceDefinition>} surfaces — registry of theming surfaces. Each surface ID is matched against `[data-sb-surface="<id>"]` elements in the DOM. The runtime writes `data-sb-<id>-theme` onto those elements plus the companion `attrs` from the active theme. Core ships `prototype`, `canvas`, `toolbar`, `codeBoxes`.
|
|
146
|
+
* @property {string} [default] — default global theme ID. Use `"system"` to follow OS preference.
|
|
128
147
|
*/
|
|
129
148
|
|
|
130
149
|
/**
|
|
@@ -145,6 +164,7 @@
|
|
|
145
164
|
* @property {HotPoolConfig} [hotPool]
|
|
146
165
|
* @property {CommandPaletteConfig} [commandPalette]
|
|
147
166
|
* @property {CustomerModeConfig} [customerMode]
|
|
167
|
+
* @property {ThemesConfig} [themes]
|
|
148
168
|
*/
|
|
149
169
|
|
|
150
170
|
/** Built-in paste rules shipped with storyboard. */
|
|
@@ -169,7 +189,18 @@ export const configDefaults = {
|
|
|
169
189
|
plugins: {},
|
|
170
190
|
workshop: {
|
|
171
191
|
enabled: false,
|
|
172
|
-
|
|
192
|
+
// Each `create<X>` flag controls whether the corresponding item appears
|
|
193
|
+
// in the workspace Create-Artifact menu. Default true — set to false in
|
|
194
|
+
// a consumer's storyboard.config.json to hide that item.
|
|
195
|
+
features: {
|
|
196
|
+
createPrototype: true,
|
|
197
|
+
createCanvas: true,
|
|
198
|
+
createComponent: true,
|
|
199
|
+
createFlow: true,
|
|
200
|
+
createPage: true,
|
|
201
|
+
createObject: true,
|
|
202
|
+
createRecord: true,
|
|
203
|
+
},
|
|
173
204
|
},
|
|
174
205
|
featureFlags: {},
|
|
175
206
|
ui: {},
|
|
@@ -184,6 +215,10 @@ export const configDefaults = {
|
|
|
184
215
|
min: 10,
|
|
185
216
|
max: 250,
|
|
186
217
|
step: 10,
|
|
218
|
+
gestures: true, // when false, disables cmd+wheel and pinch-to-zoom; +/- buttons still work
|
|
219
|
+
},
|
|
220
|
+
scroll: {
|
|
221
|
+
axis: 'both', // "both" | "vertical" | "horizontal" | "none"
|
|
187
222
|
},
|
|
188
223
|
terminal: {
|
|
189
224
|
resizable: false,
|
|
@@ -196,7 +231,6 @@ export const configDefaults = {
|
|
|
196
231
|
pollIntervalMs: 2000,
|
|
197
232
|
maxMessagesPerHub: 200,
|
|
198
233
|
},
|
|
199
|
-
inlineStories: false,
|
|
200
234
|
},
|
|
201
235
|
commandPalette: {
|
|
202
236
|
providers: ['prototypes', 'flows', 'canvases', 'pages'],
|
|
@@ -208,6 +242,28 @@ export const configDefaults = {
|
|
|
208
242
|
hideChrome: false,
|
|
209
243
|
hideHomepage: false,
|
|
210
244
|
protoHomepage: '',
|
|
245
|
+
canvasHomepage: '',
|
|
246
|
+
},
|
|
247
|
+
theming: {
|
|
248
|
+
// Minimal default theme registry. Consumers add more in their own
|
|
249
|
+
// storyboard.config.json — see README#theming. IDs starting with "dark"
|
|
250
|
+
// are picked up by the Tailwind `dark:` variant via prefix match.
|
|
251
|
+
themes: {
|
|
252
|
+
light: { label: 'Light', attrs: {} },
|
|
253
|
+
dark: { label: 'Dark', attrs: {} },
|
|
254
|
+
},
|
|
255
|
+
// Surface registry. Each surface ID is looked up in the DOM as
|
|
256
|
+
// `[data-sb-surface="<id>"]`. The runtime writes
|
|
257
|
+
// `data-sb-<id>-theme="<resolved>"` plus the active theme's companion
|
|
258
|
+
// `attrs` onto every matching element.
|
|
259
|
+
surfaces: {
|
|
260
|
+
prototype: { label: 'Prototype', sync: true },
|
|
261
|
+
canvas: { label: 'Canvas', sync: true },
|
|
262
|
+
toolbar: { label: 'Tools', sync: false },
|
|
263
|
+
codeBoxes: { label: 'Code boxes', sync: true },
|
|
264
|
+
},
|
|
265
|
+
// Default global theme. "system" follows OS prefers-color-scheme.
|
|
266
|
+
default: 'system',
|
|
211
267
|
},
|
|
212
268
|
}
|
|
213
269
|
|
|
@@ -24,12 +24,15 @@ describe('configSchema', () => {
|
|
|
24
24
|
expect(c.canvas.pasteRules).toEqual(builtinPasteRules)
|
|
25
25
|
expect(c.canvas.github.embedBehavior).toBe('link-preview')
|
|
26
26
|
expect(c.canvas.github.ghGuard).toBe('copy')
|
|
27
|
+
expect(c.canvas.zoom.gestures).toBe(true)
|
|
28
|
+
expect(c.canvas.scroll.axis).toBe('both')
|
|
27
29
|
expect(c.commandPalette.providers).toEqual(['prototypes', 'flows', 'canvases', 'pages'])
|
|
28
30
|
expect(c.commandPalette.ranking).toBe('frecency')
|
|
29
31
|
expect(c.customerMode.enabled).toBe(false)
|
|
30
32
|
expect(c.customerMode.hideChrome).toBe(false)
|
|
31
33
|
expect(c.customerMode.hideHomepage).toBe(false)
|
|
32
34
|
expect(c.customerMode.protoHomepage).toBe('')
|
|
35
|
+
expect(c.customerMode.canvasHomepage).toBe('')
|
|
33
36
|
})
|
|
34
37
|
|
|
35
38
|
it('returns full defaults when called with undefined', () => {
|
|
@@ -3,19 +3,40 @@
|
|
|
3
3
|
* Framework-agnostic (zero npm dependencies).
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
let _config = {
|
|
6
|
+
let _config = {
|
|
7
|
+
enabled: false,
|
|
8
|
+
hideChrome: false,
|
|
9
|
+
hideHomepage: false,
|
|
10
|
+
protoHomepage: '',
|
|
11
|
+
canvasHomepage: '',
|
|
12
|
+
}
|
|
7
13
|
|
|
8
14
|
/**
|
|
9
15
|
* Initialize customer mode config.
|
|
16
|
+
* If both `protoHomepage` and `canvasHomepage` are set, `canvasHomepage` wins
|
|
17
|
+
* (it's the more specific landing-page case) and a console warning is logged.
|
|
10
18
|
* @param {object} config - The customerMode object from storyboard.config.json
|
|
11
19
|
*/
|
|
12
20
|
export function initCustomerModeConfig(config) {
|
|
13
|
-
_config = {
|
|
21
|
+
_config = {
|
|
22
|
+
enabled: false,
|
|
23
|
+
hideChrome: false,
|
|
24
|
+
hideHomepage: false,
|
|
25
|
+
protoHomepage: '',
|
|
26
|
+
canvasHomepage: '',
|
|
27
|
+
...config,
|
|
28
|
+
}
|
|
29
|
+
if (_config.protoHomepage && _config.canvasHomepage) {
|
|
30
|
+
console.warn(
|
|
31
|
+
'[storyboard] customerMode has both `protoHomepage` and `canvasHomepage` set. ' +
|
|
32
|
+
'`canvasHomepage` takes precedence; ignoring `protoHomepage`.'
|
|
33
|
+
)
|
|
34
|
+
}
|
|
14
35
|
}
|
|
15
36
|
|
|
16
37
|
/**
|
|
17
38
|
* Get the current customer mode config.
|
|
18
|
-
* @returns {{ enabled: boolean, hideChrome: boolean, hideHomepage: boolean, protoHomepage: string }}
|
|
39
|
+
* @returns {{ enabled: boolean, hideChrome: boolean, hideHomepage: boolean, protoHomepage: string, canvasHomepage: string }}
|
|
19
40
|
*/
|
|
20
41
|
export function getCustomerModeConfig() {
|
|
21
42
|
return _config
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
initCustomerModeConfig,
|
|
5
|
+
getCustomerModeConfig,
|
|
6
|
+
} from './customerModeConfig.js'
|
|
7
|
+
|
|
8
|
+
describe('customerModeConfig', () => {
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
// Reset to a known empty state between tests
|
|
11
|
+
initCustomerModeConfig({})
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('defaults canvasHomepage to empty string', () => {
|
|
15
|
+
const cfg = getCustomerModeConfig()
|
|
16
|
+
expect(cfg.canvasHomepage).toBe('')
|
|
17
|
+
expect(cfg.protoHomepage).toBe('')
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('accepts canvasHomepage from config', () => {
|
|
21
|
+
initCustomerModeConfig({ enabled: true, canvasHomepage: 'landing' })
|
|
22
|
+
expect(getCustomerModeConfig().canvasHomepage).toBe('landing')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('warns when both protoHomepage and canvasHomepage are set', () => {
|
|
26
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
27
|
+
initCustomerModeConfig({
|
|
28
|
+
enabled: true,
|
|
29
|
+
protoHomepage: '/MyProto',
|
|
30
|
+
canvasHomepage: 'landing',
|
|
31
|
+
})
|
|
32
|
+
expect(warnSpy).toHaveBeenCalledTimes(1)
|
|
33
|
+
expect(warnSpy.mock.calls[0][0]).toMatch(/protoHomepage.*canvasHomepage/)
|
|
34
|
+
expect(warnSpy.mock.calls[0][0]).toMatch(/canvasHomepage.*takes precedence/)
|
|
35
|
+
warnSpy.mockRestore()
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('does not warn when only one is set', () => {
|
|
39
|
+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
40
|
+
initCustomerModeConfig({ enabled: true, canvasHomepage: 'landing' })
|
|
41
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
42
|
+
initCustomerModeConfig({ enabled: true, protoHomepage: '/Proto' })
|
|
43
|
+
expect(warnSpy).not.toHaveBeenCalled()
|
|
44
|
+
warnSpy.mockRestore()
|
|
45
|
+
})
|
|
46
|
+
})
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Presentation Store — consumer-supplied overrides for storyboard chrome elements.
|
|
3
|
+
*
|
|
4
|
+
* A chrome element is any rendered piece of storyboard UI that is wrapped in
|
|
5
|
+
* `<ChromeSlot id="...">` (toolbar buttons, canvas title, page selector, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Consumers seed this registry through `mountStoryboardCore({ presentation })`
|
|
8
|
+
* and the store applies per-element overrides at render time:
|
|
9
|
+
*
|
|
10
|
+
* {
|
|
11
|
+
* "tool:command-palette": {
|
|
12
|
+
* style: { transform: 'translateY(20px)', opacity: 0 },
|
|
13
|
+
* className: 'landing-floating-button',
|
|
14
|
+
* props: { onPointerEnter: handleHover },
|
|
15
|
+
* decorator: (Element, ctx) => <ScrollAnimator>{Element}</ScrollAnimator>,
|
|
16
|
+
* },
|
|
17
|
+
* "canvas:title": { hidden: true },
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* This is an **advanced, undocumented** extension surface — intended for
|
|
21
|
+
* landing-page / hero canvas use cases where the consumer needs heavy
|
|
22
|
+
* customization of storyboard chrome. Not part of the public API.
|
|
23
|
+
*
|
|
24
|
+
* Framework-agnostic (zero npm dependencies).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @typedef {object} PresentationOverride
|
|
29
|
+
* @property {React.CSSProperties} [style] - Spread onto the chrome element root style
|
|
30
|
+
* @property {string} [className] - Appended to the chrome element root className
|
|
31
|
+
* @property {object} [props] - Spread as additional props on the chrome element
|
|
32
|
+
* @property {Function} [decorator] - (Element, ctx) => ReactNode wrapper
|
|
33
|
+
* @property {boolean} [hidden] - If true, render null (shorthand for full hide)
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/** @type {Map<string, PresentationOverride>} */
|
|
37
|
+
let _overrides = new Map()
|
|
38
|
+
|
|
39
|
+
/** @type {Set<Function>} */
|
|
40
|
+
const _listeners = new Set()
|
|
41
|
+
|
|
42
|
+
let _snapshotVersion = 0
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Seed the presentation registry from a consumer-supplied map.
|
|
46
|
+
* Replaces any previous overrides. Called once at app startup.
|
|
47
|
+
*
|
|
48
|
+
* @param {Record<string, PresentationOverride>} [map]
|
|
49
|
+
*/
|
|
50
|
+
export function initPresentation(map) {
|
|
51
|
+
_overrides = new Map()
|
|
52
|
+
if (map && typeof map === 'object') {
|
|
53
|
+
for (const [id, override] of Object.entries(map)) {
|
|
54
|
+
if (override && typeof override === 'object') {
|
|
55
|
+
_overrides.set(id, override)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
_notify()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Set or replace a single chrome element's presentation override.
|
|
64
|
+
* @param {string} id
|
|
65
|
+
* @param {PresentationOverride|null|undefined} override Pass null/undefined to clear.
|
|
66
|
+
*/
|
|
67
|
+
export function setPresentation(id, override) {
|
|
68
|
+
if (override == null) {
|
|
69
|
+
_overrides.delete(id)
|
|
70
|
+
} else if (typeof override === 'object') {
|
|
71
|
+
_overrides.set(id, override)
|
|
72
|
+
}
|
|
73
|
+
_notify()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the presentation override for a chrome element ID, or null if none.
|
|
78
|
+
* @param {string} id
|
|
79
|
+
* @returns {PresentationOverride|null}
|
|
80
|
+
*/
|
|
81
|
+
export function getPresentation(id) {
|
|
82
|
+
return _overrides.get(id) || null
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Subscribe to presentation registry changes. Returns an unsubscribe fn.
|
|
87
|
+
* Compatible with `useSyncExternalStore` shape.
|
|
88
|
+
* @param {Function} callback
|
|
89
|
+
* @returns {Function}
|
|
90
|
+
*/
|
|
91
|
+
export function subscribeToPresentation(callback) {
|
|
92
|
+
_listeners.add(callback)
|
|
93
|
+
return () => _listeners.delete(callback)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Snapshot version for change detection. Compatible with `useSyncExternalStore`.
|
|
98
|
+
* @returns {string}
|
|
99
|
+
*/
|
|
100
|
+
export function getPresentationSnapshot() {
|
|
101
|
+
return String(_snapshotVersion)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function _notify() {
|
|
105
|
+
_snapshotVersion++
|
|
106
|
+
for (const cb of _listeners) {
|
|
107
|
+
try { cb() } catch (err) {
|
|
108
|
+
console.error('[storyboard] Error in presentation subscriber:', err)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Reset all state. Only for tests. */
|
|
114
|
+
export function _resetPresentation() {
|
|
115
|
+
_overrides = new Map()
|
|
116
|
+
_listeners.clear()
|
|
117
|
+
_snapshotVersion = 0
|
|
118
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
initPresentation,
|
|
5
|
+
setPresentation,
|
|
6
|
+
getPresentation,
|
|
7
|
+
subscribeToPresentation,
|
|
8
|
+
getPresentationSnapshot,
|
|
9
|
+
_resetPresentation,
|
|
10
|
+
} from './presentationStore.js'
|
|
11
|
+
|
|
12
|
+
describe('presentationStore', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
_resetPresentation()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns null when no override is registered', () => {
|
|
18
|
+
expect(getPresentation('tool:command-palette')).toBeNull()
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
it('seeds overrides from initPresentation', () => {
|
|
22
|
+
initPresentation({
|
|
23
|
+
'tool:command-palette': { hidden: true },
|
|
24
|
+
'canvas:title': { className: 'landing-title' },
|
|
25
|
+
})
|
|
26
|
+
expect(getPresentation('tool:command-palette')).toEqual({ hidden: true })
|
|
27
|
+
expect(getPresentation('canvas:title')).toEqual({ className: 'landing-title' })
|
|
28
|
+
expect(getPresentation('nope:nothing')).toBeNull()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('ignores non-object values in initPresentation', () => {
|
|
32
|
+
initPresentation({
|
|
33
|
+
'tool:a': null,
|
|
34
|
+
'tool:b': 'invalid',
|
|
35
|
+
'tool:c': 42,
|
|
36
|
+
'tool:d': { hidden: true },
|
|
37
|
+
})
|
|
38
|
+
expect(getPresentation('tool:a')).toBeNull()
|
|
39
|
+
expect(getPresentation('tool:b')).toBeNull()
|
|
40
|
+
expect(getPresentation('tool:c')).toBeNull()
|
|
41
|
+
expect(getPresentation('tool:d')).toEqual({ hidden: true })
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('replaces previous overrides on subsequent initPresentation calls', () => {
|
|
45
|
+
initPresentation({ 'tool:a': { hidden: true } })
|
|
46
|
+
initPresentation({ 'tool:b': { hidden: true } })
|
|
47
|
+
expect(getPresentation('tool:a')).toBeNull()
|
|
48
|
+
expect(getPresentation('tool:b')).toEqual({ hidden: true })
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('initPresentation() with no arg clears all overrides', () => {
|
|
52
|
+
initPresentation({ 'tool:a': { hidden: true } })
|
|
53
|
+
initPresentation()
|
|
54
|
+
expect(getPresentation('tool:a')).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('setPresentation adds and updates individual overrides', () => {
|
|
58
|
+
setPresentation('tool:a', { className: 'foo' })
|
|
59
|
+
expect(getPresentation('tool:a')).toEqual({ className: 'foo' })
|
|
60
|
+
|
|
61
|
+
setPresentation('tool:a', { className: 'bar' })
|
|
62
|
+
expect(getPresentation('tool:a')).toEqual({ className: 'bar' })
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('setPresentation with null/undefined deletes the override', () => {
|
|
66
|
+
setPresentation('tool:a', { hidden: true })
|
|
67
|
+
setPresentation('tool:a', null)
|
|
68
|
+
expect(getPresentation('tool:a')).toBeNull()
|
|
69
|
+
|
|
70
|
+
setPresentation('tool:b', { hidden: true })
|
|
71
|
+
setPresentation('tool:b', undefined)
|
|
72
|
+
expect(getPresentation('tool:b')).toBeNull()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('subscribers are notified on init, set, and clear', () => {
|
|
76
|
+
const cb = vi.fn()
|
|
77
|
+
const unsub = subscribeToPresentation(cb)
|
|
78
|
+
|
|
79
|
+
initPresentation({ 'tool:a': { hidden: true } })
|
|
80
|
+
setPresentation('tool:b', { className: 'x' })
|
|
81
|
+
setPresentation('tool:a', null)
|
|
82
|
+
|
|
83
|
+
expect(cb).toHaveBeenCalledTimes(3)
|
|
84
|
+
unsub()
|
|
85
|
+
|
|
86
|
+
setPresentation('tool:c', { hidden: true })
|
|
87
|
+
expect(cb).toHaveBeenCalledTimes(3) // not called after unsubscribe
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('snapshot version changes on every mutation', () => {
|
|
91
|
+
const v0 = getPresentationSnapshot()
|
|
92
|
+
setPresentation('tool:a', { hidden: true })
|
|
93
|
+
const v1 = getPresentationSnapshot()
|
|
94
|
+
expect(v1).not.toBe(v0)
|
|
95
|
+
|
|
96
|
+
setPresentation('tool:b', { hidden: true })
|
|
97
|
+
const v2 = getPresentationSnapshot()
|
|
98
|
+
expect(v2).not.toBe(v1)
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('subscriber errors are isolated', () => {
|
|
102
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
103
|
+
const goodCb = vi.fn()
|
|
104
|
+
|
|
105
|
+
subscribeToPresentation(() => { throw new Error('boom') })
|
|
106
|
+
subscribeToPresentation(goodCb)
|
|
107
|
+
|
|
108
|
+
setPresentation('tool:a', { hidden: true })
|
|
109
|
+
|
|
110
|
+
expect(goodCb).toHaveBeenCalled()
|
|
111
|
+
expect(consoleSpy).toHaveBeenCalled()
|
|
112
|
+
consoleSpy.mockRestore()
|
|
113
|
+
})
|
|
114
|
+
})
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Recent Artifacts — localStorage-backed recents for the
|
|
2
|
+
* Recent Artifacts — localStorage-backed recents for the workspace and
|
|
3
|
+
* command palette.
|
|
3
4
|
*
|
|
4
5
|
* Stores artifact identity (type + key + label), not routes.
|
|
5
6
|
* Routes are derived at read time from the live data index so they
|
|
6
7
|
* stay correct across branch/basePath changes.
|
|
8
|
+
*
|
|
9
|
+
* Subscribable via {@link subscribeToRecent} / {@link getRecentSnapshot}
|
|
10
|
+
* for `useSyncExternalStore` integration. Writes within the same tab
|
|
11
|
+
* are surfaced via a synthetic `storage` event so other listeners
|
|
12
|
+
* (including this module's own snapshot consumers) re-read immediately.
|
|
7
13
|
*/
|
|
8
14
|
|
|
9
15
|
const STORAGE_KEY = 'storyboard:recent-artifacts'
|
|
10
|
-
const MAX_ITEMS =
|
|
16
|
+
const MAX_ITEMS = 30
|
|
17
|
+
const CHANGE_EVENT = 'storyboard-recent-artifacts-changed'
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* @typedef {{ type: string, key: string, label: string }} RecentEntry
|
|
@@ -43,9 +50,42 @@ export function getRecent() {
|
|
|
43
50
|
export function clearRecent() {
|
|
44
51
|
try {
|
|
45
52
|
localStorage.removeItem(STORAGE_KEY)
|
|
53
|
+
_notify()
|
|
46
54
|
} catch { /* ignore */ }
|
|
47
55
|
}
|
|
48
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Subscribe to changes in the recent artifacts store. Fires for both
|
|
59
|
+
* intra-tab writes (via a synthetic event) and cross-tab writes (via
|
|
60
|
+
* the native `storage` event).
|
|
61
|
+
*
|
|
62
|
+
* Designed for `useSyncExternalStore` — pair with {@link getRecentSnapshot}.
|
|
63
|
+
*
|
|
64
|
+
* @param {() => void} callback
|
|
65
|
+
* @returns {() => void} unsubscribe
|
|
66
|
+
*/
|
|
67
|
+
export function subscribeToRecent(callback) {
|
|
68
|
+
if (typeof window === 'undefined') return () => {}
|
|
69
|
+
const onStorage = (e) => { if (!e || !e.key || e.key === STORAGE_KEY) callback() }
|
|
70
|
+
window.addEventListener('storage', onStorage)
|
|
71
|
+
window.addEventListener(CHANGE_EVENT, callback)
|
|
72
|
+
return () => {
|
|
73
|
+
window.removeEventListener('storage', onStorage)
|
|
74
|
+
window.removeEventListener(CHANGE_EVENT, callback)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Return a stable string snapshot of the recent artifacts store for
|
|
80
|
+
* `useSyncExternalStore`. Returns the raw JSON (or `'[]'` if absent).
|
|
81
|
+
*
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function getRecentSnapshot() {
|
|
85
|
+
try { return localStorage.getItem(STORAGE_KEY) || '[]' }
|
|
86
|
+
catch { return '[]' }
|
|
87
|
+
}
|
|
88
|
+
|
|
49
89
|
// ---------------------------------------------------------------------------
|
|
50
90
|
// Internal
|
|
51
91
|
// ---------------------------------------------------------------------------
|
|
@@ -64,5 +104,12 @@ function _read() {
|
|
|
64
104
|
function _write(entries) {
|
|
65
105
|
try {
|
|
66
106
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(entries))
|
|
107
|
+
_notify()
|
|
67
108
|
} catch { /* quota exceeded or unavailable */ }
|
|
68
109
|
}
|
|
110
|
+
|
|
111
|
+
function _notify() {
|
|
112
|
+
if (typeof window === 'undefined') return
|
|
113
|
+
try { window.dispatchEvent(new Event(CHANGE_EVENT)) }
|
|
114
|
+
catch { /* ignore (e.g. SSR / non-browser) */ }
|
|
115
|
+
}
|