@dfosco/storyboard-react 4.0.0-beta.0 → 4.0.0-beta.10
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 +7 -4
- package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
- package/src/canvas/CanvasPage.jsx +299 -119
- package/src/canvas/ComponentErrorBoundary.jsx +50 -0
- package/src/canvas/canvasReloadGuard.js +37 -0
- package/src/canvas/canvasReloadGuard.test.js +27 -0
- package/src/canvas/componentIsolate.jsx +109 -0
- package/src/canvas/useCanvas.js +6 -2
- package/src/canvas/widgets/ComponentWidget.jsx +47 -6
- package/src/canvas/widgets/ComponentWidget.module.css +8 -1
- package/src/canvas/widgets/MarkdownBlock.jsx +10 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +82 -0
- package/src/canvas/widgets/MarkdownBlock.test.jsx +39 -0
- package/src/canvas/widgets/StickyNote.test.jsx +9 -9
- package/src/canvas/widgets/WidgetChrome.module.css +2 -1
- package/src/canvas/widgets/widgetConfig.test.js +8 -8
- package/src/vite/data-plugin.js +144 -11
- package/src/vite/data-plugin.test.js +6 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Component Isolate — iframe entry point.
|
|
3
|
+
*
|
|
4
|
+
* Renders a single named export from a .canvas.jsx module inside an
|
|
5
|
+
* isolated document. The parent CanvasPage embeds this via an iframe
|
|
6
|
+
* so a broken component cannot crash the entire canvas.
|
|
7
|
+
*
|
|
8
|
+
* Query params:
|
|
9
|
+
* module — absolute or base-relative path to the .canvas.jsx file
|
|
10
|
+
* export — the named export to render
|
|
11
|
+
* theme — canvas theme (light / dark / dark_dimmed)
|
|
12
|
+
*/
|
|
13
|
+
import { createElement, Component as ReactComponent } from 'react'
|
|
14
|
+
import { createRoot } from 'react-dom/client'
|
|
15
|
+
|
|
16
|
+
// ── Error Boundary ──────────────────────────────────────────────────
|
|
17
|
+
class IsolateErrorBoundary extends ReactComponent {
|
|
18
|
+
constructor(props) {
|
|
19
|
+
super(props)
|
|
20
|
+
this.state = { error: null }
|
|
21
|
+
}
|
|
22
|
+
static getDerivedStateFromError(error) {
|
|
23
|
+
return { error }
|
|
24
|
+
}
|
|
25
|
+
render() {
|
|
26
|
+
if (this.state.error) {
|
|
27
|
+
return createElement('div', { style: errorStyle },
|
|
28
|
+
createElement('strong', null, this.props.name || 'Component'),
|
|
29
|
+
createElement('br'),
|
|
30
|
+
String(this.state.error.message || this.state.error),
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
return this.props.children
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ── Styles ──────────────────────────────────────────────────────────
|
|
38
|
+
const errorStyle = {
|
|
39
|
+
padding: '16px',
|
|
40
|
+
color: '#cf222e',
|
|
41
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
42
|
+
fontSize: '13px',
|
|
43
|
+
lineHeight: 1.5,
|
|
44
|
+
whiteSpace: 'pre-wrap',
|
|
45
|
+
wordBreak: 'break-word',
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── Resolve module path (mirrors useCanvas.resolveCanvasModuleImport) ─
|
|
49
|
+
function resolveModulePath(raw) {
|
|
50
|
+
if (!raw) return raw
|
|
51
|
+
if (/^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(raw)) return raw
|
|
52
|
+
if (!raw.startsWith('/')) return raw
|
|
53
|
+
const base = (import.meta.env?.BASE_URL || '/').replace(/\/$/, '')
|
|
54
|
+
if (!base) return raw
|
|
55
|
+
if (raw.startsWith(base)) return raw
|
|
56
|
+
return `${base}${raw}`
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Main ────────────────────────────────────────────────────────────
|
|
60
|
+
const params = new URLSearchParams(window.location.search)
|
|
61
|
+
const modulePath = params.get('module')
|
|
62
|
+
const exportName = params.get('export')
|
|
63
|
+
const theme = params.get('theme') || 'light'
|
|
64
|
+
|
|
65
|
+
// Apply theme to document for Primer / CSS-var inheritance
|
|
66
|
+
document.documentElement.setAttribute('data-color-mode', theme.startsWith('dark') ? 'dark' : 'light')
|
|
67
|
+
document.documentElement.setAttribute('data-dark-theme', theme.startsWith('dark') ? theme : '')
|
|
68
|
+
document.documentElement.setAttribute('data-light-theme', theme.startsWith('dark') ? '' : theme || 'light')
|
|
69
|
+
|
|
70
|
+
const root = createRoot(document.getElementById('root'))
|
|
71
|
+
|
|
72
|
+
async function mount() {
|
|
73
|
+
if (!modulePath || !exportName) {
|
|
74
|
+
root.render(createElement('div', { style: errorStyle }, 'Missing module or export param'))
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Validate: only allow .canvas.jsx modules
|
|
79
|
+
if (!modulePath.endsWith('.canvas.jsx')) {
|
|
80
|
+
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .canvas.jsx files are allowed'))
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
const resolved = resolveModulePath(modulePath)
|
|
86
|
+
const mod = await import(/* @vite-ignore */ resolved)
|
|
87
|
+
const Component = mod[exportName]
|
|
88
|
+
|
|
89
|
+
if (!Component || typeof Component !== 'function') {
|
|
90
|
+
throw new Error(`Export "${exportName}" not found or is not a component`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
root.render(
|
|
94
|
+
createElement(IsolateErrorBoundary, { name: exportName },
|
|
95
|
+
createElement(Component),
|
|
96
|
+
),
|
|
97
|
+
)
|
|
98
|
+
} catch (err) {
|
|
99
|
+
root.render(
|
|
100
|
+
createElement('div', { style: errorStyle },
|
|
101
|
+
createElement('strong', null, exportName),
|
|
102
|
+
createElement('br'),
|
|
103
|
+
String(err.message || err),
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
mount()
|
package/src/canvas/useCanvas.js
CHANGED
|
@@ -32,12 +32,13 @@ export function resolveCanvasModuleImport(modulePath, baseUrl = import.meta.env?
|
|
|
32
32
|
* fresh widget data from the server to pick up persisted edits.
|
|
33
33
|
*
|
|
34
34
|
* @param {string} name - Canvas name as indexed by the data plugin
|
|
35
|
-
* @returns {{ canvas: object|null, jsxExports: object|null, loading: boolean }}
|
|
35
|
+
* @returns {{ canvas: object|null, jsxExports: object|null, jsxError: boolean, loading: boolean }}
|
|
36
36
|
*/
|
|
37
37
|
export function useCanvas(name) {
|
|
38
38
|
const buildTimeCanvas = useMemo(() => getCanvasData(name), [name])
|
|
39
39
|
const [canvas, setCanvas] = useState(buildTimeCanvas)
|
|
40
40
|
const [jsxExports, setJsxExports] = useState(null)
|
|
41
|
+
const [jsxError, setJsxError] = useState(false)
|
|
41
42
|
const [loading, setLoading] = useState(true)
|
|
42
43
|
|
|
43
44
|
// Fetch fresh data from server on mount
|
|
@@ -66,6 +67,7 @@ export function useCanvas(name) {
|
|
|
66
67
|
useEffect(() => {
|
|
67
68
|
if (!jsxModule) {
|
|
68
69
|
setJsxExports(null)
|
|
70
|
+
setJsxError(false)
|
|
69
71
|
return
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -82,10 +84,12 @@ export function useCanvas(name) {
|
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
setJsxExports(exports)
|
|
87
|
+
setJsxError(false)
|
|
85
88
|
})
|
|
86
89
|
.catch((err) => {
|
|
87
90
|
console.error(`[storyboard] Failed to load canvas JSX module: ${jsxModule}`, err)
|
|
88
91
|
setJsxExports(null)
|
|
92
|
+
setJsxError(true)
|
|
89
93
|
})
|
|
90
94
|
}, [jsxModule, jsxImport])
|
|
91
95
|
|
|
@@ -109,5 +113,5 @@ export function useCanvas(name) {
|
|
|
109
113
|
}
|
|
110
114
|
}, [name, buildTimeCanvas])
|
|
111
115
|
|
|
112
|
-
return { canvas, jsxExports, loading }
|
|
116
|
+
return { canvas, jsxExports, jsxError, loading }
|
|
113
117
|
}
|
|
@@ -1,17 +1,33 @@
|
|
|
1
|
-
import { useRef, useCallback, useState, useEffect } from 'react'
|
|
1
|
+
import { useRef, useCallback, useState, useEffect, useMemo } from 'react'
|
|
2
2
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
3
3
|
import ResizeHandle from './ResizeHandle.jsx'
|
|
4
|
+
import ComponentErrorBoundary from '../ComponentErrorBoundary.jsx'
|
|
4
5
|
import styles from './ComponentWidget.module.css'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Renders a live JSX export from a .canvas.jsx companion file.
|
|
8
|
-
*
|
|
9
|
-
*
|
|
9
|
+
*
|
|
10
|
+
* In dev mode (isLocalDev), each component is rendered inside an iframe
|
|
11
|
+
* via the /_storyboard/canvas/isolate middleware. This isolates broken
|
|
12
|
+
* components so they cannot crash the entire canvas page.
|
|
13
|
+
*
|
|
14
|
+
* In production, the component is rendered directly with an ErrorBoundary
|
|
15
|
+
* as a fallback safety net.
|
|
10
16
|
*
|
|
11
17
|
* Double-click the overlay to enter interactive mode (dropdowns, buttons work).
|
|
12
18
|
* Click outside to exit interactive mode.
|
|
13
19
|
*/
|
|
14
|
-
export default function ComponentWidget({
|
|
20
|
+
export default function ComponentWidget({
|
|
21
|
+
component: Component,
|
|
22
|
+
jsxModule,
|
|
23
|
+
exportName,
|
|
24
|
+
canvasTheme,
|
|
25
|
+
isLocalDev,
|
|
26
|
+
width,
|
|
27
|
+
height,
|
|
28
|
+
onUpdate,
|
|
29
|
+
resizable,
|
|
30
|
+
}) {
|
|
15
31
|
const containerRef = useRef(null)
|
|
16
32
|
const [interactive, setInteractive] = useState(false)
|
|
17
33
|
|
|
@@ -33,7 +49,21 @@ export default function ComponentWidget({ component: Component, width, height, o
|
|
|
33
49
|
return () => document.removeEventListener('pointerdown', handlePointerDown)
|
|
34
50
|
}, [interactive])
|
|
35
51
|
|
|
36
|
-
|
|
52
|
+
// Build iframe src for dev isolation
|
|
53
|
+
const iframeSrc = useMemo(() => {
|
|
54
|
+
if (!isLocalDev || !jsxModule || !exportName) return null
|
|
55
|
+
const basePath = (import.meta.env.BASE_URL || '/').replace(/\/$/, '')
|
|
56
|
+
const params = new URLSearchParams({
|
|
57
|
+
module: jsxModule,
|
|
58
|
+
export: exportName,
|
|
59
|
+
theme: canvasTheme || 'light',
|
|
60
|
+
})
|
|
61
|
+
return `${basePath}/_storyboard/canvas/isolate?${params}`
|
|
62
|
+
}, [isLocalDev, jsxModule, exportName, canvasTheme])
|
|
63
|
+
|
|
64
|
+
const useIframe = isLocalDev && iframeSrc
|
|
65
|
+
|
|
66
|
+
if (!useIframe && !Component) return null
|
|
37
67
|
|
|
38
68
|
const sizeStyle = {}
|
|
39
69
|
if (typeof width === 'number') sizeStyle.width = `${width}px`
|
|
@@ -43,7 +73,18 @@ export default function ComponentWidget({ component: Component, width, height, o
|
|
|
43
73
|
<WidgetWrapper>
|
|
44
74
|
<div ref={containerRef} className={styles.container} style={sizeStyle}>
|
|
45
75
|
<div className={styles.content}>
|
|
46
|
-
|
|
76
|
+
{useIframe ? (
|
|
77
|
+
<iframe
|
|
78
|
+
src={iframeSrc}
|
|
79
|
+
className={styles.iframe}
|
|
80
|
+
title={exportName || 'Component widget'}
|
|
81
|
+
sandbox="allow-same-origin allow-scripts"
|
|
82
|
+
/>
|
|
83
|
+
) : Component ? (
|
|
84
|
+
<ComponentErrorBoundary name={exportName}>
|
|
85
|
+
<Component />
|
|
86
|
+
</ComponentErrorBoundary>
|
|
87
|
+
) : null}
|
|
47
88
|
</div>
|
|
48
89
|
{!interactive && (
|
|
49
90
|
<div
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
.container {
|
|
2
2
|
position: relative;
|
|
3
|
-
overflow:
|
|
3
|
+
overflow: hidden;
|
|
4
4
|
min-width: 100px;
|
|
5
5
|
min-height: 60px;
|
|
6
6
|
}
|
|
@@ -10,6 +10,13 @@
|
|
|
10
10
|
height: 100%;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
.iframe {
|
|
14
|
+
display: block;
|
|
15
|
+
width: 100%;
|
|
16
|
+
height: 100%;
|
|
17
|
+
border: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
13
20
|
.interactOverlay {
|
|
14
21
|
position: absolute;
|
|
15
22
|
inset: 0;
|
|
@@ -1,28 +1,21 @@
|
|
|
1
|
-
import { useState, useRef, useEffect, useCallback } from 'react'
|
|
1
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from 'react'
|
|
2
|
+
import { remark } from 'remark'
|
|
3
|
+
import remarkGfm from 'remark-gfm'
|
|
4
|
+
import remarkHtml from 'remark-html'
|
|
2
5
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
3
6
|
import { readProp, markdownSchema } from './widgetProps.js'
|
|
4
7
|
import styles from './MarkdownBlock.module.css'
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
|
-
* Renders markdown
|
|
10
|
+
* Renders markdown to HTML using remark with GitHub Flavored Markdown support.
|
|
8
11
|
*/
|
|
9
12
|
function renderMarkdown(text) {
|
|
10
13
|
if (!text) return ''
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
.
|
|
15
|
-
|
|
16
|
-
.replace(/\*(.+?)\*/g, '<em>$1</em>')
|
|
17
|
-
.replace(/`(.+?)`/g, '<code>$1</code>')
|
|
18
|
-
.replace(/^- (.+)$/gm, '<li>$1</li>')
|
|
19
|
-
.replace(/(<li>.*<\/li>)/s, '<ul>$1</ul>')
|
|
20
|
-
.replace(/\n\n/g, '</p><p>')
|
|
21
|
-
.replace(/\n/g, '<br>')
|
|
22
|
-
.replace(/^(.+)$/gm, (line) => {
|
|
23
|
-
if (line.startsWith('<')) return line
|
|
24
|
-
return `<p>${line}</p>`
|
|
25
|
-
})
|
|
14
|
+
const result = remark()
|
|
15
|
+
.use(remarkGfm)
|
|
16
|
+
.use(remarkHtml, { sanitize: false })
|
|
17
|
+
.processSync(text)
|
|
18
|
+
return String(result)
|
|
26
19
|
}
|
|
27
20
|
|
|
28
21
|
export default function MarkdownBlock({ props, onUpdate }) {
|
|
@@ -65,6 +65,88 @@
|
|
|
65
65
|
margin: 0 0 2px;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
.preview ol {
|
|
69
|
+
margin: 0 0 8px;
|
|
70
|
+
padding-left: 20px;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/* GFM: Task lists */
|
|
74
|
+
.preview input[type="checkbox"] {
|
|
75
|
+
margin-right: 6px;
|
|
76
|
+
pointer-events: none;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.preview li:has(input[type="checkbox"]) {
|
|
80
|
+
list-style: none;
|
|
81
|
+
margin-left: -20px;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* GFM: Strikethrough */
|
|
85
|
+
.preview del {
|
|
86
|
+
text-decoration: line-through;
|
|
87
|
+
color: var(--sb--markdown-muted);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* GFM: Tables */
|
|
91
|
+
.preview table {
|
|
92
|
+
border-collapse: collapse;
|
|
93
|
+
margin: 8px 0;
|
|
94
|
+
width: 100%;
|
|
95
|
+
font-size: 13px;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.preview th,
|
|
99
|
+
.preview td {
|
|
100
|
+
border: 1px solid var(--borderColor-default, #d0d7de);
|
|
101
|
+
padding: 6px 12px;
|
|
102
|
+
text-align: left;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.preview th {
|
|
106
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/* GFM: Autolinks */
|
|
111
|
+
.preview a {
|
|
112
|
+
color: var(--sb--markdown-accent);
|
|
113
|
+
text-decoration: none;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.preview a:hover {
|
|
117
|
+
text-decoration: underline;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* Code blocks */
|
|
121
|
+
.preview pre {
|
|
122
|
+
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
123
|
+
padding: 12px 16px;
|
|
124
|
+
border-radius: 6px;
|
|
125
|
+
overflow-x: auto;
|
|
126
|
+
margin: 8px 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.preview pre code {
|
|
130
|
+
background: none;
|
|
131
|
+
padding: 0;
|
|
132
|
+
font-size: 13px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* Blockquotes */
|
|
136
|
+
.preview blockquote {
|
|
137
|
+
border-left: 4px solid var(--borderColor-default, #d0d7de);
|
|
138
|
+
margin: 8px 0;
|
|
139
|
+
padding: 4px 16px;
|
|
140
|
+
color: var(--sb--markdown-muted);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Horizontal rules */
|
|
144
|
+
.preview hr {
|
|
145
|
+
border: none;
|
|
146
|
+
border-top: 1px solid var(--borderColor-default, #d0d7de);
|
|
147
|
+
margin: 16px 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
68
150
|
.preview :global(.placeholder) {
|
|
69
151
|
color: var(--sb--markdown-muted);
|
|
70
152
|
font-style: italic;
|
|
@@ -50,4 +50,43 @@ describe('MarkdownBlock', () => {
|
|
|
50
50
|
|
|
51
51
|
expect(setData).toHaveBeenCalledWith('text/plain', '**Hello**\n- item')
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
describe('GitHub Flavored Markdown', () => {
|
|
55
|
+
it('renders tables', () => {
|
|
56
|
+
const markdown = `| Name | Age |
|
|
57
|
+
| --- | --- |
|
|
58
|
+
| Alice | 30 |`
|
|
59
|
+
const { container } = render(<MarkdownBlock props={{ content: markdown, width: 420 }} />)
|
|
60
|
+
|
|
61
|
+
expect(container.querySelector('table')).not.toBeNull()
|
|
62
|
+
expect(container.querySelector('th')).not.toBeNull()
|
|
63
|
+
expect(screen.getByText('Alice')).toBeTruthy()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('renders task lists', () => {
|
|
67
|
+
const markdown = `- [x] Done
|
|
68
|
+
- [ ] Todo`
|
|
69
|
+
const { container } = render(<MarkdownBlock props={{ content: markdown, width: 420 }} />)
|
|
70
|
+
|
|
71
|
+
const checkboxes = container.querySelectorAll('input[type="checkbox"]')
|
|
72
|
+
expect(checkboxes).toHaveLength(2)
|
|
73
|
+
expect(checkboxes[0].checked).toBe(true)
|
|
74
|
+
expect(checkboxes[1].checked).toBe(false)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('renders strikethrough', () => {
|
|
78
|
+
const { container } = render(<MarkdownBlock props={{ content: '~~deleted~~', width: 420 }} />)
|
|
79
|
+
|
|
80
|
+
expect(container.querySelector('del')).not.toBeNull()
|
|
81
|
+
expect(screen.getByText('deleted')).toBeTruthy()
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
it('renders autolinks', () => {
|
|
85
|
+
const { container } = render(<MarkdownBlock props={{ content: 'https://github.com', width: 420 }} />)
|
|
86
|
+
|
|
87
|
+
const link = container.querySelector('a')
|
|
88
|
+
expect(link).not.toBeNull()
|
|
89
|
+
expect(link.href).toBe('https://github.com/')
|
|
90
|
+
})
|
|
91
|
+
})
|
|
53
92
|
})
|
|
@@ -13,16 +13,16 @@ describe('stickyNoteSchema', () => {
|
|
|
13
13
|
)
|
|
14
14
|
})
|
|
15
15
|
|
|
16
|
-
it('
|
|
16
|
+
it('includes default values for width/height from config', () => {
|
|
17
17
|
const defaults = getDefaults(stickyNoteSchema)
|
|
18
|
-
expect(defaults).
|
|
19
|
-
expect(defaults).
|
|
18
|
+
expect(defaults).toHaveProperty('width', 270)
|
|
19
|
+
expect(defaults).toHaveProperty('height', 170)
|
|
20
20
|
})
|
|
21
21
|
|
|
22
|
-
it('returns
|
|
22
|
+
it('returns default value when width/height are not saved in props', () => {
|
|
23
23
|
const props = { text: 'hello', color: 'yellow' }
|
|
24
|
-
expect(readProp(props, 'width', stickyNoteSchema)).
|
|
25
|
-
expect(readProp(props, 'height', stickyNoteSchema)).
|
|
24
|
+
expect(readProp(props, 'width', stickyNoteSchema)).toBe(270)
|
|
25
|
+
expect(readProp(props, 'height', stickyNoteSchema)).toBe(170)
|
|
26
26
|
})
|
|
27
27
|
|
|
28
28
|
it('returns saved width/height when present in props', () => {
|
|
@@ -33,11 +33,11 @@ describe('stickyNoteSchema', () => {
|
|
|
33
33
|
})
|
|
34
34
|
|
|
35
35
|
describe('StickyNote', () => {
|
|
36
|
-
it('
|
|
36
|
+
it('applies default dimensions as inline styles when not saved in props', () => {
|
|
37
37
|
const { container } = render(<StickyNote props={{ text: 'Hi' }} onUpdate={vi.fn()} />)
|
|
38
38
|
const sticky = container.querySelector('article')
|
|
39
|
-
expect(sticky.style.width).toBe('')
|
|
40
|
-
expect(sticky.style.height).toBe('')
|
|
39
|
+
expect(sticky.style.width).toBe('270px')
|
|
40
|
+
expect(sticky.style.height).toBe('170px')
|
|
41
41
|
})
|
|
42
42
|
|
|
43
43
|
it('applies saved dimensions as inline styles', () => {
|
|
@@ -236,7 +236,7 @@
|
|
|
236
236
|
position: absolute;
|
|
237
237
|
top: calc(100% + 10px);
|
|
238
238
|
right: 0;
|
|
239
|
-
min-width:
|
|
239
|
+
min-width: max-content;
|
|
240
240
|
padding: 4px;
|
|
241
241
|
background: var(--bgColor-default, #ffffff);
|
|
242
242
|
border-radius: 10px;
|
|
@@ -265,6 +265,7 @@
|
|
|
265
265
|
color: var(--fgColor-default, #1f2328);
|
|
266
266
|
border-radius: 6px;
|
|
267
267
|
box-sizing: border-box;
|
|
268
|
+
white-space: nowrap;
|
|
268
269
|
}
|
|
269
270
|
|
|
270
271
|
:global([data-sb-canvas-theme^='dark']) .overflowItem {
|
|
@@ -2,14 +2,14 @@ import { describe, expect, it } from 'vitest'
|
|
|
2
2
|
import { isResizable, getFeatures, getWidgetMeta } from './widgetConfig.js'
|
|
3
3
|
|
|
4
4
|
describe('isResizable', () => {
|
|
5
|
-
// Vitest runs
|
|
6
|
-
//
|
|
7
|
-
it('returns
|
|
8
|
-
expect(isResizable('sticky-note')).toBe(
|
|
9
|
-
expect(isResizable('prototype')).toBe(
|
|
10
|
-
expect(isResizable('figma-embed')).toBe(
|
|
11
|
-
expect(isResizable('image')).toBe(
|
|
12
|
-
expect(isResizable('component')).toBe(
|
|
5
|
+
// Vitest runs in dev mode by default (import.meta.env.PROD = false)
|
|
6
|
+
// In dev mode, all resize-enabled widgets are resizable
|
|
7
|
+
it('returns true for resize-enabled widgets in dev mode', () => {
|
|
8
|
+
expect(isResizable('sticky-note')).toBe(true)
|
|
9
|
+
expect(isResizable('prototype')).toBe(true)
|
|
10
|
+
expect(isResizable('figma-embed')).toBe(true)
|
|
11
|
+
expect(isResizable('image')).toBe(true)
|
|
12
|
+
expect(isResizable('component')).toBe(true)
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
it('returns false for widget types with resize disabled', () => {
|