@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.
Files changed (29) hide show
  1. package/package.json +3 -3
  2. package/src/canvas/CanvasPage.bridge.test.jsx +87 -2
  3. package/src/canvas/CanvasPage.jsx +161 -18
  4. package/src/canvas/CanvasPage.module.css +54 -0
  5. package/src/canvas/CanvasPage.multiselect.test.jsx +2 -0
  6. package/src/canvas/canvasApi.js +8 -0
  7. package/src/canvas/widgets/FigmaEmbed.jsx +42 -7
  8. package/src/canvas/widgets/FigmaEmbed.module.css +21 -0
  9. package/src/canvas/widgets/LinkPreview.jsx +247 -18
  10. package/src/canvas/widgets/LinkPreview.module.css +349 -8
  11. package/src/canvas/widgets/LinkPreview.test.jsx +71 -0
  12. package/src/canvas/widgets/MarkdownBlock.jsx +2 -1
  13. package/src/canvas/widgets/MarkdownBlock.module.css +34 -11
  14. package/src/canvas/widgets/PrototypeEmbed.jsx +101 -44
  15. package/src/canvas/widgets/PrototypeEmbed.module.css +1 -0
  16. package/src/canvas/widgets/StoryWidget.jsx +86 -42
  17. package/src/canvas/widgets/StoryWidget.module.css +1 -0
  18. package/src/canvas/widgets/WidgetChrome.jsx +20 -1
  19. package/src/canvas/widgets/embedInteraction.test.jsx +16 -18
  20. package/src/canvas/widgets/embedTheme.js +37 -1
  21. package/src/canvas/widgets/githubUrl.js +82 -0
  22. package/src/canvas/widgets/githubUrl.test.js +74 -0
  23. package/src/canvas/widgets/refreshQueue.js +108 -0
  24. package/src/canvas/widgets/snapshotDisplay.test.jsx +60 -90
  25. package/src/canvas/widgets/useSnapshotCapture.js +38 -139
  26. package/src/canvas/widgets/useSnapshotCapture.test.jsx +30 -91
  27. package/src/canvas/widgets/widgetConfig.test.js +1 -1
  28. package/src/story/StoryPage.jsx +25 -60
  29. 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
- export default function LinkPreview({ props }) {
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
- hostname = new URL(url).hostname
12
- } catch { /* invalid URL */ }
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
- <span className={styles.icon}>🔗</span>
18
- <div className={styles.text}>
19
- {title && <p className={styles.title}>{title}</p>}
20
- <a
21
- href={url}
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
- align-items: center;
4
- gap: 12px;
5
+ flex-direction: column;
6
+ gap: 10px;
5
7
  padding: 14px 16px;
6
8
  text-decoration: none;
7
9
  color: inherit;
8
- min-width: 240px;
9
- transition: background 150ms;
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.4;
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
+ }