@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.4",
3
+ "version": "4.0.0-beta.5",
4
4
  "type": "module",
5
5
  "dependencies": {
6
- "@dfosco/storyboard-core": "4.0.0-beta.4",
7
- "@dfosco/tiny-canvas": "4.0.0-beta.4",
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: 180px;
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 {