@entifix/react-controls 0.19.0 → 0.20.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/README.md +8 -0
- package/dist/i18n/catalog/en.d.ts.map +1 -1
- package/dist/i18n/catalog/en.js +55 -9
- package/dist/i18n/catalog/en.js.map +1 -1
- package/dist/i18n/catalog/es.d.ts +64 -5
- package/dist/i18n/catalog/es.d.ts.map +1 -1
- package/dist/i18n/catalog/es.js +61 -9
- package/dist/i18n/catalog/es.js.map +1 -1
- package/dist/i18n/catalog/index.d.ts +102 -10
- package/dist/i18n/catalog/index.d.ts.map +1 -1
- package/dist/i18n/entity-labels.d.ts +14 -0
- package/dist/i18n/entity-labels.d.ts.map +1 -1
- package/dist/i18n/entity-labels.js +15 -0
- package/dist/i18n/entity-labels.js.map +1 -1
- package/dist/i18n/i18n-context.js +2 -2
- package/dist/i18n/i18n-context.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/primitives.d.ts +0 -1
- package/dist/primitives.d.ts.map +1 -1
- package/dist/primitives.js +0 -1
- package/dist/primitives.js.map +1 -1
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.d.ts.map +1 -1
- package/dist/theme/index.js +1 -0
- package/dist/theme/index.js.map +1 -1
- package/dist/theme/theme-context.d.ts +15 -3
- package/dist/theme/theme-context.d.ts.map +1 -1
- package/dist/theme/theme-context.js +49 -23
- package/dist/theme/theme-context.js.map +1 -1
- package/dist/theme/theme-cookie.d.ts +9 -0
- package/dist/theme/theme-cookie.d.ts.map +1 -0
- package/dist/theme/theme-cookie.js +8 -0
- package/dist/theme/theme-cookie.js.map +1 -0
- package/dist/theme/theme-script.d.ts +8 -6
- package/dist/theme/theme-script.d.ts.map +1 -1
- package/dist/theme/theme-script.js +21 -12
- package/dist/theme/theme-script.js.map +1 -1
- package/dist/ui/actions/action-surfaces.d.ts +1 -1
- package/dist/ui/actions/action-surfaces.js +1 -1
- package/dist/ui/actions/action-surfaces.js.map +1 -1
- package/dist/ui/actions/error-actions.d.ts +32 -0
- package/dist/ui/actions/error-actions.d.ts.map +1 -0
- package/dist/ui/actions/error-actions.js +40 -0
- package/dist/ui/actions/error-actions.js.map +1 -0
- package/dist/ui/actions/index.d.ts +2 -0
- package/dist/ui/actions/index.d.ts.map +1 -1
- package/dist/ui/actions/index.js +2 -0
- package/dist/ui/actions/index.js.map +1 -1
- package/dist/ui/actions/shortcut.d.ts +13 -0
- package/dist/ui/actions/shortcut.d.ts.map +1 -0
- package/dist/ui/actions/shortcut.js +18 -0
- package/dist/ui/actions/shortcut.js.map +1 -0
- package/dist/ui/actions/use-verb-invoker.d.ts +50 -0
- package/dist/ui/actions/use-verb-invoker.d.ts.map +1 -0
- package/dist/ui/actions/use-verb-invoker.js +93 -0
- package/dist/ui/actions/use-verb-invoker.js.map +1 -0
- package/dist/ui/atoms/cell-value/cell-value.d.ts.map +1 -1
- package/dist/ui/atoms/cell-value/cell-value.js +28 -14
- package/dist/ui/atoms/cell-value/cell-value.js.map +1 -1
- package/dist/ui/atoms/field-control/field-control.d.ts +1 -0
- package/dist/ui/atoms/field-control/field-control.d.ts.map +1 -1
- package/dist/ui/atoms/field-control/field-control.js +2 -0
- package/dist/ui/atoms/field-control/field-control.js.map +1 -1
- package/dist/ui/atoms/number-input/quantity-input.d.ts +8 -1
- package/dist/ui/atoms/number-input/quantity-input.d.ts.map +1 -1
- package/dist/ui/atoms/number-input/quantity-input.js +2 -1
- package/dist/ui/atoms/number-input/quantity-input.js.map +1 -1
- package/dist/ui/atoms/table/table.d.ts +12 -3
- package/dist/ui/atoms/table/table.d.ts.map +1 -1
- package/dist/ui/atoms/table/table.js +59 -10
- package/dist/ui/atoms/table/table.js.map +1 -1
- package/dist/ui/atoms/text/text.d.ts +2 -2
- package/dist/ui/atoms/text/text.d.ts.map +1 -1
- package/dist/ui/atoms/text/text.js.map +1 -1
- package/dist/ui/molecules/action-bar/action-bar.d.ts +76 -0
- package/dist/ui/molecules/action-bar/action-bar.d.ts.map +1 -0
- package/dist/ui/molecules/action-bar/action-bar.js +218 -0
- package/dist/ui/molecules/action-bar/action-bar.js.map +1 -0
- package/dist/ui/molecules/action-bar/index.d.ts +2 -0
- package/dist/ui/molecules/action-bar/index.d.ts.map +1 -0
- package/dist/ui/molecules/action-bar/index.js +3 -0
- package/dist/ui/molecules/action-bar/index.js.map +1 -0
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.d.ts.map +1 -1
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js +8 -10
- package/dist/ui/molecules/bulk-action-bar/bulk-action-bar.js.map +1 -1
- package/dist/ui/molecules/bulk-result/bulk-result.d.ts +3 -3
- package/dist/ui/molecules/bulk-result/bulk-result.js +4 -4
- package/dist/ui/molecules/bulk-result/bulk-result.js.map +1 -1
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts +24 -9
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.d.ts.map +1 -1
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.js +36 -11
- package/dist/ui/molecules/confirm-dialog/confirm-dialog.js.map +1 -1
- package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts +33 -20
- package/dist/ui/molecules/entity-record-card/entity-record-card.d.ts.map +1 -1
- package/dist/ui/molecules/entity-record-card/entity-record-card.js +53 -20
- package/dist/ui/molecules/entity-record-card/entity-record-card.js.map +1 -1
- package/dist/ui/molecules/filter-builder/filter-builder.d.ts +8 -2
- package/dist/ui/molecules/filter-builder/filter-builder.d.ts.map +1 -1
- package/dist/ui/molecules/filter-builder/filter-builder.js +14 -5
- package/dist/ui/molecules/filter-builder/filter-builder.js.map +1 -1
- package/dist/ui/molecules/filter-chips/filter-chips.d.ts +16 -0
- package/dist/ui/molecules/filter-chips/filter-chips.d.ts.map +1 -0
- package/dist/ui/molecules/filter-chips/filter-chips.js +73 -0
- package/dist/ui/molecules/filter-chips/filter-chips.js.map +1 -0
- package/dist/ui/molecules/filter-chips/index.d.ts +2 -0
- package/dist/ui/molecules/filter-chips/index.d.ts.map +1 -0
- package/dist/ui/molecules/filter-chips/index.js +3 -0
- package/dist/ui/molecules/filter-chips/index.js.map +1 -0
- package/dist/ui/molecules/menu/menu.d.ts +11 -1
- package/dist/ui/molecules/menu/menu.d.ts.map +1 -1
- package/dist/ui/molecules/menu/menu.js +62 -2
- package/dist/ui/molecules/menu/menu.js.map +1 -1
- package/dist/ui/molecules/pagination/pagination.d.ts +5 -0
- package/dist/ui/molecules/pagination/pagination.d.ts.map +1 -1
- package/dist/ui/molecules/pagination/pagination.js +35 -18
- package/dist/ui/molecules/pagination/pagination.js.map +1 -1
- package/dist/ui/molecules/region-state/index.d.ts +2 -0
- package/dist/ui/molecules/region-state/index.d.ts.map +1 -0
- package/dist/ui/molecules/region-state/index.js +3 -0
- package/dist/ui/molecules/region-state/index.js.map +1 -0
- package/dist/ui/molecules/region-state/region-state.d.ts +42 -0
- package/dist/ui/molecules/region-state/region-state.d.ts.map +1 -0
- package/dist/ui/molecules/region-state/region-state.js +83 -0
- package/dist/ui/molecules/region-state/region-state.js.map +1 -0
- package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts +3 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.d.ts.map +1 -1
- package/dist/ui/molecules/table-toolbar/table-toolbar.js +3 -2
- package/dist/ui/molecules/table-toolbar/table-toolbar.js.map +1 -1
- package/dist/ui/molecules/toast-region/toast-region.d.ts +63 -19
- package/dist/ui/molecules/toast-region/toast-region.d.ts.map +1 -1
- package/dist/ui/molecules/toast-region/toast-region.js +129 -42
- package/dist/ui/molecules/toast-region/toast-region.js.map +1 -1
- package/dist/ui/organisms/command-palette/command-palette.d.ts.map +1 -1
- package/dist/ui/organisms/command-palette/command-palette.js +19 -12
- package/dist/ui/organisms/command-palette/command-palette.js.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.d.ts.map +1 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js +0 -1
- package/dist/ui/organisms/entity-detail-grid/entity-detail-grid.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.d.ts +1 -1
- package/dist/ui/organisms/entity-form/entity-form.d.ts.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.js +322 -214
- package/dist/ui/organisms/entity-form/entity-form.js.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts +8 -0
- package/dist/ui/organisms/entity-form/entity-form.types.d.ts.map +1 -1
- package/dist/ui/organisms/entity-form/entity-form.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.d.ts +1 -1
- package/dist/ui/organisms/entity-table/entity-table.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.js +332 -197
- package/dist/ui/organisms/entity-table/entity-table.js.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.types.d.ts +19 -4
- package/dist/ui/organisms/entity-table/entity-table.types.d.ts.map +1 -1
- package/dist/ui/organisms/entity-table/entity-table.types.js.map +1 -1
- package/dist/ui/organisms/entity-table/header-sort.d.ts +18 -0
- package/dist/ui/organisms/entity-table/header-sort.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/header-sort.js +54 -0
- package/dist/ui/organisms/entity-table/header-sort.js.map +1 -0
- package/dist/ui/organisms/entity-table/pivot.d.ts +25 -0
- package/dist/ui/organisms/entity-table/pivot.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/pivot.js +106 -0
- package/dist/ui/organisms/entity-table/pivot.js.map +1 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.d.ts +13 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.d.ts.map +1 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.js +31 -0
- package/dist/ui/organisms/entity-table/use-result-announcement.js.map +1 -0
- package/dist/ui/organisms/top-bar/live-indicator.d.ts +11 -0
- package/dist/ui/organisms/top-bar/live-indicator.d.ts.map +1 -0
- package/dist/ui/organisms/top-bar/live-indicator.js +46 -0
- package/dist/ui/organisms/top-bar/live-indicator.js.map +1 -0
- package/dist/ui/organisms/top-bar/top-bar.d.ts +11 -2
- package/dist/ui/organisms/top-bar/top-bar.d.ts.map +1 -1
- package/dist/ui/organisms/top-bar/top-bar.js +14 -5
- package/dist/ui/organisms/top-bar/top-bar.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts +1 -1
- package/dist/ui/organisms/wizard/wizard.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.js +7 -1
- package/dist/ui/organisms/wizard/wizard.js.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.d.ts +8 -0
- package/dist/ui/organisms/wizard/wizard.types.d.ts.map +1 -1
- package/dist/ui/organisms/wizard/wizard.types.js.map +1 -1
- package/dist/ui/utils/live-updates.d.ts +24 -0
- package/dist/ui/utils/live-updates.d.ts.map +1 -0
- package/dist/ui/utils/live-updates.js +20 -0
- package/dist/ui/utils/live-updates.js.map +1 -0
- package/dist/ui/utils/use-focus-on-arrival.d.ts +14 -0
- package/dist/ui/utils/use-focus-on-arrival.d.ts.map +1 -0
- package/dist/ui/utils/use-focus-on-arrival.js +42 -0
- package/dist/ui/utils/use-focus-on-arrival.js.map +1 -0
- package/dist/ui/utils/use-hotkey.d.ts +44 -25
- package/dist/ui/utils/use-hotkey.d.ts.map +1 -1
- package/dist/ui/utils/use-hotkey.js +80 -48
- package/dist/ui/utils/use-hotkey.js.map +1 -1
- package/package.json +5 -5
- package/dist/ui/molecules/split-button/index.d.ts +0 -2
- package/dist/ui/molecules/split-button/index.d.ts.map +0 -1
- package/dist/ui/molecules/split-button/index.js +0 -3
- package/dist/ui/molecules/split-button/index.js.map +0 -1
- package/dist/ui/molecules/split-button/split-button.d.ts +0 -21
- package/dist/ui/molecules/split-button/split-button.d.ts.map +0 -1
- package/dist/ui/molecules/split-button/split-button.js +0 -35
- package/dist/ui/molecules/split-button/split-button.js.map +0 -1
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { UseCaseDescriptor } from '@entifix/core';
|
|
2
|
+
import { type ReactNode } from 'react';
|
|
3
|
+
/**
|
|
4
|
+
* A `matching` selection larger than this asks for the count to be typed
|
|
5
|
+
* before a destructive verb runs over it.
|
|
6
|
+
*/
|
|
7
|
+
export declare const TYPED_SELECTION_THRESHOLD = 1000;
|
|
8
|
+
/** One verb about to run, and what it runs on. */
|
|
9
|
+
export interface VerbRequest<TTarget> {
|
|
10
|
+
readonly descriptor: Pick<UseCaseDescriptor, 'key' | 'labelKey' | 'confirm' | 'reversible' | 'inverse'>;
|
|
11
|
+
readonly target: TTarget;
|
|
12
|
+
/** What it acts on, as the dialog names it: "Acme", "12 invoices". */
|
|
13
|
+
readonly object: string;
|
|
14
|
+
/** How many records; 1 for a single record. */
|
|
15
|
+
readonly count: number;
|
|
16
|
+
/** A selection of everything a filter matches, rather than ticked rows. */
|
|
17
|
+
readonly matching?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export interface VerbInvokerOptions<TTarget> {
|
|
20
|
+
/** Runs a verb — the descriptor's own, or its inverse on Undo — by key. */
|
|
21
|
+
readonly run: (key: string, target: TTarget) => void;
|
|
22
|
+
readonly busy?: boolean;
|
|
23
|
+
readonly typedThreshold?: number;
|
|
24
|
+
/**
|
|
25
|
+
* The verbs this caller may run — the served document's `useCases`, which
|
|
26
|
+
* leave out what the caller holds no grant for. Undo is offered only when
|
|
27
|
+
* its `inverse` is one of them. Without it, every verb may run.
|
|
28
|
+
*/
|
|
29
|
+
readonly served?: readonly Pick<UseCaseDescriptor, 'key'>[];
|
|
30
|
+
}
|
|
31
|
+
export interface VerbInvoker<TTarget> {
|
|
32
|
+
readonly invoke: (request: VerbRequest<TTarget>) => void;
|
|
33
|
+
/** The confirmation, while one is open. Render it once, anywhere. */
|
|
34
|
+
readonly dialog: ReactNode;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Where every surface sends a verb, so none can reach one around its safety
|
|
38
|
+
* ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):
|
|
39
|
+
*
|
|
40
|
+
* - no `confirm` — it runs;
|
|
41
|
+
* - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its
|
|
42
|
+
* `inverse`. Without a toast region to offer Undo in, or an inverse this
|
|
43
|
+
* caller may run, it asks instead;
|
|
44
|
+
* - `confirm` — an alert dialog asks a question naming the object and the
|
|
45
|
+
* count, and the button repeats the verb with the count;
|
|
46
|
+
* - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}
|
|
47
|
+
* — the same dialog, and the name or the count has to be typed first.
|
|
48
|
+
*/
|
|
49
|
+
export declare function useVerbInvoker<TTarget>({ run, busy, typedThreshold, served, }: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget>;
|
|
50
|
+
//# sourceMappingURL=use-verb-invoker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"use-verb-invoker.d.ts","sourceRoot":"","sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AACvD,OAAO,EAAE,KAAK,SAAS,EAAY,MAAM,OAAO,CAAC;AAMjD;;;GAGG;AACH,eAAO,MAAM,yBAAyB,OAAO,CAAC;AAE9C,kDAAkD;AAClD,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,UAAU,EAAE,IAAI,CACvB,iBAAiB,EACjB,KAAK,GAAG,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAC1D,CAAC;IACF,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+CAA+C;IAC/C,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,kBAAkB,CAAC,OAAO;IACzC,2EAA2E;IAC3E,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IACrD,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,CAAC,EAAE,SAAS,IAAI,CAAC,iBAAiB,EAAE,KAAK,CAAC,EAAE,CAAC;CAC7D;AAED,MAAM,WAAW,WAAW,CAAC,OAAO;IAClC,QAAQ,CAAC,MAAM,EAAE,CAAC,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,KAAK,IAAI,CAAC;IACzD,qEAAqE;IACrE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;CAC5B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,EACtC,GAAG,EACH,IAAY,EACZ,cAA0C,EAC1C,MAAM,GACP,EAAE,kBAAkB,CAAC,OAAO,CAAC,GAAG,WAAW,CAAC,OAAO,CAAC,CAyEpD"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import { useT, useTranslateKey } from '../../i18n/index.js';
|
|
5
|
+
import { ConfirmDialog } from '../molecules/confirm-dialog/index.js';
|
|
6
|
+
import { useToasts } from '../molecules/toast-region/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* A `matching` selection larger than this asks for the count to be typed
|
|
9
|
+
* before a destructive verb runs over it.
|
|
10
|
+
*/ export const TYPED_SELECTION_THRESHOLD = 1000;
|
|
11
|
+
/**
|
|
12
|
+
* Where every surface sends a verb, so none can reach one around its safety
|
|
13
|
+
* ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):
|
|
14
|
+
*
|
|
15
|
+
* - no `confirm` — it runs;
|
|
16
|
+
* - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its
|
|
17
|
+
* `inverse`. Without a toast region to offer Undo in, or an inverse this
|
|
18
|
+
* caller may run, it asks instead;
|
|
19
|
+
* - `confirm` — an alert dialog asks a question naming the object and the
|
|
20
|
+
* count, and the button repeats the verb with the count;
|
|
21
|
+
* - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}
|
|
22
|
+
* — the same dialog, and the name or the count has to be typed first.
|
|
23
|
+
*/ export function useVerbInvoker({ run, busy = false, typedThreshold = TYPED_SELECTION_THRESHOLD, served }) {
|
|
24
|
+
var _confirm_confirmLabelKey;
|
|
25
|
+
const t = useT();
|
|
26
|
+
const translateKey = useTranslateKey();
|
|
27
|
+
const toasts = useToasts();
|
|
28
|
+
const [pending, setPending] = useState(undefined);
|
|
29
|
+
const invoke = (request)=>{
|
|
30
|
+
const { descriptor, target, object } = request;
|
|
31
|
+
const { confirm, inverse } = descriptor;
|
|
32
|
+
if (confirm === undefined) {
|
|
33
|
+
run(descriptor.key, target);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (descriptor.reversible === 'undo' && inverse !== undefined && (served === undefined || served.some((useCase)=>useCase.key === inverse)) && toasts !== undefined) {
|
|
37
|
+
run(descriptor.key, target);
|
|
38
|
+
const verb = translateKey(descriptor.labelKey);
|
|
39
|
+
toasts.show({
|
|
40
|
+
tone: 'undo',
|
|
41
|
+
message: confirm.doneKey === undefined ? t('confirm.done', {
|
|
42
|
+
verb,
|
|
43
|
+
object
|
|
44
|
+
}) : translateKey(confirm.doneKey, {
|
|
45
|
+
object,
|
|
46
|
+
count: request.count
|
|
47
|
+
}),
|
|
48
|
+
action: {
|
|
49
|
+
label: t('confirm.undo'),
|
|
50
|
+
onAction: ()=>run(inverse, target)
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
setPending(request);
|
|
56
|
+
};
|
|
57
|
+
if (pending === undefined) return {
|
|
58
|
+
invoke,
|
|
59
|
+
dialog: undefined
|
|
60
|
+
};
|
|
61
|
+
const { descriptor, object, count } = pending;
|
|
62
|
+
const confirm = descriptor.confirm;
|
|
63
|
+
const verb = translateKey((_confirm_confirmLabelKey = confirm.confirmLabelKey) != null ? _confirm_confirmLabelKey : descriptor.labelKey);
|
|
64
|
+
const typed = descriptor.reversible === 'typed' || pending.matching === true && count > typedThreshold;
|
|
65
|
+
return {
|
|
66
|
+
invoke,
|
|
67
|
+
dialog: /*#__PURE__*/ _jsx(ConfirmDialog, {
|
|
68
|
+
open: true,
|
|
69
|
+
tone: confirm.tone,
|
|
70
|
+
title: t('confirm.question', {
|
|
71
|
+
verb: translateKey(descriptor.labelKey),
|
|
72
|
+
object
|
|
73
|
+
}),
|
|
74
|
+
message: translateKey(confirm.messageKey, {
|
|
75
|
+
object,
|
|
76
|
+
count
|
|
77
|
+
}),
|
|
78
|
+
confirmLabel: t('confirm.verbCount', {
|
|
79
|
+
verb,
|
|
80
|
+
count
|
|
81
|
+
}),
|
|
82
|
+
typed: typed ? count > 1 ? String(count) : object : undefined,
|
|
83
|
+
busy: busy,
|
|
84
|
+
onCancel: ()=>setPending(undefined),
|
|
85
|
+
onConfirm: ()=>{
|
|
86
|
+
setPending(undefined);
|
|
87
|
+
run(descriptor.key, pending.target);
|
|
88
|
+
}
|
|
89
|
+
})
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
//# sourceMappingURL=use-verb-invoker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/ui/actions/use-verb-invoker.tsx"],"sourcesContent":["'use client';\n\nimport type { UseCaseDescriptor } from '@entifix/core';\nimport { type ReactNode, useState } from 'react';\n\nimport { useT, useTranslateKey } from '../../i18n/index.js';\nimport { ConfirmDialog } from '../molecules/confirm-dialog/index.js';\nimport { useToasts } from '../molecules/toast-region/index.js';\n\n/**\n * A `matching` selection larger than this asks for the count to be typed\n * before a destructive verb runs over it.\n */\nexport const TYPED_SELECTION_THRESHOLD = 1000;\n\n/** One verb about to run, and what it runs on. */\nexport interface VerbRequest<TTarget> {\n readonly descriptor: Pick<\n UseCaseDescriptor,\n 'key' | 'labelKey' | 'confirm' | 'reversible' | 'inverse'\n >;\n readonly target: TTarget;\n /** What it acts on, as the dialog names it: \"Acme\", \"12 invoices\". */\n readonly object: string;\n /** How many records; 1 for a single record. */\n readonly count: number;\n /** A selection of everything a filter matches, rather than ticked rows. */\n readonly matching?: boolean;\n}\n\nexport interface VerbInvokerOptions<TTarget> {\n /** Runs a verb — the descriptor's own, or its inverse on Undo — by key. */\n readonly run: (key: string, target: TTarget) => void;\n readonly busy?: boolean;\n readonly typedThreshold?: number;\n /**\n * The verbs this caller may run — the served document's `useCases`, which\n * leave out what the caller holds no grant for. Undo is offered only when\n * its `inverse` is one of them. Without it, every verb may run.\n */\n readonly served?: readonly Pick<UseCaseDescriptor, 'key'>[];\n}\n\nexport interface VerbInvoker<TTarget> {\n readonly invoke: (request: VerbRequest<TTarget>) => void;\n /** The confirmation, while one is open. Render it once, anywhere. */\n readonly dialog: ReactNode;\n}\n\n/**\n * Where every surface sends a verb, so none can reach one around its safety\n * ([ADR 0026](../../../../../../docs/adr/0026-actions-hierarchy-overflow-and-destructive-verbs.md)):\n *\n * - no `confirm` — it runs;\n * - `reversible: 'undo'` — it runs, and a toast offers Undo, which runs its\n * `inverse`. Without a toast region to offer Undo in, or an inverse this\n * caller may run, it asks instead;\n * - `confirm` — an alert dialog asks a question naming the object and the\n * count, and the button repeats the verb with the count;\n * - `typed`, or a `matching` selection over {@link TYPED_SELECTION_THRESHOLD}\n * — the same dialog, and the name or the count has to be typed first.\n */\nexport function useVerbInvoker<TTarget>({\n run,\n busy = false,\n typedThreshold = TYPED_SELECTION_THRESHOLD,\n served,\n}: VerbInvokerOptions<TTarget>): VerbInvoker<TTarget> {\n const t = useT();\n const translateKey = useTranslateKey();\n const toasts = useToasts();\n const [pending, setPending] = useState<VerbRequest<TTarget> | undefined>(\n undefined,\n );\n\n const invoke = (request: VerbRequest<TTarget>) => {\n const { descriptor, target, object } = request;\n const { confirm, inverse } = descriptor;\n if (confirm === undefined) {\n run(descriptor.key, target);\n return;\n }\n if (\n descriptor.reversible === 'undo' &&\n inverse !== undefined &&\n (served === undefined ||\n served.some(useCase => useCase.key === inverse)) &&\n toasts !== undefined\n ) {\n run(descriptor.key, target);\n const verb = translateKey(descriptor.labelKey);\n toasts.show({\n tone: 'undo',\n message:\n confirm.doneKey === undefined\n ? t('confirm.done', { verb, object })\n : translateKey(confirm.doneKey, { object, count: request.count }),\n action: {\n label: t('confirm.undo'),\n onAction: () => run(inverse, target),\n },\n });\n return;\n }\n setPending(request);\n };\n\n if (pending === undefined) return { invoke, dialog: undefined };\n\n const { descriptor, object, count } = pending;\n const confirm = descriptor.confirm as NonNullable<\n UseCaseDescriptor['confirm']\n >;\n const verb = translateKey(confirm.confirmLabelKey ?? descriptor.labelKey);\n const typed =\n descriptor.reversible === 'typed' ||\n (pending.matching === true && count > typedThreshold);\n\n return {\n invoke,\n dialog: (\n <ConfirmDialog\n open\n tone={confirm.tone}\n title={t('confirm.question', {\n verb: translateKey(descriptor.labelKey),\n object,\n })}\n message={translateKey(confirm.messageKey, { object, count })}\n confirmLabel={t('confirm.verbCount', { verb, count })}\n typed={typed ? (count > 1 ? String(count) : object) : undefined}\n busy={busy}\n onCancel={() => setPending(undefined)}\n onConfirm={() => {\n setPending(undefined);\n run(descriptor.key, pending.target);\n }}\n />\n ),\n };\n}\n"],"names":["useState","useT","useTranslateKey","ConfirmDialog","useToasts","TYPED_SELECTION_THRESHOLD","useVerbInvoker","run","busy","typedThreshold","served","confirm","t","translateKey","toasts","pending","setPending","undefined","invoke","request","descriptor","target","object","inverse","key","reversible","some","useCase","verb","labelKey","show","tone","message","doneKey","count","action","label","onAction","dialog","confirmLabelKey","typed","matching","open","title","messageKey","confirmLabel","String","onCancel","onConfirm"],"mappings":"AAAA;;AAGA,SAAyBA,QAAQ,QAAQ,QAAQ;AAEjD,SAASC,IAAI,EAAEC,eAAe,QAAQ,sBAAsB;AAC5D,SAASC,aAAa,QAAQ,uCAAuC;AACrE,SAASC,SAAS,QAAQ,qCAAqC;AAE/D;;;CAGC,GACD,OAAO,MAAMC,4BAA4B,KAAK;AAoC9C;;;;;;;;;;;;CAYC,GACD,OAAO,SAASC,eAAwB,EACtCC,GAAG,EACHC,OAAO,KAAK,EACZC,iBAAiBJ,yBAAyB,EAC1CK,MAAM,EACsB;QA8CFC;IA7C1B,MAAMC,IAAIX;IACV,MAAMY,eAAeX;IACrB,MAAMY,SAASV;IACf,MAAM,CAACW,SAASC,WAAW,GAAGhB,SAC5BiB;IAGF,MAAMC,SAAS,CAACC;QACd,MAAM,EAAEC,UAAU,EAAEC,MAAM,EAAEC,MAAM,EAAE,GAAGH;QACvC,MAAM,EAAER,OAAO,EAAEY,OAAO,EAAE,GAAGH;QAC7B,IAAIT,YAAYM,WAAW;YACzBV,IAAIa,WAAWI,GAAG,EAAEH;YACpB;QACF;QACA,IACED,WAAWK,UAAU,KAAK,UAC1BF,YAAYN,aACXP,CAAAA,WAAWO,aACVP,OAAOgB,IAAI,CAACC,CAAAA,UAAWA,QAAQH,GAAG,KAAKD,QAAO,KAChDT,WAAWG,WACX;YACAV,IAAIa,WAAWI,GAAG,EAAEH;YACpB,MAAMO,OAAOf,aAAaO,WAAWS,QAAQ;YAC7Cf,OAAOgB,IAAI,CAAC;gBACVC,MAAM;gBACNC,SACErB,QAAQsB,OAAO,KAAKhB,YAChBL,EAAE,gBAAgB;oBAAEgB;oBAAMN;gBAAO,KACjCT,aAAaF,QAAQsB,OAAO,EAAE;oBAAEX;oBAAQY,OAAOf,QAAQe,KAAK;gBAAC;gBACnEC,QAAQ;oBACNC,OAAOxB,EAAE;oBACTyB,UAAU,IAAM9B,IAAIgB,SAASF;gBAC/B;YACF;YACA;QACF;QACAL,WAAWG;IACb;IAEA,IAAIJ,YAAYE,WAAW,OAAO;QAAEC;QAAQoB,QAAQrB;IAAU;IAE9D,MAAM,EAAEG,UAAU,EAAEE,MAAM,EAAEY,KAAK,EAAE,GAAGnB;IACtC,MAAMJ,UAAUS,WAAWT,OAAO;IAGlC,MAAMiB,OAAOf,cAAaF,2BAAAA,QAAQ4B,eAAe,YAAvB5B,2BAA2BS,WAAWS,QAAQ;IACxE,MAAMW,QACJpB,WAAWK,UAAU,KAAK,WACzBV,QAAQ0B,QAAQ,KAAK,QAAQP,QAAQzB;IAExC,OAAO;QACLS;QACAoB,sBACE,KAACnC;YACCuC,IAAI;YACJX,MAAMpB,QAAQoB,IAAI;YAClBY,OAAO/B,EAAE,oBAAoB;gBAC3BgB,MAAMf,aAAaO,WAAWS,QAAQ;gBACtCP;YACF;YACAU,SAASnB,aAAaF,QAAQiC,UAAU,EAAE;gBAAEtB;gBAAQY;YAAM;YAC1DW,cAAcjC,EAAE,qBAAqB;gBAAEgB;gBAAMM;YAAM;YACnDM,OAAOA,QAASN,QAAQ,IAAIY,OAAOZ,SAASZ,SAAUL;YACtDT,MAAMA;YACNuC,UAAU,IAAM/B,WAAWC;YAC3B+B,WAAW;gBACThC,WAAWC;gBACXV,IAAIa,WAAWI,GAAG,EAAET,QAAQM,MAAM;YACpC;;IAGN;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cell-value.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"names":[],"mappings":"AAGA,OAAO,
|
|
1
|
+
{"version":3,"file":"cell-value.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,qBAAqB,EAK3B,MAAM,eAAe,CAAC;AAgIvB,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,OAAO,CAAC;IACf,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,EAAE,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,EAAE,cAAc,+BAyEtE"}
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { EntityCollectionLink, EntityLink, isLocalizedText, localize } from '@entifix/core';
|
|
3
|
+
import { defaultLocale, EntityCollectionLink, EntityLink, isLocalizedText, localize } from '@entifix/core';
|
|
4
4
|
import { useEnumLabel, useFormatters, useLocale, useT } from '../../../i18n/index.js';
|
|
5
5
|
import { Money } from '../money/money.js';
|
|
6
6
|
import { currencyOf } from '../money/money-math.js';
|
|
7
|
+
import { Skeleton } from '../skeleton/index.js';
|
|
7
8
|
/** Stand-in for an absent value, so an empty cell still reads as intentional. */ const EMPTY = '—';
|
|
8
9
|
/**
|
|
9
|
-
* A link reads as its target's label
|
|
10
|
-
* resolved)
|
|
11
|
-
*
|
|
10
|
+
* A link reads as its target's label once the target is loaded (embedded or
|
|
11
|
+
* resolved). Never as its id: a key is not a name, and showing one fails the
|
|
12
|
+
* reader who came to see which record it is (#200).
|
|
12
13
|
*/ function entityLabel(target, linkLabelProperty, locale) {
|
|
13
14
|
const label = target == null ? void 0 : target[linkLabelProperty];
|
|
14
15
|
if (label == null) return undefined;
|
|
@@ -17,17 +18,14 @@ import { currencyOf } from '../money/money-math.js';
|
|
|
17
18
|
}
|
|
18
19
|
function formatLink(link, linkLabelProperty, locale) {
|
|
19
20
|
var _entityLabel;
|
|
20
|
-
return (_entityLabel = entityLabel(link.value, linkLabelProperty, locale)) != null ? _entityLabel :
|
|
21
|
+
return link.id == null ? EMPTY : (_entityLabel = entityLabel(link.value, linkLabelProperty, locale)) != null ? _entityLabel : EMPTY;
|
|
21
22
|
}
|
|
22
23
|
function formatCollectionLink(link, linkLabelProperty, locale) {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}).join(', ');
|
|
29
|
-
}
|
|
30
|
-
return link.ids.map(String).join(', ');
|
|
24
|
+
var _link_values;
|
|
25
|
+
return ((_link_values = link.values) != null ? _link_values : []).map((target)=>{
|
|
26
|
+
var _entityLabel;
|
|
27
|
+
return (_entityLabel = entityLabel(target, linkLabelProperty, locale)) != null ? _entityLabel : EMPTY;
|
|
28
|
+
}).join(', ');
|
|
31
29
|
}
|
|
32
30
|
function formatByType(value, type, linkLabelProperty, formatting) {
|
|
33
31
|
switch(type){
|
|
@@ -76,7 +74,7 @@ function formatByType(value, type, linkLabelProperty, formatting) {
|
|
|
76
74
|
* is what lets a table stay generic: a link, a date and a number all arrive as
|
|
77
75
|
* "the value of an accessor", and only the descriptor decides how they read.
|
|
78
76
|
*/ export function CellValue({ value, descriptor, record }) {
|
|
79
|
-
var _NUMERIC_CLASS_descriptor_type;
|
|
77
|
+
var _value_locale, _NUMERIC_CLASS_descriptor_type;
|
|
80
78
|
const t = useT();
|
|
81
79
|
const formatters = useFormatters();
|
|
82
80
|
const enumLabel = useEnumLabel();
|
|
@@ -103,6 +101,14 @@ function formatByType(value, type, linkLabelProperty, formatting) {
|
|
|
103
101
|
});
|
|
104
102
|
}
|
|
105
103
|
}
|
|
104
|
+
// A relation whose target has not arrived yet holds its place with a
|
|
105
|
+
// skeleton — the id is all it has, and the id is not what it means.
|
|
106
|
+
if (value instanceof EntityLink && value.id != null && value.value === undefined || value instanceof EntityCollectionLink && value.ids.length > 0 && value.values === undefined) {
|
|
107
|
+
return /*#__PURE__*/ _jsx(Skeleton, {
|
|
108
|
+
shape: "line",
|
|
109
|
+
className: "w-16"
|
|
110
|
+
});
|
|
111
|
+
}
|
|
106
112
|
const text = formatByType(value, descriptor.type, descriptor.linkLabelProperty, {
|
|
107
113
|
formatters,
|
|
108
114
|
yes: t('value.yes'),
|
|
@@ -119,6 +125,14 @@ function formatByType(value, type, linkLabelProperty, formatting) {
|
|
|
119
125
|
children: EMPTY
|
|
120
126
|
});
|
|
121
127
|
}
|
|
128
|
+
// Text the reader's locale lacks falls back to the default locale's, and is
|
|
129
|
+
// marked as that language, so it is read in its own voice (WCAG 3.1.2).
|
|
130
|
+
if (descriptor.type === 'localized' && isLocalizedText(value) && ((_value_locale = value[locale]) != null ? _value_locale : '') === '') {
|
|
131
|
+
return /*#__PURE__*/ _jsx("span", {
|
|
132
|
+
lang: defaultLocale(),
|
|
133
|
+
children: text
|
|
134
|
+
});
|
|
135
|
+
}
|
|
122
136
|
return /*#__PURE__*/ _jsx("span", {
|
|
123
137
|
className: (_NUMERIC_CLASS_descriptor_type = NUMERIC_CLASS[descriptor.type]) != null ? _NUMERIC_CLASS_descriptor_type : undefined,
|
|
124
138
|
children: text
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"sourcesContent":["'use client';\n\nimport type { Formatters, Locale } from '@entifix/core';\nimport {\n type Entity,\n EntityCollectionLink,\n type EntityFieldDescriptor,\n EntityLink,\n isLocalizedText,\n localize,\n type MetaAccessorType,\n} from '@entifix/core';\n\nimport {\n useEnumLabel,\n useFormatters,\n useLocale,\n useT,\n} from '../../../i18n/index.js';\nimport { Money } from '../money/money.js';\nimport { currencyOf } from '../money/money-math.js';\n\n/** Stand-in for an absent value, so an empty cell still reads as intentional. */\nconst EMPTY = '—';\n\n/**\n * A link reads as its target's label when the target is loaded (embedded or\n * resolved), and as the bare foreign key when it is not — the same cell markup\n * covers both shapes a relation can arrive in.\n */\nfunction entityLabel(\n target: Entity | undefined,\n linkLabelProperty: string,\n locale: Locale,\n): string | undefined {\n const label = (target as Record<string, unknown> | undefined)?.[\n linkLabelProperty\n ];\n if (label == null) return undefined;\n // A target labelled by a localized member reads in the reader's locale.\n return isLocalizedText(label) ? localize(label, locale) : String(label);\n}\n\nfunction formatLink(\n link: EntityLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return (\n entityLabel(link.value, linkLabelProperty, locale) ??\n (link.id == null ? EMPTY : String(link.id))\n );\n}\n\nfunction formatCollectionLink(\n link: EntityCollectionLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n const loaded = link.values;\n if (loaded !== undefined) {\n return loaded\n .map(\n (target, index) =>\n entityLabel(target, linkLabelProperty, locale) ??\n String(target.id ?? link.ids[index] ?? ''),\n )\n .join(', ');\n }\n return link.ids.map(String).join(', ');\n}\n\n/**\n * Everything locale-dependent, resolved once by the component and handed down.\n * The formatters carry an explicit locale, which is what keeps a date rendered\n * on the server byte-identical to the same date rendered in the browser.\n */\ninterface CellFormatting {\n readonly formatters: Formatters;\n readonly yes: string;\n readonly no: string;\n readonly enumLabel: (value: string) => string;\n readonly rowCount: (count: number) => string;\n readonly locale: Locale;\n}\n\nfunction formatByType(\n value: unknown,\n type: MetaAccessorType,\n linkLabelProperty: string,\n formatting: CellFormatting,\n): string {\n switch (type) {\n case 'date':\n return formatting.formatters.date(\n value instanceof Date ? value : String(value),\n );\n case 'number':\n return typeof value === 'number'\n ? formatting.formatters.number(value)\n : String(value);\n case 'boolean':\n return value ? formatting.yes : formatting.no;\n case 'enum':\n return formatting.enumLabel(String(value));\n case 'link':\n return value instanceof EntityLink\n ? formatLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'linkCollection':\n return value instanceof EntityCollectionLink\n ? formatCollectionLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'scalarCollection':\n // The same comma list the draft holds, spaced for reading. Not `String`:\n // an array's own `toString` joins on a bare comma, which reads as one\n // run-on token in a narrow column.\n return Array.isArray(value) ? value.join(', ') : String(value);\n case 'composition':\n // Owned rows have no useful one-line form — a receipt's lines are not a\n // sentence — and the master's own screen is where they are read and\n // edited. So the cell reports the count and stops.\n return Array.isArray(value)\n ? formatting.rowCount(value.length)\n : String(value);\n case 'localized':\n // The reader's locale, falling back to the host's default; a value\n // that is not a text per locale has no reading worth guessing at.\n return isLocalizedText(value)\n ? (localize(value, formatting.locale) ?? '')\n : '';\n default:\n return String(value);\n }\n}\n\n/**\n * Tabular figures, applied once, here — not on `--font-sans` and not per call\n * site. A column of numbers or dates must align on the digit; a paragraph must\n * not, because proportional figures are what prose is set in. The descriptor\n * already knows which is which, so the cell is the only place that has to.\n */\nconst NUMERIC_CLASS: Partial<Record<MetaAccessorType, string>> = {\n number: 'tabular-nums',\n date: 'tabular-nums',\n};\n\nexport interface CellValueProps {\n value: unknown;\n descriptor: EntityFieldDescriptor;\n /**\n * The record the value belongs to. A money member whose currency is another\n * member of the record (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Renders one entity member according to its metadata type. Centralizing this\n * is what lets a table stay generic: a link, a date and a number all arrive as\n * \"the value of an accessor\", and only the descriptor decides how they read.\n */\nexport function CellValue({ value, descriptor, record }: CellValueProps) {\n const t = useT();\n const formatters = useFormatters();\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n\n if (value === undefined || value === null || value === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // An amount reads as money in the reader's locale, never as the stored\n // number. A draft holds it as a string, so a numeric string counts too. A\n // currency the record does not carry leaves it a plain number rather than\n // guessing one.\n if (descriptor.money !== undefined) {\n const amount = typeof value === 'number' ? value : Number(value);\n const currency = currencyOf(descriptor.money, record);\n if (currency !== undefined && Number.isFinite(amount)) {\n return (\n <Money\n amount={amount}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n />\n );\n }\n }\n\n const text = formatByType(\n value,\n descriptor.type,\n descriptor.linkLabelProperty,\n {\n formatters,\n yes: t('value.yes'),\n no: t('value.no'),\n enumLabel: raw => enumLabel(descriptor, raw),\n rowCount: count => t('value.rowCount', { count }),\n locale,\n },\n );\n\n if (text === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n return (\n <span className={NUMERIC_CLASS[descriptor.type] ?? undefined}>{text}</span>\n );\n}\n"],"names":["EntityCollectionLink","EntityLink","isLocalizedText","localize","useEnumLabel","useFormatters","useLocale","useT","Money","currencyOf","EMPTY","entityLabel","target","linkLabelProperty","locale","label","undefined","String","formatLink","link","value","id","formatCollectionLink","loaded","values","map","index","ids","join","formatByType","type","formatting","formatters","date","Date","number","yes","no","enumLabel","Array","isArray","rowCount","length","NUMERIC_CLASS","CellValue","descriptor","record","t","span","className","money","amount","Number","currency","isFinite","unit","text","raw","count"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/atoms/cell-value/cell-value.tsx"],"sourcesContent":["'use client';\n\nimport type { Formatters, Locale } from '@entifix/core';\nimport {\n defaultLocale,\n type Entity,\n EntityCollectionLink,\n type EntityFieldDescriptor,\n EntityLink,\n isLocalizedText,\n localize,\n type MetaAccessorType,\n} from '@entifix/core';\n\nimport {\n useEnumLabel,\n useFormatters,\n useLocale,\n useT,\n} from '../../../i18n/index.js';\nimport { Money } from '../money/money.js';\nimport { currencyOf } from '../money/money-math.js';\nimport { Skeleton } from '../skeleton/index.js';\n\n/** Stand-in for an absent value, so an empty cell still reads as intentional. */\nconst EMPTY = '—';\n\n/**\n * A link reads as its target's label once the target is loaded (embedded or\n * resolved). Never as its id: a key is not a name, and showing one fails the\n * reader who came to see which record it is (#200).\n */\nfunction entityLabel(\n target: Entity | undefined,\n linkLabelProperty: string,\n locale: Locale,\n): string | undefined {\n const label = (target as Record<string, unknown> | undefined)?.[\n linkLabelProperty\n ];\n if (label == null) return undefined;\n // A target labelled by a localized member reads in the reader's locale.\n return isLocalizedText(label) ? localize(label, locale) : String(label);\n}\n\nfunction formatLink(\n link: EntityLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return link.id == null\n ? EMPTY\n : (entityLabel(link.value, linkLabelProperty, locale) ?? EMPTY);\n}\n\nfunction formatCollectionLink(\n link: EntityCollectionLink<Entity>,\n linkLabelProperty: string,\n locale: Locale,\n): string {\n return (link.values ?? [])\n .map(target => entityLabel(target, linkLabelProperty, locale) ?? EMPTY)\n .join(', ');\n}\n\n/**\n * Everything locale-dependent, resolved once by the component and handed down.\n * The formatters carry an explicit locale, which is what keeps a date rendered\n * on the server byte-identical to the same date rendered in the browser.\n */\ninterface CellFormatting {\n readonly formatters: Formatters;\n readonly yes: string;\n readonly no: string;\n readonly enumLabel: (value: string) => string;\n readonly rowCount: (count: number) => string;\n readonly locale: Locale;\n}\n\nfunction formatByType(\n value: unknown,\n type: MetaAccessorType,\n linkLabelProperty: string,\n formatting: CellFormatting,\n): string {\n switch (type) {\n case 'date':\n return formatting.formatters.date(\n value instanceof Date ? value : String(value),\n );\n case 'number':\n return typeof value === 'number'\n ? formatting.formatters.number(value)\n : String(value);\n case 'boolean':\n return value ? formatting.yes : formatting.no;\n case 'enum':\n return formatting.enumLabel(String(value));\n case 'link':\n return value instanceof EntityLink\n ? formatLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'linkCollection':\n return value instanceof EntityCollectionLink\n ? formatCollectionLink(value, linkLabelProperty, formatting.locale)\n : String(value);\n case 'scalarCollection':\n // The same comma list the draft holds, spaced for reading. Not `String`:\n // an array's own `toString` joins on a bare comma, which reads as one\n // run-on token in a narrow column.\n return Array.isArray(value) ? value.join(', ') : String(value);\n case 'composition':\n // Owned rows have no useful one-line form — a receipt's lines are not a\n // sentence — and the master's own screen is where they are read and\n // edited. So the cell reports the count and stops.\n return Array.isArray(value)\n ? formatting.rowCount(value.length)\n : String(value);\n case 'localized':\n // The reader's locale, falling back to the host's default; a value\n // that is not a text per locale has no reading worth guessing at.\n return isLocalizedText(value)\n ? (localize(value, formatting.locale) ?? '')\n : '';\n default:\n return String(value);\n }\n}\n\n/**\n * Tabular figures, applied once, here — not on `--font-sans` and not per call\n * site. A column of numbers or dates must align on the digit; a paragraph must\n * not, because proportional figures are what prose is set in. The descriptor\n * already knows which is which, so the cell is the only place that has to.\n */\nconst NUMERIC_CLASS: Partial<Record<MetaAccessorType, string>> = {\n number: 'tabular-nums',\n date: 'tabular-nums',\n};\n\nexport interface CellValueProps {\n value: unknown;\n descriptor: EntityFieldDescriptor;\n /**\n * The record the value belongs to. A money member whose currency is another\n * member of the record (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/**\n * Renders one entity member according to its metadata type. Centralizing this\n * is what lets a table stay generic: a link, a date and a number all arrive as\n * \"the value of an accessor\", and only the descriptor decides how they read.\n */\nexport function CellValue({ value, descriptor, record }: CellValueProps) {\n const t = useT();\n const formatters = useFormatters();\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n\n if (value === undefined || value === null || value === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // An amount reads as money in the reader's locale, never as the stored\n // number. A draft holds it as a string, so a numeric string counts too. A\n // currency the record does not carry leaves it a plain number rather than\n // guessing one.\n if (descriptor.money !== undefined) {\n const amount = typeof value === 'number' ? value : Number(value);\n const currency = currencyOf(descriptor.money, record);\n if (currency !== undefined && Number.isFinite(amount)) {\n return (\n <Money\n amount={amount}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n />\n );\n }\n }\n\n // A relation whose target has not arrived yet holds its place with a\n // skeleton — the id is all it has, and the id is not what it means.\n if (\n (value instanceof EntityLink &&\n value.id != null &&\n value.value === undefined) ||\n (value instanceof EntityCollectionLink &&\n value.ids.length > 0 &&\n value.values === undefined)\n ) {\n return <Skeleton shape=\"line\" className=\"w-16\" />;\n }\n\n const text = formatByType(\n value,\n descriptor.type,\n descriptor.linkLabelProperty,\n {\n formatters,\n yes: t('value.yes'),\n no: t('value.no'),\n enumLabel: raw => enumLabel(descriptor, raw),\n rowCount: count => t('value.rowCount', { count }),\n locale,\n },\n );\n\n if (text === '') {\n return <span className=\"text-content-muted\">{EMPTY}</span>;\n }\n\n // Text the reader's locale lacks falls back to the default locale's, and is\n // marked as that language, so it is read in its own voice (WCAG 3.1.2).\n if (\n descriptor.type === 'localized' &&\n isLocalizedText(value) &&\n (value[locale] ?? '') === ''\n ) {\n return <span lang={defaultLocale()}>{text}</span>;\n }\n\n return (\n <span className={NUMERIC_CLASS[descriptor.type] ?? undefined}>{text}</span>\n );\n}\n"],"names":["defaultLocale","EntityCollectionLink","EntityLink","isLocalizedText","localize","useEnumLabel","useFormatters","useLocale","useT","Money","currencyOf","Skeleton","EMPTY","entityLabel","target","linkLabelProperty","locale","label","undefined","String","formatLink","link","id","value","formatCollectionLink","values","map","join","formatByType","type","formatting","formatters","date","Date","number","yes","no","enumLabel","Array","isArray","rowCount","length","NUMERIC_CLASS","CellValue","descriptor","record","t","span","className","money","amount","Number","currency","isFinite","unit","ids","shape","text","raw","count","lang"],"mappings":"AAAA;;AAGA,SACEA,aAAa,EAEbC,oBAAoB,EAEpBC,UAAU,EACVC,eAAe,EACfC,QAAQ,QAEH,gBAAgB;AAEvB,SACEC,YAAY,EACZC,aAAa,EACbC,SAAS,EACTC,IAAI,QACC,yBAAyB;AAChC,SAASC,KAAK,QAAQ,oBAAoB;AAC1C,SAASC,UAAU,QAAQ,yBAAyB;AACpD,SAASC,QAAQ,QAAQ,uBAAuB;AAEhD,+EAA+E,GAC/E,MAAMC,QAAQ;AAEd;;;;CAIC,GACD,SAASC,YACPC,MAA0B,EAC1BC,iBAAyB,EACzBC,MAAc;IAEd,MAAMC,QAASH,0BAAD,AAACA,MAAgD,CAC7DC,kBACD;IACD,IAAIE,SAAS,MAAM,OAAOC;IAC1B,wEAAwE;IACxE,OAAOf,gBAAgBc,SAASb,SAASa,OAAOD,UAAUG,OAAOF;AACnE;AAEA,SAASG,WACPC,IAAwB,EACxBN,iBAAyB,EACzBC,MAAc;QAITH;IAFL,OAAOQ,KAAKC,EAAE,IAAI,OACdV,SACCC,eAAAA,YAAYQ,KAAKE,KAAK,EAAER,mBAAmBC,mBAA3CH,eAAsDD;AAC7D;AAEA,SAASY,qBACPH,IAAkC,EAClCN,iBAAyB,EACzBC,MAAc;QAENK;IAAR,OAAO,EAACA,eAAAA,KAAKI,MAAM,YAAXJ,eAAe,EAAE,EACtBK,GAAG,CAACZ,CAAAA;YAAUD;gBAAAA,eAAAA,YAAYC,QAAQC,mBAAmBC,mBAAvCH,eAAkDD;OAChEe,IAAI,CAAC;AACV;AAgBA,SAASC,aACPL,KAAc,EACdM,IAAsB,EACtBd,iBAAyB,EACzBe,UAA0B;IAE1B,OAAQD;QACN,KAAK;YACH,OAAOC,WAAWC,UAAU,CAACC,IAAI,CAC/BT,iBAAiBU,OAAOV,QAAQJ,OAAOI;QAE3C,KAAK;YACH,OAAO,OAAOA,UAAU,WACpBO,WAAWC,UAAU,CAACG,MAAM,CAACX,SAC7BJ,OAAOI;QACb,KAAK;YACH,OAAOA,QAAQO,WAAWK,GAAG,GAAGL,WAAWM,EAAE;QAC/C,KAAK;YACH,OAAON,WAAWO,SAAS,CAAClB,OAAOI;QACrC,KAAK;YACH,OAAOA,iBAAiBrB,aACpBkB,WAAWG,OAAOR,mBAAmBe,WAAWd,MAAM,IACtDG,OAAOI;QACb,KAAK;YACH,OAAOA,iBAAiBtB,uBACpBuB,qBAAqBD,OAAOR,mBAAmBe,WAAWd,MAAM,IAChEG,OAAOI;QACb,KAAK;YACH,yEAAyE;YACzE,sEAAsE;YACtE,mCAAmC;YACnC,OAAOe,MAAMC,OAAO,CAAChB,SAASA,MAAMI,IAAI,CAAC,QAAQR,OAAOI;QAC1D,KAAK;YACH,wEAAwE;YACxE,oEAAoE;YACpE,mDAAmD;YACnD,OAAOe,MAAMC,OAAO,CAAChB,SACjBO,WAAWU,QAAQ,CAACjB,MAAMkB,MAAM,IAChCtB,OAAOI;QACb,KAAK;gBAIEnB;YAHL,mEAAmE;YACnE,kEAAkE;YAClE,OAAOD,gBAAgBoB,UAClBnB,YAAAA,SAASmB,OAAOO,WAAWd,MAAM,aAAjCZ,YAAsC,KACvC;QACN;YACE,OAAOe,OAAOI;IAClB;AACF;AAEA;;;;;CAKC,GACD,MAAMmB,gBAA2D;IAC/DR,QAAQ;IACRF,MAAM;AACR;AAYA;;;;CAIC,GACD,OAAO,SAASW,UAAU,EAAEpB,KAAK,EAAEqB,UAAU,EAAEC,MAAM,EAAkB;QAiElEtB,eAMgBmB;IAtEnB,MAAMI,IAAItC;IACV,MAAMuB,aAAazB;IACnB,MAAM+B,YAAYhC;IAClB,MAAMW,SAAST;IAEf,IAAIgB,UAAUL,aAAaK,UAAU,QAAQA,UAAU,IAAI;QACzD,qBAAO,KAACwB;YAAKC,WAAU;sBAAsBpC;;IAC/C;IAEA,uEAAuE;IACvE,0EAA0E;IAC1E,0EAA0E;IAC1E,gBAAgB;IAChB,IAAIgC,WAAWK,KAAK,KAAK/B,WAAW;QAClC,MAAMgC,SAAS,OAAO3B,UAAU,WAAWA,QAAQ4B,OAAO5B;QAC1D,MAAM6B,WAAW1C,WAAWkC,WAAWK,KAAK,EAAEJ;QAC9C,IAAIO,aAAalC,aAAaiC,OAAOE,QAAQ,CAACH,SAAS;YACrD,qBACE,KAACzC;gBACCyC,QAAQA;gBACRE,UAAUA;gBACVE,MAAMV,WAAWK,KAAK,CAACK,IAAI;gBAC3BtC,QAAQA;;QAGd;IACF;IAEA,qEAAqE;IACrE,oEAAoE;IACpE,IACE,AAACO,iBAAiBrB,cAChBqB,MAAMD,EAAE,IAAI,QACZC,MAAMA,KAAK,KAAKL,aACjBK,iBAAiBtB,wBAChBsB,MAAMgC,GAAG,CAACd,MAAM,GAAG,KACnBlB,MAAME,MAAM,KAAKP,WACnB;QACA,qBAAO,KAACP;YAAS6C,OAAM;YAAOR,WAAU;;IAC1C;IAEA,MAAMS,OAAO7B,aACXL,OACAqB,WAAWf,IAAI,EACfe,WAAW7B,iBAAiB,EAC5B;QACEgB;QACAI,KAAKW,EAAE;QACPV,IAAIU,EAAE;QACNT,WAAWqB,CAAAA,MAAOrB,UAAUO,YAAYc;QACxClB,UAAUmB,CAAAA,QAASb,EAAE,kBAAkB;gBAAEa;YAAM;QAC/C3C;IACF;IAGF,IAAIyC,SAAS,IAAI;QACf,qBAAO,KAACV;YAAKC,WAAU;sBAAsBpC;;IAC/C;IAEA,4EAA4E;IAC5E,wEAAwE;IACxE,IACEgC,WAAWf,IAAI,KAAK,eACpB1B,gBAAgBoB,UAChB,EAACA,gBAAAA,KAAK,CAACP,OAAO,YAAbO,gBAAiB,QAAQ,IAC1B;QACA,qBAAO,KAACwB;YAAKa,MAAM5D;sBAAkByD;;IACvC;IAEA,qBACE,KAACV;QAAKC,SAAS,GAAEN,iCAAAA,aAAa,CAACE,WAAWf,IAAI,CAAC,YAA9Ba,iCAAkCxB;kBAAYuC;;AAEnE"}
|
|
@@ -15,6 +15,7 @@ export interface FieldControlProps {
|
|
|
15
15
|
'aria-labelledby'?: string;
|
|
16
16
|
'aria-describedby'?: string;
|
|
17
17
|
'aria-invalid'?: boolean;
|
|
18
|
+
'aria-required'?: boolean;
|
|
18
19
|
/**
|
|
19
20
|
* The draft value as a string — the shape a native input round-trips. A form
|
|
20
21
|
* holds its whole draft as `Record<string, string>` and reconstructs typed
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,eAAe,CAAC;AAO7E,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,MAAM,EACN,GAAG,IAAI,EACR,EAAE,iBAAiB,+
|
|
1
|
+
{"version":3,"file":"field-control.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,qBAAqB,EAAoB,MAAM,eAAe,CAAC;AAO7E,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,qBAAqB,CAAC;IAClC;;;;;;;;;;OAUG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;OAIG;IACH,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;OAGG;IACH,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CAC5C;AAQD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,UAAU,EACV,KAAK,EACL,QAAQ,EACR,EAAE,EACF,MAAM,EACN,GAAG,IAAI,EACR,EAAE,iBAAiB,+BA2EnB"}
|
|
@@ -92,6 +92,8 @@ import { currencyOf } from '../money/money-math.js';
|
|
|
92
92
|
return /*#__PURE__*/ _jsx(TextInput, _extends({
|
|
93
93
|
id: id
|
|
94
94
|
}, aria, {
|
|
95
|
+
// What the field is for, so a browser can fill it (WCAG 1.3.5).
|
|
96
|
+
autoComplete: descriptor.autocomplete,
|
|
95
97
|
type: (_INPUT_TYPE_descriptor_type = INPUT_TYPE[descriptor.type]) != null ? _INPUT_TYPE_descriptor_type : 'text',
|
|
96
98
|
value: value,
|
|
97
99
|
disabled: disabled,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel, useLocale } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\nimport { MoneyInput } from '../money/money-input.js';\nimport { currencyOf } from '../money/money-math.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n /**\n * The record being edited, as its draft. A money member whose currency is\n * another member (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n record,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n // An amount is edited in major units, in the reader's decimal separator,\n // and written back in the unit it is stored in. Without a currency to know\n // the minor units by, it stays a plain number field.\n const currency =\n descriptor.money === undefined\n ? undefined\n : currencyOf(descriptor.money, record);\n if (descriptor.money !== undefined && currency !== undefined) {\n return (\n <MoneyInput\n id={id}\n {...aria}\n value={value}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n disabled={disabled}\n onChange={onChange}\n />\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","useLocale","Checkbox","Select","TextInput","MoneyInput","currencyOf","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","record","aria","enumLabel","locale","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map","currency","money","undefined","unit"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,EAAEC,SAAS,QAAQ,yBAAyB;AACjE,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AAChE,SAASC,UAAU,QAAQ,0BAA0B;AACrD,SAASC,UAAU,QAAQ,yBAAyB;
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/atoms/field-control/field-control.tsx"],"sourcesContent":["'use client';\n\nimport type { EntityFieldDescriptor, MetaAccessorType } from '@entifix/core';\n\nimport { useEnumLabel, useLocale } from '../../../i18n/index.js';\nimport { Checkbox, Select, TextInput } from '../field/index.js';\nimport { MoneyInput } from '../money/money-input.js';\nimport { currencyOf } from '../money/money-math.js';\n\nexport interface FieldControlProps {\n descriptor: EntityFieldDescriptor;\n /**\n * ARIA wiring the caller owns, because only the caller knows the shape it is\n * rendered into.\n *\n * A form labels its control with a `<label for>` and needs none of this; a\n * detail grid's cell has no label of its own — the column header is the\n * label, and the cell's message is the description — so it points at both by\n * id. Passing them through here rather than duplicating the type→input map\n * in the grid is what keeps one control deciding what a `date` member looks\n * like.\n */\n 'aria-labelledby'?: string;\n 'aria-describedby'?: string;\n 'aria-invalid'?: boolean;\n 'aria-required'?: boolean;\n /**\n * The draft value as a string — the shape a native input round-trips. A form\n * holds its whole draft as `Record<string, string>` and reconstructs typed\n * values only at submit, so the control never sees a domain value.\n */\n value: string;\n onChange: (value: string) => void;\n id?: string;\n /**\n * The record being edited, as its draft. A money member whose currency is\n * another member (`money.currencyMember`) reads it from here.\n */\n record?: Readonly<Record<string, unknown>>;\n}\n\n/** Native input type for the text-like controls; text is the default. */\nconst INPUT_TYPE: Partial<Record<MetaAccessorType, string>> = {\n number: 'number',\n date: 'date',\n};\n\n/**\n * The edit-mode inverse of {@link CellValue}: it turns one metadata descriptor\n * into the input that edits it. Centralizing the type→control choice is what\n * lets a form stay generic, the same way `CellValue` keeps a table generic.\n *\n * A relation (`link`/`linkCollection`) is rendered read-only here, deliberately:\n * editing one needs a picker that can search and page the target, which is a\n * different control with different dependencies. So this atom stays a pure\n * type→input map and a relation never falls back to a broken text box.\n *\n * For a to-one `link` that is only half the story — `EntityForm` wires the\n * picker in one level up from its `linkSources`, so the member does get an\n * editor, just not from here. A `linkCollection` has no editor anywhere yet\n * (#26), and read-only is all it gets. A `composition` is the same: its rows\n * need a detail grid, and a text box holding `[object Object]` would be\n * strictly worse than an honest disabled field.\n *\n * A `scalarCollection` is the exception among the collections and falls through\n * to the plain text input on purpose: a `string[]` has a lossless comma form,\n * so the draft it round-trips through *is* editable text. Splitting on the\n * comma is `reconstructEntity`'s half of that same contract.\n */\nexport function FieldControl({\n descriptor,\n value,\n onChange,\n id,\n record,\n ...aria\n}: FieldControlProps) {\n const enumLabel = useEnumLabel();\n const locale = useLocale();\n const disabled =\n descriptor.readonly ||\n descriptor.type === 'link' ||\n descriptor.type === 'linkCollection' ||\n descriptor.type === 'composition';\n\n if (descriptor.type === 'boolean') {\n return (\n <Checkbox\n id={id}\n {...aria}\n label={descriptor.label}\n checked={value === 'true'}\n disabled={disabled}\n onChange={event => onChange(String(event.currentTarget.checked))}\n />\n );\n }\n\n if (descriptor.type === 'enum') {\n return (\n <Select\n id={id}\n {...aria}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n >\n <option value=\"\">—</option>\n {(descriptor.enumValues ?? []).map(option => (\n <option key={option} value={option}>\n {enumLabel(descriptor, option)}\n </option>\n ))}\n </Select>\n );\n }\n\n // An amount is edited in major units, in the reader's decimal separator,\n // and written back in the unit it is stored in. Without a currency to know\n // the minor units by, it stays a plain number field.\n const currency =\n descriptor.money === undefined\n ? undefined\n : currencyOf(descriptor.money, record);\n if (descriptor.money !== undefined && currency !== undefined) {\n return (\n <MoneyInput\n id={id}\n {...aria}\n value={value}\n currency={currency}\n unit={descriptor.money.unit}\n locale={locale}\n disabled={disabled}\n onChange={onChange}\n />\n );\n }\n\n return (\n <TextInput\n id={id}\n {...aria}\n // What the field is for, so a browser can fill it (WCAG 1.3.5).\n autoComplete={descriptor.autocomplete}\n type={INPUT_TYPE[descriptor.type] ?? 'text'}\n value={value}\n disabled={disabled}\n onChange={event => onChange(event.currentTarget.value)}\n />\n );\n}\n"],"names":["useEnumLabel","useLocale","Checkbox","Select","TextInput","MoneyInput","currencyOf","INPUT_TYPE","number","date","FieldControl","descriptor","value","onChange","id","record","aria","enumLabel","locale","disabled","readonly","type","label","checked","event","String","currentTarget","option","enumValues","map","currency","money","undefined","unit","autoComplete","autocomplete"],"mappings":"AAAA;;;;AAIA,SAASA,YAAY,EAAEC,SAAS,QAAQ,yBAAyB;AACjE,SAASC,QAAQ,EAAEC,MAAM,EAAEC,SAAS,QAAQ,oBAAoB;AAChE,SAASC,UAAU,QAAQ,0BAA0B;AACrD,SAASC,UAAU,QAAQ,yBAAyB;AAkCpD,uEAAuE,GACvE,MAAMC,aAAwD;IAC5DC,QAAQ;IACRC,MAAM;AACR;AAEA;;;;;;;;;;;;;;;;;;;;;CAqBC,GACD,OAAO,SAASC,aAAa,EAOT;QAPS,EAC3BC,UAAU,EACVC,KAAK,EACLC,QAAQ,EACRC,EAAE,EACFC,MAAM,EAEY,OADfC;;;;;;;QAsEOT;IApEV,MAAMU,YAAYjB;IAClB,MAAMkB,SAASjB;IACf,MAAMkB,WACJR,WAAWS,QAAQ,IACnBT,WAAWU,IAAI,KAAK,UACpBV,WAAWU,IAAI,KAAK,oBACpBV,WAAWU,IAAI,KAAK;IAEtB,IAAIV,WAAWU,IAAI,KAAK,WAAW;QACjC,qBACE,KAACnB;YACCY,IAAIA;WACAE;YACJM,OAAOX,WAAWW,KAAK;YACvBC,SAASX,UAAU;YACnBO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASY,OAAOD,MAAME,aAAa,CAACH,OAAO;;IAGpE;IAEA,IAAIZ,WAAWU,IAAI,KAAK,QAAQ;YAUxBV;QATN,qBACE,MAACR;YACCW,IAAIA;WACAE;YACJJ,OAAOA;YACPO,UAAUA;YACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;8BAErD,KAACe;oBAAOf,OAAM;8BAAG;;gBAChB,EAACD,yBAAAA,WAAWiB,UAAU,YAArBjB,yBAAyB,EAAE,EAAEkB,GAAG,CAACF,CAAAA,uBACjC,KAACA;wBAAoBf,OAAOe;kCACzBV,UAAUN,YAAYgB;uBADZA;;;IAMrB;IAEA,yEAAyE;IACzE,2EAA2E;IAC3E,qDAAqD;IACrD,MAAMG,WACJnB,WAAWoB,KAAK,KAAKC,YACjBA,YACA1B,WAAWK,WAAWoB,KAAK,EAAEhB;IACnC,IAAIJ,WAAWoB,KAAK,KAAKC,aAAaF,aAAaE,WAAW;QAC5D,qBACE,KAAC3B;YACCS,IAAIA;WACAE;YACJJ,OAAOA;YACPkB,UAAUA;YACVG,MAAMtB,WAAWoB,KAAK,CAACE,IAAI;YAC3Bf,QAAQA;YACRC,UAAUA;YACVN,UAAUA;;IAGhB;IAEA,qBACE,KAACT;QACCU,IAAIA;OACAE;QACJ,gEAAgE;QAChEkB,cAAcvB,WAAWwB,YAAY;QACrCd,IAAI,GAAEd,8BAAAA,UAAU,CAACI,WAAWU,IAAI,CAAC,YAA3Bd,8BAA+B;QACrCK,OAAOA;QACPO,UAAUA;QACVN,UAAUW,CAAAA,QAASX,SAASW,MAAME,aAAa,CAACd,KAAK;;AAG3D"}
|
|
@@ -8,6 +8,13 @@ export interface QuantityInputProps {
|
|
|
8
8
|
step?: number;
|
|
9
9
|
disabled?: boolean;
|
|
10
10
|
id?: string;
|
|
11
|
+
/**
|
|
12
|
+
* Posts the count with the form it sits in, under this name — a server
|
|
13
|
+
* action's form works without JavaScript, where the field is the only part
|
|
14
|
+
* a reader can change, so the field itself carries it. The server clamps
|
|
15
|
+
* what it receives: a typed value is only clamped here once committed.
|
|
16
|
+
*/
|
|
17
|
+
name?: string;
|
|
11
18
|
className?: string;
|
|
12
19
|
}
|
|
13
20
|
/**
|
|
@@ -18,5 +25,5 @@ export interface QuantityInputProps {
|
|
|
18
25
|
* out of the Tab order, as the pattern has them, because the arrow keys
|
|
19
26
|
* already do what they do.
|
|
20
27
|
*/
|
|
21
|
-
export declare function QuantityInput({ label, value, onChange, min, max, step, disabled, id, className, }: QuantityInputProps): import("react").JSX.Element;
|
|
28
|
+
export declare function QuantityInput({ label, value, onChange, min, max, step, disabled, id, name, className, }: QuantityInputProps): import("react").JSX.Element;
|
|
22
29
|
//# sourceMappingURL=quantity-input.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"quantity-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,GAAO,EACP,GAAG,EACH,IAAQ,EACR,QAAgB,EAChB,EAAE,EACF,SAAS,GACV,EAAE,kBAAkB,+
|
|
1
|
+
{"version":3,"file":"quantity-input.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"names":[],"mappings":"AAOA,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAKD;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,EAC5B,KAAK,EACL,KAAK,EACL,QAAQ,EACR,GAAO,EACP,GAAG,EACH,IAAQ,EACR,QAAgB,EAChB,EAAE,EACF,IAAI,EACJ,SAAS,GACV,EAAE,kBAAkB,+BA8EpB"}
|
|
@@ -11,7 +11,7 @@ const STEP_BUTTON = 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 ite
|
|
|
11
11
|
* "Decrease {label}" and "Increase {label}" and sit at `--target-min`; they are
|
|
12
12
|
* out of the Tab order, as the pattern has them, because the arrow keys
|
|
13
13
|
* already do what they do.
|
|
14
|
-
*/ export function QuantityInput({ label, value, onChange, min = 0, max, step = 1, disabled = false, id, className }) {
|
|
14
|
+
*/ export function QuantityInput({ label, value, onChange, min = 0, max, step = 1, disabled = false, id, name, className }) {
|
|
15
15
|
const t = useT();
|
|
16
16
|
const [text, setText] = useState(String(value));
|
|
17
17
|
useEffect(()=>{
|
|
@@ -64,6 +64,7 @@ const STEP_BUTTON = 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 ite
|
|
|
64
64
|
}),
|
|
65
65
|
/*#__PURE__*/ _jsx("input", {
|
|
66
66
|
id: id,
|
|
67
|
+
name: name,
|
|
67
68
|
type: "text",
|
|
68
69
|
role: "spinbutton",
|
|
69
70
|
inputMode: "numeric",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"sourcesContent":["'use client';\n\nimport { type KeyboardEvent, useEffect, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface QuantityInputProps {\n /** What is being counted; names the spinbutton and both of its buttons. */\n label: string;\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n id?: string;\n className?: string;\n}\n\nconst STEP_BUTTON =\n 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md border border-border-control bg-surface-elevated text-content hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50';\n\n/**\n * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and\n * End jump to its bounds, and what is typed is committed on Enter or on\n * leaving the field, clamped to the bounds. The − and + beside it are named\n * \"Decrease {label}\" and \"Increase {label}\" and sit at `--target-min`; they are\n * out of the Tab order, as the pattern has them, because the arrow keys\n * already do what they do.\n */\nexport function QuantityInput({\n label,\n value,\n onChange,\n min = 0,\n max,\n step = 1,\n disabled = false,\n id,\n className,\n}: QuantityInputProps) {\n const t = useT();\n const [text, setText] = useState(String(value));\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)\n setText(String(value));\n }, [value]);\n\n const clamp = (next: number) =>\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, next));\n const set = (next: number) => {\n const clamped = clamp(next);\n setText(String(clamped));\n if (clamped !== value) onChange(clamped);\n };\n const commit = () => {\n const typed = Number.parseInt(text, 10);\n set(Number.isNaN(typed) ? value : typed);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const next = {\n ArrowUp: value + step,\n ArrowDown: value - step,\n Home: min,\n End: max,\n }[event.key];\n if (event.key === 'Enter') {\n commit();\n return;\n }\n if (next === undefined) return;\n event.preventDefault();\n set(next);\n };\n\n return (\n <span className={cn('inline-flex items-center gap-3xs', className)}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.decrease', { label })}\n disabled={disabled || value <= min}\n onClick={() => set(value - step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">−</span>\n </button>\n <input\n id={id}\n type=\"text\"\n role=\"spinbutton\"\n inputMode=\"numeric\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n disabled={disabled}\n value={text}\n onChange={event => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={onKeyDown}\n className=\"w-16 min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs text-center text-step-sm text-content tabular-nums focus-ring disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.increase', { label })}\n disabled={disabled || (max !== undefined && value >= max)}\n onClick={() => set(value + step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n </span>\n );\n}\n"],"names":["useEffect","useState","useT","cn","STEP_BUTTON","QuantityInput","label","value","onChange","min","max","step","disabled","id","className","t","text","setText","String","clamp","next","Math","Number","POSITIVE_INFINITY","set","clamped","commit","typed","parseInt","isNaN","onKeyDown","event","ArrowUp","ArrowDown","Home","End","key","undefined","preventDefault","span","button","type","tabIndex","aria-label","onClick","aria-hidden","input","role","inputMode","aria-valuenow","aria-valuemin","aria-valuemax","currentTarget","onBlur"],"mappings":"AAAA;;AAEA,SAA6BA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAEhE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;
|
|
1
|
+
{"version":3,"sources":["../../../../src/ui/atoms/number-input/quantity-input.tsx"],"sourcesContent":["'use client';\n\nimport { type KeyboardEvent, useEffect, useState } from 'react';\n\nimport { useT } from '../../../i18n/index.js';\nimport { cn } from '../../utils/cn.js';\n\nexport interface QuantityInputProps {\n /** What is being counted; names the spinbutton and both of its buttons. */\n label: string;\n value: number;\n onChange: (value: number) => void;\n min?: number;\n max?: number;\n step?: number;\n disabled?: boolean;\n id?: string;\n /**\n * Posts the count with the form it sits in, under this name — a server\n * action's form works without JavaScript, where the field is the only part\n * a reader can change, so the field itself carries it. The server clamps\n * what it receives: a typed value is only clamped here once committed.\n */\n name?: string;\n className?: string;\n}\n\nconst STEP_BUTTON =\n 'flex min-h-(--target-min) min-w-(--target-min) shrink-0 items-center justify-center rounded-md border border-border-control bg-surface-elevated text-content hover:bg-surface disabled:cursor-not-allowed disabled:opacity-50';\n\n/**\n * A count, on the WAI-ARIA spinbutton pattern: Up and Down step it, Home and\n * End jump to its bounds, and what is typed is committed on Enter or on\n * leaving the field, clamped to the bounds. The − and + beside it are named\n * \"Decrease {label}\" and \"Increase {label}\" and sit at `--target-min`; they are\n * out of the Tab order, as the pattern has them, because the arrow keys\n * already do what they do.\n */\nexport function QuantityInput({\n label,\n value,\n onChange,\n min = 0,\n max,\n step = 1,\n disabled = false,\n id,\n name,\n className,\n}: QuantityInputProps) {\n const t = useT();\n const [text, setText] = useState(String(value));\n\n useEffect(() => {\n // eslint-disable-next-line react-hooks/set-state-in-effect -- the value moved from outside (a step, a reset)\n setText(String(value));\n }, [value]);\n\n const clamp = (next: number) =>\n Math.min(max ?? Number.POSITIVE_INFINITY, Math.max(min, next));\n const set = (next: number) => {\n const clamped = clamp(next);\n setText(String(clamped));\n if (clamped !== value) onChange(clamped);\n };\n const commit = () => {\n const typed = Number.parseInt(text, 10);\n set(Number.isNaN(typed) ? value : typed);\n };\n\n const onKeyDown = (event: KeyboardEvent<HTMLInputElement>) => {\n const next = {\n ArrowUp: value + step,\n ArrowDown: value - step,\n Home: min,\n End: max,\n }[event.key];\n if (event.key === 'Enter') {\n commit();\n return;\n }\n if (next === undefined) return;\n event.preventDefault();\n set(next);\n };\n\n return (\n <span className={cn('inline-flex items-center gap-3xs', className)}>\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.decrease', { label })}\n disabled={disabled || value <= min}\n onClick={() => set(value - step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">−</span>\n </button>\n <input\n id={id}\n name={name}\n type=\"text\"\n role=\"spinbutton\"\n inputMode=\"numeric\"\n aria-label={label}\n aria-valuenow={value}\n aria-valuemin={min}\n aria-valuemax={max}\n disabled={disabled}\n value={text}\n onChange={event => setText(event.currentTarget.value)}\n onBlur={commit}\n onKeyDown={onKeyDown}\n className=\"w-16 min-h-(--target-min) rounded-lg border border-border-control bg-surface-elevated px-2xs py-3xs text-center text-step-sm text-content tabular-nums focus-ring disabled:cursor-not-allowed disabled:opacity-50\"\n />\n <button\n type=\"button\"\n tabIndex={-1}\n aria-label={t('quantity.increase', { label })}\n disabled={disabled || (max !== undefined && value >= max)}\n onClick={() => set(value + step)}\n className={STEP_BUTTON}\n >\n <span aria-hidden=\"true\">+</span>\n </button>\n </span>\n );\n}\n"],"names":["useEffect","useState","useT","cn","STEP_BUTTON","QuantityInput","label","value","onChange","min","max","step","disabled","id","name","className","t","text","setText","String","clamp","next","Math","Number","POSITIVE_INFINITY","set","clamped","commit","typed","parseInt","isNaN","onKeyDown","event","ArrowUp","ArrowDown","Home","End","key","undefined","preventDefault","span","button","type","tabIndex","aria-label","onClick","aria-hidden","input","role","inputMode","aria-valuenow","aria-valuemin","aria-valuemax","currentTarget","onBlur"],"mappings":"AAAA;;AAEA,SAA6BA,SAAS,EAAEC,QAAQ,QAAQ,QAAQ;AAEhE,SAASC,IAAI,QAAQ,yBAAyB;AAC9C,SAASC,EAAE,QAAQ,oBAAoB;AAsBvC,MAAMC,cACJ;AAEF;;;;;;;CAOC,GACD,OAAO,SAASC,cAAc,EAC5BC,KAAK,EACLC,KAAK,EACLC,QAAQ,EACRC,MAAM,CAAC,EACPC,GAAG,EACHC,OAAO,CAAC,EACRC,WAAW,KAAK,EAChBC,EAAE,EACFC,IAAI,EACJC,SAAS,EACU;IACnB,MAAMC,IAAId;IACV,MAAM,CAACe,MAAMC,QAAQ,GAAGjB,SAASkB,OAAOZ;IAExCP,UAAU;QACR,6GAA6G;QAC7GkB,QAAQC,OAAOZ;IACjB,GAAG;QAACA;KAAM;IAEV,MAAMa,QAAQ,CAACC,OACbC,KAAKb,GAAG,CAACC,cAAAA,MAAOa,OAAOC,iBAAiB,EAAEF,KAAKZ,GAAG,CAACD,KAAKY;IAC1D,MAAMI,MAAM,CAACJ;QACX,MAAMK,UAAUN,MAAMC;QACtBH,QAAQC,OAAOO;QACf,IAAIA,YAAYnB,OAAOC,SAASkB;IAClC;IACA,MAAMC,SAAS;QACb,MAAMC,QAAQL,OAAOM,QAAQ,CAACZ,MAAM;QACpCQ,IAAIF,OAAOO,KAAK,CAACF,SAASrB,QAAQqB;IACpC;IAEA,MAAMG,YAAY,CAACC;QACjB,MAAMX,OAAO;YACXY,SAAS1B,QAAQI;YACjBuB,WAAW3B,QAAQI;YACnBwB,MAAM1B;YACN2B,KAAK1B;QACP,CAAC,CAACsB,MAAMK,GAAG,CAAC;QACZ,IAAIL,MAAMK,GAAG,KAAK,SAAS;YACzBV;YACA;QACF;QACA,IAAIN,SAASiB,WAAW;QACxBN,MAAMO,cAAc;QACpBd,IAAIJ;IACN;IAEA,qBACE,MAACmB;QAAKzB,WAAWZ,GAAG,oCAAoCY;;0BACtD,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAEV;gBAAM;gBAC3CM,UAAUA,YAAYL,SAASE;gBAC/BoC,SAAS,IAAMpB,IAAIlB,QAAQI;gBAC3BI,WAAWX;0BAEX,cAAA,KAACoC;oBAAKM,eAAY;8BAAO;;;0BAE3B,KAACC;gBACClC,IAAIA;gBACJC,MAAMA;gBACN4B,MAAK;gBACLM,MAAK;gBACLC,WAAU;gBACVL,cAAYtC;gBACZ4C,iBAAe3C;gBACf4C,iBAAe1C;gBACf2C,iBAAe1C;gBACfE,UAAUA;gBACVL,OAAOU;gBACPT,UAAUwB,CAAAA,QAASd,QAAQc,MAAMqB,aAAa,CAAC9C,KAAK;gBACpD+C,QAAQ3B;gBACRI,WAAWA;gBACXhB,WAAU;;0BAEZ,KAAC0B;gBACCC,MAAK;gBACLC,UAAU,CAAC;gBACXC,cAAY5B,EAAE,qBAAqB;oBAAEV;gBAAM;gBAC3CM,UAAUA,YAAaF,QAAQ4B,aAAa/B,SAASG;gBACrDmC,SAAS,IAAMpB,IAAIlB,QAAQI;gBAC3BI,WAAWX;0BAEX,cAAA,KAACoC;oBAAKM,eAAY;8BAAO;;;;;AAIjC"}
|
|
@@ -1,16 +1,25 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
2
|
export interface TableProps extends ComponentPropsWithoutRef<'table'> {
|
|
3
3
|
/** Class names for the scroll container wrapping the `<table>`. */
|
|
4
4
|
containerClassName?: string;
|
|
5
|
+
/**
|
|
6
|
+
* What the table holds — "Clientes, 12 resultados". It names the table and,
|
|
7
|
+
* once the table overflows its container, the scroll region too.
|
|
8
|
+
*/
|
|
9
|
+
caption?: ReactNode;
|
|
10
|
+
/** Class names for the caption; screen-reader only unless overridden. */
|
|
11
|
+
captionClassName?: string;
|
|
5
12
|
}
|
|
6
13
|
/**
|
|
7
14
|
* Styled table primitive — no entity knowledge, no data fetching. `EntityTable`
|
|
8
15
|
* composes these; anything rendering non-entity tabular data can use them too.
|
|
9
16
|
*
|
|
10
17
|
* Wide tables scroll inside their own container rather than pushing the page
|
|
11
|
-
* sideways.
|
|
18
|
+
* sideways. A container that scrolls is a **focusable region** named by the
|
|
19
|
+
* caption, so a keyboard can scroll it — and one that fits is neither, so it
|
|
20
|
+
* adds no tab stop that does nothing.
|
|
12
21
|
*/
|
|
13
|
-
export declare function Table({ className, containerClassName, ...props }: TableProps): import("react").JSX.Element;
|
|
22
|
+
export declare function Table({ className, containerClassName, caption, captionClassName, children, ...props }: TableProps): import("react").JSX.Element;
|
|
14
23
|
export declare function TableHead({ className, ...props }: ComponentPropsWithoutRef<'thead'>): import("react").JSX.Element;
|
|
15
24
|
export declare function TableBody({ className, ...props }: ComponentPropsWithoutRef<'tbody'>): import("react").JSX.Element;
|
|
16
25
|
export declare function TableRow({ className, ...props }: ComponentPropsWithoutRef<'tr'>): import("react").JSX.Element;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/table/table.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"table.d.ts","sourceRoot":"","sources":["../../../../src/ui/atoms/table/table.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,wBAAwB,EAC7B,KAAK,SAAS,EAMf,MAAM,OAAO,CAAC;AAIf,MAAM,WAAW,UAAW,SAAQ,wBAAwB,CAAC,OAAO,CAAC;IACnE,mEAAmE;IACnE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B;;;OAGG;IACH,OAAO,CAAC,EAAE,SAAS,CAAC;IACpB,yEAAyE;IACzE,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAyBD;;;;;;;;GAQG;AACH,wBAAgB,KAAK,CAAC,EACpB,SAAS,EACT,kBAAkB,EAClB,OAAO,EACP,gBAA4B,EAC5B,QAAQ,EACR,GAAG,KAAK,EACT,EAAE,UAAU,+BAoCZ;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,OAAO,CAAC,+BAanC;AAED,wBAAgB,SAAS,CAAC,EACxB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,OAAO,CAAC,+BAInC;AAED,wBAAgB,QAAQ,CAAC,EACvB,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,IAAI,CAAC,+BAUhC;AAED,MAAM,MAAM,cAAc,GAAG,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;AAUxD,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAChD,wBAAwB,CAAC,IAAI,CAAC,EAC9B,OAAO,CACR;IACC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,eAAe,CAAC,EAC9B,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,oBAAoB,+BAYtB;AAED,MAAM,WAAW,cAAe,SAAQ,IAAI,CAC1C,wBAAwB,CAAC,IAAI,CAAC,EAC9B,OAAO,CACR;IACC,KAAK,CAAC,EAAE,cAAc,CAAC;CACxB;AAED,wBAAgB,SAAS,CAAC,EACxB,KAAe,EACf,SAAS,EACT,GAAG,KAAK,EACT,EAAE,cAAc,+BAOhB;AAED,yEAAyE;AACzE,wBAAgB,eAAe,CAAC,EAC9B,OAAO,EACP,SAAS,EACT,GAAG,KAAK,EACT,EAAE,wBAAwB,CAAC,IAAI,CAAC,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,+BAatD"}
|
|
@@ -1,31 +1,80 @@
|
|
|
1
|
+
'use client';
|
|
1
2
|
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
3
|
import { _ as _object_without_properties_loose } from "@swc/helpers/_/_object_without_properties_loose";
|
|
3
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
4
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { useEffect, useId, useRef, useState } from 'react';
|
|
4
6
|
import { cn } from '../../utils/cn.js';
|
|
7
|
+
/**
|
|
8
|
+
* Whether an element's content is larger than its box, on either axis —
|
|
9
|
+
* measured again whenever the box or the content it holds resizes.
|
|
10
|
+
*/ function useOverflows(ref) {
|
|
11
|
+
const [overflows, setOverflows] = useState(false);
|
|
12
|
+
useEffect(()=>{
|
|
13
|
+
const node = ref.current;
|
|
14
|
+
const measure = ()=>setOverflows(node.scrollWidth > node.clientWidth || node.scrollHeight > node.clientHeight);
|
|
15
|
+
measure();
|
|
16
|
+
const observer = new ResizeObserver(measure);
|
|
17
|
+
observer.observe(node);
|
|
18
|
+
// The table grows with its rows while the container keeps its size.
|
|
19
|
+
for (const child of Array.from(node.children))observer.observe(child);
|
|
20
|
+
return ()=>observer.disconnect();
|
|
21
|
+
}, [
|
|
22
|
+
ref
|
|
23
|
+
]);
|
|
24
|
+
return overflows;
|
|
25
|
+
}
|
|
5
26
|
/**
|
|
6
27
|
* Styled table primitive — no entity knowledge, no data fetching. `EntityTable`
|
|
7
28
|
* composes these; anything rendering non-entity tabular data can use them too.
|
|
8
29
|
*
|
|
9
30
|
* Wide tables scroll inside their own container rather than pushing the page
|
|
10
|
-
* sideways.
|
|
31
|
+
* sideways. A container that scrolls is a **focusable region** named by the
|
|
32
|
+
* caption, so a keyboard can scroll it — and one that fits is neither, so it
|
|
33
|
+
* adds no tab stop that does nothing.
|
|
11
34
|
*/ export function Table(_0) {
|
|
12
|
-
let { className, containerClassName } = _0, props = _object_without_properties_loose(_0, [
|
|
35
|
+
let { className, containerClassName, caption, captionClassName = 'sr-only', children } = _0, props = _object_without_properties_loose(_0, [
|
|
13
36
|
"className",
|
|
14
|
-
"containerClassName"
|
|
37
|
+
"containerClassName",
|
|
38
|
+
"caption",
|
|
39
|
+
"captionClassName",
|
|
40
|
+
"children"
|
|
15
41
|
]);
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
42
|
+
const container = useRef(null);
|
|
43
|
+
const captionId = useId();
|
|
44
|
+
const overflows = useOverflows(container);
|
|
45
|
+
const named = caption !== undefined;
|
|
46
|
+
return /*#__PURE__*/ _jsx("div", _extends({
|
|
47
|
+
ref: container,
|
|
48
|
+
"data-overflows": overflows || undefined
|
|
49
|
+
}, overflows && {
|
|
50
|
+
tabIndex: 0,
|
|
51
|
+
role: named ? 'region' : undefined,
|
|
52
|
+
'aria-labelledby': named ? captionId : undefined
|
|
53
|
+
}, {
|
|
54
|
+
className: cn('w-full overflow-auto rounded-xl border border-border bg-surface-elevated focus-ring', containerClassName),
|
|
55
|
+
children: /*#__PURE__*/ _jsxs("table", _extends({
|
|
19
56
|
className: cn('w-full border-collapse text-step-0 text-content', className)
|
|
20
|
-
}, props
|
|
21
|
-
|
|
57
|
+
}, props, {
|
|
58
|
+
children: [
|
|
59
|
+
named && /*#__PURE__*/ _jsx("caption", {
|
|
60
|
+
id: captionId,
|
|
61
|
+
className: captionClassName,
|
|
62
|
+
children: caption
|
|
63
|
+
}),
|
|
64
|
+
children
|
|
65
|
+
]
|
|
66
|
+
}))
|
|
67
|
+
}));
|
|
22
68
|
}
|
|
23
69
|
export function TableHead(_0) {
|
|
24
70
|
let { className } = _0, props = _object_without_properties_loose(_0, [
|
|
25
71
|
"className"
|
|
26
72
|
]);
|
|
27
73
|
return /*#__PURE__*/ _jsx("thead", _extends({
|
|
28
|
-
|
|
74
|
+
// Sticky inside a container that scrolls: the column names stay in view
|
|
75
|
+
// over a long page of rows. One layer above the rows, and no higher: a
|
|
76
|
+
// row menu opened under it must still paint over it.
|
|
77
|
+
className: cn('top-0 sticky z-1 border-b border-border bg-surface', className)
|
|
29
78
|
}, props));
|
|
30
79
|
}
|
|
31
80
|
export function TableBody(_0) {
|