@dfosco/storyboard-react 4.0.0-beta.4 → 4.0.0-beta.5
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.
|
|
3
|
+
"version": "4.0.0-beta.5",
|
|
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.5",
|
|
7
|
+
"@dfosco/tiny-canvas": "4.0.0-beta.5",
|
|
8
8
|
"@neodrag/react": "^2.3.1",
|
|
9
9
|
"glob": "^11.0.0",
|
|
10
10
|
"jsonc-parser": "^3.3.1"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Component } from 'react'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Error boundary for canvas component widgets.
|
|
5
|
+
* Catches render-time errors so a single broken component
|
|
6
|
+
* doesn't crash the entire canvas page.
|
|
7
|
+
*
|
|
8
|
+
* Used as a production fallback when iframe isolation is not available.
|
|
9
|
+
*/
|
|
10
|
+
export default class ComponentErrorBoundary extends Component {
|
|
11
|
+
constructor(props) {
|
|
12
|
+
super(props)
|
|
13
|
+
this.state = { error: null }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
static getDerivedStateFromError(error) {
|
|
17
|
+
return { error }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
componentDidCatch(error, info) {
|
|
21
|
+
console.error(
|
|
22
|
+
`[storyboard] Component widget "${this.props.name || 'unknown'}" crashed:`,
|
|
23
|
+
error,
|
|
24
|
+
info?.componentStack,
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
render() {
|
|
29
|
+
if (this.state.error) {
|
|
30
|
+
return (
|
|
31
|
+
<div style={{
|
|
32
|
+
padding: '16px',
|
|
33
|
+
color: '#cf222e',
|
|
34
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
35
|
+
fontSize: '13px',
|
|
36
|
+
lineHeight: 1.5,
|
|
37
|
+
whiteSpace: 'pre-wrap',
|
|
38
|
+
wordBreak: 'break-word',
|
|
39
|
+
minWidth: 200,
|
|
40
|
+
minHeight: 60,
|
|
41
|
+
}}>
|
|
42
|
+
<strong>{this.props.name || 'Component'}</strong>
|
|
43
|
+
<br />
|
|
44
|
+
{String(this.state.error.message || this.state.error)}
|
|
45
|
+
</div>
|
|
46
|
+
)
|
|
47
|
+
}
|
|
48
|
+
return this.props.children
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { enableCanvasGuard, disableCanvasGuard, isCanvasGuardActive } from './canvasReloadGuard.js'
|
|
3
|
+
|
|
4
|
+
describe('canvasReloadGuard', () => {
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
disableCanvasGuard()
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
it('starts inactive', () => {
|
|
10
|
+
expect(isCanvasGuardActive()).toBe(false)
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
it('can be enabled and disabled', () => {
|
|
14
|
+
enableCanvasGuard()
|
|
15
|
+
expect(isCanvasGuardActive()).toBe(true)
|
|
16
|
+
disableCanvasGuard()
|
|
17
|
+
expect(isCanvasGuardActive()).toBe(false)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('enable is idempotent', () => {
|
|
21
|
+
enableCanvasGuard()
|
|
22
|
+
enableCanvasGuard()
|
|
23
|
+
expect(isCanvasGuardActive()).toBe(true)
|
|
24
|
+
disableCanvasGuard()
|
|
25
|
+
expect(isCanvasGuardActive()).toBe(false)
|
|
26
|
+
})
|
|
27
|
+
})
|
|
@@ -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()
|
|
@@ -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 {
|