@sincpro/web-ui 1.7.0 → 1.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/lines/LineItems.js +55 -45
- package/dist/foundations/form.d.ts +25 -0
- package/dist/foundations/form.js +27 -0
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/index.js +1 -1
- package/dist/primitives/choice/ComboBox.js +2 -2
- package/dist/primitives/choice/SearchField.js +2 -5
- package/dist/primitives/choice/SegmentedControl.js +2 -2
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/date/DatePicker.js +4 -4
- package/dist/primitives/date/DateTime.js +3 -3
- package/dist/primitives/field/Field.d.ts +2 -1
- package/dist/primitives/field/Field.js +2 -2
- package/dist/primitives/field/Suggest.js +2 -2
- package/dist/primitives/picker/Picker.js +2 -2
- package/dist/primitives/picker/TagsField.js +2 -2
- package/dist/primitives/table/DataTable.d.ts +1 -1
- package/dist/primitives/table/DataTable.js +10 -4
- package/dist/primitives/table/EditableTable.js +102 -99
- package/package.json +1 -1
|
@@ -4,7 +4,7 @@ import { commitDraft, filterLines, planRemoval, } from "./lines.js";
|
|
|
4
4
|
import { useColumnControl, } from "../listing/columns.js";
|
|
5
5
|
import { ColumnsMenu } from "../listing/ColumnsMenu.js";
|
|
6
6
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
7
|
-
import { FieldIdsContext } from "../../foundations/form.js";
|
|
7
|
+
import { FieldBoundary, FieldIdsContext } from "../../foundations/form.js";
|
|
8
8
|
import { useFormMode } from "../../foundations/form.js";
|
|
9
9
|
import { Button, IconButton } from "../../primitives/button/Button.js";
|
|
10
10
|
import { SearchField } from "../../primitives/choice/SearchField.js";
|
|
@@ -228,7 +228,14 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
|
|
|
228
228
|
...(c.stack === undefined ? {} : { stack: c.stack }),
|
|
229
229
|
...(c.brief === undefined ? {} : { brief: c.brief }),
|
|
230
230
|
render: (line) => {
|
|
231
|
-
|
|
231
|
+
/* `cell` draws it; without one, `text` is how it READS — the raw `value` is what the
|
|
232
|
+
search and the sort compare («3» is a unit id; «caja ×12» is what it says). The
|
|
233
|
+
same order `DataTable` follows. */
|
|
234
|
+
const drawn = c.cell !== undefined
|
|
235
|
+
? c.cell(line)
|
|
236
|
+
: c.text !== undefined
|
|
237
|
+
? c.text(line)
|
|
238
|
+
: plain(c.value(line));
|
|
232
239
|
/* An amount never breaks in two: «Bs.» over «12.500,00» reads as two values. */
|
|
233
240
|
return c.numeric === true ? _jsx("span", { className: "whitespace-nowrap", children: drawn }) : drawn;
|
|
234
241
|
},
|
|
@@ -273,47 +280,50 @@ export function LineItems({ lines, columns, getLineId, lineLabel, mode, onChange
|
|
|
273
280
|
shown: shown.length.toLocaleString(copy.locale),
|
|
274
281
|
count: lineCount.toLocaleString(copy.locale),
|
|
275
282
|
});
|
|
276
|
-
return (
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
{
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
283
|
+
return (
|
|
284
|
+
/* A collection is many controls, each named by its column: no enclosing `Field`
|
|
285
|
+
reaches inside (see `FieldBoundary`). */
|
|
286
|
+
_jsx(FieldBoundary, { children: _jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 && !canAdd ? (
|
|
287
|
+
/* Said, with its way out: an empty document is a question («what is sold
|
|
288
|
+
here?»), not a blank box. In read mode there is no way out to offer. */
|
|
289
|
+
_jsx(Empty, { align: "start", sentence: copy.lines.empty, action: addButton ?? undefined })) : (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex flex-wrap items-center gap-x-3 gap-y-2 px-1", children: [searchable ? (_jsxs("div", { className: "w-full max-w-(--container-search) min-w-0 flex-1", children: [_jsx("span", { id: searchLabelId, className: "sr-only", children: copy.lines.search }), _jsx(FieldIdsContext.Provider, { value: {
|
|
290
|
+
id: `${searchLabelId}-input`,
|
|
291
|
+
labelId: searchLabelId,
|
|
292
|
+
describedBy: undefined,
|
|
293
|
+
invalid: false,
|
|
294
|
+
}, children: _jsx(SearchField, { value: query, onChange: setQuery, placeholder: copy.lines.search }) })] })) : null, _jsx("span", { className: "text-meta text-muted tabular-nums", "aria-live": "polite", children: count }), columnControl.hasOptional || addButton !== null ? (_jsxs("span", { className: "ml-auto flex items-center gap-2", children: [_jsx(ColumnsMenu, { control: columnControl }), addButton] })) : null] }), _jsx(EditableTable, { rows: shown, columns: tableColumns, getRowId: getLineId, empty: lines.length === 0
|
|
295
|
+
? copy.lines.empty
|
|
296
|
+
: fillCopy(copy.lineItems.noMatch, { query: query.trim() }), layout: layout, ...(inline ? { onAdd: () => addInline(), addLabel: copy.lines.add } : {}), ...(bandAdds.length === 0 ? {} : { adds: bandAdds }), ...(bands === undefined ? {} : { bands }), ...(flag === undefined ? {} : { flag }), ...(cellState === undefined ? {} : { cellState }), ...(rowState === undefined ? {} : { rowState }), ...(onReorder === undefined || read ? {} : { onReorder }), ...(inPlace && onRemove !== undefined
|
|
297
|
+
? /* A blank row nobody wrote in is taken back without a notice: nobody
|
|
298
|
+
removed anything, so there is nothing to undo. */
|
|
299
|
+
{ onDiscard: onRemove }
|
|
300
|
+
: {}), ...(canEdit
|
|
301
|
+
? {
|
|
302
|
+
onOpen: (id) => {
|
|
303
|
+
const line = lines.find((l) => getLineId(l) === id);
|
|
304
|
+
/* A read-only line opens no form to change it. */
|
|
305
|
+
if (line !== undefined && rowState?.(line)?.readonly !== true) {
|
|
306
|
+
openLine(id, line);
|
|
307
|
+
}
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
: {}), ...(canChange && columns.some((c) => c.edit !== undefined)
|
|
311
|
+
? { onChange: (id, next) => onChange?.(id, next) }
|
|
312
|
+
: {}) })] })), totals === undefined || ("length" in totals && totals.length === 0) ? null : (
|
|
313
|
+
/* Inset like the table's cells, so the last figure lines up with the amounts. */
|
|
314
|
+
_jsx("div", { className: "px-4", children: "length" in totals ? _jsx(Totals, { rows: totals }) : _jsx(Totals, { ...totals }) })), editor === undefined || session === null ? null : (_jsx(Dialog, { open: open, width: "sm", title: session.id === null ? copy.lineItems.newLine : copy.lines.edit, ...(context === undefined ? {} : { context }), ...(session.id === null ? {} : { subtitle: labelOf(session.draft) }), onClose: () => setOpen(false), footer: _jsxs(_Fragment, { children: [session.id === null || !canRemove ? null : (
|
|
315
|
+
/* Apart, at the other end from «Listo»: removing is not the way the
|
|
316
|
+
form is meant to close. */
|
|
317
|
+
_jsx("span", { className: "mr-auto", children: _jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", onClick: () => {
|
|
318
|
+
const line = lines.find((l) => getLineId(l) === session.id);
|
|
319
|
+
setOpen(false);
|
|
320
|
+
if (line !== undefined)
|
|
321
|
+
remove(line);
|
|
322
|
+
} }) })), _jsx(Button, { role: "ghost", size: "sm", onClick: () => setOpen(false), children: copy.dialog.cancel }), _jsx(Button, { role: "primary", size: "sm", type: "submit", form: formId, children: copy.lines.done })] }), children: _jsx("form", { id: formId, noValidate: true, onSubmit: (e) => {
|
|
323
|
+
e.preventDefault();
|
|
324
|
+
save();
|
|
325
|
+
}, className: "flex flex-col gap-3", children: editor(session.draft, setDraft, {
|
|
326
|
+
tried: session.tried,
|
|
327
|
+
isNew: session.id === null,
|
|
328
|
+
}) }) }))] }) }));
|
|
319
329
|
}
|
|
@@ -34,6 +34,19 @@ export type ControlState = {
|
|
|
34
34
|
invalid?: boolean;
|
|
35
35
|
};
|
|
36
36
|
export declare const ControlStateContext: import("react").Context<ControlState | null>;
|
|
37
|
+
/**
|
|
38
|
+
* Where a `Field` stops reaching: what is inside belongs to no field.
|
|
39
|
+
*
|
|
40
|
+
* Context: a `Field` hands its id and its label to the control inside it through context —
|
|
41
|
+
* and context goes all the way down. An editable table placed inside a field (the lines of a
|
|
42
|
+
* one2many under its label) gave EVERY cell's control the field's id and `aria-labelledby`:
|
|
43
|
+
* forty inputs with the same id, all named «Líneas». A collection is many controls, each
|
|
44
|
+
* named by its column, so the tables put this around themselves; an application's own
|
|
45
|
+
* composite control does the same.
|
|
46
|
+
*/
|
|
47
|
+
export declare function FieldBoundary({ children }: {
|
|
48
|
+
children: ReactNode;
|
|
49
|
+
}): import("react").JSX.Element;
|
|
37
50
|
/** The ids of the enclosing `Field` — or, outside one, the state of the place the control
|
|
38
51
|
* sits in (a cell) — resolved against the control's own props. */
|
|
39
52
|
export declare function useFieldIds(own: {
|
|
@@ -46,6 +59,18 @@ export declare function useFieldIds(own: {
|
|
|
46
59
|
describedBy: string | undefined;
|
|
47
60
|
invalid: boolean;
|
|
48
61
|
};
|
|
62
|
+
/**
|
|
63
|
+
* The `aria-label` a control puts on itself: none inside a `Field` — the field's label names
|
|
64
|
+
* it — and outside one the control's own, or as a last resort its placeholder (what a browser
|
|
65
|
+
* reads for a bare `<input placeholder>` anyway, so a control outside a form is never
|
|
66
|
+
* nameless).
|
|
67
|
+
*
|
|
68
|
+
* Context: a control inside a `Field` used to keep its `aria-label` too. With both, React Aria
|
|
69
|
+
* names the control by the label AND itself («Cliente Cliente», «Cantidad cantidad»), so the
|
|
70
|
+
* name was read twice. The field wins: an `aria-label` is for a control that has no label
|
|
71
|
+
* on screen — a table cell, a search box — and inside a field it is ignored.
|
|
72
|
+
*/
|
|
73
|
+
export declare function controlName(labelledBy: string | undefined, ariaLabel: string | undefined, fallback?: string): string | undefined;
|
|
49
74
|
export type FormMode = "edit" | "read";
|
|
50
75
|
export declare function FormModeProvider({ value, children, }: {
|
|
51
76
|
value: FormMode;
|
package/dist/foundations/form.js
CHANGED
|
@@ -3,6 +3,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { createContext, useContext } from "react";
|
|
4
4
|
export const FieldIdsContext = createContext(null);
|
|
5
5
|
export const ControlStateContext = createContext(null);
|
|
6
|
+
/**
|
|
7
|
+
* Where a `Field` stops reaching: what is inside belongs to no field.
|
|
8
|
+
*
|
|
9
|
+
* Context: a `Field` hands its id and its label to the control inside it through context —
|
|
10
|
+
* and context goes all the way down. An editable table placed inside a field (the lines of a
|
|
11
|
+
* one2many under its label) gave EVERY cell's control the field's id and `aria-labelledby`:
|
|
12
|
+
* forty inputs with the same id, all named «Líneas». A collection is many controls, each
|
|
13
|
+
* named by its column, so the tables put this around themselves; an application's own
|
|
14
|
+
* composite control does the same.
|
|
15
|
+
*/
|
|
16
|
+
export function FieldBoundary({ children }) {
|
|
17
|
+
return _jsx(FieldIdsContext.Provider, { value: null, children: children });
|
|
18
|
+
}
|
|
6
19
|
/** The ids of the enclosing `Field` — or, outside one, the state of the place the control
|
|
7
20
|
* sits in (a cell) — resolved against the control's own props. */
|
|
8
21
|
export function useFieldIds(own) {
|
|
@@ -15,6 +28,20 @@ export function useFieldIds(own) {
|
|
|
15
28
|
invalid: own.invalid ?? field?.invalid ?? place?.invalid ?? false,
|
|
16
29
|
};
|
|
17
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* The `aria-label` a control puts on itself: none inside a `Field` — the field's label names
|
|
33
|
+
* it — and outside one the control's own, or as a last resort its placeholder (what a browser
|
|
34
|
+
* reads for a bare `<input placeholder>` anyway, so a control outside a form is never
|
|
35
|
+
* nameless).
|
|
36
|
+
*
|
|
37
|
+
* Context: a control inside a `Field` used to keep its `aria-label` too. With both, React Aria
|
|
38
|
+
* names the control by the label AND itself («Cliente Cliente», «Cantidad cantidad»), so the
|
|
39
|
+
* name was read twice. The field wins: an `aria-label` is for a control that has no label
|
|
40
|
+
* on screen — a table cell, a search box — and inside a field it is ignored.
|
|
41
|
+
*/
|
|
42
|
+
export function controlName(labelledBy, ariaLabel, fallback) {
|
|
43
|
+
return labelledBy === undefined ? (ariaLabel ?? fallback) : undefined;
|
|
44
|
+
}
|
|
18
45
|
const Ctx = createContext("edit");
|
|
19
46
|
export function FormModeProvider({ value, children, }) {
|
|
20
47
|
return _jsx(Ctx.Provider, { value: value, children: children });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { type CopyCatalog, type CopyOverride, createCopy, mergeCopyOver, } from "./catalog.js";
|
|
2
2
|
export { currentCopy, currentCopyLocale, ENGLISH_COPY, fillCopy, installCopy, mergeCopy, type UiCopy, type UiCopyOverride, UiCopyProvider, useUiCopy, } from "./copy.js";
|
|
3
3
|
export { byDensity, type Density, DensityProvider, useDensity, } from "./density.js";
|
|
4
|
-
export { type FieldIds, type FormMode, FormModeProvider, useFieldIds, useFormMode, } from "./form.js";
|
|
4
|
+
export { controlName, FieldBoundary, type FieldIds, type FormMode, FormModeProvider, useFieldIds, useFormMode, } from "./form.js";
|
|
5
5
|
export { bySurface, SurfaceProvider, useSurface } from "./surface.js";
|
|
6
6
|
export { applyTheme, isTheme, type ResolvedTheme, resolveTheme, type Theme, THEME_ATTRIBUTE, ThemeProvider, type ThemeProviderProps, THEMES, useTheme, } from "./theme.js";
|
|
7
7
|
export type { OpenTarget, Size, Surface, Tone } from "./types.js";
|
|
@@ -5,7 +5,7 @@ export { currentCopy, currentCopyLocale, ENGLISH_COPY, fillCopy, installCopy, me
|
|
|
5
5
|
export { byDensity, DensityProvider, useDensity, } from "./density.js";
|
|
6
6
|
/* EL CONTRATO DE UN CONTROL — los ids que le da su `Field` y si se mira o se escribe. Lo
|
|
7
7
|
hablan los controles del paquete y los de la aplicación por igual. */
|
|
8
|
-
export { FormModeProvider, useFieldIds, useFormMode, } from "./form.js";
|
|
8
|
+
export { controlName, FieldBoundary, FormModeProvider, useFieldIds, useFormMode, } from "./form.js";
|
|
9
9
|
/* EL TEMA — claro, oscuro o el del sistema operativo. Las paletas viven en `tokens.css`; esto
|
|
10
10
|
sólo escribe `data-theme`. */
|
|
11
11
|
export { bySurface, SurfaceProvider, useSurface } from "./surface.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
4
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
5
5
|
import { useFormMode } from "../../foundations/form.js";
|
|
6
6
|
import { labelOf } from "./options.js";
|
|
7
7
|
import { FieldButton, FieldGroup, listOptionStyles, } from "../field/aria.js";
|
|
@@ -48,7 +48,7 @@ function EditableComboBox({ value, onChange, options, placeholder, disabled = fa
|
|
|
48
48
|
invalid: ownInvalid,
|
|
49
49
|
});
|
|
50
50
|
const { contains } = useFilter({ sensitivity: "base" });
|
|
51
|
-
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy,
|
|
51
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy,
|
|
52
52
|
/* `defaultItems` and not `items`: with `items` React Aria hands the filtering to the
|
|
53
53
|
consumer. These are the same options every render — only the filtering is React
|
|
54
54
|
Aria's. */
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
5
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
6
6
|
import { FieldButton, FieldGroup } from "../field/aria.js";
|
|
7
7
|
import { Search, X } from "lucide-react";
|
|
8
8
|
import { Input, SearchField as AriaSearchField } from "react-aria-components";
|
|
@@ -38,9 +38,6 @@ export function SearchField({ value, onChange, placeholder, onSubmit, disabled =
|
|
|
38
38
|
describedBy: ownDescribedBy,
|
|
39
39
|
});
|
|
40
40
|
/* A `Field` names it through its label; anything else falls back so it always has one. */
|
|
41
|
-
const name = ariaLabel ??
|
|
42
|
-
(labelledBy === undefined
|
|
43
|
-
? (nameFromPlaceholder(placeholder) ?? copy.choice.search)
|
|
44
|
-
: undefined);
|
|
41
|
+
const name = controlName(labelledBy, ariaLabel, nameFromPlaceholder(placeholder) ?? copy.choice.search);
|
|
45
42
|
return (_jsx(AriaLocale, { children: _jsx(AriaSearchField, { id: id, "aria-labelledby": labelledBy, "aria-label": name, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), onSubmit: onSubmit, isDisabled: disabled, className: "group w-full min-w-0", children: _jsxs(FieldGroup, { children: [_jsx(Search, { "aria-hidden": true, className: "ml-2.5 size-(--size-icon-md) shrink-0 text-muted forced-colors:text-[ButtonText]", strokeWidth: 2 }), _jsx(Input, { placeholder: placeholder, className: "h-full min-w-0 flex-1 bg-transparent px-2 text-meta text-ink outline-0 placeholder:text-muted disabled:cursor-not-allowed [&::-webkit-search-cancel-button]:hidden" }), _jsx(FieldButton, { className: "mr-1 group-data-empty:invisible", children: _jsx(X, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }) }) }));
|
|
46
43
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
3
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
4
4
|
import { useFormMode } from "../../foundations/form.js";
|
|
5
5
|
import { tv } from "../../foundations/variants.js";
|
|
6
6
|
import { labelOf } from "./options.js";
|
|
@@ -56,7 +56,7 @@ export function SegmentedControl({ value, onChange, options, size = "md", disabl
|
|
|
56
56
|
const inForm = mode !== undefined || labelledBy !== undefined;
|
|
57
57
|
if (inForm && formMode === "read")
|
|
58
58
|
return _jsx(ReadValue, { value: labelOf(options, value) });
|
|
59
|
-
return (_jsx(ToggleButtonGroup, { "aria-label": ariaLabel, "aria-labelledby": labelledBy, "aria-describedby": describedBy, selectionMode: "single",
|
|
59
|
+
return (_jsx(ToggleButtonGroup, { "aria-label": controlName(labelledBy, ariaLabel), "aria-labelledby": labelledBy, "aria-describedby": describedBy, selectionMode: "single",
|
|
60
60
|
/* Always one: a period switcher with nothing pressed shows a report of no period. */
|
|
61
61
|
disallowEmptySelection: true, selectedKeys: [value], onSelectionChange: (keys) => {
|
|
62
62
|
const [next] = keys;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
5
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
6
6
|
import { useFormMode } from "../../foundations/form.js";
|
|
7
7
|
import { tv } from "../../foundations/variants.js";
|
|
8
8
|
import { ReadValue } from "../field/Field.js";
|
|
@@ -42,5 +42,5 @@ export function Slider({ value, onChange, min, max, step = 1, format, disabled =
|
|
|
42
42
|
if (useFormMode(mode) === "read") {
|
|
43
43
|
return _jsx(ReadValue, { value: new Intl.NumberFormat(copy.locale, format).format(value) });
|
|
44
44
|
}
|
|
45
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaSlider, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), minValue: min, maxValue: max, step: step, formatOptions: format, isDisabled: disabled, className: "flex h-(--size-control-md) w-full min-w-0 items-center gap-3 disabled:opacity-45", children: [_jsxs(SliderTrack, { className: "relative flex h-(--size-control-md) min-w-0 flex-1 cursor-pointer items-center disabled:cursor-not-allowed", children: [_jsx("span", { className: "relative h-1 w-full rounded-pill bg-line-strong forced-colors:bg-[ButtonBorder]", children: _jsx(SliderFill, { className: "rounded-pill bg-primary disabled:bg-muted forced-colors:bg-[Highlight]" }) }), _jsx(SliderThumb, { "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, className: thumb })] }), output ? (_jsx(SliderOutput, { className: "w-14 shrink-0 text-right text-meta text-ink tabular-nums" })) : null] }) }));
|
|
45
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaSlider, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, value: value, onChange: (next) => onChange?.(next), minValue: min, maxValue: max, step: step, formatOptions: format, isDisabled: disabled, className: "flex h-(--size-control-md) w-full min-w-0 items-center gap-3 disabled:opacity-45", children: [_jsxs(SliderTrack, { className: "relative flex h-(--size-control-md) min-w-0 flex-1 cursor-pointer items-center disabled:cursor-not-allowed", children: [_jsx("span", { className: "relative h-1 w-full rounded-pill bg-line-strong forced-colors:bg-[ButtonBorder]", children: _jsx(SliderFill, { className: "rounded-pill bg-primary disabled:bg-muted forced-colors:bg-[Highlight]" }) }), _jsx(SliderThumb, { "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, className: thumb })] }), output ? (_jsx(SliderOutput, { className: "w-14 shrink-0 text-right text-meta text-ink tabular-nums" })) : null] }) }));
|
|
46
46
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { DateFormatter, parseDate } from "@internationalized/date";
|
|
4
4
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
5
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
6
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
7
7
|
import { useFormMode } from "../../foundations/form.js";
|
|
8
8
|
import { tv } from "../../foundations/variants.js";
|
|
9
9
|
import { Calendar, RangeCalendar } from "./Calendar.js";
|
|
@@ -71,7 +71,7 @@ export function DateField({ value, onChange, id: ownId, describedBy: ownDescribe
|
|
|
71
71
|
const read = useReadDate();
|
|
72
72
|
if (useFormMode(mode) === "read")
|
|
73
73
|
return _jsx(ReadValue, { value: read(value) });
|
|
74
|
-
return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
74
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaDateField, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
75
75
|
}
|
|
76
76
|
/** A date typed by segments, or picked on a calendar from the button. */
|
|
77
77
|
export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, min, max, mode, "aria-label": ariaLabel, }) {
|
|
@@ -83,7 +83,7 @@ export function DatePicker({ value, onChange, id: ownId, describedBy: ownDescrib
|
|
|
83
83
|
const read = useReadDate();
|
|
84
84
|
if (useFormMode(mode) === "read")
|
|
85
85
|
return _jsx(ReadValue, { value: read(value) });
|
|
86
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
86
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, value: parse(value), onChange: (next) => onChange?.(next === null ? null : next.toString()), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
87
87
|
}
|
|
88
88
|
/** A span of days — a period of a report, the validity of a contract — typed or picked on
|
|
89
89
|
* two months at once. */
|
|
@@ -99,5 +99,5 @@ export function DateRangePicker({ value, onChange, id: ownId, describedBy: ownDe
|
|
|
99
99
|
}
|
|
100
100
|
const start = parse(value?.start);
|
|
101
101
|
const end = parse(value?.end);
|
|
102
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
|
|
102
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDateRangePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, value: start === null || end === null ? null : { start, end }, onChange: (next) => onChange?.(next === null ? null : { start: next.start.toString(), end: next.end.toString() }), minValue: parse(min) ?? undefined, maxValue: parse(max) ?? undefined, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, { slot: "start" }), _jsx("span", { "aria-hidden": true, className: "text-muted", children: "\u2013" }), _jsx(Segments, { slot: "end" }), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(RangeCalendar, { months: 2 }) }) })] }) }));
|
|
103
103
|
}
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { DateFormatter, parseDateTime, parseTime, toCalendarDate, } from "@internationalized/date";
|
|
4
4
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
5
5
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
6
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
6
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
7
7
|
import { useFormMode } from "../../foundations/form.js";
|
|
8
8
|
import { Calendar } from "./Calendar.js";
|
|
9
9
|
import { Segments } from "./DatePicker.js";
|
|
@@ -81,7 +81,7 @@ export function TimeField({ value, onChange, id: ownId, describedBy: ownDescribe
|
|
|
81
81
|
? ""
|
|
82
82
|
: format.format(new Date(Date.UTC(1970, 0, 1, time.hour, time.minute))) }));
|
|
83
83
|
}
|
|
84
|
-
return (_jsx(AriaLocale, { children: _jsx(AriaTimeField, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parseClock(value), onChange: (next) => onChange?.(next === null ? null : hhmm(next)), minValue: parseClock(min) ?? undefined, maxValue: parseClock(max) ?? undefined, hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
84
|
+
return (_jsx(AriaLocale, { children: _jsx(AriaTimeField, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, value: parseClock(value), onChange: (next) => onChange?.(next === null ? null : hhmm(next)), minValue: parseClock(min) ?? undefined, maxValue: parseClock(max) ?? undefined, hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: _jsx(FieldGroup, { children: _jsx(Segments, {}) }) }) }));
|
|
85
85
|
}
|
|
86
86
|
/** A day and its time — a delivery slot, an appointment: the day typed or picked on the
|
|
87
87
|
* calendar, the time typed after it. The value is a local `"YYYY-MM-DDTHH:MM"`, or `null`. */
|
|
@@ -96,5 +96,5 @@ export function DateTimePicker({ value, onChange, id: ownId, describedBy: ownDes
|
|
|
96
96
|
const stamp = parseStamp(value);
|
|
97
97
|
return _jsx(ReadValue, { value: stamp === null ? "" : format.format(stamp.toDate("UTC")) });
|
|
98
98
|
}
|
|
99
|
-
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, value: parseStamp(value), onChange: (next) => onChange?.(next === null ? null : stampOf(next)), minValue: parseStamp(min) ?? undefined, maxValue: parseStamp(max) ?? undefined, granularity: "minute", hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
99
|
+
return (_jsx(AriaLocale, { children: _jsxs(AriaDatePicker, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, value: parseStamp(value), onChange: (next) => onChange?.(next === null ? null : stampOf(next)), minValue: parseStamp(min) ?? undefined, maxValue: parseStamp(max) ?? undefined, granularity: "minute", hourCycle: hourCycle, isInvalid: invalid, isDisabled: disabled, className: "w-full", children: [_jsxs(FieldGroup, { children: [_jsx(Segments, {}), _jsx(FieldButton, { className: "mr-1 ml-auto", children: _jsx(CalendarDays, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsx(Popover, { placement: "bottom end", children: _jsx(Dialog, { className: "p-3 outline-0", children: _jsx(Calendar, {}) }) })] }) }));
|
|
100
100
|
}
|
|
@@ -94,7 +94,8 @@ export type ControlProps = {
|
|
|
94
94
|
/** Forces the mode. By default it inherits the form's: in read mode it draws the value. */
|
|
95
95
|
mode?: FormMode;
|
|
96
96
|
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table,
|
|
97
|
-
* the box of a message. Inside a field
|
|
97
|
+
* the box of a message. Inside a field the field's label names it and this is IGNORED
|
|
98
|
+
* (`controlName`): with both, the control was read twice. */
|
|
98
99
|
"aria-label"?: string;
|
|
99
100
|
};
|
|
100
101
|
/** The accessible name of a control. Inside a `Field` the field's label names it; outside,
|
|
@@ -4,7 +4,7 @@ import { AriaLocale } from "../../foundations/aria.js";
|
|
|
4
4
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
5
5
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
6
6
|
import { FieldIdsContext, useFieldIds, } from "../../foundations/form.js";
|
|
7
|
-
import { FormModeProvider, useFormMode, } from "../../foundations/form.js";
|
|
7
|
+
import { controlName, FormModeProvider, useFormMode, } from "../../foundations/form.js";
|
|
8
8
|
import { focusRing, tv } from "../../foundations/variants.js";
|
|
9
9
|
import { FieldGroup, listOptionStyles } from "./aria.js";
|
|
10
10
|
import { FieldCopy, useFieldCopy } from "./clipboard.js";
|
|
@@ -139,7 +139,7 @@ export function controlClasses(invalid = false) {
|
|
|
139
139
|
* browser reads for a bare `<input placeholder>` anyway, so a control outside a form is
|
|
140
140
|
* never nameless. */
|
|
141
141
|
export function nameOf(labelledBy, ariaLabel, placeholder) {
|
|
142
|
-
return labelledBy
|
|
142
|
+
return controlName(labelledBy, ariaLabel, placeholder);
|
|
143
143
|
}
|
|
144
144
|
/**
|
|
145
145
|
* One line of text: a business name, a reference, a NIT.
|
|
@@ -4,7 +4,7 @@ import { AriaLocale } from "../../foundations/aria.js";
|
|
|
4
4
|
import { useFieldIds } from "../../foundations/form.js";
|
|
5
5
|
import { useFormMode } from "../../foundations/form.js";
|
|
6
6
|
import { FieldGroup, listOptionStyles } from "./aria.js";
|
|
7
|
-
import { ReadValue } from "./Field.js";
|
|
7
|
+
import { nameOf, ReadValue } from "./Field.js";
|
|
8
8
|
import { Popover } from "../overlay/Popover.js";
|
|
9
9
|
import { ComboBox as AriaComboBox, Input, ListBox, ListBoxItem, useFilter, } from "react-aria-components";
|
|
10
10
|
const MAX = 8;
|
|
@@ -34,7 +34,7 @@ function EditableSuggest({ id: ownId, value, onChange, suggestions, placeholder,
|
|
|
34
34
|
.filter((one) => needle === "" || contains(one, needle))
|
|
35
35
|
.slice(0, max)
|
|
36
36
|
.map((one) => ({ id: one }));
|
|
37
|
-
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": labelledBy
|
|
37
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": nameOf(labelledBy, ariaLabel, placeholder),
|
|
38
38
|
/* An open field: what is typed is the value, a suggestion only fills it faster. */
|
|
39
39
|
allowsCustomValue: true, inputValue: value, onInputChange: (next) => onChange?.(next), items: shown,
|
|
40
40
|
/* It proposes as soon as the field is entered, like the hand-written one did: the
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
5
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
6
6
|
import { useFormMode } from "../../foundations/form.js";
|
|
7
7
|
import { matchedWord, matches } from "../../foundations/match.js";
|
|
8
8
|
import { cn, tv } from "../../foundations/variants.js";
|
|
@@ -198,7 +198,7 @@ function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabe
|
|
|
198
198
|
if (next !== value)
|
|
199
199
|
onChange?.(next);
|
|
200
200
|
};
|
|
201
|
-
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
201
|
+
return (_jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, items: listed, value: value ?? null, onChange: take, inputValue: text, onInputChange: (next) => {
|
|
202
202
|
setText(next);
|
|
203
203
|
setQuery(next);
|
|
204
204
|
}, onOpenChange: (isOpen, trigger) => {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { AriaLocale } from "../../foundations/aria.js";
|
|
4
4
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
5
|
-
import { useFieldIds } from "../../foundations/form.js";
|
|
5
|
+
import { controlName, useFieldIds } from "../../foundations/form.js";
|
|
6
6
|
import { useFormMode } from "../../foundations/form.js";
|
|
7
7
|
import { cn } from "../../foundations/variants.js";
|
|
8
8
|
import { fieldGroupStyles, listOptionStyles } from "../field/aria.js";
|
|
@@ -171,7 +171,7 @@ function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBro
|
|
|
171
171
|
? ariaLabel === undefined
|
|
172
172
|
? {}
|
|
173
173
|
: { label: ariaLabel }
|
|
174
|
-
: { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": ariaLabel, "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
|
|
174
|
+
: { labelledBy }) })), _jsxs(AriaComboBox, { id: id, "aria-labelledby": labelledBy, "aria-label": controlName(labelledBy, ariaLabel), "aria-describedby": describedBy, items: listed, selectedKey: null, onSelectionChange: commit, inputValue: text, onInputChange: setText,
|
|
175
175
|
/* Arriving at the field shows what can still be added, the way a many2one opens its
|
|
176
176
|
list on focus. With every tag chosen and no text there is nothing to show, and the
|
|
177
177
|
list stays closed. */
|
|
@@ -29,7 +29,7 @@ export interface DataColumn<T> {
|
|
|
29
29
|
field?: string;
|
|
30
30
|
/** The raw value: this is what sorting and grouping run on. */
|
|
31
31
|
value: (row: T) => string | number | boolean | null;
|
|
32
|
-
/** How it is drawn. Without this the raw value
|
|
32
|
+
/** How it is drawn. Without this, `text`; without either, the raw value. */
|
|
33
33
|
cell?: (row: T) => ReactNode;
|
|
34
34
|
/** A column of CODES — a SKU, a hash, a key — set in the monospaced face. Numbers do not
|
|
35
35
|
* need it: they align with tabular figures in the same face as the rest of the row. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { FieldBoundary } from "../../foundations/form.js";
|
|
4
5
|
import { pathKey, pileFoot, pileItems, } from "../../foundations/grouping.js";
|
|
5
6
|
import { CellCopy } from "./CellCopy.js";
|
|
6
7
|
import { CellEditor } from "./CellEditor.js";
|
|
@@ -65,9 +66,14 @@ function raw(copy, v) {
|
|
|
65
66
|
return v ? copy.table.yes : copy.table.no;
|
|
66
67
|
return v;
|
|
67
68
|
}
|
|
68
|
-
/** What a cell draws: its `cell
|
|
69
|
+
/** What a cell draws: its `cell`; without one, how it READS (`text`); only then the raw
|
|
70
|
+
* value — which is the sort key, «posted» and not «publicada». */
|
|
69
71
|
function drawCell(copy, column, row) {
|
|
70
|
-
|
|
72
|
+
if (column.cell !== undefined)
|
|
73
|
+
return column.cell(row);
|
|
74
|
+
if (column.text !== undefined)
|
|
75
|
+
return column.text(row);
|
|
76
|
+
return raw(copy, column.value(row));
|
|
71
77
|
}
|
|
72
78
|
/* The features are declared once. In v9 they are opt-in: asking only for what
|
|
73
79
|
is used is what keeps the bundle down to what is actually needed. */
|
|
@@ -419,8 +425,8 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
|
|
|
419
425
|
return (_jsx("td", { className: `px-3 py-1.5 text-meta font-semibold text-ink ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: i === 0 && value === null ? (_jsxs("span", { className: "text-muted", children: [data.length.toLocaleString(copy.locale), " ", data.length === 1 ? copy.table.row : copy.table.rows] })) : value === null ? null : (aggregateLabel(copy, c, value)) }, c.id));
|
|
420
426
|
}), rowActions === undefined ? null : _jsx("td", {})] }) }))] }));
|
|
421
427
|
if (create === undefined)
|
|
422
|
-
return grid;
|
|
423
|
-
return (_jsxs("div", { className: "flex min-w-0 flex-col", children: [grid, writing ? null : (_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { ref: addButton, type: "button", onClick: start, className: "flex w-full items-center gap-1.5 rounded-control px-3 py-1 text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), create.label ?? copy.table.add] }) }))] }));
|
|
428
|
+
return _jsx(FieldBoundary, { children: grid });
|
|
429
|
+
return (_jsx(FieldBoundary, { children: _jsxs("div", { className: "flex min-w-0 flex-col", children: [grid, writing ? null : (_jsx("div", { className: "px-1 py-0.5", children: _jsxs("button", { ref: addButton, type: "button", onClick: start, className: "flex w-full items-center gap-1.5 rounded-control px-3 py-1 text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), create.label ?? copy.table.add] }) }))] }) }));
|
|
424
430
|
}
|
|
425
431
|
/* A cell of the row being written: the table's cell, washed like an edited row. */
|
|
426
432
|
const DRAFT_CELL = "border-b border-line bg-primary/5 px-3 py-1 align-middle text-meta text-ink first:rounded-l-control last:rounded-r-control";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
5
|
-
import { ControlStateContext } from "../../foundations/form.js";
|
|
5
|
+
import { ControlStateContext, FieldBoundary } from "../../foundations/form.js";
|
|
6
6
|
import { pathKey, pileIsOpen, pileItems, pilesOf, } from "../../foundations/grouping.js";
|
|
7
7
|
import { HANDHELD_WIDTH } from "../../foundations/layout.js";
|
|
8
8
|
import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
|
|
@@ -816,106 +816,109 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
816
816
|
return null;
|
|
817
817
|
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
|
|
818
818
|
}) }))] }));
|
|
819
|
-
return (
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
(reorderable ? 1 : 0), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (view.rows.flatMap((slot, at) => {
|
|
839
|
-
const entry = drawn[slot.index];
|
|
840
|
-
if (entry === undefined)
|
|
841
|
-
return [];
|
|
842
|
-
const windowed = view.windowed(slot.index);
|
|
843
|
-
/* What is not drawn is a spacer of its height: the rows above the window
|
|
844
|
-
before the first, and the gap before a row kept only because it holds
|
|
845
|
-
the focus. */
|
|
846
|
-
const gap = view.pad(at === 0 ? 0 : view.rows[at - 1].end, slot.start, width, `gap:${slot.index}`);
|
|
847
|
-
if (entry.pile !== undefined) {
|
|
848
|
-
const pile = entry.pile;
|
|
849
|
-
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
850
|
-
return [
|
|
851
|
-
gap,
|
|
852
|
-
/* La banda respeta la cuadrícula: el rótulo en la primera
|
|
853
|
-
columna y cada total bajo la suya. Un total en un rincón
|
|
854
|
-
obliga a buscar de qué columna es; alineado no hay nada que
|
|
855
|
-
buscar. */
|
|
856
|
-
_jsxs("tr", { ...windowed, children: [reorderable ? _jsx("td", { className: "w-0 bg-ground" }) : null, columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
|
|
857
|
-
? {
|
|
858
|
-
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
859
|
-
}
|
|
860
|
-
: undefined, children: ci === 0
|
|
861
|
-
? pileTag(pile, copy.locale)
|
|
862
|
-
: (groupTotal?.(c, inside) ?? null) }, c.id))), onRemove === undefined ? null : _jsx("td", { className: "w-0 bg-ground" })] }, `pile:${pathKey(pile.path)}`),
|
|
863
|
-
];
|
|
819
|
+
return (
|
|
820
|
+
/* A collection is many controls, each named by its column: no enclosing `Field`
|
|
821
|
+
reaches inside (see `FieldBoundary`). */
|
|
822
|
+
_jsx(FieldBoundary, { children: _jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, tabIndex: box.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", ...(view.on
|
|
823
|
+
? { "aria-rowcount": 1 + drawn.length + (totals === undefined ? 0 : 1) }
|
|
824
|
+
: {}), children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { ...(view.on ? { "aria-rowindex": 1 } : {}), children: [reorderable ? _jsx("td", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => c.header === "" ? (_jsx("td", { className: "w-0 border-b border-line-strong" }, c.id)) : (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width },
|
|
825
|
+
/* A column of figures takes its figures' width (`w-0` in an auto
|
|
826
|
+
table is «as narrow as its content») and the text takes the rest:
|
|
827
|
+
the product name was cut while «Cantidad» had room for ten digits. */
|
|
828
|
+
className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"} ${c.numeric === true && c.width === undefined ? "w-0" : ""}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "w-0 border-b border-line-strong" }))] }) }), _jsxs("tbody", { ref: view.body.ref, ...(reorderable
|
|
829
|
+
? {
|
|
830
|
+
onDragOver: overRow,
|
|
831
|
+
onDrop: (event) => {
|
|
832
|
+
event.preventDefault();
|
|
833
|
+
if (dragged.current !== null && landing !== undefined)
|
|
834
|
+
land(dragged.current, landing);
|
|
835
|
+
dragged.current = null;
|
|
836
|
+
setLanding(undefined);
|
|
837
|
+
},
|
|
864
838
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
const
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
839
|
+
: {}), children: [rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length +
|
|
840
|
+
(onRemove === undefined ? 0 : 1) +
|
|
841
|
+
(reorderable ? 1 : 0), className: `text-meta text-muted italic ${cell}`, children: emptyText }) })) : (view.rows.flatMap((slot, at) => {
|
|
842
|
+
const entry = drawn[slot.index];
|
|
843
|
+
if (entry === undefined)
|
|
844
|
+
return [];
|
|
845
|
+
const windowed = view.windowed(slot.index);
|
|
846
|
+
/* What is not drawn is a spacer of its height: the rows above the window
|
|
847
|
+
before the first, and the gap before a row kept only because it holds
|
|
848
|
+
the focus. */
|
|
849
|
+
const gap = view.pad(at === 0 ? 0 : view.rows[at - 1].end, slot.start, width, `gap:${slot.index}`);
|
|
850
|
+
if (entry.pile !== undefined) {
|
|
851
|
+
const pile = entry.pile;
|
|
852
|
+
const inside = pile.piles.length === 0 ? pile.items : pileItems(pile.piles);
|
|
853
|
+
return [
|
|
854
|
+
gap,
|
|
855
|
+
/* La banda respeta la cuadrícula: el rótulo en la primera
|
|
856
|
+
columna y cada total bajo la suya. Un total en un rincón
|
|
857
|
+
obliga a buscar de qué columna es; alineado no hay nada que
|
|
858
|
+
buscar. */
|
|
859
|
+
_jsxs("tr", { ...windowed, children: [reorderable ? _jsx("td", { className: "w-0 bg-ground" }) : null, columns.map((c, ci) => (_jsx("td", { className: `bg-ground text-meta ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, style: ci === 0 && pile.depth > 0
|
|
860
|
+
? {
|
|
861
|
+
paddingLeft: `calc(var(--spacing-indent) * ${pile.depth} + 0.625rem)`,
|
|
862
|
+
}
|
|
863
|
+
: undefined, children: ci === 0
|
|
864
|
+
? pileTag(pile, copy.locale)
|
|
865
|
+
: (groupTotal?.(c, inside) ?? null) }, c.id))), onRemove === undefined ? null : _jsx("td", { className: "w-0 bg-ground" })] }, `pile:${pathKey(pile.path)}`),
|
|
866
|
+
];
|
|
867
|
+
}
|
|
868
|
+
const row = entry.row;
|
|
869
|
+
const id = getRowId(row);
|
|
870
|
+
const isEditing = editing === id;
|
|
871
|
+
const band = bandOf(row);
|
|
872
|
+
if (band !== null) {
|
|
873
|
+
const { span, figures } = bandFigures(row);
|
|
874
|
+
const bandWash = isEditing
|
|
875
|
+
? "bg-primary/5"
|
|
876
|
+
: band === "section"
|
|
877
|
+
? "bg-ground group-hover/row:bg-ink/6"
|
|
878
|
+
: "group-hover/row:bg-ink/4";
|
|
879
|
+
const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
|
|
880
|
+
const control = isEditing && editable && bands?.edit !== undefined;
|
|
881
|
+
return [
|
|
882
|
+
gap,
|
|
883
|
+
_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), windowed: windowed, className: `group/row ${editable ? RING : ""} ${editable && !isEditing ? "cursor-text" : ""}`, children: [reorderable ? grip(id, bandMark) : null, _jsx("td", { colSpan: span, ...(control ? { "data-edit": "" } : {}), className: `text-meta ${bandMark} ${lineOver(id)} ${cell} ${band === "section"
|
|
884
|
+
? "font-semibold text-ink"
|
|
885
|
+
: "text-muted italic"}`, children: control ? bands?.edit?.(row, setter(id)) : bands?.render(row) }), columns.slice(span).map((c, i) => (_jsx("td", { className: `text-meta text-ink ${bandMark} ${cell} ${c.numeric === true
|
|
886
|
+
? "text-right font-semibold tabular-nums"
|
|
887
|
+
: ""}`, children: figures[span + i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${bandMark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id),
|
|
888
|
+
];
|
|
889
|
+
}
|
|
890
|
+
/* The wash lives on the cells. A background on the row paints
|
|
891
|
+
edge to edge, and `border-collapse` would ignore a radius. */
|
|
892
|
+
const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
|
|
893
|
+
const mark = `border-b border-line first:rounded-l-control last:rounded-r-control ${wash}`;
|
|
894
|
+
const set = setter(id);
|
|
878
895
|
return [
|
|
879
896
|
gap,
|
|
880
|
-
_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), windowed: windowed,
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
897
|
+
_jsxs(EditRow, { as: "tr", ...rowWiring(id, row), windowed: windowed,
|
|
898
|
+
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
899
|
+
the box and does not move the cells. */
|
|
900
|
+
className: `group/row ${editable ? RING : ""} ${editable && !isEditing && !locked(row) ? "cursor-text" : ""}`, children: [reorderable ? grip(id, mark) : null, columns.map((c, ci) => {
|
|
901
|
+
const state = stateOf(row, c);
|
|
902
|
+
const control = isEditing && writes(row, c, state);
|
|
903
|
+
/* Not `mark`: that name is the row's border and wash, above. Shadowed,
|
|
904
|
+
the cells were drawn with the class «null» — no hairline, no hover. */
|
|
905
|
+
const flagged = ci === 0 ? flagOf(row, true) : null;
|
|
906
|
+
const cellFlag = cellFlagOf(row, c, state, true);
|
|
907
|
+
const value = state?.hidden === true
|
|
908
|
+
? null
|
|
909
|
+
: control
|
|
910
|
+
? controlOf(row, c, state, set)
|
|
911
|
+
: c.render(row);
|
|
912
|
+
return (_jsxs("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${lineOver(id)} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: [flagged === null && cellFlag === null ? (value) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [flagged, cellFlag, _jsx("span", { className: "min-w-0 flex-1", children: value })] })), errorOf(row, c, state)] }, c.id));
|
|
913
|
+
}), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${mark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id),
|
|
885
914
|
];
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
/* The focus ring is drawn on the row, inset, so it is not cut by
|
|
896
|
-
the box and does not move the cells. */
|
|
897
|
-
className: `group/row ${editable ? RING : ""} ${editable && !isEditing && !locked(row) ? "cursor-text" : ""}`, children: [reorderable ? grip(id, mark) : null, columns.map((c, ci) => {
|
|
898
|
-
const state = stateOf(row, c);
|
|
899
|
-
const control = isEditing && writes(row, c, state);
|
|
900
|
-
/* Not `mark`: that name is the row's border and wash, above. Shadowed,
|
|
901
|
-
the cells were drawn with the class «null» — no hairline, no hover. */
|
|
902
|
-
const flagged = ci === 0 ? flagOf(row, true) : null;
|
|
903
|
-
const cellFlag = cellFlagOf(row, c, state, true);
|
|
904
|
-
const value = state?.hidden === true
|
|
905
|
-
? null
|
|
906
|
-
: control
|
|
907
|
-
? controlOf(row, c, state, set)
|
|
908
|
-
: c.render(row);
|
|
909
|
-
return (_jsxs("td", { ...(control ? { "data-edit": "" } : {}), className: `text-meta text-ink ${mark} ${lineOver(id)} ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: [flagged === null && cellFlag === null ? (value) : (_jsxs("span", { className: "flex min-w-0 items-start gap-1.5", children: [flagged, cellFlag, _jsx("span", { className: "min-w-0 flex-1", children: value })] })), errorOf(row, c, state)] }, c.id));
|
|
910
|
-
}), onRemove === undefined ? null : (_jsx("td", { className: `w-0 py-1 pr-2 pl-1 align-middle ${mark}`, children: _jsx("span", { className: "flex opacity-0 transition-opacity duration-(--duration-instant) group-hover/row:opacity-100 group-focus-visible/row:opacity-100 focus-within:opacity-100", onClick: (e) => e.stopPropagation(), children: _jsx(IconButton, { icon: Trash2, label: copy.editable.remove, size: "sm", tone: "danger", ...(editable && stop !== id ? { tabIndex: -1 } : {}), onClick: () => onRemove(id) }) }) }))] }, id),
|
|
911
|
-
];
|
|
912
|
-
})), rows.length === 0 ? null : view.tail(width)] }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "border-t border-line-strong" }) : null, columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
|
|
913
|
-
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
914
|
-
what is about to happen is that one more row shows up right here.
|
|
915
|
-
The hover is the same inset mark as a row: the shell lines the label
|
|
916
|
-
up with the cells, and the button itself is the rounded wash. It sits
|
|
917
|
-
outside the scroll box, so a table with its own height keeps it in view.
|
|
918
|
-
A section and a note sit beside it, quieter: the line is what is added
|
|
919
|
-
most, so it keeps the lead and the whole free width. */
|
|
920
|
-
_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 px-1 py-0.5", children: [onAdd === undefined ? null : (_jsxs("button", { ref: addButton, type: "button", onClick: () => add(), className: `flex items-center gap-1.5 rounded-control text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${adds === undefined || adds.length === 0 ? "w-full" : ""} ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), addText] })), (adds ?? []).map((extra) => (_jsx("button", { type: "button", onClick: () => add(extra.onAdd), className: `rounded-control text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-primary/5 hover:text-primary focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: extra.label }, extra.label)))] }))] }));
|
|
915
|
+
})), rows.length === 0 ? null : view.tail(width)] }), totals === undefined ? null : (_jsx("tfoot", { children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "border-t border-line-strong" }) : null, columns.map((c, i) => (_jsx("td", { className: `border-t border-line-strong text-meta font-semibold text-ink ${cell} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: totals[i] ?? null }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "border-t border-line-strong" }))] }) }))] })) }), onAdd === undefined && (adds === undefined || adds.length === 0) ? null : (
|
|
916
|
+
/* The «add» goes under the grid, as text, not as a button away from it:
|
|
917
|
+
what is about to happen is that one more row shows up right here.
|
|
918
|
+
The hover is the same inset mark as a row: the shell lines the label
|
|
919
|
+
up with the cells, and the button itself is the rounded wash. It sits
|
|
920
|
+
outside the scroll box, so a table with its own height keeps it in view.
|
|
921
|
+
A section and a note sit beside it, quieter: the line is what is added
|
|
922
|
+
most, so it keeps the lead and the whole free width. */
|
|
923
|
+
_jsxs("div", { className: "flex flex-wrap items-center gap-x-1 px-1 py-0.5", children: [onAdd === undefined ? null : (_jsxs("button", { ref: addButton, type: "button", onClick: () => add(), className: `flex items-center gap-1.5 rounded-control text-left text-meta text-primary transition-colors duration-(--duration-instant) hover:bg-primary/5 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${adds === undefined || adds.length === 0 ? "w-full" : ""} ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: [_jsx(Plus, { className: "size-(--size-icon-sm)", strokeWidth: 2.5, "aria-hidden": true }), addText] })), (adds ?? []).map((extra) => (_jsx("button", { type: "button", onClick: () => add(extra.onAdd), className: `rounded-control text-left text-meta text-muted transition-colors duration-(--duration-instant) hover:bg-primary/5 hover:text-primary focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${byDensity(d, "px-3 py-1", "px-2 py-0.5")}`, children: extra.label }, extra.label)))] }))] }) }));
|
|
921
924
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/web-ui",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "An enterprise web design system for business software — ERPs, back offices, management tools — in React and Tailwind: tokens, primitives, components and the two views every management screen is made of.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|