@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@duro-app/ui",
3
- "version": "5.8.0",
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.0"
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
- // Anchors the copy button to the block's top-right corner.
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
- position: 'relative',
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.md,
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
- backgroundColor: colors.bgCard,
21
- borderWidth: borders.hairline,
22
- borderStyle: 'solid',
23
- borderColor: colors.border,
24
- borderRadius: radii.md,
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
- scroll-margin-top: var(--duro-spacing-lg);
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);