Skip to content

Commit e29e99b

Browse files
frankieyanclaude
andauthored
docs(storybook): fix crash when Reactist tooltip is triggered in the Storybook frame (#1080)
* docs(storybook): rework figma & badges toolbar with manager-native components Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): stop flagging non-visual pages as missing a Figma link Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): consolidate Modal docs into a single entry Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): remove horizontal margin from toolbar badge group Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): drop BadgeGroup wrapper in favor of an inline container Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): document the figma & badges toolbar tools Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): map storybook/internal/components in the React 18 tsconfig Co-Authored-By: Claude <noreply@anthropic.com> * docs(storybook): scaffold without a placeholder Figma link, default badge to notAccessible Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 061150d commit e29e99b

21 files changed

Lines changed: 272 additions & 105 deletions

.plop/templates/component/component.stories.tsx.hbs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,26 @@ const meta = {
77
title: 'Design system/{{pascalCase name}}',
88
component: {{pascalCase name}},
99
parameters: {
10-
badges: ['accessible'],
11-
// Link a Figma design to show it in the toolbar, or set `figma: false`
12-
// to hide the tool on pages with no matching design (e.g. hook docs):
13-
// figma: { path: 'Figma file › Page › Section › Component', url: 'https://www.figma.com/design/…?node-id=…' },
10+
/**
11+
* Storybook toolbar badges
12+
* Used to highlight the web accessibility standards the component meets or
13+
* whether the component is deprecated.
14+
* Possible values: `accessible`, `partiallyAccessible`, `notAccessible`, `deprecated`
15+
* @see .storybook/badges/README.md
16+
*/
17+
badges: ['notAccessible'],
18+
19+
/**
20+
* Figma design link
21+
* Used to link to the component's design in Figma. Until you add one, the toolbar shows a
22+
* "No Figma link" reminder; for non-visual components set `figma: FIGMA_NOT_NEEDED` (imported
23+
* from `.storybook/figma/constants`).
24+
* @see .storybook/figma/README.md
25+
*/
26+
figma: {
27+
// path: 'Figma file › Page › Section › Component',
28+
// url: 'https://www.figma.com/design/…?node-id=…',
29+
},
1430
},
1531
} satisfies Meta<typeof {{pascalCase name}}>
1632

.storybook/badges/README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Badges toolbar tool
2+
3+
Allows adding a toolbar badge (or badges) for the current story, used to flag things viewers should know at a glance, e.g. a component's accessibility status or deprecation.
4+
5+
## Usage
6+
7+
Set the `badges` parameter on the component's CSF `meta`:
8+
9+
```ts
10+
const meta = {
11+
title: '📝 Form/CheckboxField',
12+
component: CheckboxField,
13+
parameters: {
14+
badges: ['accessible'],
15+
},
16+
}
17+
```
18+
19+
You can list more than one key:
20+
21+
```ts
22+
badges: ['partiallyAccessible', 'deprecated']
23+
```
24+
25+
### MDX docs
26+
27+
Badges come from the story parameters, so an MDX docs page inherits them when it is **attached** to a stories file via `<Meta of={ComponentStories} />`. Storybook will **drop** parameters set directly on a standalone `<Meta title="…" />` (i.e. no `of`).
28+
29+
## Available badges
30+
31+
| Key | Label | Tone |
32+
| --------------------- | --------------------------- | ---------------- |
33+
| `accessible` | ✔ Accessible (WCAG 2.0 AA) | positive (green) |
34+
| `partiallyAccessible` | ⚠ Partially Accessible | warning (orange) |
35+
| `notAccessible` | ✖ Not accessible | attention (red) |
36+
| `deprecated` | ✖ Deprecated | attention (red) |

.storybook/badges/badges-tool.tsx

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,16 @@ import * as React from 'react'
22

33
import { useParameter } from 'storybook/manager-api'
44

5-
import { Badge } from '../../src/badge/badge'
6-
import { Inline } from '../../src/inline'
5+
import { Badge } from '../components/badge'
76

87
import { resolveBadges } from './badges'
9-
import { BADGES_CONFIG_PARAMETER, BADGES_PARAMETER } from './constants'
8+
import { BADGES_CONFIG_PARAMETER, BADGES_PARAMETER, TOOL_ID } from './constants'
9+
10+
const containerStyle: React.CSSProperties = {
11+
alignItems: 'center',
12+
display: 'inline-flex',
13+
gap: '4px',
14+
}
1015

1116
const emptyBadges: unknown[] = []
1217
const emptyBadgesConfig: Record<string, unknown> = {}
@@ -25,10 +30,15 @@ export const BadgesTool = React.memo(function BadgesTool() {
2530
}
2631

2732
return (
28-
<Inline space="xsmall">
29-
{badges.map(({ id, title, tone }) => (
30-
<Badge key={id} tone={tone} label={title} />
33+
<div
34+
key={TOOL_ID}
35+
aria-label="Story badges"
36+
style={containerStyle}
37+
title={badges.map(({ title }) => title).join(', ')}
38+
>
39+
{badges.map(({ id, title, styles }, index) => (
40+
<Badge key={`${id}-${index}`} label={title} styles={styles} />
3141
))}
32-
</Inline>
42+
</div>
3343
)
3444
})

.storybook/badges/badges.test.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,46 +6,42 @@ describe('resolveBadges', () => {
66
resolveBadges(['accessible', 'deprecated'], {
77
accessible: {
88
title: 'Accessible',
9-
tone: 'positive',
9+
styles: { color: 'green' },
1010
},
1111
deprecated: {
1212
title: 'Deprecated',
13-
tone: 'attention',
13+
styles: { color: 'red' },
1414
},
1515
}),
1616
).toEqual([
1717
{
1818
id: 'accessible',
1919
title: 'Accessible',
20-
tone: 'positive',
20+
styles: { color: 'green' },
2121
},
2222
{
2323
id: 'deprecated',
2424
title: 'Deprecated',
25-
tone: 'attention',
25+
styles: { color: 'red' },
2626
},
2727
])
2828
})
2929

3030
it('filters unknown badges and invalid badge entries', () => {
3131
expect(
32-
resolveBadges(['accessible', 'missing', 42, 'untitled', 'untoned'], {
32+
resolveBadges(['accessible', 'missing', 42, 'untitled'], {
3333
accessible: {
3434
title: 'Accessible',
35-
tone: 'positive',
3635
},
3736
untitled: {
38-
tone: 'positive',
39-
},
40-
untoned: {
41-
title: 'Untoned',
37+
styles: { color: 'orange' },
4238
},
4339
}),
4440
).toEqual([
4541
{
4642
id: 'accessible',
4743
title: 'Accessible',
48-
tone: 'positive',
44+
styles: undefined,
4945
},
5046
])
5147
})
@@ -57,14 +53,20 @@ describe('resolveBadges', () => {
5753
expect(resolveBadges(['accessible'], [])).toEqual([])
5854
})
5955

60-
it('rejects an unknown tone', () => {
56+
it('ignores non-object styles', () => {
6157
expect(
6258
resolveBadges(['accessible'], {
6359
accessible: {
6460
title: 'Accessible',
65-
tone: 'rainbow',
61+
styles: 'color: green',
6662
},
6763
}),
68-
).toEqual([])
64+
).toEqual([
65+
{
66+
id: 'accessible',
67+
title: 'Accessible',
68+
styles: undefined,
69+
},
70+
])
6971
})
7072
})

.storybook/badges/badges.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,28 @@
1-
import type { BadgeProps } from '../../src/badge/badge'
1+
import type { CSSProperties } from 'react'
22

3-
type BadgeTone = BadgeProps['tone']
3+
type UnknownRecord = Record<string, unknown>
4+
5+
type BadgeConfig = {
6+
title?: unknown
7+
styles?: unknown
8+
}
49

510
type ResolvedBadge = {
611
id: string
712
title: string
8-
tone: BadgeTone
13+
styles: CSSProperties | undefined
914
}
1015

11-
// Ensure valid tones are checked against the Badge's tone prop
12-
const TONE_SET: Record<BadgeTone, true> = {
13-
info: true,
14-
positive: true,
15-
promote: true,
16-
attention: true,
17-
warning: true,
16+
function isRecord(value: unknown): value is UnknownRecord {
17+
return typeof value === 'object' && value !== null && !Array.isArray(value)
1818
}
1919

20-
const VALID_TONES = Object.keys(TONE_SET) as BadgeTone[]
21-
22-
function isRecord(value: unknown): value is Record<string, unknown> {
23-
return typeof value === 'object' && value !== null && !Array.isArray(value)
20+
function isBadgeConfig(value: unknown): value is BadgeConfig {
21+
return isRecord(value)
2422
}
2523

26-
function isBadgeTone(value: unknown): value is BadgeTone {
27-
return typeof value === 'string' && (VALID_TONES as readonly string[]).includes(value)
24+
function resolveStyles(styles: unknown): CSSProperties | undefined {
25+
return isRecord(styles) ? (styles as CSSProperties) : undefined
2826
}
2927

3028
function resolveBadges(badges: unknown, badgesConfig: unknown): ResolvedBadge[] {
@@ -39,19 +37,15 @@ function resolveBadges(badges: unknown, badgesConfig: unknown): ResolvedBadge[]
3937

4038
const badgeConfig = badgesConfig[badge]
4139

42-
if (!isRecord(badgeConfig) || typeof badgeConfig.title !== 'string') {
43-
return []
44-
}
45-
46-
if (!isBadgeTone(badgeConfig.tone)) {
40+
if (!isBadgeConfig(badgeConfig) || typeof badgeConfig.title !== 'string') {
4741
return []
4842
}
4943

5044
return [
5145
{
5246
id: badge,
5347
title: badgeConfig.title,
54-
tone: badgeConfig.tone,
48+
styles: resolveStyles(badgeConfig.styles),
5549
},
5650
]
5751
})
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { CSSProperties } from 'react'
2+
3+
/**
4+
* Toolbar badge colors mirroring the Reactist Badge tones (see `src/badge/badge.module.css`)
5+
*/
6+
const reactistBadgeTones: Record<string, CSSProperties> = {
7+
info: { color: '#666666', backgroundColor: '#eeeeee', borderColor: '#666666' },
8+
positive: { color: '#058527', backgroundColor: '#e0f0e3', borderColor: '#058527' },
9+
promote: { color: '#8f4700', backgroundColor: '#faead1', borderColor: '#8f4700' },
10+
attention: { color: '#cf473a', backgroundColor: '#f9e3e2', borderColor: '#cf473a' },
11+
warning: { color: '#ffffff', backgroundColor: '#eb8909', borderColor: '#eb8909' },
12+
}
13+
14+
export { reactistBadgeTones }

.storybook/components/badge.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import * as React from 'react'
2+
3+
const badgeStyle: React.CSSProperties = {
4+
alignItems: 'center',
5+
border: '1px solid currentColor',
6+
borderRadius: '3px',
7+
boxSizing: 'border-box',
8+
display: 'inline-flex',
9+
fontSize: '12px',
10+
fontWeight: 600,
11+
lineHeight: '14px',
12+
minHeight: '20px',
13+
padding: '2px 6px',
14+
whiteSpace: 'nowrap',
15+
}
16+
17+
interface BadgeProps {
18+
label: string
19+
styles?: React.CSSProperties
20+
}
21+
22+
/** A toolbar badge used for the Storybook frame */
23+
function Badge({ label, styles }: BadgeProps) {
24+
return <span style={{ ...badgeStyle, ...styles }}>{label}</span>
25+
}
26+
27+
export { Badge }
28+
export type { BadgeProps }

.storybook/figma/README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
# Figma toolbar tool
2+
3+
Allows adding a toolbar link from the current story to its Figma design, so anyone can jump to the source of truth at a glance. When no design is linked, a neutral "No Figma link" hint shows as a reminder to add one.
4+
5+
## Usage
6+
7+
Set the `figma` parameter on the component's CSF `meta`. Provide the design's `url`, plus an optional `path` breadcrumb shown as the link's tooltip:
8+
9+
```tsx
10+
const meta = {
11+
title: '📊 Data display/Avatar',
12+
component: Avatar,
13+
parameters: {
14+
figma: {
15+
url: 'https://www.figma.com/design/…?node-id=123-456',
16+
path: 'Web › Components / Todoist › Avatar',
17+
},
18+
},
19+
}
20+
```
21+
22+
Link several frames at once by passing an array:
23+
24+
```tsx
25+
figma: [
26+
{ url: 'https://www.figma.com/design/…?node-id=1-1', path: 'Default' },
27+
{ url: 'https://www.figma.com/design/…?node-id=1-2', path: 'Compact' },
28+
]
29+
```
30+
31+
### MDX docs
32+
33+
A Figma link comes from the story parameters, so an MDX docs page inherits it when it is **attached** to a stories file via `<Meta of={ComponentStories} />`. Storybook will **drop** parameters set directly on a standalone `<Meta title="…" />` (i.e. no `of`). Standalone documentation pages not attached to stories (e.g. the intro and Tips & tricks pages) will never show the "No Figma link" hint.
34+
35+
## Opting out
36+
37+
Non-visual entries (behaviour utilities, hooks) have no Figma design and shouldn't be flagged. Mark them with `FIGMA_NOT_NEEDED` to hide the hint:
38+
39+
```tsx
40+
import { FIGMA_NOT_NEEDED } from '../../.storybook/figma/constants'
41+
42+
const meta = {
43+
title: '⚙️ Utility/KeyCapturer',
44+
component: KeyCapturer,
45+
parameters: { figma: FIGMA_NOT_NEEDED },
46+
}
47+
```

.storybook/figma/constants.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
11
export const ADDON_ID = 'reactist/figma'
22
export const TOOL_ID = `${ADDON_ID}/tool`
33
export const FIGMA_PARAMETER = 'figma'
4+
5+
/**
6+
* Constant to be used with the `figma` parameter to mark a story as intentionally having no Figma design,
7+
* hiding the "No Figma link" hint. Use this for non-visual entries like behavior utilities or hooks, as well
8+
* as non-component-specific or non-visual documentation.
9+
*
10+
* Requires non-falsey values because Storybook's manager coerces them to `undefined`.
11+
*/
12+
export const FIGMA_NOT_NEEDED = 'not-needed'

0 commit comments

Comments
 (0)