@dfosco/storyboard 0.11.2 → 0.11.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dfosco/storyboard",
3
- "version": "0.11.2",
3
+ "version": "0.11.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "description": "Storyboard prototyping framework — core engine, React integration, and canvas",
@@ -19,26 +19,37 @@ function branchLabel(entry, homeTitle, homeFolder) {
19
19
  return pieces.join(' ')
20
20
  }
21
21
 
22
- export default function BranchesDropdown({ branches, branchBasePath, currentBranch, homeTitle = '', homeFolder = null }) {
22
+ export default function BranchesDropdown({ branches, branchBasePath, currentBranch, homeTitle = '', homeFolder = null, isBranchOnly = false }) {
23
23
  if (!currentBranch || isLocalDev() || !Array.isArray(branches)) return null
24
24
 
25
25
  const visibleBranches = branches.filter(entry => entry?.branch && entry.branch !== currentBranch)
26
26
  if (visibleBranches.length === 0) return null
27
27
 
28
+ // When the prototype is branch-only (i.e. the current branch doesn't
29
+ // have it), give the button a subtle accent so users can spot the
30
+ // affordance without painting the whole card. Tooltip text shifts to
31
+ // make the meaning explicit.
32
+ const btnClass = isBranchOnly ? `${css.iconBtn} ${css.iconBtnAccent}` : css.iconBtn
33
+ const label = isBranchOnly
34
+ ? 'Open on another branch'
35
+ : 'See branches'
36
+
28
37
  return (
29
38
  <Menu.Root>
30
39
  <Menu.Trigger
31
- className={css.iconBtn}
40
+ className={btnClass}
32
41
  onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
33
- aria-label="See branches"
34
- title="See branches"
42
+ aria-label={label}
43
+ title={label}
35
44
  >
36
45
  <GitBranchIcon size={16} />
37
46
  </Menu.Trigger>
38
47
  <Menu.Portal>
39
48
  <Menu.Positioner className={css.branchesPositioner} side="bottom" align="end" sideOffset={4}>
40
49
  <Menu.Popup className={css.branchesPopup}>
41
- <div className={css.branchesTitle}>Branches</div>
50
+ <div className={css.branchesTitle}>
51
+ {isBranchOnly ? 'Available on' : 'Branches'}
52
+ </div>
42
53
  {visibleBranches.map(entry => {
43
54
  const href = entry.isExternal && entry.externalUrl
44
55
  ? entry.externalUrl
@@ -17,6 +17,22 @@
17
17
  color: var(--color-foreground-muted, #555);
18
18
  }
19
19
 
20
+ /* Subtle accent applied when the current branch doesn't have the
21
+ * prototype (the card is "branch-only" elsewhere). Replaces the
22
+ * previous loud card-level blue background — now the only marker
23
+ * that a prototype lives on another branch is this single tinted
24
+ * button, plus an accent ring on hover/open. */
25
+ .iconBtnAccent {
26
+ color: var(--color-foreground-accent, #0969da);
27
+ background: var(--color-background-accent-muted, #ddf4ff);
28
+ }
29
+
30
+ .iconBtnAccent:hover {
31
+ background: var(--color-background-accent-muted, #ddf4ff);
32
+ color: var(--color-foreground-accent, #0969da);
33
+ box-shadow: inset 0 0 0 1px var(--color-border-accent-muted, #54aeff66);
34
+ }
35
+
20
36
  .branchesPositioner {
21
37
  z-index: 200;
22
38
  }
@@ -0,0 +1,62 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest'
2
+ import { render, cleanup } from '@testing-library/react'
3
+ import BranchesDropdown from './BranchesDropdown.jsx'
4
+
5
+ beforeEach(() => {
6
+ cleanup()
7
+ // BranchesDropdown bails out when window.__SB_LOCAL_DEV__ === true.
8
+ if (typeof window !== 'undefined') {
9
+ delete window.__SB_LOCAL_DEV__
10
+ }
11
+ })
12
+
13
+ const branches = [
14
+ { branch: 'main', folder: 'demos', route: '/Signup' },
15
+ { branch: 'feature-x', folder: 'demos', route: '/Signup' },
16
+ ]
17
+
18
+ describe('BranchesDropdown', () => {
19
+ it('renders the trigger when other branches have the prototype', () => {
20
+ const { container } = render(
21
+ <BranchesDropdown branches={branches} branchBasePath="/" currentBranch="main" />,
22
+ )
23
+ const trigger = container.querySelector('button')
24
+ expect(trigger).toBeTruthy()
25
+ expect(trigger.getAttribute('aria-label')).toBe('See branches')
26
+ })
27
+
28
+ it('applies the accent class and re-labels when isBranchOnly is true', () => {
29
+ const { container } = render(
30
+ <BranchesDropdown
31
+ branches={branches}
32
+ branchBasePath="/"
33
+ currentBranch="main"
34
+ isBranchOnly
35
+ />,
36
+ )
37
+ const trigger = container.querySelector('button')
38
+ expect(trigger).toBeTruthy()
39
+ // CSS Modules hash class names — assert by suffix match.
40
+ expect(trigger.className).toMatch(/iconBtnAccent/)
41
+ expect(trigger.getAttribute('aria-label')).toBe('Open on another branch')
42
+ })
43
+
44
+ it('does NOT apply the accent class when isBranchOnly is false', () => {
45
+ const { container } = render(
46
+ <BranchesDropdown branches={branches} branchBasePath="/" currentBranch="main" />,
47
+ )
48
+ const trigger = container.querySelector('button')
49
+ expect(trigger.className).not.toMatch(/iconBtnAccent/)
50
+ })
51
+
52
+ it('returns null when only the current branch has the prototype', () => {
53
+ const { container } = render(
54
+ <BranchesDropdown
55
+ branches={[{ branch: 'main', folder: 'demos', route: '/Signup' }]}
56
+ branchBasePath="/"
57
+ currentBranch="main"
58
+ />,
59
+ )
60
+ expect(container.querySelector('button')).toBeNull()
61
+ })
62
+ })
@@ -530,7 +530,12 @@ function ArtifactCard({ item, basePath, branchBasePath, currentBranch, starred,
530
530
  const dirName = item.id.split(':').slice(1).join(':')
531
531
  const typeLabel = item.type === 'canvas' ? 'Canvas' : item.type === 'prototype' ? 'Prototype' : 'Component'
532
532
  const canEditDelete = !isBranchOnly && (item.type === 'canvas' || item.type === 'prototype')
533
- const cardClass = `${css.card}${item.isPrivate ? ' ' + css.cardPrivate : ''}${isBranchOnly ? ' ' + css.branchOnlyCard : ''}`
533
+ // Cards intentionally look identical regardless of `isBranchOnly`. The
534
+ // only marker that a prototype lives on another branch is now the
535
+ // subtle blue tint on the BranchesDropdown button (see below). The
536
+ // previous full-card .branchOnlyCard background drew the eye too
537
+ // strongly for what is effectively a "hint, not a blocker" state.
538
+ const cardClass = `${css.card}${item.isPrivate ? ' ' + css.cardPrivate : ''}`
534
539
 
535
540
  return (
536
541
  <>
@@ -538,7 +543,6 @@ function ArtifactCard({ item, basePath, branchBasePath, currentBranch, starred,
538
543
  <div className={css.cardHeader}>
539
544
  <div className={css.cardBadgeGroup}>
540
545
  <span className={css.cardBadge}>{getTypeLabel(item.type)}</span>
541
- {isBranchOnly && <span className={css.branchOnlyBadge}>Branch-only</span>}
542
546
  {item.isPrivate && !isBranchOnly && (
543
547
  <Tooltip text={`${typeLabel} not published, only visible to you`} direction="n">
544
548
  <button
@@ -562,6 +566,7 @@ function ArtifactCard({ item, basePath, branchBasePath, currentBranch, starred,
562
566
  currentBranch={currentBranch}
563
567
  homeTitle={item.name}
564
568
  homeFolder={item.folder}
569
+ isBranchOnly={isBranchOnly}
565
570
  />
566
571
  {canEditDelete && (
567
572
  <CardActionsMenu
@@ -650,15 +650,6 @@
650
650
  opacity: 1;
651
651
  }
652
652
 
653
- .branchOnlyCard {
654
- background: var(--color-background-muted, #f6f8fa);
655
- border-color: var(--color-border-muted, #d8dee4);
656
- }
657
-
658
- .branchOnlyCard:hover {
659
- border-color: var(--color-border-accent-muted, #54aeff66);
660
- }
661
-
662
653
  .cardThumb {
663
654
  height: 140px;
664
655
  background: var(--color-background-muted, #f5f5f5);
@@ -680,17 +671,6 @@
680
671
  color: var(--color-foreground-muted, #555);
681
672
  }
682
673
 
683
- .branchOnlyBadge {
684
- padding: 3px 8px;
685
- border-radius: 999px;
686
- font-size: 11px;
687
- font-weight: 700;
688
- text-transform: uppercase;
689
- letter-spacing: 0.4px;
690
- background: var(--color-background-accent-muted, #ddf4ff);
691
- color: var(--color-foreground-accent, #0969da);
692
- }
693
-
694
674
  .cardPrivateBadge {
695
675
  display: inline-flex;
696
676
  align-items: center;
@@ -57,7 +57,7 @@
57
57
  .text > :last-child { margin-bottom: 0; }
58
58
 
59
59
  .text p {
60
- margin: 0 0 0.5em;
60
+ margin: 0 0 0.4em;
61
61
  }
62
62
 
63
63
  .text h1,
@@ -76,13 +76,31 @@
76
76
 
77
77
  .text ul,
78
78
  .text ol {
79
- margin: 0 0 0.5em;
79
+ margin: 0 0 0.4em;
80
80
  padding-left: 1.25em;
81
81
  }
82
82
 
83
83
  .text ul { list-style: disc; }
84
84
  .text ol { list-style: decimal; }
85
- .text li { margin: 0 0 0.15em; }
85
+ .text li { margin: 0 0 0.1em; }
86
+
87
+ /* CommonMark loose-list compatibility: blank lines between list items
88
+ * cause remark to wrap each item's content in <p>. Without these resets
89
+ * the `.text p` margin pushes the bullet's text down and away from its
90
+ * marker, and adds extra vertical gap between items. Mirror of the
91
+ * shared markdownContent.module.css fix — duplicated here because the
92
+ * sticky's `.text p` rule competes with the shared `.contentSmall p`
93
+ * rule at the same specificity, so we need a local override too. */
94
+ .text li > p { margin: 0; }
95
+ .text li > p + p { margin-top: 0.25em; }
96
+
97
+ /* Em-relative <hr> margin so the rule scales with the sticky's
98
+ * autoScaleText (--sticky-text-scale). The shared
99
+ * markdownContent.module.css uses fixed `16px 0`, which produces
100
+ * visually huge gaps once the sticky scales its font-size up. */
101
+ .text hr {
102
+ margin: 0.6em 0;
103
+ }
86
104
 
87
105
  .text a {
88
106
  color: inherit;
@@ -29,4 +29,23 @@ describe('MarkdownView typography', () => {
29
29
  const cls = container.querySelector('div').className
30
30
  expect(cls).toMatch(/inert/)
31
31
  })
32
+
33
+ it('renders loose lists (blank lines between items) with <p>-wrapped items', () => {
34
+ // CommonMark turns a list with blank lines between items into a
35
+ // "loose list" — each <li> contains <p> rather than raw text. The
36
+ // shared markdownContent.module.css resets `li > p` margin so the
37
+ // bullet text stays aligned to its marker; without that the gap
38
+ // between items balloons. This test pins the HTML contract so the
39
+ // CSS reset has a stable shape to target.
40
+ const { container } = render(
41
+ <MarkdownView content={'- first\n\n- second\n\n- third'} />,
42
+ )
43
+ const items = container.querySelectorAll('li')
44
+ expect(items.length).toBe(3)
45
+ for (const li of items) {
46
+ const directParagraphs = Array.from(li.children).filter(c => c.tagName === 'P')
47
+ expect(directParagraphs.length).toBeGreaterThanOrEqual(1)
48
+ }
49
+ })
32
50
  })
51
+
@@ -71,6 +71,13 @@
71
71
  .contentSmall ul { margin: 0 0 8px; padding-left: 20px; list-style-type: disc; }
72
72
  .contentSmall ol { margin: 0 0 8px; padding-left: 20px; list-style-type: decimal; }
73
73
  .contentSmall li { margin: 0 0 2px; display: list-item; }
74
+ /* CommonMark loose-list compatibility: when items have blank lines between
75
+ * them, remark wraps each item's content in <p>. The global `.contentSmall p`
76
+ * margin (8px) would then blow up spacing inside the bullet and visually
77
+ * detach the text from its marker. Reset to zero inside <li>; restore a
78
+ * tighter rhythm only when an <li> contains multiple <p>s. */
79
+ .contentSmall li > p { margin: 0; }
80
+ .contentSmall li > p + p { margin-top: 4px; }
74
81
  .contentSmall input[type="checkbox"] {
75
82
  margin-right: 6px;
76
83
  accent-color: var(--sb--markdown-accent);
@@ -182,6 +189,9 @@
182
189
  .contentLarge ul { margin: 0 0 12px; padding-left: 24px; list-style: disc; }
183
190
  .contentLarge ol { margin: 0 0 12px; padding-left: 24px; list-style: decimal; }
184
191
  .contentLarge li { margin: 0 0 4px; display: list-item; }
192
+ /* CommonMark loose-list compatibility — see .contentSmall comment above. */
193
+ .contentLarge li > p { margin: 0; }
194
+ .contentLarge li > p + p { margin-top: 6px; }
185
195
  .contentLarge input[type="checkbox"] {
186
196
  margin-right: 6px;
187
197
  accent-color: var(--sb--markdown-accent);