@dfosco/storyboard-core 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/storyboard-ui.css +1 -0
- package/dist/storyboard-ui.js +26298 -0
- package/dist/storyboard-ui.js.map +1 -0
- package/dist/tailwind.css +1 -1
- package/package.json +24 -18
- package/scaffold/manifest.json +35 -0
- package/scaffold/scripts/link.sh +26 -0
- package/scaffold/scripts/unlink.sh +10 -0
- package/scaffold/skills/create/SKILL.md +501 -0
- package/scaffold/skills/storyboard/SKILL.md +360 -0
- package/scaffold/skills/update-storyboard/SKILL.md +16 -0
- package/scaffold/skills/update-storyboard/update-storyboard-packages.sh +26 -0
- package/scaffold/skills/vitest/GENERATION.md +5 -0
- package/scaffold/skills/vitest/SKILL.md +52 -0
- package/scaffold/skills/vitest/references/advanced-environments.md +264 -0
- package/scaffold/skills/vitest/references/advanced-projects.md +300 -0
- package/scaffold/skills/vitest/references/advanced-type-testing.md +237 -0
- package/scaffold/skills/vitest/references/advanced-vi.md +249 -0
- package/scaffold/skills/vitest/references/core-cli.md +166 -0
- package/scaffold/skills/vitest/references/core-config.md +174 -0
- package/scaffold/skills/vitest/references/core-describe.md +193 -0
- package/scaffold/skills/vitest/references/core-expect.md +219 -0
- package/scaffold/skills/vitest/references/core-hooks.md +244 -0
- package/scaffold/skills/vitest/references/core-test-api.md +233 -0
- package/scaffold/skills/vitest/references/features-concurrency.md +250 -0
- package/scaffold/skills/vitest/references/features-context.md +238 -0
- package/scaffold/skills/vitest/references/features-coverage.md +207 -0
- package/scaffold/skills/vitest/references/features-filtering.md +211 -0
- package/scaffold/skills/vitest/references/features-mocking.md +265 -0
- package/scaffold/skills/vitest/references/features-snapshots.md +207 -0
- package/scaffold/skills/worktree/SKILL.md +51 -0
- package/scaffold/storyboard.config.json +26 -0
- package/scaffold/svelte.config.js +1 -0
- package/scaffold/toolbar.config.json +4 -0
- package/src/ActionMenuButton.svelte +1 -1
- package/src/CanvasCreateMenu.svelte +1 -1
- package/src/CoreUIBar.svelte +20 -9
- package/src/CreateMenuButton.svelte +1 -1
- package/src/InspectorPanel.svelte +144 -49
- package/src/SidePanel.svelte +10 -10
- package/src/commandActions.js +1 -1
- package/src/comments/index.js +0 -3
- package/src/devtools.js +4 -1
- package/src/index.js +5 -2
- package/src/inspector/highlighter.js +3 -4
- package/src/lib/components/ui/dropdown-menu/dropdown-menu-content.svelte +1 -1
- package/src/lib/components/ui/trigger-button/trigger-button.svelte +8 -4
- package/src/mountStoryboardCore.js +223 -0
- package/src/scaffold.js +100 -0
- package/src/stores/themeStore.ts +29 -8
- package/src/styles/tailwind.css +16 -0
- package/src/svelte-plugin-ui/components/Viewfinder.svelte +18 -0
- package/src/ui-entry.js +30 -0
- package/src/vite/server-plugin.js +8 -24
- package/src/workshop/features/createCanvas/CreateCanvasForm.svelte +24 -6
- package/src/workshop/features/createFlow/CreateFlowForm.svelte +1 -1
- package/src/workshop/features/createFlow/index.js +0 -1
- package/src/workshop/features/createPrototype/CreatePrototypeForm.svelte +1 -1
- package/src/workshop/features/createPrototype/index.js +0 -1
- /package/{core-ui.config.json → toolbar.config.json} +0 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* mountStoryboardCore — single entry point for consumer apps.
|
|
3
|
+
*
|
|
4
|
+
* Initializes all storyboard systems (URL state, history, comments, devtools)
|
|
5
|
+
* and mounts the compiled Svelte UI. Consumers call this once at app startup.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { mountStoryboardCore } from '@dfosco/storyboard-core'
|
|
9
|
+
* import storyboardConfig from '../storyboard.config.json'
|
|
10
|
+
* mountStoryboardCore(storyboardConfig, { basePath: import.meta.env.BASE_URL })
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { installHideParamListener } from './interceptHideParams.js'
|
|
14
|
+
import { installHistorySync } from './hideMode.js'
|
|
15
|
+
import { installBodyClassSync } from './bodyClasses.js'
|
|
16
|
+
import { initCommentsConfig, isCommentsEnabled } from './comments/config.js'
|
|
17
|
+
import { initFeatureFlags } from './featureFlags.js'
|
|
18
|
+
import { initPlugins } from './plugins.js'
|
|
19
|
+
import { initUIConfig } from './uiConfig.js'
|
|
20
|
+
|
|
21
|
+
let _mounted = false
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Apply the saved theme to Primer CSS attributes immediately, before
|
|
25
|
+
* React or Svelte mount. This prevents a flash of wrong-theme content.
|
|
26
|
+
* Reads the same `sb-color-scheme` localStorage key used by themeStore.
|
|
27
|
+
*/
|
|
28
|
+
function applyEarlyTheme() {
|
|
29
|
+
if (typeof document === 'undefined') return
|
|
30
|
+
|
|
31
|
+
const stored =
|
|
32
|
+
typeof localStorage !== 'undefined'
|
|
33
|
+
? localStorage.getItem('sb-color-scheme')
|
|
34
|
+
: null
|
|
35
|
+
const theme = stored || 'system'
|
|
36
|
+
const el = document.documentElement
|
|
37
|
+
|
|
38
|
+
// Resolve "system" to an actual theme for data-sb-theme
|
|
39
|
+
let resolved = theme
|
|
40
|
+
if (theme === 'system') {
|
|
41
|
+
resolved =
|
|
42
|
+
typeof window !== 'undefined' &&
|
|
43
|
+
window.matchMedia('(prefers-color-scheme: dark)').matches
|
|
44
|
+
? 'dark'
|
|
45
|
+
: 'light'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
el.setAttribute('data-sb-theme', resolved)
|
|
49
|
+
|
|
50
|
+
if (theme === 'system') {
|
|
51
|
+
el.setAttribute('data-color-mode', 'auto')
|
|
52
|
+
el.setAttribute('data-light-theme', 'light')
|
|
53
|
+
el.setAttribute('data-dark-theme', 'dark')
|
|
54
|
+
} else if (resolved.startsWith('dark')) {
|
|
55
|
+
el.setAttribute('data-color-mode', 'dark')
|
|
56
|
+
el.setAttribute('data-dark-theme', resolved)
|
|
57
|
+
el.setAttribute('data-light-theme', 'light')
|
|
58
|
+
} else {
|
|
59
|
+
el.setAttribute('data-color-mode', 'light')
|
|
60
|
+
el.setAttribute('data-light-theme', resolved)
|
|
61
|
+
el.setAttribute('data-dark-theme', 'dark')
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Inject the compiled UI stylesheet if not already present.
|
|
67
|
+
*/
|
|
68
|
+
async function injectUIStyles() {
|
|
69
|
+
if (document.querySelector('[data-storyboard-ui-css]')) return
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
// Dynamic import of CSS — Vite handles this as a side-effect import.
|
|
73
|
+
// In consumer repos: loads dist/storyboard-ui.css
|
|
74
|
+
// In source repo: Vite injects component styles via HMR
|
|
75
|
+
await import('@dfosco/storyboard-core/ui-runtime/style.css')
|
|
76
|
+
} catch {
|
|
77
|
+
// Graceful fallback — CSS may already be loaded by other means
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Mount the full storyboard core system.
|
|
83
|
+
*
|
|
84
|
+
* @param {object} [config={}] - Contents of storyboard.config.json
|
|
85
|
+
* @param {object} [options={}]
|
|
86
|
+
* @param {string} [options.basePath='/'] - Base URL path (e.g. import.meta.env.BASE_URL)
|
|
87
|
+
* @param {HTMLElement} [options.container=document.body] - Where to mount devtools
|
|
88
|
+
*/
|
|
89
|
+
export async function mountStoryboardCore(config = {}, options = {}) {
|
|
90
|
+
if (_mounted) return
|
|
91
|
+
_mounted = true
|
|
92
|
+
|
|
93
|
+
const basePath = options.basePath || '/'
|
|
94
|
+
|
|
95
|
+
// Apply saved theme to DOM immediately — before Svelte/React mount
|
|
96
|
+
applyEarlyTheme()
|
|
97
|
+
|
|
98
|
+
// Initialize framework-agnostic systems
|
|
99
|
+
installHideParamListener()
|
|
100
|
+
installHistorySync()
|
|
101
|
+
installBodyClassSync()
|
|
102
|
+
|
|
103
|
+
// Initialize config-driven systems
|
|
104
|
+
if (config.featureFlags) {
|
|
105
|
+
initFeatureFlags(config.featureFlags)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (config.plugins) {
|
|
109
|
+
initPlugins(config.plugins)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (config.ui) {
|
|
113
|
+
initUIConfig(config.ui)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Initialize comments config (framework-agnostic)
|
|
117
|
+
if (config.comments) {
|
|
118
|
+
initCommentsConfig(config, { basePath })
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Inject compiled UI styles
|
|
122
|
+
injectUIStyles()
|
|
123
|
+
|
|
124
|
+
// Load and merge toolbar config.
|
|
125
|
+
// Core defaults come from toolbar.config.json (bundled).
|
|
126
|
+
// Client can provide overrides via config.toolbar or a toolbar.config.json at repo root.
|
|
127
|
+
const { deepMerge } = await import('./loader.js')
|
|
128
|
+
const defaultConfig = (await import('../toolbar.config.json')).default
|
|
129
|
+
let toolbarConfig = config.toolbar
|
|
130
|
+
? deepMerge(defaultConfig, config.toolbar)
|
|
131
|
+
: { ...defaultConfig }
|
|
132
|
+
|
|
133
|
+
// Inject repository URL from storyboard.config.json into the command menu
|
|
134
|
+
if (config.repository?.owner && config.repository?.name) {
|
|
135
|
+
const repoUrl = `https://github.com/${config.repository.owner}/${config.repository.name}`
|
|
136
|
+
const commandMenu = toolbarConfig.menus?.command
|
|
137
|
+
if (commandMenu?.actions) {
|
|
138
|
+
const repoAction = commandMenu.actions.find(a => a.id === 'core/repository')
|
|
139
|
+
if (repoAction) repoAction.url = repoUrl
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Skip all UI mounting when loaded inside a prototype embed iframe
|
|
144
|
+
const isEmbed = typeof window !== 'undefined' && new URLSearchParams(window.location.search).has('_sb_embed')
|
|
145
|
+
if (isEmbed) return
|
|
146
|
+
|
|
147
|
+
// Dynamically import the compiled UI bundle.
|
|
148
|
+
// Uses the package self-reference so resolution differs by context:
|
|
149
|
+
// Source repo: Vite alias overrides to src/ui-entry.js (source, HMR)
|
|
150
|
+
// Consumer repos: package.json exports resolve to dist/storyboard-ui.js (compiled)
|
|
151
|
+
const ui = await import('@dfosco/storyboard-core/ui-runtime')
|
|
152
|
+
|
|
153
|
+
// Mount devtools (CoreUIBar)
|
|
154
|
+
await ui.mountDevTools({
|
|
155
|
+
container: options.container,
|
|
156
|
+
basePath,
|
|
157
|
+
toolbarConfig,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// Mount comments system if configured
|
|
161
|
+
if (isCommentsEnabled()) {
|
|
162
|
+
ui.mountComments()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Show pending workshop notifications (e.g. canvas created before Vite reload)
|
|
166
|
+
showPendingNotification(basePath)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check sessionStorage for a pending workshop creation notification.
|
|
171
|
+
* Vite does a full-reload when new files are created, so the create form's
|
|
172
|
+
* success message is lost. This shows a temporary toast with the link.
|
|
173
|
+
*/
|
|
174
|
+
function showPendingNotification(basePath) {
|
|
175
|
+
const KEYS = ['sb-canvas-created', 'sb-prototype-created', 'sb-flow-created']
|
|
176
|
+
for (const key of KEYS) {
|
|
177
|
+
try {
|
|
178
|
+
const raw = sessionStorage.getItem(key)
|
|
179
|
+
if (!raw) continue
|
|
180
|
+
sessionStorage.removeItem(key)
|
|
181
|
+
const { success: message, route } = JSON.parse(raw)
|
|
182
|
+
if (!message) continue
|
|
183
|
+
showToast(message, route, basePath)
|
|
184
|
+
return
|
|
185
|
+
} catch { /* ignore malformed session entry */ }
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function showToast(message, route, basePath) {
|
|
190
|
+
const toast = document.createElement('div')
|
|
191
|
+
Object.assign(toast.style, {
|
|
192
|
+
position: 'fixed',
|
|
193
|
+
bottom: '7rem',
|
|
194
|
+
right: '1.5rem',
|
|
195
|
+
zIndex: '10000',
|
|
196
|
+
padding: '0.75rem 1rem',
|
|
197
|
+
borderRadius: '0.75rem',
|
|
198
|
+
background: 'var(--color-popover, #fff)',
|
|
199
|
+
color: 'var(--color-foreground, #1e293b)',
|
|
200
|
+
fontSize: '0.8125rem',
|
|
201
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif",
|
|
202
|
+
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
|
|
203
|
+
border: '1px solid var(--color-border, #cbd5e1)',
|
|
204
|
+
display: 'flex',
|
|
205
|
+
flexDirection: 'column',
|
|
206
|
+
gap: '0.25rem',
|
|
207
|
+
opacity: '0',
|
|
208
|
+
transition: 'opacity 0.15s ease',
|
|
209
|
+
maxWidth: '280px',
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
const href = route?.startsWith('/') ? (basePath.replace(/\/$/, '') + route) : route
|
|
213
|
+
toast.innerHTML = `<span style="font-weight:500">✓ ${message.replace(/</g, '<')}</span>`
|
|
214
|
+
+ (href ? `<a href="${href}" style="color:var(--color-primary, #0969da);text-decoration:underline;font-size:0.8125rem">Open canvas</a>` : '')
|
|
215
|
+
|
|
216
|
+
document.body.appendChild(toast)
|
|
217
|
+
requestAnimationFrame(() => { toast.style.opacity = '1' })
|
|
218
|
+
|
|
219
|
+
setTimeout(() => {
|
|
220
|
+
toast.style.opacity = '0'
|
|
221
|
+
setTimeout(() => toast.remove(), 300)
|
|
222
|
+
}, 8000)
|
|
223
|
+
}
|
package/src/scaffold.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* storyboard-scaffold — sync scaffold files from @dfosco/storyboard-core.
|
|
4
|
+
*
|
|
5
|
+
* Reads scaffold/manifest.json and copies files to the consumer repo:
|
|
6
|
+
* - "scaffold" mode: only if target doesn't exist (never overwrites config)
|
|
7
|
+
* - "updateable" mode: always overwrites with latest (skills, scripts)
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* npx storyboard-scaffold
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import fs from 'node:fs'
|
|
14
|
+
import path from 'node:path'
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(new URL(import.meta.url).pathname)
|
|
17
|
+
const scaffoldRoot = path.resolve(__dirname, '..', 'scaffold')
|
|
18
|
+
const consumerRoot = process.cwd()
|
|
19
|
+
|
|
20
|
+
const manifestPath = path.join(scaffoldRoot, 'manifest.json')
|
|
21
|
+
if (!fs.existsSync(manifestPath)) {
|
|
22
|
+
console.error('❌ Could not find scaffold/manifest.json in @dfosco/storyboard-core')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'))
|
|
27
|
+
|
|
28
|
+
function copyFileSync(src, dest) {
|
|
29
|
+
const dir = path.dirname(dest)
|
|
30
|
+
if (!fs.existsSync(dir)) {
|
|
31
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
32
|
+
}
|
|
33
|
+
fs.copyFileSync(src, dest)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function copyDirSync(src, dest) {
|
|
37
|
+
if (!fs.existsSync(dest)) {
|
|
38
|
+
fs.mkdirSync(dest, { recursive: true })
|
|
39
|
+
}
|
|
40
|
+
const entries = fs.readdirSync(src, { withFileTypes: true })
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
const srcPath = path.join(src, entry.name)
|
|
43
|
+
const destPath = path.join(dest, entry.name)
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
copyDirSync(srcPath, destPath)
|
|
46
|
+
} else {
|
|
47
|
+
copyFileSync(srcPath, destPath)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let created = 0
|
|
53
|
+
let updated = 0
|
|
54
|
+
let skipped = 0
|
|
55
|
+
|
|
56
|
+
for (const file of manifest.files) {
|
|
57
|
+
const srcPath = path.join(scaffoldRoot, path.relative('scaffold', file.source))
|
|
58
|
+
const destPath = path.join(consumerRoot, file.target)
|
|
59
|
+
|
|
60
|
+
if (file.directory) {
|
|
61
|
+
if (file.mode === 'updateable') {
|
|
62
|
+
copyDirSync(srcPath, destPath)
|
|
63
|
+
updated++
|
|
64
|
+
console.log(` ✔ Updated ${file.target} (sync)`)
|
|
65
|
+
} else {
|
|
66
|
+
if (!fs.existsSync(destPath)) {
|
|
67
|
+
copyDirSync(srcPath, destPath)
|
|
68
|
+
created++
|
|
69
|
+
console.log(` ✔ Created ${file.target} (scaffold)`)
|
|
70
|
+
} else {
|
|
71
|
+
skipped++
|
|
72
|
+
console.log(` ⏭ Skipped ${file.target} (already exists)`)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (file.mode === 'scaffold') {
|
|
79
|
+
if (fs.existsSync(destPath)) {
|
|
80
|
+
skipped++
|
|
81
|
+
console.log(` ⏭ Skipped ${file.target} (already exists)`)
|
|
82
|
+
} else {
|
|
83
|
+
copyFileSync(srcPath, destPath)
|
|
84
|
+
created++
|
|
85
|
+
console.log(` ✔ Created ${file.target} (scaffold)`)
|
|
86
|
+
}
|
|
87
|
+
} else if (file.mode === 'updateable') {
|
|
88
|
+
copyFileSync(srcPath, destPath)
|
|
89
|
+
updated++
|
|
90
|
+
console.log(` ✔ Updated ${file.target} (sync)`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Make shell scripts executable
|
|
94
|
+
if (file.target.endsWith('.sh')) {
|
|
95
|
+
fs.chmodSync(destPath, 0o755)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('')
|
|
100
|
+
console.log(`✔ Scaffold complete: ${created} created, ${updated} updated, ${skipped} skipped.`)
|
package/src/stores/themeStore.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Theme Store — manages the active color scheme for the entire app.
|
|
3
3
|
*
|
|
4
|
-
* Reads/writes `sb-color-scheme` in localStorage, sets
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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-Svelte consumers
|
|
8
|
+
* (React ThemeProvider, etc.) can react.
|
|
7
9
|
*
|
|
8
10
|
* Supports a "system" value that follows the OS preference via
|
|
9
11
|
* `prefers-color-scheme`, updating automatically when the user changes
|
|
@@ -86,9 +88,28 @@ function snapshot(theme: ThemeValue): ThemeState {
|
|
|
86
88
|
let _current: ThemeValue = readStoredTheme()
|
|
87
89
|
const _store = writable<ThemeState>(snapshot(_current))
|
|
88
90
|
|
|
89
|
-
function _applyToDOM(resolved: string): void {
|
|
91
|
+
function _applyToDOM(theme: ThemeValue, resolved: string): void {
|
|
90
92
|
if (typeof document === 'undefined') return
|
|
91
|
-
document.documentElement
|
|
93
|
+
const el = document.documentElement
|
|
94
|
+
|
|
95
|
+
// Internal attribute
|
|
96
|
+
el.setAttribute('data-sb-theme', resolved)
|
|
97
|
+
|
|
98
|
+
// Primer CSS attributes — these drive @primer/react ThemeProvider and
|
|
99
|
+
// Primer CSS custom-property layers without needing React state updates.
|
|
100
|
+
if (theme === 'system') {
|
|
101
|
+
el.setAttribute('data-color-mode', 'auto')
|
|
102
|
+
el.setAttribute('data-light-theme', 'light')
|
|
103
|
+
el.setAttribute('data-dark-theme', 'dark')
|
|
104
|
+
} else if (resolved.startsWith('dark')) {
|
|
105
|
+
el.setAttribute('data-color-mode', 'dark')
|
|
106
|
+
el.setAttribute('data-dark-theme', resolved)
|
|
107
|
+
el.setAttribute('data-light-theme', 'light')
|
|
108
|
+
} else {
|
|
109
|
+
el.setAttribute('data-color-mode', 'light')
|
|
110
|
+
el.setAttribute('data-light-theme', resolved)
|
|
111
|
+
el.setAttribute('data-dark-theme', 'dark')
|
|
112
|
+
}
|
|
92
113
|
}
|
|
93
114
|
|
|
94
115
|
function _dispatchEvent(theme: ThemeValue, resolved: string): void {
|
|
@@ -117,7 +138,7 @@ export function setTheme(value: ThemeValue): void {
|
|
|
117
138
|
|
|
118
139
|
const state = snapshot(value)
|
|
119
140
|
_store.set(state)
|
|
120
|
-
_applyToDOM(state.resolved)
|
|
141
|
+
_applyToDOM(value, state.resolved)
|
|
121
142
|
_dispatchEvent(value, state.resolved)
|
|
122
143
|
}
|
|
123
144
|
|
|
@@ -137,7 +158,7 @@ if (typeof window !== 'undefined') {
|
|
|
137
158
|
if (_current !== 'system') return
|
|
138
159
|
const state = snapshot('system')
|
|
139
160
|
_store.set(state)
|
|
140
|
-
_applyToDOM(state.resolved)
|
|
161
|
+
_applyToDOM('system', state.resolved)
|
|
141
162
|
_dispatchEvent('system', state.resolved)
|
|
142
163
|
})
|
|
143
164
|
}
|
|
@@ -146,4 +167,4 @@ if (typeof window !== 'undefined') {
|
|
|
146
167
|
// Boot — apply the stored theme immediately on import
|
|
147
168
|
// ---------------------------------------------------------------------------
|
|
148
169
|
|
|
149
|
-
_applyToDOM(resolveTheme(_current))
|
|
170
|
+
_applyToDOM(_current, resolveTheme(_current))
|
package/src/styles/tailwind.css
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
2
3
|
@source "../**/*.{svelte,js,ts}";
|
|
3
4
|
|
|
4
5
|
@custom-variant dark (&:where([data-sb-theme^="dark"], [data-sb-theme^="dark"] *));
|
|
@@ -118,3 +119,18 @@
|
|
|
118
119
|
--smooth-corners: 4;
|
|
119
120
|
}
|
|
120
121
|
}
|
|
122
|
+
|
|
123
|
+
/*
|
|
124
|
+
* Prevent unlayered CSS framework resets (e.g. Primer's `button { border-radius: 0 }`)
|
|
125
|
+
* from overriding layered Tailwind utilities on storyboard UI elements.
|
|
126
|
+
* `revert-layer` falls back to the value from the cascade layers (Tailwind utilities).
|
|
127
|
+
*/
|
|
128
|
+
[data-slot="button"] {
|
|
129
|
+
border-radius: revert-layer;
|
|
130
|
+
font-weight: revert-layer;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
[data-slot="panel-content"] select,
|
|
134
|
+
[data-slot="panel-content"] input {
|
|
135
|
+
border-radius: revert-layer;
|
|
136
|
+
}
|
|
@@ -502,6 +502,10 @@
|
|
|
502
502
|
{/if}
|
|
503
503
|
{:else}
|
|
504
504
|
<!-- Canvas view -->
|
|
505
|
+
<div class="canvasWarning">
|
|
506
|
+
<Icon size={14} name="primer/alert" color="#9a6700" offsetY={-1} />
|
|
507
|
+
<span>Canvas is an experimental feature. Use with caution.</span>
|
|
508
|
+
</div>
|
|
505
509
|
{#each canvasFolders as folder (folder.dirName)}
|
|
506
510
|
<section class="folderGroup" class:folderGroupOpen={isExpanded(`folder:${folder.dirName}`)}>
|
|
507
511
|
<button
|
|
@@ -958,4 +962,18 @@
|
|
|
958
962
|
max-width: 720px;
|
|
959
963
|
margin: 0 auto;
|
|
960
964
|
}
|
|
965
|
+
|
|
966
|
+
.canvasWarning {
|
|
967
|
+
display: flex;
|
|
968
|
+
align-items: center;
|
|
969
|
+
gap: 8px;
|
|
970
|
+
padding: 10px 14px;
|
|
971
|
+
margin-bottom: 16px;
|
|
972
|
+
border-radius: 8px;
|
|
973
|
+
border: 1px solid var(--borderColor-default, var(--color-border, #d0d7de));
|
|
974
|
+
background: var(--bgColor-attention-muted, #3d2e00);
|
|
975
|
+
color: var(--fgColor-attention, #9a6700);
|
|
976
|
+
font-size: 13px;
|
|
977
|
+
line-height: 1.4;
|
|
978
|
+
}
|
|
961
979
|
</style>
|
package/src/ui-entry.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI entry point — compiled into dist/storyboard-ui.js via Vite library build.
|
|
3
|
+
*
|
|
4
|
+
* This file is the entry for the pre-compiled Svelte UI bundle.
|
|
5
|
+
* It re-exports all UI mount functions that depend on Svelte.
|
|
6
|
+
* Consumers never import this directly — they use mountStoryboardCore()
|
|
7
|
+
* or the package self-reference '@dfosco/storyboard-core/ui-runtime'.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
// Tailwind utility + component CSS — bundled into storyboard-ui.css
|
|
11
|
+
import './styles/tailwind.css'
|
|
12
|
+
|
|
13
|
+
// Comments CSS
|
|
14
|
+
import './comments/ui/comment-layout.css'
|
|
15
|
+
import './comments/ui/comments.css'
|
|
16
|
+
|
|
17
|
+
// Modes CSS (design mode body classes)
|
|
18
|
+
import './modes.css'
|
|
19
|
+
|
|
20
|
+
// CoreUIBar (floating toolbar)
|
|
21
|
+
export { mountDevTools, unmountDevTools } from './devtools.js'
|
|
22
|
+
|
|
23
|
+
// Comments UI (Svelte-based comment pins, windows, drawers)
|
|
24
|
+
export { mountComments } from './comments/ui/mount.js'
|
|
25
|
+
|
|
26
|
+
// Viewfinder dashboard (Svelte component)
|
|
27
|
+
export { mountViewfinder, unmountViewfinder } from './ui/viewfinder.ts'
|
|
28
|
+
|
|
29
|
+
// Design modes (Svelte component)
|
|
30
|
+
export { mountDesignModesUI as mountDesignModes } from './ui/design-modes.ts'
|
|
@@ -57,14 +57,6 @@ function readConfig(root) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
|
|
60
|
-
/**
|
|
61
|
-
* Check if any workshop feature is enabled.
|
|
62
|
-
*/
|
|
63
|
-
function hasAnyWorkshopFeature(workshopConfig) {
|
|
64
|
-
if (!workshopConfig?.features) return false
|
|
65
|
-
return Object.values(workshopConfig.features).some(Boolean)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
60
|
/**
|
|
69
61
|
* Core storyboard server Vite plugin.
|
|
70
62
|
*/
|
|
@@ -114,17 +106,17 @@ export default function storyboardServer() {
|
|
|
114
106
|
// Wire canvas API routes (always enabled — CRUD for .canvas.jsonl files)
|
|
115
107
|
routeHandlers.set('canvas', createCanvasHandler({ root, sendJson }))
|
|
116
108
|
|
|
117
|
-
// Watch
|
|
109
|
+
// Watch toolbar.config.json for changes — trigger full reload so
|
|
118
110
|
// CoreUIBar.svelte picks up menu/mode config changes during dev
|
|
119
|
-
const
|
|
111
|
+
const toolbarConfigPath = path.resolve(
|
|
120
112
|
path.dirname(new URL(import.meta.url).pathname),
|
|
121
|
-
'../../
|
|
113
|
+
'../../toolbar.config.json'
|
|
122
114
|
)
|
|
123
|
-
server.watcher.add(
|
|
115
|
+
server.watcher.add(toolbarConfigPath)
|
|
124
116
|
server.watcher.on('change', (filePath) => {
|
|
125
|
-
if (path.resolve(filePath) ===
|
|
117
|
+
if (path.resolve(filePath) === toolbarConfigPath) {
|
|
126
118
|
// Invalidate the cached JSON module so Vite re-reads from disk
|
|
127
|
-
const mods = server.moduleGraph.getModulesByFile(
|
|
119
|
+
const mods = server.moduleGraph.getModulesByFile(toolbarConfigPath)
|
|
128
120
|
if (mods) {
|
|
129
121
|
for (const mod of mods) {
|
|
130
122
|
server.moduleGraph.invalidateModule(mod)
|
|
@@ -134,16 +126,8 @@ export default function storyboardServer() {
|
|
|
134
126
|
}
|
|
135
127
|
})
|
|
136
128
|
|
|
137
|
-
//
|
|
138
|
-
|
|
139
|
-
// Resolve the actual filesystem path for the mount script.
|
|
140
|
-
// Use /@fs/ prefix so Vite serves it through its module pipeline.
|
|
141
|
-
const mountPath = path.resolve(
|
|
142
|
-
path.dirname(new URL(import.meta.url).pathname),
|
|
143
|
-
'../workshop/ui/mount.ts'
|
|
144
|
-
)
|
|
145
|
-
clientScripts.push('/@fs' + mountPath)
|
|
146
|
-
}
|
|
129
|
+
// Workshop client UI is now mounted by mountStoryboardCore() via the
|
|
130
|
+
// compiled UI bundle. No script injection needed.
|
|
147
131
|
|
|
148
132
|
// Plugin registry for external plugins (future use).
|
|
149
133
|
// Plugins call registerRoutes/registerClientScript in their setup().
|
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
let loading = $state(true)
|
|
27
27
|
let submitting = $state(false)
|
|
28
28
|
let error: string | null = $state(null)
|
|
29
|
+
let success: string | null = $state(null)
|
|
30
|
+
let createdRoute: string | null = $state(null)
|
|
29
31
|
|
|
30
32
|
const kebabName = $derived(
|
|
31
33
|
name.replace(/[^a-zA-Z0-9\s_-]/g, '').trim().replace(/[\s_]+/g, '-').toLowerCase().replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
@@ -47,7 +49,20 @@
|
|
|
47
49
|
return basePath.replace(/\/$/, '') + '/_storyboard/canvas'
|
|
48
50
|
}
|
|
49
51
|
|
|
52
|
+
const CANVAS_SUCCESS_KEY = 'sb-canvas-created'
|
|
53
|
+
|
|
50
54
|
onMount(async () => {
|
|
55
|
+
// Restore success state after Vite's full-reload on file creation
|
|
56
|
+
try {
|
|
57
|
+
const saved = sessionStorage.getItem(CANVAS_SUCCESS_KEY)
|
|
58
|
+
if (saved) {
|
|
59
|
+
const parsed = JSON.parse(saved)
|
|
60
|
+
success = parsed.success
|
|
61
|
+
createdRoute = parsed.route
|
|
62
|
+
sessionStorage.removeItem(CANVAS_SUCCESS_KEY)
|
|
63
|
+
}
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
51
66
|
try {
|
|
52
67
|
const res = await fetch(getApiUrl() + '/folders')
|
|
53
68
|
if (res.ok) {
|
|
@@ -62,7 +77,7 @@
|
|
|
62
77
|
|
|
63
78
|
async function submit() {
|
|
64
79
|
if (!canSubmit) return
|
|
65
|
-
submitting = true; error = null
|
|
80
|
+
submitting = true; error = null; success = null; createdRoute = null
|
|
66
81
|
try {
|
|
67
82
|
const res = await fetch(getApiUrl() + '/create', {
|
|
68
83
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
@@ -70,10 +85,12 @@
|
|
|
70
85
|
})
|
|
71
86
|
const data = await res.json()
|
|
72
87
|
if (!res.ok) { error = data.error || 'Failed to create canvas'; return }
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
success = `Created ${data.path}`
|
|
89
|
+
createdRoute = data.route
|
|
90
|
+
// Persist success state — Vite does a full-reload when new files are created
|
|
91
|
+
try {
|
|
92
|
+
sessionStorage.setItem(CANVAS_SUCCESS_KEY, JSON.stringify({ success, route: createdRoute }))
|
|
93
|
+
} catch {}
|
|
77
94
|
} catch (err: any) { error = err.message || 'Network error' } finally { submitting = false }
|
|
78
95
|
}
|
|
79
96
|
|
|
@@ -86,7 +103,7 @@
|
|
|
86
103
|
</Panel.Header>
|
|
87
104
|
|
|
88
105
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
89
|
-
<div class="p-4 space-y-3" onkeydown={handleKeydown}>
|
|
106
|
+
<div class="p-4 pt-2 space-y-3" onkeydown={handleKeydown}>
|
|
90
107
|
<div class="space-y-1">
|
|
91
108
|
<Label for="sb-canvas-name">Name</Label>
|
|
92
109
|
<Input id="sb-canvas-name" placeholder="e.g. design-overview" autocomplete="off" spellcheck="false" bind:value={name} />
|
|
@@ -118,6 +135,7 @@
|
|
|
118
135
|
</div>
|
|
119
136
|
|
|
120
137
|
{#if error}<Alert.Root variant="destructive"><Alert.Description>{error}</Alert.Description></Alert.Root>{/if}
|
|
138
|
+
{#if success}<Alert.Root><Alert.Description class="text-success">{success}{#if createdRoute} — <a href={createdRoute} class="underline font-medium">Go to canvas</a>{/if}</Alert.Description></Alert.Root>{/if}
|
|
121
139
|
</div>
|
|
122
140
|
|
|
123
141
|
<Panel.Footer>
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
</Panel.Header>
|
|
103
103
|
|
|
104
104
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
105
|
-
<div class="p-4 space-y-3" onkeydown={handleKeydown}>
|
|
105
|
+
<div class="p-4 pt-2 space-y-3" onkeydown={handleKeydown}>
|
|
106
106
|
<div class="space-y-1">
|
|
107
107
|
<Label for="sb-flow-name">Name</Label>
|
|
108
108
|
<Input id="sb-flow-name" placeholder="e.g. empty-state" autocomplete="off" spellcheck="false" bind:value={name} />
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
</Panel.Header>
|
|
122
122
|
|
|
123
123
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
124
|
-
<div class="p-4 space-y-3" onkeydown={handleKeydown}>
|
|
124
|
+
<div class="p-4 pt-2 space-y-3" onkeydown={handleKeydown}>
|
|
125
125
|
<div class="space-y-1">
|
|
126
126
|
<Label for="sb-proto-name">Name</Label>
|
|
127
127
|
<Input id="sb-proto-name" placeholder="e.g. my-prototype" autocomplete="off" spellcheck="false" bind:value={name} />
|
|
File without changes
|