@dfosco/storyboard-react 4.0.0-beta.26 → 4.0.0-beta.28
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/CanvasPage.bridge.test.jsx +87 -2
- package/src/canvas/CanvasPage.jsx +161 -18
- package/src/canvas/CanvasPage.module.css +54 -0
- package/src/canvas/CanvasPage.multiselect.test.jsx +2 -0
- package/src/canvas/canvasApi.js +8 -0
- package/src/canvas/widgets/FigmaEmbed.jsx +42 -7
- package/src/canvas/widgets/FigmaEmbed.module.css +21 -0
- package/src/canvas/widgets/LinkPreview.jsx +247 -18
- package/src/canvas/widgets/LinkPreview.module.css +349 -8
- package/src/canvas/widgets/LinkPreview.test.jsx +71 -0
- package/src/canvas/widgets/MarkdownBlock.jsx +2 -1
- package/src/canvas/widgets/MarkdownBlock.module.css +34 -11
- package/src/canvas/widgets/PrototypeEmbed.jsx +101 -44
- package/src/canvas/widgets/PrototypeEmbed.module.css +1 -0
- package/src/canvas/widgets/StoryWidget.jsx +86 -42
- package/src/canvas/widgets/StoryWidget.module.css +1 -0
- package/src/canvas/widgets/WidgetChrome.jsx +20 -1
- package/src/canvas/widgets/embedInteraction.test.jsx +16 -18
- package/src/canvas/widgets/embedTheme.js +37 -1
- package/src/canvas/widgets/githubUrl.js +82 -0
- package/src/canvas/widgets/githubUrl.test.js +74 -0
- package/src/canvas/widgets/refreshQueue.js +108 -0
- package/src/canvas/widgets/snapshotDisplay.test.jsx +60 -90
- package/src/canvas/widgets/useSnapshotCapture.js +38 -139
- package/src/canvas/widgets/useSnapshotCapture.test.jsx +30 -91
- package/src/canvas/widgets/widgetConfig.test.js +1 -1
- package/src/story/StoryPage.jsx +25 -60
- package/src/story/StoryPage.module.css +0 -55
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for useSnapshotCapture hook —
|
|
2
|
+
* Tests for useSnapshotCapture hook — single-capture orchestration.
|
|
3
3
|
*/
|
|
4
4
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
5
5
|
import { renderHook, act } from '@testing-library/react'
|
|
6
6
|
import { useSnapshotCapture } from './useSnapshotCapture.js'
|
|
7
7
|
|
|
8
8
|
vi.mock('../canvasApi.js', () => ({
|
|
9
|
-
uploadImage: vi.fn().mockResolvedValue({ filename: 'snapshot-test-widget
|
|
9
|
+
uploadImage: vi.fn().mockResolvedValue({ filename: 'snapshot-test-widget.webp' }),
|
|
10
10
|
}))
|
|
11
11
|
|
|
12
12
|
import { uploadImage } from '../canvasApi.js'
|
|
@@ -35,15 +35,6 @@ describe('useSnapshotCapture', () => {
|
|
|
35
35
|
if (type === 'message') listeners = listeners.filter(l => l !== fn)
|
|
36
36
|
origRemove.call(window, type, fn, opts)
|
|
37
37
|
})
|
|
38
|
-
// Mock Image so preload resolves immediately in tests
|
|
39
|
-
vi.stubGlobal('Image', class MockImage {
|
|
40
|
-
constructor() { this._onload = null }
|
|
41
|
-
set onload(fn) { this._onload = fn }
|
|
42
|
-
get onload() { return this._onload }
|
|
43
|
-
set onerror(fn) { this._onerror = fn }
|
|
44
|
-
set src(v) { this._src = v; Promise.resolve().then(() => this._onload?.()) }
|
|
45
|
-
get src() { return this._src }
|
|
46
|
-
})
|
|
47
38
|
})
|
|
48
39
|
|
|
49
40
|
afterEach(() => { vi.restoreAllMocks() })
|
|
@@ -56,7 +47,7 @@ describe('useSnapshotCapture', () => {
|
|
|
56
47
|
it('returns iframeReady=false initially', () => {
|
|
57
48
|
const iframeRef = createMockIframeRef()
|
|
58
49
|
const { result } = renderHook(() =>
|
|
59
|
-
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: vi.fn()
|
|
50
|
+
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: vi.fn() })
|
|
60
51
|
)
|
|
61
52
|
expect(result.current.iframeReady).toBe(false)
|
|
62
53
|
})
|
|
@@ -65,7 +56,7 @@ describe('useSnapshotCapture', () => {
|
|
|
65
56
|
const cw = createMockContentWindow()
|
|
66
57
|
const iframeRef = createMockIframeRef(cw)
|
|
67
58
|
const { result } = renderHook(() =>
|
|
68
|
-
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: vi.fn()
|
|
59
|
+
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: vi.fn() })
|
|
69
60
|
)
|
|
70
61
|
act(() => { dispatchMessage(cw, { type: 'storyboard:embed:snapshot-ready' }) })
|
|
71
62
|
expect(result.current.iframeReady).toBe(true)
|
|
@@ -75,7 +66,7 @@ describe('useSnapshotCapture', () => {
|
|
|
75
66
|
const cw = createMockContentWindow()
|
|
76
67
|
const iframeRef = createMockIframeRef(cw)
|
|
77
68
|
const { result } = renderHook(() =>
|
|
78
|
-
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: null
|
|
69
|
+
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: null })
|
|
79
70
|
)
|
|
80
71
|
await act(async () => { await result.current.requestCapture() })
|
|
81
72
|
expect(cw.postMessage).not.toHaveBeenCalled()
|
|
@@ -85,141 +76,89 @@ describe('useSnapshotCapture', () => {
|
|
|
85
76
|
const cw = createMockContentWindow()
|
|
86
77
|
const iframeRef = createMockIframeRef(cw)
|
|
87
78
|
const { result } = renderHook(() =>
|
|
88
|
-
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: vi.fn()
|
|
79
|
+
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate: vi.fn() })
|
|
89
80
|
)
|
|
90
81
|
await act(async () => { await result.current.requestCapture() })
|
|
91
82
|
expect(cw.postMessage).not.toHaveBeenCalled()
|
|
92
83
|
})
|
|
93
84
|
|
|
94
|
-
it('sends capture
|
|
85
|
+
it('sends single capture and calls onUpdate with snapshot', async () => {
|
|
95
86
|
const cw = createMockContentWindow()
|
|
96
|
-
|
|
97
|
-
const style = { visibility: "" }
|
|
98
|
-
|
|
99
|
-
const iframeRef = { current: { contentWindow: cw, style } }
|
|
87
|
+
const iframeRef = createMockIframeRef(cw)
|
|
100
88
|
const onUpdate = vi.fn()
|
|
101
89
|
const { result } = renderHook(() =>
|
|
102
|
-
useSnapshotCapture({ iframeRef, widgetId: 'test-widget', onUpdate
|
|
90
|
+
useSnapshotCapture({ iframeRef, widgetId: 'test-widget', onUpdate })
|
|
103
91
|
)
|
|
104
92
|
|
|
105
93
|
act(() => { dispatchMessage(cw, { type: 'storyboard:embed:snapshot-ready' }) })
|
|
106
94
|
|
|
107
|
-
uploadImage
|
|
108
|
-
.mockResolvedValueOnce({ filename: 'snapshot-test-widget--light.webp' })
|
|
109
|
-
.mockResolvedValueOnce({ filename: 'snapshot-test-widget--dark.webp' })
|
|
95
|
+
uploadImage.mockResolvedValueOnce({ filename: 'snapshot-test-widget.webp' })
|
|
110
96
|
|
|
111
97
|
await act(async () => {
|
|
112
98
|
const promise = result.current.requestCapture()
|
|
113
99
|
|
|
114
|
-
// Respond to capture 1 (current=light)
|
|
115
|
-
await new Promise(r => setTimeout(r, 10))
|
|
116
|
-
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId: 1, dataUrl: 'data:image/webp;base64,LIGHT' })
|
|
117
|
-
|
|
118
|
-
// Respond to set-theme (switch to dark)
|
|
119
|
-
await new Promise(r => setTimeout(r, 10))
|
|
120
|
-
dispatchMessage(cw, { type: 'storyboard:embed:theme-applied', requestId: 2 })
|
|
121
|
-
|
|
122
|
-
// Respond to capture 2 (alternate=dark)
|
|
123
100
|
await new Promise(r => setTimeout(r, 10))
|
|
124
|
-
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId:
|
|
125
|
-
|
|
126
|
-
// Respond to set-theme back (switch to light)
|
|
127
|
-
await new Promise(r => setTimeout(r, 10))
|
|
128
|
-
dispatchMessage(cw, { type: 'storyboard:embed:theme-applied', requestId: 4 })
|
|
101
|
+
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId: 1, dataUrl: 'data:image/webp;base64,IMG' })
|
|
129
102
|
|
|
130
103
|
await promise
|
|
131
104
|
})
|
|
132
105
|
|
|
133
|
-
//
|
|
134
|
-
expect(cw.postMessage).toHaveBeenCalledTimes(
|
|
135
|
-
expect(uploadImage).toHaveBeenCalledTimes(
|
|
136
|
-
// Intermediate onUpdate for current theme + final with both
|
|
137
|
-
expect(onUpdate).toHaveBeenCalledWith(
|
|
138
|
-
expect.objectContaining({
|
|
139
|
-
snapshotLight: expect.stringContaining('snapshot-test-widget--light.webp'),
|
|
140
|
-
})
|
|
141
|
-
)
|
|
106
|
+
// Single capture, single postMessage
|
|
107
|
+
expect(cw.postMessage).toHaveBeenCalledTimes(1)
|
|
108
|
+
expect(uploadImage).toHaveBeenCalledTimes(1)
|
|
142
109
|
expect(onUpdate).toHaveBeenCalledWith(
|
|
143
110
|
expect.objectContaining({
|
|
144
|
-
|
|
145
|
-
snapshotDark: expect.stringContaining('snapshot-test-widget--dark.webp'),
|
|
111
|
+
snapshot: expect.stringContaining('snapshot-test-widget.webp'),
|
|
146
112
|
})
|
|
147
113
|
)
|
|
148
114
|
})
|
|
149
115
|
|
|
150
|
-
it('
|
|
116
|
+
it('guards against concurrent captures', async () => {
|
|
151
117
|
const cw = createMockContentWindow()
|
|
152
|
-
const
|
|
153
|
-
const iframeRef = { current: { contentWindow: cw, style } }
|
|
118
|
+
const iframeRef = createMockIframeRef(cw)
|
|
154
119
|
const onUpdate = vi.fn()
|
|
155
120
|
const { result } = renderHook(() =>
|
|
156
|
-
useSnapshotCapture({ iframeRef, widgetId: '
|
|
121
|
+
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate })
|
|
157
122
|
)
|
|
158
123
|
|
|
159
124
|
act(() => { dispatchMessage(cw, { type: 'storyboard:embed:snapshot-ready' }) })
|
|
125
|
+
uploadImage.mockResolvedValue({ filename: 'snapshot-w1.webp' })
|
|
160
126
|
|
|
161
|
-
uploadImage.mockResolvedValue({ filename: 'snapshot-gen-widget--light.webp' })
|
|
162
|
-
|
|
163
|
-
// Start first capture — will be waiting for response
|
|
164
|
-
let capture1Done = false
|
|
165
|
-
let capture2Done = false
|
|
166
127
|
await act(async () => {
|
|
167
|
-
const
|
|
128
|
+
const p1 = result.current.requestCapture()
|
|
129
|
+
// Second call while first is in-flight should no-op
|
|
130
|
+
const p2 = result.current.requestCapture()
|
|
168
131
|
|
|
169
|
-
// Respond to first capture
|
|
170
132
|
await new Promise(r => setTimeout(r, 10))
|
|
171
|
-
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId: 1, dataUrl: 'data:image/webp;base64,
|
|
133
|
+
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId: 1, dataUrl: 'data:image/webp;base64,IMG' })
|
|
172
134
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
const promise2 = result.current.requestCapture().then(() => { capture2Done = true })
|
|
176
|
-
|
|
177
|
-
// Complete first capture's theme switch and alternate capture
|
|
178
|
-
await new Promise(r => setTimeout(r, 10))
|
|
179
|
-
dispatchMessage(cw, { type: 'storyboard:embed:theme-applied', requestId: 2 })
|
|
180
|
-
await new Promise(r => setTimeout(r, 10))
|
|
181
|
-
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId: 3, dataUrl: 'data:image/webp;base64,DARK' })
|
|
182
|
-
await new Promise(r => setTimeout(r, 10))
|
|
183
|
-
dispatchMessage(cw, { type: 'storyboard:embed:theme-applied', requestId: 4 })
|
|
184
|
-
|
|
185
|
-
await promise1
|
|
186
|
-
await promise2
|
|
135
|
+
await p1
|
|
136
|
+
await p2
|
|
187
137
|
})
|
|
188
138
|
|
|
189
|
-
|
|
190
|
-
expect(capture1Done).toBe(true)
|
|
191
|
-
expect(capture2Done).toBe(true)
|
|
139
|
+
expect(cw.postMessage).toHaveBeenCalledTimes(1)
|
|
192
140
|
})
|
|
193
141
|
|
|
194
|
-
it('
|
|
142
|
+
it('handles capture failure gracefully', async () => {
|
|
195
143
|
const cw = createMockContentWindow()
|
|
196
|
-
const
|
|
197
|
-
const iframeRef = { current: { contentWindow: cw, style } }
|
|
144
|
+
const iframeRef = createMockIframeRef(cw)
|
|
198
145
|
const onUpdate = vi.fn()
|
|
199
146
|
const { result } = renderHook(() =>
|
|
200
|
-
useSnapshotCapture({ iframeRef, widgetId: '
|
|
147
|
+
useSnapshotCapture({ iframeRef, widgetId: 'w1', onUpdate })
|
|
201
148
|
)
|
|
202
149
|
|
|
203
150
|
act(() => { dispatchMessage(cw, { type: 'storyboard:embed:snapshot-ready' }) })
|
|
204
|
-
|
|
205
|
-
// Make uploadImage throw to trigger error path
|
|
206
151
|
uploadImage.mockRejectedValueOnce(new Error('upload failed'))
|
|
207
152
|
|
|
208
153
|
await act(async () => {
|
|
209
154
|
const promise = result.current.requestCapture()
|
|
210
155
|
|
|
211
|
-
// Respond to capture
|
|
212
156
|
await new Promise(r => setTimeout(r, 10))
|
|
213
157
|
dispatchMessage(cw, { type: 'storyboard:embed:snapshot', requestId: 1, dataUrl: 'data:image/webp;base64,FAIL' })
|
|
214
158
|
|
|
215
159
|
await promise
|
|
216
160
|
})
|
|
217
161
|
|
|
218
|
-
|
|
219
|
-
expect(style.visibility).toBe('')
|
|
220
|
-
// onUpdate should not be called with failed data
|
|
221
|
-
expect(onUpdate).not.toHaveBeenCalledWith(
|
|
222
|
-
expect.objectContaining({ snapshotLight: expect.any(String) })
|
|
223
|
-
)
|
|
162
|
+
expect(onUpdate).not.toHaveBeenCalled()
|
|
224
163
|
})
|
|
225
164
|
})
|
|
@@ -12,7 +12,7 @@ describe('isResizable', () => {
|
|
|
12
12
|
expect(isResizable('component')).toBe(true)
|
|
13
13
|
})
|
|
14
14
|
|
|
15
|
-
it('returns false for
|
|
15
|
+
it('returns false for link-preview (resize disabled)', () => {
|
|
16
16
|
expect(isResizable('link-preview')).toBe(false)
|
|
17
17
|
})
|
|
18
18
|
|
package/src/story/StoryPage.jsx
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* StoryPage — renders a .story.jsx module at its own route.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* When ?export=ExportName is present, renders only that single export
|
|
8
|
-
* (used by iframe embeds from canvas StoryWidget).
|
|
4
|
+
* Renders only the bare component(s) with no layout chrome.
|
|
5
|
+
* When ?export=ExportName is present, renders that single export.
|
|
6
|
+
* Without ?export, renders all named exports stacked.
|
|
9
7
|
*/
|
|
10
8
|
import { useState, useEffect, useMemo } from 'react'
|
|
11
9
|
import { useLocation } from 'react-router-dom'
|
|
@@ -13,7 +11,7 @@ import { getStoryData } from '@dfosco/storyboard-core'
|
|
|
13
11
|
import { ThemeProvider, BaseStyles } from '@primer/react'
|
|
14
12
|
import styles from './StoryPage.module.css'
|
|
15
13
|
|
|
16
|
-
function
|
|
14
|
+
function StoryErrorFallback({ name, error }) {
|
|
17
15
|
return (
|
|
18
16
|
<div className={styles.error}>
|
|
19
17
|
<strong>{name}</strong>
|
|
@@ -60,8 +58,6 @@ export default function StoryPage({ name }) {
|
|
|
60
58
|
}, [name, story])
|
|
61
59
|
|
|
62
60
|
// Signal snapshot-ready after story renders in embed mode.
|
|
63
|
-
// Stories have no data loading so they render fast — fire
|
|
64
|
-
// the explicit ready signal instead of waiting for the 1.5s fallback.
|
|
65
61
|
useEffect(() => {
|
|
66
62
|
if (!isEmbed || !exports || window.parent === window) return
|
|
67
63
|
document.fonts.ready.then(() => {
|
|
@@ -73,80 +69,49 @@ export default function StoryPage({ name }) {
|
|
|
73
69
|
|
|
74
70
|
if (error) {
|
|
75
71
|
return (
|
|
76
|
-
<
|
|
77
|
-
<StoryErrorBoundaryFallback name={name} error={error} />
|
|
78
|
-
</div>
|
|
72
|
+
<StoryErrorFallback name={name} error={error} />
|
|
79
73
|
)
|
|
80
74
|
}
|
|
81
75
|
|
|
82
76
|
if (!exports) {
|
|
77
|
+
if (isEmbed) return null
|
|
83
78
|
return (
|
|
84
|
-
<div className={styles.
|
|
85
|
-
<div className={styles.loading}>Loading story…</div>
|
|
86
|
-
</div>
|
|
79
|
+
<div className={styles.loading}>Loading story…</div>
|
|
87
80
|
)
|
|
88
81
|
}
|
|
89
82
|
|
|
90
|
-
// Single export mode
|
|
83
|
+
// Single export mode
|
|
91
84
|
if (exportFilter) {
|
|
92
85
|
const Component = exports[exportFilter]
|
|
93
86
|
if (!Component) {
|
|
94
87
|
return (
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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>
|
|
88
|
+
<StoryErrorFallback
|
|
89
|
+
name={`${name}/${exportFilter}`}
|
|
90
|
+
error={`Export "${exportFilter}" not found in story "${name}"`}
|
|
91
|
+
/>
|
|
112
92
|
)
|
|
113
93
|
}
|
|
114
94
|
|
|
115
95
|
return (
|
|
116
|
-
<
|
|
117
|
-
<
|
|
118
|
-
<h1 className={styles.title}>{name}</h1>
|
|
119
|
-
<span className={styles.exportBadge}>{exportFilter}</span>
|
|
120
|
-
</header>
|
|
121
|
-
<section className={styles.storySection}>
|
|
96
|
+
<ThemeProvider colorMode="day">
|
|
97
|
+
<BaseStyles>
|
|
122
98
|
<Component />
|
|
123
|
-
</
|
|
124
|
-
</
|
|
99
|
+
</BaseStyles>
|
|
100
|
+
</ThemeProvider>
|
|
125
101
|
)
|
|
126
102
|
}
|
|
127
103
|
|
|
128
|
-
//
|
|
104
|
+
// All exports — render each component bare
|
|
129
105
|
const exportNames = Object.keys(exports)
|
|
130
106
|
|
|
131
107
|
return (
|
|
132
|
-
<
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
<
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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>
|
|
108
|
+
<ThemeProvider colorMode="day">
|
|
109
|
+
<BaseStyles>
|
|
110
|
+
{exportNames.map((exportName) => {
|
|
111
|
+
const Component = exports[exportName]
|
|
112
|
+
return <Component key={exportName} />
|
|
113
|
+
})}
|
|
114
|
+
</BaseStyles>
|
|
115
|
+
</ThemeProvider>
|
|
151
116
|
)
|
|
152
117
|
}
|
|
@@ -1,58 +1,3 @@
|
|
|
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
1
|
.loading {
|
|
57
2
|
display: flex;
|
|
58
3
|
align-items: center;
|