@dfosco/storyboard-react 4.0.0-beta.8 → 4.0.0
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 +6 -3
- package/src/AuthModal/AuthModal.jsx +134 -0
- package/src/AuthModal/AuthModal.module.css +221 -0
- package/src/BranchBar/BranchBar.jsx +56 -0
- package/src/BranchBar/BranchBar.module.css +230 -0
- package/src/BranchBar/useBranches.js +79 -0
- package/src/CommandPalette/CommandPalette.jsx +936 -0
- package/src/CommandPalette/CreateDialog.jsx +219 -0
- package/src/CommandPalette/command-palette.css +111 -0
- package/src/Icon.jsx +180 -0
- package/src/Viewfinder.jsx +1104 -57
- package/src/Viewfinder.module.css +1107 -149
- package/src/canvas/CanvasControls.jsx +51 -2
- package/src/canvas/CanvasControls.module.css +31 -0
- package/src/canvas/CanvasPage.bridge.test.jsx +142 -19
- package/src/canvas/CanvasPage.dragdrop.test.jsx +346 -0
- package/src/canvas/CanvasPage.jsx +807 -251
- package/src/canvas/CanvasPage.module.css +98 -50
- package/src/canvas/CanvasPage.multiselect.test.jsx +13 -11
- package/src/canvas/CanvasToolbar.jsx +2 -2
- package/src/canvas/MarqueeOverlay.jsx +20 -0
- package/src/canvas/PageSelector.jsx +239 -0
- package/src/canvas/PageSelector.module.css +165 -0
- package/src/canvas/PageSelector.test.jsx +104 -0
- package/src/canvas/canvasApi.js +22 -8
- package/src/canvas/canvasTheme.js +96 -52
- package/src/canvas/componentIsolate.jsx +33 -7
- package/src/canvas/useCanvas.js +9 -8
- package/src/canvas/useCanvas.test.js +4 -4
- package/src/canvas/useMarqueeSelect.js +187 -0
- package/src/canvas/useMarqueeSelect.test.js +78 -0
- package/src/canvas/widgets/CodePenEmbed.jsx +292 -0
- package/src/canvas/widgets/CodePenEmbed.module.css +161 -0
- package/src/canvas/widgets/ComponentWidget.jsx +42 -10
- package/src/canvas/widgets/ComponentWidget.module.css +6 -5
- package/src/canvas/widgets/FigmaEmbed.jsx +110 -24
- package/src/canvas/widgets/FigmaEmbed.module.css +21 -7
- package/src/canvas/widgets/LinkPreview.jsx +297 -11
- package/src/canvas/widgets/LinkPreview.module.css +386 -18
- package/src/canvas/widgets/LinkPreview.test.jsx +193 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +86 -5
- package/src/canvas/widgets/MarkdownBlock.module.css +64 -15
- package/src/canvas/widgets/PrototypeEmbed.jsx +96 -145
- package/src/canvas/widgets/PrototypeEmbed.module.css +74 -4
- package/src/canvas/widgets/StickyNote.module.css +5 -0
- package/src/canvas/widgets/StickyNote.test.jsx +9 -9
- package/src/canvas/widgets/StoryWidget.jsx +277 -0
- package/src/canvas/widgets/StoryWidget.module.css +211 -0
- package/src/canvas/widgets/WidgetChrome.jsx +76 -20
- package/src/canvas/widgets/WidgetChrome.module.css +2 -6
- package/src/canvas/widgets/WidgetWrapper.module.css +2 -0
- package/src/canvas/widgets/codepenUrl.js +75 -0
- package/src/canvas/widgets/codepenUrl.test.js +76 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +235 -0
- package/src/canvas/widgets/embedOverlay.module.css +35 -0
- package/src/canvas/widgets/embedTheme.js +138 -39
- package/src/canvas/widgets/githubUrl.js +82 -0
- package/src/canvas/widgets/githubUrl.test.js +74 -0
- package/src/canvas/widgets/iframeDevLogs.js +49 -0
- package/src/canvas/widgets/iframeDevLogs.test.jsx +81 -0
- package/src/canvas/widgets/index.js +4 -0
- package/src/canvas/widgets/pasteRules.js +295 -0
- package/src/canvas/widgets/pasteRules.test.js +474 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +259 -0
- package/src/canvas/widgets/widgetConfig.js +16 -5
- package/src/canvas/widgets/widgetConfig.test.js +34 -12
- package/src/context.jsx +145 -16
- package/src/hooks/useSceneData.js +4 -2
- package/src/hooks/useThemeState.js +61 -0
- package/src/hooks/useThemeState.test.js +66 -0
- package/src/index.js +10 -0
- package/src/story/StoryPage.jsx +117 -0
- package/src/story/StoryPage.module.css +18 -0
- package/src/vite/data-plugin.js +348 -66
- package/src/vite/data-plugin.test.js +405 -5
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
font-size: 13px;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
.trigger {
|
|
7
|
+
display: inline-flex;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: 6px;
|
|
10
|
+
padding: 5px 10px;
|
|
11
|
+
border: 1px solid var(--borderColor-default, rgba(0, 0, 0, 0.15));
|
|
12
|
+
border-radius: 6px;
|
|
13
|
+
background: var(--bgColor-default, #fff);
|
|
14
|
+
color: var(--fgColor-default, #1f2328);
|
|
15
|
+
cursor: pointer;
|
|
16
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
|
17
|
+
transition: border-color 0.15s, box-shadow 0.15s;
|
|
18
|
+
line-height: 1;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.trigger:hover {
|
|
22
|
+
border-color: var(--borderColor-emphasis, rgba(0, 0, 0, 0.3));
|
|
23
|
+
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.12);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.label {
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
max-width: 200px;
|
|
29
|
+
overflow: hidden;
|
|
30
|
+
text-overflow: ellipsis;
|
|
31
|
+
white-space: nowrap;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.badge {
|
|
35
|
+
font-size: 11px;
|
|
36
|
+
color: var(--fgColor-muted, #656d76);
|
|
37
|
+
font-variant-numeric: tabular-nums;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.chevron {
|
|
41
|
+
color: var(--fgColor-muted, #656d76);
|
|
42
|
+
transition: transform 0.15s;
|
|
43
|
+
flex-shrink: 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.chevronOpen {
|
|
47
|
+
transform: rotate(180deg);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.menu {
|
|
51
|
+
position: absolute;
|
|
52
|
+
top: calc(100% + 4px);
|
|
53
|
+
left: 0;
|
|
54
|
+
min-width: 180px;
|
|
55
|
+
max-width: 300px;
|
|
56
|
+
max-height: 320px;
|
|
57
|
+
overflow-y: auto;
|
|
58
|
+
margin: 0;
|
|
59
|
+
padding: 4px;
|
|
60
|
+
list-style: none;
|
|
61
|
+
background: var(--bgColor-default, #fff);
|
|
62
|
+
border: 1px solid var(--borderColor-default, rgba(0, 0, 0, 0.15));
|
|
63
|
+
border-radius: 8px;
|
|
64
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.item {
|
|
68
|
+
padding: 6px 10px;
|
|
69
|
+
border-radius: 4px;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
white-space: nowrap;
|
|
72
|
+
overflow: hidden;
|
|
73
|
+
text-overflow: ellipsis;
|
|
74
|
+
color: var(--fgColor-default, #1f2328);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.item:hover {
|
|
78
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.item:focus-visible {
|
|
82
|
+
outline: 2px solid var(--focus-outlineColor, #0969da);
|
|
83
|
+
outline-offset: -2px;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.itemActive {
|
|
87
|
+
font-weight: 600;
|
|
88
|
+
background: var(--bgColor-accent-muted, #ddf4ff);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.itemActive:hover {
|
|
92
|
+
background: var(--bgColor-accent-muted, #ddf4ff);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.separator {
|
|
96
|
+
height: 1px;
|
|
97
|
+
background: var(--borderColor-default, rgba(0, 0, 0, 0.15));
|
|
98
|
+
margin: 4px 8px;
|
|
99
|
+
list-style: none;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.addItem {
|
|
103
|
+
padding: 6px 10px;
|
|
104
|
+
border-radius: 4px;
|
|
105
|
+
cursor: pointer;
|
|
106
|
+
white-space: nowrap;
|
|
107
|
+
color: var(--fgColor-muted, #656d76);
|
|
108
|
+
font-size: 12px;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.addItem:hover {
|
|
112
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
113
|
+
color: var(--fgColor-default, #1f2328);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.addForm {
|
|
117
|
+
display: flex;
|
|
118
|
+
gap: 4px;
|
|
119
|
+
padding: 4px 6px;
|
|
120
|
+
list-style: none;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.addInput {
|
|
124
|
+
flex: 1;
|
|
125
|
+
min-width: 0;
|
|
126
|
+
padding: 4px 8px;
|
|
127
|
+
border: 1px solid var(--borderColor-default, rgba(0, 0, 0, 0.15));
|
|
128
|
+
border-radius: 4px;
|
|
129
|
+
font-size: 12px;
|
|
130
|
+
font-family: inherit;
|
|
131
|
+
outline: none;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.addInput:focus {
|
|
135
|
+
border-color: var(--focus-outlineColor, #0969da);
|
|
136
|
+
box-shadow: 0 0 0 2px rgba(9, 105, 218, 0.3);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.addSubmit {
|
|
140
|
+
padding: 4px 10px;
|
|
141
|
+
border: none;
|
|
142
|
+
border-radius: 4px;
|
|
143
|
+
background: var(--bgColor-accent-emphasis, #0969da);
|
|
144
|
+
color: #fff;
|
|
145
|
+
font-size: 12px;
|
|
146
|
+
font-family: inherit;
|
|
147
|
+
cursor: pointer;
|
|
148
|
+
white-space: nowrap;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.addSubmit:hover {
|
|
152
|
+
opacity: 0.9;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.addSubmit:disabled {
|
|
156
|
+
opacity: 0.5;
|
|
157
|
+
cursor: default;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.successMsg {
|
|
161
|
+
padding: 4px 12px 6px;
|
|
162
|
+
font-size: 11px;
|
|
163
|
+
color: #1a7f37;
|
|
164
|
+
font-weight: 500;
|
|
165
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react'
|
|
3
|
+
import PageSelector from './PageSelector.jsx'
|
|
4
|
+
|
|
5
|
+
const PAGES = [
|
|
6
|
+
{ name: 'research/interviews', route: '/canvas/research/interviews', title: 'Interviews' },
|
|
7
|
+
{ name: 'research/surveys', route: '/canvas/research/surveys', title: 'Surveys' },
|
|
8
|
+
{ name: 'research/analysis', route: '/canvas/research/analysis', title: 'Analysis' },
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
describe('PageSelector', () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
// Reset location mock
|
|
14
|
+
delete window.location
|
|
15
|
+
window.location = { href: '' }
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('renders nothing when fewer than 2 pages', () => {
|
|
19
|
+
const { container } = render(<PageSelector currentName="solo" pages={[{ name: 'solo', route: '/canvas/solo', title: 'Solo' }]} />)
|
|
20
|
+
expect(container.innerHTML).toBe('')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('renders nothing when pages is empty', () => {
|
|
24
|
+
const { container } = render(<PageSelector currentName="foo" pages={[]} />)
|
|
25
|
+
expect(container.innerHTML).toBe('')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('shows current page label and page count', () => {
|
|
29
|
+
render(<PageSelector currentName="research/interviews" pages={PAGES} />)
|
|
30
|
+
expect(screen.getByText('Interviews')).toBeTruthy()
|
|
31
|
+
expect(screen.getByText('1/3')).toBeTruthy()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('shows correct index for non-first page', () => {
|
|
35
|
+
render(<PageSelector currentName="research/surveys" pages={PAGES} />)
|
|
36
|
+
expect(screen.getByText('Surveys')).toBeTruthy()
|
|
37
|
+
expect(screen.getByText('2/3')).toBeTruthy()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
it('opens dropdown on click and shows all pages', () => {
|
|
41
|
+
render(<PageSelector currentName="research/interviews" pages={PAGES} />)
|
|
42
|
+
const trigger = screen.getByTitle('Switch canvas page')
|
|
43
|
+
fireEvent.click(trigger)
|
|
44
|
+
|
|
45
|
+
const options = screen.getAllByRole('option')
|
|
46
|
+
expect(options).toHaveLength(3)
|
|
47
|
+
expect(options[0].textContent).toBe('Interviews')
|
|
48
|
+
expect(options[1].textContent).toBe('Surveys')
|
|
49
|
+
expect(options[2].textContent).toBe('Analysis')
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('marks the current page as active', () => {
|
|
53
|
+
render(<PageSelector currentName="research/surveys" pages={PAGES} />)
|
|
54
|
+
fireEvent.click(screen.getByTitle('Switch canvas page'))
|
|
55
|
+
|
|
56
|
+
const options = screen.getAllByRole('option')
|
|
57
|
+
expect(options[1].getAttribute('aria-selected')).toBe('true')
|
|
58
|
+
expect(options[0].getAttribute('aria-selected')).toBe('false')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('navigates to selected page', () => {
|
|
62
|
+
render(<PageSelector currentName="research/interviews" pages={PAGES} />)
|
|
63
|
+
fireEvent.click(screen.getByTitle('Switch canvas page'))
|
|
64
|
+
// Click the option in the menu (not the trigger label)
|
|
65
|
+
const options = screen.getAllByRole('option')
|
|
66
|
+
fireEvent.click(options[1]) // Surveys
|
|
67
|
+
|
|
68
|
+
expect(window.location.href).toContain('/canvas/research/surveys')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('closes dropdown on Escape', () => {
|
|
72
|
+
render(<PageSelector currentName="research/interviews" pages={PAGES} />)
|
|
73
|
+
fireEvent.click(screen.getByTitle('Switch canvas page'))
|
|
74
|
+
expect(screen.queryByRole('listbox')).toBeTruthy()
|
|
75
|
+
|
|
76
|
+
fireEvent.keyDown(document, { key: 'Escape' })
|
|
77
|
+
expect(screen.queryByRole('listbox')).toBeNull()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('closes dropdown on outside click', () => {
|
|
81
|
+
render(
|
|
82
|
+
<div>
|
|
83
|
+
<PageSelector currentName="research/interviews" pages={PAGES} />
|
|
84
|
+
<span data-testid="outside">Outside</span>
|
|
85
|
+
</div>
|
|
86
|
+
)
|
|
87
|
+
fireEvent.click(screen.getByTitle('Switch canvas page'))
|
|
88
|
+
expect(screen.queryByRole('listbox')).toBeTruthy()
|
|
89
|
+
|
|
90
|
+
fireEvent.mouseDown(screen.getByTestId('outside'))
|
|
91
|
+
expect(screen.queryByRole('listbox')).toBeNull()
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('does not navigate when selecting the current page', () => {
|
|
95
|
+
render(<PageSelector currentName="research/interviews" pages={PAGES} />)
|
|
96
|
+
fireEvent.click(screen.getByTitle('Switch canvas page'))
|
|
97
|
+
// Click the current page option
|
|
98
|
+
const options = screen.getAllByRole('option')
|
|
99
|
+
fireEvent.click(options[0]) // Interviews (current)
|
|
100
|
+
|
|
101
|
+
// location.href was set to '' initially, should remain unchanged
|
|
102
|
+
expect(window.location.href).toBe('')
|
|
103
|
+
})
|
|
104
|
+
})
|
package/src/canvas/canvasApi.js
CHANGED
|
@@ -28,22 +28,36 @@ export function createCanvas(data) {
|
|
|
28
28
|
return request('/create', 'POST', data)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
export function updateCanvas(
|
|
32
|
-
return request('/update', 'PUT', { name, widgets, sources, settings })
|
|
31
|
+
export function updateCanvas(canvasId, { widgets, sources, settings }) {
|
|
32
|
+
return request('/update', 'PUT', { name: canvasId, widgets, sources, settings })
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function addWidget(
|
|
36
|
-
return request('/widget', 'POST', { name, type, props, position })
|
|
35
|
+
export function addWidget(canvasId, { type, props, position }) {
|
|
36
|
+
return request('/widget', 'POST', { name: canvasId, type, props, position })
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export function removeWidget(
|
|
40
|
-
return request('/widget', 'DELETE', { name, widgetId })
|
|
39
|
+
export function removeWidget(canvasId, widgetId) {
|
|
40
|
+
return request('/widget', 'DELETE', { name: canvasId, widgetId })
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function uploadImage(dataUrl,
|
|
44
|
-
|
|
43
|
+
export function uploadImage(dataUrl, canvasId, filename) {
|
|
44
|
+
const body = { dataUrl, canvasName: canvasId }
|
|
45
|
+
if (filename) body.filename = filename
|
|
46
|
+
return request('/image', 'POST', body)
|
|
45
47
|
}
|
|
46
48
|
|
|
47
49
|
export function toggleImagePrivacy(filename) {
|
|
48
50
|
return request('/image/toggle-private', 'POST', { filename })
|
|
49
51
|
}
|
|
52
|
+
|
|
53
|
+
export function getCanvas(canvasId) {
|
|
54
|
+
return request(`/read?name=${encodeURIComponent(canvasId)}`, 'GET')
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function checkGitHubCliAvailable() {
|
|
58
|
+
return request('/github/available', 'GET')
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function fetchGitHubEmbed(url) {
|
|
62
|
+
return request('/github/embed', 'POST', { url })
|
|
63
|
+
}
|
|
@@ -1,74 +1,118 @@
|
|
|
1
1
|
export function getCanvasPrimerAttrs(theme) {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
'data-color-mode': 'dark',
|
|
5
|
-
'data-dark-theme': 'dark_dimmed',
|
|
6
|
-
'data-light-theme': 'light',
|
|
7
|
-
}
|
|
8
|
-
}
|
|
9
|
-
if (String(theme || 'light').startsWith('dark')) {
|
|
2
|
+
const value = String(theme || 'light')
|
|
3
|
+
if (value.startsWith('dark')) {
|
|
10
4
|
return {
|
|
11
5
|
'data-color-mode': 'dark',
|
|
12
|
-
'data-dark-theme':
|
|
6
|
+
'data-dark-theme': value,
|
|
13
7
|
'data-light-theme': 'light',
|
|
14
8
|
}
|
|
15
9
|
}
|
|
16
10
|
return {
|
|
17
11
|
'data-color-mode': 'light',
|
|
18
12
|
'data-dark-theme': 'dark',
|
|
19
|
-
'data-light-theme': 'light',
|
|
13
|
+
'data-light-theme': value.startsWith('light') ? value : 'light',
|
|
20
14
|
}
|
|
21
15
|
}
|
|
22
16
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
'--bgColor-neutral-muted': 'rgba(99, 110, 123, 0.3)',
|
|
31
|
-
'--bgColor-accent-emphasis': '#316dca',
|
|
32
|
-
'--tc-bg-muted': '#22272e',
|
|
33
|
-
'--tc-dot-color': 'rgba(205, 217, 229, 0.22)',
|
|
34
|
-
'--overlay-backdrop-bgColor': 'rgba(205, 217, 229, 0.22)',
|
|
35
|
-
'--fgColor-muted': '#768390',
|
|
36
|
-
'--fgColor-default': '#adbac7',
|
|
37
|
-
'--fgColor-onEmphasis': '#ffffff',
|
|
38
|
-
'--borderColor-default': '#444c56',
|
|
39
|
-
'--borderColor-muted': '#545d68',
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
if (value.startsWith('dark')) {
|
|
43
|
-
return {
|
|
44
|
-
'--sb--canvas-bg': '#161b22',
|
|
45
|
-
'--bgColor-default': '#161b22',
|
|
46
|
-
'--bgColor-muted': '#161b22',
|
|
47
|
-
'--bgColor-neutral-muted': 'rgba(110, 118, 129, 0.2)',
|
|
48
|
-
'--bgColor-accent-emphasis': '#2f81f7',
|
|
49
|
-
'--tc-bg-muted': '#161b22',
|
|
50
|
-
'--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
|
|
51
|
-
'--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
|
|
52
|
-
'--fgColor-muted': '#8b949e',
|
|
53
|
-
'--fgColor-default': '#e6edf3',
|
|
54
|
-
'--fgColor-onEmphasis': '#ffffff',
|
|
55
|
-
'--borderColor-default': '#30363d',
|
|
56
|
-
'--borderColor-muted': '#30363d',
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return {
|
|
17
|
+
/**
|
|
18
|
+
* Per-theme canvas CSS custom properties sourced from @primer/primitives.
|
|
19
|
+
* Each theme gets its own entry so high-contrast, colorblind, and dimmed
|
|
20
|
+
* variants all render with the correct background, dot, and text colors.
|
|
21
|
+
*/
|
|
22
|
+
const THEME_VARS = {
|
|
23
|
+
light: {
|
|
60
24
|
'--sb--canvas-bg': '#f6f8fa',
|
|
61
25
|
'--bgColor-default': '#ffffff',
|
|
26
|
+
'--bgColor-muted': '#f6f8fa',
|
|
27
|
+
'--bgColor-neutral-muted': '#818b981f',
|
|
28
|
+
'--bgColor-accent-emphasis': '#0969da',
|
|
62
29
|
'--tc-bg-muted': '#f6f8fa',
|
|
63
30
|
'--tc-dot-color': 'rgba(0, 0, 0, 0.08)',
|
|
64
31
|
'--overlay-backdrop-bgColor': 'rgba(0, 0, 0, 0.08)',
|
|
32
|
+
'--fgColor-muted': '#59636e',
|
|
33
|
+
'--fgColor-default': '#1f2328',
|
|
34
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
35
|
+
'--borderColor-default': '#d1d9e0',
|
|
36
|
+
'--borderColor-muted': '#d1d9e0b3',
|
|
37
|
+
},
|
|
38
|
+
light_colorblind: {
|
|
39
|
+
'--sb--canvas-bg': '#f6f8fa',
|
|
40
|
+
'--bgColor-default': '#ffffff',
|
|
65
41
|
'--bgColor-muted': '#f6f8fa',
|
|
66
|
-
'--bgColor-neutral-muted': '#
|
|
67
|
-
'--bgColor-accent-emphasis': '#
|
|
68
|
-
'--
|
|
42
|
+
'--bgColor-neutral-muted': '#818b981f',
|
|
43
|
+
'--bgColor-accent-emphasis': '#0969da',
|
|
44
|
+
'--tc-bg-muted': '#f6f8fa',
|
|
45
|
+
'--tc-dot-color': 'rgba(0, 0, 0, 0.08)',
|
|
46
|
+
'--overlay-backdrop-bgColor': 'rgba(0, 0, 0, 0.08)',
|
|
47
|
+
'--fgColor-muted': '#59636e',
|
|
69
48
|
'--fgColor-default': '#1f2328',
|
|
70
49
|
'--fgColor-onEmphasis': '#ffffff',
|
|
71
50
|
'--borderColor-default': '#d1d9e0',
|
|
72
|
-
'--borderColor-muted': '#
|
|
73
|
-
}
|
|
51
|
+
'--borderColor-muted': '#d1d9e0b3',
|
|
52
|
+
},
|
|
53
|
+
dark: {
|
|
54
|
+
'--sb--canvas-bg': '#151b23',
|
|
55
|
+
'--bgColor-default': '#0d1117',
|
|
56
|
+
'--bgColor-muted': '#151b23',
|
|
57
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
58
|
+
'--bgColor-accent-emphasis': '#1f6feb',
|
|
59
|
+
'--tc-bg-muted': '#151b23',
|
|
60
|
+
'--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
|
|
61
|
+
'--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
|
|
62
|
+
'--fgColor-muted': '#9198a1',
|
|
63
|
+
'--fgColor-default': '#f0f6fc',
|
|
64
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
65
|
+
'--borderColor-default': '#3d444d',
|
|
66
|
+
'--borderColor-muted': '#3d444db3',
|
|
67
|
+
},
|
|
68
|
+
dark_dimmed: {
|
|
69
|
+
'--sb--canvas-bg': '#262c36',
|
|
70
|
+
'--bgColor-default': '#212830',
|
|
71
|
+
'--bgColor-muted': '#262c36',
|
|
72
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
73
|
+
'--bgColor-accent-emphasis': '#316dca',
|
|
74
|
+
'--tc-bg-muted': '#262c36',
|
|
75
|
+
'--tc-dot-color': 'rgba(209, 215, 224, 0.18)',
|
|
76
|
+
'--overlay-backdrop-bgColor': 'rgba(209, 215, 224, 0.18)',
|
|
77
|
+
'--fgColor-muted': '#9198a1',
|
|
78
|
+
'--fgColor-default': '#d1d7e0',
|
|
79
|
+
'--fgColor-onEmphasis': '#f0f6fc',
|
|
80
|
+
'--borderColor-default': '#3d444d',
|
|
81
|
+
'--borderColor-muted': '#3d444db3',
|
|
82
|
+
},
|
|
83
|
+
dark_colorblind: {
|
|
84
|
+
'--sb--canvas-bg': '#151b23',
|
|
85
|
+
'--bgColor-default': '#0d1117',
|
|
86
|
+
'--bgColor-muted': '#151b23',
|
|
87
|
+
'--bgColor-neutral-muted': '#656c7633',
|
|
88
|
+
'--bgColor-accent-emphasis': '#1f6feb',
|
|
89
|
+
'--tc-bg-muted': '#151b23',
|
|
90
|
+
'--tc-dot-color': 'rgba(255, 255, 255, 0.1)',
|
|
91
|
+
'--overlay-backdrop-bgColor': 'rgba(255, 255, 255, 0.1)',
|
|
92
|
+
'--fgColor-muted': '#9198a1',
|
|
93
|
+
'--fgColor-default': '#f0f6fc',
|
|
94
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
95
|
+
'--borderColor-default': '#3d444d',
|
|
96
|
+
'--borderColor-muted': '#3d444db3',
|
|
97
|
+
},
|
|
98
|
+
dark_high_contrast: {
|
|
99
|
+
'--sb--canvas-bg': '#151b23',
|
|
100
|
+
'--bgColor-default': '#010409',
|
|
101
|
+
'--bgColor-muted': '#151b23',
|
|
102
|
+
'--bgColor-neutral-muted': '#212830',
|
|
103
|
+
'--bgColor-accent-emphasis': '#194fb1',
|
|
104
|
+
'--tc-bg-muted': '#151b23',
|
|
105
|
+
'--tc-dot-color': 'rgba(183, 189, 200, 0.25)',
|
|
106
|
+
'--overlay-backdrop-bgColor': 'rgba(183, 189, 200, 0.25)',
|
|
107
|
+
'--fgColor-muted': '#b7bdc8',
|
|
108
|
+
'--fgColor-default': '#ffffff',
|
|
109
|
+
'--fgColor-onEmphasis': '#ffffff',
|
|
110
|
+
'--borderColor-default': '#b7bdc8',
|
|
111
|
+
'--borderColor-muted': '#b7bdc8',
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function getCanvasThemeVars(theme) {
|
|
116
|
+
const value = String(theme || 'light')
|
|
117
|
+
return THEME_VARS[value] || THEME_VARS.light
|
|
74
118
|
}
|
|
@@ -1,17 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Canvas Component Isolate — iframe entry point.
|
|
3
3
|
*
|
|
4
|
-
* Renders a single named export from a .
|
|
4
|
+
* Renders a single named export from a .story.jsx module inside an
|
|
5
5
|
* isolated document. The parent CanvasPage embeds this via an iframe
|
|
6
6
|
* so a broken component cannot crash the entire canvas.
|
|
7
7
|
*
|
|
8
8
|
* Query params:
|
|
9
|
-
* module — absolute or base-relative path to the .
|
|
9
|
+
* module — absolute or base-relative path to the .story.jsx file
|
|
10
10
|
* export — the named export to render
|
|
11
11
|
* theme — canvas theme (light / dark / dark_dimmed)
|
|
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 : '')
|
|
@@ -75,9 +97,9 @@ async function mount() {
|
|
|
75
97
|
return
|
|
76
98
|
}
|
|
77
99
|
|
|
78
|
-
// Validate: only allow .
|
|
79
|
-
if (!modulePath.
|
|
80
|
-
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .
|
|
100
|
+
// Validate: only allow .story.{jsx,tsx} modules
|
|
101
|
+
if (!modulePath.match(/\.story\.(jsx|tsx)$/)) {
|
|
102
|
+
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .story.jsx/.tsx files are allowed'))
|
|
81
103
|
return
|
|
82
104
|
}
|
|
83
105
|
|
|
@@ -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) {
|
package/src/canvas/useCanvas.js
CHANGED
|
@@ -31,11 +31,11 @@ export function resolveCanvasModuleImport(modulePath, baseUrl = import.meta.env?
|
|
|
31
31
|
* Uses build-time data for static config (routes, JSX path), but fetches
|
|
32
32
|
* fresh widget data from the server to pick up persisted edits.
|
|
33
33
|
*
|
|
34
|
-
* @param {string}
|
|
34
|
+
* @param {string} canvasId - Canonical canvas ID as indexed by the data plugin
|
|
35
35
|
* @returns {{ canvas: object|null, jsxExports: object|null, jsxError: boolean, loading: boolean }}
|
|
36
36
|
*/
|
|
37
|
-
export function useCanvas(
|
|
38
|
-
const buildTimeCanvas = useMemo(() => getCanvasData(
|
|
37
|
+
export function useCanvas(canvasId) {
|
|
38
|
+
const buildTimeCanvas = useMemo(() => getCanvasData(canvasId), [canvasId])
|
|
39
39
|
const [canvas, setCanvas] = useState(buildTimeCanvas)
|
|
40
40
|
const [jsxExports, setJsxExports] = useState(null)
|
|
41
41
|
const [jsxError, setJsxError] = useState(false)
|
|
@@ -50,7 +50,7 @@ export function useCanvas(name) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
setLoading(true)
|
|
53
|
-
fetchCanvasFromServer(
|
|
53
|
+
fetchCanvasFromServer(canvasId).then((fresh) => {
|
|
54
54
|
if (fresh) {
|
|
55
55
|
// Merge: use server data for widgets/sources, keep build-time for _route/_jsxModule
|
|
56
56
|
setCanvas({ ...buildTimeCanvas, ...fresh })
|
|
@@ -59,7 +59,7 @@ export function useCanvas(name) {
|
|
|
59
59
|
}
|
|
60
60
|
setLoading(false)
|
|
61
61
|
})
|
|
62
|
-
}, [
|
|
62
|
+
}, [canvasId, buildTimeCanvas])
|
|
63
63
|
|
|
64
64
|
const jsxModule = canvas?._jsxModule
|
|
65
65
|
const jsxImport = canvas?._jsxImport
|
|
@@ -99,8 +99,9 @@ export function useCanvas(name) {
|
|
|
99
99
|
if (!import.meta.hot || !buildTimeCanvas) return
|
|
100
100
|
|
|
101
101
|
const handleCanvasFileChanged = ({ data }) => {
|
|
102
|
-
|
|
103
|
-
|
|
102
|
+
const eventId = data?.canvasId || data?.name
|
|
103
|
+
if (!data || eventId !== canvasId) return
|
|
104
|
+
fetchCanvasFromServer(canvasId).then((fresh) => {
|
|
104
105
|
if (fresh) {
|
|
105
106
|
setCanvas((prev) => ({ ...(prev || buildTimeCanvas), ...fresh }))
|
|
106
107
|
}
|
|
@@ -111,7 +112,7 @@ export function useCanvas(name) {
|
|
|
111
112
|
return () => {
|
|
112
113
|
import.meta.hot.off('storyboard:canvas-file-changed', handleCanvasFileChanged)
|
|
113
114
|
}
|
|
114
|
-
}, [
|
|
115
|
+
}, [canvasId, buildTimeCanvas])
|
|
115
116
|
|
|
116
117
|
return { canvas, jsxExports, jsxError, loading }
|
|
117
118
|
}
|
|
@@ -3,14 +3,14 @@ import { resolveCanvasModuleImport } from './useCanvas.js'
|
|
|
3
3
|
|
|
4
4
|
describe('resolveCanvasModuleImport', () => {
|
|
5
5
|
it('prefixes root-relative module paths with BASE_URL', () => {
|
|
6
|
-
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.
|
|
7
|
-
'/feature-branch/src/canvas/button-patterns.
|
|
6
|
+
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.story.jsx', '/feature-branch/')).toBe(
|
|
7
|
+
'/feature-branch/src/canvas/button-patterns.story.jsx',
|
|
8
8
|
)
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
it('keeps root-relative paths unchanged when BASE_URL is root', () => {
|
|
12
|
-
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.
|
|
13
|
-
'/src/canvas/button-patterns.
|
|
12
|
+
expect(resolveCanvasModuleImport('/src/canvas/button-patterns.story.jsx', '/')).toBe(
|
|
13
|
+
'/src/canvas/button-patterns.story.jsx',
|
|
14
14
|
)
|
|
15
15
|
})
|
|
16
16
|
|