@sincpro/web-ui 1.3.0 → 1.5.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 (46) hide show
  1. package/dist/components/form/Sheet.js +4 -1
  2. package/dist/components/listing/RecordCard.d.ts +4 -0
  3. package/dist/components/listing/RecordCard.js +4 -1
  4. package/dist/components/listing/Row.d.ts +5 -4
  5. package/dist/components/listing/Row.js +5 -4
  6. package/dist/components/record/DirtyActions.d.ts +20 -6
  7. package/dist/components/record/DirtyActions.js +55 -4
  8. package/dist/components/record/RecordHeader.d.ts +1 -1
  9. package/dist/components/record/RecordHeader.js +32 -13
  10. package/dist/components/record/RecordSkeleton.js +1 -1
  11. package/dist/components/search/SearchBar.js +12 -3
  12. package/dist/foundations/copy.d.ts +15 -0
  13. package/dist/foundations/copy.es.js +8 -0
  14. package/dist/foundations/copy.js +8 -0
  15. package/dist/foundations/grouping.d.ts +28 -0
  16. package/dist/foundations/grouping.js +0 -0
  17. package/dist/foundations/index.d.ts +1 -1
  18. package/dist/foundations/index.js +1 -1
  19. package/dist/primitives/button/Button.d.ts +12 -2
  20. package/dist/primitives/button/Button.js +12 -2
  21. package/dist/primitives/cell/CellStack.js +1 -1
  22. package/dist/primitives/field/Field.d.ts +8 -3
  23. package/dist/primitives/field/Field.js +7 -3
  24. package/dist/primitives/notice/Notice.js +3 -1
  25. package/dist/primitives/picker/MatchedText.d.ts +3 -2
  26. package/dist/primitives/picker/MatchedText.js +3 -2
  27. package/dist/primitives/picker/Picker.d.ts +18 -1
  28. package/dist/primitives/picker/Picker.js +5 -5
  29. package/dist/primitives/picker/TagsField.d.ts +10 -0
  30. package/dist/primitives/picker/TagsField.js +2 -2
  31. package/dist/primitives/picker/index.d.ts +2 -1
  32. package/dist/primitives/picker/index.js +2 -0
  33. package/dist/primitives/picker/tagChoices.d.ts +7 -0
  34. package/dist/primitives/picker/tagChoices.js +9 -4
  35. package/dist/primitives/table/DataTable.js +10 -11
  36. package/dist/primitives/table/EditableTable.js +5 -1
  37. package/dist/primitives/table/PileFootLine.d.ts +14 -0
  38. package/dist/primitives/table/PileFootLine.js +22 -0
  39. package/dist/primitives/table/index.d.ts +1 -0
  40. package/dist/primitives/table/index.js +1 -0
  41. package/dist/tokens.css +42 -0
  42. package/dist/views/list/List.d.ts +11 -3
  43. package/dist/views/list/ListView.js +19 -8
  44. package/dist/views/list/index.d.ts +1 -1
  45. package/dist/views/list/types.d.ts +24 -2
  46. package/package.json +1 -1
@@ -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
@@ -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 }))] }));
@@ -1,3 +1,4 @@
1
+ import type { PileFoot } from "../../foundations/grouping.js";
1
2
  import { type MenuGroup } from "../../primitives/menu/Menu.js";
2
3
  import type { LucideIcon } from "lucide-react";
3
4
  import type { ReactNode } from "react";
@@ -127,10 +128,10 @@ export declare function GroupHeader({ field, label, count, detail, depth, open,
127
128
  open?: boolean;
128
129
  onToggle?: () => void;
129
130
  }): 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;
131
+ /** Under a short pile: «Ver 50 más de 49.950», or «Se muestran 500 de 49.950» when there is
132
+ * no next page. Indented like the pile's rows. */
133
+ export declare function GroupMore({ foot, depth, onMore, }: {
134
+ foot: PileFoot;
134
135
  depth?: number;
135
136
  onMore: () => void;
136
137
  }): import("react").JSX.Element;
@@ -5,6 +5,7 @@ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
5
5
  import { byDensity, useDensity } from "../../foundations/density.js";
6
6
  import { stickyTop } from "../../foundations/sticky.js";
7
7
  import { Menu } from "../../primitives/menu/Menu.js";
8
+ import { PileFootLine } from "../../primitives/table/PileFootLine.js";
8
9
  import { HEADER_TEXT } from "../../primitives/table/Table.js";
9
10
  import { CopyButton } from "../../primitives/value/Copyable.js";
10
11
  import { Avatar } from "../../primitives/value/Person.js";
@@ -137,9 +138,9 @@ export function GroupHeader({ field, label, count, detail, depth = 0, open = tru
137
138
  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
139
  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
140
  }
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, }) {
141
+ /** Under a short pile: «Ver 50 más de 49.950», or «Se muestran 500 de 49.950» when there is
142
+ * no next page. Indented like the pile's rows. */
143
+ export function GroupMore({ foot, depth = 0, onMore, }) {
142
144
  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
+ 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(PileFootLine, { foot: foot, onMore: onMore }) }));
145
146
  }
@@ -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
  }
@@ -74,6 +74,11 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
74
74
  const input = useRef(null);
75
75
  const box = useRef(null);
76
76
  const id = useId();
77
+ /* The suggestions are the input's popup (the ARIA combobox pattern): the input keeps the
78
+ focus and says which list it controls and which option is the active one, so a screen
79
+ reader announces «Nombre del cliente · contiene Globex» as the option it is. */
80
+ const listId = useId();
81
+ const optionId = (at) => `${listId}-${at}`;
77
82
  /**
78
83
  * Los campos por los que se puede preguntar.
79
84
  *
@@ -290,7 +295,9 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
290
295
  removeLabel: copy.search.dropOp,
291
296
  },
292
297
  ]),
293
- ] }), _jsx("input", { ref: input, "aria-label": copy.choice.search, value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
298
+ ] }), _jsx("input", { ref: input, "aria-label": copy.choice.search, role: "combobox", "aria-autocomplete": "list", "aria-expanded": open, ...(open ? { "aria-controls": listId } : {}), ...(open && !asTags && suggestions[active] !== undefined
299
+ ? { "aria-activedescendant": optionId(active) }
300
+ : {}), value: text, onChange: (e) => setText(e.target.value), onKeyDown: (e) => {
294
301
  /* Retroceso con el campo vacío saca la ÚLTIMA instancia: el gesto
295
302
  que ya existe en cualquier campo de fichas y el único que se
296
303
  descubre sin que nadie lo explique. La última y no la última de
@@ -392,7 +399,7 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
392
399
  /* UNA LISTA, no una tira. Se está ELIGIENDO: el ojo baja por una
393
400
  columna y el teclado también. Doce atributos doblados en tres
394
401
  renglones son doce cosas que barrer en zigzag para encontrar una. */
395
- _jsx("div", { className: "max-h-72 overflow-y-auto p-1", children: _jsx(Tags, { stack: true, surface: "canvas", label: step.field !== undefined ? copy.search.askOp : copy.search.askAttribute, items: suggestions.map((one, at) => ({
402
+ _jsx("div", { id: listId, className: "max-h-72 overflow-y-auto p-1", children: _jsx(Tags, { stack: true, surface: "canvas", label: step.field !== undefined ? copy.search.askOp : copy.search.askAttribute, items: suggestions.map((one, at) => ({
396
403
  id: one.id,
397
404
  label: one.label,
398
405
  ...(one.kind === "op"
@@ -402,7 +409,9 @@ export function SearchBar({ facets, model, criteria, onCriteriaChange, placehold
402
409
  : {}),
403
410
  active: at === active,
404
411
  onSelect: () => pick(one),
405
- })) }) })) : (_jsx("div", { className: "p-1", children: suggestions.map((one, at) => (_jsxs("button", { type: "button", onClick: () => pick(one), onMouseEnter: () => setActive(at), className: `flex w-full items-baseline gap-1.5 rounded-control px-2 py-1.5 text-left text-meta transition-colors duration-(--duration-instant) ${at === active ? "bg-primary/8" : "hover:bg-ink/4"}`, children: [_jsx("span", { className: "shrink-0 font-medium text-ink", children: one.label }), one.kind === "condition" || one.kind === "literal" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "shrink-0 text-muted", children: ":" }), _jsx("span", { className: "min-w-0 truncate font-mono text-micro text-ink", children: one.hint })] })) : one.kind === "field" && one.hint !== undefined ? (_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 text-muted", children: one.hint })) : null] }, one.id))) }))] })) : null, panel && (columns.length > 0 || onOpenBuilder !== undefined) ? (_jsxs("div", { id: id, className: "absolute top-full right-0 z-30 mt-1 flex w-max max-w-full flex-wrap overflow-hidden rounded-panel border border-line-strong bg-surface shadow-float", children: [columns.map((c, i) => (_jsxs("div", { className: `flex min-w-48 flex-1 flex-col ${i === 0 ? "" : "border-t border-line sm:border-t-0 sm:border-l"}`, children: [_jsx("div", { className: "border-b border-line px-2.5 py-1.5 text-micro font-medium text-muted", children: c.title }), _jsx("div", { className: "flex flex-col gap-0.5 p-1", children: c.content ??
412
+ })) }) })) : (_jsx("div", { id: listId, role: "listbox", "aria-label": copy.search.suggestions, className: "p-1", children: suggestions.map((one, at) => (_jsxs("div", { id: optionId(at), role: "option", "aria-selected": at === active,
413
+ /* The input keeps the focus: pressing an option must not take it. */
414
+ onMouseDown: (event) => event.preventDefault(), onClick: () => pick(one), onMouseEnter: () => setActive(at), className: `flex w-full cursor-pointer items-baseline gap-1.5 rounded-control px-2 py-1.5 text-left text-meta transition-colors duration-(--duration-instant) ${at === active ? "bg-primary/8" : "hover:bg-ink/4"}`, children: [_jsx("span", { className: "shrink-0 font-medium text-ink", children: one.label }), one.kind === "condition" || one.kind === "literal" ? (_jsxs(_Fragment, { children: [_jsx("span", { className: "shrink-0 text-muted", children: ":" }), _jsx("span", { className: "min-w-0 truncate font-mono text-micro text-ink", children: one.hint })] })) : one.kind === "field" && one.hint !== undefined ? (_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 text-muted", children: one.hint })) : null] }, one.id))) }))] })) : null, panel && (columns.length > 0 || onOpenBuilder !== undefined) ? (_jsxs("div", { id: id, className: "absolute top-full right-0 z-30 mt-1 flex w-max max-w-full flex-wrap overflow-hidden rounded-panel border border-line-strong bg-surface shadow-float", children: [columns.map((c, i) => (_jsxs("div", { className: `flex min-w-48 flex-1 flex-col ${i === 0 ? "" : "border-t border-line sm:border-t-0 sm:border-l"}`, children: [_jsx("div", { className: "border-b border-line px-2.5 py-1.5 text-micro font-medium text-muted", children: c.title }), _jsx("div", { className: "flex flex-col gap-0.5 p-1", children: c.content ??
406
415
  c.groups?.map((g, gi) => (_jsx(Group, { group: g, first: gi === 0, onSelect: c.onSelect }, g.label ?? gi))) })] }, c.title))), onOpenBuilder === undefined ? null : (_jsx("div", { className: "w-full border-t border-line p-1", children: _jsxs("button", { type: "button", onClick: () => {
407
416
  onOpenBuilder();
408
417
  setPanel(false);
@@ -70,8 +70,13 @@ export type UiCopy = {
70
70
  faces: string;
71
71
  /** The mark of a record with changes that are not saved yet. */
72
72
  unsaved: string;
73
+ /** The same mark when the record says how many fields changed. */
74
+ unsavedOne: string;
75
+ unsavedMany: string;
73
76
  save: string;
74
77
  saving: string;
78
+ /** Said, briefly, once what was saved is saved. */
79
+ saved: string;
75
80
  discard: string;
76
81
  /** Read aloud next to a stage that failed: the red fill alone says nothing to a
77
82
  * screen reader. */
@@ -115,6 +120,8 @@ export type UiCopy = {
115
120
  unset: string;
116
121
  /** A list of checks read with nothing chosen. */
117
122
  none: string;
123
+ /** Next to the label of a field whose value changed and is not saved yet. */
124
+ changed: string;
118
125
  };
119
126
  /** The choice controls: combo boxes, selects, suggestion lists. */
120
127
  choice: {
@@ -127,6 +134,8 @@ export type UiCopy = {
127
134
  };
128
135
  search: {
129
136
  tools: string;
137
+ /** The name of the list of what the bar offers for what was typed. */
138
+ suggestions: string;
130
139
  /** The name of the panel button once something is set: it shows the count, and what
131
140
  * the button SHOWS has to be inside its name (WCAG 2.5.3) — `{label}` is `tools`. */
132
141
  toolsSetOne: string;
@@ -230,6 +239,10 @@ export type UiCopy = {
230
239
  distinct: string;
231
240
  /** Under a pile that holds its first page and counts them all: ask for the rest. */
232
241
  pileMore: string;
242
+ /** The same, when whoever pages says how many one press brings: «Show 50 more of 49,950». */
243
+ pileMoreOf: string;
244
+ /** Under a short pile with no next page: what it shows of what it counts. */
245
+ pileShown: string;
233
246
  };
234
247
  menu: {
235
248
  empty: string;
@@ -259,6 +272,8 @@ export type UiCopy = {
259
272
  * record itself. */
260
273
  peek: string;
261
274
  open: string;
275
+ /** Asked again while the rows stay: the name of the discreet bar over them. */
276
+ refreshing: string;
262
277
  };
263
278
  /** La historia de un registro: eventos y mensajes. */
264
279
  /** The panel of everything around a record: its notices, its conversation and its
@@ -72,8 +72,11 @@ export const SPANISH_COPY = {
72
72
  status: "Estado",
73
73
  faces: "Caras",
74
74
  unsaved: "sin guardar",
75
+ unsavedOne: "1 cambio sin guardar",
76
+ unsavedMany: "{count} cambios sin guardar",
75
77
  save: "Guardar",
76
78
  saving: "Guardando…",
79
+ saved: "Guardado",
77
80
  discard: "Descartar",
78
81
  failed: "falló",
79
82
  notices: "Avisos sobre este registro",
@@ -106,6 +109,7 @@ export const SPANISH_COPY = {
106
109
  help: "Ayuda",
107
110
  unset: "sin definir",
108
111
  none: "Ninguna",
112
+ changed: "modificado",
109
113
  },
110
114
  choice: {
111
115
  noResults: "Sin resultados",
@@ -127,6 +131,7 @@ export const SPANISH_COPY = {
127
131
  },
128
132
  search: {
129
133
  tools: "Filtrar, agrupar y favoritos",
134
+ suggestions: "Sugerencias",
130
135
  toolsSetOne: "{label}, {count} puesto",
131
136
  toolsSetMany: "{label}, {count} puestos",
132
137
  editChip: "Clic para editar",
@@ -200,6 +205,8 @@ export const SPANISH_COPY = {
200
205
  max: "máx {value}",
201
206
  distinct: "{value} distintos",
202
207
  pileMore: "Ver {count} más",
208
+ pileMoreOf: "Ver {count} más de {left}",
209
+ pileShown: "Se muestran {shown} de {count}",
203
210
  },
204
211
  menu: { empty: "Nada coincide." },
205
212
  list: {
@@ -209,6 +216,7 @@ export const SPANISH_COPY = {
209
216
  count: "{count} registros",
210
217
  peek: "Vista previa",
211
218
  open: "Abrir",
219
+ refreshing: "Actualizando la lista…",
212
220
  },
213
221
  activity: {
214
222
  bySystem: "el sistema",
@@ -48,8 +48,11 @@ export const ENGLISH_COPY = {
48
48
  status: "Status",
49
49
  faces: "Record faces",
50
50
  unsaved: "unsaved",
51
+ unsavedOne: "1 change unsaved",
52
+ unsavedMany: "{count} changes unsaved",
51
53
  save: "Save",
52
54
  saving: "Saving…",
55
+ saved: "Saved",
53
56
  discard: "Discard",
54
57
  failed: "failed",
55
58
  notices: "Notices about this record",
@@ -82,6 +85,7 @@ export const ENGLISH_COPY = {
82
85
  help: "Help",
83
86
  unset: "not set",
84
87
  none: "None",
88
+ changed: "changed",
85
89
  },
86
90
  choice: {
87
91
  noResults: "No results",
@@ -89,6 +93,7 @@ export const ENGLISH_COPY = {
89
93
  },
90
94
  search: {
91
95
  tools: "Filter, group and favorites",
96
+ suggestions: "Suggestions",
92
97
  toolsSetOne: "{label}, {count} applied",
93
98
  toolsSetMany: "{label}, {count} applied",
94
99
  editChip: "Click to edit",
@@ -146,6 +151,8 @@ export const ENGLISH_COPY = {
146
151
  max: "max {value}",
147
152
  distinct: "{value} distinct",
148
153
  pileMore: "Show {count} more",
154
+ pileMoreOf: "Show {count} more of {left}",
155
+ pileShown: "Showing {shown} of {count}",
149
156
  },
150
157
  menu: {
151
158
  empty: "Nothing matches.",
@@ -157,6 +164,7 @@ export const ENGLISH_COPY = {
157
164
  count: "{count} records",
158
165
  peek: "Quick look",
159
166
  open: "Open",
167
+ refreshing: "Updating the list…",
160
168
  },
161
169
  activity: {
162
170
  bySystem: "the system",
@@ -104,6 +104,15 @@ export type Pile<T> = {
104
104
  items: readonly T[];
105
105
  /** El nivel de abajo. Vacío en el más profundo. */
106
106
  piles: readonly Pile<T>[];
107
+ /**
108
+ * What the next «show more» of a short pile brings — said by whoever pages it.
109
+ *
110
+ * `50` reads «Show 50 more of 49,950»: the button says what one press adds, not what is
111
+ * missing. `false` says there is no next page although the count says more (a cap on
112
+ * the rows held, a server that stops paging): the button goes and the pile says how many
113
+ * it shows. Absent, the button counts what is left, as it did before this existed.
114
+ */
115
+ more?: number | false;
107
116
  };
108
117
  /**
109
118
  * La identidad de un NIVEL: el campo y, si es fecha, su grano.
@@ -280,6 +289,25 @@ export declare function itemPath<T>(item: T, levels: readonly GroupLevel[], of:
280
289
  * presentaciones que necesitan la secuencia y no el árbol —la planilla
281
290
  * virtualizada, que cuenta índices— y el que quiera saber qué quedó visible. */
282
291
  export declare function pileItems<T>(piles: readonly Pile<T>[]): readonly T[];
292
+ /**
293
+ * What the foot of a pile offers: the next page, or the word that there is none.
294
+ *
295
+ * Context: every presentation that draws piles —rows, cards, the table, one of the
296
+ * application's own— asks this, so «Show 50 more of 49,950» and «Showing 500 of 49,950»
297
+ * are decided once. `null` when the pile holds every row it counts, or holds piles and not
298
+ * rows. A short pile with no way to page (`more: false`, or no `onMore`) still says so: a
299
+ * header that reads 49,950 over 500 rows is a count the screen silently contradicts.
300
+ */
301
+ export type PileFoot = {
302
+ kind: "more";
303
+ left: number;
304
+ next?: number;
305
+ } | {
306
+ kind: "end";
307
+ shown: number;
308
+ count: number;
309
+ };
310
+ export declare function pileFoot<T>(pile: Pile<T>, canAsk: boolean): PileFoot | null;
283
311
  /** Cuántas filas hay en total. Suma los conteos del primer nivel, que ya
284
312
  * incluyen los de abajo — no recorre las filas, porque puede no tenerlas. */
285
313
  export declare function pileCount<T>(piles: readonly Pile<T>[]): number;
Binary file
@@ -9,7 +9,7 @@ export type { OpenTarget, Size, Surface, Tone } from "./types.js";
9
9
  export { SPANISH_COPY } from "./copy.es.js";
10
10
  export { flatten, loose, pick, pickAll, propsOf, slot, slotOf, type Slotted, } from "./slots.js";
11
11
  export { dateKey, dateLabel, grainName, monthName, } from "./calendar.js";
12
- export { GRAIN_ORDER, type GroupableField, type GroupGrain, type Grouping, type GroupLevel, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, type Pile, pileCount, pileIsOpen, pileItems, pileLabel, type PileLabelOf, type PileOf, pilesFrom, pilesOf, } from "./grouping.js";
12
+ export { GRAIN_ORDER, type GroupableField, type GroupGrain, type Grouping, type GroupLevel, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, type Pile, pileCount, type PileFoot, pileFoot, pileIsOpen, pileItems, pileLabel, type PileLabelOf, type PileOf, pilesFrom, pilesOf, } from "./grouping.js";
13
13
  export { axisLabels, axisSpans, keyStarts, type Pivot, type PivotAxis, type PivotCell, type PivotCrossing, type PivotFold, pivotFrom, pivotIsEmpty, type PivotKey, type PivotLevel, type PivotMargin, type PivotMeasure, pivotOf, type PivotSource, type PivotSpec, } from "./pivot.js";
14
14
  export { downloadPivot, pivotSheet, type PivotSheetOptions, pivotXlsx, } from "./pivotSheet.js";
15
15
  export { STICKY_TOP, stickyReset, stickyTop, useScrollBox, useStickyLayer, } from "./sticky.js";
@@ -23,7 +23,7 @@ export { dateKey, dateLabel, grainName, monthName, } from "./calendar.js";
23
23
  /* LA AGRUPACIÓN — el vocabulario, que hablan las dos capas: la lista arma el
24
24
  árbol de montones y la tabla lo dibuja. Vive acá y no en `contexts/list`
25
25
  porque una primitiva no puede importar de un contexto. */
26
- export { GRAIN_ORDER, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, pileCount, pileIsOpen, pileItems, pileLabel, pilesFrom, pilesOf, } from "./grouping.js";
26
+ export { GRAIN_ORDER, itemPath, levelKey, moveLevel, normalizeLevels, pathKey, pathLabels, pileCount, pileFoot, pileIsOpen, pileItems, pileLabel, pilesFrom, pilesOf, } from "./grouping.js";
27
27
  /* EL PIVOTE — el mismo reparto: el vocabulario acá, la grilla en `primitives`.
28
28
  Que los márgenes vengan de la base y que un agujero no sea un cero son reglas
29
29
  del CONTRATO, no del dibujo, y por eso viven de este lado. */
@@ -1,5 +1,5 @@
1
1
  import type { Surface } from "../../foundations/types.js";
2
- import type { LucideIcon } from "lucide-react";
2
+ import { type LucideIcon } from "lucide-react";
3
3
  import type { ComponentPropsWithRef, ReactNode } from "react";
4
4
  export type ButtonRole = "primary" | "secondary" | "ghost" | "danger";
5
5
  export type ButtonSize = "sm" | "md" | "lg";
@@ -53,8 +53,18 @@ export type ButtonProps = NativeProps<"button", "type" | "disabled" | "title" |
53
53
  expanded?: boolean;
54
54
  /** What it opens, written to `aria-haspopup`. `menu` when absent. */
55
55
  haspopup?: ButtonPopup;
56
+ /**
57
+ * What it started is running — saving, publishing, registering a payment.
58
+ *
59
+ * Context: the buttons said it by turning `disabled`, and a disabled button drops the focus
60
+ * to the page: whoever pressed Save with the keyboard was left nowhere, and a press while
61
+ * it ran was silently lost or — worse — sent twice. Busy, the button KEEPS the focus and
62
+ * its width (the label stays), says so (`aria-busy`, a turning mark where its icon was) and
63
+ * ignores presses until it is done.
64
+ */
65
+ busy?: boolean;
56
66
  };
57
- export declare function Button({ children, role, size, surface: surfaceProp, icon: Icon, disabled, title, type, expanded, haspopup, ref, ...native }: ButtonProps): import("react").JSX.Element;
67
+ export declare function Button({ children, role, size, surface: surfaceProp, icon: Icon, disabled, title, type, expanded, haspopup, busy, ref, onClick, ...native }: ButtonProps): import("react").JSX.Element;
58
68
  export type ButtonLinkProps = NativeProps<"a", "href" | "download" | "title"> & {
59
69
  children: ReactNode;
60
70
  href: string;