@dfosco/storyboard-react 4.0.0-beta.43 → 4.0.0-beta.44
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/package.json +4 -3
- package/src/CommandPalette/CommandPalette.jsx +918 -0
- package/src/CommandPalette/CreateDialog.jsx +219 -0
- package/src/CommandPalette/command-palette.css +66 -0
- package/src/Viewfinder.jsx +49 -11
- package/src/Viewfinder.module.css +43 -16
- package/src/canvas/CanvasPage.jsx +90 -4
- package/src/canvas/CanvasPage.module.css +25 -0
- package/src/canvas/MarqueeOverlay.jsx +20 -0
- package/src/canvas/PageSelector.jsx +20 -3
- package/src/canvas/componentIsolate.jsx +5 -5
- package/src/canvas/useCanvas.test.js +4 -4
- package/src/canvas/useMarqueeSelect.js +187 -0
- package/src/canvas/useMarqueeSelect.test.js +78 -0
- package/src/canvas/widgets/ComponentWidget.jsx +1 -1
- package/src/canvas/widgets/PrototypeEmbed.jsx +1 -1
- package/src/canvas/widgets/StoryWidget.jsx +1 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +1 -1
- package/src/index.js +3 -0
- package/src/vite/data-plugin.js +2 -9
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import styles from './CanvasPage.module.css'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renders the translucent selection rectangle during a marquee drag.
|
|
5
|
+
* Positioned relative to the scroll container.
|
|
6
|
+
*/
|
|
7
|
+
export default function MarqueeOverlay({ rect }) {
|
|
8
|
+
if (!rect) return null
|
|
9
|
+
return (
|
|
10
|
+
<div
|
|
11
|
+
className={styles.marqueeRect}
|
|
12
|
+
style={{
|
|
13
|
+
left: rect.x,
|
|
14
|
+
top: rect.y,
|
|
15
|
+
width: rect.w,
|
|
16
|
+
height: rect.h,
|
|
17
|
+
}}
|
|
18
|
+
/>
|
|
19
|
+
)
|
|
20
|
+
}
|
|
@@ -10,7 +10,15 @@ import styles from './PageSelector.module.css'
|
|
|
10
10
|
* @param {{ currentName: string, pages: Array<{ name: string, route: string, title: string }>, isLocalDev?: boolean }} props
|
|
11
11
|
*/
|
|
12
12
|
export default function PageSelector({ currentName, pages: initialPages, isLocalDev = false }) {
|
|
13
|
-
const [open, setOpen] = useState(
|
|
13
|
+
const [open, setOpen] = useState(() => {
|
|
14
|
+
try {
|
|
15
|
+
if (typeof sessionStorage !== 'undefined' && sessionStorage.getItem('sb-open-page-selector')) {
|
|
16
|
+
sessionStorage.removeItem('sb-open-page-selector')
|
|
17
|
+
return true
|
|
18
|
+
}
|
|
19
|
+
} catch { /* ignore */ }
|
|
20
|
+
return false
|
|
21
|
+
})
|
|
14
22
|
const [adding, setAdding] = useState(false)
|
|
15
23
|
const [newName, setNewName] = useState('')
|
|
16
24
|
const [creating, setCreating] = useState(false)
|
|
@@ -45,7 +53,13 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
|
|
|
45
53
|
if (!trimmed || creating) return
|
|
46
54
|
setCreating(true)
|
|
47
55
|
try {
|
|
48
|
-
|
|
56
|
+
// Single-page canvas (no folder) → convert to multi-page folder
|
|
57
|
+
const isSinglePage = !currentName.includes('/')
|
|
58
|
+
const createBody = isSinglePage
|
|
59
|
+
? { name: trimmed, convertFrom: currentName }
|
|
60
|
+
: { name: trimmed, folder: folder || undefined }
|
|
61
|
+
|
|
62
|
+
const result = await createCanvas(createBody)
|
|
49
63
|
if (result.error) {
|
|
50
64
|
console.error('Failed to create canvas page:', result.error)
|
|
51
65
|
setCreating(false)
|
|
@@ -65,6 +79,9 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
|
|
|
65
79
|
setNewName('')
|
|
66
80
|
setCreating(false)
|
|
67
81
|
|
|
82
|
+
// Stash a flag so the page selector opens automatically on the new page
|
|
83
|
+
try { sessionStorage.setItem('sb-open-page-selector', '1') } catch { /* ignore */ }
|
|
84
|
+
|
|
68
85
|
// Navigate to the new page after Vite picks up the new file
|
|
69
86
|
if (import.meta.hot) {
|
|
70
87
|
const timer = setTimeout(() => {
|
|
@@ -81,7 +98,7 @@ export default function PageSelector({ currentName, pages: initialPages, isLocal
|
|
|
81
98
|
console.error('Failed to create canvas page:', err)
|
|
82
99
|
setCreating(false)
|
|
83
100
|
}
|
|
84
|
-
}, [newName, folder, creating])
|
|
101
|
+
}, [newName, currentName, folder, creating])
|
|
85
102
|
|
|
86
103
|
// Focus input when entering add mode
|
|
87
104
|
useEffect(() => {
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canvas Component Isolate — iframe entry point.
|
|
3
3
|
*
|
|
4
|
-
* Renders a single named export from a .
|
|
4
|
+
* Renders a single named export from a .story.jsx module inside an
|
|
5
5
|
* isolated document. The parent CanvasPage embeds this via an iframe
|
|
6
6
|
* so a broken component cannot crash the entire canvas.
|
|
7
7
|
*
|
|
8
8
|
* Query params:
|
|
9
|
-
* module — absolute or base-relative path to the .
|
|
9
|
+
* module — absolute or base-relative path to the .story.jsx file
|
|
10
10
|
* export — the named export to render
|
|
11
11
|
* theme — canvas theme (light / dark / dark_dimmed)
|
|
12
12
|
*/
|
|
@@ -97,9 +97,9 @@ async function mount() {
|
|
|
97
97
|
return
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// Validate: only allow .
|
|
101
|
-
if (!modulePath.
|
|
102
|
-
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .
|
|
100
|
+
// Validate: only allow .story.{jsx,tsx} modules
|
|
101
|
+
if (!modulePath.match(/\.story\.(jsx|tsx)$/)) {
|
|
102
|
+
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .story.jsx/.tsx files are allowed'))
|
|
103
103
|
return
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -3,14 +3,14 @@ import { resolveCanvasModuleImport } from './useCanvas.js'
|
|
|
3
3
|
|
|
4
4
|
describe('resolveCanvasModuleImport', () => {
|
|
5
5
|
it('prefixes root-relative module paths with BASE_URL', () => {
|
|
6
|
-
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.
|
|
7
|
-
'/feature-branch/src/canvas/button-patterns.
|
|
6
|
+
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.story.jsx', '/feature-branch/')).toBe(
|
|
7
|
+
'/feature-branch/src/canvas/button-patterns.story.jsx',
|
|
8
8
|
)
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
it('keeps root-relative paths unchanged when BASE_URL is root', () => {
|
|
12
|
-
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.
|
|
13
|
-
'/src/canvas/button-patterns.
|
|
12
|
+
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.story.jsx', '/')).toBe(
|
|
13
|
+
'/src/canvas/button-patterns.story.jsx',
|
|
14
14
|
)
|
|
15
15
|
})
|
|
16
16
|
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Returns the bounding-box list for all widgets on the canvas.
|
|
5
|
+
* Each entry: { id, x, y, width, height }
|
|
6
|
+
*/
|
|
7
|
+
function getWidgetBounds(widgets, componentEntries, fallbackSizes) {
|
|
8
|
+
const bounds = []
|
|
9
|
+
for (const w of (widgets ?? [])) {
|
|
10
|
+
const fb = fallbackSizes[w.type] || { width: 200, height: 150 }
|
|
11
|
+
bounds.push({
|
|
12
|
+
id: w.id,
|
|
13
|
+
x: w?.position?.x ?? 0,
|
|
14
|
+
y: w?.position?.y ?? 0,
|
|
15
|
+
width: w.props?.width ?? fb.width,
|
|
16
|
+
height: w.props?.height ?? fb.height,
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
for (const entry of componentEntries) {
|
|
20
|
+
const fb = fallbackSizes['component'] || { width: 200, height: 150 }
|
|
21
|
+
bounds.push({
|
|
22
|
+
id: `jsx-${entry.exportName}`,
|
|
23
|
+
x: entry.sourceData?.position?.x ?? 0,
|
|
24
|
+
y: entry.sourceData?.position?.y ?? 0,
|
|
25
|
+
width: entry.sourceData?.width ?? fb.width,
|
|
26
|
+
height: entry.sourceData?.height ?? fb.height,
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
return bounds
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Test whether two axis-aligned rectangles overlap. */
|
|
33
|
+
function rectsIntersect(a, b) {
|
|
34
|
+
return a.x < b.x + b.width &&
|
|
35
|
+
a.x + a.width > b.x &&
|
|
36
|
+
a.y < b.y + b.height &&
|
|
37
|
+
a.y + a.height > b.y
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Hook that powers marquee (lasso) selection on the canvas.
|
|
42
|
+
*
|
|
43
|
+
* @param {Object} opts
|
|
44
|
+
* @param {React.RefObject} opts.scrollRef — ref to the scroll container
|
|
45
|
+
* @param {React.RefObject} opts.zoomRef — ref holding current zoom (number 25-200)
|
|
46
|
+
* @param {Function} opts.setSelectedWidgetIds — state setter for selected IDs (Set)
|
|
47
|
+
* @param {Array} opts.widgets — current localWidgets array
|
|
48
|
+
* @param {Array} opts.componentEntries — current componentEntries array
|
|
49
|
+
* @param {Object} opts.fallbackSizes — WIDGET_FALLBACK_SIZES map
|
|
50
|
+
* @param {boolean} opts.spaceHeld — whether the space key is pressed (panning)
|
|
51
|
+
* @param {boolean} opts.isLocalDev — only enable in local dev
|
|
52
|
+
*
|
|
53
|
+
* @returns {{ marqueeScreenRect: {x,y,w,h}|null, handleMarqueeMouseDown: Function }}
|
|
54
|
+
*/
|
|
55
|
+
export default function useMarqueeSelect({
|
|
56
|
+
scrollRef,
|
|
57
|
+
zoomRef,
|
|
58
|
+
setSelectedWidgetIds,
|
|
59
|
+
widgets,
|
|
60
|
+
componentEntries,
|
|
61
|
+
fallbackSizes,
|
|
62
|
+
spaceHeld,
|
|
63
|
+
isLocalDev,
|
|
64
|
+
}) {
|
|
65
|
+
const [marqueeScreenRect, setMarqueeScreenRect] = useState(null)
|
|
66
|
+
const marqueeState = useRef(null) // { startCanvasX, startCanvasY, startClientX, startClientY }
|
|
67
|
+
|
|
68
|
+
/** Convert a client (screen) point to canvas-space coords. */
|
|
69
|
+
const clientToCanvas = useCallback((clientX, clientY) => {
|
|
70
|
+
const el = scrollRef.current
|
|
71
|
+
if (!el) return { x: 0, y: 0 }
|
|
72
|
+
const rect = el.getBoundingClientRect()
|
|
73
|
+
const scale = (zoomRef.current ?? 100) / 100
|
|
74
|
+
return {
|
|
75
|
+
x: (el.scrollLeft + (clientX - rect.left)) / scale,
|
|
76
|
+
y: (el.scrollTop + (clientY - rect.top)) / scale,
|
|
77
|
+
}
|
|
78
|
+
}, [scrollRef, zoomRef])
|
|
79
|
+
|
|
80
|
+
// Ref to track active drag listeners for cleanup on unmount
|
|
81
|
+
const cleanupRef = useRef(null)
|
|
82
|
+
|
|
83
|
+
const handleMarqueeMouseDown = useCallback((e) => {
|
|
84
|
+
if (!isLocalDev) return
|
|
85
|
+
if (spaceHeld) return
|
|
86
|
+
// Only start on direct background click (not on a widget)
|
|
87
|
+
if (e.button !== 0) return
|
|
88
|
+
if (e.target.closest('[data-tc-x]')) return
|
|
89
|
+
|
|
90
|
+
const canvasStart = clientToCanvas(e.clientX, e.clientY)
|
|
91
|
+
marqueeState.current = {
|
|
92
|
+
startCanvasX: canvasStart.x,
|
|
93
|
+
startCanvasY: canvasStart.y,
|
|
94
|
+
startClientX: e.clientX,
|
|
95
|
+
startClientY: e.clientY,
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Minimum drag distance before showing the marquee (avoids flicker on clicks)
|
|
99
|
+
const MIN_DRAG = 4
|
|
100
|
+
|
|
101
|
+
function handleMove(ev) {
|
|
102
|
+
const ms = marqueeState.current
|
|
103
|
+
if (!ms) return
|
|
104
|
+
|
|
105
|
+
const dx = ev.clientX - ms.startClientX
|
|
106
|
+
const dy = ev.clientY - ms.startClientY
|
|
107
|
+
if (Math.abs(dx) < MIN_DRAG && Math.abs(dy) < MIN_DRAG) return
|
|
108
|
+
|
|
109
|
+
const el = scrollRef.current
|
|
110
|
+
if (!el) return
|
|
111
|
+
const containerRect = el.getBoundingClientRect()
|
|
112
|
+
|
|
113
|
+
// Content-space rectangle (accounts for scroll offset)
|
|
114
|
+
const sx = Math.min(ms.startClientX, ev.clientX) - containerRect.left + el.scrollLeft
|
|
115
|
+
const sy = Math.min(ms.startClientY, ev.clientY) - containerRect.top + el.scrollTop
|
|
116
|
+
const sw = Math.abs(dx)
|
|
117
|
+
const sh = Math.abs(dy)
|
|
118
|
+
|
|
119
|
+
setMarqueeScreenRect({ x: sx, y: sy, w: sw, h: sh })
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function removeListeners() {
|
|
123
|
+
document.removeEventListener('mousemove', handleMove)
|
|
124
|
+
document.removeEventListener('mouseup', handleUp)
|
|
125
|
+
window.removeEventListener('blur', handleCancel)
|
|
126
|
+
cleanupRef.current = null
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function handleCancel() {
|
|
130
|
+
removeListeners()
|
|
131
|
+
marqueeState.current = null
|
|
132
|
+
setMarqueeScreenRect(null)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function handleUp(ev) {
|
|
136
|
+
removeListeners()
|
|
137
|
+
|
|
138
|
+
const ms = marqueeState.current
|
|
139
|
+
marqueeState.current = null
|
|
140
|
+
setMarqueeScreenRect(null)
|
|
141
|
+
|
|
142
|
+
if (!ms) return
|
|
143
|
+
|
|
144
|
+
// If the user barely moved, treat as a deselect click
|
|
145
|
+
const dx = ev.clientX - ms.startClientX
|
|
146
|
+
const dy = ev.clientY - ms.startClientY
|
|
147
|
+
if (Math.abs(dx) < MIN_DRAG && Math.abs(dy) < MIN_DRAG) {
|
|
148
|
+
setSelectedWidgetIds(new Set())
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Compute the selection rect in canvas space
|
|
153
|
+
const canvasEnd = clientToCanvas(ev.clientX, ev.clientY)
|
|
154
|
+
const selRect = {
|
|
155
|
+
x: Math.min(ms.startCanvasX, canvasEnd.x),
|
|
156
|
+
y: Math.min(ms.startCanvasY, canvasEnd.y),
|
|
157
|
+
width: Math.abs(canvasEnd.x - ms.startCanvasX),
|
|
158
|
+
height: Math.abs(canvasEnd.y - ms.startCanvasY),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Find intersecting widgets
|
|
162
|
+
const allBounds = getWidgetBounds(widgets, componentEntries, fallbackSizes)
|
|
163
|
+
const selected = new Set()
|
|
164
|
+
for (const wb of allBounds) {
|
|
165
|
+
if (rectsIntersect(selRect, wb)) {
|
|
166
|
+
selected.add(wb.id)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
setSelectedWidgetIds(selected)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
document.addEventListener('mousemove', handleMove)
|
|
174
|
+
document.addEventListener('mouseup', handleUp)
|
|
175
|
+
window.addEventListener('blur', handleCancel)
|
|
176
|
+
cleanupRef.current = removeListeners
|
|
177
|
+
}, [isLocalDev, spaceHeld, clientToCanvas, scrollRef, setSelectedWidgetIds, widgets, componentEntries, fallbackSizes])
|
|
178
|
+
|
|
179
|
+
// Clean up listeners if component unmounts mid-drag
|
|
180
|
+
useEffect(() => {
|
|
181
|
+
return () => { cleanupRef.current?.() }
|
|
182
|
+
}, [])
|
|
183
|
+
|
|
184
|
+
return { marqueeScreenRect, handleMarqueeMouseDown }
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export { getWidgetBounds, rectsIntersect }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { rectsIntersect, getWidgetBounds } from './useMarqueeSelect.js'
|
|
3
|
+
|
|
4
|
+
describe('rectsIntersect', () => {
|
|
5
|
+
it('returns true for overlapping rectangles', () => {
|
|
6
|
+
const a = { x: 0, y: 0, width: 100, height: 100 }
|
|
7
|
+
const b = { x: 50, y: 50, width: 100, height: 100 }
|
|
8
|
+
expect(rectsIntersect(a, b)).toBe(true)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('returns false for non-overlapping rectangles', () => {
|
|
12
|
+
const a = { x: 0, y: 0, width: 100, height: 100 }
|
|
13
|
+
const b = { x: 200, y: 200, width: 100, height: 100 }
|
|
14
|
+
expect(rectsIntersect(a, b)).toBe(false)
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('returns false for edge-touching rectangles (no overlap)', () => {
|
|
18
|
+
const a = { x: 0, y: 0, width: 100, height: 100 }
|
|
19
|
+
const b = { x: 100, y: 0, width: 100, height: 100 }
|
|
20
|
+
expect(rectsIntersect(a, b)).toBe(false)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('returns true when one rectangle contains the other', () => {
|
|
24
|
+
const a = { x: 0, y: 0, width: 200, height: 200 }
|
|
25
|
+
const b = { x: 50, y: 50, width: 50, height: 50 }
|
|
26
|
+
expect(rectsIntersect(a, b)).toBe(true)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('returns true for partial horizontal overlap', () => {
|
|
30
|
+
const a = { x: 0, y: 0, width: 100, height: 100 }
|
|
31
|
+
const b = { x: 50, y: 0, width: 100, height: 50 }
|
|
32
|
+
expect(rectsIntersect(a, b)).toBe(true)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('getWidgetBounds', () => {
|
|
37
|
+
const fallbackSizes = {
|
|
38
|
+
'sticky-note': { width: 270, height: 170 },
|
|
39
|
+
'component': { width: 200, height: 150 },
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it('returns bounds for JSON widgets', () => {
|
|
43
|
+
const widgets = [
|
|
44
|
+
{ id: 'w1', type: 'sticky-note', position: { x: 10, y: 20 }, props: { width: 300, height: 200 } },
|
|
45
|
+
{ id: 'w2', type: 'sticky-note', position: { x: 100, y: 200 }, props: {} },
|
|
46
|
+
]
|
|
47
|
+
const result = getWidgetBounds(widgets, [], fallbackSizes)
|
|
48
|
+
expect(result).toEqual([
|
|
49
|
+
{ id: 'w1', x: 10, y: 20, width: 300, height: 200 },
|
|
50
|
+
{ id: 'w2', x: 100, y: 200, width: 270, height: 170 },
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('returns bounds for component entries', () => {
|
|
55
|
+
const entries = [
|
|
56
|
+
{ exportName: 'MyComp', sourceData: { position: { x: 5, y: 10 }, width: 400, height: 300 } },
|
|
57
|
+
]
|
|
58
|
+
const result = getWidgetBounds([], entries, fallbackSizes)
|
|
59
|
+
expect(result).toEqual([
|
|
60
|
+
{ id: 'jsx-MyComp', x: 5, y: 10, width: 400, height: 300 },
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('handles null/missing widget data gracefully', () => {
|
|
65
|
+
const widgets = [
|
|
66
|
+
{ id: 'w1', type: 'unknown' },
|
|
67
|
+
]
|
|
68
|
+
const result = getWidgetBounds(widgets, [], fallbackSizes)
|
|
69
|
+
expect(result).toEqual([
|
|
70
|
+
{ id: 'w1', x: 0, y: 0, width: 200, height: 150 },
|
|
71
|
+
])
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('handles null widgets array', () => {
|
|
75
|
+
const result = getWidgetBounds(null, [], fallbackSizes)
|
|
76
|
+
expect(result).toEqual([])
|
|
77
|
+
})
|
|
78
|
+
})
|
|
@@ -7,7 +7,7 @@ import styles from './ComponentWidget.module.css'
|
|
|
7
7
|
import overlayStyles from './embedOverlay.module.css'
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Renders a live JSX export from a .
|
|
10
|
+
* Renders a live JSX export from a .story.jsx file.
|
|
11
11
|
*
|
|
12
12
|
* In dev mode (isLocalDev), each component is rendered inside an iframe
|
|
13
13
|
* via the /_storyboard/canvas/isolate middleware. This isolates broken
|
|
@@ -81,7 +81,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
|
|
|
81
81
|
const base = hashIdx >= 0 ? rawSrc.slice(0, hashIdx) : rawSrc
|
|
82
82
|
const hash = hashIdx >= 0 ? rawSrc.slice(hashIdx) : ''
|
|
83
83
|
const sep = base.includes('?') ? '&' : '?'
|
|
84
|
-
return `${base}${sep}_sb_embed&_sb_theme_target=prototype&_sb_canvas_theme=${canvasTheme}${hash}`
|
|
84
|
+
return `${base}${sep}_sb_embed&_sb_hide_branch_bar&_sb_theme_target=prototype&_sb_canvas_theme=${canvasTheme}${hash}`
|
|
85
85
|
}, [rawSrc, canvasTheme])
|
|
86
86
|
|
|
87
87
|
const prototypeIndex = useMemo(() => {
|
|
@@ -35,6 +35,7 @@ function resolveStoryUrl(storyId, exportName) {
|
|
|
35
35
|
const params = new URLSearchParams()
|
|
36
36
|
if (exportName) params.set('export', exportName)
|
|
37
37
|
params.set('_sb_embed', '')
|
|
38
|
+
params.set('_sb_hide_branch_bar', '')
|
|
38
39
|
return `${base}${story._route}?${params}`
|
|
39
40
|
}
|
|
40
41
|
|
package/src/index.js
CHANGED
|
@@ -37,6 +37,9 @@ export { FormContext } from './context/FormContext.js'
|
|
|
37
37
|
// Viewfinder dashboard
|
|
38
38
|
export { default as Viewfinder } from './Viewfinder.jsx'
|
|
39
39
|
|
|
40
|
+
// Command Palette
|
|
41
|
+
export { default as StoryboardCommandPalette } from './CommandPalette/CommandPalette.jsx'
|
|
42
|
+
|
|
40
43
|
// Canvas
|
|
41
44
|
export { default as CanvasPage } from './canvas/CanvasPage.jsx'
|
|
42
45
|
export { useCanvas } from './canvas/useCanvas.js'
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -649,13 +649,6 @@ function generateModule({ index, protoFolders, flowRoutes, canvasRoutes, canvasA
|
|
|
649
649
|
`[storyboard-data] Canvas "${name}" references JSX file "${parsed.jsx}" but it was not found at ${jsxPath}`
|
|
650
650
|
)
|
|
651
651
|
}
|
|
652
|
-
} else {
|
|
653
|
-
// Auto-detect a same-name .canvas.jsx companion
|
|
654
|
-
const autoJsx = absPath.replace(/\.canvas\.(jsonl|jsonc?)$/, '.canvas.jsx')
|
|
655
|
-
if (fs.existsSync(autoJsx)) {
|
|
656
|
-
const relJsx = '/' + path.relative(root, autoJsx).replace(/\\/g, '/')
|
|
657
|
-
parsed = { ...parsed, _jsxModule: relJsx }
|
|
658
|
-
}
|
|
659
652
|
}
|
|
660
653
|
}
|
|
661
654
|
|
|
@@ -834,7 +827,7 @@ export default function storyboardDataPlugin() {
|
|
|
834
827
|
// can't trace into its deps. Include the remark entry points so
|
|
835
828
|
// Vite pre-bundles the full chain — covers all transitive CJS
|
|
836
829
|
// packages (debug, extend, etc.) without whack-a-mole.
|
|
837
|
-
include: ['remark', 'remark-gfm', 'remark-html', 'use-sync-external-store/shim', 'use-sync-external-store/shim/with-selector'],
|
|
830
|
+
include: ['react-cmdk', 'remark', 'remark-gfm', 'remark-html', 'use-sync-external-store/shim', 'use-sync-external-store/shim/with-selector'],
|
|
838
831
|
exclude: ['@dfosco/storyboard-react'],
|
|
839
832
|
},
|
|
840
833
|
}
|
|
@@ -858,7 +851,7 @@ export default function storyboardDataPlugin() {
|
|
|
858
851
|
// ── Component isolate middleware ───────────────────────────────
|
|
859
852
|
// Serves a minimal HTML shell for iframe-isolated component widgets.
|
|
860
853
|
// The iframe loads componentIsolate.jsx which reads query params
|
|
861
|
-
// (module, export, theme) and renders a single
|
|
854
|
+
// (module, export, theme) and renders a single story export.
|
|
862
855
|
const isolateEntryPath = new URL('../canvas/componentIsolate.jsx', import.meta.url).pathname
|
|
863
856
|
server.middlewares.use(async (req, res, next) => {
|
|
864
857
|
if (!req.url) return next()
|