@sincpro/web-ui 0.1.0 → 0.2.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/README.md +37 -10
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/{chatter → activity}/index.d.ts +1 -1
- package/dist/components/activity/index.js +4 -0
- package/dist/components/assist/AssistMark.d.ts +3 -3
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/assist.d.ts +1 -1
- package/dist/components/assist/assist.js +1 -1
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +11 -4
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/lines/LineItems.d.ts +42 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +41 -2
- package/dist/components/listing/ListingBar.js +43 -4
- package/dist/components/listing/Row.d.ts +5 -1
- package/dist/components/listing/Row.js +3 -2
- package/dist/components/record/RecordHeader.d.ts +21 -17
- package/dist/components/record/RecordHeader.js +46 -14
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/foundations/copy.d.ts +96 -12
- package/dist/foundations/copy.es.js +63 -10
- package/dist/foundations/copy.js +55 -7
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +2 -2
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/date/Calendar.js +30 -6
- package/dist/primitives/date/DatePicker.d.ts +11 -3
- package/dist/primitives/date/DatePicker.js +12 -8
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +1 -0
- package/dist/primitives/date/index.js +4 -2
- package/dist/primitives/dialog/Drill.d.ts +90 -0
- package/dist/primitives/dialog/Drill.js +172 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- package/dist/primitives/field/DropZone.js +1 -1
- package/dist/primitives/field/Field.d.ts +14 -2
- package/dist/primitives/field/Field.js +25 -14
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +2 -0
- package/dist/primitives/field/index.js +4 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/menu/Menu.d.ts +28 -0
- package/dist/primitives/menu/Menu.js +47 -17
- package/dist/primitives/notice/Callout.js +1 -1
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/index.d.ts +1 -0
- package/dist/primitives/notice/index.js +1 -0
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +60 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/Picker.d.ts +41 -0
- package/dist/primitives/picker/Picker.js +62 -8
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/DataTable.d.ts +55 -1
- package/dist/primitives/table/DataTable.js +126 -40
- package/dist/primitives/table/EditableTable.d.ts +97 -2
- package/dist/primitives/table/EditableTable.js +223 -29
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/index.d.ts +2 -2
- package/dist/primitives/table/index.js +1 -1
- package/dist/primitives/tabs/Tabs.d.ts +2 -2
- package/dist/primitives/tabs/Tabs.js +1 -1
- package/dist/primitives/tag/TagsField.d.ts +3 -0
- package/dist/primitives/tag/TagsField.js +6 -2
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Copyable.d.ts +11 -0
- package/dist/primitives/value/Copyable.js +27 -6
- package/dist/primitives/value/Fecha.js +10 -1
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +6 -1
- package/dist/primitives/value/Relation.js +27 -10
- package/dist/primitives/value/Totals.d.ts +102 -7
- package/dist/primitives/value/Totals.js +52 -2
- package/dist/primitives/value/Value.d.ts +0 -37
- package/dist/primitives/value/Value.js +0 -39
- package/dist/primitives/value/index.d.ts +4 -3
- package/dist/primitives/value/index.js +4 -3
- package/dist/tokens.css +44 -7
- package/dist/views/list/List.d.ts +8 -2
- package/dist/views/list/List.js +6 -2
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +34 -8
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +2 -2
- package/dist/views/list/presentations.js +5 -3
- package/dist/views/list/types.d.ts +13 -1
- package/dist/views/record/Record.d.ts +27 -8
- package/dist/views/record/Record.js +97 -27
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +5 -1
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.js +0 -4
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { Fragment as _Fragment, jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { Flag } from "../notice/Flag.js";
|
|
4
|
+
import { createContext, useContext, useLayoutEffect, useRef, useSyncExternalStore, } from "react";
|
|
5
|
+
export function createNoteRegistry() {
|
|
6
|
+
const entries = new Map();
|
|
7
|
+
const listeners = new Set();
|
|
8
|
+
let list = [];
|
|
9
|
+
const changed = () => {
|
|
10
|
+
list = [...entries.values()];
|
|
11
|
+
for (const listener of listeners)
|
|
12
|
+
listener();
|
|
13
|
+
};
|
|
14
|
+
return {
|
|
15
|
+
put: (entry) => {
|
|
16
|
+
const known = entries.get(entry.id);
|
|
17
|
+
if (known === undefined) {
|
|
18
|
+
entries.set(entry.id, entry);
|
|
19
|
+
changed();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
/* In place: the snapshot keeps its identity, so `useSyncExternalStore` sees no change
|
|
23
|
+
— a new array here re-rendered the record, which re-rendered the field, which put
|
|
24
|
+
again, for ever. */
|
|
25
|
+
Object.assign(known, entry);
|
|
26
|
+
},
|
|
27
|
+
drop: (id) => {
|
|
28
|
+
if (entries.delete(id))
|
|
29
|
+
changed();
|
|
30
|
+
},
|
|
31
|
+
subscribe: (listener) => {
|
|
32
|
+
listeners.add(listener);
|
|
33
|
+
return () => listeners.delete(listener);
|
|
34
|
+
},
|
|
35
|
+
snapshot: () => list,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** The registry of the record around — `Record` provides it. */
|
|
39
|
+
export const NoteRegistryContext = createContext(null);
|
|
40
|
+
const NONE = [];
|
|
41
|
+
const noop = () => () => { };
|
|
42
|
+
/** The notes mounted in the record around, in the order they were mounted. */
|
|
43
|
+
export function useNoteEntries() {
|
|
44
|
+
const registry = useContext(NoteRegistryContext);
|
|
45
|
+
const snapshot = registry?.snapshot ?? (() => NONE);
|
|
46
|
+
/* The same snapshot on the server: nothing registers there (no effects run), so it is
|
|
47
|
+
empty unless someone filled the registry by hand. */
|
|
48
|
+
return useSyncExternalStore(registry?.subscribe ?? noop, snapshot, snapshot);
|
|
49
|
+
}
|
|
50
|
+
/** What a card belongs to when nobody said: the row it sits in, by the first words of its
|
|
51
|
+
* first cell — «Impresora térmica de tickets 80 mm» —, or nothing. The card's own words
|
|
52
|
+
* (it sits in that cell) are skipped. */
|
|
53
|
+
function ownerFromRow(card) {
|
|
54
|
+
const cell = card.closest("tr")?.querySelector("td");
|
|
55
|
+
if (cell === null || cell === undefined)
|
|
56
|
+
return "";
|
|
57
|
+
const walk = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT);
|
|
58
|
+
for (let node = walk.nextNode(); node !== null; node = walk.nextNode()) {
|
|
59
|
+
if (node.parentElement?.closest("[data-field-note]") !== null)
|
|
60
|
+
continue;
|
|
61
|
+
const text = node.textContent?.trim() ?? "";
|
|
62
|
+
if (text !== "")
|
|
63
|
+
return text.slice(0, 60);
|
|
64
|
+
}
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
/** The field or row a card is anchored to: the row, the fact (a `<dd>`'s group), or the
|
|
68
|
+
* field box the card is the last child of. */
|
|
69
|
+
function anchorOf(card) {
|
|
70
|
+
if (card === null)
|
|
71
|
+
return null;
|
|
72
|
+
const row = card.closest("tr");
|
|
73
|
+
if (row !== null)
|
|
74
|
+
return row;
|
|
75
|
+
const parent = card.parentElement;
|
|
76
|
+
return parent?.tagName === "DD" ? parent.parentElement : parent;
|
|
77
|
+
}
|
|
78
|
+
/* ---------------------------------------------------------------- *
|
|
79
|
+
* The two faces
|
|
80
|
+
* ---------------------------------------------------------------- */
|
|
81
|
+
/** What a note leaves in its field: its sentence, hidden, for the field's
|
|
82
|
+
* `aria-describedby`, and its entry in the record's activity — where it is listed with
|
|
83
|
+
* what it belongs to and taken back to on press. Nothing is drawn: the mark by the label
|
|
84
|
+
* (`NoteFlag`) says WHERE, the activity says WHAT. */
|
|
85
|
+
export function NoteCard({ note, id, owner, }) {
|
|
86
|
+
const registry = useContext(NoteRegistryContext);
|
|
87
|
+
const ref = useRef(null);
|
|
88
|
+
/* Before paint, so the record's activity knows on its first frame whether there is
|
|
89
|
+
anything to align — its default view depends on it. On EVERY render: the words are
|
|
90
|
+
kept current, silently (a new `note` object each render must not re-render the
|
|
91
|
+
record); only appearing and going away are news. */
|
|
92
|
+
useLayoutEffect(() => {
|
|
93
|
+
if (registry === null)
|
|
94
|
+
return;
|
|
95
|
+
const card = ref.current;
|
|
96
|
+
registry.put({
|
|
97
|
+
id,
|
|
98
|
+
note,
|
|
99
|
+
owner: owner ?? (card === null ? "" : ownerFromRow(card)),
|
|
100
|
+
anchor: () => anchorOf(ref.current),
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
useLayoutEffect(() => {
|
|
104
|
+
if (registry === null)
|
|
105
|
+
return;
|
|
106
|
+
return () => registry.drop(id);
|
|
107
|
+
}, [registry, id]);
|
|
108
|
+
return (_jsx("div", { ref: ref, "data-field-note": "", hidden: true, children: _jsxs("span", { id: id, children: [note.label, note.detail === undefined ? null : _jsxs(_Fragment, { children: [" \u2014 ", note.detail] })] }) }));
|
|
109
|
+
}
|
|
110
|
+
/** The mark next to the label: WHERE the note is. It opens the note on press. */
|
|
111
|
+
export function NoteFlag({ note }) {
|
|
112
|
+
return (_jsx("span", { className: "inline-flex shrink-0", children: _jsx(Flag, { tone: note.tone ?? "info", label: note.label, children: note.detail === undefined && note.action === undefined ? undefined : (_jsxs("span", { className: "flex flex-col gap-2", children: [note.detail === undefined ? null : _jsx("span", { children: note.detail }), note.action === undefined ? null : _jsx("span", { className: "flex", children: note.action })] })) }) }));
|
|
113
|
+
}
|
|
114
|
+
/** Joins ids for `aria-describedby`, dropping the absent ones. */
|
|
115
|
+
export function describedByOf(...ids) {
|
|
116
|
+
return ids.filter((x) => x !== undefined && x !== null).join(" ") || undefined;
|
|
117
|
+
}
|
package/dist/primitives/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ButtonRole, type ButtonSize } from "../button/Button.js";
|
|
2
2
|
import type { LucideIcon } from "lucide-react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import { type Key } from "react-aria-components";
|
|
4
5
|
export interface MenuOption {
|
|
5
6
|
id: string;
|
|
6
7
|
label: string;
|
|
@@ -27,6 +28,16 @@ export interface MenuOption {
|
|
|
27
28
|
* menu built from a registry —twenty-five operations, each one with its own
|
|
28
29
|
* action— has no reasonable `switch` on the other side. */
|
|
29
30
|
onSelect?: () => void;
|
|
31
|
+
/** The keys that run this option without opening the menu, as the user reads them
|
|
32
|
+
* —«Ctrl+P», «⌘D»—. Drawn at the end of the row and linked to it for a screen reader.
|
|
33
|
+
* It is a HINT: binding the keys is the application's job, because the menu does not
|
|
34
|
+
* exist until it is opened. In an ERP the frequent verbs have a shortcut, and the menu
|
|
35
|
+
* is where people learn it. */
|
|
36
|
+
shortcut?: string;
|
|
37
|
+
/** The option opens a SUBMENU with these options instead of running. Its own `onSelect`
|
|
38
|
+
* and `checked` are ignored — a row that opens something does not also do something.
|
|
39
|
+
* Children may carry `items` too, but a third level is usually a dialog in disguise. */
|
|
40
|
+
items?: readonly MenuOption[];
|
|
30
41
|
}
|
|
31
42
|
export interface MenuGroup {
|
|
32
43
|
/** The group's label, in the brand voice. Without a label, the group is just
|
|
@@ -51,6 +62,9 @@ declare const WIDTH: {
|
|
|
51
62
|
readonly md: "min-w-56 max-w-80";
|
|
52
63
|
readonly lg: "min-w-80 max-w-(--container-short)";
|
|
53
64
|
};
|
|
65
|
+
/** Every option of the tree by id, children included: a submenu is a separate React Aria
|
|
66
|
+
* menu with its own `onAction`, and both have to reach the same handlers. */
|
|
67
|
+
export declare function indexOptions(options: readonly MenuOption[], into?: Map<string, MenuOption>): Map<string, MenuOption>;
|
|
54
68
|
export declare function Menu({ label, groups, onSelect, surface, align, icon: Trigger,
|
|
55
69
|
/** The menu as a control of a list ("filters ▾") carries the chevron; the
|
|
56
70
|
* actions `⋯` does not, because the icon already says there is more. */
|
|
@@ -97,4 +111,18 @@ chevron, active, iconOnly, variant, searchPlaceholder, emptyText, width, role, s
|
|
|
97
111
|
* each click turns three hides into three open-close round trips. */
|
|
98
112
|
closeOnSelect?: boolean;
|
|
99
113
|
}): import("react").JSX.Element;
|
|
114
|
+
/**
|
|
115
|
+
* The panel of a `Menu`: its groups and rows, and the submenus hanging from them. Internal —
|
|
116
|
+
* it is not exported from the family — and separate from the trigger for two reasons: a
|
|
117
|
+
* submenu is this same panel one level down, and a panel can be rendered on the server
|
|
118
|
+
* without the trigger's popover, which is how the tests see the rows.
|
|
119
|
+
*/
|
|
120
|
+
export declare function MenuList({ label, groups, onAction, closeOnSelect, emptyText, width, }: {
|
|
121
|
+
label: string;
|
|
122
|
+
groups: readonly MenuGroup[];
|
|
123
|
+
onAction: (key: Key) => void;
|
|
124
|
+
closeOnSelect: boolean;
|
|
125
|
+
emptyText: string;
|
|
126
|
+
width?: keyof typeof WIDTH;
|
|
127
|
+
}): import("react").JSX.Element;
|
|
100
128
|
export {};
|
|
@@ -3,14 +3,29 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
4
|
import { buttonClasses, } from "../button/Button.js";
|
|
5
5
|
import { Popover } from "../overlay/Popover.js";
|
|
6
|
-
import { Check, ChevronDown } from "lucide-react";
|
|
7
|
-
import { Autocomplete, Button as AriaButton, Header, Input, Menu as AriaMenu, MenuItem, MenuSection, MenuTrigger, SearchField, Separator, useFilter, } from "react-aria-components";
|
|
6
|
+
import { Check, ChevronDown, ChevronRight } from "lucide-react";
|
|
7
|
+
import { Autocomplete, Button as AriaButton, Header, Input, Keyboard, Menu as AriaMenu, MenuItem, MenuSection, MenuTrigger, SearchField, Separator, SubmenuTrigger, useFilter, } from "react-aria-components";
|
|
8
8
|
const WIDTH = {
|
|
9
9
|
/** A menu of short verbs hanging off an icon: "duplicate", "delete". */
|
|
10
10
|
sm: "min-w-44 max-w-64",
|
|
11
11
|
md: "min-w-56 max-w-80",
|
|
12
12
|
lg: "min-w-80 max-w-(--container-short)",
|
|
13
13
|
};
|
|
14
|
+
/** A submenu opens beside its row, flush against the parent panel and pulled up by the
|
|
15
|
+
* panel's padding plus its hairline (`p-1` + 1), so its first row lines up with the row that
|
|
16
|
+
* opened it — a submenu that starts half a row lower reads as belonging to the next one. */
|
|
17
|
+
const SUBMENU_OFFSET = 0;
|
|
18
|
+
const SUBMENU_CROSS_OFFSET = -5;
|
|
19
|
+
/** Every option of the tree by id, children included: a submenu is a separate React Aria
|
|
20
|
+
* menu with its own `onAction`, and both have to reach the same handlers. */
|
|
21
|
+
export function indexOptions(options, into = new Map()) {
|
|
22
|
+
for (const o of options) {
|
|
23
|
+
into.set(o.id, o);
|
|
24
|
+
if (o.items !== undefined)
|
|
25
|
+
indexOptions(o.items, into);
|
|
26
|
+
}
|
|
27
|
+
return into;
|
|
28
|
+
}
|
|
14
29
|
export function Menu({ label, groups, onSelect, surface = "canvas", align = "start", icon: Trigger,
|
|
15
30
|
/** The menu as a control of a list ("filters ▾") carries the chevron; the
|
|
16
31
|
* actions `⋯` does not, because the icon already says there is more. */
|
|
@@ -29,18 +44,41 @@ chevron = true, active = false, iconOnly = false, variant = "button", searchPlac
|
|
|
29
44
|
: surface === "chrome"
|
|
30
45
|
? `border-chrome-muted/35 bg-chrome-raised text-chrome-ink hover:border-chrome-accent/45 ${active ? "border-chrome-accent/60" : ""}`
|
|
31
46
|
: `border-line-strong bg-surface hover:border-primary/45 hover:bg-primary/5 ${active ? "border-primary/60 text-primary" : "text-ink"}`;
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
const byId = new Map(groups.flatMap((g) => g.options.map((o) => [o.id, o])));
|
|
35
|
-
const menu = (_jsx(AriaMenu, { "aria-label": label, shouldCloseOnSelect: closeOnSelect, onAction: (key) => {
|
|
47
|
+
const byId = indexOptions(groups.flatMap((g) => g.options));
|
|
48
|
+
const menu = (_jsx(MenuList, { label: label, groups: groups, width: width, closeOnSelect: closeOnSelect, onAction: (key) => {
|
|
36
49
|
const option = byId.get(String(key));
|
|
37
50
|
option?.onSelect?.();
|
|
38
51
|
onSelect?.(String(key));
|
|
39
|
-
},
|
|
52
|
+
}, emptyText: emptyText ?? copy.menu.empty }));
|
|
53
|
+
return (_jsxs(MenuTrigger, { children: [_jsx(AriaButton, { "aria-label": iconOnly ? label : undefined, ...(role === undefined ? {} : { "data-emphasis": role }), className: ({ isPressed, isFocusVisible }) => role === undefined
|
|
54
|
+
? `inline-flex h-(--size-control-sm) shrink-0 cursor-default items-center gap-1.5 border text-micro font-medium whitespace-nowrap outline-0 transition-colors duration-(--duration-instant) ${isFocusVisible ? "outline-2 outline-offset-2 outline-primary" : ""} ${iconOnly ? "w-(--size-control-sm) justify-center rounded-pill" : "rounded-control px-2.5"} ${triggerClass(isPressed)}`
|
|
55
|
+
: `${buttonClasses(role, size, surface)} gap-1.5`, children: _jsxs("span", { className: "contents", title: title ?? (iconOnly ? label : undefined), children: [Trigger === undefined ? (iconOnly ? (_jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })) : null) : (_jsx(Trigger, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })), iconOnly ? null : label, chevron && !iconOnly ? (_jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })) : null] }) }), _jsx(Popover, { placement: align === "end" ? "bottom end" : "bottom start", className: WIDTH[width], children: searchPlaceholder === undefined ? (menu) : (
|
|
56
|
+
/* The search box stays at the top: in a menu of twenty-five entries,
|
|
57
|
+
scrolling loses exactly the control that explains why only three are
|
|
58
|
+
visible. It filters on label AND hint — half the entries of this
|
|
59
|
+
product are told apart only by the hint. */
|
|
60
|
+
_jsxs(Autocomplete, { filter: contains, children: [_jsx(SearchField, { "aria-label": searchPlaceholder, autoFocus: true, className: "border-b border-line px-2 pt-2 pb-2", children: _jsx(Input, { placeholder: searchPlaceholder, className: "h-(--size-control-sm) w-full rounded-control border border-line-strong bg-surface px-2.5 text-meta text-ink outline-0 placeholder:text-muted focus-visible:border-primary" }) }), menu] })) })] }));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The panel of a `Menu`: its groups and rows, and the submenus hanging from them. Internal —
|
|
64
|
+
* it is not exported from the family — and separate from the trigger for two reasons: a
|
|
65
|
+
* submenu is this same panel one level down, and a panel can be rendered on the server
|
|
66
|
+
* without the trigger's popover, which is how the tests see the rows.
|
|
67
|
+
*/
|
|
68
|
+
export function MenuList({ label, groups, onAction, closeOnSelect, emptyText, width = "md", }) {
|
|
69
|
+
/* Per panel: a submenu without icons does not inherit its parent's mark column. */
|
|
70
|
+
const hasMarks = groups.some((g) => g.options.some((o) => o.checked !== undefined || o.icon !== undefined));
|
|
71
|
+
const hasHints = groups.some((g) => g.options.some((o) => o.hint !== undefined));
|
|
72
|
+
const row = (o) => {
|
|
73
|
+
const Icon = o.icon;
|
|
74
|
+
const opens = o.items !== undefined;
|
|
75
|
+
const checked = !opens && o.checked === true;
|
|
76
|
+
return (_jsxs(MenuItem, { id: o.id, textValue: o.hint === undefined ? o.label : `${o.label} ${o.hint}`, isDisabled: o.disabled, className: `flex w-full cursor-default rounded-control ${hasHints ? "items-start py-1.5" : "items-center py-1"} gap-2 px-2 text-left outline-0 transition-colors duration-(--duration-instant) data-focused:bg-primary/8 data-open:bg-primary/8 disabled:opacity-45`, children: [hasMarks ? (_jsx("span", { className: `${hasHints ? "mt-0.5" : ""} flex size-(--size-icon-sm) shrink-0 items-center justify-center`, children: checked ? (_jsx(Check, { className: "size-(--size-icon-sm) text-primary", strokeWidth: 2.5, "aria-hidden": true })) : Icon !== undefined ? (_jsx(Icon, { className: "size-(--size-icon-sm) text-muted", strokeWidth: 2, "aria-hidden": true })) : null })) : null, _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: `truncate text-meta ${checked ? "font-medium text-ink" : "text-ink"}`, children: o.label }), o.badge === undefined ? null : typeof o.badge === "string" ? (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-warn/30 bg-warn/10 px-2 py-0.5 text-micro text-warn", children: o.badge })) : (_jsx("span", { className: "ml-auto shrink-0", children: o.badge }))] }), o.hint === undefined ? null : (_jsx("span", { className: "mt-0.5 block truncate text-micro text-muted", children: o.hint }))] }), o.shortcut === undefined ? null : (_jsx(Keyboard, { className: "shrink-0 self-center pl-3 font-mono text-micro text-muted", children: o.shortcut })), opens ? (_jsx(ChevronRight, { className: "size-(--size-icon-sm) shrink-0 self-center text-muted", strokeWidth: 2, "aria-hidden": true })) : null] }, o.id));
|
|
77
|
+
};
|
|
78
|
+
return (_jsx(AriaMenu, { "aria-label": label, shouldCloseOnSelect: closeOnSelect, onAction: onAction, renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-meta text-muted italic", children: emptyText })), className: "max-h-[inherit] overflow-y-auto p-1 outline-0", children: groups.map((g, gi) => (_jsxs(MenuSection, { className: "[&:not(:first-child)]:mt-1 [&:not(:first-child)]:border-t [&:not(:first-child)]:border-line [&:not(:first-child)]:pt-1", children: [g.label === undefined ? null : (
|
|
40
79
|
/* Same skeleton as the row: the label sits on the text column. */
|
|
41
80
|
_jsxs(Header, { className: "flex items-center gap-2 px-2 pt-1 pb-0.5 text-micro font-medium text-muted", children: [hasMarks ? (_jsx("span", { className: "size-(--size-icon-sm) shrink-0", "aria-hidden": true })) : null, g.label] })), g.options.flatMap((o) => {
|
|
42
|
-
const
|
|
43
|
-
const item = (_jsxs(MenuItem, { id: o.id, textValue: o.hint === undefined ? o.label : `${o.label} ${o.hint}`, isDisabled: o.disabled, className: `flex w-full cursor-default rounded-control ${hasHints ? "items-start py-1.5" : "items-center py-1"} gap-2 px-2 text-left outline-0 transition-colors duration-(--duration-instant) data-focused:bg-primary/8 disabled:opacity-45`, children: [hasMarks ? (_jsx("span", { className: `${hasHints ? "mt-0.5" : ""} flex size-(--size-icon-sm) shrink-0 items-center justify-center`, children: o.checked === true ? (_jsx(Check, { className: "size-(--size-icon-sm) text-primary", strokeWidth: 2.5, "aria-hidden": true })) : Icon !== undefined ? (_jsx(Icon, { className: "size-(--size-icon-sm) text-muted", strokeWidth: 2, "aria-hidden": true })) : null })) : null, _jsxs("span", { className: "min-w-0 flex-1", children: [_jsxs("span", { className: "flex items-center gap-1.5", children: [_jsx("span", { className: `truncate text-meta ${o.checked === true ? "font-medium text-ink" : "text-ink"}`, children: o.label }), o.badge === undefined ? null : typeof o.badge === "string" ? (_jsx("span", { className: "inline-flex shrink-0 items-center rounded-pill border border-warn/30 bg-warn/10 px-2 py-0.5 text-micro text-warn", children: o.badge })) : (_jsx("span", { className: "ml-auto shrink-0", children: o.badge }))] }), o.hint === undefined ? null : (_jsx("span", { className: "mt-0.5 block truncate text-micro text-muted", children: o.hint }))] })] }, o.id));
|
|
81
|
+
const item = o.items === undefined ? (row(o)) : (_jsxs(SubmenuTrigger, { children: [row(o), _jsx(Popover, { offset: SUBMENU_OFFSET, crossOffset: SUBMENU_CROSS_OFFSET, className: WIDTH[width], children: _jsx(MenuList, { label: o.label, groups: [{ options: [...o.items] }], onAction: onAction, closeOnSelect: closeOnSelect, emptyText: emptyText, width: width }) })] }, o.id));
|
|
44
82
|
return o.divider === true
|
|
45
83
|
? [
|
|
46
84
|
_jsx(Separator, { className: "my-1 h-px border-0 bg-line" }, `${o.id}-divider`),
|
|
@@ -48,12 +86,4 @@ chevron = true, active = false, iconOnly = false, variant = "button", searchPlac
|
|
|
48
86
|
]
|
|
49
87
|
: [item];
|
|
50
88
|
})] }, g.label ?? gi))) }));
|
|
51
|
-
return (_jsxs(MenuTrigger, { children: [_jsx(AriaButton, { "aria-label": iconOnly ? label : undefined, ...(role === undefined ? {} : { "data-emphasis": role }), className: ({ isPressed, isFocusVisible }) => role === undefined
|
|
52
|
-
? `inline-flex h-(--size-control-sm) shrink-0 cursor-default items-center gap-1.5 border text-micro font-medium whitespace-nowrap outline-0 transition-colors duration-(--duration-instant) ${isFocusVisible ? "outline-2 outline-offset-2 outline-primary" : ""} ${iconOnly ? "w-(--size-control-sm) justify-center rounded-pill" : "rounded-control px-2.5"} ${triggerClass(isPressed)}`
|
|
53
|
-
: `${buttonClasses(role, size, surface)} gap-1.5`, children: _jsxs("span", { className: "contents", title: title ?? (iconOnly ? label : undefined), children: [Trigger === undefined ? (iconOnly ? (_jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })) : null) : (_jsx(Trigger, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })), iconOnly ? null : label, chevron && !iconOnly ? (_jsx(ChevronDown, { className: "size-(--size-icon-sm)", strokeWidth: 2, "aria-hidden": true })) : null] }) }), _jsx(Popover, { placement: align === "end" ? "bottom end" : "bottom start", className: WIDTH[width], children: searchPlaceholder === undefined ? (menu) : (
|
|
54
|
-
/* The search box stays at the top: in a menu of twenty-five entries,
|
|
55
|
-
scrolling loses exactly the control that explains why only three are
|
|
56
|
-
visible. It filters on label AND hint — half the entries of this
|
|
57
|
-
product are told apart only by the hint. */
|
|
58
|
-
_jsxs(Autocomplete, { filter: contains, children: [_jsx(SearchField, { "aria-label": searchPlaceholder, autoFocus: true, className: "border-b border-line px-2 pt-2 pb-2", children: _jsx(Input, { placeholder: searchPlaceholder, className: "h-(--size-control-sm) w-full rounded-control border border-line-strong bg-surface px-2.5 text-meta text-ink outline-0 placeholder:text-muted focus-visible:border-primary" }) }), menu] })) })] }));
|
|
59
89
|
}
|
|
@@ -24,5 +24,5 @@ export function Callout({ children, tone = "info", title, action, onDismiss, dis
|
|
|
24
24
|
/* `status` y no `alert`: `alert` interrumpe al lector de pantalla y se
|
|
25
25
|
reserva para lo que pasó JUSTO AHORA. Un aviso que ya estaba cuando la
|
|
26
26
|
pantalla abrió no interrumpe nada. */
|
|
27
|
-
role: "status", className: `flex min-w-0 items-start gap-2.5 rounded-box border ${SKIN[tone]} ${byDensity(d, "px-3 py-2.5", "px-2.5 py-2")}`, children: [_jsx(Icon, { className: "mt-0.5 size-(--size-icon-sm) shrink-0", strokeWidth: 2, "aria-hidden": true }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [title === undefined ? null : (_jsx("p", { className: "text-meta font-medium text-ink", children: title })), _jsx("div", { className: "min-w-0 text-meta text-ink", children: children })] }), action === undefined ? null : _jsx("span", { className: "shrink-0", children: action }), onDismiss === undefined ? null : (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": dismissLabel ?? "cerrar", title: dismissLabel ?? "cerrar", className: "-mr-1 shrink-0 rounded-control p-0.5 text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink", children: _jsx(X, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) }))] }));
|
|
27
|
+
role: "status", className: `flex min-w-0 items-start gap-2.5 rounded-box border ${SKIN[tone]} ${byDensity(d, "px-3 py-2.5", "px-2.5 py-2")}`, children: [_jsx(Icon, { className: "mt-0.5 size-(--size-icon-sm) shrink-0", strokeWidth: 2, "aria-hidden": true }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-wrap items-start gap-x-3 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 flex-1 basis-(--container-name) flex-col gap-1", children: [title === undefined ? null : (_jsx("p", { className: "text-meta font-medium text-ink", children: title })), _jsx("div", { className: "min-w-0 text-meta text-ink", children: children })] }), action === undefined ? null : _jsx("span", { className: "shrink-0", children: action })] }), onDismiss === undefined ? null : (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": dismissLabel ?? "cerrar", title: dismissLabel ?? "cerrar", className: "-mr-1 shrink-0 rounded-control p-0.5 text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink", children: _jsx(X, { className: "size-(--size-icon-sm)", strokeWidth: 2 }) }))] }));
|
|
28
28
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Tone } from "../../foundations/types.js";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type FlagTone = Extract<Tone, "bad" | "warn" | "info" | "good">;
|
|
4
|
+
export type FlagProps = {
|
|
5
|
+
tone: FlagTone;
|
|
6
|
+
/** The sentence the mark stands for — «Sin existencias». It names the mark and heads
|
|
7
|
+
* the popover. */
|
|
8
|
+
label: string;
|
|
9
|
+
/** What explains it, inside the popover: the figures, what to do. */
|
|
10
|
+
children?: ReactNode;
|
|
11
|
+
};
|
|
12
|
+
/** A toned mark that opens its sentence. See the file's header. */
|
|
13
|
+
export declare function Flag({ tone, label, children }: FlagProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { tv } from "../../foundations/variants.js";
|
|
4
|
+
import { Popover } from "../overlay/Popover.js";
|
|
5
|
+
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react";
|
|
6
|
+
import { Button, Dialog, DialogTrigger, Heading } from "react-aria-components";
|
|
7
|
+
const ICON = {
|
|
8
|
+
bad: CircleAlert,
|
|
9
|
+
warn: TriangleAlert,
|
|
10
|
+
info: Info,
|
|
11
|
+
good: CircleCheck,
|
|
12
|
+
};
|
|
13
|
+
const mark = tv({
|
|
14
|
+
base: "inline-flex size-5 shrink-0 cursor-default items-center justify-center rounded-pill outline-0 transition-colors duration-(--duration-instant) forced-colors:text-[ButtonText]",
|
|
15
|
+
variants: {
|
|
16
|
+
tone: {
|
|
17
|
+
bad: "bg-bad/12 text-bad hover:bg-bad/20 aria-expanded:bg-bad/20",
|
|
18
|
+
warn: "bg-warn/15 text-warn hover:bg-warn/25 aria-expanded:bg-warn/25",
|
|
19
|
+
info: "bg-info/12 text-info hover:bg-info/20 aria-expanded:bg-info/20",
|
|
20
|
+
good: "bg-good/12 text-good hover:bg-good/20 aria-expanded:bg-good/20",
|
|
21
|
+
},
|
|
22
|
+
isFocusVisible: {
|
|
23
|
+
true: "outline-2 outline-offset-1 outline-primary forced-colors:outline-[Highlight]",
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
/** A toned mark that opens its sentence. See the file's header. */
|
|
28
|
+
export function Flag({ tone, label, children }) {
|
|
29
|
+
const Icon = ICON[tone];
|
|
30
|
+
return (_jsxs(DialogTrigger, { children: [_jsx("span", { className: "inline-flex shrink-0", onClick: (e) => e.stopPropagation(), children: _jsx(Button, { "aria-label": label, className: (state) => mark({ ...state, tone }), children: _jsx(Icon, { className: "size-(--size-icon-sm)", strokeWidth: 2.25, "aria-hidden": true }) }) }), _jsx(Popover, { placement: "bottom start", className: "max-w-72 rounded-box", children: _jsxs(Dialog, { className: "flex flex-col gap-1 px-3 py-2.5 outline-0", children: [_jsx(Heading, { slot: "title", className: "text-meta font-medium text-ink", children: label }), children === undefined ? null : (_jsx("div", { className: "text-micro leading-relaxed text-muted", children: children }))] }) })] }));
|
|
31
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Callout, type CalloutProps } from "./Callout.js";
|
|
2
|
+
export { Flag, type FlagProps, type FlagTone } from "./Flag.js";
|
|
2
3
|
export { Empty, Failed, ICONO_ROTO, Missing, Skeleton, } from "./Notice.js";
|
|
3
4
|
export { ToastLayer, ToastProvider, type ToastSpec, useToast, useToastIfAny, } from "./Toast.js";
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/* How a view says that there is nothing, that it failed, or that nobody knows
|
|
2
2
|
how to draw it. */
|
|
3
3
|
export { Callout } from "./Callout.js";
|
|
4
|
+
export { Flag } from "./Flag.js";
|
|
4
5
|
export { Empty, Failed, ICONO_ROTO, Missing, Skeleton, } from "./Notice.js";
|
|
5
6
|
export { ToastLayer, ToastProvider, useToast, useToastIfAny, } from "./Toast.js";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type PopoverProps } from "./Popover.js";
|
|
2
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
3
|
+
export type PreviewProps = {
|
|
4
|
+
/** What the preview is of — a link to the record, written as an element: a React Aria
|
|
5
|
+
* `Link`, or anything that forwards `ref` and spreads native props. It must be
|
|
6
|
+
* focusable: the keyboard reaches the preview through it. */
|
|
7
|
+
trigger: ReactElement;
|
|
8
|
+
/** The accessible name of the floating card: «Resumen de Distribuidora Andina». */
|
|
9
|
+
label: string;
|
|
10
|
+
/** What floats: a `RelationCard`, a few facts. It may hold actions — «Abrir». */
|
|
11
|
+
children: ReactNode;
|
|
12
|
+
placement?: PopoverProps["placement"];
|
|
13
|
+
/** Milliseconds resting on the trigger before it opens. 600 by default — see above. */
|
|
14
|
+
delay?: number;
|
|
15
|
+
closeDelay?: number;
|
|
16
|
+
/** The trigger stays, the preview does not open. */
|
|
17
|
+
disabled?: boolean;
|
|
18
|
+
};
|
|
19
|
+
export declare function Preview({ trigger, label, children, placement, delay, closeDelay, disabled, }: PreviewProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Popover } from "./Popover.js";
|
|
4
|
+
import { isValidElement } from "react";
|
|
5
|
+
import { Focusable, Link, PreviewTrigger } from "react-aria-components";
|
|
6
|
+
/**
|
|
7
|
+
* A glance at a record without leaving the screen: the customer of an invoice, the product
|
|
8
|
+
* of a line — who or what it is, shown when the pointer rests on its name, the keyboard
|
|
9
|
+
* stops on it or a finger presses it long.
|
|
10
|
+
*
|
|
11
|
+
* Context: it sits between the two other things that float from a mark.
|
|
12
|
+
*
|
|
13
|
+
* - `Tooltip` is a LABEL: a few words about a control, never interactive — React Aria makes
|
|
14
|
+
* it a `role="tooltip"` the keyboard cannot enter. A customer's debt, phone and «Abrir»
|
|
15
|
+
* are not a label.
|
|
16
|
+
* - `PopoverTrigger` is ASKED FOR: a press on a button opens a dialog that takes the focus
|
|
17
|
+
* until it is closed. Right for a small form; too much ceremony to see whose NIT it is.
|
|
18
|
+
*
|
|
19
|
+
* `Preview` is React Aria's `PreviewTrigger`: a NON-modal popover (`role="dialog"`, named by
|
|
20
|
+
* `label`) that opens on hover, on keyboard focus and on long press, and that MAY hold
|
|
21
|
+
* interactive content. It does not take the focus when it opens — the person who tabs past
|
|
22
|
+
* a column of customers keeps walking the table — but Tab from the trigger goes into it,
|
|
23
|
+
* Escape closes it and returns to the trigger, and the pointer can travel diagonally from the
|
|
24
|
+
* name to the card without it closing (React Aria's safe area). A long press moves the focus
|
|
25
|
+
* into it, so a touch screen reader lands on the card. All of that is React Aria's; this
|
|
26
|
+
* only floats it on the system's `Popover` — the surface, hairline, radius and `float`
|
|
27
|
+
* shadow everything that opens over the page has.
|
|
28
|
+
*
|
|
29
|
+
* ── The delays: 600 ms to open, 200 ms to close ──────────────────────
|
|
30
|
+
*
|
|
31
|
+
* They are React Aria's defaults, written here so a change in the library does not change
|
|
32
|
+
* the system silently. 600 ms is the line between crossing and resting: in a table the
|
|
33
|
+
* pointer crosses customer names all day on its way to something else, and a card of 18rem
|
|
34
|
+
* that covered four rows on every pass would be noise. Below ~400 ms it opens on the way;
|
|
35
|
+
* above a second it feels broken to the person who did stop to look. Keyboard focus waits the
|
|
36
|
+
* same, so tabbing quickly does not open a card per row. Once one is open the next opens at
|
|
37
|
+
* once (React Aria's warm-up): comparing the customers of six invoices is a run down the
|
|
38
|
+
* column, not six waits. It is shorter than a tooltip's on purpose — a preview is asked for
|
|
39
|
+
* by resting on a NAME you want to know about; a tooltip names an icon you mostly know.
|
|
40
|
+
* 200 ms to close is only for the jittery exit; the trip to the card is covered by the
|
|
41
|
+
* safe area, not by the delay.
|
|
42
|
+
*
|
|
43
|
+
* ── The trigger ──────────────────────────────────────────────────────
|
|
44
|
+
*
|
|
45
|
+
* React Aria hands the trigger its hover, focus, long-press and `aria-*` props through
|
|
46
|
+
* context. A React Aria `Link` — the name of a `RelationValue` — reads that context itself,
|
|
47
|
+
* so it is passed as is. Anything else — a system `Button`, a `ButtonLink`, a plain `<a>` —
|
|
48
|
+
* goes through `Focusable`, the adapter that clones those props and a `ref` onto an element
|
|
49
|
+
* that forwards `ref` and spreads native props (the same one `TooltipTrigger` uses). Wrapping
|
|
50
|
+
* the `Link` too would hand it the props twice.
|
|
51
|
+
*/
|
|
52
|
+
const PREVIEW_DELAY = 600;
|
|
53
|
+
const PREVIEW_CLOSE_DELAY = 200;
|
|
54
|
+
export function Preview({ trigger, label, children, placement = "bottom start", delay = PREVIEW_DELAY, closeDelay = PREVIEW_CLOSE_DELAY, disabled = false, }) {
|
|
55
|
+
const readsContext = isValidElement(trigger) && trigger.type === Link;
|
|
56
|
+
return (_jsxs(PreviewTrigger, { delay: delay, closeDelay: closeDelay, isDisabled: disabled, children: [readsContext ? (trigger) : (
|
|
57
|
+
/* React Aria types the child as an intrinsic element; a component that forwards
|
|
58
|
+
`ref` and spreads native props is the same thing to it at runtime. */
|
|
59
|
+
_jsx(Focusable, { children: trigger })), _jsx(Popover, { placement: placement, "aria-label": label, children: _jsx("div", { className: "max-h-[inherit] max-w-sm overflow-auto p-3", children: children }) })] }));
|
|
60
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
export { Popover, type PopoverProps } from "./Popover.js";
|
|
2
2
|
export { PopoverTrigger, type PopoverTriggerProps, } from "./PopoverTrigger.js";
|
|
3
|
+
export { Preview, type PreviewProps } from "./Preview.js";
|
|
3
4
|
export { Tooltip, type TooltipProps, TooltipTrigger, } from "./Tooltip.js";
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/* LO QUE FLOTA anclado a algo: el calendario de un selector de fecha, la lista de un
|
|
2
|
-
combobox, un formulario chico abierto desde un botón, el nombre de un ícono
|
|
2
|
+
combobox, un formulario chico abierto desde un botón, el nombre de un ícono, el vistazo
|
|
3
|
+
a un registro al pasar por su nombre. */
|
|
3
4
|
export { Popover } from "./Popover.js";
|
|
4
5
|
export { PopoverTrigger, } from "./PopoverTrigger.js";
|
|
6
|
+
export { Preview } from "./Preview.js";
|
|
5
7
|
export { Tooltip, TooltipTrigger, } from "./Tooltip.js";
|
|
@@ -18,7 +18,22 @@ export interface PickerItem {
|
|
|
18
18
|
* on the box and finding the product, and having to remember what it was
|
|
19
19
|
* called. Drawing every code of every option is not an alternative. */
|
|
20
20
|
haystack?: string;
|
|
21
|
+
/** The header this item is listed under. Consecutive items with the same section share
|
|
22
|
+
* one header; an item without it is listed loose, outside any section. */
|
|
23
|
+
section?: string;
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* What the list shows and how many are left out of it. Capped at 8 with the footer count,
|
|
27
|
+
* unless the list loads incrementally — then everything that arrived is listed and the
|
|
28
|
+
* scroll asks for more.
|
|
29
|
+
*/
|
|
30
|
+
export declare function shownRows(filtered: readonly PickerItem[], { loading, incremental }: {
|
|
31
|
+
loading: boolean;
|
|
32
|
+
incremental: boolean;
|
|
33
|
+
}): {
|
|
34
|
+
visible: readonly PickerItem[];
|
|
35
|
+
remaining: number;
|
|
36
|
+
};
|
|
22
37
|
export type PickerProps = {
|
|
23
38
|
id?: string;
|
|
24
39
|
items: readonly PickerItem[];
|
|
@@ -39,10 +54,36 @@ export type PickerProps = {
|
|
|
39
54
|
* while loading reads as "there is nothing", which is a different answer. */
|
|
40
55
|
loading?: boolean;
|
|
41
56
|
loadingText?: string;
|
|
57
|
+
/** The list grows from the backend: called when its end scrolls into view. The consumer
|
|
58
|
+
* appends the next page to `items`; with nothing more to fetch it does nothing. Turns off
|
|
59
|
+
* the cap of 8, the «see all…» row and the footer count. */
|
|
60
|
+
onLoadMore?: () => void;
|
|
61
|
+
/** A page asked for by `onLoadMore` is on its way: a quiet row at the end says so. Unlike
|
|
62
|
+
* `loading`, the rows already there stay. */
|
|
63
|
+
loadingMore?: boolean;
|
|
42
64
|
describedBy?: string;
|
|
43
65
|
invalid?: boolean;
|
|
44
66
|
disabled?: boolean;
|
|
67
|
+
/** The name of the control when it is NOT inside a `Field` — a cell of an editable table.
|
|
68
|
+
* Inside a field, the field's label names it. */
|
|
69
|
+
"aria-label"?: string;
|
|
45
70
|
/** Forces the mode. By default it inherits the form's: in read mode it draws the name. */
|
|
46
71
|
mode?: FormMode;
|
|
47
72
|
};
|
|
48
73
|
export declare function Picker({ mode, ...props }: PickerProps): import("react").JSX.Element;
|
|
74
|
+
/**
|
|
75
|
+
* The list of a `Picker`: its rows, the section headers, the «see all…» row and the loading
|
|
76
|
+
* row at the end. Internal — not exported from the family — and apart from the combobox so it
|
|
77
|
+
* can be rendered on the server without a popover, which is how the tests see the rows.
|
|
78
|
+
*/
|
|
79
|
+
export declare function PickerOptions({ items, marks, hints, remaining, emptyText, onLoadMore, loadingMore, "aria-label": ariaLabel, }: {
|
|
80
|
+
items: readonly PickerItem[];
|
|
81
|
+
marks: boolean;
|
|
82
|
+
hints: boolean;
|
|
83
|
+
remaining: number;
|
|
84
|
+
emptyText: string;
|
|
85
|
+
onLoadMore?: () => void;
|
|
86
|
+
loadingMore?: boolean;
|
|
87
|
+
/** Only when rendered outside the combobox, which otherwise names the list. */
|
|
88
|
+
"aria-label"?: string;
|
|
89
|
+
}): import("react").JSX.Element;
|