@tailor-platform/app-shell 1.17.0 → 1.18.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/app-shell-core.css +1 -1
- package/dist/app-shell.d.ts +228 -103
- package/dist/app-shell.js +4236 -3585
- package/dist/{appshell-CahhZy7v.js → appshell-Y1AYCuVJ.js} +10 -10
- package/dist/semantic-contrast.test.ts +73 -0
- package/dist/style-entry.d.ts +102 -102
- package/dist/testing.d.ts +102 -102
- package/dist/testing.js +1 -1
- package/dist/theme-bridge.test.ts +30 -7
- package/dist/theme.bridge.css +127 -40
- package/dist/theme.css +4 -3
- package/dist/themes/_template.css +5 -6
- package/dist/themes/bloom.css +0 -2
- package/dist/themes/cream.css +0 -2
- package/dist/themes/default.css +112 -77
- package/dist/vite-plugin.d.ts +102 -102
- package/package.json +2 -2
- package/skills/app-shell-patterns/SKILL.md +41 -32
- package/skills/app-shell-patterns/references/api/define-i18n-labels.md +123 -91
- package/skills/app-shell-patterns/references/api/define-module.md +2 -4
- package/skills/app-shell-patterns/references/components/data-table.md +332 -25
- package/skills/app-shell-patterns/references/components/sidebar-group.md +6 -6
- package/skills/app-shell-patterns/references/components/text.md +198 -0
- package/skills/app-shell-patterns/references/concepts/styling-theming.md +30 -14
- package/skills/app-shell-patterns/references/migrations.md +22 -1
- package/skills/app-shell-patterns/references/patterns/i18n-labels.md +139 -0
- package/skills/app-shell-patterns/references/patterns/list-dense-scan.md +3 -2
|
@@ -30,6 +30,12 @@ import {
|
|
|
30
30
|
type UseDataTableReturn,
|
|
31
31
|
type MetadataFieldOptions,
|
|
32
32
|
type DataTableContextValue,
|
|
33
|
+
type TextCellEditOptions,
|
|
34
|
+
type NumberCellEditOptions,
|
|
35
|
+
type BadgeCellEditOptions,
|
|
36
|
+
type DateCellEditOptions,
|
|
37
|
+
type CellEditState,
|
|
38
|
+
type DateTimeDraft,
|
|
33
39
|
} from "@tailor-platform/app-shell";
|
|
34
40
|
```
|
|
35
41
|
|
|
@@ -371,6 +377,305 @@ The trigger is a native `<button>`, so Enter/Space activation and the focus ring
|
|
|
371
377
|
|
|
372
378
|
**Known limitation — row count.** Detail rows are real `<tr>` elements, so a screen reader counts them: ten records with two expanded announces as twelve rows. Fixing this needs `aria-rowcount` plus explicit `aria-rowindex` on every row (with detail rows sharing their parent's index) and correct interaction with pagination; `role="presentation"` on the detail row would fix the count but remove the panel from screen-reader table navigation. Neither is implemented.
|
|
373
379
|
|
|
380
|
+
## Inline editing
|
|
381
|
+
|
|
382
|
+
Give a typed column an `edit` config to use its built-in editor. This standardizes the editing UI — draft input, IME, keyboard movement, and change notifications — not business validation or persistence. Columns without `edit` keep their existing behavior.
|
|
383
|
+
|
|
384
|
+
| Column type | Editor | `onValueChange` draft | `onValueCommitted` output |
|
|
385
|
+
| ------------------------------------ | ----------------------------------------- | ----------------------------------------------------------------- | -------------------------------------- |
|
|
386
|
+
| `text` / `link` | Direct text input | Raw `string`, including `""` | `string \| null` |
|
|
387
|
+
| `number` / `money` | Direct numeric input | Raw `string`, including incomplete input such as `"-"` | JavaScript `number \| null` |
|
|
388
|
+
| `text` / `link` with `edit.options` | Searchable dropdown | Selected string `value`, or `null`; not search text or highlights | Selected string `value`, or `null` |
|
|
389
|
+
| `badge` | Searchable dropdown using the same badges | Selected string `value`, or `null`; not search text or highlights | Selected string `value`, or `null` |
|
|
390
|
+
| `date` | Calendar | `"YYYY-MM-DD" \| null` | `"YYYY-MM-DD" \| null` |
|
|
391
|
+
| `date` with `dateFormat: "datetime"` | Calendar and time field | Complete `DateTimeDraft` snapshot | UTC ISO timestamp, or `null` for Clear |
|
|
392
|
+
|
|
393
|
+
Callback types describe the **editor's output**, not the original row field's runtime type. For example, text/link/badge editors confirm strings or `null` even when their display accepts numbers or booleans. The editing option types, `CellEditState`, and `DateTimeDraft` are exported from the package root; parsing and navigation helpers remain internal.
|
|
394
|
+
|
|
395
|
+
For example, inside a receipt page, accept or reject each proposed quantity yourself. Patch the changed field with a functional state update rather than replacing a whole row captured before another edit:
|
|
396
|
+
|
|
397
|
+
```tsx
|
|
398
|
+
const { column } = createColumnHelper<ReceiptLine>();
|
|
399
|
+
const [lines, setLines] = useState<ReceiptLine[]>(initialLines);
|
|
400
|
+
const toast = useToast();
|
|
401
|
+
|
|
402
|
+
const columns = [
|
|
403
|
+
column({ id: "sku", label: "SKU", type: "text" }),
|
|
404
|
+
column({
|
|
405
|
+
id: "received",
|
|
406
|
+
label: "Received",
|
|
407
|
+
type: "number",
|
|
408
|
+
typeOptions: { maxDecimals: 0 }, // Display precision, not an input rule.
|
|
409
|
+
edit: {
|
|
410
|
+
canEdit: (_row, { selected }) => selected,
|
|
411
|
+
onValueCommitted: (row, value) => {
|
|
412
|
+
if (value !== null && (!Number.isInteger(value) || value < 0 || value > row.ordered)) {
|
|
413
|
+
toast.error(`Enter a whole number from 0 to ${row.ordered}`);
|
|
414
|
+
return; // Do not adopt the value: the cell returns to data.
|
|
415
|
+
}
|
|
416
|
+
setLines((current) =>
|
|
417
|
+
current.map((line) => (line.id === row.id ? { ...line, received: value } : line)),
|
|
418
|
+
);
|
|
419
|
+
},
|
|
420
|
+
},
|
|
421
|
+
}),
|
|
422
|
+
];
|
|
423
|
+
|
|
424
|
+
const table = useDataTable({ columns, data: { rows: lines }, rowSelection: {} });
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
`ReceiptLine` and `initialLines` are the page's row type and initial data. `useState` comes from React; `useToast`, `createColumnHelper`, and `useDataTable` come from AppShell.
|
|
428
|
+
|
|
429
|
+
### Editing options
|
|
430
|
+
|
|
431
|
+
| Option | Column types | Contract |
|
|
432
|
+
| ------------------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
433
|
+
| `onValueChange` | all typed columns | Optional: `(row, draft) => void`. Observes changed editor drafts, including restored drafts on cancellation. See the draft types above. Updating `data` here is outside the supported coordination contract and is the consumer's responsibility. Return values are ignored. |
|
|
434
|
+
| `onValueCommitted` | all typed columns | Required: `(row, value) => void`. Notifies a changed, confirmed value, compared with current consumer-provided `data`, not an edit-start snapshot. Empty or explicitly cleared cells confirm `null`; equal values do not notify. Confirmation does not imply successful business validation, adoption, or persistence. Return values are ignored. |
|
|
435
|
+
| `canEdit` | all typed columns | `(row, { selected }) => boolean`. Decides which rows can be edited, like `rowExpansion.canExpand` and an action's `canApply`. Default: rows with an `id`. `selected` is `false` when selection is off; provide `rowSelection: {}` to enable it. |
|
|
436
|
+
| `options` | `text`, `link`, `badge` | `readonly { value, label }[]`. Text/link columns become searchable dropdowns; values are strings and their labels are displayed, including in read-only cells. Badge choices default to `typeOptions.badgeLabelMap`, then the column's enum filter options. Without choices, a badge column stays read-only. The "None" choice confirms `null`. |
|
|
437
|
+
|
|
438
|
+
Rows need a string or number `id`; rows without one remain read-only and produce a one-time warning. Multi-value badge cells also remain read-only. Editable cells carry a subtle tint at rest, layered over the row's hover and selected states, and drop it while being edited. Dropdown and date cells also show their editor icon on hover or focus; typed-into cells show none. The whole cell is the click target. Editing a cell does not fire `onClickRow`.
|
|
439
|
+
|
|
440
|
+
### Confirmation and keyboard behavior
|
|
441
|
+
|
|
442
|
+
- **Enter / Shift+Enter:** confirm and end editing, returning to normal cell display while keeping focus on the same logical cell. Enter or clicking that focused display cell starts editing again.
|
|
443
|
+
- **Tab / Shift+Tab:** confirm and move to the next / previous editable cell, skipping read-only cells. At the ends, focus leaves the table normally.
|
|
444
|
+
- **Esc:** restore the draft from the latest `data`, notifying `onValueChange` only if the draft changed, without `onValueCommitted`.
|
|
445
|
+
- **Dropdowns:** type to filter choices. Exact labels rank first, then prefix matches, then substring matches, with original order retained within each rank. "None" is excluded while a search query is present and available with an empty query. Tab focuses a closed dropdown without opening it; one Enter opens it without confirming. While open, Enter confirms the candidate and ends editing in the same cell; Tab confirms a selected or searched candidate and moves on. Tab without selecting or searching only moves focus. A keyboard-highlighted candidate takes precedence over ranked search matches; hovering does not count as an explicit selection. Clicking a choice confirms immediately. Only Enter, Tab, or clicking a choice confirms: outside click, blur, and Esc discard the search and restore the latest `data`, even for an exactly typed label or keyboard-highlighted candidate, without either callback. Search/highlight changes emit neither callback; confirming a different value emits `onValueChange` followed by `onValueCommitted`.
|
|
446
|
+
- **Dates:** selecting a day emits its draft and confirms immediately. Clear explicitly confirms `null`.
|
|
447
|
+
- **Date-times:** day and time are independent. Both must be valid to enable Done; an empty time does not become midnight. Done confirms the timestamp in the AppShell timezone. Outside closing or Esc cancels and restores the latest data; Clear explicitly confirms `null`.
|
|
448
|
+
- **Blur:** confirms a changed direct-input value. Enter during IME composition confirms the conversion, not the cell. Composition changes may notify raw drafts; normalization waits until cell confirmation.
|
|
449
|
+
|
|
450
|
+
A date-time draft preserves missing parts rather than inventing a timestamp:
|
|
451
|
+
|
|
452
|
+
```ts
|
|
453
|
+
type DateTimeDraft = {
|
|
454
|
+
day: string | null; // "YYYY-MM-DD", or an unselected day.
|
|
455
|
+
time: string | null; // "HH:mm", or an empty time field.
|
|
456
|
+
};
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
`onValueChange` receives the complete snapshot whenever either part changes, not just the changed part.
|
|
460
|
+
|
|
461
|
+
Numeric confirmation uses JavaScript `number`, **not arbitrary-precision Decimal**. Values outside the supported safe range are rejected, and high-precision decimal strings can lose precision when converted to numbers. Editing uses `.` as the decimal separator independently of display locale; `typeOptions.locale` does not select a locale-aware input parser. Full-width digits, valid comma thousands grouping, and currency symbols are normalized on confirmation; a decimal comma such as `1,5` is not accepted. The existing numeric renderer's `Number` conversion is unchanged.
|
|
462
|
+
|
|
463
|
+
Malformed numeric input is allowed while typing and can notify raw drafts. Enter/Tab cannot confirm it: they show basic parsing feedback and hold focus. Blur discards it, restores the latest `data`, and notifies the restored draft without a commit. Cosmetic changes such as `10` to `10.00` can notify draft changes but do not emit `onValueCommitted`. This is basic parsing, not a business-rule API; `typeOptions` controls display only and never limits the entered value.
|
|
464
|
+
|
|
465
|
+
### Data ownership and limits
|
|
466
|
+
|
|
467
|
+
The consumer's `data` is the source of truth. Normally, observe drafts in `onValueChange` and synchronously adopt confirmed values in `onValueCommitted`, optionally after your own validation or normalization. If you reject a confirmed value by leaving `data` unchanged, the cell returns to that value and Tab navigation remains available: the table does not retain a business-invalid draft, add a cell error, or block Tab navigation for you.
|
|
468
|
+
|
|
469
|
+
External data updates do not overwrite an active draft or emit draft callbacks themselves. Confirmation and cancellation use the latest data. With no active draft, the display follows data normally. Unmount silently discards internal drafts without either callback; if an ordinary blur occurs before unmount, its normal behavior still applies.
|
|
470
|
+
|
|
471
|
+
There are no `edit.min`, `max`, `maxDecimals`, `required`, `validate`, or `schema` options. Dropdowns and calendars are not constrained by business rules. Required-field handling, error feedback, saving, optimistic updates, rollback, and save indicators belong to the screen. Callback return values are not awaited; handle rejected saves in your own code rather than returning a Promise for DataTable to track. There is no built-in unsaved-changes or leave-page guard.
|
|
472
|
+
|
|
473
|
+
Editing does not re-run sorting or filtering: rows stay in place until the consumer supplies changed query data. Update only the changed field when persisting, and keep local rows current so later edits, row actions, and bulk actions see the intended data. Conflict resolution, consumer-state rollback, draft persistence across unmounts, and navigation guards remain outside DataTable's responsibility.
|
|
474
|
+
|
|
475
|
+
For a different editor or validation experience, leave `edit` unset and use the existing cell `render` escape hatch. A `render` supplied alongside `edit` customizes the display, not the built-in editor. Multi-value badge editing, a custom-editor option within `edit`, multi-cell paste, fill-down, and undo history are not provided.
|
|
476
|
+
|
|
477
|
+
### Consumer-owned saving patterns
|
|
478
|
+
|
|
479
|
+
DataTable has no save modes, shared saving hook, built-in Save controls, or validation framework. The following are page-level patterns. Both examples use `onValueCommitted`; neither treats observing `onValueChange` as adopting or saving a value.
|
|
480
|
+
|
|
481
|
+
Both examples use these imports:
|
|
482
|
+
|
|
483
|
+
```tsx
|
|
484
|
+
import { useRef, useState } from "react";
|
|
485
|
+
import { Button, DataTable, createColumnHelper, useDataTable } from "@tailor-platform/app-shell";
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
Their simulated persistence rejects quantity `13` so error handling can be tried without a backend. Replace it with your application's API.
|
|
489
|
+
|
|
490
|
+
#### Immediate saving
|
|
491
|
+
|
|
492
|
+
Validate the confirmed value, adopt it with a functional field update, then persist only that field. This example permits later edits while saves are pending. A per-row request revision prevents a stale failure from rolling back a newer edit; rollback also patches only this field, preserving changes to other fields. Feedback, saved-value tracking, and in-flight status are consumer-owned.
|
|
493
|
+
|
|
494
|
+
Revision checks protect local UI updates, not server write ordering or external conflicts. For a real API, serialize writes to the same field or use server-side version checks, and reconcile/refetch after uncertain outcomes. The [goods-receipt showcase](../../examples/vite-app/src/pages/showcase/data-table-lab/inline-editing-demo.tsx) demonstrates the same ownership boundary across editor types.
|
|
495
|
+
|
|
496
|
+
<example-preview name="immediate-saving"></example-preview>
|
|
497
|
+
|
|
498
|
+
```tsx
|
|
499
|
+
function ImmediateSaving() {
|
|
500
|
+
type Line = { id: string; item: string; ordered: number; received: number | null };
|
|
501
|
+
const initialRows: Line[] = [
|
|
502
|
+
{ id: "1", item: "Small carton", ordered: 24, received: 12 },
|
|
503
|
+
{ id: "2", item: "Large carton", ordered: 36, received: null },
|
|
504
|
+
];
|
|
505
|
+
const [rows, setRows] = useState(initialRows);
|
|
506
|
+
const [error, setError] = useState<string | null>(null);
|
|
507
|
+
const [savingCount, setSavingCount] = useState(0);
|
|
508
|
+
const saves = useRef(
|
|
509
|
+
new Map(
|
|
510
|
+
initialRows.map((row) => [
|
|
511
|
+
row.id,
|
|
512
|
+
{
|
|
513
|
+
revision: 0,
|
|
514
|
+
savedRevision: 0,
|
|
515
|
+
savedValue: row.received,
|
|
516
|
+
},
|
|
517
|
+
]),
|
|
518
|
+
),
|
|
519
|
+
);
|
|
520
|
+
const { column } = createColumnHelper<Line>();
|
|
521
|
+
const update = (id: string, received: number | null) =>
|
|
522
|
+
setRows((current) => current.map((row) => (row.id === id ? { ...row, received } : row)));
|
|
523
|
+
|
|
524
|
+
async function persistField(id: string, received: number | null) {
|
|
525
|
+
// Demo API: replace with a request that patches only this row's received field.
|
|
526
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
527
|
+
if (received === 13) throw new Error(`Could not save row ${id}; quantity 13 was rejected`);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const table = useDataTable({
|
|
531
|
+
data: { rows },
|
|
532
|
+
columns: [
|
|
533
|
+
column({ id: "item", label: "Item", type: "text" }),
|
|
534
|
+
column({ id: "ordered", label: "Ordered", type: "number" }),
|
|
535
|
+
column({
|
|
536
|
+
id: "received",
|
|
537
|
+
label: "Received",
|
|
538
|
+
type: "number",
|
|
539
|
+
edit: {
|
|
540
|
+
onValueCommitted: (row, value) => {
|
|
541
|
+
if (value !== null && (!Number.isInteger(value) || value < 0 || value > row.ordered)) {
|
|
542
|
+
setError(`Enter a whole number from 0 to ${row.ordered}, or clear the cell`);
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
const request = saves.current.get(row.id)!;
|
|
546
|
+
const revision = ++request.revision;
|
|
547
|
+
setError(null);
|
|
548
|
+
update(row.id, value);
|
|
549
|
+
setSavingCount((count) => count + 1);
|
|
550
|
+
void persistField(row.id, value)
|
|
551
|
+
.then(() => {
|
|
552
|
+
if (revision > request.savedRevision) {
|
|
553
|
+
request.savedRevision = revision;
|
|
554
|
+
request.savedValue = value;
|
|
555
|
+
}
|
|
556
|
+
})
|
|
557
|
+
.catch((cause: unknown) => {
|
|
558
|
+
if (revision !== request.revision) return;
|
|
559
|
+
update(row.id, request.savedValue);
|
|
560
|
+
setError(cause instanceof Error ? cause.message : "Save failed");
|
|
561
|
+
})
|
|
562
|
+
.finally(() => setSavingCount((count) => count - 1));
|
|
563
|
+
},
|
|
564
|
+
},
|
|
565
|
+
}),
|
|
566
|
+
],
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<div className="space-y-3">
|
|
571
|
+
<p role="status">
|
|
572
|
+
{savingCount ? `${savingCount} field save(s) pending` : "No saves pending"}
|
|
573
|
+
</p>
|
|
574
|
+
{error && <p role="alert">{error}</p>}
|
|
575
|
+
<DataTable.Root value={table}>
|
|
576
|
+
<DataTable.Table />
|
|
577
|
+
</DataTable.Root>
|
|
578
|
+
</div>
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
```
|
|
582
|
+
|
|
583
|
+
#### Collect then submit
|
|
584
|
+
|
|
585
|
+
Confirmed values update local draft rows. Save validates those rows and persists the batch; Cancel restores the last saved rows. The page disables editing while its batch is saving to avoid replacing a later local edit with an older save result.
|
|
586
|
+
|
|
587
|
+
This batch collects **confirmed/adopted row values**, not unfinished input inside editors. Save does not automatically gather or validate active editor drafts. An ordinary blur may confirm a valid direct input before a button click; malformed numeric blur discards that input, and clicking outside a dropdown or date-time picker cancels its unfinished edit. Tell users to confirm edits first; if your workflow needs to submit unfinished inputs as one coordinated form, use the `render` escape hatch or a dedicated form instead.
|
|
588
|
+
|
|
589
|
+
<example-preview name="collect-then-submit"></example-preview>
|
|
590
|
+
|
|
591
|
+
```tsx
|
|
592
|
+
function CollectThenSubmit() {
|
|
593
|
+
type Line = { id: string; item: string; ordered: number; received: number | null };
|
|
594
|
+
const initialRows: Line[] = [
|
|
595
|
+
{ id: "1", item: "Small carton", ordered: 24, received: 12 },
|
|
596
|
+
{ id: "2", item: "Large carton", ordered: 36, received: null },
|
|
597
|
+
];
|
|
598
|
+
const [savedRows, setSavedRows] = useState(initialRows);
|
|
599
|
+
const [rows, setRows] = useState(initialRows);
|
|
600
|
+
const [saving, setSaving] = useState(false);
|
|
601
|
+
const [error, setError] = useState<string | null>(null);
|
|
602
|
+
const { column } = createColumnHelper<Line>();
|
|
603
|
+
|
|
604
|
+
async function save() {
|
|
605
|
+
const invalid = rows.find(
|
|
606
|
+
(row) =>
|
|
607
|
+
row.received !== null &&
|
|
608
|
+
(!Number.isInteger(row.received) || row.received < 0 || row.received > row.ordered),
|
|
609
|
+
);
|
|
610
|
+
if (invalid) {
|
|
611
|
+
setError(`${invalid.item}: enter a whole number from 0 to ${invalid.ordered}, or clear it`);
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
// This snapshot contains adopted confirmations, not unfinished editor input.
|
|
615
|
+
const confirmedRows = rows;
|
|
616
|
+
setSaving(true);
|
|
617
|
+
setError(null);
|
|
618
|
+
try {
|
|
619
|
+
// Demo API: replace with a batch request for confirmedRows.
|
|
620
|
+
await new Promise((resolve) => setTimeout(resolve, 400));
|
|
621
|
+
if (confirmedRows.some((row) => row.received === 13)) {
|
|
622
|
+
throw new Error("Batch rejected quantity 13; correct it and try again");
|
|
623
|
+
}
|
|
624
|
+
setSavedRows(confirmedRows);
|
|
625
|
+
} catch (cause) {
|
|
626
|
+
setError(cause instanceof Error ? cause.message : "Save failed");
|
|
627
|
+
} finally {
|
|
628
|
+
setSaving(false);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
const table = useDataTable({
|
|
633
|
+
data: { rows },
|
|
634
|
+
columns: [
|
|
635
|
+
column({ id: "item", label: "Item", type: "text" }),
|
|
636
|
+
column({ id: "ordered", label: "Ordered", type: "number" }),
|
|
637
|
+
column({
|
|
638
|
+
id: "received",
|
|
639
|
+
label: "Received",
|
|
640
|
+
type: "number",
|
|
641
|
+
edit: {
|
|
642
|
+
canEdit: () => !saving,
|
|
643
|
+
onValueCommitted: (row, value) =>
|
|
644
|
+
setRows((current) =>
|
|
645
|
+
current.map((line) => (line.id === row.id ? { ...line, received: value } : line)),
|
|
646
|
+
),
|
|
647
|
+
},
|
|
648
|
+
}),
|
|
649
|
+
],
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
return (
|
|
653
|
+
<div className="space-y-3">
|
|
654
|
+
<p role="status">{saving ? "Saving batch…" : "Confirm edits, then Save or Cancel"}</p>
|
|
655
|
+
{error && <p role="alert">{error}</p>}
|
|
656
|
+
<DataTable.Root value={table}>
|
|
657
|
+
<DataTable.Table />
|
|
658
|
+
</DataTable.Root>
|
|
659
|
+
<div className="flex gap-2">
|
|
660
|
+
<Button disabled={saving} onClick={() => void save()}>
|
|
661
|
+
Save
|
|
662
|
+
</Button>
|
|
663
|
+
<Button
|
|
664
|
+
disabled={saving}
|
|
665
|
+
variant="outline"
|
|
666
|
+
onClick={() => {
|
|
667
|
+
setRows(savedRows);
|
|
668
|
+
setError(null);
|
|
669
|
+
}}
|
|
670
|
+
>
|
|
671
|
+
Cancel
|
|
672
|
+
</Button>
|
|
673
|
+
</div>
|
|
674
|
+
</div>
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
```
|
|
678
|
+
|
|
374
679
|
## Row selection
|
|
375
680
|
|
|
376
681
|
Pass `rowSelection` to `useDataTable` to enable the checkbox column. Rows must have a string or number `id`; selected ids are their string-normalized values, and rows without an id are excluded. An empty configuration (`rowSelection: {}`) enables uncontrolled selection without a notification callback. Bulk actions belong in `rowSelection.actions`; `rowSelection: { actions: [] }` still enables checkboxes, but does not show the bulk-action bar.
|
|
@@ -521,35 +826,35 @@ const table = useDataTable({
|
|
|
521
826
|
|
|
522
827
|
## `Column`
|
|
523
828
|
|
|
524
|
-
A column definition passed to `useDataTable`. `Column<TRow>` is a discriminated union on `type` — the
|
|
829
|
+
A column definition passed to `useDataTable`. `Column<TRow>` is a discriminated union on `type` — the shapes of `typeOptions`, `accessor`, and optional `edit` are narrowed per branch, so mismatches are compile errors rather than silent runtime no-ops. See [Inline editing](#inline-editing) for typed editors and their change-notification contract.
|
|
525
830
|
|
|
526
831
|
### Shared fields
|
|
527
832
|
|
|
528
|
-
| Property | Type | Description
|
|
529
|
-
| ---------- | ----------------------------------------- |
|
|
530
|
-
| `label` | `string` | Column header text. Omit for icon-only columns.
|
|
531
|
-
| `header` | `(ctx: HeaderRenderContext) => ReactNode` | Custom header renderer. Presentation only. When omitted, the built-in header renders `label` and owns the sort button/indicator. When provided, the return value replaces the built-in header entirely; sortable custom headers receive `sortDirection` and `activateSort()` via `ctx` and must render their own click surface for left-click sorting. The header context menu still uses the column metadata (`label`, `sort`, etc.).
|
|
532
|
-
| `render` | `(row: TRow) => ReactNode` | Renders the cell content. Presentation only. Optional — overrides the built-in `type` renderer when set
|
|
533
|
-
| `id` | `string` | Stable identifier for column visibility, persisted layout state, raw-value fallback (`row[col.id]`), and the React key. Falls back to `label` when omitted. Set this explicitly when `label` is absent, not unique, or when a custom-rendered column should still participate in built-in cell behaviors that need a raw value.
|
|
534
|
-
| `width` | `number` | Fixed column width in pixels. Optional.
|
|
535
|
-
| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via the toolbar's column-settings control). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection and expand columns auto-pin left and the row-actions column auto-pin right.
|
|
536
|
-
| `align` | `"left" \| "right"` | Horizontal alignment. Defaults to `"right"` for `type: "number"` and `type: "money"`; `"left"` otherwise. Pass `"left"` to opt a numeric column out.
|
|
537
|
-
| `truncate` | `boolean` | Truncate overflowing text with an ellipsis. Wires up an app-shell `<Tooltip>` automatically when the resolved raw cell value is a string or number (`accessor` first, then `row[col.id]`). With `inferColumns`, no explicit `accessor` is needed because `id` is pinned to the field name. Requires another column to anchor the row width (`width` on a neighbor, or a fixed-size column like selection / row actions).
|
|
538
|
-
| `accessor` | _(narrowed per `type`)_ | Extracts the raw value. This is the primary source of truth for built-in behaviors that need a value independent of presentation (typed rendering, truncate tooltip, cell copy/filter context-menu actions). The return type is narrowed per `type` branch — returning an array is a compile error on all typed columns except `badge`, and returning a plain object is a compile error on all typed columns. Untyped columns (`type` omitted) retain `unknown`. `null` and `undefined` are always allowed.
|
|
539
|
-
| `sort` | `SortConfig` | Sort configuration. When set, the column participates in the built-in sort behaviors: the default clickable header and the header context-menu sort submenu. Custom headers still need to call `ctx.activateSort()` to opt into left-click sorting.
|
|
540
|
-
| `filter` | `FilterConfig` | Filter configuration. When set, the column appears as an option in `DataTable.Filters` and can also drive the cell **Add filter** context-menu submenu when collection `control` is available. The cell menu narrows this to single-value operators only.
|
|
833
|
+
| Property | Type | Description |
|
|
834
|
+
| ---------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
835
|
+
| `label` | `string` | Column header text. Omit for icon-only columns. |
|
|
836
|
+
| `header` | `(ctx: HeaderRenderContext) => ReactNode` | Custom header renderer. Presentation only. When omitted, the built-in header renders `label` and owns the sort button/indicator. When provided, the return value replaces the built-in header entirely; sortable custom headers receive `sortDirection` and `activateSort()` via `ctx` and must render their own click surface for left-click sorting. The header context menu still uses the column metadata (`label`, `sort`, etc.). |
|
|
837
|
+
| `render` | `(row: TRow) => ReactNode` | Renders the cell content. Presentation only. Optional — overrides the built-in `type` display renderer when set, not an editor configured through `edit`. Leave `edit` unset to draw your own editor with `render`. Built-in behaviors that need the raw value (truncate tooltip, cell copy/filter context-menu actions) still resolve it from `accessor` first, then `row[col.id]`, and only fall back to the return value of `render(row)` when it is a primitive (`string`, `number`, `boolean`, or `bigint`). |
|
|
838
|
+
| `id` | `string` | Stable identifier for column visibility, persisted layout state, raw-value fallback (`row[col.id]`), and the React key. Falls back to `label` when omitted. Set this explicitly when `label` is absent, not unique, or when a custom-rendered column should still participate in built-in cell behaviors that need a raw value. |
|
|
839
|
+
| `width` | `number` | Fixed column width in pixels. Optional. |
|
|
840
|
+
| `pin` | `"left" \| "right"` | Freezes the column to that edge so it stays visible during horizontal scroll (the default; the user can override it via the toolbar's column-settings control). Sticky offsets are measured from the rendered layout, so `width` isn't required — but setting `width` on pinned columns is recommended for stable sizing. The selection and expand columns auto-pin left and the row-actions column auto-pin right. |
|
|
841
|
+
| `align` | `"left" \| "right"` | Horizontal alignment. Defaults to `"right"` for `type: "number"` and `type: "money"`; `"left"` otherwise. Pass `"left"` to opt a numeric column out. |
|
|
842
|
+
| `truncate` | `boolean` | Truncate overflowing text with an ellipsis. Wires up an app-shell `<Tooltip>` automatically when the resolved raw cell value is a string or number (`accessor` first, then `row[col.id]`). With `inferColumns`, no explicit `accessor` is needed because `id` is pinned to the field name. Requires another column to anchor the row width (`width` on a neighbor, or a fixed-size column like selection / row actions). |
|
|
843
|
+
| `accessor` | _(narrowed per `type`)_ | Extracts the raw value. This is the primary source of truth for built-in behaviors that need a value independent of presentation (typed rendering, truncate tooltip, cell copy/filter context-menu actions). The return type is narrowed per `type` branch — returning an array is a compile error on all typed columns except `badge`, and returning a plain object is a compile error on all typed columns. Untyped columns (`type` omitted) retain `unknown`. `null` and `undefined` are always allowed. |
|
|
844
|
+
| `sort` | `SortConfig` | Sort configuration. When set, the column participates in the built-in sort behaviors: the default clickable header and the header context-menu sort submenu. Custom headers still need to call `ctx.activateSort()` to opt into left-click sorting. |
|
|
845
|
+
| `filter` | `FilterConfig` | Filter configuration. When set, the column appears as an option in `DataTable.Filters` and can also drive the cell **Add filter** context-menu submenu when collection `control` is available. The cell menu narrows this to single-value operators only. |
|
|
541
846
|
|
|
542
847
|
### `type`-specific fields
|
|
543
848
|
|
|
544
|
-
| `type` | `typeOptions` |
|
|
545
|
-
| ----------- | ------------------------------------------------------------ |
|
|
546
|
-
| _(omitted)_ | _(not allowed; provide `render` to draw the cell)_ |
|
|
547
|
-
| `"text"` | _(not allowed)_ |
|
|
548
|
-
| `"number"` | `NumberCellOptions` |
|
|
549
|
-
| `"money"` | `MoneyCellOptions<TRow>` |
|
|
550
|
-
| `"date"` | `DateCellOptions` |
|
|
551
|
-
| `"badge"` | `BadgeCellOptions` |
|
|
552
|
-
| `"link"` | **Required** — `LinkCellOptions<TRow>` (must include `href`) |
|
|
849
|
+
| `type` | `typeOptions` | Optional `edit` |
|
|
850
|
+
| ----------- | ------------------------------------------------------------ | ----------------------------- |
|
|
851
|
+
| _(omitted)_ | _(not allowed; provide `render` to draw the cell)_ | _(not allowed)_ |
|
|
852
|
+
| `"text"` | _(not allowed)_ | `TextCellEditOptions<TRow>` |
|
|
853
|
+
| `"number"` | `NumberCellOptions` | `NumberCellEditOptions<TRow>` |
|
|
854
|
+
| `"money"` | `MoneyCellOptions<TRow>` | `NumberCellEditOptions<TRow>` |
|
|
855
|
+
| `"date"` | `DateCellOptions` | `DateCellEditOptions<TRow>` |
|
|
856
|
+
| `"badge"` | `BadgeCellOptions` | `BadgeCellEditOptions<TRow>` |
|
|
857
|
+
| `"link"` | **Required** — `LinkCellOptions<TRow>` (must include `href`) | `TextCellEditOptions<TRow>` |
|
|
553
858
|
|
|
554
859
|
## Cell types
|
|
555
860
|
|
|
@@ -1017,10 +1322,12 @@ const { variables, control } = useCollectionVariables({
|
|
|
1017
1322
|
});
|
|
1018
1323
|
|
|
1019
1324
|
// variables.pagination → { first, after? } or { last, before? }
|
|
1020
|
-
// variables.query → filter input object
|
|
1325
|
+
// variables.query → filter input object (undefined when no filters are active)
|
|
1021
1326
|
// variables.order → sort input array or undefined
|
|
1022
1327
|
```
|
|
1023
1328
|
|
|
1329
|
+
When you need an object for `Object.keys` or destructuring, use `variables.query ?? {}`.
|
|
1330
|
+
|
|
1024
1331
|
### Options
|
|
1025
1332
|
|
|
1026
1333
|
| Option | Type | Description |
|
|
@@ -1052,7 +1359,7 @@ const { variables, control } = useCollectionVariables({
|
|
|
1052
1359
|
params: { pageSize: 20 },
|
|
1053
1360
|
});
|
|
1054
1361
|
|
|
1055
|
-
// variables.query is now BuildQueryVariables<typeof tableMetadata.order>
|
|
1362
|
+
// variables.query is now BuildQueryVariables<typeof tableMetadata.order> | undefined
|
|
1056
1363
|
// variables.order is now { field: OrderableFieldName; direction: "Asc" | "Desc" }[]
|
|
1057
1364
|
const [result] = useQuery({
|
|
1058
1365
|
query: LIST_ORDERS, // TypedDocumentNode — variables are fully type-checked
|
|
@@ -124,13 +124,13 @@ Titles support i18n with `defineI18nLabels`:
|
|
|
124
124
|
import { defineI18nLabels } from "@tailor-platform/app-shell";
|
|
125
125
|
|
|
126
126
|
const labels = defineI18nLabels({
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
127
|
+
en: {
|
|
128
|
+
products: "Products",
|
|
129
|
+
settings: "Settings",
|
|
130
130
|
},
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
ja: {
|
|
132
|
+
products: "商品",
|
|
133
|
+
settings: "設定",
|
|
134
134
|
},
|
|
135
135
|
});
|
|
136
136
|
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Text
|
|
3
|
+
description: Text with a typography role, rendered as an element you choose
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
<!-- GENERATED by docs-kit from docs-src/components/text.docs.outline.md — do not edit by hand. -->
|
|
7
|
+
|
|
8
|
+
# Text
|
|
9
|
+
|
|
10
|
+
The `Text` component applies a [typography role](../concepts/styling-theming.md#typography) to its content. A role sets font size, line height, font weight, and letter spacing together. The role does not set the color, and it does not choose the HTML element. Use the `render` prop to choose the element (Base UI render pattern).
|
|
11
|
+
|
|
12
|
+
## Import
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { Text } from "@tailor-platform/app-shell";
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Basic Usage
|
|
19
|
+
|
|
20
|
+
<example-preview name="basic-usage"></example-preview>
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
function BasicUsage() {
|
|
24
|
+
return (
|
|
25
|
+
<div className="flex flex-col gap-1">
|
|
26
|
+
<Text variant="heading-md">Purchase orders</Text>
|
|
27
|
+
<Text className="text-muted-foreground">Orders sent to suppliers this month.</Text>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Props
|
|
34
|
+
|
|
35
|
+
_Generated from the type surface — first-party props only._
|
|
36
|
+
|
|
37
|
+
### TextProps
|
|
38
|
+
|
|
39
|
+
| Prop | Type | Default | Description |
|
|
40
|
+
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------ |
|
|
41
|
+
| `ref?` | `React.Ref<HTMLElement>` | - | Ref to the rendered element. |
|
|
42
|
+
| `render?` | `React.ReactElement` | - | Custom element to render as (Base UI render prop). Use it to set the heading level, for example `render={<h3 />}`. |
|
|
43
|
+
| `variant?` | `"heading-lg" \| "heading-md" \| "heading-sm" \| "body-md" \| "body-sm" \| "label-md" \| "label-sm" \| "code-sm" \| "body-md-relaxed" \| "body-sm-relaxed" \| null` | `"body-md"` | Typography role. Sets font size, line height, font weight, and letter spacing together. |
|
|
44
|
+
|
|
45
|
+
## Exports
|
|
46
|
+
|
|
47
|
+
- `Text` — function
|
|
48
|
+
- `TextProps` — type
|
|
49
|
+
|
|
50
|
+
All standard HTML attributes are also accepted. Put an attribute that belongs to one element, such as `htmlFor` on a `label`, on the `render` element:
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
<Text variant="label-md" render={<label htmlFor="quantity" />}>
|
|
54
|
+
Quantity
|
|
55
|
+
</Text>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Some lint rules report this example; see [Lint](#lint).
|
|
59
|
+
|
|
60
|
+
## Roles
|
|
61
|
+
|
|
62
|
+
`variant` takes one of the ten roles. Each role renders a default element when you do not pass `render`:
|
|
63
|
+
|
|
64
|
+
| `variant` | Default element | Use |
|
|
65
|
+
| ----------------- | --------------- | ----------------------------------------------- |
|
|
66
|
+
| `heading-lg` | `h2` | Page title (`Layout.Header`) |
|
|
67
|
+
| `heading-md` | `h2` | Section heading, card title |
|
|
68
|
+
| `heading-sm` | `h2` | Sub-section heading |
|
|
69
|
+
| `body-md` | `p` | Body copy, descriptions (default role) |
|
|
70
|
+
| `body-sm` | `p` | Caption, timestamp, metadata |
|
|
71
|
+
| `body-md-relaxed` | `p` | Long text, such as a chat message or a document |
|
|
72
|
+
| `body-sm-relaxed` | `p` | Long text at the small size |
|
|
73
|
+
| `label-md` | `span` | Field label, short UI text, figures in a table |
|
|
74
|
+
| `label-sm` | `span` | Small label, badge text |
|
|
75
|
+
| `code-sm` | `span` | ID and code. `Text` adds `font-mono` for you |
|
|
76
|
+
|
|
77
|
+
<example-preview name="roles"></example-preview>
|
|
78
|
+
|
|
79
|
+
```tsx
|
|
80
|
+
function Roles() {
|
|
81
|
+
return (
|
|
82
|
+
<div className="flex flex-col gap-3">
|
|
83
|
+
{/* The headings render a div so that this specimen is not a document heading. */}
|
|
84
|
+
<Text variant="heading-lg" render={<div />}>
|
|
85
|
+
heading-lg: Purchase orders
|
|
86
|
+
</Text>
|
|
87
|
+
<Text variant="heading-md" render={<div />}>
|
|
88
|
+
heading-md: Line items
|
|
89
|
+
</Text>
|
|
90
|
+
<Text variant="heading-sm" render={<div />}>
|
|
91
|
+
heading-sm: Shipping address
|
|
92
|
+
</Text>
|
|
93
|
+
<Text variant="body-md">body-md: Orders sent to suppliers this month.</Text>
|
|
94
|
+
<Text variant="body-sm">body-sm: Updated 2 hours ago</Text>
|
|
95
|
+
<Text variant="label-md">label-md: Total amount</Text>
|
|
96
|
+
<Text variant="label-sm">label-sm: Draft</Text>
|
|
97
|
+
<Text variant="code-sm">code-sm: PO-2026-0001</Text>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The sizes and line heights are in the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
|
|
104
|
+
|
|
105
|
+
## Heading Level
|
|
106
|
+
|
|
107
|
+
All three heading roles render `h2` by default. The heading level comes from the document structure, not from the visual size, so set it with `render`:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
<Text variant="heading-md" render={<h3 />}>
|
|
111
|
+
Line items
|
|
112
|
+
</Text>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
To use a heading role for text that is not a heading, render a `div` or a `span`:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
<Text variant="heading-md" render={<div />}>
|
|
119
|
+
1,240 orders
|
|
120
|
+
</Text>
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Lint
|
|
124
|
+
|
|
125
|
+
Some `jsx-a11y` rules check that an element has text inside it, for example `heading-has-content` for `<h3 />` and `label-has-associated-control` for `<label />`. These rules report the element in `render` as empty, because they do not see that `Text` passes its children to that element. If your app enables them, disable the rule on that line. ESLint and oxlint both read this comment:
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
// eslint-disable-next-line jsx-a11y/heading-has-content -- Text renders the children
|
|
129
|
+
<Text variant="heading-md" render={<h3 />}>
|
|
130
|
+
Line items
|
|
131
|
+
</Text>
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Color and Modifiers
|
|
135
|
+
|
|
136
|
+
Set the color with a color token in `className`. Four modifiers may also be combined with a role: `leading-none` for single-line text, `tabular-nums`, `font-semibold` for emphasis, and `uppercase`.
|
|
137
|
+
|
|
138
|
+
<example-preview name="color-and-modifiers"></example-preview>
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
function ColorAndModifiers() {
|
|
142
|
+
return (
|
|
143
|
+
<div className="flex flex-col gap-3">
|
|
144
|
+
<Text variant="body-md" className="text-muted-foreground">
|
|
145
|
+
Orders sent this month.
|
|
146
|
+
</Text>
|
|
147
|
+
<Text variant="label-md" className="tabular-nums">
|
|
148
|
+
1,240.00
|
|
149
|
+
</Text>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
<Text variant="body-md" className="text-muted-foreground">
|
|
157
|
+
Orders sent this month.
|
|
158
|
+
</Text>
|
|
159
|
+
<Text variant="heading-md" render={<h3 />} className="leading-none">
|
|
160
|
+
Card title
|
|
161
|
+
</Text>
|
|
162
|
+
<Text variant="label-md" className="tabular-nums">
|
|
163
|
+
1,240.00
|
|
164
|
+
</Text>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
Do not combine a role with other `leading-*`, `tracking-*`, or `text-*` size values. Pick a different `variant` instead. A `text-*` size class such as `text-sm` has no effect on `Text`: AppShell's stylesheet loads after yours, so the role's size wins.
|
|
168
|
+
|
|
169
|
+
## Using Roles Without Text
|
|
170
|
+
|
|
171
|
+
Each role is also a plain utility, for example `text-heading-md`. Use the utility when you style your own element and do not need `Text`:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
<th className="text-label-md text-muted-foreground">Amount</th>
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## TypeScript
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { type TextProps } from "@tailor-platform/app-shell";
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Accessibility
|
|
184
|
+
|
|
185
|
+
- A heading role renders a real heading (`h2`) by default, so screen readers list it with the other headings. Set the level with `render` so the levels follow the page structure.
|
|
186
|
+
- Do not choose a heading level for its size. `Text` keeps the size and the level separate.
|
|
187
|
+
- `body-*` roles render `p`. Inside another `p`, render a `span` instead (`<Text render={<span />}>`): the browser closes the outer `p` before the inner one, so React warns and server rendering fails to hydrate. Inside inline elements such as `button`, `label` or `span`, a `p` is invalid HTML too, so render a `span` there as well.
|
|
188
|
+
|
|
189
|
+
## Palettes
|
|
190
|
+
|
|
191
|
+
The `cream` and `bloom` palettes set the line height and letter spacing of `h1` to `h6` elements. Under these palettes, a heading role on a heading element uses the palette values (line height 1.2, letter spacing `-0.03em`), including when you add `leading-none`. The same role on a `div` or `span` uses the role values. See the [Typography section of the styling guide](../concepts/styling-theming.md#typography).
|
|
192
|
+
|
|
193
|
+
## Related Components
|
|
194
|
+
|
|
195
|
+
- [Card](./card.md) - The table above lists `heading-md` for card titles
|
|
196
|
+
- [Layout](./layout.md) - The table above lists `heading-lg` for the page title
|
|
197
|
+
|
|
198
|
+
AppShell's own components, including these two, still use the stock utilities. They will move to the roles in a later change.
|