@dfosco/storyboard 0.7.0 → 0.7.1

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.
@@ -0,0 +1,12 @@
1
+ <svg width="1040" height="1040" viewBox="0 0 1040 1040" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <g clip-path="url(#clip0_281_5346)">
3
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M1040 520C1040 607.255 1040.14 680.911 1033.5 740.187C1026.63 801.432 1011.5 859.346 972.737 910.26C954.816 933.8 933.8 954.816 910.26 972.737C859.346 1011.5 801.432 1026.63 740.187 1033.5C680.911 1040.14 607.255 1040 520 1040C432.747 1040 359.09 1040.14 299.813 1033.5C238.568 1026.63 180.656 1011.5 129.74 972.737C106.201 954.816 85.1857 933.8 67.263 910.26C28.496 859.346 13.3667 801.432 6.50058 740.187C-0.144753 680.911 0.000253571 607.255 0.000253571 520C0.000253571 432.747 -0.144754 359.09 6.50058 299.813C13.3667 238.568 28.496 180.656 67.263 129.74C85.1857 106.201 106.201 85.1857 129.74 67.263C180.656 28.496 238.568 13.3667 299.813 6.50058C359.09 -0.144754 432.747 0.000253571 520 0.000253571C607.255 0.000253571 680.911 -0.144753 740.187 6.50058C801.432 13.3667 859.346 28.496 910.26 67.263C933.8 85.1857 954.816 106.201 972.737 129.74C1011.5 180.656 1026.63 238.568 1033.5 299.813C1040.14 359.09 1040 432.747 1040 520Z" fill="#170C1F"/>
4
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M984 520.5C984 325.013 984 226.507 933.401 158.522L932.197 156.924C918.393 138.793 902.207 122.608 884.078 108.804C816.039 56.9996 717.525 57 520.5 57C323.474 57 224.961 56.9996 156.924 108.804L155.229 110.105C137.784 123.621 122.176 139.36 108.804 156.924L107.6 158.522C57 226.507 57 325.013 57 520.5C57 717.525 56.9997 816.039 108.804 884.078C122.608 902.207 138.793 918.393 156.924 932.197C224.961 984 323.474 984 520.5 984C715.986 984 814.495 984 882.477 933.401L884.078 932.197C901.64 918.824 917.379 903.215 930.895 885.771L932.197 884.078C984 816.039 984 717.525 984 520.5Z" fill="#170C1F"/>
5
+ <path fill-rule="evenodd" clip-rule="evenodd" d="M208.988 378.291C208.988 407.969 220.778 436.431 241.763 457.416C262.748 478.401 291.21 490.19 320.887 490.19C350.564 490.19 379.026 478.401 400.011 457.416C420.997 436.431 432.786 407.969 432.786 378.291C432.786 348.614 420.997 320.152 400.011 299.167C379.026 278.182 350.564 266.393 320.887 266.393C291.21 266.393 262.748 278.182 241.763 299.167C220.778 320.152 208.988 348.614 208.988 378.291ZM605.719 378.291C605.719 392.986 608.613 407.537 614.237 421.113C619.86 434.689 628.102 447.025 638.493 457.416C648.884 467.806 661.219 476.049 674.796 481.672C688.372 487.296 702.923 490.19 717.617 490.19C732.312 490.19 746.863 487.296 760.439 481.672C774.015 476.049 786.351 467.806 796.742 457.416C807.133 447.025 815.375 434.689 820.998 421.113C826.622 407.537 829.516 392.986 829.516 378.291C829.516 363.597 826.622 349.046 820.998 335.47C815.375 321.893 807.133 309.558 796.742 299.167C786.351 288.776 774.015 280.534 760.439 274.91C746.863 269.287 732.312 266.393 717.617 266.393C702.923 266.393 688.372 269.287 674.796 274.91C661.219 280.534 648.884 288.776 638.493 299.167C628.102 309.558 619.86 321.893 614.237 335.47C608.613 349.046 605.719 363.597 605.719 378.291ZM631.928 541.203C646.198 541.203 658.91 553.016 656.488 568.2C645.989 634.034 589.271 684.367 520.824 684.367C452.376 684.367 395.658 634.034 385.16 568.2C382.737 553.016 395.449 541.203 409.72 541.203C424.454 541.203 435.266 553.171 438.371 566.171C447.328 603.669 480.868 631.497 520.824 631.497C560.78 631.497 594.32 603.669 603.277 566.171C606.382 553.171 617.193 541.203 631.928 541.203Z" fill="#EBD7EF"/>
6
+ </g>
7
+ <defs>
8
+ <clipPath id="clip0_281_5346">
9
+ <rect width="1040" height="1040" fill="white"/>
10
+ </clipPath>
11
+ </defs>
12
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -17,6 +17,7 @@
17
17
  "files": [
18
18
  "src",
19
19
  "dist",
20
+ "assets",
20
21
  "scaffold",
21
22
  "mascot",
22
23
  "toolbar.config.json",
@@ -153,6 +153,10 @@ function toContext(target) {
153
153
 
154
154
  function buildPullRequestSnapshot(target, pr) {
155
155
  const number = pr?.number ?? target.number
156
+ const merged = Boolean(pr?.merged_at || pr?.merged)
157
+ const draft = Boolean(pr?.draft)
158
+ const rawState = pr?.state === 'closed' ? 'closed' : 'open'
159
+ const state = merged ? 'merged' : (draft && rawState === 'open' ? 'draft' : rawState)
156
160
  return {
157
161
  kind: 'pull_request',
158
162
  parentKind: 'pull_request',
@@ -164,6 +168,14 @@ function buildPullRequestSnapshot(target, pr) {
164
168
  createdAt: pr?.created_at ?? null,
165
169
  updatedAt: pr?.updated_at ?? null,
166
170
  url: target.url,
171
+ state,
172
+ merged,
173
+ draft,
174
+ baseRef: typeof pr?.base?.ref === 'string' ? pr.base.ref : null,
175
+ headRef: typeof pr?.head?.ref === 'string' ? pr.head.ref : null,
176
+ additions: typeof pr?.additions === 'number' ? pr.additions : null,
177
+ deletions: typeof pr?.deletions === 'number' ? pr.deletions : null,
178
+ changedFiles: typeof pr?.changed_files === 'number' ? pr.changed_files : null,
167
179
  }
168
180
  }
169
181
 
@@ -202,6 +202,76 @@ describe('fetchGitHubEmbedSnapshot', () => {
202
202
  expect(graphqlArgs.slice(0, 2)).toEqual(['api', 'graphql'])
203
203
  })
204
204
 
205
+ it('hydrates pull request snapshot metadata (state, branches, diff)', () => {
206
+ ghExec
207
+ .mockReturnValueOnce('gh version 2.58.0')
208
+ .mockReturnValueOnce(JSON.stringify({
209
+ number: 79,
210
+ title: 'fix(viewfinder): eliminate FOUC',
211
+ body: 'A PR body',
212
+ state: 'closed',
213
+ merged: true,
214
+ merged_at: '2026-04-10T00:00:00Z',
215
+ draft: false,
216
+ user: { login: 'dfosco' },
217
+ created_at: '2026-04-01T00:00:00Z',
218
+ updated_at: '2026-04-10T00:00:00Z',
219
+ base: { ref: '3.11.0' },
220
+ head: { ref: '3.11.0--fix-viewfinder-fouc' },
221
+ additions: 63,
222
+ deletions: 18,
223
+ changed_files: 4,
224
+ }))
225
+
226
+ const snapshot = fetchGitHubEmbedSnapshot('https://github.com/dfosco/storyboard/pull/79')
227
+
228
+ expect(snapshot).toMatchObject({
229
+ kind: 'pull_request',
230
+ parentKind: 'pull_request',
231
+ title: '#79 fix(viewfinder): eliminate FOUC',
232
+ state: 'merged',
233
+ merged: true,
234
+ draft: false,
235
+ baseRef: '3.11.0',
236
+ headRef: '3.11.0--fix-viewfinder-fouc',
237
+ additions: 63,
238
+ deletions: 18,
239
+ changedFiles: 4,
240
+ })
241
+ })
242
+
243
+ it('marks draft PRs and unmerged closed PRs correctly', () => {
244
+ ghExec
245
+ .mockReturnValueOnce('gh version 2.58.0')
246
+ .mockReturnValueOnce(JSON.stringify({
247
+ number: 5,
248
+ title: 'WIP: refactor',
249
+ state: 'open',
250
+ draft: true,
251
+ merged: false,
252
+ user: { login: 'dfosco' },
253
+ }))
254
+
255
+ const draftSnap = fetchGitHubEmbedSnapshot('https://github.com/dfosco/storyboard/pull/5')
256
+ expect(draftSnap.state).toBe('draft')
257
+ expect(draftSnap.draft).toBe(true)
258
+ expect(draftSnap.merged).toBe(false)
259
+
260
+ ghExec
261
+ .mockReturnValueOnce('gh version 2.58.0')
262
+ .mockReturnValueOnce(JSON.stringify({
263
+ number: 6,
264
+ state: 'closed',
265
+ merged: false,
266
+ merged_at: null,
267
+ user: { login: 'dfosco' },
268
+ }))
269
+
270
+ const closedSnap = fetchGitHubEmbedSnapshot('https://github.com/dfosco/storyboard/pull/6')
271
+ expect(closedSnap.state).toBe('closed')
272
+ expect(closedSnap.merged).toBe(false)
273
+ })
274
+
205
275
  it('rejects issue comment URLs when comment parent does not match', () => {
206
276
  ghExec
207
277
  .mockReturnValueOnce('gh version 2.58.0')
@@ -270,8 +340,7 @@ describe('fetchGitHubEmbedSnapshot', () => {
270
340
  })
271
341
  })
272
342
 
273
- it('hydrates PR top-level comment snapshot metadata', () => {
274
- ghExec
343
+ it('hydrates PR top-level comment snapshot metadata', () => { ghExec
275
344
  .mockReturnValueOnce('gh version 2.58.0')
276
345
  .mockReturnValueOnce(JSON.stringify({
277
346
  body: 'Looks good!',
@@ -110,6 +110,18 @@ export default function storyboardServer() {
110
110
  const routeHandlers = new Map()
111
111
  const clientScripts = []
112
112
 
113
+ // Load packaged favicon once and cache as data URI so every consumer
114
+ // gets the storyboard brand favicon without shipping/copying any asset.
115
+ let faviconDataUri = ''
116
+ try {
117
+ const faviconPath = path.resolve(
118
+ path.dirname(new URL(import.meta.url).pathname),
119
+ '../../../assets/favicon.svg'
120
+ )
121
+ const svg = fs.readFileSync(faviconPath, 'utf8')
122
+ faviconDataUri = 'data:image/svg+xml;base64,' + Buffer.from(svg, 'utf8').toString('base64')
123
+ } catch { /* favicon missing — skip injection */ }
124
+
113
125
  return {
114
126
  name: 'storyboard-server',
115
127
 
@@ -872,6 +884,17 @@ export default function storyboardServer() {
872
884
  })
873
885
  }
874
886
 
887
+ // Inject the storyboard brand favicon as a data URI. Appending to
888
+ // head means it wins over any earlier <link rel="icon"> the consumer
889
+ // declared (browsers use the last declaration).
890
+ if (faviconDataUri) {
891
+ tags.push({
892
+ tag: 'link',
893
+ attrs: { rel: 'icon', type: 'image/svg+xml', href: faviconDataUri },
894
+ injectTo: 'head',
895
+ })
896
+ }
897
+
875
898
  return tags
876
899
  },
877
900
 
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState, forwardRef, useImper
2
2
  import { remark } from 'remark'
3
3
  import remarkGfm from 'remark-gfm'
4
4
  import remarkHtml from 'remark-html'
5
- import { MarkGithubIcon } from '@primer/octicons-react'
5
+ import { GitBranchIcon, GitMergeIcon, GitPullRequestClosedIcon, GitPullRequestDraftIcon, GitPullRequestIcon, MarkGithubIcon } from '@primer/octicons-react'
6
6
  import WidgetWrapper from './WidgetWrapper.jsx'
7
7
  import ResizeHandle from './ResizeHandle.jsx'
8
8
  import { readProp, linkPreviewSchema } from './widgetProps.js'
@@ -119,6 +119,48 @@ function getCommentKindLabel(github) {
119
119
  return 'Comment'
120
120
  }
121
121
 
122
+ const PR_STATE_META = {
123
+ merged: { label: 'Merged', Icon: GitMergeIcon, className: 'prStateMerged' },
124
+ closed: { label: 'Closed', Icon: GitPullRequestClosedIcon, className: 'prStateClosed' },
125
+ draft: { label: 'Draft', Icon: GitPullRequestDraftIcon, className: 'prStateDraft' },
126
+ open: { label: 'Open', Icon: GitPullRequestIcon, className: 'prStateOpen' },
127
+ }
128
+
129
+ function PullRequestMeta({ github }) {
130
+ if (github?.kind !== 'pull_request') return null
131
+ const stateKey = github?.state || 'open'
132
+ const stateMeta = PR_STATE_META[stateKey] || PR_STATE_META.open
133
+ const { Icon: StateIcon } = stateMeta
134
+ const baseRef = github?.baseRef || null
135
+ const headRef = github?.headRef || null
136
+ const additions = typeof github?.additions === 'number' ? github.additions : null
137
+ const deletions = typeof github?.deletions === 'number' ? github.deletions : null
138
+ const hasDiff = additions !== null || deletions !== null
139
+
140
+ return (
141
+ <div className={styles.prMeta}>
142
+ <span className={`${styles.prStateBadge} ${styles[stateMeta.className]}`}>
143
+ <StateIcon size={12} />
144
+ {stateMeta.label}
145
+ </span>
146
+ {(baseRef || headRef) && (
147
+ <span className={styles.prBranchRef} title={`${headRef || '?'} → ${baseRef || '?'}`}>
148
+ <GitBranchIcon size={12} />
149
+ <span className={styles.prBranchName}>{baseRef || '?'}</span>
150
+ <span className={styles.prBranchArrow}>←</span>
151
+ <span className={styles.prBranchName}>{headRef || '?'}</span>
152
+ </span>
153
+ )}
154
+ {hasDiff && (
155
+ <span className={styles.prDiffStat}>
156
+ {additions !== null && <span className={styles.prDiffAdd}>+{additions}</span>}
157
+ {deletions !== null && <span className={styles.prDiffDel}>-{deletions}</span>}
158
+ </span>
159
+ )}
160
+ </div>
161
+ )
162
+ }
163
+
122
164
  function GitHubIssueCard({ id, url, title, github, width, collapsed, expanded, expandMode, onCloseExpand }) {
123
165
  const authors = Array.isArray(github?.authors)
124
166
  ? github.authors.filter((a) => typeof a === 'string' && a.trim())
@@ -209,6 +251,7 @@ function GitHubIssueCard({ id, url, title, github, width, collapsed, expanded, e
209
251
  {primaryAuthor && createdAgo ? ` opened ${createdAgo}` : createdAgo ? `Opened ${createdAgo}` : ''}
210
252
  </span>
211
253
  </div>
254
+ <PullRequestMeta github={github} />
212
255
  </div>
213
256
 
214
257
  {bodyHtml && (
@@ -243,6 +286,7 @@ function GitHubIssueCard({ id, url, title, github, width, collapsed, expanded, e
243
286
  )}
244
287
  {createdAgo && <span className={styles.expandedBylineText}>{primaryAuthor ? ` opened ${createdAgo}` : `Opened ${createdAgo}`}</span>}
245
288
  </div>
289
+ <PullRequestMeta github={github} />
246
290
  </header>
247
291
  {bodyHtml && <div className={styles.expandedIssueBody} dangerouslySetInnerHTML={{ __html: bodyHtml }} />}
248
292
  </div>
@@ -583,3 +583,74 @@
583
583
  .expandedUrl:hover {
584
584
  text-decoration: underline;
585
585
  }
586
+
587
+ /* ── PR metadata (state badge + branch refs + diff stats) ──────────── */
588
+
589
+ .prMeta {
590
+ display: flex;
591
+ align-items: center;
592
+ gap: 8px;
593
+ flex-wrap: wrap;
594
+ font-size: 12px;
595
+ color: var(--fgColor-muted, #656d76);
596
+ min-width: 0;
597
+ }
598
+
599
+ .prStateBadge {
600
+ display: inline-flex;
601
+ align-items: center;
602
+ gap: 4px;
603
+ padding: 3px 8px;
604
+ border-radius: 2em;
605
+ font-size: 12px;
606
+ font-weight: 500;
607
+ line-height: 1;
608
+ color: var(--fgColor-onEmphasis, #ffffff);
609
+ white-space: nowrap;
610
+ }
611
+
612
+ .prStateOpen { background: var(--bgColor-open-emphasis, #1f883d); }
613
+ .prStateMerged { background: var(--bgColor-done-emphasis, #8250df); }
614
+ .prStateClosed { background: var(--bgColor-closed-emphasis, #cf222e); }
615
+ .prStateDraft {
616
+ background: var(--bgColor-neutral-emphasis, #59636e);
617
+ }
618
+
619
+ .prBranchRef {
620
+ display: inline-flex;
621
+ align-items: center;
622
+ gap: 4px;
623
+ padding: 2px 6px;
624
+ border-radius: 4px;
625
+ background: var(--bgColor-muted, #f6f8fa);
626
+ color: var(--fgColor-default, #1f2328);
627
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
628
+ font-size: 11px;
629
+ max-width: 240px;
630
+ overflow: hidden;
631
+ }
632
+
633
+ .prBranchName {
634
+ overflow: hidden;
635
+ text-overflow: ellipsis;
636
+ white-space: nowrap;
637
+ max-width: 96px;
638
+ }
639
+
640
+ .prBranchArrow {
641
+ color: var(--fgColor-muted, #656d76);
642
+ font-family: -apple-system, system-ui, sans-serif;
643
+ flex-shrink: 0;
644
+ }
645
+
646
+ .prDiffStat {
647
+ display: inline-flex;
648
+ align-items: center;
649
+ gap: 4px;
650
+ font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
651
+ font-size: 12px;
652
+ font-weight: 600;
653
+ }
654
+
655
+ .prDiffAdd { color: var(--fgColor-success, #1a7f37); }
656
+ .prDiffDel { color: var(--fgColor-danger, #cf222e); }