@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.
- package/dist/components/form/Sheet.js +4 -1
- package/dist/components/listing/RecordCard.d.ts +4 -0
- package/dist/components/listing/RecordCard.js +4 -1
- package/dist/components/listing/Row.d.ts +5 -4
- package/dist/components/listing/Row.js +5 -4
- package/dist/components/record/DirtyActions.d.ts +20 -6
- package/dist/components/record/DirtyActions.js +55 -4
- package/dist/components/record/RecordHeader.d.ts +1 -1
- package/dist/components/record/RecordHeader.js +32 -13
- package/dist/components/record/RecordSkeleton.js +1 -1
- package/dist/components/search/SearchBar.js +12 -3
- package/dist/foundations/copy.d.ts +15 -0
- package/dist/foundations/copy.es.js +8 -0
- package/dist/foundations/copy.js +8 -0
- package/dist/foundations/grouping.d.ts +28 -0
- package/dist/foundations/grouping.js +0 -0
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/index.js +1 -1
- package/dist/primitives/button/Button.d.ts +12 -2
- package/dist/primitives/button/Button.js +12 -2
- package/dist/primitives/cell/CellStack.js +1 -1
- package/dist/primitives/field/Field.d.ts +8 -3
- package/dist/primitives/field/Field.js +7 -3
- package/dist/primitives/notice/Notice.js +3 -1
- package/dist/primitives/picker/MatchedText.d.ts +3 -2
- package/dist/primitives/picker/MatchedText.js +3 -2
- package/dist/primitives/picker/Picker.d.ts +18 -1
- package/dist/primitives/picker/Picker.js +5 -5
- package/dist/primitives/picker/TagsField.d.ts +10 -0
- package/dist/primitives/picker/TagsField.js +2 -2
- package/dist/primitives/picker/index.d.ts +2 -1
- package/dist/primitives/picker/index.js +2 -0
- package/dist/primitives/picker/tagChoices.d.ts +7 -0
- package/dist/primitives/picker/tagChoices.js +9 -4
- package/dist/primitives/table/DataTable.js +10 -11
- package/dist/primitives/table/EditableTable.js +5 -1
- package/dist/primitives/table/PileFootLine.d.ts +14 -0
- package/dist/primitives/table/PileFootLine.js +22 -0
- package/dist/primitives/table/index.d.ts +1 -0
- package/dist/primitives/table/index.js +1 -0
- package/dist/tokens.css +42 -0
- package/dist/views/list/List.d.ts +11 -3
- package/dist/views/list/ListView.js +19 -8
- package/dist/views/list/index.d.ts +1 -1
- package/dist/views/list/types.d.ts +24 -2
- 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 }),
|
|
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 :
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
141
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
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)
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
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 ?
|
|
77
|
-
|
|
78
|
-
|
|
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,
|
|
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", {
|
|
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",
|
package/dist/foundations/copy.js
CHANGED
|
@@ -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
|
|
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;
|