@sincpro/web-ui 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/components/activity/Activity.js +1 -1
  2. package/dist/components/activity/Messages.d.ts +4 -2
  3. package/dist/components/activity/Messages.js +4 -4
  4. package/dist/components/activity/Timeline.js +1 -1
  5. package/dist/components/assist/AssistMark.js +1 -1
  6. package/dist/components/assist/AssistSuggestion.js +1 -1
  7. package/dist/components/assist/ChangeReview.js +2 -2
  8. package/dist/components/calendar/RecordCalendar.js +4 -1
  9. package/dist/components/form/Sheet.js +5 -2
  10. package/dist/components/lines/LineItems.d.ts +1 -1
  11. package/dist/components/lines/LineItems.js +2 -2
  12. package/dist/components/listing/ListingBar.js +1 -1
  13. package/dist/components/listing/RecordCard.d.ts +4 -0
  14. package/dist/components/listing/RecordCard.js +4 -1
  15. package/dist/components/listing/Row.d.ts +7 -0
  16. package/dist/components/listing/Row.js +7 -1
  17. package/dist/components/listing/ViewSwitcher.d.ts +7 -2
  18. package/dist/components/listing/index.d.ts +1 -1
  19. package/dist/components/listing/set.d.ts +4 -1
  20. package/dist/components/record/Breadcrumb.js +5 -1
  21. package/dist/components/record/DirtyActions.d.ts +20 -6
  22. package/dist/components/record/DirtyActions.js +55 -4
  23. package/dist/components/record/RecordHeader.d.ts +1 -1
  24. package/dist/components/record/RecordHeader.js +32 -13
  25. package/dist/components/record/RecordSkeleton.js +1 -1
  26. package/dist/components/search/CriteriaBuilder.d.ts +1 -1
  27. package/dist/components/search/CriteriaBuilder.js +3 -3
  28. package/dist/components/search/FilterBuilder.d.ts +1 -18
  29. package/dist/components/search/FilterBuilder.js +4 -4
  30. package/dist/components/search/JsonEditor.js +5 -1
  31. package/dist/components/search/SearchBar.d.ts +1 -2
  32. package/dist/components/search/SearchBar.js +12 -3
  33. package/dist/components/search/criteria.d.ts +17 -1
  34. package/dist/components/search/index.d.ts +2 -1
  35. package/dist/components/search/index.js +1 -0
  36. package/dist/components/shell/Panes.d.ts +18 -0
  37. package/dist/components/shell/Panes.js +36 -0
  38. package/dist/components/shell/index.d.ts +1 -0
  39. package/dist/components/shell/index.js +2 -0
  40. package/dist/foundations/calendar.d.ts +3 -1
  41. package/dist/foundations/catalog.d.ts +27 -0
  42. package/dist/foundations/catalog.js +48 -0
  43. package/dist/foundations/copy.d.ts +40 -62
  44. package/dist/foundations/copy.es.js +10 -0
  45. package/dist/foundations/copy.js +25 -93
  46. package/dist/{primitives/field/context.d.ts → foundations/form.d.ts} +9 -0
  47. package/dist/{primitives/field/mode.js → foundations/form.js} +11 -0
  48. package/dist/foundations/grouping.d.ts +9 -3
  49. package/dist/foundations/index.d.ts +2 -0
  50. package/dist/foundations/index.js +5 -0
  51. package/dist/foundations/sticky.d.ts +4 -0
  52. package/dist/foundations/sticky.js +1 -0
  53. package/dist/primitives/button/Button.d.ts +12 -2
  54. package/dist/primitives/button/Button.js +12 -2
  55. package/dist/primitives/cell/CellStack.js +1 -1
  56. package/dist/primitives/choice/ComboBox.d.ts +1 -1
  57. package/dist/primitives/choice/ComboBox.js +4 -4
  58. package/dist/primitives/choice/RadioGroup.d.ts +1 -1
  59. package/dist/primitives/choice/RadioGroup.js +3 -3
  60. package/dist/primitives/choice/SearchField.js +1 -1
  61. package/dist/primitives/choice/SegmentedControl.d.ts +1 -1
  62. package/dist/primitives/choice/SegmentedControl.js +2 -2
  63. package/dist/primitives/choice/Slider.d.ts +1 -1
  64. package/dist/primitives/choice/Slider.js +2 -2
  65. package/dist/primitives/choice/Switch.d.ts +1 -1
  66. package/dist/primitives/choice/Switch.js +2 -2
  67. package/dist/primitives/date/Calendar.js +1 -1
  68. package/dist/primitives/date/DatePicker.d.ts +1 -1
  69. package/dist/primitives/date/DatePicker.js +2 -2
  70. package/dist/primitives/date/DateTime.d.ts +1 -1
  71. package/dist/primitives/date/DateTime.js +2 -2
  72. package/dist/primitives/field/Checks.d.ts +1 -1
  73. package/dist/primitives/field/Checks.js +4 -4
  74. package/dist/primitives/field/DropZone.d.ts +1 -1
  75. package/dist/primitives/field/DropZone.js +2 -2
  76. package/dist/primitives/field/Field.d.ts +10 -5
  77. package/dist/primitives/field/Field.js +10 -6
  78. package/dist/primitives/field/Suggest.d.ts +1 -1
  79. package/dist/primitives/field/Suggest.js +2 -2
  80. package/dist/primitives/field/index.d.ts +1 -3
  81. package/dist/primitives/field/index.js +4 -4
  82. package/dist/primitives/notice/Notice.js +3 -1
  83. package/dist/primitives/picker/Picker.d.ts +1 -1
  84. package/dist/primitives/picker/Picker.js +4 -4
  85. package/dist/primitives/{tag → picker}/TagsField.d.ts +2 -2
  86. package/dist/primitives/{tag → picker}/TagsField.js +5 -5
  87. package/dist/primitives/picker/index.d.ts +2 -0
  88. package/dist/primitives/picker/index.js +5 -1
  89. package/dist/primitives/table/CellCopy.js +1 -1
  90. package/dist/primitives/table/CellEditor.js +2 -4
  91. package/dist/primitives/table/DataTable.d.ts +11 -1
  92. package/dist/primitives/table/DataTable.js +111 -38
  93. package/dist/primitives/table/EditableTable.js +6 -2
  94. package/dist/primitives/table/PivotTable.js +0 -0
  95. package/dist/primitives/table/ReportTable.js +4 -3
  96. package/dist/primitives/table/SheetTable.js +8 -14
  97. package/dist/primitives/table/Table.js +2 -2
  98. package/dist/primitives/tabs/Tabs.d.ts +22 -4
  99. package/dist/primitives/tabs/Tabs.js +77 -23
  100. package/dist/primitives/tabs/index.d.ts +1 -1
  101. package/dist/primitives/tabs/index.js +1 -1
  102. package/dist/primitives/tag/Tags.js +4 -1
  103. package/dist/primitives/tag/index.d.ts +0 -2
  104. package/dist/primitives/tag/index.js +0 -3
  105. package/dist/primitives/value/Avatar.js +8 -1
  106. package/dist/primitives/value/{Fecha.d.ts → DateValue.d.ts} +5 -2
  107. package/dist/primitives/value/{Fecha.js → DateValue.js} +9 -8
  108. package/dist/primitives/value/Relation.d.ts +1 -1
  109. package/dist/primitives/value/Relation.js +2 -2
  110. package/dist/primitives/value/Value.js +4 -1
  111. package/dist/primitives/value/index.d.ts +1 -1
  112. package/dist/primitives/value/index.js +1 -1
  113. package/dist/tokens.css +42 -0
  114. package/dist/views/list/List.d.ts +22 -2
  115. package/dist/views/list/List.js +28 -6
  116. package/dist/views/list/ListView.d.ts +1 -1
  117. package/dist/views/list/ListView.js +47 -16
  118. package/dist/views/list/index.d.ts +2 -2
  119. package/dist/views/list/presentations.js +3 -1
  120. package/dist/views/list/types.d.ts +57 -1
  121. package/dist/views/list/useNarrowList.d.ts +2 -2
  122. package/dist/views/list/useNarrowList.js +5 -5
  123. package/dist/views/record/Record.js +46 -42
  124. package/package.json +1 -1
  125. package/dist/primitives/field/context.js +0 -13
  126. package/dist/primitives/field/mode.d.ts +0 -9
  127. /package/dist/primitives/field/{copy.d.ts → clipboard.d.ts} +0 -0
  128. /package/dist/primitives/field/{copy.js → clipboard.js} +0 -0
  129. /package/dist/primitives/{tag/choices.d.ts → picker/tagChoices.d.ts} +0 -0
  130. /package/dist/primitives/{tag/choices.js → picker/tagChoices.js} +0 -0
@@ -1,8 +1,8 @@
1
- import { type FieldIds } from "./context.js";
2
- import { type FormMode } from "./mode.js";
1
+ import { type FieldIds } from "../../foundations/form.js";
2
+ import { type FormMode } from "../../foundations/form.js";
3
3
  import { type FieldNote } from "./note.js";
4
4
  import { type ReactNode } from "react";
5
- export declare function Field({ label, hint, error, required, children, htmlFor, layout, action, mode, note, copy: copyAs, }: {
5
+ export declare function Field({ label, hint, error, required, children, htmlFor, layout, action, mode, note, changed, copy: copyAs, }: {
6
6
  label: string;
7
7
  /** What is expected. A `?` on the label that opens it on press. */
8
8
  hint?: string;
@@ -32,8 +32,13 @@ export declare function Field({ label, hint, error, required, children, htmlFor,
32
32
  * Aplicar». A card in the record's margin when there is one, a `Flag` by the label when
33
33
  * there is not; its sentence describes the control (`aria-describedby`). See `note.tsx`. */
34
34
  note?: FieldNote;
35
- /** The copy button by the label (see `copy.tsx`): on by default, copying what the control
36
- * shows. A string copies that instead; `false` removes the button from this field. */
35
+ /** Its value changed and is not saved yet. A dot by the label says WHICH fields a
36
+ * «2 cambios sin guardar» is about — in a form of twenty, the reason to look before
37
+ * saving — and it is read as part of the field's description. */
38
+ changed?: boolean;
39
+ /** The copy button by the label (see `clipboard.tsx`): on by default, copying what the
40
+ * control shows. A string copies that instead; `false` removes the button from this
41
+ * field. */
37
42
  copy?: string | false;
38
43
  }): import("react").JSX.Element;
39
44
  /**
@@ -3,12 +3,12 @@ import { jsxs as _jsxs, Fragment as _Fragment, jsx as _jsx } from "react/jsx-run
3
3
  import { AriaLocale } from "../../foundations/aria.js";
4
4
  import { useUiCopy } from "../../foundations/copy.js";
5
5
  import { byDensity, useDensity } from "../../foundations/density.js";
6
+ import { FieldIdsContext, useFieldIds, } from "../../foundations/form.js";
7
+ import { FormModeProvider, useFormMode, } from "../../foundations/form.js";
6
8
  import { focusRing, tv } from "../../foundations/variants.js";
7
9
  import { FieldGroup, listOptionStyles } from "./aria.js";
8
- import { FieldIdsContext, useFieldIds, } from "./context.js";
9
- import { FieldCopy, useFieldCopy } from "./copy.js";
10
+ import { FieldCopy, useFieldCopy } from "./clipboard.js";
10
11
  import { useSheetLayout } from "./layout.js";
11
- import { FormModeProvider, useFormMode, } from "./mode.js";
12
12
  import { describedByOf, NoteCard, NoteFlag, } from "./note.js";
13
13
  import { Hint } from "../hint/Hint.js";
14
14
  import { Popover } from "../overlay/Popover.js";
@@ -40,7 +40,7 @@ import { Button as AriaButton, Input, ListBox, ListBoxItem, NumberField as AriaN
40
40
  * control ate a line on every field. The error still goes below, close to
41
41
  * the cursor, and it says what to put.
42
42
  * ==================================================================== */
43
- export function Field({ label, hint, error, required = false, children, htmlFor, layout, action, mode, note, copy: copyAs, }) {
43
+ export function Field({ label, hint, error, required = false, children, htmlFor, layout, action, mode, note, changed = false, copy: copyAs, }) {
44
44
  const d = useDensity();
45
45
  const copy = useUiCopy();
46
46
  const copies = useFieldCopy() && copyAs !== false;
@@ -52,9 +52,13 @@ export function Field({ label, hint, error, required = false, children, htmlFor,
52
52
  const hintId = `${id}-hint`;
53
53
  const errorId = `${id}-error`;
54
54
  const noteId = `${id}-note`;
55
+ const changedId = `${id}-changed`;
55
56
  /* The error first: what has to be corrected is heard before what is worth knowing. */
56
- const describedBy = describedByOf(hint === undefined ? null : hintId, error === undefined ? null : errorId, note === undefined ? null : noteId);
57
- const labelNode = (_jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted ${inline ? "pt-2" : ""}`, children: [_jsxs("label", { id: `${id}-label`, htmlFor: id, className: inline ? "min-w-0 truncate" : "", children: [label, required ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-ink", "aria-hidden": true, children: [" ", "*"] }), _jsxs("span", { className: "sr-only", children: [", ", copy.field.required] })] })) : null] }), hint === undefined ? null : _jsx(Hint, { id: hintId, children: hint }), note === undefined ? null : _jsx(NoteFlag, { note: note }), copies ? (_jsx(FieldCopy, { label: label, scope: shown, ...(typeof copyAs === "string" ? { value: copyAs } : {}) })) : null] }));
57
+ const describedBy = describedByOf(hint === undefined ? null : hintId, error === undefined ? null : errorId, note === undefined ? null : noteId, changed ? changedId : null);
58
+ const labelNode = (
59
+ /* The label follows its control's focus: in a form of twenty fields, the eye finds the
60
+ row being written by its name as much as by the ring. */
61
+ _jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted transition-colors duration-(--duration-quick) group-focus-within/field:text-primary ${inline ? "pt-2" : ""}`, children: [_jsxs("label", { id: `${id}-label`, htmlFor: id, className: inline ? "min-w-0 truncate" : "", children: [label, required ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-ink", "aria-hidden": true, children: [" ", "*"] }), _jsxs("span", { className: "sr-only", children: [", ", copy.field.required] })] })) : null] }), hint === undefined ? null : _jsx(Hint, { id: hintId, children: hint }), note === undefined ? null : _jsx(NoteFlag, { note: note }), changed ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, title: copy.field.changed, className: "size-1.5 shrink-0 rounded-pill bg-accent transition-transform duration-(--duration-quick) ease-(--ease-out) starting:scale-0" }), _jsx("span", { id: changedId, className: "sr-only", children: copy.field.changed })] })) : null, copies ? (_jsx(FieldCopy, { label: label, scope: shown, ...(typeof copyAs === "string" ? { value: copyAs } : {}) })) : null] }));
58
62
  /* Last in the field so it is read last; absolutely placed, it still sits at the field's
59
63
  top — the static position of an absolute child of a flex or grid box is its start. */
60
64
  const noteCard = note === undefined ? null : _jsx(NoteCard, { note: note, id: noteId, owner: label });
@@ -1,4 +1,4 @@
1
- import { type FormMode } from "./mode.js";
1
+ import { type FormMode } from "../../foundations/form.js";
2
2
  /**
3
3
  * A text with suggestions: the open field that proposes.
4
4
  *
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { AriaLocale } from "../../foundations/aria.js";
4
+ import { useFieldIds } from "../../foundations/form.js";
5
+ import { useFormMode } from "../../foundations/form.js";
4
6
  import { FieldGroup, listOptionStyles } from "./aria.js";
5
- import { useFieldIds } from "./context.js";
6
7
  import { ReadValue } from "./Field.js";
7
- import { useFormMode } from "./mode.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;
@@ -1,9 +1,7 @@
1
1
  export { Checkbox, type CheckItem, CheckList } from "./Checks.js";
2
- export { type FieldIds, useFieldIds } from "./context.js";
3
- export { FieldCopyProvider, shownText, useFieldCopy, } from "./copy.js";
2
+ export { FieldCopyProvider, shownText, useFieldCopy, } from "./clipboard.js";
4
3
  export { DropZone, type DropZoneProps } from "./DropZone.js";
5
4
  export { controlClasses, DataField, Field, NumberInput, ReadValue, Select, type SelectOption, TextArea, TextInput, } from "./Field.js";
6
5
  export { type FieldLayout, FieldLayoutProvider, useSheetLayout, } from "./layout.js";
7
- export { type FormMode, FormModeProvider, useFormMode, } from "./mode.js";
8
6
  export { Suggest, type SuggestProps } from "./Suggest.js";
9
7
  export { type FieldNote } from "./note.js";
@@ -1,15 +1,15 @@
1
- /* THE CONTROLS a form is written with.
1
+ /* THE CONTROLS a form is written with. The contract they speak with their `Field` —
2
+ `useFieldIds`, `useFormMode`— is in `foundations/form`: an application's own control
3
+ speaks it the same way.
2
4
  What ARRANGES them —the sheet, the section, the footer— lives in `components/form`: a
3
5
  control does not know a form exists, and is used the same way inside a cell, a filter or
4
6
  a dialog. */
5
7
  export { Checkbox, CheckList } from "./Checks.js";
6
- export { useFieldIds } from "./context.js";
7
8
  /* The copy button every field carries by its label — on by default, off by field or by form. */
8
- export { FieldCopyProvider, shownText, useFieldCopy, } from "./copy.js";
9
+ export { FieldCopyProvider, shownText, useFieldCopy, } from "./clipboard.js";
9
10
  export { DropZone } from "./DropZone.js";
10
11
  export { controlClasses, DataField, Field, NumberInput, ReadValue, Select, TextArea, TextInput, } from "./Field.js";
11
12
  export { FieldLayoutProvider, useSheetLayout, } from "./layout.js";
12
- export { FormModeProvider, useFormMode, } from "./mode.js";
13
13
  export { Suggest } from "./Suggest.js";
14
14
  /* A note that belongs to one field: a card in the record's margin, or a flag by its label. */
15
15
  export {} from "./note.js";
@@ -27,7 +27,9 @@ import { AlertTriangle, PackageX } from "lucide-react";
27
27
  const SKELETON_NAME = ["w-48", "w-36", "w-56", "w-40", "w-44", "w-32"];
28
28
  export function Skeleton({ rows = 6, label, }) {
29
29
  const d = useDensity();
30
- return (_jsxs("div", { "aria-busy": "true", className: "flex flex-col", children: [_jsx("span", { className: "sr-only", children: label }), Array.from({ length: rows }, (_, i) => (_jsxs("div", { "aria-hidden": true, className: `flex items-center ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`, children: [_jsx("span", { className: `h-3.5 rounded-control bg-ink/8 ${SKELETON_NAME[i % SKELETON_NAME.length]}` }), _jsx("span", { className: "ml-auto h-3 w-14 rounded-control bg-ink/5" })] }, i)))] }));
30
+ return (
31
+ /* It appears late (`animate-reveal-late`): a read faster than 200ms never shows it. */
32
+ _jsxs("div", { "aria-busy": "true", className: "flex animate-reveal-late flex-col", children: [_jsx("span", { className: "sr-only", children: label }), Array.from({ length: rows }, (_, i) => (_jsxs("div", { "aria-hidden": true, className: `flex items-center ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`, children: [_jsx("span", { className: `h-3.5 rounded-control bg-ink/8 ${SKELETON_NAME[i % SKELETON_NAME.length]}` }), _jsx("span", { className: "ml-auto h-3 w-14 rounded-control bg-ink/5" })] }, i)))] }));
31
33
  }
32
34
  /** What is shown when there is nothing to show: a sentence, and the gesture
33
35
  * that resolves it if there is one. Never a drawing. */
@@ -1,5 +1,5 @@
1
+ import { type FormMode } from "../../foundations/form.js";
1
2
  import type { OpenTarget } from "../../foundations/types.js";
2
- import { type FormMode } from "../field/mode.js";
3
3
  import type { LucideIcon } from "lucide-react";
4
4
  import { type ReactNode } from "react";
5
5
  export interface PickerItem {
@@ -2,20 +2,20 @@
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";
6
+ import { useFormMode } from "../../foundations/form.js";
5
7
  import { matchedWord, matches } from "../../foundations/match.js";
6
8
  import { cn, tv } from "../../foundations/variants.js";
7
9
  import { IconButton } from "../button/Button.js";
8
10
  import { FieldButton, FieldGroup } from "../field/aria.js";
9
- import { useFieldIds } from "../field/context.js";
10
11
  import { ReadValue } from "../field/Field.js";
11
- import { useFormMode } from "../field/mode.js";
12
12
  import { Menu } from "../menu/Menu.js";
13
13
  import { Popover } from "../overlay/Popover.js";
14
14
  import { MatchedText } from "./MatchedText.js";
15
15
  import { Avatar } from "../value/Person.js";
16
16
  import { AppWindow, Check, ChevronDown, Ellipsis, Eye, PanelRight, PictureInPicture2, SquareArrowOutUpRight, } from "lucide-react";
17
17
  import { useContext, useMemo, useRef, useState } from "react";
18
- import { Collection, ComboBox as AriaComboBox, ComboBoxStateContext, Header, Input, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, Text, useFilter, } from "react-aria-components";
18
+ import { Collection, ComboBox as AriaComboBox, ComboBoxStateContext, Header, Input, ListBox, ListBoxItem, ListBoxLoadMoreItem, ListBoxSection, useFilter, } from "react-aria-components";
19
19
  /** The icon of each place a record opens — the same in every field and list. */
20
20
  const OPEN_ICON = {
21
21
  modal: PictureInPicture2,
@@ -271,7 +271,7 @@ function Row({ item, selected, marks, hints, query, locale, }) {
271
271
  item.haystack === undefined
272
272
  ? null
273
273
  : matchedWord(item.haystack, query, locale);
274
- return (_jsxs(_Fragment, { children: [marks ? (_jsx("span", { className: cn(hints ? "mt-0.5" : "", "flex size-(--size-icon-sm) shrink-0 items-center justify-center"), children: selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : Icon !== undefined ? (_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 1.75 })) : null })) : null, item.avatar === undefined ? null : (_jsx(Avatar, { name: item.name, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx(Text, { slot: "label", className: cn("min-w-0 truncate text-meta text-ink", selected ? "font-medium" : ""), children: _jsx(MatchedText, { text: item.name, query: query, locale: locale }) }), item.tags] }), item.meta === undefined ? null : (_jsx(Text, { slot: "description", className: "truncate text-micro tabular-nums text-muted", children: _jsx(MatchedText, { text: item.meta, query: query, locale: locale }) })), hidden === null ? null : (_jsxs("span", { className: "truncate text-micro text-muted", children: [copy.picker.matched, " ", _jsx("span", { className: "font-mono", children: _jsx(MatchedText, { text: hidden, query: query, locale: locale }) })] }))] }), item.aside === undefined ? null : (_jsx("span", { className: "flex shrink-0 items-center gap-1.5 self-center text-meta text-ink tabular-nums", children: item.aside })), !marks && selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] }));
274
+ return (_jsxs(_Fragment, { children: [marks ? (_jsx("span", { className: cn(hints ? "mt-0.5" : "", "flex size-(--size-icon-sm) shrink-0 items-center justify-center"), children: selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : Icon !== undefined ? (_jsx(Icon, { "aria-hidden": true, className: "size-(--size-icon-sm) text-muted", strokeWidth: 1.75 })) : null })) : null, item.avatar === undefined ? null : (_jsx(Avatar, { name: item.name, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: cn("min-w-0 truncate text-meta text-ink", selected ? "font-medium" : ""), children: _jsx(MatchedText, { text: item.name, query: query, locale: locale }) }), item.tags] }), item.meta === undefined && hidden === null ? null : (_jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [item.meta === undefined ? null : (_jsx("span", { className: "truncate text-micro tabular-nums text-muted", children: _jsx(MatchedText, { text: item.meta, query: query, locale: locale }) })), hidden === null ? null : (_jsxs("span", { className: "truncate text-micro text-muted", children: [copy.picker.matched, " ", _jsx("span", { className: "font-mono", children: _jsx(MatchedText, { text: hidden, query: query, locale: locale }) })] }))] }))] }), item.aside === undefined ? null : (_jsx("span", { className: "flex shrink-0 items-center gap-1.5 self-center text-meta text-ink tabular-nums", children: item.aside })), !marks && selected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] }));
275
275
  }
276
276
  /** Hands the combobox's `close` to the code outside it: React Aria owns the open state and
277
277
  * offers no controlled `isOpen`, and the «Buscar más…» row has to close the list it lives in. */
@@ -1,6 +1,6 @@
1
+ import { type FormMode } from "../../foundations/form.js";
1
2
  import type { OpenTarget } from "../../foundations/types.js";
2
- import { type FormMode } from "../field/mode.js";
3
- import { type TagOption } from "./choices.js";
3
+ import { type TagOption } from "./tagChoices.js";
4
4
  export type TagsFieldProps = {
5
5
  /** The ids of the chosen tags, in the order they were added. */
6
6
  value: readonly string[];
@@ -2,15 +2,15 @@
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";
6
+ import { useFormMode } from "../../foundations/form.js";
5
7
  import { cn } from "../../foundations/variants.js";
6
8
  import { fieldGroupStyles, listOptionStyles } from "../field/aria.js";
7
- import { useFieldIds } from "../field/context.js";
8
9
  import { ReadValue } from "../field/Field.js";
9
- import { useFormMode } from "../field/mode.js";
10
10
  import { Popover } from "../overlay/Popover.js";
11
- import { MatchedText } from "../picker/MatchedText.js";
12
- import { tagChoices, withoutTag, withTag, } from "./choices.js";
13
- import { Tags } from "./Tags.js";
11
+ import { MatchedText } from "./MatchedText.js";
12
+ import { tagChoices, withoutTag, withTag, } from "./tagChoices.js";
13
+ import { Tags } from "../tag/Tags.js";
14
14
  import { Avatar } from "../value/Person.js";
15
15
  import { Plus, Search } from "lucide-react";
16
16
  import { useContext, useEffect, useMemo, useRef, useState, } from "react";
@@ -1 +1,3 @@
1
1
  export { Picker, type PickerItem, type PickerProps, } from "./Picker.js";
2
+ export { type TagOption } from "./tagChoices.js";
3
+ export { TagsField, type TagsFieldProps } from "./TagsField.js";
@@ -1,2 +1,6 @@
1
- /* The replacement for `<select>` when you have to SEE in order to choose. */
1
+ /* CHOOSING RECORDS — one (`Picker`, the many2one) or several (`TagsField`, the many2many).
2
+ The replacement for `<select>` when you have to SEE in order to choose. */
2
3
  export { Picker, } from "./Picker.js";
4
+ /* El many2many: las fichas elegidas más una entrada que agrega la siguiente. */
5
+ export {} from "./tagChoices.js";
6
+ export { TagsField } from "./TagsField.js";
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
- import { shownText } from "../field/copy.js";
4
+ import { shownText } from "../field/clipboard.js";
5
5
  import { CopyButton } from "../value/Copyable.js";
6
6
  import { useRef } from "react";
7
7
  /**
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn, focusRing } from "../../foundations/variants.js";
4
4
  import { Popover } from "../overlay/Popover.js";
5
5
  import { Pencil } from "lucide-react";
6
- import { useEffect, useId, useRef } from "react";
6
+ import { useEffect, useRef } from "react";
7
7
  import { Button as AriaButton, Dialog, DialogTrigger } from "react-aria-components";
8
8
  /* ==================================================================== *
9
9
  * CELL EDITOR — change one value of a list without leaving the list.
@@ -75,9 +75,7 @@ function EditorDialog({ label, editor, }) {
75
75
  * Internal to the table family: a column asks for it with `DataColumn.edit`.
76
76
  */
77
77
  export function CellEditor({ label, numeric, editor, children, }) {
78
- const verb = useId();
79
- const shown = useId();
80
78
  return (
81
79
  /* The row may open its record on click; changing one cell must not also open it. */
82
- _jsx("div", { onClick: (event) => event.stopPropagation(), className: "-mx-1 min-w-0", children: _jsxs(DialogTrigger, { children: [_jsxs(AriaButton, { "aria-labelledby": `${verb} ${shown}`, className: ({ isFocusVisible }) => cn("group/edit flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-mark px-1 text-inherit outline-0 transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10 aria-expanded:bg-primary/8", numeric ? "flex-row-reverse text-right" : "text-left", focusRing({ isFocusVisible })), children: [_jsx("span", { id: verb, hidden: true, children: label }), _jsx("span", { id: shown, className: "min-w-0 flex-1 truncate", children: children }), _jsx(Pencil, { "aria-hidden": true, strokeWidth: 2, className: "size-(--size-icon-sm) shrink-0 text-muted opacity-0 transition-opacity duration-(--duration-instant) group-hover/edit:opacity-100 group-focus-visible/edit:opacity-100 group-aria-expanded/edit:opacity-100" })] }), _jsx(Popover, { placement: numeric ? "bottom end" : "bottom start", children: _jsx(EditorDialog, { label: label, editor: editor }) })] }) }));
80
+ _jsx("div", { onClick: (event) => event.stopPropagation(), className: "-mx-1 min-w-0", children: _jsxs(DialogTrigger, { children: [_jsxs(AriaButton, { className: ({ isFocusVisible }) => cn("group/edit flex w-full min-w-0 cursor-pointer items-center gap-1.5 rounded-mark px-1 text-inherit outline-0 transition-colors duration-(--duration-instant) hover:bg-ink/6 pressed:bg-ink/10 aria-expanded:bg-primary/8", numeric ? "flex-row-reverse text-right" : "text-left", focusRing({ isFocusVisible })), children: [_jsx("span", { className: "sr-only", children: `${label} ` }), _jsx("span", { className: "min-w-0 flex-1 truncate", children: children }), _jsx(Pencil, { "aria-hidden": true, strokeWidth: 2, className: "size-(--size-icon-sm) shrink-0 text-muted opacity-0 transition-opacity duration-(--duration-instant) group-hover/edit:opacity-100 group-focus-visible/edit:opacity-100 group-aria-expanded/edit:opacity-100" })] }), _jsx(Popover, { placement: numeric ? "bottom end" : "bottom start", children: _jsx(EditorDialog, { label: label, editor: editor }) })] }) }));
83
81
  }
@@ -1,3 +1,4 @@
1
+ import { type Pile } from "../../foundations/grouping.js";
1
2
  import type { Tone } from "../../foundations/types.js";
2
3
  import { type RowAction } from "./RowActions.js";
3
4
  import { type SortState } from "./Table.js";
@@ -148,7 +149,7 @@ export type GroupColumn<T> = {
148
149
  /** Cómo se lee esa clave. */
149
150
  labelOf: (key: string) => string;
150
151
  };
151
- export declare function DataTable<T extends object>({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals, collapsed, onGroupToggle, create, copyable, }: {
152
+ export declare function DataTable<T extends object>({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals, collapsed, onGroupToggle, piles, onMore, create, copyable, }: {
152
153
  data: readonly T[];
153
154
  columns: readonly DataColumn<T>[];
154
155
  getRowId: (row: T) => string;
@@ -181,6 +182,15 @@ export declare function DataTable<T extends object>({ data, columns, getRowId, s
181
182
  * table does not reopen what rows just folded. */
182
183
  collapsed?: readonly string[];
183
184
  onGroupToggle?: (key: string) => void;
185
+ /**
186
+ * The piles ALREADY counted by whoever keeps the data — «187» over the whole set, not
187
+ * the 12 rows of the page. Given, the table draws them as they come (their order, their
188
+ * labels, their `detail`) instead of grouping `data` by `levels`.
189
+ */
190
+ piles?: readonly Pile<T>[];
191
+ /** A pile shows its first page and counts them all: this asks for the next ones, and
192
+ * the table offers it under each pile that holds fewer than it counts. */
193
+ onMore?: (pile: Pile<T>) => void;
184
194
  /** Write a new record as a row of the list. See the file's header. */
185
195
  create?: DataTableCreate<T>;
186
196
  /** The cells' copy buttons, on by default. `false` takes them off the whole table; a
@@ -1,7 +1,7 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
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 { pathKey } from "../../foundations/grouping.js";
4
+ import { pathKey, pileItems } from "../../foundations/grouping.js";
5
5
  import { CellCopy } from "./CellCopy.js";
6
6
  import { CellEditor } from "./CellEditor.js";
7
7
  import { EditRow, firstControl } from "./EditableTable.js";
@@ -118,7 +118,47 @@ export function aggregateOf(column, rows) {
118
118
  return numbers.length;
119
119
  }
120
120
  }
121
- export function DataTable({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals = false, collapsed, onGroupToggle, create, copyable = true, }) {
121
+ /** The piles someone else counted, as the body draws them: open ones with what is inside,
122
+ * the deepest with its records and, when it holds fewer than it counts, its «show more». */
123
+ function entriesOfPiles(piles, keyOf, collapsed, onToggle, onMore) {
124
+ return piles.flatMap((pile) => {
125
+ const key = pathKey(pile.path);
126
+ const open = collapsed?.includes(key) !== true;
127
+ const head = {
128
+ kind: "pile",
129
+ id: `\u0000pile${key}`,
130
+ depth: pile.depth,
131
+ level: pile.field,
132
+ label: pile.label,
133
+ count: pile.count,
134
+ detail: pile.detail,
135
+ open,
136
+ toggle: onToggle === undefined ? undefined : () => onToggle(key),
137
+ };
138
+ if (!open)
139
+ return [head];
140
+ if (pile.piles.length > 0) {
141
+ return [head, ...entriesOfPiles(pile.piles, keyOf, collapsed, onToggle, onMore)];
142
+ }
143
+ const left = pile.count - pile.items.length;
144
+ return [
145
+ head,
146
+ ...pile.items.map((item) => ({ kind: "row", id: keyOf(item), item })),
147
+ ...(left > 0 && onMore !== undefined
148
+ ? [
149
+ {
150
+ kind: "more",
151
+ id: `\u0000more${key}`,
152
+ depth: pile.depth,
153
+ left,
154
+ more: () => onMore(pile),
155
+ },
156
+ ]
157
+ : []),
158
+ ];
159
+ });
160
+ }
161
+ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels, selected, onSelectedChange, height, onRowClick, rowActions, totals = false, collapsed, onGroupToggle, piles, onMore, create, copyable = true, }) {
122
162
  const copy = useUiCopy();
123
163
  const draft = create?.draft ?? null;
124
164
  const draftRow = useRef(null);
@@ -209,7 +249,8 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
209
249
  sorting: sort === null || sort === undefined
210
250
  ? []
211
251
  : [{ id: sort.key, desc: sort.direction === "desc" }],
212
- grouping: (levels ?? []).map((level) => level.id),
252
+ /* Counted elsewhere, the tree comes made: grouping the page again would undo it. */
253
+ grouping: piles === undefined ? (levels ?? []).map((level) => level.id) : [],
213
254
  },
214
255
  // Groups start open: grouping and then having to open each group is two
215
256
  // gestures for what the user asked for in one.
@@ -247,7 +288,7 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
247
288
  the same thing; with several, folding the outer pile has to take the inner
248
289
  ones with it — otherwise closing «Ferretería Sur» leaves its months on screen,
249
290
  orphaned under a header that says it is closed. */
250
- const drawn = collapsed === undefined
291
+ const shown = collapsed === undefined
251
292
  ? rows
252
293
  : rows.filter((row) => {
253
294
  let node = row.getParentRow();
@@ -258,22 +299,56 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
258
299
  }
259
300
  return true;
260
301
  });
302
+ /* ONE sequence for the body, whoever grouped: the piles counted elsewhere, or the tree
303
+ TanStack built from the page. Before, the table only knew the second — grouped on the
304
+ server, a list said «187» in rows and «12» in the table. */
305
+ const entries = piles !== undefined
306
+ ? entriesOfPiles(piles, getRowId, collapsed, onGroupToggle, onMore)
307
+ : shown.map((row) => {
308
+ if (row.subRows.length === 0)
309
+ return { kind: "row", id: row.id, item: row.original };
310
+ const path = pathOf(row);
311
+ const key = pathKey(path);
312
+ const depth = path.length - 1;
313
+ const level = levels?.find((one) => one.id === row.groupingColumnId);
314
+ /* La CLAVE del montón —la del motor— y el rótulo, que lo pone el nivel: son dos
315
+ textos y sólo el segundo cambia con el idioma. */
316
+ const value = path[depth] ?? key;
317
+ return {
318
+ kind: "pile",
319
+ id: row.id,
320
+ depth,
321
+ level: level?.label,
322
+ label: level === undefined ? value : level.labelOf(value),
323
+ /* Las FILAS de adentro, no los subgrupos: `subRows` de un montón con niveles
324
+ abajo son los montones del nivel siguiente, y contarlos decía «2» donde hay
325
+ cuarenta filas. */
326
+ count: row.getLeafRows().filter((leaf) => leaf.subRows.length === 0).length,
327
+ detail: undefined,
328
+ open: collapsed === undefined ? row.getIsExpanded() : !collapsed.includes(key),
329
+ toggle: () => onGroupToggle === undefined ? row.toggleExpanded() : onGroupToggle(key),
330
+ };
331
+ });
261
332
  const selecting = onSelectedChange !== undefined;
262
333
  const width = columns.length + (selecting ? 1 : 0) + (rowActions === undefined ? 0 : 1);
263
334
  /* Past a few hundred rows only the ones in view are drawn — see `rowWindow.tsx`. It
264
- windows what is DRAWN, group rows included and folded rows left out: that is the
335
+ windows what is DRAWN, pile headers included and folded rows left out: that is the
265
336
  sequence a person scrolls through. */
266
337
  const view = useRowWindow({
267
- count: drawn.length,
268
- getKey: (i) => drawn[i]?.id ?? String(i),
338
+ count: entries.length,
339
+ getKey: (i) => entries[i]?.id ?? String(i),
269
340
  });
270
- const leaves = useMemo(() => rows.filter((r) => r.subRows.length === 0), [rows]);
341
+ /* Every record the selection can reach — folded ones included: «select all» over a
342
+ grouped list means all of them, not the ones whose pile happens to be open. */
343
+ const leaves = useMemo(() => piles !== undefined
344
+ ? pileItems(piles).map(getRowId)
345
+ : rows.filter((r) => r.subRows.length === 0).map((r) => r.id), [piles, rows, getRowId]);
271
346
  /* The aggregates are computed by the component and not by TanStack: its
272
347
  `aggregationFn` lives in the GROUP model, and a footer exists precisely when
273
348
  there are no groups. */
274
349
  const footer = useMemo(() => (totals ? columns.map((c) => aggregateOf(c, data)) : null), [totals, columns, data]);
275
- const all = leaves.length > 0 && leaves.every((r) => selected?.has(r.id) === true);
276
- const some = leaves.some((r) => selected?.has(r.id) === true);
350
+ const all = leaves.length > 0 && leaves.every((id) => selected?.has(id) === true);
351
+ const some = leaves.some((id) => selected?.has(id) === true);
277
352
  function toggle(id, on) {
278
353
  const n = new Set(selected ?? []);
279
354
  if (on)
@@ -284,49 +359,47 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
284
359
  }
285
360
  const grid = (_jsxs(Table, { height: height, ...(view.on
286
361
  ? {
287
- rowCount: 1 + drawn.length + (draft === null ? 0 : 1) + (totals ? 1 : 0),
362
+ rowCount: 1 + entries.length + (draft === null ? 0 : 1) + (totals ? 1 : 0),
288
363
  }
289
- : {}), children: [_jsxs(THead, { ...(view.on ? { rowIndex: 1 } : {}), children: [selecting ? (_jsx(Th, { width: 36, children: _jsx(SelectBox, { label: copy.selection.all, checked: all, indeterminate: some, onChange: (v) => onSelectedChange?.(v ? new Set(leaves.map((r) => r.id)) : new Set()) }) })) : null, columns.map((c) => (_jsx(Th, { sortKey: c.sortable === false ? undefined : c.id, sort: sort, onSort: onSortChange, numeric: c.numeric, width: c.width, children: c.header }, c.id))), rowActions === undefined ? null : _jsx(Th, { width: 1, children: "" })] }), _jsxs("tbody", { ref: view.body.ref, children: [view.rows.flatMap((slot, at) => {
290
- const row = drawn[slot.index];
291
- if (row === undefined)
364
+ : {}), children: [_jsxs(THead, { ...(view.on ? { rowIndex: 1 } : {}), children: [selecting ? (_jsx(Th, { width: 36, children: _jsx(SelectBox, { label: copy.selection.all, checked: all, indeterminate: some, onChange: (v) => onSelectedChange?.(v ? new Set(leaves) : new Set()) }) })) : null, columns.map((c) => (_jsx(Th, { sortKey: c.sortable === false ? undefined : c.id, sort: sort, onSort: onSortChange, numeric: c.numeric, width: c.width, children: c.header }, c.id))), rowActions === undefined ? null : (_jsx("td", { className: "w-px border-b border-line-strong" }))] }), _jsxs("tbody", { ref: view.body.ref, children: [view.rows.flatMap((slot, at) => {
365
+ const entry = entries[slot.index];
366
+ if (entry === undefined)
292
367
  return [];
293
- const isGroup = row.subRows.length > 0;
294
368
  const windowed = view.windowed(slot.index);
295
369
  /* What is not drawn between the previous drawn row and this one: the rows
296
370
  above the view before the first, and the gap before a row kept only because
297
371
  it has the focus. */
298
372
  const gap = view.pad(at === 0 ? 0 : view.rows[at - 1].end, slot.start, width, `\u0000pad${slot.index}`);
299
- if (isGroup) {
300
- const path = pathOf(row);
301
- const key = pathKey(path);
302
- const depth = path.length - 1;
303
- const id = row.groupingColumnId ?? undefined;
304
- const level = levels?.find((one) => one.id === id);
305
- /* La CLAVE del montón —la del motor— y el rótulo, que lo pone el
306
- nivel: son dos textos y sólo el segundo cambia con el idioma. */
307
- const value = path[depth] ?? key;
308
- const open = collapsed === undefined
309
- ? row.getIsExpanded()
310
- : collapsed.includes(key) !== true;
373
+ const indent = entry.kind === "row" || entry.depth === 0
374
+ ? undefined
375
+ : { paddingLeft: `calc(var(--spacing-indent) * ${entry.depth} + 0.75rem)` };
376
+ if (entry.kind === "pile") {
377
+ const head = (_jsxs(_Fragment, { children: [entry.toggle === undefined ? null : (_jsx(ChevronRight, { className: `size-(--size-icon-sm) text-muted transition-transform duration-(--duration-instant) ${entry.open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true })), entry.level === undefined ? null : (_jsx("span", { className: "text-micro font-normal text-muted", children: entry.level })), _jsx("span", { children: entry.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: entry.count.toLocaleString(copy.locale) }), entry.detail === undefined ? null : (_jsx("span", { className: "text-micro font-normal tabular-nums text-muted", children: entry.detail }))] }));
378
+ return [
379
+ gap,
380
+ /* The pile's row: it spans the whole width, says which value it groups by
381
+ and how many are inside — what makes it useful without opening it. */
382
+ _jsx("tr", { ...windowed, className: "border-b border-line bg-ground", children: _jsx("td", { colSpan: width, className: "px-3 py-1.5", style: indent, children: entry.toggle === undefined ? (_jsx("span", { className: "inline-flex items-center gap-1.5 text-meta font-medium text-ink", children: head })) : (_jsx("button", { type: "button", onClick: entry.toggle, "aria-expanded": entry.open, className: "inline-flex items-center gap-1.5 rounded-control text-meta font-medium text-ink transition-colors duration-(--duration-instant) hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: head })) }) }, entry.id),
383
+ ];
384
+ }
385
+ if (entry.kind === "more") {
311
386
  return [
312
387
  gap,
313
- /* The group row: it spans the whole width, says which value it
314
- groups by and how many are inside. The counter is what makes it
315
- useful without opening it. */
316
- _jsx("tr", { ...windowed, className: "border-b border-line bg-ground", children: _jsx("td", { colSpan: width, className: "px-3 py-1.5", style: depth === 0
317
- ? undefined
318
- : { paddingLeft: `calc(var(--spacing-indent) * ${depth} + 0.75rem)` }, children: _jsxs("button", { type: "button", onClick: () => onGroupToggle === undefined ? row.toggleExpanded() : onGroupToggle(key), "aria-expanded": open, className: "inline-flex items-center gap-1.5 rounded-control text-meta font-medium text-ink transition-colors duration-(--duration-instant) hover:text-primary focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: [_jsx(ChevronRight, { className: `size-(--size-icon-sm) text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true }), level === undefined ? null : (_jsx("span", { className: "text-micro font-normal text-muted/70", children: level.label })), _jsx("span", { children: level === undefined ? value : level.labelOf(value) }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: row.getLeafRows().length.toLocaleString(copy.locale) })] }) }) }, row.id),
388
+ _jsx("tr", { ...windowed, className: "border-b border-line", children: _jsx("td", { colSpan: width, className: "px-3 py-1", style: indent, children: _jsx("button", { type: "button", onClick: entry.more, className: "rounded-control text-meta text-primary transition-colors duration-(--duration-instant) hover:underline focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-primary", children: fillCopy(copy.table.pileMore, {
389
+ count: entry.left.toLocaleString(copy.locale),
390
+ }) }) }) }, entry.id),
319
391
  ];
320
392
  }
321
- const original = row.original;
393
+ const original = entry.item;
394
+ const id = entry.id;
322
395
  return [
323
396
  gap,
324
- _jsxs(Tr, { ...(windowed === undefined ? {} : { windowed }), selected: selected?.has(row.id) === true, onClick: onRowClick === undefined ? undefined : () => onRowClick(original), children: [selecting ? (_jsx(Td, { children: _jsx(SelectBox, { label: copy.selection.row, checked: selected?.has(row.id) === true, onChange: (v) => toggle(row.id, v) }) })) : null, columns.map((c) => {
397
+ _jsxs(Tr, { ...(windowed === undefined ? {} : { windowed }), selected: selected?.has(id) === true, onClick: onRowClick === undefined ? undefined : () => onRowClick(original), children: [selecting ? (_jsx(Td, { children: _jsx(SelectBox, { label: copy.selection.row, checked: selected?.has(id) === true, onChange: (v) => toggle(id, v) }) })) : null, columns.map((c) => {
325
398
  const t = c.tone?.(original);
326
399
  const n = c.note?.(original);
327
400
  const edit = c.edit?.when?.(original) === false ? undefined : c.edit;
328
401
  return (_jsx(Td, { numeric: c.numeric, mono: c.mono === true, ...(t === undefined ? {} : { tone: t }), ...(n === undefined ? {} : { title: n }), children: edit !== undefined ? (_jsx(CellEditor, { label: fillCopy(copy.inlineEdit.editCell, { column: c.header }), numeric: c.numeric === true, editor: (done) => edit.render(original, done), children: drawCell(copy, c, original) })) : copyable && c.copy !== false ? (_jsx(CellCopy, { header: c.header, numeric: c.numeric === true, ...(c.copy === undefined ? {} : { value: c.copy(original) }), children: drawCell(copy, c, original) })) : (drawCell(copy, c, original)) }, c.id));
329
- }), rowActions === undefined ? null : (_jsx(RowActions, { actions: rowActions(original) }))] }, row.id),
402
+ }), rowActions === undefined ? null : (_jsx(RowActions, { actions: rowActions(original) }))] }, id),
330
403
  ];
331
404
  }), view.tail(width), create === undefined || draft === null ? null : (_jsxs(EditRow, { as: "tr", rowId: "\\u0000draft", rowRef: (node) => {
332
405
  draftRow.current = node;
@@ -344,7 +417,7 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
344
417
  }, children: [selecting ? _jsx("td", { className: DRAFT_CELL }) : null, columns.map((c) => (_jsx("td", { ...(c.input === undefined ? {} : { "data-edit": "" }), className: `${DRAFT_CELL} ${c.numeric === true ? "text-right tabular-nums" : ""}`, children: c.input?.(draft, (next) => {
345
418
  touched.current = true;
346
419
  create.onChange(next);
347
- }) ?? null }, c.id))), rowActions === undefined ? null : _jsx("td", { className: DRAFT_CELL })] }))] }), footer === null ? null : (_jsx("tfoot", { className: "sticky bottom-0 z-10 bg-ground", children: _jsxs("tr", { className: "border-t border-line-strong", "aria-rowindex": view.on ? 2 + drawn.length + (draft === null ? 0 : 1) : undefined, children: [selecting ? _jsx("td", {}) : null, columns.map((c, i) => {
420
+ }) ?? null }, c.id))), rowActions === undefined ? null : _jsx("td", { className: DRAFT_CELL })] }))] }), footer === null ? null : (_jsx("tfoot", { className: "sticky bottom-0 z-10 bg-ground", children: _jsxs("tr", { className: "border-t border-line-strong", "aria-rowindex": view.on ? 2 + entries.length + (draft === null ? 0 : 1) : undefined, children: [selecting ? _jsx("td", {}) : null, columns.map((c, i) => {
348
421
  const value = footer[i] ?? null;
349
422
  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));
350
423
  }), rowActions === undefined ? null : _jsx("td", {})] }) }))] }));
@@ -50,7 +50,7 @@ function says(node) {
50
50
  /** The label of a group band — its field, its value, how many rows it holds. The same
51
51
  * in the table's band and the stacked list's. */
52
52
  function pileTag(pile, locale) {
53
- return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted/70", children: pile.field }), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: pile.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: pile.count.toLocaleString(locale) })] }));
53
+ return (_jsxs("span", { className: "flex min-w-0 items-center gap-1.5", children: [_jsx("span", { className: "shrink-0 text-micro text-muted", children: pile.field }), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: pile.label }), _jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro tabular-nums text-muted", children: pile.count.toLocaleString(locale) })] }));
54
54
  }
55
55
  /** The focus ring of an operable row: drawn on the row, inset, so it is not cut by the
56
56
  * box and does not move the cells. */
@@ -602,7 +602,11 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
602
602
  return null;
603
603
  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));
604
604
  }) }))] }));
605
- return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, className: `min-w-0 ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [reorderable ? _jsx("th", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width }, className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("th", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { ...(reorderable
605
+ 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", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { 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 },
606
+ /* A column of figures takes its figures' width (`w-0` in an auto
607
+ table is «as narrow as its content») and the text takes the rest:
608
+ the product name was cut while «Cantidad» had room for ten digits. */
609
+ 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" }))] }) }), _jsx("tbody", { ...(reorderable
606
610
  ? {
607
611
  onDragOver: overRow,
608
612
  onDrop: (event) => {
Binary file
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { currentCopyLocale, fillCopy, useUiCopy } from "../../foundations/copy.js";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
4
  import { byDensity, useDensity } from "../../foundations/density.js";
5
5
  import { stickyTop, useScrollBox } from "../../foundations/sticky.js";
6
6
  import { HEADER_TEXT } from "./Table.js";
@@ -44,11 +44,12 @@ const SKIN = {
44
44
  },
45
45
  };
46
46
  function Amount({ value, format }) {
47
+ const { locale } = useUiCopy();
47
48
  if (value === null)
48
49
  return _jsx("span", { className: "text-line-strong", children: "\u00B7" });
49
50
  const zero = value === 0;
50
51
  const negative = value < 0;
51
- return (_jsx("span", { className: `tabular-nums ${zero ? "text-muted" : negative ? "text-bad" : "text-ink"}`, children: format === undefined ? amountFormat(currentCopyLocale()).format(value) : format(value) }));
52
+ return (_jsx("span", { className: `tabular-nums ${zero ? "text-muted" : negative ? "text-bad" : "text-ink"}`, children: format === undefined ? amountFormat(locale).format(value) : format(value) }));
52
53
  }
53
54
  function ReportLine({ row, columns, open, onToggle, compact, }) {
54
55
  const copy = useUiCopy();
@@ -103,5 +104,5 @@ defaultOpen, caption, height, }) {
103
104
  desplazamiento de la barra y quedaba empujado 68 px hacia adentro de su
104
105
  propia caja — banda en blanco arriba y títulos de columna cayendo entre
105
106
  las filas. */
106
- _jsx("div", { ref: caja.ref, className: `min-w-0 ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right ${HEADER_TEXT} ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("th", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
107
+ _jsx("div", { ref: caja.ref, tabIndex: caja.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${caja.className}`, style: caja.style, children: _jsxs("table", { className: "w-full border-collapse px-1", children: [caption === undefined ? null : (_jsx("caption", { className: `text-left text-meta text-muted ${byDensity(d, "px-3 pb-2", "px-2 pb-1.5")}`, children: caption })), _jsx("thead", { className: "sticky z-20 bg-ground", style: stickyTop, children: _jsxs("tr", { className: "border-b border-line-strong", children: [_jsx("th", { className: `sticky left-0 z-10 bg-ground text-left ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: _jsx("span", { className: "sr-only", children: copy.report.concept }) }), columns.map((c) => (_jsx("th", { scope: "col", className: `text-right ${HEADER_TEXT} ${compact ? "px-2 py-1.5" : "px-3 py-2"}`, children: c.header }, c.id))), _jsx("td", { className: "w-0" })] }) }), _jsx("tbody", { children: rows.map((r) => (_jsx(ReportLine, { row: r, columns: columns, open: open, onToggle: toggle, compact: compact }, r.id))) })] }) }));
107
108
  }