@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,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workleaf path helpers.
|
|
3
|
+
*
|
|
4
|
+
* A workleaf is an ephemeral sub-worktree owned by a parent worktree (or
|
|
5
|
+
* main). It lives at:
|
|
6
|
+
*
|
|
7
|
+
* <repo-root>/workleafs/<parent>/workleaf--<slug>/
|
|
8
|
+
*
|
|
9
|
+
* The parent name is encoded in the path — no metadata file is needed.
|
|
10
|
+
* Workleaves cannot be nested: spawning a workleaf from inside another
|
|
11
|
+
* workleaf is refused at the CLI layer using `detectWorkleafContext`.
|
|
12
|
+
*
|
|
13
|
+
* Branch naming convention is `<parent>--workleaf--<slug>` so the branch
|
|
14
|
+
* is self-describing in `git branch` output even without spatial context.
|
|
15
|
+
*
|
|
16
|
+
* This module is intentionally pure and side-effect-free so it can be
|
|
17
|
+
* unit-tested without touching the filesystem (apart from the listing
|
|
18
|
+
* helper, which obviously must read disk).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readdirSync, realpathSync } from 'fs'
|
|
22
|
+
import { join } from 'path'
|
|
23
|
+
import { repoRoot, slugify } from '../worktree/port.js'
|
|
24
|
+
|
|
25
|
+
/** Literal prefix every workleaf directory and branch slug carries. */
|
|
26
|
+
export const WORKLEAF_PREFIX = 'workleaf--'
|
|
27
|
+
|
|
28
|
+
/** Branch convention: `<parent>--workleaf--<slug>`. */
|
|
29
|
+
export const WORKLEAF_BRANCH_SEPARATOR = '--workleaf--'
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build the absolute on-disk path for a workleaf.
|
|
33
|
+
*
|
|
34
|
+
* @param {string} parent — parent worktree name (e.g. 'main' or '4.3.0')
|
|
35
|
+
* @param {string} name — user-supplied workleaf name (will be slugified)
|
|
36
|
+
* @param {string} [cwd] — for tests; falls back to process.cwd()
|
|
37
|
+
* @returns {string} absolute path
|
|
38
|
+
*/
|
|
39
|
+
export function workleafDir(parent, name, cwd) {
|
|
40
|
+
const root = repoRoot(cwd)
|
|
41
|
+
const slug = slugify(name)
|
|
42
|
+
return join(root, 'workleafs', parent, WORKLEAF_PREFIX + slug)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compose the git branch name for a workleaf.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} parent
|
|
49
|
+
* @param {string} name
|
|
50
|
+
* @returns {string} e.g. '4.3.0--workleaf--quick-fix'
|
|
51
|
+
*/
|
|
52
|
+
export function workleafBranchName(parent, name) {
|
|
53
|
+
const slug = slugify(name)
|
|
54
|
+
return `${parent}${WORKLEAF_BRANCH_SEPARATOR}${slug}`
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Inverse of {@link workleafBranchName}. Splits `parent--workleaf--slug`
|
|
59
|
+
* into its parts. Uses the LAST occurrence of `--workleaf--` so the
|
|
60
|
+
* function survives parent names that contain `--workleaf--` literally
|
|
61
|
+
* (unlikely, but cheap to handle).
|
|
62
|
+
*
|
|
63
|
+
* Returns null if the input is not a workleaf branch.
|
|
64
|
+
*/
|
|
65
|
+
export function parentOfWorkleaf(branchName) {
|
|
66
|
+
if (typeof branchName !== 'string') return null
|
|
67
|
+
const idx = branchName.lastIndexOf(WORKLEAF_BRANCH_SEPARATOR)
|
|
68
|
+
if (idx <= 0) return null
|
|
69
|
+
const parent = branchName.slice(0, idx)
|
|
70
|
+
const slug = branchName.slice(idx + WORKLEAF_BRANCH_SEPARATOR.length)
|
|
71
|
+
if (!parent || !slug) return null
|
|
72
|
+
return { parent, slug }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect whether the given cwd is inside a workleaf, and if so, which
|
|
77
|
+
* parent and slug it belongs to.
|
|
78
|
+
*
|
|
79
|
+
* Returns:
|
|
80
|
+
* { inWorkleaf: true, parent, slug, branch, dir, root } when inside
|
|
81
|
+
* { inWorkleaf: false } when outside
|
|
82
|
+
*
|
|
83
|
+
* Uses realpathSync to resolve symlinks before pattern-matching — same
|
|
84
|
+
* convention as `worktree/port.js`. Falls back to the raw cwd if the
|
|
85
|
+
* path does not exist on disk yet (e.g. during dry-run tests).
|
|
86
|
+
*/
|
|
87
|
+
export function detectWorkleafContext(cwd = process.cwd()) {
|
|
88
|
+
let realCwd
|
|
89
|
+
try {
|
|
90
|
+
realCwd = realpathSync(cwd)
|
|
91
|
+
} catch {
|
|
92
|
+
realCwd = cwd
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Match <root>/workleafs/<parent>/workleaf--<slug> (anywhere within it).
|
|
96
|
+
// Capture groups: 1=root, 2=parent, 3=slug.
|
|
97
|
+
const re = new RegExp(`^(.+)[/\\\\]workleafs[/\\\\]([^/\\\\]+)[/\\\\]${WORKLEAF_PREFIX}([^/\\\\]+)`)
|
|
98
|
+
const m = realCwd.match(re)
|
|
99
|
+
if (!m) return { inWorkleaf: false }
|
|
100
|
+
|
|
101
|
+
const [, root, parent, slug] = m
|
|
102
|
+
return {
|
|
103
|
+
inWorkleaf: true,
|
|
104
|
+
parent,
|
|
105
|
+
slug,
|
|
106
|
+
branch: workleafBranchName(parent, slug),
|
|
107
|
+
dir: join(root, 'workleafs', parent, WORKLEAF_PREFIX + slug),
|
|
108
|
+
root,
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Enumerate every workleaf on disk under <repoRoot>/workleafs/, grouped
|
|
114
|
+
* by parent. Skips entries that don't look like real git worktrees
|
|
115
|
+
* (must contain a `.git` file at the workleaf root).
|
|
116
|
+
*
|
|
117
|
+
* @param {string} [cwd]
|
|
118
|
+
* @returns {Array<{ parent: string, slug: string, branch: string, dir: string }>}
|
|
119
|
+
*/
|
|
120
|
+
export function listWorkleafs(cwd) {
|
|
121
|
+
const root = repoRoot(cwd)
|
|
122
|
+
const base = join(root, 'workleafs')
|
|
123
|
+
if (!existsSync(base)) return []
|
|
124
|
+
|
|
125
|
+
const out = []
|
|
126
|
+
for (const parentEntry of readdirSync(base, { withFileTypes: true })) {
|
|
127
|
+
if (!parentEntry.isDirectory()) continue
|
|
128
|
+
const parent = parentEntry.name
|
|
129
|
+
const parentDir = join(base, parent)
|
|
130
|
+
for (const child of readdirSync(parentDir, { withFileTypes: true })) {
|
|
131
|
+
if (!child.isDirectory()) continue
|
|
132
|
+
if (!child.name.startsWith(WORKLEAF_PREFIX)) continue
|
|
133
|
+
const dir = join(parentDir, child.name)
|
|
134
|
+
// A worktree-managed directory has a `.git` file (not dir) inside.
|
|
135
|
+
if (!existsSync(join(dir, '.git'))) continue
|
|
136
|
+
const slug = child.name.slice(WORKLEAF_PREFIX.length)
|
|
137
|
+
out.push({
|
|
138
|
+
parent,
|
|
139
|
+
slug,
|
|
140
|
+
branch: workleafBranchName(parent, slug),
|
|
141
|
+
dir,
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return out
|
|
146
|
+
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from 'fs'
|
|
3
|
+
import { join } from 'path'
|
|
4
|
+
import { tmpdir } from 'os'
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
WORKLEAF_PREFIX,
|
|
8
|
+
WORKLEAF_BRANCH_SEPARATOR,
|
|
9
|
+
workleafDir,
|
|
10
|
+
workleafBranchName,
|
|
11
|
+
parentOfWorkleaf,
|
|
12
|
+
detectWorkleafContext,
|
|
13
|
+
listWorkleafs,
|
|
14
|
+
} from './paths.js'
|
|
15
|
+
|
|
16
|
+
describe('workleafBranchName', () => {
|
|
17
|
+
it('builds <parent>--workleaf--<slug>', () => {
|
|
18
|
+
expect(workleafBranchName('main', 'quick-fix')).toBe('main--workleaf--quick-fix')
|
|
19
|
+
expect(workleafBranchName('4.3.0', 'Quick Fix')).toBe('4.3.0--workleaf--quick-fix')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('slugifies the name', () => {
|
|
23
|
+
expect(workleafBranchName('main', 'My Cool.Feature')).toBe('main--workleaf--my-cool-feature')
|
|
24
|
+
})
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
describe('parentOfWorkleaf', () => {
|
|
28
|
+
it('splits a normal workleaf branch', () => {
|
|
29
|
+
expect(parentOfWorkleaf('main--workleaf--quick-fix')).toEqual({ parent: 'main', slug: 'quick-fix' })
|
|
30
|
+
expect(parentOfWorkleaf('4.3.0--workleaf--bench')).toEqual({ parent: '4.3.0', slug: 'bench' })
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('returns null for non-workleaf branches', () => {
|
|
34
|
+
expect(parentOfWorkleaf('main')).toBeNull()
|
|
35
|
+
expect(parentOfWorkleaf('4.3.0--feature')).toBeNull()
|
|
36
|
+
expect(parentOfWorkleaf('')).toBeNull()
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('returns null for malformed inputs', () => {
|
|
40
|
+
expect(parentOfWorkleaf(null)).toBeNull()
|
|
41
|
+
expect(parentOfWorkleaf(undefined)).toBeNull()
|
|
42
|
+
expect(parentOfWorkleaf(123)).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('uses the LAST --workleaf-- separator (parent names containing it)', () => {
|
|
46
|
+
// A pathological but possible parent name: someone named their parent
|
|
47
|
+
// worktree `something--workleaf--child`. We should still split it correctly.
|
|
48
|
+
expect(parentOfWorkleaf('something--workleaf--child--workleaf--slug')).toEqual({
|
|
49
|
+
parent: 'something--workleaf--child',
|
|
50
|
+
slug: 'slug',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns null when parent or slug ends up empty', () => {
|
|
55
|
+
expect(parentOfWorkleaf('--workleaf--slug')).toBeNull()
|
|
56
|
+
expect(parentOfWorkleaf('parent--workleaf--')).toBeNull()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe('workleafDir', () => {
|
|
61
|
+
let tempRoot
|
|
62
|
+
let originalCwd
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-workleaf-dir-')))
|
|
66
|
+
originalCwd = process.cwd()
|
|
67
|
+
process.chdir(tempRoot)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
afterEach(() => {
|
|
71
|
+
process.chdir(originalCwd)
|
|
72
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it('returns workleafs/<parent>/workleaf--<slug> when called from repo root', () => {
|
|
76
|
+
expect(workleafDir('main', 'quick-fix')).toBe(
|
|
77
|
+
join(tempRoot, 'workleafs', 'main', 'workleaf--quick-fix')
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('slugifies the workleaf name', () => {
|
|
82
|
+
expect(workleafDir('main', 'My Cool.Feature')).toBe(
|
|
83
|
+
join(tempRoot, 'workleafs', 'main', 'workleaf--my-cool-feature')
|
|
84
|
+
)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('resolves repo root even when called from inside a worktree', () => {
|
|
88
|
+
const wt = join(tempRoot, 'worktrees', '4.3.0')
|
|
89
|
+
mkdirSync(wt, { recursive: true })
|
|
90
|
+
expect(workleafDir('4.3.0', 'fix', wt)).toBe(
|
|
91
|
+
join(tempRoot, 'workleafs', '4.3.0', 'workleaf--fix')
|
|
92
|
+
)
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('resolves repo root even when called from inside a workleaf', () => {
|
|
96
|
+
const wl = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--outer')
|
|
97
|
+
mkdirSync(wl, { recursive: true })
|
|
98
|
+
expect(workleafDir('4.3.0', 'inner', wl)).toBe(
|
|
99
|
+
join(tempRoot, 'workleafs', '4.3.0', 'workleaf--inner')
|
|
100
|
+
)
|
|
101
|
+
})
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
describe('detectWorkleafContext', () => {
|
|
105
|
+
let tempRoot
|
|
106
|
+
|
|
107
|
+
beforeEach(() => {
|
|
108
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-workleaf-ctx-')))
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
afterEach(() => {
|
|
112
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('returns inWorkleaf=false from repo root', () => {
|
|
116
|
+
expect(detectWorkleafContext(tempRoot).inWorkleaf).toBe(false)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('returns inWorkleaf=false from inside a worktree', () => {
|
|
120
|
+
const wt = join(tempRoot, 'worktrees', '4.3.0')
|
|
121
|
+
mkdirSync(wt, { recursive: true })
|
|
122
|
+
expect(detectWorkleafContext(wt).inWorkleaf).toBe(false)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('detects parent=main when cwd is inside a main-parented workleaf', () => {
|
|
126
|
+
const dir = join(tempRoot, 'workleafs', 'main', 'workleaf--fix')
|
|
127
|
+
mkdirSync(dir, { recursive: true })
|
|
128
|
+
const ctx = detectWorkleafContext(dir)
|
|
129
|
+
expect(ctx.inWorkleaf).toBe(true)
|
|
130
|
+
expect(ctx.parent).toBe('main')
|
|
131
|
+
expect(ctx.slug).toBe('fix')
|
|
132
|
+
expect(ctx.branch).toBe('main--workleaf--fix')
|
|
133
|
+
expect(ctx.dir).toBe(dir)
|
|
134
|
+
expect(ctx.root).toBe(tempRoot)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('detects parent=<worktree> when cwd is inside a worktree-parented workleaf', () => {
|
|
138
|
+
const dir = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--bench')
|
|
139
|
+
mkdirSync(dir, { recursive: true })
|
|
140
|
+
const ctx = detectWorkleafContext(dir)
|
|
141
|
+
expect(ctx.inWorkleaf).toBe(true)
|
|
142
|
+
expect(ctx.parent).toBe('4.3.0')
|
|
143
|
+
expect(ctx.slug).toBe('bench')
|
|
144
|
+
expect(ctx.branch).toBe('4.3.0--workleaf--bench')
|
|
145
|
+
expect(ctx.root).toBe(tempRoot)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('detects context from a nested cwd inside a workleaf', () => {
|
|
149
|
+
const dir = join(tempRoot, 'workleafs', 'main', 'workleaf--fix', 'src', 'nested')
|
|
150
|
+
mkdirSync(dir, { recursive: true })
|
|
151
|
+
const ctx = detectWorkleafContext(dir)
|
|
152
|
+
expect(ctx.inWorkleaf).toBe(true)
|
|
153
|
+
expect(ctx.parent).toBe('main')
|
|
154
|
+
expect(ctx.slug).toBe('fix')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('does not match a directory that lacks the workleaf-- prefix', () => {
|
|
158
|
+
const dir = join(tempRoot, 'workleafs', 'main', 'not-a-workleaf')
|
|
159
|
+
mkdirSync(dir, { recursive: true })
|
|
160
|
+
expect(detectWorkleafContext(dir).inWorkleaf).toBe(false)
|
|
161
|
+
})
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
describe('listWorkleafs', () => {
|
|
165
|
+
let tempRoot
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-workleaf-list-')))
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
afterEach(() => {
|
|
172
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
it('returns [] when workleafs/ does not exist', () => {
|
|
176
|
+
expect(listWorkleafs(tempRoot)).toEqual([])
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('returns [] when workleafs/ has no valid entries', () => {
|
|
180
|
+
mkdirSync(join(tempRoot, 'workleafs'), { recursive: true })
|
|
181
|
+
expect(listWorkleafs(tempRoot)).toEqual([])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('lists workleafs that have a .git marker', () => {
|
|
185
|
+
const dir = join(tempRoot, 'workleafs', 'main', 'workleaf--fix')
|
|
186
|
+
mkdirSync(dir, { recursive: true })
|
|
187
|
+
writeFileSync(join(dir, '.git'), 'gitdir: /fake')
|
|
188
|
+
|
|
189
|
+
const result = listWorkleafs(tempRoot)
|
|
190
|
+
expect(result).toEqual([
|
|
191
|
+
{ parent: 'main', slug: 'fix', branch: 'main--workleaf--fix', dir },
|
|
192
|
+
])
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('skips entries missing the workleaf-- prefix and entries without .git', () => {
|
|
196
|
+
mkdirSync(join(tempRoot, 'workleafs', 'main', 'workleaf--ok'), { recursive: true })
|
|
197
|
+
writeFileSync(join(tempRoot, 'workleafs', 'main', 'workleaf--ok', '.git'), 'gitdir: /fake')
|
|
198
|
+
// Missing .git marker
|
|
199
|
+
mkdirSync(join(tempRoot, 'workleafs', 'main', 'workleaf--no-git'), { recursive: true })
|
|
200
|
+
// Missing prefix
|
|
201
|
+
mkdirSync(join(tempRoot, 'workleafs', 'main', 'just-a-folder'), { recursive: true })
|
|
202
|
+
|
|
203
|
+
const result = listWorkleafs(tempRoot)
|
|
204
|
+
expect(result).toHaveLength(1)
|
|
205
|
+
expect(result[0].slug).toBe('ok')
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('groups workleafs across multiple parents', () => {
|
|
209
|
+
const a = join(tempRoot, 'workleafs', 'main', 'workleaf--alpha')
|
|
210
|
+
const b = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--beta')
|
|
211
|
+
mkdirSync(a, { recursive: true })
|
|
212
|
+
mkdirSync(b, { recursive: true })
|
|
213
|
+
writeFileSync(join(a, '.git'), 'gitdir: /fake')
|
|
214
|
+
writeFileSync(join(b, '.git'), 'gitdir: /fake')
|
|
215
|
+
|
|
216
|
+
const result = listWorkleafs(tempRoot)
|
|
217
|
+
const parents = result.map((r) => r.parent).sort()
|
|
218
|
+
expect(parents).toEqual(['4.3.0', 'main'])
|
|
219
|
+
})
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
describe('constants', () => {
|
|
223
|
+
it('exposes WORKLEAF_PREFIX', () => {
|
|
224
|
+
expect(WORKLEAF_PREFIX).toBe('workleaf--')
|
|
225
|
+
})
|
|
226
|
+
|
|
227
|
+
it('exposes WORKLEAF_BRANCH_SEPARATOR', () => {
|
|
228
|
+
expect(WORKLEAF_BRANCH_SEPARATOR).toBe('--workleaf--')
|
|
229
|
+
})
|
|
230
|
+
})
|
|
@@ -8,6 +8,9 @@ import * as Alert from '../../../lib/components/ui/alert/index.js'
|
|
|
8
8
|
|
|
9
9
|
const CANVAS_SUCCESS_KEY = 'sb-canvas-created'
|
|
10
10
|
|
|
11
|
+
const selectClass =
|
|
12
|
+
'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-xs transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50'
|
|
13
|
+
|
|
11
14
|
function getApiUrl() {
|
|
12
15
|
const basePath = window.__STORYBOARD_BASE_PATH__ || '/'
|
|
13
16
|
return basePath.replace(/\/$/, '') + '/_storyboard/canvas'
|
|
@@ -19,8 +22,15 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
19
22
|
const [titleTouched, setTitleTouched] = useState(false)
|
|
20
23
|
const [description, setDescription] = useState('')
|
|
21
24
|
const [folder, setFolder] = useState('')
|
|
25
|
+
// Special folder select value indicating the user wants to create a new
|
|
26
|
+
// folder rather than pick an existing one. Triggers the new-folder input.
|
|
27
|
+
const NEW_FOLDER_VALUE = '__new__'
|
|
28
|
+
const [folderSelectValue, setFolderSelectValue] = useState('')
|
|
29
|
+
const [newFolderName, setNewFolderName] = useState('')
|
|
30
|
+
const [newFolderKind, setNewFolderKind] = useState('workspace')
|
|
22
31
|
const [grid, setGrid] = useState(true)
|
|
23
|
-
|
|
32
|
+
// folderEntries: [{ name, kind: 'workspace' | 'pages' }, ...]
|
|
33
|
+
const [folderEntries, setFolderEntries] = useState([])
|
|
24
34
|
const [loading, setLoading] = useState(true)
|
|
25
35
|
const [submitting, setSubmitting] = useState(false)
|
|
26
36
|
const [error, setError] = useState(null)
|
|
@@ -56,8 +66,13 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
56
66
|
)
|
|
57
67
|
|
|
58
68
|
const routePreview = useMemo(
|
|
59
|
-
() =>
|
|
60
|
-
|
|
69
|
+
() => {
|
|
70
|
+
if (!kebabName) return ''
|
|
71
|
+
// Both `.folder/` and plain dir produce the same URL — `.folder` is
|
|
72
|
+
// stripped by the data plugin's route resolver.
|
|
73
|
+
return folder ? `/canvas/${folder}/${kebabName}` : `/canvas/${kebabName}`
|
|
74
|
+
},
|
|
75
|
+
[kebabName, folder],
|
|
61
76
|
)
|
|
62
77
|
|
|
63
78
|
const nameError = useMemo(() => {
|
|
@@ -68,10 +83,14 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
68
83
|
return null
|
|
69
84
|
}, [name, kebabName])
|
|
70
85
|
|
|
71
|
-
const canSubmit = useMemo(
|
|
72
|
-
(
|
|
73
|
-
|
|
74
|
-
|
|
86
|
+
const canSubmit = useMemo(() => {
|
|
87
|
+
if (!kebabName || nameError || submitting) return false
|
|
88
|
+
// When the user picked "+ Create new folder" they must also provide a
|
|
89
|
+
// valid (kebab-case) folder name. Other selections (existing folder or
|
|
90
|
+
// "No folder") always have a usable `folder` value already.
|
|
91
|
+
if (folderSelectValue === NEW_FOLDER_VALUE && !folder) return false
|
|
92
|
+
return true
|
|
93
|
+
}, [kebabName, nameError, submitting, folderSelectValue, folder])
|
|
75
94
|
|
|
76
95
|
useEffect(() => {
|
|
77
96
|
// Restore success state after Vite's full-reload on file creation
|
|
@@ -90,7 +109,13 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
90
109
|
const res = await fetch(getApiUrl() + '/folders')
|
|
91
110
|
if (res.ok) {
|
|
92
111
|
const data = await res.json()
|
|
93
|
-
|
|
112
|
+
// Prefer the new tagged `entries` shape; fall back to legacy
|
|
113
|
+
// `folders` string array (treat as 'pages' for back-compat).
|
|
114
|
+
if (Array.isArray(data.entries)) {
|
|
115
|
+
setFolderEntries(data.entries.filter((e) => e && e.name))
|
|
116
|
+
} else if (Array.isArray(data.folders)) {
|
|
117
|
+
setFolderEntries(data.folders.map((name) => ({ name, kind: 'pages' })))
|
|
118
|
+
}
|
|
94
119
|
}
|
|
95
120
|
} catch { /* empty */ } finally {
|
|
96
121
|
setLoading(false)
|
|
@@ -99,6 +124,38 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
99
124
|
fetchFolders()
|
|
100
125
|
}, [])
|
|
101
126
|
|
|
127
|
+
function handleFolderSelectChange(value) {
|
|
128
|
+
setFolderSelectValue(value)
|
|
129
|
+
if (value === NEW_FOLDER_VALUE) {
|
|
130
|
+
// Defer folder commit until the user types a name.
|
|
131
|
+
setFolder('')
|
|
132
|
+
} else {
|
|
133
|
+
setFolder(value)
|
|
134
|
+
setNewFolderName('')
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Whenever the user types into the new-folder input, mirror it into `folder`
|
|
139
|
+
// so the submit handler picks it up. Kebab-case to match canvas folder
|
|
140
|
+
// naming conventions (data plugin / route resolution assumes lowercase).
|
|
141
|
+
const kebabNewFolder = useMemo(
|
|
142
|
+
() =>
|
|
143
|
+
newFolderName
|
|
144
|
+
.replace(/[^a-zA-Z0-9\s_-]/g, '')
|
|
145
|
+
.trim()
|
|
146
|
+
.replace(/[\s_]+/g, '-')
|
|
147
|
+
.toLowerCase()
|
|
148
|
+
.replace(/-+/g, '-')
|
|
149
|
+
.replace(/^-|-$/g, ''),
|
|
150
|
+
[newFolderName],
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
if (folderSelectValue === NEW_FOLDER_VALUE) {
|
|
155
|
+
setFolder(kebabNewFolder)
|
|
156
|
+
}
|
|
157
|
+
}, [folderSelectValue, kebabNewFolder])
|
|
158
|
+
|
|
102
159
|
function handleTitleInput(e) {
|
|
103
160
|
setTitle(e.target.value)
|
|
104
161
|
setTitleTouched(true)
|
|
@@ -123,6 +180,10 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
123
180
|
title: displayTitle,
|
|
124
181
|
description: description || undefined,
|
|
125
182
|
folder: folder || undefined,
|
|
183
|
+
// Only meaningful when the folder doesn't exist yet — the server
|
|
184
|
+
// resolves existing folders by inspecting the filesystem and
|
|
185
|
+
// ignores `folderKind` in that case.
|
|
186
|
+
folderKind: folder && folderSelectValue === NEW_FOLDER_VALUE ? newFolderKind : undefined,
|
|
126
187
|
grid,
|
|
127
188
|
}),
|
|
128
189
|
})
|
|
@@ -152,45 +213,46 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
152
213
|
}
|
|
153
214
|
|
|
154
215
|
return (
|
|
155
|
-
|
|
216
|
+
<>
|
|
156
217
|
<Panel.Header>
|
|
157
218
|
<Panel.Title>Create canvas</Panel.Title>
|
|
158
|
-
<Panel.Close
|
|
219
|
+
<Panel.Close />
|
|
159
220
|
</Panel.Header>
|
|
160
221
|
|
|
161
|
-
<div
|
|
162
|
-
<div>
|
|
222
|
+
<div className="p-4 pt-2 space-y-3" onKeyDown={handleKeydown}>
|
|
223
|
+
<div className="space-y-1">
|
|
163
224
|
<Label htmlFor="canvas-name">Name</Label>
|
|
164
225
|
<Input
|
|
165
226
|
id="canvas-name"
|
|
166
227
|
value={name}
|
|
167
228
|
onChange={(e) => setName(e.target.value)}
|
|
168
229
|
placeholder="my-canvas"
|
|
230
|
+
autoComplete="off"
|
|
231
|
+
spellCheck={false}
|
|
169
232
|
/>
|
|
170
|
-
{nameError &&
|
|
171
|
-
<p style={{ color: 'var(--color-danger-fg, red)', fontSize: '0.875rem', marginTop: '0.25rem' }}>
|
|
172
|
-
{nameError}
|
|
173
|
-
</p>
|
|
174
|
-
)}
|
|
233
|
+
{nameError && <p className="text-sm text-destructive">{nameError}</p>}
|
|
175
234
|
{routePreview && (
|
|
176
|
-
<p
|
|
177
|
-
{
|
|
235
|
+
<p className="text-xs text-muted-foreground">
|
|
236
|
+
Route:{' '}
|
|
237
|
+
<code className="px-1 py-0.5 bg-muted rounded font-mono text-foreground text-xs">
|
|
238
|
+
{routePreview}
|
|
239
|
+
</code>
|
|
178
240
|
</p>
|
|
179
241
|
)}
|
|
180
242
|
</div>
|
|
181
243
|
|
|
182
|
-
<div>
|
|
244
|
+
<div className="space-y-1">
|
|
183
245
|
<Label htmlFor="canvas-title">Title</Label>
|
|
184
246
|
<Input
|
|
185
247
|
id="canvas-title"
|
|
186
248
|
value={displayTitle}
|
|
187
249
|
onChange={handleTitleInput}
|
|
188
250
|
onBlur={handleTitleBlur}
|
|
189
|
-
placeholder=
|
|
251
|
+
placeholder={autoTitle || 'Auto-derived from name'}
|
|
190
252
|
/>
|
|
191
253
|
</div>
|
|
192
254
|
|
|
193
|
-
<div>
|
|
255
|
+
<div className="space-y-1">
|
|
194
256
|
<Label htmlFor="canvas-description">Description</Label>
|
|
195
257
|
<Input
|
|
196
258
|
id="canvas-description"
|
|
@@ -200,30 +262,64 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
200
262
|
/>
|
|
201
263
|
</div>
|
|
202
264
|
|
|
203
|
-
<div>
|
|
265
|
+
<div className="space-y-1">
|
|
204
266
|
<Label htmlFor="canvas-folder">Folder</Label>
|
|
205
267
|
<select
|
|
206
268
|
id="canvas-folder"
|
|
207
|
-
|
|
208
|
-
|
|
269
|
+
className={selectClass}
|
|
270
|
+
value={folderSelectValue}
|
|
271
|
+
onChange={(e) => handleFolderSelectChange(e.target.value)}
|
|
209
272
|
disabled={loading}
|
|
210
273
|
>
|
|
211
274
|
<option value="">No folder</option>
|
|
212
|
-
{
|
|
213
|
-
<option key={f} value={f}>
|
|
214
|
-
{f}
|
|
275
|
+
{folderEntries.map((f) => (
|
|
276
|
+
<option key={`${f.kind}:${f.name}`} value={f.name}>
|
|
277
|
+
{f.name} {f.kind === 'workspace' ? '— workspace folder' : '— canvas pages'}
|
|
215
278
|
</option>
|
|
216
279
|
))}
|
|
280
|
+
<option value={NEW_FOLDER_VALUE}>+ Create new folder…</option>
|
|
217
281
|
</select>
|
|
218
282
|
</div>
|
|
219
283
|
|
|
220
|
-
|
|
284
|
+
{folderSelectValue === NEW_FOLDER_VALUE && (
|
|
285
|
+
<div className="space-y-2 pl-4 border-l-2 border-muted">
|
|
286
|
+
<div className="space-y-1">
|
|
287
|
+
<Label htmlFor="canvas-new-folder-name">New folder name</Label>
|
|
288
|
+
<Input
|
|
289
|
+
id="canvas-new-folder-name"
|
|
290
|
+
value={newFolderName}
|
|
291
|
+
onChange={(e) => setNewFolderName(e.target.value)}
|
|
292
|
+
placeholder="kebab-case-folder"
|
|
293
|
+
/>
|
|
294
|
+
</div>
|
|
295
|
+
<div className="space-y-1">
|
|
296
|
+
<Label htmlFor="canvas-new-folder-kind">Folder type</Label>
|
|
297
|
+
<select
|
|
298
|
+
id="canvas-new-folder-kind"
|
|
299
|
+
className={selectClass}
|
|
300
|
+
value={newFolderKind}
|
|
301
|
+
onChange={(e) => setNewFolderKind(e.target.value)}
|
|
302
|
+
>
|
|
303
|
+
<option value="workspace">
|
|
304
|
+
Workspace folder — sibling canvases grouped in the workspace
|
|
305
|
+
</option>
|
|
306
|
+
<option value="pages">
|
|
307
|
+
Canvas pages — multi-page canvas, canvases become pages
|
|
308
|
+
</option>
|
|
309
|
+
</select>
|
|
310
|
+
</div>
|
|
311
|
+
</div>
|
|
312
|
+
)}
|
|
313
|
+
|
|
314
|
+
<div className="flex items-center gap-2">
|
|
221
315
|
<Checkbox
|
|
222
316
|
id="canvas-grid"
|
|
223
317
|
checked={grid}
|
|
224
318
|
onCheckedChange={setGrid}
|
|
225
319
|
/>
|
|
226
|
-
<Label htmlFor="canvas-grid"
|
|
320
|
+
<Label htmlFor="canvas-grid" className="text-sm font-normal cursor-pointer">
|
|
321
|
+
Show grid
|
|
322
|
+
</Label>
|
|
227
323
|
</div>
|
|
228
324
|
|
|
229
325
|
{error && (
|
|
@@ -234,12 +330,12 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
234
330
|
|
|
235
331
|
{success && (
|
|
236
332
|
<Alert.Root>
|
|
237
|
-
<Alert.Description>
|
|
333
|
+
<Alert.Description className="text-success">
|
|
238
334
|
{success}
|
|
239
335
|
{createdRoute && (
|
|
240
336
|
<>
|
|
241
337
|
{' — '}
|
|
242
|
-
<a href={createdRoute}>Open canvas</a>
|
|
338
|
+
<a href={createdRoute} className="underline">Open canvas</a>
|
|
243
339
|
</>
|
|
244
340
|
)}
|
|
245
341
|
</Alert.Description>
|
|
@@ -252,9 +348,9 @@ export default function CreateCanvasForm({ onClose }) {
|
|
|
252
348
|
Cancel
|
|
253
349
|
</Button>
|
|
254
350
|
<Button onClick={submit} disabled={!canSubmit}>
|
|
255
|
-
{submitting ? 'Creating
|
|
351
|
+
{submitting ? 'Creating\u2026' : 'Create'}
|
|
256
352
|
</Button>
|
|
257
353
|
</Panel.Footer>
|
|
258
|
-
|
|
354
|
+
</>
|
|
259
355
|
)
|
|
260
356
|
}
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
buildTemplateRecipeIndex,
|
|
14
14
|
resolveTemplateRecipeEntry,
|
|
15
15
|
} from '../templateIndex.js'
|
|
16
|
+
import { readWorkshopPartials } from '../partialRender.js'
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Helpers
|
|
@@ -461,7 +462,10 @@ function generateFlowJson({ title, author, description, globals, sourceData, rou
|
|
|
461
462
|
*/
|
|
462
463
|
export function createFlowsHandler(ctx) {
|
|
463
464
|
const { root, sendJson, workshopConfig = {} } = ctx
|
|
464
|
-
const getTemplateRecipes = () =>
|
|
465
|
+
const getTemplateRecipes = () => {
|
|
466
|
+
const partials = readWorkshopPartials(root)
|
|
467
|
+
return buildTemplateRecipeIndex(root, partials.length ? partials : workshopConfig.partials)
|
|
468
|
+
}
|
|
465
469
|
|
|
466
470
|
return async (req, res, { body, path: routePath, method }) => {
|
|
467
471
|
const templateRecipes = getTemplateRecipes()
|