@duro-app/ui 5.34.2 → 5.35.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.
- package/dist/components/Card/Card.d.ts +2 -1
- package/dist/components/Card/Card.d.ts.map +1 -1
- package/dist/components/Card/styles.css.d.ts +4 -0
- package/dist/components/Card/styles.css.d.ts.map +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.stories.d.ts +7 -0
- package/dist/components/List/List.stories.d.ts.map +1 -1
- package/dist/components/Switch/Switch.d.ts +1 -1
- package/dist/components/Switch/Switch.d.ts.map +1 -1
- package/dist/components/Switch/Switch.stories.d.ts +2 -0
- package/dist/components/Switch/Switch.stories.d.ts.map +1 -1
- package/dist/components/Switch/Switch.types.d.ts +2 -0
- package/dist/components/Switch/Switch.types.d.ts.map +1 -1
- package/dist/index.js +1834 -1817
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/components/Card/Card.tsx +3 -1
- package/src/components/Card/styles.css.ts +5 -0
- package/src/components/List/List.stories.tsx +43 -0
- package/src/components/List/List.tsx +30 -2
- package/src/components/Switch/Switch.native.tsx +2 -0
- package/src/components/Switch/Switch.stories.tsx +10 -0
- package/src/components/Switch/Switch.tsx +2 -0
- package/src/components/Switch/Switch.types.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.35.0",
|
|
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.
|
|
95
|
+
"@duro-app/tokens": "^5.35.0"
|
|
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
|
-
|
|
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 {
|
|
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
|
>
|
|
@@ -17,6 +17,7 @@ export function Switch({
|
|
|
17
17
|
onCheckedChange,
|
|
18
18
|
disabled = false,
|
|
19
19
|
children,
|
|
20
|
+
'aria-label': ariaLabel,
|
|
20
21
|
}: SwitchProps) {
|
|
21
22
|
const [checked, setChecked] = useControllableValue(
|
|
22
23
|
controlledChecked,
|
|
@@ -33,6 +34,7 @@ export function Switch({
|
|
|
33
34
|
type="button"
|
|
34
35
|
role="switch"
|
|
35
36
|
aria-checked={checked}
|
|
37
|
+
aria-label={!children ? ariaLabel : undefined}
|
|
36
38
|
disabled={disabled}
|
|
37
39
|
onClick={handleClick}
|
|
38
40
|
style={[styles.track, checked ? styles.trackChecked : styles.trackUnchecked]}
|
|
@@ -40,6 +40,16 @@ export const Checked: Story = {
|
|
|
40
40
|
},
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/** No visible label: a settings row shows the title, and `aria-label` names the switch. */
|
|
44
|
+
export const Labelled: Story = {
|
|
45
|
+
args: {'aria-label': 'Analytics'},
|
|
46
|
+
play: async ({canvas, userEvent, args}) => {
|
|
47
|
+
const switchEl = canvas.getByRole('switch', {name: 'Analytics'})
|
|
48
|
+
await userEvent.click(switchEl)
|
|
49
|
+
await expect(args.onCheckedChange).toHaveBeenCalledWith(true)
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
43
53
|
export const Disabled: Story = {
|
|
44
54
|
args: {disabled: true, children: 'Cannot toggle'},
|
|
45
55
|
play: async ({canvas, userEvent, args}) => {
|
|
@@ -15,6 +15,7 @@ export function Switch({
|
|
|
15
15
|
name,
|
|
16
16
|
value,
|
|
17
17
|
children,
|
|
18
|
+
'aria-label': ariaLabel,
|
|
18
19
|
}: SwitchProps) {
|
|
19
20
|
const [checked, setChecked] = useControllableValue(
|
|
20
21
|
controlledChecked,
|
|
@@ -46,6 +47,7 @@ export function Switch({
|
|
|
46
47
|
defaultChecked={controlledChecked === undefined ? defaultChecked : undefined}
|
|
47
48
|
disabled={disabled}
|
|
48
49
|
aria-checked={checked}
|
|
50
|
+
aria-label={!children ? ariaLabel : undefined}
|
|
49
51
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
50
52
|
onCheckedChange?.(e.target.checked)
|
|
51
53
|
}}
|