@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
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
}
|
|
@@ -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
|
+
})
|
|
@@ -97,9 +97,9 @@ async function mount() {
|
|
|
97
97
|
return
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
// Validate: only allow .canvas.jsx modules
|
|
101
|
-
if (!modulePath.endsWith('.canvas.jsx')) {
|
|
102
|
-
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .canvas.jsx files are allowed'))
|
|
100
|
+
// Validate: only allow .canvas.jsx and .story.{jsx,tsx} modules
|
|
101
|
+
if (!modulePath.endsWith('.canvas.jsx') && !modulePath.match(/\.story\.(jsx|tsx)$/)) {
|
|
102
|
+
root.render(createElement('div', { style: errorStyle }, 'Invalid module path — only .canvas.jsx and .story.jsx/.tsx files are allowed'))
|
|
103
103
|
return
|
|
104
104
|
}
|
|
105
105
|
|
|
@@ -188,8 +188,13 @@ export default forwardRef(function FigmaEmbed({ props, onUpdate, resizable }, re
|
|
|
188
188
|
style={expanded && embedUrl ? undefined : { display: 'none' }}
|
|
189
189
|
onClick={() => setExpanded(false)}
|
|
190
190
|
onPointerDown={(e) => e.stopPropagation()}
|
|
191
|
-
onKeyDown={(e) =>
|
|
191
|
+
onKeyDown={(e) => {
|
|
192
|
+
e.stopPropagation()
|
|
193
|
+
if (e.key === 'Escape') setExpanded(false)
|
|
194
|
+
}}
|
|
192
195
|
onWheel={(e) => e.stopPropagation()}
|
|
196
|
+
tabIndex={-1}
|
|
197
|
+
ref={(el) => { if (el && expanded) el.focus() }}
|
|
193
198
|
>
|
|
194
199
|
<div
|
|
195
200
|
ref={modalContainerRef}
|
|
@@ -3,9 +3,13 @@ import { remark } from 'remark'
|
|
|
3
3
|
import remarkGfm from 'remark-gfm'
|
|
4
4
|
import remarkHtml from 'remark-html'
|
|
5
5
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
6
|
-
import
|
|
6
|
+
import ResizeHandle from './ResizeHandle.jsx'
|
|
7
|
+
import { readProp } from './widgetProps.js'
|
|
8
|
+
import { schemas } from './widgetConfig.js'
|
|
7
9
|
import styles from './MarkdownBlock.module.css'
|
|
8
10
|
|
|
11
|
+
const markdownSchema = schemas['markdown']
|
|
12
|
+
|
|
9
13
|
/**
|
|
10
14
|
* Renders markdown to HTML using remark with GitHub Flavored Markdown support.
|
|
11
15
|
*/
|
|
@@ -18,9 +22,51 @@ function renderMarkdown(text) {
|
|
|
18
22
|
return String(result)
|
|
19
23
|
}
|
|
20
24
|
|
|
21
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Post-process rendered HTML to syntax-highlight fenced code blocks.
|
|
27
|
+
* remark-html outputs <pre><code class="language-xxx">...</code></pre>.
|
|
28
|
+
* We replace the code content with highlight.js output.
|
|
29
|
+
*/
|
|
30
|
+
let hljsPromise = null
|
|
31
|
+
function getHljs() {
|
|
32
|
+
if (!hljsPromise) {
|
|
33
|
+
hljsPromise = import('@dfosco/storyboard-core/inspector/highlighter').then((mod) => mod)
|
|
34
|
+
}
|
|
35
|
+
return hljsPromise
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function highlightCodeBlocks(html) {
|
|
39
|
+
if (!html.includes('<code class="language-')) return html
|
|
40
|
+
const { createInspectorHighlighter } = await getHljs()
|
|
41
|
+
const hl = await createInspectorHighlighter()
|
|
42
|
+
return html.replace(
|
|
43
|
+
/<pre><code class="language-(\w+)">([\s\S]*?)<\/code><\/pre>/g,
|
|
44
|
+
(match, lang, code) => {
|
|
45
|
+
try {
|
|
46
|
+
// Decode all HTML entities that remark-html may produce
|
|
47
|
+
const decoded = code
|
|
48
|
+
.replace(/</gi, '<')
|
|
49
|
+
.replace(/>/gi, '>')
|
|
50
|
+
.replace(/&/gi, '&')
|
|
51
|
+
.replace(/"/gi, '"')
|
|
52
|
+
.replace(/'/gi, "'")
|
|
53
|
+
.replace(/</g, '<')
|
|
54
|
+
.replace(/>/g, '>')
|
|
55
|
+
.replace(/"/g, '"')
|
|
56
|
+
.replace(/&/g, '&')
|
|
57
|
+
// codeToHtml returns a full <pre style="bg;fg"><code>...</code></pre>
|
|
58
|
+
return hl.codeToHtml(decoded, { lang })
|
|
59
|
+
} catch {
|
|
60
|
+
return match
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export default function MarkdownBlock({ props, onUpdate, resizable }) {
|
|
22
67
|
const content = readProp(props, 'content', markdownSchema)
|
|
23
68
|
const width = readProp(props, 'width', markdownSchema)
|
|
69
|
+
const height = props?.height
|
|
24
70
|
const canEdit = typeof onUpdate === 'function'
|
|
25
71
|
const [editing, setEditing] = useState(false)
|
|
26
72
|
const editingActive = canEdit && editing
|
|
@@ -28,6 +74,32 @@ export default function MarkdownBlock({ props, onUpdate }) {
|
|
|
28
74
|
const blockRef = useRef(null)
|
|
29
75
|
const [editHeight, setEditHeight] = useState(null)
|
|
30
76
|
|
|
77
|
+
const handleResize = useCallback((w, h) => {
|
|
78
|
+
onUpdate?.({ width: w, height: h })
|
|
79
|
+
}, [onUpdate])
|
|
80
|
+
|
|
81
|
+
const rawHtml = useMemo(() => renderMarkdown(content), [content])
|
|
82
|
+
const [renderedHtml, setRenderedHtml] = useState(rawHtml)
|
|
83
|
+
|
|
84
|
+
// Re-highlight when theme changes
|
|
85
|
+
const [themeKey, setThemeKey] = useState(0)
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
function onThemeChanged() { setThemeKey((k) => k + 1) }
|
|
88
|
+
document.addEventListener('storyboard:theme:changed', onThemeChanged)
|
|
89
|
+
return () => document.removeEventListener('storyboard:theme:changed', onThemeChanged)
|
|
90
|
+
}, [])
|
|
91
|
+
|
|
92
|
+
// Async-highlight code blocks after initial render or theme change
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
setRenderedHtml(rawHtml)
|
|
95
|
+
if (!rawHtml.includes('<code class="language-')) return
|
|
96
|
+
let cancelled = false
|
|
97
|
+
highlightCodeBlocks(rawHtml).then((highlighted) => {
|
|
98
|
+
if (!cancelled) setRenderedHtml(highlighted)
|
|
99
|
+
})
|
|
100
|
+
return () => { cancelled = true }
|
|
101
|
+
}, [rawHtml, themeKey])
|
|
102
|
+
|
|
31
103
|
const handleContentChange = useCallback((e) => {
|
|
32
104
|
onUpdate?.({ content: e.target.value })
|
|
33
105
|
}, [onUpdate])
|
|
@@ -60,7 +132,7 @@ export default function MarkdownBlock({ props, onUpdate }) {
|
|
|
60
132
|
<div
|
|
61
133
|
ref={blockRef}
|
|
62
134
|
className={styles.block}
|
|
63
|
-
style={{ width, minHeight: editHeight || undefined }}
|
|
135
|
+
style={{ width, ...(height ? { height, overflow: 'auto' } : {}), minHeight: editHeight || undefined }}
|
|
64
136
|
>
|
|
65
137
|
{editingActive ? (
|
|
66
138
|
<textarea
|
|
@@ -90,12 +162,20 @@ export default function MarkdownBlock({ props, onUpdate }) {
|
|
|
90
162
|
tabIndex={canEdit ? 0 : undefined}
|
|
91
163
|
onKeyDown={canEdit ? (e) => { if (e.key === 'Enter') setEditing(true) } : undefined}
|
|
92
164
|
dangerouslySetInnerHTML={{
|
|
93
|
-
__html:
|
|
165
|
+
__html: renderedHtml || (canEdit
|
|
94
166
|
? '<p class="placeholder">Double-click to edit…</p>'
|
|
95
167
|
: '<p class="placeholder">No content</p>'),
|
|
96
168
|
}}
|
|
97
169
|
/>
|
|
98
170
|
)}
|
|
171
|
+
{resizable && (
|
|
172
|
+
<ResizeHandle
|
|
173
|
+
targetRef={blockRef}
|
|
174
|
+
minWidth={200}
|
|
175
|
+
minHeight={60}
|
|
176
|
+
onResize={handleResize}
|
|
177
|
+
/>
|
|
178
|
+
)}
|
|
99
179
|
</div>
|
|
100
180
|
</WidgetWrapper>
|
|
101
181
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
.block {
|
|
2
|
+
position: relative;
|
|
2
3
|
min-height: 80px;
|
|
3
4
|
--sb--markdown-bg: var(--bgColor-default, #ffffff);
|
|
4
5
|
--sb--markdown-fg: var(--fgColor-default, #1f2328);
|
|
@@ -52,22 +53,26 @@
|
|
|
52
53
|
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
53
54
|
padding: 2px 5px;
|
|
54
55
|
border-radius: 4px;
|
|
55
|
-
font-size:
|
|
56
|
-
font-
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
font-weight: 400;
|
|
58
|
+
font-family: "Ioskeley Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
.preview ul {
|
|
60
62
|
margin: 0 0 8px;
|
|
61
63
|
padding-left: 20px;
|
|
64
|
+
list-style-type: disc;
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
.preview li {
|
|
65
68
|
margin: 0 0 2px;
|
|
69
|
+
display: list-item;
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
.preview ol {
|
|
69
73
|
margin: 0 0 8px;
|
|
70
74
|
padding-left: 20px;
|
|
75
|
+
list-style-type: decimal;
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
/* GFM: Task lists */
|
|
@@ -119,9 +124,19 @@
|
|
|
119
124
|
|
|
120
125
|
/* Code blocks */
|
|
121
126
|
.preview pre {
|
|
122
|
-
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
123
127
|
padding: 12px 16px;
|
|
124
128
|
border-radius: 6px;
|
|
129
|
+
border: 1px solid var(--borderColor-muted, #d8dee4);
|
|
130
|
+
overflow-x: auto;
|
|
131
|
+
margin: 8px 0;
|
|
132
|
+
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
133
|
+
line-height: 1.4;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/* When inspector highlighter sets inline background, let it through */
|
|
137
|
+
.preview pre[style] {
|
|
138
|
+
border-radius: 6px;
|
|
139
|
+
border: 1px solid var(--borderColor-muted, #d8dee4);
|
|
125
140
|
overflow-x: auto;
|
|
126
141
|
margin: 8px 0;
|
|
127
142
|
}
|
|
@@ -129,7 +144,18 @@
|
|
|
129
144
|
.preview pre code {
|
|
130
145
|
background: none;
|
|
131
146
|
padding: 0;
|
|
132
|
-
font-size:
|
|
147
|
+
font-size: 12px;
|
|
148
|
+
font-weight: 400;
|
|
149
|
+
font-family: "Ioskeley Mono", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
150
|
+
white-space: pre;
|
|
151
|
+
word-break: normal;
|
|
152
|
+
overflow-wrap: normal;
|
|
153
|
+
display: block;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/* Suppress line numbers from inspector highlighter's .line spans */
|
|
157
|
+
.preview :global(.line::before) {
|
|
158
|
+
display: none !important;
|
|
133
159
|
}
|
|
134
160
|
|
|
135
161
|
/* Blockquotes */
|