@dfosco/storyboard-react 4.2.0-beta.1 → 4.2.0-beta.18

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 (85) hide show
  1. package/package.json +5 -4
  2. package/src/AuthModal/AuthModal.jsx +6 -2
  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 +478 -186
  8. package/src/CommandPalette/command-palette.css +142 -78
  9. package/src/Icon.jsx +157 -58
  10. package/src/Viewfinder.jsx +561 -191
  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 +10 -6
  15. package/src/canvas/CanvasPage.jsx +738 -216
  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 -153
  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/connectorGeometry.js +132 -0
  23. package/src/canvas/hotPoolDevLogs.js +25 -0
  24. package/src/canvas/useCanvas.js +1 -1
  25. package/src/canvas/useMarqueeSelect.js +30 -4
  26. package/src/canvas/widgets/CodePenEmbed.jsx +1 -0
  27. package/src/canvas/widgets/ComponentSetWidget.jsx +199 -0
  28. package/src/canvas/widgets/ComponentSetWidget.module.css +89 -0
  29. package/src/canvas/widgets/ComponentWidget.jsx +1 -0
  30. package/src/canvas/widgets/CropOverlay.jsx +219 -0
  31. package/src/canvas/widgets/CropOverlay.module.css +118 -0
  32. package/src/canvas/widgets/ExpandedPane.jsx +472 -0
  33. package/src/canvas/widgets/ExpandedPane.module.css +179 -0
  34. package/src/canvas/widgets/ExpandedPane.test.jsx +240 -0
  35. package/src/canvas/widgets/ExpandedPaneTopBar.jsx +111 -0
  36. package/src/canvas/widgets/ExpandedPaneTopBar.module.css +59 -0
  37. package/src/canvas/widgets/ExpandedPaneTopBar.test.jsx +45 -0
  38. package/src/canvas/widgets/FigmaEmbed.jsx +62 -47
  39. package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
  40. package/src/canvas/widgets/ImageWidget.jsx +130 -9
  41. package/src/canvas/widgets/ImageWidget.module.css +30 -0
  42. package/src/canvas/widgets/LinkPreview.jsx +112 -4
  43. package/src/canvas/widgets/LinkPreview.module.css +127 -0
  44. package/src/canvas/widgets/MarkdownBlock.jsx +164 -17
  45. package/src/canvas/widgets/MarkdownBlock.module.css +148 -0
  46. package/src/canvas/widgets/PromptWidget.jsx +414 -0
  47. package/src/canvas/widgets/PromptWidget.module.css +273 -0
  48. package/src/canvas/widgets/PrototypeEmbed.jsx +77 -38
  49. package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
  50. package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
  51. package/src/canvas/widgets/ResizeHandle.jsx +17 -6
  52. package/src/canvas/widgets/StoryWidget.jsx +72 -15
  53. package/src/canvas/widgets/TerminalReadWidget.jsx +146 -0
  54. package/src/canvas/widgets/TerminalReadWidget.module.css +94 -0
  55. package/src/canvas/widgets/TerminalWidget.jsx +496 -69
  56. package/src/canvas/widgets/TerminalWidget.module.css +271 -8
  57. package/src/canvas/widgets/TilesWidget.jsx +302 -0
  58. package/src/canvas/widgets/TilesWidget.module.css +133 -0
  59. package/src/canvas/widgets/WidgetChrome.jsx +73 -153
  60. package/src/canvas/widgets/WidgetChrome.module.css +30 -1
  61. package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
  62. package/src/canvas/widgets/expandUtils.js +557 -0
  63. package/src/canvas/widgets/expandUtils.test.js +155 -0
  64. package/src/canvas/widgets/index.js +9 -0
  65. package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
  66. package/src/canvas/widgets/tilePool.js +23 -0
  67. package/src/canvas/widgets/tiles/diagonal-bl.png +0 -0
  68. package/src/canvas/widgets/tiles/diagonal-br.png +0 -0
  69. package/src/canvas/widgets/tiles/diagonal-tl.png +0 -0
  70. package/src/canvas/widgets/tiles/leaf.png +0 -0
  71. package/src/canvas/widgets/tiles/quarter-tl.png +0 -0
  72. package/src/canvas/widgets/tiles/quarter-tr.png +0 -0
  73. package/src/canvas/widgets/tiles/solid-a.png +0 -0
  74. package/src/canvas/widgets/tiles/solid-b.png +0 -0
  75. package/src/canvas/widgets/widgetConfig.js +55 -4
  76. package/src/canvas/widgets/widgetIcons.jsx +190 -0
  77. package/src/canvas/widgets/widgetProps.js +1 -0
  78. package/src/context.jsx +47 -19
  79. package/src/hooks/useConfig.js +14 -0
  80. package/src/hooks/usePrototypeReloadGuard.js +64 -0
  81. package/src/index.js +8 -2
  82. package/src/story/ComponentSetPage.jsx +186 -0
  83. package/src/story/ComponentSetPage.module.css +121 -0
  84. package/src/story/StoryPage.jsx +32 -2
  85. package/src/vite/data-plugin.js +324 -30
@@ -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])
@@ -115,12 +127,17 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
115
127
  }
116
128
  }, [canEdit, content])
117
129
 
130
+ const startEditing = useCallback(() => {
131
+ // Capture the preview height BEFORE React swaps to the textarea
132
+ if (blockRef.current) {
133
+ setEditHeight(blockRef.current.offsetHeight)
134
+ blockRef.current.dataset.scrollTop = blockRef.current.scrollTop
135
+ }
136
+ setEditing(true)
137
+ }, [])
138
+
118
139
  useEffect(() => {
119
140
  if (editingActive) {
120
- // Capture the preview height before switching to editor
121
- if (blockRef.current && !editHeight) {
122
- setEditHeight(blockRef.current.offsetHeight)
123
- }
124
141
  if (textareaRef.current) {
125
142
  // Place cursor at end and prevent scroll jump to top
126
143
  const len = textareaRef.current.value.length
@@ -134,9 +151,10 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
134
151
  } else {
135
152
  setEditHeight(null)
136
153
  }
137
- }, [editingActive, editHeight])
154
+ }, [editingActive])
138
155
 
139
156
  return (
157
+ <>
140
158
  <WidgetWrapper>
141
159
  <div
142
160
  ref={blockRef}
@@ -170,18 +188,11 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
170
188
  data-canvas-allow-text-selection={!canEdit ? '' : undefined}
171
189
  onClick={!canEdit ? (e) => e.stopPropagation() : undefined}
172
190
  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}
191
+ onDoubleClick={canEdit ? startEditing : undefined}
178
192
  role={canEdit ? 'button' : undefined}
179
193
  tabIndex={canEdit ? 0 : undefined}
180
194
  onKeyDown={canEdit ? (e) => {
181
- if (e.key === 'Enter') {
182
- if (blockRef.current) blockRef.current.dataset.scrollTop = blockRef.current.scrollTop
183
- setEditing(true)
184
- }
195
+ if (e.key === 'Enter') startEditing()
185
196
  } : undefined}
186
197
  dangerouslySetInnerHTML={{
187
198
  __html: renderedHtml || (canEdit
@@ -200,5 +211,141 @@ export default function MarkdownBlock({ props, onUpdate, resizable }) {
200
211
  )}
201
212
  </div>
202
213
  </WidgetWrapper>
214
+ {expanded && (
215
+ <MarkdownExpandPane
216
+ widgetId={id}
217
+ content={content}
218
+ splitMode={expandMode === 'split'}
219
+ onClose={() => setExpandMode(null)}
220
+ onUpdate={onUpdate}
221
+ />
222
+ )}
223
+ </>
224
+ )
225
+ })
226
+
227
+ /**
228
+ * Builds pane configs and renders ExpandedPane for an expanded markdown widget.
229
+ */
230
+ function MarkdownExpandPane({ widgetId, content, splitMode, onClose, onUpdate }) {
231
+ const [editing, setEditing] = useState(false)
232
+ const canEdit = typeof onUpdate === 'function'
233
+
234
+ const connectedWidgets = useMemo(
235
+ () => splitMode ? findAllConnectedSplitTargets(widgetId) : [],
236
+ [widgetId, splitMode],
237
+ )
238
+ const primaryWidget = useMemo(() => {
239
+ const bridge = window.__storyboardCanvasBridgeState
240
+ return bridge?.widgets?.find((w) => w.id === widgetId) || { id: widgetId, type: 'markdown', position: { x: 0, y: 0 }, props: {} }
241
+ }, [widgetId])
242
+
243
+ // Surface: fullbar for single expand, splitbar for split
244
+ const surface = splitMode ? 'splitbar' : 'fullbar'
245
+ const surfaceFeatures = useMemo(
246
+ () => canEdit ? getFeaturesForSurface('markdown', surface) : [],
247
+ [canEdit, surface],
248
+ )
249
+
250
+ const getState = useCallback((key) => {
251
+ if (key === 'editing') return editing
252
+ return undefined
253
+ }, [editing])
254
+
255
+ const handleAction = useCallback((actionId) => {
256
+ if (actionId === 'toggle-edit') {
257
+ setEditing((v) => !v)
258
+ }
259
+ }, [])
260
+
261
+ const buildPaneFn = useCallback((widget) => {
262
+ if (widget.id === widgetId) {
263
+ return {
264
+ id: widgetId,
265
+ label: getSplitPaneLabel(primaryWidget) || 'Markdown',
266
+ widgetType: 'markdown',
267
+ kind: 'react',
268
+ features: surfaceFeatures,
269
+ getState,
270
+ onAction: handleAction,
271
+ render: () => (
272
+ <ExpandedMarkdownEditor
273
+ content={content}
274
+ onUpdate={onUpdate}
275
+ editing={editing}
276
+ onToggleEdit={() => setEditing((v) => !v)}
277
+ />
278
+ ),
279
+ }
280
+ }
281
+ return buildPaneForWidget(widget, surface)
282
+ }, [widgetId, primaryWidget, content, onUpdate, editing, surfaceFeatures, getState, handleAction, surface])
283
+
284
+ const layout = useMemo(
285
+ () => buildSplitLayout(primaryWidget, connectedWidgets, buildPaneFn),
286
+ [primaryWidget, connectedWidgets, buildPaneFn],
287
+ )
288
+
289
+ return (
290
+ <ExpandedPane
291
+ initialLayout={layout}
292
+ variant={layout.flat().length <= 1 ? 'modal' : 'full'}
293
+ onClose={onClose}
294
+ />
295
+ )
296
+ }
297
+
298
+ /**
299
+ * Editable markdown view for expanded/split-screen panes.
300
+ * Self-contained: renders markdown from raw content with syntax highlighting.
301
+ * Editing state is controlled externally via props (toggle button lives in the title bar).
302
+ */
303
+ export function ExpandedMarkdownEditor({ content, onUpdate, editing, onToggleEdit }) {
304
+ const textareaRef = useRef(null)
305
+ const canEdit = typeof onUpdate === 'function'
306
+
307
+ const rawHtml = useMemo(() => renderMarkdown(content), [content])
308
+ const [renderedHtml, setRenderedHtml] = useState(rawHtml)
309
+
310
+ useEffect(() => {
311
+ setRenderedHtml(rawHtml)
312
+ if (!rawHtml.includes('<code class="language-')) return
313
+ let cancelled = false
314
+ highlightCodeBlocks(rawHtml).then((highlighted) => {
315
+ if (!cancelled) setRenderedHtml(highlighted)
316
+ })
317
+ return () => { cancelled = true }
318
+ }, [rawHtml])
319
+
320
+ useEffect(() => {
321
+ if (editing && textareaRef.current) {
322
+ const len = textareaRef.current.value.length
323
+ textareaRef.current.setSelectionRange(len, len)
324
+ textareaRef.current.focus({ preventScroll: true })
325
+ }
326
+ }, [editing])
327
+
328
+ if (editing && canEdit) {
329
+ return (
330
+ <textarea
331
+ ref={textareaRef}
332
+ className={styles.expandedEditor}
333
+ value={content}
334
+ onChange={(e) => onUpdate({ content: e.target.value })}
335
+ onKeyDown={(e) => { if (e.key === 'Escape') onToggleEdit?.() }}
336
+ placeholder="Write markdown…"
337
+ />
338
+ )
339
+ }
340
+
341
+ return (
342
+ <div
343
+ className={styles.expandedPreview}
344
+ style={{ flex: 1, overflow: 'auto' }}
345
+ onDoubleClick={canEdit ? onToggleEdit : undefined}
346
+ dangerouslySetInnerHTML={{
347
+ __html: renderedHtml || '<p>No content</p>',
348
+ }}
349
+ />
203
350
  )
204
351
  }
@@ -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
+ }