@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
@@ -9,7 +9,7 @@ import { SegmentedControl } from "../../primitives/choice/SegmentedControl.js";
9
9
  import { useNoteEntries } from "../../primitives/field/note.js";
10
10
  import { Callout } from "../../primitives/notice/Callout.js";
11
11
  import { Tag } from "../../primitives/tag/Tag.js";
12
- import { DateValue } from "../../primitives/value/Fecha.js";
12
+ import { DateValue } from "../../primitives/value/DateValue.js";
13
13
  import { Avatar } from "../../primitives/value/Person.js";
14
14
  import { Activity as ActivityIcon, ArrowRight, CheckCircle2, ChevronDown, ChevronRight, ChevronUp, CornerDownRight, History, Info, Lock, MessageSquare, OctagonAlert, PanelRightClose, PanelRightOpen, TriangleAlert, Zap, } from "lucide-react";
15
15
  import { Fragment, useId, useState } from "react";
@@ -1,3 +1,4 @@
1
+ import { type UiCopy } from "../../foundations/copy.js";
1
2
  import { type ReactNode } from "react";
2
3
  /**
3
4
  * The box to write in. What gets written is drawn by `Activity`, together with
@@ -51,9 +52,10 @@ export type MessageKind = {
51
52
  *
52
53
  * Es una función y no una constante porque sus palabras salen de la copia, y una
53
54
  * constante las congelaría con las que hubiera al cargar el módulo —o sea, las
54
- * de fábrica— por más que después se instale otra.
55
+ * de fábrica— por más que después se instale otra. Sin `copy`, las instaladas; un
56
+ * componente le pasa las de su rama.
55
57
  */
56
- export declare function writeANote(): MessageKind;
58
+ export declare function writeANote(copy?: UiCopy): MessageKind;
57
59
  export type MessageComposerProps = {
58
60
  /** The ways of writing this record takes. Its own list because it is the
59
61
  * application that knows whether there is WhatsApp, and to whose number. */
@@ -10,10 +10,10 @@ import { useState } from "react";
10
10
  *
11
11
  * Es una función y no una constante porque sus palabras salen de la copia, y una
12
12
  * constante las congelaría con las que hubiera al cargar el módulo —o sea, las
13
- * de fábrica— por más que después se instale otra.
13
+ * de fábrica— por más que después se instale otra. Sin `copy`, las instaladas; un
14
+ * componente le pasa las de su rama.
14
15
  */
15
- export function writeANote() {
16
- const copy = currentCopy();
16
+ export function writeANote(copy = currentCopy()) {
17
17
  return {
18
18
  id: "note",
19
19
  label: copy.activity.writeNote,
@@ -26,7 +26,7 @@ export function MessageComposer({ kinds, onSend, sending = false, footer, }) {
26
26
  /* La manera por default se arma acá adentro y no en la firma: un default de
27
27
  parámetro se evalúa en cada render, sí, pero escrito como constante del
28
28
  módulo congelaría las palabras de fábrica. */
29
- const ways = kinds ?? [writeANote()];
29
+ const ways = kinds ?? [writeANote(copy)];
30
30
  const [writing, setWriting] = useState(undefined);
31
31
  const [body, setBody] = useState("");
32
32
  const ready = body.trim() !== "" && !sending;
@@ -2,7 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { SurfaceProvider, useSurface } from "../../foundations/surface.js";
4
4
  import { Empty } from "../../primitives/notice/Notice.js";
5
- import { DateValue } from "../../primitives/value/Fecha.js";
5
+ import { DateValue } from "../../primitives/value/DateValue.js";
6
6
  import { Children } from "react";
7
7
  const DOT_TONE = {
8
8
  neutral: { canvas: "bg-muted", chrome: "bg-chrome-muted" },
@@ -5,7 +5,7 @@ import { currentCopy, fillCopy, useUiCopy } from "../../foundations/copy.js";
5
5
  import { cn, focusRing } from "../../foundations/variants.js";
6
6
  import { PopoverTrigger } from "../../primitives/overlay/PopoverTrigger.js";
7
7
  import { Tooltip, TooltipTrigger } from "../../primitives/overlay/Tooltip.js";
8
- import { DateValue } from "../../primitives/value/Fecha.js";
8
+ import { DateValue } from "../../primitives/value/DateValue.js";
9
9
  import { Link } from "react-aria-components";
10
10
  /* ==================================================================== *
11
11
  * THE ASSISTANT'S MARK — how a screen says «this came from the assistant».
@@ -3,8 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { acceptedMessage, applyWithUndo, } from "./assist.js";
4
4
  import { AssistLabel, AssistSourceLink } from "./AssistMark.js";
5
5
  import { useUiCopy } from "../../foundations/copy.js";
6
+ import { FieldIdsContext } from "../../foundations/form.js";
6
7
  import { Button, IconButton } from "../../primitives/button/Button.js";
7
- import { FieldIdsContext } from "../../primitives/field/context.js";
8
8
  import { useToastIfAny } from "../../primitives/notice/Toast.js";
9
9
  import { Check, X } from "lucide-react";
10
10
  import { useContext, useId } from "react";
@@ -79,7 +79,7 @@ function ReviewBody({ review }) {
79
79
  const copy = useUiCopy();
80
80
  const { groups, order, current, choose } = review;
81
81
  const state = chosenState(order, current);
82
- return (_jsxs("div", { className: "@container flex min-w-0 flex-col", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-between gap-x-3 gap-y-1 border-b border-line px-4 py-2", children: [_jsx(Checkbox, { checked: state === "all", mixed: state === "some", label: copy.assist.all, onChange: (on) => choose(on ? [...order] : []) }), _jsx("span", { className: "text-micro text-muted tabular-nums", children: fillCopy(copy.assist.chosen, { count: current.length, total: order.length }) })] }), groups.map((group, i) => (_jsxs("section", { className: "flex min-w-0 flex-col", children: [group.title === undefined ? null : (_jsx("h4", { className: "px-4 pt-3 pb-1 text-micro font-medium text-muted", children: group.title })), _jsx("ul", { className: "flex min-w-0 flex-col", children: group.changes.map((change) => (_jsx(ChangeRow, { change: change, on: current.includes(change.id), onToggle: (on) => choose(toggleChosen(order, current, change.id, on)) }, change.id))) })] }, group.title ?? i)))] }));
82
+ return (_jsxs("div", { className: "@container flex min-w-0 flex-col", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-between gap-x-3 gap-y-1 border-b border-line px-4 py-2", children: [_jsx(Checkbox, { checked: state === "all", mixed: state === "some", label: copy.assist.all, onChange: (on) => choose(on ? [...order] : []) }), _jsx("span", { className: "text-micro text-muted tabular-nums", children: fillCopy(copy.assist.chosen, { count: current.length, total: order.length }) })] }), groups.map((group, i) => (_jsxs("section", { className: "flex min-w-0 flex-col", children: [group.title === undefined ? null : (_jsx("h3", { className: "px-4 pt-3 pb-1 text-micro font-medium text-muted", children: group.title })), _jsx("ul", { className: "flex min-w-0 flex-col", children: group.changes.map((change) => (_jsx(ChangeRow, { change: change, on: current.includes(change.id), onToggle: (on) => choose(toggleChosen(order, current, change.id, on)) }, change.id))) })] }, group.title ?? i)))] }));
83
83
  }
84
84
  /** «Elegí al menos un cambio.», «Descartar» and «Aplicar N cambios». */
85
85
  function ReviewActions({ review, emphasis, onDiscard, after, }) {
@@ -102,7 +102,7 @@ export function ChangeReview(props) {
102
102
  const copy = useUiCopy();
103
103
  const review = useReview(props);
104
104
  const titleId = useId();
105
- return (_jsxs("section", { "aria-labelledby": titleId, "data-assist": "review", className: "flex min-w-0 flex-col overflow-clip rounded-box border border-dashed border-info/40 bg-surface", children: [_jsxs("header", { className: "flex min-w-0 flex-col gap-1 border-b border-line px-4 py-3", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1", children: [_jsx(AssistLabel, {}), source === undefined ? null : _jsx(AssistSourceLink, { source: source })] }), _jsx("h3", { id: titleId, className: "text-lead font-semibold text-ink", children: title ?? copy.assist.review }), summary === undefined ? null : (_jsx("p", { className: "max-w-prose text-meta text-muted", children: summary }))] }), _jsx(ReviewBody, { review: review }), _jsx("footer", { className: "flex min-w-0 flex-wrap items-center justify-end gap-2 border-t border-line bg-ground px-4 py-2.5", children: _jsx(ReviewActions, { review: review, emphasis: emphasis, onDiscard: onDiscard }) })] }));
105
+ return (_jsxs("section", { "aria-labelledby": titleId, "data-assist": "review", className: "flex min-w-0 flex-col overflow-clip rounded-box border border-dashed border-info/40 bg-surface", children: [_jsxs("header", { className: "flex min-w-0 flex-col gap-1 border-b border-line px-4 py-3", children: [_jsxs("span", { className: "flex min-w-0 flex-wrap items-baseline gap-x-3 gap-y-1", children: [_jsx(AssistLabel, {}), source === undefined ? null : _jsx(AssistSourceLink, { source: source })] }), _jsx("h2", { id: titleId, className: "text-lead font-semibold text-ink", children: title ?? copy.assist.review }), summary === undefined ? null : (_jsx("p", { className: "max-w-prose text-meta text-muted", children: summary }))] }), _jsx(ReviewBody, { review: review }), _jsx("footer", { className: "flex min-w-0 flex-wrap items-center justify-end gap-2 border-t border-line bg-ground px-4 py-2.5", children: _jsx(ReviewActions, { review: review, emphasis: emphasis, onDiscard: onDiscard }) })] }));
106
106
  }
107
107
  ChangeReview.Group = Group;
108
108
  ChangeReview.Change = Change;
@@ -79,7 +79,10 @@ export function RecordCalendar({ items, keyOf, titleOf, startOf, endOf, allDayOf
79
79
  listPlugin,
80
80
  interactionPlugin,
81
81
  formaThemePlugin,
82
- ], locales: [esLocale], locale: copy.locale.toLowerCase().startsWith("es") ? "es" : "en", initialView: VIEW[view], ...(date === undefined ? {} : { initialDate: date }), headerToolbar: {
82
+ ], locales: [esLocale], locale: copy.locale.toLowerCase().startsWith("es") ? "es" : "en", initialView: VIEW[view], ...(date === undefined ? {} : { initialDate: date }),
83
+ /* «Hoy» is what the button SAYS, and so what it is called: the locale's hint named
84
+ it «Este mes» — a name that hides the visible word from voice control. */
85
+ todayText: copy.calendar.today, todayHint: () => copy.calendar.today, headerToolbar: {
83
86
  start: "prev,next today",
84
87
  center: "title",
85
88
  end: "dayGridMonth,timeGridWeek,timeGridDay,listWeek",
@@ -29,7 +29,10 @@ function Facts({ fields, columns, layout, }) {
29
29
  /* The note's card goes INSIDE the value: a `<dl>` holds only its terms and values.
30
30
  With a note the value is a flex box, so the absolute card keeps the value's top
31
31
  as its static position while being read after it. */
32
- const value = (_jsxs("dd", { ...(noteId === undefined ? {} : { "aria-describedby": noteId }), className: `min-w-0 text-meta ${card === null ? "" : "flex"} ${empty ? "text-muted italic" : "text-ink"} ${field.mono === true ? "font-mono tabular-nums" : ""}`, children: [empty ? "—" : field.value, card] }));
32
+ const value = (_jsxs("dd", { ...(noteId === undefined ? {} : { "aria-describedby": noteId }),
33
+ /* `wrap-anywhere`: an email or a code has no space to break at, and in two
34
+ columns it ran over the fact beside it. */
35
+ className: `min-w-0 text-meta wrap-anywhere ${card === null ? "" : "flex"} ${empty ? "text-muted italic" : "text-ink"} ${field.mono === true ? "font-mono tabular-nums" : ""}`, children: [empty ? "—" : field.value, card] }));
33
36
  const name = (_jsxs("dt", { className: `flex items-center gap-1 text-micro text-muted ${horizontal ? "shrink-0" : "min-w-0"}`, children: [_jsx("span", { children: field.label }), field.hint === undefined ? null : _jsx(Hint, { children: field.hint }), field.note === undefined ? null : _jsx(NoteFlag, { note: field.note })] }));
34
37
  if (horizontal) {
35
38
  /* A header fact is a pair: the number sits next to its name. The
@@ -90,5 +93,5 @@ export function SheetGroup({ title, hint, icon: Icon, children, }) {
90
93
  /* A group title is ABOVE its labels, not one of them: at the labels' size and weight
91
94
  «Cliente» the group read like «Cliente» the field. One step up in size and weight,
92
95
  and a hairline that says where the group starts. */
93
- _jsxs("header", { className: "flex min-w-0 items-center gap-2 border-b border-line pb-1.5", children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsx("h3", { className: "min-w-0 truncate text-body font-semibold text-ink", children: title }), hint === undefined ? null : _jsx(Hint, { children: hint })] })) : hint === undefined ? null : (_jsx(Hint, { children: hint })), _jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: children })] }));
96
+ _jsxs("header", { className: "flex min-w-0 items-center gap-2 border-b border-line pb-1.5", children: [Icon === undefined ? null : (_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0 text-muted", strokeWidth: 1.75, "aria-hidden": true })), _jsx("h2", { className: "min-w-0 truncate text-body font-semibold text-ink", children: title }), hint === undefined ? null : _jsx(Hint, { children: hint })] })) : hint === undefined ? null : (_jsx(Hint, { children: hint })), _jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: children })] }));
94
97
  }
@@ -1,5 +1,5 @@
1
+ import { type FormMode } from "../../foundations/form.js";
1
2
  import { type DialogContext } from "../../primitives/dialog/Dialog.js";
2
- import { type FormMode } from "../../primitives/field/mode.js";
3
3
  import type { FlagTone } from "../../primitives/notice/Flag.js";
4
4
  import type { DataColumn } from "../../primitives/table/DataTable.js";
5
5
  import { type EditColumn, type EditLayout } from "../../primitives/table/EditableTable.js";
@@ -4,12 +4,12 @@ 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";
8
+ import { useFormMode } from "../../foundations/form.js";
7
9
  import { Button, IconButton } from "../../primitives/button/Button.js";
8
10
  import { SearchField } from "../../primitives/choice/SearchField.js";
9
11
  import { Dialog } from "../../primitives/dialog/Dialog.js";
10
- import { FieldIdsContext } from "../../primitives/field/context.js";
11
12
  import { TextInput } from "../../primitives/field/Field.js";
12
- import { useFormMode } from "../../primitives/field/mode.js";
13
13
  import { Empty } from "../../primitives/notice/Notice.js";
14
14
  import { useToast } from "../../primitives/notice/Toast.js";
15
15
  import { EditableTable, } from "../../primitives/table/EditableTable.js";
@@ -101,7 +101,7 @@ export function ListingBar({ actions, title, description, counters, selection, s
101
101
  (count === undefined ? null : (_jsx("span", { role: "status", className: "shrink-0 text-meta text-muted tabular-nums", children: count === 1
102
102
  ? copy.list.countOne
103
103
  : fillCopy(copy.list.count, { count: count.toLocaleString(copy.locale) }) })));
104
- return (_jsx(SurfaceProvider, { value: "canvas", children: _jsxs("div", { className: `flex min-w-0 flex-wrap items-center gap-y-1.5 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [title === undefined ? null : (_jsx("h3", { className: "min-w-0 shrink-0 truncate text-body font-semibold text-ink", children: title })), center === null ? null : (_jsx("div", { className: "flex min-w-0 flex-1 basis-(--container-name) items-center overflow-x-clip overflow-y-visible", children: center })), howMany, views === undefined ? null : (_jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: views })), actions === undefined ? null : (_jsx("div", { className: `flex shrink-0 items-center gap-1.5 ${views === undefined ? "ml-auto" : ""}`, children: actions }))] }) }));
104
+ return (_jsx(SurfaceProvider, { value: "canvas", children: _jsxs("div", { className: `flex min-w-0 flex-wrap items-center gap-y-1.5 ${byDensity(d, "gap-x-3", "gap-x-2")}`, children: [title === undefined ? null : (_jsx("h2", { className: "min-w-0 shrink-0 truncate text-body font-semibold text-ink", children: title })), center === null ? null : (_jsx("div", { className: "flex min-w-0 flex-1 basis-(--container-name) items-center overflow-x-clip overflow-y-visible", children: center })), howMany, views === undefined ? null : (_jsx("div", { className: "ml-auto flex shrink-0 items-center gap-2", children: views })), actions === undefined ? null : (_jsx("div", { className: `flex shrink-0 items-center gap-1.5 ${views === undefined ? "ml-auto" : ""}`, children: actions }))] }) }));
105
105
  }
106
106
  /* El campo de búsqueda va en el CENTRO de la línea, y centrado contra la
107
107
  línea: pide `--container-search` y los dos lados se reparten lo que sobra
@@ -19,10 +19,14 @@ export declare function RecordCard({ title, subtitle, tags, meta, figure, media,
19
19
  tags?: ReactNode;
20
20
  meta?: ReactNode;
21
21
  figure?: ReactNode;
22
+ /** The picture of the record. A product's is a square (`Thumb`); a PERSON's is round, with
23
+ * the initials when there is no photo — `person`, like `RelationCard`'s. `alt` names the
24
+ * person when the title is not plain text. */
22
25
  media?: {
23
26
  src?: string;
24
27
  alt?: string;
25
28
  icon?: LucideIcon;
29
+ person?: true;
26
30
  };
27
31
  /** The card's last line, its controls: priority, kanban state, next activity, the owner's
28
32
  * avatar — each its own button, pressable without opening the card. */
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Thumb } from "../../primitives/cell/CellStack.js";
4
+ import { Avatar } from "../../primitives/value/Avatar.js";
4
5
  /**
5
6
  * One record as a card — the board presentation of a list.
6
7
  *
@@ -21,7 +22,9 @@ export function RecordCard({ title, subtitle, tags, meta, figure, media, footer,
21
22
  const titleText = _jsx("span", { className: "min-w-0 truncate", children: title });
22
23
  return (_jsxs("div", { className: `relative flex w-full min-w-0 flex-col gap-2 rounded-box border border-line bg-surface p-4 text-left ${onOpen === undefined
23
24
  ? ""
24
- : "transition-colors duration-(--duration-instant) focus-within:border-line-strong hover:border-line-strong"}`, children: [media === undefined ? null : (_jsx(Thumb, { size: "lg", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), tags === undefined ? null : _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags }), _jsxs("div", { className: "flex min-w-0 items-baseline justify-between gap-3", children: [onOpen === undefined ? (_jsx("p", { className: "flex min-w-0 text-lead font-medium text-ink", children: titleText })) : (_jsx("button", { type: "button", onClick: onOpen, className: "flex min-w-0 text-left text-lead font-medium text-ink outline-none after:absolute after:inset-0 after:rounded-box focus-visible:after:outline-2 focus-visible:after:outline-primary", children: titleText })), figure === undefined ? null : (_jsx("p", { className: "shrink-0 text-lead font-semibold tabular-nums text-ink", children: figure }))] }), subtitle === undefined ? null : (
25
+ : "transition-colors duration-(--duration-instant) focus-within:border-line-strong hover:border-line-strong"}`, children: [media === undefined ? null : media.person === true ? (
26
+ /* The name is written right below it: the photo's `alt` would say it twice. */
27
+ _jsx("span", { "aria-hidden": true, className: "flex shrink-0", children: _jsx(Avatar, { name: media.alt ?? (typeof title === "string" ? title : ""), size: "xl", ...(media.src === undefined ? {} : { src: media.src }) }) })) : (_jsx(Thumb, { size: "lg", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), tags === undefined ? null : _jsx("div", { className: "flex flex-wrap gap-1.5", children: tags }), _jsxs("div", { className: "flex min-w-0 items-baseline justify-between gap-3", children: [onOpen === undefined ? (_jsx("p", { className: "flex min-w-0 text-lead font-medium text-ink", children: titleText })) : (_jsx("button", { type: "button", onClick: onOpen, className: "flex min-w-0 text-left text-lead font-medium text-ink outline-none after:absolute after:inset-0 after:rounded-box focus-visible:after:outline-2 focus-visible:after:outline-primary", children: titleText })), figure === undefined ? null : (_jsx("p", { className: "shrink-0 text-lead font-semibold tabular-nums text-ink", children: figure }))] }), subtitle === undefined ? null : (
25
28
  /* Above the stretched title, at its own width: the subtitle may be a link to ANOTHER
26
29
  record (the customer), which must open that one and not the card. */
27
30
  _jsx("p", { className: "relative z-10 -mt-1.5 w-fit max-w-full min-w-0 truncate text-meta text-muted", children: subtitle })), meta === undefined ? null : (_jsx("p", { className: "text-micro tabular-nums text-muted", children: meta })), footer === undefined ? null : (_jsx("div", { className: "relative z-10 flex min-w-0 items-center gap-1.5", children: footer }))] }));
@@ -127,3 +127,10 @@ export declare function GroupHeader({ field, label, count, detail, depth, open,
127
127
  open?: boolean;
128
128
  onToggle?: () => void;
129
129
  }): import("react").JSX.Element;
130
+ /** Under a pile that shows its first page: «Ver 38 más». Indented like the pile's rows. */
131
+ export declare function GroupMore({ left, depth, onMore, }: {
132
+ /** How many the pile counts that are not here yet. */
133
+ left: number;
134
+ depth?: number;
135
+ onMore: () => void;
136
+ }): import("react").JSX.Element;
@@ -127,7 +127,7 @@ export function RowHeader({ name, meta, tags, withSelection = false, withIcon =
127
127
  export function GroupHeader({ field, label, count, detail, depth = 0, open = true, onToggle, }) {
128
128
  const d = useDensity();
129
129
  const copy = useUiCopy();
130
- const content = (_jsxs(_Fragment, { children: [onToggle === undefined ? null : (_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true })), field === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro text-muted/70", children: field })), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: label }), count === undefined ? null : (_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: count.toLocaleString(copy.locale) })), detail === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro tabular-nums text-muted", children: detail }))] }));
130
+ const content = (_jsxs(_Fragment, { children: [onToggle === undefined ? null : (_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2, "aria-hidden": true })), field === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro text-muted", children: field })), _jsx("span", { className: "truncate text-micro font-medium text-ink", children: label }), count === undefined ? null : (_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: count.toLocaleString(copy.locale) })), detail === undefined ? null : (_jsx("span", { className: "shrink-0 text-micro tabular-nums text-muted", children: detail }))] }));
131
131
  /* La sangría va en el relleno de la izquierda y no en un margen: el
132
132
  encabezado tiene fondo y borde inferior, y un margen los cortaría antes del
133
133
  borde de la lista — la banda dejaría de leerse como una franja. El hover
@@ -137,3 +137,9 @@ export function GroupHeader({ field, label, count, detail, depth = 0, open = tru
137
137
  const mark = (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-1 inset-y-0.5 rounded-control group-hover/head:bg-ink/4" }));
138
138
  return onToggle === undefined ? (_jsx("div", { className: box, style: indent, children: content })) : (_jsxs("button", { type: "button", onClick: onToggle, "aria-expanded": open, style: indent, className: `group/head relative ${box} w-full text-left focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary`, children: [mark, content] }));
139
139
  }
140
+ /** Under a pile that shows its first page: «Ver 38 más». Indented like the pile's rows. */
141
+ export function GroupMore({ left, depth = 0, onMore, }) {
142
+ const d = useDensity();
143
+ const copy = useUiCopy();
144
+ return (_jsx("div", { className: `border-b border-line ${byDensity(d, "px-3 py-1", "px-2.5 py-0.5")}`, style: depth === 0 ? undefined : { paddingLeft: `calc(var(--spacing-indent) * ${depth})` }, children: _jsx("button", { type: "button", onClick: onMore, 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, { count: left.toLocaleString(copy.locale) }) }) }));
145
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Surface } from "../../foundations/types.js";
2
- import type { LucideIcon } from "lucide-react";
2
+ import type { ComponentType } from "react";
3
3
  /** How the same list is shown: table, board, timeline.
4
4
  *
5
5
  * The icons go together and touching —a segmented group and not three loose
@@ -13,8 +13,13 @@ import type { LucideIcon } from "lucide-react";
13
13
  export interface ViewOption {
14
14
  id: string;
15
15
  label: string;
16
- icon: LucideIcon;
16
+ /** Any icon component — lucide's are what the package uses, not what it requires. */
17
+ icon: ViewIcon;
17
18
  }
19
+ export type ViewIcon = ComponentType<{
20
+ className?: string;
21
+ strokeWidth?: number;
22
+ }>;
18
23
  export declare function ViewSwitcher({ options, current, onSelect, surface, }: {
19
24
  options: readonly ViewOption[];
20
25
  current: string;
@@ -10,7 +10,7 @@ export { GroupHeader, Row } from "./Row.js";
10
10
  export { type Selection, useSelection } from "./selection.js";
11
11
  export { type SetControl, useSetControl } from "./set.js";
12
12
  export { toggleCollapsed, type TreeControl, useTreeControl, } from "./tree.js";
13
- export { type ViewOption, ViewSwitcher, } from "./ViewSwitcher.js";
13
+ export { type ViewIcon, type ViewOption, ViewSwitcher, } from "./ViewSwitcher.js";
14
14
  export { type BoardColumn, BoardColumns, type BoardStages, } from "./BoardColumns.js";
15
15
  export { type GroupingControl, useGroupingControl, } from "./grouping.js";
16
16
  export { GroupingLevels, type GroupingLevelsProps, } from "./GroupingLevels.js";
@@ -33,7 +33,10 @@ import { type Pages } from "./pages.js";
33
33
  * también; lo que se lee está en el cruce. Tampoco se
34
34
  * ordena: el orden de cada eje es el de sus claves.
35
35
  */
36
- export type ListPresent = "rows" | "table" | "board" | "sheet" | "report" | "pivot" | "calendar" | "gantt" | "timeline";
36
+ export type BuiltInPresent = "rows" | "table" | "board" | "sheet" | "report" | "pivot" | "calendar" | "gantt" | "timeline";
37
+ /** A presentation of a list: one the package draws, or the `id` of a `List.Present` the
38
+ * application wrote. `string & {}` keeps the built-in names in autocomplete. */
39
+ export type ListPresent = BuiltInPresent | (string & {});
37
40
  export interface SetControl<Row, Present extends string = ListPresent> extends Pages<Row> {
38
41
  present: Present;
39
42
  setPresent: (next: Present) => void;
@@ -69,7 +69,11 @@ export function Breadcrumb({ crumbs, surface: surfaceProp, folded = 3, }) {
69
69
  const shown = folds
70
70
  ? [crumbs[0], null, crumbs[crumbs.length - 1]]
71
71
  : crumbs;
72
- return (_jsx("nav", { "aria-label": copy.record.location, className: "flex min-w-0", children: _jsx(Breadcrumbs, { "aria-label": copy.record.location, className: "flex min-w-0 flex-wrap items-center gap-1", children: shown.map((crumb, i) => {
72
+ /* Named by WHERE it ends, not only by what it is: two records side by side (panes, a
73
+ panel next to the page) are two trails, and a screen reader lists landmarks by name. */
74
+ const last = crumbs[crumbs.length - 1];
75
+ const name = last === undefined ? copy.record.location : `${copy.record.location}: ${last.label}`;
76
+ return (_jsx("nav", { "aria-label": name, className: "flex min-w-0", children: _jsx(Breadcrumbs, { "aria-label": name, className: "flex min-w-0 flex-wrap items-center gap-1", children: shown.map((crumb, i) => {
73
77
  const key = crumb === null ? "folded" : `${i}-${crumb.label}`;
74
78
  const last = i === shown.length - 1;
75
79
  return (_jsxs(AriaBreadcrumb, { id: key, className: "flex min-w-0 items-center gap-1", children: [i === 0 ? null : (_jsx(ChevronRight, { className: `size-(--size-icon-sm) shrink-0 ${edge}`, strokeWidth: 2, "aria-hidden": true })), crumb === null ? (_jsx(Menu, { label: copy.record.location, title: copy.record.location, icon: MoreHorizontal, iconOnly: true, chevron: false, variant: "text", size: "sm", surface: surface === "chrome" ? "chrome" : "canvas", align: "start", groups: [
@@ -7,9 +7,13 @@ export interface DirtyState {
7
7
  /** What gets saved, when it is not obvious: «guardar la factura», «guardar la
8
8
  * regla». */
9
9
  label?: string;
10
- /** Why it CANNOT be saved yet. Present = the button is off and the reason is
11
- * in its `title` — a button that is off with no reason is a wall. */
10
+ /** Why it CANNOT be saved yet. Present = the button is off and the reason is written
11
+ * where the «unsaved» mark was — a button that is off with no reason is a wall, and a
12
+ * reason in a `title` reaches neither the keyboard nor a finger. */
12
13
  blocked?: string;
14
+ /** How many fields changed: «2 cambios sin guardar» instead of a bare «sin guardar». The
15
+ * fields themselves are marked by their `Field changed`. */
16
+ changes?: number;
13
17
  }
14
18
  /** Save and discard, in the rail and in place of the action buttons.
15
19
  *
@@ -20,7 +24,17 @@ export interface DirtyState {
20
24
  * The «sin guardar» warning is text and not just a color: whoever is leaving
21
25
  * the screen has to be able to read what is going on, not deduce it from a
22
26
  * dot. */
23
- export declare function DirtyActions({ onSave, onDiscard, saving, label, blocked, }: DirtyState): import("react").JSX.Element;
24
- /** The one action a view leads with: the element written with `role="primary"` — the
25
- * same prop that paints it gold and that `make smoke` counts, so there is no second way to
26
- * say «this is the main one» that could disagree with the first. */
27
+ export declare function DirtyActions({ onSave, onDiscard, saving, label, blocked, changes, }: DirtyState): import("react").JSX.Element;
28
+ /**
29
+ * Whether the record has JUST been saved: `dirty` went from saving straight to gone.
30
+ *
31
+ * Context: saving ended with the rail silently swapping Save for the usual actions — the
32
+ * screen changed and said nothing, so each application wrote its own toast, or none. A
33
+ * failed save does not count: `saving` turns off and `dirty` stays.
34
+ */
35
+ export declare function useJustSaved(dirty: DirtyState | undefined): boolean;
36
+ /** «✓ Guardado», where Save was, for a moment. The live region is always there — a region
37
+ * inserted together with its words is not reliably announced. */
38
+ export declare function SavedMark({ saved }: {
39
+ saved: boolean;
40
+ }): import("react").JSX.Element;
@@ -1,9 +1,10 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useUiCopy } from "../../foundations/copy.js";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
4
  import { bySurface, useSurface } from "../../foundations/surface.js";
5
5
  import { Button } from "../../primitives/button/Button.js";
6
- import { Save, Undo2 } from "lucide-react";
6
+ import { Check, Save, Undo2 } from "lucide-react";
7
+ import { useEffect, useId, useRef, useState } from "react";
7
8
  /** Save and discard, in the rail and in place of the action buttons.
8
9
  *
9
10
  * Context: discard goes to the LEFT of save and with the quieter look. It is
@@ -13,10 +14,60 @@ import { Save, Undo2 } from "lucide-react";
13
14
  * The «sin guardar» warning is text and not just a color: whoever is leaving
14
15
  * the screen has to be able to read what is going on, not deduce it from a
15
16
  * dot. */
16
- export function DirtyActions({ onSave, onDiscard, saving = false, label, blocked, }) {
17
+ export function DirtyActions({ onSave, onDiscard, saving = false, label, blocked, changes, }) {
17
18
  const copy = useUiCopy();
19
+ const blockedId = useId();
18
20
  /* It follows the rail it sits on instead of assuming the dark one: on a light rail the
19
21
  chrome palette was white text on white. */
20
22
  const surface = useSurface();
21
- return (_jsxs(_Fragment, { children: [_jsx("span", { className: `inline-flex h-(--size-control-md) shrink-0 items-center gap-1.5 rounded-control px-2.5 text-meta font-medium ${bySurface(surface, "bg-warn/12 text-warn", "bg-chrome-warn/15 text-chrome-warn")}`, children: copy.record.unsaved }), _jsx(Button, { surface: surface, onClick: onDiscard, disabled: saving, icon: Undo2, children: copy.record.discard }), _jsx("span", { ...(blocked === undefined ? {} : { title: blocked }), children: _jsx(Button, { role: "primary", surface: surface, onClick: onSave, disabled: saving || blocked !== undefined, icon: Save, children: saving ? copy.record.saving : (label ?? copy.record.save) }) })] }));
23
+ return (_jsxs(_Fragment, { children: [_jsx("span", { ...(blocked === undefined ? {} : { id: blockedId }), className: `inline-flex h-(--size-control-md) min-w-0 shrink items-center gap-1.5 truncate rounded-control px-2.5 text-meta font-medium ${bySurface(surface, "bg-warn/12 text-warn", "bg-chrome-warn/15 text-chrome-warn")}`, children: blocked ??
24
+ (changes === undefined || changes === 0
25
+ ? copy.record.unsaved
26
+ : fillCopy(changes === 1 ? copy.record.unsavedOne : copy.record.unsavedMany, {
27
+ count: changes.toLocaleString(copy.locale),
28
+ })) }), _jsx(Button, { surface: surface, onClick: onDiscard, disabled: saving, icon: Undo2, children: copy.record.discard }), _jsx(Button, { role: "primary", surface: surface, onClick: onSave, busy: saving, disabled: blocked !== undefined, icon: Save, ...(blocked === undefined ? {} : { "aria-describedby": blockedId }), children: saving ? copy.record.saving : (label ?? copy.record.save) })] }));
29
+ }
30
+ /** The one action a view leads with: the element written with `role="primary"` — the
31
+ * same prop that paints it gold and that `make smoke` counts, so there is no second way to
32
+ * say «this is the main one» that could disagree with the first. */
33
+ /** How long «Guardado» stays: long enough to be seen without looking for it, short enough
34
+ * not to stay as one more thing on the rail. */
35
+ const SAVED_FOR = 2400;
36
+ /**
37
+ * Whether the record has JUST been saved: `dirty` went from saving straight to gone.
38
+ *
39
+ * Context: saving ended with the rail silently swapping Save for the usual actions — the
40
+ * screen changed and said nothing, so each application wrote its own toast, or none. A
41
+ * failed save does not count: `saving` turns off and `dirty` stays.
42
+ */
43
+ export function useJustSaved(dirty) {
44
+ const wasSaving = useRef(false);
45
+ const [saved, setSaved] = useState(false);
46
+ const saving = dirty?.saving === true;
47
+ const present = dirty !== undefined;
48
+ useEffect(() => {
49
+ if (saving) {
50
+ wasSaving.current = true;
51
+ return;
52
+ }
53
+ if (present) {
54
+ wasSaving.current = false;
55
+ setSaved(false);
56
+ return;
57
+ }
58
+ if (!wasSaving.current)
59
+ return;
60
+ wasSaving.current = false;
61
+ setSaved(true);
62
+ const timer = setTimeout(() => setSaved(false), SAVED_FOR);
63
+ return () => clearTimeout(timer);
64
+ }, [saving, present]);
65
+ return saved;
66
+ }
67
+ /** «✓ Guardado», where Save was, for a moment. The live region is always there — a region
68
+ * inserted together with its words is not reliably announced. */
69
+ export function SavedMark({ saved }) {
70
+ const copy = useUiCopy();
71
+ const surface = useSurface();
72
+ return (_jsxs(_Fragment, { children: [_jsx("span", { role: "status", className: "sr-only", children: saved ? copy.record.saved : "" }), saved ? (_jsxs("span", { "aria-hidden": true, className: `inline-flex h-(--size-control-md) shrink-0 items-center gap-1.5 rounded-control px-2.5 text-meta font-medium transition-[opacity,translate] duration-(--duration-quick) ease-(--ease-out) starting:-translate-y-0.5 starting:opacity-0 ${bySurface(surface, "bg-good/12 text-good", "bg-chrome-good/15 text-chrome-good")}`, children: [_jsx(Check, { className: "size-(--size-icon-sm)", strokeWidth: 2.5 }), copy.record.saved] })) : null] }));
22
73
  }
@@ -2,7 +2,7 @@ import { type Crumb } from "./Breadcrumb.js";
2
2
  import { type DirtyState } from "./DirtyActions.js";
3
3
  import type { Surface, Tone } from "../../foundations/types.js";
4
4
  import type { LucideIcon } from "lucide-react";
5
- import type { ReactNode } from "react";
5
+ import { type ReactNode } from "react";
6
6
  export type { Surface };
7
7
  export declare function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface, smart, aspects, dirty, activity, activityLayout, width, ribbon, blocking, indicator, children, }: {
8
8
  crumbs?: readonly Crumb[];
@@ -1,15 +1,31 @@
1
1
  "use client";
2
2
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Breadcrumb } from "./Breadcrumb.js";
4
- import { DirtyActions, } from "./DirtyActions.js";
4
+ import { DirtyActions, SavedMark, useJustSaved, } from "./DirtyActions.js";
5
5
  import { isPrimary, ThumbBar, thumbPlan } from "./ThumbBar.js";
6
6
  import { byDensity, useDensity } from "../../foundations/density.js";
7
7
  import { SurfaceProvider } from "../../foundations/surface.js";
8
8
  import { Line } from "../../primitives/block/Line.js";
9
9
  import { Tag } from "../../primitives/tag/Tag.js";
10
+ import { useEffect, useRef } from "react";
10
11
  export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, activity, activityLayout = "auto", width = "reading", ribbon, blocking, indicator, children, }) {
11
12
  const d = useDensity();
12
- const hasRail = crumbs !== undefined ||
13
+ /* «Guardado» is said where Save was; a record whose rail exists only while it has changes
14
+ keeps it for that moment, or the answer would leave with the question. */
15
+ const saved = useJustSaved(dirty);
16
+ /* Save is gone the moment it has saved, and the focus that was on it fell to the page —
17
+ a keyboard was left at the top of the document. It comes back to the record's name:
18
+ where reading the record starts, and announced together with «Guardado». */
19
+ const nameRef = useRef(null);
20
+ useEffect(() => {
21
+ if (!saved)
22
+ return;
23
+ const lost = document.activeElement === null || document.activeElement === document.body;
24
+ if (lost)
25
+ nameRef.current?.focus({ preventScroll: true });
26
+ }, [saved]);
27
+ const hasRail = saved ||
28
+ crumbs !== undefined ||
13
29
  pager !== undefined ||
14
30
  actions !== undefined ||
15
31
  status !== undefined ||
@@ -40,12 +56,14 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
40
56
  shadow. It exists on the PAGE, where the chrome rail is a real band
41
57
  (crumbs, pager, actions). A drawer is compact so `withRail` is already
42
58
  false. A dialog is comfortable but has no crumbs or actions — and it IS
43
- already a card. Status alone is not a page. */
44
- const paper = withRail &&
45
- (crumbs !== undefined ||
46
- pager !== undefined ||
47
- actions !== undefined ||
48
- dirty !== undefined);
59
+ already a card. Status alone is not a page.
60
+
61
+ And `dirty` does not decide it. Whether the record sits on paper is a fact of WHERE it
62
+ is mounted, and that does not change while someone types; `dirty` does, with the first
63
+ letter. Deciding by it swapped the whole tree under the paper at that letter — the field
64
+ being written was unmounted and lost the focus. Save and Discard still appear: in the
65
+ rail, which is a sibling and moves nothing. */
66
+ const paper = withRail && (crumbs !== undefined || pager !== undefined || actions !== undefined);
49
67
  /* ONE column for the rail and the page: the rail used to span the screen while the sheet
50
68
  stopped at 72rem, so on a wide desk the buttons sat 250 px left of the paper they act on
51
69
  and the record read as two things. */
@@ -58,7 +76,7 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
58
76
  /* `display`, not `title`: the record's name is the entry point of the
59
77
  screen. At the title size (19 px over a 13 px body, 1.46:1) nothing on
60
78
  the sheet stood out and the eye had nowhere to start. */
61
- _jsx("h1", { className: "min-w-0 truncate text-display font-semibold text-ink", children: title })), tag === undefined ? null : _jsx(Tag, { tone: tag.tone, children: tag.label })] }), subtitle === undefined || !withIdentity ? null : (_jsx("p", { className: "mt-0.5 truncate text-lead text-ink", children: subtitle }))] })] }), figure === undefined || !withIdentity ? null : (_jsxs("div", { className: "ml-auto flex shrink-0 flex-col items-end", children: [_jsx("span", { className: "text-micro font-medium text-muted", children: figure.label }), _jsx("span", { className: "text-display font-semibold tabular-nums text-ink", children: figure.value })] }))] }), withRail || status === undefined ? null : (_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: status })), aspects === undefined ? null : (
79
+ _jsx("h1", { ref: nameRef, tabIndex: -1, className: "min-w-0 truncate text-display font-semibold text-ink outline-0", children: title })), tag === undefined ? null : _jsx(Tag, { tone: tag.tone, children: tag.label })] }), subtitle === undefined || !withIdentity ? null : (_jsx("p", { className: "mt-0.5 truncate text-lead text-ink", children: subtitle }))] })] }), figure === undefined || !withIdentity ? null : (_jsxs("div", { className: "ml-auto flex shrink-0 flex-col items-end", children: [_jsx("span", { className: "text-micro font-medium text-muted", children: figure.label }), _jsx("span", { className: "text-display font-semibold tabular-nums text-ink", children: figure.value })] }))] }), withRail || status === undefined ? null : (_jsx("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: status })), aspects === undefined ? null : (
62
80
  /* The line is the sheet's mouth. In comfortable it walks to the
63
81
  edges; the current tab sits on it and opens into the page below. */
64
82
  _jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspects }))] }));
@@ -68,14 +86,15 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
68
86
  it. The line wraps on a phone, so it is never pushed out of reach. */
69
87
  smart === undefined && indicator === undefined ? undefined : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-center gap-2", children: [smart, indicator] })), end: pager }), actions === undefined &&
70
88
  status === undefined &&
71
- dirty === undefined ? null : (
89
+ dirty === undefined &&
90
+ !saved ? null : (
72
91
  /* Row 2 — WHAT I CAN DO and WHERE IT IS AT: actions stuck to the
73
92
  left edge, the history and the state stuck to the right one. No
74
93
  `flex-wrap`: wrapping split the row in two and the state ended up
75
94
  below the actions, which is another hierarchy. */
76
- _jsxs("div", { className: "flex min-w-0 items-start gap-x-4", children: [dirty !== undefined ? (_jsx("div", { className: "flex shrink-0 items-center gap-1.5 @max-handheld/record:hidden", children: _jsx(DirtyActions, { ...dirty }) })) : actions === undefined ? null : (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: primaries.length === 0
77
- ? actions
78
- : railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)) })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) }) })) : null, paper ? (
95
+ _jsxs("div", { className: "flex min-w-0 items-start gap-x-4", children: [dirty !== undefined ? (_jsx("div", { className: "flex shrink-0 items-center gap-1.5 @max-handheld/record:hidden", children: _jsx(DirtyActions, { ...dirty }) })) : actions === undefined ? (_jsx(SavedMark, { saved: saved })) : (_jsxs("div", { className: "flex shrink-0 items-center gap-1.5", children: [primaries.length === 0
96
+ ? actions
97
+ : railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)), _jsx(SavedMark, { saved: saved })] })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) }) })) : null, paper ? (
79
98
  /* ==================================================================== *
80
99
  * THE PAGE — [sheet | Actividad], on the ground.
81
100
  *
@@ -17,5 +17,5 @@ const SOLAPAS = ["w-14", "w-16", "w-20"];
17
17
  export function RecordSkeleton({ label }) {
18
18
  const d = useDensity();
19
19
  const page = d === "comfortable";
20
- return (_jsxs("article", { "aria-busy": "true", className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "sr-only", children: label }), page ? (_jsxs("div", { "aria-hidden": true, className: "flex items-center gap-3 bg-chrome px-3.5 py-2.5", children: [_jsx("span", { className: "h-3 w-24 rounded-control bg-chrome-ink/15" }), _jsx("span", { className: "h-3 w-40 rounded-control bg-chrome-ink/10" }), _jsx("span", { className: "ml-auto h-(--size-control-sm) w-28 rounded-control bg-chrome-ink/15" })] })) : null, _jsx("div", { "aria-hidden": true, className: page ? "border border-line bg-surface" : "bg-surface", children: _jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-3 px-4 pt-3.5 pb-4", "gap-2 py-2")}`, children: [_jsx("span", { className: "h-5 w-72 max-w-full rounded-control bg-ink/8" }), _jsx("div", { className: "flex flex-wrap gap-x-6 gap-y-2", children: HECHOS.map((width) => (_jsxs("span", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "h-2.5 w-10 rounded-control bg-ink/5" }), _jsx("span", { className: `h-3.5 rounded-control bg-ink/8 ${width}` })] }, width))) }), _jsx("div", { className: "flex gap-4 border-b border-line pb-2", children: SOLAPAS.map((width, i) => (_jsx("span", { className: `h-3 rounded-control ${i === 0 ? "bg-ink/10" : "bg-ink/5"} ${width}` }, width))) }), _jsx("span", { className: "h-24 rounded-box bg-ink/5" })] }) })] }));
20
+ return (_jsxs("article", { "aria-busy": "true", className: "flex min-w-0 animate-reveal-late flex-col", children: [_jsx("span", { className: "sr-only", children: label }), page ? (_jsxs("div", { "aria-hidden": true, className: "flex items-center gap-3 bg-chrome px-3.5 py-2.5", children: [_jsx("span", { className: "h-3 w-24 rounded-control bg-chrome-ink/15" }), _jsx("span", { className: "h-3 w-40 rounded-control bg-chrome-ink/10" }), _jsx("span", { className: "ml-auto h-(--size-control-sm) w-28 rounded-control bg-chrome-ink/15" })] })) : null, _jsx("div", { "aria-hidden": true, className: page ? "border border-line bg-surface" : "bg-surface", children: _jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-3 px-4 pt-3.5 pb-4", "gap-2 py-2")}`, children: [_jsx("span", { className: "h-5 w-72 max-w-full rounded-control bg-ink/8" }), _jsx("div", { className: "flex flex-wrap gap-x-6 gap-y-2", children: HECHOS.map((width) => (_jsxs("span", { className: "flex flex-col gap-1", children: [_jsx("span", { className: "h-2.5 w-10 rounded-control bg-ink/5" }), _jsx("span", { className: `h-3.5 rounded-control bg-ink/8 ${width}` })] }, width))) }), _jsx("div", { className: "flex gap-4 border-b border-line pb-2", children: SOLAPAS.map((width, i) => (_jsx("span", { className: `h-3 rounded-control ${i === 0 ? "bg-ink/10" : "bg-ink/5"} ${width}` }, width))) }), _jsx("span", { className: "h-24 rounded-box bg-ink/5" })] }) })] }));
21
21
  }
@@ -1,4 +1,4 @@
1
- import { type BuiltFilter } from "./FilterBuilder.js";
1
+ import type { BuiltFilter } from "./criteria.js";
2
2
  import { type ModelDefinition } from "./model.js";
3
3
  export interface BuiltSort {
4
4
  field: string;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { blankGroup, ExpressionView, treeFromFilter, treeToFilter, WhereTree, } from "./FilterBuilder.js";
4
4
  import { JsonEditor } from "./JsonEditor.js";
5
5
  import { filterableNames, } from "./model.js";
6
- import { currentCopy, fillCopy, useUiCopy } from "../../foundations/copy.js";
6
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
7
7
  import { GRAIN_ORDER } from "../../foundations/grouping.js";
8
8
  import { useSurface } from "../../foundations/surface.js";
9
9
  import { Rule } from "../../primitives/block/Block.js";
@@ -174,11 +174,11 @@ export function CriteriaBuilder({ model, initial, focus, onApply, onCancel, }) {
174
174
  parsed = JSON.parse(text);
175
175
  }
176
176
  catch {
177
- setRawError(currentCopy().builder.invalidJson);
177
+ setRawError(copy.builder.invalidJson);
178
178
  return;
179
179
  }
180
180
  if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
181
- setRawError(currentCopy().builder.notCriteria);
181
+ setRawError(copy.builder.notCriteria);
182
182
  return;
183
183
  }
184
184
  setRawError(null);