@dfosco/storyboard 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/assets/favicon.svg +12 -0
  2. package/dist/storyboard-ui.css +1 -1
  3. package/dist/storyboard-ui.js +13907 -10816
  4. package/dist/storyboard-ui.js.map +1 -1
  5. package/dist/tailwind.css +1 -1
  6. package/package.json +138 -127
  7. package/scaffold/AGENTS.md +9 -7
  8. package/scaffold/README.md +18 -0
  9. package/scaffold/deploy.yml +21 -0
  10. package/scaffold/gitignore +8 -14
  11. package/scaffold/preview.yml +40 -0
  12. package/scaffold/{manifest.json → scaffold.config.json} +73 -3
  13. package/scaffold/scripts/aggregate-artifacts.mjs +257 -0
  14. package/scaffold/skills/canvas/SKILL.md +1 -1
  15. package/scaffold/skills/migrate-0.5.0/SKILL.md +1 -1
  16. package/scaffold/skills/ship/SKILL.md +2 -2
  17. package/scaffold/skills/worktree/SKILL.md +1 -1
  18. package/scaffold/src/_prototype.jsx +17 -0
  19. package/scaffold/src/_story.jsx +13 -0
  20. package/scaffold/src/library/README.md +34 -0
  21. package/scaffold/src/library/_app.jsx +53 -0
  22. package/scaffold/src/library/_app.module.css +8 -0
  23. package/scaffold/src/library/create.jsx +10 -0
  24. package/scaffold/src/library/index.jsx +14 -0
  25. package/scaffold/src/library/mount.jsx +52 -0
  26. package/scaffold/src/library/prototypes-entry.jsx +57 -0
  27. package/scaffold/src/library/routes.jsx +205 -0
  28. package/scaffold/src/library/viewfinder.jsx +13 -0
  29. package/scaffold/src/library/workspace.jsx +17 -0
  30. package/scripts/aggregate-artifacts.mjs +257 -0
  31. package/src/core/artifact/operations.js +19 -13
  32. package/src/core/autosync/server.test.js +1 -1
  33. package/src/core/canvas/githubEmbeds.js +12 -0
  34. package/src/core/canvas/githubEmbeds.test.js +71 -2
  35. package/src/core/canvas/hot-pool.js +10 -11
  36. package/src/core/canvas/materializer.js +39 -13
  37. package/src/core/canvas/server.js +324 -69
  38. package/src/core/canvas/server.test.js +365 -2
  39. package/src/core/canvas/terminal-server.js +144 -57
  40. package/src/core/canvas/undoRedo.js +174 -0
  41. package/src/core/canvas/undoRedo.test.js +250 -0
  42. package/src/core/cli/buildManifest.js +34 -0
  43. package/src/core/cli/canvasRedo.js +44 -0
  44. package/src/core/cli/canvasUndo.js +40 -0
  45. package/src/core/cli/create.js +116 -18
  46. package/src/core/cli/dev.js +23 -35
  47. package/src/core/cli/index.js +21 -0
  48. package/src/core/cli/schemas.js +2 -3
  49. package/src/core/cli/serverUrl.js +24 -0
  50. package/src/core/cli/setup.js +14 -25
  51. package/src/core/cli/terminal-welcome.js +12 -6
  52. package/src/core/cli/updateVersion.js +42 -2
  53. package/src/core/cli/workleaf.js +140 -0
  54. package/src/core/comments/api.js +1 -1
  55. package/src/core/comments/queries.js +3 -0
  56. package/src/core/comments/ui/CommentWindow.jsx +3 -2
  57. package/src/core/data/artifactIndex.js +235 -0
  58. package/src/core/data/artifactIndex.test.js +203 -0
  59. package/src/core/data/artifactManifest.js +290 -0
  60. package/src/core/data/artifactManifest.test.js +207 -0
  61. package/src/core/data/viewfinder.js +47 -7
  62. package/src/core/index.js +6 -1
  63. package/src/core/lib/components/ui/avatar/avatar-fallback.jsx +2 -2
  64. package/src/core/lib/components/ui/avatar/avatar-image.jsx +2 -2
  65. package/src/core/lib/components/ui/avatar/avatar.jsx +2 -2
  66. package/src/core/lib/components/ui/checkbox/checkbox.jsx +12 -9
  67. package/src/core/lib/components/ui/dialog/dialog-content.jsx +45 -31
  68. package/src/core/lib/components/ui/dialog/dialog-trigger.jsx +9 -5
  69. package/src/core/lib/components/ui/dialog/dialog.jsx +12 -3
  70. package/src/core/lib/components/ui/dialog/dialog.module.css +118 -0
  71. package/src/core/lib/components/ui/dialog/index.js +4 -34
  72. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-group.jsx +3 -3
  73. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-checkbox-item.jsx +19 -9
  74. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-content.jsx +8 -8
  75. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group-heading.jsx +2 -2
  76. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-group.jsx +2 -2
  77. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-item.jsx +15 -4
  78. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-portal.jsx +2 -2
  79. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-group.jsx +2 -2
  80. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.jsx +14 -7
  81. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-separator.jsx +2 -2
  82. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.jsx +8 -4
  83. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.jsx +4 -4
  84. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-sub.jsx +2 -2
  85. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu-trigger.jsx +8 -2
  86. package/src/core/lib/components/ui/dropdown-menu/dropdown-menu.jsx +2 -2
  87. package/src/core/lib/components/ui/label/label.jsx +1 -2
  88. package/src/core/lib/components/ui/panel/panel-close.jsx +7 -4
  89. package/src/core/lib/components/ui/panel/panel-content.jsx +7 -9
  90. package/src/core/lib/components/ui/panel/panel-title.jsx +3 -3
  91. package/src/core/lib/components/ui/panel/panel.jsx +11 -2
  92. package/src/core/lib/components/ui/separator/separator.jsx +6 -4
  93. package/src/core/lib/components/ui/tooltip/tooltip-content.jsx +7 -6
  94. package/src/core/lib/components/ui/tooltip/tooltip-trigger.jsx +13 -6
  95. package/src/core/lib/components/ui/tooltip/tooltip.jsx +5 -5
  96. package/src/core/mountStoryboardCore.js +234 -74
  97. package/src/core/mountStoryboardCore.test.js +174 -0
  98. package/src/core/rename-watcher/watcher.js +329 -36
  99. package/src/core/rename-watcher/watcher.test.js +253 -0
  100. package/src/core/scaffold/fragments.js +287 -0
  101. package/src/core/scaffold/fragments.test.js +401 -0
  102. package/src/core/scaffold/index.js +291 -0
  103. package/src/core/scaffold/migrateLegacyGitignore.js +157 -0
  104. package/src/core/scaffold/scaffold-integration.test.js +247 -0
  105. package/src/core/scaffold.js +4 -97
  106. package/src/core/stores/configSchema.js +41 -2
  107. package/src/core/stores/recentArtifacts.js +49 -2
  108. package/src/core/stores/recentArtifacts.test.js +59 -6
  109. package/src/core/stores/themeBootstrap.js +264 -0
  110. package/src/core/stores/themeBootstrap.test.js +211 -0
  111. package/src/core/stores/themeStore.test.js +154 -0
  112. package/src/core/stores/themeStore.ts +235 -220
  113. package/src/core/styles/generateThemesCss.js +63 -0
  114. package/src/core/styles/tailwind.css +12 -4
  115. package/src/core/styles/workspace-chrome.css +30 -0
  116. package/src/core/tools/handlers/paletteTheme.js +11 -8
  117. package/src/core/ui/ActionMenuButton.jsx +2 -1
  118. package/src/core/ui/CanvasCreateMenu.jsx +1 -19
  119. package/src/core/ui/CoreUIBar.jsx +10 -7
  120. package/src/core/ui/ThemeMenuButton.jsx +45 -30
  121. package/src/core/vite/server-plugin.js +36 -0
  122. package/src/core/workleaf/cleanup.js +102 -0
  123. package/src/core/workleaf/create.js +199 -0
  124. package/src/core/workleaf/delete.js +101 -0
  125. package/src/core/workleaf/list.js +57 -0
  126. package/src/core/workleaf/merge.js +238 -0
  127. package/src/core/workleaf/paths.js +146 -0
  128. package/src/core/workleaf/paths.test.js +230 -0
  129. package/src/core/workshop/features/createCanvas/CreateCanvasForm.jsx +28 -21
  130. package/src/core/workshop/features/createFlow/server.js +5 -1
  131. package/src/core/workshop/features/createPrototype/server.js +5 -1
  132. package/src/core/workshop/features/templateIndex.js +28 -1
  133. package/src/core/worktree/port.js +48 -5
  134. package/src/core/worktree/port.test.js +42 -0
  135. package/src/internals/ArtifactForm/ArtifactForm.jsx +173 -112
  136. package/src/internals/ArtifactForm/ArtifactForm.module.css +48 -66
  137. package/src/internals/ArtifactForm/artifactSchemas.js +40 -5
  138. package/src/internals/BaseUiForm/BaseUiForm.jsx +293 -0
  139. package/src/internals/BaseUiForm/BaseUiForm.module.css +542 -0
  140. package/src/internals/BranchesDropdown/BranchesDropdown.jsx +69 -0
  141. package/src/internals/BranchesDropdown/BranchesDropdown.module.css +84 -0
  142. package/src/internals/CommandPalette/CommandPalette.jsx +25 -19
  143. package/src/internals/CommandPalette/CreateArtifactForm.jsx +150 -0
  144. package/src/internals/CommandPalette/CreateDialog.jsx +20 -110
  145. package/src/internals/CommandPalette/WidgetArtifactDialog.jsx +1 -1
  146. package/src/internals/CommandPalette/createHelpers.js +30 -0
  147. package/src/internals/CreatePage/CreatePage.jsx +57 -0
  148. package/src/internals/CreatePage/CreatePage.module.css +34 -0
  149. package/src/internals/Dialog.jsx +51 -0
  150. package/src/internals/SimpleWorkspace/SimpleWorkspace.jsx +533 -0
  151. package/src/internals/SimpleWorkspace/SimpleWorkspace.module.css +323 -0
  152. package/src/internals/Tooltip.jsx +39 -0
  153. package/src/internals/Viewfinder.jsx +196 -48
  154. package/src/internals/Viewfinder.module.css +72 -1
  155. package/src/internals/canvas/CanvasPage.jsx +267 -124
  156. package/src/internals/canvas/CanvasPage.multiselect.test.jsx +39 -26
  157. package/src/internals/canvas/PageSelector.jsx +15 -1
  158. package/src/internals/canvas/PageSelector.module.css +14 -0
  159. package/src/internals/canvas/canvasApi.js +40 -5
  160. package/src/internals/canvas/isolation/createIsolationMiddleware.js +68 -0
  161. package/src/internals/canvas/isolation/mountIsolationApp.jsx +88 -0
  162. package/src/internals/canvas/isolation/prototypeRoutes.jsx +131 -0
  163. package/src/internals/canvas/isolation/prototypesEntry.jsx +28 -0
  164. package/src/internals/canvas/isolation/storiesEntry.jsx +20 -0
  165. package/src/internals/canvas/isolation/storyRoutes.jsx +55 -0
  166. package/src/internals/canvas/useUndoRedo.js +75 -60
  167. package/src/internals/canvas/useUndoRedo.test.js +46 -178
  168. package/src/internals/canvas/widgets/ExpandedPaneTopBar.jsx +1 -1
  169. package/src/internals/canvas/widgets/FrozenTerminalOverlay.jsx +30 -21
  170. package/src/internals/canvas/widgets/ImageWidget.jsx +7 -2
  171. package/src/internals/canvas/widgets/LinkPreview.jsx +45 -1
  172. package/src/internals/canvas/widgets/LinkPreview.module.css +71 -0
  173. package/src/internals/canvas/widgets/PrototypeEmbed.jsx +69 -10
  174. package/src/internals/canvas/widgets/PrototypeEmbed.test.jsx +114 -0
  175. package/src/internals/canvas/widgets/StorySetWidget.jsx +8 -4
  176. package/src/internals/canvas/widgets/StoryWidget.jsx +21 -38
  177. package/src/internals/canvas/widgets/TerminalReadWidget.jsx +13 -15
  178. package/src/internals/canvas/widgets/WidgetChrome.jsx +1 -1
  179. package/src/internals/canvas/widgets/WidgetChrome.module.css +15 -0
  180. package/src/internals/canvas/widgets/embedTheme.js +2 -2
  181. package/src/internals/canvas/widgets/expandUtils.js +19 -19
  182. package/src/internals/canvas/widgets/normalizeLegacyEmbedSrc.js +74 -0
  183. package/src/internals/context.jsx +22 -1
  184. package/src/internals/context.test.jsx +47 -0
  185. package/src/internals/hooks/useArtifactIndex.js +31 -0
  186. package/src/internals/hooks/useArtifactIndex.test.js +45 -0
  187. package/src/internals/hooks/usePrototypeReloadGuard.js +14 -8
  188. package/src/internals/index.js +6 -0
  189. package/src/internals/loadConsumerWrapper.js +60 -0
  190. package/src/internals/story/ComponentSetPage.jsx +75 -34
  191. package/src/internals/story/StoryPage.jsx +16 -25
  192. package/src/internals/vite/data-plugin.js +313 -148
  193. package/src/internals/vite/data-plugin.test.js +11 -11
  194. package/{scaffold/storyboard.config.json → storyboard.config.json} +25 -0
  195. package/terminal.config.json +1 -1
  196. package/src/core/lib/components/ui/card/card-action.jsx +0 -11
  197. package/src/core/lib/components/ui/card/card-content.jsx +0 -11
  198. package/src/core/lib/components/ui/card/card-description.jsx +0 -11
  199. package/src/core/lib/components/ui/card/card-footer.jsx +0 -11
  200. package/src/core/lib/components/ui/card/card-header.jsx +0 -19
  201. package/src/core/lib/components/ui/card/card-title.jsx +0 -11
  202. package/src/core/lib/components/ui/card/card.jsx +0 -17
  203. package/src/core/lib/components/ui/card/index.js +0 -25
  204. package/src/core/lib/components/ui/collapsible/collapsible-content.jsx +0 -7
  205. package/src/core/lib/components/ui/collapsible/collapsible-trigger.jsx +0 -7
  206. package/src/core/lib/components/ui/collapsible/collapsible.jsx +0 -7
  207. package/src/core/lib/components/ui/collapsible/index.js +0 -13
  208. package/src/core/lib/components/ui/dialog/dialog-close.jsx +0 -7
  209. package/src/core/lib/components/ui/dialog/dialog-description.jsx +0 -15
  210. package/src/core/lib/components/ui/dialog/dialog-footer.jsx +0 -23
  211. package/src/core/lib/components/ui/dialog/dialog-header.jsx +0 -11
  212. package/src/core/lib/components/ui/dialog/dialog-overlay.jsx +0 -15
  213. package/src/core/lib/components/ui/dialog/dialog-portal.jsx +0 -4
  214. package/src/core/lib/components/ui/dialog/dialog-title.jsx +0 -15
  215. package/src/core/lib/components/ui/popover/index.js +0 -28
  216. package/src/core/lib/components/ui/popover/popover-close.jsx +0 -7
  217. package/src/core/lib/components/ui/popover/popover-content.jsx +0 -22
  218. package/src/core/lib/components/ui/popover/popover-description.jsx +0 -11
  219. package/src/core/lib/components/ui/popover/popover-header.jsx +0 -11
  220. package/src/core/lib/components/ui/popover/popover-portal.jsx +0 -4
  221. package/src/core/lib/components/ui/popover/popover-title.jsx +0 -11
  222. package/src/core/lib/components/ui/popover/popover-trigger.jsx +0 -8
  223. package/src/core/lib/components/ui/popover/popover.jsx +0 -4
  224. package/src/core/lib/components/ui/select/index.js +0 -37
  225. package/src/core/lib/components/ui/select/select-content.jsx +0 -30
  226. package/src/core/lib/components/ui/select/select-group-heading.jsx +0 -17
  227. package/src/core/lib/components/ui/select/select-group.jsx +0 -15
  228. package/src/core/lib/components/ui/select/select-item.jsx +0 -26
  229. package/src/core/lib/components/ui/select/select-label.jsx +0 -11
  230. package/src/core/lib/components/ui/select/select-portal.jsx +0 -4
  231. package/src/core/lib/components/ui/select/select-scroll-down-button.jsx +0 -18
  232. package/src/core/lib/components/ui/select/select-scroll-up-button.jsx +0 -18
  233. package/src/core/lib/components/ui/select/select-separator.jsx +0 -15
  234. package/src/core/lib/components/ui/select/select-trigger.jsx +0 -25
  235. package/src/core/lib/components/ui/select/select.jsx +0 -4
  236. package/src/core/lib/components/ui/sheet/index.js +0 -34
  237. package/src/core/lib/components/ui/sheet/sheet-close.jsx +0 -7
  238. package/src/core/lib/components/ui/sheet/sheet-content.jsx +0 -35
  239. package/src/core/lib/components/ui/sheet/sheet-description.jsx +0 -15
  240. package/src/core/lib/components/ui/sheet/sheet-footer.jsx +0 -11
  241. package/src/core/lib/components/ui/sheet/sheet-header.jsx +0 -11
  242. package/src/core/lib/components/ui/sheet/sheet-overlay.jsx +0 -15
  243. package/src/core/lib/components/ui/sheet/sheet-portal.jsx +0 -4
  244. package/src/core/lib/components/ui/sheet/sheet-title.jsx +0 -15
  245. package/src/core/lib/components/ui/sheet/sheet-trigger.jsx +0 -7
  246. package/src/core/lib/components/ui/sheet/sheet.jsx +0 -4
  247. package/src/core/lib/components/ui/toggle/index.js +0 -8
  248. package/src/core/lib/components/ui/toggle/toggle.jsx +0 -36
  249. package/src/core/lib/components/ui/toggle-group/index.js +0 -10
  250. package/src/core/lib/components/ui/toggle-group/toggle-group-item.jsx +0 -29
  251. package/src/core/lib/components/ui/toggle-group/toggle-group.jsx +0 -43
  252. package/src/internals/canvas/StoryErrorBoundary.jsx +0 -47
  253. package/src/internals/canvas/componentIsolate.jsx +0 -144
  254. package/src/internals/canvas/componentSetIsolate.jsx +0 -300
  255. package/src/internals/canvas/widgets/InlineStoryRenderer.jsx +0 -129
  256. package/src/internals/canvas/widgets/InlineStoryRenderer.test.jsx +0 -68
  257. package/src/primer/ThemeSync.jsx +0 -73
  258. package/src/primer/index.js +0 -1
@@ -1,211 +1,292 @@
1
1
  /**
2
- * Theme Store — manages the active color scheme for the entire app.
2
+ * Theme Store — config-driven theming layer.
3
3
  *
4
- * Reads/writes `sb-color-scheme` in localStorage, sets Primer CSS attributes
5
- * (`data-color-mode`, `data-light-theme`, `data-dark-theme`) and the internal
6
- * `data-sb-theme` attribute on `<html>`, and dispatches a
7
- * `storyboard:theme:changed` custom event so that non-store consumers
8
- * (React ThemeProvider, etc.) can react.
4
+ * Reads its theme + surface registry from the unified config store
5
+ * (`getConfig("theming")`). For each configured surface this module:
9
6
  *
10
- * Supports a "system" value that follows the OS preference via
11
- * `prefers-color-scheme`, updating automatically when the user changes
12
- * their system theme.
7
+ * 1. Watches the DOM for `[data-sb-surface="<id>"]` elements and writes
8
+ * `data-sb-<id>-theme="<resolvedThemeId>"` plus every key/value pair
9
+ * from the active theme's `attrs` companion map on those elements.
10
+ * 2. Writes the legacy `<html>` attribute for the canonical surfaces
11
+ * (`data-sb-theme`, `data-sb-toolbar-theme`, `data-sb-code-theme`,
12
+ * `data-sb-canvas-theme`) so existing CSS selectors keep working
13
+ * without migration.
14
+ *
15
+ * State:
16
+ * - `sb-color-scheme` — the active global theme id (or "system")
17
+ * - `sb-theme-sync` — `Record<surfaceId, boolean>` of per-surface sync
18
+ *
19
+ * Both keys are persisted in localStorage. A `storage` event listener picks
20
+ * up cross-frame writes so prototype iframes stay in sync with the parent.
21
+ *
22
+ * Public API stays source-compatible with the pre-refactor module:
23
+ * - `setTheme`, `getTheme`, `themeState`, `THEMES` (now derived from config)
24
+ * - `setThemeSyncTarget`, `getThemeSyncTargets`, `themeSyncState`
25
+ *
26
+ * New exports:
27
+ * - `getSurfaceSync`, `setSurfaceSync` — config-key-aware accessors
28
+ * - `getResolvedFor(surfaceId)` — convenience for consumers
13
29
  */
14
30
 
31
+ import { getConfig, subscribeToConfig } from './configStore.js'
32
+ import { configDefaults } from './configSchema.js'
33
+ import {
34
+ THEME_STORAGE_KEY,
35
+ SYNC_STORAGE_KEY,
36
+ LEGACY_SURFACE_ATTR,
37
+ readStoredTheme,
38
+ readStoredSync,
39
+ resolveTheme,
40
+ computeBySurface,
41
+ applyThemeToDom,
42
+ } from './themeBootstrap.js'
43
+
15
44
  // ---------------------------------------------------------------------------
16
45
  // Types
17
46
  // ---------------------------------------------------------------------------
18
47
 
19
- export type ThemeValue =
20
- | 'system'
21
- | 'light'
22
- | 'light_colorblind'
23
- | 'dark'
24
- | 'dark_colorblind'
25
- | 'dark_high_contrast'
26
- | 'dark_dimmed'
27
-
28
- export interface ThemeOption {
29
- name: string
30
- value: ThemeValue
31
- }
32
-
33
- export interface ThemeState {
34
- /** The stored theme value (may be "system") */
35
- theme: ThemeValue
36
- /** The resolved CSS theme value (never "system") */
37
- resolved: string
38
- }
48
+ export type ThemeOption = { name: string; value: string }
49
+ export type ThemeValue = string
50
+ export interface ThemeState { theme: ThemeValue; resolved: string }
39
51
 
40
52
  type Subscriber<T> = (value: T) => void
41
53
  type Unsubscriber = () => void
42
54
 
43
- interface Readable<T> {
44
- subscribe(run: Subscriber<T>): Unsubscriber
45
- }
46
-
47
- interface Writable<T> extends Readable<T> {
48
- set(value: T): void
49
- }
55
+ interface Readable<T> { subscribe(run: Subscriber<T>): Unsubscriber }
56
+ interface Writable<T> extends Readable<T> { set(value: T): void }
50
57
 
51
58
  function writable<T>(initial: T): Writable<T> {
52
59
  let value = initial
53
60
  const subs = new Set<Subscriber<T>>()
54
61
  return {
55
- set(v: T) {
56
- value = v
57
- subs.forEach((fn) => fn(value))
58
- },
62
+ set(v: T) { value = v; subs.forEach((fn) => fn(value)) },
59
63
  subscribe(run: Subscriber<T>): Unsubscriber {
60
- subs.add(run)
61
- run(value)
64
+ subs.add(run); run(value)
62
65
  return () => { subs.delete(run) }
63
66
  },
64
67
  }
65
68
  }
66
69
 
67
70
  // ---------------------------------------------------------------------------
68
- // Constants
71
+ // Config snapshot
69
72
  // ---------------------------------------------------------------------------
70
73
 
71
- const STORAGE_KEY = 'sb-color-scheme'
72
-
73
- export const THEMES: ThemeOption[] = [
74
- { name: 'System', value: 'system' },
75
- { name: 'Light', value: 'light' },
76
- { name: 'Light colorblind', value: 'light_colorblind' },
77
- { name: 'Dark', value: 'dark' },
78
- { name: 'Dark colorblind', value: 'dark_colorblind' },
79
- { name: 'Dark high contrast', value: 'dark_high_contrast' },
80
- { name: 'Dark Dimmed', value: 'dark_dimmed' },
81
- ]
74
+ function themesConfig(): any {
75
+ // configStore may not be seeded yet (themeStore is imported at the top
76
+ // of the core barrel, before mountStoryboardCore runs). Fall back to the
77
+ // schema defaults so consumers always see the canonical 4-surface set
78
+ // even if storyboard.config.json omits the `theming` block entirely.
79
+ const fromStore = getConfig('theming')
80
+ if (fromStore && (fromStore.surfaces || fromStore.themes)) return fromStore
81
+ return (configDefaults as any).theming || {}
82
+ }
82
83
 
83
84
  // ---------------------------------------------------------------------------
84
- // Helpers
85
+ // Persistent state
85
86
  // ---------------------------------------------------------------------------
86
87
 
87
- function getSystemTheme(): string {
88
- if (typeof window === 'undefined') return 'light'
89
- return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
90
- }
91
-
92
- function resolveTheme(value: ThemeValue): string {
93
- return value === 'system' ? getSystemTheme() : value
94
- }
95
-
96
- function readStoredTheme(): ThemeValue {
97
- if (typeof localStorage === 'undefined') return 'system'
98
- const stored = localStorage.getItem(STORAGE_KEY)
99
- if (!stored) return 'system'
100
- // Legacy values (pre-system support) are plain scheme names
101
- if (stored === 'system') return 'system'
102
- return stored as ThemeValue
103
- }
88
+ let _current: ThemeValue = readStoredTheme(themesConfig())
89
+ let _syncTargets: Record<string, boolean> = readStoredSync(themesConfig())
104
90
 
105
91
  function snapshot(theme: ThemeValue): ThemeState {
106
92
  return { theme, resolved: resolveTheme(theme) }
107
93
  }
108
94
 
95
+ const _store = writable<ThemeState>(snapshot(_current))
96
+ const _syncStore = writable<Record<string, boolean>>(_syncTargets)
97
+
109
98
  // ---------------------------------------------------------------------------
110
- // Store
99
+ // DOM write + change broadcast
111
100
  // ---------------------------------------------------------------------------
112
101
 
113
- let _current: ThemeValue = readStoredTheme()
114
- const _store = writable<ThemeState>(snapshot(_current))
115
-
116
102
  function _applyToDOM(theme: ThemeValue, resolved: string): void {
117
- if (typeof document === 'undefined') return
118
- const el = document.documentElement
119
-
120
- // Per-target resolved themes
121
- const prototypeTheme = _syncTargets.prototype ? resolved : 'light'
122
- const toolbarTheme = _syncTargets.toolbar ? resolved : 'light'
123
- const codeTheme = _syncTargets.codeBoxes ? resolved : 'light'
124
- const canvasTheme = _syncTargets.canvas ? resolved : 'light'
125
-
126
- // Internal attributes
127
- el.setAttribute('data-sb-theme', prototypeTheme)
128
- el.setAttribute('data-sb-code-theme', codeTheme)
129
-
130
- // Toolbar theme — follows global theme when synced, stays light otherwise
131
- el.setAttribute('data-sb-toolbar-theme', toolbarTheme)
132
- el.setAttribute('data-sb-canvas-theme', canvasTheme)
133
-
134
- // Primer CSS attributes — these drive @primer/react ThemeProvider and
135
- // Primer CSS custom-property layers without needing React state updates.
136
- if (theme === 'system' && _syncTargets.prototype) {
137
- el.setAttribute('data-color-mode', 'auto')
138
- el.setAttribute('data-light-theme', 'light')
139
- el.setAttribute('data-dark-theme', 'dark')
140
- } else if (prototypeTheme.startsWith('dark')) {
141
- el.setAttribute('data-color-mode', 'dark')
142
- el.setAttribute('data-dark-theme', prototypeTheme)
143
- el.setAttribute('data-light-theme', 'light')
144
- } else {
145
- el.setAttribute('data-color-mode', 'light')
146
- el.setAttribute('data-light-theme', prototypeTheme)
147
- el.setAttribute('data-dark-theme', 'dark')
148
- }
103
+ const cfg = themesConfig()
104
+ const bySurface = computeBySurface(cfg, resolved, _syncTargets)
105
+ applyThemeToDom(cfg, bySurface)
149
106
  }
150
107
 
151
108
  function _dispatchEvent(theme: ThemeValue, resolved: string): void {
152
109
  if (typeof document === 'undefined') return
153
- const prototypeTheme = _syncTargets.prototype ? theme : 'light'
154
- const prototypeResolved = _syncTargets.prototype ? resolved : 'light'
155
- const toolbarResolved = _syncTargets.toolbar ? resolved : 'light'
156
- const codeResolved = _syncTargets.codeBoxes ? resolved : 'light'
157
- const canvasResolved = _syncTargets.canvas ? resolved : 'light'
158
-
159
- document.dispatchEvent(
160
- new CustomEvent('storyboard:theme:changed', {
161
- detail: {
162
- theme,
163
- resolved,
164
- prototypeTheme,
165
- prototypeResolved,
166
- toolbarResolved,
167
- codeResolved,
168
- canvasResolved,
169
- },
170
- }),
171
- )
110
+ const cfg = themesConfig()
111
+ const bySurface = computeBySurface(cfg, resolved, _syncTargets)
112
+
113
+ // bySurface carries the new shape. Legacy keys (prototypeTheme,
114
+ // prototypeResolved, toolbarResolved, codeResolved, canvasResolved) are
115
+ // included so existing listeners — Primer's ThemeSync, embed bridges —
116
+ // continue to work without a coordinated upgrade.
117
+ document.dispatchEvent(new CustomEvent('storyboard:theme:changed', {
118
+ detail: {
119
+ theme,
120
+ resolved,
121
+ bySurface,
122
+ // Legacy mirror — keep until consumers migrate.
123
+ prototypeTheme: bySurface.prototype,
124
+ prototypeResolved: bySurface.prototype,
125
+ toolbarResolved: bySurface.toolbar,
126
+ codeResolved: bySurface.codeBoxes,
127
+ canvasResolved: bySurface.canvas,
128
+ },
129
+ }))
172
130
  }
173
131
 
174
- /**
175
- * Return the current theme value (may be "system").
176
- */
177
- export function getTheme(): ThemeValue {
178
- return _current
132
+ // ---------------------------------------------------------------------------
133
+ // MutationObserver pick up surface elements mounted/changed post-load
134
+ // ---------------------------------------------------------------------------
135
+
136
+ let _observerInstalled = false
137
+
138
+ function _installSurfaceObserver(): void {
139
+ if (_observerInstalled) return
140
+ if (typeof document === 'undefined' || typeof MutationObserver === 'undefined') return
141
+ if (!document.body) {
142
+ // Body not ready yet — try again once it is.
143
+ document.addEventListener('DOMContentLoaded', _installSurfaceObserver, { once: true })
144
+ return
145
+ }
146
+ _observerInstalled = true
147
+
148
+ const reapply = () => _applyToDOM(_current, resolveTheme(_current))
149
+ const observer = new MutationObserver((mutations) => {
150
+ let needsReapply = false
151
+ for (const m of mutations) {
152
+ if (m.type === 'attributes' && m.attributeName === 'data-sb-surface') {
153
+ needsReapply = true; break
154
+ }
155
+ if (m.type === 'childList') {
156
+ // Only re-apply when an added node could carry/contain a surface marker.
157
+ for (const node of m.addedNodes) {
158
+ if (!(node instanceof Element)) continue
159
+ if (node.hasAttribute('data-sb-surface') ||
160
+ node.querySelector?.('[data-sb-surface]')) {
161
+ needsReapply = true; break
162
+ }
163
+ }
164
+ if (needsReapply) break
165
+ }
166
+ }
167
+ if (needsReapply) reapply()
168
+ })
169
+ observer.observe(document.body, {
170
+ childList: true,
171
+ subtree: true,
172
+ attributes: true,
173
+ attributeFilter: ['data-sb-surface'],
174
+ })
179
175
  }
180
176
 
181
- /**
182
- * Set the active theme. Updates localStorage, the DOM attribute,
183
- * and dispatches a change event.
184
- */
177
+ // ---------------------------------------------------------------------------
178
+ // Config change subscription re-derive state and re-apply when the
179
+ // themes registry changes (e.g. HMR of storyboard.config.json).
180
+ // ---------------------------------------------------------------------------
181
+
182
+ subscribeToConfig(() => {
183
+ const cfg = themesConfig()
184
+ // Re-read sync defaults for any newly-added surfaces (they get the
185
+ // config default if not present in storage).
186
+ _syncTargets = readStoredSync(cfg)
187
+ _syncStore.set(_syncTargets)
188
+ const state = snapshot(_current)
189
+ _store.set(state)
190
+ _applyToDOM(_current, state.resolved)
191
+ _dispatchEvent(_current, state.resolved)
192
+ })
193
+
194
+ // ---------------------------------------------------------------------------
195
+ // Public API
196
+ // ---------------------------------------------------------------------------
197
+
198
+ export function getTheme(): ThemeValue { return _current }
199
+
185
200
  export function setTheme(value: ThemeValue): void {
186
201
  _current = value
187
-
188
202
  if (typeof localStorage !== 'undefined') {
189
- if (value === 'system') {
190
- localStorage.removeItem(STORAGE_KEY)
191
- } else {
192
- localStorage.setItem(STORAGE_KEY, value)
193
- }
203
+ if (value === 'system') localStorage.removeItem(THEME_STORAGE_KEY)
204
+ else localStorage.setItem(THEME_STORAGE_KEY, value)
194
205
  }
195
-
196
206
  const state = snapshot(value)
197
207
  _store.set(state)
198
208
  _applyToDOM(value, state.resolved)
199
209
  _dispatchEvent(value, state.resolved)
200
210
  }
201
211
 
212
+ export const themeState: Readable<ThemeState> = { subscribe: _store.subscribe }
213
+
202
214
  /**
203
- * Readable store for the current theme state.
215
+ * Get a copy of the current per-surface sync map.
204
216
  */
205
- export const themeState: Readable<ThemeState> = { subscribe: _store.subscribe }
217
+ export function getSurfaceSync(): Record<string, boolean> { return { ..._syncTargets } }
218
+
219
+ /**
220
+ * Set the sync flag for a single surface. Persisted to localStorage.
221
+ */
222
+ export function setSurfaceSync(surfaceId: string, value: boolean): void {
223
+ _syncTargets = { ..._syncTargets, [surfaceId]: value }
224
+ _syncStore.set(_syncTargets)
225
+ if (typeof localStorage !== 'undefined') {
226
+ localStorage.setItem(SYNC_STORAGE_KEY, JSON.stringify(_syncTargets))
227
+ }
228
+ const state = snapshot(_current)
229
+ _applyToDOM(_current, state.resolved)
230
+ _dispatchEvent(_current, state.resolved)
231
+ }
232
+
233
+ export const surfaceSyncState: Readable<Record<string, boolean>> = { subscribe: _syncStore.subscribe }
234
+
235
+ /**
236
+ * Convenience accessor — what theme is the given surface currently
237
+ * resolving to?
238
+ */
239
+ export function getResolvedFor(surfaceId: string): string {
240
+ const resolved = resolveTheme(_current)
241
+ return _syncTargets[surfaceId] ? resolved : 'light'
242
+ }
206
243
 
207
244
  // ---------------------------------------------------------------------------
208
- // OS preference listener
245
+ // Backward-compat exports
246
+ // ---------------------------------------------------------------------------
247
+
248
+ /**
249
+ * Derived from the active config. Kept so existing switchers
250
+ * (ThemeMenuButton, CoreUIBar, paletteTheme) keep working until they migrate
251
+ * to the config-driven shape. Always reflects the current registry.
252
+ */
253
+ export const THEMES: ThemeOption[] = new Proxy([] as ThemeOption[], {
254
+ get(_target, prop) {
255
+ const themes = themesConfig().themes || {}
256
+ const list: ThemeOption[] = Object.entries(themes).map(([value, def]: [string, any]) => ({
257
+ value,
258
+ name: def?.label || value,
259
+ }))
260
+ // "system" is a synthetic theme — surface it at the top if not present
261
+ // and the default is system. (Matches the legacy fixed ordering.)
262
+ if (!list.some(t => t.value === 'system')) {
263
+ list.unshift({ name: 'System', value: 'system' })
264
+ }
265
+ const value = (list as any)[prop]
266
+ return typeof value === 'function' ? value.bind(list) : value
267
+ },
268
+ })
269
+
270
+ /** Deprecated alias for getSurfaceSync — typed shape kept for old call sites. */
271
+ export interface ThemeSyncTargets {
272
+ prototype: boolean
273
+ toolbar: boolean
274
+ codeBoxes: boolean
275
+ canvas: boolean
276
+ [key: string]: boolean
277
+ }
278
+ export function getThemeSyncTargets(): ThemeSyncTargets {
279
+ return getSurfaceSync() as ThemeSyncTargets
280
+ }
281
+ export function setThemeSyncTarget(target: keyof ThemeSyncTargets, value: boolean): void {
282
+ setSurfaceSync(String(target), value)
283
+ }
284
+ export const themeSyncState: Readable<ThemeSyncTargets> = {
285
+ subscribe(run) { return _syncStore.subscribe((v) => run(v as ThemeSyncTargets)) },
286
+ }
287
+
288
+ // ---------------------------------------------------------------------------
289
+ // OS preference + cross-frame storage listeners
209
290
  // ---------------------------------------------------------------------------
210
291
 
211
292
  if (typeof window !== 'undefined') {
@@ -219,23 +300,20 @@ if (typeof window !== 'undefined') {
219
300
  _dispatchEvent('system', state.resolved)
220
301
  })
221
302
 
222
- // Cross-frame sync: the native `storage` event fires in OTHER windows
223
- // when localStorage is mutated. Prototype iframes embed the same app on
224
- // the same origin, so when the parent toolbar flips the theme we must
225
- // pick that up here and re-apply the DOM attributes / dispatch the
226
- // theme-changed event. Without this, Tailwind utilities (and Primer
227
- // CSS vars) inside the iframe stay stuck on the load-time theme.
303
+ // Cross-frame sync via the storage event: prototype iframes share the
304
+ // same origin as the parent, so a localStorage write from the toolbar
305
+ // theme switcher needs to repaint the iframe too.
228
306
  window.addEventListener('storage', (e) => {
229
307
  if (e.storageArea !== localStorage) return
230
- if (e.key === STORAGE_KEY || e.key === null) {
231
- const next = readStoredTheme()
308
+ if (e.key === THEME_STORAGE_KEY || e.key === null) {
309
+ const next = readStoredTheme(themesConfig())
232
310
  _current = next
233
311
  const state = snapshot(next)
234
312
  _store.set(state)
235
313
  _applyToDOM(next, state.resolved)
236
314
  _dispatchEvent(next, state.resolved)
237
315
  } else if (e.key === SYNC_STORAGE_KEY) {
238
- _syncTargets = readStoredSync()
316
+ _syncTargets = readStoredSync(themesConfig())
239
317
  _syncStore.set(_syncTargets)
240
318
  const state = snapshot(_current)
241
319
  _applyToDOM(_current, state.resolved)
@@ -245,71 +323,8 @@ if (typeof window !== 'undefined') {
245
323
  }
246
324
 
247
325
  // ---------------------------------------------------------------------------
248
- // Theme sync targets
249
- // ---------------------------------------------------------------------------
250
-
251
- /** Which parts of the UI follow the global theme */
252
- export interface ThemeSyncTargets {
253
- prototype: boolean
254
- toolbar: boolean
255
- codeBoxes: boolean
256
- canvas: boolean
257
- }
258
-
259
- const SYNC_STORAGE_KEY = 'sb-theme-sync'
260
-
261
- const DEFAULT_SYNC: ThemeSyncTargets = {
262
- prototype: true,
263
- toolbar: false,
264
- codeBoxes: true,
265
- canvas: true,
266
- }
267
-
268
- function readStoredSync(): ThemeSyncTargets {
269
- if (typeof localStorage === 'undefined') return { ...DEFAULT_SYNC }
270
- try {
271
- const raw = localStorage.getItem(SYNC_STORAGE_KEY)
272
- if (!raw) return { ...DEFAULT_SYNC }
273
- return { ...DEFAULT_SYNC, ...JSON.parse(raw) }
274
- } catch {
275
- return { ...DEFAULT_SYNC }
276
- }
277
- }
278
-
279
- let _syncTargets: ThemeSyncTargets = readStoredSync()
280
- const _syncStore = writable<ThemeSyncTargets>(_syncTargets)
281
-
282
- /**
283
- * Get the current theme sync targets.
284
- */
285
- export function getThemeSyncTargets(): ThemeSyncTargets {
286
- return { ..._syncTargets }
287
- }
288
-
289
- /**
290
- * Set a theme sync target. Persists to localStorage.
291
- */
292
- export function setThemeSyncTarget(target: keyof ThemeSyncTargets, value: boolean): void {
293
- _syncTargets = { ..._syncTargets, [target]: value }
294
- _syncStore.set(_syncTargets)
295
-
296
- if (typeof localStorage !== 'undefined') {
297
- localStorage.setItem(SYNC_STORAGE_KEY, JSON.stringify(_syncTargets))
298
- }
299
-
300
- // Re-apply DOM attributes so toolbar/codebox can react
301
- const state = snapshot(_current)
302
- _applyToDOM(_current, state.resolved)
303
- _dispatchEvent(_current, state.resolved)
304
- }
305
-
306
- /**
307
- * Readable store for sync target state.
308
- */
309
- export const themeSyncState: Readable<ThemeSyncTargets> = { subscribe: _syncStore.subscribe }
310
-
311
- // ---------------------------------------------------------------------------
312
- // Boot — apply the stored theme immediately on import
326
+ // Boot install observer and paint immediately
313
327
  // ---------------------------------------------------------------------------
314
328
 
329
+ _installSurfaceObserver()
315
330
  _applyToDOM(_current, resolveTheme(_current))
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Generate the contents of `themes.generated.css` from the project's
3
+ * themes config. Emits only the Tailwind scaffolding needed at compile
4
+ * time — the `@custom-variant dark (…)` selector list — so the `dark:`
5
+ * utility variant is aware of every configured surface attribute
6
+ * (`data-sb-<surface>-theme`) and the legacy `<html>` attribute set.
7
+ *
8
+ * No CSS-variable values are emitted here. Components own their own
9
+ * variable mappings in tailwind.css / core-ui-colors.css / per-component CSS.
10
+ *
11
+ * Theme ids whose key starts with "dark" trip the variant via prefix match
12
+ * (`[data-sb-…-theme^="dark"]`), so new dark variants like "dark_midnight"
13
+ * just work without changing this rule.
14
+ *
15
+ * This module is consumed by:
16
+ * - packages/storyboard/src/internals/vite/data-plugin.js — writes the
17
+ * file on dev boot and on storyboard.config.json changes.
18
+ * - packages/storyboard/scripts/gen-themes-css.mjs — pre-step for the
19
+ * standalone `tailwindcss` CLI invoked by `build:css`.
20
+ */
21
+
22
+ export const GENERATED_THEMES_CSS_FILENAME = 'themes.generated.css'
23
+
24
+ export const GENERATED_BANNER =
25
+ '/* Auto-generated by @dfosco/storyboard data plugin — do not edit. */'
26
+
27
+ const LEGACY_SURFACE_ATTRS = [
28
+ 'data-sb-toolbar-theme',
29
+ 'data-sb-code-theme',
30
+ 'data-sb-canvas-theme',
31
+ ]
32
+
33
+ export function generateThemesCss(themesCfg) {
34
+ const surfaces = Object.keys(themesCfg?.surfaces || {})
35
+ const selectors = ['[data-sb-theme^="dark"]', '[data-sb-theme^="dark"] *']
36
+
37
+ for (const id of surfaces) {
38
+ const attr = `data-sb-${id.toLowerCase()}-theme`
39
+ if (!selectors.includes(`[${attr}^="dark"]`)) {
40
+ selectors.push(`[${attr}^="dark"]`, `[${attr}^="dark"] *`)
41
+ }
42
+ }
43
+
44
+ // Legacy <html> attrs — kept in the selector list even when the
45
+ // corresponding surface is removed from config so existing CSS that
46
+ // selects on the legacy attribute names continues to participate in
47
+ // the `dark:` variant.
48
+ for (const attr of LEGACY_SURFACE_ATTRS) {
49
+ if (!selectors.includes(`[${attr}^="dark"]`)) {
50
+ selectors.push(`[${attr}^="dark"]`, `[${attr}^="dark"] *`)
51
+ }
52
+ }
53
+
54
+ return [
55
+ GENERATED_BANNER,
56
+ '@custom-variant dark (',
57
+ ' &:where(',
58
+ ' ' + selectors.join(',\n '),
59
+ ' )',
60
+ ');',
61
+ '',
62
+ ].join('\n')
63
+ }
@@ -1,5 +1,6 @@
1
1
  @import "tailwindcss";
2
2
  @import "tw-animate-css";
3
+ @import "./themes.generated.css";
3
4
  @source "../**/*.{jsx,js,ts}";
4
5
 
5
6
  @font-face {
@@ -22,8 +23,6 @@
22
23
  font-display: swap;
23
24
  }
24
25
 
25
- @custom-variant dark (&:where([data-sb-theme^="dark"], [data-sb-theme^="dark"] *));
26
-
27
26
  @theme {
28
27
  --color-background: hsl(0 0% 100%);
29
28
  --color-foreground: hsl(222.2 84% 4.9%);
@@ -176,9 +175,18 @@
176
175
  }
177
176
  }
178
177
 
178
+ /* Superellipse corners via the CSS Houdini Paint API.
179
+ * Only Chromium browsers ship paint() today — Firefox and Safari fall back
180
+ * to a regular border-radius via the @supports query below. Override the
181
+ * fallback radius per-element with --sb--smooth-corners-fallback if needed. */
179
182
  @utility smooth-corners {
180
- mask-image: paint(smooth-corners);
181
- -webkit-mask-image: paint(smooth-corners);
183
+ border-radius: var(--sb--smooth-corners-fallback, 0.75rem);
184
+
185
+ @supports (mask-image: paint(smooth-corners)) {
186
+ border-radius: 0;
187
+ mask-image: paint(smooth-corners);
188
+ -webkit-mask-image: paint(smooth-corners);
189
+ }
182
190
  }
183
191
 
184
192
  @layer base {
@@ -0,0 +1,30 @@
1
+ /**
2
+ * workspace-chrome.css — CSS dependencies for storyboard's own workspace
3
+ * UI (Workspace, Viewfinder, CanvasPage, BranchBar, CoreUIBar). Imported
4
+ * by `src/library/index.jsx` so the workspace SPA loads them.
5
+ *
6
+ * Prototypes and stories do NOT load this stylesheet — their CSS chain
7
+ * is owned by the consumer's `src/_prototype.jsx` / `src/_story.jsx`.
8
+ *
9
+ * Sources:
10
+ * - @primer/primitives — supplies the --fgColor-*, --bgColor-*,
11
+ * --borderColor-*, and theme attr selectors that the ~200 CSS module
12
+ * references across the workspace surfaces depend on.
13
+ */
14
+ @import '@primer/primitives/dist/css/base/size/size.css';
15
+ @import '@primer/primitives/dist/css/base/typography/typography.css';
16
+ @import '@primer/primitives/dist/css/base/motion/motion.css';
17
+ @import '@primer/primitives/dist/css/functional/size/border.css';
18
+ @import '@primer/primitives/dist/css/functional/size/breakpoints.css';
19
+ @import '@primer/primitives/dist/css/functional/size/size-coarse.css';
20
+ @import '@primer/primitives/dist/css/functional/size/size-fine.css';
21
+ @import '@primer/primitives/dist/css/functional/size/size.css';
22
+ @import '@primer/primitives/dist/css/functional/size/viewport.css';
23
+ @import '@primer/primitives/dist/css/functional/typography/typography.css';
24
+
25
+ @import '@primer/primitives/dist/css/functional/themes/light.css';
26
+ @import '@primer/primitives/dist/css/functional/themes/light-colorblind.css';
27
+ @import '@primer/primitives/dist/css/functional/themes/dark.css';
28
+ @import '@primer/primitives/dist/css/functional/themes/dark-colorblind.css';
29
+ @import '@primer/primitives/dist/css/functional/themes/dark-high-contrast.css';
30
+ @import '@primer/primitives/dist/css/functional/themes/dark-dimmed.css';