@duro-app/ui 5.7.0 → 5.8.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.
Files changed (127) hide show
  1. package/dist/{Table-Dj7PXqO_.js → Table-CAjPFgTX.js} +397 -394
  2. package/dist/Table-CAjPFgTX.js.map +1 -0
  3. package/dist/{compiler-runtime-B2IcI8_O.js → compiler-runtime-BuugS4D0.js} +67 -59
  4. package/dist/{compiler-runtime-B2IcI8_O.js.map → compiler-runtime-BuugS4D0.js.map} +1 -1
  5. package/dist/components/AppShell/AppShell.d.ts +74 -0
  6. package/dist/components/AppShell/AppShell.d.ts.map +1 -0
  7. package/dist/components/AppShell/AppShell.meta.d.ts +3 -0
  8. package/dist/components/AppShell/AppShell.meta.d.ts.map +1 -0
  9. package/dist/components/AppShell/AppShell.stories.d.ts +192 -0
  10. package/dist/components/AppShell/AppShell.stories.d.ts.map +1 -0
  11. package/dist/components/AppShell/styles.css.d.ts +187 -0
  12. package/dist/components/AppShell/styles.css.d.ts.map +1 -0
  13. package/dist/components/Aside/Aside.d.ts +23 -0
  14. package/dist/components/Aside/Aside.d.ts.map +1 -0
  15. package/dist/components/Aside/Aside.meta.d.ts +3 -0
  16. package/dist/components/Aside/Aside.meta.d.ts.map +1 -0
  17. package/dist/components/Aside/Aside.stories.d.ts +45 -0
  18. package/dist/components/Aside/Aside.stories.d.ts.map +1 -0
  19. package/dist/components/Aside/styles.css.d.ts +44 -0
  20. package/dist/components/Aside/styles.css.d.ts.map +1 -0
  21. package/dist/components/Button/styles.css.d.ts +2 -0
  22. package/dist/components/Button/styles.css.d.ts.map +1 -1
  23. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +3 -3
  24. package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
  25. package/dist/components/CodeBlock/CodeBlock.d.ts +29 -0
  26. package/dist/components/CodeBlock/CodeBlock.d.ts.map +1 -0
  27. package/dist/components/CodeBlock/CodeBlock.meta.d.ts +3 -0
  28. package/dist/components/CodeBlock/CodeBlock.meta.d.ts.map +1 -0
  29. package/dist/components/CodeBlock/CodeBlock.stories.d.ts +73 -0
  30. package/dist/components/CodeBlock/CodeBlock.stories.d.ts.map +1 -0
  31. package/dist/components/CodeBlock/styles.css.d.ts +41 -0
  32. package/dist/components/CodeBlock/styles.css.d.ts.map +1 -0
  33. package/dist/components/Grid/Grid.d.ts +13 -4
  34. package/dist/components/Grid/Grid.d.ts.map +1 -1
  35. package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
  36. package/dist/components/Grid/Grid.stories.d.ts +28 -0
  37. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  38. package/dist/components/Grid/styles.css.d.ts +3 -0
  39. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  40. package/dist/components/Heading/Heading.d.ts +3 -1
  41. package/dist/components/Heading/Heading.d.ts.map +1 -1
  42. package/dist/components/Heading/Heading.stories.d.ts +9 -0
  43. package/dist/components/Heading/Heading.stories.d.ts.map +1 -0
  44. package/dist/components/LinkButton/LinkButton.stories.d.ts +7 -0
  45. package/dist/components/LinkButton/LinkButton.stories.d.ts.map +1 -1
  46. package/dist/components/LinkButton/styles.css.d.ts +2 -0
  47. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  48. package/dist/components/LiveRegion/LiveRegion.d.ts +28 -0
  49. package/dist/components/LiveRegion/LiveRegion.d.ts.map +1 -0
  50. package/dist/components/LiveRegion/LiveRegion.meta.d.ts +3 -0
  51. package/dist/components/LiveRegion/LiveRegion.meta.d.ts.map +1 -0
  52. package/dist/components/LiveRegion/LiveRegion.stories.d.ts +66 -0
  53. package/dist/components/LiveRegion/LiveRegion.stories.d.ts.map +1 -0
  54. package/dist/components/Menu/Menu.stories.d.ts +7 -0
  55. package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
  56. package/dist/components/Menu/styles.css.d.ts +2 -0
  57. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  58. package/dist/components/PageNav/styles.css.d.ts +1 -1
  59. package/dist/components/Prose/Prose.d.ts +17 -0
  60. package/dist/components/Prose/Prose.d.ts.map +1 -0
  61. package/dist/components/Prose/Prose.meta.d.ts +3 -0
  62. package/dist/components/Prose/Prose.meta.d.ts.map +1 -0
  63. package/dist/components/Prose/Prose.stories.d.ts +63 -0
  64. package/dist/components/Prose/Prose.stories.d.ts.map +1 -0
  65. package/dist/components/SideNav/SideNav.d.ts +7 -1
  66. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  67. package/dist/components/SideNav/SideNav.stories.d.ts +3 -0
  68. package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
  69. package/dist/components/Time/Time.d.ts +26 -0
  70. package/dist/components/Time/Time.d.ts.map +1 -0
  71. package/dist/components/Time/Time.meta.d.ts +3 -0
  72. package/dist/components/Time/Time.meta.d.ts.map +1 -0
  73. package/dist/components/Time/Time.stories.d.ts +77 -0
  74. package/dist/components/Time/Time.stories.d.ts.map +1 -0
  75. package/dist/components/Time/relative.d.ts +27 -0
  76. package/dist/components/Time/relative.d.ts.map +1 -0
  77. package/dist/form.js +1 -1
  78. package/dist/hooks/useContainerBelow.d.ts +18 -0
  79. package/dist/hooks/useContainerBelow.d.ts.map +1 -0
  80. package/dist/index.css +1 -1
  81. package/dist/index.d.ts +9 -1
  82. package/dist/index.d.ts.map +1 -1
  83. package/dist/index.js +4870 -4183
  84. package/dist/index.js.map +1 -1
  85. package/dist/table.js +2 -2
  86. package/package.json +2 -2
  87. package/src/components/AppShell/AppShell.meta.ts +62 -0
  88. package/src/components/AppShell/AppShell.stories.tsx +825 -0
  89. package/src/components/AppShell/AppShell.tsx +346 -0
  90. package/src/components/AppShell/styles.css.ts +219 -0
  91. package/src/components/Aside/Aside.meta.ts +33 -0
  92. package/src/components/Aside/Aside.stories.tsx +106 -0
  93. package/src/components/Aside/Aside.tsx +42 -0
  94. package/src/components/Aside/styles.css.ts +60 -0
  95. package/src/components/Button/styles.css.ts +7 -2
  96. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +4 -5
  97. package/src/components/CodeBlock/CodeBlock.meta.ts +29 -0
  98. package/src/components/CodeBlock/CodeBlock.stories.tsx +192 -0
  99. package/src/components/CodeBlock/CodeBlock.tsx +99 -0
  100. package/src/components/CodeBlock/styles.css.ts +48 -0
  101. package/src/components/Grid/Grid.meta.ts +2 -1
  102. package/src/components/Grid/Grid.stories.tsx +202 -3
  103. package/src/components/Grid/Grid.tsx +35 -6
  104. package/src/components/Grid/styles.css.ts +11 -0
  105. package/src/components/Heading/Heading.stories.tsx +51 -0
  106. package/src/components/Heading/Heading.tsx +38 -7
  107. package/src/components/LinkButton/LinkButton.stories.tsx +41 -1
  108. package/src/components/LinkButton/styles.css.ts +6 -2
  109. package/src/components/LiveRegion/LiveRegion.meta.ts +29 -0
  110. package/src/components/LiveRegion/LiveRegion.stories.tsx +139 -0
  111. package/src/components/LiveRegion/LiveRegion.tsx +48 -0
  112. package/src/components/Menu/Menu.stories.tsx +62 -0
  113. package/src/components/Menu/styles.css.ts +6 -3
  114. package/src/components/Prose/Prose.meta.ts +32 -0
  115. package/src/components/Prose/Prose.stories.tsx +240 -0
  116. package/src/components/Prose/Prose.tsx +25 -0
  117. package/src/components/Prose/prose.css +179 -0
  118. package/src/components/SideNav/SideNav.stories.tsx +29 -0
  119. package/src/components/SideNav/SideNav.tsx +14 -2
  120. package/src/components/Time/Time.meta.ts +17 -0
  121. package/src/components/Time/Time.stories.tsx +173 -0
  122. package/src/components/Time/Time.tsx +33 -0
  123. package/src/components/Time/relative.ts +89 -0
  124. package/src/hooks/useContainerBelow.ts +39 -0
  125. package/src/index.ts +13 -1
  126. package/src/strict.css +4 -1
  127. package/dist/Table-Dj7PXqO_.js.map +0 -1
@@ -0,0 +1,346 @@
1
+ import {
2
+ Children,
3
+ createContext,
4
+ isValidElement,
5
+ useCallback,
6
+ useContext,
7
+ useEffect,
8
+ useId,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ type ReactNode,
13
+ type Ref,
14
+ type RefObject,
15
+ } from 'react'
16
+ import {html} from 'react-strict-dom'
17
+ import {Button} from '../Button/Button'
18
+ import {Drawer} from '../Drawer/Drawer'
19
+ import {Icon} from '../Icon'
20
+ import {TextLink} from '../TextLink/TextLink'
21
+ import {useContainerBelow} from '../../hooks/useContainerBelow'
22
+ import type {OnNavigate} from '../../shared/navigate'
23
+ import {styles} from './styles.css'
24
+
25
+ /* AppShell — an application's frame: a navigation rail beside the main
26
+ * content, with an optional Header. At and above `collapseBelow` (the
27
+ * shell's own width, a container query) the rail sits on the left, the brand
28
+ * at its top, and the Header is a row above Main that scrolls with the page.
29
+ * Below it the rail is hidden and ONE sticky bar takes the top: a Menu button
30
+ * that opens the same navigation in a left Drawer, the brand, then the
31
+ * Header's content when there is a Header — so a shell without a Header still
32
+ * has its navigation on a phone. CSS makes the switch, so the server renders
33
+ * one HTML for every width and nothing moves after hydration: the brand is in
34
+ * the HTML twice (rail and bar) and `display: none` hides the inactive copy.
35
+ *
36
+ * Landmarks: the rail is a `nav` named by its `aria-label`, the bar a
37
+ * `header` (banner), the content a `main`, which a skip link (the first tab
38
+ * stop) focuses. */
39
+
40
+ export type AppShellCollapse = 'sm' | 'md' | 'lg'
41
+
42
+ interface AppShellContextValue {
43
+ open: boolean
44
+ setOpen: (open: boolean) => void
45
+ closeLabel: string
46
+ brand: ReactNode
47
+ collapseBelow: AppShellCollapse
48
+ mainId: string
49
+ mainRef: RefObject<HTMLElement | null>
50
+ }
51
+
52
+ const AppShellContext = createContext<AppShellContextValue | null>(null)
53
+
54
+ // True only where Root placed a Header itself: Root finds Header among its
55
+ // direct children, so a Header wrapped in a fragment or a component would
56
+ // otherwise render as a stray grid item and never reach the bar.
57
+ const HeaderSlotContext = createContext(false)
58
+
59
+ function useAppShell(): AppShellContextValue {
60
+ const ctx = useContext(AppShellContext)
61
+ if (!ctx) throw new Error('AppShell compound components must be used within AppShell.Root')
62
+ return ctx
63
+ }
64
+
65
+ const variants = {
66
+ sm: {
67
+ grid: styles.gridSm,
68
+ rail: styles.railSm,
69
+ bar: styles.barSm,
70
+ barEmpty: styles.barEmptySm,
71
+ narrow: styles.narrowSm,
72
+ main: styles.mainSm,
73
+ },
74
+ md: {
75
+ grid: styles.gridMd,
76
+ rail: styles.railMd,
77
+ bar: styles.barMd,
78
+ barEmpty: styles.barEmptyMd,
79
+ narrow: styles.narrowMd,
80
+ main: styles.mainMd,
81
+ },
82
+ lg: {
83
+ grid: styles.gridLg,
84
+ rail: styles.railLg,
85
+ bar: styles.barLg,
86
+ barEmpty: styles.barEmptyLg,
87
+ narrow: styles.narrowLg,
88
+ main: styles.mainLg,
89
+ },
90
+ } as const satisfies Record<AppShellCollapse, unknown>
91
+
92
+ // --- Root ---
93
+
94
+ interface RootProps {
95
+ /** The narrow-width button that opens the navigation ("Menu"), from your text catalog. */
96
+ menuLabel: string
97
+ /** The accessible name of the drawer's close button ("Close navigation"). */
98
+ closeLabel: string
99
+ /** The skip link's text ("Skip to content"): the first tab stop, it moves focus to Main. */
100
+ skipLabel: string
101
+ /**
102
+ * The app's name or logo — usually a link home. Shown at the top of the
103
+ * rail and, below `collapseBelow`, in the bar beside the Menu button.
104
+ */
105
+ brand?: ReactNode
106
+ /**
107
+ * Below this breakpoint, measured on the shell's own width, the rail
108
+ * becomes a Drawer behind the Menu button. Default `sm`.
109
+ */
110
+ collapseBelow?: AppShellCollapse
111
+ /**
112
+ * `AppShell.Rail`, `AppShell.Main` and an optional `AppShell.Header`, as
113
+ * direct children (Root places the Header's content in its bar).
114
+ */
115
+ children: ReactNode
116
+ }
117
+
118
+ function Root({
119
+ menuLabel,
120
+ closeLabel,
121
+ skipLabel,
122
+ brand,
123
+ collapseBelow = 'sm',
124
+ children,
125
+ }: RootProps) {
126
+ const [open, setOpen] = useState(false)
127
+ const containerRef = useRef<HTMLDivElement | null>(null)
128
+ const menuButtonRef = useRef<HTMLButtonElement | null>(null)
129
+ const mainRef = useRef<HTMLElement | null>(null)
130
+ const narrow = useContainerBelow(containerRef, collapseBelow)
131
+ const generatedId = useId()
132
+ const variant = variants[collapseBelow]
133
+
134
+ // The Header's content goes in the bar, after the Menu button and the
135
+ // brand; Main may name its own id, which the skip link then targets.
136
+ let header: ReactNode = null
137
+ let hasHeader = false
138
+ let mainId = `${generatedId}main`
139
+ const rest: ReactNode[] = []
140
+ Children.forEach(children, (child) => {
141
+ if (isValidElement<HeaderProps>(child) && child.type === Header) {
142
+ hasHeader = true
143
+ header = child
144
+ return
145
+ }
146
+ if (isValidElement<MainProps>(child) && child.type === Main && child.props.id) {
147
+ mainId = child.props.id
148
+ }
149
+ rest.push(child)
150
+ })
151
+
152
+ // Widening past the collapse point while the drawer is open puts the rail
153
+ // back: close it.
154
+ useEffect(() => {
155
+ if (!narrow) setOpen(false)
156
+ }, [narrow])
157
+
158
+ // Focus goes back to the Menu button when the drawer closes (Escape, the
159
+ // close button, the backdrop, a navigation).
160
+ const wasOpen = useRef(false)
161
+ useEffect(() => {
162
+ if (wasOpen.current && !open) menuButtonRef.current?.focus()
163
+ wasOpen.current = open
164
+ }, [open])
165
+
166
+ const value = useMemo(
167
+ () => ({open, setOpen, closeLabel, brand, collapseBelow, mainId, mainRef}),
168
+ [open, closeLabel, brand, collapseBelow, mainId],
169
+ )
170
+
171
+ // Focus Main itself (tabIndex -1), not just scroll to it, and leave the
172
+ // URL alone: a router may read the hash.
173
+ const skip: OnNavigate = (_href, event) => {
174
+ event.preventDefault()
175
+ mainRef.current?.focus()
176
+ }
177
+
178
+ return (
179
+ <AppShellContext.Provider value={value}>
180
+ <html.div ref={containerRef} style={styles.container}>
181
+ <html.div style={[styles.grid, variant.grid]}>
182
+ <html.div style={styles.skip}>
183
+ <TextLink href={`#${mainId}`} onNavigate={skip}>
184
+ {skipLabel}
185
+ </TextLink>
186
+ </html.div>
187
+ <html.header style={[styles.bar, variant.bar, !hasHeader && variant.barEmpty]}>
188
+ <html.div style={[styles.narrowItem, variant.narrow]}>
189
+ <Button
190
+ ref={menuButtonRef}
191
+ variant="secondary"
192
+ size="small"
193
+ aria-expanded={open}
194
+ onClick={() => setOpen(true)}
195
+ >
196
+ <Icon name="menu" size="sm" />
197
+ {menuLabel}
198
+ </Button>
199
+ </html.div>
200
+ {brand != null && (
201
+ <html.div style={[styles.narrowItem, variant.narrow]}>{brand}</html.div>
202
+ )}
203
+ <HeaderSlotContext.Provider value={true}>{header}</HeaderSlotContext.Provider>
204
+ </html.header>
205
+ {rest}
206
+ </html.div>
207
+ </html.div>
208
+ </AppShellContext.Provider>
209
+ )
210
+ }
211
+
212
+ // --- Rail ---
213
+
214
+ export interface AppShellRailState {
215
+ /**
216
+ * Closes the drawer. Call it after any selection in the navigation — the
217
+ * current page included, where no route change would close it.
218
+ */
219
+ close: () => void
220
+ }
221
+
222
+ type RailContent = ReactNode | ((state: AppShellRailState) => ReactNode)
223
+
224
+ interface RailProps {
225
+ /** Names the navigation landmark and the drawer ("Navigation"); required. */
226
+ 'aria-label': string
227
+ /**
228
+ * The navigation (a SideNav, a Tree). Rendered in the rail, or in the
229
+ * drawer while it is open (never both); a function receives `close`.
230
+ */
231
+ children: RailContent
232
+ /**
233
+ * What sits at the rail's foot — the signed-in user, a licence line —
234
+ * and at the end of the drawer. A function receives `close`.
235
+ */
236
+ footer?: RailContent
237
+ }
238
+
239
+ const FOCUSABLE = 'a[href], button:not([disabled]), [tabindex]:not([tabindex="-1"])'
240
+
241
+ function Rail({'aria-label': ariaLabel, children, footer}: RailProps) {
242
+ const {open, setOpen, closeLabel, brand, collapseBelow} = useAppShell()
243
+ const close = useCallback(() => setOpen(false), [setOpen])
244
+ const drawerNavRef = useRef<HTMLElement | null>(null)
245
+ const content = typeof children === 'function' ? children({close}) : children
246
+ const foot = typeof footer === 'function' ? footer({close}) : footer
247
+ const variant = variants[collapseBelow]
248
+
249
+ // The drawer is modal: move focus into it, onto the first control.
250
+ useEffect(() => {
251
+ if (!open) return
252
+ drawerNavRef.current?.querySelector<HTMLElement>(FOCUSABLE)?.focus()
253
+ }, [open])
254
+
255
+ return (
256
+ <>
257
+ {/* Rendered in one place at a time, so ids (a Tree's) and the landmark
258
+ name are never duplicated: the drawer only opens below the collapse
259
+ point, where the rail is hidden. The brand stays: the bar holds the
260
+ narrow copy, and display: none hides this one there. */}
261
+ <html.nav
262
+ aria-label={ariaLabel}
263
+ style={[styles.rail, variant.rail]}
264
+ hidden={open || undefined}
265
+ >
266
+ {brand != null && <html.div style={styles.railBrand}>{brand}</html.div>}
267
+ <html.div style={styles.railBody}>{open ? null : content}</html.div>
268
+ {foot != null && <html.div style={styles.railFooter}>{open ? null : foot}</html.div>}
269
+ </html.nav>
270
+ <Drawer.Root anchor="left" open={open} onOpenChange={setOpen}>
271
+ <Drawer.Portal size="sm">
272
+ <Drawer.Header>
273
+ <Drawer.Title>{ariaLabel}</Drawer.Title>
274
+ <Drawer.Close aria-label={closeLabel} />
275
+ </Drawer.Header>
276
+ <Drawer.Body>
277
+ <html.nav ref={drawerNavRef} aria-label={ariaLabel}>
278
+ {content}
279
+ {foot != null && <html.div style={styles.drawerFooter}>{foot}</html.div>}
280
+ </html.nav>
281
+ </Drawer.Body>
282
+ </Drawer.Portal>
283
+ </Drawer.Root>
284
+ </>
285
+ )
286
+ }
287
+
288
+ // --- Header ---
289
+
290
+ interface HeaderProps {
291
+ /**
292
+ * The header's content: a search form, the account menu. Below
293
+ * `collapseBelow` it joins the Menu button and the brand in the sticky
294
+ * bar; above, it is a row over Main that scrolls with the page.
295
+ */
296
+ children?: ReactNode
297
+ }
298
+
299
+ function Header({children}: HeaderProps) {
300
+ useAppShell()
301
+ if (!useContext(HeaderSlotContext)) {
302
+ throw new Error(
303
+ 'AppShell.Header must be a direct child of AppShell.Root (not inside a fragment or another component)',
304
+ )
305
+ }
306
+ return <html.div style={styles.header}>{children}</html.div>
307
+ }
308
+
309
+ // --- Main ---
310
+
311
+ interface MainProps {
312
+ children: ReactNode
313
+ /** The skip link's target; Root makes one with `useId` when it is left out. */
314
+ id?: string
315
+ /** The `main` element: the container to hand `useContainerBelow`. */
316
+ ref?: Ref<HTMLElement>
317
+ }
318
+
319
+ function Main({children, id, ref}: MainProps) {
320
+ const {mainId, mainRef, collapseBelow} = useAppShell()
321
+ const setRef = useCallback(
322
+ (node: HTMLElement | null) => {
323
+ mainRef.current = node
324
+ if (typeof ref === 'function') ref(node)
325
+ else if (ref) ref.current = node
326
+ },
327
+ [mainRef, ref],
328
+ )
329
+ return (
330
+ <html.main
331
+ ref={setRef}
332
+ id={id ?? mainId}
333
+ tabIndex={-1}
334
+ style={[styles.main, variants[collapseBelow].main]}
335
+ >
336
+ {children}
337
+ </html.main>
338
+ )
339
+ }
340
+
341
+ export const AppShell = {
342
+ Root,
343
+ Rail,
344
+ Header,
345
+ Main,
346
+ }
@@ -0,0 +1,219 @@
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 {sizes} from '@duro-app/tokens/tokens/sizes.css'
5
+ import {borders} from '@duro-app/tokens/tokens/borders.css'
6
+ import {layers} from '@duro-app/tokens/tokens/layers.css'
7
+ import {breakpoints} from '@duro-app/tokens/tokens/breakpoints.css'
8
+
9
+ // At and above the collapse point (measured on the shell's own width, not
10
+ // the window) the rail sits beside the page and the Header is a row that
11
+ // scrolls with it; below it the rail gives way to one sticky bar — the Menu
12
+ // button, the brand and the Header's content — and its Drawer. CSS makes the
13
+ // switch, so the server's HTML is the same at every width and the first
14
+ // paint is already right.
15
+ //
16
+ // `collapseBelow` picks one of three static sets (sm, md, lg): StyleX needs
17
+ // every query written out, so each breakpoint-dependent style comes three
18
+ // times, identical but for the query.
19
+ const WIDE_SM = `@container (min-width: ${breakpoints.sm})`
20
+ const WIDE_MD = `@container (min-width: ${breakpoints.md})`
21
+ const WIDE_LG = `@container (min-width: ${breakpoints.lg})`
22
+
23
+ // The custom property an Aside inside the shell adds to its sticky `top`
24
+ // (Aside/styles.css.ts spells the same name). It is set on the grid, inside
25
+ // the query container (a container query cannot style the container itself)
26
+ // and an ancestor of Main. Above the collapse point it is `initial` (unset),
27
+ // so the Aside's fallback 0px applies: StyleX would write a `0px` as a
28
+ // unitless 0, which makes the Aside's calc() invalid.
29
+ // '--duro-app-shell-bar'
30
+
31
+ const RAIL_COLUMNS = `${sizes.sidebarW} minmax(0, 1fr)`
32
+ const NARROW_AREAS = '"bar" "main"'
33
+ const WIDE_AREAS = '"rail bar" "rail main"'
34
+
35
+ export const styles = css.create({
36
+ // Hosts the container query: a container cannot query itself.
37
+ container: {
38
+ containerType: 'inline-size',
39
+ position: 'relative',
40
+ minWidth: 0,
41
+ },
42
+ // The page canvas of the app, in the theme.
43
+ grid: {
44
+ display: 'grid',
45
+ minHeight: '100dvh',
46
+ color: colors.text,
47
+ backgroundColor: colors.bg,
48
+ gridTemplateRows: 'auto 1fr',
49
+ },
50
+ gridSm: {
51
+ gridTemplateColumns: {default: 'minmax(0, 1fr)', [WIDE_SM]: RAIL_COLUMNS},
52
+ gridTemplateAreas: {default: NARROW_AREAS, [WIDE_SM]: WIDE_AREAS},
53
+ '--duro-app-shell-bar': {default: sizes.appBarH, [WIDE_SM]: 'initial'},
54
+ },
55
+ gridMd: {
56
+ gridTemplateColumns: {default: 'minmax(0, 1fr)', [WIDE_MD]: RAIL_COLUMNS},
57
+ gridTemplateAreas: {default: NARROW_AREAS, [WIDE_MD]: WIDE_AREAS},
58
+ '--duro-app-shell-bar': {default: sizes.appBarH, [WIDE_MD]: 'initial'},
59
+ },
60
+ gridLg: {
61
+ gridTemplateColumns: {default: 'minmax(0, 1fr)', [WIDE_LG]: RAIL_COLUMNS},
62
+ gridTemplateAreas: {default: NARROW_AREAS, [WIDE_LG]: WIDE_AREAS},
63
+ '--duro-app-shell-bar': {default: sizes.appBarH, [WIDE_LG]: 'initial'},
64
+ },
65
+
66
+ // Holds the skip link, the first tab stop: off screen until the link
67
+ // has focus.
68
+ skip: {
69
+ position: 'absolute',
70
+ top: spacing.sm,
71
+ left: spacing.sm,
72
+ zIndex: layers.floatingRaised,
73
+ paddingTop: spacing.xs,
74
+ paddingBottom: spacing.xs,
75
+ paddingLeft: spacing.sm,
76
+ paddingRight: spacing.sm,
77
+ borderRadius: radii.sm,
78
+ backgroundColor: colors.bgCard,
79
+ transform: {default: 'translateY(-200%)', ':focus-within': 'none'},
80
+ opacity: {default: 0, ':focus-within': 1},
81
+ },
82
+
83
+ // The rail scrolls on its own beside the page: the brand at the top, the
84
+ // navigation in between, the footer at its foot.
85
+ rail: {
86
+ gridArea: 'rail',
87
+ alignSelf: 'start',
88
+ position: 'sticky',
89
+ top: 0,
90
+ height: '100dvh',
91
+ flexDirection: 'column',
92
+ minWidth: 0,
93
+ backgroundColor: colors.bgCard,
94
+ borderRightWidth: borders.hairline,
95
+ borderRightStyle: 'solid',
96
+ borderRightColor: colors.border,
97
+ },
98
+ railSm: {display: {default: 'none', [WIDE_SM]: 'flex'}},
99
+ railMd: {display: {default: 'none', [WIDE_MD]: 'flex'}},
100
+ railLg: {display: {default: 'none', [WIDE_LG]: 'flex'}},
101
+ railBrand: {
102
+ flexShrink: 0,
103
+ paddingTop: spacing.md,
104
+ paddingBottom: spacing.sm,
105
+ paddingLeft: spacing.md,
106
+ paddingRight: spacing.md,
107
+ },
108
+ railBody: {
109
+ flexGrow: 1,
110
+ minHeight: 0,
111
+ overflowY: 'auto',
112
+ overscrollBehavior: 'contain',
113
+ },
114
+ railFooter: {
115
+ flexShrink: 0,
116
+ paddingTop: spacing.sm,
117
+ paddingBottom: spacing.sm,
118
+ paddingLeft: spacing.md,
119
+ paddingRight: spacing.md,
120
+ borderTopWidth: borders.hairline,
121
+ borderTopStyle: 'solid',
122
+ borderTopColor: colors.border,
123
+ },
124
+ drawerFooter: {
125
+ marginTop: spacing.md,
126
+ paddingTop: spacing.md,
127
+ borderTopWidth: borders.hairline,
128
+ borderTopStyle: 'solid',
129
+ borderTopColor: colors.border,
130
+ },
131
+
132
+ // The bar: the banner. Below the collapse point it is sticky and exactly
133
+ // appBarH tall, so the page never moves under it and an Aside can stick
134
+ // right below it; above, it is the Header's row and scrolls with the page.
135
+ bar: {
136
+ gridArea: 'bar',
137
+ display: 'flex',
138
+ alignItems: 'center',
139
+ gap: spacing.sm,
140
+ minWidth: 0,
141
+ backgroundColor: colors.bgCard,
142
+ borderBottomWidth: borders.hairline,
143
+ borderBottomStyle: 'solid',
144
+ borderBottomColor: colors.border,
145
+ },
146
+ barSm: {
147
+ position: {default: 'sticky', [WIDE_SM]: 'static'},
148
+ top: 0,
149
+ zIndex: {default: layers.floating, [WIDE_SM]: 'auto'},
150
+ height: {default: sizes.appBarH, [WIDE_SM]: 'auto'},
151
+ paddingTop: {default: spacing.sm, [WIDE_SM]: spacing.ms},
152
+ paddingBottom: {default: spacing.sm, [WIDE_SM]: spacing.ms},
153
+ paddingLeft: {default: spacing.md, [WIDE_SM]: spacing.lg},
154
+ paddingRight: {default: spacing.md, [WIDE_SM]: spacing.lg},
155
+ },
156
+ barMd: {
157
+ position: {default: 'sticky', [WIDE_MD]: 'static'},
158
+ top: 0,
159
+ zIndex: {default: layers.floating, [WIDE_MD]: 'auto'},
160
+ height: {default: sizes.appBarH, [WIDE_MD]: 'auto'},
161
+ paddingTop: {default: spacing.sm, [WIDE_MD]: spacing.ms},
162
+ paddingBottom: {default: spacing.sm, [WIDE_MD]: spacing.ms},
163
+ paddingLeft: {default: spacing.md, [WIDE_MD]: spacing.lg},
164
+ paddingRight: {default: spacing.md, [WIDE_MD]: spacing.lg},
165
+ },
166
+ barLg: {
167
+ position: {default: 'sticky', [WIDE_LG]: 'static'},
168
+ top: 0,
169
+ zIndex: {default: layers.floating, [WIDE_LG]: 'auto'},
170
+ height: {default: sizes.appBarH, [WIDE_LG]: 'auto'},
171
+ paddingTop: {default: spacing.sm, [WIDE_LG]: spacing.ms},
172
+ paddingBottom: {default: spacing.sm, [WIDE_LG]: spacing.ms},
173
+ paddingLeft: {default: spacing.md, [WIDE_LG]: spacing.lg},
174
+ paddingRight: {default: spacing.md, [WIDE_LG]: spacing.lg},
175
+ },
176
+ // Without a Header there is nothing to show above the collapse point.
177
+ barEmptySm: {display: {default: 'flex', [WIDE_SM]: 'none'}},
178
+ barEmptyMd: {display: {default: 'flex', [WIDE_MD]: 'none'}},
179
+ barEmptyLg: {display: {default: 'flex', [WIDE_LG]: 'none'}},
180
+ // The Menu button and the bar's copy of the brand: below the collapse
181
+ // point only (display: none takes the hidden copy out of the a11y tree).
182
+ narrowSm: {display: {default: 'flex', [WIDE_SM]: 'none'}},
183
+ narrowMd: {display: {default: 'flex', [WIDE_MD]: 'none'}},
184
+ narrowLg: {display: {default: 'flex', [WIDE_LG]: 'none'}},
185
+ narrowItem: {
186
+ flexShrink: 0,
187
+ alignItems: 'center',
188
+ minWidth: 0,
189
+ },
190
+ // The Header's content, after the Menu button and the brand.
191
+ header: {
192
+ display: 'flex',
193
+ alignItems: 'center',
194
+ gap: spacing.sm,
195
+ flexGrow: 1,
196
+ minWidth: 0,
197
+ },
198
+
199
+ main: {
200
+ gridArea: 'main',
201
+ minWidth: 0,
202
+ paddingTop: spacing.lg,
203
+ paddingBottom: spacing.lg,
204
+ // Focused by the skip link: no ring on a region that is not a control.
205
+ outlineStyle: 'none',
206
+ },
207
+ mainSm: {
208
+ paddingLeft: {default: spacing.md, [WIDE_SM]: spacing.lg},
209
+ paddingRight: {default: spacing.md, [WIDE_SM]: spacing.lg},
210
+ },
211
+ mainMd: {
212
+ paddingLeft: {default: spacing.md, [WIDE_MD]: spacing.lg},
213
+ paddingRight: {default: spacing.md, [WIDE_MD]: spacing.lg},
214
+ },
215
+ mainLg: {
216
+ paddingLeft: {default: spacing.md, [WIDE_LG]: spacing.lg},
217
+ paddingRight: {default: spacing.md, [WIDE_LG]: spacing.lg},
218
+ },
219
+ })
@@ -0,0 +1,33 @@
1
+ import type {ComponentMeta} from '../component-meta'
2
+
3
+ export const meta: ComponentMeta = {
4
+ description:
5
+ 'Content beside the main reading column — a table of contents, related pages: a labelled aside landmark that sticks within the viewport while the page scrolls and scrolls on its own when taller (offset from a spacing token, the raised layer). Forwards ref to the aside element.',
6
+ whenToUse: [
7
+ 'The second track of Grid layout="content-aside": an "On this page" list, related links',
8
+ 'Secondary content that should stay in view beside a long page',
9
+ ],
10
+ whenNotToUse: [
11
+ 'Inspecting a selected record — use DetailPanel',
12
+ 'A navigation rail for the app — use AppShell.Rail',
13
+ ],
14
+ relatedTo: [
15
+ {
16
+ component: 'Grid',
17
+ kind: 'composition',
18
+ relationship:
19
+ 'Grid layout="content-aside" gives Aside its asideW track and stacks it below md',
20
+ },
21
+ {
22
+ component: 'TableOfContents',
23
+ kind: 'composition',
24
+ relationship: 'The usual content of an Aside on a docs page',
25
+ },
26
+ ],
27
+ example: `<Grid layout="content-aside" gap="xl">
28
+ <Prose>{body}</Prose>
29
+ <Aside aria-label="Page outline">
30
+ <TableOfContents.Root aria-label="On this page" title="On this page">…</TableOfContents.Root>
31
+ </Aside>
32
+ </Grid>`,
33
+ }