@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.
Files changed (119) hide show
  1. package/README.md +37 -10
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  5. package/dist/components/{chatter → activity}/Messages.js +9 -6
  6. package/dist/components/{chatter → activity}/index.d.ts +1 -1
  7. package/dist/components/activity/index.js +4 -0
  8. package/dist/components/assist/AssistMark.d.ts +3 -3
  9. package/dist/components/assist/AssistMark.js +1 -1
  10. package/dist/components/assist/assist.d.ts +1 -1
  11. package/dist/components/assist/assist.js +1 -1
  12. package/dist/components/form/Sheet.d.ts +4 -0
  13. package/dist/components/form/Sheet.js +11 -4
  14. package/dist/components/index.d.ts +1 -1
  15. package/dist/components/index.js +1 -1
  16. package/dist/components/lines/LineItems.d.ts +42 -5
  17. package/dist/components/lines/LineItems.js +106 -18
  18. package/dist/components/listing/Listing.js +1 -1
  19. package/dist/components/listing/ListingBar.d.ts +41 -2
  20. package/dist/components/listing/ListingBar.js +43 -4
  21. package/dist/components/listing/Row.d.ts +5 -1
  22. package/dist/components/listing/Row.js +3 -2
  23. package/dist/components/record/RecordHeader.d.ts +21 -17
  24. package/dist/components/record/RecordHeader.js +46 -14
  25. package/dist/components/record/RecordNotices.d.ts +37 -0
  26. package/dist/components/record/RecordNotices.js +75 -0
  27. package/dist/components/record/ThumbBar.js +1 -1
  28. package/dist/foundations/copy.d.ts +96 -12
  29. package/dist/foundations/copy.es.js +63 -10
  30. package/dist/foundations/copy.js +55 -7
  31. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  32. package/dist/primitives/choice/ComboBox.js +2 -2
  33. package/dist/primitives/choice/SearchField.d.ts +10 -1
  34. package/dist/primitives/choice/SearchField.js +21 -2
  35. package/dist/primitives/date/Calendar.js +30 -6
  36. package/dist/primitives/date/DatePicker.d.ts +11 -3
  37. package/dist/primitives/date/DatePicker.js +12 -8
  38. package/dist/primitives/date/DateTime.d.ts +35 -0
  39. package/dist/primitives/date/DateTime.js +100 -0
  40. package/dist/primitives/date/index.d.ts +1 -0
  41. package/dist/primitives/date/index.js +4 -2
  42. package/dist/primitives/dialog/Drill.d.ts +90 -0
  43. package/dist/primitives/dialog/Drill.js +172 -0
  44. package/dist/primitives/dialog/index.d.ts +1 -0
  45. package/dist/primitives/dialog/index.js +2 -0
  46. package/dist/primitives/field/DropZone.js +1 -1
  47. package/dist/primitives/field/Field.d.ts +14 -2
  48. package/dist/primitives/field/Field.js +25 -14
  49. package/dist/primitives/field/copy.d.ts +16 -0
  50. package/dist/primitives/field/copy.js +69 -0
  51. package/dist/primitives/field/index.d.ts +2 -0
  52. package/dist/primitives/field/index.js +4 -0
  53. package/dist/primitives/field/note.d.ts +55 -0
  54. package/dist/primitives/field/note.js +117 -0
  55. package/dist/primitives/index.d.ts +1 -0
  56. package/dist/primitives/index.js +1 -0
  57. package/dist/primitives/menu/Menu.d.ts +28 -0
  58. package/dist/primitives/menu/Menu.js +47 -17
  59. package/dist/primitives/notice/Callout.js +1 -1
  60. package/dist/primitives/notice/Flag.d.ts +13 -0
  61. package/dist/primitives/notice/Flag.js +31 -0
  62. package/dist/primitives/notice/index.d.ts +1 -0
  63. package/dist/primitives/notice/index.js +1 -0
  64. package/dist/primitives/overlay/Preview.d.ts +19 -0
  65. package/dist/primitives/overlay/Preview.js +60 -0
  66. package/dist/primitives/overlay/index.d.ts +1 -0
  67. package/dist/primitives/overlay/index.js +3 -1
  68. package/dist/primitives/picker/Picker.d.ts +41 -0
  69. package/dist/primitives/picker/Picker.js +62 -8
  70. package/dist/primitives/table/CellCopy.d.ts +20 -0
  71. package/dist/primitives/table/CellCopy.js +23 -0
  72. package/dist/primitives/table/DataTable.d.ts +55 -1
  73. package/dist/primitives/table/DataTable.js +126 -40
  74. package/dist/primitives/table/EditableTable.d.ts +97 -2
  75. package/dist/primitives/table/EditableTable.js +223 -29
  76. package/dist/primitives/table/SheetTable.d.ts +4 -1
  77. package/dist/primitives/table/SheetTable.js +38 -5
  78. package/dist/primitives/table/index.d.ts +2 -2
  79. package/dist/primitives/table/index.js +1 -1
  80. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  81. package/dist/primitives/tabs/Tabs.js +1 -1
  82. package/dist/primitives/tag/TagsField.d.ts +3 -0
  83. package/dist/primitives/tag/TagsField.js +6 -2
  84. package/dist/primitives/tree/Tree.d.ts +56 -0
  85. package/dist/primitives/tree/Tree.js +74 -0
  86. package/dist/primitives/tree/index.d.ts +1 -0
  87. package/dist/primitives/tree/index.js +3 -0
  88. package/dist/primitives/value/Copyable.d.ts +11 -0
  89. package/dist/primitives/value/Copyable.js +27 -6
  90. package/dist/primitives/value/Fecha.js +10 -1
  91. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  92. package/dist/primitives/value/ProgressBar.js +53 -0
  93. package/dist/primitives/value/Relation.d.ts +6 -1
  94. package/dist/primitives/value/Relation.js +27 -10
  95. package/dist/primitives/value/Totals.d.ts +102 -7
  96. package/dist/primitives/value/Totals.js +52 -2
  97. package/dist/primitives/value/Value.d.ts +0 -37
  98. package/dist/primitives/value/Value.js +0 -39
  99. package/dist/primitives/value/index.d.ts +4 -3
  100. package/dist/primitives/value/index.js +4 -3
  101. package/dist/tokens.css +44 -7
  102. package/dist/views/list/List.d.ts +8 -2
  103. package/dist/views/list/List.js +6 -2
  104. package/dist/views/list/ListView.d.ts +1 -1
  105. package/dist/views/list/ListView.js +34 -8
  106. package/dist/views/list/derive.d.ts +12 -1
  107. package/dist/views/list/derive.js +29 -3
  108. package/dist/views/list/presentations.d.ts +2 -2
  109. package/dist/views/list/presentations.js +5 -3
  110. package/dist/views/list/types.d.ts +13 -1
  111. package/dist/views/record/Record.d.ts +27 -8
  112. package/dist/views/record/Record.js +97 -27
  113. package/dist/views/record/index.d.ts +1 -1
  114. package/package.json +5 -1
  115. package/dist/components/chatter/Chatter.d.ts +0 -161
  116. package/dist/components/chatter/Chatter.js +0 -232
  117. package/dist/components/chatter/index.js +0 -4
  118. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  119. /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
+ }
@@ -16,4 +16,5 @@ export * from "./table/index.js";
16
16
  export * from "./tabs/index.js";
17
17
  export * from "./tag/index.js";
18
18
  export * from "./toolbar/index.js";
19
+ export * from "./tree/index.js";
19
20
  export * from "./value/index.js";
@@ -17,4 +17,5 @@ export * from "./table/index.js";
17
17
  export * from "./tabs/index.js";
18
18
  export * from "./tag/index.js";
19
19
  export * from "./toolbar/index.js";
20
+ export * from "./tree/index.js";
20
21
  export * from "./value/index.js";
@@ -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 hasMarks = groups.some((g) => g.options.some((o) => o.checked !== undefined || o.icon !== undefined));
33
- const hasHints = groups.some((g) => g.options.some((o) => o.hint !== undefined));
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
- }, renderEmptyState: () => (_jsx("p", { className: "px-3 py-3 text-meta text-muted italic", children: emptyText ?? copy.menu.empty })), 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 : (
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 Icon = o.icon;
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;