@dfosco/storyboard-react 4.0.0-beta.10 → 4.0.0-beta.11

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.
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard-react",
3
- "version": "4.0.0-beta.10",
3
+ "version": "4.0.0-beta.11",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "4.0.0-beta.10",
7
- "@dfosco/tiny-canvas": "4.0.0-beta.10",
6
+ "@dfosco/storyboard-core": "4.0.0-beta.11",
7
+ "@dfosco/tiny-canvas": "4.0.0-beta.11",
8
8
  "@neodrag/react": "^2.3.1",
9
9
  "glob": "^11.0.0",
10
10
  "jsonc-parser": "^3.3.1",
@@ -257,7 +257,7 @@ function ChromeWrappedWidget({
257
257
  readOnly,
258
258
  }) {
259
259
  const widgetRef = useRef(null)
260
- const features = getFeatures(widget.type)
260
+ const features = getFeatures(widget.type, { isLocalDev: !readOnly })
261
261
 
262
262
  const handleAction = useCallback((actionId) => {
263
263
  if (actionId === 'delete') {
@@ -1502,7 +1502,7 @@ export default function CanvasPage({ name }) {
1502
1502
  const allChildren = []
1503
1503
 
1504
1504
  // 1. Component widgets (from jsxExports or sources fallback)
1505
- const componentFeatures = getFeatures('component')
1505
+ const componentFeatures = getFeatures('component', { isLocalDev })
1506
1506
  for (const entry of componentEntries) {
1507
1507
  const { exportName, Component, sourceData } = entry
1508
1508
  const sourcePosition = sourceData.position || { x: 0, y: 0 }
@@ -12,6 +12,25 @@
12
12
  */
13
13
  import { createElement, Component as ReactComponent } from 'react'
14
14
  import { createRoot } from 'react-dom/client'
15
+ import { ThemeProvider, BaseStyles } from '@primer/react'
16
+
17
+ // ── Primer Primitives CSS (required for CSS variables) ──────────────
18
+ import '@primer/primitives/dist/css/base/size/size.css'
19
+ import '@primer/primitives/dist/css/base/typography/typography.css'
20
+ import '@primer/primitives/dist/css/base/motion/motion.css'
21
+ import '@primer/primitives/dist/css/functional/size/border.css'
22
+ import '@primer/primitives/dist/css/functional/size/breakpoints.css'
23
+ import '@primer/primitives/dist/css/functional/size/size-coarse.css'
24
+ import '@primer/primitives/dist/css/functional/size/size-fine.css'
25
+ import '@primer/primitives/dist/css/functional/size/size.css'
26
+ import '@primer/primitives/dist/css/functional/size/viewport.css'
27
+ import '@primer/primitives/dist/css/functional/typography/typography.css'
28
+ import '@primer/primitives/dist/css/functional/themes/light.css'
29
+ import '@primer/primitives/dist/css/functional/themes/light-colorblind.css'
30
+ import '@primer/primitives/dist/css/functional/themes/dark.css'
31
+ import '@primer/primitives/dist/css/functional/themes/dark-colorblind.css'
32
+ import '@primer/primitives/dist/css/functional/themes/dark-high-contrast.css'
33
+ import '@primer/primitives/dist/css/functional/themes/dark-dimmed.css'
15
34
 
16
35
  // ── Error Boundary ──────────────────────────────────────────────────
17
36
  class IsolateErrorBoundary extends ReactComponent {
@@ -62,6 +81,9 @@ const modulePath = params.get('module')
62
81
  const exportName = params.get('export')
63
82
  const theme = params.get('theme') || 'light'
64
83
 
84
+ // Map theme to Primer colorMode
85
+ const colorMode = theme.startsWith('dark') ? 'night' : 'day'
86
+
65
87
  // Apply theme to document for Primer / CSS-var inheritance
66
88
  document.documentElement.setAttribute('data-color-mode', theme.startsWith('dark') ? 'dark' : 'light')
67
89
  document.documentElement.setAttribute('data-dark-theme', theme.startsWith('dark') ? theme : '')
@@ -91,8 +113,12 @@ async function mount() {
91
113
  }
92
114
 
93
115
  root.render(
94
- createElement(IsolateErrorBoundary, { name: exportName },
95
- createElement(Component),
116
+ createElement(ThemeProvider, { colorMode },
117
+ createElement(BaseStyles, null,
118
+ createElement(IsolateErrorBoundary, { name: exportName },
119
+ createElement(Component),
120
+ ),
121
+ ),
96
122
  ),
97
123
  )
98
124
  } catch (err) {
@@ -3,6 +3,7 @@ import WidgetWrapper from './WidgetWrapper.jsx'
3
3
  import ResizeHandle from './ResizeHandle.jsx'
4
4
  import ComponentErrorBoundary from '../ComponentErrorBoundary.jsx'
5
5
  import styles from './ComponentWidget.module.css'
6
+ import overlayStyles from './embedOverlay.module.css'
6
7
 
7
8
  /**
8
9
  * Renders a live JSX export from a .canvas.jsx companion file.
@@ -88,9 +89,25 @@ export default function ComponentWidget({
88
89
  </div>
89
90
  {!interactive && (
90
91
  <div
91
- className={styles.interactOverlay}
92
- onDoubleClick={enterInteractive}
93
- />
92
+ className={overlayStyles.interactOverlay}
93
+ onClick={(e) => {
94
+ // Don't enter interactive mode for modifier clicks (shift/meta/ctrl for multi-select)
95
+ if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return
96
+ enterInteractive()
97
+ }}
98
+ role="button"
99
+ tabIndex={0}
100
+ onKeyDown={(e) => {
101
+ if (e.key === 'Enter' || e.key === ' ') {
102
+ e.preventDefault()
103
+ e.stopPropagation()
104
+ enterInteractive()
105
+ }
106
+ }}
107
+ aria-label="Click to interact with component"
108
+ >
109
+ <span className={overlayStyles.interactHint}>Click to interact</span>
110
+ </div>
94
111
  )}
95
112
  {resizable && (
96
113
  <ResizeHandle
@@ -16,10 +16,3 @@
16
16
  height: 100%;
17
17
  border: none;
18
18
  }
19
-
20
- .interactOverlay {
21
- position: absolute;
22
- inset: 0;
23
- z-index: 1;
24
- cursor: default;
25
- }
@@ -5,6 +5,7 @@ import { readProp } from './widgetProps.js'
5
5
  import { schemas } from './widgetConfig.js'
6
6
  import { toFigmaEmbedUrl, getFigmaTitle, getFigmaType, isFigmaUrl } from './figmaUrl.js'
7
7
  import styles from './FigmaEmbed.module.css'
8
+ import overlayStyles from './embedOverlay.module.css'
8
9
 
9
10
  const figmaEmbedSchema = schemas['figma-embed']
10
11
 
@@ -126,9 +127,25 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, re
126
127
  </div>
127
128
  {!interactive && !expanded && (
128
129
  <div
129
- className={styles.dragOverlay}
130
- onDoubleClick={enterInteractive}
131
- />
130
+ className={overlayStyles.interactOverlay}
131
+ onClick={(e) => {
132
+ // Don't enter interactive mode for modifier clicks (shift/meta/ctrl for multi-select)
133
+ if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return
134
+ enterInteractive()
135
+ }}
136
+ role="button"
137
+ tabIndex={0}
138
+ onKeyDown={(e) => {
139
+ if (e.key === 'Enter' || e.key === ' ') {
140
+ e.preventDefault()
141
+ e.stopPropagation()
142
+ enterInteractive()
143
+ }
144
+ }}
145
+ aria-label="Click to interact with Figma embed"
146
+ >
147
+ <span className={overlayStyles.interactHint}>Click to interact</span>
148
+ </div>
132
149
  )}
133
150
  </>
134
151
  ) : (
@@ -47,13 +47,6 @@
47
47
  display: block;
48
48
  }
49
49
 
50
- .dragOverlay {
51
- position: absolute;
52
- inset: 0;
53
- z-index: 1;
54
- cursor: grab;
55
- }
56
-
57
50
  .resizeHandle {
58
51
  position: absolute;
59
52
  bottom: 0;
@@ -5,6 +5,7 @@ import WidgetWrapper from './WidgetWrapper.jsx'
5
5
  import { readProp, prototypeEmbedSchema } from './widgetProps.js'
6
6
  import { getEmbedChromeVars } from './embedTheme.js'
7
7
  import styles from './PrototypeEmbed.module.css'
8
+ import overlayStyles from './embedOverlay.module.css'
8
9
 
9
10
  function formatName(name) {
10
11
  return name
@@ -401,9 +402,25 @@ export default forwardRef(function PrototypeEmbed({ props, onUpdate, resizable }
401
402
  </div>
402
403
  {!interactive && !expanded && (
403
404
  <div
404
- className={styles.dragOverlay}
405
- onDoubleClick={enterInteractive}
406
- />
405
+ className={overlayStyles.interactOverlay}
406
+ onClick={(e) => {
407
+ // Don't enter interactive mode for modifier clicks (shift/meta/ctrl for multi-select)
408
+ if (e.shiftKey || e.metaKey || e.ctrlKey || e.altKey) return
409
+ enterInteractive()
410
+ }}
411
+ role="button"
412
+ tabIndex={0}
413
+ onKeyDown={(e) => {
414
+ if (e.key === 'Enter' || e.key === ' ') {
415
+ e.preventDefault()
416
+ e.stopPropagation()
417
+ enterInteractive()
418
+ }
419
+ }}
420
+ aria-label="Click to interact with prototype"
421
+ >
422
+ <span className={overlayStyles.interactHint}>Click to interact</span>
423
+ </div>
407
424
  )}
408
425
  </>
409
426
  ) : (
@@ -18,13 +18,6 @@
18
18
  display: block;
19
19
  }
20
20
 
21
- .dragOverlay {
22
- position: absolute;
23
- inset: 0;
24
- z-index: 1;
25
- cursor: grab;
26
- }
27
-
28
21
  .empty {
29
22
  display: flex;
30
23
  align-items: center;
@@ -411,14 +411,18 @@ export default function WidgetChrome({
411
411
  onUpdate?.({ color })
412
412
  }, [onUpdate])
413
413
 
414
- const showToolbar = !readOnly && (hovered || selected)
414
+ // In readOnly mode, features are already filtered to prod-only by getFeatures.
415
+ // Show toolbar if there are prod features even when readOnly.
416
+ const hasFeatures = features.length > 0
417
+ const showToolbar = (hovered || selected) && (!readOnly || hasFeatures)
415
418
  const showFeatures = showToolbar && !multiSelected
419
+ const menuFeatures = features.filter((f) => f.menu)
416
420
 
417
421
  return (
418
422
  <div
419
423
  className={styles.chromeContainer}
420
- onMouseEnter={readOnly ? undefined : handleMouseEnter}
421
- onMouseLeave={readOnly ? undefined : handleMouseLeave}
424
+ onMouseEnter={(readOnly && !hasFeatures) ? undefined : handleMouseEnter}
425
+ onMouseLeave={(readOnly && !hasFeatures) ? undefined : handleMouseLeave}
422
426
  >
423
427
  <div className={`tc-drag-surface ${styles.widgetSlot} ${selected ? styles.widgetSlotSelected : ''} ${multiSelected ? styles.widgetSlotMultiSelected : ''}`}>
424
428
  {children}
@@ -495,22 +499,26 @@ export default function WidgetChrome({
495
499
 
496
500
  return null
497
501
  })}
498
- <WidgetOverflowMenu
499
- widgetId={widgetId}
500
- menuFeatures={features.filter((f) => f.menu)}
501
- onAction={onAction}
502
- />
502
+ {menuFeatures.length > 0 && (
503
+ <WidgetOverflowMenu
504
+ widgetId={widgetId}
505
+ menuFeatures={menuFeatures}
506
+ onAction={onAction}
507
+ />
508
+ )}
503
509
  </div>
504
510
  )}
505
511
 
506
- <Tooltip text={selected ? "Click and drag to move" : "Select"} direction="n">
507
- <button
508
- className={`tc-drag-handle ${styles.selectHandle} ${selected ? styles.selectHandleActive : ''}`}
509
- onClick={handleHandleClick}
510
- aria-label={selected ? "Drag to move widget" : "Select widget"}
511
- aria-pressed={selected}
512
- />
513
- </Tooltip>
512
+ {!readOnly && (
513
+ <Tooltip text={selected ? "Click and drag to move" : "Select"} direction="n">
514
+ <button
515
+ className={`tc-drag-handle ${styles.selectHandle} ${selected ? styles.selectHandleActive : ''}`}
516
+ onClick={handleHandleClick}
517
+ aria-label={selected ? "Drag to move widget" : "Select widget"}
518
+ aria-pressed={selected}
519
+ />
520
+ </Tooltip>
521
+ )}
514
522
  </div>
515
523
  </div>
516
524
  </div>
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Tests for embed interaction UX (click-to-interact overlay).
3
+ */
4
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
5
+ import { render, fireEvent, screen } from '@testing-library/react'
6
+ import PrototypeEmbed from './PrototypeEmbed.jsx'
7
+ import FigmaEmbed from './FigmaEmbed.jsx'
8
+ import ComponentWidget from './ComponentWidget.jsx'
9
+
10
+ // Mock buildPrototypeIndex for PrototypeEmbed
11
+ vi.mock('@dfosco/storyboard-core', () => ({
12
+ buildPrototypeIndex: () => ({ folders: [], prototypes: [], globalFlows: [], sorted: { title: { prototypes: [], folders: [] } } }),
13
+ }))
14
+
15
+ // Simple mock wrapper for WidgetWrapper
16
+ vi.mock('./WidgetWrapper.jsx', () => ({
17
+ default: ({ children }) => <div data-testid="widget-wrapper">{children}</div>,
18
+ }))
19
+
20
+ // Mock ResizeHandle
21
+ vi.mock('./ResizeHandle.jsx', () => ({
22
+ default: () => <div data-testid="resize-handle" />,
23
+ }))
24
+
25
+ // Mock ComponentErrorBoundary
26
+ vi.mock('../ComponentErrorBoundary.jsx', () => ({
27
+ default: ({ children }) => <div data-testid="error-boundary">{children}</div>,
28
+ }))
29
+
30
+ describe('Embed interaction overlay', () => {
31
+ describe('PrototypeEmbed', () => {
32
+ const defaultProps = {
33
+ props: { src: '/test', width: 400, height: 300, zoom: 100 },
34
+ onUpdate: vi.fn(),
35
+ resizable: false,
36
+ }
37
+
38
+ it('renders "Click to interact" hint on hover', () => {
39
+ render(<PrototypeEmbed {...defaultProps} />)
40
+
41
+ const hint = screen.getByText('Click to interact')
42
+ expect(hint).toBeInTheDocument()
43
+ // CSS modules mangle class names, just check the element exists
44
+ })
45
+
46
+ it('enters interactive mode on single click (not double-click)', async () => {
47
+ render(<PrototypeEmbed {...defaultProps} />)
48
+
49
+ // Overlay should exist before interaction
50
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
51
+ expect(overlay).toBeInTheDocument()
52
+
53
+ // Single click should remove the overlay (enter interactive mode)
54
+ fireEvent.click(overlay)
55
+
56
+ // Overlay should no longer exist
57
+ expect(screen.queryByRole('button', { name: /click to interact/i })).not.toBeInTheDocument()
58
+ })
59
+
60
+ it('does not enter interactive mode on shift+click (preserves multi-select)', () => {
61
+ render(<PrototypeEmbed {...defaultProps} />)
62
+
63
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
64
+ fireEvent.click(overlay, { shiftKey: true })
65
+
66
+ // Overlay should still exist (did not enter interactive mode)
67
+ expect(screen.getByRole('button', { name: /click to interact/i })).toBeInTheDocument()
68
+ })
69
+
70
+ it('does not enter interactive mode on meta+click (preserves multi-select)', () => {
71
+ render(<PrototypeEmbed {...defaultProps} />)
72
+
73
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
74
+ fireEvent.click(overlay, { metaKey: true })
75
+
76
+ expect(screen.getByRole('button', { name: /click to interact/i })).toBeInTheDocument()
77
+ })
78
+
79
+ it('supports keyboard interaction (Enter key) with event prevention', () => {
80
+ render(<PrototypeEmbed {...defaultProps} />)
81
+
82
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
83
+ const event = { key: 'Enter', preventDefault: vi.fn(), stopPropagation: vi.fn() }
84
+ fireEvent.keyDown(overlay, event)
85
+
86
+ expect(screen.queryByRole('button', { name: /click to interact/i })).not.toBeInTheDocument()
87
+ })
88
+
89
+ it('supports keyboard interaction (Space key) with event prevention', () => {
90
+ render(<PrototypeEmbed {...defaultProps} />)
91
+
92
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
93
+ const event = { key: ' ', preventDefault: vi.fn(), stopPropagation: vi.fn() }
94
+ fireEvent.keyDown(overlay, event)
95
+
96
+ expect(screen.queryByRole('button', { name: /click to interact/i })).not.toBeInTheDocument()
97
+ })
98
+ })
99
+
100
+ describe('FigmaEmbed', () => {
101
+ const defaultProps = {
102
+ props: { url: 'https://www.figma.com/design/abc123/Test', width: 400, height: 300 },
103
+ onUpdate: vi.fn(),
104
+ resizable: false,
105
+ }
106
+
107
+ it('renders "Click to interact" hint', () => {
108
+ render(<FigmaEmbed {...defaultProps} />)
109
+
110
+ const hint = screen.getByText('Click to interact')
111
+ expect(hint).toBeInTheDocument()
112
+ })
113
+
114
+ it('enters interactive mode on single click', () => {
115
+ render(<FigmaEmbed {...defaultProps} />)
116
+
117
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
118
+ fireEvent.click(overlay)
119
+
120
+ expect(screen.queryByRole('button', { name: /click to interact/i })).not.toBeInTheDocument()
121
+ })
122
+ })
123
+
124
+ describe('ComponentWidget', () => {
125
+ const MockComponent = () => <div>Mock Component</div>
126
+
127
+ const defaultProps = {
128
+ component: MockComponent,
129
+ jsxModule: null,
130
+ exportName: 'MockComponent',
131
+ canvasTheme: 'light',
132
+ isLocalDev: false,
133
+ width: 200,
134
+ height: 150,
135
+ onUpdate: vi.fn(),
136
+ resizable: false,
137
+ }
138
+
139
+ it('renders "Click to interact" hint', () => {
140
+ render(<ComponentWidget {...defaultProps} />)
141
+
142
+ const hint = screen.getByText('Click to interact')
143
+ expect(hint).toBeInTheDocument()
144
+ })
145
+
146
+ it('enters interactive mode on single click', () => {
147
+ render(<ComponentWidget {...defaultProps} />)
148
+
149
+ const overlay = screen.getByRole('button', { name: /click to interact/i })
150
+ fireEvent.click(overlay)
151
+
152
+ expect(screen.queryByRole('button', { name: /click to interact/i })).not.toBeInTheDocument()
153
+ })
154
+ })
155
+ })
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared styles for embed interaction overlays.
3
+ * Used by PrototypeEmbed, FigmaEmbed, and ComponentWidget.
4
+ */
5
+
6
+ .interactOverlay {
7
+ position: absolute;
8
+ inset: 0;
9
+ z-index: 1;
10
+ cursor: pointer;
11
+ display: flex;
12
+ align-items: center;
13
+ justify-content: center;
14
+ transition: background-color 150ms ease;
15
+ }
16
+
17
+ .interactOverlay:hover {
18
+ background-color: rgba(0, 0, 0, 0.15);
19
+ }
20
+
21
+ .interactHint {
22
+ opacity: 0;
23
+ color: var(--fgColor-onInverse);
24
+ background-color: var(--bgColor-inverse);
25
+ padding: var(--base-size-12) var(--base-size-16);
26
+ border-radius: var(--base-size-6);
27
+ font-size: 14px;
28
+ font-weight: 600;
29
+ pointer-events: none;
30
+ transition: opacity 150ms ease;
31
+ }
32
+
33
+ .interactOverlay:hover .interactHint {
34
+ opacity: 1;
35
+ }
@@ -103,14 +103,16 @@ export const widgetTypes = buildWidgetTypes()
103
103
 
104
104
  /**
105
105
  * Get the feature list for a widget type.
106
- * In production, only features with `prod: true` are returned.
106
+ * In production (or when isLocalDev is false, e.g. ?prodMode simulation),
107
+ * only features with `prod: true` are returned.
107
108
  * In dev, all features are returned.
108
109
  * @param {string} type — widget type string
110
+ * @param {{ isLocalDev?: boolean }} [options]
109
111
  * @returns {Array} features array from config (variables resolved), or empty array
110
112
  */
111
- export function getFeatures(type) {
113
+ export function getFeatures(type, { isLocalDev = true } = {}) {
112
114
  const features = widgetTypes[type]?.features ?? []
113
- if (import.meta.env?.PROD) {
115
+ if (import.meta.env?.PROD || !isLocalDev) {
114
116
  return features.filter(f => f.prod)
115
117
  }
116
118
  return features
@@ -32,6 +32,25 @@ describe('getFeatures', () => {
32
32
  it('returns empty array for unknown widget types', () => {
33
33
  expect(getFeatures('nonexistent')).toEqual([])
34
34
  })
35
+
36
+ it('returns only prod features when isLocalDev is false', () => {
37
+ const features = getFeatures('figma-embed', { isLocalDev: false })
38
+ expect(features.length).toBeGreaterThan(0)
39
+ expect(features.every(f => f.prod === true)).toBe(true)
40
+ })
41
+
42
+ it('returns all features when isLocalDev is true (default)', () => {
43
+ const allFeatures = getFeatures('figma-embed')
44
+ const prodFeatures = getFeatures('figma-embed', { isLocalDev: false })
45
+ expect(allFeatures.length).toBeGreaterThan(prodFeatures.length)
46
+ })
47
+
48
+ it('includes menu-only prod features when isLocalDev is false', () => {
49
+ const features = getFeatures('figma-embed', { isLocalDev: false })
50
+ const menuFeature = features.find(f => f.menu)
51
+ expect(menuFeature).toBeDefined()
52
+ expect(menuFeature.prod).toBe(true)
53
+ })
35
54
  })
36
55
 
37
56
  describe('getWidgetMeta', () => {
@@ -655,10 +655,11 @@ export default function storyboardDataPlugin() {
655
655
  config() {
656
656
  return {
657
657
  optimizeDeps: {
658
- // debug is CJS-only but micromark's development export does
659
- // `import createDebug from 'debug'`. Vite must pre-bundle it
660
- // so the ESM default import resolves correctly.
661
- include: ['debug'],
658
+ // @dfosco/storyboard-react is excluded (virtual module), so Vite
659
+ // can't trace into its deps. Include the remark entry points so
660
+ // Vite pre-bundles the full chain covers all transitive CJS
661
+ // packages (debug, extend, etc.) without whack-a-mole.
662
+ include: ['remark', 'remark-gfm', 'remark-html'],
662
663
  exclude: ['@dfosco/storyboard-react'],
663
664
  },
664
665
  }
@@ -53,10 +53,12 @@ describe('storyboardDataPlugin', () => {
53
53
  expect(config.optimizeDeps.exclude).toContain('@dfosco/storyboard-react')
54
54
  })
55
55
 
56
- it('config() includes debug in optimizeDeps for ESM/CJS interop', () => {
56
+ it('config() includes remark stack in optimizeDeps so Vite pre-bundles transitive CJS deps', () => {
57
57
  const plugin = storyboardDataPlugin()
58
58
  const config = plugin.config()
59
- expect(config.optimizeDeps.include).toContain('debug')
59
+ expect(config.optimizeDeps.include).toContain('remark')
60
+ expect(config.optimizeDeps.include).toContain('remark-gfm')
61
+ expect(config.optimizeDeps.include).toContain('remark-html')
60
62
  })
61
63
 
62
64
  it("resolveId returns resolved ID for 'virtual:storyboard-data-index'", () => {