@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 +1 -1
- package/src/internals/BranchesDropdown/BranchesDropdown.jsx +16 -5
- package/src/internals/BranchesDropdown/BranchesDropdown.module.css +16 -0
- package/src/internals/BranchesDropdown/BranchesDropdown.test.jsx +62 -0
- package/src/internals/Viewfinder.jsx +7 -2
- package/src/internals/Viewfinder.module.css +0 -20
- package/src/internals/canvas/widgets/StickyNote.module.css +21 -3
- package/src/internals/canvas/widgets/markdown/MarkdownView.test.jsx +19 -0
- package/src/internals/canvas/widgets/markdown/markdownContent.module.css +10 -0
package/package.json
CHANGED
|
@@ -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={
|
|
40
|
+
className={btnClass}
|
|
32
41
|
onClick={(e) => { e.preventDefault(); e.stopPropagation() }}
|
|
33
|
-
aria-label=
|
|
34
|
-
title=
|
|
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}>
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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);
|