@dfosco/storyboard-react 4.2.0-beta.1 → 4.2.0-beta.17
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 +5 -4
- package/src/AuthModal/AuthModal.jsx +6 -2
- package/src/BranchBar/BranchBar.jsx +17 -5
- package/src/BranchBar/BranchBar.module.css +11 -2
- package/src/CommandPalette/CommandPalette.jsx +267 -164
- package/src/CommandPalette/command-palette.css +130 -78
- package/src/Icon.jsx +112 -48
- package/src/Viewfinder.jsx +511 -61
- package/src/Viewfinder.module.css +414 -2
- package/src/canvas/CanvasPage.bridge.test.jsx +14 -6
- package/src/canvas/CanvasPage.dragdrop.test.jsx +10 -6
- package/src/canvas/CanvasPage.jsx +157 -174
- package/src/canvas/CanvasPage.module.css +0 -15
- package/src/canvas/CanvasPage.multiselect.test.jsx +10 -6
- package/src/canvas/ConnectorLayer.jsx +5 -5
- package/src/canvas/PageSelector.test.jsx +15 -6
- package/src/canvas/useCanvas.js +1 -1
- package/src/canvas/widgets/ActionWidget.jsx +200 -0
- package/src/canvas/widgets/ActionWidget.module.css +122 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +97 -29
- package/src/canvas/widgets/FigmaEmbed.module.css +61 -0
- package/src/canvas/widgets/ImageWidget.jsx +1 -1
- package/src/canvas/widgets/LinkPreview.jsx +64 -5
- package/src/canvas/widgets/LinkPreview.module.css +127 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +39 -17
- package/src/canvas/widgets/MarkdownBlock.module.css +123 -0
- package/src/canvas/widgets/PrototypeEmbed.jsx +183 -20
- package/src/canvas/widgets/PrototypeEmbed.module.css +117 -0
- package/src/canvas/widgets/PrototypeEmbed.test.jsx +2 -2
- package/src/canvas/widgets/SplitExpandModal.jsx +234 -0
- package/src/canvas/widgets/SplitExpandModal.module.css +335 -0
- package/src/canvas/widgets/SplitScreenTopBar.jsx +30 -0
- package/src/canvas/widgets/SplitScreenTopBar.module.css +58 -0
- package/src/canvas/widgets/StoryWidget.jsx +7 -4
- package/src/canvas/widgets/TerminalReadWidget.jsx +140 -0
- package/src/canvas/widgets/TerminalReadWidget.module.css +92 -0
- package/src/canvas/widgets/TerminalWidget.jsx +299 -49
- package/src/canvas/widgets/TerminalWidget.module.css +155 -1
- package/src/canvas/widgets/WidgetChrome.jsx +19 -14
- package/src/canvas/widgets/WidgetChrome.module.css +10 -0
- package/src/canvas/widgets/embedInteraction.test.jsx +24 -26
- package/src/canvas/widgets/expandUtils.js +188 -0
- package/src/canvas/widgets/index.js +5 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +23 -71
- package/src/canvas/widgets/widgetConfig.js +19 -1
- package/src/hooks/useConfig.js +14 -0
- package/src/index.js +4 -0
- package/src/vite/data-plugin.js +264 -14
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/* ── Backdrop ──────────────────────────────────────────────────────── */
|
|
2
|
+
|
|
3
|
+
.backdrop {
|
|
4
|
+
position: fixed;
|
|
5
|
+
inset: 0;
|
|
6
|
+
z-index: 100000;
|
|
7
|
+
background: rgba(0, 0, 0, 0.8);
|
|
8
|
+
display: flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
justify-content: center;
|
|
11
|
+
animation: splitExpandFadeIn 0.15s ease;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
@keyframes splitExpandFadeIn {
|
|
15
|
+
from { opacity: 0; }
|
|
16
|
+
to { opacity: 1; }
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/* ── Modal container ──────────────────────────────────────────────── */
|
|
20
|
+
|
|
21
|
+
.modal {
|
|
22
|
+
width: 90vw;
|
|
23
|
+
height: 90vh;
|
|
24
|
+
position: relative;
|
|
25
|
+
border-radius: 12px;
|
|
26
|
+
overflow: hidden;
|
|
27
|
+
background: var(--bgColor-default, #ffffff);
|
|
28
|
+
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.4);
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
animation: splitExpandScaleIn 0.2s ease;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.modalFullscreen {
|
|
35
|
+
position: fixed;
|
|
36
|
+
inset: 0;
|
|
37
|
+
overflow: hidden;
|
|
38
|
+
background: var(--bgColor-default, #ffffff);
|
|
39
|
+
display: flex;
|
|
40
|
+
flex-direction: column;
|
|
41
|
+
animation: splitExpandScaleIn 0.2s ease;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@keyframes splitExpandScaleIn {
|
|
45
|
+
from { transform: scale(0.95); opacity: 0; }
|
|
46
|
+
to { transform: scale(1); opacity: 1; }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* ── Top bar ──────────────────────────────────────────────────────── */
|
|
50
|
+
|
|
51
|
+
.topBar {
|
|
52
|
+
display: flex;
|
|
53
|
+
align-items: center;
|
|
54
|
+
height: 40px;
|
|
55
|
+
padding: 0 12px;
|
|
56
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
57
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
58
|
+
flex-shrink: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.topBarTitle {
|
|
62
|
+
font-size: 12px;
|
|
63
|
+
font-weight: 500;
|
|
64
|
+
color: var(--fgColor-muted, #656d76);
|
|
65
|
+
overflow: hidden;
|
|
66
|
+
text-overflow: ellipsis;
|
|
67
|
+
white-space: nowrap;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.closeBtn {
|
|
71
|
+
all: unset;
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
margin-left: auto;
|
|
74
|
+
width: 28px;
|
|
75
|
+
height: 28px;
|
|
76
|
+
display: flex;
|
|
77
|
+
align-items: center;
|
|
78
|
+
justify-content: center;
|
|
79
|
+
border-radius: 6px;
|
|
80
|
+
color: var(--fgColor-muted, #656d76);
|
|
81
|
+
font-size: 14px;
|
|
82
|
+
transition: background 100ms, color 100ms;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.closeBtn:hover {
|
|
86
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
87
|
+
color: var(--fgColor-default, #1f2328);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.closeBtnFloat {
|
|
91
|
+
all: unset;
|
|
92
|
+
cursor: pointer;
|
|
93
|
+
position: absolute;
|
|
94
|
+
top: 12px;
|
|
95
|
+
right: 12px;
|
|
96
|
+
width: 32px;
|
|
97
|
+
height: 32px;
|
|
98
|
+
display: flex;
|
|
99
|
+
align-items: center;
|
|
100
|
+
justify-content: center;
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
background: rgba(0, 0, 0, 0.5);
|
|
103
|
+
color: #ffffff;
|
|
104
|
+
font-size: 16px;
|
|
105
|
+
z-index: 1;
|
|
106
|
+
transition: background 100ms;
|
|
107
|
+
backdrop-filter: blur(4px);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.closeBtnFloat:hover {
|
|
111
|
+
background: rgba(0, 0, 0, 0.7);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/* ── Body ─────────────────────────────────────────────────────────── */
|
|
115
|
+
|
|
116
|
+
.body {
|
|
117
|
+
flex: 1;
|
|
118
|
+
min-height: 0;
|
|
119
|
+
display: flex;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
.bodySplit {
|
|
123
|
+
flex: 1;
|
|
124
|
+
min-height: 0;
|
|
125
|
+
display: flex;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.bodySplit .splitLeft,
|
|
129
|
+
.bodySplit .splitRight {
|
|
130
|
+
flex: 1;
|
|
131
|
+
min-width: 0;
|
|
132
|
+
height: 100%;
|
|
133
|
+
overflow: hidden;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.bodySplit .splitLeft {
|
|
137
|
+
border-right: 1px solid var(--borderColor-muted, #d8dee4);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── Panes ────────────────────────────────────────────────────────── */
|
|
141
|
+
|
|
142
|
+
.primaryPane {
|
|
143
|
+
width: 100%;
|
|
144
|
+
height: 100%;
|
|
145
|
+
overflow: auto;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.secondaryPane {
|
|
149
|
+
width: 100%;
|
|
150
|
+
height: 100%;
|
|
151
|
+
overflow: auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.secondaryIframe {
|
|
155
|
+
border: none;
|
|
156
|
+
width: 100%;
|
|
157
|
+
height: 100%;
|
|
158
|
+
display: block;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
.terminalContainer {
|
|
162
|
+
width: 100%;
|
|
163
|
+
height: 100%;
|
|
164
|
+
background: #0d1117;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/* ── Markdown content in secondary pane ───────────────────────────── */
|
|
168
|
+
|
|
169
|
+
.markdownContent {
|
|
170
|
+
padding: 32px 40px;
|
|
171
|
+
font-size: 15px;
|
|
172
|
+
line-height: 1.7;
|
|
173
|
+
color: var(--fgColor-default, #1f2328);
|
|
174
|
+
font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
|
|
175
|
+
max-width: 800px;
|
|
176
|
+
margin: 0 auto;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.markdownContent * {
|
|
180
|
+
pointer-events: auto;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.markdownContent a {
|
|
184
|
+
color: var(--fgColor-accent, #0969da);
|
|
185
|
+
text-decoration: none;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.markdownContent a:hover {
|
|
189
|
+
text-decoration: underline;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.markdownContent img {
|
|
193
|
+
max-width: 100%;
|
|
194
|
+
height: auto;
|
|
195
|
+
border-radius: 6px;
|
|
196
|
+
border: 1px solid var(--borderColor-default, #d0d7de);
|
|
197
|
+
margin: 8px 0;
|
|
198
|
+
display: block;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.markdownContent video {
|
|
202
|
+
max-width: 100%;
|
|
203
|
+
height: auto;
|
|
204
|
+
border-radius: 6px;
|
|
205
|
+
margin: 8px 0;
|
|
206
|
+
display: block;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.markdownContent pre {
|
|
210
|
+
padding: 12px 16px;
|
|
211
|
+
border-radius: 6px;
|
|
212
|
+
border: 1px solid var(--borderColor-muted, #d8dee4);
|
|
213
|
+
overflow-x: auto;
|
|
214
|
+
margin: 8px 0;
|
|
215
|
+
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
216
|
+
line-height: 1.4;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
.markdownContent code {
|
|
220
|
+
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
221
|
+
padding: 2px 5px;
|
|
222
|
+
border-radius: 4px;
|
|
223
|
+
font-size: 13px;
|
|
224
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.markdownContent pre code {
|
|
228
|
+
background: none;
|
|
229
|
+
padding: 0;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.markdownContent h1 { font-size: 22px; font-weight: 700; margin: 0 0 12px; }
|
|
233
|
+
.markdownContent h2 { font-size: 18px; font-weight: 600; margin: 0 0 8px; }
|
|
234
|
+
.markdownContent h3 { font-size: 16px; font-weight: 600; margin: 0 0 6px; }
|
|
235
|
+
.markdownContent p { margin: 0 0 12px; }
|
|
236
|
+
.markdownContent ul { margin: 0 0 12px; padding-left: 24px; list-style: disc; }
|
|
237
|
+
.markdownContent ol { margin: 0 0 12px; padding-left: 24px; list-style: decimal; }
|
|
238
|
+
.markdownContent li { margin: 0 0 4px; display: list-item; }
|
|
239
|
+
|
|
240
|
+
.markdownContent blockquote {
|
|
241
|
+
border-left: 4px solid var(--borderColor-default, #d0d7de);
|
|
242
|
+
margin: 12px 0;
|
|
243
|
+
padding: 4px 16px;
|
|
244
|
+
color: var(--fgColor-muted, #656d76);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.markdownContent table {
|
|
248
|
+
border-collapse: collapse;
|
|
249
|
+
margin: 12px 0;
|
|
250
|
+
width: 100%;
|
|
251
|
+
font-size: 14px;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
.markdownContent th,
|
|
255
|
+
.markdownContent td {
|
|
256
|
+
border: 1px solid var(--borderColor-default, #d0d7de);
|
|
257
|
+
padding: 6px 12px;
|
|
258
|
+
text-align: left;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.markdownContent th {
|
|
262
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
263
|
+
font-weight: 600;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/* ── GitHub card in secondary pane ────────────────────────────────── */
|
|
267
|
+
|
|
268
|
+
.githubCard {
|
|
269
|
+
height: 100%;
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-direction: column;
|
|
272
|
+
background: var(--bgColor-default, #ffffff);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
.githubHeader {
|
|
276
|
+
display: flex;
|
|
277
|
+
align-items: center;
|
|
278
|
+
gap: 8px;
|
|
279
|
+
padding: 16px 24px;
|
|
280
|
+
font-size: 18px;
|
|
281
|
+
font-weight: 600;
|
|
282
|
+
color: var(--fgColor-default, #1f2328);
|
|
283
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.githubTitle {
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
text-overflow: ellipsis;
|
|
289
|
+
white-space: nowrap;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
.githubBody {
|
|
293
|
+
flex: 1;
|
|
294
|
+
overflow: auto;
|
|
295
|
+
padding: 24px;
|
|
296
|
+
font-size: 15px;
|
|
297
|
+
line-height: 1.7;
|
|
298
|
+
color: var(--fgColor-default, #1f2328);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.githubBody * { pointer-events: auto; }
|
|
302
|
+
.githubBody a { color: var(--fgColor-accent, #0969da); text-decoration: none; }
|
|
303
|
+
.githubBody a:hover { text-decoration: underline; }
|
|
304
|
+
.githubBody img { max-width: 100%; height: auto; border-radius: 6px; margin: 8px 0; display: block; }
|
|
305
|
+
.githubBody video { max-width: 100%; height: auto; border-radius: 6px; margin: 8px 0; display: block; }
|
|
306
|
+
.githubBody pre { padding: 12px 16px; border-radius: 6px; border: 1px solid var(--borderColor-muted, #d8dee4); overflow-x: auto; margin: 12px 0; background: var(--bgColor-neutral-muted, #afb8c133); }
|
|
307
|
+
.githubBody code { background: var(--bgColor-neutral-muted, #afb8c133); padding: 2px 5px; border-radius: 4px; font-size: 13px; font-family: ui-monospace, SFMono-Regular, monospace; }
|
|
308
|
+
.githubBody pre code { background: none; padding: 0; }
|
|
309
|
+
.githubBody h1 { font-size: 20px; font-weight: 700; margin: 16px 0 8px; }
|
|
310
|
+
.githubBody h2 { font-size: 17px; font-weight: 600; margin: 14px 0 6px; }
|
|
311
|
+
.githubBody p { margin: 0 0 12px; }
|
|
312
|
+
|
|
313
|
+
/* ── Plain link card in secondary pane ────────────────────────────── */
|
|
314
|
+
|
|
315
|
+
.linkCard {
|
|
316
|
+
padding: 32px 40px;
|
|
317
|
+
font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.linkTitle {
|
|
321
|
+
font-size: 18px;
|
|
322
|
+
font-weight: 600;
|
|
323
|
+
color: var(--fgColor-default, #1f2328);
|
|
324
|
+
margin: 0 0 8px;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.linkUrl {
|
|
328
|
+
font-size: 14px;
|
|
329
|
+
color: var(--fgColor-accent, #0969da);
|
|
330
|
+
text-decoration: none;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.linkUrl:hover {
|
|
334
|
+
text-decoration: underline;
|
|
335
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SplitScreenTopBar — terminal-style slim top bar for split-screen modals.
|
|
3
|
+
* Shows "Type · Metadata" for each pane, with active pane in default color
|
|
4
|
+
* and inactive pane in muted color. Theme-responsive.
|
|
5
|
+
*/
|
|
6
|
+
import { ScreenNormalIcon } from '@primer/octicons-react'
|
|
7
|
+
import styles from './SplitScreenTopBar.module.css'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {Object} props
|
|
11
|
+
* @param {string} props.leftLabel — "Type · Metadata" for the left pane
|
|
12
|
+
* @param {string} props.rightLabel — "Type · Metadata" for the right pane
|
|
13
|
+
* @param {'left' | 'right'} props.activePane — which pane has focus
|
|
14
|
+
* @param {() => void} props.onClose — close handler
|
|
15
|
+
*/
|
|
16
|
+
export default function SplitScreenTopBar({ leftLabel, rightLabel, activePane, onClose }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={styles.bar}>
|
|
19
|
+
<span className={`${styles.leftLabel} ${activePane === 'left' ? styles.active : styles.muted}`}>
|
|
20
|
+
{leftLabel}
|
|
21
|
+
</span>
|
|
22
|
+
<span className={`${styles.rightLabel} ${activePane === 'right' ? styles.active : styles.muted}`}>
|
|
23
|
+
{rightLabel}
|
|
24
|
+
</span>
|
|
25
|
+
<button className={styles.closeBtn} onClick={onClose} aria-label="Close expanded view" autoFocus>
|
|
26
|
+
<ScreenNormalIcon size={16} />
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* Theme-responsive split-screen top bar (terminal-style slim bar) */
|
|
2
|
+
|
|
3
|
+
.bar {
|
|
4
|
+
display: flex;
|
|
5
|
+
align-items: center;
|
|
6
|
+
height: 40px;
|
|
7
|
+
padding: 0 12px;
|
|
8
|
+
gap: 12px;
|
|
9
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
10
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
11
|
+
flex-shrink: 0;
|
|
12
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
13
|
+
font-size: 12px;
|
|
14
|
+
font-weight: 500;
|
|
15
|
+
user-select: none;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.leftLabel {
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
overflow: hidden;
|
|
21
|
+
text-overflow: ellipsis;
|
|
22
|
+
min-width: 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.rightLabel {
|
|
26
|
+
white-space: nowrap;
|
|
27
|
+
overflow: hidden;
|
|
28
|
+
text-overflow: ellipsis;
|
|
29
|
+
min-width: 0;
|
|
30
|
+
margin-left: auto;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.active {
|
|
34
|
+
color: var(--fgColor-default, #1f2328);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.muted {
|
|
38
|
+
color: var(--fgColor-muted, #656d76);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.closeBtn {
|
|
42
|
+
all: unset;
|
|
43
|
+
cursor: pointer;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
width: 28px;
|
|
46
|
+
height: 28px;
|
|
47
|
+
display: flex;
|
|
48
|
+
align-items: center;
|
|
49
|
+
justify-content: center;
|
|
50
|
+
border-radius: 6px;
|
|
51
|
+
color: var(--fgColor-muted, #656d76);
|
|
52
|
+
transition: background 100ms, color 100ms;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
.closeBtn:hover {
|
|
56
|
+
background: var(--bgColor-neutral-muted, #eaeef2);
|
|
57
|
+
color: var(--fgColor-default, #1f2328);
|
|
58
|
+
}
|
|
@@ -171,10 +171,13 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
171
171
|
[storyId, exportName, storyIndexKey],
|
|
172
172
|
)
|
|
173
173
|
|
|
174
|
+
// When paused and not interactive, freeze the iframe src to prevent reloads
|
|
175
|
+
const effectiveSrc = iframeSrc
|
|
176
|
+
|
|
174
177
|
useIframeDevLogs({
|
|
175
178
|
widget: 'StoryWidget',
|
|
176
|
-
loaded: interactive && !showCode && Boolean(
|
|
177
|
-
src:
|
|
179
|
+
loaded: interactive && !showCode && Boolean(effectiveSrc),
|
|
180
|
+
src: effectiveSrc,
|
|
178
181
|
})
|
|
179
182
|
|
|
180
183
|
const displayName = exportName ? `${storyId} / ${exportName}` : storyId
|
|
@@ -192,7 +195,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
192
195
|
)
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
if (!
|
|
198
|
+
if (!effectiveSrc) {
|
|
196
199
|
return (
|
|
197
200
|
<WidgetWrapper>
|
|
198
201
|
<div className={styles.container} ref={containerRef}>
|
|
@@ -241,7 +244,7 @@ export default forwardRef(function StoryWidget({ id: widgetId, props, onUpdate,
|
|
|
241
244
|
<div className={styles.content}>
|
|
242
245
|
<iframe
|
|
243
246
|
ref={iframeRef}
|
|
244
|
-
src={
|
|
247
|
+
src={effectiveSrc}
|
|
245
248
|
className={styles.iframe}
|
|
246
249
|
title={displayName}
|
|
247
250
|
/>
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { useRef, useEffect, useState } from 'react'
|
|
2
|
+
import { readProp, schemas } from './widgetProps.js'
|
|
3
|
+
import styles from './TerminalReadWidget.module.css'
|
|
4
|
+
|
|
5
|
+
const terminalSchema = schemas['terminal']
|
|
6
|
+
|
|
7
|
+
let Convert = null
|
|
8
|
+
let ansiLoadAttempted = false
|
|
9
|
+
|
|
10
|
+
async function getConverter() {
|
|
11
|
+
if (Convert) return new Convert({ fg: '#e6edf3', bg: '#0d1117', newline: true })
|
|
12
|
+
if (ansiLoadAttempted) return null
|
|
13
|
+
ansiLoadAttempted = true
|
|
14
|
+
try {
|
|
15
|
+
const mod = await import(/* @vite-ignore */ 'ansi-to-html')
|
|
16
|
+
Convert = mod.default || mod
|
|
17
|
+
return new Convert({ fg: '#e6edf3', bg: '#0d1117', newline: true })
|
|
18
|
+
} catch {
|
|
19
|
+
return null
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function stripAnsi(text) {
|
|
24
|
+
// eslint-disable-next-line no-control-regex
|
|
25
|
+
return text.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '')
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getBaseUrl() {
|
|
29
|
+
const base = (typeof import.meta !== 'undefined' && import.meta.env?.BASE_URL) || '/'
|
|
30
|
+
return base.endsWith('/') ? base : base + '/'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function getCanvasId() {
|
|
34
|
+
return window.__storyboardCanvasBridgeState?.canvasId || null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function isProduction() {
|
|
38
|
+
return typeof import.meta !== 'undefined' && import.meta.env?.PROD
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default function TerminalReadWidget({ id, props }) {
|
|
42
|
+
const width = readProp(props, 'width', terminalSchema)
|
|
43
|
+
const height = readProp(props, 'height', terminalSchema)
|
|
44
|
+
const prettyName = props?.prettyName || '...'
|
|
45
|
+
|
|
46
|
+
const [content, setContent] = useState(null)
|
|
47
|
+
const [html, setHtml] = useState(null)
|
|
48
|
+
const [failed, setFailed] = useState(false)
|
|
49
|
+
const contentRef = useRef(null)
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
let cancelled = false
|
|
53
|
+
async function fetchSnapshot() {
|
|
54
|
+
const baseUrl = getBaseUrl()
|
|
55
|
+
const canvasId = getCanvasId()
|
|
56
|
+
if (!canvasId) { setFailed(true); return }
|
|
57
|
+
|
|
58
|
+
const urls = isProduction()
|
|
59
|
+
? [`${baseUrl}_storyboard/terminal-snapshots/${canvasId}/${id}.json`]
|
|
60
|
+
: [
|
|
61
|
+
`${baseUrl}_storyboard/canvas/${canvasId}/terminal-snapshot/${id}`,
|
|
62
|
+
`${baseUrl}_storyboard/terminal-snapshots/${canvasId}/${id}.json`,
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
for (const url of urls) {
|
|
66
|
+
try {
|
|
67
|
+
const res = await fetch(url)
|
|
68
|
+
if (!res.ok) continue
|
|
69
|
+
const data = await res.json()
|
|
70
|
+
if (cancelled) return
|
|
71
|
+
const text = data.content || data.output || ''
|
|
72
|
+
setContent(text)
|
|
73
|
+
|
|
74
|
+
const converter = await getConverter()
|
|
75
|
+
if (cancelled) return
|
|
76
|
+
if (converter) {
|
|
77
|
+
setHtml(converter.toHtml(text))
|
|
78
|
+
} else {
|
|
79
|
+
setContent(stripAnsi(text))
|
|
80
|
+
}
|
|
81
|
+
return
|
|
82
|
+
} catch {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
if (!cancelled) setFailed(true)
|
|
87
|
+
}
|
|
88
|
+
fetchSnapshot()
|
|
89
|
+
return () => { cancelled = true }
|
|
90
|
+
}, [id])
|
|
91
|
+
|
|
92
|
+
// Auto-scroll to bottom
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
if (contentRef.current) {
|
|
95
|
+
contentRef.current.scrollTop = contentRef.current.scrollHeight
|
|
96
|
+
}
|
|
97
|
+
}, [html, content])
|
|
98
|
+
|
|
99
|
+
const titleLabel = `terminal · ${prettyName}`
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<div className={styles.container}>
|
|
103
|
+
<div className={styles.titleBar}>
|
|
104
|
+
<span>{titleLabel}</span>
|
|
105
|
+
<span className={styles.readOnlyBadge}>read only</span>
|
|
106
|
+
</div>
|
|
107
|
+
<div
|
|
108
|
+
ref={contentRef}
|
|
109
|
+
className={styles.content}
|
|
110
|
+
style={{
|
|
111
|
+
...(typeof width === 'number' ? { width: `${width}px` } : undefined),
|
|
112
|
+
...(typeof height === 'number' ? { height: `${height}px` } : undefined),
|
|
113
|
+
}}
|
|
114
|
+
>
|
|
115
|
+
{failed && (
|
|
116
|
+
<div className={styles.placeholder}>
|
|
117
|
+
<span className={styles.placeholderTitle}>Terminal session · {prettyName}</span>
|
|
118
|
+
<span className={styles.placeholderSub}>No captured output available</span>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
{!failed && content === null && (
|
|
122
|
+
<div className={styles.placeholder}>
|
|
123
|
+
<span className={styles.placeholderSub}>Loading…</span>
|
|
124
|
+
</div>
|
|
125
|
+
)}
|
|
126
|
+
{!failed && html && (
|
|
127
|
+
<pre
|
|
128
|
+
style={{ margin: 0, whiteSpace: 'pre', fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit' }}
|
|
129
|
+
dangerouslySetInnerHTML={{ __html: html }}
|
|
130
|
+
/>
|
|
131
|
+
)}
|
|
132
|
+
{!failed && content !== null && !html && (
|
|
133
|
+
<pre style={{ margin: 0, whiteSpace: 'pre', fontFamily: 'inherit', fontSize: 'inherit', lineHeight: 'inherit' }}>
|
|
134
|
+
{content}
|
|
135
|
+
</pre>
|
|
136
|
+
)}
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
.container {
|
|
2
|
+
position: relative;
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
border-radius: var(--base-size-16, 16px);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
:global(.tc-drag-surface):has(.container) {
|
|
9
|
+
border-radius: 16px;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.titleBar {
|
|
13
|
+
position: absolute;
|
|
14
|
+
top: -28px;
|
|
15
|
+
left: 4px;
|
|
16
|
+
display: flex;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: 8px;
|
|
19
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
20
|
+
font-size: 11px;
|
|
21
|
+
color: #8b949e;
|
|
22
|
+
pointer-events: none;
|
|
23
|
+
user-select: none;
|
|
24
|
+
white-space: nowrap;
|
|
25
|
+
z-index: 2;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
[data-widget-selected] .titleBar {
|
|
29
|
+
color: var(--borderColor-accent-emphasis, #0969da);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.readOnlyBadge {
|
|
33
|
+
font-size: 10px;
|
|
34
|
+
color: #6e7681;
|
|
35
|
+
background: rgba(110, 118, 129, 0.15);
|
|
36
|
+
padding: 1px 6px;
|
|
37
|
+
border-radius: 4px;
|
|
38
|
+
font-weight: 500;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.content {
|
|
42
|
+
background: #0d1117;
|
|
43
|
+
color: #e6edf3;
|
|
44
|
+
font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace;
|
|
45
|
+
font-size: 13px;
|
|
46
|
+
line-height: 17px;
|
|
47
|
+
padding: 12px;
|
|
48
|
+
overflow-y: auto;
|
|
49
|
+
white-space: pre;
|
|
50
|
+
flex: 1;
|
|
51
|
+
border: 1px solid var(--borderColor-default, #30363d);
|
|
52
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.24);
|
|
53
|
+
border-radius: var(--base-size-16, 16px);
|
|
54
|
+
box-sizing: border-box;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.content::-webkit-scrollbar {
|
|
58
|
+
width: 6px;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.content::-webkit-scrollbar-thumb {
|
|
62
|
+
background: rgba(255, 255, 255, 0.15);
|
|
63
|
+
border-radius: 3px;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.content::-webkit-scrollbar-track {
|
|
67
|
+
background: transparent;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.placeholder {
|
|
71
|
+
display: flex;
|
|
72
|
+
flex-direction: column;
|
|
73
|
+
align-items: center;
|
|
74
|
+
justify-content: center;
|
|
75
|
+
gap: 8px;
|
|
76
|
+
height: 100%;
|
|
77
|
+
color: #8b949e;
|
|
78
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
79
|
+
font-size: 13px;
|
|
80
|
+
text-align: center;
|
|
81
|
+
user-select: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.placeholderTitle {
|
|
85
|
+
color: #e6edf3;
|
|
86
|
+
font-weight: 500;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.placeholderSub {
|
|
90
|
+
color: #6e7681;
|
|
91
|
+
font-size: 12px;
|
|
92
|
+
}
|