@duro-app/ui 0.22.0 → 0.24.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/Alert/Alert.meta.d.ts.map +1 -1
- package/dist/components/ButtonGroup/ButtonGroup.d.ts +14 -0
- package/dist/components/ButtonGroup/ButtonGroup.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts +3 -0
- package/dist/components/ButtonGroup/ButtonGroup.meta.d.ts.map +1 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts +11 -0
- package/dist/components/ButtonGroup/ButtonGroup.stories.d.ts.map +1 -0
- package/dist/components/ButtonGroup/styles.css.d.ts +45 -0
- package/dist/components/ButtonGroup/styles.css.d.ts.map +1 -0
- package/dist/components/Callout/Callout.meta.d.ts.map +1 -1
- package/dist/components/Card/Card.meta.d.ts.map +1 -1
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts +22 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts +3 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.meta.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts +9 -0
- package/dist/components/CheckboxGroup/CheckboxGroup.stories.d.ts.map +1 -0
- package/dist/components/CheckboxGroup/styles.css.d.ts +12 -0
- package/dist/components/CheckboxGroup/styles.css.d.ts.map +1 -0
- package/dist/components/EmptyState/EmptyState.meta.d.ts.map +1 -1
- package/dist/components/InputGroup/InputGroup.meta.d.ts.map +1 -1
- package/dist/components/List/List.d.ts.map +1 -1
- package/dist/components/List/List.meta.d.ts.map +1 -1
- package/dist/components/List/List.stories.d.ts.map +1 -1
- package/dist/components/RadioGroup/RadioGroup.d.ts +22 -0
- package/dist/components/RadioGroup/RadioGroup.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts +3 -0
- package/dist/components/RadioGroup/RadioGroup.meta.d.ts.map +1 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts +9 -0
- package/dist/components/RadioGroup/RadioGroup.stories.d.ts.map +1 -0
- package/dist/components/RadioGroup/styles.css.d.ts +60 -0
- package/dist/components/RadioGroup/styles.css.d.ts.map +1 -0
- package/dist/components/StatusIcon/StatusIcon.meta.d.ts.map +1 -1
- package/dist/components/Table/ColumnFilter.d.ts +8 -0
- package/dist/components/Table/ColumnFilter.d.ts.map +1 -0
- package/dist/components/Table/Pagination.d.ts +7 -0
- package/dist/components/Table/Pagination.d.ts.map +1 -0
- package/dist/components/Table/SortIndicator.d.ts +16 -0
- package/dist/components/Table/SortIndicator.d.ts.map +1 -0
- package/dist/components/Table/Table.d.ts +10 -3
- package/dist/components/Table/Table.d.ts.map +1 -1
- package/dist/components/Table/Table.stories.d.ts +4 -0
- package/dist/components/Table/Table.stories.d.ts.map +1 -1
- package/dist/components/Table/styles.css.d.ts +12 -4
- package/dist/components/Table/styles.css.d.ts.map +1 -1
- package/dist/components/Table/useDataTable.d.ts +32 -0
- package/dist/components/Table/useDataTable.d.ts.map +1 -0
- package/dist/components/ThemeProvider/ThemeProvider.meta.d.ts.map +1 -1
- package/dist/index.css +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2399 -1927
- package/dist/index.js.map +1 -1
- package/package.json +3 -2
- package/src/components/ActionBar/ActionBar.stories.tsx +3 -3
- package/src/components/Alert/Alert.meta.ts +4 -1
- package/src/components/ButtonGroup/ButtonGroup.meta.ts +24 -0
- package/src/components/ButtonGroup/ButtonGroup.stories.tsx +103 -0
- package/src/components/ButtonGroup/ButtonGroup.tsx +59 -0
- package/src/components/ButtonGroup/styles.css.ts +28 -0
- package/src/components/Callout/Callout.meta.ts +4 -1
- package/src/components/Card/Card.meta.ts +4 -1
- package/src/components/Checkbox/styles.css.ts +1 -1
- package/src/components/CheckboxGroup/CheckboxGroup.meta.ts +25 -0
- package/src/components/CheckboxGroup/CheckboxGroup.stories.tsx +88 -0
- package/src/components/CheckboxGroup/CheckboxGroup.tsx +105 -0
- package/src/components/CheckboxGroup/styles.css.ts +13 -0
- package/src/components/EmptyState/EmptyState.meta.ts +1 -4
- package/src/components/Icon/Icon.meta.ts +5 -5
- package/src/components/InputGroup/InputGroup.meta.ts +4 -1
- package/src/components/List/List.meta.ts +8 -2
- package/src/components/List/List.stories.tsx +35 -15
- package/src/components/List/List.tsx +1 -5
- package/src/components/RadioGroup/RadioGroup.meta.ts +25 -0
- package/src/components/RadioGroup/RadioGroup.stories.tsx +83 -0
- package/src/components/RadioGroup/RadioGroup.tsx +113 -0
- package/src/components/RadioGroup/styles.css.ts +66 -0
- package/src/components/StatusIcon/StatusIcon.meta.ts +1 -4
- package/src/components/Table/ColumnFilter.tsx +46 -0
- package/src/components/Table/Pagination.tsx +82 -0
- package/src/components/Table/SortIndicator.tsx +56 -0
- package/src/components/Table/Table.stories.tsx +355 -9
- package/src/components/Table/Table.tsx +62 -17
- package/src/components/Table/styles.css.ts +18 -8
- package/src/components/Table/useDataTable.ts +113 -0
- package/src/components/ThemeProvider/ThemeProvider.meta.ts +1 -3
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +5 -0
- package/src/strict.css +5 -0
|
@@ -32,7 +32,9 @@ export const Default: Story = {
|
|
|
32
32
|
<List.Description>Reverse proxy · Port 443</List.Description>
|
|
33
33
|
</List.Content>
|
|
34
34
|
<List.Actions>
|
|
35
|
-
<Badge variant="success" size="sm">
|
|
35
|
+
<Badge variant="success" size="sm">
|
|
36
|
+
Running
|
|
37
|
+
</Badge>
|
|
36
38
|
</List.Actions>
|
|
37
39
|
</List.Item>
|
|
38
40
|
<List.Item>
|
|
@@ -41,7 +43,9 @@ export const Default: Story = {
|
|
|
41
43
|
<List.Description>Web server · Port 80</List.Description>
|
|
42
44
|
</List.Content>
|
|
43
45
|
<List.Actions>
|
|
44
|
-
<Badge variant="success" size="sm">
|
|
46
|
+
<Badge variant="success" size="sm">
|
|
47
|
+
Running
|
|
48
|
+
</Badge>
|
|
45
49
|
</List.Actions>
|
|
46
50
|
</List.Item>
|
|
47
51
|
<List.Item>
|
|
@@ -50,7 +54,9 @@ export const Default: Story = {
|
|
|
50
54
|
<List.Description>DNS filter · Port 53</List.Description>
|
|
51
55
|
</List.Content>
|
|
52
56
|
<List.Actions>
|
|
53
|
-
<Badge variant="default" size="sm">
|
|
57
|
+
<Badge variant="default" size="sm">
|
|
58
|
+
Stopped
|
|
59
|
+
</Badge>
|
|
54
60
|
</List.Actions>
|
|
55
61
|
</List.Item>
|
|
56
62
|
</List.Root>
|
|
@@ -77,9 +83,27 @@ const narrowContainer = css.create({
|
|
|
77
83
|
|
|
78
84
|
function SelectionDemo() {
|
|
79
85
|
const certs = [
|
|
80
|
-
{
|
|
81
|
-
|
|
82
|
-
|
|
86
|
+
{
|
|
87
|
+
id: 'c1',
|
|
88
|
+
serial: 'aa:bb:cc:dd:01',
|
|
89
|
+
issued: 'Mar 21, 2026',
|
|
90
|
+
expires: 'Jun 19, 2026',
|
|
91
|
+
status: 'active' as const,
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'c2',
|
|
95
|
+
serial: 'aa:bb:cc:dd:02',
|
|
96
|
+
issued: 'Mar 21, 2026',
|
|
97
|
+
expires: 'Jun 19, 2026',
|
|
98
|
+
status: 'active' as const,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
id: 'c3',
|
|
102
|
+
serial: 'ee:ff:00:11:03',
|
|
103
|
+
issued: 'Jan 10, 2025',
|
|
104
|
+
expires: 'Jan 10, 2026',
|
|
105
|
+
status: 'expired' as const,
|
|
106
|
+
},
|
|
83
107
|
]
|
|
84
108
|
|
|
85
109
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
|
@@ -117,10 +141,7 @@ function SelectionDemo() {
|
|
|
117
141
|
</List.Description>
|
|
118
142
|
</List.Content>
|
|
119
143
|
<List.Actions>
|
|
120
|
-
<Badge
|
|
121
|
-
variant={cert.status === 'active' ? 'success' : 'default'}
|
|
122
|
-
size="sm"
|
|
123
|
-
>
|
|
144
|
+
<Badge variant={cert.status === 'active' ? 'success' : 'default'} size="sm">
|
|
124
145
|
{cert.status}
|
|
125
146
|
</Badge>
|
|
126
147
|
{cert.status === 'active' && (
|
|
@@ -178,13 +199,12 @@ function DetailPanelDemo() {
|
|
|
178
199
|
>
|
|
179
200
|
<List.Content>
|
|
180
201
|
<List.Text>{user.name}</List.Text>
|
|
181
|
-
<List.Description>
|
|
202
|
+
<List.Description>
|
|
203
|
+
{user.email} · {user.role}
|
|
204
|
+
</List.Description>
|
|
182
205
|
</List.Content>
|
|
183
206
|
<List.Actions>
|
|
184
|
-
<Badge
|
|
185
|
-
variant={user.status === 'Active' ? 'success' : 'default'}
|
|
186
|
-
size="sm"
|
|
187
|
-
>
|
|
207
|
+
<Badge variant={user.status === 'Active' ? 'success' : 'default'} size="sm">
|
|
188
208
|
{user.status}
|
|
189
209
|
</Badge>
|
|
190
210
|
</List.Actions>
|
|
@@ -60,11 +60,7 @@ function Item({children, selected, disabled, onClick}: ItemProps) {
|
|
|
60
60
|
aria-selected={isSelectable ? selected : undefined}
|
|
61
61
|
aria-disabled={disabled || undefined}
|
|
62
62
|
onClick={disabled ? undefined : onClick}
|
|
63
|
-
style={[
|
|
64
|
-
styles.item,
|
|
65
|
-
selected && styles.itemSelected,
|
|
66
|
-
disabled && styles.itemDisabled,
|
|
67
|
-
]}
|
|
63
|
+
style={[styles.item, selected && styles.itemSelected, disabled && styles.itemDisabled]}
|
|
68
64
|
>
|
|
69
65
|
{children}
|
|
70
66
|
</html.div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Radio button group for single-select from a list of options. Compound component with Root container and Item children.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Selecting exactly one option from a small set (2-5 items)',
|
|
8
|
+
'Options need to be visible at all times (not hidden in a dropdown)',
|
|
9
|
+
],
|
|
10
|
+
whenNotToUse: [
|
|
11
|
+
'Many options (5+) — use Select for space efficiency',
|
|
12
|
+
'Multi-select — use CheckboxGroup instead',
|
|
13
|
+
'Binary on/off — use Switch',
|
|
14
|
+
],
|
|
15
|
+
relatedTo: [
|
|
16
|
+
{component: 'CheckboxGroup', relationship: 'CheckboxGroup for multi-select; RadioGroup for single-select'},
|
|
17
|
+
{component: 'Select', relationship: 'Select for larger option sets or when space is limited'},
|
|
18
|
+
{component: 'ToggleGroup', relationship: 'ToggleGroup for toolbar-style selection'},
|
|
19
|
+
],
|
|
20
|
+
example: `<RadioGroup.Root defaultValue="email" onValueChange={(v) => console.log(v)}>
|
|
21
|
+
<RadioGroup.Item value="email">Email</RadioGroup.Item>
|
|
22
|
+
<RadioGroup.Item value="sms">SMS</RadioGroup.Item>
|
|
23
|
+
<RadioGroup.Item value="push">Push notification</RadioGroup.Item>
|
|
24
|
+
</RadioGroup.Root>`,
|
|
25
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import {useState} from 'react'
|
|
2
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
3
|
+
import {expect, fn} from 'storybook/test'
|
|
4
|
+
import {css, html} from 'react-strict-dom'
|
|
5
|
+
import {RadioGroup} from './RadioGroup'
|
|
6
|
+
|
|
7
|
+
const meta: Meta = {
|
|
8
|
+
title: 'Components/RadioGroup',
|
|
9
|
+
argTypes: {
|
|
10
|
+
orientation: {control: 'select', options: ['horizontal', 'vertical']},
|
|
11
|
+
disabled: {control: 'boolean'},
|
|
12
|
+
},
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export default meta
|
|
16
|
+
type Story = StoryObj
|
|
17
|
+
|
|
18
|
+
export const Default: Story = {
|
|
19
|
+
render: () => (
|
|
20
|
+
<RadioGroup.Root defaultValue="apple">
|
|
21
|
+
<RadioGroup.Item value="apple">Apple</RadioGroup.Item>
|
|
22
|
+
<RadioGroup.Item value="banana">Banana</RadioGroup.Item>
|
|
23
|
+
<RadioGroup.Item value="cherry">Cherry</RadioGroup.Item>
|
|
24
|
+
</RadioGroup.Root>
|
|
25
|
+
),
|
|
26
|
+
play: async ({canvas}) => {
|
|
27
|
+
const radios = canvas.getAllByRole('radio')
|
|
28
|
+
await expect(radios.length).toBe(3)
|
|
29
|
+
await expect(radios[0]).toBeChecked()
|
|
30
|
+
await expect(radios[1]).not.toBeChecked()
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const Horizontal: Story = {
|
|
35
|
+
render: () => (
|
|
36
|
+
<RadioGroup.Root defaultValue="sm" orientation="horizontal">
|
|
37
|
+
<RadioGroup.Item value="sm">Small</RadioGroup.Item>
|
|
38
|
+
<RadioGroup.Item value="md">Medium</RadioGroup.Item>
|
|
39
|
+
<RadioGroup.Item value="lg">Large</RadioGroup.Item>
|
|
40
|
+
</RadioGroup.Root>
|
|
41
|
+
),
|
|
42
|
+
play: async ({canvas}) => {
|
|
43
|
+
const group = canvas.getByRole('radiogroup')
|
|
44
|
+
await expect(group).toHaveAttribute('aria-orientation', 'horizontal')
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const Disabled: Story = {
|
|
49
|
+
render: () => (
|
|
50
|
+
<RadioGroup.Root defaultValue="on" disabled>
|
|
51
|
+
<RadioGroup.Item value="on">On</RadioGroup.Item>
|
|
52
|
+
<RadioGroup.Item value="off">Off</RadioGroup.Item>
|
|
53
|
+
</RadioGroup.Root>
|
|
54
|
+
),
|
|
55
|
+
play: async ({canvas}) => {
|
|
56
|
+
const radios = canvas.getAllByRole('radio')
|
|
57
|
+
await expect(radios[0]).toBeDisabled()
|
|
58
|
+
await expect(radios[1]).toBeDisabled()
|
|
59
|
+
},
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const Controlled: Story = {
|
|
63
|
+
render: () => {
|
|
64
|
+
const [value, setValue] = useState('red')
|
|
65
|
+
return (
|
|
66
|
+
<html.div>
|
|
67
|
+
<RadioGroup.Root value={value} onValueChange={setValue}>
|
|
68
|
+
<RadioGroup.Item value="red">Red</RadioGroup.Item>
|
|
69
|
+
<RadioGroup.Item value="green">Green</RadioGroup.Item>
|
|
70
|
+
<RadioGroup.Item value="blue">Blue</RadioGroup.Item>
|
|
71
|
+
</RadioGroup.Root>
|
|
72
|
+
</html.div>
|
|
73
|
+
)
|
|
74
|
+
},
|
|
75
|
+
play: async ({canvas, userEvent}) => {
|
|
76
|
+
const radios = canvas.getAllByRole('radio')
|
|
77
|
+
await expect(radios[0]).toBeChecked()
|
|
78
|
+
|
|
79
|
+
await userEvent.click(radios[2])
|
|
80
|
+
await expect(radios[2]).toBeChecked()
|
|
81
|
+
await expect(radios[0]).not.toBeChecked()
|
|
82
|
+
},
|
|
83
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import {type ReactNode, createContext, useContext, useCallback} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {useControllableValue} from '../../hooks/useControllableValue'
|
|
4
|
+
import {styles} from './styles.css'
|
|
5
|
+
|
|
6
|
+
// --- Context ---
|
|
7
|
+
|
|
8
|
+
interface RadioGroupContextValue {
|
|
9
|
+
value: string
|
|
10
|
+
onSelect: (value: string) => void
|
|
11
|
+
disabled: boolean
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const RadioGroupContext = createContext<RadioGroupContextValue | null>(null)
|
|
15
|
+
|
|
16
|
+
function useRadioGroup() {
|
|
17
|
+
const ctx = useContext(RadioGroupContext)
|
|
18
|
+
if (!ctx) {
|
|
19
|
+
throw new Error('RadioGroup.Item must be used within RadioGroup.Root')
|
|
20
|
+
}
|
|
21
|
+
return ctx
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// --- Root ---
|
|
25
|
+
|
|
26
|
+
interface RootProps {
|
|
27
|
+
value?: string
|
|
28
|
+
defaultValue?: string
|
|
29
|
+
onValueChange?: (value: string) => void
|
|
30
|
+
orientation?: 'horizontal' | 'vertical'
|
|
31
|
+
disabled?: boolean
|
|
32
|
+
children: ReactNode
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function Root({
|
|
36
|
+
value: controlledValue,
|
|
37
|
+
defaultValue = '',
|
|
38
|
+
onValueChange,
|
|
39
|
+
orientation = 'vertical',
|
|
40
|
+
disabled = false,
|
|
41
|
+
children,
|
|
42
|
+
}: RootProps) {
|
|
43
|
+
const [value, setValue] = useControllableValue(controlledValue, defaultValue, onValueChange)
|
|
44
|
+
|
|
45
|
+
const onSelect = useCallback(
|
|
46
|
+
(itemValue: string) => {
|
|
47
|
+
if (!disabled) {
|
|
48
|
+
setValue(itemValue)
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
[disabled, setValue],
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
return (
|
|
55
|
+
<RadioGroupContext.Provider value={{value, onSelect, disabled}}>
|
|
56
|
+
<html.div
|
|
57
|
+
role="radiogroup"
|
|
58
|
+
aria-orientation={orientation}
|
|
59
|
+
style={[styles.root, orientation === 'horizontal' && styles.rootHorizontal]}
|
|
60
|
+
>
|
|
61
|
+
{children}
|
|
62
|
+
</html.div>
|
|
63
|
+
</RadioGroupContext.Provider>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// --- Item ---
|
|
68
|
+
|
|
69
|
+
interface ItemProps {
|
|
70
|
+
value: string
|
|
71
|
+
disabled?: boolean
|
|
72
|
+
children: ReactNode
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Item({value, disabled: itemDisabled = false, children}: ItemProps) {
|
|
76
|
+
const {value: groupValue, onSelect, disabled: groupDisabled} = useRadioGroup()
|
|
77
|
+
const isChecked = groupValue === value
|
|
78
|
+
const isDisabled = groupDisabled || itemDisabled
|
|
79
|
+
|
|
80
|
+
const handleClick = () => {
|
|
81
|
+
if (!isDisabled) {
|
|
82
|
+
onSelect(value)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<html.label
|
|
88
|
+
style={[styles.item, isDisabled && styles.itemDisabled]}
|
|
89
|
+
onClick={handleClick}
|
|
90
|
+
>
|
|
91
|
+
<html.input
|
|
92
|
+
type="radio"
|
|
93
|
+
value={value}
|
|
94
|
+
checked={isChecked}
|
|
95
|
+
disabled={isDisabled}
|
|
96
|
+
onChange={() => onSelect(value)}
|
|
97
|
+
style={styles.input}
|
|
98
|
+
/>
|
|
99
|
+
<html.span
|
|
100
|
+
style={[styles.circle, isChecked ? styles.circleChecked : styles.circleUnchecked]}
|
|
101
|
+
aria-hidden
|
|
102
|
+
>
|
|
103
|
+
{isChecked && <html.span style={styles.dot} />}
|
|
104
|
+
</html.span>
|
|
105
|
+
{children && <html.span>{children}</html.span>}
|
|
106
|
+
</html.label>
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const RadioGroup = {
|
|
111
|
+
Root,
|
|
112
|
+
Item,
|
|
113
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
3
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
4
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
5
|
+
|
|
6
|
+
export const styles = css.create({
|
|
7
|
+
root: {
|
|
8
|
+
display: 'flex',
|
|
9
|
+
flexDirection: 'column',
|
|
10
|
+
gap: spacing.sm,
|
|
11
|
+
},
|
|
12
|
+
rootHorizontal: {
|
|
13
|
+
flexDirection: 'row',
|
|
14
|
+
},
|
|
15
|
+
item: {
|
|
16
|
+
display: 'inline-flex',
|
|
17
|
+
alignItems: 'center',
|
|
18
|
+
gap: spacing.sm,
|
|
19
|
+
cursor: 'pointer',
|
|
20
|
+
fontSize: typography.fontSizeSm,
|
|
21
|
+
color: colors.text,
|
|
22
|
+
lineHeight: typography.lineHeight,
|
|
23
|
+
},
|
|
24
|
+
itemDisabled: {
|
|
25
|
+
opacity: 0.5,
|
|
26
|
+
cursor: 'not-allowed',
|
|
27
|
+
},
|
|
28
|
+
circle: {
|
|
29
|
+
width: 18,
|
|
30
|
+
height: 18,
|
|
31
|
+
borderWidth: 1,
|
|
32
|
+
borderStyle: 'solid',
|
|
33
|
+
borderRadius: radii.full,
|
|
34
|
+
display: 'inline-flex',
|
|
35
|
+
alignItems: 'center',
|
|
36
|
+
justifyContent: 'center',
|
|
37
|
+
flexShrink: 0,
|
|
38
|
+
transitionProperty: 'background-color, border-color',
|
|
39
|
+
transitionDuration: '150ms',
|
|
40
|
+
transitionTimingFunction: 'ease',
|
|
41
|
+
},
|
|
42
|
+
circleUnchecked: {
|
|
43
|
+
backgroundColor: colors.bg,
|
|
44
|
+
borderColor: {
|
|
45
|
+
default: colors.border,
|
|
46
|
+
':hover': colors.textMuted,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
circleChecked: {
|
|
50
|
+
backgroundColor: colors.bg,
|
|
51
|
+
borderColor: colors.accent,
|
|
52
|
+
},
|
|
53
|
+
dot: {
|
|
54
|
+
width: 8,
|
|
55
|
+
height: 8,
|
|
56
|
+
borderRadius: radii.full,
|
|
57
|
+
backgroundColor: colors.accent,
|
|
58
|
+
},
|
|
59
|
+
input: {
|
|
60
|
+
position: 'absolute',
|
|
61
|
+
width: 1,
|
|
62
|
+
height: 1,
|
|
63
|
+
opacity: 0,
|
|
64
|
+
overflow: 'hidden',
|
|
65
|
+
},
|
|
66
|
+
})
|
|
@@ -8,10 +8,7 @@ export const meta: ComponentMeta = {
|
|
|
8
8
|
'List item status icons with colored backgrounds',
|
|
9
9
|
'Feature or capability indicators',
|
|
10
10
|
],
|
|
11
|
-
whenNotToUse: [
|
|
12
|
-
'Inline icons without background — use Icon',
|
|
13
|
-
'Status text labels — use Badge',
|
|
14
|
-
],
|
|
11
|
+
whenNotToUse: ['Inline icons without background — use Icon', 'Status text labels — use Badge'],
|
|
15
12
|
relatedTo: [
|
|
16
13
|
{component: 'Icon', relationship: 'StatusIcon wraps Icon with background; Icon is bare SVG'},
|
|
17
14
|
{component: 'Badge', relationship: 'Badge for text status; StatusIcon for icon status'},
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type {Column} from '@tanstack/react-table'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {css} from 'react-strict-dom'
|
|
4
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
5
|
+
import {spacing, radii} from '@duro-app/tokens/tokens/spacing.css'
|
|
6
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
7
|
+
|
|
8
|
+
const styles = css.create({
|
|
9
|
+
input: {
|
|
10
|
+
width: '100%',
|
|
11
|
+
paddingTop: 2,
|
|
12
|
+
paddingBottom: 2,
|
|
13
|
+
paddingLeft: spacing.xs,
|
|
14
|
+
paddingRight: spacing.xs,
|
|
15
|
+
marginTop: spacing.xs,
|
|
16
|
+
borderWidth: 1,
|
|
17
|
+
borderStyle: 'solid',
|
|
18
|
+
borderColor: colors.border,
|
|
19
|
+
borderRadius: radii.xs,
|
|
20
|
+
backgroundColor: colors.bg,
|
|
21
|
+
color: colors.text,
|
|
22
|
+
fontSize: typography.fontSizeXs,
|
|
23
|
+
outline: 'none',
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
interface ColumnFilterProps<TData> {
|
|
28
|
+
column: Column<TData, unknown>
|
|
29
|
+
placeholder?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function ColumnFilter<TData>({column, placeholder}: ColumnFilterProps<TData>) {
|
|
33
|
+
if (!column.getCanFilter()) return null
|
|
34
|
+
|
|
35
|
+
const filterValue = (column.getFilterValue() as string) ?? ''
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<html.input
|
|
39
|
+
style={styles.input}
|
|
40
|
+
type="text"
|
|
41
|
+
value={filterValue}
|
|
42
|
+
onChange={(e: any) => column.setFilterValue(e.target.value || undefined)}
|
|
43
|
+
placeholder={placeholder ?? `Filter...`}
|
|
44
|
+
/>
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type {Table} from '@tanstack/react-table'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {css} from 'react-strict-dom'
|
|
4
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
5
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
6
|
+
import {typography} from '@duro-app/tokens/tokens/typography.css'
|
|
7
|
+
|
|
8
|
+
const styles = css.create({
|
|
9
|
+
root: {
|
|
10
|
+
display: 'flex',
|
|
11
|
+
alignItems: 'center',
|
|
12
|
+
justifyContent: 'center',
|
|
13
|
+
gap: spacing.sm,
|
|
14
|
+
paddingTop: spacing.md,
|
|
15
|
+
paddingBottom: spacing.sm,
|
|
16
|
+
},
|
|
17
|
+
button: {
|
|
18
|
+
display: 'inline-flex',
|
|
19
|
+
alignItems: 'center',
|
|
20
|
+
justifyContent: 'center',
|
|
21
|
+
paddingTop: spacing.xs,
|
|
22
|
+
paddingBottom: spacing.xs,
|
|
23
|
+
paddingLeft: spacing.sm,
|
|
24
|
+
paddingRight: spacing.sm,
|
|
25
|
+
borderWidth: 1,
|
|
26
|
+
borderStyle: 'solid',
|
|
27
|
+
borderColor: colors.border,
|
|
28
|
+
borderRadius: 6,
|
|
29
|
+
backgroundColor: {
|
|
30
|
+
default: 'transparent',
|
|
31
|
+
':hover': colors.bgCardHover,
|
|
32
|
+
},
|
|
33
|
+
color: colors.text,
|
|
34
|
+
fontSize: typography.fontSizeSm,
|
|
35
|
+
cursor: 'pointer',
|
|
36
|
+
transitionProperty: 'background-color',
|
|
37
|
+
transitionDuration: '150ms',
|
|
38
|
+
},
|
|
39
|
+
buttonDisabled: {
|
|
40
|
+
opacity: 0.4,
|
|
41
|
+
cursor: 'not-allowed',
|
|
42
|
+
backgroundColor: 'transparent',
|
|
43
|
+
},
|
|
44
|
+
pageInfo: {
|
|
45
|
+
fontSize: typography.fontSizeSm,
|
|
46
|
+
color: colors.textMuted,
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
interface PaginationProps<TData> {
|
|
51
|
+
table: Table<TData>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function Pagination<TData>({table}: PaginationProps<TData>) {
|
|
55
|
+
const {pageIndex, pageSize} = table.getState().pagination
|
|
56
|
+
const pageCount = table.getPageCount()
|
|
57
|
+
const totalRows = table.getFilteredRowModel().rows.length
|
|
58
|
+
|
|
59
|
+
if (totalRows <= pageSize) return null
|
|
60
|
+
|
|
61
|
+
return (
|
|
62
|
+
<html.div style={styles.root}>
|
|
63
|
+
<html.button
|
|
64
|
+
style={[styles.button, !table.getCanPreviousPage() && styles.buttonDisabled]}
|
|
65
|
+
onClick={() => table.previousPage()}
|
|
66
|
+
disabled={!table.getCanPreviousPage()}
|
|
67
|
+
>
|
|
68
|
+
Previous
|
|
69
|
+
</html.button>
|
|
70
|
+
<html.span style={styles.pageInfo}>
|
|
71
|
+
{pageIndex + 1} / {pageCount}
|
|
72
|
+
</html.span>
|
|
73
|
+
<html.button
|
|
74
|
+
style={[styles.button, !table.getCanNextPage() && styles.buttonDisabled]}
|
|
75
|
+
onClick={() => table.nextPage()}
|
|
76
|
+
disabled={!table.getCanNextPage()}
|
|
77
|
+
>
|
|
78
|
+
Next
|
|
79
|
+
</html.button>
|
|
80
|
+
</html.div>
|
|
81
|
+
)
|
|
82
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type {Column} from '@tanstack/react-table'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {css} from 'react-strict-dom'
|
|
4
|
+
import {colors} from '@duro-app/tokens/tokens/colors.css'
|
|
5
|
+
|
|
6
|
+
const styles = css.create({
|
|
7
|
+
root: {
|
|
8
|
+
display: 'inline-flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
marginLeft: 4,
|
|
11
|
+
fontSize: 10,
|
|
12
|
+
userSelect: 'none',
|
|
13
|
+
// Hidden by default, shown on hover or when active
|
|
14
|
+
opacity: 0,
|
|
15
|
+
transitionProperty: 'opacity',
|
|
16
|
+
transitionDuration: '120ms',
|
|
17
|
+
},
|
|
18
|
+
active: {
|
|
19
|
+
opacity: 1,
|
|
20
|
+
color: colors.accent,
|
|
21
|
+
},
|
|
22
|
+
inactive: {
|
|
23
|
+
color: colors.textMuted,
|
|
24
|
+
},
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
interface SortIndicatorProps<TData> {
|
|
28
|
+
column: Column<TData, unknown>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Sort direction chevron for table column headers.
|
|
33
|
+
*
|
|
34
|
+
* - Shows \u25B2 (asc), \u25BC (desc), or \u21C5 (unsorted)
|
|
35
|
+
* - Hidden by default, visible on header hover or when actively sorted
|
|
36
|
+
* - Clicking the header cell should call column.getToggleSortingHandler()
|
|
37
|
+
*
|
|
38
|
+
* The parent HeaderCell applies a :hover rule that sets child opacity to 1.
|
|
39
|
+
*/
|
|
40
|
+
export function SortIndicator<TData>({column}: SortIndicatorProps<TData>) {
|
|
41
|
+
if (!column.getCanSort()) return null
|
|
42
|
+
|
|
43
|
+
const sorted = column.getIsSorted()
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<html.span
|
|
47
|
+
style={[
|
|
48
|
+
styles.root,
|
|
49
|
+
sorted ? styles.active : styles.inactive,
|
|
50
|
+
]}
|
|
51
|
+
data-sort-indicator=""
|
|
52
|
+
>
|
|
53
|
+
{sorted === 'asc' ? '\u25B2' : sorted === 'desc' ? '\u25BC' : '\u21C5'}
|
|
54
|
+
</html.span>
|
|
55
|
+
)
|
|
56
|
+
}
|