@duro-app/ui 0.23.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/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/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/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 +7 -1
- 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 +11 -3
- 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/index.css +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2181 -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/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/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/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 +346 -0
- package/src/components/Table/Table.tsx +52 -23
- package/src/components/Table/styles.css.ts +16 -6
- package/src/components/Table/useDataTable.ts +113 -0
- package/src/docs/helpers.tsx +2 -2
- package/src/index.ts +3 -0
- package/src/strict.css +5 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@duro-app/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
},
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@hookform/resolvers": "^5.0.0",
|
|
37
|
+
"@tanstack/react-table": ">=8.0.0",
|
|
37
38
|
"effect": "^3.0.0",
|
|
38
39
|
"react": "^19.0.0",
|
|
39
40
|
"react-dom": "^19.0.0",
|
|
@@ -52,7 +53,7 @@
|
|
|
52
53
|
}
|
|
53
54
|
},
|
|
54
55
|
"dependencies": {
|
|
55
|
-
"@duro-app/tokens": "^0.
|
|
56
|
+
"@duro-app/tokens": "^0.24.0"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
58
59
|
"@babel/preset-typescript": "^7.28.0",
|
|
@@ -85,7 +85,7 @@ function SelectableTable({
|
|
|
85
85
|
|
|
86
86
|
return (
|
|
87
87
|
<>
|
|
88
|
-
<Table.Root
|
|
88
|
+
<Table.Root>
|
|
89
89
|
<Table.Header>
|
|
90
90
|
<Table.Row>
|
|
91
91
|
<Table.HeaderCell aria-label="Select">
|
|
@@ -172,7 +172,7 @@ export const Emphasized: Story = {
|
|
|
172
172
|
}
|
|
173
173
|
return (
|
|
174
174
|
<>
|
|
175
|
-
<Table.Root
|
|
175
|
+
<Table.Root>
|
|
176
176
|
<Table.Header>
|
|
177
177
|
<Table.Row>
|
|
178
178
|
<Table.HeaderCell aria-label="Select">
|
|
@@ -230,7 +230,7 @@ export const Emphasized: Story = {
|
|
|
230
230
|
export const CustomLabel: Story = {
|
|
231
231
|
render: () => (
|
|
232
232
|
<>
|
|
233
|
-
<Table.Root
|
|
233
|
+
<Table.Root>
|
|
234
234
|
<Table.Header>
|
|
235
235
|
<Table.Row>
|
|
236
236
|
<Table.HeaderCell>Name</Table.HeaderCell>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {ComponentMeta} from '../component-meta'
|
|
2
|
+
|
|
3
|
+
export const meta: ComponentMeta = {
|
|
4
|
+
description:
|
|
5
|
+
'Groups related buttons together with consistent spacing and layout. Supports horizontal and vertical orientation with alignment control.',
|
|
6
|
+
whenToUse: [
|
|
7
|
+
'Dialog footers with Save/Cancel actions',
|
|
8
|
+
'Toolbar-style groupings of related actions',
|
|
9
|
+
'Form submission areas with multiple actions',
|
|
10
|
+
],
|
|
11
|
+
whenNotToUse: [
|
|
12
|
+
'Single button — just use Button directly',
|
|
13
|
+
'Wrapping tags or badges — use Cluster instead',
|
|
14
|
+
'Navigation items — use Inline or SideNav',
|
|
15
|
+
],
|
|
16
|
+
relatedTo: [
|
|
17
|
+
{component: 'Inline', relationship: 'Inline is a general-purpose horizontal layout; ButtonGroup adds role="group" and disabled support'},
|
|
18
|
+
{component: 'ActionBar', relationship: 'ActionBar is a floating bulk-selection toolbar; ButtonGroup is a static grouping'},
|
|
19
|
+
],
|
|
20
|
+
example: `<ButtonGroup align="end" gap="sm">
|
|
21
|
+
<Button variant="secondary">Cancel</Button>
|
|
22
|
+
<Button variant="primary">Save</Button>
|
|
23
|
+
</ButtonGroup>`,
|
|
24
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {Meta, StoryObj} from '@storybook/react'
|
|
2
|
+
import {expect} from 'storybook/test'
|
|
3
|
+
import {ButtonGroup} from './ButtonGroup'
|
|
4
|
+
import {Button} from '../Button/Button'
|
|
5
|
+
|
|
6
|
+
const meta: Meta<typeof ButtonGroup> = {
|
|
7
|
+
title: 'Components/ButtonGroup',
|
|
8
|
+
component: ButtonGroup,
|
|
9
|
+
argTypes: {
|
|
10
|
+
orientation: {control: 'select', options: ['horizontal', 'vertical']},
|
|
11
|
+
align: {control: 'select', options: ['start', 'end', 'center']},
|
|
12
|
+
gap: {control: 'select', options: ['xs', 'sm', 'md']},
|
|
13
|
+
disabled: {control: 'boolean'},
|
|
14
|
+
},
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default meta
|
|
18
|
+
type Story = StoryObj<typeof ButtonGroup>
|
|
19
|
+
|
|
20
|
+
export const Default: Story = {
|
|
21
|
+
render: () => (
|
|
22
|
+
<ButtonGroup>
|
|
23
|
+
<Button variant="primary">Save</Button>
|
|
24
|
+
<Button variant="secondary">Cancel</Button>
|
|
25
|
+
<Button variant="danger">Delete</Button>
|
|
26
|
+
</ButtonGroup>
|
|
27
|
+
),
|
|
28
|
+
play: async ({canvas}) => {
|
|
29
|
+
const group = canvas.getByRole('group')
|
|
30
|
+
await expect(group).toBeInTheDocument()
|
|
31
|
+
const buttons = canvas.getAllByRole('button')
|
|
32
|
+
await expect(buttons.length).toBe(3)
|
|
33
|
+
},
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const Vertical: Story = {
|
|
37
|
+
render: () => (
|
|
38
|
+
<ButtonGroup orientation="vertical">
|
|
39
|
+
<Button variant="primary">Save</Button>
|
|
40
|
+
<Button variant="secondary">Cancel</Button>
|
|
41
|
+
<Button variant="danger">Delete</Button>
|
|
42
|
+
</ButtonGroup>
|
|
43
|
+
),
|
|
44
|
+
play: async ({canvas}) => {
|
|
45
|
+
const group = canvas.getByRole('group')
|
|
46
|
+
await expect(group).toBeInTheDocument()
|
|
47
|
+
const buttons = canvas.getAllByRole('button')
|
|
48
|
+
await expect(buttons.length).toBe(3)
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const EndAligned: Story = {
|
|
53
|
+
render: () => (
|
|
54
|
+
<ButtonGroup align="end">
|
|
55
|
+
<Button variant="secondary">Cancel</Button>
|
|
56
|
+
<Button variant="primary">Save</Button>
|
|
57
|
+
</ButtonGroup>
|
|
58
|
+
),
|
|
59
|
+
play: async ({canvas}) => {
|
|
60
|
+
const group = canvas.getByRole('group')
|
|
61
|
+
await expect(group).toBeInTheDocument()
|
|
62
|
+
const buttons = canvas.getAllByRole('button')
|
|
63
|
+
await expect(buttons.length).toBe(2)
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Disabled: Story = {
|
|
68
|
+
render: () => (
|
|
69
|
+
<ButtonGroup disabled>
|
|
70
|
+
<Button variant="primary">Save</Button>
|
|
71
|
+
<Button variant="secondary">Cancel</Button>
|
|
72
|
+
<Button variant="danger">Delete</Button>
|
|
73
|
+
</ButtonGroup>
|
|
74
|
+
),
|
|
75
|
+
play: async ({canvas}) => {
|
|
76
|
+
const group = canvas.getByRole('group')
|
|
77
|
+
await expect(group).toBeInTheDocument()
|
|
78
|
+
await expect(group).toHaveStyle({opacity: '0.5', pointerEvents: 'none'})
|
|
79
|
+
},
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const WithGaps: Story = {
|
|
83
|
+
render: () => (
|
|
84
|
+
<div style={{display: 'flex', flexDirection: 'column', gap: 24}}>
|
|
85
|
+
<ButtonGroup gap="xs">
|
|
86
|
+
<Button variant="primary">XS Gap</Button>
|
|
87
|
+
<Button variant="secondary">XS Gap</Button>
|
|
88
|
+
</ButtonGroup>
|
|
89
|
+
<ButtonGroup gap="sm">
|
|
90
|
+
<Button variant="primary">SM Gap</Button>
|
|
91
|
+
<Button variant="secondary">SM Gap</Button>
|
|
92
|
+
</ButtonGroup>
|
|
93
|
+
<ButtonGroup gap="md">
|
|
94
|
+
<Button variant="primary">MD Gap</Button>
|
|
95
|
+
<Button variant="secondary">MD Gap</Button>
|
|
96
|
+
</ButtonGroup>
|
|
97
|
+
</div>
|
|
98
|
+
),
|
|
99
|
+
play: async ({canvas}) => {
|
|
100
|
+
const groups = canvas.getAllByRole('group')
|
|
101
|
+
await expect(groups.length).toBe(3)
|
|
102
|
+
},
|
|
103
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type {ReactNode} from 'react'
|
|
2
|
+
import {html} from 'react-strict-dom'
|
|
3
|
+
import {styles} from './styles.css'
|
|
4
|
+
|
|
5
|
+
export interface ButtonGroupProps {
|
|
6
|
+
children: ReactNode
|
|
7
|
+
/** Layout direction. Default: 'horizontal' */
|
|
8
|
+
orientation?: 'horizontal' | 'vertical'
|
|
9
|
+
/** Alignment within container. Default: 'start' */
|
|
10
|
+
align?: 'start' | 'end' | 'center'
|
|
11
|
+
/** Disable all buttons in group */
|
|
12
|
+
disabled?: boolean
|
|
13
|
+
/** Gap between buttons. Default: 'sm' */
|
|
14
|
+
gap?: 'xs' | 'sm' | 'md'
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const gapMap = {
|
|
18
|
+
xs: styles.gapXs,
|
|
19
|
+
sm: styles.gapSm,
|
|
20
|
+
md: styles.gapMd,
|
|
21
|
+
} as const
|
|
22
|
+
|
|
23
|
+
const horizontalAlignMap = {
|
|
24
|
+
start: styles.alignStart,
|
|
25
|
+
center: styles.alignCenter,
|
|
26
|
+
end: styles.alignEnd,
|
|
27
|
+
} as const
|
|
28
|
+
|
|
29
|
+
const verticalAlignMap = {
|
|
30
|
+
start: styles.verticalAlignStart,
|
|
31
|
+
center: styles.verticalAlignCenter,
|
|
32
|
+
end: styles.verticalAlignEnd,
|
|
33
|
+
} as const
|
|
34
|
+
|
|
35
|
+
export function ButtonGroup({
|
|
36
|
+
children,
|
|
37
|
+
orientation = 'horizontal',
|
|
38
|
+
align = 'start',
|
|
39
|
+
disabled = false,
|
|
40
|
+
gap = 'sm',
|
|
41
|
+
}: ButtonGroupProps) {
|
|
42
|
+
const isVertical = orientation === 'vertical'
|
|
43
|
+
const alignStyle = isVertical ? verticalAlignMap[align] : horizontalAlignMap[align]
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<html.div
|
|
47
|
+
role="group"
|
|
48
|
+
style={[
|
|
49
|
+
styles.base,
|
|
50
|
+
isVertical ? styles.vertical : styles.horizontal,
|
|
51
|
+
gapMap[gap],
|
|
52
|
+
alignStyle,
|
|
53
|
+
disabled && styles.disabled,
|
|
54
|
+
]}
|
|
55
|
+
>
|
|
56
|
+
{children}
|
|
57
|
+
</html.div>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {css} from 'react-strict-dom'
|
|
2
|
+
import {spacing} from '@duro-app/tokens/tokens/spacing.css'
|
|
3
|
+
|
|
4
|
+
export const styles = css.create({
|
|
5
|
+
base: {
|
|
6
|
+
display: 'flex',
|
|
7
|
+
},
|
|
8
|
+
horizontal: {
|
|
9
|
+
flexDirection: 'row',
|
|
10
|
+
flexWrap: 'wrap',
|
|
11
|
+
},
|
|
12
|
+
vertical: {
|
|
13
|
+
flexDirection: 'column',
|
|
14
|
+
},
|
|
15
|
+
alignStart: {justifyContent: 'flex-start'},
|
|
16
|
+
alignCenter: {justifyContent: 'center'},
|
|
17
|
+
alignEnd: {justifyContent: 'flex-end'},
|
|
18
|
+
verticalAlignStart: {alignItems: 'flex-start'},
|
|
19
|
+
verticalAlignCenter: {alignItems: 'center'},
|
|
20
|
+
verticalAlignEnd: {alignItems: 'flex-end'},
|
|
21
|
+
gapXs: {gap: spacing.xs},
|
|
22
|
+
gapSm: {gap: spacing.sm},
|
|
23
|
+
gapMd: {gap: spacing.md},
|
|
24
|
+
disabled: {
|
|
25
|
+
opacity: 0.5,
|
|
26
|
+
pointerEvents: 'none',
|
|
27
|
+
},
|
|
28
|
+
})
|
|
@@ -18,9 +18,15 @@ export const meta: ComponentMeta = {
|
|
|
18
18
|
optional: ['Description', 'Actions', 'Empty'],
|
|
19
19
|
},
|
|
20
20
|
relatedTo: [
|
|
21
|
-
{
|
|
21
|
+
{
|
|
22
|
+
component: 'Table',
|
|
23
|
+
relationship: 'Table for wide multi-column data; List for narrow compact items',
|
|
24
|
+
},
|
|
22
25
|
{component: 'DetailPanel', relationship: 'List fits naturally inside DetailPanel.Body'},
|
|
23
|
-
{
|
|
26
|
+
{
|
|
27
|
+
component: 'Menu',
|
|
28
|
+
relationship: 'Menu for action menus; List for data display with selection',
|
|
29
|
+
},
|
|
24
30
|
],
|
|
25
31
|
example: `<List.Root selectionMode="multiple" aria-label="Certificates">
|
|
26
32
|
<List.Item selected onClick={toggle}>
|
|
@@ -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,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
|
+
}
|