@duro-app/ui 5.4.0 → 5.6.0

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.
Files changed (147) hide show
  1. package/dist/{Table-Cm5cCnZD.js → Table-B1sWL3GA.js} +668 -650
  2. package/dist/Table-B1sWL3GA.js.map +1 -0
  3. package/dist/components/Breadcrumb/Breadcrumb.d.ts +37 -0
  4. package/dist/components/Breadcrumb/Breadcrumb.d.ts.map +1 -0
  5. package/dist/components/Breadcrumb/Breadcrumb.meta.d.ts +3 -0
  6. package/dist/components/Breadcrumb/Breadcrumb.meta.d.ts.map +1 -0
  7. package/dist/components/Breadcrumb/Breadcrumb.stories.d.ts +18 -0
  8. package/dist/components/Breadcrumb/Breadcrumb.stories.d.ts.map +1 -0
  9. package/dist/components/Breadcrumb/styles.css.d.ts +46 -0
  10. package/dist/components/Breadcrumb/styles.css.d.ts.map +1 -0
  11. package/dist/components/Button/Button.d.ts +2 -1
  12. package/dist/components/Button/Button.d.ts.map +1 -1
  13. package/dist/components/Button/styles.css.d.ts +1 -0
  14. package/dist/components/Button/styles.css.d.ts.map +1 -1
  15. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +14 -0
  16. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
  17. package/dist/components/Inline/Inline.meta.d.ts.map +1 -1
  18. package/dist/components/LinkButton/LinkButton.d.ts +8 -1
  19. package/dist/components/LinkButton/LinkButton.d.ts.map +1 -1
  20. package/dist/components/LinkButton/LinkButton.stories.d.ts +9 -0
  21. package/dist/components/LinkButton/LinkButton.stories.d.ts.map +1 -1
  22. package/dist/components/LinkButton/styles.css.d.ts +1 -0
  23. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  24. package/dist/components/Menu/Menu.d.ts +4 -1
  25. package/dist/components/Menu/Menu.d.ts.map +1 -1
  26. package/dist/components/Menu/styles.css.d.ts +14 -0
  27. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  28. package/dist/components/PageNav/PageNav.d.ts +30 -0
  29. package/dist/components/PageNav/PageNav.d.ts.map +1 -0
  30. package/dist/components/PageNav/PageNav.meta.d.ts +3 -0
  31. package/dist/components/PageNav/PageNav.meta.d.ts.map +1 -0
  32. package/dist/components/PageNav/PageNav.stories.d.ts +16 -0
  33. package/dist/components/PageNav/PageNav.stories.d.ts.map +1 -0
  34. package/dist/components/PageNav/styles.css.d.ts +51 -0
  35. package/dist/components/PageNav/styles.css.d.ts.map +1 -0
  36. package/dist/components/Popover/Popover.d.ts +14 -2
  37. package/dist/components/Popover/Popover.d.ts.map +1 -1
  38. package/dist/components/Popover/Popover.meta.d.ts.map +1 -1
  39. package/dist/components/Popover/PopoverLayerContext.d.ts +3 -0
  40. package/dist/components/Popover/PopoverLayerContext.d.ts.map +1 -1
  41. package/dist/components/Popover/PopoverRaised.stories.d.ts +28 -0
  42. package/dist/components/Popover/PopoverRaised.stories.d.ts.map +1 -0
  43. package/dist/components/Popover/styles.css.d.ts +15 -0
  44. package/dist/components/Popover/styles.css.d.ts.map +1 -1
  45. package/dist/components/Select/Select.d.ts +4 -1
  46. package/dist/components/Select/Select.d.ts.map +1 -1
  47. package/dist/components/Select/styles.css.d.ts +9 -0
  48. package/dist/components/Select/styles.css.d.ts.map +1 -1
  49. package/dist/components/TableOfContents/TableOfContents.d.ts +53 -0
  50. package/dist/components/TableOfContents/TableOfContents.d.ts.map +1 -0
  51. package/dist/components/TableOfContents/TableOfContents.meta.d.ts +3 -0
  52. package/dist/components/TableOfContents/TableOfContents.meta.d.ts.map +1 -0
  53. package/dist/components/TableOfContents/TableOfContents.stories.d.ts +21 -0
  54. package/dist/components/TableOfContents/TableOfContents.stories.d.ts.map +1 -0
  55. package/dist/components/TableOfContents/TableOfContentsContext.d.ts +11 -0
  56. package/dist/components/TableOfContents/TableOfContentsContext.d.ts.map +1 -0
  57. package/dist/components/TableOfContents/styles.css.d.ts +114 -0
  58. package/dist/components/TableOfContents/styles.css.d.ts.map +1 -0
  59. package/dist/components/TextLink/TextLink.d.ts +8 -1
  60. package/dist/components/TextLink/TextLink.d.ts.map +1 -1
  61. package/dist/components/TextLink/TextLink.stories.d.ts +9 -0
  62. package/dist/components/TextLink/TextLink.stories.d.ts.map +1 -1
  63. package/dist/components/Toggle/Toggle.d.ts +2 -1
  64. package/dist/components/Toggle/Toggle.d.ts.map +1 -1
  65. package/dist/components/Toggle/styles.css.d.ts +1 -0
  66. package/dist/components/Toggle/styles.css.d.ts.map +1 -1
  67. package/dist/components/Tree/Tree.d.ts +13 -1
  68. package/dist/components/Tree/Tree.d.ts.map +1 -1
  69. package/dist/components/Tree/Tree.meta.d.ts.map +1 -1
  70. package/dist/components/Tree/Tree.stories.d.ts +9 -0
  71. package/dist/components/Tree/Tree.stories.d.ts.map +1 -1
  72. package/dist/components/Tree/styles.css.d.ts +5 -0
  73. package/dist/components/Tree/styles.css.d.ts.map +1 -1
  74. package/dist/docs/clickLink.d.ts +4 -0
  75. package/dist/docs/clickLink.d.ts.map +1 -0
  76. package/dist/docs/coarsePointer.d.ts +15 -0
  77. package/dist/docs/coarsePointer.d.ts.map +1 -0
  78. package/dist/docs/themedSurface.d.ts +8 -0
  79. package/dist/docs/themedSurface.d.ts.map +1 -0
  80. package/dist/index.css +1 -1
  81. package/dist/index.d.ts +6 -0
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +2718 -2212
  84. package/dist/index.js.map +1 -1
  85. package/dist/shared/ModalContext.d.ts +7 -0
  86. package/dist/shared/ModalContext.d.ts.map +1 -1
  87. package/dist/shared/exports.typecheck.d.ts +9 -0
  88. package/dist/shared/exports.typecheck.d.ts.map +1 -0
  89. package/dist/shared/navigate.d.ts +25 -0
  90. package/dist/shared/navigate.d.ts.map +1 -0
  91. package/dist/shared/types.d.ts +2 -0
  92. package/dist/shared/types.d.ts.map +1 -1
  93. package/dist/table.js +1 -1
  94. package/package.json +2 -2
  95. package/src/components/Breadcrumb/Breadcrumb.meta.ts +37 -0
  96. package/src/components/Breadcrumb/Breadcrumb.stories.tsx +110 -0
  97. package/src/components/Breadcrumb/Breadcrumb.tsx +95 -0
  98. package/src/components/Breadcrumb/styles.css.ts +76 -0
  99. package/src/components/Button/Button.tsx +2 -1
  100. package/src/components/Button/styles.css.ts +3 -0
  101. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +172 -14
  102. package/src/components/Inline/Inline.meta.ts +2 -1
  103. package/src/components/LinkButton/LinkButton.meta.ts +1 -1
  104. package/src/components/LinkButton/LinkButton.stories.tsx +42 -1
  105. package/src/components/LinkButton/LinkButton.tsx +9 -0
  106. package/src/components/LinkButton/styles.css.ts +4 -0
  107. package/src/components/Menu/Menu.meta.ts +1 -1
  108. package/src/components/Menu/Menu.tsx +17 -2
  109. package/src/components/Menu/styles.css.ts +18 -0
  110. package/src/components/PageNav/PageNav.meta.ts +35 -0
  111. package/src/components/PageNav/PageNav.stories.tsx +146 -0
  112. package/src/components/PageNav/PageNav.tsx +93 -0
  113. package/src/components/PageNav/styles.css.ts +100 -0
  114. package/src/components/Popover/Popover.meta.ts +2 -1
  115. package/src/components/Popover/Popover.tsx +29 -4
  116. package/src/components/Popover/PopoverLayerContext.ts +3 -0
  117. package/src/components/Popover/PopoverRaised.stories.tsx +334 -0
  118. package/src/components/Popover/styles.css.ts +21 -0
  119. package/src/components/Select/Select.meta.ts +1 -1
  120. package/src/components/Select/Select.tsx +5 -1
  121. package/src/components/Select/styles.css.ts +12 -0
  122. package/src/components/TableOfContents/TableOfContents.meta.ts +41 -0
  123. package/src/components/TableOfContents/TableOfContents.stories.tsx +222 -0
  124. package/src/components/TableOfContents/TableOfContents.tsx +173 -0
  125. package/src/components/TableOfContents/TableOfContentsContext.ts +20 -0
  126. package/src/components/TableOfContents/styles.css.ts +147 -0
  127. package/src/components/TextLink/TextLink.meta.ts +1 -1
  128. package/src/components/TextLink/TextLink.stories.tsx +42 -1
  129. package/src/components/TextLink/TextLink.tsx +9 -0
  130. package/src/components/Toggle/Toggle.tsx +2 -1
  131. package/src/components/Toggle/styles.css.ts +2 -0
  132. package/src/components/Tree/Tree.meta.ts +2 -1
  133. package/src/components/Tree/Tree.stories.tsx +93 -1
  134. package/src/components/Tree/Tree.tsx +69 -27
  135. package/src/components/Tree/styles.css.ts +6 -0
  136. package/src/docs/clickLink.ts +24 -0
  137. package/src/docs/coarsePointer.ts +42 -0
  138. package/src/docs/themedSurface.tsx +23 -0
  139. package/src/index.ts +11 -0
  140. package/src/shared/ModalContext.ts +8 -1
  141. package/src/shared/exports.typecheck.ts +16 -0
  142. package/src/shared/linkParts.typecheck.tsx +47 -0
  143. package/src/shared/navigate.ts +62 -0
  144. package/src/shared/types.ts +3 -0
  145. package/dist/Table-Cm5cCnZD.js.map +0 -1
  146. package/dist/shared/length.typecheck.d.ts +0 -31
  147. package/dist/shared/length.typecheck.d.ts.map +0 -1
@@ -10,5 +10,12 @@
10
10
  * closes on a nested modal opening; no consumer needs it yet.
11
11
  */
12
12
  export declare const ModalContext: import('react').Context<boolean>;
13
+ /**
14
+ * True inside a Dialog or Drawer, false outside. A floating surface that is not
15
+ * a Duro Popover picks its layer with it, the way a Popover does:
16
+ * `useInModal() ? layers.popover : layers.floating`, and for one that must
17
+ * cover another floating surface (a link editor over a format bar)
18
+ * `useInModal() ? layers.popoverRaised : layers.floatingRaised`.
19
+ */
13
20
  export declare function useInModal(): boolean;
14
21
  //# sourceMappingURL=ModalContext.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ModalContext.d.ts","sourceRoot":"","sources":["../../src/shared/ModalContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,kCAAuB,CAAA;AAEhD,wBAAgB,UAAU,YAEzB"}
1
+ {"version":3,"file":"ModalContext.d.ts","sourceRoot":"","sources":["../../src/shared/ModalContext.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,eAAO,MAAM,YAAY,kCAAuB,CAAA;AAEhD;;;;;;GAMG;AACH,wBAAgB,UAAU,IAAI,OAAO,CAEpC"}
@@ -0,0 +1,9 @@
1
+ import { ButtonSize, ControlSize, ToggleSize, useInModal } from '../index';
2
+ type Same<T, U> = [T] extends [U] ? ([U] extends [T] ? true : false) : false;
3
+ export declare const useInModalIsBoolean: Same<ReturnType<typeof useInModal>, boolean>;
4
+ export declare const useInModalTakesNothing: Same<Parameters<typeof useInModal>, []>;
5
+ export declare const controlSizes: Same<ControlSize, 'default' | 'small'>;
6
+ export declare const buttonSizeIsControlSize: Same<ButtonSize, ControlSize>;
7
+ export declare const toggleSizeIsControlSize: Same<ToggleSize, ControlSize>;
8
+ export {};
9
+ //# sourceMappingURL=exports.typecheck.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exports.typecheck.d.ts","sourceRoot":"","sources":["../../src/shared/exports.typecheck.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAC,UAAU,EAAE,WAAW,EAAE,UAAU,EAAE,UAAU,EAAC,MAAM,UAAU,CAAA;AAE7E,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI,GAAG,KAAK,CAAC,GAAG,KAAK,CAAA;AAG5E,eAAO,MAAM,mBAAmB,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,EAAE,OAAO,CAAQ,CAAA;AACrF,eAAO,MAAM,sBAAsB,EAAE,IAAI,CAAC,UAAU,CAAC,OAAO,UAAU,CAAC,EAAE,EAAE,CAAQ,CAAA;AAGnF,eAAO,MAAM,YAAY,EAAE,IAAI,CAAC,WAAW,EAAE,SAAS,GAAG,OAAO,CAAQ,CAAA;AACxE,eAAO,MAAM,uBAAuB,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAQ,CAAA;AAC1E,eAAO,MAAM,uBAAuB,EAAE,IAAI,CAAC,UAAU,EAAE,WAAW,CAAQ,CAAA"}
@@ -0,0 +1,25 @@
1
+ /** The part of a click event a link part reads and hands to `onNavigate`. */
2
+ export interface LinkClickEvent {
3
+ readonly button: number;
4
+ readonly metaKey: boolean;
5
+ readonly ctrlKey: boolean;
6
+ readonly shiftKey: boolean;
7
+ readonly altKey: boolean;
8
+ readonly defaultPrevented: boolean;
9
+ preventDefault(): void;
10
+ }
11
+ /**
12
+ * Called for a plain primary click on a link part. Call
13
+ * `event.preventDefault()`, then your router's navigate; leave the event alone
14
+ * to let the browser follow the link.
15
+ */
16
+ export type OnNavigate = (href: string, event: LinkClickEvent) => void;
17
+ /** A primary-button click with no modifier key, not already prevented. */
18
+ export declare function isPlainClick(event: LinkClickEvent): boolean;
19
+ /**
20
+ * The `onClick` of a link part: hands a plain click on a link with no
21
+ * `target` to `onNavigate`, and does nothing otherwise, so the browser default
22
+ * runs. Undefined when there is no `onNavigate` (no handler at all).
23
+ */
24
+ export declare function linkClickHandler(href: string, onNavigate: OnNavigate | undefined, target?: string | null): ((event: LinkClickEvent) => void) | undefined;
25
+ //# sourceMappingURL=navigate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigate.d.ts","sourceRoot":"","sources":["../../src/shared/navigate.ts"],"names":[],"mappings":"AAgBA,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;IAC1B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAA;IACxB,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAA;IAClC,cAAc,IAAI,IAAI,CAAA;CACvB;AAED;;;;GAIG;AACH,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,cAAc,KAAK,IAAI,CAAA;AAEtE,0EAA0E;AAC1E,wBAAgB,YAAY,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAS3D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,UAAU,EAAE,UAAU,GAAG,SAAS,EAClC,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,GACrB,CAAC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,SAAS,CAM/C"}
@@ -1,2 +1,4 @@
1
1
  export type Orientation = 'horizontal' | 'vertical';
2
+ /** The two control sizes shared by Button, Toggle and the Select, Menu and Popover triggers. */
3
+ export type ControlSize = 'default' | 'small';
2
4
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,UAAU,CAAA"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,WAAW,GAAG,YAAY,GAAG,UAAU,CAAA;AAEnD,gGAAgG;AAChG,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,OAAO,CAAA"}
package/dist/table.js CHANGED
@@ -1,4 +1,4 @@
1
- import { M as F, l as xt, R as rt, H as kt, m as pt, n as bt, o as ft, p as mt, j as ht, q as yt, r as St, t as Tt } from "./Table-Cm5cCnZD.js";
1
+ import { M as F, m as xt, R as rt, H as kt, n as pt, o as bt, p as ft, q as mt, k as ht, t as yt, r as St, v as Tt } from "./Table-B1sWL3GA.js";
2
2
  import { jsx as u, jsxs as q } from "react/jsx-runtime";
3
3
  import { c as H, m as p, d as b } from "./compiler-runtime-B2IcI8_O.js";
4
4
  import { flexRender as E, useReactTable as it, getPaginationRowModel as Ct, getFilteredRowModel as vt, getSortedRowModel as at, getCoreRowModel as st } from "@tanstack/react-table";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "5.4.0",
3
+ "version": "5.6.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "@tanstack/react-virtual": "^3.14.6",
69
- "@duro-app/tokens": "^5.4.0"
69
+ "@duro-app/tokens": "^5.6.0"
70
70
  },
71
71
  "devDependencies": {
72
72
  "@babel/preset-typescript": "^7.28.0",
@@ -0,0 +1,37 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Where the current page sits in the hierarchy: a labelled nav landmark with an ordered list of links, outermost first. An Item is either a link (href, optional onNavigate) or the current page (current: text with aria-current="page"), never both — the types refuse the mix. The › separators are aria-hidden. Links take href and an optional onNavigate(href, event) for a client router (plain clicks only). Compound component — Root is required.',
6
+ whenToUse: [
7
+ 'A page deep in a hierarchy — docs › section › page, admin › collection › record',
8
+ 'A detail page whose way back to its collection should be one click',
9
+ ],
10
+ whenNotToUse: [
11
+ 'The previous and next pages in reading order — use PageNav',
12
+ 'The sections of the page being read — use TableOfContents',
13
+ 'Primary app navigation — use SideNav',
14
+ ],
15
+ anatomy: {
16
+ required: ['Root', 'Item'],
17
+ },
18
+ relatedTo: [
19
+ {
20
+ component: 'PageNav',
21
+ kind: 'contrast',
22
+ relationship: 'Breadcrumb goes up the hierarchy; PageNav goes along the reading order',
23
+ },
24
+ {
25
+ component: 'admin-detail-page',
26
+ kind: 'composition',
27
+ relationship: 'The admin-detail-page recipe heads the record with a Breadcrumb',
28
+ },
29
+ ],
30
+ example: `<Breadcrumb.Root aria-label="Breadcrumb">
31
+ <Breadcrumb.Item href="/docs" onNavigate={go}>Docs</Breadcrumb.Item>
32
+ <Breadcrumb.Item href="/docs/architecture" onNavigate={go}>Architecture</Breadcrumb.Item>
33
+ <Breadcrumb.Item current>AI-ops platform</Breadcrumb.Item>
34
+ </Breadcrumb.Root>
35
+
36
+ // go = (href, event) => { event.preventDefault(); navigate(href) }`,
37
+ }
@@ -0,0 +1,110 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, fn, waitFor} from 'storybook/test'
3
+ import {Breadcrumb} from './Breadcrumb'
4
+ import {clickLink} from '../../docs/clickLink'
5
+ import {onThemeSurface} from '../../docs/themedSurface'
6
+ import {withCoarsePointer, withFinePointer} from '../../docs/coarsePointer'
7
+ import {SIZES_PX} from '@duro-app/tokens/keys'
8
+ import type {OnNavigate} from '../../shared/navigate'
9
+
10
+ const meta: Meta = {
11
+ title: 'Components/Breadcrumb',
12
+ parameters: {a11y: {test: 'error'}},
13
+ decorators: [onThemeSurface],
14
+ }
15
+
16
+ export default meta
17
+ type Story = StoryObj
18
+
19
+ function Trail({onNavigate}: {onNavigate?: OnNavigate}) {
20
+ return (
21
+ <Breadcrumb.Root aria-label="Breadcrumb">
22
+ <Breadcrumb.Item href="#docs" onNavigate={onNavigate}>
23
+ Docs
24
+ </Breadcrumb.Item>
25
+ <Breadcrumb.Item href="#architecture" onNavigate={onNavigate}>
26
+ Architecture
27
+ </Breadcrumb.Item>
28
+ <Breadcrumb.Item current>AI-ops platform</Breadcrumb.Item>
29
+ </Breadcrumb.Root>
30
+ )
31
+ }
32
+
33
+ export const Default: Story = {
34
+ render: () => <Trail />,
35
+ play: async ({canvas}) => {
36
+ const nav = canvas.getByRole('navigation', {name: 'Breadcrumb'})
37
+ await expect(nav).toBeInTheDocument()
38
+ // the list holds the three items only: the separators are aria-hidden
39
+ await expect(canvas.getAllByRole('listitem')).toHaveLength(3)
40
+ await expect(canvas.getAllByRole('link').map((a) => a.textContent)).toEqual([
41
+ 'Docs',
42
+ 'Architecture',
43
+ ])
44
+ // the current page is text, not a link, and says so
45
+ const current = canvas.getByText('AI-ops platform')
46
+ await expect(current).toHaveAttribute('aria-current', 'page')
47
+ await expect(current.closest('a')).toBeNull()
48
+ },
49
+ }
50
+
51
+ export const KeyboardOrder: Story = {
52
+ render: () => <Trail />,
53
+ play: async ({canvas, userEvent}) => {
54
+ await userEvent.tab()
55
+ await expect(canvas.getByRole('link', {name: 'Docs'})).toHaveFocus()
56
+ await userEvent.tab()
57
+ await expect(canvas.getByRole('link', {name: 'Architecture'})).toHaveFocus()
58
+ // the current page is not a tab stop
59
+ await userEvent.tab()
60
+ await expect(canvas.getByRole('link', {name: 'Architecture'})).not.toHaveFocus()
61
+ },
62
+ }
63
+
64
+ export const ClientNavigation: StoryObj<{onNavigate: OnNavigate}> = {
65
+ args: {onNavigate: fn()},
66
+ render: (args) => <Trail onNavigate={args.onNavigate} />,
67
+ play: async ({args, canvas}) => {
68
+ const onNavigate = args.onNavigate as unknown as ReturnType<typeof fn>
69
+ onNavigate.mockImplementation((_href: string, event: {preventDefault(): void}) =>
70
+ event.preventDefault(),
71
+ )
72
+ const link = canvas.getByRole('link', {name: 'Architecture'})
73
+
74
+ // a plain click goes to the router, which prevents the default
75
+ await expect(clickLink(link).defaultPrevented).toBe(true)
76
+ await expect(onNavigate).toHaveBeenCalledTimes(1)
77
+ await expect(onNavigate.mock.calls[0][0]).toBe('#architecture')
78
+
79
+ // a modified or non-primary click keeps the browser default
80
+ for (const init of [{metaKey: true}, {ctrlKey: true}, {shiftKey: true}, {button: 1}]) {
81
+ await expect(clickLink(link, init).defaultPrevented).toBe(false)
82
+ }
83
+ await expect(onNavigate).toHaveBeenCalledTimes(1)
84
+ },
85
+ }
86
+
87
+ /**
88
+ * A touch screen gets 44px links (coarse pointer); a mouse keeps the
89
+ * artboard's text-height row.
90
+ * It drives Chrome's touch emulation through Vitest's CDP session, so it runs
91
+ * as a test and stays out of the dev sidebar (there is no Vitest there).
92
+ */
93
+ export const TouchTargets: Story = {
94
+ tags: ['!dev', '!autodocs'],
95
+ render: () => <Trail />,
96
+ play: async ({canvas}) => {
97
+ await withFinePointer()
98
+ const links = () => canvas.getAllByRole('link')
99
+ for (const link of links()) {
100
+ await expect(link.getBoundingClientRect().height).toBeLessThan(SIZES_PX.touchTarget)
101
+ }
102
+ await withCoarsePointer(async () => {
103
+ for (const link of links()) {
104
+ await waitFor(() =>
105
+ expect(link.getBoundingClientRect().height).toBeGreaterThanOrEqual(SIZES_PX.touchTarget),
106
+ )
107
+ }
108
+ })
109
+ },
110
+ }
@@ -0,0 +1,95 @@
1
+ import {Children, createContext, Fragment, isValidElement, type ReactNode, useContext} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import {styles} from './styles.css'
4
+ import {linkClickHandler, type OnNavigate} from '../../shared/navigate'
5
+
6
+ /* Breadcrumb — where the current page sits in the site's hierarchy: a
7
+ * labelled `nav` holding an ordered list of links, the last one the current
8
+ * page (`aria-current="page"`, shown as text). The separators between items
9
+ * are decorative (`aria-hidden`), so a screen reader hears the list alone. */
10
+
11
+ // Items render list items: outside Root's list they would be invalid markup.
12
+ const BreadcrumbContext = createContext(false)
13
+
14
+ function useBreadcrumb() {
15
+ if (!useContext(BreadcrumbContext))
16
+ throw new Error('Breadcrumb compound components must be used within Breadcrumb.Root')
17
+ }
18
+
19
+ // --- Root ---
20
+
21
+ interface RootProps {
22
+ /** Names the landmark ("Breadcrumb"); required, from your text catalog. */
23
+ 'aria-label': string
24
+ /** `Breadcrumb.Item`s, outermost first. */
25
+ children: ReactNode
26
+ }
27
+
28
+ function Root({'aria-label': ariaLabel, children}: RootProps) {
29
+ const items = Children.toArray(children).filter(isValidElement)
30
+ return (
31
+ <BreadcrumbContext.Provider value={true}>
32
+ <html.nav aria-label={ariaLabel} style={styles.root}>
33
+ <html.ol style={styles.list}>
34
+ {items.map((item, i) => (
35
+ <Fragment key={item.key ?? i}>
36
+ {i > 0 ? (
37
+ <html.li aria-hidden style={styles.separator}>
38
+ ›
39
+ </html.li>
40
+ ) : null}
41
+ {item}
42
+ </Fragment>
43
+ ))}
44
+ </html.ol>
45
+ </html.nav>
46
+ </BreadcrumbContext.Provider>
47
+ )
48
+ }
49
+
50
+ // --- Item ---
51
+
52
+ /**
53
+ * A link to an ancestor page, or the current page. The two are exclusive: the
54
+ * current page has no `href` (it is text) and a link is never `current`.
55
+ */
56
+ type ItemProps = {children: ReactNode} & (
57
+ | {
58
+ /** Where the item links to. */
59
+ href: string
60
+ /**
61
+ * Client-side navigation: called for a plain primary click (no modifier
62
+ * key). Call `event.preventDefault()`, then your router's navigate.
63
+ */
64
+ onNavigate?: OnNavigate
65
+ current?: never
66
+ }
67
+ | {
68
+ /** The page the user is on: rendered as text with `aria-current="page"`. */
69
+ current: true
70
+ href?: never
71
+ onNavigate?: never
72
+ }
73
+ )
74
+
75
+ function Item({href, current, onNavigate, children}: ItemProps) {
76
+ useBreadcrumb()
77
+ return (
78
+ <html.li style={styles.item}>
79
+ {current ? (
80
+ <html.span aria-current="page" style={styles.current}>
81
+ {children}
82
+ </html.span>
83
+ ) : (
84
+ <html.a href={href} onClick={linkClickHandler(href, onNavigate)} style={styles.link}>
85
+ {children}
86
+ </html.a>
87
+ )}
88
+ </html.li>
89
+ )
90
+ }
91
+
92
+ export const Breadcrumb = {
93
+ Root,
94
+ Item,
95
+ }
@@ -0,0 +1,76 @@
1
+ import {css} from 'react-strict-dom'
2
+ import {colors} from '@duro-app/tokens/tokens/colors.css'
3
+ import {radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
4
+ import {typography} from '@duro-app/tokens/tokens/typography.css'
5
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
6
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
7
+ import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
8
+
9
+ // The picked docs browser (docs/mockups/docs-nav/Reader.dc.html, `.crumbs`):
10
+ // small muted text, accent links, a `›` between items, the current page plain.
11
+ export const styles = css.create({
12
+ root: {
13
+ minWidth: 0,
14
+ },
15
+ list: {
16
+ display: 'flex',
17
+ flexWrap: 'wrap',
18
+ alignItems: 'center',
19
+ gap: spacing.xs,
20
+ margin: 0,
21
+ padding: 0,
22
+ listStyleType: 'none',
23
+ fontFamily: typography.fontFamily,
24
+ fontSize: typography.fontSizeSm,
25
+ color: colors.textMuted,
26
+ },
27
+ item: {
28
+ display: 'inline-flex',
29
+ minWidth: 0,
30
+ },
31
+ separator: {
32
+ display: 'inline-flex',
33
+ color: colors.textMuted,
34
+ userSelect: 'none',
35
+ },
36
+ // On a touch screen (coarse pointer, as Toggle does) each link is a
37
+ // touch target; a mouse keeps the artboard's text-height row.
38
+ link: {
39
+ display: 'inline-flex',
40
+ alignItems: 'center',
41
+ minHeight: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
42
+ color: {
43
+ default: colors.accent,
44
+ ':hover': colors.accentHover,
45
+ },
46
+ textDecorationLine: {
47
+ default: 'none',
48
+ ':hover': 'underline',
49
+ },
50
+ borderRadius: radii.xs,
51
+ transitionProperty: 'color',
52
+ transitionDuration: duration.fast,
53
+ transitionTimingFunction: easing.standard,
54
+ outlineWidth: {
55
+ default: 0,
56
+ ':focus-visible': borders.focusRing,
57
+ },
58
+ outlineStyle: {
59
+ default: 'none',
60
+ ':focus-visible': 'solid',
61
+ },
62
+ outlineColor: {
63
+ default: 'transparent',
64
+ ':focus-visible': colors.accent,
65
+ },
66
+ outlineOffset: {
67
+ default: 0,
68
+ ':focus-visible': borders.focusOffset,
69
+ },
70
+ },
71
+ // the artboard keeps the current page in the list's muted colour; it is
72
+ // told apart by not being a link (and by aria-current for assistive tech)
73
+ current: {
74
+ color: 'inherit',
75
+ },
76
+ })
@@ -2,11 +2,12 @@ import type {ReactNode, Ref} from 'react'
2
2
  import {html} from 'react-strict-dom'
3
3
  import {isNative} from '../../platform'
4
4
  import {mergeRefs} from '../../shared/mergeRefs'
5
+ import type {ControlSize} from '../../shared/types'
5
6
  import {useGroupedControl} from '../ButtonGroup/useGroupedControl'
6
7
  import {styles} from './styles.css'
7
8
 
8
9
  export type ButtonVariant = 'primary' | 'secondary' | 'inverseSecondary' | 'link' | 'danger'
9
- export type ButtonSize = 'default' | 'small'
10
+ export type ButtonSize = ControlSize
10
11
 
11
12
  interface ButtonProps {
12
13
  variant?: ButtonVariant
@@ -4,6 +4,7 @@ import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
4
4
  import {typography} from '@duro-app/tokens/tokens/typography.css'
5
5
  import {duration, easing} from '@duro-app/tokens/tokens/motion.css'
6
6
  import {borders} from '@duro-app/tokens/tokens/borders.css'
7
+ import {sizes} from '@duro-app/tokens/tokens/sizes.css'
7
8
 
8
9
  export const styles = css.create({
9
10
  base: {
@@ -62,6 +63,8 @@ export const styles = css.create({
62
63
  paddingLeft: spacing.sm,
63
64
  paddingRight: spacing.sm,
64
65
  fontSize: typography.fontSizeXs,
66
+ // Every small control is at least controlSm tall, whatever it holds (5.6).
67
+ minHeight: sizes.controlSm,
65
68
  },
66
69
  primary: {
67
70
  backgroundColor: {