@dfosco/storyboard-react 4.2.0-beta.4 → 4.2.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 (89) hide show
  1. package/package.json +10 -11
  2. package/src/AuthModal/AuthModal.jsx +6 -8
  3. package/src/BranchBar/BranchBar.jsx +20 -6
  4. package/src/BranchBar/BranchBar.module.css +13 -4
  5. package/src/BranchBar/useBranches.js +20 -6
  6. package/src/BranchBar/useBranches.test.js +68 -0
  7. package/src/CommandPalette/CommandPalette.jsx +480 -187
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +562 -207
  11. package/src/Viewfinder.module.css +434 -93
  12. package/src/Workspace.jsx +7 -0
  13. package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
  14. package/src/canvas/CanvasPage.dragdrop.test.jsx +11 -7
  15. package/src/canvas/CanvasPage.jsx +739 -219
  16. package/src/canvas/CanvasPage.module.css +13 -15
  17. package/src/canvas/CanvasPage.multiselect.test.jsx +17 -6
  18. package/src/canvas/ConnectorLayer.jsx +121 -165
  19. package/src/canvas/ConnectorLayer.module.css +69 -0
  20. package/src/canvas/PageSelector.test.jsx +15 -6
  21. package/src/canvas/canvasApi.js +68 -2
  22. package/src/canvas/canvasReloadGuard.test.js +1 -1
  23. package/src/canvas/connectorGeometry.js +132 -0
  24. package/src/canvas/hotPoolDevLogs.js +25 -0
  25. package/src/canvas/useCanvas.js +1 -1
  26. package/src/canvas/useMarqueeSelect.js +30 -4
  27. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  28. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  29. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  30. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  31. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  32. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  33. package/src/canvas/widgets/ExpandedPane.jsx +474 -0
  34. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  35. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  38. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  39. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  40. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  41. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  42. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  43. package/src/canvas/widgets/LinkPreview.jsx +113 -5
  44. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  45. package/src/canvas/widgets/MarkdownBlock.jsx +167 -17
  46. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  47. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  48. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  49. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -39
  50. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  51. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  52. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  53. package/src/canvas/widgets/StoryWidget.jsx +73 -15
  54. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  55. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  56. package/src/canvas/widgets/TerminalWidget.jsx +445 -67
  57. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  58. package/src/canvas/widgets/TilesWidget.jsx +300 -0
  59. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  60. package/src/canvas/widgets/WidgetChrome.jsx +74 -153
  61. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  62. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  63. package/src/canvas/widgets/expandUtils.js +560 -0
  64. package/src/canvas/widgets/expandUtils.test.js +155 -0
  65. package/src/canvas/widgets/index.js +9 -0
  66. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  67. package/src/canvas/widgets/tilePool.js +23 -0
  68. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  70. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  71. package/src/canvas/widgets/tiles/leaf.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  73. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  75. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  76. package/src/canvas/widgets/widgetConfig.js +55 -4
  77. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  78. package/src/canvas/widgets/widgetProps.js +1 -0
  79. package/src/context.jsx +48 -20
  80. package/src/hooks/useConfig.js +14 -0
  81. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  82. package/src/hooks/useSceneData.js +1 -0
  83. package/src/hooks/useThemeState.test.js +1 -1
  84. package/src/index.js +8 -2
  85. package/src/story/ComponentSetPage.jsx +186 -0
  86. package/src/story/ComponentSetPage.module.css +121 -0
  87. package/src/story/StoryPage.jsx +32 -2
  88. package/src/vite/data-plugin.js +407 -67
  89. package/src/vite/data-plugin.test.js +1 -1
@@ -0,0 +1,219 @@
1
+ /**
2
+ * CropOverlay — interactive crop selection over an image.
3
+ *
4
+ * Renders:
5
+ * - A crop region with drag handles (corners + edges)
6
+ * - Dark overlay on excluded area (via box-shadow)
7
+ * - Rule-of-thirds grid
8
+ * - A floating bar anchored to the crop region with Save / Undo / Cancel + dimensions
9
+ *
10
+ * Props:
11
+ * containerWidth / containerHeight — pixel dimensions of the image container
12
+ * naturalWidth / naturalHeight — natural image dimensions
13
+ * onSave(cropRect) — called with { x, y, width, height } in natural pixels
14
+ * onCancel() — exit crop mode without saving
15
+ * onUndo() — revert to previous image (only when canUndo is true)
16
+ * canUndo — whether undo is available
17
+ */
18
+ import { useState, useRef, useCallback, useEffect } from 'react'
19
+ import styles from './CropOverlay.module.css'
20
+
21
+ const MIN_CROP = 20
22
+
23
+ function clamp(v, min, max) { return Math.min(Math.max(v, min), max) }
24
+
25
+ const HANDLES = ['NW', 'NE', 'SW', 'SE', 'N', 'S', 'W', 'E']
26
+
27
+ /* SVG micro-icons */
28
+ function CheckIcon() {
29
+ return (
30
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
31
+ <path d="M13.78 4.22a.75.75 0 0 1 0 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L2.22 9.28a.751.751 0 0 1 .018-1.042.751.751 0 0 1 1.042-.018L6 10.94l6.72-6.72a.75.75 0 0 1 1.06 0Z" />
32
+ </svg>
33
+ )
34
+ }
35
+
36
+ function UndoIcon() {
37
+ return (
38
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
39
+ <path d="M1.22 6.28a.749.749 0 0 1 0-1.06l3.5-3.5a.749.749 0 1 1 1.06 1.06L3.56 5h7.19a5.25 5.25 0 0 1 0 10.5H9.25a.75.75 0 0 1 0-1.5h1.5a3.75 3.75 0 0 0 0-7.5H3.56l2.22 2.22a.749.749 0 1 1-1.06 1.06Z" />
40
+ </svg>
41
+ )
42
+ }
43
+
44
+ function XIcon() {
45
+ return (
46
+ <svg width="14" height="14" viewBox="0 0 16 16" fill="currentColor">
47
+ <path d="M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z" />
48
+ </svg>
49
+ )
50
+ }
51
+
52
+ export default function CropOverlay({
53
+ containerWidth,
54
+ containerHeight,
55
+ naturalWidth,
56
+ naturalHeight,
57
+ onSave,
58
+ onCancel,
59
+ onUndo,
60
+ canUndo,
61
+ }) {
62
+ const cw = containerWidth || 400
63
+ const ch = containerHeight || 300
64
+
65
+ const [cropRect, setCropRect] = useState({
66
+ x: Math.round(cw * 0.05),
67
+ y: Math.round(ch * 0.05),
68
+ width: Math.round(cw * 0.9),
69
+ height: Math.round(ch * 0.9),
70
+ })
71
+
72
+ const dragging = useRef(null)
73
+
74
+ // Escape key cancels crop
75
+ useEffect(() => {
76
+ const handleKey = (e) => { if (e.key === 'Escape') onCancel?.() }
77
+ window.addEventListener('keydown', handleKey)
78
+ return () => window.removeEventListener('keydown', handleKey)
79
+ }, [onCancel])
80
+
81
+ const startDrag = useCallback((handleId, e) => {
82
+ e.preventDefault()
83
+ e.stopPropagation()
84
+ dragging.current = {
85
+ handle: handleId,
86
+ startX: e.clientX,
87
+ startY: e.clientY,
88
+ startCrop: { ...cropRect },
89
+ }
90
+
91
+ const onMove = (me) => {
92
+ if (!dragging.current) return
93
+ const { handle, startX, startY, startCrop } = dragging.current
94
+ const dx = me.clientX - startX
95
+ const dy = me.clientY - startY
96
+
97
+ let { x, y, width, height } = startCrop
98
+
99
+ if (handle === 'move') {
100
+ x = clamp(startCrop.x + dx, 0, cw - width)
101
+ y = clamp(startCrop.y + dy, 0, ch - height)
102
+ } else {
103
+ if (handle.includes('W') || handle === 'W') {
104
+ const newX = clamp(startCrop.x + dx, 0, startCrop.x + startCrop.width - MIN_CROP)
105
+ width = startCrop.width - (newX - startCrop.x)
106
+ x = newX
107
+ }
108
+ if (handle.includes('E') || handle === 'E') {
109
+ width = clamp(startCrop.width + dx, MIN_CROP, cw - startCrop.x)
110
+ }
111
+ if (handle.includes('N') || handle === 'N') {
112
+ const newY = clamp(startCrop.y + dy, 0, startCrop.y + startCrop.height - MIN_CROP)
113
+ height = startCrop.height - (newY - startCrop.y)
114
+ y = newY
115
+ }
116
+ if (handle.includes('S') || handle === 'S') {
117
+ height = clamp(startCrop.height + dy, MIN_CROP, ch - startCrop.y)
118
+ }
119
+ }
120
+
121
+ setCropRect({ x, y, width, height })
122
+ }
123
+
124
+ const onUp = () => {
125
+ dragging.current = null
126
+ window.removeEventListener('pointermove', onMove)
127
+ window.removeEventListener('pointerup', onUp)
128
+ }
129
+
130
+ window.addEventListener('pointermove', onMove)
131
+ window.addEventListener('pointerup', onUp)
132
+ }, [cropRect, cw, ch])
133
+
134
+ const handleSave = useCallback(() => {
135
+ const scaleX = (naturalWidth || cw) / cw
136
+ const scaleY = (naturalHeight || ch) / ch
137
+ onSave?.({
138
+ x: Math.round(cropRect.x * scaleX),
139
+ y: Math.round(cropRect.y * scaleY),
140
+ width: Math.round(cropRect.width * scaleX),
141
+ height: Math.round(cropRect.height * scaleY),
142
+ })
143
+ }, [cropRect, cw, ch, naturalWidth, naturalHeight, onSave])
144
+
145
+ const cropW = Math.round(((naturalWidth || cw) / cw) * cropRect.width)
146
+ const cropH = Math.round(((naturalHeight || ch) / ch) * cropRect.height)
147
+
148
+ return (
149
+ <div
150
+ className={styles.overlay}
151
+ onPointerDown={(e) => e.stopPropagation()}
152
+ onMouseDown={(e) => e.stopPropagation()}
153
+ >
154
+ <div
155
+ className={styles.cropRegion}
156
+ style={{
157
+ left: cropRect.x,
158
+ top: cropRect.y,
159
+ width: cropRect.width,
160
+ height: cropRect.height,
161
+ }}
162
+ onPointerDown={(e) => startDrag('move', e)}
163
+ >
164
+ {HANDLES.map((h) => (
165
+ <div
166
+ key={h}
167
+ className={`${styles.handle} ${styles[`handle${h}`]}`}
168
+ onPointerDown={(e) => startDrag(h, e)}
169
+ />
170
+ ))}
171
+ </div>
172
+
173
+ <FloatingCropBar
174
+ cropRect={cropRect}
175
+ containerWidth={cw}
176
+ cropW={cropW}
177
+ cropH={cropH}
178
+ onSave={handleSave}
179
+ onUndo={onUndo}
180
+ onCancel={onCancel}
181
+ canUndo={canUndo}
182
+ />
183
+ </div>
184
+ )
185
+ }
186
+
187
+ function FloatingCropBar({ cropRect, containerWidth, cropW, cropH, onSave, onUndo, onCancel, canUndo }) {
188
+ const barHeight = 36
189
+ const gap = 10
190
+ const centerX = cropRect.x + cropRect.width / 2
191
+ const aboveY = cropRect.y - barHeight - gap
192
+ const belowY = cropRect.y + cropRect.height + gap
193
+
194
+ const anchorBelow = aboveY < 0
195
+ const barTop = anchorBelow ? belowY : aboveY
196
+ const barLeft = clamp(centerX, 80, containerWidth - 80)
197
+
198
+ return (
199
+ <div
200
+ className={`${styles.floatingBar} ${anchorBelow ? styles.floatingBarBelow : ''}`}
201
+ style={{ top: barTop, left: barLeft }}
202
+ onPointerDown={(e) => e.stopPropagation()}
203
+ >
204
+ <span className={styles.dimensions}>{cropW} × {cropH}</span>
205
+ <span className={styles.separator} />
206
+ <button className={`${styles.floatingBtn} ${styles.floatingBtnSave}`} onClick={onSave}>
207
+ <CheckIcon /> Save
208
+ </button>
209
+ {canUndo && (
210
+ <button className={styles.floatingBtn} onClick={onUndo}>
211
+ <UndoIcon /> Undo
212
+ </button>
213
+ )}
214
+ <button className={`${styles.floatingBtn} ${styles.floatingBtnCancel}`} onClick={onCancel}>
215
+ <XIcon />
216
+ </button>
217
+ </div>
218
+ )
219
+ }
@@ -0,0 +1,118 @@
1
+ /* ── Crop overlay ─────────────────────────────────────────── */
2
+
3
+ .overlay {
4
+ position: absolute;
5
+ inset: 0;
6
+ z-index: 10;
7
+ cursor: default;
8
+ }
9
+
10
+ .cropRegion {
11
+ position: absolute;
12
+ border: 2px solid #ffffff;
13
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.3), 0 0 0 9999px rgba(0, 0, 0, 0.5);
14
+ cursor: move;
15
+ z-index: 11;
16
+ }
17
+
18
+ /* Rule-of-thirds grid */
19
+ .cropRegion::after {
20
+ content: '';
21
+ position: absolute;
22
+ inset: 0;
23
+ background:
24
+ linear-gradient(to right, rgba(255,255,255,0.3) 1px, transparent 1px) 33.33% 0 / 33.34% 100%,
25
+ linear-gradient(to bottom, rgba(255,255,255,0.3) 1px, transparent 1px) 0 33.33% / 100% 33.34%;
26
+ pointer-events: none;
27
+ }
28
+
29
+ /* ── Drag handles ─────────────────────────────────────────── */
30
+
31
+ .handle {
32
+ position: absolute;
33
+ width: 12px;
34
+ height: 12px;
35
+ background: #ffffff;
36
+ border: 2px solid var(--fgColor-accent, #0969da);
37
+ border-radius: 2px;
38
+ z-index: 12;
39
+ }
40
+
41
+ .handleNW { top: -6px; left: -6px; cursor: nw-resize; }
42
+ .handleNE { top: -6px; right: -6px; cursor: ne-resize; }
43
+ .handleSW { bottom: -6px; left: -6px; cursor: sw-resize; }
44
+ .handleSE { bottom: -6px; right: -6px; cursor: se-resize; }
45
+ .handleN { top: -6px; left: 50%; transform: translateX(-50%); cursor: n-resize; }
46
+ .handleS { bottom: -6px; left: 50%; transform: translateX(-50%); cursor: s-resize; }
47
+ .handleW { left: -6px; top: 50%; transform: translateY(-50%); cursor: w-resize; }
48
+ .handleE { right: -6px; top: 50%; transform: translateY(-50%); cursor: e-resize; }
49
+
50
+ /* ── Floating crop bar ────────────────────────────────────── */
51
+
52
+ .floatingBar {
53
+ position: absolute;
54
+ display: flex;
55
+ align-items: center;
56
+ gap: 6px;
57
+ padding: 4px 8px;
58
+ background: rgba(0, 0, 0, 0.75);
59
+ backdrop-filter: blur(8px);
60
+ -webkit-backdrop-filter: blur(8px);
61
+ border-radius: 8px;
62
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.3);
63
+ z-index: 20;
64
+ transform: translateX(-50%);
65
+ transition: top 80ms ease-out, left 80ms ease-out;
66
+ pointer-events: auto;
67
+ white-space: nowrap;
68
+ }
69
+
70
+ .floatingBtn {
71
+ all: unset;
72
+ cursor: pointer;
73
+ display: flex;
74
+ align-items: center;
75
+ gap: 4px;
76
+ padding: 4px 10px;
77
+ border-radius: 6px;
78
+ font-size: 12px;
79
+ font-weight: 500;
80
+ color: #ffffff;
81
+ transition: background 100ms;
82
+ }
83
+
84
+ .floatingBtn:hover {
85
+ background: rgba(255, 255, 255, 0.15);
86
+ }
87
+
88
+ .floatingBtnSave {
89
+ background: var(--bgColor-success-emphasis, #1a7f37);
90
+ color: #ffffff;
91
+ }
92
+
93
+ .floatingBtnSave:hover {
94
+ background: var(--bgColor-success-emphasis, #2da44e);
95
+ }
96
+
97
+ .floatingBtnCancel {
98
+ color: rgba(255, 255, 255, 0.7);
99
+ }
100
+
101
+ .floatingBtnCancel:hover {
102
+ color: #ffffff;
103
+ background: rgba(255, 255, 255, 0.1);
104
+ }
105
+
106
+ .dimensions {
107
+ font-size: 11px;
108
+ font-weight: 500;
109
+ color: rgba(255, 255, 255, 0.6);
110
+ padding: 0 4px;
111
+ font-variant-numeric: tabular-nums;
112
+ }
113
+
114
+ .separator {
115
+ width: 1px;
116
+ height: 16px;
117
+ background: rgba(255, 255, 255, 0.2);
118
+ }