@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.
@@ -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
- const drawn = c.cell === undefined ? plain(c.value(line)) : c.cell(line);
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 (_jsxs("section", { ref: root, "aria-label": copy.lineItems.table, className: "flex min-w-0 flex-col gap-4", children: [lines.length === 0 && !canAdd ? (
277
- /* Said, with its way out: an empty document is a question («what is sold
278
- here?»), not a blank box. In read mode there is no way out to offer. */
279
- _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: {
280
- id: `${searchLabelId}-input`,
281
- labelId: searchLabelId,
282
- describedBy: undefined,
283
- invalid: false,
284
- }, 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
285
- ? copy.lines.empty
286
- : 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
287
- ? /* A blank row nobody wrote in is taken back without a notice: nobody
288
- removed anything, so there is nothing to undo. */
289
- { onDiscard: onRemove }
290
- : {}), ...(canEdit
291
- ? {
292
- onOpen: (id) => {
293
- const line = lines.find((l) => getLineId(l) === id);
294
- /* A read-only line opens no form to change it. */
295
- if (line !== undefined && rowState?.(line)?.readonly !== true) {
296
- openLine(id, line);
297
- }
298
- },
299
- }
300
- : {}), ...(canChange && columns.some((c) => c.edit !== undefined)
301
- ? { onChange: (id, next) => onChange?.(id, next) }
302
- : {}) })] })), totals === undefined || ("length" in totals && totals.length === 0) ? null : (
303
- /* Inset like the table's cells, so the last figure lines up with the amounts. */
304
- _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 : (
305
- /* Apart, at the other end from «Listo»: removing is not the way the
306
- form is meant to close. */
307
- _jsx("span", { className: "mr-auto", children: _jsx(IconButton, { icon: Trash2, label: copy.lines.remove, tone: "danger", onClick: () => {
308
- const line = lines.find((l) => getLineId(l) === session.id);
309
- setOpen(false);
310
- if (line !== undefined)
311
- remove(line);
312
- } }) })), _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) => {
313
- e.preventDefault();
314
- save();
315
- }, className: "flex flex-col gap-3", children: editor(session.draft, setDraft, {
316
- tried: session.tried,
317
- isNew: session.id === null,
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;
@@ -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, the field's label names it. */
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 === undefined ? (ariaLabel ?? placeholder) : ariaLabel;
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 === undefined ? (ariaLabel ?? placeholder) : ariaLabel,
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 is drawn. */
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`, or the raw value. */
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
- return column.cell === undefined ? raw(copy, column.value(row)) : column.cell(row);
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 (_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
820
- ? { "aria-rowcount": 1 + drawn.length + (totals === undefined ? 0 : 1) }
821
- : {}), 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 },
822
- /* A column of figures takes its figures' width (`w-0` in an auto
823
- table is «as narrow as its content») and the text takes the rest:
824
- the product name was cut while «Cantidad» had room for ten digits. */
825
- 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
826
- ? {
827
- onDragOver: overRow,
828
- onDrop: (event) => {
829
- event.preventDefault();
830
- if (dragged.current !== null && landing !== undefined)
831
- land(dragged.current, landing);
832
- dragged.current = null;
833
- setLanding(undefined);
834
- },
835
- }
836
- : {}), children: [rows.length === 0 ? (_jsx("tr", { children: _jsx("td", { colSpan: columns.length +
837
- (onRemove === undefined ? 0 : 1) +
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
- const row = entry.row;
866
- const id = getRowId(row);
867
- const isEditing = editing === id;
868
- const band = bandOf(row);
869
- if (band !== null) {
870
- const { span, figures } = bandFigures(row);
871
- const bandWash = isEditing
872
- ? "bg-primary/5"
873
- : band === "section"
874
- ? "bg-ground group-hover/row:bg-ink/6"
875
- : "group-hover/row:bg-ink/4";
876
- const bandMark = `border-b border-line first:rounded-l-control last:rounded-r-control ${bandWash}`;
877
- const control = isEditing && editable && bands?.edit !== undefined;
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, 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"
881
- ? "font-semibold text-ink"
882
- : "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
883
- ? "text-right font-semibold tabular-nums"
884
- : ""}`, 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),
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
- /* The wash lives on the cells. A background on the row paints
888
- edge to edge, and `border-collapse` would ignore a radius. */
889
- const wash = isEditing ? "bg-primary/5" : "group-hover/row:bg-ink/4";
890
- const mark = `border-b border-line first:rounded-l-control last:rounded-r-control ${wash}`;
891
- const set = setter(id);
892
- return [
893
- gap,
894
- _jsxs(EditRow, { as: "tr", ...rowWiring(id, row), windowed: windowed,
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.0",
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",