@dfosco/storyboard-core 3.4.0 → 3.6.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.
@@ -5,11 +5,9 @@
5
5
  * (javascript, typescript, xml for JSX), producing small bundles with
6
6
  * no WASM dependencies.
7
7
  *
8
- * Theme is configurable via toolbar.config.json's `highlighting` key:
9
- * { "highlighting": { "dark": "github-dark-dimmed", "light": "github" } }
10
- *
11
- * Returns an adapter object matching the codeToHtml() call signature
12
- * used by InspectorPanel.svelte.
8
+ * Theme is resolved at render time via toolbar.config.json's
9
+ * `highlighting` key and the theme sync settings. Colors are applied
10
+ * as inline styles — no global CSS injection, no theme conflicts.
13
11
  */
14
12
 
15
13
  import hljs from 'highlight.js/lib/core'
@@ -21,70 +19,174 @@ import { getToolbarConfig } from '../toolbarConfigStore.js'
21
19
  hljs.registerLanguage('javascript', javascript)
22
20
  hljs.registerLanguage('typescript', typescript)
23
21
  hljs.registerLanguage('xml', xml)
24
- // Aliases for JSX/TSX — highlight.js uses xml for the JSX part,
25
- // but we map these to typescript which handles JSX well enough
26
22
  hljs.registerLanguage('jsx', javascript)
27
23
  hljs.registerLanguage('tsx', typescript)
28
24
 
29
- /** Map of highlight.js theme name → loaded CSS text (cached). */
30
- const _loadedThemes = new Map()
25
+ // ---------------------------------------------------------------------------
26
+ // Color palettes inline styles, no external CSS needed
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const THEMES = {
30
+ 'github-dark-dimmed': {
31
+ bg: '#22272e',
32
+ fg: '#adbac7',
33
+ headerBg: '#2d333b',
34
+ headerFg: '#768390',
35
+ border: '#373e47',
36
+ lineHighlight: 'rgba(99, 110, 123, 0.15)',
37
+ linkHover: '#adbac7',
38
+ keyword: '#f47067',
39
+ string: '#96d0ff',
40
+ number: '#6cb6ff',
41
+ comment: '#768390',
42
+ function: '#dcbdfb',
43
+ title: '#dcbdfb',
44
+ built_in: '#6cb6ff',
45
+ literal: '#6cb6ff',
46
+ type: '#6cb6ff',
47
+ attr: '#6cb6ff',
48
+ tag: '#8ddb8c',
49
+ name: '#8ddb8c',
50
+ attribute: '#6cb6ff',
51
+ variable: '#f69d50',
52
+ 'template-variable': '#f69d50',
53
+ params: '#adbac7',
54
+ meta: '#768390',
55
+ regexp: '#96d0ff',
56
+ symbol: '#6cb6ff',
57
+ operator: '#adbac7',
58
+ punctuation: '#adbac7',
59
+ selector: '#8ddb8c',
60
+ property: '#6cb6ff',
61
+ },
62
+ 'github-dark': {
63
+ bg: '#0d1117',
64
+ fg: '#e6edf3',
65
+ headerBg: '#161b22',
66
+ headerFg: '#8b949e',
67
+ border: '#30363d',
68
+ lineHighlight: 'rgba(110, 118, 129, 0.15)',
69
+ linkHover: '#c9d1d9',
70
+ keyword: '#ff7b72',
71
+ string: '#a5d6ff',
72
+ number: '#79c0ff',
73
+ comment: '#8b949e',
74
+ function: '#d2a8ff',
75
+ title: '#d2a8ff',
76
+ built_in: '#79c0ff',
77
+ literal: '#79c0ff',
78
+ type: '#79c0ff',
79
+ attr: '#79c0ff',
80
+ tag: '#7ee787',
81
+ name: '#7ee787',
82
+ attribute: '#79c0ff',
83
+ variable: '#ffa657',
84
+ 'template-variable': '#ffa657',
85
+ params: '#e6edf3',
86
+ meta: '#8b949e',
87
+ regexp: '#a5d6ff',
88
+ symbol: '#79c0ff',
89
+ operator: '#e6edf3',
90
+ punctuation: '#e6edf3',
91
+ selector: '#7ee787',
92
+ property: '#79c0ff',
93
+ },
94
+ github: {
95
+ bg: '#ffffff',
96
+ fg: '#1f2328',
97
+ headerBg: '#f6f8fa',
98
+ headerFg: '#656d76',
99
+ border: '#d1d9e0',
100
+ lineHighlight: 'rgba(234, 179, 8, 0.12)',
101
+ linkHover: '#1f2328',
102
+ keyword: '#cf222e',
103
+ string: '#0a3069',
104
+ number: '#0550ae',
105
+ comment: '#6e7781',
106
+ function: '#8250df',
107
+ title: '#8250df',
108
+ built_in: '#0550ae',
109
+ literal: '#0550ae',
110
+ type: '#0550ae',
111
+ attr: '#0550ae',
112
+ tag: '#116329',
113
+ name: '#116329',
114
+ attribute: '#0550ae',
115
+ variable: '#953800',
116
+ 'template-variable': '#953800',
117
+ params: '#1f2328',
118
+ meta: '#6e7781',
119
+ regexp: '#0a3069',
120
+ symbol: '#0550ae',
121
+ operator: '#1f2328',
122
+ punctuation: '#1f2328',
123
+ selector: '#116329',
124
+ property: '#0550ae',
125
+ },
126
+ }
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // Theme resolution
130
+ // ---------------------------------------------------------------------------
31
131
 
32
132
  /**
33
- * Get the highlight.js theme name for the current context.
34
- * Reads highlighting config (dark/light groups) from toolbar config.
35
- * Uses theme sync targets to decide whether to follow global theme.
36
- * When code boxes are not synced (default), always uses the dark theme.
133
+ * Resolve the current theme ID based on page theme and config.
134
+ * Follows code-box theme (data-sb-code-theme attribute).
37
135
  */
38
- function getThemeName() {
39
- const config = getToolbarConfig()
40
- const highlighting = config?.highlighting || {}
41
- const darkTheme = highlighting.dark || 'github-dark-dimmed'
42
- const lightTheme = highlighting.light || 'github'
43
-
44
- // Check if code boxes should follow the global theme
45
- let codeBoxesSynced = false
46
- if (typeof localStorage !== 'undefined') {
47
- try {
48
- const raw = localStorage.getItem('sb-theme-sync')
49
- if (raw) codeBoxesSynced = JSON.parse(raw).codeBoxes === true
50
- } catch { /* ignore malformed localStorage */ }
136
+ function normalizeThemeId(requested, mode) {
137
+ const fallback = mode === 'light' ? 'github' : 'github-dark-dimmed'
138
+ if (!requested || typeof requested !== 'string') return fallback
139
+ if (THEMES[requested]) return requested
140
+
141
+ const key = requested.trim().toLowerCase().replace(/[\s_]+/g, '-')
142
+ const aliases = {
143
+ github: 'github',
144
+ 'github-light': 'github',
145
+ light: 'github',
146
+ 'night-owl-light': 'github',
147
+ 'github-dark': 'github-dark',
148
+ dark: 'github-dark-dimmed',
149
+ 'dark-dimmed': 'github-dark-dimmed',
150
+ 'github-dark-dimmed': 'github-dark-dimmed',
151
+ 'night-owl': 'github-dark-dimmed',
51
152
  }
52
153
 
53
- // When not synced, always use dark theme for code boxes
54
- if (!codeBoxesSynced) return darkTheme
154
+ const resolved = aliases[key]
155
+ if (resolved && THEMES[resolved]) return resolved
156
+ return fallback
157
+ }
55
158
 
56
- // When synced, follow the current resolved theme
57
- const sbTheme = typeof document !== 'undefined'
58
- ? document.documentElement.getAttribute('data-sb-theme') || 'dark'
159
+ /**
160
+ * Resolve the current theme ID based on page theme and config.
161
+ * Always follows the page theme (data-sb-theme attribute).
162
+ */
163
+ function resolveThemeId() {
164
+ const config = getToolbarConfig()
165
+ const highlighting = config?.highlighting || {}
166
+ const darkTheme = normalizeThemeId(highlighting.dark, 'dark')
167
+ const lightTheme = normalizeThemeId(highlighting.light, 'light')
168
+
169
+ const codeTheme = typeof document !== 'undefined'
170
+ ? document.documentElement.getAttribute('data-sb-code-theme') || 'light'
59
171
  : 'dark'
60
172
 
61
- return sbTheme.startsWith('dark') ? darkTheme : lightTheme
173
+ return codeTheme.startsWith('dark') ? darkTheme : lightTheme
62
174
  }
63
175
 
64
176
  /**
65
- * Ensure a highlight.js theme CSS is loaded into the document.
66
- * Dynamically imports the CSS file and injects a <style> tag.
177
+ * Get the color palette for the current theme.
178
+ * Falls back to github-dark-dimmed for unknown theme names.
179
+ * Exported so InspectorPanel can use it for header/container colors.
67
180
  */
68
- async function ensureThemeLoaded(themeName) {
69
- if (_loadedThemes.has(themeName)) return
70
- try {
71
- // Dynamic import of CSS — Vite handles this
72
- await import(/* @vite-ignore */ `highlight.js/styles/${themeName}.css`)
73
- _loadedThemes.set(themeName, true)
74
- } catch {
75
- // Fallback: try github-dark-dimmed if the requested theme doesn't exist
76
- if (themeName !== 'github-dark-dimmed') {
77
- try {
78
- await import('highlight.js/styles/github-dark-dimmed.css')
79
- _loadedThemes.set(themeName, true)
80
- } catch { /* ignore */ }
81
- }
82
- }
181
+ export function getColors() {
182
+ const id = resolveThemeId()
183
+ return THEMES[id] || THEMES['github-dark-dimmed']
83
184
  }
84
185
 
85
- /**
86
- * Escape HTML entities in a string.
87
- */
186
+ // ---------------------------------------------------------------------------
187
+ // HTML generation
188
+ // ---------------------------------------------------------------------------
189
+
88
190
  function escapeHtml(str) {
89
191
  return str
90
192
  .replace(/&/g, '&amp;')
@@ -93,36 +195,46 @@ function escapeHtml(str) {
93
195
  .replace(/"/g, '&quot;')
94
196
  }
95
197
 
198
+ /**
199
+ * Convert highlight.js class-based spans to inline-styled spans.
200
+ * highlight.js emits `<span class="hljs-keyword">` etc.
201
+ * We replace each with `<span style="color:...">` using the palette.
202
+ */
203
+ function applyInlineColors(html, colors) {
204
+ return html.replace(
205
+ /<span class="hljs-([^"]+)">/g,
206
+ (_, cls) => {
207
+ const color = colors[cls] || colors.fg
208
+ return `<span style="color:${color}">`
209
+ }
210
+ )
211
+ }
212
+
213
+ // ---------------------------------------------------------------------------
214
+ // Public API
215
+ // ---------------------------------------------------------------------------
216
+
96
217
  /**
97
218
  * Create the inspector highlighter.
98
- * Returns an object with codeToHtml() matching the Shiki API shape.
99
- *
100
- * @returns {Promise<{ codeToHtml: (code: string, options: { lang?: string, theme?: string, decorations?: Array }) => string }>}
219
+ * Returns an object with codeToHtml() matching the Shiki-compatible API.
101
220
  */
102
221
  export async function createInspectorHighlighter() {
103
- const themeName = getThemeName()
104
- await ensureThemeLoaded(themeName)
105
-
106
222
  return {
107
223
  /**
108
- * Highlight code and return HTML string.
224
+ * Highlight code and return HTML string with inline styles.
109
225
  *
110
226
  * @param {string} code - Source code to highlight
111
227
  * @param {object} options
112
228
  * @param {string} [options.lang] - Language identifier
113
- * @param {string} [options.theme] - Ignored (theme comes from config)
229
+ * @param {string} [options.theme] - Ignored (theme resolved from config)
114
230
  * @param {Array<{ start: { line: number }, end: { line: number }, properties: { class: string } }>} [options.decorations]
115
231
  * @returns {string} HTML string with highlighted code
116
232
  */
117
233
  codeToHtml(code, options = {}) {
118
234
  const lang = options.lang || 'javascript'
119
235
  const decorations = options.decorations || []
236
+ const colors = getColors()
120
237
 
121
- // Ensure current theme is loaded (non-blocking, already cached after first call)
122
- const currentTheme = getThemeName()
123
- ensureThemeLoaded(currentTheme)
124
-
125
- // Highlight the code
126
238
  let highlighted
127
239
  try {
128
240
  highlighted = hljs.highlight(code, { language: lang, ignoreIllegals: true }).value
@@ -130,9 +242,10 @@ export async function createInspectorHighlighter() {
130
242
  highlighted = escapeHtml(code)
131
243
  }
132
244
 
133
- // Split into lines and wrap each in a <span class="line">
245
+ // Convert class-based spans to inline styles
246
+ highlighted = applyInlineColors(highlighted, colors)
247
+
134
248
  const lines = highlighted.split('\n')
135
- // Build a set of highlighted line numbers (0-indexed from decorations)
136
249
  const highlightedLines = new Set()
137
250
  for (const dec of decorations) {
138
251
  if (dec.start && dec.properties?.class) {
@@ -148,7 +261,7 @@ export async function createInspectorHighlighter() {
148
261
  return `<span class="${classes.join(' ')}">${line}</span>`
149
262
  }).join('\n')
150
263
 
151
- return `<pre class="hljs"><code>${wrappedLines}</code></pre>`
264
+ return `<pre style="background:${colors.bg};color:${colors.fg};margin:0;padding:0;overflow-x:auto"><code>${wrappedLines}</code></pre>`
152
265
  },
153
266
  }
154
267
  }
package/src/sidepanel.css CHANGED
@@ -34,7 +34,7 @@ html:not(.sb-sidepanel-open) > body > #root {
34
34
 
35
35
  /* Push the CoreUIBar (fixed bottom-right) — side mode */
36
36
  html.sb-sidepanel-open:not(.sb-sidepanel-bottom) [data-core-ui-bar] {
37
- right: calc(var(--sidepanel-width) + 24px);
37
+ right: calc(var(--sidepanel-width) + 24px) !important;
38
38
  transition: right 0.25s ease;
39
39
  }
40
40
 
@@ -76,7 +76,7 @@ html.sb-sidepanel-open.sb-sidepanel-bottom.storyboard-mode-inspect > body > #roo
76
76
 
77
77
  /* Push the CoreUIBar up — bottom mode */
78
78
  html.sb-sidepanel-open.sb-sidepanel-bottom [data-core-ui-bar] {
79
- bottom: calc(var(--sidepanel-height) + 24px);
79
+ bottom: calc(var(--sidepanel-height) + 24px) !important;
80
80
  transition: bottom 0.25s ease;
81
81
  }
82
82
 
@@ -92,35 +92,52 @@ function _applyToDOM(theme: ThemeValue, resolved: string): void {
92
92
  if (typeof document === 'undefined') return
93
93
  const el = document.documentElement
94
94
 
95
- // Internal attribute
96
- el.setAttribute('data-sb-theme', resolved)
95
+ // Per-target resolved themes
96
+ const prototypeTheme = _syncTargets.prototype ? resolved : 'light'
97
+ const toolbarTheme = _syncTargets.toolbar ? resolved : 'light'
98
+ const codeTheme = _syncTargets.codeBoxes ? resolved : 'light'
99
+
100
+ // Internal attributes
101
+ el.setAttribute('data-sb-theme', prototypeTheme)
102
+ el.setAttribute('data-sb-code-theme', codeTheme)
97
103
 
98
104
  // Toolbar theme — follows global theme when synced, stays light otherwise
99
- const toolbarTheme = _syncTargets.toolbar ? resolved : 'light'
100
105
  el.setAttribute('data-sb-toolbar-theme', toolbarTheme)
101
106
 
102
107
  // Primer CSS attributes — these drive @primer/react ThemeProvider and
103
108
  // Primer CSS custom-property layers without needing React state updates.
104
- if (theme === 'system') {
109
+ if (theme === 'system' && _syncTargets.prototype) {
105
110
  el.setAttribute('data-color-mode', 'auto')
106
111
  el.setAttribute('data-light-theme', 'light')
107
112
  el.setAttribute('data-dark-theme', 'dark')
108
- } else if (resolved.startsWith('dark')) {
113
+ } else if (prototypeTheme.startsWith('dark')) {
109
114
  el.setAttribute('data-color-mode', 'dark')
110
- el.setAttribute('data-dark-theme', resolved)
115
+ el.setAttribute('data-dark-theme', prototypeTheme)
111
116
  el.setAttribute('data-light-theme', 'light')
112
117
  } else {
113
118
  el.setAttribute('data-color-mode', 'light')
114
- el.setAttribute('data-light-theme', resolved)
119
+ el.setAttribute('data-light-theme', prototypeTheme)
115
120
  el.setAttribute('data-dark-theme', 'dark')
116
121
  }
117
122
  }
118
123
 
119
124
  function _dispatchEvent(theme: ThemeValue, resolved: string): void {
120
125
  if (typeof document === 'undefined') return
126
+ const prototypeTheme = _syncTargets.prototype ? theme : 'light'
127
+ const prototypeResolved = _syncTargets.prototype ? resolved : 'light'
128
+ const toolbarResolved = _syncTargets.toolbar ? resolved : 'light'
129
+ const codeResolved = _syncTargets.codeBoxes ? resolved : 'light'
130
+
121
131
  document.dispatchEvent(
122
132
  new CustomEvent('storyboard:theme:changed', {
123
- detail: { theme, resolved },
133
+ detail: {
134
+ theme,
135
+ resolved,
136
+ prototypeTheme,
137
+ prototypeResolved,
138
+ toolbarResolved,
139
+ codeResolved,
140
+ },
124
141
  }),
125
142
  )
126
143
  }
@@ -183,7 +200,7 @@ const SYNC_STORAGE_KEY = 'sb-theme-sync'
183
200
  const DEFAULT_SYNC: ThemeSyncTargets = {
184
201
  prototype: true,
185
202
  toolbar: false,
186
- codeBoxes: false,
203
+ codeBoxes: true,
187
204
  }
188
205
 
189
206
  function readStoredSync(): ThemeSyncTargets {
@@ -18,7 +18,11 @@ export async function handler(ctx) {
18
18
  if (base && path.startsWith(base)) path = path.slice(base.length)
19
19
  path = path.replace(/\/+$/, '') || '/'
20
20
  const segments = path.split('/').filter(Boolean)
21
- const proto = segments[0] || null
21
+
22
+ // Detect and preserve branch-- segment on deployed branch builds
23
+ const branchSegment = (segments[0] && segments[0].startsWith('branch--')) ? segments[0] : null
24
+ const protoIdx = branchSegment ? 1 : 0
25
+ const proto = segments[protoIdx] || null
22
26
  if (!proto) return []
23
27
 
24
28
  const params = new URLSearchParams(window.location.search)
@@ -46,7 +50,13 @@ export async function handler(ctx) {
46
50
  label: meta?.title || f.name,
47
51
  type: 'radio',
48
52
  active: f.key === active,
49
- execute: () => { window.location.href = vf.resolveFlowRoute(f.key) },
53
+ execute: () => {
54
+ let url = vf.resolveFlowRoute(f.key)
55
+ // Re-apply basePath and branch-- prefix so deployed branch builds stay on the correct path
56
+ const prefix = (base || '') + (branchSegment ? `/${branchSegment}` : '')
57
+ if (prefix) url = prefix + url
58
+ window.location.href = url
59
+ },
50
60
  }
51
61
  })
52
62
  },
@@ -16,23 +16,11 @@
16
16
  },
17
17
 
18
18
  "highlighting": {
19
- "dark": "night owl",
20
- "light": "night owl light"
19
+ "dark": "github-dark-dimmed",
20
+ "light": "github"
21
21
  },
22
22
 
23
23
  "tools": {
24
- "inspector": {
25
- "ariaLabel": "Inspect components",
26
- "icon": "iconoir/square-dashed",
27
- "render": "sidepanel",
28
- "surface": "main-toolbar",
29
- "sidepanel": "inspector",
30
- "handler": "core:inspector",
31
- "modes": ["*"],
32
- "excludeRoutes": ["^/$", "/viewfinder", "/canvas/"],
33
- "meta": { "strokeWeight": 2, "scale": 1.1 },
34
- "shortcut": { "key": "i", "label": "⌘I" }
35
- },
36
24
  "flows": {
37
25
  "label": "Flows",
38
26
  "ariaLabel": "Switch flow",
@@ -42,7 +30,7 @@
42
30
  "modes": ["*"],
43
31
  "menuWidth": "260px",
44
32
  "handler": "core:flows"
45
- },
33
+ },
46
34
  "theme": {
47
35
  "label": "Theme",
48
36
  "ariaLabel": "Switch theme",
@@ -63,11 +51,18 @@
63
51
  "modes": ["*"],
64
52
  "excludeRoutes": ["^/$", "/viewfinder"]
65
53
  },
66
- "sep-actions": {
67
- "render": "separator",
68
- "surface": "command-list",
69
- "modes": ["*"]
70
- },
54
+ "inspector": {
55
+ "ariaLabel": "Inspect components",
56
+ "icon": "iconoir/square-dashed",
57
+ "render": "sidepanel",
58
+ "surface": "main-toolbar",
59
+ "sidepanel": "inspector",
60
+ "handler": "core:inspector",
61
+ "modes": ["*"],
62
+ "excludeRoutes": ["^/$", "/viewfinder", "/canvas/"],
63
+ "meta": { "strokeWeight": 2, "scale": 1.1 },
64
+ "shortcut": { "key": "i", "label": "⌘I" }
65
+ },
71
66
  "create": {
72
67
  "label": "Create",
73
68
  "ariaLabel": "Create",