@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
@@ -1,11 +1,13 @@
1
- import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
1
+ import { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'
2
2
  import { remark } from 'remark'
3
3
  import remarkGfm from 'remark-gfm'
4
4
  import remarkHtml from 'remark-html'
5
5
  import WidgetWrapper from './WidgetWrapper.jsx'
6
6
  import ResizeHandle from './ResizeHandle.jsx'
7
7
  import { readProp } from './widgetProps.js'
8
- import { schemas } from './widgetConfig.js'
8
+ import { schemas, getFeaturesForSurface } from './widgetConfig.js'
9
+ import ExpandedPane from './ExpandedPane.jsx'
10
+ import { findAllConnectedSplitTargets, getSplitPaneLabel, buildPaneForWidget, buildSplitLayout } from './expandUtils.js'
9
11
  import styles from './MarkdownBlock.module.css'
10
12
 
11
13
  const markdownSchema = schemas['markdown']
@@ -64,18 +66,28 @@ async function highlightCodeBlocks(html) {
64
66
  )
65
67
  }
66
68
 
67
- export default function MarkdownBlock({ props, onUpdate, resizable }) {
69
+ export default forwardRef(function MarkdownBlock({ id, props, onUpdate, resizable }, ref) {
68
70
  const content = readProp(props, 'content', markdownSchema)
69
71
  const width = readProp(props, 'width', markdownSchema)
70
72
  const height = props?.height
71
73
  const collapsed = !!props?.collapsed
72
74
  const canEdit = typeof onUpdate === 'function'
73
75
  const [editing, setEditing] = useState(false)
76
+ const [expandMode, setExpandMode] = useState(null)
77
+ const expanded = expandMode !== null
74
78
  const editingActive = canEdit && editing
75
79
  const textareaRef = useRef(null)
76
80
  const blockRef = useRef(null)
77
81
  const [editHeight, setEditHeight] = useState(null)
78
82
 
83
+ useImperativeHandle(ref, () => ({
84
+ handleAction(actionId) {
85
+ if (actionId === 'expand' || actionId === 'expand-single') { setExpandMode('single'); return true }
86
+ if (actionId === 'split-screen') { setExpandMode('split'); return true }
87
+ return false
88
+ },
89
+ }), [])
90
+
79
91
  const handleResize = useCallback((w, h) => {
80
92
  onUpdate?.({ width: w, height: h })
81
93
  }, [onUpdate])
@@ -93,6 +105,7 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
93
105
 
94
106
  // Async-highlight code blocks after initial render or theme change
95
107
  useEffect(() => {
108
+ // eslint-disable-next-line react-hooks/set-state-in-effect
96
109
  setRenderedHtml(rawHtml)
97
110
  if (!rawHtml.includes('<code class="language-')) return
98
111
  let cancelled = false
@@ -115,12 +128,17 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
115
128
  }
116
129
  }, [canEdit, content])
117
130
 
131
+ const startEditing = useCallback(() => {
132
+ // Capture the preview height BEFORE React swaps to the textarea
133
+ if (blockRef.current) {
134
+ setEditHeight(blockRef.current.offsetHeight)
135
+ blockRef.current.dataset.scrollTop = blockRef.current.scrollTop
136
+ }
137
+ setEditing(true)
138
+ }, [])
139
+
118
140
  useEffect(() => {
119
141
  if (editingActive) {
120
- // Capture the preview height before switching to editor
121
- if (blockRef.current && !editHeight) {
122
- setEditHeight(blockRef.current.offsetHeight)
123
- }
124
142
  if (textareaRef.current) {
125
143
  // Place cursor at end and prevent scroll jump to top
126
144
  const len = textareaRef.current.value.length
@@ -132,11 +150,13 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
132
150
  }
133
151
  }
134
152
  } else {
153
+ // eslint-disable-next-line react-hooks/set-state-in-effect
135
154
  setEditHeight(null)
136
155
  }
137
- }, [editingActive, editHeight])
156
+ }, [editingActive])
138
157
 
139
158
  return (
159
+ <>
140
160
  <WidgetWrapper>
141
161
  <div
142
162
  ref={blockRef}
@@ -170,18 +190,11 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
170
190
  data-canvas-allow-text-selection={!canEdit ? '' : undefined}
171
191
  onClick={!canEdit ? (e) => e.stopPropagation() : undefined}
172
192
  onCopy={!canEdit ? handleReadOnlyCopy : undefined}
173
- onDoubleClick={canEdit ? () => {
174
- // Save scroll position before switching to editor
175
- if (blockRef.current) blockRef.current.dataset.scrollTop = blockRef.current.scrollTop
176
- setEditing(true)
177
- } : undefined}
193
+ onDoubleClick={canEdit ? startEditing : undefined}
178
194
  role={canEdit ? 'button' : undefined}
179
195
  tabIndex={canEdit ? 0 : undefined}
180
196
  onKeyDown={canEdit ? (e) => {
181
- if (e.key === 'Enter') {
182
- if (blockRef.current) blockRef.current.dataset.scrollTop = blockRef.current.scrollTop
183
- setEditing(true)
184
- }
197
+ if (e.key === 'Enter') startEditing()
185
198
  } : undefined}
186
199
  dangerouslySetInnerHTML={{
187
200
  __html: renderedHtml || (canEdit
@@ -200,5 +213,142 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
200
213
  )}
201
214
  </div>
202
215
  </WidgetWrapper>
216
+ {expanded && (
217
+ <MarkdownExpandPane
218
+ widgetId={id}
219
+ content={content}
220
+ splitMode={expandMode === 'split'}
221
+ onClose={() => setExpandMode(null)}
222
+ onUpdate={onUpdate}
223
+ />
224
+ )}
225
+ </>
226
+ )
227
+ })
228
+
229
+ /**
230
+ * Builds pane configs and renders ExpandedPane for an expanded markdown widget.
231
+ */
232
+ function MarkdownExpandPane({ widgetId, content, splitMode, onClose, onUpdate }) {
233
+ const [editing, setEditing] = useState(false)
234
+ const canEdit = typeof onUpdate === 'function'
235
+
236
+ const connectedWidgets = useMemo(
237
+ () => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
238
+ [widgetId, splitMode],
239
+ )
240
+ const primaryWidget = useMemo(() => {
241
+ const bridge = window.__storyboardCanvasBridgeState
242
+ return bridge?.widgets?.find((w) => w.id === widgetId) || { id: widgetId, type: 'markdown', position: { x: 0, y: 0 }, props: {} }
243
+ }, [widgetId])
244
+
245
+ // Surface: fullbar for single expand, splitbar for split
246
+ const surface = splitMode ? 'splitbar' : 'fullbar'
247
+ const surfaceFeatures = useMemo(
248
+ () => canEdit ? getFeaturesForSurface('markdown', surface) : [],
249
+ [canEdit, surface],
250
+ )
251
+
252
+ const getState = useCallback((key) => {
253
+ if (key === 'editing') return editing
254
+ return undefined
255
+ }, [editing])
256
+
257
+ const handleAction = useCallback((actionId) => {
258
+ if (actionId === 'toggle-edit') {
259
+ setEditing((v) => !v)
260
+ }
261
+ }, [])
262
+
263
+ const buildPaneFn = useCallback((widget) => {
264
+ if (widget.id === widgetId) {
265
+ return {
266
+ id: widgetId,
267
+ label: getSplitPaneLabel(primaryWidget) || 'Markdown',
268
+ widgetType: 'markdown',
269
+ kind: 'react',
270
+ features: surfaceFeatures,
271
+ getState,
272
+ onAction: handleAction,
273
+ render: () => (
274
+ <ExpandedMarkdownEditor
275
+ content={content}
276
+ onUpdate={onUpdate}
277
+ editing={editing}
278
+ onToggleEdit={() => setEditing((v) => !v)}
279
+ />
280
+ ),
281
+ }
282
+ }
283
+ return buildPaneForWidget(widget, surface)
284
+ }, [widgetId, primaryWidget, content, onUpdate, editing, surfaceFeatures, getState, handleAction, surface])
285
+
286
+ const layout = useMemo(
287
+ () => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
288
+ [primaryWidget, connectedWidgets, buildPaneFn],
289
+ )
290
+
291
+ return (
292
+ <ExpandedPane
293
+ initialLayout={layout}
294
+ variant={layout.flat().length <= 1 ? 'modal' : 'full'}
295
+ onClose={onClose}
296
+ />
297
+ )
298
+ }
299
+
300
+ /**
301
+ * Editable markdown view for expanded/split-screen panes.
302
+ * Self-contained: renders markdown from raw content with syntax highlighting.
303
+ * Editing state is controlled externally via props (toggle button lives in the title bar).
304
+ */
305
+ export function ExpandedMarkdownEditor({ content, onUpdate, editing, onToggleEdit }) {
306
+ const textareaRef = useRef(null)
307
+ const canEdit = typeof onUpdate === 'function'
308
+
309
+ const rawHtml = useMemo(() => renderMarkdown(content), [content])
310
+ const [renderedHtml, setRenderedHtml] = useState(rawHtml)
311
+
312
+ useEffect(() => {
313
+ // eslint-disable-next-line react-hooks/set-state-in-effect
314
+ setRenderedHtml(rawHtml)
315
+ if (!rawHtml.includes('<code class="language-')) return
316
+ let cancelled = false
317
+ highlightCodeBlocks(rawHtml).then((highlighted) => {
318
+ if (!cancelled) setRenderedHtml(highlighted)
319
+ })
320
+ return () => { cancelled = true }
321
+ }, [rawHtml])
322
+
323
+ useEffect(() => {
324
+ if (editing && textareaRef.current) {
325
+ const len = textareaRef.current.value.length
326
+ textareaRef.current.setSelectionRange(len, len)
327
+ textareaRef.current.focus({ preventScroll: true })
328
+ }
329
+ }, [editing])
330
+
331
+ if (editing && canEdit) {
332
+ return (
333
+ <textarea
334
+ ref={textareaRef}
335
+ className={styles.expandedEditor}
336
+ value={content}
337
+ onChange={(e) => onUpdate({ content: e.target.value })}
338
+ onKeyDown={(e) => { if (e.key === 'Escape') onToggleEdit?.() }}
339
+ placeholder="Write markdown…"
340
+ />
341
+ )
342
+ }
343
+
344
+ return (
345
+ <div
346
+ className={styles.expandedPreview}
347
+ style={{ flex: 1, overflow: 'auto' }}
348
+ onDoubleClick={canEdit ? onToggleEdit : undefined}
349
+ dangerouslySetInnerHTML={{
350
+ __html: renderedHtml || '<p>No content</p>',
351
+ }}
352
+ />
203
353
  )
204
354
  }
@@ -227,3 +227,151 @@
227
227
  color: var(--sb--markdown-fg);
228
228
  resize: none;
229
229
  }
230
+
231
+ /* ── Expanded preview in modal ──────────────────────────────────── */
232
+
233
+ .expandedPreview {
234
+ --sb--markdown-bg: var(--bgColor-default, #ffffff);
235
+ --sb--markdown-fg: var(--fgColor-default, #1f2328);
236
+ --sb--markdown-muted: var(--fgColor-muted, #656d76);
237
+ --sb--markdown-accent: var(--bgColor-accent-emphasis, #2f81f7);
238
+ padding: 32px 40px;
239
+ font-size: 15px;
240
+ line-height: 1.7;
241
+ color: var(--sb--markdown-fg);
242
+ background: var(--sb--markdown-bg);
243
+ font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
244
+ max-width: 800px;
245
+ margin: 0 auto;
246
+ }
247
+
248
+ .expandedPreview * {
249
+ pointer-events: auto;
250
+ color: inherit;
251
+ }
252
+
253
+ .expandedPreview a {
254
+ color: var(--sb--markdown-accent);
255
+ text-decoration: none;
256
+ cursor: pointer;
257
+ }
258
+
259
+ .expandedPreview a:hover {
260
+ text-decoration: underline;
261
+ }
262
+
263
+ .expandedPreview img {
264
+ max-width: 100%;
265
+ height: auto;
266
+ border-radius: 6px;
267
+ border: 1px solid var(--borderColor-default, #d0d7de);
268
+ margin: 8px 0;
269
+ display: block;
270
+ }
271
+
272
+ .expandedPreview video {
273
+ max-width: 100%;
274
+ height: auto;
275
+ border-radius: 6px;
276
+ border: 1px solid var(--borderColor-default, #d0d7de);
277
+ margin: 8px 0;
278
+ display: block;
279
+ }
280
+
281
+ .expandedPreview h1 { font-size: 24px; font-weight: 700; margin: 0 0 12px; line-height: 1.3; }
282
+ .expandedPreview h2 { font-size: 20px; font-weight: 600; margin: 0 0 10px; line-height: 1.3; }
283
+ .expandedPreview h3 { font-size: 17px; font-weight: 600; margin: 0 0 6px; line-height: 1.3; }
284
+ .expandedPreview p { margin: 0 0 12px; }
285
+
286
+ .expandedPreview code {
287
+ background: var(--bgColor-neutral-muted, #afb8c133);
288
+ padding: 2px 5px;
289
+ border-radius: 4px;
290
+ font-size: 13px;
291
+ font-family: ui-monospace, SFMono-Regular, monospace;
292
+ }
293
+
294
+ .expandedPreview pre {
295
+ padding: 12px 16px;
296
+ border-radius: 6px;
297
+ border: 1px solid var(--borderColor-muted, #d8dee4);
298
+ overflow-x: auto;
299
+ margin: 8px 0;
300
+ background: var(--bgColor-neutral-muted, #afb8c133);
301
+ line-height: 1.4;
302
+ }
303
+
304
+ .expandedPreview pre code {
305
+ background: none;
306
+ padding: 0;
307
+ font-size: 13px;
308
+ white-space: pre;
309
+ display: block;
310
+ }
311
+
312
+ .expandedPreview ul { margin: 0 0 12px; padding-left: 24px; list-style: disc; }
313
+ .expandedPreview ol { margin: 0 0 12px; padding-left: 24px; list-style: decimal; }
314
+ .expandedPreview li { margin: 0 0 4px; display: list-item; }
315
+
316
+ .expandedPreview blockquote {
317
+ border-left: 4px solid var(--borderColor-default, #d0d7de);
318
+ margin: 12px 0;
319
+ padding: 4px 16px;
320
+ color: var(--sb--markdown-muted);
321
+ }
322
+
323
+ .expandedPreview table {
324
+ border-collapse: collapse;
325
+ margin: 12px 0;
326
+ width: 100%;
327
+ font-size: 14px;
328
+ }
329
+
330
+ .expandedPreview th,
331
+ .expandedPreview td {
332
+ border: 1px solid var(--borderColor-default, #d0d7de);
333
+ padding: 6px 12px;
334
+ text-align: left;
335
+ }
336
+
337
+ .expandedPreview th {
338
+ background: var(--bgColor-muted, #f6f8fa);
339
+ font-weight: 600;
340
+ }
341
+
342
+ .expandedPreview hr {
343
+ border: none;
344
+ border-top: 1px solid var(--borderColor-default, #d0d7de);
345
+ margin: 16px 0;
346
+ }
347
+
348
+ .expandedPreview input[type="checkbox"] {
349
+ margin-right: 6px;
350
+ pointer-events: none;
351
+ accent-color: var(--sb--markdown-accent);
352
+ }
353
+
354
+ .expandedPreview li:has(input[type="checkbox"]) {
355
+ list-style: none;
356
+ margin-left: -24px;
357
+ }
358
+
359
+ /* ── Expanded editor ─────────────────────────────────────────────── */
360
+
361
+ .expandedEditor {
362
+ display: block;
363
+ width: 100%;
364
+ height: 100%;
365
+ box-sizing: border-box;
366
+ padding: 32px 40px;
367
+ border: none;
368
+ outline: none;
369
+ resize: none;
370
+ background: var(--bgColor-default, #ffffff);
371
+ font-family: ui-monospace, SFMono-Regular, monospace;
372
+ font-size: 14px;
373
+ line-height: 1.6;
374
+ color: var(--fgColor-default, #1f2328);
375
+ max-width: 800px;
376
+ margin: 0 auto;
377
+ }