@marianmeres/stuic 3.160.0 → 3.162.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/DataTable/DataTable.svelte +145 -45
- package/dist/components/DataTable/DataTable.svelte.d.ts +46 -0
- package/dist/components/DataTable/README.md +146 -53
- package/dist/components/DataTable/index.css +34 -0
- package/dist/components/RegisterForm/README.md +32 -32
- package/dist/components/RegisterForm/RegisterForm.svelte +18 -19
- package/dist/components/RegisterForm/RegisterForm.svelte.d.ts +9 -10
- package/dist/components/RegisterForm/RegisterFormModal.svelte +1 -1
- package/dist/components/RegisterForm/RegisterFormModal.svelte.d.ts +1 -1
- package/dist/components/RegisterForm/index.css +4 -5
- package/docs/domains/components.md +24 -24
- package/package.json +1 -1
|
@@ -98,6 +98,54 @@
|
|
|
98
98
|
/** Callback when a row is clicked */
|
|
99
99
|
onRowClick?: (row: T, index: number) => void;
|
|
100
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Where this row's primary action navigates. When it returns a href for a row, the
|
|
103
|
+
* content of that row's "lead" cell is wrapped in an anchor -- which is what makes a
|
|
104
|
+
* clickable row keyboard-, middle- and modifier-click-reachable.
|
|
105
|
+
*
|
|
106
|
+
* Return `undefined` for a row with no destination (a placeholder, a row still
|
|
107
|
+
* minting its id) -- that row's lead cell renders exactly as it does today.
|
|
108
|
+
*
|
|
109
|
+
* Independent of `onRowClick`: supply both and the anchor handles the keyboard and
|
|
110
|
+
* modified clicks while `onRowClick` keeps handling a plain click anywhere else on
|
|
111
|
+
* the row -- the row click handler ignores events originating inside an anchor, so
|
|
112
|
+
* the two never double-fire. (Consequence: a plain click on the lead cell navigates
|
|
113
|
+
* via the href and does NOT call `onRowClick` -- point both at the same destination.)
|
|
114
|
+
*/
|
|
115
|
+
rowHref?: (row: T, index: number) => string | undefined;
|
|
116
|
+
/**
|
|
117
|
+
* Which column is the "lead" cell for `rowHref`. Defaults to the first column in
|
|
118
|
+
* `columns`. On mobile, if that column is `hideOnMobile`, the first visible card
|
|
119
|
+
* field is used instead (so the card keeps a keyboard-reachable link).
|
|
120
|
+
*/
|
|
121
|
+
rowHrefColumn?: string;
|
|
122
|
+
/** Additional CSS classes for the anchor generated by `rowHref`. */
|
|
123
|
+
classRowLink?: string;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Make the desktop `<tr>` itself focusable and Enter-activatable.
|
|
127
|
+
*
|
|
128
|
+
* For a row whose action is NOT a navigation -- prefer `rowHref` when it is, since a
|
|
129
|
+
* real link is better in every respect (tab order, `Enter`, middle/cmd-click, "copy
|
|
130
|
+
* link address", and an announcement of where it goes).
|
|
131
|
+
*
|
|
132
|
+
* Deliberately does NOT set `role="button"`: a `<tr>` carries an implicit `row` role
|
|
133
|
+
* that the table's own structure depends on, and overriding it detaches the row from
|
|
134
|
+
* the table for assistive technology. The row stays a row; it merely becomes focusable.
|
|
135
|
+
*
|
|
136
|
+
* Also deliberately `Enter`-only, unlike the mobile card (which IS a `role="button"`,
|
|
137
|
+
* where `Space` is expected). Taking `Space` -- page scroll -- away from a table is a
|
|
138
|
+
* worse trade than the one the card makes.
|
|
139
|
+
*
|
|
140
|
+
* No-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
141
|
+
*/
|
|
142
|
+
rowActivatable?: boolean;
|
|
143
|
+
/**
|
|
144
|
+
* Accessible name for an activatable row, e.g. `(row) => "Open " + row.name`.
|
|
145
|
+
* Only applied when the row is actually activatable.
|
|
146
|
+
*/
|
|
147
|
+
rowLabel?: (row: T, index: number) => string | undefined;
|
|
148
|
+
|
|
101
149
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
102
150
|
loading?: boolean;
|
|
103
151
|
|
|
@@ -203,6 +251,11 @@
|
|
|
203
251
|
selectedAll = $bindable(false),
|
|
204
252
|
excluded = $bindable(new Set()),
|
|
205
253
|
onRowClick,
|
|
254
|
+
rowHref,
|
|
255
|
+
rowHrefColumn,
|
|
256
|
+
classRowLink,
|
|
257
|
+
rowActivatable = false,
|
|
258
|
+
rowLabel,
|
|
206
259
|
loading = false,
|
|
207
260
|
cell,
|
|
208
261
|
row,
|
|
@@ -342,22 +395,46 @@
|
|
|
342
395
|
return allOnPageSelected;
|
|
343
396
|
});
|
|
344
397
|
|
|
345
|
-
// --- Row click ---
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
398
|
+
// --- Row click / activation ---
|
|
399
|
+
|
|
400
|
+
// A row is a big target that legitimately contains its own controls. Anything that
|
|
401
|
+
// originates inside one of those is that control's business, not the row's - this
|
|
402
|
+
// guards the click handler AND both keydown handlers (Enter on a focused lead link
|
|
403
|
+
// must navigate, not fire onRowClick; Space on a focused checkbox must toggle it).
|
|
404
|
+
function isInteractiveTarget(e: Event): boolean {
|
|
405
|
+
const target = e.target as HTMLElement | null;
|
|
406
|
+
if (!target?.closest) return false;
|
|
407
|
+
return !!(
|
|
349
408
|
target.closest('input[type="checkbox"]') ||
|
|
350
409
|
target.closest("button") ||
|
|
351
410
|
target.closest("a")
|
|
352
|
-
)
|
|
353
|
-
|
|
354
|
-
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function activateRow(row: T, index: number) {
|
|
355
415
|
if (selectable && selectOnRowClick) {
|
|
356
416
|
toggleSelectRow(getRowId(row, index));
|
|
357
417
|
}
|
|
358
418
|
onRowClick?.(row, index);
|
|
359
419
|
}
|
|
360
420
|
|
|
421
|
+
function handleRowClick(row: T, index: number, e: MouseEvent) {
|
|
422
|
+
if (isInteractiveTarget(e)) return;
|
|
423
|
+
activateRow(row, index);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Shared by the desktop `<tr>` (Enter only) and the mobile card (Enter + Space, since
|
|
428
|
+
* the card actually is a `role="button"`).
|
|
429
|
+
*/
|
|
430
|
+
function handleRowKeydown(row: T, index: number, e: KeyboardEvent, withSpace: boolean) {
|
|
431
|
+
if (!rowClickable) return;
|
|
432
|
+
if (e.key !== "Enter" && !(withSpace && e.key === " ")) return;
|
|
433
|
+
if (isInteractiveTarget(e)) return;
|
|
434
|
+
e.preventDefault();
|
|
435
|
+
activateRow(row, index);
|
|
436
|
+
}
|
|
437
|
+
|
|
361
438
|
// --- Cell value helpers ---
|
|
362
439
|
function getCellValue(row: T, column: DataTableColumn<T>): any {
|
|
363
440
|
return column.key.split(".").reduce((obj: any, k) => obj?.[k], row);
|
|
@@ -373,8 +450,45 @@
|
|
|
373
450
|
let rootClass = $derived(unstyled ? classProp : twMerge("stuic-data-table", classProp));
|
|
374
451
|
|
|
375
452
|
let mobileColumns = $derived(columns.filter((col) => !col.hideOnMobile));
|
|
453
|
+
|
|
454
|
+
// --- Row link / activation derivations ---
|
|
455
|
+
let rowClickable = $derived(!!(onRowClick || selectOnRowClick));
|
|
456
|
+
let rowIsActivatable = $derived(rowActivatable && rowClickable);
|
|
457
|
+
|
|
458
|
+
// The lead cell is the one whose content gets wrapped in the `rowHref` anchor.
|
|
459
|
+
let leadColumnKey = $derived(rowHrefColumn ?? columns[0]?.key);
|
|
460
|
+
// A `hideOnMobile` lead column would leave the card with no link at all, so there we
|
|
461
|
+
// fall back to the first visible field rather than silently dropping the anchor.
|
|
462
|
+
let mobileLeadColumnKey = $derived(
|
|
463
|
+
mobileColumns.some((col) => col.key === leadColumnKey)
|
|
464
|
+
? leadColumnKey
|
|
465
|
+
: mobileColumns[0]?.key
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
let rowLinkClass = $derived(
|
|
469
|
+
unstyled ? classRowLink : twMerge("stuic-data-table-row-link", classRowLink)
|
|
470
|
+
);
|
|
376
471
|
</script>
|
|
377
472
|
|
|
473
|
+
<!--
|
|
474
|
+
The rendered content of a single cell, shared by both layouts so that a `rowHref`
|
|
475
|
+
anchor wraps exactly what the cell would have rendered anyway (a consumer `cell`
|
|
476
|
+
snippet included).
|
|
477
|
+
-->
|
|
478
|
+
{#snippet cellBody(
|
|
479
|
+
col: DataTableColumn<T>,
|
|
480
|
+
rowData: T,
|
|
481
|
+
value: any,
|
|
482
|
+
rowIndex: number,
|
|
483
|
+
variant: "desktop" | "mobile"
|
|
484
|
+
)}
|
|
485
|
+
{#if cell}
|
|
486
|
+
{@render cell({ column: col, row: rowData, value, rowIndex, variant })}
|
|
487
|
+
{:else}
|
|
488
|
+
{getCellDisplay(rowData, col)}
|
|
489
|
+
{/if}
|
|
490
|
+
{/snippet}
|
|
491
|
+
|
|
378
492
|
<!-- Batch action bar -->
|
|
379
493
|
{#if selectable && effectiveCount > 0 && batchActions}
|
|
380
494
|
<div class={!unstyled ? "stuic-data-table-batch" : undefined}>
|
|
@@ -461,16 +575,20 @@
|
|
|
461
575
|
{@const rowId = getRowId(rowData, rowIndex)}
|
|
462
576
|
{@const isSelected = selectable && isRowSelected(rowId)}
|
|
463
577
|
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
578
|
+
{@const rowLink = rowHref?.(rowData, rowIndex)}
|
|
464
579
|
{#if row}
|
|
465
580
|
{@render row({ row: rowData, columns, rowIndex, isSelected })}
|
|
466
581
|
{:else}
|
|
467
582
|
<tr
|
|
468
583
|
data-hoverable={!unstyled ? "true" : undefined}
|
|
469
|
-
data-clickable={!unstyled &&
|
|
470
|
-
? "true"
|
|
471
|
-
: undefined}
|
|
584
|
+
data-clickable={!unstyled && rowClickable ? "true" : undefined}
|
|
472
585
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
586
|
+
tabindex={rowIsActivatable ? 0 : undefined}
|
|
587
|
+
aria-label={rowIsActivatable ? rowLabel?.(rowData, rowIndex) : undefined}
|
|
473
588
|
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
589
|
+
onkeydown={(e) => {
|
|
590
|
+
if (rowActivatable) handleRowKeydown(rowData, rowIndex, e, false);
|
|
591
|
+
}}
|
|
474
592
|
>
|
|
475
593
|
{#if selectable}
|
|
476
594
|
<td data-checkbox class="stuic-checkbox">
|
|
@@ -485,20 +603,17 @@
|
|
|
485
603
|
{/if}
|
|
486
604
|
{#each columns as col (col.key)}
|
|
487
605
|
{@const value = getCellValue(rowData, col)}
|
|
606
|
+
{@const href = col.key === leadColumnKey ? rowLink : undefined}
|
|
488
607
|
<td
|
|
489
608
|
class={col.class}
|
|
490
609
|
data-align={!unstyled && col.align ? col.align : undefined}
|
|
491
610
|
>
|
|
492
|
-
{#if
|
|
493
|
-
{
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
value,
|
|
497
|
-
rowIndex,
|
|
498
|
-
variant: "desktop",
|
|
499
|
-
})}
|
|
611
|
+
{#if href}
|
|
612
|
+
<a class={rowLinkClass} {href}>
|
|
613
|
+
{@render cellBody(col, rowData, value, rowIndex, "desktop")}
|
|
614
|
+
</a>
|
|
500
615
|
{:else}
|
|
501
|
-
{
|
|
616
|
+
{@render cellBody(col, rowData, value, rowIndex, "desktop")}
|
|
502
617
|
{/if}
|
|
503
618
|
</td>
|
|
504
619
|
{/each}
|
|
@@ -531,6 +646,7 @@
|
|
|
531
646
|
{@const rowId = getRowId(rowData, rowIndex)}
|
|
532
647
|
{@const isSelected = selectable && isRowSelected(rowId)}
|
|
533
648
|
{@const selectDisabled = !!selectDisabledBy?.(rowData, rowIndex)}
|
|
649
|
+
{@const rowLink = rowHref?.(rowData, rowIndex)}
|
|
534
650
|
{#if mobileRow}
|
|
535
651
|
{@render mobileRow({
|
|
536
652
|
row: rowData,
|
|
@@ -542,25 +658,12 @@
|
|
|
542
658
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
543
659
|
<div
|
|
544
660
|
class={!unstyled ? "stuic-data-table-card" : undefined}
|
|
545
|
-
data-clickable={!unstyled &&
|
|
546
|
-
? "true"
|
|
547
|
-
: undefined}
|
|
661
|
+
data-clickable={!unstyled && rowClickable ? "true" : undefined}
|
|
548
662
|
data-selected={!unstyled && isSelected ? "true" : undefined}
|
|
549
|
-
role={
|
|
550
|
-
tabindex={
|
|
663
|
+
role={rowClickable ? "button" : undefined}
|
|
664
|
+
tabindex={rowClickable ? 0 : undefined}
|
|
551
665
|
onclick={(e) => handleRowClick(rowData, rowIndex, e)}
|
|
552
|
-
onkeydown={(e) =>
|
|
553
|
-
if (
|
|
554
|
-
(onRowClick || selectOnRowClick) &&
|
|
555
|
-
(e.key === "Enter" || e.key === " ")
|
|
556
|
-
) {
|
|
557
|
-
e.preventDefault();
|
|
558
|
-
if (selectable && selectOnRowClick) {
|
|
559
|
-
toggleSelectRow(rowId);
|
|
560
|
-
}
|
|
561
|
-
onRowClick?.(rowData, rowIndex);
|
|
562
|
-
}
|
|
563
|
-
}}
|
|
666
|
+
onkeydown={(e) => handleRowKeydown(rowData, rowIndex, e, true)}
|
|
564
667
|
>
|
|
565
668
|
{#if selectable}
|
|
566
669
|
<div
|
|
@@ -579,6 +682,7 @@
|
|
|
579
682
|
{/if}
|
|
580
683
|
{#each mobileColumns as col (col.key)}
|
|
581
684
|
{@const value = getCellValue(rowData, col)}
|
|
685
|
+
{@const href = col.key === mobileLeadColumnKey ? rowLink : undefined}
|
|
582
686
|
<div class={!unstyled ? "stuic-data-table-card-row" : undefined}>
|
|
583
687
|
<span class={!unstyled ? "stuic-data-table-card-label" : undefined}>
|
|
584
688
|
{#if isTHCNotEmpty(col.label)}
|
|
@@ -588,16 +692,12 @@
|
|
|
588
692
|
{/if}
|
|
589
693
|
</span>
|
|
590
694
|
<span class={!unstyled ? "stuic-data-table-card-value" : undefined}>
|
|
591
|
-
{#if
|
|
592
|
-
{
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
value,
|
|
596
|
-
rowIndex,
|
|
597
|
-
variant: "mobile",
|
|
598
|
-
})}
|
|
695
|
+
{#if href}
|
|
696
|
+
<a class={rowLinkClass} {href}>
|
|
697
|
+
{@render cellBody(col, rowData, value, rowIndex, "mobile")}
|
|
698
|
+
</a>
|
|
599
699
|
{:else}
|
|
600
|
-
{
|
|
700
|
+
{@render cellBody(col, rowData, value, rowIndex, "mobile")}
|
|
601
701
|
{/if}
|
|
602
702
|
</span>
|
|
603
703
|
</div>
|
|
@@ -58,6 +58,52 @@ export interface Props<T = Record<string, any>> extends Omit<HTMLAttributes<HTML
|
|
|
58
58
|
excluded?: Set<string | number>;
|
|
59
59
|
/** Callback when a row is clicked */
|
|
60
60
|
onRowClick?: (row: T, index: number) => void;
|
|
61
|
+
/**
|
|
62
|
+
* Where this row's primary action navigates. When it returns a href for a row, the
|
|
63
|
+
* content of that row's "lead" cell is wrapped in an anchor -- which is what makes a
|
|
64
|
+
* clickable row keyboard-, middle- and modifier-click-reachable.
|
|
65
|
+
*
|
|
66
|
+
* Return `undefined` for a row with no destination (a placeholder, a row still
|
|
67
|
+
* minting its id) -- that row's lead cell renders exactly as it does today.
|
|
68
|
+
*
|
|
69
|
+
* Independent of `onRowClick`: supply both and the anchor handles the keyboard and
|
|
70
|
+
* modified clicks while `onRowClick` keeps handling a plain click anywhere else on
|
|
71
|
+
* the row -- the row click handler ignores events originating inside an anchor, so
|
|
72
|
+
* the two never double-fire. (Consequence: a plain click on the lead cell navigates
|
|
73
|
+
* via the href and does NOT call `onRowClick` -- point both at the same destination.)
|
|
74
|
+
*/
|
|
75
|
+
rowHref?: (row: T, index: number) => string | undefined;
|
|
76
|
+
/**
|
|
77
|
+
* Which column is the "lead" cell for `rowHref`. Defaults to the first column in
|
|
78
|
+
* `columns`. On mobile, if that column is `hideOnMobile`, the first visible card
|
|
79
|
+
* field is used instead (so the card keeps a keyboard-reachable link).
|
|
80
|
+
*/
|
|
81
|
+
rowHrefColumn?: string;
|
|
82
|
+
/** Additional CSS classes for the anchor generated by `rowHref`. */
|
|
83
|
+
classRowLink?: string;
|
|
84
|
+
/**
|
|
85
|
+
* Make the desktop `<tr>` itself focusable and Enter-activatable.
|
|
86
|
+
*
|
|
87
|
+
* For a row whose action is NOT a navigation -- prefer `rowHref` when it is, since a
|
|
88
|
+
* real link is better in every respect (tab order, `Enter`, middle/cmd-click, "copy
|
|
89
|
+
* link address", and an announcement of where it goes).
|
|
90
|
+
*
|
|
91
|
+
* Deliberately does NOT set `role="button"`: a `<tr>` carries an implicit `row` role
|
|
92
|
+
* that the table's own structure depends on, and overriding it detaches the row from
|
|
93
|
+
* the table for assistive technology. The row stays a row; it merely becomes focusable.
|
|
94
|
+
*
|
|
95
|
+
* Also deliberately `Enter`-only, unlike the mobile card (which IS a `role="button"`,
|
|
96
|
+
* where `Space` is expected). Taking `Space` -- page scroll -- away from a table is a
|
|
97
|
+
* worse trade than the one the card makes.
|
|
98
|
+
*
|
|
99
|
+
* No-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
100
|
+
*/
|
|
101
|
+
rowActivatable?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Accessible name for an activatable row, e.g. `(row) => "Open " + row.name`.
|
|
104
|
+
* Only applied when the row is actually activatable.
|
|
105
|
+
*/
|
|
106
|
+
rowLabel?: (row: T, index: number) => string | undefined;
|
|
61
107
|
/** Show loading state (spinner overlay + reduced opacity) */
|
|
62
108
|
loading?: boolean;
|
|
63
109
|
/** Custom cell renderer snippet (rendered in both desktop table and mobile card layouts; use `variant` to tell them apart) */
|
|
@@ -143,6 +143,89 @@ New records inserted while all-pages mode is active are implicitly selected (the
|
|
|
143
143
|
|
|
144
144
|
**Filter changes:** when filters change in the consumer, reset the bound selection stores (`selected`, `selectedAll`, `excluded`) explicitly — DataTable doesn't track which filter produced the current state.
|
|
145
145
|
|
|
146
|
+
### Clickable Rows That Work Without a Mouse
|
|
147
|
+
|
|
148
|
+
`onRowClick` alone makes a row **mouse-only** on the desktop table layout: a `<tr>` is not
|
|
149
|
+
focusable, so nothing about the row is reachable by keyboard. (The mobile card layout has
|
|
150
|
+
always been fine — it is a `<div role="button">`.) Two opt-in props close that gap, and
|
|
151
|
+
they are not alternatives:
|
|
152
|
+
|
|
153
|
+
| Your row action is… | Use |
|
|
154
|
+
| ------------------------------- | ---------------- |
|
|
155
|
+
| a navigation (it has a URL) | `rowHref` |
|
|
156
|
+
| anything else (drawer, expand…) | `rowActivatable` |
|
|
157
|
+
|
|
158
|
+
#### `rowHref` — the lead cell becomes a real link
|
|
159
|
+
|
|
160
|
+
```svelte
|
|
161
|
+
<DataTable
|
|
162
|
+
{columns}
|
|
163
|
+
{data}
|
|
164
|
+
getRowId={(row) => row.id}
|
|
165
|
+
rowHref={(row) => `/users/${row.id}`}
|
|
166
|
+
/>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The content of the row's **lead cell** (the first column by default, or `rowHrefColumn`)
|
|
170
|
+
is wrapped in an `<a href>` — including whatever your `cell` snippet renders for it, and
|
|
171
|
+
respecting the column's `renderValue`. A link gets, for free and correctly: keyboard focus
|
|
172
|
+
in the right tab order, `Enter`, ⌘/middle-click and "open in new tab", "copy link address",
|
|
173
|
+
and a screen-reader announcement of _where it goes_ rather than an anonymous "button".
|
|
174
|
+
|
|
175
|
+
Return `undefined` for a row with no destination (a placeholder, a row still minting its
|
|
176
|
+
id) — that row's lead cell renders exactly as it would without `rowHref`.
|
|
177
|
+
|
|
178
|
+
`rowHref` composes with `onRowClick`: the row click handler ignores clicks originating
|
|
179
|
+
inside an anchor, so the two never double-fire. The consequence is worth stating — a plain
|
|
180
|
+
click **on the lead cell** navigates via the href and does _not_ call `onRowClick`, so point
|
|
181
|
+
both at the same destination.
|
|
182
|
+
|
|
183
|
+
On mobile the same field is linked. If the lead column is `hideOnMobile`, the card links its
|
|
184
|
+
first visible field instead, rather than silently dropping the anchor.
|
|
185
|
+
|
|
186
|
+
By default the link looks like the text it replaced (`color: inherit`, no underline until
|
|
187
|
+
hover) — a table where every lead cell is blue-and-underlined is a worse table. Three CSS
|
|
188
|
+
variables and `classRowLink` are there if you disagree.
|
|
189
|
+
|
|
190
|
+
#### `rowActivatable` — the `<tr>` itself
|
|
191
|
+
|
|
192
|
+
For a row whose action has no address:
|
|
193
|
+
|
|
194
|
+
```svelte
|
|
195
|
+
<DataTable
|
|
196
|
+
{columns}
|
|
197
|
+
{data}
|
|
198
|
+
onRowClick={(row) => openDrawer(row)}
|
|
199
|
+
rowActivatable
|
|
200
|
+
rowLabel={(row) => `Open ${row.name}`}
|
|
201
|
+
/>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
The `<tr>` gets `tabindex="0"`, an optional `aria-label` from `rowLabel`, and a
|
|
205
|
+
`:focus-visible` ring. It is a no-op unless `onRowClick` or `selectOnRowClick` is set.
|
|
206
|
+
|
|
207
|
+
Two deliberate choices, so nobody "fixes" them later:
|
|
208
|
+
|
|
209
|
+
- **No `role="button"` on the `<tr>`.** A `<tr>` carries an implicit `row` role that the
|
|
210
|
+
table's own structure depends on. Overriding it detaches the row from the table for
|
|
211
|
+
assistive technology — row/column counts stop making sense and cell-by-cell navigation
|
|
212
|
+
breaks. The row stays a row; it merely becomes focusable. (The mobile card is a `<div>`,
|
|
213
|
+
so `role="button"` is correct _there_.)
|
|
214
|
+
- **`Enter` only, no `Space`.** `Space` on a focused row scrolls the page, and a row is not
|
|
215
|
+
a button — taking `Space` away from a table is a worse trade than the one the card makes.
|
|
216
|
+
|
|
217
|
+
With `selectable` + `selectOnRowClick`, `Enter` toggles selection _and_ fires `onRowClick`,
|
|
218
|
+
matching the mobile card.
|
|
219
|
+
|
|
220
|
+
#### Both are opt-in
|
|
221
|
+
|
|
222
|
+
With neither prop set, the rendered markup is exactly what it was before they existed. A
|
|
223
|
+
consumer who replaces the whole `<tr>` with the `row` snippet opts out of both and owns the
|
|
224
|
+
keyboard story themselves.
|
|
225
|
+
|
|
226
|
+
Interactive descendants always win: a click, `Enter` or `Space` that originates inside a
|
|
227
|
+
`<button>`, an `<a>` or a checkbox in the row is that control's business, not the row's.
|
|
228
|
+
|
|
146
229
|
### Custom Cell Rendering
|
|
147
230
|
|
|
148
231
|
The `cell` snippet is used for both desktop and mobile layouts. Use the `variant` param if rendering differs per layout.
|
|
@@ -204,33 +287,38 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
|
|
|
204
287
|
|
|
205
288
|
## Props
|
|
206
289
|
|
|
207
|
-
| Prop | Type
|
|
208
|
-
| --------------------- |
|
|
209
|
-
| `columns` | `DataTableColumn<T>[]`
|
|
210
|
-
| `data` | `T[]`
|
|
211
|
-
| `getRowId` | `(row, index) => string \| number`
|
|
212
|
-
| `paging` | `PagingCalcResult`
|
|
213
|
-
| `onPageChange` | `(offset: number) => void`
|
|
214
|
-
| `selectable` | `boolean`
|
|
215
|
-
| `selected` | `Set<string \| number>`
|
|
216
|
-
| `selectOnRowClick` | `boolean`
|
|
217
|
-
| `selectDisabledBy` | `(row, index) => boolean`
|
|
218
|
-
| `allowSelectAllPages` | `boolean`
|
|
219
|
-
| `selectedAll` | `boolean`
|
|
220
|
-
| `excluded` | `Set<string \| number>`
|
|
221
|
-
| `onRowClick` | `(row, index) => void`
|
|
222
|
-
| `
|
|
223
|
-
| `
|
|
224
|
-
| `
|
|
225
|
-
| `
|
|
226
|
-
| `
|
|
227
|
-
| `
|
|
228
|
-
| `
|
|
229
|
-
| `
|
|
230
|
-
| `
|
|
231
|
-
| `
|
|
232
|
-
| `
|
|
233
|
-
| `
|
|
290
|
+
| Prop | Type | Default | Description |
|
|
291
|
+
| --------------------- | ------------------------------------- | ------------- | -------------------------------------------------------------------------- |
|
|
292
|
+
| `columns` | `DataTableColumn<T>[]` | required | Column definitions |
|
|
293
|
+
| `data` | `T[]` | required | Array of row data |
|
|
294
|
+
| `getRowId` | `(row, index) => string \| number` | `(_, i) => i` | Row ID extractor |
|
|
295
|
+
| `paging` | `PagingCalcResult` | - | Paging state (from `@marianmeres/paging-store`) |
|
|
296
|
+
| `onPageChange` | `(offset: number) => void` | - | Called with the new offset when the user navigates pages |
|
|
297
|
+
| `selectable` | `boolean` | `false` | Enable selection checkboxes |
|
|
298
|
+
| `selected` | `Set<string \| number>` | `new Set()` | Selected row IDs (bindable) |
|
|
299
|
+
| `selectOnRowClick` | `boolean` | `false` | Clicking anywhere on a row toggles its selection |
|
|
300
|
+
| `selectDisabledBy` | `(row, index) => boolean` | - | Return `true` to disable selection for a specific row |
|
|
301
|
+
| `allowSelectAllPages` | `boolean` | `false` | Show a banner offering "select all results" across paged data |
|
|
302
|
+
| `selectedAll` | `boolean` | `false` | All-pages mode flag (bindable). In this mode `excluded` drives selection |
|
|
303
|
+
| `excluded` | `Set<string \| number>` | `new Set()` | Deselected row IDs while in all-pages mode (bindable) |
|
|
304
|
+
| `onRowClick` | `(row, index) => void` | - | Row click callback |
|
|
305
|
+
| `rowHref` | `(row, index) => string \| undefined` | - | Wrap the lead cell's content in an `<a href>` (keyboard/⌘-click reachable) |
|
|
306
|
+
| `rowHrefColumn` | `string` | first column | Which column is the lead cell for `rowHref` |
|
|
307
|
+
| `classRowLink` | `string` | - | Extra classes for the `rowHref` anchor |
|
|
308
|
+
| `rowActivatable` | `boolean` | `false` | Make the desktop `<tr>` focusable + `Enter`-activatable (no `role`) |
|
|
309
|
+
| `rowLabel` | `(row, index) => string \| undefined` | - | Accessible name for an activatable row |
|
|
310
|
+
| `loading` | `boolean` | `false` | Show loading overlay |
|
|
311
|
+
| `small` | `boolean` | `false` | Force mobile/card layout regardless of viewport |
|
|
312
|
+
| `t` | `TranslateFn` | built-in | Optional translation function |
|
|
313
|
+
| `cell` | `Snippet` | - | Custom cell renderer (desktop + mobile) |
|
|
314
|
+
| `row` | `Snippet` | - | Custom desktop `<tr>` renderer (overrides default row) |
|
|
315
|
+
| `mobileRow` | `Snippet` | - | Custom mobile card renderer |
|
|
316
|
+
| `batchActions` | `Snippet` | - | Batch action bar content |
|
|
317
|
+
| `selectAllBanner` | `Snippet` | - | Override default "select all across pages" banner |
|
|
318
|
+
| `empty` | `Snippet` | - | Custom empty state |
|
|
319
|
+
| `unstyled` | `boolean` | `false` | Skip default styling |
|
|
320
|
+
| `class` | `string` | - | Additional CSS classes |
|
|
321
|
+
| `el` | `HTMLDivElement` | - | Bindable element ref |
|
|
234
322
|
|
|
235
323
|
### Snippet signatures
|
|
236
324
|
|
|
@@ -262,29 +350,34 @@ Replace the entire `<tr>` on desktop. When this snippet is provided, DataTable d
|
|
|
262
350
|
|
|
263
351
|
## CSS Variables
|
|
264
352
|
|
|
265
|
-
| Variable
|
|
266
|
-
|
|
|
267
|
-
| `--stuic-data-table-radius`
|
|
268
|
-
| `--stuic-data-table-border-color`
|
|
269
|
-
| `--stuic-data-table-header-bg`
|
|
270
|
-
| `--stuic-data-table-header-color`
|
|
271
|
-
| `--stuic-data-table-header-font-size`
|
|
272
|
-
| `--stuic-data-table-header-font-weight`
|
|
273
|
-
| `--stuic-data-table-header-padding-x`
|
|
274
|
-
| `--stuic-data-table-header-padding-y`
|
|
275
|
-
| `--stuic-data-table-row-bg`
|
|
276
|
-
| `--stuic-data-table-row-bg-hover`
|
|
277
|
-
| `--stuic-data-table-row-bg-selected`
|
|
278
|
-
| `--stuic-data-table-row-border-color`
|
|
279
|
-
| `--stuic-data-table-
|
|
280
|
-
| `--stuic-data-table-
|
|
281
|
-
| `--stuic-data-table-
|
|
282
|
-
| `--stuic-data-table-
|
|
283
|
-
| `--stuic-data-table-
|
|
284
|
-
| `--stuic-data-table-
|
|
285
|
-
| `--stuic-data-table-
|
|
286
|
-
| `--stuic-data-table-
|
|
287
|
-
| `--stuic-data-table-
|
|
288
|
-
| `--stuic-data-table-
|
|
289
|
-
| `--stuic-data-table-
|
|
290
|
-
| `--stuic-data-table-
|
|
353
|
+
| Variable | Default | Description |
|
|
354
|
+
| ---------------------------------------------- | ------------------------------------- | ---------------------------- |
|
|
355
|
+
| `--stuic-data-table-radius` | `var(--radius-md)` | Border radius |
|
|
356
|
+
| `--stuic-data-table-border-color` | `var(--stuic-color-border)` | Border color |
|
|
357
|
+
| `--stuic-data-table-header-bg` | `var(--stuic-color-muted)` | Header background |
|
|
358
|
+
| `--stuic-data-table-header-color` | `var(--stuic-color-muted-foreground)` | Header text |
|
|
359
|
+
| `--stuic-data-table-header-font-size` | `0.875rem` | Header font size |
|
|
360
|
+
| `--stuic-data-table-header-font-weight` | `var(--font-weight-semibold)` | Header font weight |
|
|
361
|
+
| `--stuic-data-table-header-padding-x` | `0.75rem` | Header horizontal padding |
|
|
362
|
+
| `--stuic-data-table-header-padding-y` | `0.5rem` | Header vertical padding |
|
|
363
|
+
| `--stuic-data-table-row-bg` | `transparent` | Row background |
|
|
364
|
+
| `--stuic-data-table-row-bg-hover` | `var(--stuic-color-muted)` | Row hover background |
|
|
365
|
+
| `--stuic-data-table-row-bg-selected` | `color-mix(primary 10%)` | Selected row background |
|
|
366
|
+
| `--stuic-data-table-row-border-color` | `var(--stuic-color-border)` | Row border color |
|
|
367
|
+
| `--stuic-data-table-row-link-color` | `inherit` | `rowHref` anchor color |
|
|
368
|
+
| `--stuic-data-table-row-link-decoration` | `none` | `rowHref` anchor decoration |
|
|
369
|
+
| `--stuic-data-table-row-link-decoration-hover` | `underline` | …on hover |
|
|
370
|
+
| `--stuic-data-table-row-ring-width` | `3px` | Activatable row focus ring |
|
|
371
|
+
| `--stuic-data-table-row-ring-color` | `var(--stuic-color-ring)` | Activatable row ring color |
|
|
372
|
+
| `--stuic-data-table-cell-padding-x` | `0.75rem` | Cell horizontal padding |
|
|
373
|
+
| `--stuic-data-table-cell-padding-y` | `0.75rem` | Cell vertical padding |
|
|
374
|
+
| `--stuic-data-table-cell-font-size` | `0.875rem` | Cell font size |
|
|
375
|
+
| `--stuic-data-table-loading-opacity` | `0.5` | Loading state opacity |
|
|
376
|
+
| `--stuic-data-table-card-bg` | `var(--stuic-color-background)` | Mobile card background |
|
|
377
|
+
| `--stuic-data-table-card-border-color` | `var(--stuic-color-border)` | Mobile card border |
|
|
378
|
+
| `--stuic-data-table-card-radius` | `var(--radius-md)` | Mobile card radius |
|
|
379
|
+
| `--stuic-data-table-card-padding` | `0.75rem` | Mobile card padding |
|
|
380
|
+
| `--stuic-data-table-card-gap` | `0.5rem` | Gap between mobile cards |
|
|
381
|
+
| `--stuic-data-table-select-all-bg` | `color-mix(primary 10%)` | Select-all banner background |
|
|
382
|
+
| `--stuic-data-table-select-all-padding-x` | `0.75rem` | Banner horizontal padding |
|
|
383
|
+
| `--stuic-data-table-select-all-padding-y` | `0.5rem` | Banner vertical padding |
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
--stuic-data-table-row-bg-selected: color-mix(in srgb, var(--stuic-color-primary) 10%, var(--stuic-color-background));
|
|
25
25
|
--stuic-data-table-row-border-color: var(--stuic-color-border);
|
|
26
26
|
|
|
27
|
+
/* Row link (rowHref) — defaults to "looks like the text it replaced" */
|
|
28
|
+
--stuic-data-table-row-link-color: inherit;
|
|
29
|
+
--stuic-data-table-row-link-decoration: none;
|
|
30
|
+
--stuic-data-table-row-link-decoration-hover: underline;
|
|
31
|
+
|
|
32
|
+
/* Focus ring for an activatable row (rowActivatable) */
|
|
33
|
+
--stuic-data-table-row-ring-width: 3px;
|
|
34
|
+
--stuic-data-table-row-ring-color: var(--stuic-color-ring);
|
|
35
|
+
|
|
27
36
|
/* Cell */
|
|
28
37
|
--stuic-data-table-cell-padding-x: 0.75rem;
|
|
29
38
|
--stuic-data-table-cell-padding-y: 0.75rem;
|
|
@@ -152,6 +161,31 @@
|
|
|
152
161
|
);
|
|
153
162
|
}
|
|
154
163
|
|
|
164
|
+
/*
|
|
165
|
+
`rowActivatable` puts tabindex on the <tr>, and a focusable element with no
|
|
166
|
+
visible ring is worse than no tabindex at all. The negative offset keeps the
|
|
167
|
+
ring inside the row box — an outset ring on a <tr> is clipped unpredictably
|
|
168
|
+
by border-collapse.
|
|
169
|
+
*/
|
|
170
|
+
.stuic-data-table tbody tr:focus-visible {
|
|
171
|
+
outline: var(--stuic-data-table-row-ring-width) solid
|
|
172
|
+
var(--stuic-data-table-row-ring-color);
|
|
173
|
+
outline-offset: calc(var(--stuic-data-table-row-ring-width) * -1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* ============================================================================
|
|
177
|
+
ROW LINK (rowHref)
|
|
178
|
+
============================================================================ */
|
|
179
|
+
|
|
180
|
+
.stuic-data-table-row-link {
|
|
181
|
+
color: var(--stuic-data-table-row-link-color);
|
|
182
|
+
text-decoration: var(--stuic-data-table-row-link-decoration);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
.stuic-data-table-row-link:hover {
|
|
186
|
+
text-decoration: var(--stuic-data-table-row-link-decoration-hover);
|
|
187
|
+
}
|
|
188
|
+
|
|
155
189
|
/* ============================================================================
|
|
156
190
|
CELL
|
|
157
191
|
============================================================================ */
|
|
@@ -40,36 +40,36 @@ interface RegisterFieldConfig {
|
|
|
40
40
|
|
|
41
41
|
## RegisterForm — Props
|
|
42
42
|
|
|
43
|
-
| Prop | Type | Default | Description
|
|
44
|
-
| --------------------------- | --------------------------------------- | ---------- |
|
|
45
|
-
| `formData` | `RegisterFormData` | empty | Bindable form data.
|
|
46
|
-
| `onSubmit` | `(data: RegisterFormData) => void` | required | Called after client-side validation passes.
|
|
47
|
-
| `isSubmitting` | `boolean` | `false` | Disables the CTA during submission.
|
|
48
|
-
| `submitDisabled` | `boolean` | `false` | Consumer-owned submit block. Disables the CTA **and** blocks `onSubmit`.
|
|
49
|
-
| `errors` | `RegisterFormValidationError[]` | `[]` | Field-specific server errors (merged with internal validation). See [Server errors](#server-supplied-errors).
|
|
50
|
-
| `error` | `string` | - | General error rendered as a `DismissibleMessage` above the form.
|
|
51
|
-
| `showEmail` | `boolean` | `true` | Render the email field. `false` **unmounts** it and skips its validation.
|
|
52
|
-
| `showPassword` | `boolean` | `true` | Render the password field (and, transitively, the confirm field).
|
|
53
|
-
| `showPasswordConfirm` | `boolean` | `true` | Render the password-confirm field. Subordinate to `showPassword`.
|
|
54
|
-
| `passwordMinLength` | `number` | `8` | Minimum password length (fed into both the FieldInput attribute and the validator).
|
|
55
|
-
| `credentialsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Rendered at the credentials position (after the core fields, before bottom extra fields).
|
|
56
|
-
| `emailFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in email field.
|
|
57
|
-
| `passwordFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in password field.
|
|
58
|
-
| `passwordConfirmFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in confirm field.
|
|
59
|
-
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields. Rendered as `FieldInput`s positioned top or bottom.
|
|
60
|
-
| `extraFieldsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Escape hatch for non-FieldInput extras. Rendered after declarative bottom fields.
|
|
61
|
-
| `topFieldsSeparator` | `boolean` |
|
|
62
|
-
| `submitLabel` | `string` | i18n | Override the CTA label.
|
|
63
|
-
| `submittingLabel` | `string` | i18n | Override the CTA label while submitting.
|
|
64
|
-
| `submitButton` | `Snippet<[{ isSubmitting, disabled }]>` | - | Override the entire CTA section. `disabled` is `isSubmitting \|\| submitDisabled`.
|
|
65
|
-
| `socialLogins` | `Snippet` | - | Social/OAuth buttons. A divider is shown when set.
|
|
66
|
-
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | `"top"` renders the block above the credentials, with the divider **below** the buttons.
|
|
67
|
-
| `socialDividerLabel` | `string \| false` | i18n | Override (or hide with `false`) the divider. Defaults to `social_divider` ("or continue with") at the bottom, `social_divider_alt` ("or") at the top.
|
|
68
|
-
| `footer` | `Snippet` | - | Content below the form (e.g., "Already have an account? Log in").
|
|
69
|
-
| `notifications` | `NotificationsStack` | - | When set, general errors are also pushed via `notifications.error()`.
|
|
70
|
-
| `t` | `TranslateFn` | English | i18n function.
|
|
71
|
-
| `unstyled` / `class` | - | - | Standard styling escape hatches.
|
|
72
|
-
| `el` | `HTMLFormElement` | - | Bindable form element.
|
|
43
|
+
| Prop | Type | Default | Description |
|
|
44
|
+
| --------------------------- | --------------------------------------- | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
45
|
+
| `formData` | `RegisterFormData` | empty | Bindable form data. |
|
|
46
|
+
| `onSubmit` | `(data: RegisterFormData) => void` | required | Called after client-side validation passes. |
|
|
47
|
+
| `isSubmitting` | `boolean` | `false` | Disables the CTA during submission. |
|
|
48
|
+
| `submitDisabled` | `boolean` | `false` | Consumer-owned submit block. Disables the CTA **and** blocks `onSubmit`. |
|
|
49
|
+
| `errors` | `RegisterFormValidationError[]` | `[]` | Field-specific server errors (merged with internal validation). See [Server errors](#server-supplied-errors). |
|
|
50
|
+
| `error` | `string` | - | General error rendered as a `DismissibleMessage` above the form. |
|
|
51
|
+
| `showEmail` | `boolean` | `true` | Render the email field. `false` **unmounts** it and skips its validation. |
|
|
52
|
+
| `showPassword` | `boolean` | `true` | Render the password field (and, transitively, the confirm field). |
|
|
53
|
+
| `showPasswordConfirm` | `boolean` | `true` | Render the password-confirm field. Subordinate to `showPassword`. |
|
|
54
|
+
| `passwordMinLength` | `number` | `8` | Minimum password length (fed into both the FieldInput attribute and the validator). |
|
|
55
|
+
| `credentialsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Rendered at the credentials position (after the core fields, before bottom extra fields). |
|
|
56
|
+
| `emailFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in email field. |
|
|
57
|
+
| `passwordFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in password field. |
|
|
58
|
+
| `passwordConfirmFieldProps` | `Partial<FieldInputProps>` | - | Passthrough props for the built-in confirm field. |
|
|
59
|
+
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields. Rendered as `FieldInput`s positioned top or bottom. |
|
|
60
|
+
| `extraFieldsSlot` | `Snippet<[{ formData, fieldError }]>` | - | Escape hatch for non-FieldInput extras. Rendered after declarative bottom fields. |
|
|
61
|
+
| `topFieldsSeparator` | `boolean` | `true` | Section rule closing the top-position extra-field group. Drawn whenever at least one such field exists, whatever follows it. `false` opts out. |
|
|
62
|
+
| `submitLabel` | `string` | i18n | Override the CTA label. |
|
|
63
|
+
| `submittingLabel` | `string` | i18n | Override the CTA label while submitting. |
|
|
64
|
+
| `submitButton` | `Snippet<[{ isSubmitting, disabled }]>` | - | Override the entire CTA section. `disabled` is `isSubmitting \|\| submitDisabled`. |
|
|
65
|
+
| `socialLogins` | `Snippet` | - | Social/OAuth buttons. A divider is shown when set. |
|
|
66
|
+
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | `"top"` renders the block above the credentials, with the divider **below** the buttons. |
|
|
67
|
+
| `socialDividerLabel` | `string \| false` | i18n | Override (or hide with `false`) the divider. Defaults to `social_divider` ("or continue with") at the bottom, `social_divider_alt` ("or") at the top. |
|
|
68
|
+
| `footer` | `Snippet` | - | Content below the form (e.g., "Already have an account? Log in"). |
|
|
69
|
+
| `notifications` | `NotificationsStack` | - | When set, general errors are also pushed via `notifications.error()`. |
|
|
70
|
+
| `t` | `TranslateFn` | English | i18n function. |
|
|
71
|
+
| `unstyled` / `class` | - | - | Standard styling escape hatches. |
|
|
72
|
+
| `el` | `HTMLFormElement` | - | Bindable form element. |
|
|
73
73
|
|
|
74
74
|
### Imperative methods (via `bind:this`)
|
|
75
75
|
|
|
@@ -215,7 +215,7 @@ Once an external party has confirmed who the user is, the credential fields are
|
|
|
215
215
|
|
|
216
216
|
Call `form.focusField("tenant_id")` right after the provider confirms: the button the user clicked is about to unmount, and without an explicit move focus falls to `<body>` — a keyboard user's next Tab restarts at the top of the document.
|
|
217
217
|
|
|
218
|
-
Those top-position fields are closed off with a section rule
|
|
218
|
+
Those top-position fields are closed off with a section rule. At the form's ordinary field rhythm the last of them sits one field-gap above the first provider button and reads as a caption for it; the break says instead that the workspace id is settled and what follows is the account. The rule is not conditioned on what comes next — provider buttons, a `credentialsSlot`, or the plain credentials all get it — so it does not blink in and out as this flow moves from "pick a provider" to "signing up as jane@…", and a reader of the call site does not have to work out which combination of other props switched it on. `topFieldsSeparator={false}` opts out.
|
|
219
219
|
|
|
220
220
|
Other shapes the same three props cover:
|
|
221
221
|
|
|
@@ -310,7 +310,7 @@ The social block carries `data-position="top" \| "bottom"` (suppressed under `un
|
|
|
310
310
|
|
|
311
311
|
`credentialsSlot` content is wrapped in `.stuic-register-form-credentials` (suppressed under `unstyled`) so it inherits the same bottom rhythm the fields have — the form itself is a zero-gap flex column.
|
|
312
312
|
|
|
313
|
-
Top-position extra fields are wrapped in `.stuic-register-form-fields-top` (suppressed under `unstyled`), which carries `data-separator`
|
|
313
|
+
Top-position extra fields are wrapped in `.stuic-register-form-fields-top` (suppressed under `unstyled`), which carries `data-separator` unless `topFieldsSeparator={false}`. The wrapper is otherwise inert — the fields keep their own margins — so targeting `[data-separator]` is the way to restyle the break without touching the ungrouped case. The default padding stacks on top of the last field's own `margin-bottom`, which is why the two spacing tokens are not equal.
|
|
314
314
|
|
|
315
315
|
## Gotchas
|
|
316
316
|
|
|
@@ -128,17 +128,16 @@
|
|
|
128
128
|
* Close the top-position extra fields with a section break — a hairline
|
|
129
129
|
* rule plus extra space below the group.
|
|
130
130
|
*
|
|
131
|
-
*
|
|
132
|
-
* workspace id, an invite code
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
*
|
|
131
|
+
* A field is at the top position because it is about something other than
|
|
132
|
+
* the credentials (a workspace id, an invite code, an org name), so the
|
|
133
|
+
* group is drawn as its own section. Without the break the last of them
|
|
134
|
+
* sits at the ordinary field gap above whatever comes next and reads as a
|
|
135
|
+
* caption for it — a label over the first provider button, or just another
|
|
136
|
+
* row of the credential column.
|
|
137
137
|
*
|
|
138
|
-
* Default:
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* rule drawn through the middle of it.
|
|
138
|
+
* Default: true whenever there is at least one top-position field,
|
|
139
|
+
* regardless of what follows. Set `false` for a form whose top fields
|
|
140
|
+
* really do belong to the same column as the credentials.
|
|
142
141
|
*/
|
|
143
142
|
topFieldsSeparator?: boolean;
|
|
144
143
|
|
|
@@ -268,15 +267,15 @@
|
|
|
268
267
|
// validated against it).
|
|
269
268
|
let renderPasswordConfirm = $derived(showPassword && showPasswordConfirm);
|
|
270
269
|
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
);
|
|
270
|
+
// A field at the top position is there because it is about something other
|
|
271
|
+
// than the credentials — the workspace, the invite, the org — so it is closed
|
|
272
|
+
// off as its own section whichever way the sign-up continues below it.
|
|
273
|
+
// Deliberately NOT conditioned on what follows (provider buttons, a
|
|
274
|
+
// credentialsSlot, plain inputs): the last top field otherwise sits one field
|
|
275
|
+
// gap above the next thing and reads as a caption for it either way, and a
|
|
276
|
+
// rule that comes and goes with an unrelated prop is not explicable from the
|
|
277
|
+
// call site. `topFieldsSeparator={false}` opts out.
|
|
278
|
+
let renderTopFieldsSeparator = $derived(topFieldsSeparator ?? true);
|
|
280
279
|
|
|
281
280
|
// Internal validation errors (set on submit)
|
|
282
281
|
let internalErrors = $state<RegisterFormValidationError[]>([]);
|
|
@@ -101,17 +101,16 @@ export interface Props extends Omit<HTMLAttributes<HTMLFormElement>, "children">
|
|
|
101
101
|
* Close the top-position extra fields with a section break — a hairline
|
|
102
102
|
* rule plus extra space below the group.
|
|
103
103
|
*
|
|
104
|
-
*
|
|
105
|
-
* workspace id, an invite code
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
104
|
+
* A field is at the top position because it is about something other than
|
|
105
|
+
* the credentials (a workspace id, an invite code, an org name), so the
|
|
106
|
+
* group is drawn as its own section. Without the break the last of them
|
|
107
|
+
* sits at the ordinary field gap above whatever comes next and reads as a
|
|
108
|
+
* caption for it — a label over the first provider button, or just another
|
|
109
|
+
* row of the credential column.
|
|
110
110
|
*
|
|
111
|
-
* Default:
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
* rule drawn through the middle of it.
|
|
111
|
+
* Default: true whenever there is at least one top-position field,
|
|
112
|
+
* regardless of what follows. Set `false` for a form whose top fields
|
|
113
|
+
* really do belong to the same column as the credentials.
|
|
115
114
|
*/
|
|
116
115
|
topFieldsSeparator?: boolean;
|
|
117
116
|
/** Override CTA label */
|
|
@@ -51,7 +51,7 @@ export interface Props {
|
|
|
51
51
|
fieldError: (name: string) => string | undefined;
|
|
52
52
|
}
|
|
53
53
|
]>;
|
|
54
|
-
/** Section break below the top-position extra fields. Default:
|
|
54
|
+
/** Section break below the top-position extra fields. Default: true. */
|
|
55
55
|
topFieldsSeparator?: InnerProps["topFieldsSeparator"];
|
|
56
56
|
/** Override CTA label */
|
|
57
57
|
submitLabel?: string;
|
|
@@ -46,11 +46,10 @@
|
|
|
46
46
|
margin-bottom: var(--stuic-register-form-credentials-margin-bottom);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
/* Top-position extra fields.
|
|
50
|
-
|
|
51
|
-
the
|
|
52
|
-
|
|
53
|
-
choice of sign-up path, not another field. */
|
|
49
|
+
/* Top-position extra fields. `data-separator` is on unless the consumer opts
|
|
50
|
+
out, so the group reads as its own section rather than as the first rows of
|
|
51
|
+
the credential column; the wrapper is inert without it, leaving the fields
|
|
52
|
+
their own margins. */
|
|
54
53
|
.stuic-register-form-fields-top[data-separator] {
|
|
55
54
|
padding-bottom: var(--stuic-register-form-fields-top-padding-bottom);
|
|
56
55
|
margin-bottom: var(--stuic-register-form-fields-top-margin-bottom);
|
|
@@ -300,7 +300,7 @@ Prefix: `--stuic-login-form-*`
|
|
|
300
300
|
|
|
301
301
|
Standalone registration form. Mirrors `LoginForm` conventions: `formData`, `onSubmit`, validation, errors, i18n, notifications, social-logins. Adds declarative `extraFields` (top/bottom positioning, custom validators) and an `extraFieldsSlot` escape hatch (e.g., terms checkbox).
|
|
302
302
|
|
|
303
|
-
Also covers **identity-first signup** (identity established by an OAuth provider / invite / magic link before the account exists): `showEmail` / `showPassword` unmount the credential fields, `credentialsSlot` replaces them, `socialPosition="top"` moves the provider buttons above the credentials, and the top-position extra fields (workspace id, invite code — required on every path) are closed off with a section rule so they don't read as a caption for the first provider button (`topFieldsSeparator`,
|
|
303
|
+
Also covers **identity-first signup** (identity established by an OAuth provider / invite / magic link before the account exists): `showEmail` / `showPassword` unmount the credential fields, `credentialsSlot` replaces them, `socialPosition="top"` moves the provider buttons above the credentials, and the top-position extra fields (workspace id, invite code — required on every path) are closed off with a section rule so they don't read as a caption for the first provider button (`topFieldsSeparator`, on by default whenever such a field exists).
|
|
304
304
|
|
|
305
305
|
### Exports
|
|
306
306
|
|
|
@@ -317,29 +317,29 @@ Also covers **identity-first signup** (identity established by an OAuth provider
|
|
|
317
317
|
|
|
318
318
|
### Key Props
|
|
319
319
|
|
|
320
|
-
| Prop | Type | Default | Description
|
|
321
|
-
| ---------------------------------------------------------------------- | ------------------------------- | ---------- |
|
|
322
|
-
| `formData` | `RegisterFormData` | empty | Bindable form data
|
|
323
|
-
| `onSubmit` | `(data) => void` | required | Submit callback
|
|
324
|
-
| `isSubmitting` | `boolean` | `false` | Disables CTA
|
|
325
|
-
| `submitDisabled` | `boolean` | `false` | Consumer-owned block: disables CTA + blocks submit
|
|
326
|
-
| `errors` | `RegisterFormValidationError[]` | `[]` | Server field errors — self-clearing (see below)
|
|
327
|
-
| `error` | `string` | — | General error (alert above form)
|
|
328
|
-
| `showEmail` | `boolean` | `true` | Render (mount) the email field
|
|
329
|
-
| `showPassword` | `boolean` | `true` | Render (mount) the password + confirm fields
|
|
330
|
-
| `showPasswordConfirm` | `boolean` | `true` | Render password-confirm field
|
|
331
|
-
| `passwordMinLength` | `number` | `8` | Min password length (input + validator)
|
|
332
|
-
| `credentialsSlot` | `Snippet` | — | Content at the credentials position
|
|
333
|
-
| `emailFieldProps` / `passwordFieldProps` / `passwordConfirmFieldProps` | `Partial<FieldInputProps>` | — | Passthrough props per core field (`validate` composed, `value` ignored)
|
|
334
|
-
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields (top/bottom)
|
|
335
|
-
| `extraFieldsSlot` | `Snippet` | — | Escape-hatch for non-FieldInput extras
|
|
336
|
-
| `topFieldsSeparator` | `boolean` |
|
|
337
|
-
| `submitButton` | `Snippet` | — | Custom CTA section
|
|
338
|
-
| `socialLogins` | `Snippet` | — | OAuth buttons
|
|
339
|
-
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | Social block above the credentials or after the CTA
|
|
340
|
-
| `footer` | `Snippet` | — | Content below form
|
|
341
|
-
| `notifications` | `NotificationsStack` | — | Route errors to notifications
|
|
342
|
-
| `t` | `TranslateFn` | built-in | Translation function
|
|
320
|
+
| Prop | Type | Default | Description |
|
|
321
|
+
| ---------------------------------------------------------------------- | ------------------------------- | ---------- | ------------------------------------------------------------------------- |
|
|
322
|
+
| `formData` | `RegisterFormData` | empty | Bindable form data |
|
|
323
|
+
| `onSubmit` | `(data) => void` | required | Submit callback |
|
|
324
|
+
| `isSubmitting` | `boolean` | `false` | Disables CTA |
|
|
325
|
+
| `submitDisabled` | `boolean` | `false` | Consumer-owned block: disables CTA + blocks submit |
|
|
326
|
+
| `errors` | `RegisterFormValidationError[]` | `[]` | Server field errors — self-clearing (see below) |
|
|
327
|
+
| `error` | `string` | — | General error (alert above form) |
|
|
328
|
+
| `showEmail` | `boolean` | `true` | Render (mount) the email field |
|
|
329
|
+
| `showPassword` | `boolean` | `true` | Render (mount) the password + confirm fields |
|
|
330
|
+
| `showPasswordConfirm` | `boolean` | `true` | Render password-confirm field |
|
|
331
|
+
| `passwordMinLength` | `number` | `8` | Min password length (input + validator) |
|
|
332
|
+
| `credentialsSlot` | `Snippet` | — | Content at the credentials position |
|
|
333
|
+
| `emailFieldProps` / `passwordFieldProps` / `passwordConfirmFieldProps` | `Partial<FieldInputProps>` | — | Passthrough props per core field (`validate` composed, `value` ignored) |
|
|
334
|
+
| `extraFields` | `RegisterFieldConfig[]` | `[]` | Declarative extra fields (top/bottom) |
|
|
335
|
+
| `extraFieldsSlot` | `Snippet` | — | Escape-hatch for non-FieldInput extras |
|
|
336
|
+
| `topFieldsSeparator` | `boolean` | `true` | Section rule closing the top-position extra-field group; `false` opts out |
|
|
337
|
+
| `submitButton` | `Snippet` | — | Custom CTA section |
|
|
338
|
+
| `socialLogins` | `Snippet` | — | OAuth buttons |
|
|
339
|
+
| `socialPosition` | `"top" \| "bottom"` | `"bottom"` | Social block above the credentials or after the CTA |
|
|
340
|
+
| `footer` | `Snippet` | — | Content below form |
|
|
341
|
+
| `notifications` | `NotificationsStack` | — | Route errors to notifications |
|
|
342
|
+
| `t` | `TranslateFn` | built-in | Translation function |
|
|
343
343
|
|
|
344
344
|
**Imperative** (`bind:this`): `validate()`, `scrollToFirstError(opts?)`, `focusField(name)` — also forwarded by `RegisterFormModal`.
|
|
345
345
|
|