@duro-app/ui 5.6.0 → 5.8.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 (176) hide show
  1. package/dist/{Table-B1sWL3GA.js → Table-CAjPFgTX.js} +398 -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 +186 -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 +67 -0
  30. package/dist/components/CodeBlock/CodeBlock.stories.d.ts.map +1 -0
  31. package/dist/components/CodeBlock/styles.css.d.ts +32 -0
  32. package/dist/components/CodeBlock/styles.css.d.ts.map +1 -0
  33. package/dist/components/DragDrop/DragDrop.d.ts +95 -14
  34. package/dist/components/DragDrop/DragDrop.d.ts.map +1 -1
  35. package/dist/components/DragDrop/DragDrop.meta.d.ts.map +1 -1
  36. package/dist/components/DragDrop/DragDropBoard.stories.d.ts +69 -0
  37. package/dist/components/DragDrop/DragDropBoard.stories.d.ts.map +1 -0
  38. package/dist/components/DragDrop/styles.css.d.ts +80 -3
  39. package/dist/components/DragDrop/styles.css.d.ts.map +1 -1
  40. package/dist/components/Grid/Grid.d.ts +13 -4
  41. package/dist/components/Grid/Grid.d.ts.map +1 -1
  42. package/dist/components/Grid/Grid.meta.d.ts.map +1 -1
  43. package/dist/components/Grid/Grid.stories.d.ts +28 -0
  44. package/dist/components/Grid/Grid.stories.d.ts.map +1 -1
  45. package/dist/components/Grid/styles.css.d.ts +3 -0
  46. package/dist/components/Grid/styles.css.d.ts.map +1 -1
  47. package/dist/components/Heading/Heading.d.ts +3 -1
  48. package/dist/components/Heading/Heading.d.ts.map +1 -1
  49. package/dist/components/Heading/Heading.stories.d.ts +9 -0
  50. package/dist/components/Heading/Heading.stories.d.ts.map +1 -0
  51. package/dist/components/LinkButton/LinkButton.stories.d.ts +7 -0
  52. package/dist/components/LinkButton/LinkButton.stories.d.ts.map +1 -1
  53. package/dist/components/LinkButton/styles.css.d.ts +2 -0
  54. package/dist/components/LinkButton/styles.css.d.ts.map +1 -1
  55. package/dist/components/LiveRegion/LiveRegion.d.ts +28 -0
  56. package/dist/components/LiveRegion/LiveRegion.d.ts.map +1 -0
  57. package/dist/components/LiveRegion/LiveRegion.meta.d.ts +3 -0
  58. package/dist/components/LiveRegion/LiveRegion.meta.d.ts.map +1 -0
  59. package/dist/components/LiveRegion/LiveRegion.stories.d.ts +66 -0
  60. package/dist/components/LiveRegion/LiveRegion.stories.d.ts.map +1 -0
  61. package/dist/components/Menu/Menu.stories.d.ts +7 -0
  62. package/dist/components/Menu/Menu.stories.d.ts.map +1 -1
  63. package/dist/components/Menu/styles.css.d.ts +2 -0
  64. package/dist/components/Menu/styles.css.d.ts.map +1 -1
  65. package/dist/components/Meter/Meter.d.ts +17 -0
  66. package/dist/components/Meter/Meter.d.ts.map +1 -0
  67. package/dist/components/Meter/Meter.meta.d.ts +3 -0
  68. package/dist/components/Meter/Meter.meta.d.ts.map +1 -0
  69. package/dist/components/Meter/Meter.stories.d.ts +12 -0
  70. package/dist/components/Meter/Meter.stories.d.ts.map +1 -0
  71. package/dist/components/Meter/styles.css.d.ts +34 -0
  72. package/dist/components/Meter/styles.css.d.ts.map +1 -0
  73. package/dist/components/PageNav/styles.css.d.ts +1 -1
  74. package/dist/components/Prose/Prose.d.ts +17 -0
  75. package/dist/components/Prose/Prose.d.ts.map +1 -0
  76. package/dist/components/Prose/Prose.meta.d.ts +3 -0
  77. package/dist/components/Prose/Prose.meta.d.ts.map +1 -0
  78. package/dist/components/Prose/Prose.stories.d.ts +63 -0
  79. package/dist/components/Prose/Prose.stories.d.ts.map +1 -0
  80. package/dist/components/SideNav/SideNav.d.ts +7 -1
  81. package/dist/components/SideNav/SideNav.d.ts.map +1 -1
  82. package/dist/components/SideNav/SideNav.stories.d.ts +3 -0
  83. package/dist/components/SideNav/SideNav.stories.d.ts.map +1 -1
  84. package/dist/components/Time/Time.d.ts +26 -0
  85. package/dist/components/Time/Time.d.ts.map +1 -0
  86. package/dist/components/Time/Time.meta.d.ts +3 -0
  87. package/dist/components/Time/Time.meta.d.ts.map +1 -0
  88. package/dist/components/Time/Time.stories.d.ts +77 -0
  89. package/dist/components/Time/Time.stories.d.ts.map +1 -0
  90. package/dist/components/Time/relative.d.ts +27 -0
  91. package/dist/components/Time/relative.d.ts.map +1 -0
  92. package/dist/components/Timeline/Timeline.d.ts +71 -0
  93. package/dist/components/Timeline/Timeline.d.ts.map +1 -0
  94. package/dist/components/Timeline/Timeline.meta.d.ts +3 -0
  95. package/dist/components/Timeline/Timeline.meta.d.ts.map +1 -0
  96. package/dist/components/Timeline/Timeline.stories.d.ts +26 -0
  97. package/dist/components/Timeline/Timeline.stories.d.ts.map +1 -0
  98. package/dist/components/Timeline/styles.css.d.ts +158 -0
  99. package/dist/components/Timeline/styles.css.d.ts.map +1 -0
  100. package/dist/components/Toast/Toast.meta.d.ts.map +1 -1
  101. package/dist/components/Toast/Toast.stories.d.ts +9 -0
  102. package/dist/components/Toast/Toast.stories.d.ts.map +1 -1
  103. package/dist/components/Toast/ToastProvider.d.ts +11 -5
  104. package/dist/components/Toast/ToastProvider.d.ts.map +1 -1
  105. package/dist/components/Toast/index.d.ts +1 -1
  106. package/dist/components/Toast/index.d.ts.map +1 -1
  107. package/dist/components/Toast/styles.css.d.ts +6 -0
  108. package/dist/components/Toast/styles.css.d.ts.map +1 -1
  109. package/dist/form.js +1 -1
  110. package/dist/hooks/useContainerBelow.d.ts +18 -0
  111. package/dist/hooks/useContainerBelow.d.ts.map +1 -0
  112. package/dist/index.css +1 -1
  113. package/dist/index.d.ts +13 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.js +7316 -5536
  116. package/dist/index.js.map +1 -1
  117. package/dist/table.js +2 -2
  118. package/package.json +2 -2
  119. package/src/components/AppShell/AppShell.meta.ts +62 -0
  120. package/src/components/AppShell/AppShell.stories.tsx +770 -0
  121. package/src/components/AppShell/AppShell.tsx +346 -0
  122. package/src/components/AppShell/styles.css.ts +219 -0
  123. package/src/components/Aside/Aside.meta.ts +33 -0
  124. package/src/components/Aside/Aside.stories.tsx +106 -0
  125. package/src/components/Aside/Aside.tsx +42 -0
  126. package/src/components/Aside/styles.css.ts +60 -0
  127. package/src/components/Button/styles.css.ts +7 -2
  128. package/src/components/ButtonGroup/ButtonGroup.stories.tsx +4 -5
  129. package/src/components/CodeBlock/CodeBlock.meta.ts +29 -0
  130. package/src/components/CodeBlock/CodeBlock.stories.tsx +162 -0
  131. package/src/components/CodeBlock/CodeBlock.tsx +99 -0
  132. package/src/components/CodeBlock/styles.css.ts +36 -0
  133. package/src/components/DragDrop/DragDrop.meta.ts +22 -21
  134. package/src/components/DragDrop/DragDrop.tsx +1147 -246
  135. package/src/components/DragDrop/DragDropBoard.stories.tsx +668 -0
  136. package/src/components/DragDrop/styles.css.ts +74 -4
  137. package/src/components/Grid/Grid.meta.ts +2 -1
  138. package/src/components/Grid/Grid.stories.tsx +202 -3
  139. package/src/components/Grid/Grid.tsx +35 -6
  140. package/src/components/Grid/styles.css.ts +11 -0
  141. package/src/components/Heading/Heading.stories.tsx +51 -0
  142. package/src/components/Heading/Heading.tsx +38 -7
  143. package/src/components/LinkButton/LinkButton.stories.tsx +41 -1
  144. package/src/components/LinkButton/styles.css.ts +6 -2
  145. package/src/components/LiveRegion/LiveRegion.meta.ts +29 -0
  146. package/src/components/LiveRegion/LiveRegion.stories.tsx +139 -0
  147. package/src/components/LiveRegion/LiveRegion.tsx +48 -0
  148. package/src/components/Menu/Menu.stories.tsx +62 -0
  149. package/src/components/Menu/styles.css.ts +6 -3
  150. package/src/components/Meter/Meter.meta.ts +24 -0
  151. package/src/components/Meter/Meter.stories.tsx +64 -0
  152. package/src/components/Meter/Meter.tsx +55 -0
  153. package/src/components/Meter/styles.css.ts +30 -0
  154. package/src/components/Prose/Prose.meta.ts +32 -0
  155. package/src/components/Prose/Prose.stories.tsx +233 -0
  156. package/src/components/Prose/Prose.tsx +25 -0
  157. package/src/components/Prose/prose.css +177 -0
  158. package/src/components/SideNav/SideNav.stories.tsx +29 -0
  159. package/src/components/SideNav/SideNav.tsx +14 -2
  160. package/src/components/Time/Time.meta.ts +17 -0
  161. package/src/components/Time/Time.stories.tsx +173 -0
  162. package/src/components/Time/Time.tsx +33 -0
  163. package/src/components/Time/relative.ts +89 -0
  164. package/src/components/Timeline/Timeline.meta.ts +46 -0
  165. package/src/components/Timeline/Timeline.stories.tsx +238 -0
  166. package/src/components/Timeline/Timeline.tsx +332 -0
  167. package/src/components/Timeline/styles.css.ts +162 -0
  168. package/src/components/Toast/Toast.meta.ts +2 -1
  169. package/src/components/Toast/Toast.stories.tsx +84 -0
  170. package/src/components/Toast/ToastProvider.tsx +49 -11
  171. package/src/components/Toast/index.ts +7 -1
  172. package/src/components/Toast/styles.css.ts +2 -0
  173. package/src/hooks/useContainerBelow.ts +39 -0
  174. package/src/index.ts +29 -1
  175. package/src/strict.css +4 -1
  176. package/dist/Table-B1sWL3GA.js.map +0 -1
@@ -0,0 +1,233 @@
1
+ import type {Meta, StoryObj} from '@storybook/react'
2
+ import {expect, waitFor} from 'storybook/test'
3
+ import {Prose} from './Prose'
4
+ import {Heading} from '../Heading/Heading'
5
+ import {Text} from '../Text/Text'
6
+ import {CodeBlock} from '../CodeBlock/CodeBlock'
7
+ import {onThemeSurface} from '../../docs/themedSurface'
8
+
9
+ // What a Markdown renderer hands over: HTML that is never authored as JSX.
10
+ const RENDERED_MARKDOWN = `
11
+ <p>Flux reconciles every cluster from <code>kubernetes/</code>. Read the
12
+ <a href="#bootstrap">bootstrap notes</a> first.</p>
13
+ <h2 id="bootstrap" class="docs_title__a1b2">Bootstrap</h2>
14
+ <p>Each cluster starts from the same three steps:</p>
15
+ <ol>
16
+ <li>Install the operator.</li>
17
+ <li>Seed the secrets:
18
+ <ul>
19
+ <li>the Vault token</li>
20
+ <li>the registry pull secret</li>
21
+ </ul>
22
+ </li>
23
+ <li>Point Flux at the repository.</li>
24
+ </ol>
25
+ <blockquote><p>Never commit a decrypted secret.</p></blockquote>
26
+ <h3>Clusters</h3>
27
+ <table>
28
+ <thead><tr><th>Cluster</th><th>Nodes</th><th>Role</th></tr></thead>
29
+ <tbody>
30
+ <tr><td>homelab</td><td>5</td><td>Applications</td></tr>
31
+ <tr><td>nas</td><td>1</td><td>Storage</td></tr>
32
+ </tbody>
33
+ </table>
34
+ <pre><code>flux bootstrap git --url=ssh://git@forge/homelab</code></pre>
35
+ <hr>
36
+ <p><img alt="" src="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8'/%3E"></p>
37
+ `
38
+
39
+ const RELEASE_NOTES = `
40
+ <h2>5.6.0</h2>
41
+ <p>Small controls line up: every <code>size="small"</code> control is at least
42
+ <code>controlSm</code> tall, whatever it holds.</p>
43
+ <h3>Fixed</h3>
44
+ <ul>
45
+ <li>A small <code>Menu.Trigger</code> in a toolbar no longer shrinks.</li>
46
+ <li>Toggle and Button share one small padding.</li>
47
+ </ul>
48
+ <blockquote><p>Upgrade with <code>pnpm add @duro-app/ui@5.6.0</code>.</p></blockquote>
49
+ `
50
+
51
+ const REFERENCE = `
52
+ <h2>relative(date, options)</h2>
53
+ <p>Formats <code>date</code> against <code>options.now</code> with
54
+ <a href="https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Intl/RelativeTimeFormat">Intl.RelativeTimeFormat</a>.</p>
55
+ <table>
56
+ <thead><tr><th>Option</th><th>Type</th><th>Default</th></tr></thead>
57
+ <tbody>
58
+ <tr><td><code>now</code></td><td>Date | number</td><td>required</td></tr>
59
+ <tr><td><code>locale</code></td><td>string</td><td>required</td></tr>
60
+ <tr><td><code>numeric</code></td><td>'auto' | 'always'</td><td>'auto'</td></tr>
61
+ </tbody>
62
+ </table>
63
+ <pre><code>relative(commit.date, {now, locale: 'fr'})</code></pre>
64
+ `
65
+
66
+ const DOCUMENTS = {
67
+ runbook: RENDERED_MARKDOWN,
68
+ 'release-notes': RELEASE_NOTES,
69
+ reference: REFERENCE,
70
+ } as const
71
+
72
+ type DocumentName = keyof typeof DOCUMENTS
73
+
74
+ interface ProseArgs {
75
+ /** A sample of what a Markdown renderer hands over. */
76
+ document: DocumentName
77
+ /** Your own HTML, in place of the sample; empty to show the sample. */
78
+ html: string
79
+ }
80
+
81
+ /** Sets the HTML on the element, as a Markdown renderer's output arrives. */
82
+ function Markdown({html = RENDERED_MARKDOWN}: {html?: string}) {
83
+ return (
84
+ <Prose
85
+ ref={(el) => {
86
+ if (el) el.innerHTML = html
87
+ }}
88
+ >
89
+ {null}
90
+ </Prose>
91
+ )
92
+ }
93
+
94
+ const meta = {
95
+ title: 'Components/Prose',
96
+ args: {document: 'runbook', html: ''},
97
+ argTypes: {
98
+ document: {
99
+ control: {
100
+ type: 'select',
101
+ labels: {runbook: 'runbook', 'release-notes': 'release notes', reference: 'API reference'},
102
+ },
103
+ options: Object.keys(DOCUMENTS),
104
+ },
105
+ html: {control: 'text'},
106
+ },
107
+ parameters: {a11y: {test: 'error'}},
108
+ decorators: [onThemeSurface],
109
+ render: (args) => <Markdown html={args.html || DOCUMENTS[args.document]} />,
110
+ } satisfies Meta<ProseArgs>
111
+
112
+ export default meta
113
+ type Story = StoryObj<typeof meta>
114
+
115
+ const TEST_ONLY = ['!dev', '!autodocs']
116
+
117
+ /** The computed value of a token, read the way the page resolves it. */
118
+ function resolved(property: 'color' | 'fontSize', token: string, host: HTMLElement) {
119
+ const probe = document.createElement('span')
120
+ probe.style[property] = `var(${token})`
121
+ host.appendChild(probe)
122
+ const value = getComputedStyle(probe)[property]
123
+ probe.remove()
124
+ return value
125
+ }
126
+
127
+ const style = (el: Element) => getComputedStyle(el)
128
+
129
+ /**
130
+ * Rendered Markdown styled by Prose. Pick another sample in `document`, or
131
+ * paste your own HTML in `html`.
132
+ */
133
+ export const Playground: Story = {}
134
+
135
+ /** The runbook sample, measured: flow, headings, lists, tables, code, links. */
136
+ export const RenderedMarkdown: Story = {
137
+ tags: TEST_ONLY,
138
+ play: async ({canvasElement}) => {
139
+ const prose = canvasElement.querySelector('[data-duro-prose]') as HTMLElement
140
+ await waitFor(() => expect(prose.querySelector('h2')).not.toBeNull())
141
+ const q = (selector: string) => prose.querySelector(selector) as HTMLElement
142
+
143
+ // flow: no gap before the first block, the scale between the rest
144
+ await expect(style(q('p')).marginTop).toBe('0px')
145
+ await expect(style(q('ol')).marginTop).toBe('16px')
146
+ // headings as the docs artboard draws them (Reader.dc.html): a section is
147
+ // a step below the page title, with more room above than below
148
+ await expect(style(q('h2')).fontSize).toBe('20px')
149
+ await expect(style(q('h2')).fontWeight).toBe('600')
150
+ await expect(style(q('h2')).marginTop).toBe('24px')
151
+ await expect(style(q('h2')).marginBottom).toBe('8px')
152
+ await expect(style(q('h3')).fontSize).toBe('18px')
153
+ // lists keep their markers and indent
154
+ await expect(style(q('ol')).listStyleType).toBe('decimal')
155
+ await expect(style(q('ol ul')).listStyleType).toBe('disc')
156
+ await expect(style(q('ol')).paddingLeft).toBe('24px')
157
+ await expect(style(q('li + li')).marginTop).toBe('4px')
158
+ // tables: the column's full width, horizontal rules only, a muted header
159
+ const table = q('table')
160
+ await expect(table.getBoundingClientRect().width).toBe(prose.getBoundingClientRect().width)
161
+ await expect(style(q('td')).borderBottomWidth).toBe('1px')
162
+ await expect(style(q('td')).borderLeftWidth).toBe('0px')
163
+ await expect(style(q('th')).borderTopWidth).toBe('0px')
164
+ await expect(style(q('th')).fontWeight).toBe('500')
165
+ await expect(style(q('th')).color).toBe(resolved('color', '--duro-color-text-muted', prose))
166
+ await expect(style(q('td')).paddingLeft).toBe('8px')
167
+ // code: monospace inline and in a block
168
+ await expect(style(q('p code')).fontFamily).toMatch(/mono|Menlo|Consolas|monospace/i)
169
+ await expect(style(q('pre')).overflowX).toBe('auto')
170
+ await expect(style(q('pre code')).borderTopWidth).toBe('0px')
171
+ // links read as links
172
+ await expect(style(q('a')).color).toBe(resolved('color', '--duro-color-accent', prose))
173
+ await expect(style(q('a')).textDecorationLine).toBe('underline')
174
+ await expect(style(q('blockquote')).borderLeftWidth).toBe('3px')
175
+ await expect(style(q('img')).maxWidth).toBe('100%')
176
+ },
177
+ }
178
+
179
+ /**
180
+ * An app's own CSS wins over every Prose rule: a CSS Module class and even a
181
+ * bare element selector, loaded after Duro's stylesheet. Prose sits in its
182
+ * own cascade layer and weighs nothing (:where()).
183
+ */
184
+ export const AppCssWins: Story = {
185
+ tags: TEST_ONLY,
186
+ play: async ({canvasElement}) => {
187
+ const prose = canvasElement.querySelector('[data-duro-prose]') as HTMLElement
188
+ await waitFor(() => expect(prose.querySelector('h2')).not.toBeNull())
189
+ const h2 = prose.querySelector('h2') as HTMLElement
190
+ const h3 = prose.querySelector('h3') as HTMLElement
191
+ await expect(style(h2).fontSize).toBe('20px')
192
+ await expect(style(h3).fontSize).toBe('18px')
193
+
194
+ const sheet = document.createElement('style')
195
+ // what kb-vision's docs.module.css compiles to, and the weakest rule an app can write
196
+ sheet.textContent = `.docs_title__a1b2 { font-size: 13px; margin-top: 0px; } h3 { font-size: 15px; }`
197
+ document.head.appendChild(sheet)
198
+ try {
199
+ await expect(style(h2).fontSize).toBe('13px')
200
+ await expect(style(h2).marginTop).toBe('0px')
201
+ await expect(style(h3).fontSize).toBe('15px')
202
+ } finally {
203
+ sheet.remove()
204
+ }
205
+ },
206
+ }
207
+
208
+ /** A Duro component inside Prose keeps its own styles: Prose is below them. */
209
+ export const DuroComponentsInside: Story = {
210
+ render: () => (
211
+ <Prose>
212
+ <Heading level={2} variant="headingLg">
213
+ Kept at headingLg
214
+ </Heading>
215
+ <Text>A paragraph from Text.</Text>
216
+ <CodeBlock copyLabel="Copy" copiedLabel="Copied">
217
+ pnpm add @duro-app/ui
218
+ </CodeBlock>
219
+ </Prose>
220
+ ),
221
+ }
222
+
223
+ /** DuroComponentsInside's checks, kept off the visible story so a Control change cannot fail them. */
224
+ export const DuroComponentsInsideChecks: Story = {
225
+ ...DuroComponentsInside,
226
+ tags: TEST_ONLY,
227
+ play: async ({canvas}) => {
228
+ const heading = canvas.getByRole('heading', {level: 2, name: 'Kept at headingLg'})
229
+ // headingLg is 30px; a Prose h2 would be 20px
230
+ await expect(style(heading).fontSize).toBe('30px')
231
+ await expect(canvas.getByRole('button', {name: 'Copy'})).toBeVisible()
232
+ },
233
+ }
@@ -0,0 +1,25 @@
1
+ import type {ReactNode, Ref} from 'react'
2
+ import {html} from 'react-strict-dom'
3
+ import './prose.css'
4
+
5
+ interface ProseProps {
6
+ /** HTML Duro does not author: the output of a Markdown renderer. */
7
+ children: ReactNode
8
+ /** The container element. */
9
+ ref?: Ref<HTMLDivElement>
10
+ }
11
+
12
+ /**
13
+ * Prose — the container for HTML Duro does not author (rendered Markdown, CMS
14
+ * output): headings, paragraphs, lists, tables, blockquotes, inline code,
15
+ * links and images are styled from tokens by descendant rules scoped to it.
16
+ * Duro components inside it (a TextLink, a CodeBlock) keep their own styles,
17
+ * and an app's own CSS wins over every Prose rule. Web only.
18
+ */
19
+ export function Prose({children, ref}: ProseProps) {
20
+ return (
21
+ <html.div ref={ref} data-duro-prose="">
22
+ {children}
23
+ </html.div>
24
+ )
25
+ }
@@ -0,0 +1,177 @@
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. */
44
+ scroll-margin-top: var(--duro-spacing-lg);
45
+ }
46
+ :where(.duro-prose, [data-duro-prose]) :where(h1) {
47
+ font-size: var(--duro-type-scale-font-size9);
48
+ line-height: var(--duro-type-scale-line-height9);
49
+ font-weight: var(--duro-typography-font-weight-bold);
50
+ }
51
+ /* h2 down follow the picked docs artboard (Reader.dc.html): a reading
52
+ column's sections are a step below the page title, not a second one. */
53
+ :where(.duro-prose, [data-duro-prose]) :where(h2) {
54
+ font-size: var(--duro-type-scale-font-size6);
55
+ line-height: var(--duro-type-scale-line-height6);
56
+ font-weight: var(--duro-typography-font-weight-semibold);
57
+ margin-block-start: var(--duro-spacing-lg);
58
+ }
59
+ :where(.duro-prose, [data-duro-prose]) :where(h3) {
60
+ font-size: var(--duro-type-scale-font-size5);
61
+ line-height: var(--duro-type-scale-line-height5);
62
+ font-weight: var(--duro-typography-font-weight-semibold);
63
+ margin-block-start: var(--duro-spacing-lg);
64
+ }
65
+ :where(.duro-prose, [data-duro-prose]) :where(h4, h5, h6) {
66
+ font-size: var(--duro-type-scale-font-size4);
67
+ line-height: var(--duro-type-scale-line-height4);
68
+ font-weight: var(--duro-typography-font-weight-semibold);
69
+ margin-block-start: var(--duro-spacing-md);
70
+ }
71
+ :where(.duro-prose, [data-duro-prose]) > :where(:first-child) {
72
+ margin-block-start: 0;
73
+ }
74
+
75
+ /* Lists: markers back (the reset removes the indent they hang in). */
76
+ :where(.duro-prose, [data-duro-prose]) :where(ul, ol) {
77
+ padding-inline-start: var(--duro-spacing-lg);
78
+ }
79
+ :where(.duro-prose, [data-duro-prose]) :where(ul) {
80
+ list-style-type: disc;
81
+ }
82
+ :where(.duro-prose, [data-duro-prose]) :where(ol) {
83
+ list-style-type: decimal;
84
+ }
85
+ :where(.duro-prose, [data-duro-prose]) :where(li + li, li > ul, li > ol) {
86
+ margin-block-start: var(--duro-spacing-xs);
87
+ }
88
+
89
+ /* Links: the accent, underlined so they read as links in running text. */
90
+ :where(.duro-prose, [data-duro-prose]) :where(a) {
91
+ color: var(--duro-color-accent);
92
+ text-decoration-line: underline;
93
+ text-underline-offset: var(--duro-micro-spacing-px2);
94
+ }
95
+ :where(.duro-prose, [data-duro-prose]) :where(a:hover) {
96
+ color: var(--duro-color-accent-hover);
97
+ }
98
+ :where(.duro-prose, [data-duro-prose]) :where(a:focus-visible) {
99
+ outline: var(--duro-border-focus-ring) solid var(--duro-color-accent);
100
+ outline-offset: var(--duro-border-focus-offset);
101
+ border-radius: var(--duro-radius-xs);
102
+ }
103
+
104
+ :where(.duro-prose, [data-duro-prose]) :where(strong, b) {
105
+ font-weight: var(--duro-typography-font-weight-semibold);
106
+ }
107
+
108
+ :where(.duro-prose, [data-duro-prose]) :where(blockquote) {
109
+ padding-inline-start: var(--duro-spacing-md);
110
+ border-inline-start: var(--duro-border-accent) solid var(--duro-color-border);
111
+ color: var(--duro-color-text-muted);
112
+ }
113
+
114
+ /* Inline code; a heading keeps its own size around it. */
115
+ :where(.duro-prose, [data-duro-prose]) :where(code) {
116
+ font-family: var(--duro-typography-font-family-mono);
117
+ font-size: var(--duro-type-scale-font-size3);
118
+ padding-inline: var(--duro-spacing-xs);
119
+ background-color: var(--duro-color-bg-card);
120
+ border: var(--duro-border-hairline) solid var(--duro-color-border);
121
+ border-radius: var(--duro-radius-xs);
122
+ }
123
+ :where(.duro-prose, [data-duro-prose]) :where(h1, h2, h3, h4, h5, h6) :where(code) {
124
+ font-size: inherit;
125
+ }
126
+
127
+ /* A code block outside CodeBlock (CodeBlock styles its own). */
128
+ :where(.duro-prose, [data-duro-prose]) :where(pre) {
129
+ font-family: var(--duro-typography-font-family-mono);
130
+ font-size: var(--duro-type-scale-font-size3);
131
+ line-height: var(--duro-type-scale-line-height3);
132
+ padding: var(--duro-spacing-md);
133
+ overflow-x: auto;
134
+ background-color: var(--duro-color-bg-card);
135
+ border: var(--duro-border-hairline) solid var(--duro-color-border);
136
+ border-radius: var(--duro-radius-md);
137
+ }
138
+ :where(.duro-prose, [data-duro-prose]) :where(pre code) {
139
+ padding: 0;
140
+ font-size: inherit;
141
+ background-color: transparent;
142
+ border: 0;
143
+ }
144
+
145
+ /* Tables as the docs artboard draws them: the column's full width,
146
+ horizontal rules only, a muted header. A long cell wraps rather than
147
+ widening the page (a full-width table cannot also scroll on its own). */
148
+ :where(.duro-prose, [data-duro-prose]) :where(table) {
149
+ width: 100%;
150
+ border-collapse: collapse;
151
+ font-size: var(--duro-type-scale-font-size3);
152
+ line-height: var(--duro-type-scale-line-height3);
153
+ }
154
+ :where(.duro-prose, [data-duro-prose]) :where(th, td) {
155
+ padding: var(--duro-spacing-xs) var(--duro-spacing-sm);
156
+ border-block-end: var(--duro-border-hairline) solid var(--duro-color-border);
157
+ text-align: start;
158
+ vertical-align: top;
159
+ overflow-wrap: anywhere;
160
+ }
161
+ :where(.duro-prose, [data-duro-prose]) :where(th) {
162
+ color: var(--duro-color-text-muted);
163
+ font-weight: var(--duro-typography-font-weight-medium);
164
+ }
165
+
166
+ :where(.duro-prose, [data-duro-prose]) :where(img) {
167
+ max-width: 100%;
168
+ height: auto;
169
+ border-radius: var(--duro-radius-sm);
170
+ }
171
+
172
+ :where(.duro-prose, [data-duro-prose]) :where(hr) {
173
+ border: 0;
174
+ border-block-start: var(--duro-border-hairline) solid var(--duro-color-border);
175
+ margin-block: var(--duro-spacing-xl);
176
+ }
177
+ }
@@ -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({children, value: controlledValue, defaultValue, onValueChange}: RootProps) {
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
+ }