@maestro-js/components 1.0.0-alpha.2 → 1.0.0-alpha.20
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/commands/add.ts +16 -1
- package/dist/components.json +1 -1
- package/dist/index.js +14 -1
- package/package.json +5 -5
- package/registry.json +610 -802
- package/scripts/build.ts +11 -4
- package/src/components/alert-dialog.tsx +3 -3
- package/src/components/avatar.tsx +1 -1
- package/src/components/button-link.tsx +3 -0
- package/src/components/button.tsx +4 -0
- package/src/components/checkbox.tsx +7 -7
- package/src/components/chip.tsx +7 -7
- package/src/components/container.tsx +3 -3
- package/src/components/date-range.tsx +138 -0
- package/src/components/dialog.tsx +21 -7
- package/src/components/disclosure.tsx +1 -1
- package/src/components/drawer.tsx +19 -5
- package/src/components/form.tsx +27 -5
- package/src/components/helpers/form-field.tsx +7 -8
- package/src/components/helpers/get-button-classes.ts +23 -23
- package/src/components/helpers/headless-button.tsx +1 -1
- package/src/components/inline-alert.tsx +3 -3
- package/src/components/labeled-value.tsx +2 -2
- package/src/components/menu.tsx +60 -26
- package/src/components/month-day-input.tsx +2 -1
- package/src/components/multiselect.tsx +74 -70
- package/src/components/optional-link.tsx +15 -0
- package/src/components/radio.tsx +8 -8
- package/src/components/select.tsx +9 -8
- package/src/components/stepper.tsx +6 -6
- package/src/components/switch.tsx +7 -7
- package/src/components/table/headless-templated-row-table.ts +550 -0
- package/src/components/table/index.tsx +41 -0
- package/src/components/table/server-table.ts +143 -0
- package/src/components/table/table-actions.tsx +76 -0
- package/src/components/table/table-container.tsx +173 -0
- package/src/components/table/table-container.type-test.ts +155 -0
- package/src/components/table/table-filters/date-range-filter.tsx +227 -0
- package/src/components/table/table-filters/select-filter.tsx +211 -0
- package/src/components/table/table-filters/sort.tsx +85 -0
- package/src/components/table/table-primitives.tsx +226 -0
- package/src/components/table/table-toolbar.tsx +300 -0
- package/src/components/table/table-types.ts +61 -0
- package/src/components/table/use-client-table.ts +157 -0
- package/src/components/table/use-client-table.type-test.ts +217 -0
- package/src/components/table/use-server-table.ts +192 -0
- package/src/components/table/use-server-table.type-test.ts +174 -0
- package/src/components/tabs.tsx +1 -1
- package/src/components/tag-field.tsx +3 -3
- package/src/components/text-area.tsx +1 -1
- package/src/components/text-field.tsx +4 -1
- package/src/components/toast.tsx +6 -6
- package/src/components/toggle-button-group.tsx +2 -2
- package/src/utils/icons.d.ts +2 -1
- package/src/utils/use-query-state.ts +626 -0
- package/src/utils/use-tab-indicator.ts +9 -9
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import { useServerTable } from './use-server-table'
|
|
2
|
+
import type { ServerTable } from './server-table'
|
|
3
|
+
import type { TableLocalization } from './table-types'
|
|
4
|
+
import type { Iso } from 'iso-fns'
|
|
5
|
+
|
|
6
|
+
// =============================================================================
|
|
7
|
+
// Helpers
|
|
8
|
+
// =============================================================================
|
|
9
|
+
|
|
10
|
+
type Expect<T extends true> = T
|
|
11
|
+
type Equal<A, B> = (<T>() => T extends A ? 1 : 2) extends <T>() => T extends B ? 1 : 2 ? true : false
|
|
12
|
+
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// Setup
|
|
15
|
+
// =============================================================================
|
|
16
|
+
|
|
17
|
+
type Row = {
|
|
18
|
+
id: string
|
|
19
|
+
status: 'active' | 'inactive'
|
|
20
|
+
name: string
|
|
21
|
+
createdAt: string
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// =============================================================================
|
|
25
|
+
// Filter and sort config inference
|
|
26
|
+
// =============================================================================
|
|
27
|
+
|
|
28
|
+
function testFilterAndSortInference() {
|
|
29
|
+
const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true; oldest: true }>
|
|
30
|
+
|
|
31
|
+
const table = useServerTable(serverTable, { getRowId: (row) => row.id })
|
|
32
|
+
|
|
33
|
+
// getFilterConfig() keys match the provided filter names
|
|
34
|
+
type _FilterKeys = Expect<Equal<keyof ReturnType<typeof table.getFilterConfig>, 'status'>>
|
|
35
|
+
|
|
36
|
+
// getSortConfig() keys match the provided sort names
|
|
37
|
+
type _SortKeys = Expect<Equal<keyof ReturnType<typeof table.getSortConfig>, 'newest' | 'oldest'>>
|
|
38
|
+
|
|
39
|
+
// state.sort is the union of sort keys | null
|
|
40
|
+
type _SortState = Expect<Equal<typeof table.state.sort, 'newest' | 'oldest' | null>>
|
|
41
|
+
|
|
42
|
+
// state.filters keys match filter names
|
|
43
|
+
type _FilterStateKeys = Expect<Equal<keyof typeof table.state.filters, 'status'>>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// =============================================================================
|
|
47
|
+
// Localization compatibility — select filter
|
|
48
|
+
// =============================================================================
|
|
49
|
+
|
|
50
|
+
function testLocalizationWithSelectFilter() {
|
|
51
|
+
const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true; oldest: true }>
|
|
52
|
+
|
|
53
|
+
const table = useServerTable(serverTable, { getRowId: (row) => row.id })
|
|
54
|
+
|
|
55
|
+
type Loc = TableLocalization<
|
|
56
|
+
ReturnType<typeof table.getFilterConfig>,
|
|
57
|
+
ReturnType<typeof table.getSortConfig>
|
|
58
|
+
>
|
|
59
|
+
|
|
60
|
+
type _LocFilterKeys = Expect<Equal<keyof Loc['filters'], 'status'>>
|
|
61
|
+
type _LocSortKeys = Expect<Equal<keyof Loc['sorts'], 'newest' | 'oldest'>>
|
|
62
|
+
|
|
63
|
+
// Valid localization with typed option values
|
|
64
|
+
const _loc: Loc = {
|
|
65
|
+
filters: {
|
|
66
|
+
status: {
|
|
67
|
+
label: 'Status',
|
|
68
|
+
options: [
|
|
69
|
+
{ label: 'Active', value: 'active' },
|
|
70
|
+
{ label: 'Inactive', value: 'inactive' }
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
sorts: {
|
|
75
|
+
newest: { label: 'Newest' },
|
|
76
|
+
oldest: { label: 'Oldest' }
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// =============================================================================
|
|
82
|
+
// Localization compatibility — date range filter
|
|
83
|
+
// =============================================================================
|
|
84
|
+
|
|
85
|
+
function testLocalizationWithDateRangeFilter() {
|
|
86
|
+
type RowWithDate = {
|
|
87
|
+
id: string
|
|
88
|
+
dateRange: { startDate: Iso.Date; endDate: Iso.Date }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const serverTable = null as unknown as ServerTable.ServerTable<
|
|
92
|
+
RowWithDate,
|
|
93
|
+
{ dateRange: { startDate: Iso.Date; endDate: Iso.Date } },
|
|
94
|
+
{}
|
|
95
|
+
>
|
|
96
|
+
|
|
97
|
+
const table = useServerTable(serverTable, { getRowId: (row) => row.id })
|
|
98
|
+
|
|
99
|
+
type Loc = TableLocalization<
|
|
100
|
+
ReturnType<typeof table.getFilterConfig>,
|
|
101
|
+
ReturnType<typeof table.getSortConfig>
|
|
102
|
+
>
|
|
103
|
+
|
|
104
|
+
// Valid: date-range filter accepts DateRangeTableFilter form
|
|
105
|
+
const _loc: Loc = {
|
|
106
|
+
filters: {
|
|
107
|
+
dateRange: { label: 'Date Range', type: 'date-range' }
|
|
108
|
+
},
|
|
109
|
+
sorts: {}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Invalid: date-range filter rejects select-style options
|
|
113
|
+
const _badLoc: Loc = {
|
|
114
|
+
filters: {
|
|
115
|
+
dateRange: {
|
|
116
|
+
label: 'Date Range',
|
|
117
|
+
type: 'date-range',
|
|
118
|
+
// @ts-expect-error — select-style options array is not valid for date-range
|
|
119
|
+
options: [{ label: 'X', value: 'x' }]
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
sorts: {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// =============================================================================
|
|
127
|
+
// Negative: wrong localization option value
|
|
128
|
+
// =============================================================================
|
|
129
|
+
|
|
130
|
+
declare function checkLocalization(l: TableLocalization<{ status: 'active' | 'inactive' }, {}>): void
|
|
131
|
+
|
|
132
|
+
function testWrongLocalizationOptionValue() {
|
|
133
|
+
checkLocalization({
|
|
134
|
+
filters: {
|
|
135
|
+
status: {
|
|
136
|
+
label: 'Status',
|
|
137
|
+
// @ts-expect-error — 'unknown' is not assignable to 'active' | 'inactive'
|
|
138
|
+
options: [{ label: 'Unknown', value: 'unknown' }]
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
sorts: {}
|
|
142
|
+
})
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// =============================================================================
|
|
146
|
+
// No filters/sorts → empty localization
|
|
147
|
+
// =============================================================================
|
|
148
|
+
|
|
149
|
+
function testNoFiltersOrSorts() {
|
|
150
|
+
const serverTable = null as unknown as ServerTable.ServerTable<Row, {}, {}>
|
|
151
|
+
|
|
152
|
+
const table = useServerTable(serverTable, { getRowId: (row) => row.id })
|
|
153
|
+
|
|
154
|
+
type Loc = TableLocalization<
|
|
155
|
+
ReturnType<typeof table.getFilterConfig>,
|
|
156
|
+
ReturnType<typeof table.getSortConfig>
|
|
157
|
+
>
|
|
158
|
+
|
|
159
|
+
type _EmptyFilters = Expect<Equal<Loc['filters'], {}>>
|
|
160
|
+
type _EmptySorts = Expect<Equal<Loc['sorts'], {}>>
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// =============================================================================
|
|
164
|
+
// State shape — search and pagination
|
|
165
|
+
// =============================================================================
|
|
166
|
+
|
|
167
|
+
function testStateShape() {
|
|
168
|
+
const serverTable = null as unknown as ServerTable.ServerTable<Row, { status: 'active' | 'inactive' }, { newest: true }>
|
|
169
|
+
|
|
170
|
+
const table = useServerTable(serverTable, { getRowId: (row) => row.id })
|
|
171
|
+
|
|
172
|
+
type _Search = Expect<Equal<typeof table.state.search, string>>
|
|
173
|
+
type _Pagination = Expect<Equal<typeof table.state.pagination, { limit: number; skip: number }>>
|
|
174
|
+
}
|
package/src/components/tabs.tsx
CHANGED
|
@@ -96,7 +96,7 @@ TabList.displayName = 'TabList'
|
|
|
96
96
|
function TabItem({ children, className, ...props }: TabProps) {
|
|
97
97
|
const { color, variant } = React.useContext(TabListContext)
|
|
98
98
|
const colors = tabColorMap[color]
|
|
99
|
-
const focusClasses = `outline-none
|
|
99
|
+
const focusClasses = `outline-none focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:${colors.outline}`
|
|
100
100
|
|
|
101
101
|
return (
|
|
102
102
|
<AriaTab
|
|
@@ -226,7 +226,7 @@ function Tag({
|
|
|
226
226
|
return (
|
|
227
227
|
<span
|
|
228
228
|
className={tw(
|
|
229
|
-
'inline-flex items-center rounded-full
|
|
229
|
+
'inline-flex items-center rounded-full pl-2 pr-1.5 py-0.5 text-xs font-normal bg-primary-500/10 text-primary-500',
|
|
230
230
|
isDisabled && 'opacity-50'
|
|
231
231
|
)}
|
|
232
232
|
>
|
|
@@ -238,11 +238,11 @@ function Tag({
|
|
|
238
238
|
e.stopPropagation()
|
|
239
239
|
onRemove()
|
|
240
240
|
}}
|
|
241
|
-
className="inline-flex items-center justify-center
|
|
241
|
+
className="inline-flex items-center justify-center size-4 -m-0.5 p-0.5 rounded-full translate-x-1 hover:bg-neutral-950/10"
|
|
242
242
|
aria-label="Remove"
|
|
243
243
|
tabIndex={-1}
|
|
244
244
|
>
|
|
245
|
-
<Icon name="x-mark" className="
|
|
245
|
+
<Icon name="x-mark" className="size-2" aria-hidden="true" />
|
|
246
246
|
</button>
|
|
247
247
|
)}
|
|
248
248
|
</span>
|
|
@@ -113,7 +113,7 @@ export function TextArea(props: TextAreaProps) {
|
|
|
113
113
|
ref={textAreaRef}
|
|
114
114
|
id={textAreaId}
|
|
115
115
|
className={tw(
|
|
116
|
-
'w-full bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm text-neutral-900 placeholder:text-neutral-400',
|
|
116
|
+
'w-full bg-transparent px-3 py-2 border-0 focus:ring-0 focus:outline-none text-sm max-sm:text-base text-neutral-900 placeholder:text-neutral-400',
|
|
117
117
|
autoResize && 'resize-none overflow-hidden col-span-full row-span-full',
|
|
118
118
|
(props.isDisabled || props.isReadonly) && 'cursor-not-allowed',
|
|
119
119
|
!autoResize && props.resize === 'none' && 'resize-none',
|
|
@@ -47,6 +47,8 @@ export type TextFieldProps = {
|
|
|
47
47
|
|
|
48
48
|
export function TextField(props: TextFieldProps) {
|
|
49
49
|
const inputRef = React.useRef<HTMLInputElement>(null)
|
|
50
|
+
const visibleRef = React.useRef<HTMLInputElement>(null)
|
|
51
|
+
|
|
50
52
|
const helpTextId = React.useId()
|
|
51
53
|
const inputId = React.useId()
|
|
52
54
|
|
|
@@ -66,7 +68,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
66
68
|
isRequired: props.isRequired,
|
|
67
69
|
form: props.form,
|
|
68
70
|
focus() {
|
|
69
|
-
|
|
71
|
+
visibleRef.current?.focus()
|
|
70
72
|
}
|
|
71
73
|
},
|
|
72
74
|
inputRef
|
|
@@ -113,6 +115,7 @@ export function TextField(props: TextFieldProps) {
|
|
|
113
115
|
inputMode={props.inputMode}
|
|
114
116
|
aria-invalid={isInvalid || undefined}
|
|
115
117
|
aria-describedby={helpTextId}
|
|
118
|
+
ref={visibleRef}
|
|
116
119
|
/>
|
|
117
120
|
{props.trailingInlineAddon}
|
|
118
121
|
</FormFieldComponents.FieldGroup>
|
package/src/components/toast.tsx
CHANGED
|
@@ -99,7 +99,7 @@ function ToastItem({ toast }: { toast: { content: Toast; key: string } }) {
|
|
|
99
99
|
style={{ viewTransitionName: toast.key } as React.CSSProperties}
|
|
100
100
|
className={tw(
|
|
101
101
|
'pointer-events-auto',
|
|
102
|
-
'flex gap-3 rounded-
|
|
102
|
+
'flex gap-3 rounded-lg px-4 py-3 shadow-lg',
|
|
103
103
|
'min-w-[300px] max-w-md',
|
|
104
104
|
'[view-transition-class:toast]',
|
|
105
105
|
hasDescription ? 'items-start' : 'items-center',
|
|
@@ -152,11 +152,11 @@ function DeclaredToast({
|
|
|
152
152
|
// --- Variant Maps ---
|
|
153
153
|
|
|
154
154
|
const variantClasses = {
|
|
155
|
-
info: 'bg-info-
|
|
156
|
-
positive: 'bg-positive-50 text-positive-800
|
|
157
|
-
warning: 'bg-notice-50 text-notice-800
|
|
158
|
-
negative: 'bg-negative-50 text-negative-800
|
|
159
|
-
notice: 'bg-neutral-
|
|
155
|
+
info: 'bg-info-50 text-info-800 ring-1 ring-info-200',
|
|
156
|
+
positive: 'bg-positive-50 text-positive-800 ring-1 ring-positive-200',
|
|
157
|
+
warning: 'bg-notice-50 text-notice-800 ring-1 ring-notice-200',
|
|
158
|
+
negative: 'bg-negative-50 text-negative-800 ring-1 ring-negative-200',
|
|
159
|
+
notice: 'bg-neutral-50 text-neutral-700 ring-1 ring-neutral-200'
|
|
160
160
|
}
|
|
161
161
|
|
|
162
162
|
const variantIconNames = {
|
|
@@ -82,12 +82,12 @@ function getToggleButtonClasses({ size = 'md', isSelected }: { size?: ButtonSize
|
|
|
82
82
|
// Unselected
|
|
83
83
|
!isSelected && 'bg-white ring-1 ring-inset ring-neutral-300',
|
|
84
84
|
!isSelected && 'data-[hovered]:bg-neutral-50 data-[hovered]:ring-neutral-400',
|
|
85
|
-
!isSelected && 'text-neutral-700 data-[focus-visible]:outline-
|
|
85
|
+
!isSelected && 'text-neutral-700 data-[focus-visible]:outline-primary-600',
|
|
86
86
|
|
|
87
87
|
// Selected
|
|
88
88
|
isSelected && 'bg-primary-500 text-white ring-1 ring-inset ring-primary-700',
|
|
89
89
|
isSelected && 'data-[hovered]:bg-primary-600 data-[hovered]:ring-primary-700',
|
|
90
|
-
isSelected && 'data-[focus-visible]:outline-primary-
|
|
90
|
+
isSelected && 'data-[focus-visible]:outline-primary-600',
|
|
91
91
|
|
|
92
92
|
'inline-flex justify-center items-center gap-x-2'
|
|
93
93
|
)
|