@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.
- package/dist/{Table-Dj7PXqO_.js → Table-CAjPFgTX.js} +397 -394
- package/dist/Table-CAjPFgTX.js.map +1 -0
- package/dist/{compiler-runtime-B2IcI8_O.js → compiler-runtime-BuugS4D0.js} +67 -59
- package/dist/{compiler-runtime-B2IcI8_O.js.map → compiler-runtime-BuugS4D0.js.map} +1 -1
- package/dist/components/AppShell/AppShell.d.ts +74 -0
- package/dist/components/AppShell/AppShell.d.ts.map +1 -0
- package/dist/components/AppShell/AppShell.meta.d.ts +3 -0
- package/dist/components/AppShell/AppShell.meta.d.ts.map +1 -0
- package/dist/components/AppShell/AppShell.stories.d.ts +192 -0
- package/dist/components/AppShell/AppShell.stories.d.ts.map +1 -0
- package/dist/components/AppShell/styles.css.d.ts +187 -0
- package/dist/components/AppShell/styles.css.d.ts.map +1 -0
- package/dist/components/Aside/Aside.d.ts +23 -0
- package/dist/components/Aside/Aside.d.ts.map +1 -0
- package/dist/components/Aside/Aside.meta.d.ts +3 -0
- package/dist/components/Aside/Aside.meta.d.ts.map +1 -0
- package/dist/components/Aside/Aside.stories.d.ts +45 -0
- package/dist/components/Aside/Aside.stories.d.ts.map +1 -0
- package/dist/components/Aside/styles.css.d.ts +44 -0
- package/dist/components/Aside/styles.css.d.ts.map +1 -0
- package/dist/components/Button/styles.css.d.ts +2 -0
- package/dist/components/Button/styles.css.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +3 -3
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.d.ts +29 -0
- package/dist/components/CodeBlock/CodeBlock.d.ts.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.meta.d.ts +3 -0
- package/dist/components/CodeBlock/CodeBlock.meta.d.ts.map +1 -0
- package/dist/components/CodeBlock/CodeBlock.stories.d.ts +73 -0
- package/dist/components/CodeBlock/CodeBlock.stories.d.ts.map +1 -0
- package/dist/components/CodeBlock/styles.css.d.ts +41 -0
- package/dist/components/CodeBlock/styles.css.d.ts.map +1 -0
- package/dist/components/Grid/Grid.d.ts +13 -4
- package/dist/components/Grid/Grid.d.ts.map +1 -1
- package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
- package/dist/components/Grid/Grid.stories.d.ts +28 -0
- package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
- package/dist/components/Grid/styles.css.d.ts +3 -0
- package/dist/components/Grid/styles.css.d.ts.map +1 -1
- package/dist/components/Heading/Heading.d.ts +3 -1
- package/dist/components/Heading/Heading.d.ts.map +1 -1
- package/dist/components/Heading/Heading.stories.d.ts +9 -0
- package/dist/components/Heading/Heading.stories.d.ts.map +1 -0
- package/dist/components/LinkButton/LinkButton.stories.d.ts +7 -0
- package/dist/components/LinkButton/LinkButton.stories.d.ts.map +1 -1
- package/dist/components/LinkButton/styles.css.d.ts +2 -0
- package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
- package/dist/components/LiveRegion/LiveRegion.d.ts +28 -0
- package/dist/components/LiveRegion/LiveRegion.d.ts.map +1 -0
- package/dist/components/LiveRegion/LiveRegion.meta.d.ts +3 -0
- package/dist/components/LiveRegion/LiveRegion.meta.d.ts.map +1 -0
- package/dist/components/LiveRegion/LiveRegion.stories.d.ts +66 -0
- package/dist/components/LiveRegion/LiveRegion.stories.d.ts.map +1 -0
- package/dist/components/Menu/Menu.stories.d.ts +7 -0
- package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
- package/dist/components/Menu/styles.css.d.ts +2 -0
- package/dist/components/Menu/styles.css.d.ts.map +1 -1
- package/dist/components/PageNav/styles.css.d.ts +1 -1
- package/dist/components/Prose/Prose.d.ts +17 -0
- package/dist/components/Prose/Prose.d.ts.map +1 -0
- package/dist/components/Prose/Prose.meta.d.ts +3 -0
- package/dist/components/Prose/Prose.meta.d.ts.map +1 -0
- package/dist/components/Prose/Prose.stories.d.ts +63 -0
- package/dist/components/Prose/Prose.stories.d.ts.map +1 -0
- package/dist/components/SideNav/SideNav.d.ts +7 -1
- package/dist/components/SideNav/SideNav.d.ts.map +1 -1
- package/dist/components/SideNav/SideNav.stories.d.ts +3 -0
- package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
- package/dist/components/Time/Time.d.ts +26 -0
- package/dist/components/Time/Time.d.ts.map +1 -0
- package/dist/components/Time/Time.meta.d.ts +3 -0
- package/dist/components/Time/Time.meta.d.ts.map +1 -0
- package/dist/components/Time/Time.stories.d.ts +77 -0
- package/dist/components/Time/Time.stories.d.ts.map +1 -0
- package/dist/components/Time/relative.d.ts +27 -0
- package/dist/components/Time/relative.d.ts.map +1 -0
- package/dist/form.js +1 -1
- package/dist/hooks/useContainerBelow.d.ts +18 -0
- package/dist/hooks/useContainerBelow.d.ts.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4870 -4183
- package/dist/index.js.map +1 -1
- package/dist/table.js +2 -2
- package/package.json +2 -2
- package/src/components/AppShell/AppShell.meta.ts +62 -0
- package/src/components/AppShell/AppShell.stories.tsx +825 -0
- package/src/components/AppShell/AppShell.tsx +346 -0
- package/src/components/AppShell/styles.css.ts +219 -0
- package/src/components/Aside/Aside.meta.ts +33 -0
- package/src/components/Aside/Aside.stories.tsx +106 -0
- package/src/components/Aside/Aside.tsx +42 -0
- package/src/components/Aside/styles.css.ts +60 -0
- package/src/components/Button/styles.css.ts +7 -2
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +4 -5
- package/src/components/CodeBlock/CodeBlock.meta.ts +29 -0
- package/src/components/CodeBlock/CodeBlock.stories.tsx +192 -0
- package/src/components/CodeBlock/CodeBlock.tsx +99 -0
- package/src/components/CodeBlock/styles.css.ts +48 -0
- package/src/components/Grid/Grid.meta.ts +2 -1
- package/src/components/Grid/Grid.stories.tsx +202 -3
- package/src/components/Grid/Grid.tsx +35 -6
- package/src/components/Grid/styles.css.ts +11 -0
- package/src/components/Heading/Heading.stories.tsx +51 -0
- package/src/components/Heading/Heading.tsx +38 -7
- package/src/components/LinkButton/LinkButton.stories.tsx +41 -1
- package/src/components/LinkButton/styles.css.ts +6 -2
- package/src/components/LiveRegion/LiveRegion.meta.ts +29 -0
- package/src/components/LiveRegion/LiveRegion.stories.tsx +139 -0
- package/src/components/LiveRegion/LiveRegion.tsx +48 -0
- package/src/components/Menu/Menu.stories.tsx +62 -0
- package/src/components/Menu/styles.css.ts +6 -3
- package/src/components/Prose/Prose.meta.ts +32 -0
- package/src/components/Prose/Prose.stories.tsx +240 -0
- package/src/components/Prose/Prose.tsx +25 -0
- package/src/components/Prose/prose.css +179 -0
- package/src/components/SideNav/SideNav.stories.tsx +29 -0
- package/src/components/SideNav/SideNav.tsx +14 -2
- package/src/components/Time/Time.meta.ts +17 -0
- package/src/components/Time/Time.stories.tsx +173 -0
- package/src/components/Time/Time.tsx +33 -0
- package/src/components/Time/relative.ts +89 -0
- package/src/hooks/useContainerBelow.ts +39 -0
- package/src/index.ts +13 -1
- package/src/strict.css +4 -1
- package/dist/Table-Dj7PXqO_.js.map +0 -1
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Prose — styles for HTML that Duro does not author: rendered Markdown, CMS
|
|
3
|
+
* output. StyleX cannot write descendant rules, so these ship as plain CSS in
|
|
4
|
+
* dist/index.css. Only var(--duro-*) tokens: the check in
|
|
5
|
+
* packages/ui/test/prose-css.test.ts fails on any raw length or colour.
|
|
6
|
+
*
|
|
7
|
+
* Two things keep it out of everyone's way:
|
|
8
|
+
*
|
|
9
|
+
* - The `duro-prose` layer sits after `reset` (so these margins beat the
|
|
10
|
+
* reset's `margin: 0`) and before StyleX's own layers (strict.css declares
|
|
11
|
+
* the order). A Duro component rendered inside Prose (a TextLink, a
|
|
12
|
+
* CodeBlock) keeps its own styles, and any unlayered app CSS — a CSS
|
|
13
|
+
* Module rule — wins over every rule here.
|
|
14
|
+
* - Every selector is wrapped in :where(), so the rules weigh nothing even
|
|
15
|
+
* against an app's own layered CSS.
|
|
16
|
+
*
|
|
17
|
+
* Scoped to the Prose element: `data-duro-prose` (what the component renders;
|
|
18
|
+
* react-strict-dom elements take no className) or a `duro-prose` class.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
@layer reset, duro-prose;
|
|
22
|
+
|
|
23
|
+
@layer duro-prose {
|
|
24
|
+
:where(.duro-prose, [data-duro-prose]) {
|
|
25
|
+
color: var(--duro-color-text);
|
|
26
|
+
font-family: var(--duro-typography-font-family);
|
|
27
|
+
font-size: var(--duro-type-scale-font-size4);
|
|
28
|
+
line-height: var(--duro-type-scale-line-height4);
|
|
29
|
+
overflow-wrap: break-word;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* Flow: one gap between blocks, none before the first. */
|
|
33
|
+
:where(.duro-prose, [data-duro-prose]) > :where(* + *) {
|
|
34
|
+
margin-block-start: var(--duro-spacing-md);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Headings: the Heading presets, with more room above than below. */
|
|
38
|
+
:where(.duro-prose, [data-duro-prose]) :where(h1, h2, h3, h4, h5, h6) {
|
|
39
|
+
font-family: var(--duro-typography-font-family);
|
|
40
|
+
letter-spacing: var(--duro-type-scale-letter-spacing-tight);
|
|
41
|
+
margin-block-start: var(--duro-spacing-xl);
|
|
42
|
+
margin-block-end: var(--duro-spacing-sm);
|
|
43
|
+
/* A table-of-contents jump lands with room above the heading, and below
|
|
44
|
+
AppShell's sticky bar when there is one (it sets --duro-app-shell-bar
|
|
45
|
+
below its collapse point; elsewhere the fallback adds nothing). */
|
|
46
|
+
scroll-margin-top: calc(var(--duro-spacing-lg) + var(--duro-app-shell-bar, 0px));
|
|
47
|
+
}
|
|
48
|
+
:where(.duro-prose, [data-duro-prose]) :where(h1) {
|
|
49
|
+
font-size: var(--duro-type-scale-font-size9);
|
|
50
|
+
line-height: var(--duro-type-scale-line-height9);
|
|
51
|
+
font-weight: var(--duro-typography-font-weight-bold);
|
|
52
|
+
}
|
|
53
|
+
/* h2 down follow the picked docs artboard (Reader.dc.html): a reading
|
|
54
|
+
column's sections are a step below the page title, not a second one. */
|
|
55
|
+
:where(.duro-prose, [data-duro-prose]) :where(h2) {
|
|
56
|
+
font-size: var(--duro-type-scale-font-size6);
|
|
57
|
+
line-height: var(--duro-type-scale-line-height6);
|
|
58
|
+
font-weight: var(--duro-typography-font-weight-semibold);
|
|
59
|
+
margin-block-start: var(--duro-spacing-lg);
|
|
60
|
+
}
|
|
61
|
+
:where(.duro-prose, [data-duro-prose]) :where(h3) {
|
|
62
|
+
font-size: var(--duro-type-scale-font-size5);
|
|
63
|
+
line-height: var(--duro-type-scale-line-height5);
|
|
64
|
+
font-weight: var(--duro-typography-font-weight-semibold);
|
|
65
|
+
margin-block-start: var(--duro-spacing-lg);
|
|
66
|
+
}
|
|
67
|
+
:where(.duro-prose, [data-duro-prose]) :where(h4, h5, h6) {
|
|
68
|
+
font-size: var(--duro-type-scale-font-size4);
|
|
69
|
+
line-height: var(--duro-type-scale-line-height4);
|
|
70
|
+
font-weight: var(--duro-typography-font-weight-semibold);
|
|
71
|
+
margin-block-start: var(--duro-spacing-md);
|
|
72
|
+
}
|
|
73
|
+
:where(.duro-prose, [data-duro-prose]) > :where(:first-child) {
|
|
74
|
+
margin-block-start: 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Lists: markers back (the reset removes the indent they hang in). */
|
|
78
|
+
:where(.duro-prose, [data-duro-prose]) :where(ul, ol) {
|
|
79
|
+
padding-inline-start: var(--duro-spacing-lg);
|
|
80
|
+
}
|
|
81
|
+
:where(.duro-prose, [data-duro-prose]) :where(ul) {
|
|
82
|
+
list-style-type: disc;
|
|
83
|
+
}
|
|
84
|
+
:where(.duro-prose, [data-duro-prose]) :where(ol) {
|
|
85
|
+
list-style-type: decimal;
|
|
86
|
+
}
|
|
87
|
+
:where(.duro-prose, [data-duro-prose]) :where(li + li, li > ul, li > ol) {
|
|
88
|
+
margin-block-start: var(--duro-spacing-xs);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Links: the accent, underlined so they read as links in running text. */
|
|
92
|
+
:where(.duro-prose, [data-duro-prose]) :where(a) {
|
|
93
|
+
color: var(--duro-color-accent);
|
|
94
|
+
text-decoration-line: underline;
|
|
95
|
+
text-underline-offset: var(--duro-micro-spacing-px2);
|
|
96
|
+
}
|
|
97
|
+
:where(.duro-prose, [data-duro-prose]) :where(a:hover) {
|
|
98
|
+
color: var(--duro-color-accent-hover);
|
|
99
|
+
}
|
|
100
|
+
:where(.duro-prose, [data-duro-prose]) :where(a:focus-visible) {
|
|
101
|
+
outline: var(--duro-border-focus-ring) solid var(--duro-color-accent);
|
|
102
|
+
outline-offset: var(--duro-border-focus-offset);
|
|
103
|
+
border-radius: var(--duro-radius-xs);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
:where(.duro-prose, [data-duro-prose]) :where(strong, b) {
|
|
107
|
+
font-weight: var(--duro-typography-font-weight-semibold);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
:where(.duro-prose, [data-duro-prose]) :where(blockquote) {
|
|
111
|
+
padding-inline-start: var(--duro-spacing-md);
|
|
112
|
+
border-inline-start: var(--duro-border-accent) solid var(--duro-color-border);
|
|
113
|
+
color: var(--duro-color-text-muted);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/* Inline code; a heading keeps its own size around it. */
|
|
117
|
+
:where(.duro-prose, [data-duro-prose]) :where(code) {
|
|
118
|
+
font-family: var(--duro-typography-font-family-mono);
|
|
119
|
+
font-size: var(--duro-type-scale-font-size3);
|
|
120
|
+
padding-inline: var(--duro-spacing-xs);
|
|
121
|
+
background-color: var(--duro-color-bg-card);
|
|
122
|
+
border: var(--duro-border-hairline) solid var(--duro-color-border);
|
|
123
|
+
border-radius: var(--duro-radius-xs);
|
|
124
|
+
}
|
|
125
|
+
:where(.duro-prose, [data-duro-prose]) :where(h1, h2, h3, h4, h5, h6) :where(code) {
|
|
126
|
+
font-size: inherit;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* A code block outside CodeBlock (CodeBlock styles its own). */
|
|
130
|
+
:where(.duro-prose, [data-duro-prose]) :where(pre) {
|
|
131
|
+
font-family: var(--duro-typography-font-family-mono);
|
|
132
|
+
font-size: var(--duro-type-scale-font-size3);
|
|
133
|
+
line-height: var(--duro-type-scale-line-height3);
|
|
134
|
+
padding: var(--duro-spacing-md);
|
|
135
|
+
overflow-x: auto;
|
|
136
|
+
background-color: var(--duro-color-bg-card);
|
|
137
|
+
border: var(--duro-border-hairline) solid var(--duro-color-border);
|
|
138
|
+
border-radius: var(--duro-radius-md);
|
|
139
|
+
}
|
|
140
|
+
:where(.duro-prose, [data-duro-prose]) :where(pre code) {
|
|
141
|
+
padding: 0;
|
|
142
|
+
font-size: inherit;
|
|
143
|
+
background-color: transparent;
|
|
144
|
+
border: 0;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* Tables as the docs artboard draws them: the column's full width,
|
|
148
|
+
horizontal rules only, a muted header. A long cell wraps rather than
|
|
149
|
+
widening the page (a full-width table cannot also scroll on its own). */
|
|
150
|
+
:where(.duro-prose, [data-duro-prose]) :where(table) {
|
|
151
|
+
width: 100%;
|
|
152
|
+
border-collapse: collapse;
|
|
153
|
+
font-size: var(--duro-type-scale-font-size3);
|
|
154
|
+
line-height: var(--duro-type-scale-line-height3);
|
|
155
|
+
}
|
|
156
|
+
:where(.duro-prose, [data-duro-prose]) :where(th, td) {
|
|
157
|
+
padding: var(--duro-spacing-xs) var(--duro-spacing-sm);
|
|
158
|
+
border-block-end: var(--duro-border-hairline) solid var(--duro-color-border);
|
|
159
|
+
text-align: start;
|
|
160
|
+
vertical-align: top;
|
|
161
|
+
overflow-wrap: anywhere;
|
|
162
|
+
}
|
|
163
|
+
:where(.duro-prose, [data-duro-prose]) :where(th) {
|
|
164
|
+
color: var(--duro-color-text-muted);
|
|
165
|
+
font-weight: var(--duro-typography-font-weight-medium);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
:where(.duro-prose, [data-duro-prose]) :where(img) {
|
|
169
|
+
max-width: 100%;
|
|
170
|
+
height: auto;
|
|
171
|
+
border-radius: var(--duro-radius-sm);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
:where(.duro-prose, [data-duro-prose]) :where(hr) {
|
|
175
|
+
border: 0;
|
|
176
|
+
border-block-start: var(--duro-border-hairline) solid var(--duro-color-border);
|
|
177
|
+
margin-block: var(--duro-spacing-xl);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -2,6 +2,7 @@ import type {Meta, StoryObj} from '@storybook/react'
|
|
|
2
2
|
import {expect, fn} from 'storybook/test'
|
|
3
3
|
import {SideNav} from './SideNav'
|
|
4
4
|
import {Icon} from '../Icon/Icon'
|
|
5
|
+
import {onThemeSurface} from '../../docs/themedSurface'
|
|
5
6
|
|
|
6
7
|
const meta: Meta<typeof SideNav.Root> = {
|
|
7
8
|
title: 'Components/SideNav',
|
|
@@ -200,3 +201,31 @@ export const FlatWithIcons: Story = {
|
|
|
200
201
|
await expect(identities).toHaveAttribute('aria-current', 'page')
|
|
201
202
|
},
|
|
202
203
|
}
|
|
204
|
+
|
|
205
|
+
/** `aria-label` names the landmark, so a page with a breadcrumb or a pager
|
|
206
|
+
* beside it still lists the side navigation by name. */
|
|
207
|
+
export const Named: Story = {
|
|
208
|
+
parameters: {
|
|
209
|
+
a11y: {
|
|
210
|
+
test: 'error',
|
|
211
|
+
// holds-until: https://github.com/fredericrous/duro-design-system/issues/79
|
|
212
|
+
// — the selected item's accent on bgCardHover is 6.76:1, under AAA's
|
|
213
|
+
// 7:1 (AA passes). Re-enable this rule when that pair reaches 7:1.
|
|
214
|
+
options: {rules: {'color-contrast-enhanced': {enabled: false}}},
|
|
215
|
+
},
|
|
216
|
+
},
|
|
217
|
+
decorators: [onThemeSurface],
|
|
218
|
+
render: (args) => (
|
|
219
|
+
<SideNav.Root {...args} aria-label="Settings" defaultValue="profile">
|
|
220
|
+
<SideNav.Section label="Account">
|
|
221
|
+
<SideNav.Item value="profile">Profile</SideNav.Item>
|
|
222
|
+
<SideNav.Item value="security">Security</SideNav.Item>
|
|
223
|
+
</SideNav.Section>
|
|
224
|
+
</SideNav.Root>
|
|
225
|
+
),
|
|
226
|
+
play: async ({canvas}) => {
|
|
227
|
+
const nav = canvas.getByRole('navigation', {name: 'Settings'})
|
|
228
|
+
await expect(nav).toHaveAttribute('aria-label', 'Settings')
|
|
229
|
+
await expect(canvas.getByRole('button', {name: 'Profile'})).toBeInTheDocument()
|
|
230
|
+
},
|
|
231
|
+
}
|
|
@@ -34,12 +34,24 @@ import {SideNavContext, useSideNav} from './SideNavContext'
|
|
|
34
34
|
|
|
35
35
|
interface RootProps {
|
|
36
36
|
children: ReactNode
|
|
37
|
+
/**
|
|
38
|
+
* Names the navigation landmark ("Main", "Settings"). Give one whenever
|
|
39
|
+
* the page has another nav (a breadcrumb, a pager): screen readers list
|
|
40
|
+
* landmarks by name.
|
|
41
|
+
*/
|
|
42
|
+
'aria-label'?: string
|
|
37
43
|
value?: string
|
|
38
44
|
defaultValue?: string
|
|
39
45
|
onValueChange?: (value: string) => void
|
|
40
46
|
}
|
|
41
47
|
|
|
42
|
-
function Root({
|
|
48
|
+
function Root({
|
|
49
|
+
children,
|
|
50
|
+
'aria-label': ariaLabel,
|
|
51
|
+
value: controlledValue,
|
|
52
|
+
defaultValue,
|
|
53
|
+
onValueChange,
|
|
54
|
+
}: RootProps) {
|
|
43
55
|
const [activeValue, onSelect] = useControllableValue<string | null>(
|
|
44
56
|
controlledValue,
|
|
45
57
|
defaultValue ?? null,
|
|
@@ -98,7 +110,7 @@ function Root({children, value: controlledValue, defaultValue, onValueChange}: R
|
|
|
98
110
|
orderRef,
|
|
99
111
|
}}
|
|
100
112
|
>
|
|
101
|
-
<html.nav role="navigation" style={styles.root}>
|
|
113
|
+
<html.nav role="navigation" aria-label={ariaLabel} style={styles.root}>
|
|
102
114
|
{children}
|
|
103
115
|
</html.nav>
|
|
104
116
|
</SideNavContext.Provider>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'A date or time in running text as a <time> element: the visible text comes from you, the exact instant goes in dateTime (an ISO string) and title. relative(date, {now, locale}) gives the text on Intl.RelativeTimeFormat ("3 days ago", "il y a 3 jours"); now is required, so the server and the client render the same string. Web only.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'When something happened: "Updated 3 days ago", a commit date, a last-indexed time',
|
|
8
|
+
'Any date shown as relative text whose exact value should stay available',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'A date the person edits — use an Input',
|
|
12
|
+
'A live countdown or a clock — Time renders once from the now you pass',
|
|
13
|
+
],
|
|
14
|
+
example: `<Time dateTime={doc.lastCommitAt} title={formatFull(doc.lastCommitAt, locale)}>
|
|
15
|
+
{relative(doc.lastCommitAt, {now: loaderNow, locale})}
|
|
16
|
+
</Time>`,
|
|
17
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect, waitFor} from 'storybook/test'
|
|
3
|
+
import {html} from 'react-strict-dom'
|
|
4
|
+
import {renderToString} from 'react-dom/server'
|
|
5
|
+
import {hydrateRoot} from 'react-dom/client'
|
|
6
|
+
import {Time} from './Time'
|
|
7
|
+
import {relative} from './relative'
|
|
8
|
+
import {Stack} from '../Stack/Stack'
|
|
9
|
+
import {Text} from '../Text/Text'
|
|
10
|
+
import {onThemeSurface} from '../../docs/themedSurface'
|
|
11
|
+
|
|
12
|
+
// A fixed instant, as a loader would pass it: never the clock during render.
|
|
13
|
+
const NOW = Date.UTC(2026, 9, 8, 12, 0, 0)
|
|
14
|
+
const COMMIT = '2026-10-05T09:30:00.000Z'
|
|
15
|
+
const DAY_MS = 24 * 60 * 60 * 1000
|
|
16
|
+
|
|
17
|
+
type Locale = 'en' | 'fr' | 'de'
|
|
18
|
+
|
|
19
|
+
interface TimeArgs {
|
|
20
|
+
/** The instant shown, in days from `now` (negative: in the past). */
|
|
21
|
+
offsetDays: number
|
|
22
|
+
/** The locale `relative` and the title use. */
|
|
23
|
+
locale: Locale
|
|
24
|
+
/** `auto` says "yesterday" where the locale has a word; `always` keeps the number. */
|
|
25
|
+
numeric: 'auto' | 'always'
|
|
26
|
+
/** The text before the date. */
|
|
27
|
+
label: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const meta = {
|
|
31
|
+
title: 'Components/Time',
|
|
32
|
+
args: {offsetDays: -3, locale: 'en', numeric: 'auto', label: 'Updated'},
|
|
33
|
+
argTypes: {
|
|
34
|
+
offsetDays: {control: {type: 'range', min: -400, max: 400, step: 1}},
|
|
35
|
+
locale: {
|
|
36
|
+
control: {type: 'select', labels: {en: 'English', fr: 'Français', de: 'Deutsch'}},
|
|
37
|
+
options: ['en', 'fr', 'de'],
|
|
38
|
+
},
|
|
39
|
+
numeric: {control: 'radio', options: ['auto', 'always']},
|
|
40
|
+
label: {control: 'text'},
|
|
41
|
+
},
|
|
42
|
+
parameters: {a11y: {test: 'error'}},
|
|
43
|
+
decorators: [onThemeSurface],
|
|
44
|
+
render: ({offsetDays, locale, numeric, label}) => {
|
|
45
|
+
const iso = new Date(NOW + offsetDays * DAY_MS).toISOString()
|
|
46
|
+
return (
|
|
47
|
+
<Text variant="bodySm" color="muted">
|
|
48
|
+
{label}{' '}
|
|
49
|
+
<Time dateTime={iso} title={full(iso, locale)}>
|
|
50
|
+
{relative(iso, {now: NOW, locale, numeric})}
|
|
51
|
+
</Time>
|
|
52
|
+
</Text>
|
|
53
|
+
)
|
|
54
|
+
},
|
|
55
|
+
} satisfies Meta<TimeArgs>
|
|
56
|
+
|
|
57
|
+
export default meta
|
|
58
|
+
type Story = StoryObj<typeof meta>
|
|
59
|
+
|
|
60
|
+
const TEST_ONLY = ['!dev', '!autodocs']
|
|
61
|
+
|
|
62
|
+
function full(iso: string, locale: string) {
|
|
63
|
+
return new Intl.DateTimeFormat(locale, {
|
|
64
|
+
dateStyle: 'long',
|
|
65
|
+
timeStyle: 'short',
|
|
66
|
+
timeZone: 'UTC',
|
|
67
|
+
}).format(new Date(iso))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function Updated({locale, label}: {locale: string; label: string}) {
|
|
71
|
+
return (
|
|
72
|
+
<Text variant="bodySm" color="muted">
|
|
73
|
+
{label}{' '}
|
|
74
|
+
<Time dateTime={COMMIT} title={full(COMMIT, locale)}>
|
|
75
|
+
{relative(COMMIT, {now: NOW, locale})}
|
|
76
|
+
</Time>
|
|
77
|
+
</Text>
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A date relative to a fixed `now` (2026-10-08 12:00 UTC). Move `offsetDays`
|
|
83
|
+
* to walk through the units (days, weeks, months, years), switch `locale`, and
|
|
84
|
+
* `numeric` to always for "1 day ago" instead of "yesterday". Hover the date
|
|
85
|
+
* for the exact instant (title).
|
|
86
|
+
*/
|
|
87
|
+
export const Playground: Story = {}
|
|
88
|
+
|
|
89
|
+
/** Playground's checks, kept off the visible story so a Control change cannot fail them. */
|
|
90
|
+
export const PlaygroundChecks: Story = {
|
|
91
|
+
...Playground,
|
|
92
|
+
tags: TEST_ONLY,
|
|
93
|
+
play: async ({args, canvasElement}) => {
|
|
94
|
+
const time = canvasElement.querySelector('time')!
|
|
95
|
+
const iso = new Date(NOW + args.offsetDays * DAY_MS).toISOString()
|
|
96
|
+
await expect(time).toHaveAttribute('datetime', iso)
|
|
97
|
+
await expect(time).toHaveAttribute('title', full(iso, args.locale))
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The words come from the locale; the exact instant stays in dateTime and title. */
|
|
102
|
+
export const TwoLocales: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<Stack gap="sm">
|
|
105
|
+
<Updated locale="en" label="Updated" />
|
|
106
|
+
<Updated locale="fr" label="Mis à jour" />
|
|
107
|
+
</Stack>
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** TwoLocales' checks, kept off the visible story so a Control change cannot fail them. */
|
|
112
|
+
export const TwoLocalesChecks: Story = {
|
|
113
|
+
...TwoLocales,
|
|
114
|
+
tags: TEST_ONLY,
|
|
115
|
+
play: async ({canvasElement}) => {
|
|
116
|
+
const times = [...canvasElement.querySelectorAll('time')]
|
|
117
|
+
await expect(times).toHaveLength(2)
|
|
118
|
+
await expect(times[0]).toHaveTextContent('3 days ago')
|
|
119
|
+
await expect(times[1]).toHaveTextContent('il y a 3 jours')
|
|
120
|
+
for (const time of times) {
|
|
121
|
+
await expect(time).toHaveAttribute('datetime', COMMIT)
|
|
122
|
+
}
|
|
123
|
+
await expect(times[0]).toHaveAttribute('title', full(COMMIT, 'en'))
|
|
124
|
+
await expect(times[1]).toHaveAttribute('title', full(COMMIT, 'fr'))
|
|
125
|
+
},
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A Date is written as its ISO string. */
|
|
129
|
+
export const FromDate: Story = {
|
|
130
|
+
tags: TEST_ONLY,
|
|
131
|
+
render: () => (
|
|
132
|
+
<Text>
|
|
133
|
+
<Time dateTime={new Date(NOW)}>{relative(NOW, {now: NOW, locale: 'en'})}</Time>
|
|
134
|
+
</Text>
|
|
135
|
+
),
|
|
136
|
+
play: async ({canvasElement}) => {
|
|
137
|
+
const time = canvasElement.querySelector('time')!
|
|
138
|
+
await expect(time).toHaveAttribute('datetime', new Date(NOW).toISOString())
|
|
139
|
+
await expect(time).toHaveTextContent('now')
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Server and client render the same text from the same `now`: hydration
|
|
144
|
+
* reports nothing. It replaces the canvas's content, so it runs as a test only. */
|
|
145
|
+
export const ServerRender: Story = {
|
|
146
|
+
tags: TEST_ONLY,
|
|
147
|
+
render: () => <html.div data-ssr-host="" />,
|
|
148
|
+
play: async ({canvasElement}) => {
|
|
149
|
+
const host = canvasElement.querySelector('[data-ssr-host]') as HTMLElement
|
|
150
|
+
const tree = <Updated locale="en" label="Updated" />
|
|
151
|
+
host.innerHTML = renderToString(tree)
|
|
152
|
+
const serverText = host.textContent
|
|
153
|
+
await expect(host.querySelector('time')).toHaveTextContent('3 days ago')
|
|
154
|
+
|
|
155
|
+
const errors: unknown[] = []
|
|
156
|
+
const consoleError = console.error
|
|
157
|
+
console.error = (...args: unknown[]) => {
|
|
158
|
+
errors.push(args)
|
|
159
|
+
consoleError(...args)
|
|
160
|
+
}
|
|
161
|
+
const root = hydrateRoot(host, tree, {onRecoverableError: (error) => errors.push(error)})
|
|
162
|
+
try {
|
|
163
|
+
// let React finish hydrating before reading the result
|
|
164
|
+
await waitFor(() => expect(host.querySelector('time')).toHaveTextContent('3 days ago'))
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
166
|
+
await expect(host.textContent).toBe(serverText)
|
|
167
|
+
await expect(errors).toEqual([])
|
|
168
|
+
} finally {
|
|
169
|
+
console.error = consoleError
|
|
170
|
+
root.unmount()
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
|
|
3
|
+
interface TimeProps {
|
|
4
|
+
/**
|
|
5
|
+
* The instant, machine-readable: a `Date` (written as its ISO string) or an
|
|
6
|
+
* ISO 8601 string, which is passed through unchanged.
|
|
7
|
+
*/
|
|
8
|
+
dateTime: Date | string
|
|
9
|
+
/**
|
|
10
|
+
* The exact date and time, shown on hover ("8 October 2026, 14:03 UTC"),
|
|
11
|
+
* formatted by you in the reader's locale.
|
|
12
|
+
*/
|
|
13
|
+
title?: string
|
|
14
|
+
/** What the reader sees: a relative label from `relative()`, or a date. */
|
|
15
|
+
children: ReactNode
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Time — a date or time in running text as a `<time>` element: the visible
|
|
20
|
+
* text is yours (often `relative(date, {now, locale})`), the exact instant
|
|
21
|
+
* goes in `dateTime` for machines and `title` for people.
|
|
22
|
+
*
|
|
23
|
+
* Web only. A raw `<time>`: react-strict-dom has no `html.time`, and the
|
|
24
|
+
* element carries no style of its own (it inherits the text around it).
|
|
25
|
+
*/
|
|
26
|
+
export function Time({dateTime, title, children}: TimeProps) {
|
|
27
|
+
const iso = typeof dateTime === 'string' ? dateTime : dateTime.toISOString()
|
|
28
|
+
return (
|
|
29
|
+
<time dateTime={iso} title={title}>
|
|
30
|
+
{children}
|
|
31
|
+
</time>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Pure date arithmetic behind Time: no React, no react-strict-dom, so the
|
|
2
|
+
// unit project tests it in Node.
|
|
3
|
+
|
|
4
|
+
export interface RelativeOptions {
|
|
5
|
+
/**
|
|
6
|
+
* The instant to measure from. Required: read the clock once where the data
|
|
7
|
+
* is loaded (a route loader) and pass the same value to the server and the
|
|
8
|
+
* client render, so both produce the same text. Never `Date.now()` during a
|
|
9
|
+
* render — the client would hydrate a different string.
|
|
10
|
+
*/
|
|
11
|
+
now: Date | number
|
|
12
|
+
/** BCP 47 locale(s) for the words ("3 days ago", "il y a 3 jours"). */
|
|
13
|
+
locale: string | readonly string[]
|
|
14
|
+
/**
|
|
15
|
+
* `auto` (default) says "yesterday" / "tomorrow" / "now" where the locale has
|
|
16
|
+
* a word for it; `always` keeps the number ("1 day ago").
|
|
17
|
+
*/
|
|
18
|
+
numeric?: 'auto' | 'always'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type Step = readonly [Intl.RelativeTimeFormatUnit, number]
|
|
22
|
+
|
|
23
|
+
// Each unit is used while |difference| stays below the next one. Months and
|
|
24
|
+
// years are average lengths: a relative label is a rounded reading, the
|
|
25
|
+
// exact instant goes in the dateTime attribute.
|
|
26
|
+
const SECOND = 1000
|
|
27
|
+
const MINUTE = 60 * SECOND
|
|
28
|
+
const HOUR = 60 * MINUTE
|
|
29
|
+
const DAY = 24 * HOUR
|
|
30
|
+
const WEEK = 7 * DAY
|
|
31
|
+
const MONTH = 30.436875 * DAY
|
|
32
|
+
const YEAR = 365.2425 * DAY
|
|
33
|
+
|
|
34
|
+
const STEPS: readonly Step[] = [
|
|
35
|
+
['second', SECOND],
|
|
36
|
+
['minute', MINUTE],
|
|
37
|
+
['hour', HOUR],
|
|
38
|
+
['day', DAY],
|
|
39
|
+
['week', WEEK],
|
|
40
|
+
['month', MONTH],
|
|
41
|
+
['year', YEAR],
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
const LIMITS: Record<Intl.RelativeTimeFormatUnit, number> = {
|
|
45
|
+
second: MINUTE,
|
|
46
|
+
seconds: MINUTE,
|
|
47
|
+
minute: HOUR,
|
|
48
|
+
minutes: HOUR,
|
|
49
|
+
hour: DAY,
|
|
50
|
+
hours: DAY,
|
|
51
|
+
day: WEEK,
|
|
52
|
+
days: WEEK,
|
|
53
|
+
week: MONTH,
|
|
54
|
+
weeks: MONTH,
|
|
55
|
+
month: YEAR,
|
|
56
|
+
months: YEAR,
|
|
57
|
+
quarter: Infinity,
|
|
58
|
+
quarters: Infinity,
|
|
59
|
+
year: Infinity,
|
|
60
|
+
years: Infinity,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function toTime(value: Date | number | string): number {
|
|
64
|
+
return typeof value === 'number' ? value : new Date(value).getTime()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* How far `date` is from `now`, in words, in the caller's locale:
|
|
69
|
+
* `relative(lastCommit, {now, locale: 'en'})` → "3 days ago". Built on
|
|
70
|
+
* `Intl.RelativeTimeFormat`; the largest unit that keeps the count at least 1
|
|
71
|
+
* is used, rounded to the nearest whole unit.
|
|
72
|
+
*
|
|
73
|
+
* An unparseable date throws a `RangeError` (as `Intl` does): check the data
|
|
74
|
+
* before rendering it.
|
|
75
|
+
*/
|
|
76
|
+
export function relative(date: Date | number | string, options: RelativeOptions): string {
|
|
77
|
+
const {now, locale, numeric = 'auto'} = options
|
|
78
|
+
const target = toTime(date)
|
|
79
|
+
const from = toTime(now)
|
|
80
|
+
if (!Number.isFinite(target) || !Number.isFinite(from)) {
|
|
81
|
+
throw new RangeError(`relative: invalid date ${String(date)}`)
|
|
82
|
+
}
|
|
83
|
+
const diff = target - from
|
|
84
|
+
const [unit, size] =
|
|
85
|
+
STEPS.find(([unit]) => Math.abs(diff) < LIMITS[unit]) ?? STEPS[STEPS.length - 1]
|
|
86
|
+
// `+ 0` turns a -0 into 0, so "now" is not "0 seconds ago" in `always` mode.
|
|
87
|
+
const value = Math.round(diff / size) + 0
|
|
88
|
+
return new Intl.RelativeTimeFormat(locale as string | string[], {numeric}).format(value, unit)
|
|
89
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import {useEffect, useState, type RefObject} from 'react'
|
|
2
|
+
import {breakpointsPx, type Breakpoint} from '@duro-app/tokens/tokens/breakpoints.css'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Whether the element behind `ref` is narrower than a Duro breakpoint — its
|
|
6
|
+
* own width, as a container query sees it, not the window's. For the choices
|
|
7
|
+
* CSS cannot make (a table of contents as a list or a menu, a row of buttons
|
|
8
|
+
* or an overflow `Menu`).
|
|
9
|
+
*
|
|
10
|
+
* `false` on the server and until the first measurement, so the server and
|
|
11
|
+
* the first client render agree on the wide layout; a ResizeObserver then
|
|
12
|
+
* follows the element. A width of 0 (the element is not laid out, e.g.
|
|
13
|
+
* `display: none`) keeps the last answer.
|
|
14
|
+
*
|
|
15
|
+
* Narrower than means `width < breakpoint`: at exactly 768px an element is not
|
|
16
|
+
* below `md`, the same line `Grid`'s named layouts collapse on.
|
|
17
|
+
*/
|
|
18
|
+
export function useContainerBelow(
|
|
19
|
+
ref: RefObject<HTMLElement | null>,
|
|
20
|
+
breakpoint: Breakpoint,
|
|
21
|
+
): boolean {
|
|
22
|
+
const [below, setBelow] = useState(false)
|
|
23
|
+
const limit = breakpointsPx[breakpoint]
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const element = ref.current
|
|
27
|
+
if (!element || typeof ResizeObserver === 'undefined') return
|
|
28
|
+
const observer = new ResizeObserver((entries) => {
|
|
29
|
+
const entry = entries[0]
|
|
30
|
+
if (!entry) return
|
|
31
|
+
const width = entry.contentBoxSize?.[0]?.inlineSize ?? entry.contentRect.width
|
|
32
|
+
if (width > 0) setBelow(width < limit)
|
|
33
|
+
})
|
|
34
|
+
observer.observe(element)
|
|
35
|
+
return () => observer.disconnect()
|
|
36
|
+
}, [ref, limit])
|
|
37
|
+
|
|
38
|
+
return below
|
|
39
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,12 @@ import './strict.css'
|
|
|
10
10
|
export {ActionBar, type ActionBarProps} from './components/ActionBar/ActionBar'
|
|
11
11
|
export {ActionBarProvider} from './components/ActionBar/ActionBarProvider'
|
|
12
12
|
export {Alert, type AlertVariant} from './components/Alert/Alert'
|
|
13
|
+
export {
|
|
14
|
+
AppShell,
|
|
15
|
+
type AppShellCollapse,
|
|
16
|
+
type AppShellRailState,
|
|
17
|
+
} from './components/AppShell/AppShell'
|
|
18
|
+
export {Aside} from './components/Aside/Aside'
|
|
13
19
|
export {Icon, type IconName} from './components/Icon'
|
|
14
20
|
export {Badge, type BadgeVariant, type BadgeSize} from './components/Badge/Badge'
|
|
15
21
|
export {Breadcrumb} from './components/Breadcrumb/Breadcrumb'
|
|
@@ -19,6 +25,7 @@ export {ButtonGroup, type ButtonGroupProps} from './components/ButtonGroup/Butto
|
|
|
19
25
|
export {Callout, type CalloutVariant} from './components/Callout/Callout'
|
|
20
26
|
export {Card, type CardVariant, type CardSize} from './components/Card/Card'
|
|
21
27
|
export {Checkbox} from './components/Checkbox/Checkbox'
|
|
28
|
+
export {CodeBlock} from './components/CodeBlock/CodeBlock'
|
|
22
29
|
export {CheckboxGroup} from './components/CheckboxGroup/CheckboxGroup'
|
|
23
30
|
export {ColorInput} from './components/ColorInput/ColorInput'
|
|
24
31
|
export {ConfirmDialog} from './components/ConfirmDialog/ConfirmDialog'
|
|
@@ -41,6 +48,7 @@ export {Drawer, type DrawerAnchor, type DrawerSize} from './components/Drawer/Dr
|
|
|
41
48
|
export {EmptyState} from './components/EmptyState/EmptyState'
|
|
42
49
|
export {Heading, type HeadingVariant} from './components/Heading/Heading'
|
|
43
50
|
export {List, type ListSelectionMode} from './components/List/List'
|
|
51
|
+
export {LiveRegion, type LiveRegionPoliteness} from './components/LiveRegion/LiveRegion'
|
|
44
52
|
export {Field} from './components/Field/Field'
|
|
45
53
|
export type {LabelPosition, NecessityIndicator} from './components/Form/FormContext'
|
|
46
54
|
export {Fieldset, type FieldsetGap} from './components/Fieldset/Fieldset'
|
|
@@ -55,6 +63,7 @@ export {Menu} from './components/Menu/Menu'
|
|
|
55
63
|
export {Meter, type MeterProps, type MeterTone} from './components/Meter/Meter'
|
|
56
64
|
export {PageNav} from './components/PageNav/PageNav'
|
|
57
65
|
export {Panel} from './components/Panel/Panel'
|
|
66
|
+
export {Prose} from './components/Prose/Prose'
|
|
58
67
|
export {RadioGroup} from './components/RadioGroup/RadioGroup'
|
|
59
68
|
export {ScrollArea} from './components/ScrollArea/ScrollArea'
|
|
60
69
|
export {Combobox} from './components/Combobox/Combobox'
|
|
@@ -114,6 +123,8 @@ export {
|
|
|
114
123
|
type ColorModePreference,
|
|
115
124
|
} from './components/ColorMode'
|
|
116
125
|
export {Text, type TextVariant, type TextColor} from './components/Text/Text'
|
|
126
|
+
export {Time} from './components/Time/Time'
|
|
127
|
+
export {relative, type RelativeOptions} from './components/Time/relative'
|
|
117
128
|
export {Toggle, type ToggleSize} from './components/Toggle/Toggle'
|
|
118
129
|
export {ToggleGroup} from './components/ToggleGroup/ToggleGroup'
|
|
119
130
|
export {Tooltip} from './components/Tooltip/Tooltip'
|
|
@@ -127,7 +138,7 @@ export {Switch} from './components/Switch/Switch'
|
|
|
127
138
|
export {Stack, type SpacingKey} from './components/Stack/Stack'
|
|
128
139
|
export {Inline} from './components/Inline/Inline'
|
|
129
140
|
export {Cluster} from './components/Cluster/Cluster'
|
|
130
|
-
export {Grid} from './components/Grid/Grid'
|
|
141
|
+
export {Grid, type GridLayout} from './components/Grid/Grid'
|
|
131
142
|
export type {GridTrack} from './shared/length'
|
|
132
143
|
export {PageShell, type PageShellMaxWidth, type PageShellPadding} from './components/PageShell'
|
|
133
144
|
|
|
@@ -136,6 +147,7 @@ export type {LinkClickEvent, OnNavigate} from './shared/navigate'
|
|
|
136
147
|
|
|
137
148
|
// Hooks
|
|
138
149
|
export {useContainerQuery, type ContainerSize} from './hooks/useContainerQuery'
|
|
150
|
+
export {useContainerBelow} from './hooks/useContainerBelow'
|
|
139
151
|
|
|
140
152
|
// Tokens — import directly from @duro-app/tokens with deep imports:
|
|
141
153
|
// import { colors } from '@duro-app/tokens/tokens/colors.css'
|
package/src/strict.css
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/* Layer order for the whole page: the reset, then Prose's descendant rules
|
|
2
|
+
* (above the reset's margins, below every component style), then StyleX's
|
|
3
|
+
* priority layers, which @react-strict-dom declares. */
|
|
4
|
+
@layer reset, duro-prose;
|
|
2
5
|
@react-strict-dom;
|
|
3
6
|
|
|
4
7
|
/* Table: reveal sort indicator on header cell hover */
|