@charcuterie/ui 4.9.1 → 4.10.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.
- package/dist/ActionMenu/ActionMenu.d.ts +36 -0
- package/dist/ActionMenu/ActionMenu.d.ts.map +1 -0
- package/dist/ActionMenu/ActionMenu.js +40 -0
- package/dist/ActionMenu/ActionMenu.js.map +1 -0
- package/dist/Breadcrumbs/Breadcrumbs.d.ts +71 -0
- package/dist/Breadcrumbs/Breadcrumbs.d.ts.map +1 -0
- package/dist/Breadcrumbs/Breadcrumbs.js +52 -0
- package/dist/Breadcrumbs/Breadcrumbs.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/ActionMenu/ActionMenu.mdx +51 -0
- package/src/ActionMenu/ActionMenu.stories.tsx +183 -0
- package/src/ActionMenu/ActionMenu.test.tsx +187 -0
- package/src/ActionMenu/ActionMenu.tsx +96 -0
- package/src/Breadcrumbs/Breadcrumbs.mdx +71 -0
- package/src/Breadcrumbs/Breadcrumbs.stories.tsx +319 -0
- package/src/Breadcrumbs/Breadcrumbs.test.tsx +250 -0
- package/src/Breadcrumbs/Breadcrumbs.tsx +179 -0
- package/src/index.ts +7 -0
- package/src/sourceRules.test.ts +3 -1
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { composeStories } from "@storybook/react"
|
|
2
|
+
import { expect, userEvent } from "storybook/test"
|
|
3
|
+
import { test } from "vitest"
|
|
4
|
+
|
|
5
|
+
import { expectNoAxeViolations } from "../expectNoAxeViolations.testHelpers.ts"
|
|
6
|
+
import { mountStory } from "../mountStory.testHelpers.ts"
|
|
7
|
+
import { expectAgentDrivable } from "../testing/index.ts"
|
|
8
|
+
import * as stories from "./Breadcrumbs.stories.tsx"
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
AllStates,
|
|
12
|
+
AllVariants,
|
|
13
|
+
Interactive,
|
|
14
|
+
Playground,
|
|
15
|
+
Responsive,
|
|
16
|
+
Routed,
|
|
17
|
+
} = composeStories(stories)
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The APG breadcrumb pattern: a named `<nav>` landmark around an
|
|
21
|
+
* ordered list, one `listitem` per rung.
|
|
22
|
+
*/
|
|
23
|
+
test("is a named navigation landmark around an ordered list", async () => {
|
|
24
|
+
const { canvas, canvasElement } =
|
|
25
|
+
await mountStory(Playground)
|
|
26
|
+
|
|
27
|
+
const nav = expectAgentDrivable(canvas, {
|
|
28
|
+
name: "Breadcrumb",
|
|
29
|
+
role: "navigation",
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
const list = nav.querySelector("ol")
|
|
33
|
+
|
|
34
|
+
await expect(list).not.toBeNull()
|
|
35
|
+
await expect(
|
|
36
|
+
canvas.getAllByRole("listitem"),
|
|
37
|
+
).toHaveLength(4)
|
|
38
|
+
|
|
39
|
+
await expectNoAxeViolations(canvasElement)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test("every rung but the last is a link; the last is the current page", async () => {
|
|
43
|
+
const { canvas } = await mountStory(Playground)
|
|
44
|
+
|
|
45
|
+
const links = canvas.getAllByRole("link")
|
|
46
|
+
|
|
47
|
+
await expect(
|
|
48
|
+
links.map((link: HTMLElement) => link.textContent),
|
|
49
|
+
).toEqual(["Library", "Handbook", "runbooks"])
|
|
50
|
+
await expect(links[1]).toHaveAttribute(
|
|
51
|
+
"href",
|
|
52
|
+
"/handbook",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const current = canvas.getByText(
|
|
56
|
+
"restoring-a-snapshot.md",
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
await expect(current.tagName).toBe("SPAN")
|
|
60
|
+
await expect(current).toHaveAttribute(
|
|
61
|
+
"aria-current",
|
|
62
|
+
"page",
|
|
63
|
+
)
|
|
64
|
+
// Exactly one current rung.
|
|
65
|
+
await expect(
|
|
66
|
+
canvas
|
|
67
|
+
.getByRole("navigation")
|
|
68
|
+
.querySelectorAll("[aria-current]"),
|
|
69
|
+
).toHaveLength(1)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The Whereabouts shape: the trail sits above a heading that already
|
|
74
|
+
* names the page, so every rung links and nothing claims to be current.
|
|
75
|
+
*/
|
|
76
|
+
test("a trail of links only has no current rung", async () => {
|
|
77
|
+
const { canvas, canvasElement } =
|
|
78
|
+
await mountStory(AllVariants)
|
|
79
|
+
|
|
80
|
+
const nav: HTMLElement = canvas.getByRole("navigation", {
|
|
81
|
+
name: "Full place trail",
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
await expect(nav.querySelectorAll("a")).toHaveLength(3)
|
|
85
|
+
await expect(
|
|
86
|
+
nav.querySelector("[aria-current]"),
|
|
87
|
+
).toBeNull()
|
|
88
|
+
|
|
89
|
+
await expectNoAxeViolations(canvasElement)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("the separator is decorative", async () => {
|
|
93
|
+
const { canvas } = await mountStory(AllVariants)
|
|
94
|
+
|
|
95
|
+
const nav: HTMLElement = canvas.getByRole("navigation", {
|
|
96
|
+
name: "Slash trail",
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
// Three separators between four rungs, all hidden, none before the
|
|
100
|
+
// first rung.
|
|
101
|
+
const separators = nav.querySelectorAll(
|
|
102
|
+
'[aria-hidden="true"]',
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
await expect(separators).toHaveLength(3)
|
|
106
|
+
await expect(
|
|
107
|
+
nav.querySelector("li")?.querySelector("[aria-hidden]"),
|
|
108
|
+
).toBeNull()
|
|
109
|
+
// The slash never reaches a rung's name.
|
|
110
|
+
await expect(
|
|
111
|
+
nav.querySelector('a[href="/handbook"]'),
|
|
112
|
+
).toHaveAccessibleName("Handbook")
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A link rung and a button rung are the same thing to a reader, so
|
|
117
|
+
* they are painted the same: same colour, same weight.
|
|
118
|
+
*/
|
|
119
|
+
test("a button rung is painted as a link rung", async () => {
|
|
120
|
+
const { canvas, canvasElement } =
|
|
121
|
+
await mountStory(AllStates)
|
|
122
|
+
|
|
123
|
+
const link = canvas.getAllByRole("link", {
|
|
124
|
+
name: "Places",
|
|
125
|
+
})[0]
|
|
126
|
+
|
|
127
|
+
const button = canvas.getAllByRole("button", {
|
|
128
|
+
name: "Places",
|
|
129
|
+
})[0]
|
|
130
|
+
|
|
131
|
+
if (link === undefined || button === undefined) {
|
|
132
|
+
throw new Error("Both rungs must render.")
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await expect(getComputedStyle(button).color).toBe(
|
|
136
|
+
getComputedStyle(link).color,
|
|
137
|
+
)
|
|
138
|
+
await expect(getComputedStyle(button).fontWeight).toBe(
|
|
139
|
+
getComputedStyle(link).fontWeight,
|
|
140
|
+
)
|
|
141
|
+
await expect(
|
|
142
|
+
getComputedStyle(button).backgroundColor,
|
|
143
|
+
).toBe("rgba(0, 0, 0, 0)")
|
|
144
|
+
|
|
145
|
+
await expectNoAxeViolations(canvasElement)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Wraps, never truncates: at the narrowest panel every rung's full
|
|
150
|
+
* text is still in the layout, and no rung is wider than the trail.
|
|
151
|
+
*/
|
|
152
|
+
test("a long trail wraps inside a narrow container and hides nothing", async () => {
|
|
153
|
+
const { canvas, canvasElement } =
|
|
154
|
+
await mountStory(Responsive)
|
|
155
|
+
|
|
156
|
+
const nav: HTMLElement = canvas.getByRole("navigation", {
|
|
157
|
+
name: "Trail at 15rem",
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
const navWidth = nav.getBoundingClientRect().width
|
|
161
|
+
|
|
162
|
+
for (const item of Array.from(
|
|
163
|
+
nav.querySelectorAll("li"),
|
|
164
|
+
)) {
|
|
165
|
+
await expect(
|
|
166
|
+
item.getBoundingClientRect().width,
|
|
167
|
+
).toBeLessThanOrEqual(navWidth + 0.5)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const fileName = canvas.getAllByText(
|
|
171
|
+
/a-very-long-file-name/,
|
|
172
|
+
)[0]
|
|
173
|
+
|
|
174
|
+
if (fileName === undefined) {
|
|
175
|
+
throw new Error("The file rung must render.")
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await expect(
|
|
179
|
+
getComputedStyle(fileName).textOverflow,
|
|
180
|
+
).not.toBe("ellipsis")
|
|
181
|
+
await expect(fileName.scrollWidth).toBeLessThanOrEqual(
|
|
182
|
+
fileName.clientWidth + 1,
|
|
183
|
+
)
|
|
184
|
+
// More than one line: the list wrapped.
|
|
185
|
+
await expect(
|
|
186
|
+
nav.querySelector("ol")?.getBoundingClientRect().height,
|
|
187
|
+
).toBeGreaterThan(30)
|
|
188
|
+
|
|
189
|
+
await expectNoAxeViolations(canvasElement)
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
test("an in-app href goes through the injected router", async () => {
|
|
193
|
+
const { canvas } = await mountStory(Routed)
|
|
194
|
+
|
|
195
|
+
await expect(
|
|
196
|
+
canvas.getByRole("link", { name: "Places" }),
|
|
197
|
+
).toHaveAttribute("data-router", "soft")
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Tab walks the rungs in order and skips the current folder, which is
|
|
202
|
+
* text. Enter climbs to a rung; Space does the same on the next one.
|
|
203
|
+
*/
|
|
204
|
+
test("the keyboard walks and climbs the trail", async () => {
|
|
205
|
+
const { canvas, canvasElement } =
|
|
206
|
+
await mountStory(Interactive)
|
|
207
|
+
|
|
208
|
+
await expect(
|
|
209
|
+
canvas
|
|
210
|
+
.getAllByRole("button")
|
|
211
|
+
.map((button: HTMLElement) => button.textContent),
|
|
212
|
+
).toEqual(["G:", "Photos", "2026"])
|
|
213
|
+
|
|
214
|
+
await userEvent.tab()
|
|
215
|
+
await expect(
|
|
216
|
+
canvas.getByRole("button", { name: "G:" }),
|
|
217
|
+
).toHaveFocus()
|
|
218
|
+
|
|
219
|
+
await userEvent.tab()
|
|
220
|
+
await userEvent.tab()
|
|
221
|
+
await expect(
|
|
222
|
+
canvas.getByRole("button", { name: "2026" }),
|
|
223
|
+
).toHaveFocus()
|
|
224
|
+
|
|
225
|
+
await userEvent.keyboard("{Enter}")
|
|
226
|
+
|
|
227
|
+
await expect(
|
|
228
|
+
canvas.getByText("2026").closest("[aria-current]"),
|
|
229
|
+
).toHaveAttribute("aria-current", "page")
|
|
230
|
+
await expect(
|
|
231
|
+
canvas.queryByText("Garden", { selector: "li *" }),
|
|
232
|
+
).toBeNull()
|
|
233
|
+
|
|
234
|
+
// The pressed rung became text and left the tab order, so Tab
|
|
235
|
+
// starts again from the top.
|
|
236
|
+
await userEvent.tab()
|
|
237
|
+
await userEvent.tab()
|
|
238
|
+
await expect(
|
|
239
|
+
canvas.getByRole("button", { name: "Photos" }),
|
|
240
|
+
).toHaveFocus()
|
|
241
|
+
|
|
242
|
+
await userEvent.keyboard(" ")
|
|
243
|
+
|
|
244
|
+
await expect(canvas.getByText("Photos")).toHaveAttribute(
|
|
245
|
+
"aria-current",
|
|
246
|
+
"page",
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
await expectNoAxeViolations(canvasElement)
|
|
250
|
+
})
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import type { Key, ReactNode } from "react"
|
|
2
|
+
|
|
3
|
+
import { FOCUS_RING_CLASS } from "../intentStyles.ts"
|
|
4
|
+
import { TextLink } from "../TextLink/TextLink.tsx"
|
|
5
|
+
import { toClassName } from "../toClassName.ts"
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* One rung of the trail.
|
|
9
|
+
*
|
|
10
|
+
* Which of the three it becomes is read off the fields it carries, so
|
|
11
|
+
* a caller mapping a path to items never has to say what the rung
|
|
12
|
+
* *is* — only where it goes:
|
|
13
|
+
*
|
|
14
|
+
* - `href` — a real `<a href>`, through `TextLink`, so an in-app path
|
|
15
|
+
* goes through the injected router exactly as every other link in
|
|
16
|
+
* the library does.
|
|
17
|
+
* - `onSelect` and no `href` — a `<button>` painted as the same link.
|
|
18
|
+
* For a trail over state that has no URL: image-viewer's folder
|
|
19
|
+
* path and mux-magic's file-explorer modal both move a browser
|
|
20
|
+
* around a disk without touching the address bar.
|
|
21
|
+
* - neither — plain text. On the **last** item that is the current
|
|
22
|
+
* page, and it carries `aria-current="page"`.
|
|
23
|
+
*/
|
|
24
|
+
export type BreadcrumbItem = {
|
|
25
|
+
href?: string
|
|
26
|
+
/**
|
|
27
|
+
* React's list key. Optional, because `label` is a node and cannot
|
|
28
|
+
* be one; the fallback is `href`, then the position. Pass it when
|
|
29
|
+
* two rungs can share an `href` or the trail reorders.
|
|
30
|
+
*/
|
|
31
|
+
key?: Key
|
|
32
|
+
label: ReactNode
|
|
33
|
+
/**
|
|
34
|
+
* Runs on activation. With an `href` it runs alongside the
|
|
35
|
+
* navigation (closing a drawer, say); without one it IS the
|
|
36
|
+
* navigation.
|
|
37
|
+
*/
|
|
38
|
+
onSelect?: () => void
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type BreadcrumbsProps = {
|
|
42
|
+
className?: string
|
|
43
|
+
items: readonly BreadcrumbItem[]
|
|
44
|
+
/**
|
|
45
|
+
* The `<nav>` landmark's name. Two trails on one page need two
|
|
46
|
+
* different names, or axe's `landmark-unique` fails.
|
|
47
|
+
*/
|
|
48
|
+
label?: string
|
|
49
|
+
/**
|
|
50
|
+
* Drawn between rungs, always `aria-hidden`. The default is a
|
|
51
|
+
* chevron drawn in CSS borders rather than a `›` character: the
|
|
52
|
+
* library ships no symbol glyphs in a default, because a font that
|
|
53
|
+
* lacks one paints nothing
|
|
54
|
+
* (`docs/decisions/2026-07-29-ship-no-icons-and-no-symbol-glyphs.md`).
|
|
55
|
+
* Folio's `/` is a fine override — it is the app's choice to make.
|
|
56
|
+
*/
|
|
57
|
+
separator?: ReactNode
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The link paint without the `<a>`: `TextLink`'s standalone look,
|
|
62
|
+
* restated for the `onSelect` rung so a button crumb and a link crumb
|
|
63
|
+
* are indistinguishable at rest, on hover, and under focus.
|
|
64
|
+
*/
|
|
65
|
+
const BUTTON_CRUMB_CLASS =
|
|
66
|
+
"inline cursor-pointer rounded-xs border-0 bg-transparent p-0 text-start font-medium text-intent-accent-content underline-offset-2 hover:underline"
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* `wrap-anywhere` on every label, and no `truncate`: a trail that cuts
|
|
70
|
+
* a folder name to `Office Sh…` hides the one word the reader came to
|
|
71
|
+
* find. A long unbroken segment — a hash, a file name — breaks inside
|
|
72
|
+
* itself instead of pushing the row wider than its container.
|
|
73
|
+
*/
|
|
74
|
+
const LABEL_CLASS = "min-w-0 wrap-anywhere"
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Right-pointing in a left-to-right page, left-pointing in a
|
|
78
|
+
* right-to-left one: the two borders are logical (`border-e`), and the
|
|
79
|
+
* rotation flips with the direction so the corner always points the
|
|
80
|
+
* way the trail reads.
|
|
81
|
+
*/
|
|
82
|
+
const DEFAULT_SEPARATOR = (
|
|
83
|
+
<span className="inline-block size-1.5 shrink-0 rotate-45 border-e-[1.5px] border-t-[1.5px] border-current rtl:-rotate-45" />
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
const getItemKey = (
|
|
87
|
+
item: BreadcrumbItem,
|
|
88
|
+
index: number,
|
|
89
|
+
): Key => item.key ?? item.href ?? index
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* A path back up a hierarchy: `Places › Office › Office Shelves`.
|
|
93
|
+
*
|
|
94
|
+
* Folio, image-viewer and mux-magic each hand-rolled one, and they
|
|
95
|
+
* disagreed on everything a shared shape should settle — one was a
|
|
96
|
+
* `<nav>` of bare links with no list, two were `<div>`s of buttons
|
|
97
|
+
* with no landmark, one truncated the current segment, one gave the
|
|
98
|
+
* current folder no `aria-current`. This is the WAI-ARIA APG
|
|
99
|
+
* breadcrumb pattern: a `<nav>` landmark around an `<ol>`, because
|
|
100
|
+
* the order is the meaning.
|
|
101
|
+
*
|
|
102
|
+
* The current page is the **last item with nowhere to go**. A trail
|
|
103
|
+
* that sits above a heading already naming the page — Whereabouts'
|
|
104
|
+
* `Places › Office` over an `Office Shelves` heading — gives every
|
|
105
|
+
* item an `href`, and then no rung is current, which is correct: the
|
|
106
|
+
* page is named once, by the heading.
|
|
107
|
+
*/
|
|
108
|
+
export const Breadcrumbs = ({
|
|
109
|
+
className,
|
|
110
|
+
items,
|
|
111
|
+
label = "Breadcrumb",
|
|
112
|
+
separator,
|
|
113
|
+
}: BreadcrumbsProps): ReactNode => (
|
|
114
|
+
<nav aria-label={label} className={className}>
|
|
115
|
+
<ol className="m-0 flex list-none flex-wrap items-center gap-x-2 gap-y-1 p-0 text-content-secondary text-sm">
|
|
116
|
+
{items.map((item, index) => {
|
|
117
|
+
const isLast = index === items.length - 1
|
|
118
|
+
|
|
119
|
+
const isCurrent =
|
|
120
|
+
isLast &&
|
|
121
|
+
item.href === undefined &&
|
|
122
|
+
item.onSelect === undefined
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<li
|
|
126
|
+
className="inline-flex min-w-0 items-center gap-x-2"
|
|
127
|
+
key={getItemKey(item, index)}
|
|
128
|
+
>
|
|
129
|
+
{index > 0 ? (
|
|
130
|
+
<span
|
|
131
|
+
aria-hidden="true"
|
|
132
|
+
className="inline-flex shrink-0 items-center text-content-muted"
|
|
133
|
+
>
|
|
134
|
+
{separator ?? DEFAULT_SEPARATOR}
|
|
135
|
+
</span>
|
|
136
|
+
) : null}
|
|
137
|
+
|
|
138
|
+
{item.href !== undefined ? (
|
|
139
|
+
<TextLink
|
|
140
|
+
appearance="standalone"
|
|
141
|
+
className={LABEL_CLASS}
|
|
142
|
+
href={item.href}
|
|
143
|
+
onClick={item.onSelect}
|
|
144
|
+
>
|
|
145
|
+
{item.label}
|
|
146
|
+
</TextLink>
|
|
147
|
+
) : item.onSelect !== undefined ? (
|
|
148
|
+
<button
|
|
149
|
+
className={toClassName(
|
|
150
|
+
BUTTON_CRUMB_CLASS,
|
|
151
|
+
FOCUS_RING_CLASS,
|
|
152
|
+
LABEL_CLASS,
|
|
153
|
+
)}
|
|
154
|
+
onClick={item.onSelect}
|
|
155
|
+
type="button"
|
|
156
|
+
>
|
|
157
|
+
{item.label}
|
|
158
|
+
</button>
|
|
159
|
+
) : (
|
|
160
|
+
<span
|
|
161
|
+
aria-current={
|
|
162
|
+
isCurrent ? "page" : undefined
|
|
163
|
+
}
|
|
164
|
+
className={toClassName(
|
|
165
|
+
LABEL_CLASS,
|
|
166
|
+
isCurrent
|
|
167
|
+
? "font-medium text-content-primary"
|
|
168
|
+
: undefined,
|
|
169
|
+
)}
|
|
170
|
+
>
|
|
171
|
+
{item.label}
|
|
172
|
+
</span>
|
|
173
|
+
)}
|
|
174
|
+
</li>
|
|
175
|
+
)
|
|
176
|
+
})}
|
|
177
|
+
</ol>
|
|
178
|
+
</nav>
|
|
179
|
+
)
|
package/src/index.ts
CHANGED
|
@@ -37,6 +37,8 @@ export type {
|
|
|
37
37
|
AccordionProps,
|
|
38
38
|
} from "./Accordion/Accordion.tsx"
|
|
39
39
|
export { Accordion } from "./Accordion/Accordion.tsx"
|
|
40
|
+
export type { ActionMenuProps } from "./ActionMenu/ActionMenu.tsx"
|
|
41
|
+
export { ActionMenu } from "./ActionMenu/ActionMenu.tsx"
|
|
40
42
|
export type {
|
|
41
43
|
ActionTileItem,
|
|
42
44
|
ActionTilesAccent,
|
|
@@ -99,6 +101,11 @@ export {
|
|
|
99
101
|
getIsMoveMeaningful,
|
|
100
102
|
toSettledIndex,
|
|
101
103
|
} from "./Board/boardMove.ts"
|
|
104
|
+
export type {
|
|
105
|
+
BreadcrumbItem,
|
|
106
|
+
BreadcrumbsProps,
|
|
107
|
+
} from "./Breadcrumbs/Breadcrumbs.tsx"
|
|
108
|
+
export { Breadcrumbs } from "./Breadcrumbs/Breadcrumbs.tsx"
|
|
102
109
|
export type { ButtonProps } from "./Button/Button.tsx"
|
|
103
110
|
export { Button } from "./Button/Button.tsx"
|
|
104
111
|
export type { ButtonLinkProps } from "./ButtonLink/ButtonLink.tsx"
|
package/src/sourceRules.test.ts
CHANGED
|
@@ -615,7 +615,9 @@ test("the barrel is the only place components are re-exported", async () => {
|
|
|
615
615
|
// question the same way the bars are placed, or the two answers
|
|
616
616
|
// drift by a day and nothing says so.
|
|
617
617
|
// ProgressCard composes Card and ProgressBar for progress-first dashboards.
|
|
618
|
-
|
|
618
|
+
// Breadcrumbs composes TextLink for the path back up a hierarchy.
|
|
619
|
+
// ActionMenu composes IconButton and Menu for the overflow control.
|
|
620
|
+
expect(componentNames.length).toBe(69)
|
|
619
621
|
|
|
620
622
|
for (const name of componentNames) {
|
|
621
623
|
expect(barrel).toContain(`export { ${name} }`)
|