@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
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
buildTemplateRecipeIndex,
|
|
22
22
|
resolveTemplateRecipeEntry,
|
|
23
23
|
} from '../templateIndex.js'
|
|
24
|
+
import { readWorkshopPartials } from '../partialRender.js'
|
|
24
25
|
|
|
25
26
|
const FLOW_SKELETON = JSON.stringify({ $global: [] }, null, 2) + '\n'
|
|
26
27
|
|
|
@@ -178,7 +179,10 @@ function generatePrototypeJson({ title, author, description, partialEntry, url }
|
|
|
178
179
|
*/
|
|
179
180
|
export function createPrototypesHandler(ctx) {
|
|
180
181
|
const { root, sendJson, workshopConfig = {} } = ctx
|
|
181
|
-
const getTemplateRecipes = () =>
|
|
182
|
+
const getTemplateRecipes = () => {
|
|
183
|
+
const partials = readWorkshopPartials(root)
|
|
184
|
+
return buildTemplateRecipeIndex(root, partials.length ? partials : workshopConfig.partials)
|
|
185
|
+
}
|
|
182
186
|
|
|
183
187
|
return async (req, res, { body, path: routePath, method }) => {
|
|
184
188
|
const templateRecipes = getTemplateRecipes()
|
|
@@ -70,6 +70,26 @@ function makeId(prefix, directory, name, prototype, folder) {
|
|
|
70
70
|
return `prototype:${folderPart}${prototype}:${directory}/${name}`
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Resolve the actual filesystem directory for a global partial. The
|
|
75
|
+
* config says `directory: "template"` (singular) but the filesystem may
|
|
76
|
+
* use `templates` (plural). Probe both, return whichever exists. Returns
|
|
77
|
+
* `null` when neither directory contains the named partial.
|
|
78
|
+
*/
|
|
79
|
+
function resolveGlobalPartialBaseDir(srcRoot, configDirectory, name) {
|
|
80
|
+
const kind = toPartialKind(configDirectory)
|
|
81
|
+
if (!kind) return null
|
|
82
|
+
// Both spellings are accepted in config; the actual on-disk dir may
|
|
83
|
+
// differ. Try the config spelling first, then the alternative.
|
|
84
|
+
const alternatives = kind === 'template'
|
|
85
|
+
? Array.from(new Set([configDirectory, 'templates', 'template']))
|
|
86
|
+
: Array.from(new Set([configDirectory, 'recipes', 'recipe']))
|
|
87
|
+
for (const dir of alternatives) {
|
|
88
|
+
if (fs.existsSync(path.join(srcRoot, dir, name))) return dir
|
|
89
|
+
}
|
|
90
|
+
return null
|
|
91
|
+
}
|
|
92
|
+
|
|
73
93
|
export function buildTemplateRecipeIndex(root, configPartials = []) {
|
|
74
94
|
const srcRoot = path.join(root, 'src')
|
|
75
95
|
const results = []
|
|
@@ -82,13 +102,20 @@ export function buildTemplateRecipeIndex(root, configPartials = []) {
|
|
|
82
102
|
const id = makeId('global', partial.directory, partial.name)
|
|
83
103
|
if (seenIds.has(id)) continue
|
|
84
104
|
seenIds.add(id)
|
|
105
|
+
// Resolve baseDir to the directory that actually exists on disk so
|
|
106
|
+
// downstream code (operations.js createPrototype) reads the right
|
|
107
|
+
// path. Without this, a config with `directory: "template"` while
|
|
108
|
+
// the files live in `src/templates/` would silently fail at write
|
|
109
|
+
// time and leave an orphaned .prototype.json behind.
|
|
110
|
+
const resolvedBaseDir = resolveGlobalPartialBaseDir(srcRoot, partial.directory, partial.name)
|
|
111
|
+
|| partial.directory
|
|
85
112
|
results.push({
|
|
86
113
|
id,
|
|
87
114
|
name: partial.name,
|
|
88
115
|
directory: partial.directory,
|
|
89
116
|
kind,
|
|
90
117
|
scope: 'global',
|
|
91
|
-
baseDir:
|
|
118
|
+
baseDir: resolvedBaseDir,
|
|
92
119
|
globals: Array.isArray(partial.globals) ? partial.globals : undefined,
|
|
93
120
|
})
|
|
94
121
|
}
|
|
@@ -30,7 +30,22 @@ const BASE_PORT = 1234
|
|
|
30
30
|
* @returns {string} absolute path to ports.json
|
|
31
31
|
*/
|
|
32
32
|
export function portsFilePath(cwd = process.cwd()) {
|
|
33
|
-
|
|
33
|
+
// Defensive: cwd may have just been deleted out from under us (e.g. a
|
|
34
|
+
// workleaf merge cleaning up its own directory before releasing the
|
|
35
|
+
// port). Fall back to the raw path so callers don't ENOENT.
|
|
36
|
+
let realCwd
|
|
37
|
+
try {
|
|
38
|
+
realCwd = realpathSync(cwd)
|
|
39
|
+
} catch {
|
|
40
|
+
realCwd = cwd
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Check workleaf first — its path is `<root>/workleafs/<parent>/workleaf--<slug>`
|
|
44
|
+
// so the prefix capture must NOT swallow that segment when we're inside one.
|
|
45
|
+
const workleafMatch = realCwd.match(/^(.+)[/\\]workleafs[/\\][^/\\]+[/\\]workleaf--[^/\\]+/)
|
|
46
|
+
if (workleafMatch) {
|
|
47
|
+
return join(workleafMatch[1], 'worktrees', 'ports.json')
|
|
48
|
+
}
|
|
34
49
|
|
|
35
50
|
// Check if we're inside worktrees/<name>/ or .worktrees/<name>/
|
|
36
51
|
const worktreeMatch = realCwd.match(/^(.+)[/\\]\.?worktrees[/\\][^/\\]+/)
|
|
@@ -45,20 +60,35 @@ export function portsFilePath(cwd = process.cwd()) {
|
|
|
45
60
|
/**
|
|
46
61
|
* Detect the worktree name from the current git context.
|
|
47
62
|
*
|
|
48
|
-
* Returns
|
|
49
|
-
*
|
|
63
|
+
* Returns:
|
|
64
|
+
* - The compound branch name `<parent>--workleaf--<slug>` when cwd
|
|
65
|
+
* is inside a `workleafs/<parent>/workleaf--<slug>/` directory.
|
|
66
|
+
* This keeps `storyboard dev` (and every consumer that calls
|
|
67
|
+
* this helper) working from inside a workleaf with zero changes.
|
|
68
|
+
* - The worktree name when cwd is inside `worktrees/<name>/` or
|
|
69
|
+
* `.worktrees/<name>/`.
|
|
70
|
+
* - The current branch name when on a non-main/master branch.
|
|
71
|
+
* - `'main'` otherwise.
|
|
50
72
|
*/
|
|
51
73
|
export function detectWorktreeName() {
|
|
52
74
|
try {
|
|
53
75
|
const topLevel = execSync('git rev-parse --show-toplevel', { encoding: 'utf8' }).trim()
|
|
54
76
|
const realTop = realpathSync(topLevel)
|
|
55
77
|
|
|
78
|
+
// Check workleaf first — `workleafs/<parent>/workleaf--<slug>` is
|
|
79
|
+
// the most specific pattern and overlaps with the worktree match
|
|
80
|
+
// when the parent is itself a worktree.
|
|
81
|
+
const workleafMatch = realTop.match(/[/\\]workleafs[/\\]([^/\\]+)[/\\]workleaf--([^/\\]+)$/)
|
|
82
|
+
if (workleafMatch) return `${workleafMatch[1]}--workleaf--${workleafMatch[2]}`
|
|
83
|
+
|
|
56
84
|
// Check if we're inside a worktrees/<name> or .worktrees/<name> directory
|
|
57
85
|
const wtMatch = realTop.match(/[/\\]\.?worktrees[/\\]([^/\\]+)$/)
|
|
58
86
|
if (wtMatch) return wtMatch[1]
|
|
59
87
|
|
|
60
|
-
// Also check the cwd pattern
|
|
88
|
+
// Also check the cwd pattern (workleaf first, then worktree)
|
|
61
89
|
const realCwd = realpathSync(process.cwd())
|
|
90
|
+
const cwdWorkleafMatch = realCwd.match(/[/\\]workleafs[/\\]([^/\\]+)[/\\]workleaf--([^/\\]+)/)
|
|
91
|
+
if (cwdWorkleafMatch) return `${cwdWorkleafMatch[1]}--workleaf--${cwdWorkleafMatch[2]}`
|
|
62
92
|
const worktreeMatch = realCwd.match(/\.?worktrees[/\\]([^/\\]+)/)
|
|
63
93
|
if (worktreeMatch) return worktreeMatch[1]
|
|
64
94
|
|
|
@@ -226,7 +256,20 @@ export function slugify(name) {
|
|
|
226
256
|
* @returns {string} absolute path to repo root
|
|
227
257
|
*/
|
|
228
258
|
export function repoRoot(cwd = process.cwd()) {
|
|
229
|
-
|
|
259
|
+
// Defensive: cwd may have been deleted (e.g. during workleaf cleanup).
|
|
260
|
+
// Fall back to raw cwd so the regex matchers still get a useful path.
|
|
261
|
+
let realCwd
|
|
262
|
+
try {
|
|
263
|
+
realCwd = realpathSync(cwd)
|
|
264
|
+
} catch {
|
|
265
|
+
realCwd = cwd
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Workleaf wins over worktree: `workleafs/<parent>/workleaf--<slug>`
|
|
269
|
+
// appears below the parent worktree on disk, but the repo root is
|
|
270
|
+
// always the directory containing the top-level `workleafs/` dir.
|
|
271
|
+
const workleafMatch = realCwd.match(/^(.+)[/\\]workleafs[/\\][^/\\]+[/\\]workleaf--[^/\\]+/)
|
|
272
|
+
if (workleafMatch) return workleafMatch[1]
|
|
230
273
|
|
|
231
274
|
const worktreeMatch = realCwd.match(/^(.+)[/\\]\.?worktrees[/\\][^/\\]+/)
|
|
232
275
|
if (worktreeMatch) return worktreeMatch[1]
|
|
@@ -164,6 +164,48 @@ describe('repoRoot', () => {
|
|
|
164
164
|
const wt = join(tempRoot, '.worktrees', 'legacy-branch')
|
|
165
165
|
expect(repoRoot(wt)).toBe(tempRoot)
|
|
166
166
|
})
|
|
167
|
+
|
|
168
|
+
it('returns parent of workleafs when inside a workleaf (main-parented)', () => {
|
|
169
|
+
const wl = join(tempRoot, 'workleafs', 'main', 'workleaf--fix')
|
|
170
|
+
mkdirSync(wl, { recursive: true })
|
|
171
|
+
expect(repoRoot(wl)).toBe(tempRoot)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('returns parent of workleafs when inside a workleaf (worktree-parented)', () => {
|
|
175
|
+
const wl = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--bench')
|
|
176
|
+
mkdirSync(wl, { recursive: true })
|
|
177
|
+
expect(repoRoot(wl)).toBe(tempRoot)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('returns parent of workleafs even from a nested cwd inside a workleaf', () => {
|
|
181
|
+
const wl = join(tempRoot, 'workleafs', 'main', 'workleaf--fix', 'src', 'core')
|
|
182
|
+
mkdirSync(wl, { recursive: true })
|
|
183
|
+
expect(repoRoot(wl)).toBe(tempRoot)
|
|
184
|
+
})
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
describe('portsFilePath (workleaf-aware)', () => {
|
|
188
|
+
let tempRoot
|
|
189
|
+
|
|
190
|
+
beforeEach(() => {
|
|
191
|
+
tempRoot = realpathSync(mkdtempSync(join(tmpdir(), 'sb-port-wl-')))
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
rmSync(tempRoot, { recursive: true, force: true })
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
it('returns root worktrees/ports.json from inside a workleaf', () => {
|
|
199
|
+
const wl = join(tempRoot, 'workleafs', 'main', 'workleaf--fix')
|
|
200
|
+
mkdirSync(wl, { recursive: true })
|
|
201
|
+
expect(portsFilePath(wl)).toBe(join(tempRoot, 'worktrees', 'ports.json'))
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('returns root worktrees/ports.json from inside a worktree-parented workleaf', () => {
|
|
205
|
+
const wl = join(tempRoot, 'workleafs', '4.3.0', 'workleaf--bench')
|
|
206
|
+
mkdirSync(wl, { recursive: true })
|
|
207
|
+
expect(portsFilePath(wl)).toBe(join(tempRoot, 'worktrees', 'ports.json'))
|
|
208
|
+
})
|
|
167
209
|
})
|
|
168
210
|
|
|
169
211
|
describe('worktreeDir', () => {
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
* submit actions for any artifact type. Surfaces identically across the
|
|
4
4
|
* command palette, canvas add menu, and workspace.
|
|
5
5
|
*
|
|
6
|
+
* Styling: the form is built on the BaseUiForm primitives (`@base-ui/react`
|
|
7
|
+
* + workspace-style CSS) — Mona Sans, pill buttons, clean white cards.
|
|
8
|
+
* This replaced the prior Primer-based implementation so artifact creation
|
|
9
|
+
* shares the SimpleWorkspace look across every surface.
|
|
10
|
+
*
|
|
6
11
|
* Modes:
|
|
7
12
|
* <ArtifactForm type="prototype" onSubmit={fn} /> // single type
|
|
8
13
|
* <ArtifactForm onSubmit={fn} /> // type picker
|
|
@@ -11,32 +16,64 @@
|
|
|
11
16
|
* Schemas may declare `field.dynamic = 'prototypes'`. Pass
|
|
12
17
|
* `dynamicOptions={{ prototypes: ['my-app', ...] }}` to populate them.
|
|
13
18
|
*/
|
|
14
|
-
import { useState, useMemo, useEffect } from 'react'
|
|
15
|
-
import {
|
|
16
|
-
|
|
19
|
+
import { useState, useMemo, useEffect, useRef } from 'react'
|
|
20
|
+
import {
|
|
21
|
+
Form,
|
|
22
|
+
Field,
|
|
23
|
+
Label,
|
|
24
|
+
Description,
|
|
25
|
+
Validation,
|
|
26
|
+
TextInput,
|
|
27
|
+
Textarea,
|
|
28
|
+
Select,
|
|
29
|
+
Checkbox,
|
|
30
|
+
Button,
|
|
31
|
+
Flash,
|
|
32
|
+
Menu,
|
|
33
|
+
} from '../BaseUiForm/BaseUiForm.jsx'
|
|
34
|
+
import { ARTIFACT_SCHEMAS, validateArtifact, isFieldVisible } from './artifactSchemas.js'
|
|
17
35
|
import styles from './ArtifactForm.module.css'
|
|
18
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Slugify a freeform string into the kebab-case form the `name` field expects.
|
|
39
|
+
* Mirrors the NAME_PATTERN in artifactSchemas.js — lowercase letters, digits,
|
|
40
|
+
* and hyphens. Used to auto-derive `name` from `title` while the user types,
|
|
41
|
+
* until they manually edit `name` themselves.
|
|
42
|
+
*/
|
|
43
|
+
function slugifyName(value) {
|
|
44
|
+
return String(value || '')
|
|
45
|
+
.toLowerCase()
|
|
46
|
+
.normalize('NFKD')
|
|
47
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
48
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
49
|
+
.replace(/^-+|-+$/g, '')
|
|
50
|
+
.slice(0, 64)
|
|
51
|
+
}
|
|
52
|
+
|
|
19
53
|
function TypeSelector({ selected, onChange }) {
|
|
20
54
|
const types = Object.entries(ARTIFACT_SCHEMAS)
|
|
21
55
|
const current = ARTIFACT_SCHEMAS[selected]
|
|
22
56
|
|
|
23
57
|
return (
|
|
24
|
-
<
|
|
25
|
-
<
|
|
26
|
-
{current?.icon}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
58
|
+
<Menu>
|
|
59
|
+
<Menu.Button>
|
|
60
|
+
<span>{current?.icon}</span>
|
|
61
|
+
<span>{current?.label || 'Select type…'}</span>
|
|
62
|
+
</Menu.Button>
|
|
63
|
+
<Menu.Items>
|
|
64
|
+
{types.map(([key, schema]) => (
|
|
65
|
+
<Menu.Item
|
|
66
|
+
key={key}
|
|
67
|
+
selected={key === selected}
|
|
68
|
+
onSelect={() => onChange(key)}
|
|
69
|
+
description={schema.description}
|
|
70
|
+
>
|
|
71
|
+
<span>{schema.icon}</span>
|
|
72
|
+
<span>{schema.label}</span>
|
|
73
|
+
</Menu.Item>
|
|
74
|
+
))}
|
|
75
|
+
</Menu.Items>
|
|
76
|
+
</Menu>
|
|
40
77
|
)
|
|
41
78
|
}
|
|
42
79
|
|
|
@@ -49,9 +86,8 @@ function FieldRenderer({ field, value, error, onChange, options }) {
|
|
|
49
86
|
value={value || ''}
|
|
50
87
|
onChange={e => onChange(e.target.value)}
|
|
51
88
|
placeholder={field.placeholder}
|
|
52
|
-
block
|
|
53
89
|
type={field.type === 'url' ? 'url' : 'text'}
|
|
54
|
-
|
|
90
|
+
invalid={!!error}
|
|
55
91
|
/>
|
|
56
92
|
)
|
|
57
93
|
case 'textarea':
|
|
@@ -60,8 +96,8 @@ function FieldRenderer({ field, value, error, onChange, options }) {
|
|
|
60
96
|
value={value || ''}
|
|
61
97
|
onChange={e => onChange(e.target.value)}
|
|
62
98
|
placeholder={field.placeholder}
|
|
63
|
-
block
|
|
64
99
|
rows={3}
|
|
100
|
+
invalid={!!error}
|
|
65
101
|
/>
|
|
66
102
|
)
|
|
67
103
|
case 'code':
|
|
@@ -70,52 +106,26 @@ function FieldRenderer({ field, value, error, onChange, options }) {
|
|
|
70
106
|
value={value || ''}
|
|
71
107
|
onChange={e => onChange(e.target.value)}
|
|
72
108
|
placeholder={field.placeholder}
|
|
73
|
-
block
|
|
74
109
|
rows={8}
|
|
75
110
|
className={styles.codeField}
|
|
111
|
+
invalid={!!error}
|
|
76
112
|
/>
|
|
77
113
|
)
|
|
78
114
|
case 'select':
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
<select
|
|
89
|
-
className={styles.select}
|
|
90
|
-
value={value || ''}
|
|
91
|
-
onChange={e => onChange(e.target.value)}
|
|
92
|
-
>
|
|
93
|
-
<option value="">{field.placeholder || 'Select…'}</option>
|
|
94
|
-
{grouped
|
|
95
|
-
? Object.entries(norm.reduce((acc, opt) => {
|
|
96
|
-
const key = opt.group || 'Other'
|
|
97
|
-
;(acc[key] = acc[key] || []).push(opt)
|
|
98
|
-
return acc
|
|
99
|
-
}, {})).map(([group, opts]) => (
|
|
100
|
-
<optgroup key={group} label={group}>
|
|
101
|
-
{opts.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
|
102
|
-
</optgroup>
|
|
103
|
-
))
|
|
104
|
-
: norm.map(opt => <option key={opt.value} value={opt.value}>{opt.label}</option>)
|
|
105
|
-
}
|
|
106
|
-
</select>
|
|
107
|
-
)
|
|
108
|
-
}
|
|
115
|
+
return (
|
|
116
|
+
<Select
|
|
117
|
+
value={value || ''}
|
|
118
|
+
onChange={val => onChange(val)}
|
|
119
|
+
options={options || []}
|
|
120
|
+
placeholder={field.placeholder || 'Select…'}
|
|
121
|
+
invalid={!!error}
|
|
122
|
+
/>
|
|
123
|
+
)
|
|
109
124
|
case 'checkbox':
|
|
110
125
|
return (
|
|
111
|
-
<
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
checked={!!value}
|
|
115
|
-
onChange={e => onChange(e.target.checked)}
|
|
116
|
-
/>
|
|
117
|
-
<span>{field.checkboxLabel || field.label}</span>
|
|
118
|
-
</label>
|
|
126
|
+
<Checkbox checked={!!value} onChange={v => onChange(v)}>
|
|
127
|
+
{field.checkboxLabel || field.label}
|
|
128
|
+
</Checkbox>
|
|
119
129
|
)
|
|
120
130
|
default:
|
|
121
131
|
return null
|
|
@@ -132,6 +142,38 @@ function initialValues(schema) {
|
|
|
132
142
|
return initial
|
|
133
143
|
}
|
|
134
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Render one Field wrapper for a schema field. Checkbox fields hide the
|
|
147
|
+
* standalone label because the inline label inside the Checkbox already
|
|
148
|
+
* names the control — otherwise the label appears twice. Descriptions
|
|
149
|
+
* render under the input when there's no error (same pattern as
|
|
150
|
+
* `patternHint`).
|
|
151
|
+
*/
|
|
152
|
+
function renderField(field, { values, errors, dynamicOptions, handleChange }) {
|
|
153
|
+
const options = field.dynamic ? dynamicOptions[field.dynamic] : field.options
|
|
154
|
+
const isCheckbox = field.type === 'checkbox'
|
|
155
|
+
const errorMessage = errors[field.name]
|
|
156
|
+
return (
|
|
157
|
+
<Field key={field.name} invalid={!!errorMessage}>
|
|
158
|
+
{isCheckbox
|
|
159
|
+
? <Label visuallyHidden>{field.label}</Label>
|
|
160
|
+
: <Label required={field.required}>{field.label}</Label>
|
|
161
|
+
}
|
|
162
|
+
<FieldRenderer
|
|
163
|
+
field={field}
|
|
164
|
+
value={values[field.name]}
|
|
165
|
+
error={errorMessage}
|
|
166
|
+
onChange={val => handleChange(field.name, val)}
|
|
167
|
+
options={options}
|
|
168
|
+
/>
|
|
169
|
+
{errorMessage && <Validation>{errorMessage}</Validation>}
|
|
170
|
+
{!errorMessage && (field.caption || field.patternHint) && (
|
|
171
|
+
<Description>{field.caption || field.patternHint}</Description>
|
|
172
|
+
)}
|
|
173
|
+
</Field>
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
|
|
135
177
|
export default function ArtifactForm({
|
|
136
178
|
type: fixedType,
|
|
137
179
|
onSubmit,
|
|
@@ -150,18 +192,66 @@ export default function ArtifactForm({
|
|
|
150
192
|
const [errors, setErrors] = useState({})
|
|
151
193
|
const [submitting, setSubmitting] = useState(false)
|
|
152
194
|
|
|
195
|
+
// Tracks whether the `name` field should keep auto-following `title`.
|
|
196
|
+
// Flips to false the first time the user edits `name` directly, so we don't
|
|
197
|
+
// clobber their input. Reset on type change or unmount.
|
|
198
|
+
const nameAutoFollowRef = useRef(true)
|
|
199
|
+
|
|
153
200
|
// Reset form when fixed type changes (parent-controlled)
|
|
154
201
|
useEffect(() => {
|
|
155
202
|
if (!fixedType) return
|
|
156
203
|
setValues({ ...initialValues(ARTIFACT_SCHEMAS[fixedType]), ...(initialOverride || {}) })
|
|
157
204
|
setErrors({})
|
|
158
205
|
setSubmitting(false)
|
|
206
|
+
nameAutoFollowRef.current = true
|
|
159
207
|
}, [fixedType])
|
|
160
208
|
|
|
209
|
+
// Merge late-arriving initial overrides (e.g. a gh-login fetch that resolves
|
|
210
|
+
// after mount) into empty fields. Only fills fields the user hasn't typed
|
|
211
|
+
// into yet, so we never overwrite their input. Stringify the override so the
|
|
212
|
+
// dep check fires on content changes rather than parent re-render identity.
|
|
213
|
+
const overrideKey = useMemo(() => JSON.stringify(initialOverride || {}), [initialOverride])
|
|
214
|
+
useEffect(() => {
|
|
215
|
+
if (!initialOverride || !schema) return
|
|
216
|
+
setValues(prev => {
|
|
217
|
+
let next = prev
|
|
218
|
+
for (const [k, v] of Object.entries(initialOverride)) {
|
|
219
|
+
if (v == null || v === '') continue
|
|
220
|
+
const fieldExists = schema.fields.some(f => f.name === k)
|
|
221
|
+
const isEmpty = prev[k] === '' || prev[k] == null
|
|
222
|
+
if (fieldExists && isEmpty) {
|
|
223
|
+
if (next === prev) next = { ...prev }
|
|
224
|
+
next[k] = v
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return next
|
|
228
|
+
})
|
|
229
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
230
|
+
}, [overrideKey, schema])
|
|
231
|
+
|
|
232
|
+
// Derive `name` (kebab-case) from `title` as the user types, until they
|
|
233
|
+
// manually edit `name`. Only applies to schemas that expose both fields
|
|
234
|
+
// (prototype, canvas, flow). Component uses PascalCase and has no title,
|
|
235
|
+
// so it's naturally excluded.
|
|
236
|
+
useEffect(() => {
|
|
237
|
+
if (!schema || !nameAutoFollowRef.current) return
|
|
238
|
+
const hasName = schema.fields.some(f => f.name === 'name' && f.type === 'text')
|
|
239
|
+
const hasTitle = schema.fields.some(f => f.name === 'title')
|
|
240
|
+
if (!hasName || !hasTitle) return
|
|
241
|
+
const derived = slugifyName(values.title)
|
|
242
|
+
if (derived === (values.name || '')) return
|
|
243
|
+
setValues(prev => ({ ...prev, name: derived }))
|
|
244
|
+
if (errors.name) {
|
|
245
|
+
setErrors(prev => { const next = { ...prev }; delete next.name; return next })
|
|
246
|
+
}
|
|
247
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
248
|
+
}, [values.title, schema])
|
|
249
|
+
|
|
161
250
|
function handleTypeChange(newType) {
|
|
162
251
|
setSelectedType(newType)
|
|
163
252
|
setValues(initialValues(ARTIFACT_SCHEMAS[newType]))
|
|
164
253
|
setErrors({})
|
|
254
|
+
nameAutoFollowRef.current = true
|
|
165
255
|
}
|
|
166
256
|
|
|
167
257
|
const [showAdvanced, setShowAdvanced] = useState(false)
|
|
@@ -169,8 +259,11 @@ export default function ArtifactForm({
|
|
|
169
259
|
const visibleFields = useMemo(() => {
|
|
170
260
|
if (!schema) return []
|
|
171
261
|
if (compact) return schema.fields.filter(f => f.required)
|
|
172
|
-
// Hide mutually-exclusive partner once one side is set
|
|
173
262
|
let fields = schema.fields
|
|
263
|
+
// Hide fields whose `showIf` predicate is not satisfied (e.g. external
|
|
264
|
+
// URL only appears when the "External prototype" checkbox is on).
|
|
265
|
+
fields = fields.filter(f => isFieldVisible(f, values))
|
|
266
|
+
// Hide mutually-exclusive partner once one side is set
|
|
174
267
|
if (schema.mutuallyExclusive) {
|
|
175
268
|
fields = fields.filter(f => {
|
|
176
269
|
for (const group of schema.mutuallyExclusive) {
|
|
@@ -205,6 +298,9 @@ export default function ArtifactForm({
|
|
|
205
298
|
}
|
|
206
299
|
|
|
207
300
|
function handleChange(fieldName, value) {
|
|
301
|
+
if (fieldName === 'name') {
|
|
302
|
+
nameAutoFollowRef.current = false
|
|
303
|
+
}
|
|
208
304
|
setValues(prev => ({ ...prev, [fieldName]: value }))
|
|
209
305
|
if (errors[fieldName]) {
|
|
210
306
|
setErrors(prev => { const next = { ...prev }; delete next[fieldName]; return next })
|
|
@@ -229,14 +325,19 @@ export default function ArtifactForm({
|
|
|
229
325
|
}
|
|
230
326
|
|
|
231
327
|
return (
|
|
232
|
-
<
|
|
328
|
+
<Form
|
|
329
|
+
className={styles.formShell}
|
|
330
|
+
onSubmit={handleSubmit}
|
|
331
|
+
data-compact={compact || undefined}
|
|
332
|
+
data-bare={hideHeader || undefined}
|
|
333
|
+
>
|
|
233
334
|
{!hideHeader && (
|
|
234
335
|
<header className={styles.header}>
|
|
235
336
|
<h3 className={styles.title}>
|
|
236
337
|
{operation === 'create' ? 'New' : 'Edit'} {schema.label}
|
|
237
338
|
</h3>
|
|
238
339
|
{!compact && (
|
|
239
|
-
<
|
|
340
|
+
<p className={styles.description}>{schema.description}</p>
|
|
240
341
|
)}
|
|
241
342
|
</header>
|
|
242
343
|
)}
|
|
@@ -248,29 +349,9 @@ export default function ArtifactForm({
|
|
|
248
349
|
)}
|
|
249
350
|
|
|
250
351
|
<div className={styles.fields}>
|
|
251
|
-
{basicFields.map(field => {
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<FormControl key={field.name} required={field.required}>
|
|
255
|
-
<FormControl.Label>{field.label}</FormControl.Label>
|
|
256
|
-
<FieldRenderer
|
|
257
|
-
field={field}
|
|
258
|
-
value={values[field.name]}
|
|
259
|
-
error={errors[field.name]}
|
|
260
|
-
onChange={val => handleChange(field.name, val)}
|
|
261
|
-
options={options}
|
|
262
|
-
/>
|
|
263
|
-
{errors[field.name] && (
|
|
264
|
-
<FormControl.Validation variant="error">
|
|
265
|
-
{errors[field.name]}
|
|
266
|
-
</FormControl.Validation>
|
|
267
|
-
)}
|
|
268
|
-
{field.patternHint && !errors[field.name] && (
|
|
269
|
-
<FormControl.Caption>{field.patternHint}</FormControl.Caption>
|
|
270
|
-
)}
|
|
271
|
-
</FormControl>
|
|
272
|
-
)
|
|
273
|
-
})}
|
|
352
|
+
{basicFields.map(field => renderField(field, {
|
|
353
|
+
values, errors, dynamicOptions, handleChange,
|
|
354
|
+
}))}
|
|
274
355
|
|
|
275
356
|
{advancedFields.length > 0 && (
|
|
276
357
|
<>
|
|
@@ -283,29 +364,9 @@ export default function ArtifactForm({
|
|
|
283
364
|
>
|
|
284
365
|
{showAdvanced ? '− Hide advanced fields' : '+ Advanced fields'}
|
|
285
366
|
</Button>
|
|
286
|
-
{showAdvanced && advancedFields.map(field => {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<FormControl key={field.name} required={field.required}>
|
|
290
|
-
<FormControl.Label>{field.label}</FormControl.Label>
|
|
291
|
-
<FieldRenderer
|
|
292
|
-
field={field}
|
|
293
|
-
value={values[field.name]}
|
|
294
|
-
error={errors[field.name]}
|
|
295
|
-
onChange={val => handleChange(field.name, val)}
|
|
296
|
-
options={options}
|
|
297
|
-
/>
|
|
298
|
-
{errors[field.name] && (
|
|
299
|
-
<FormControl.Validation variant="error">
|
|
300
|
-
{errors[field.name]}
|
|
301
|
-
</FormControl.Validation>
|
|
302
|
-
)}
|
|
303
|
-
{field.patternHint && !errors[field.name] && (
|
|
304
|
-
<FormControl.Caption>{field.patternHint}</FormControl.Caption>
|
|
305
|
-
)}
|
|
306
|
-
</FormControl>
|
|
307
|
-
)
|
|
308
|
-
})}
|
|
367
|
+
{showAdvanced && advancedFields.map(field => renderField(field, {
|
|
368
|
+
values, errors, dynamicOptions, handleChange,
|
|
369
|
+
}))}
|
|
309
370
|
</>
|
|
310
371
|
)}
|
|
311
372
|
</div>
|
|
@@ -324,8 +385,8 @@ export default function ArtifactForm({
|
|
|
324
385
|
{submitting ? 'Creating…' : `${operation === 'create' ? 'Create' : 'Save'} ${schema.label}`}
|
|
325
386
|
</Button>
|
|
326
387
|
</footer>
|
|
327
|
-
</
|
|
388
|
+
</Form>
|
|
328
389
|
)
|
|
329
390
|
}
|
|
330
391
|
|
|
331
|
-
export { TypeSelector, ARTIFACT_SCHEMAS, validateArtifact }
|
|
392
|
+
export { TypeSelector, ARTIFACT_SCHEMAS, validateArtifact, isFieldVisible }
|