@dfosco/storyboard-react 4.0.0-beta.13 → 4.0.0-beta.15
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 +3 -3
- package/src/canvas/CanvasControls.jsx +51 -2
- package/src/canvas/CanvasControls.module.css +31 -0
- package/src/canvas/CanvasPage.jsx +77 -109
- package/src/canvas/CanvasPage.module.css +3 -47
- package/src/canvas/PageSelector.jsx +102 -0
- package/src/canvas/PageSelector.module.css +93 -0
- package/src/canvas/PageSelector.test.jsx +104 -0
- package/src/canvas/componentIsolate.jsx +3 -3
- package/src/canvas/widgets/FigmaEmbed.jsx +6 -1
- package/src/canvas/widgets/MarkdownBlock.jsx +84 -4
- package/src/canvas/widgets/MarkdownBlock.module.css +30 -4
- package/src/canvas/widgets/PrototypeEmbed.jsx +177 -38
- package/src/canvas/widgets/PrototypeEmbed.module.css +34 -0
- package/src/canvas/widgets/StickyNote.module.css +5 -0
- package/src/canvas/widgets/StoryWidget.jsx +438 -0
- package/src/canvas/widgets/StoryWidget.module.css +200 -0
- package/src/canvas/widgets/WidgetChrome.jsx +30 -3
- package/src/canvas/widgets/index.js +2 -0
- package/src/canvas/widgets/pasteRules.js +295 -0
- package/src/canvas/widgets/pasteRules.test.js +474 -0
- package/src/canvas/widgets/widgetConfig.js +1 -1
- package/src/canvas/widgets/widgetConfig.test.js +4 -1
- package/src/context.jsx +138 -13
- package/src/story/StoryPage.jsx +152 -0
- package/src/story/StoryPage.module.css +73 -0
- package/src/vite/data-plugin.js +234 -27
- package/src/vite/data-plugin.test.js +179 -4
package/src/context.jsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useEffect, useMemo, Suspense, lazy } from 'react'
|
|
2
2
|
import { useParams, useLocation } from 'react-router-dom'
|
|
3
|
-
// Named import seeds the core data index via init() AND provides canvas route data
|
|
4
|
-
import { canvases } from 'virtual:storyboard-data-index'
|
|
3
|
+
// Named import seeds the core data index via init() AND provides canvas/story route data
|
|
4
|
+
import { canvases, canvasAliases, stories } from 'virtual:storyboard-data-index'
|
|
5
5
|
import { loadFlow, flowExists, findRecord, deepMerge, setFlowClass, installBodyClassSync, resolveFlowName, resolveRecordName, isModesEnabled } from '@dfosco/storyboard-core'
|
|
6
6
|
import { StoryboardContext } from './StoryboardContext.js'
|
|
7
7
|
import styles from './FlowError.module.css'
|
|
@@ -9,24 +9,77 @@ import styles from './FlowError.module.css'
|
|
|
9
9
|
export { StoryboardContext }
|
|
10
10
|
|
|
11
11
|
const CanvasPageLazy = lazy(() => import('./canvas/CanvasPage.jsx'))
|
|
12
|
+
const StoryPageLazy = lazy(() => import('./story/StoryPage.jsx'))
|
|
12
13
|
|
|
13
14
|
// Build a map from canvas route paths → canvas names at module load time
|
|
14
15
|
const canvasRouteMap = new Map()
|
|
16
|
+
// Build a map from group name → array of { name, route, title } for page selector
|
|
17
|
+
const canvasGroupMap = new Map()
|
|
15
18
|
for (const [name, data] of Object.entries(canvases || {})) {
|
|
16
|
-
const route = (data?._route ||
|
|
19
|
+
const route = (data?._route || `/canvas/${name}`).replace(/\/+$/, '')
|
|
17
20
|
canvasRouteMap.set(route, name)
|
|
21
|
+
const group = data?._group
|
|
22
|
+
if (group) {
|
|
23
|
+
if (!canvasGroupMap.has(group)) canvasGroupMap.set(group, [])
|
|
24
|
+
canvasGroupMap.get(group).push({
|
|
25
|
+
name,
|
|
26
|
+
route,
|
|
27
|
+
title: data?.title || name.split('/').pop(),
|
|
28
|
+
_canvasMeta: data?._canvasMeta || null,
|
|
29
|
+
})
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Build a map from story route paths → story names at module load time
|
|
34
|
+
const storyRouteMap = new Map()
|
|
35
|
+
for (const [name, data] of Object.entries(stories || {})) {
|
|
36
|
+
if (data?._route) {
|
|
37
|
+
const route = data._route.replace(/\/+$/, '')
|
|
38
|
+
storyRouteMap.set(route, name)
|
|
39
|
+
}
|
|
18
40
|
}
|
|
19
41
|
|
|
20
42
|
function matchCanvasRoute(pathname) {
|
|
21
|
-
const normalized = pathname
|
|
43
|
+
const normalized = stripBasePath(pathname)
|
|
22
44
|
return canvasRouteMap.get(normalized) || null
|
|
23
45
|
}
|
|
24
46
|
|
|
47
|
+
function matchStoryRoute(pathname) {
|
|
48
|
+
const normalized = stripBasePath(pathname)
|
|
49
|
+
return storyRouteMap.get(normalized) || null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Strip the app's sub-path prefix (e.g. /storyboard) from the pathname.
|
|
54
|
+
* React Router's basename strips the branch prefix but not the app name prefix
|
|
55
|
+
* when the app runs under a nested base path.
|
|
56
|
+
*/
|
|
57
|
+
function stripBasePath(pathname) {
|
|
58
|
+
let p = pathname.replace(/\/+$/, '') || '/'
|
|
59
|
+
// BASE_URL includes branch prefix + app path (e.g. /branch--name/storyboard/)
|
|
60
|
+
// React Router strips the branch prefix but may leave the app sub-path
|
|
61
|
+
const base = (import.meta.env?.BASE_URL || '/').replace(/\/+$/, '')
|
|
62
|
+
if (base && base !== '/') {
|
|
63
|
+
// Extract just the last segment(s) after the branch prefix
|
|
64
|
+
const withoutBranch = base.replace(/^\/branch--[^/]+/, '')
|
|
65
|
+
const subPath = withoutBranch.replace(/\/+$/, '')
|
|
66
|
+
if (subPath && p.startsWith(subPath)) {
|
|
67
|
+
p = p.slice(subPath.length) || '/'
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return p
|
|
71
|
+
}
|
|
72
|
+
|
|
25
73
|
function isCanvasPath(pathname) {
|
|
26
|
-
const normalized = pathname
|
|
74
|
+
const normalized = stripBasePath(pathname)
|
|
27
75
|
return normalized === '/canvas' || normalized.startsWith('/canvas/')
|
|
28
76
|
}
|
|
29
77
|
|
|
78
|
+
function isStoryPath(pathname) {
|
|
79
|
+
const normalized = stripBasePath(pathname)
|
|
80
|
+
return normalized === '/components' || normalized.startsWith('/components/')
|
|
81
|
+
}
|
|
82
|
+
|
|
30
83
|
/**
|
|
31
84
|
* Derives the top-level prototype name from a pathname.
|
|
32
85
|
* "/Dashboard" → "Dashboard", "/Dashboard/sub" → "Dashboard"
|
|
@@ -68,18 +121,25 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
68
121
|
// Canvas route detection — matches current URL against registered canvas routes
|
|
69
122
|
const canvasName = useMemo(() => matchCanvasRoute(location.pathname), [location.pathname])
|
|
70
123
|
const isMissingCanvasRoute = useMemo(
|
|
71
|
-
() => isCanvasPath(location.pathname) && !canvasName,
|
|
124
|
+
() => isCanvasPath(location.pathname) && !canvasName && !matchStoryRoute(location.pathname),
|
|
72
125
|
[location.pathname, canvasName],
|
|
73
126
|
)
|
|
74
127
|
|
|
128
|
+
// Story route detection — matches current URL against registered story routes
|
|
129
|
+
const storyName = useMemo(() => matchStoryRoute(location.pathname), [location.pathname])
|
|
130
|
+
const isMissingStoryRoute = useMemo(
|
|
131
|
+
() => isStoryPath(location.pathname) && !storyName,
|
|
132
|
+
[location.pathname, storyName],
|
|
133
|
+
)
|
|
134
|
+
|
|
75
135
|
const searchParams = new URLSearchParams(location.search)
|
|
76
136
|
const sceneParam = searchParams.get('flow') || searchParams.get('scene')
|
|
77
137
|
const prototypeName = getPrototypeName(location.pathname)
|
|
78
138
|
const pageFlow = getPageFlowName(location.pathname)
|
|
79
139
|
|
|
80
|
-
// Resolve flow name with prototype scoping (skip for canvas pages)
|
|
140
|
+
// Resolve flow name with prototype scoping (skip for canvas/story pages)
|
|
81
141
|
const activeFlowName = useMemo(() => {
|
|
82
|
-
if (canvasName || isMissingCanvasRoute) return null
|
|
142
|
+
if (canvasName || isMissingCanvasRoute || storyName || isMissingStoryRoute) return null
|
|
83
143
|
const requested = sceneParam || flowName || sceneName
|
|
84
144
|
if (requested) {
|
|
85
145
|
// Allow fully-scoped flow names from URLs/widgets without re-prefixing
|
|
@@ -103,11 +163,32 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
103
163
|
// 4. Global default — or null if no flow exists at all
|
|
104
164
|
if (flowExists('default')) return 'default'
|
|
105
165
|
return null
|
|
106
|
-
}, [canvasName, isMissingCanvasRoute, sceneParam, flowName, sceneName, prototypeName, pageFlow])
|
|
166
|
+
}, [canvasName, isMissingCanvasRoute, storyName, isMissingStoryRoute, sceneParam, flowName, sceneName, prototypeName, pageFlow])
|
|
107
167
|
|
|
108
168
|
// Auto-install body class sync (sb-key--value classes on <body>)
|
|
109
169
|
useEffect(() => installBodyClassSync(), [])
|
|
110
170
|
|
|
171
|
+
// Update document.title to reflect the current artifact
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
const base = import.meta.env?.BASE_URL || '/'
|
|
174
|
+
const branchMatch = base.match(/\/branch--([^/]+)/)
|
|
175
|
+
const branchSuffix = branchMatch ? ` (${branchMatch[1]})` : ''
|
|
176
|
+
|
|
177
|
+
let title
|
|
178
|
+
if (canvasName) {
|
|
179
|
+
const canvasData = canvases?.[canvasName]
|
|
180
|
+
const meta = canvasData?._canvasMeta
|
|
181
|
+
const pageTitle = canvasData?.title || canvasName.split('/').pop()
|
|
182
|
+
title = (meta?.title || pageTitle) + ' · Storyboard'
|
|
183
|
+
} else if (prototypeName) {
|
|
184
|
+
title = prototypeName + ' · Storyboard'
|
|
185
|
+
} else {
|
|
186
|
+
title = 'Storyboard'
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
document.title = title + branchSuffix
|
|
190
|
+
}, [canvasName, prototypeName])
|
|
191
|
+
|
|
111
192
|
// Mount design modes UI when enabled in storyboard.config.json
|
|
112
193
|
useEffect(() => {
|
|
113
194
|
if (!isModesEnabled()) return
|
|
@@ -124,9 +205,9 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
124
205
|
return () => cleanup?.()
|
|
125
206
|
}, [])
|
|
126
207
|
|
|
127
|
-
// Skip flow loading for canvas pages and flow-less pages
|
|
208
|
+
// Skip flow loading for canvas/story pages and flow-less pages
|
|
128
209
|
const { data, error } = useMemo(() => {
|
|
129
|
-
if (canvasName || isMissingCanvasRoute) return { data: null, error: null }
|
|
210
|
+
if (canvasName || isMissingCanvasRoute || storyName || isMissingStoryRoute) return { data: null, error: null }
|
|
130
211
|
if (!activeFlowName) return { data: {}, error: null }
|
|
131
212
|
try {
|
|
132
213
|
let flowData = loadFlow(activeFlowName)
|
|
@@ -145,10 +226,14 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
145
226
|
} catch (err) {
|
|
146
227
|
return { data: null, error: err.message }
|
|
147
228
|
}
|
|
148
|
-
}, [canvasName, isMissingCanvasRoute, activeFlowName, recordName, recordParam, params, prototypeName])
|
|
229
|
+
}, [canvasName, isMissingCanvasRoute, storyName, isMissingStoryRoute, activeFlowName, recordName, recordParam, params, prototypeName])
|
|
149
230
|
|
|
150
231
|
// Canvas pages get their own rendering path — no flow data needed
|
|
151
232
|
if (canvasName) {
|
|
233
|
+
const canvasData = canvases?.[canvasName]
|
|
234
|
+
const group = canvasData?._group
|
|
235
|
+
const siblingPages = group ? canvasGroupMap.get(group) || [] : []
|
|
236
|
+
const canvasMeta = canvasData?._canvasMeta || null
|
|
152
237
|
const canvasValue = {
|
|
153
238
|
data: null,
|
|
154
239
|
error: null,
|
|
@@ -160,7 +245,26 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
160
245
|
return (
|
|
161
246
|
<StoryboardContext.Provider value={canvasValue}>
|
|
162
247
|
<Suspense fallback={null}>
|
|
163
|
-
<CanvasPageLazy name={canvasName} />
|
|
248
|
+
<CanvasPageLazy name={canvasName} siblingPages={siblingPages} canvasMeta={canvasMeta} />
|
|
249
|
+
</Suspense>
|
|
250
|
+
</StoryboardContext.Provider>
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Story pages get their own rendering path — no flow data needed
|
|
255
|
+
if (storyName) {
|
|
256
|
+
const storyValue = {
|
|
257
|
+
data: null,
|
|
258
|
+
error: null,
|
|
259
|
+
loading: false,
|
|
260
|
+
flowName: null,
|
|
261
|
+
sceneName: null,
|
|
262
|
+
prototypeName: null,
|
|
263
|
+
}
|
|
264
|
+
return (
|
|
265
|
+
<StoryboardContext.Provider value={storyValue}>
|
|
266
|
+
<Suspense fallback={null}>
|
|
267
|
+
<StoryPageLazy name={storyName} />
|
|
164
268
|
</Suspense>
|
|
165
269
|
</StoryboardContext.Provider>
|
|
166
270
|
)
|
|
@@ -187,6 +291,27 @@ export default function StoryboardProvider({ flowName, sceneName, recordName, re
|
|
|
187
291
|
)
|
|
188
292
|
}
|
|
189
293
|
|
|
294
|
+
if (isMissingStoryRoute) {
|
|
295
|
+
const currentUrl = `${location.pathname}${location.search}`
|
|
296
|
+
const truncatedUrl = currentUrl.length > 60
|
|
297
|
+
? currentUrl.slice(0, 60) + '…'
|
|
298
|
+
: currentUrl
|
|
299
|
+
|
|
300
|
+
return (
|
|
301
|
+
<main className={styles.container}>
|
|
302
|
+
<div className={styles.banner}>
|
|
303
|
+
<strong>Story not found</strong>
|
|
304
|
+
No story matches this route.
|
|
305
|
+
</div>
|
|
306
|
+
<p className={styles.meta}>
|
|
307
|
+
Tried to open{' '}
|
|
308
|
+
<a href={currentUrl} title={currentUrl}>{truncatedUrl}</a>
|
|
309
|
+
</p>
|
|
310
|
+
<a className={styles.homeLink} href="/">← Go to index page</a>
|
|
311
|
+
</main>
|
|
312
|
+
)
|
|
313
|
+
}
|
|
314
|
+
|
|
190
315
|
const value = {
|
|
191
316
|
data,
|
|
192
317
|
error,
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StoryPage — renders a .story.jsx module at its own route.
|
|
3
|
+
*
|
|
4
|
+
* When visited at e.g. /canvas/button-patterns, renders all named exports
|
|
5
|
+
* from button-patterns.story.jsx in a gallery layout.
|
|
6
|
+
*
|
|
7
|
+
* When ?export=ExportName is present, renders only that single export
|
|
8
|
+
* (used by iframe embeds from canvas StoryWidget).
|
|
9
|
+
*/
|
|
10
|
+
import { useState, useEffect, useMemo } from 'react'
|
|
11
|
+
import { useLocation } from 'react-router-dom'
|
|
12
|
+
import { getStoryData } from '@dfosco/storyboard-core'
|
|
13
|
+
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
14
|
+
import styles from './StoryPage.module.css'
|
|
15
|
+
|
|
16
|
+
function StoryErrorBoundaryFallback({ name, error }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={styles.error}>
|
|
19
|
+
<strong>{name}</strong>
|
|
20
|
+
<span>{String(error?.message || error)}</span>
|
|
21
|
+
</div>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function StoryPage({ name }) {
|
|
26
|
+
const location = useLocation()
|
|
27
|
+
const searchParams = new URLSearchParams(location.search)
|
|
28
|
+
const exportFilter = searchParams.get('export')
|
|
29
|
+
const isEmbed = searchParams.has('_sb_embed')
|
|
30
|
+
|
|
31
|
+
const story = useMemo(() => getStoryData(name), [name])
|
|
32
|
+
const [exports, setExports] = useState(null)
|
|
33
|
+
const [error, setError] = useState(null)
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!story?._storyImport) {
|
|
37
|
+
Promise.resolve().then(() => setError(`Story "${name}" not found or missing import`))
|
|
38
|
+
return
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let cancelled = false
|
|
42
|
+
story._storyImport()
|
|
43
|
+
.then((mod) => {
|
|
44
|
+
if (cancelled) return
|
|
45
|
+
const namedExports = {}
|
|
46
|
+
for (const [key, value] of Object.entries(mod)) {
|
|
47
|
+
if (key !== 'default' && typeof value === 'function') {
|
|
48
|
+
namedExports[key] = value
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
setExports(namedExports)
|
|
52
|
+
setError(null)
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
if (cancelled) return
|
|
56
|
+
setError(`Failed to load story "${name}": ${err.message || err}`)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
return () => { cancelled = true }
|
|
60
|
+
}, [name, story])
|
|
61
|
+
|
|
62
|
+
// Signal snapshot-ready after story renders in embed mode
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (!isEmbed || !exports || window.parent === window) return
|
|
65
|
+
// Wait for fonts + paint to settle before signaling ready
|
|
66
|
+
Promise.all([
|
|
67
|
+
document.fonts?.ready || Promise.resolve(),
|
|
68
|
+
new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r))),
|
|
69
|
+
]).then(() => {
|
|
70
|
+
window.parent.postMessage({ type: 'storyboard:embed:snapshot-ready' }, '*')
|
|
71
|
+
})
|
|
72
|
+
}, [isEmbed, exports])
|
|
73
|
+
|
|
74
|
+
if (error) {
|
|
75
|
+
return (
|
|
76
|
+
<div className={styles.page}>
|
|
77
|
+
<StoryErrorBoundaryFallback name={name} error={error} />
|
|
78
|
+
</div>
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!exports) {
|
|
83
|
+
return (
|
|
84
|
+
<div className={styles.page}>
|
|
85
|
+
<div className={styles.loading}>Loading story…</div>
|
|
86
|
+
</div>
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Single export mode (for iframe embedding)
|
|
91
|
+
if (exportFilter) {
|
|
92
|
+
const Component = exports[exportFilter]
|
|
93
|
+
if (!Component) {
|
|
94
|
+
return (
|
|
95
|
+
<div className={styles.page}>
|
|
96
|
+
<StoryErrorBoundaryFallback
|
|
97
|
+
name={`${name}/${exportFilter}`}
|
|
98
|
+
error={`Export "${exportFilter}" not found in story "${name}"`}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Minimal wrapper for embed mode
|
|
105
|
+
if (isEmbed) {
|
|
106
|
+
return (
|
|
107
|
+
<ThemeProvider colorMode="day">
|
|
108
|
+
<BaseStyles>
|
|
109
|
+
<Component />
|
|
110
|
+
</BaseStyles>
|
|
111
|
+
</ThemeProvider>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<div className={styles.page}>
|
|
117
|
+
<header className={styles.header}>
|
|
118
|
+
<h1 className={styles.title}>{name}</h1>
|
|
119
|
+
<span className={styles.exportBadge}>{exportFilter}</span>
|
|
120
|
+
</header>
|
|
121
|
+
<section className={styles.storySection}>
|
|
122
|
+
<Component />
|
|
123
|
+
</section>
|
|
124
|
+
</div>
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Gallery mode — render all exports
|
|
129
|
+
const exportNames = Object.keys(exports)
|
|
130
|
+
|
|
131
|
+
return (
|
|
132
|
+
<div className={styles.page}>
|
|
133
|
+
{!isEmbed && (
|
|
134
|
+
<header className={styles.header}>
|
|
135
|
+
<h1 className={styles.title}>{name}</h1>
|
|
136
|
+
<span className={styles.count}>{exportNames.length} {exportNames.length === 1 ? 'export' : 'exports'}</span>
|
|
137
|
+
</header>
|
|
138
|
+
)}
|
|
139
|
+
{exportNames.map((exportName) => {
|
|
140
|
+
const Component = exports[exportName]
|
|
141
|
+
return (
|
|
142
|
+
<section key={exportName} className={styles.storySection}>
|
|
143
|
+
{!isEmbed && <h2 className={styles.exportName}>{exportName}</h2>}
|
|
144
|
+
<div className={styles.storyContent}>
|
|
145
|
+
<Component />
|
|
146
|
+
</div>
|
|
147
|
+
</section>
|
|
148
|
+
)
|
|
149
|
+
})}
|
|
150
|
+
</div>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
.page {
|
|
2
|
+
max-width: 960px;
|
|
3
|
+
margin: 0 auto;
|
|
4
|
+
padding: 2rem;
|
|
5
|
+
font-family: system-ui, -apple-system, sans-serif;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.header {
|
|
9
|
+
display: flex;
|
|
10
|
+
align-items: baseline;
|
|
11
|
+
gap: 12px;
|
|
12
|
+
margin-bottom: 2rem;
|
|
13
|
+
padding-bottom: 1rem;
|
|
14
|
+
border-bottom: 1px solid var(--borderColor-muted, #d0d7de);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.title {
|
|
18
|
+
font-size: 1.5rem;
|
|
19
|
+
font-weight: 600;
|
|
20
|
+
margin: 0;
|
|
21
|
+
color: var(--fgColor-default, #1f2328);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.exportBadge {
|
|
25
|
+
font-size: 0.875rem;
|
|
26
|
+
font-weight: 500;
|
|
27
|
+
padding: 2px 8px;
|
|
28
|
+
border-radius: 6px;
|
|
29
|
+
background: var(--bgColor-accent-muted, #ddf4ff);
|
|
30
|
+
color: var(--fgColor-accent, #0969da);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.count {
|
|
34
|
+
font-size: 0.875rem;
|
|
35
|
+
color: var(--fgColor-muted, #656d76);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.storySection {
|
|
39
|
+
margin-bottom: 2rem;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.exportName {
|
|
43
|
+
font-size: 1rem;
|
|
44
|
+
font-weight: 500;
|
|
45
|
+
margin: 0 0 0.75rem;
|
|
46
|
+
color: var(--fgColor-default, #1f2328);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.storyContent {
|
|
50
|
+
border: 1px solid var(--borderColor-muted, #d0d7de);
|
|
51
|
+
border-radius: 8px;
|
|
52
|
+
padding: 1.5rem;
|
|
53
|
+
background: var(--bgColor-default, #ffffff);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.loading {
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
justify-content: center;
|
|
60
|
+
padding: 3rem;
|
|
61
|
+
color: var(--fgColor-muted, #656d76);
|
|
62
|
+
font-size: 0.875rem;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.error {
|
|
66
|
+
display: flex;
|
|
67
|
+
flex-direction: column;
|
|
68
|
+
gap: 4px;
|
|
69
|
+
padding: 1rem;
|
|
70
|
+
color: var(--fgColor-danger, #cf222e);
|
|
71
|
+
font-size: 0.875rem;
|
|
72
|
+
line-height: 1.5;
|
|
73
|
+
}
|