@dfosco/storyboard-react 2.8.0 → 3.1.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/package.json +6 -3
- package/src/Viewfinder.jsx +5 -4
- package/src/canvas/CanvasControls.jsx +123 -0
- package/src/canvas/CanvasControls.module.css +133 -0
- package/src/canvas/CanvasPage.jsx +433 -0
- package/src/canvas/CanvasPage.module.css +73 -0
- package/src/canvas/CanvasToolbar.jsx +76 -0
- package/src/canvas/CanvasToolbar.module.css +92 -0
- package/src/canvas/canvasApi.js +41 -0
- package/src/canvas/useCanvas.js +74 -0
- package/src/canvas/widgets/ComponentWidget.jsx +15 -0
- package/src/canvas/widgets/LinkPreview.jsx +34 -0
- package/src/canvas/widgets/LinkPreview.module.css +51 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +91 -0
- package/src/canvas/widgets/MarkdownBlock.module.css +78 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +179 -0
- package/src/canvas/widgets/PrototypeEmbed.module.css +242 -0
- package/src/canvas/widgets/StickyNote.jsx +98 -0
- package/src/canvas/widgets/StickyNote.module.css +111 -0
- package/src/canvas/widgets/WidgetWrapper.jsx +15 -0
- package/src/canvas/widgets/WidgetWrapper.module.css +23 -0
- package/src/canvas/widgets/index.js +23 -0
- package/src/canvas/widgets/widgetProps.js +151 -0
- package/src/hooks/useFeatureFlag.js +2 -4
- package/src/hooks/useFlows.js +50 -0
- package/src/hooks/useFlows.test.js +134 -0
- package/src/index.js +5 -0
- package/src/vite/data-plugin.js +131 -29
- package/src/vite/data-plugin.test.js +3 -3
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "
|
|
6
|
+
"@dfosco/storyboard-core": "3.1.0",
|
|
7
|
+
"@dfosco/tiny-canvas": "^1.1.0",
|
|
8
|
+
"@neodrag/react": "^2.3.1",
|
|
7
9
|
"glob": "^11.0.0",
|
|
8
10
|
"jsonc-parser": "^3.3.1"
|
|
9
11
|
},
|
|
@@ -24,6 +26,7 @@
|
|
|
24
26
|
"exports": {
|
|
25
27
|
".": "./src/index.js",
|
|
26
28
|
"./vite": "./src/vite/data-plugin.js",
|
|
27
|
-
"./hash-preserver": "./src/hashPreserver.js"
|
|
29
|
+
"./hash-preserver": "./src/hashPreserver.js",
|
|
30
|
+
"./canvas/CanvasPage": "./src/canvas/CanvasPage.jsx"
|
|
28
31
|
}
|
|
29
32
|
}
|
package/src/Viewfinder.jsx
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { useRef, useEffect } from 'react'
|
|
2
|
+
import { useRef, useEffect, useMemo } from 'react'
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Viewfinder — thin React wrapper around the Svelte Viewfinder component.
|
|
@@ -23,9 +23,10 @@ export default function Viewfinder({ pageModules = {}, basePath, title = 'Storyb
|
|
|
23
23
|
|
|
24
24
|
const shouldHideDefault = hideDefaultFlow ?? hideDefaultScene
|
|
25
25
|
|
|
26
|
-
const knownRoutes = Object.keys(pageModules)
|
|
26
|
+
const knownRoutes = useMemo(() => Object.keys(pageModules)
|
|
27
27
|
.map(p => p.replace('/src/prototypes/', '').replace('.jsx', ''))
|
|
28
|
-
.filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder')
|
|
28
|
+
.filter(n => !n.startsWith('_') && n !== 'index' && n !== 'viewfinder'),
|
|
29
|
+
[pageModules])
|
|
29
30
|
|
|
30
31
|
useEffect(() => {
|
|
31
32
|
if (!containerRef.current) return
|
|
@@ -53,7 +54,7 @@ export default function Viewfinder({ pageModules = {}, basePath, title = 'Storyb
|
|
|
53
54
|
handleRef.current = null
|
|
54
55
|
}
|
|
55
56
|
}
|
|
56
|
-
}, [title, subtitle, basePath, showThumbnails, shouldHideDefault])
|
|
57
|
+
}, [title, subtitle, basePath, knownRoutes, showThumbnails, shouldHideDefault])
|
|
57
58
|
|
|
58
59
|
return <div ref={containerRef} style={{ minHeight: '100vh' }} />
|
|
59
60
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
2
|
+
import styles from './CanvasControls.module.css'
|
|
3
|
+
|
|
4
|
+
const ZOOM_STEPS = [25, 50, 75, 100, 125, 150, 200]
|
|
5
|
+
export const ZOOM_MIN = ZOOM_STEPS[0]
|
|
6
|
+
export const ZOOM_MAX = ZOOM_STEPS[ZOOM_STEPS.length - 1]
|
|
7
|
+
|
|
8
|
+
const WIDGET_TYPES = [
|
|
9
|
+
{ type: 'sticky-note', label: 'Sticky Note' },
|
|
10
|
+
{ type: 'markdown', label: 'Markdown' },
|
|
11
|
+
{ type: 'prototype', label: 'Prototype' },
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Focused canvas toolbar — bottom-left controls for zoom and widget creation.
|
|
16
|
+
*/
|
|
17
|
+
export default function CanvasControls({ zoom, onZoomChange, onAddWidget }) {
|
|
18
|
+
const [menuOpen, setMenuOpen] = useState(false)
|
|
19
|
+
const menuRef = useRef(null)
|
|
20
|
+
|
|
21
|
+
// Close menu on outside click
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!menuOpen) return
|
|
24
|
+
function handlePointerDown(e) {
|
|
25
|
+
if (menuRef.current && !menuRef.current.contains(e.target)) {
|
|
26
|
+
setMenuOpen(false)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
document.addEventListener('pointerdown', handlePointerDown)
|
|
30
|
+
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
31
|
+
}, [menuOpen])
|
|
32
|
+
|
|
33
|
+
const zoomIn = useCallback(() => {
|
|
34
|
+
onZoomChange((z) => {
|
|
35
|
+
const next = ZOOM_STEPS.find((s) => s > z)
|
|
36
|
+
return next ?? ZOOM_MAX
|
|
37
|
+
})
|
|
38
|
+
}, [onZoomChange])
|
|
39
|
+
|
|
40
|
+
const zoomOut = useCallback(() => {
|
|
41
|
+
onZoomChange((z) => {
|
|
42
|
+
const next = [...ZOOM_STEPS].reverse().find((s) => s < z)
|
|
43
|
+
return next ?? ZOOM_MIN
|
|
44
|
+
})
|
|
45
|
+
}, [onZoomChange])
|
|
46
|
+
|
|
47
|
+
const resetZoom = useCallback(() => {
|
|
48
|
+
onZoomChange(100)
|
|
49
|
+
}, [onZoomChange])
|
|
50
|
+
|
|
51
|
+
const handleAddWidget = useCallback((type) => {
|
|
52
|
+
onAddWidget(type)
|
|
53
|
+
setMenuOpen(false)
|
|
54
|
+
}, [onAddWidget])
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className={styles.toolbar} role="toolbar" aria-label="Canvas controls">
|
|
58
|
+
{/* Create widget */}
|
|
59
|
+
<div ref={menuRef} className={styles.createGroup}>
|
|
60
|
+
<button
|
|
61
|
+
className={styles.btn}
|
|
62
|
+
onClick={() => setMenuOpen((v) => !v)}
|
|
63
|
+
aria-label="Add widget"
|
|
64
|
+
aria-expanded={menuOpen}
|
|
65
|
+
title="Add widget"
|
|
66
|
+
>
|
|
67
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
68
|
+
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
{menuOpen && (
|
|
72
|
+
<div className={styles.menu} role="menu">
|
|
73
|
+
<div className={styles.menuLabel}>Add to canvas</div>
|
|
74
|
+
{WIDGET_TYPES.map((wt) => (
|
|
75
|
+
<button
|
|
76
|
+
key={wt.type}
|
|
77
|
+
className={styles.menuItem}
|
|
78
|
+
role="menuitem"
|
|
79
|
+
onClick={() => handleAddWidget(wt.type)}
|
|
80
|
+
>
|
|
81
|
+
{wt.label}
|
|
82
|
+
</button>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
)}
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<div className={styles.divider} />
|
|
89
|
+
|
|
90
|
+
{/* Zoom controls */}
|
|
91
|
+
<button
|
|
92
|
+
className={styles.btn}
|
|
93
|
+
onClick={zoomOut}
|
|
94
|
+
disabled={zoom <= ZOOM_MIN}
|
|
95
|
+
aria-label="Zoom out"
|
|
96
|
+
title="Zoom out"
|
|
97
|
+
>
|
|
98
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
99
|
+
<path d="M2.75 7.25h10.5a.75.75 0 0 1 0 1.5H2.75a.75.75 0 0 1 0-1.5Z" />
|
|
100
|
+
</svg>
|
|
101
|
+
</button>
|
|
102
|
+
<button
|
|
103
|
+
className={styles.zoomLevel}
|
|
104
|
+
onClick={resetZoom}
|
|
105
|
+
title="Reset to 100%"
|
|
106
|
+
aria-label={`Zoom ${zoom}%, click to reset`}
|
|
107
|
+
>
|
|
108
|
+
{zoom}%
|
|
109
|
+
</button>
|
|
110
|
+
<button
|
|
111
|
+
className={styles.btn}
|
|
112
|
+
onClick={zoomIn}
|
|
113
|
+
disabled={zoom >= ZOOM_MAX}
|
|
114
|
+
aria-label="Zoom in"
|
|
115
|
+
title="Zoom in"
|
|
116
|
+
>
|
|
117
|
+
<svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
|
|
118
|
+
<path d="M7.75 2a.75.75 0 0 1 .75.75V7h4.25a.75.75 0 0 1 0 1.5H8.5v4.25a.75.75 0 0 1-1.5 0V8.5H2.75a.75.75 0 0 1 0-1.5H7V2.75A.75.75 0 0 1 7.75 2Z" />
|
|
119
|
+
</svg>
|
|
120
|
+
</button>
|
|
121
|
+
</div>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
.toolbar {
|
|
2
|
+
position: fixed;
|
|
3
|
+
bottom: 24px;
|
|
4
|
+
left: 24px;
|
|
5
|
+
z-index: 9998;
|
|
6
|
+
display: flex;
|
|
7
|
+
align-items: center;
|
|
8
|
+
gap: 2px;
|
|
9
|
+
padding: 4px;
|
|
10
|
+
background: rgba(255, 255, 255, 0.92);
|
|
11
|
+
backdrop-filter: blur(12px);
|
|
12
|
+
-webkit-backdrop-filter: blur(12px);
|
|
13
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
14
|
+
border-radius: 10px;
|
|
15
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
|
|
16
|
+
font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (prefers-color-scheme: dark) {
|
|
20
|
+
.toolbar {
|
|
21
|
+
background: rgba(22, 27, 34, 0.88);
|
|
22
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
23
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.4);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.btn {
|
|
28
|
+
all: unset;
|
|
29
|
+
cursor: pointer;
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
width: 32px;
|
|
34
|
+
height: 32px;
|
|
35
|
+
border-radius: 8px;
|
|
36
|
+
color: var(--fgColor-default, #1f2328);
|
|
37
|
+
transition: background 120ms;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.btn:hover:not(:disabled) {
|
|
41
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.btn:active:not(:disabled) {
|
|
45
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.btn:disabled {
|
|
49
|
+
opacity: 0.35;
|
|
50
|
+
cursor: default;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
.zoomLevel {
|
|
54
|
+
all: unset;
|
|
55
|
+
cursor: pointer;
|
|
56
|
+
display: flex;
|
|
57
|
+
align-items: center;
|
|
58
|
+
justify-content: center;
|
|
59
|
+
min-width: 44px;
|
|
60
|
+
height: 32px;
|
|
61
|
+
padding: 0 4px;
|
|
62
|
+
border-radius: 8px;
|
|
63
|
+
font-size: 12px;
|
|
64
|
+
font-weight: 500;
|
|
65
|
+
font-variant-numeric: tabular-nums;
|
|
66
|
+
color: var(--fgColor-muted, #656d76);
|
|
67
|
+
transition: background 120ms;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.zoomLevel:hover {
|
|
71
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
72
|
+
color: var(--fgColor-default, #1f2328);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.divider {
|
|
76
|
+
width: 1px;
|
|
77
|
+
height: 20px;
|
|
78
|
+
margin: 0 2px;
|
|
79
|
+
background: var(--borderColor-muted, #d8dee4);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Create widget menu */
|
|
83
|
+
.createGroup {
|
|
84
|
+
position: relative;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.menu {
|
|
88
|
+
position: absolute;
|
|
89
|
+
bottom: calc(100% + 8px);
|
|
90
|
+
left: 0;
|
|
91
|
+
min-width: 160px;
|
|
92
|
+
padding: 4px;
|
|
93
|
+
background: rgba(255, 255, 255, 0.95);
|
|
94
|
+
backdrop-filter: blur(12px);
|
|
95
|
+
-webkit-backdrop-filter: blur(12px);
|
|
96
|
+
border: 1px solid rgba(0, 0, 0, 0.12);
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
|
99
|
+
z-index: 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
@media (prefers-color-scheme: dark) {
|
|
103
|
+
.menu {
|
|
104
|
+
background: rgba(22, 27, 34, 0.92);
|
|
105
|
+
border-color: rgba(255, 255, 255, 0.1);
|
|
106
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.5);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.menuLabel {
|
|
111
|
+
padding: 6px 10px 4px;
|
|
112
|
+
font-size: 11px;
|
|
113
|
+
font-weight: 600;
|
|
114
|
+
color: var(--fgColor-muted, #656d76);
|
|
115
|
+
text-transform: uppercase;
|
|
116
|
+
letter-spacing: 0.4px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.menuItem {
|
|
120
|
+
all: unset;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
display: block;
|
|
123
|
+
width: 100%;
|
|
124
|
+
padding: 6px 10px;
|
|
125
|
+
font-size: 13px;
|
|
126
|
+
color: var(--fgColor-default, #1f2328);
|
|
127
|
+
border-radius: 6px;
|
|
128
|
+
box-sizing: border-box;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.menuItem:hover {
|
|
132
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
133
|
+
}
|