@dfosco/storyboard 0.8.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.
Files changed (114) hide show
  1. package/dist/storyboard-ui.css +1 -1
  2. package/dist/storyboard-ui.js +6420 -6336
  3. package/dist/storyboard-ui.js.map +1 -1
  4. package/dist/tailwind.css +1 -1
  5. package/dist/tiny-canvas.css +1 -1
  6. package/package.json +1 -1
  7. package/scaffold/src/library/_app.jsx +2 -14
  8. package/scaffold/src/library/_app.module.css +2 -8
  9. package/scaffold/src/library/mount.jsx +0 -1
  10. package/src/canvas/style.css +16 -16
  11. package/src/core/artifact/operations.js +35 -2
  12. package/src/core/artifact/schemas/canvas.schema.json +7 -1
  13. package/src/core/canvas/hot-pool.js +11 -22
  14. package/src/core/canvas/selectedWidgets.js +1 -1
  15. package/src/core/canvas/server.js +52 -31
  16. package/src/core/canvas/server.test.js +107 -0
  17. package/src/core/cli/artifact.js +2 -0
  18. package/src/core/cli/create.js +35 -5
  19. package/src/core/cli/schemas.js +6 -1
  20. package/src/core/comments/ui/comment-layout.css +14 -14
  21. package/src/core/index.js +18 -0
  22. package/src/core/inspector/highlighter.js +1 -1
  23. package/src/core/inspector/mouseMode.js +2 -2
  24. package/src/core/lib/components/ui/dialog/dialog.module.css +8 -8
  25. package/src/core/modes/modes.css +4 -4
  26. package/src/core/mountStoryboardCore.js +53 -9
  27. package/src/core/mountStoryboardCore.test.js +25 -1
  28. package/src/core/rename-watcher/config.json +1 -1
  29. package/src/core/rename-watcher/watcher.js +1 -1
  30. package/src/core/stores/canvasInteractionStore.js +120 -0
  31. package/src/core/stores/canvasInteractionStore.test.js +126 -0
  32. package/src/core/stores/configSchema.js +23 -6
  33. package/src/core/stores/configSchema.test.js +3 -0
  34. package/src/core/stores/customerModeConfig.js +24 -3
  35. package/src/core/stores/customerModeConfig.test.js +46 -0
  36. package/src/core/stores/presentationStore.js +118 -0
  37. package/src/core/stores/presentationStore.test.js +114 -0
  38. package/src/core/stores/uiConfig.js +10 -2
  39. package/src/core/styles/tailwind.css +138 -90
  40. package/src/core/styles/workspace-chrome.css +4 -21
  41. package/src/core/ui/AutosyncMenuButton.css +7 -7
  42. package/src/core/ui/BranchSelect.module.css +5 -5
  43. package/src/core/ui/ChromeSlot.jsx +92 -0
  44. package/src/core/ui/ChromeSlot.test.jsx +140 -0
  45. package/src/core/ui/CoreUIBar.css +5 -5
  46. package/src/core/ui/CoreUIBar.jsx +161 -136
  47. package/src/core/ui/InspectorPanel.css +5 -5
  48. package/src/core/ui/InspectorPanel.jsx +9 -9
  49. package/src/core/ui/PwaInstallBanner.css +5 -5
  50. package/src/core/ui/PwaInstallBanner.jsx +5 -5
  51. package/src/core/ui/core-ui-colors.css +28 -66
  52. package/src/core/ui/sidepanel.css +12 -12
  53. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +102 -13
  54. package/src/internals/ArtifactForm/ArtifactForm.module.css +7 -7
  55. package/src/internals/AuthModal/AuthModal.module.css +34 -34
  56. package/src/internals/BaseUiForm/BaseUiForm.module.css +85 -85
  57. package/src/internals/BranchBar/BranchBar.module.css +4 -4
  58. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +9 -9
  59. package/src/internals/CommandPalette/CommandPalette.jsx +16 -10
  60. package/src/internals/CommandPalette/WidgetArtifactDialog.css +16 -16
  61. package/src/internals/CommandPalette/command-palette.css +14 -14
  62. package/src/internals/CreatePage/CreatePage.module.css +1 -1
  63. package/src/internals/FlowError.module.css +5 -5
  64. package/src/internals/PrototypeErrorBoundary.module.css +15 -15
  65. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +5 -4
  66. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +38 -38
  67. package/src/internals/Viewfinder.jsx +33 -16
  68. package/src/internals/Viewfinder.module.css +189 -189
  69. package/src/internals/canvas/CanvasControls.module.css +11 -11
  70. package/src/internals/canvas/CanvasPage.bridge.test.jsx +11 -37
  71. package/src/internals/canvas/CanvasPage.jsx +91 -93
  72. package/src/internals/canvas/CanvasPage.module.css +20 -19
  73. package/src/internals/canvas/CanvasToolbar.module.css +11 -11
  74. package/src/internals/canvas/ConnectorLayer.jsx +39 -2
  75. package/src/internals/canvas/ConnectorLayer.module.css +10 -10
  76. package/src/internals/canvas/PageSelector.module.css +28 -28
  77. package/src/internals/canvas/canvasTheme.js +22 -111
  78. package/src/internals/canvas/isolation/prototypeRoutes.jsx +12 -3
  79. package/src/internals/canvas/isolation/storyRoutes.jsx +1 -1
  80. package/src/internals/canvas/widgets/CodePenEmbed.jsx +16 -36
  81. package/src/internals/canvas/widgets/CodePenEmbed.module.css +12 -12
  82. package/src/internals/canvas/widgets/CropOverlay.module.css +23 -23
  83. package/src/internals/canvas/widgets/ExpandedPane.module.css +8 -8
  84. package/src/internals/canvas/widgets/FigmaEmbed.jsx +15 -36
  85. package/src/internals/canvas/widgets/FigmaEmbed.module.css +15 -15
  86. package/src/internals/canvas/widgets/FrozenTerminalOverlay.module.css +5 -5
  87. package/src/internals/canvas/widgets/ImageWidget.module.css +3 -3
  88. package/src/internals/canvas/widgets/LinkPreview.module.css +77 -78
  89. package/src/internals/canvas/widgets/MarkdownBlock.module.css +31 -31
  90. package/src/internals/canvas/widgets/PromptWidget.jsx +2 -16
  91. package/src/internals/canvas/widgets/PromptWidget.module.css +33 -33
  92. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +23 -7
  93. package/src/internals/canvas/widgets/PrototypeEmbed.module.css +58 -56
  94. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +4 -4
  95. package/src/internals/canvas/widgets/ResizeHandle.module.css +4 -4
  96. package/src/internals/canvas/widgets/StorySetWidget.jsx +45 -21
  97. package/src/internals/canvas/widgets/StoryWidget.jsx +53 -21
  98. package/src/internals/canvas/widgets/StoryWidget.module.css +22 -20
  99. package/src/internals/canvas/widgets/TerminalReadWidget.module.css +4 -4
  100. package/src/internals/canvas/widgets/TerminalWidget.jsx +56 -45
  101. package/src/internals/canvas/widgets/TerminalWidget.module.css +10 -10
  102. package/src/internals/canvas/widgets/TilesWidget.module.css +17 -17
  103. package/src/internals/canvas/widgets/WidgetChrome.module.css +61 -61
  104. package/src/internals/canvas/widgets/WidgetWrapper.module.css +2 -2
  105. package/src/internals/canvas/widgets/embedOverlay.module.css +14 -4
  106. package/src/internals/canvas/widgets/embedTheme.js +10 -102
  107. package/src/internals/canvas/widgets/widgetConfig.js +5 -5
  108. package/src/internals/story/ComponentSetPage.module.css +14 -14
  109. package/src/internals/story/StoryPage.module.css +2 -2
  110. package/src/internals/vite/data-plugin.js +5 -2
  111. package/src/internals/canvas/WebGLContextPool.jsx +0 -291
  112. package/src/internals/canvas/WebGLContextPool.test.jsx +0 -165
  113. package/src/internals/canvas/widgets/ComponentSetWidget.module.css +0 -89
  114. package/src/internals/canvas/widgets/StorySetWidget.module.css +0 -89
@@ -509,18 +509,18 @@ export default function InspectorPanel() {
509
509
  {/* Empty state */}
510
510
  {!hasSelection && !inspecting && (
511
511
  <div className="flex flex-col items-center justify-center h-full gap-3 px-6 py-12 text-center">
512
- <span style={{ color: 'var(--fgColor-muted)', opacity: 0.4 }}>
512
+ <span style={{ color: 'var(--color-foreground-muted)', opacity: 0.4 }}>
513
513
  <IconoirSquareDashed size={48} />
514
514
  </span>
515
- <p className="text-sm font-medium m-0" style={{ color: 'var(--fgColor-default)' }}>
515
+ <p className="text-sm font-medium m-0" style={{ color: 'var(--color-foreground)' }}>
516
516
  Select an element to start
517
517
  </p>
518
- <p className="text-xs m-0" style={{ color: 'var(--fgColor-muted)' }}>
518
+ <p className="text-xs m-0" style={{ color: 'var(--color-foreground-muted)' }}>
519
519
  Click the inspect button to enter selection mode
520
520
  </p>
521
521
  <button
522
522
  className="mt-2 px-4 py-1.5 text-xs font-medium rounded-md border-none cursor-pointer transition-colors"
523
- style={{ background: 'var(--sb--color-purple, #7655a4)', color: '#fff' }}
523
+ style={{ background: 'var(--color-foreground-accent, #7655a4)', color: '#fff' }}
524
524
  onClick={startInspecting}
525
525
  >
526
526
  Start inspecting
@@ -533,7 +533,7 @@ export default function InspectorPanel() {
533
533
  <div className="flex flex-col items-center justify-center h-full gap-3 px-6 py-12 text-center">
534
534
  <div className="flex items-center gap-2">
535
535
  <span className="inspector-pulse-dot" />
536
- <p className="text-sm m-0" style={{ color: 'var(--fgColor-default)' }}>
536
+ <p className="text-sm m-0" style={{ color: 'var(--color-foreground)' }}>
537
537
  Click any element on the page to inspect it
538
538
  </p>
539
539
  </div>
@@ -541,8 +541,8 @@ export default function InspectorPanel() {
541
541
  className="mt-2 px-4 py-1.5 text-xs font-medium rounded-md border cursor-pointer transition-colors"
542
542
  style={{
543
543
  background: 'transparent',
544
- color: 'var(--fgColor-muted)',
545
- borderColor: 'var(--borderColor-default, var(--sb--color-border, #d1d9e0))',
544
+ color: 'var(--color-foreground-muted)',
545
+ borderColor: 'var(--color-border, #d1d9e0)',
546
546
  }}
547
547
  onClick={stopInspecting}
548
548
  >
@@ -556,7 +556,7 @@ export default function InspectorPanel() {
556
556
  <div className="flex flex-col flex-1 min-h-0 p-3 pt-0 gap-3">
557
557
  {/* Component name */}
558
558
  <div>
559
- <h3 className="text-base font-bold m-0 inspector-mono" style={{ color: 'var(--sb--color-purple, #7655a4)' }}>
559
+ <h3 className="text-base font-bold m-0 inspector-mono" style={{ color: 'var(--color-foreground-accent, #7655a4)' }}>
560
560
  {componentInfo.name}
561
561
  </h3>
562
562
  </div>
@@ -640,7 +640,7 @@ export default function InspectorPanel() {
640
640
  {/* Re-select button */}
641
641
  <button
642
642
  className="flex items-center justify-center gap-1.5 w-full px-3 py-1.5 text-xs font-medium rounded-md border-none cursor-pointer transition-colors shrink-0"
643
- style={{ background: 'var(--sb--color-purple, #7655a4)', color: '#fff' }}
643
+ style={{ background: 'var(--color-foreground-accent, #7655a4)', color: '#fff' }}
644
644
  onClick={startInspecting}
645
645
  >
646
646
  <OcticonSvg name="search" size={12} />
@@ -8,9 +8,9 @@
8
8
  align-items: center;
9
9
  gap: 0.5rem;
10
10
  padding: 0.75rem 1rem;
11
- background: var(--bgColor-default, #0d1117);
12
- color: var(--fgColor-default, #e6edf3);
13
- border: 1px solid var(--borderColor-default, #30363d);
11
+ background: var(--color-background, #0d1117);
12
+ color: var(--color-foreground, #e6edf3);
13
+ border: 1px solid var(--color-border, #30363d);
14
14
  border-radius: 0.75rem;
15
15
  font-family: "Mona Sans", -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Noto Sans', Helvetica, Arial, sans-serif;
16
16
  font-size: 0.8125rem;
@@ -31,12 +31,12 @@
31
31
  }
32
32
 
33
33
  .pwa-banner-btn.install {
34
- background: var(--button-primary-bgColor-rest, #238636);
34
+ background: var(--color-background-success-emphasis, #238636);
35
35
  color: #fff;
36
36
  }
37
37
 
38
38
  .pwa-banner-btn.dismiss {
39
39
  background: transparent;
40
- color: var(--fgColor-muted, #8d96a0);
40
+ color: var(--color-foreground-muted, #8d96a0);
41
41
  padding: 0.375rem;
42
42
  }
@@ -33,9 +33,9 @@ const bannerStyle = {
33
33
  alignItems: 'center',
34
34
  gap: '0.5rem',
35
35
  padding: '0.75rem 1rem',
36
- background: 'var(--bgColor-default, #0d1117)',
37
- color: 'var(--fgColor-default, #e6edf3)',
38
- border: '1px solid var(--borderColor-default, #30363d)',
36
+ background: 'var(--color-background, #0d1117)',
37
+ color: 'var(--color-foreground, #e6edf3)',
38
+ border: '1px solid var(--color-border, #30363d)',
39
39
  borderRadius: '0.75rem',
40
40
  fontFamily: '"Mona Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", "Noto Sans", Helvetica, Arial, sans-serif',
41
41
  fontSize: '0.8125rem',
@@ -53,14 +53,14 @@ const btnBase = {
53
53
 
54
54
  const installBtnStyle = {
55
55
  ...btnBase,
56
- background: 'var(--button-primary-bgColor-rest, #238636)',
56
+ background: 'var(--color-background-success-emphasis, #238636)',
57
57
  color: '#fff',
58
58
  }
59
59
 
60
60
  const dismissBtnStyle = {
61
61
  ...btnBase,
62
62
  background: 'transparent',
63
- color: 'var(--fgColor-muted, #8d96a0)',
63
+ color: 'var(--color-foreground-muted, #8d96a0)',
64
64
  padding: '0.375rem',
65
65
  }
66
66
 
@@ -16,6 +16,10 @@
16
16
  *
17
17
  * TriggerButton uses the `trigger` button variant (no Tailwind dark:
18
18
  * classes) so its scoped CSS has full control over background/color.
19
+ *
20
+ * Concrete color values are pinned (not aliased) so toolbar styling
21
+ * stays locked to its own scheme regardless of the global theme cascade
22
+ * defined in `core/styles/tailwind.css`.
19
23
  */
20
24
 
21
25
  /* Default: light tokens */
@@ -27,43 +31,23 @@
27
31
  [data-slot="dialog-overlay"] {
28
32
  color-scheme: light;
29
33
 
30
- /* shadcn / Tailwind semantic tokens — pinned to light values */
31
- --color-background: hsl(0 0% 100%);
32
- --color-foreground: hsl(222.2 84% 4.9%);
33
- --color-popover: hsl(0 0% 100%);
34
- --color-popover-foreground: hsl(222.2 84% 4.9%);
35
- --color-primary: hsl(222.2 47.4% 11.2%);
36
- --color-primary-foreground: hsl(210 40% 98%);
37
- --color-secondary: hsl(210 40% 96.1%);
38
- --color-secondary-foreground: hsl(222.2 47.4% 11.2%);
39
- --color-muted: hsl(210 40% 96.1%);
40
- --color-muted-foreground: hsl(215.4 16.3% 46.9%);
41
- --color-accent: hsl(210 40% 96.1%);
42
- --color-accent-foreground: hsl(222.2 47.4% 11.2%);
43
- --color-border: hsl(214.3 31.8% 91.4%);
44
- --color-input: hsl(214.3 31.8% 91.4%);
45
-
46
- --sb--color-background: hsl(0 0% 100%);
47
- --sb--color-foreground: hsl(222.2 84% 4.9%);
48
- --sb--color-popover: hsl(0 0% 100%);
49
- --sb--color-popover-foreground: hsl(222.2 84% 4.9%);
50
- --sb--color-primary: hsl(222.2 47.4% 11.2%);
51
- --sb--color-primary-foreground: hsl(210 40% 98%);
52
- --sb--color-secondary: hsl(210 40% 96.1%);
53
- --sb--color-secondary-foreground: hsl(222.2 47.4% 11.2%);
54
- --sb--color-muted: hsl(210 40% 96.1%);
55
- --sb--color-muted-foreground: hsl(215.4 16.3% 46.9%);
56
- --sb--color-accent: hsl(210 40% 96.1%);
57
- --sb--color-accent-foreground: hsl(222.2 47.4% 11.2%);
58
- --sb--color-border: hsl(214.3 31.8% 91.4%);
59
- --sb--color-input: hsl(214.3 31.8% 91.4%);
60
-
61
- /* Primer token aliases used in side panel + inspector chrome */
62
- --bgColor-default: #ffffff;
63
- --fgColor-default: #1f2328;
64
- --fgColor-muted: #656d76;
65
- --borderColor-default: #d1d9e0;
66
- --bgColor-neutral-muted: rgba(175, 184, 193, 0.2);
34
+ --color-background: #ffffff;
35
+ --color-foreground: #1f2328;
36
+ --color-foreground-muted: #59636e;
37
+ --color-popover: #ffffff;
38
+ --color-popover-foreground: #1f2328;
39
+ --color-primary: #1f2328;
40
+ --color-primary-foreground: #ffffff;
41
+ --color-secondary: #f6f8fa;
42
+ --color-secondary-foreground: #1f2328;
43
+ --color-muted: #f6f8fa;
44
+ --color-muted-foreground: #59636e;
45
+ --color-accent: #ddf4ff;
46
+ --color-accent-foreground: #0969da;
47
+ --color-border: #d1d9e0;
48
+ --color-border-muted: #d1d9e0b3;
49
+ --color-background-neutral-muted: rgba(175, 184, 193, 0.2);
50
+ --color-input: #d1d9e0;
67
51
 
68
52
  /* Slate palette tokens — used by trigger buttons and canvas toolbar */
69
53
  --color-slate-100: oklch(96.8% .007 247.896);
@@ -72,10 +56,7 @@
72
56
  --color-slate-600: oklch(44.6% .043 257.281);
73
57
  }
74
58
 
75
- /* Dark tokens — applied when toolbar syncs with a dark theme.
76
- * Uses Primer-aligned colors (matching base.css --sb-* dark tokens)
77
- * instead of generic shadcn dark values.
78
- */
59
+ /* Dark tokens — applied when toolbar syncs with a dark theme. */
79
60
  :root[data-sb-toolbar-theme^="dark"] [data-core-ui-bar],
80
61
  :root[data-sb-toolbar-theme^="dark"] [data-sidepanel],
81
62
  :root[data-sb-toolbar-theme^="dark"] [data-slot="dropdown-menu-content"],
@@ -86,6 +67,7 @@
86
67
 
87
68
  --color-background: #161b22;
88
69
  --color-foreground: #e6edf3;
70
+ --color-foreground-muted: #8b949e;
89
71
  --color-popover: #161b22;
90
72
  --color-popover-foreground: #e6edf3;
91
73
  --color-primary: #e6edf3;
@@ -94,36 +76,16 @@
94
76
  --color-secondary-foreground: #e6edf3;
95
77
  --color-muted: #21262d;
96
78
  --color-muted-foreground: #8b949e;
97
- --color-accent: #21262d;
98
- --color-accent-foreground: #e6edf3;
79
+ --color-accent: #388bfd1a;
80
+ --color-accent-foreground: #4493f8;
99
81
  --color-border: #30363d;
82
+ --color-border-muted: #30363db3;
83
+ --color-background-neutral-muted: rgba(110, 118, 129, 0.2);
100
84
  --color-input: #30363d;
101
85
 
102
- --sb--color-background: #161b22;
103
- --sb--color-foreground: #e6edf3;
104
- --sb--color-popover: #161b22;
105
- --sb--color-popover-foreground: #e6edf3;
106
- --sb--color-primary: #e6edf3;
107
- --sb--color-primary-foreground: #161b22;
108
- --sb--color-secondary: #21262d;
109
- --sb--color-secondary-foreground: #e6edf3;
110
- --sb--color-muted: #21262d;
111
- --sb--color-muted-foreground: #8b949e;
112
- --sb--color-accent: #21262d;
113
- --sb--color-accent-foreground: #e6edf3;
114
- --sb--color-border: #30363d;
115
- --sb--color-input: #30363d;
116
-
117
- /* Primer token aliases used in side panel + inspector chrome */
118
- --bgColor-default: #161b22;
119
- --fgColor-default: #e6edf3;
120
- --fgColor-muted: #8b949e;
121
- --borderColor-default: #30363d;
122
- --bgColor-neutral-muted: rgba(110, 118, 129, 0.2);
123
-
124
86
  /* Slate palette tokens — dark equivalents */
125
87
  --color-slate-100: oklch(20.8% .042 265.755);
126
88
  --color-slate-300: oklch(29.3% .042 264.531);
127
89
  --color-slate-400: oklch(44.6% .043 257.281);
128
- --color-slate-600: oklch(70.4% .04 256.788);
90
+ --color-slate-600: oklch(44.6% .043 257.281);
129
91
  }
@@ -99,11 +99,11 @@ html.sb-sidepanel-open.sb-sidepanel-bottom .sb-mode-switch {
99
99
  z-index: 9998;
100
100
  display: flex;
101
101
  flex-direction: column;
102
- background-color: var(--bgColor-default, var(--sb--color-background, #ffffff));
103
- border-left: 1px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
102
+ background-color: var(--color-background, #ffffff);
103
+ border-left: 1px solid var(--color-border, #d0d7de);
104
104
  box-shadow: -4px 0 24px rgba(0, 0, 0, 0.15);
105
105
  font-family: "Mona Sans", -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif;
106
- color: var(--fgColor-default, var(--sb--color-foreground, #1f2328));
106
+ color: var(--color-foreground, #1f2328);
107
107
  animation: sb-sidepanel-slide-in 0.25s ease;
108
108
  }
109
109
 
@@ -115,7 +115,7 @@ html.sb-sidepanel-open.sb-sidepanel-bottom .sb-mode-switch {
115
115
  width: 100% !important;
116
116
  height: var(--sb--sidepanel-height, 300px);
117
117
  border-left: none;
118
- border-top: 1px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
118
+ border-top: 1px solid var(--color-border, #d0d7de);
119
119
  box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.15);
120
120
  animation: sb-sidepanel-slide-up 0.25s ease;
121
121
  }
@@ -199,7 +199,7 @@ html.sb-sidepanel-open.sb-sidepanel-bottom .sb-mode-switch {
199
199
  font-weight: 600;
200
200
  text-transform: uppercase;
201
201
  letter-spacing: 0.05em;
202
- color: var(--fgColor-muted, var(--sb--color-muted-foreground, #656d76));
202
+ color: var(--color-foreground-muted, #656d76);
203
203
  padding-left: 4px;
204
204
  }
205
205
 
@@ -213,7 +213,7 @@ html.sb-sidepanel-open.sb-sidepanel-bottom .sb-mode-switch {
213
213
  appearance: none;
214
214
  border: none;
215
215
  background: transparent;
216
- color: var(--fgColor-muted, var(--sb--color-muted-foreground, #656d76));
216
+ color: var(--color-foreground-muted, #656d76);
217
217
  cursor: pointer;
218
218
  padding: 6px;
219
219
  border-radius: 6px;
@@ -224,8 +224,8 @@ html.sb-sidepanel-open.sb-sidepanel-bottom .sb-mode-switch {
224
224
  }
225
225
 
226
226
  .sb-sidepanel-action-btn:hover {
227
- background: var(--bgColor-neutral-muted, rgba(110, 118, 129, 0.1));
228
- color: var(--fgColor-default, var(--sb--color-foreground, #1f2328));
227
+ background: var(--color-background-neutral-muted, rgba(110, 118, 129, 0.1));
228
+ color: var(--color-foreground, #1f2328);
229
229
  }
230
230
 
231
231
  /* Body */
@@ -246,8 +246,8 @@ html.sb-sidepanel-open.sb-sidepanel-bottom .sb-mode-switch {
246
246
  .sb-sidepanel-spinner {
247
247
  width: 20px;
248
248
  height: 20px;
249
- border: 2px solid var(--borderColor-default, var(--sb--color-border, #d0d7de));
250
- border-top-color: var(--fgColor-muted, var(--sb--color-muted-foreground, #656d76));
249
+ border: 2px solid var(--color-border, #d0d7de);
250
+ border-top-color: var(--color-foreground-muted, #656d76);
251
251
  border-radius: 50%;
252
252
  animation: sb-spin 0.6s linear infinite;
253
253
  }
@@ -263,7 +263,7 @@ html.storyboard-mode-inspect .sb-sidepanel:not(.sb-sidepanel--bottom) {
263
263
  top: 12px;
264
264
  right: 12px;
265
265
  bottom: 12px;
266
- border-radius: var(--borderRadius-default);
266
+ border-radius: var(--radius);
267
267
  border-left: none;
268
268
  }
269
269
 
@@ -275,7 +275,7 @@ html.storyboard-mode-inspect .sb-sidepanel--bottom {
275
275
  right: 12px;
276
276
  bottom: 12px;
277
277
  width: auto !important;
278
- border-radius: var(--borderRadius-default);
278
+ border-radius: var(--radius);
279
279
  border-top: none;
280
280
  }
281
281
 
@@ -22,8 +22,15 @@ export default function CreateCanvasForm({ onClose }) {
22
22
  const [titleTouched, setTitleTouched] = useState(false)
23
23
  const [description, setDescription] = useState('')
24
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')
25
31
  const [grid, setGrid] = useState(true)
26
- const [folders, setFolders] = useState([])
32
+ // folderEntries: [{ name, kind: 'workspace' | 'pages' }, ...]
33
+ const [folderEntries, setFolderEntries] = useState([])
27
34
  const [loading, setLoading] = useState(true)
28
35
  const [submitting, setSubmitting] = useState(false)
29
36
  const [error, setError] = useState(null)
@@ -59,8 +66,13 @@ export default function CreateCanvasForm({ onClose }) {
59
66
  )
60
67
 
61
68
  const routePreview = useMemo(
62
- () => (kebabName ? `/canvas/${kebabName}` : ''),
63
- [kebabName],
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],
64
76
  )
65
77
 
66
78
  const nameError = useMemo(() => {
@@ -71,10 +83,14 @@ export default function CreateCanvasForm({ onClose }) {
71
83
  return null
72
84
  }, [name, kebabName])
73
85
 
74
- const canSubmit = useMemo(
75
- () => !!kebabName && !nameError && !submitting,
76
- [kebabName, nameError, submitting],
77
- )
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])
78
94
 
79
95
  useEffect(() => {
80
96
  // Restore success state after Vite's full-reload on file creation
@@ -93,7 +109,13 @@ export default function CreateCanvasForm({ onClose }) {
93
109
  const res = await fetch(getApiUrl() + '/folders')
94
110
  if (res.ok) {
95
111
  const data = await res.json()
96
- setFolders(data.folders || [])
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
+ }
97
119
  }
98
120
  } catch { /* empty */ } finally {
99
121
  setLoading(false)
@@ -102,6 +124,38 @@ export default function CreateCanvasForm({ onClose }) {
102
124
  fetchFolders()
103
125
  }, [])
104
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
+
105
159
  function handleTitleInput(e) {
106
160
  setTitle(e.target.value)
107
161
  setTitleTouched(true)
@@ -126,6 +180,10 @@ export default function CreateCanvasForm({ onClose }) {
126
180
  title: displayTitle,
127
181
  description: description || undefined,
128
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,
129
187
  grid,
130
188
  }),
131
189
  })
@@ -209,19 +267,50 @@ export default function CreateCanvasForm({ onClose }) {
209
267
  <select
210
268
  id="canvas-folder"
211
269
  className={selectClass}
212
- value={folder}
213
- onChange={(e) => setFolder(e.target.value)}
270
+ value={folderSelectValue}
271
+ onChange={(e) => handleFolderSelectChange(e.target.value)}
214
272
  disabled={loading}
215
273
  >
216
274
  <option value="">No folder</option>
217
- {folders.map((f) => (
218
- <option key={f} value={f}>
219
- {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'}
220
278
  </option>
221
279
  ))}
280
+ <option value={NEW_FOLDER_VALUE}>+ Create new folder…</option>
222
281
  </select>
223
282
  </div>
224
283
 
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
+
225
314
  <div className="flex items-center gap-2">
226
315
  <Checkbox
227
316
  id="canvas-grid"
@@ -5,8 +5,8 @@
5
5
  .formShell {
6
6
  max-width: 480px;
7
7
  padding: 28px;
8
- background: var(--bgColor-default, #ffffff);
9
- border: 1px solid var(--borderColor-default, #d0d7de);
8
+ background: var(--color-background, #ffffff);
9
+ border: 1px solid var(--color-border, #d0d7de);
10
10
  border-radius: 14px;
11
11
  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
12
12
  }
@@ -38,7 +38,7 @@
38
38
  font-size: 20px;
39
39
  font-weight: 700;
40
40
  line-height: 1.2;
41
- color: var(--fgColor-default, #1a1a1a);
41
+ color: var(--color-foreground, #1a1a1a);
42
42
  letter-spacing: -0.01em;
43
43
  }
44
44
 
@@ -49,7 +49,7 @@
49
49
  .description {
50
50
  margin: 0;
51
51
  font-size: 14px;
52
- color: var(--fgColor-muted, #656d76);
52
+ color: var(--color-foreground-muted, #656d76);
53
53
  line-height: 1.45;
54
54
  }
55
55
 
@@ -70,7 +70,7 @@
70
70
  /* ─── Code field override (textarea variant) ─── */
71
71
 
72
72
  .codeField {
73
- font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace);
73
+ font-family: var(--font-mono, ui-monospace, SFMono-Regular, 'SF Mono', Menlo, Consolas, monospace);
74
74
  font-size: 13px;
75
75
  line-height: 1.5;
76
76
  }
@@ -84,7 +84,7 @@
84
84
  gap: 8px;
85
85
  margin-top: 4px;
86
86
  padding-top: 16px;
87
- border-top: 1px solid var(--borderColor-muted, #eaeef2);
87
+ border-top: 1px solid var(--color-border-muted, #eaeef2);
88
88
  }
89
89
 
90
90
  /* ─── Advanced toggle ─── */
@@ -92,7 +92,7 @@
92
92
  .advancedToggle {
93
93
  align-self: flex-start;
94
94
  margin-top: 2px;
95
- color: var(--fgColor-muted, #656d76);
95
+ color: var(--color-foreground-muted, #656d76);
96
96
  padding-left: 10px;
97
97
  padding-right: 10px;
98
98
  }