@dfosco/storyboard-react 4.0.0-beta.27 → 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 +152 -9
- 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,34 +1,263 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef } from 'react'
|
|
2
|
+
import { remark } from 'remark'
|
|
3
|
+
import remarkGfm from 'remark-gfm'
|
|
4
|
+
import remarkHtml from 'remark-html'
|
|
5
|
+
import { MarkGithubIcon } from '@primer/octicons-react'
|
|
1
6
|
import WidgetWrapper from './WidgetWrapper.jsx'
|
|
7
|
+
import ResizeHandle from './ResizeHandle.jsx'
|
|
2
8
|
import { readProp, linkPreviewSchema } from './widgetProps.js'
|
|
3
9
|
import styles from './LinkPreview.module.css'
|
|
4
10
|
|
|
5
|
-
|
|
11
|
+
const VIDEO_EXT_RE = /\.(mp4|mov|webm|ogg)(\?[^)]*)?$/i
|
|
12
|
+
const VIDEO_URL_LINE_RE = /^<p>\s*(https?:\/\/[^\s<]+\.(mp4|mov|webm|ogg)(?:\?[^\s<]*)?)\s*<\/p>$/gim
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Post-process HTML body for canvas rendering:
|
|
16
|
+
* - Links open in new tabs
|
|
17
|
+
* - Unwrap <details> wrappers around videos (GitHub wraps them)
|
|
18
|
+
* - Convert bare video URLs and video-linked images to <video> elements
|
|
19
|
+
* - Mark checked checkboxes with data attribute for accent styling
|
|
20
|
+
*/
|
|
21
|
+
function postProcessHtml(html) {
|
|
22
|
+
if (!html) return ''
|
|
23
|
+
let out = html
|
|
24
|
+
|
|
25
|
+
// Open all links in new tabs
|
|
26
|
+
out = out.replace(/<a\s/g, '<a target="_blank" rel="noopener noreferrer" ')
|
|
27
|
+
// Dedupe target if GitHub already set it
|
|
28
|
+
out = out.replace(/target="_blank"\s*rel="noopener noreferrer"\s*target="_blank"/g, 'target="_blank"')
|
|
29
|
+
|
|
30
|
+
// Unwrap <details><summary>...</summary><video ...></details> → just the <video>
|
|
31
|
+
out = out.replace(/<details[^>]*>\s*<summary[^>]*>[\s\S]*?<\/summary>\s*(<video[\s\S]*?<\/video>)\s*<\/details>/gi, '$1')
|
|
32
|
+
|
|
33
|
+
// Convert bare video URLs (wrapped in <p>) into <video> elements
|
|
34
|
+
out = out.replace(VIDEO_URL_LINE_RE, (_, url) =>
|
|
35
|
+
`<video src="${url}" controls preload="none"></video>`
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// Convert img tags pointing at video files to <video>
|
|
39
|
+
out = out.replace(/<img\s+([^>]*?)src="([^"]+\.(mp4|mov|webm|ogg)(?:\?[^"]*)?)"([^>]*)\/?>/gi, (_, _pre, url) =>
|
|
40
|
+
`<video src="${url}" controls preload="none"></video>`
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
// Existing <video> tags from GitHub: set preload=none to prevent auto-loading spinner
|
|
44
|
+
out = out.replace(/<video\s/g, '<video preload="none" ')
|
|
45
|
+
// Dedupe if we already added it
|
|
46
|
+
out = out.replace(/preload="none"\s+preload="[^"]*"/g, 'preload="none"')
|
|
47
|
+
|
|
48
|
+
// Remove disabled from checkboxes so accent-color works (CSS blocks interaction instead)
|
|
49
|
+
out = out.replace(/<input\s+([^>]*?)disabled([^>]*)>/gi, (match, before, after) => {
|
|
50
|
+
if (!match.includes('type="checkbox"')) return match
|
|
51
|
+
return `<input ${before}${after}>`
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
return out
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function renderMarkdown(text) {
|
|
58
|
+
if (!text) return ''
|
|
59
|
+
const result = remark()
|
|
60
|
+
.use(remarkGfm)
|
|
61
|
+
.use(remarkHtml, { sanitize: false })
|
|
62
|
+
.processSync(text)
|
|
63
|
+
return postProcessHtml(String(result))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function timeAgo(dateStr) {
|
|
67
|
+
if (!dateStr) return ''
|
|
68
|
+
const date = new Date(dateStr)
|
|
69
|
+
if (Number.isNaN(date.getTime())) return ''
|
|
70
|
+
const seconds = Math.floor((Date.now() - date.getTime()) / 1000)
|
|
71
|
+
if (seconds < 60) return 'just now'
|
|
72
|
+
const minutes = Math.floor(seconds / 60)
|
|
73
|
+
if (minutes < 60) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`
|
|
74
|
+
const hours = Math.floor(minutes / 60)
|
|
75
|
+
if (hours < 24) return `${hours} hour${hours === 1 ? '' : 's'} ago`
|
|
76
|
+
const days = Math.floor(hours / 24)
|
|
77
|
+
if (days < 30) return `${days} day${days === 1 ? '' : 's'} ago`
|
|
78
|
+
const months = Math.floor(days / 30)
|
|
79
|
+
if (months < 12) return `${months} month${months === 1 ? '' : 's'} ago`
|
|
80
|
+
const years = Math.floor(months / 12)
|
|
81
|
+
return `${years} year${years === 1 ? '' : 's'} ago`
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Split a title like "#42 Ship GitHub embeds" into { number: "#42", rest: "Ship GitHub embeds" }.
|
|
86
|
+
*/
|
|
87
|
+
function splitIssueTitle(title) {
|
|
88
|
+
if (!title) return { number: '', rest: '' }
|
|
89
|
+
const match = title.match(/^(#\d+)\s+(.*)$/)
|
|
90
|
+
if (match) return { number: match[1], rest: match[2] }
|
|
91
|
+
return { number: '', rest: title }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const KIND_LABELS = {
|
|
95
|
+
issue: 'Issue',
|
|
96
|
+
pull_request: 'Pull Request',
|
|
97
|
+
discussion: 'Discussion',
|
|
98
|
+
comment: 'Comment',
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function getCommentKindLabel(github) {
|
|
102
|
+
if (github?.kind !== 'comment') return KIND_LABELS[github?.kind] || 'GitHub'
|
|
103
|
+
if (github?.parentKind === 'issue') return 'Issue Comment'
|
|
104
|
+
if (github?.parentKind === 'pull_request') return 'PR Comment'
|
|
105
|
+
if (github?.parentKind === 'discussion') return 'Discussion Comment'
|
|
106
|
+
return 'Comment'
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function GitHubIssueCard({ url, title, github, width, collapsed, onUpdate }) {
|
|
110
|
+
const authors = Array.isArray(github?.authors)
|
|
111
|
+
? github.authors.filter((a) => typeof a === 'string' && a.trim())
|
|
112
|
+
: []
|
|
113
|
+
const primaryAuthor = authors[0] || ''
|
|
114
|
+
const createdAgo = timeAgo(github?.createdAt)
|
|
115
|
+
const { number: issueNumber, rest: titleText } = splitIssueTitle(title)
|
|
116
|
+
|
|
117
|
+
const kindLabel = getCommentKindLabel(github)
|
|
118
|
+
|
|
119
|
+
// Prefer pre-rendered bodyHtml (has signed image URLs), fall back to remark for discussions
|
|
120
|
+
const bodyHtml = useMemo(() => {
|
|
121
|
+
if (github?.bodyHtml) return postProcessHtml(github.bodyHtml)
|
|
122
|
+
return renderMarkdown(github?.body || '')
|
|
123
|
+
}, [github?.bodyHtml, github?.body])
|
|
124
|
+
|
|
125
|
+
// Set body HTML via ref — avoids React destroying/recreating video elements on re-render
|
|
126
|
+
const bodyRef = useRef(null)
|
|
127
|
+
const lastHtmlRef = useRef('')
|
|
128
|
+
useEffect(() => {
|
|
129
|
+
if (bodyRef.current && bodyHtml !== lastHtmlRef.current) {
|
|
130
|
+
bodyRef.current.innerHTML = bodyHtml
|
|
131
|
+
lastHtmlRef.current = bodyHtml
|
|
132
|
+
}
|
|
133
|
+
}, [bodyHtml])
|
|
134
|
+
|
|
135
|
+
// Also set on initial mount via callback ref
|
|
136
|
+
const setBodyRef = useCallback((el) => {
|
|
137
|
+
bodyRef.current = el
|
|
138
|
+
if (el && bodyHtml && bodyHtml !== lastHtmlRef.current) {
|
|
139
|
+
el.innerHTML = bodyHtml
|
|
140
|
+
lastHtmlRef.current = bodyHtml
|
|
141
|
+
}
|
|
142
|
+
}, [bodyHtml])
|
|
143
|
+
|
|
144
|
+
const sizeStyle = {
|
|
145
|
+
...(width ? { width: `${width}px` } : {}),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<WidgetWrapper>
|
|
150
|
+
<div className={`${styles.issueCard} ${collapsed ? styles.issueCardCollapsed : ''}`} style={sizeStyle}>
|
|
151
|
+
<div className={styles.typeBar}>
|
|
152
|
+
<MarkGithubIcon size={16} />
|
|
153
|
+
<span>{kindLabel}</span>
|
|
154
|
+
</div>
|
|
155
|
+
<header className={styles.issueHeader}>
|
|
156
|
+
<h2 className={styles.issueTitle}>
|
|
157
|
+
<a
|
|
158
|
+
href={url || '#'}
|
|
159
|
+
target="_blank"
|
|
160
|
+
rel="noopener noreferrer"
|
|
161
|
+
className={styles.issueTitleLink}
|
|
162
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
163
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
164
|
+
>
|
|
165
|
+
{titleText || url}
|
|
166
|
+
{issueNumber && <span className={styles.issueNumber}> {issueNumber}</span>}
|
|
167
|
+
</a>
|
|
168
|
+
</h2>
|
|
169
|
+
</header>
|
|
170
|
+
|
|
171
|
+
<div className={styles.issueByline}>
|
|
172
|
+
<div className={styles.issueBylineLeft}>
|
|
173
|
+
{primaryAuthor && (
|
|
174
|
+
<a
|
|
175
|
+
href={`https://github.com/${primaryAuthor}`}
|
|
176
|
+
target="_blank"
|
|
177
|
+
rel="noopener noreferrer"
|
|
178
|
+
className={styles.authorLink}
|
|
179
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
180
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
181
|
+
>
|
|
182
|
+
<img
|
|
183
|
+
className={styles.avatar}
|
|
184
|
+
src={`https://github.com/${primaryAuthor}.png?size=40`}
|
|
185
|
+
alt=""
|
|
186
|
+
width="20"
|
|
187
|
+
height="20"
|
|
188
|
+
loading="lazy"
|
|
189
|
+
/>
|
|
190
|
+
{primaryAuthor}
|
|
191
|
+
</a>
|
|
192
|
+
)}
|
|
193
|
+
<span className={styles.bylineText}>
|
|
194
|
+
{primaryAuthor && createdAgo ? ` opened ${createdAgo}` : createdAgo ? `Opened ${createdAgo}` : ''}
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
|
|
199
|
+
{bodyHtml && (
|
|
200
|
+
<div
|
|
201
|
+
className={`${styles.issueBody} ${collapsed ? styles.issueBodyScrollable : ''}`}
|
|
202
|
+
ref={setBodyRef}
|
|
203
|
+
/>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</WidgetWrapper>
|
|
207
|
+
)
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export default function LinkPreview({ id, props, onUpdate, resizable }) {
|
|
6
211
|
const url = readProp(props, 'url', linkPreviewSchema)
|
|
7
212
|
const title = readProp(props, 'title', linkPreviewSchema)
|
|
213
|
+
const github = props?.github && typeof props.github === 'object' ? props.github : null
|
|
214
|
+
|
|
215
|
+
const width = typeof props?.width === 'number' ? props.width : null
|
|
216
|
+
const height = typeof props?.height === 'number' ? props.height : null
|
|
217
|
+
|
|
218
|
+
if (github) {
|
|
219
|
+
return (
|
|
220
|
+
<GitHubIssueCard
|
|
221
|
+
url={url}
|
|
222
|
+
title={title}
|
|
223
|
+
github={github}
|
|
224
|
+
width={width}
|
|
225
|
+
collapsed={!!props?.collapsed}
|
|
226
|
+
onUpdate={onUpdate}
|
|
227
|
+
/>
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const sizeStyle = (width || height)
|
|
232
|
+
? { ...(width ? { width: `${width}px` } : {}), ...(height ? { minHeight: `${height}px` } : {}) }
|
|
233
|
+
: undefined
|
|
8
234
|
|
|
9
235
|
let hostname = ''
|
|
10
|
-
try {
|
|
11
|
-
|
|
12
|
-
|
|
236
|
+
try { hostname = new URL(url).hostname } catch { /* */ }
|
|
237
|
+
|
|
238
|
+
const handleResize = (w, h) => onUpdate?.({ width: w, height: h })
|
|
13
239
|
|
|
14
240
|
return (
|
|
15
241
|
<WidgetWrapper>
|
|
16
|
-
<div className={styles.card}>
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
target="_blank"
|
|
23
|
-
rel="noopener noreferrer"
|
|
24
|
-
className={styles.url}
|
|
25
|
-
onMouseDown={(e) => e.stopPropagation()}
|
|
26
|
-
onPointerDown={(e) => e.stopPropagation()}
|
|
27
|
-
>
|
|
28
|
-
{hostname || url}
|
|
29
|
-
</a>
|
|
242
|
+
<div className={styles.card} style={sizeStyle}>
|
|
243
|
+
<div className={styles.header}>
|
|
244
|
+
<span className={styles.icon}>🔗</span>
|
|
245
|
+
<div className={styles.text}>
|
|
246
|
+
{title && <p className={styles.title}>{title}</p>}
|
|
247
|
+
</div>
|
|
30
248
|
</div>
|
|
249
|
+
<a
|
|
250
|
+
href={url || '#'}
|
|
251
|
+
target="_blank"
|
|
252
|
+
rel="noopener noreferrer"
|
|
253
|
+
className={styles.url}
|
|
254
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
255
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
256
|
+
>
|
|
257
|
+
{hostname || url}
|
|
258
|
+
</a>
|
|
31
259
|
</div>
|
|
260
|
+
{resizable && <ResizeHandle width={width} height={height} onResize={handleResize} />}
|
|
32
261
|
</WidgetWrapper>
|
|
33
262
|
)
|
|
34
263
|
}
|
|
@@ -1,16 +1,31 @@
|
|
|
1
|
+
/* ── Plain link-preview card ──────────────────────────────────────── */
|
|
2
|
+
|
|
1
3
|
.card {
|
|
2
4
|
display: flex;
|
|
3
|
-
|
|
4
|
-
gap:
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 10px;
|
|
5
7
|
padding: 14px 16px;
|
|
6
8
|
text-decoration: none;
|
|
7
9
|
color: inherit;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
width: 320px;
|
|
11
|
+
min-height: 120px;
|
|
12
|
+
border-radius: 10px;
|
|
13
|
+
border: 1px solid var(--borderColor-default, #d1d9e0);
|
|
14
|
+
background: var(--bgColor-default, #ffffff);
|
|
15
|
+
transition: background 150ms, border-color 150ms;
|
|
16
|
+
box-sizing: border-box;
|
|
10
17
|
}
|
|
11
18
|
|
|
12
19
|
.card:hover {
|
|
13
20
|
background: var(--bgColor-muted, #f6f8fa);
|
|
21
|
+
border-color: var(--borderColor-muted, #afb8c1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.header {
|
|
25
|
+
display: flex;
|
|
26
|
+
align-items: flex-start;
|
|
27
|
+
gap: 12px;
|
|
28
|
+
width: 100%;
|
|
14
29
|
}
|
|
15
30
|
|
|
16
31
|
.icon {
|
|
@@ -21,17 +36,15 @@
|
|
|
21
36
|
.text {
|
|
22
37
|
overflow: hidden;
|
|
23
38
|
min-width: 0;
|
|
39
|
+
flex: 1;
|
|
24
40
|
}
|
|
25
41
|
|
|
26
42
|
.title {
|
|
27
43
|
margin: 0;
|
|
28
44
|
font-size: 14px;
|
|
29
45
|
font-weight: 600;
|
|
30
|
-
line-height: 1.
|
|
46
|
+
line-height: 1.35;
|
|
31
47
|
color: var(--fgColor-default, #1f2328);
|
|
32
|
-
white-space: nowrap;
|
|
33
|
-
overflow: hidden;
|
|
34
|
-
text-overflow: ellipsis;
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
.url {
|
|
@@ -49,3 +62,331 @@
|
|
|
49
62
|
text-decoration: underline;
|
|
50
63
|
color: var(--fgColor-accent, #0969da);
|
|
51
64
|
}
|
|
65
|
+
|
|
66
|
+
/* ── GitHub issue / discussion card ──────────────────────────────── */
|
|
67
|
+
|
|
68
|
+
.issueCard {
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
width: 580px;
|
|
72
|
+
min-height: 300px;
|
|
73
|
+
border-radius: 10px;
|
|
74
|
+
border: 1px solid var(--borderColor-default, #d1d9e0);
|
|
75
|
+
background: var(--bgColor-default, #ffffff);
|
|
76
|
+
box-sizing: border-box;
|
|
77
|
+
overflow: hidden;
|
|
78
|
+
font-family: var(--tc-font-stack, system-ui, -apple-system, sans-serif);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.issueCardCollapsed {
|
|
82
|
+
max-height: 800px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.typeBar {
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 6px;
|
|
89
|
+
padding: 10px 10px;
|
|
90
|
+
font-size: 12px;
|
|
91
|
+
font-weight: 500;
|
|
92
|
+
color: var(--fgColor-muted, #656d76);
|
|
93
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
94
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
95
|
+
white-space: nowrap;
|
|
96
|
+
user-select: none;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.issueBodyScrollable {
|
|
100
|
+
flex: 1;
|
|
101
|
+
min-height: 0;
|
|
102
|
+
overflow-y: auto;
|
|
103
|
+
overflow-x: hidden;
|
|
104
|
+
pointer-events: auto;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.issueHeader {
|
|
108
|
+
padding: 24px 24px 0;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.issueTitle {
|
|
112
|
+
margin: 0 0 4px;
|
|
113
|
+
font-size: 32px;
|
|
114
|
+
font-weight: 400;
|
|
115
|
+
line-height: 1.25;
|
|
116
|
+
color: var(--fgColor-default, #1f2328);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.issueTitleLink {
|
|
120
|
+
color: var(--fgColor-default, #1f2328) !important;
|
|
121
|
+
text-decoration: none !important;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.issueTitleLink:hover {
|
|
125
|
+
text-decoration: underline !important;
|
|
126
|
+
color: var(--fgColor-default, #1f2328) !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.issueNumber {
|
|
130
|
+
font-size: 32px;
|
|
131
|
+
font-weight: 400;
|
|
132
|
+
color: var(--fgColor-muted, #656d76);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.issueContext {
|
|
136
|
+
margin: 0;
|
|
137
|
+
font-size: 12px;
|
|
138
|
+
color: var(--fgColor-muted, #656d76);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.issueByline {
|
|
142
|
+
display: flex;
|
|
143
|
+
align-items: center;
|
|
144
|
+
justify-content: space-between;
|
|
145
|
+
gap: 8px;
|
|
146
|
+
padding: 12px 24px;
|
|
147
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.issueBylineLeft {
|
|
151
|
+
display: flex;
|
|
152
|
+
align-items: center;
|
|
153
|
+
gap: 8px;
|
|
154
|
+
min-width: 0;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.avatar {
|
|
158
|
+
width: 20px;
|
|
159
|
+
height: 20px;
|
|
160
|
+
border-radius: 50%;
|
|
161
|
+
flex-shrink: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.authorLink {
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
gap: 8px;
|
|
168
|
+
text-decoration: none !important;
|
|
169
|
+
color: var(--fgColor-default, #1f2328) !important;
|
|
170
|
+
font-size: 13px;
|
|
171
|
+
font-weight: 600;
|
|
172
|
+
white-space: nowrap;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.authorLink:hover {
|
|
176
|
+
text-decoration: underline !important;
|
|
177
|
+
color: var(--fgColor-default, #1f2328) !important;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.bylineText {
|
|
181
|
+
font-size: 13px;
|
|
182
|
+
color: var(--fgColor-muted, #656d76);
|
|
183
|
+
white-space: nowrap;
|
|
184
|
+
overflow: hidden;
|
|
185
|
+
text-overflow: ellipsis;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
.bylineText strong {
|
|
189
|
+
font-weight: 600;
|
|
190
|
+
color: var(--fgColor-default, #1f2328);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/* ── Issue body (rendered markdown) ──────────────────────────────── */
|
|
194
|
+
|
|
195
|
+
.issueBody {
|
|
196
|
+
padding: 16px 24px 24px;
|
|
197
|
+
font-size: 14px;
|
|
198
|
+
line-height: 1.6;
|
|
199
|
+
color: var(--fgColor-default, #1f2328);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.issueBody :global(*) {
|
|
203
|
+
pointer-events: none;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
.issueBody a {
|
|
207
|
+
color: var(--fgColor-accent, #0969da);
|
|
208
|
+
text-decoration: none;
|
|
209
|
+
pointer-events: auto;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.issueBody a:hover {
|
|
214
|
+
text-decoration: underline;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.issueBody img {
|
|
218
|
+
max-width: 100%;
|
|
219
|
+
height: auto;
|
|
220
|
+
border-radius: 6px;
|
|
221
|
+
border: 1px solid var(--borderColor-muted, #d8dee4);
|
|
222
|
+
margin: 8px 0;
|
|
223
|
+
display: block;
|
|
224
|
+
pointer-events: auto;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.issueBody video {
|
|
228
|
+
max-width: 100%;
|
|
229
|
+
height: auto;
|
|
230
|
+
border-radius: 6px;
|
|
231
|
+
border: 1px solid var(--borderColor-muted, #d8dee4);
|
|
232
|
+
margin: 8px 0;
|
|
233
|
+
display: block;
|
|
234
|
+
pointer-events: auto;
|
|
235
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
236
|
+
}
|
|
237
|
+
.issueBody h1 {
|
|
238
|
+
font-size: 20px;
|
|
239
|
+
font-weight: 700;
|
|
240
|
+
margin: 16px 0 8px;
|
|
241
|
+
padding-bottom: 4px;
|
|
242
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
243
|
+
line-height: 1.3;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.issueBody h1:first-child {
|
|
247
|
+
margin-top: 0;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.issueBody h2 {
|
|
251
|
+
font-size: 17px;
|
|
252
|
+
font-weight: 600;
|
|
253
|
+
margin: 14px 0 6px;
|
|
254
|
+
padding-bottom: 3px;
|
|
255
|
+
border-bottom: 1px solid var(--borderColor-muted, #d8dee4);
|
|
256
|
+
line-height: 1.3;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.issueBody h3 {
|
|
260
|
+
font-size: 15px;
|
|
261
|
+
font-weight: 600;
|
|
262
|
+
margin: 12px 0 4px;
|
|
263
|
+
line-height: 1.3;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.issueBody p {
|
|
267
|
+
margin: 0 0 12px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
.issueBody code {
|
|
271
|
+
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
272
|
+
padding: 2px 5px;
|
|
273
|
+
border-radius: 4px;
|
|
274
|
+
font-size: 12px;
|
|
275
|
+
font-weight: 400;
|
|
276
|
+
font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
.issueBody ul {
|
|
280
|
+
margin: 0 0 12px;
|
|
281
|
+
padding-left: 24px;
|
|
282
|
+
list-style-type: disc;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
.issueBody ol {
|
|
286
|
+
margin: 0 0 12px;
|
|
287
|
+
padding-left: 24px;
|
|
288
|
+
list-style-type: decimal;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.issueBody li {
|
|
292
|
+
margin: 0 0 4px;
|
|
293
|
+
display: list-item;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
.issueBody li > ul,
|
|
297
|
+
.issueBody li > ol {
|
|
298
|
+
margin-top: 4px;
|
|
299
|
+
margin-bottom: 4px;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/* Accent-color checkboxes */
|
|
303
|
+
.issueBody input[type="checkbox"] {
|
|
304
|
+
margin-right: 6px;
|
|
305
|
+
pointer-events: none;
|
|
306
|
+
accent-color: var(--fgColor-accent, #0969da);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.issueBody li:has(input[type="checkbox"]) {
|
|
310
|
+
list-style: none;
|
|
311
|
+
margin-left: -24px;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/* GFM: Strikethrough */
|
|
315
|
+
.issueBody del {
|
|
316
|
+
text-decoration: line-through;
|
|
317
|
+
color: var(--fgColor-muted, #656d76);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* GFM: Tables */
|
|
321
|
+
.issueBody table {
|
|
322
|
+
border-collapse: collapse;
|
|
323
|
+
margin: 12px 0;
|
|
324
|
+
width: 100%;
|
|
325
|
+
font-size: 13px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.issueBody th,
|
|
329
|
+
.issueBody td {
|
|
330
|
+
border: 1px solid var(--borderColor-default, #d0d7de);
|
|
331
|
+
padding: 6px 12px;
|
|
332
|
+
text-align: left;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.issueBody th {
|
|
336
|
+
background: var(--bgColor-muted, #f6f8fa);
|
|
337
|
+
font-weight: 600;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/* Code blocks */
|
|
341
|
+
.issueBody pre {
|
|
342
|
+
padding: 12px 16px;
|
|
343
|
+
border-radius: 6px;
|
|
344
|
+
border: 1px solid var(--borderColor-muted, #d8dee4);
|
|
345
|
+
overflow-x: auto;
|
|
346
|
+
margin: 12px 0;
|
|
347
|
+
background: var(--bgColor-neutral-muted, #afb8c133);
|
|
348
|
+
line-height: 1.4;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.issueBody pre code {
|
|
352
|
+
background: none;
|
|
353
|
+
padding: 0;
|
|
354
|
+
font-size: 12px;
|
|
355
|
+
white-space: pre;
|
|
356
|
+
word-break: normal;
|
|
357
|
+
overflow-wrap: normal;
|
|
358
|
+
display: block;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Blockquotes */
|
|
362
|
+
.issueBody blockquote {
|
|
363
|
+
border-left: 4px solid var(--borderColor-default, #d0d7de);
|
|
364
|
+
margin: 12px 0;
|
|
365
|
+
padding: 4px 16px;
|
|
366
|
+
color: var(--fgColor-muted, #656d76);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* Horizontal rules */
|
|
370
|
+
.issueBody hr {
|
|
371
|
+
border: none;
|
|
372
|
+
border-top: 1px solid var(--borderColor-default, #d0d7de);
|
|
373
|
+
margin: 16px 0;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* Details/summary — show content expanded, no collapse UI */
|
|
377
|
+
.issueBody details {
|
|
378
|
+
border: none;
|
|
379
|
+
margin: 0;
|
|
380
|
+
padding: 0;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.issueBody summary {
|
|
384
|
+
display: none;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.error {
|
|
388
|
+
margin: 0;
|
|
389
|
+
padding: 0 24px 12px;
|
|
390
|
+
font-size: 12px;
|
|
391
|
+
color: var(--fgColor-danger, #cf222e);
|
|
392
|
+
}
|