@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 +3 -3
- package/src/canvas/CanvasPage.jsx +2 -2
- package/src/canvas/componentIsolate.jsx +28 -2
- package/src/canvas/widgets/ComponentWidget.jsx +20 -3
- package/src/canvas/widgets/ComponentWidget.module.css +0 -7
- package/src/canvas/widgets/FigmaEmbed.jsx +20 -3
- package/src/canvas/widgets/FigmaEmbed.module.css +0 -7
- package/src/canvas/widgets/PrototypeEmbed.jsx +20 -3
- package/src/canvas/widgets/PrototypeEmbed.module.css +0 -7
- package/src/canvas/widgets/WidgetChrome.jsx +24 -16
- package/src/canvas/widgets/embedInteraction.test.jsx +155 -0
- package/src/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/canvas/widgets/widgetConfig.js +5 -3
- package/src/canvas/widgets/widgetConfig.test.js +19 -0
- package/src/vite/data-plugin.js +5 -4
- package/src/vite/data-plugin.test.js +4 -2
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dfosco/storyboard-react",
|
|
3
|
-
"version": "4.0.0-beta.
|
|
3
|
+
"version": "4.0.0-beta.11",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@dfosco/storyboard-core": "4.0.0-beta.
|
|
7
|
-
"@dfosco/tiny-canvas": "4.0.0-beta.
|
|
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(
|
|
95
|
-
createElement(
|
|
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={
|
|
92
|
-
|
|
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
|
|
@@ -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={
|
|
130
|
-
|
|
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
|
) : (
|
|
@@ -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={
|
|
405
|
-
|
|
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
|
) : (
|
|
@@ -411,14 +411,18 @@ export default function WidgetChrome({
|
|
|
411
411
|
onUpdate?.({ color })
|
|
412
412
|
}, [onUpdate])
|
|
413
413
|
|
|
414
|
-
|
|
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
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
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
|
-
|
|
507
|
-
<
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
|
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', () => {
|
package/src/vite/data-plugin.js
CHANGED
|
@@ -655,10 +655,11 @@ export default function storyboardDataPlugin() {
|
|
|
655
655
|
config() {
|
|
656
656
|
return {
|
|
657
657
|
optimizeDeps: {
|
|
658
|
-
//
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
|
|
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
|
|
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('
|
|
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'", () => {
|