@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,139 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect, fn} from 'storybook/test'
|
|
4
|
+
import {LiveRegion, type LiveRegionPoliteness} from './LiveRegion'
|
|
5
|
+
import {Button} from '../Button/Button'
|
|
6
|
+
import {Inline} from '../Inline/Inline'
|
|
7
|
+
import {Stack} from '../Stack/Stack'
|
|
8
|
+
import {onThemeSurface} from '../../docs/themedSurface'
|
|
9
|
+
|
|
10
|
+
interface LiveRegionArgs {
|
|
11
|
+
/** `polite` (role="status") waits for a pause; `assertive` (role="alert") interrupts. */
|
|
12
|
+
politeness: LiveRegionPoliteness
|
|
13
|
+
/** Off screen, still in the accessibility tree. */
|
|
14
|
+
visuallyHidden: boolean
|
|
15
|
+
/** What the region announces when the trigger button is pressed. */
|
|
16
|
+
message: string
|
|
17
|
+
/** The trigger button's label. */
|
|
18
|
+
triggerLabel: string
|
|
19
|
+
/** The button that empties the region. */
|
|
20
|
+
clearLabel: string
|
|
21
|
+
/** Called with the message the region now holds. */
|
|
22
|
+
onAnnounce: (message: string) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The region is mounted empty from the start; a button puts the message in
|
|
27
|
+
* it, which is what assistive tech announces.
|
|
28
|
+
*/
|
|
29
|
+
function Announcer({
|
|
30
|
+
politeness,
|
|
31
|
+
visuallyHidden,
|
|
32
|
+
message,
|
|
33
|
+
triggerLabel,
|
|
34
|
+
clearLabel,
|
|
35
|
+
onAnnounce,
|
|
36
|
+
}: LiveRegionArgs) {
|
|
37
|
+
const [content, setContent] = useState('')
|
|
38
|
+
return (
|
|
39
|
+
<Stack gap="sm">
|
|
40
|
+
<Inline gap="sm">
|
|
41
|
+
<Button
|
|
42
|
+
variant="secondary"
|
|
43
|
+
onClick={() => {
|
|
44
|
+
setContent(message)
|
|
45
|
+
onAnnounce(message)
|
|
46
|
+
}}
|
|
47
|
+
>
|
|
48
|
+
{triggerLabel}
|
|
49
|
+
</Button>
|
|
50
|
+
<Button variant="secondary" onClick={() => setContent('')}>
|
|
51
|
+
{clearLabel}
|
|
52
|
+
</Button>
|
|
53
|
+
</Inline>
|
|
54
|
+
<LiveRegion politeness={politeness} visuallyHidden={visuallyHidden}>
|
|
55
|
+
{content}
|
|
56
|
+
</LiveRegion>
|
|
57
|
+
</Stack>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const meta = {
|
|
62
|
+
title: 'Components/LiveRegion',
|
|
63
|
+
args: {
|
|
64
|
+
politeness: 'polite',
|
|
65
|
+
visuallyHidden: false,
|
|
66
|
+
message: 'Link copied',
|
|
67
|
+
triggerLabel: 'Copy link',
|
|
68
|
+
clearLabel: 'Clear',
|
|
69
|
+
onAnnounce: fn(),
|
|
70
|
+
},
|
|
71
|
+
argTypes: {
|
|
72
|
+
politeness: {control: 'radio', options: ['polite', 'assertive']},
|
|
73
|
+
visuallyHidden: {control: 'boolean'},
|
|
74
|
+
message: {control: 'text'},
|
|
75
|
+
triggerLabel: {control: 'text'},
|
|
76
|
+
clearLabel: {control: 'text'},
|
|
77
|
+
},
|
|
78
|
+
parameters: {a11y: {test: 'error'}},
|
|
79
|
+
decorators: [onThemeSurface],
|
|
80
|
+
render: (args) => <Announcer {...args} />,
|
|
81
|
+
} satisfies Meta<LiveRegionArgs>
|
|
82
|
+
|
|
83
|
+
export default meta
|
|
84
|
+
type Story = StoryObj<typeof meta>
|
|
85
|
+
|
|
86
|
+
const TEST_ONLY = ['!dev', '!autodocs']
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Press the trigger: the message appears in the region (unless
|
|
90
|
+
* `visuallyHidden`) and the Actions panel logs `onAnnounce`. Switch
|
|
91
|
+
* `politeness` to assertive for role="alert".
|
|
92
|
+
*/
|
|
93
|
+
export const Playground: Story = {}
|
|
94
|
+
|
|
95
|
+
/** Mounted empty from the start; a change of content is what gets announced. */
|
|
96
|
+
export const Polite: Story = {
|
|
97
|
+
tags: TEST_ONLY,
|
|
98
|
+
play: async ({args, canvas, userEvent}) => {
|
|
99
|
+
const region = canvas.getByRole('status')
|
|
100
|
+
await expect(region).toHaveAttribute('aria-live', 'polite')
|
|
101
|
+
await expect(region).toHaveAttribute('aria-atomic', 'true')
|
|
102
|
+
await expect(region).toBeEmptyDOMElement()
|
|
103
|
+
|
|
104
|
+
await userEvent.click(canvas.getByRole('button', {name: args.triggerLabel}))
|
|
105
|
+
// the same element, now holding the message: a change, not a new region
|
|
106
|
+
await expect(canvas.getByRole('status')).toBe(region)
|
|
107
|
+
await expect(region).toHaveTextContent(args.message)
|
|
108
|
+
await expect(args.onAnnounce).toHaveBeenCalledWith(args.message)
|
|
109
|
+
|
|
110
|
+
await userEvent.click(canvas.getByRole('button', {name: args.clearLabel}))
|
|
111
|
+
await expect(region).toBeInTheDocument()
|
|
112
|
+
await expect(canvas.getByRole('status')).toBe(region)
|
|
113
|
+
await expect(region).toBeEmptyDOMElement()
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export const Assertive: Story = {
|
|
118
|
+
tags: TEST_ONLY,
|
|
119
|
+
render: () => <LiveRegion politeness="assertive">Connection lost</LiveRegion>,
|
|
120
|
+
play: async ({canvas}) => {
|
|
121
|
+
const region = canvas.getByRole('alert')
|
|
122
|
+
await expect(region).toHaveAttribute('aria-live', 'assertive')
|
|
123
|
+
await expect(region).toHaveTextContent('Connection lost')
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Off screen, still in the accessibility tree. */
|
|
128
|
+
export const VisuallyHidden: Story = {
|
|
129
|
+
tags: TEST_ONLY,
|
|
130
|
+
args: {visuallyHidden: true},
|
|
131
|
+
play: async ({args, canvas, userEvent}) => {
|
|
132
|
+
await userEvent.click(canvas.getByRole('button', {name: args.triggerLabel}))
|
|
133
|
+
const region = canvas.getByRole('status')
|
|
134
|
+
await expect(region).toHaveTextContent(args.message)
|
|
135
|
+
const box = region.getBoundingClientRect()
|
|
136
|
+
await expect(box.width).toBeLessThanOrEqual(1)
|
|
137
|
+
await expect(box.height).toBeLessThanOrEqual(1)
|
|
138
|
+
},
|
|
139
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {visuallyHidden as hidden} from '../../styles/visually-hidden.css'
|
|
4
|
+
|
|
5
|
+
export type LiveRegionPoliteness = 'polite' | 'assertive'
|
|
6
|
+
|
|
7
|
+
interface LiveRegionProps {
|
|
8
|
+
/**
|
|
9
|
+
* What to announce. Change it to announce; leave it empty when there is
|
|
10
|
+
* nothing to say — never unmount the region to clear it.
|
|
11
|
+
*/
|
|
12
|
+
children?: ReactNode
|
|
13
|
+
/**
|
|
14
|
+
* `polite` (default) waits for the screen reader to finish (`role="status"`):
|
|
15
|
+
* "Copied", "3 results". `assertive` interrupts (`role="alert"`): only for
|
|
16
|
+
* what the person must hear now, such as an error that blocks them.
|
|
17
|
+
*/
|
|
18
|
+
politeness?: LiveRegionPoliteness
|
|
19
|
+
/** Keeps the region off screen and in the accessibility tree. Default false. */
|
|
20
|
+
visuallyHidden?: boolean
|
|
21
|
+
id?: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* LiveRegion — tells assistive tech about a change that has no focus move to
|
|
26
|
+
* go with it ("Link copied", "Answer ready"). A screen reader only announces
|
|
27
|
+
* a change to a region that was already in the page, so render it from the
|
|
28
|
+
* start (empty) and change its content; a region mounted together with its
|
|
29
|
+
* message is often not read at all.
|
|
30
|
+
*/
|
|
31
|
+
export function LiveRegion({
|
|
32
|
+
children,
|
|
33
|
+
politeness = 'polite',
|
|
34
|
+
visuallyHidden = false,
|
|
35
|
+
id,
|
|
36
|
+
}: LiveRegionProps) {
|
|
37
|
+
return (
|
|
38
|
+
<html.div
|
|
39
|
+
id={id}
|
|
40
|
+
role={politeness === 'assertive' ? 'alert' : 'status'}
|
|
41
|
+
aria-live={politeness}
|
|
42
|
+
aria-atomic={true}
|
|
43
|
+
style={visuallyHidden ? [hidden.base, hidden.text] : null}
|
|
44
|
+
>
|
|
45
|
+
{children}
|
|
46
|
+
</html.div>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -8,6 +8,9 @@ import {Menu} from './Menu'
|
|
|
8
8
|
import {Button} from '../Button/Button'
|
|
9
9
|
import {Dialog} from '../Dialog/Dialog'
|
|
10
10
|
import {Icon} from '../Icon/Icon'
|
|
11
|
+
import {onThemeSurface} from '../../docs/themedSurface'
|
|
12
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
13
|
+
import {withCoarsePointer, withFinePointer} from '../../docs/coarsePointer'
|
|
11
14
|
|
|
12
15
|
const meta: Meta = {
|
|
13
16
|
title: 'Components/Menu',
|
|
@@ -477,3 +480,62 @@ async function realKeyboard(): Promise<((keys: string) => Promise<void>) | null>
|
|
|
477
480
|
return null
|
|
478
481
|
}
|
|
479
482
|
}
|
|
483
|
+
|
|
484
|
+
/**
|
|
485
|
+
* A touch screen gets 44px triggers (coarse pointer), the default and the
|
|
486
|
+
* ghost variant alike; a mouse keeps the compact control.
|
|
487
|
+
* It drives Chrome's touch emulation through Vitest's CDP session, so it runs
|
|
488
|
+
* as a test and stays out of the dev sidebar (there is no Vitest there).
|
|
489
|
+
*/
|
|
490
|
+
export const TouchTargets: Story = {
|
|
491
|
+
tags: ['!dev', '!autodocs'],
|
|
492
|
+
parameters: {a11y: {test: 'error'}},
|
|
493
|
+
decorators: [onThemeSurface],
|
|
494
|
+
render: () => (
|
|
495
|
+
<html.div style={touchStyles.row}>
|
|
496
|
+
<Menu.Root>
|
|
497
|
+
<Menu.Trigger>Actions</Menu.Trigger>
|
|
498
|
+
<Menu.Popup>
|
|
499
|
+
<Menu.Item>Rename</Menu.Item>
|
|
500
|
+
</Menu.Popup>
|
|
501
|
+
</Menu.Root>
|
|
502
|
+
<Menu.Root>
|
|
503
|
+
<Menu.Trigger variant="ghost" aria-label="Page actions">
|
|
504
|
+
<Icon name="menu" size="sm" />
|
|
505
|
+
</Menu.Trigger>
|
|
506
|
+
<Menu.Popup align="end">
|
|
507
|
+
<Menu.Item>Copy link</Menu.Item>
|
|
508
|
+
</Menu.Popup>
|
|
509
|
+
</Menu.Root>
|
|
510
|
+
<Menu.Root>
|
|
511
|
+
<Menu.Trigger size="small">Sort</Menu.Trigger>
|
|
512
|
+
<Menu.Popup>
|
|
513
|
+
<Menu.Item>Name</Menu.Item>
|
|
514
|
+
</Menu.Popup>
|
|
515
|
+
</Menu.Root>
|
|
516
|
+
</html.div>
|
|
517
|
+
),
|
|
518
|
+
play: async ({canvas}) => {
|
|
519
|
+
const triggers = () => [
|
|
520
|
+
canvas.getByRole('button', {name: 'Actions'}),
|
|
521
|
+
canvas.getByRole('button', {name: 'Page actions'}),
|
|
522
|
+
canvas.getByRole('button', {name: 'Sort'}),
|
|
523
|
+
]
|
|
524
|
+
await withFinePointer()
|
|
525
|
+
const ghost = canvas.getByRole('button', {name: 'Page actions'}).getBoundingClientRect()
|
|
526
|
+
await expect(ghost.height).toBeLessThan(SIZES_PX.touchTarget)
|
|
527
|
+
await withCoarsePointer(async () => {
|
|
528
|
+
for (const trigger of triggers()) {
|
|
529
|
+
await waitFor(() => {
|
|
530
|
+
const box = trigger.getBoundingClientRect()
|
|
531
|
+
expect(box.height).toBeGreaterThanOrEqual(SIZES_PX.touchTarget)
|
|
532
|
+
expect(box.width).toBeGreaterThanOrEqual(SIZES_PX.touchTarget)
|
|
533
|
+
})
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
const touchStyles = css.create({
|
|
540
|
+
row: {display: 'flex', gap: spacing.md, alignItems: 'center'},
|
|
541
|
+
})
|
|
@@ -32,6 +32,9 @@ export const styles = css.create({
|
|
|
32
32
|
borderStyle: 'solid',
|
|
33
33
|
borderColor: colors.border,
|
|
34
34
|
borderRadius: radii.sm,
|
|
35
|
+
// A touch screen gets the 44px target; a mouse keeps the compact control.
|
|
36
|
+
minWidth: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
|
|
37
|
+
minHeight: {default: null, '@media (pointer: coarse)': sizes.touchTarget},
|
|
35
38
|
cursor: 'pointer',
|
|
36
39
|
transitionProperty: 'background-color, border-color',
|
|
37
40
|
transitionDuration: duration.fast,
|
|
@@ -39,8 +42,8 @@ export const styles = css.create({
|
|
|
39
42
|
// Toolbar trigger: no border or fill until hover, at least icon-button size.
|
|
40
43
|
triggerGhost: {
|
|
41
44
|
justifyContent: 'center',
|
|
42
|
-
minWidth: sizes.iconButton,
|
|
43
|
-
minHeight: sizes.iconButton,
|
|
45
|
+
minWidth: {default: sizes.iconButton, '@media (pointer: coarse)': sizes.touchTarget},
|
|
46
|
+
minHeight: {default: sizes.iconButton, '@media (pointer: coarse)': sizes.touchTarget},
|
|
44
47
|
paddingTop: spacing.xs,
|
|
45
48
|
paddingBottom: spacing.xs,
|
|
46
49
|
paddingLeft: spacing.xs,
|
|
@@ -62,7 +65,7 @@ export const styles = css.create({
|
|
|
62
65
|
// A small ghost trigger keeps the ghost's xs padding at the small icon-button size.
|
|
63
66
|
triggerGhostSmall: {
|
|
64
67
|
fontSize: typography.fontSizeXs,
|
|
65
|
-
minWidth: sizes.iconButtonSm,
|
|
68
|
+
minWidth: {default: sizes.iconButtonSm, '@media (pointer: coarse)': sizes.touchTarget},
|
|
66
69
|
minHeight: {default: sizes.iconButtonSm, '@media (pointer: coarse)': sizes.touchTarget},
|
|
67
70
|
},
|
|
68
71
|
popup: {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
"The container for HTML Duro does not author — rendered Markdown, CMS output: headings, paragraphs, lists, tables, blockquotes, inline code, links and images are styled from tokens by descendant rules scoped to it (plain CSS in dist/index.css, :where() and its own cascade layer). Duro components inside it keep their styles, and an app's own CSS wins over every Prose rule. Web only.",
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'The body of a docs page rendered from Markdown',
|
|
8
|
+
'Any HTML produced by a renderer you do not write as JSX',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Text you write as components — use Heading, Text and Stack',
|
|
12
|
+
'A short label or paragraph — use Text',
|
|
13
|
+
],
|
|
14
|
+
relatedTo: [
|
|
15
|
+
{
|
|
16
|
+
component: 'CodeBlock',
|
|
17
|
+
kind: 'composition',
|
|
18
|
+
relationship: "Pass CodeBlock as the Markdown renderer's pre, for a copy button",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
component: 'TextLink',
|
|
22
|
+
kind: 'composition',
|
|
23
|
+
relationship:
|
|
24
|
+
"Pass TextLink as the renderer's a, for onNavigate; it keeps its own styles inside Prose",
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
example: `<Prose>
|
|
28
|
+
<ReactMarkdown components={{pre: ({children}) => <CodeBlock copyLabel="Copy" copiedLabel="Copied">{children}</CodeBlock>}}>
|
|
29
|
+
{body}
|
|
30
|
+
</ReactMarkdown>
|
|
31
|
+
</Prose>`,
|
|
32
|
+
}
|
|
@@ -0,0 +1,240 @@
|
|
|
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
|
+
// CodeBlock draws one block: Prose's own pre rule must not add a second
|
|
233
|
+
// border, background or rounded top inside it
|
|
234
|
+
const pre = canvas.getByText('pnpm add @duro-app/ui').closest('pre') as HTMLElement
|
|
235
|
+
await expect(style(pre).borderTopWidth).toBe('0px')
|
|
236
|
+
await expect(style(pre).borderLeftWidth).toBe('0px')
|
|
237
|
+
await expect(style(pre).backgroundColor).toBe('rgba(0, 0, 0, 0)')
|
|
238
|
+
await expect(style(pre).borderTopLeftRadius).toBe('0px')
|
|
239
|
+
},
|
|
240
|
+
}
|
|
@@ -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
|
+
}
|