@rovula/ui 0.1.28 → 0.1.29
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/cjs/bundle.css +501 -67
- package/dist/cjs/bundle.js +589 -589
- package/dist/cjs/bundle.js.map +1 -1
- package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
- package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
- package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
- package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +268 -6
- package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
- package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
- package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
- package/dist/cjs/types/components/Table/Table.d.ts +33 -3
- package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
- package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
- package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
- package/dist/components/DataTable/DataTable.editing.js +385 -0
- package/dist/components/DataTable/DataTable.editing.types.js +1 -0
- package/dist/components/DataTable/DataTable.js +983 -50
- package/dist/components/DataTable/DataTable.stories.js +1077 -25
- package/dist/components/Dropdown/Dropdown.js +8 -6
- package/dist/components/ScrollArea/ScrollArea.js +2 -2
- package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
- package/dist/components/Table/Table.js +103 -13
- package/dist/components/Table/Table.stories.js +226 -9
- package/dist/components/TextInput/TextInput.js +6 -4
- package/dist/components/TextInput/TextInput.stories.js +8 -0
- package/dist/components/TextInput/TextInput.styles.js +7 -1
- package/dist/esm/bundle.css +501 -67
- package/dist/esm/bundle.js +1545 -1545
- package/dist/esm/bundle.js.map +1 -1
- package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
- package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
- package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
- package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +268 -6
- package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
- package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
- package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
- package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
- package/dist/esm/types/components/Table/Table.d.ts +33 -3
- package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
- package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
- package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
- package/dist/index.d.ts +493 -122
- package/dist/src/theme/global.css +747 -96
- package/package.json +14 -2
- package/src/components/DataTable/DataTable.editing.tsx +861 -0
- package/src/components/DataTable/DataTable.editing.types.ts +192 -0
- package/src/components/DataTable/DataTable.stories.tsx +2169 -31
- package/src/components/DataTable/DataTable.test.tsx +696 -0
- package/src/components/DataTable/DataTable.tsx +2260 -94
- package/src/components/Dropdown/Dropdown.tsx +22 -6
- package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
- package/src/components/ScrollArea/ScrollArea.tsx +6 -6
- package/src/components/Table/Table.stories.tsx +789 -44
- package/src/components/Table/Table.tsx +294 -28
- package/src/components/TextInput/TextInput.stories.tsx +80 -0
- package/src/components/TextInput/TextInput.styles.ts +7 -1
- package/src/components/TextInput/TextInput.tsx +21 -14
- package/src/test/setup.ts +50 -0
- package/src/theme/global.css +81 -42
- package/src/theme/presets/colors.js +12 -0
- package/src/theme/themes/variable.css +27 -28
- package/src/theme/tokens/baseline.css +2 -1
- package/src/theme/tokens/components/scrollbar.css +9 -4
- package/src/theme/tokens/components/table.css +63 -0
|
@@ -1,52 +1,151 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
|
|
2
|
+
import {
|
|
3
|
+
ChevronDown,
|
|
4
|
+
ChevronLeft,
|
|
5
|
+
ChevronRight,
|
|
6
|
+
ChevronFirst,
|
|
7
|
+
ChevronLast,
|
|
8
|
+
} from "lucide-react";
|
|
3
9
|
import { cn } from "@/utils/cn";
|
|
10
|
+
import ActionButton from "@/components/ActionButton/ActionButton";
|
|
4
11
|
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Table — root scroll container + <table>
|
|
14
|
+
// Uses border-separate / border-spacing-0 so border-r on cells works cleanly.
|
|
15
|
+
//
|
|
16
|
+
// Colours adapt automatically when an ancestor carries data-surface="panel".
|
|
17
|
+
// See src/theme/tokens/components/table.css for the token definitions.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
5
19
|
const Table = React.forwardRef<
|
|
6
20
|
HTMLTableElement,
|
|
7
21
|
{
|
|
8
22
|
rootClassName?: string;
|
|
9
23
|
rootRef?: React.LegacyRef<HTMLDivElement>;
|
|
24
|
+
/**
|
|
25
|
+
* Wraps the table in a rounded frame (`rounded-md` + `border-table-c-border`).
|
|
26
|
+
* Uses an outer `overflow-hidden` shell and an inner scroll area so corners clip
|
|
27
|
+
* cleanly (same pattern as scrollable bordered tables elsewhere in the system).
|
|
28
|
+
* For pagination in the same frame, wrap `Table` + `TablePagination` in your own
|
|
29
|
+
* bordered div instead of relying on this prop alone.
|
|
30
|
+
*/
|
|
31
|
+
bordered?: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* When false, render only `<table>` (no inner `overflow-auto` wrapper).
|
|
34
|
+
* Use when a parent already provides the scrollport — e.g. `DataTable` — so
|
|
35
|
+
* `sticky` header rows stick to the correct ancestor.
|
|
36
|
+
* Ignored when `bordered` is true (bordered tables always use the inner scroll shell).
|
|
37
|
+
*/
|
|
38
|
+
scrollableWrapper?: boolean;
|
|
10
39
|
} & React.HTMLAttributes<HTMLTableElement>
|
|
11
|
-
>(({ rootClassName, className, rootRef, ...props }, ref) =>
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
40
|
+
>(({ rootClassName, className, rootRef, bordered = false, scrollableWrapper = true, ...props }, ref) => {
|
|
41
|
+
const scrollClassName = cn(
|
|
42
|
+
"relative h-full w-full min-h-0 overflow-auto",
|
|
43
|
+
"ui-scrollbar ui-scrollbar-x-m ui-scrollbar-y-s",
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const tableClassName = cn(
|
|
47
|
+
"min-w-full caption-bottom border-separate border-spacing-0",
|
|
48
|
+
className,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const tableEl = (
|
|
52
|
+
<table ref={ref} className={tableClassName} {...props} />
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
if (!bordered && scrollableWrapper === false) {
|
|
56
|
+
return (
|
|
57
|
+
<table
|
|
58
|
+
ref={ref}
|
|
59
|
+
className={cn(tableClassName, rootClassName)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
23
64
|
|
|
65
|
+
if (bordered) {
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
className={cn(
|
|
69
|
+
"relative flex h-full w-full min-h-0 flex-col overflow-hidden rounded-md border border-table-c-border",
|
|
70
|
+
rootClassName,
|
|
71
|
+
)}
|
|
72
|
+
ref={rootRef}
|
|
73
|
+
>
|
|
74
|
+
<div className={scrollClassName}>{tableEl}</div>
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return (
|
|
80
|
+
<div className={cn(scrollClassName, rootClassName)} ref={rootRef}>
|
|
81
|
+
{tableEl}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
});
|
|
24
85
|
Table.displayName = "Table";
|
|
25
86
|
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// TableHeader
|
|
89
|
+
// The header-to-body separator resolves from --table-c-header-line:
|
|
90
|
+
// default → transparent (bg contrast between table-bg-main and row-bg
|
|
91
|
+
// provides visual separation — no explicit line needed)
|
|
92
|
+
// panel → table-panel-main-line (all bgs are transparent, explicit
|
|
93
|
+
// line required)
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
26
95
|
const TableHeader = React.forwardRef<
|
|
27
96
|
HTMLTableSectionElement,
|
|
28
97
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
29
98
|
>(({ className, ...props }, ref) => (
|
|
30
99
|
<thead
|
|
31
100
|
ref={ref}
|
|
32
|
-
className={cn(
|
|
101
|
+
className={cn(
|
|
102
|
+
"[&_tr>th]:border-b [&_tr>th]:border-b-table-c-header-line",
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
33
105
|
{...props}
|
|
34
106
|
/>
|
|
35
107
|
));
|
|
36
108
|
TableHeader.displayName = "TableHeader";
|
|
37
109
|
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
// TableBody
|
|
112
|
+
// striped=true → alternating bg colours (odd=row-bg, even=row-bg-even),
|
|
113
|
+
// NO horizontal row strokes
|
|
114
|
+
// striped=false → single row-bg colour, row strokes via TableRow divided prop
|
|
115
|
+
//
|
|
116
|
+
// Hover, selected, and bg colours all resolve from table-c-* tokens so they
|
|
117
|
+
// automatically switch when inside a [data-surface="panel"] ancestor.
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
38
119
|
const TableBody = React.forwardRef<
|
|
39
120
|
HTMLTableSectionElement,
|
|
40
|
-
React.HTMLAttributes<HTMLTableSectionElement>
|
|
41
|
-
>(({ className, ...props }, ref) => (
|
|
121
|
+
{ striped?: boolean } & React.HTMLAttributes<HTMLTableSectionElement>
|
|
122
|
+
>(({ className, striped = false, ...props }, ref) => (
|
|
42
123
|
<tbody
|
|
43
124
|
ref={ref}
|
|
44
|
-
className={cn(
|
|
125
|
+
className={cn(
|
|
126
|
+
// Remove the bottom border on the very last row's cells in every mode.
|
|
127
|
+
"[&_tr:last-child>td]:border-b-0 [&_tr:last-child>th]:border-b-0",
|
|
128
|
+
"[&_tr]:bg-table-c-row-bg",
|
|
129
|
+
// !important beats nth-child specificity so hover always wins.
|
|
130
|
+
"[&_tr:hover]:!bg-table-c-hover",
|
|
131
|
+
"[&_tr[data-state=selected]]:!bg-table-c-selected",
|
|
132
|
+
"[&_tr[data-highlighted=true]]:!bg-table-c-selected",
|
|
133
|
+
striped && [
|
|
134
|
+
"[&_tr:nth-child(odd)]:bg-table-c-row-bg",
|
|
135
|
+
"[&_tr:nth-child(even)]:bg-table-c-row-bg-even",
|
|
136
|
+
// Remove row stroke — colour alternation provides separation.
|
|
137
|
+
"[&_tr>td]:border-b-0",
|
|
138
|
+
],
|
|
139
|
+
className,
|
|
140
|
+
)}
|
|
45
141
|
{...props}
|
|
46
142
|
/>
|
|
47
143
|
));
|
|
48
144
|
TableBody.displayName = "TableBody";
|
|
49
145
|
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
// TableFooter
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
50
149
|
const TableFooter = React.forwardRef<
|
|
51
150
|
HTMLTableSectionElement,
|
|
52
151
|
React.HTMLAttributes<HTMLTableSectionElement>
|
|
@@ -54,29 +153,53 @@ const TableFooter = React.forwardRef<
|
|
|
54
153
|
<tfoot
|
|
55
154
|
ref={ref}
|
|
56
155
|
className={cn(
|
|
57
|
-
"border-t
|
|
58
|
-
className
|
|
156
|
+
"bg-table-c-header-bg border-t border-t-table-c-header-line font-medium [&>tr]:last:border-b-0",
|
|
157
|
+
className,
|
|
59
158
|
)}
|
|
60
159
|
{...props}
|
|
61
160
|
/>
|
|
62
161
|
));
|
|
63
162
|
TableFooter.displayName = "TableFooter";
|
|
64
163
|
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// TableRow
|
|
166
|
+
// divided=true → horizontal separator on child td/th cells (border-b)
|
|
167
|
+
// NOTE: with border-separate tables, borders on <tr> are
|
|
168
|
+
// invisible — must target cells directly via CSS selectors.
|
|
169
|
+
// divided=false → no separator (striped rows / empty row)
|
|
170
|
+
// colDivided=true → vertical column dividers on every non-last td / th child
|
|
171
|
+
// ---------------------------------------------------------------------------
|
|
65
172
|
const TableRow = React.forwardRef<
|
|
66
173
|
HTMLTableRowElement,
|
|
67
|
-
|
|
68
|
-
|
|
174
|
+
{
|
|
175
|
+
divided?: boolean;
|
|
176
|
+
colDivided?: boolean;
|
|
177
|
+
} & React.HTMLAttributes<HTMLTableRowElement>
|
|
178
|
+
>(({ className, divided = true, colDivided = false, ...props }, ref) => (
|
|
69
179
|
<tr
|
|
70
180
|
ref={ref}
|
|
71
181
|
className={cn(
|
|
72
|
-
"
|
|
73
|
-
|
|
182
|
+
"transition-colors",
|
|
183
|
+
// Row separator — applied on cells because border-separate ignores tr borders
|
|
184
|
+
divided && [
|
|
185
|
+
"[&>td]:border-b [&>td]:border-b-table-c-row-line",
|
|
186
|
+
"[&>th]:border-b [&>th]:border-b-table-c-row-line",
|
|
187
|
+
],
|
|
188
|
+
// Column dividers on every non-last cell
|
|
189
|
+
colDivided && [
|
|
190
|
+
"[&>td:not(:last-child)]:border-r [&>td:not(:last-child)]:border-r-table-c-col-line",
|
|
191
|
+
"[&>th:not(:last-child)]:border-r [&>th:not(:last-child)]:border-r-table-c-col-line",
|
|
192
|
+
],
|
|
193
|
+
className,
|
|
74
194
|
)}
|
|
75
195
|
{...props}
|
|
76
196
|
/>
|
|
77
197
|
));
|
|
78
198
|
TableRow.displayName = "TableRow";
|
|
79
199
|
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// TableHead (th)
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
80
203
|
const TableHead = React.forwardRef<
|
|
81
204
|
HTMLTableCellElement,
|
|
82
205
|
React.ThHTMLAttributes<HTMLTableCellElement>
|
|
@@ -84,14 +207,22 @@ const TableHead = React.forwardRef<
|
|
|
84
207
|
<th
|
|
85
208
|
ref={ref}
|
|
86
209
|
className={cn(
|
|
87
|
-
"
|
|
88
|
-
|
|
210
|
+
"h-[44px] box-border py-3 px-3 text-left align-middle",
|
|
211
|
+
"typography-body2 text-text-contrast-max",
|
|
212
|
+
"bg-table-c-header-bg",
|
|
213
|
+
// Prefer min-width only so callers (e.g. DataTable exactWidth) can set
|
|
214
|
+
// a different width via inline style without fighting a fixed `w-10`.
|
|
215
|
+
"[&:has([role=checkbox])]:px-3 [&:has([role=checkbox])]:min-w-10",
|
|
216
|
+
className,
|
|
89
217
|
)}
|
|
90
218
|
{...props}
|
|
91
219
|
/>
|
|
92
220
|
));
|
|
93
221
|
TableHead.displayName = "TableHead";
|
|
94
222
|
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// TableCell (td)
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
95
226
|
const TableCell = React.forwardRef<
|
|
96
227
|
HTMLTableCellElement,
|
|
97
228
|
React.TdHTMLAttributes<HTMLTableCellElement>
|
|
@@ -99,26 +230,160 @@ const TableCell = React.forwardRef<
|
|
|
99
230
|
<td
|
|
100
231
|
ref={ref}
|
|
101
232
|
className={cn(
|
|
102
|
-
" py-3 px-
|
|
103
|
-
|
|
233
|
+
"h-[42px] box-border py-3 px-3 text-left align-middle",
|
|
234
|
+
"typography-body3 text-text-contrast-max",
|
|
235
|
+
// Inherit row background from <tr> so every cell paints the same fill
|
|
236
|
+
// (some table layouts / browsers leave the last column visually “empty”).
|
|
237
|
+
"bg-inherit",
|
|
238
|
+
"[&:has([role=checkbox])]:px-3 [&:has([role=checkbox])]:min-w-10",
|
|
239
|
+
className,
|
|
104
240
|
)}
|
|
105
241
|
{...props}
|
|
106
242
|
/>
|
|
107
243
|
));
|
|
108
244
|
TableCell.displayName = "TableCell";
|
|
109
245
|
|
|
246
|
+
// ---------------------------------------------------------------------------
|
|
247
|
+
// TableCaption
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
110
249
|
const TableCaption = React.forwardRef<
|
|
111
250
|
HTMLTableCaptionElement,
|
|
112
251
|
React.HTMLAttributes<HTMLTableCaptionElement>
|
|
113
252
|
>(({ className, ...props }, ref) => (
|
|
114
253
|
<caption
|
|
115
254
|
ref={ref}
|
|
116
|
-
className={cn("mt-4 text-sm text-text-g-contrast-medium", className)}
|
|
255
|
+
className={cn("mt-4 pb-3 text-sm text-text-g-contrast-medium", className)}
|
|
117
256
|
{...props}
|
|
118
257
|
/>
|
|
119
258
|
));
|
|
120
259
|
TableCaption.displayName = "TableCaption";
|
|
121
260
|
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
// TablePagination
|
|
263
|
+
// Resolves bg and top-border from the same table-c-* tokens so it adapts
|
|
264
|
+
// automatically — no variant prop needed.
|
|
265
|
+
// default → bg-table-c-header-bg (= table-bg-main), border transparent
|
|
266
|
+
// panel → bg transparent, border table-panel-main-line
|
|
267
|
+
// ---------------------------------------------------------------------------
|
|
268
|
+
export interface TablePaginationProps {
|
|
269
|
+
pageIndex: number;
|
|
270
|
+
pageSize: number;
|
|
271
|
+
totalCount: number;
|
|
272
|
+
pageSizeOptions?: number[];
|
|
273
|
+
onPageChange: (pageIndex: number) => void;
|
|
274
|
+
onPageSizeChange: (pageSize: number) => void;
|
|
275
|
+
className?: string;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const TablePagination = React.forwardRef<HTMLDivElement, TablePaginationProps>(
|
|
279
|
+
(
|
|
280
|
+
{
|
|
281
|
+
pageIndex,
|
|
282
|
+
pageSize,
|
|
283
|
+
totalCount,
|
|
284
|
+
pageSizeOptions = [10, 20, 50, 100],
|
|
285
|
+
onPageChange,
|
|
286
|
+
onPageSizeChange,
|
|
287
|
+
className,
|
|
288
|
+
},
|
|
289
|
+
ref,
|
|
290
|
+
) => {
|
|
291
|
+
const totalPages = Math.max(1, Math.ceil(totalCount / pageSize));
|
|
292
|
+
const from = totalCount === 0 ? 0 : pageIndex * pageSize + 1;
|
|
293
|
+
const to = Math.min((pageIndex + 1) * pageSize, totalCount);
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<div
|
|
297
|
+
ref={ref}
|
|
298
|
+
className={cn(
|
|
299
|
+
"flex items-center justify-end gap-8 px-6 py-2",
|
|
300
|
+
"bg-table-c-header-bg",
|
|
301
|
+
"border-t border-t-table-c-header-line",
|
|
302
|
+
className,
|
|
303
|
+
)}
|
|
304
|
+
>
|
|
305
|
+
{/* Items per page */}
|
|
306
|
+
<div className="flex items-center gap-3">
|
|
307
|
+
<span className="typography-subtitle4 text-text-g-contrast-high whitespace-nowrap">
|
|
308
|
+
items per page
|
|
309
|
+
</span>
|
|
310
|
+
<div className="relative">
|
|
311
|
+
<select
|
|
312
|
+
value={pageSize}
|
|
313
|
+
onChange={(e) => {
|
|
314
|
+
onPageSizeChange(Number(e.target.value));
|
|
315
|
+
onPageChange(0);
|
|
316
|
+
}}
|
|
317
|
+
className={cn(
|
|
318
|
+
"appearance-none w-[72px] rounded-md border border-input-default-stroke",
|
|
319
|
+
"bg-table-c-header-bg p-2 pr-7",
|
|
320
|
+
"typography-small2 text-text-g-contrast-high",
|
|
321
|
+
"cursor-pointer focus:outline-none focus:ring-1 focus:ring-input-active-stroke",
|
|
322
|
+
)}
|
|
323
|
+
>
|
|
324
|
+
{pageSizeOptions.map((size) => (
|
|
325
|
+
<option key={size} value={size}>
|
|
326
|
+
{size}
|
|
327
|
+
</option>
|
|
328
|
+
))}
|
|
329
|
+
</select>
|
|
330
|
+
<ChevronDown
|
|
331
|
+
className="pointer-events-none absolute right-2 top-1/2 -translate-y-1/2 size-[14px] text-text-g-contrast-high"
|
|
332
|
+
aria-hidden
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
</div>
|
|
336
|
+
|
|
337
|
+
{/* Range label */}
|
|
338
|
+
<span className="typography-subtitle4 text-text-g-contrast-high whitespace-nowrap tabular-nums">
|
|
339
|
+
{from}–{to} of {totalCount} items
|
|
340
|
+
</span>
|
|
341
|
+
|
|
342
|
+
{/* Page navigation */}
|
|
343
|
+
<div className="flex items-center gap-2">
|
|
344
|
+
<ActionButton
|
|
345
|
+
variant="icon"
|
|
346
|
+
size="sm"
|
|
347
|
+
onClick={() => onPageChange(0)}
|
|
348
|
+
disabled={pageIndex === 0}
|
|
349
|
+
aria-label="First page"
|
|
350
|
+
>
|
|
351
|
+
<ChevronFirst />
|
|
352
|
+
</ActionButton>
|
|
353
|
+
<ActionButton
|
|
354
|
+
variant="icon"
|
|
355
|
+
size="sm"
|
|
356
|
+
onClick={() => onPageChange(pageIndex - 1)}
|
|
357
|
+
disabled={pageIndex === 0}
|
|
358
|
+
aria-label="Previous page"
|
|
359
|
+
>
|
|
360
|
+
<ChevronLeft />
|
|
361
|
+
</ActionButton>
|
|
362
|
+
<ActionButton
|
|
363
|
+
variant="icon"
|
|
364
|
+
size="sm"
|
|
365
|
+
onClick={() => onPageChange(pageIndex + 1)}
|
|
366
|
+
disabled={pageIndex >= totalPages - 1}
|
|
367
|
+
aria-label="Next page"
|
|
368
|
+
>
|
|
369
|
+
<ChevronRight />
|
|
370
|
+
</ActionButton>
|
|
371
|
+
<ActionButton
|
|
372
|
+
variant="icon"
|
|
373
|
+
size="sm"
|
|
374
|
+
onClick={() => onPageChange(totalPages - 1)}
|
|
375
|
+
disabled={pageIndex >= totalPages - 1}
|
|
376
|
+
aria-label="Last page"
|
|
377
|
+
>
|
|
378
|
+
<ChevronLast />
|
|
379
|
+
</ActionButton>
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
},
|
|
384
|
+
);
|
|
385
|
+
TablePagination.displayName = "TablePagination";
|
|
386
|
+
|
|
122
387
|
export {
|
|
123
388
|
Table,
|
|
124
389
|
TableHeader,
|
|
@@ -128,4 +393,5 @@ export {
|
|
|
128
393
|
TableRow,
|
|
129
394
|
TableCell,
|
|
130
395
|
TableCaption,
|
|
396
|
+
TablePagination,
|
|
131
397
|
};
|
|
@@ -45,6 +45,86 @@ export const Default = {
|
|
|
45
45
|
},
|
|
46
46
|
} satisfies StoryObj;
|
|
47
47
|
|
|
48
|
+
/**
|
|
49
|
+
* `isFloatingLabel={false}` — visible **`placeholder`**, no floating animation,
|
|
50
|
+
* and no inner `<label>` when `label` is empty (e.g. table cells, compact filters).
|
|
51
|
+
* Pair with `required={false}` + `hasClearIcon={false}` when mimicking inline fields.
|
|
52
|
+
*/
|
|
53
|
+
export const NonFloatingLabel = {
|
|
54
|
+
render: () => (
|
|
55
|
+
<div className="flex w-full max-w-4xl flex-col gap-8">
|
|
56
|
+
<div className="flex flex-col gap-3">
|
|
57
|
+
<h4 className="typography-subtitle4 text-text-g-contrast-medium">
|
|
58
|
+
Placeholder only — empty <code className="typography-small2">label</code>{" "}
|
|
59
|
+
(no floating label, no label node)
|
|
60
|
+
</h4>
|
|
61
|
+
<p className="typography-small2 text-text-g-contrast-low max-w-2xl">
|
|
62
|
+
Tab order follows DOM, like native inputs in a row.
|
|
63
|
+
</p>
|
|
64
|
+
<div className="flex flex-row flex-wrap items-end gap-4">
|
|
65
|
+
<TextInput
|
|
66
|
+
id="nf-cell-lg"
|
|
67
|
+
size="lg"
|
|
68
|
+
isFloatingLabel={false}
|
|
69
|
+
label=""
|
|
70
|
+
required={false}
|
|
71
|
+
hasClearIcon={false}
|
|
72
|
+
keepFooterSpace={false}
|
|
73
|
+
placeholder="Column name"
|
|
74
|
+
fullwidth={false}
|
|
75
|
+
aria-label="Column name"
|
|
76
|
+
/>
|
|
77
|
+
<TextInput
|
|
78
|
+
id="nf-cell-md"
|
|
79
|
+
size="md"
|
|
80
|
+
isFloatingLabel={false}
|
|
81
|
+
label=""
|
|
82
|
+
required={false}
|
|
83
|
+
hasClearIcon={false}
|
|
84
|
+
keepFooterSpace={false}
|
|
85
|
+
placeholder="Column name"
|
|
86
|
+
fullwidth={false}
|
|
87
|
+
aria-label="Column name"
|
|
88
|
+
/>
|
|
89
|
+
<TextInput
|
|
90
|
+
id="nf-cell-sm"
|
|
91
|
+
size="sm"
|
|
92
|
+
isFloatingLabel={false}
|
|
93
|
+
label=""
|
|
94
|
+
required={false}
|
|
95
|
+
hasClearIcon={false}
|
|
96
|
+
keepFooterSpace={false}
|
|
97
|
+
placeholder="Column name"
|
|
98
|
+
fullwidth={false}
|
|
99
|
+
aria-label="Column name"
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
<div className="flex flex-col gap-3">
|
|
105
|
+
<h4 className="typography-subtitle4 text-text-g-contrast-medium">
|
|
106
|
+
Static label inside field (non-floating layout)
|
|
107
|
+
</h4>
|
|
108
|
+
<div className="flex flex-row flex-wrap gap-4">
|
|
109
|
+
<TextInput
|
|
110
|
+
id="nf-static-md"
|
|
111
|
+
size="md"
|
|
112
|
+
isFloatingLabel={false}
|
|
113
|
+
label="Department"
|
|
114
|
+
required={false}
|
|
115
|
+
hasClearIcon
|
|
116
|
+
keepFooterSpace
|
|
117
|
+
placeholder="e.g. Engineering"
|
|
118
|
+
helperText="Shown as fixed label + real placeholder."
|
|
119
|
+
fullwidth={false}
|
|
120
|
+
className="min-w-[240px]"
|
|
121
|
+
/>
|
|
122
|
+
</div>
|
|
123
|
+
</div>
|
|
124
|
+
</div>
|
|
125
|
+
),
|
|
126
|
+
} satisfies StoryObj;
|
|
127
|
+
|
|
48
128
|
const InputWithRef = (props: any) => {
|
|
49
129
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
50
130
|
|
|
@@ -5,10 +5,15 @@ export const inputVariant = cva(
|
|
|
5
5
|
"truncate",
|
|
6
6
|
"border-0 outline-none",
|
|
7
7
|
"flex w-auto box-border",
|
|
8
|
-
"peer text-input-filled-text
|
|
8
|
+
"peer text-input-filled-text bg-transparent caret-primary",
|
|
9
9
|
],
|
|
10
10
|
{
|
|
11
11
|
variants: {
|
|
12
|
+
/** When `true`, placeholder is transparent — floating label uses a space placeholder. */
|
|
13
|
+
floatingLabelPlaceholder: {
|
|
14
|
+
true: "placeholder:text-transparent",
|
|
15
|
+
false: "placeholder:text-input-default-text",
|
|
16
|
+
},
|
|
12
17
|
size: {
|
|
13
18
|
sm: "p-2 px-3 typography-small1",
|
|
14
19
|
md: "py-2 px-3 typography-subtitle4",
|
|
@@ -144,6 +149,7 @@ export const inputVariant = cva(
|
|
|
144
149
|
hasSearchIcon: false,
|
|
145
150
|
leftSectionIcon: false, // TODO function style
|
|
146
151
|
rightSectionIcon: false,
|
|
152
|
+
floatingLabelPlaceholder: true,
|
|
147
153
|
},
|
|
148
154
|
},
|
|
149
155
|
);
|
|
@@ -74,7 +74,7 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
74
74
|
rounded = "normal",
|
|
75
75
|
variant = "outline",
|
|
76
76
|
type = "text",
|
|
77
|
-
iconMode = "
|
|
77
|
+
iconMode = "flat",
|
|
78
78
|
helperText,
|
|
79
79
|
errorMessage,
|
|
80
80
|
warningMessage,
|
|
@@ -101,6 +101,7 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
101
101
|
format,
|
|
102
102
|
trimOnCommit,
|
|
103
103
|
normalizeOnCommit,
|
|
104
|
+
placeholder,
|
|
104
105
|
...props
|
|
105
106
|
},
|
|
106
107
|
ref,
|
|
@@ -143,6 +144,7 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
143
144
|
(iconMode === "flat" && hasRightSectionIcon) || hasClearIcon,
|
|
144
145
|
leftSectionIcon: iconMode === "solid" ? hasLeftSectionIcon : false,
|
|
145
146
|
rightSectionIcon: iconMode === "solid" ? hasRightSectionIcon : false,
|
|
147
|
+
floatingLabelPlaceholder: isFloatingLabel,
|
|
146
148
|
});
|
|
147
149
|
const labelClassname = labelVariant({
|
|
148
150
|
size,
|
|
@@ -377,12 +379,15 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
377
379
|
handleOnClickRightSectionIcon,
|
|
378
380
|
]);
|
|
379
381
|
|
|
382
|
+
const showLabel =
|
|
383
|
+
isFloatingLabel || (label != null && String(label) !== "");
|
|
384
|
+
|
|
380
385
|
return (
|
|
381
386
|
<div className={`inline-flex flex-col ${fullwidth ? "w-full" : ""}`}>
|
|
382
387
|
<div className="relative">
|
|
383
388
|
<input
|
|
384
389
|
{...props}
|
|
385
|
-
placeholder=" "
|
|
390
|
+
placeholder={isFloatingLabel ? " " : placeholder ?? ""}
|
|
386
391
|
ref={stableRef}
|
|
387
392
|
type={type}
|
|
388
393
|
id={_id}
|
|
@@ -435,18 +440,20 @@ export const TextInput = forwardRef<HTMLInputElement, InputProps>(
|
|
|
435
440
|
)}
|
|
436
441
|
{endIconElement}
|
|
437
442
|
|
|
438
|
-
|
|
439
|
-
{
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
"text-input-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
443
|
+
{showLabel && (
|
|
444
|
+
<label htmlFor={_id} className={cn(labelClassname, labelClassName)}>
|
|
445
|
+
{label}{" "}
|
|
446
|
+
{required && (
|
|
447
|
+
<span
|
|
448
|
+
className={cn("text-input-error", {
|
|
449
|
+
"text-input-disable-text": disabled,
|
|
450
|
+
})}
|
|
451
|
+
>
|
|
452
|
+
*
|
|
453
|
+
</span>
|
|
454
|
+
)}
|
|
455
|
+
</label>
|
|
456
|
+
)}
|
|
450
457
|
</div>
|
|
451
458
|
{(feedbackMessage || keepFooterSpace) && (
|
|
452
459
|
<span className={helperTextClassname}>
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import "@testing-library/jest-dom/vitest";
|
|
2
|
+
import { cleanup } from "@testing-library/react";
|
|
3
|
+
import { afterEach } from "vitest";
|
|
4
|
+
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
cleanup();
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/* jsdom: scrollTo must update scrollTop or TanStack Virtual keeps scrollOffset > 0 */
|
|
10
|
+
Element.prototype.scrollTo = function scrollTo(
|
|
11
|
+
this: Element,
|
|
12
|
+
arg?: ScrollToOptions | number,
|
|
13
|
+
y?: number,
|
|
14
|
+
): void {
|
|
15
|
+
if (!(this instanceof HTMLElement)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
if (typeof arg === "object" && arg != null && "top" in arg) {
|
|
19
|
+
const t = (arg as { top?: number }).top;
|
|
20
|
+
this.scrollTop = typeof t === "number" ? t : 0;
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (typeof arg === "number") {
|
|
24
|
+
this.scrollTop = typeof y === "number" ? y : arg;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
if (typeof HTMLElement.prototype.scrollIntoView !== "function") {
|
|
28
|
+
HTMLElement.prototype.scrollIntoView = function scrollIntoView() {
|
|
29
|
+
/* no-op */
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* TanStack Virtual subscribes to ResizeObserver; a no-op observe() never
|
|
35
|
+
* re-measures after layout. Schedule an initial callback so scrollport size
|
|
36
|
+
* updates after mount (still relies on getBoundingClientRect / layout mocks in tests).
|
|
37
|
+
*/
|
|
38
|
+
globalThis.ResizeObserver = class ResizeObserver {
|
|
39
|
+
constructor(private readonly cb: ResizeObserverCallback) {}
|
|
40
|
+
|
|
41
|
+
observe(): void {
|
|
42
|
+
queueMicrotask(() => {
|
|
43
|
+
this.cb([], this as unknown as ResizeObserver);
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
unobserve(): void {}
|
|
48
|
+
|
|
49
|
+
disconnect(): void {}
|
|
50
|
+
};
|