@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
|
@@ -60,6 +60,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
60
60
|
* ==================================================================== */
|
|
61
61
|
import { useSurface } from "../../foundations/surface.js";
|
|
62
62
|
import { Tooltip, TooltipTrigger, useInTooltipTrigger, } from "../overlay/Tooltip.js";
|
|
63
|
+
import { LoaderCircle } from "lucide-react";
|
|
63
64
|
/** The height comes from `--size-control-*`; the padding and the type follow.
|
|
64
65
|
*
|
|
65
66
|
* sm a row, the header of a block
|
|
@@ -146,10 +147,19 @@ function popupAttrs(expanded, haspopup) {
|
|
|
146
147
|
...(expanded === undefined ? {} : { "aria-expanded": expanded }),
|
|
147
148
|
};
|
|
148
149
|
}
|
|
149
|
-
export function Button({ children, role = "secondary", size = "md", surface: surfaceProp, icon: Icon, disabled = false, title, type = "button", expanded, haspopup, ref, ...native }) {
|
|
150
|
+
export function Button({ children, role = "secondary", size = "md", surface: surfaceProp, icon: Icon, disabled = false, title, type = "button", expanded, haspopup, busy = false, ref, onClick, ...native }) {
|
|
150
151
|
const surface = useSurface(surfaceProp);
|
|
151
152
|
const ic = ICON[size];
|
|
152
|
-
|
|
153
|
+
const Mark = busy ? LoaderCircle : Icon;
|
|
154
|
+
return (_jsxs("button", { ...native, ref: ref, type: type, title: title, disabled: disabled, ...(busy ? { "aria-busy": true, "aria-disabled": true } : {}), onClick: (event) => {
|
|
155
|
+
if (busy) {
|
|
156
|
+
event.preventDefault();
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
onClick?.(event);
|
|
160
|
+
}, ...popupAttrs(expanded, haspopup), "data-emphasis": role, className: busy
|
|
161
|
+
? `${buttonClasses(role, size, surface)} cursor-progress`
|
|
162
|
+
: buttonClasses(role, size, surface), children: [Mark === undefined ? null : (_jsx(Mark, { className: `${ic.size} shrink-0 ${busy ? "motion-safe:animate-spin" : ""}`, strokeWidth: ic.stroke, "aria-hidden": true })), children] }));
|
|
153
163
|
}
|
|
154
164
|
/**
|
|
155
165
|
* The same button, but it is a real link.
|
|
@@ -32,5 +32,5 @@ export function Thumb({ src, alt, icon: Icon, size = "md", }) {
|
|
|
32
32
|
export function CellStack({ primary, secondary, media, avatar, wrap = false, }) {
|
|
33
33
|
return (_jsxs("span", { className: "flex min-w-0 items-center gap-2.5", children: [avatar !== undefined ? (
|
|
34
34
|
/* The name is written beside it: the photo's `alt` would say it twice. */
|
|
35
|
-
_jsx("span", { "aria-hidden": true, className: "flex shrink-0", children: _jsx(Avatar, { name: avatar.name, size: "lg", ...(avatar.src === undefined ? {} : { src: avatar.src }) }) })) : media === undefined ? null : (_jsx(Thumb, { size: "md", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "
|
|
35
|
+
_jsx("span", { "aria-hidden": true, className: "flex shrink-0", children: _jsx(Avatar, { name: avatar.name, size: "lg", ...(avatar.src === undefined ? {} : { src: avatar.src }) }) })) : media === undefined ? null : (_jsx(Thumb, { size: "md", ...(media.src === undefined ? {} : { src: media.src }), ...(media.alt === undefined ? {} : { alt: media.alt }), ...(media.icon === undefined ? {} : { icon: media.icon }) })), _jsxs("span", { className: "flex min-w-0 flex-col", children: [_jsx("span", { className: "line-clamp-1 text-meta font-medium break-all text-ink", ...(typeof primary === "string" ? { title: primary } : {}), children: primary }), secondary === undefined ? null : (_jsx("span", { className: `text-micro text-muted ${wrap ? "line-clamp-2" : "line-clamp-1 break-all"}`, ...(typeof secondary === "string" ? { title: secondary } : {}), children: secondary }))] })] }));
|
|
36
36
|
}
|
|
@@ -2,7 +2,7 @@ import { type FieldIds } from "../../foundations/form.js";
|
|
|
2
2
|
import { type FormMode } from "../../foundations/form.js";
|
|
3
3
|
import { type FieldNote } from "./note.js";
|
|
4
4
|
import { type ReactNode } from "react";
|
|
5
|
-
export declare function Field({ label, hint, error, required, children, htmlFor, layout, action, mode, note, copy: copyAs, }: {
|
|
5
|
+
export declare function Field({ label, hint, error, required, children, htmlFor, layout, action, mode, note, changed, copy: copyAs, }: {
|
|
6
6
|
label: string;
|
|
7
7
|
/** What is expected. A `?` on the label that opens it on press. */
|
|
8
8
|
hint?: string;
|
|
@@ -32,8 +32,13 @@ export declare function Field({ label, hint, error, required, children, htmlFor,
|
|
|
32
32
|
* Aplicar». A card in the record's margin when there is one, a `Flag` by the label when
|
|
33
33
|
* there is not; its sentence describes the control (`aria-describedby`). See `note.tsx`. */
|
|
34
34
|
note?: FieldNote;
|
|
35
|
-
/**
|
|
36
|
-
*
|
|
35
|
+
/** Its value changed and is not saved yet. A dot by the label says WHICH fields a
|
|
36
|
+
* «2 cambios sin guardar» is about — in a form of twenty, the reason to look before
|
|
37
|
+
* saving — and it is read as part of the field's description. */
|
|
38
|
+
changed?: boolean;
|
|
39
|
+
/** The copy button by the label (see `clipboard.tsx`): on by default, copying what the
|
|
40
|
+
* control shows. A string copies that instead; `false` removes the button from this
|
|
41
|
+
* field. */
|
|
37
42
|
copy?: string | false;
|
|
38
43
|
}): import("react").JSX.Element;
|
|
39
44
|
/**
|
|
@@ -40,7 +40,7 @@ import { Button as AriaButton, Input, ListBox, ListBoxItem, NumberField as AriaN
|
|
|
40
40
|
* control ate a line on every field. The error still goes below, close to
|
|
41
41
|
* the cursor, and it says what to put.
|
|
42
42
|
* ==================================================================== */
|
|
43
|
-
export function Field({ label, hint, error, required = false, children, htmlFor, layout, action, mode, note, copy: copyAs, }) {
|
|
43
|
+
export function Field({ label, hint, error, required = false, children, htmlFor, layout, action, mode, note, changed = false, copy: copyAs, }) {
|
|
44
44
|
const d = useDensity();
|
|
45
45
|
const copy = useUiCopy();
|
|
46
46
|
const copies = useFieldCopy() && copyAs !== false;
|
|
@@ -52,9 +52,13 @@ export function Field({ label, hint, error, required = false, children, htmlFor,
|
|
|
52
52
|
const hintId = `${id}-hint`;
|
|
53
53
|
const errorId = `${id}-error`;
|
|
54
54
|
const noteId = `${id}-note`;
|
|
55
|
+
const changedId = `${id}-changed`;
|
|
55
56
|
/* The error first: what has to be corrected is heard before what is worth knowing. */
|
|
56
|
-
const describedBy = describedByOf(hint === undefined ? null : hintId, error === undefined ? null : errorId, note === undefined ? null : noteId);
|
|
57
|
-
const labelNode = (
|
|
57
|
+
const describedBy = describedByOf(hint === undefined ? null : hintId, error === undefined ? null : errorId, note === undefined ? null : noteId, changed ? changedId : null);
|
|
58
|
+
const labelNode = (
|
|
59
|
+
/* The label follows its control's focus: in a form of twenty fields, the eye finds the
|
|
60
|
+
row being written by its name as much as by the ring. */
|
|
61
|
+
_jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted transition-colors duration-(--duration-quick) group-focus-within/field:text-primary ${inline ? "pt-2" : ""}`, children: [_jsxs("label", { id: `${id}-label`, htmlFor: id, className: inline ? "min-w-0 truncate" : "", children: [label, required ? (_jsxs(_Fragment, { children: [_jsxs("span", { className: "text-ink", "aria-hidden": true, children: [" ", "*"] }), _jsxs("span", { className: "sr-only", children: [", ", copy.field.required] })] })) : null] }), hint === undefined ? null : _jsx(Hint, { id: hintId, children: hint }), note === undefined ? null : _jsx(NoteFlag, { note: note }), changed ? (_jsxs(_Fragment, { children: [_jsx("span", { "aria-hidden": true, title: copy.field.changed, className: "size-1.5 shrink-0 rounded-pill bg-accent transition-transform duration-(--duration-quick) ease-(--ease-out) starting:scale-0" }), _jsx("span", { id: changedId, className: "sr-only", children: copy.field.changed })] })) : null, copies ? (_jsx(FieldCopy, { label: label, scope: shown, ...(typeof copyAs === "string" ? { value: copyAs } : {}) })) : null] }));
|
|
58
62
|
/* Last in the field so it is read last; absolutely placed, it still sits at the field's
|
|
59
63
|
top — the static position of an absolute child of a flex or grid box is its start. */
|
|
60
64
|
const noteCard = note === undefined ? null : _jsx(NoteCard, { note: note, id: noteId, owner: label });
|
|
@@ -27,7 +27,9 @@ import { AlertTriangle, PackageX } from "lucide-react";
|
|
|
27
27
|
const SKELETON_NAME = ["w-48", "w-36", "w-56", "w-40", "w-44", "w-32"];
|
|
28
28
|
export function Skeleton({ rows = 6, label, }) {
|
|
29
29
|
const d = useDensity();
|
|
30
|
-
return (
|
|
30
|
+
return (
|
|
31
|
+
/* It appears late (`animate-reveal-late`): a read faster than 200ms never shows it. */
|
|
32
|
+
_jsxs("div", { "aria-busy": "true", className: "flex animate-reveal-late flex-col", children: [_jsx("span", { className: "sr-only", children: label }), Array.from({ length: rows }, (_, i) => (_jsxs("div", { "aria-hidden": true, className: `flex items-center ${byDensity(d, "gap-3 px-2 py-2", "gap-2 px-1.5 py-1")}`, children: [_jsx("span", { className: `h-3.5 rounded-control bg-ink/8 ${SKELETON_NAME[i % SKELETON_NAME.length]}` }), _jsx("span", { className: "ml-auto h-3 w-14 rounded-control bg-ink/5" })] }, i)))] }));
|
|
31
33
|
}
|
|
32
34
|
/** What is shown when there is nothing to show: a sentence, and the gesture
|
|
33
35
|
* that resolves it if there is one. Never a drawing. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A text with the part that matched the search marked —
|
|
3
|
-
*
|
|
2
|
+
* A text with the part that matched the search marked — what the lists that filter as you
|
|
3
|
+
* type (`Picker`, `TagsField`) draw in every row, and what an option of the application's own
|
|
4
|
+
* (`renderOption`) uses to say the same. A `<mark>`, so it is also said as marked text, and
|
|
4
5
|
* bold besides the tint: in forced colors the tint is gone and the weight is what remains.
|
|
5
6
|
*/
|
|
6
7
|
export declare function MatchedText({ text, query, locale, }: {
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { matchSpans } from "../../foundations/match.js";
|
|
3
3
|
/**
|
|
4
|
-
* A text with the part that matched the search marked —
|
|
5
|
-
*
|
|
4
|
+
* A text with the part that matched the search marked — what the lists that filter as you
|
|
5
|
+
* type (`Picker`, `TagsField`) draw in every row, and what an option of the application's own
|
|
6
|
+
* (`renderOption`) uses to say the same. A `<mark>`, so it is also said as marked text, and
|
|
6
7
|
* bold besides the tint: in forced colors the tint is gone and the weight is what remains.
|
|
7
8
|
*/
|
|
8
9
|
export function MatchedText({ text, query, locale, }) {
|
|
@@ -30,6 +30,14 @@ export interface PickerItem {
|
|
|
30
30
|
* lined up in one column, eight customers' debts are read top to bottom. */
|
|
31
31
|
aside?: ReactNode;
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* What a `renderOption` is handed beside the item: whether it is the chosen one, and what was
|
|
35
|
+
* typed — mark it with `MatchedText`, so the row still says why it is listed.
|
|
36
|
+
*/
|
|
37
|
+
export type PickerOptionState = {
|
|
38
|
+
selected: boolean;
|
|
39
|
+
query: string;
|
|
40
|
+
};
|
|
33
41
|
/**
|
|
34
42
|
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
35
43
|
* unless the list loads incrementally — then everything that arrived is listed and the
|
|
@@ -94,6 +102,14 @@ export type PickerProps = {
|
|
|
94
102
|
/** A page asked for by `onLoadMore` is on its way: a quiet row at the end says so. Unlike
|
|
95
103
|
* `loading`, the rows already there stay. */
|
|
96
104
|
loadingMore?: boolean;
|
|
105
|
+
/**
|
|
106
|
+
* The content of an option, for one that does not fit «name + meta + aside» — a product
|
|
107
|
+
* with its photo, a slot of an agenda. The row around it stays the picker's: keys, focus,
|
|
108
|
+
* the search by `name` (and `haystack`), the ✓ of the chosen one. What it draws is also
|
|
109
|
+
* what the option is called, so it should show the name. Data beyond `PickerItem` is looked
|
|
110
|
+
* up by `item.id` in the consumer's own records.
|
|
111
|
+
*/
|
|
112
|
+
renderOption?: (item: PickerItem, state: PickerOptionState) => ReactNode;
|
|
97
113
|
describedBy?: string;
|
|
98
114
|
invalid?: boolean;
|
|
99
115
|
disabled?: boolean;
|
|
@@ -109,7 +125,7 @@ export declare function Picker({ mode, ...props }: PickerProps): import("react")
|
|
|
109
125
|
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
110
126
|
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
111
127
|
*/
|
|
112
|
-
export declare function PickerOptions({ items, marks, hints, remaining, query, emptyText, onLoadMore, loadingMore, "aria-label": ariaLabel, }: {
|
|
128
|
+
export declare function PickerOptions({ items, marks, hints, remaining, query, emptyText, onLoadMore, loadingMore, renderOption, "aria-label": ariaLabel, }: {
|
|
113
129
|
items: readonly PickerItem[];
|
|
114
130
|
marks: boolean;
|
|
115
131
|
hints: boolean;
|
|
@@ -119,6 +135,7 @@ export declare function PickerOptions({ items, marks, hints, remaining, query, e
|
|
|
119
135
|
emptyText: string;
|
|
120
136
|
onLoadMore?: () => void;
|
|
121
137
|
loadingMore?: boolean;
|
|
138
|
+
renderOption?: (item: PickerItem, state: PickerOptionState) => ReactNode;
|
|
122
139
|
/** Only when rendered outside the combobox, which otherwise names the list. */
|
|
123
140
|
"aria-label"?: string;
|
|
124
141
|
}): import("react").JSX.Element;
|
|
@@ -98,7 +98,7 @@ export function Picker({ mode, ...props }) {
|
|
|
98
98
|
to be ABOVE the component that asks for it. */
|
|
99
99
|
return (_jsx(AriaLocale, { children: _jsx(EditablePicker, { ...props }) }));
|
|
100
100
|
}
|
|
101
|
-
function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabel, placeholder, onBrowseAll, onCreate, onCreateAndEdit, onOpen, openTargets = ["modal", "peek", "side", "page"], emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
101
|
+
function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabel, placeholder, onBrowseAll, onCreate, onCreateAndEdit, onOpen, openTargets = ["modal", "peek", "side", "page"], emptyText, searchPlaceholder, loading = false, loadingText, onLoadMore, loadingMore = false, renderOption, describedBy: ownDescribedBy, invalid: ownInvalid, disabled = false, "aria-label": ariaLabel, }) {
|
|
102
102
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
103
103
|
id: ownId,
|
|
104
104
|
describedBy: ownDescribedBy,
|
|
@@ -235,14 +235,14 @@ function EditablePicker({ id: ownId, items, value, onChange, onSearch, valueLabe
|
|
|
235
235
|
], onSelect: (to) => onOpen(value, to) }))] })) : null, _jsx(FieldButton, { className: "mr-1", children: _jsx(ChevronDown, { "aria-hidden": true, className: "size-(--size-icon-md)", strokeWidth: 2 }) })] }), _jsxs(Popover, { placement: "bottom start", className: "flex w-(--trigger-width) flex-col overflow-hidden", children: [_jsx(PickerOptions, { items: listed, marks: hasMarks, hints: hasHints, remaining: remaining, query: query, emptyText: loading ? shownLoading : shownEmpty,
|
|
236
236
|
/* Not while the first page loads: the sentinel of an empty list is in view and
|
|
237
237
|
would ask for page two before page one arrived. */
|
|
238
|
-
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
238
|
+
onLoadMore: loading ? undefined : onLoadMore, loadingMore: loadingMore, ...(renderOption === undefined ? {} : { renderOption }) }), remaining > 0 && !browsable ? (_jsxs("div", { className: "flex shrink-0 items-center gap-2 border-t border-line bg-ground px-2.5 py-1.5", children: [_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, { count: remaining.toLocaleString(copy.locale) }) }), _jsx("span", { className: "text-micro text-muted", children: copy.picker.refine })] })) : null] })] }));
|
|
239
239
|
}
|
|
240
240
|
/**
|
|
241
241
|
* The list of a `Picker`: its rows, the section headers, the «Buscar más…» row and the loading
|
|
242
242
|
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
243
243
|
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
244
244
|
*/
|
|
245
|
-
export function PickerOptions({ items, marks, hints, remaining, query = "", emptyText, onLoadMore, loadingMore = false, "aria-label": ariaLabel, }) {
|
|
245
|
+
export function PickerOptions({ items, marks, hints, remaining, query = "", emptyText, onLoadMore, loadingMore = false, renderOption, "aria-label": ariaLabel, }) {
|
|
246
246
|
const copy = useUiCopy();
|
|
247
247
|
const entries = useMemo(() => sectioned(items), [items]);
|
|
248
248
|
/* The actions are one block under the records: one hairline above the first of them. */
|
|
@@ -251,14 +251,14 @@ export function PickerOptions({ items, marks, hints, remaining, query = "", empt
|
|
|
251
251
|
? "mt-1 border-t border-line"
|
|
252
252
|
: ""), children: _jsxs("span", { className: "flex w-full items-center justify-between gap-2", children: [_jsx("span", { className: "min-w-0 truncate font-medium", children: item.name }), item.id !== BROWSE_ALL || remaining === 0 ? null : (_jsx("span", { className: "text-micro tabular-nums text-muted", children: fillCopy(copy.picker.more, {
|
|
253
253
|
count: remaining.toLocaleString(copy.locale),
|
|
254
|
-
}) }))] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints, query: query, locale: copy.locale })) }));
|
|
254
|
+
}) }))] }) })) : (_jsx(ListBoxItem, { id: item.id, textValue: item.name, isDisabled: item.disabled, className: (state) => option({ ...state, hints }), children: ({ isSelected }) => renderOption === undefined ? (_jsx(Row, { item: item, selected: isSelected, marks: marks, hints: hints, query: query, locale: copy.locale })) : (_jsxs(_Fragment, { children: [_jsx("span", { className: "min-w-0 flex-1", children: renderOption(item, { selected: isSelected, query }) }), isSelected ? (_jsx(Check, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0 self-center text-primary forced-colors:text-[HighlightText]", strokeWidth: 2.5 })) : null] })) }));
|
|
255
255
|
return (_jsxs(ListBox, { "aria-label": ariaLabel,
|
|
256
256
|
/* Nothing matched YET while the next page is coming: «nothing matches» above «loading
|
|
257
257
|
more…» says two contradicting things, so the empty list says only the second. */
|
|
258
258
|
renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-center text-meta text-muted", children: loadingMore ? copy.picker.loadingMore : emptyText })),
|
|
259
259
|
/* Incremental, the list is not capped: it scrolls inside its own height and the
|
|
260
260
|
sentinel at its end is what asks for more. */
|
|
261
|
-
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, dependencies: [query], children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, dependencies: [query], children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
261
|
+
className: cn("min-h-0 flex-1 overflow-y-auto p-1 outline-0", onLoadMore === undefined ? "" : "max-h-(--size-dropdown)"), children: [_jsx(Collection, { items: entries, dependencies: [query, renderOption], children: (entry) => "items" in entry ? (_jsxs(ListBoxSection, { id: entry.id, className: "not-first:mt-1 not-first:border-t not-first:border-line not-first:pt-1", children: [_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [marks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, entry.section] }), _jsx(Collection, { items: entry.items, dependencies: [query, renderOption], children: renderItem })] })) : (renderItem(entry)) }), onLoadMore === undefined ? null : (_jsx(ListBoxLoadMoreItem, { onLoadMore: onLoadMore, isLoading: loadingMore && items.length > 0, className: "px-3 py-1.5 text-center text-micro text-muted", children: copy.picker.loadingMore }))] }));
|
|
262
262
|
}
|
|
263
263
|
function Row({ item, selected, marks, hints, query, locale, }) {
|
|
264
264
|
const Icon = item.icon;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type FormMode } from "../../foundations/form.js";
|
|
2
2
|
import type { OpenTarget } from "../../foundations/types.js";
|
|
3
3
|
import { type TagOption } from "./tagChoices.js";
|
|
4
|
+
import { type ReactNode } from "react";
|
|
4
5
|
export type TagsFieldProps = {
|
|
5
6
|
/** The ids of the chosen tags, in the order they were added. */
|
|
6
7
|
value: readonly string[];
|
|
@@ -31,6 +32,15 @@ export type TagsFieldProps = {
|
|
|
31
32
|
/** Where a pressed chip opens. A peek by default: a chip is a glance at one of many — «who is
|
|
32
33
|
* this follower?» — not a place to go and work. */
|
|
33
34
|
openTarget?: OpenTarget;
|
|
35
|
+
/**
|
|
36
|
+
* The content of an option in the list, for one that does not fit «face + name + meta». The
|
|
37
|
+
* row stays the field's — keys, the search by `label`, Enter that adds — and «Create…» and
|
|
38
|
+
* «Buscar más…» stay the field's too. Mark what was typed with `MatchedText`. The chips are
|
|
39
|
+
* not touched: a chosen tag is read by its name.
|
|
40
|
+
*/
|
|
41
|
+
renderOption?: (option: TagOption, state: {
|
|
42
|
+
query: string;
|
|
43
|
+
}) => ReactNode;
|
|
34
44
|
/** The input's placeholder while no tag is chosen. Defaults to the copy table's «Add…». */
|
|
35
45
|
placeholder?: string;
|
|
36
46
|
disabled?: boolean;
|
|
@@ -84,7 +84,7 @@ export function TagsField({ mode, ...props }) {
|
|
|
84
84
|
to be ABOVE the component that asks for it. */
|
|
85
85
|
return (_jsx(AriaLocale, { children: _jsx(EditableTagsField, { ...props }) }));
|
|
86
86
|
}
|
|
87
|
-
function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBrowseAll, onOpen, openTarget, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
|
|
87
|
+
function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBrowseAll, onOpen, openTarget, renderOption, placeholder, disabled = false, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, "aria-label": ariaLabel, }) {
|
|
88
88
|
const { id, labelledBy, describedBy, invalid } = useFieldIds({
|
|
89
89
|
id: ownId,
|
|
90
90
|
describedBy: ownDescribedBy,
|
|
@@ -184,7 +184,7 @@ function EditableTagsField({ value, onChange, options, onCreate, onSearch, onBro
|
|
|
184
184
|
onChange?.(withoutTag(value, last));
|
|
185
185
|
}, empty: text === "" }), _jsx(Popover, { triggerRef: boxRef, ...(width === null
|
|
186
186
|
? {}
|
|
187
|
-
: { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, dependencies: [text], className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE || item.value === BROWSE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : item.value === BROWSE ? (_jsxs(_Fragment, { children: [_jsx(Search, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : (_jsxs(_Fragment, { children: [item.avatar === undefined ? null : (_jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsx("span", { className: "truncate", children: _jsx(MatchedText, { text: item.label, query: text, locale: copy.locale }) })] })) })) }) })] })] }));
|
|
187
|
+
: { style: { "--trigger-width": `${width}px` } }), placement: "bottom start", className: "w-(--trigger-width)", children: _jsx(ListBox, { items: listed, dependencies: [text, renderOption], className: "max-h-[inherit] overflow-auto p-1 outline-0", children: (item) => (_jsx(ListBoxItem, { id: item.value, textValue: item.label, className: (state) => cn(listOptionStyles(state), item.value === CREATE || item.value === BROWSE ? "text-primary" : ""), children: item.value === CREATE ? (_jsxs(_Fragment, { children: [_jsx(Plus, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : item.value === BROWSE ? (_jsxs(_Fragment, { children: [_jsx(Search, { "aria-hidden": true, className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2 }), _jsx("span", { className: "truncate font-medium", children: item.label })] })) : renderOption !== undefined ? (_jsx("span", { className: "min-w-0 flex-1", children: renderOption(item, { query: text }) })) : (_jsxs(_Fragment, { children: [item.avatar === undefined ? null : (_jsx(Avatar, { name: item.label, size: "sm", ...(typeof item.avatar === "string" ? { src: item.avatar } : {}) })), _jsxs("span", { className: "flex min-w-0 flex-col gap-0.5", children: [_jsx("span", { className: "truncate", children: _jsx(MatchedText, { text: item.label, query: text, locale: copy.locale }) }), item.meta === undefined ? null : (_jsx("span", { className: "truncate text-micro tabular-nums text-muted", children: _jsx(MatchedText, { text: item.meta, query: text, locale: copy.locale }) }))] })] })) })) }) })] })] }));
|
|
188
188
|
}
|
|
189
189
|
/** The input after the chips. It reads the combobox's state to know whether the keyboard
|
|
190
190
|
* marked an option — Enter with nothing marked takes the first match. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
export { Picker, type PickerItem, type PickerProps, } from "./Picker.js";
|
|
1
|
+
export { Picker, type PickerItem, type PickerOptionState, type PickerProps, } from "./Picker.js";
|
|
2
|
+
export { MatchedText } from "./MatchedText.js";
|
|
2
3
|
export { type TagOption } from "./tagChoices.js";
|
|
3
4
|
export { TagsField, type TagsFieldProps } from "./TagsField.js";
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/* CHOOSING RECORDS — one (`Picker`, the many2one) or several (`TagsField`, the many2many).
|
|
2
2
|
The replacement for `<select>` when you have to SEE in order to choose. */
|
|
3
3
|
export { Picker, } from "./Picker.js";
|
|
4
|
+
/* Lo buscado, marcado: para que una opción propia (`renderOption`) lo diga como las demás. */
|
|
5
|
+
export { MatchedText } from "./MatchedText.js";
|
|
4
6
|
/* El many2many: las fichas elegidas más una entrada que agrega la siguiente. */
|
|
5
7
|
export {} from "./tagChoices.js";
|
|
6
8
|
export { TagsField } from "./TagsField.js";
|
|
@@ -13,6 +13,13 @@ export interface TagOption {
|
|
|
13
13
|
tone?: Tone;
|
|
14
14
|
/** A person: its face on the chip and in the list — `true` for initials, a URL for a photo. */
|
|
15
15
|
avatar?: true | string;
|
|
16
|
+
/**
|
|
17
|
+
* The datum that tells two options apart, already written — the NIT of a customer, the
|
|
18
|
+
* email of a follower. Drawn under the name in the list and searched like it; NOT on the
|
|
19
|
+
* chip: a chip is recognised by its name, and a NIT on every chip turns the strip into a
|
|
20
|
+
* table. Same role as `PickerItem.meta`.
|
|
21
|
+
*/
|
|
22
|
+
meta?: string;
|
|
16
23
|
}
|
|
17
24
|
/** How the typed text is compared — the locale's, handed in by the component. */
|
|
18
25
|
export type TagMatch = {
|
|
@@ -14,10 +14,15 @@ searched = false) {
|
|
|
14
14
|
const chosen = new Set(value);
|
|
15
15
|
const query = text.trim();
|
|
16
16
|
const available = options.filter((option) => !chosen.has(option.value) &&
|
|
17
|
-
(searched ||
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
17
|
+
(searched ||
|
|
18
|
+
query === "" ||
|
|
19
|
+
match.contains(option.label, query) ||
|
|
20
|
+
(option.meta !== undefined && match.contains(option.meta, query))));
|
|
21
|
+
/* Typing a NIT that one option carries names that option: creating a tag called «1023…»
|
|
22
|
+
next to it is the duplicate this rule exists to prevent. */
|
|
23
|
+
const named = options.some((option) => match.same(option.label, query) ||
|
|
24
|
+
(option.meta !== undefined && match.same(option.meta, query)));
|
|
25
|
+
const create = creatable && query !== "" && !named ? query : null;
|
|
21
26
|
return { available, create };
|
|
22
27
|
}
|
|
23
28
|
/** The value with one more id at the end; the same value when it is already there. */
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
-
import { pathKey, pileItems } from "../../foundations/grouping.js";
|
|
4
|
+
import { pathKey, pileFoot, pileItems, } from "../../foundations/grouping.js";
|
|
5
5
|
import { CellCopy } from "./CellCopy.js";
|
|
6
6
|
import { CellEditor } from "./CellEditor.js";
|
|
7
7
|
import { EditRow, firstControl } from "./EditableTable.js";
|
|
8
|
+
import { PileFootLine } from "./PileFootLine.js";
|
|
8
9
|
import { RowActions } from "./RowActions.js";
|
|
9
10
|
import { useRowWindow } from "./rowWindow.js";
|
|
10
11
|
import { SelectBox, Table, Td, Th, THead, Tr, } from "./Table.js";
|
|
@@ -140,21 +141,21 @@ function entriesOfPiles(piles, keyOf, collapsed, onToggle, onMore) {
|
|
|
140
141
|
if (pile.piles.length > 0) {
|
|
141
142
|
return [head, ...entriesOfPiles(pile.piles, keyOf, collapsed, onToggle, onMore)];
|
|
142
143
|
}
|
|
143
|
-
const
|
|
144
|
+
const foot = pileFoot(pile, onMore !== undefined);
|
|
144
145
|
return [
|
|
145
146
|
head,
|
|
146
147
|
...pile.items.map((item) => ({ kind: "row", id: keyOf(item), item })),
|
|
147
|
-
...(
|
|
148
|
-
? [
|
|
148
|
+
...(foot === null
|
|
149
|
+
? []
|
|
150
|
+
: [
|
|
149
151
|
{
|
|
150
152
|
kind: "more",
|
|
151
153
|
id: `\u0000more${key}`,
|
|
152
154
|
depth: pile.depth,
|
|
153
|
-
|
|
154
|
-
more: () => onMore(pile),
|
|
155
|
+
foot,
|
|
156
|
+
more: () => onMore?.(pile),
|
|
155
157
|
},
|
|
156
|
-
]
|
|
157
|
-
: []),
|
|
158
|
+
]),
|
|
158
159
|
];
|
|
159
160
|
});
|
|
160
161
|
}
|
|
@@ -385,9 +386,7 @@ export function DataTable({ data, columns, getRowId, sort, onSortChange, levels,
|
|
|
385
386
|
if (entry.kind === "more") {
|
|
386
387
|
return [
|
|
387
388
|
gap,
|
|
388
|
-
_jsx("tr", { ...windowed, className: "border-b border-line", children: _jsx("td", { colSpan: width, className: "px-3 py-1", style: indent, children: _jsx(
|
|
389
|
-
count: entry.left.toLocaleString(copy.locale),
|
|
390
|
-
}) }) }) }, entry.id),
|
|
389
|
+
_jsx("tr", { ...windowed, className: "border-b border-line", children: _jsx("td", { colSpan: width, className: "px-3 py-1", style: indent, children: _jsx(PileFootLine, { foot: entry.foot, onMore: entry.more }) }) }, entry.id),
|
|
391
390
|
];
|
|
392
391
|
}
|
|
393
392
|
const original = entry.item;
|
|
@@ -602,7 +602,11 @@ groupTotal, height, layout = "auto", onOpen, onDiscard, bands, adds, flag, onReo
|
|
|
602
602
|
return null;
|
|
603
603
|
return (_jsxs("div", { className: "flex items-baseline justify-between gap-3 text-meta", children: [_jsx("dt", { className: "text-muted", children: c.header }), _jsx("dd", { className: "font-semibold text-ink tabular-nums", children: total })] }, c.id));
|
|
604
604
|
}) }))] }));
|
|
605
|
-
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, tabIndex: box.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => c.header === "" ? (_jsx("td", { className: "w-0 border-b border-line-strong" }, c.id)) : (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width },
|
|
605
|
+
return (_jsxs("div", { ref: frame, className: "min-w-0", children: [_jsx("div", { ref: box.ref, tabIndex: box.tabIndex, className: `min-w-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary ${box.className}`, style: box.style, children: stacked ? (list) : (_jsxs("table", { className: "w-full border-separate border-spacing-0 px-1 text-left", children: [_jsx("thead", { className: "sticky z-10 bg-ground", style: stickyTop, children: _jsxs("tr", { children: [reorderable ? _jsx("td", { className: "w-0 border-b border-line-strong" }) : null, columns.map((c) => c.header === "" ? (_jsx("td", { className: "w-0 border-b border-line-strong" }, c.id)) : (_jsx("th", { scope: "col", style: c.width === undefined ? undefined : { width: c.width },
|
|
606
|
+
/* A column of figures takes its figures' width (`w-0` in an auto
|
|
607
|
+
table is «as narrow as its content») and the text takes the rest:
|
|
608
|
+
the product name was cut while «Cantidad» had room for ten digits. */
|
|
609
|
+
className: `border-b border-line-strong ${HEADER_TEXT} ${cell} ${c.numeric === true ? "text-right" : "text-left"} ${c.numeric === true && c.width === undefined ? "w-0" : ""}`, children: c.header }, c.id))), onRemove === undefined ? null : (_jsx("td", { className: "w-0 border-b border-line-strong" }))] }) }), _jsx("tbody", { ...(reorderable
|
|
606
610
|
? {
|
|
607
611
|
onDragOver: overRow,
|
|
608
612
|
onDrop: (event) => {
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { PileFoot } from "../../foundations/grouping.js";
|
|
2
|
+
/**
|
|
3
|
+
* The foot of a short pile, as words: «Show 50 more of 49,950», or «Showing 500 of 49,950»
|
|
4
|
+
* when there is no next page.
|
|
5
|
+
*
|
|
6
|
+
* Context: the table's row and the list's line both draw it, each in its own frame (a `td`
|
|
7
|
+
* with a `colSpan`, an indented line), so this is only what goes inside. The end is quiet
|
|
8
|
+
* text and not a disabled button: there is nothing to press, and a control that does
|
|
9
|
+
* nothing is the bug this replaced.
|
|
10
|
+
*/
|
|
11
|
+
export declare function PileFootLine({ foot, onMore }: {
|
|
12
|
+
foot: PileFoot;
|
|
13
|
+
onMore: () => void;
|
|
14
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { fillCopy, useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
/**
|
|
5
|
+
* The foot of a short pile, as words: «Show 50 more of 49,950», or «Showing 500 of 49,950»
|
|
6
|
+
* when there is no next page.
|
|
7
|
+
*
|
|
8
|
+
* Context: the table's row and the list's line both draw it, each in its own frame (a `td`
|
|
9
|
+
* with a `colSpan`, an indented line), so this is only what goes inside. The end is quiet
|
|
10
|
+
* text and not a disabled button: there is nothing to press, and a control that does
|
|
11
|
+
* nothing is the bug this replaced.
|
|
12
|
+
*/
|
|
13
|
+
export function PileFootLine({ foot, onMore }) {
|
|
14
|
+
const copy = useUiCopy();
|
|
15
|
+
const n = (value) => value.toLocaleString(copy.locale);
|
|
16
|
+
if (foot.kind === "end") {
|
|
17
|
+
return (_jsx("span", { className: "text-meta text-muted", children: fillCopy(copy.table.pileShown, { shown: n(foot.shown), count: n(foot.count) }) }));
|
|
18
|
+
}
|
|
19
|
+
return (_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: foot.next === undefined
|
|
20
|
+
? fillCopy(copy.table.pileMore, { count: n(foot.left) })
|
|
21
|
+
: fillCopy(copy.table.pileMoreOf, { count: n(foot.next), left: n(foot.left) }) }));
|
|
22
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { type Aggregate, type DataColumn, DataTable, type DataTableCreate, type GroupColumn, } from "./DataTable.js";
|
|
2
2
|
export { EditableTable, type EditAdd, type EditBands, type EditColumn, type EditLayout, sectionLines, type StackPlace, stackPlaces, type TabTarget, tabTarget, } from "./EditableTable.js";
|
|
3
|
+
export { PileFootLine } from "./PileFootLine.js";
|
|
3
4
|
export { PivotTable, type PivotTableProps, } from "./PivotTable.js";
|
|
4
5
|
export { type ReportColumn, type ReportRow, type ReportRowKind, ReportTable, } from "./ReportTable.js";
|
|
5
6
|
export { type RowAction, RowActions } from "./RowActions.js";
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
wiring to TanStack for grouping and sorting. See the note in `DataTable.tsx`. */
|
|
3
3
|
export { DataTable, } from "./DataTable.js";
|
|
4
4
|
export { EditableTable, sectionLines, stackPlaces, tabTarget, } from "./EditableTable.js";
|
|
5
|
+
export { PileFootLine } from "./PileFootLine.js";
|
|
5
6
|
export { PivotTable, } from "./PivotTable.js";
|
|
6
7
|
export { ReportTable, } from "./ReportTable.js";
|
|
7
8
|
export { RowActions } from "./RowActions.js";
|
package/dist/tokens.css
CHANGED
|
@@ -630,6 +630,28 @@
|
|
|
630
630
|
the corner of the eye. The bar uses it behind `motion-safe:`: with reduced
|
|
631
631
|
motion it stands still. */
|
|
632
632
|
--animate-progress-sweep: progress-sweep 1.6s var(--ease-in-out) infinite;
|
|
633
|
+
|
|
634
|
+
/* What appears only if waiting takes long enough to be seen: a skeleton. A read that
|
|
635
|
+
answers in 150ms showed a skeleton for one frame — a flash that says «something broke»
|
|
636
|
+
more than «loading». It waits 200ms invisible, then fades in. */
|
|
637
|
+
--animate-reveal-late: reveal var(--duration-quick) var(--ease-out) 200ms both;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
@keyframes reveal {
|
|
641
|
+
from {
|
|
642
|
+
opacity: 0;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* The focus ring ARRIVES: it closes in from a little farther out while it takes its colour,
|
|
647
|
+
in 160ms. A ring that pops is found; one that moves is found before the eye looks for it.
|
|
648
|
+
Only on the controls a person moves through — a dialog or a popover that takes the focus to
|
|
649
|
+
be announced keeps its own entrance — and only with motion allowed. */
|
|
650
|
+
@keyframes focus-in {
|
|
651
|
+
from {
|
|
652
|
+
outline-offset: 5px;
|
|
653
|
+
outline-color: transparent;
|
|
654
|
+
}
|
|
633
655
|
}
|
|
634
656
|
|
|
635
657
|
/* The segment is two fifths of its track: from just off the left edge (-100 % of
|
|
@@ -924,6 +946,26 @@
|
|
|
924
946
|
font-variant-numeric: tabular-nums;
|
|
925
947
|
}
|
|
926
948
|
|
|
949
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
950
|
+
:where(
|
|
951
|
+
button,
|
|
952
|
+
a[href],
|
|
953
|
+
input,
|
|
954
|
+
textarea,
|
|
955
|
+
select,
|
|
956
|
+
summary,
|
|
957
|
+
[role="tab"],
|
|
958
|
+
[role="option"],
|
|
959
|
+
[role="gridcell"],
|
|
960
|
+
[role="row"],
|
|
961
|
+
[role="checkbox"],
|
|
962
|
+
[role="radio"],
|
|
963
|
+
[role="switch"]
|
|
964
|
+
):focus-visible {
|
|
965
|
+
animation: focus-in var(--duration-quick) var(--ease-out);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
927
969
|
/* Thin and warm scrollbars. With six panels open, six grey system bars are
|
|
928
970
|
six stripes that say nothing. */
|
|
929
971
|
* {
|
|
@@ -6,7 +6,7 @@ import type { Surface } from "../../foundations/types.js";
|
|
|
6
6
|
import type { DataColumn, DataTableCreate } from "../../primitives/table/DataTable.js";
|
|
7
7
|
import type { RowAction } from "../../primitives/table/RowActions.js";
|
|
8
8
|
import type { SortState } from "../../primitives/table/Table.js";
|
|
9
|
-
import { type ListCalendar, type ListCrumb, type ListEmpty, type ListGantt, type ListGroup, type ListPivot, type ListPresent, type ListReport, type ListSet, type ListSheet, type ListTimeline, type RowOf } from "./types.js";
|
|
9
|
+
import { type CardWays, type ListCalendar, type ListCrumb, type ListEmpty, type ListGantt, type ListGroup, type ListPivot, type ListPresent, type ListReport, type ListSet, type ListSheet, type ListTimeline, type RowOf } from "./types.js";
|
|
10
10
|
import type { ReactNode } from "react";
|
|
11
11
|
type BarProps = {
|
|
12
12
|
children?: ReactNode;
|
|
@@ -51,7 +51,8 @@ type TableProps<T extends object> = {
|
|
|
51
51
|
* nombre, el `meta` y las fichas. Escribirla igual es declarar que ESTA lista
|
|
52
52
|
* se puede ver como tablero. */
|
|
53
53
|
type BoardProps<T> = {
|
|
54
|
-
|
|
54
|
+
/** `ways` carries the list's rule of what a press does — call `ways.press` from the card. */
|
|
55
|
+
card?: (item: T, ways: CardWays) => ReactNode;
|
|
55
56
|
/** One column per stage, with drag (and Alt + ← / →) to move a card between them. */
|
|
56
57
|
stages?: BoardStages<T>;
|
|
57
58
|
};
|
|
@@ -120,9 +121,16 @@ export type ListProps<T extends object> = {
|
|
|
120
121
|
* actions — a list to read, like the lines of a record in view mode. One switch, so whoever
|
|
121
122
|
* passes the handlers generically does not have to strip them one by one. */
|
|
122
123
|
readOnly?: boolean;
|
|
123
|
-
/** Los
|
|
124
|
+
/** Los estados del conjunto, en un solo lugar.
|
|
125
|
+
*
|
|
126
|
+
* `loading` es la PRIMERA lectura: no hay filas todavía, y se dibuja el esqueleto.
|
|
127
|
+
* `refreshing` es volver a preguntar con filas en la mano —se tipeó en la búsqueda, se
|
|
128
|
+
* cambió de página—: las filas se quedan y una barra discreta dice que se está
|
|
129
|
+
* preguntando. Cambiarlas por un esqueleto en cada pregunta hacía parpadear la tabla, y un
|
|
130
|
+
* clic podía caer sobre una fila que ya no estaba. */
|
|
124
131
|
state?: {
|
|
125
132
|
loading?: boolean;
|
|
133
|
+
refreshing?: boolean;
|
|
126
134
|
error?: string | null;
|
|
127
135
|
};
|
|
128
136
|
/** Vacía POR UN FILTRO es otra situación: no falta contenido, sobra un
|