@duro-app/ui 5.8.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/components/AppShell/AppShell.stories.d.ts +6 -0
- package/dist/components/AppShell/AppShell.stories.d.ts.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.stories.d.ts +6 -0
- package/dist/components/CodeBlock/CodeBlock.stories.d.ts.map +1 -1
- package/dist/components/CodeBlock/styles.css.d.ts +20 -11
- package/dist/components/CodeBlock/styles.css.d.ts.map +1 -1
- package/dist/components/Prose/Prose.stories.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.js +26 -17
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/AppShell/AppShell.stories.tsx +55 -0
- package/src/components/CodeBlock/CodeBlock.stories.tsx +30 -0
- package/src/components/CodeBlock/CodeBlock.tsx +5 -5
- package/src/components/CodeBlock/styles.css.ts +25 -13
- package/src/components/Prose/Prose.stories.tsx +7 -0
- package/src/components/Prose/prose.css +4 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "5.8.
|
|
3
|
+
"version": "5.8.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
},
|
|
67
67
|
"dependencies": {
|
|
68
68
|
"@tanstack/react-virtual": "^3.14.6",
|
|
69
|
-
"@duro-app/tokens": "^5.8.
|
|
69
|
+
"@duro-app/tokens": "^5.8.1"
|
|
70
70
|
},
|
|
71
71
|
"devDependencies": {
|
|
72
72
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -19,6 +19,7 @@ import {Menu} from '../Menu/Menu'
|
|
|
19
19
|
import {TextLink} from '../TextLink/TextLink'
|
|
20
20
|
import {Aside} from '../Aside/Aside'
|
|
21
21
|
import {Stack} from '../Stack/Stack'
|
|
22
|
+
import {Prose} from '../Prose/Prose'
|
|
22
23
|
|
|
23
24
|
// --- the story's frame -----------------------------------------------------
|
|
24
25
|
|
|
@@ -651,6 +652,60 @@ export const AsideBelowBar: Story = {
|
|
|
651
652
|
},
|
|
652
653
|
}
|
|
653
654
|
|
|
655
|
+
/**
|
|
656
|
+
* A jump to a heading inside Prose lands below the phone bar, not under it:
|
|
657
|
+
* Prose's scroll margin adds the bar's height that AppShell publishes. Above
|
|
658
|
+
* the collapse point (no sticky bar) it is the spacing offset alone.
|
|
659
|
+
*/
|
|
660
|
+
export const ProseAnchorBelowBar: Story = {
|
|
661
|
+
tags: TEST_ONLY,
|
|
662
|
+
args: {width: 'phone', brand: ''},
|
|
663
|
+
render: ({width, ...args}) => (
|
|
664
|
+
<Frame px={FRAME_PX[width]}>
|
|
665
|
+
<Shell {...args}>
|
|
666
|
+
<Prose>
|
|
667
|
+
{PARAGRAPHS.slice(0, 40).map((text) => (
|
|
668
|
+
<html.p key={text}>{text}</html.p>
|
|
669
|
+
))}
|
|
670
|
+
<html.h2 id="recovery-keys">Recovery keys</html.h2>
|
|
671
|
+
{PARAGRAPHS.slice(40).map((text) => (
|
|
672
|
+
<html.p key={text}>{text}</html.p>
|
|
673
|
+
))}
|
|
674
|
+
</Prose>
|
|
675
|
+
</Shell>
|
|
676
|
+
</Frame>
|
|
677
|
+
),
|
|
678
|
+
play: async ({canvas, canvasElement}) => {
|
|
679
|
+
const frame = frameOf(canvasElement)
|
|
680
|
+
setWidth(frame, PHONE_PX)
|
|
681
|
+
const heading = canvas.getByRole('heading', {name: 'Recovery keys'})
|
|
682
|
+
const banner = canvas.getByRole('banner')
|
|
683
|
+
const scroller = document.scrollingElement as HTMLElement
|
|
684
|
+
const start = scroller.scrollTop
|
|
685
|
+
try {
|
|
686
|
+
await waitFor(() =>
|
|
687
|
+
expect(getComputedStyle(heading).scrollMarginTop).toBe(
|
|
688
|
+
`${SIZES_PX.appBarH + SPACING_PX.lg}px`,
|
|
689
|
+
),
|
|
690
|
+
)
|
|
691
|
+
heading.scrollIntoView({block: 'start'})
|
|
692
|
+
await waitFor(() =>
|
|
693
|
+
expect(Math.round(heading.getBoundingClientRect().top)).toBeGreaterThanOrEqual(
|
|
694
|
+
Math.round(banner.getBoundingClientRect().bottom),
|
|
695
|
+
),
|
|
696
|
+
)
|
|
697
|
+
// above the collapse point: the spacing offset alone
|
|
698
|
+
setWidth(frame, DESKTOP_PX)
|
|
699
|
+
await waitFor(() =>
|
|
700
|
+
expect(getComputedStyle(heading).scrollMarginTop).toBe(`${SPACING_PX.lg}px`),
|
|
701
|
+
)
|
|
702
|
+
} finally {
|
|
703
|
+
scroller.scrollTop = start
|
|
704
|
+
frame.style.width = ''
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
}
|
|
708
|
+
|
|
654
709
|
/**
|
|
655
710
|
* The server renders the shell once, for every width: CSS picks the rail or
|
|
656
711
|
* the Menu button before any script runs, and hydration reports nothing. The
|
|
@@ -3,6 +3,8 @@ import {expect, fn, spyOn, waitFor} from 'storybook/test'
|
|
|
3
3
|
import {css, html} from 'react-strict-dom'
|
|
4
4
|
import {CodeBlock} from './CodeBlock'
|
|
5
5
|
import {onThemeSurface} from '../../docs/themedSurface'
|
|
6
|
+
import {withCoarsePointer, withFinePointer} from '../../docs/coarsePointer'
|
|
7
|
+
import {SIZES_PX} from '@duro-app/tokens/keys'
|
|
6
8
|
|
|
7
9
|
interface CodeBlockArgs {
|
|
8
10
|
/** The code shown (and copied). */
|
|
@@ -93,6 +95,34 @@ export const LongLineScrollsChecks: Story = {
|
|
|
93
95
|
},
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
/**
|
|
99
|
+
* The copy button never covers the code — a long first line included, and
|
|
100
|
+
* on a touch screen where the button grows to the 44px target (seen on a
|
|
101
|
+
* phone in kb-vision: it sat over `kubectl ... get`).
|
|
102
|
+
*/
|
|
103
|
+
export const CopyButtonClearsTheCode: Story = {
|
|
104
|
+
...LongLineScrolls,
|
|
105
|
+
tags: TEST_ONLY,
|
|
106
|
+
play: async ({canvas, canvasElement}) => {
|
|
107
|
+
const pre = canvasElement.querySelector('pre') as HTMLElement
|
|
108
|
+
const button = () => canvas.getByRole('button', {name: 'Copy'})
|
|
109
|
+
const clear = () =>
|
|
110
|
+
expect(button().getBoundingClientRect().bottom).toBeLessThanOrEqual(
|
|
111
|
+
pre.getBoundingClientRect().top,
|
|
112
|
+
)
|
|
113
|
+
await withFinePointer()
|
|
114
|
+
await clear()
|
|
115
|
+
await withCoarsePointer(async () => {
|
|
116
|
+
await waitFor(() =>
|
|
117
|
+
expect(button().getBoundingClientRect().height).toBeGreaterThanOrEqual(
|
|
118
|
+
SIZES_PX.touchTarget,
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
await clear()
|
|
122
|
+
})
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
96
126
|
/** Copies the text, confirms on the button and announces it, then resets. */
|
|
97
127
|
export const CopiesAndAnnounces: Story = {
|
|
98
128
|
tags: TEST_ONLY,
|
|
@@ -81,6 +81,11 @@ export function CodeBlock({
|
|
|
81
81
|
|
|
82
82
|
return (
|
|
83
83
|
<html.div style={styles.root}>
|
|
84
|
+
<html.div style={styles.bar}>
|
|
85
|
+
<Button variant="secondary" size="small" onClick={() => void copy()}>
|
|
86
|
+
{copied ? copiedLabel : copyLabel}
|
|
87
|
+
</Button>
|
|
88
|
+
</html.div>
|
|
84
89
|
<html.pre
|
|
85
90
|
ref={preRef}
|
|
86
91
|
tabIndex={scrollable ? 0 : undefined}
|
|
@@ -88,11 +93,6 @@ export function CodeBlock({
|
|
|
88
93
|
>
|
|
89
94
|
{children}
|
|
90
95
|
</html.pre>
|
|
91
|
-
<html.div style={styles.copy}>
|
|
92
|
-
<Button variant="secondary" size="small" onClick={() => void copy()}>
|
|
93
|
-
{copied ? copiedLabel : copyLabel}
|
|
94
|
-
</Button>
|
|
95
|
-
</html.div>
|
|
96
96
|
<LiveRegion visuallyHidden>{copied ? copiedLabel : ''}</LiveRegion>
|
|
97
97
|
</html.div>
|
|
98
98
|
)
|
|
@@ -4,33 +4,45 @@ import {radii, spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
|
4
4
|
import {borders} from '@duro-app/tokens/tokens/borders.css'
|
|
5
5
|
|
|
6
6
|
export const styles = css.create({
|
|
7
|
-
//
|
|
7
|
+
// One bordered block: a row for the copy button, then the code. The button
|
|
8
|
+
// never sits over the code, which scrolls sideways under a long line.
|
|
8
9
|
root: {
|
|
9
|
-
|
|
10
|
+
display: 'flex',
|
|
11
|
+
flexDirection: 'column',
|
|
10
12
|
minWidth: 0,
|
|
13
|
+
backgroundColor: colors.bgCard,
|
|
14
|
+
borderWidth: borders.hairline,
|
|
15
|
+
borderStyle: 'solid',
|
|
16
|
+
borderColor: colors.border,
|
|
17
|
+
borderRadius: radii.md,
|
|
18
|
+
},
|
|
19
|
+
bar: {
|
|
20
|
+
display: 'flex',
|
|
21
|
+
justifyContent: 'flex-end',
|
|
22
|
+
paddingTop: spacing.xs,
|
|
23
|
+
paddingRight: spacing.xs,
|
|
24
|
+
paddingLeft: spacing.xs,
|
|
11
25
|
},
|
|
12
26
|
pre: {
|
|
13
27
|
margin: 0,
|
|
14
|
-
paddingTop: spacing.
|
|
28
|
+
paddingTop: spacing.xs,
|
|
15
29
|
paddingBottom: spacing.md,
|
|
16
30
|
paddingLeft: spacing.md,
|
|
17
31
|
paddingRight: spacing.md,
|
|
18
32
|
overflowX: 'auto',
|
|
19
33
|
color: colors.text,
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
// The root draws the block; inside Prose its pre rule would add a second
|
|
35
|
+
// border, background and rounded top, so this pre states its own.
|
|
36
|
+
backgroundColor: 'transparent',
|
|
37
|
+
borderWidth: 0,
|
|
38
|
+
borderTopLeftRadius: 0,
|
|
39
|
+
borderTopRightRadius: 0,
|
|
40
|
+
borderBottomLeftRadius: radii.md,
|
|
41
|
+
borderBottomRightRadius: radii.md,
|
|
25
42
|
whiteSpace: 'pre',
|
|
26
43
|
outlineWidth: {default: 0, ':focus-visible': borders.focusRing},
|
|
27
44
|
outlineStyle: {default: 'none', ':focus-visible': 'solid'},
|
|
28
45
|
outlineColor: {default: 'transparent', ':focus-visible': colors.accent},
|
|
29
46
|
outlineOffset: {default: 0, ':focus-visible': borders.focusOffset},
|
|
30
47
|
},
|
|
31
|
-
copy: {
|
|
32
|
-
position: 'absolute',
|
|
33
|
-
top: spacing.sm,
|
|
34
|
-
right: spacing.sm,
|
|
35
|
-
},
|
|
36
48
|
})
|
|
@@ -229,5 +229,12 @@ export const DuroComponentsInsideChecks: Story = {
|
|
|
229
229
|
// headingLg is 30px; a Prose h2 would be 20px
|
|
230
230
|
await expect(style(heading).fontSize).toBe('30px')
|
|
231
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')
|
|
232
239
|
},
|
|
233
240
|
}
|
|
@@ -40,8 +40,10 @@
|
|
|
40
40
|
letter-spacing: var(--duro-type-scale-letter-spacing-tight);
|
|
41
41
|
margin-block-start: var(--duro-spacing-xl);
|
|
42
42
|
margin-block-end: var(--duro-spacing-sm);
|
|
43
|
-
/* A table-of-contents jump lands with room above the heading
|
|
44
|
-
|
|
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));
|
|
45
47
|
}
|
|
46
48
|
:where(.duro-prose, [data-duro-prose]) :where(h1) {
|
|
47
49
|
font-size: var(--duro-type-scale-font-size9);
|