@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.
Files changed (65) hide show
  1. package/dist/cjs/bundle.css +501 -67
  2. package/dist/cjs/bundle.js +589 -589
  3. package/dist/cjs/bundle.js.map +1 -1
  4. package/dist/cjs/types/components/DataTable/DataTable.d.ts +195 -4
  5. package/dist/cjs/types/components/DataTable/DataTable.editing.d.ts +20 -0
  6. package/dist/cjs/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  7. package/dist/cjs/types/components/DataTable/DataTable.stories.d.ts +268 -6
  8. package/dist/cjs/types/components/Dropdown/Dropdown.d.ts +22 -0
  9. package/dist/cjs/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  10. package/dist/cjs/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  11. package/dist/cjs/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  12. package/dist/cjs/types/components/Table/Table.d.ts +33 -3
  13. package/dist/cjs/types/components/Table/Table.stories.d.ts +86 -4
  14. package/dist/cjs/types/components/TextInput/TextInput.stories.d.ts +8 -0
  15. package/dist/cjs/types/components/TextInput/TextInput.styles.d.ts +1 -0
  16. package/dist/components/DataTable/DataTable.editing.js +385 -0
  17. package/dist/components/DataTable/DataTable.editing.types.js +1 -0
  18. package/dist/components/DataTable/DataTable.js +983 -50
  19. package/dist/components/DataTable/DataTable.stories.js +1077 -25
  20. package/dist/components/Dropdown/Dropdown.js +8 -6
  21. package/dist/components/ScrollArea/ScrollArea.js +2 -2
  22. package/dist/components/ScrollArea/ScrollArea.stories.js +68 -2
  23. package/dist/components/Table/Table.js +103 -13
  24. package/dist/components/Table/Table.stories.js +226 -9
  25. package/dist/components/TextInput/TextInput.js +6 -4
  26. package/dist/components/TextInput/TextInput.stories.js +8 -0
  27. package/dist/components/TextInput/TextInput.styles.js +7 -1
  28. package/dist/esm/bundle.css +501 -67
  29. package/dist/esm/bundle.js +1545 -1545
  30. package/dist/esm/bundle.js.map +1 -1
  31. package/dist/esm/types/components/DataTable/DataTable.d.ts +195 -4
  32. package/dist/esm/types/components/DataTable/DataTable.editing.d.ts +20 -0
  33. package/dist/esm/types/components/DataTable/DataTable.editing.types.d.ts +145 -0
  34. package/dist/esm/types/components/DataTable/DataTable.stories.d.ts +268 -6
  35. package/dist/esm/types/components/Dropdown/Dropdown.d.ts +22 -0
  36. package/dist/esm/types/components/Dropdown/Dropdown.stories.d.ts +4 -0
  37. package/dist/esm/types/components/ScrollArea/ScrollArea.d.ts +3 -3
  38. package/dist/esm/types/components/ScrollArea/ScrollArea.stories.d.ts +4 -0
  39. package/dist/esm/types/components/Table/Table.d.ts +33 -3
  40. package/dist/esm/types/components/Table/Table.stories.d.ts +86 -4
  41. package/dist/esm/types/components/TextInput/TextInput.stories.d.ts +8 -0
  42. package/dist/esm/types/components/TextInput/TextInput.styles.d.ts +1 -0
  43. package/dist/index.d.ts +493 -122
  44. package/dist/src/theme/global.css +747 -96
  45. package/package.json +14 -2
  46. package/src/components/DataTable/DataTable.editing.tsx +861 -0
  47. package/src/components/DataTable/DataTable.editing.types.ts +192 -0
  48. package/src/components/DataTable/DataTable.stories.tsx +2169 -31
  49. package/src/components/DataTable/DataTable.test.tsx +696 -0
  50. package/src/components/DataTable/DataTable.tsx +2260 -94
  51. package/src/components/Dropdown/Dropdown.tsx +22 -6
  52. package/src/components/ScrollArea/ScrollArea.stories.tsx +146 -3
  53. package/src/components/ScrollArea/ScrollArea.tsx +6 -6
  54. package/src/components/Table/Table.stories.tsx +789 -44
  55. package/src/components/Table/Table.tsx +294 -28
  56. package/src/components/TextInput/TextInput.stories.tsx +80 -0
  57. package/src/components/TextInput/TextInput.styles.ts +7 -1
  58. package/src/components/TextInput/TextInput.tsx +21 -14
  59. package/src/test/setup.ts +50 -0
  60. package/src/theme/global.css +81 -42
  61. package/src/theme/presets/colors.js +12 -0
  62. package/src/theme/themes/variable.css +27 -28
  63. package/src/theme/tokens/baseline.css +2 -1
  64. package/src/theme/tokens/components/scrollbar.css +9 -4
  65. 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
- <div
13
- className={cn("relative h-full w-full overflow-auto", rootClassName)}
14
- ref={rootRef}
15
- >
16
- <table
17
- ref={ref}
18
- className={cn("w-full caption-bottom text-sm border-collapse", className)}
19
- {...props}
20
- />
21
- </div>
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("[&_tr]:border-b bg-secondary-80", className)}
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("[&_tr:last-child]:border-0", className)}
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 bg-transparent-grey2-8 font-medium [&>tr]:last:border-b-0",
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
- React.HTMLAttributes<HTMLTableRowElement>
68
- >(({ className, ...props }, ref) => (
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
- "border-b transition-colors hover:bg-transparent-grey2-8 data-[state=selected]:bg-grey-20",
73
- className
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
- " h-12 py-3 px-6 text-left align-middle typography-body2 text-text-g-contrast-low [&:has([role=checkbox])]:pr-4 [&:has([role=checkbox])]:w-4",
88
- className
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-6 text-left align-middle typography-body3 text-text-g-contrast-low [&:has([role=checkbox])]:pr-4 [&:has([role=checkbox])]:w-4",
103
- className
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 placeholder:text-transparent bg-transparent caret-primary",
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 = "solid",
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
- <label htmlFor={_id} className={cn(labelClassname)}>
439
- {label}{" "}
440
- {required && (
441
- <span
442
- className={cn("text-input-error", {
443
- "text-input-disable-text": disabled,
444
- })}
445
- >
446
- *
447
- </span>
448
- )}
449
- </label>
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
+ };