@duro-app/ui 5.34.2 → 5.34.3

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.34.2",
3
+ "version": "5.34.3",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -92,7 +92,7 @@
92
92
  "dependencies": {
93
93
  "@tanstack/react-virtual": "^3.14.13",
94
94
  "qrcode": "^1.5.4",
95
- "@duro-app/tokens": "^5.34.2"
95
+ "@duro-app/tokens": "^5.34.3"
96
96
  },
97
97
  "devDependencies": {
98
98
  "@babel/preset-typescript": "^7.28.0",
@@ -3,7 +3,8 @@ import {html} from 'react-strict-dom'
3
3
  import {styles} from './styles.css'
4
4
 
5
5
  export type CardVariant = 'elevated' | 'outlined' | 'filled' | 'interactive'
6
- export type CardSize = 'default' | 'compact' | 'full'
6
+ /** `flush`: no padding, the content reaching the card's rounded edge (a List of rows). */
7
+ export type CardSize = 'default' | 'compact' | 'full' | 'flush'
7
8
 
8
9
  interface CardProps {
9
10
  variant?: CardVariant
@@ -17,6 +18,7 @@ const sizeMap = {
17
18
  default: styles.sizeDefault,
18
19
  compact: styles.sizeCompact,
19
20
  full: styles.sizeFull,
21
+ flush: styles.sizeFlush,
20
22
  } as const
21
23
 
22
24
  export function Card({
@@ -65,6 +65,11 @@ export const styles = css.create({
65
65
  sizeFull: {
66
66
  padding: spacing.xl,
67
67
  },
68
+ // Rows reach the edge; the rounded corners clip them.
69
+ sizeFlush: {
70
+ padding: 0,
71
+ overflow: 'hidden',
72
+ },
68
73
  // Header
69
74
  header: {
70
75
  fontSize: typography.fontSizeLg,
@@ -7,6 +7,7 @@ import {clickLink} from '../../docs/clickLink'
7
7
  import {onThemeSurface} from '../../docs/themedSurface'
8
8
  import type {OnNavigate} from '../../shared/navigate'
9
9
  import {Icon} from '../Icon/Icon'
10
+ import {Card} from '../Card/Card'
10
11
  import {Badge} from '../Badge/Badge'
11
12
  import {Button} from '../Button/Button'
12
13
  import {Checkbox} from '../Checkbox/Checkbox'
@@ -365,3 +366,45 @@ export const LinkItems: StoryObj<{onNavigate: OnNavigate}> = {
365
366
  await expect(canvas.queryByRole('link', {name: 'Usage'})).toBeNull()
366
367
  },
367
368
  }
369
+
370
+ /**
371
+ * A settings group: the rows in a flush Card, reaching its rounded edge, with a divider between
372
+ * rows and none under the last (a hidden conditional row does not count).
373
+ */
374
+ export const InFlushCard: StoryObj<{showHidden: boolean}> = {
375
+ decorators: [onThemeSurface],
376
+ args: {showHidden: false},
377
+ render: (args) => (
378
+ <Card variant="outlined" size="flush">
379
+ <List.Root aria-label="General">
380
+ <List.Item href="#updates">
381
+ <List.Content>
382
+ <List.Text>Updates</List.Text>
383
+ </List.Content>
384
+ </List.Item>
385
+ <List.Item href="#appearance">
386
+ <List.Content>
387
+ <List.Text>Appearance</List.Text>
388
+ </List.Content>
389
+ </List.Item>
390
+ {args.showHidden ? (
391
+ <List.Item>
392
+ <List.Content>
393
+ <List.Text>Hidden</List.Text>
394
+ </List.Content>
395
+ </List.Item>
396
+ ) : null}
397
+ </List.Root>
398
+ </Card>
399
+ ),
400
+ play: async ({canvas}) => {
401
+ const list = canvas.getByRole('list', {name: 'General'})
402
+ const items = canvas.getAllByRole('listitem')
403
+ await expect(items).toHaveLength(2)
404
+ await expect(getComputedStyle(items[0]).borderBottomWidth).toBe('1px')
405
+ await expect(getComputedStyle(items[1]).borderBottomWidth).toBe('0px')
406
+ const card = list.parentElement as HTMLElement
407
+ await expect(getComputedStyle(card).paddingTop).toBe('0px')
408
+ await expect(getComputedStyle(card).overflow).toBe('hidden')
409
+ },
410
+ }
@@ -1,4 +1,13 @@
1
- import {type ReactNode, createContext, useContext, useId, useMemo, useState} from 'react'
1
+ import {
2
+ Children,
3
+ type ReactNode,
4
+ createContext,
5
+ isValidElement,
6
+ useContext,
7
+ useId,
8
+ useMemo,
9
+ useState,
10
+ } from 'react'
2
11
  import {html} from 'react-strict-dom'
3
12
  import type {OnNavigate} from '../../shared/navigate'
4
13
  import {
@@ -22,6 +31,9 @@ interface ListContextValue {
22
31
 
23
32
  const ListContext = createContext<ListContextValue>({selectionMode: 'none'})
24
33
 
34
+ // The Root's last item draws no divider below it: the list (or the card around it) ends there.
35
+ const LastItemContext = createContext(false)
36
+
25
37
  function useList() {
26
38
  return useContext(ListContext)
27
39
  }
@@ -56,7 +68,7 @@ function Root({
56
68
  aria-labelledby={hasTitle ? titleId : undefined}
57
69
  style={styles.root}
58
70
  >
59
- {children}
71
+ {markLastItem(children)}
60
72
  </html.div>
61
73
  )
62
74
  return (
@@ -78,6 +90,20 @@ function Root({
78
90
  )
79
91
  }
80
92
 
93
+ /** The Root's children, the last one told it is last (a hidden conditional row is not counted). */
94
+ function markLastItem(children: ReactNode): ReactNode {
95
+ const items = Children.toArray(children)
96
+ return items.map((child, index) =>
97
+ index === items.length - 1 ? (
98
+ <LastItemContext.Provider key={isValidElement(child) ? child.key : index} value={true}>
99
+ {child}
100
+ </LastItemContext.Provider>
101
+ ) : (
102
+ child
103
+ ),
104
+ )
105
+ }
106
+
81
107
  // --- Item ---
82
108
 
83
109
  interface ItemBaseProps {
@@ -130,6 +156,7 @@ function Item({children, selected, disabled, onClick, href, onNavigate}: ItemPro
130
156
  const {selectionMode} = useList()
131
157
  const isSelectable = selectionMode !== 'none'
132
158
  const isDisabled = disabled === true
159
+ const isLast = useContext(LastItemContext)
133
160
  const [linkState] = useState(createItemLinkState)
134
161
  const action = useMemo<ItemAction | null>(() => {
135
162
  if (href !== undefined) return {kind: 'link', href, onNavigate, disabled: isDisabled, linkState}
@@ -155,6 +182,7 @@ function Item({children, selected, disabled, onClick, href, onNavigate}: ItemPro
155
182
  styles.item,
156
183
  (onClick != null || href !== undefined) && !disabled && styles.itemClickable,
157
184
  selected && styles.itemSelected,
185
+ isLast && styles.itemLast,
158
186
  disabled && styles.itemDisabled,
159
187
  ]}
160
188
  >