@dfosco/storyboard-react 3.11.0-beta.6 → 3.11.0-beta.8

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.
@@ -1,4 +1,5 @@
1
- import { forwardRef, useImperativeHandle, useMemo, useCallback, useState } from 'react'
1
+ import { forwardRef, useImperativeHandle, useMemo, useCallback, useState, useEffect, useRef } from 'react'
2
+ import { createPortal } from 'react-dom'
2
3
  import WidgetWrapper from './WidgetWrapper.jsx'
3
4
  import { readProp } from './widgetProps.js'
4
5
  import { schemas } from './widgetConfig.js'
@@ -22,12 +23,17 @@ function FigmaLogo() {
22
23
 
23
24
  const TYPE_LABELS = { board: 'Board', design: 'Design', proto: 'Prototype' }
24
25
 
25
- export default forwardRef(function FigmaEmbed({ props, onUpdate }, ref) {
26
+ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, ref) {
26
27
  const url = readProp(props, 'url', figmaEmbedSchema)
27
28
  const width = readProp(props, 'width', figmaEmbedSchema)
28
29
  const height = readProp(props, 'height', figmaEmbedSchema)
29
30
 
30
31
  const [interactive, setInteractive] = useState(false)
32
+ const [expanded, setExpanded] = useState(false)
33
+
34
+ const iframeRef = useRef(null)
35
+ const inlineContainerRef = useRef(null)
36
+ const modalContainerRef = useRef(null)
31
37
 
32
38
  // Validate URL at render time — only embed known Figma URLs
33
39
  const isValid = useMemo(() => isFigmaUrl(url), [url])
@@ -38,15 +44,65 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate }, ref) {
38
44
 
39
45
  const enterInteractive = useCallback(() => setInteractive(true), [])
40
46
 
47
+ // Close expanded modal on Escape
48
+ useEffect(() => {
49
+ if (!expanded) return
50
+ function handleKeyDown(e) {
51
+ if (e.key === 'Escape') {
52
+ e.stopPropagation()
53
+ setExpanded(false)
54
+ }
55
+ }
56
+ document.addEventListener('keydown', handleKeyDown, true)
57
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
58
+ }, [expanded])
59
+
60
+ // Reparent iframe DOM node between inline container and modal.
61
+ // Uses moveBefore() (Chrome 133+) which preserves the iframe's
62
+ // browsing context — no reload. Falls back to appendChild.
63
+ useEffect(() => {
64
+ const iframe = iframeRef.current
65
+ if (!iframe) return
66
+
67
+ if (expanded && modalContainerRef.current) {
68
+ iframe._savedClassName = iframe.className
69
+ iframe._savedStyle = iframe.getAttribute('style') || ''
70
+ iframe.className = styles.expandIframe
71
+ iframe.removeAttribute('style')
72
+ const target = modalContainerRef.current
73
+ if (target.moveBefore) {
74
+ target.moveBefore(iframe, target.firstChild)
75
+ } else {
76
+ target.prepend(iframe)
77
+ }
78
+ } else if (!expanded && inlineContainerRef.current) {
79
+ if (iframe._savedClassName !== undefined) {
80
+ iframe.className = iframe._savedClassName
81
+ iframe.setAttribute('style', iframe._savedStyle)
82
+ delete iframe._savedClassName
83
+ delete iframe._savedStyle
84
+ }
85
+ const target = inlineContainerRef.current
86
+ if (target.moveBefore) {
87
+ target.moveBefore(iframe, null)
88
+ } else {
89
+ target.appendChild(iframe)
90
+ }
91
+ }
92
+ }, [expanded])
93
+
41
94
  useImperativeHandle(ref, () => ({
42
95
  handleAction(actionId) {
43
96
  if (actionId === 'open-external') {
44
97
  if (url) window.open(url, '_blank', 'noopener')
98
+ } else if (actionId === 'expand') {
99
+ setExpanded(true)
45
100
  }
46
101
  },
47
102
  }), [url])
48
103
 
49
104
  return (
105
+ <>
50
106
  <WidgetWrapper>
51
107
  <div className={styles.embed} style={{ width, height }}>
52
108
  <div className={styles.header}>
@@ -55,15 +111,20 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate }, ref) {
55
111
  </div>
56
112
  {embedUrl ? (
57
113
  <>
58
- <div className={styles.iframeContainer}>
114
+ <div
115
+ ref={inlineContainerRef}
116
+ className={styles.iframeContainer}
117
+ style={expanded ? { visibility: 'hidden' } : undefined}
118
+ >
59
119
  <iframe
120
+ ref={iframeRef}
60
121
  src={embedUrl}
61
122
  className={styles.iframe}
62
123
  title={`Figma ${typeLabel}: ${title}`}
63
124
  allowFullScreen
64
125
  />
65
126
  </div>
66
- {!interactive && (
127
+ {!interactive && !expanded && (
67
128
  <div
68
129
  className={styles.dragOverlay}
69
130
  onDoubleClick={enterInteractive}
@@ -78,29 +139,57 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate }, ref) {
78
139
  </div>
79
140
  )}
80
141
  </div>
142
+ {resizable && (
143
+ <div
144
+ className={styles.resizeHandle}
145
+ onMouseDown={(e) => {
146
+ e.stopPropagation()
147
+ e.preventDefault()
148
+ const startX = e.clientX
149
+ const startY = e.clientY
150
+ const startW = width
151
+ const startH = height
152
+ function onMove(ev) {
153
+ const newW = Math.max(200, startW + ev.clientX - startX)
154
+ const newH = Math.max(150, startH + ev.clientY - startY)
155
+ onUpdate?.({ width: newW, height: newH })
156
+ }
157
+ function onUp() {
158
+ document.removeEventListener('mousemove', onMove)
159
+ document.removeEventListener('mouseup', onUp)
160
+ }
161
+ document.addEventListener('mousemove', onMove)
162
+ document.addEventListener('mouseup', onUp)
163
+ }}
164
+ onPointerDown={(e) => e.stopPropagation()}
165
+ />
166
+ )}
167
+ </WidgetWrapper>
168
+ {createPortal(
81
169
  <div
82
- className={styles.resizeHandle}
83
- onMouseDown={(e) => {
84
- e.stopPropagation()
85
- e.preventDefault()
86
- const startX = e.clientX
87
- const startY = e.clientY
88
- const startW = width
89
- const startH = height
90
- function onMove(ev) {
91
- const newW = Math.max(200, startW + ev.clientX - startX)
92
- const newH = Math.max(150, startH + ev.clientY - startY)
93
- onUpdate?.({ width: newW, height: newH })
94
- }
95
- function onUp() {
96
- document.removeEventListener('mousemove', onMove)
97
- document.removeEventListener('mouseup', onUp)
98
- }
99
- document.addEventListener('mousemove', onMove)
100
- document.addEventListener('mouseup', onUp)
101
- }}
170
+ className={styles.expandBackdrop}
171
+ style={expanded && embedUrl ? undefined : { display: 'none' }}
172
+ onClick={() => setExpanded(false)}
102
173
  onPointerDown={(e) => e.stopPropagation()}
103
- />
104
- </WidgetWrapper>
174
+ onKeyDown={(e) => e.stopPropagation()}
175
+ onWheel={(e) => e.stopPropagation()}
176
+ >
177
+ <div
178
+ ref={modalContainerRef}
179
+ className={styles.expandContainer}
180
+ onClick={(e) => e.stopPropagation()}
181
+ >
182
+ {/* iframe is reparented here via useEffect */}
183
+ <button
184
+ className={styles.expandClose}
185
+ onClick={() => setExpanded(false)}
186
+ aria-label="Close expanded view"
187
+ autoFocus
188
+ >✕</button>
189
+ </div>
190
+ </div>,
191
+ document.body
192
+ )}
193
+ </>
105
194
  )
106
195
  })
@@ -36,7 +36,7 @@
36
36
 
37
37
  .iframeContainer {
38
38
  width: 100%;
39
- height: calc(100% - 31px); /* subtract header height */
39
+ height: calc(100% - 10px); /* subtract header height */
40
40
  overflow: hidden;
41
41
  }
42
42
 
@@ -81,3 +81,67 @@
81
81
  .resizeHandle:hover {
82
82
  opacity: 1;
83
83
  }
84
+
85
+ /* Expand modal — fullscreen overlay for expanded iframe */
86
+ .expandBackdrop {
87
+ position: fixed;
88
+ inset: 0;
89
+ z-index: 100000;
90
+ background: rgba(0, 0, 0, 0.8);
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ animation: expandFadeIn 0.15s ease;
95
+ }
96
+
97
+ @keyframes expandFadeIn {
98
+ from { opacity: 0; }
99
+ to { opacity: 1; }
100
+ }
101
+
102
+ .expandContainer {
103
+ width: 90vw;
104
+ height: 90vh;
105
+ position: relative;
106
+ border-radius: 12px;
107
+ overflow: hidden;
108
+ background: var(--bgColor-default, #ffffff);
109
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
110
+ animation: expandScaleIn 0.2s ease;
111
+ }
112
+
113
+ @keyframes expandScaleIn {
114
+ from { transform: scale(0.95); opacity: 0; }
115
+ to { transform: scale(1); opacity: 1; }
116
+ }
117
+
118
+ .expandIframe {
119
+ border: none;
120
+ display: block;
121
+ width: 100%;
122
+ height: 100%;
123
+ }
124
+
125
+ .expandClose {
126
+ all: unset;
127
+ cursor: pointer;
128
+ position: absolute;
129
+ top: 12px;
130
+ right: 12px;
131
+ width: 32px;
132
+ height: 32px;
133
+ display: flex;
134
+ align-items: center;
135
+ justify-content: center;
136
+ border-radius: 8px;
137
+ background: rgba(0, 0, 0, 0.5);
138
+ color: #ffffff;
139
+ font-size: 16px;
140
+ z-index: 1;
141
+ transition: background 100ms;
142
+ backdrop-filter: blur(4px);
143
+ }
144
+
145
+ .expandClose:hover {
146
+ background: rgba(0, 0, 0, 0.7);
147
+ }
@@ -18,7 +18,7 @@ function getImageUrl(src) {
18
18
  * Canvas widget that displays a pasted image.
19
19
  * Supports aspect-ratio locked resize and privacy toggle.
20
20
  */
21
- const ImageWidget = forwardRef(function ImageWidget({ props, onUpdate }, ref) {
21
+ const ImageWidget = forwardRef(function ImageWidget({ props, onUpdate, resizable }, ref) {
22
22
  const containerRef = useRef(null)
23
23
  const [naturalRatio, setNaturalRatio] = useState(null)
24
24
 
@@ -99,12 +99,14 @@ const ImageWidget = forwardRef(function ImageWidget({ props, onUpdate }, ref) {
99
99
  </span>
100
100
  )}
101
101
  </div>
102
- <ResizeHandle
103
- targetRef={containerRef}
104
- minWidth={100}
105
- minHeight={60}
106
- onResize={(w) => handleResize(w)}
107
- />
102
+ {resizable && (
103
+ <ResizeHandle
104
+ targetRef={containerRef}
105
+ minWidth={100}
106
+ minHeight={60}
107
+ onResize={(w) => handleResize(w)}
108
+ />
109
+ )}
108
110
  </div>
109
111
  </WidgetWrapper>
110
112
  )
@@ -1,4 +1,5 @@
1
1
  import { useState, useRef, useEffect, useCallback, useMemo, forwardRef, useImperativeHandle } from 'react'
2
+ import { createPortal } from 'react-dom'
2
3
  import { buildPrototypeIndex } from '@dfosco/storyboard-core'
3
4
  import WidgetWrapper from './WidgetWrapper.jsx'
4
5
  import { readProp, prototypeEmbedSchema } from './widgetProps.js'
@@ -28,7 +29,7 @@ function resolveCanvasThemeFromStorage() {
28
29
  return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
29
30
  }
30
31
 
31
- export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
32
+ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }, ref) {
32
33
  const src = readProp(props, 'src', prototypeEmbedSchema)
33
34
  const width = readProp(props, 'width', prototypeEmbedSchema)
34
35
  const height = readProp(props, 'height', prototypeEmbedSchema)
@@ -51,12 +52,15 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
51
52
 
52
53
  const [editing, setEditing] = useState(false)
53
54
  const [interactive, setInteractive] = useState(false)
55
+ const [expanded, setExpanded] = useState(false)
54
56
  const [filter, setFilter] = useState('')
55
57
  const [canvasTheme, setCanvasTheme] = useState(() => resolveCanvasThemeFromStorage())
56
58
  const inputRef = useRef(null)
57
59
  const filterRef = useRef(null)
58
60
  const embedRef = useRef(null)
59
61
  const iframeRef = useRef(null)
62
+ const inlineContainerRef = useRef(null)
63
+ const modalContainerRef = useRef(null)
60
64
 
61
65
  const iframeSrc = useMemo(() => {
62
66
  if (!rawSrc) return ''
@@ -178,6 +182,54 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
178
182
  return () => document.removeEventListener('storyboard:theme:changed', readToolbarTheme)
179
183
  }, [])
180
184
 
185
+ // Close expanded modal on Escape
186
+ useEffect(() => {
187
+ if (!expanded) return
188
+ function handleKeyDown(e) {
189
+ if (e.key === 'Escape') {
190
+ e.stopPropagation()
191
+ setExpanded(false)
192
+ }
193
+ }
194
+ document.addEventListener('keydown', handleKeyDown, true)
195
+ return () => document.removeEventListener('keydown', handleKeyDown, true)
196
+ }, [expanded])
197
+
198
+ // Reparent iframe DOM node between inline container and modal.
199
+ // Uses moveBefore() (Chrome 133+) which preserves the iframe's
200
+ // browsing context — no reload. Falls back to appendChild which
201
+ // will reload but still works functionally.
202
+ useEffect(() => {
203
+ const iframe = iframeRef.current
204
+ if (!iframe) return
205
+
206
+ if (expanded && modalContainerRef.current) {
207
+ iframe._savedClassName = iframe.className
208
+ iframe._savedStyle = iframe.getAttribute('style') || ''
209
+ iframe.className = styles.expandIframe
210
+ iframe.removeAttribute('style')
211
+ const target = modalContainerRef.current
212
+ if (target.moveBefore) {
213
+ target.moveBefore(iframe, target.firstChild)
214
+ } else {
215
+ target.prepend(iframe)
216
+ }
217
+ } else if (!expanded && inlineContainerRef.current) {
218
+ if (iframe._savedClassName !== undefined) {
219
+ iframe.className = iframe._savedClassName
220
+ iframe.setAttribute('style', iframe._savedStyle)
221
+ delete iframe._savedClassName
222
+ delete iframe._savedStyle
223
+ }
224
+ const target = inlineContainerRef.current
225
+ if (target.moveBefore) {
226
+ target.moveBefore(iframe, null)
227
+ } else {
228
+ target.appendChild(iframe)
229
+ }
230
+ }
231
+ }, [expanded])
232
+
181
233
  // Listen for navigation events from the embedded prototype iframe
182
234
  useEffect(() => {
183
235
  function handleMessage(e) {
@@ -202,6 +254,8 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
202
254
  handleAction(actionId) {
203
255
  if (actionId === 'edit') {
204
256
  setEditing(true)
257
+ } else if (actionId === 'expand') {
258
+ setExpanded(true)
205
259
  } else if (actionId === 'open-external') {
206
260
  if (rawSrc) window.open(rawSrc, '_blank', 'noopener')
207
261
  } else if (actionId === 'zoom-in') {
@@ -234,6 +288,7 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
234
288
  }
235
289
 
236
290
  return (
291
+ <>
237
292
  <WidgetWrapper>
238
293
  <div
239
294
  ref={embedRef}
@@ -323,7 +378,11 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
323
378
  </div>
324
379
  ) : iframeSrc ? (
325
380
  <>
326
- <div className={styles.iframeContainer}>
381
+ <div
382
+ ref={inlineContainerRef}
383
+ className={styles.iframeContainer}
384
+ style={expanded ? { visibility: 'hidden' } : undefined}
385
+ >
327
386
  <iframe
328
387
  ref={iframeRef}
329
388
  src={iframeSrc}
@@ -338,7 +397,7 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
338
397
  sandbox="allow-same-origin allow-scripts allow-forms allow-popups"
339
398
  />
340
399
  </div>
341
- {!interactive && (
400
+ {!interactive && !expanded && (
342
401
  <div
343
402
  className={styles.dragOverlay}
344
403
  onDoubleClick={enterInteractive}
@@ -357,29 +416,57 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate }, ref) {
357
416
  </div>
358
417
  )}
359
418
  </div>
419
+ {resizable && (
420
+ <div
421
+ className={styles.resizeHandle}
422
+ onMouseDown={(e) => {
423
+ e.stopPropagation()
424
+ e.preventDefault()
425
+ const startX = e.clientX
426
+ const startY = e.clientY
427
+ const startW = width
428
+ const startH = height
429
+ function onMove(ev) {
430
+ const newW = Math.max(200, startW + ev.clientX - startX)
431
+ const newH = Math.max(150, startH + ev.clientY - startY)
432
+ onUpdate?.({ width: newW, height: newH })
433
+ }
434
+ function onUp() {
435
+ document.removeEventListener('mousemove', onMove)
436
+ document.removeEventListener('mouseup', onUp)
437
+ }
438
+ document.addEventListener('mousemove', onMove)
439
+ document.addEventListener('mouseup', onUp)
440
+ }}
441
+ onPointerDown={(e) => e.stopPropagation()}
442
+ />
443
+ )}
444
+ </WidgetWrapper>
445
+ {createPortal(
360
446
  <div
361
- className={styles.resizeHandle}
362
- onMouseDown={(e) => {
363
- e.stopPropagation()
364
- e.preventDefault()
365
- const startX = e.clientX
366
- const startY = e.clientY
367
- const startW = width
368
- const startH = height
369
- function onMove(ev) {
370
- const newW = Math.max(200, startW + ev.clientX - startX)
371
- const newH = Math.max(150, startH + ev.clientY - startY)
372
- onUpdate?.({ width: newW, height: newH })
373
- }
374
- function onUp() {
375
- document.removeEventListener('mousemove', onMove)
376
- document.removeEventListener('mouseup', onUp)
377
- }
378
- document.addEventListener('mousemove', onMove)
379
- document.addEventListener('mouseup', onUp)
380
- }}
447
+ className={styles.expandBackdrop}
448
+ style={expanded ? undefined : { display: 'none' }}
449
+ onClick={() => setExpanded(false)}
381
450
  onPointerDown={(e) => e.stopPropagation()}
382
- />
383
- </WidgetWrapper>
451
+ onKeyDown={(e) => e.stopPropagation()}
452
+ onWheel={(e) => e.stopPropagation()}
453
+ >
454
+ <div
455
+ ref={modalContainerRef}
456
+ className={styles.expandContainer}
457
+ onClick={(e) => e.stopPropagation()}
458
+ >
459
+ {/* iframe is reparented here via useEffect */}
460
+ <button
461
+ className={styles.expandClose}
462
+ onClick={() => setExpanded(false)}
463
+ aria-label="Close expanded view"
464
+ autoFocus
465
+ >✕</button>
466
+ </div>
467
+ </div>,
468
+ document.body
469
+ )}
470
+ </>
384
471
  )
385
472
  })
@@ -150,7 +150,7 @@
150
150
  }
151
151
 
152
152
  .pickerItem:focus-visible {
153
- outline: 2px solid var(--bgColor-accent-emphasis, #2f81f7);
153
+ outline: 4px solid var(--bgColor-accent-emphasis, #2f81f7);
154
154
  outline-offset: -2px;
155
155
  }
156
156
 
@@ -326,3 +326,67 @@
326
326
  border-right: 1.5px solid var(--trigger-border, var(--borderColor-muted, #d0d7de));
327
327
  user-select: none;
328
328
  }
329
+
330
+ /* Expand modal — fullscreen overlay for expanded iframe */
331
+ .expandBackdrop {
332
+ position: fixed;
333
+ inset: 0;
334
+ z-index: 100000;
335
+ background: rgba(0, 0, 0, 0.8);
336
+ display: flex;
337
+ align-items: center;
338
+ justify-content: center;
339
+ animation: expandFadeIn 0.15s ease;
340
+ }
341
+
342
+ @keyframes expandFadeIn {
343
+ from { opacity: 0; }
344
+ to { opacity: 1; }
345
+ }
346
+
347
+ .expandContainer {
348
+ width: 90vw;
349
+ height: 90vh;
350
+ position: relative;
351
+ border-radius: 12px;
352
+ overflow: hidden;
353
+ background: var(--bgColor-default, #ffffff);
354
+ box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
355
+ animation: expandScaleIn 0.2s ease;
356
+ }
357
+
358
+ @keyframes expandScaleIn {
359
+ from { transform: scale(0.95); opacity: 0; }
360
+ to { transform: scale(1); opacity: 1; }
361
+ }
362
+
363
+ .expandIframe {
364
+ border: none;
365
+ display: block;
366
+ width: 100%;
367
+ height: 100%;
368
+ }
369
+
370
+ .expandClose {
371
+ all: unset;
372
+ cursor: pointer;
373
+ position: absolute;
374
+ top: 12px;
375
+ right: 12px;
376
+ width: 32px;
377
+ height: 32px;
378
+ display: flex;
379
+ align-items: center;
380
+ justify-content: center;
381
+ border-radius: 8px;
382
+ background: rgba(0, 0, 0, 0.5);
383
+ color: #ffffff;
384
+ font-size: 16px;
385
+ z-index: 1;
386
+ transition: background 100ms;
387
+ backdrop-filter: blur(4px);
388
+ }
389
+
390
+ .expandClose:hover {
391
+ background: rgba(0, 0, 0, 0.7);
392
+ }
@@ -12,7 +12,7 @@ const COLORS = {
12
12
  orange: { bg: '#fff1e5', border: '#d18616', dot: '#e8a844' },
13
13
  }
14
14
 
15
- export default function StickyNote({ props, onUpdate }) {
15
+ export default function StickyNote({ props, onUpdate, resizable }) {
16
16
  const text = readProp(props, 'text', stickyNoteSchema)
17
17
  const color = readProp(props, 'color', stickyNoteSchema)
18
18
  const width = readProp(props, 'width', stickyNoteSchema)
@@ -75,12 +75,14 @@ export default function StickyNote({ props, onUpdate }) {
75
75
  placeholder="Type here…"
76
76
  />
77
77
  )}
78
- <ResizeHandle
79
- targetRef={stickyRef}
80
- minWidth={180}
81
- minHeight={60}
82
- onResize={handleResize}
83
- />
78
+ {resizable && (
79
+ <ResizeHandle
80
+ targetRef={stickyRef}
81
+ minWidth={180}
82
+ minHeight={60}
83
+ onResize={handleResize}
84
+ />
85
+ )}
84
86
  </article>
85
87
  </div>
86
88
  )
@@ -49,16 +49,22 @@ describe('StickyNote', () => {
49
49
  expect(sticky.style.height).toBe('200px')
50
50
  })
51
51
 
52
- it('renders a resize handle', () => {
53
- const { container } = render(<StickyNote props={{ text: 'Hi' }} onUpdate={vi.fn()} />)
52
+ it('renders a resize handle when resizable', () => {
53
+ const { container } = render(<StickyNote props={{ text: 'Hi' }} onUpdate={vi.fn()} resizable />)
54
54
  const handle = container.querySelector('[role="separator"]')
55
55
  expect(handle).not.toBeNull()
56
56
  })
57
57
 
58
+ it('does not render a resize handle when not resizable', () => {
59
+ const { container } = render(<StickyNote props={{ text: 'Hi' }} onUpdate={vi.fn()} resizable={false} />)
60
+ const handle = container.querySelector('[role="separator"]')
61
+ expect(handle).toBeNull()
62
+ })
63
+
58
64
  it('calls onUpdate with new dimensions on resize drag', () => {
59
65
  const onUpdate = vi.fn()
60
66
  const { container } = render(
61
- <StickyNote props={{ text: 'Hi', width: 200, height: 150 }} onUpdate={onUpdate} />
67
+ <StickyNote props={{ text: 'Hi', width: 200, height: 150 }} onUpdate={onUpdate} resizable />
62
68
  )
63
69
  const handle = container.querySelector('[role="separator"]')
64
70
  const sticky = container.querySelector('article')
@@ -78,7 +84,7 @@ describe('StickyNote', () => {
78
84
  it('enforces minimum dimensions during resize', () => {
79
85
  const onUpdate = vi.fn()
80
86
  const { container } = render(
81
- <StickyNote props={{ text: 'Hi', width: 200, height: 150 }} onUpdate={onUpdate} />
87
+ <StickyNote props={{ text: 'Hi', width: 200, height: 150 }} onUpdate={onUpdate} resizable />
82
88
  )
83
89
  const handle = container.querySelector('[role="separator"]')
84
90
  const sticky = container.querySelector('article')