@dfosco/storyboard-react 4.0.0-beta.35 โ†’ 4.0.0-beta.37

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.
@@ -1663,6 +1663,20 @@ export default function CanvasPage({ canvasId: canvasIdProp, name, siblingPages
1663
1663
  return () => document.removeEventListener('wheel', handleWheel)
1664
1664
  }, [])
1665
1665
 
1666
+ // Receive cmd+wheel events forwarded from prototype/story iframes
1667
+ useEffect(() => {
1668
+ function handleMessage(e) {
1669
+ if (e.data?.type !== 'storyboard:embed:wheel') return
1670
+ zoomAccum.current += -e.data.deltaY
1671
+ const step = Math.trunc(zoomAccum.current)
1672
+ if (step === 0) return
1673
+ zoomAccum.current -= step
1674
+ applyZoom(zoomRef.current + step)
1675
+ }
1676
+ window.addEventListener('message', handleMessage)
1677
+ return () => window.removeEventListener('message', handleMessage)
1678
+ }, [])
1679
+
1666
1680
  // Touch pinch-to-zoom for mobile โ€” two-finger pinch zooms the canvas
1667
1681
  const pinchState = useRef({ active: false, startDist: 0, startZoom: 0, centerX: 0, centerY: 0 })
1668
1682
  useEffect(() => {
@@ -228,6 +228,38 @@ export default function LinkPreview({ id, props, onUpdate, resizable }) {
228
228
  )
229
229
  }
230
230
 
231
+ const ogImage = props?.ogImage || null
232
+ const description = props?.description || ''
233
+ const canEdit = typeof onUpdate === 'function'
234
+ const cardRef = useRef(null)
235
+ const inputRef = useRef(null)
236
+ const [editing, setEditing] = useState(false)
237
+ const [editValue, setEditValue] = useState(title)
238
+
239
+ // Sync editValue when title prop changes externally
240
+ useEffect(() => { setEditValue(title) }, [title])
241
+
242
+ const startEditing = useCallback(() => {
243
+ if (!canEdit) return
244
+ setEditValue(title)
245
+ setEditing(true)
246
+ }, [canEdit, title])
247
+
248
+ const commitEdit = useCallback(() => {
249
+ setEditing(false)
250
+ const trimmed = editValue.trim()
251
+ if (trimmed !== title) {
252
+ onUpdate?.({ title: trimmed })
253
+ }
254
+ }, [editValue, title, onUpdate])
255
+
256
+ useEffect(() => {
257
+ if (editing && inputRef.current) {
258
+ inputRef.current.focus()
259
+ inputRef.current.select()
260
+ }
261
+ }, [editing])
262
+
231
263
  const sizeStyle = (width || height)
232
264
  ? { ...(width ? { width: `${width}px` } : {}), ...(height ? { minHeight: `${height}px` } : {}) }
233
265
  : undefined
@@ -238,14 +270,46 @@ export default function LinkPreview({ id, props, onUpdate, resizable }) {
238
270
  const handleResize = (w, h) => onUpdate?.({ width: w, height: h })
239
271
 
240
272
  return (
241
- <WidgetWrapper>
242
- <div className={styles.card} style={sizeStyle}>
243
- <div className={styles.header}>
244
- <span className={styles.icon}>๐Ÿ”—</span>
245
- <div className={styles.text}>
246
- {title && <p className={styles.title}>{title}</p>}
247
- </div>
248
- </div>
273
+ <div ref={cardRef} className={styles.card} style={sizeStyle}>
274
+ {ogImage && (
275
+ <img
276
+ className={styles.ogImage}
277
+ src={ogImage}
278
+ alt=""
279
+ loading="lazy"
280
+ onError={(e) => { e.target.style.display = 'none' }}
281
+ />
282
+ )}
283
+ <div className={styles.body}>
284
+ {editing ? (
285
+ <input
286
+ ref={inputRef}
287
+ className={styles.titleInput}
288
+ data-canvas-allow-text-selection
289
+ type="text"
290
+ value={editValue}
291
+ onChange={(e) => setEditValue(e.target.value)}
292
+ onBlur={commitEdit}
293
+ onKeyDown={(e) => {
294
+ if (e.key === 'Enter') commitEdit()
295
+ if (e.key === 'Escape') setEditing(false)
296
+ }}
297
+ onMouseDown={(e) => e.stopPropagation()}
298
+ onPointerDown={(e) => e.stopPropagation()}
299
+ />
300
+ ) : (
301
+ <p
302
+ className={styles.title}
303
+ data-canvas-allow-text-selection={!canEdit ? '' : undefined}
304
+ onDoubleClick={startEditing}
305
+ role={canEdit ? 'button' : undefined}
306
+ tabIndex={canEdit ? 0 : undefined}
307
+ onKeyDown={canEdit ? (e) => { if (e.key === 'Enter') startEditing() } : undefined}
308
+ >
309
+ {title || hostname || url || 'Untitled'}
310
+ </p>
311
+ )}
312
+ {description && <p className={styles.description}>{description}</p>}
249
313
  <a
250
314
  href={url || '#'}
251
315
  target="_blank"
@@ -257,7 +321,7 @@ export default function LinkPreview({ id, props, onUpdate, resizable }) {
257
321
  {hostname || url}
258
322
  </a>
259
323
  </div>
260
- {resizable && <ResizeHandle width={width} height={height} onResize={handleResize} />}
261
- </WidgetWrapper>
324
+ {resizable && <ResizeHandle targetRef={cardRef} width={width} height={height} onResize={handleResize} />}
325
+ </div>
262
326
  )
263
327
  }
@@ -87,7 +87,7 @@
87
87
  border-radius: 4px;
88
88
  font-size: 12px;
89
89
  font-weight: 400;
90
- font-family: "Ioskeley Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
90
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
91
91
  }
92
92
 
93
93
  .preview ul {
@@ -169,7 +169,7 @@
169
169
  padding: 0;
170
170
  font-size: 12px;
171
171
  font-weight: 400;
172
- font-family: "Ioskeley Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
172
+ font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
173
173
  white-space: pre;
174
174
  word-break: normal;
175
175
  overflow-wrap: normal;
@@ -133,19 +133,22 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
133
133
  .filter(Boolean)
134
134
  }, [pickerGroups, filter])
135
135
 
136
- const prototypeName = useMemo(() => {
137
- if (!src) return ''
136
+ const prototypeTitle = useMemo(() => {
137
+ if (!src) return label || 'Prototype'
138
+ const cleanSrc = src.replace(/^\/branch--[^/]+/, '')
138
139
  for (const group of pickerGroups) {
139
140
  for (const item of group.items) {
140
141
  const cleanRoute = item.route.replace(/^\/branch--[^/]+/, '')
141
- const cleanSrc = src.replace(/^\/branch--[^/]+/, '')
142
- if (cleanRoute === cleanSrc) return item.name
142
+ if (cleanRoute === cleanSrc) {
143
+ // If the flow name matches the group name, just show the name
144
+ if (item.name === group.label) return group.label
145
+ return `${group.label} ยท ${item.name}`
146
+ }
143
147
  }
144
148
  }
145
- return ''
146
- }, [src, pickerGroups])
149
+ return label || 'Prototype'
150
+ }, [src, label, pickerGroups])
147
151
 
148
- const prototypeTitle = prototypeName || label || 'Prototype'
149
152
  const hasPicker = pickerGroups.length > 0
150
153
 
151
154
  useIframeDevLogs({
@@ -400,7 +403,7 @@ export default forwardRef(function PrototypeEmbed({ id: widgetId, props, onUpdat
400
403
  </div>
401
404
  )}
402
405
  </div>
403
- {resizable && <ResizeHandle width={width} height={height} onResize={handleResize} />}
406
+ {resizable && <ResizeHandle targetRef={embedRef} width={width} height={height} onResize={handleResize} />}
404
407
  </WidgetWrapper>
405
408
  {createPortal(
406
409
  <div