@sincpro/web-ui 0.1.0 → 1.0.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 (198) hide show
  1. package/README.md +40 -13
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/activity/ActivityBadge.d.ts +16 -0
  5. package/dist/components/activity/ActivityBadge.js +29 -0
  6. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  7. package/dist/components/{chatter → activity}/Messages.js +9 -6
  8. package/dist/components/activity/index.d.ts +4 -0
  9. package/dist/components/activity/index.js +5 -0
  10. package/dist/components/assist/AssistMark.d.ts +3 -3
  11. package/dist/components/assist/AssistMark.js +1 -1
  12. package/dist/components/assist/assist.d.ts +1 -1
  13. package/dist/components/assist/assist.js +1 -1
  14. package/dist/components/calendar/RecordCalendar.d.ts +48 -0
  15. package/dist/components/calendar/RecordCalendar.js +101 -0
  16. package/dist/components/calendar/index.d.ts +1 -0
  17. package/dist/components/calendar/index.js +1 -0
  18. package/dist/components/form/Sheet.d.ts +4 -0
  19. package/dist/components/form/Sheet.js +16 -5
  20. package/dist/components/gantt/RecordGantt.d.ts +44 -0
  21. package/dist/components/gantt/RecordGantt.js +110 -0
  22. package/dist/components/gantt/index.d.ts +1 -0
  23. package/dist/components/gantt/index.js +1 -0
  24. package/dist/components/index.d.ts +4 -1
  25. package/dist/components/index.js +4 -1
  26. package/dist/components/lines/LineItems.d.ts +45 -5
  27. package/dist/components/lines/LineItems.js +106 -18
  28. package/dist/components/listing/BoardColumns.d.ts +39 -0
  29. package/dist/components/listing/BoardColumns.js +138 -0
  30. package/dist/components/listing/Listing.js +1 -1
  31. package/dist/components/listing/ListingBar.d.ts +46 -2
  32. package/dist/components/listing/ListingBar.js +47 -5
  33. package/dist/components/listing/PivotExplorer.d.ts +34 -0
  34. package/dist/components/listing/PivotExplorer.js +86 -0
  35. package/dist/components/listing/RecordCard.d.ts +6 -8
  36. package/dist/components/listing/RecordCard.js +13 -7
  37. package/dist/components/listing/Row.d.ts +25 -2
  38. package/dist/components/listing/Row.js +30 -13
  39. package/dist/components/listing/index.d.ts +2 -0
  40. package/dist/components/listing/index.js +2 -0
  41. package/dist/components/listing/set.d.ts +1 -1
  42. package/dist/components/navigation/index.d.ts +0 -2
  43. package/dist/components/navigation/index.js +2 -3
  44. package/dist/components/record/RecordAudit.d.ts +17 -0
  45. package/dist/components/record/RecordAudit.js +27 -0
  46. package/dist/components/record/RecordHeader.d.ts +27 -17
  47. package/dist/components/record/RecordHeader.js +64 -20
  48. package/dist/components/record/RecordNotices.d.ts +37 -0
  49. package/dist/components/record/RecordNotices.js +75 -0
  50. package/dist/components/record/RecordRibbon.d.ts +13 -0
  51. package/dist/components/record/RecordRibbon.js +22 -0
  52. package/dist/components/record/ThumbBar.js +1 -1
  53. package/dist/components/record/index.d.ts +2 -0
  54. package/dist/components/record/index.js +2 -0
  55. package/dist/components/search/CriteriaChips.d.ts +6 -1
  56. package/dist/components/search/CriteriaChips.js +71 -20
  57. package/dist/components/search/SearchBar.d.ts +4 -1
  58. package/dist/components/search/SearchBar.js +15 -7
  59. package/dist/components/timeline/RecordTimeline.d.ts +60 -0
  60. package/dist/components/timeline/RecordTimeline.js +214 -0
  61. package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
  62. package/dist/components/timeline/TimelineToolbar.js +31 -0
  63. package/dist/components/timeline/index.d.ts +3 -0
  64. package/dist/components/timeline/index.js +3 -0
  65. package/dist/components/timeline/time.d.ts +43 -0
  66. package/dist/components/timeline/time.js +135 -0
  67. package/dist/foundations/copy.d.ts +194 -18
  68. package/dist/foundations/copy.es.js +142 -15
  69. package/dist/foundations/copy.js +133 -11
  70. package/dist/foundations/index.d.ts +1 -1
  71. package/dist/foundations/match.d.ts +23 -0
  72. package/dist/foundations/match.js +63 -0
  73. package/dist/foundations/slots.d.ts +1 -1
  74. package/dist/foundations/slots.js +8 -3
  75. package/dist/foundations/types.d.ts +7 -0
  76. package/dist/foundations/types.js +0 -36
  77. package/dist/primitives/block/index.d.ts +1 -1
  78. package/dist/primitives/block/index.js +1 -1
  79. package/dist/primitives/cell/CellStack.d.ts +7 -1
  80. package/dist/primitives/cell/CellStack.js +5 -2
  81. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  82. package/dist/primitives/choice/ComboBox.js +10 -6
  83. package/dist/primitives/choice/KanbanState.d.ts +14 -0
  84. package/dist/primitives/choice/KanbanState.js +37 -0
  85. package/dist/primitives/choice/Priority.d.ts +20 -0
  86. package/dist/primitives/choice/Priority.js +41 -0
  87. package/dist/primitives/choice/SearchField.d.ts +10 -1
  88. package/dist/primitives/choice/SearchField.js +21 -2
  89. package/dist/primitives/choice/Slider.d.ts +6 -1
  90. package/dist/primitives/choice/Slider.js +2 -2
  91. package/dist/primitives/choice/index.d.ts +2 -0
  92. package/dist/primitives/choice/index.js +2 -0
  93. package/dist/primitives/date/Calendar.js +30 -6
  94. package/dist/primitives/date/DatePicker.d.ts +11 -3
  95. package/dist/primitives/date/DatePicker.js +12 -8
  96. package/dist/primitives/date/DateTime.d.ts +35 -0
  97. package/dist/primitives/date/DateTime.js +100 -0
  98. package/dist/primitives/date/index.d.ts +1 -0
  99. package/dist/primitives/date/index.js +4 -2
  100. package/dist/primitives/dialog/Dialog.d.ts +9 -3
  101. package/dist/primitives/dialog/Dialog.js +41 -5
  102. package/dist/primitives/field/DropZone.js +1 -1
  103. package/dist/primitives/field/Field.d.ts +14 -2
  104. package/dist/primitives/field/Field.js +25 -14
  105. package/dist/primitives/field/copy.d.ts +16 -0
  106. package/dist/primitives/field/copy.js +69 -0
  107. package/dist/primitives/field/index.d.ts +2 -0
  108. package/dist/primitives/field/index.js +4 -0
  109. package/dist/primitives/field/note.d.ts +55 -0
  110. package/dist/primitives/field/note.js +117 -0
  111. package/dist/primitives/index.d.ts +1 -0
  112. package/dist/primitives/index.js +1 -0
  113. package/dist/primitives/menu/Menu.d.ts +28 -0
  114. package/dist/primitives/menu/Menu.js +47 -17
  115. package/dist/primitives/notice/Callout.js +1 -1
  116. package/dist/primitives/notice/Flag.d.ts +13 -0
  117. package/dist/primitives/notice/Flag.js +31 -0
  118. package/dist/primitives/notice/index.d.ts +2 -1
  119. package/dist/primitives/notice/index.js +2 -1
  120. package/dist/primitives/overlay/Preview.d.ts +19 -0
  121. package/dist/primitives/overlay/Preview.js +69 -0
  122. package/dist/primitives/overlay/index.d.ts +1 -0
  123. package/dist/primitives/overlay/index.js +3 -1
  124. package/dist/primitives/picker/MatchedText.d.ts +10 -0
  125. package/dist/primitives/picker/MatchedText.js +12 -0
  126. package/dist/primitives/picker/Picker.d.ts +67 -4
  127. package/dist/primitives/picker/Picker.js +142 -23
  128. package/dist/primitives/table/CellCopy.d.ts +20 -0
  129. package/dist/primitives/table/CellCopy.js +23 -0
  130. package/dist/primitives/table/DataTable.d.ts +55 -1
  131. package/dist/primitives/table/DataTable.js +126 -40
  132. package/dist/primitives/table/EditableTable.d.ts +102 -2
  133. package/dist/primitives/table/EditableTable.js +287 -32
  134. package/dist/primitives/table/ReportTable.js +2 -1
  135. package/dist/primitives/table/RowActions.js +7 -2
  136. package/dist/primitives/table/SheetTable.d.ts +4 -1
  137. package/dist/primitives/table/SheetTable.js +38 -5
  138. package/dist/primitives/table/Table.d.ts +5 -0
  139. package/dist/primitives/table/Table.js +17 -8
  140. package/dist/primitives/table/index.d.ts +2 -2
  141. package/dist/primitives/table/index.js +1 -1
  142. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  143. package/dist/primitives/tabs/Tabs.js +1 -1
  144. package/dist/primitives/tag/Tags.d.ts +2 -0
  145. package/dist/primitives/tag/Tags.js +2 -1
  146. package/dist/primitives/tag/TagsField.d.ts +13 -0
  147. package/dist/primitives/tag/TagsField.js +39 -10
  148. package/dist/primitives/tag/choices.d.ts +2 -0
  149. package/dist/primitives/tree/Tree.d.ts +56 -0
  150. package/dist/primitives/tree/Tree.js +74 -0
  151. package/dist/primitives/tree/index.d.ts +1 -0
  152. package/dist/primitives/tree/index.js +3 -0
  153. package/dist/primitives/value/Avatar.d.ts +16 -0
  154. package/dist/primitives/value/Avatar.js +91 -0
  155. package/dist/primitives/value/Copyable.d.ts +11 -0
  156. package/dist/primitives/value/Copyable.js +27 -6
  157. package/dist/primitives/value/Fecha.js +10 -1
  158. package/dist/primitives/value/Person.d.ts +6 -51
  159. package/dist/primitives/value/Person.js +9 -148
  160. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  161. package/dist/primitives/value/ProgressBar.js +53 -0
  162. package/dist/primitives/value/Relation.d.ts +22 -5
  163. package/dist/primitives/value/Relation.js +42 -14
  164. package/dist/primitives/value/Totals.d.ts +102 -7
  165. package/dist/primitives/value/Totals.js +52 -2
  166. package/dist/primitives/value/Value.d.ts +0 -37
  167. package/dist/primitives/value/Value.js +0 -39
  168. package/dist/primitives/value/contact.d.ts +30 -0
  169. package/dist/primitives/value/contact.js +51 -0
  170. package/dist/primitives/value/index.d.ts +5 -4
  171. package/dist/primitives/value/index.js +5 -4
  172. package/dist/tokens.css +179 -7
  173. package/dist/views/list/List.d.ts +32 -3
  174. package/dist/views/list/List.js +51 -7
  175. package/dist/views/list/ListView.d.ts +1 -1
  176. package/dist/views/list/ListView.js +70 -13
  177. package/dist/views/list/columns.js +8 -1
  178. package/dist/views/list/derive.d.ts +12 -1
  179. package/dist/views/list/derive.js +29 -3
  180. package/dist/views/list/presentations.d.ts +5 -2
  181. package/dist/views/list/presentations.js +35 -3
  182. package/dist/views/list/types.d.ts +57 -1
  183. package/dist/views/list/useNarrowList.d.ts +10 -0
  184. package/dist/views/list/useNarrowList.js +20 -6
  185. package/dist/views/record/Record.d.ts +41 -8
  186. package/dist/views/record/Record.js +113 -28
  187. package/dist/views/record/index.d.ts +1 -1
  188. package/package.json +15 -4
  189. package/dist/components/chatter/Chatter.d.ts +0 -161
  190. package/dist/components/chatter/Chatter.js +0 -232
  191. package/dist/components/chatter/index.d.ts +0 -3
  192. package/dist/components/chatter/index.js +0 -4
  193. package/dist/components/navigation/CommandPalette.d.ts +0 -40
  194. package/dist/components/navigation/CommandPalette.js +0 -114
  195. package/dist/components/navigation/commands.d.ts +0 -30
  196. package/dist/components/navigation/commands.js +0 -30
  197. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  198. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -1,10 +1,11 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useUiCopy } from "../../foundations/copy.js";
4
+ import { HANDHELD_WIDTH } from "../../foundations/layout.js";
4
5
  import { cn, tv } from "../../foundations/variants.js";
5
6
  import { Button, IconButton } from "../button/Button.js";
6
7
  import { Maximize2, X } from "lucide-react";
7
- import { useId } from "react";
8
+ import { useEffect, useId, useState } from "react";
8
9
  import { Dialog as AriaDialog, Heading, Modal, ModalOverlay } from "react-aria-components";
9
10
  /** The scrim. The page stays visible under it — the dialog is ABOUT something on it.
10
11
  * On a phone it drops the padding and pushes the window to the bottom edge. */
@@ -39,11 +40,11 @@ export const dialogStyles = { overlay, modal };
39
40
  *
40
41
  * Internal — exported only so a test can read its markup, which the modal (a portal)
41
42
  * does not produce on the server. */
42
- export function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, }) {
43
+ export function DialogSurface({ title, subtitle, context, onClose, onExpand, expandLabel, footer, children, onDragStart, }) {
43
44
  const copy = useUiCopy();
44
45
  const subtitleId = useId();
45
46
  const expand = expandLabel ?? copy.dialog.expand;
46
- return (_jsxs(AriaDialog, { "aria-describedby": subtitle === undefined ? undefined : subtitleId, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: [_jsxs("header", { className: "flex shrink-0 items-start gap-3 border-b border-line px-4 py-3", children: [_jsxs("div", { className: "min-w-0 flex-1", children: [context === undefined ? null : (
47
+ return (_jsxs(AriaDialog, { "aria-describedby": subtitle === undefined ? undefined : subtitleId, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: [_jsxs("header", { onPointerDown: onDragStart, className: cn("flex shrink-0 items-start gap-3 border-b border-line px-4 py-3", onDragStart !== undefined && "cursor-grab select-none active:cursor-grabbing"), children: [_jsxs("div", { className: "min-w-0 flex-1", children: [context === undefined ? null : (
47
48
  /* The context, on top and small. It is what keeps the first field
48
49
  from being «about what?». Same voice as a field label, not a
49
50
  console kicker: mono uppercase next to a form title was a
@@ -59,11 +60,46 @@ export function DialogSurface({ title, subtitle, context, onClose, onExpand, exp
59
60
  * stays mounted for the exit transition after `open` turns `false`, so the window fades
60
61
  * out instead of vanishing.
61
62
  */
62
- export function Dialog({ open, width = "md", onClose, ...surface }) {
63
+ export function Dialog({ open, width = "md", onClose, movable = false, ...surface }) {
64
+ const [offset, setOffset] = useState({ x: 0, y: 0 });
65
+ /* Every opening starts centred: where it was dragged last time is not where it belongs. */
66
+ useEffect(() => {
67
+ if (open)
68
+ setOffset({ x: 0, y: 0 });
69
+ }, [open]);
70
+ /* `transform` and not `translate`: the window enters and leaves on `translate`, which is
71
+ transitioned — a drag on it would trail the pointer by the panel's duration. */
72
+ const drag = (event) => {
73
+ if (event.button !== 0 || event.pointerType === "touch")
74
+ return;
75
+ if (event.target.closest("button, a, input") !== null)
76
+ return;
77
+ /* Below it the window is a bottom sheet, and a sheet is not dragged around. */
78
+ if (window.innerWidth < HANDHELD_WIDTH)
79
+ return;
80
+ const from = { x: event.clientX - offset.x, y: event.clientY - offset.y };
81
+ /* Never further than half the screen from the centre: the header — the only handle — has
82
+ to stay where a pointer can reach it. */
83
+ const halfW = window.innerWidth / 2;
84
+ const halfH = window.innerHeight / 2;
85
+ const clamp = (value, half) => Math.max(-half, Math.min(half, value));
86
+ const move = (next) => setOffset({
87
+ x: clamp(next.clientX - from.x, halfW),
88
+ y: clamp(next.clientY - from.y, halfH),
89
+ });
90
+ const stop = () => {
91
+ window.removeEventListener("pointermove", move);
92
+ window.removeEventListener("pointerup", stop);
93
+ };
94
+ window.addEventListener("pointermove", move);
95
+ window.addEventListener("pointerup", stop);
96
+ };
63
97
  return (_jsx(ModalOverlay, { isOpen: open, onOpenChange: (next) => {
64
98
  if (!next)
65
99
  onClose();
66
- }, isDismissable: true, className: overlay, children: _jsx(Modal, { className: (state) => modal({ ...state, width }), children: _jsx(DialogSurface, { onClose: onClose, ...surface }) }) }));
100
+ }, isDismissable: true, className: overlay, children: _jsx(Modal, { className: (state) => modal({ ...state, width }), ...(movable
101
+ ? { style: { transform: `translate(${offset.x}px, ${offset.y}px)` } }
102
+ : {}), children: _jsx(DialogSurface, { onClose: onClose, ...surface, ...(movable ? { onDragStart: drag } : {}) }) }) }));
67
103
  }
68
104
  /** A dialog with steps, for what CANNOT be answered in one go.
69
105
  *
@@ -7,7 +7,7 @@ import { useFieldIds } from "./context.js";
7
7
  import { ReadValue } from "./Field.js";
8
8
  import { useFormMode } from "./mode.js";
9
9
  import { Failed } from "../notice/Notice.js";
10
- import { ProgressBar } from "../value/Value.js";
10
+ import { ProgressBar } from "../value/ProgressBar.js";
11
11
  import { Button as AriaButton, DropZone as AriaDropZone, FileTrigger, } from "react-aria-components";
12
12
  export function DropZone({ accept, label, busy = false, progress, fileName, fileDetail, error, onFile, onCancel, onRetry, id: ownId, describedBy: ownDescribedBy, invalid: ownInvalid, mode, }) {
13
13
  const copy = useUiCopy();
@@ -1,7 +1,8 @@
1
1
  import { type FieldIds } from "./context.js";
2
2
  import { type FormMode } from "./mode.js";
3
+ import { type FieldNote } from "./note.js";
3
4
  import { type ReactNode } from "react";
4
- export declare function Field({ label, hint, error, required, children, htmlFor, layout, action, mode, }: {
5
+ export declare function Field({ label, hint, error, required, children, htmlFor, layout, action, mode, note, copy: copyAs, }: {
5
6
  label: string;
6
7
  /** What is expected. A `?` on the label that opens it on press. */
7
8
  hint?: string;
@@ -27,6 +28,13 @@ export declare function Field({ label, hint, error, required, children, htmlFor,
27
28
  action?: ReactNode;
28
29
  /** Forces the mode of this field. By default it inherits it from the view. */
29
30
  mode?: FormMode;
31
+ /** A note that belongs to THIS field — «vence en 3 días», «tiene una NC sin aplicar ·
32
+ * Aplicar». A card in the record's margin when there is one, a `Flag` by the label when
33
+ * there is not; its sentence describes the control (`aria-describedby`). See `note.tsx`. */
34
+ note?: FieldNote;
35
+ /** The copy button by the label (see `copy.tsx`): on by default, copying what the control
36
+ * shows. A string copies that instead; `false` removes the button from this field. */
37
+ copy?: string | false;
30
38
  }): import("react").JSX.Element;
31
39
  /**
32
40
  * A label with its VALUE, without a control below. What was missing to show a
@@ -40,7 +48,7 @@ export declare function Field({ label, hint, error, required, children, htmlFor,
40
48
  * It uses the same geometry as `Field`, so a field that goes from being looked
41
49
  * at to being edited does not move anything around it.
42
50
  */
43
- export declare function DataField({ label, value, hint, layout, mono, action, }: {
51
+ export declare function DataField({ label, value, hint, layout, mono, action, note, copy: copyAs, }: {
44
52
  label: string;
45
53
  /** `undefined` and `null` are drawn with the missing mark: saying there is no
46
54
  * data is information, and a blank line does not give it. */
@@ -50,6 +58,10 @@ export declare function DataField({ label, value, hint, layout, mono, action, }:
50
58
  /** The value was written by the machine: an id, a code, an account number. */
51
59
  mono?: boolean;
52
60
  action?: ReactNode;
61
+ /** A note that belongs to this value — the same as `Field`'s. */
62
+ note?: FieldNote;
63
+ /** The copy button by the label, as `Field`'s: on by default for a value that is there. */
64
+ copy?: string | false;
53
65
  }): import("react").JSX.Element;
54
66
  /** What a control draws when the form is LOOKED AT instead of written into.
55
67
  *
@@ -6,12 +6,14 @@ import { byDensity, useDensity } from "../../foundations/density.js";
6
6
  import { focusRing, tv } from "../../foundations/variants.js";
7
7
  import { FieldGroup, listOptionStyles } from "./aria.js";
8
8
  import { FieldIdsContext, useFieldIds, } from "./context.js";
9
+ import { FieldCopy, useFieldCopy } from "./copy.js";
9
10
  import { useSheetLayout } from "./layout.js";
10
11
  import { FormModeProvider, useFormMode, } from "./mode.js";
12
+ import { describedByOf, NoteCard, NoteFlag, } from "./note.js";
11
13
  import { Hint } from "../hint/Hint.js";
12
14
  import { Popover } from "../overlay/Popover.js";
13
15
  import { AlertCircle, Check, ChevronDown, ChevronUp } from "lucide-react";
14
- import { useId } from "react";
16
+ import { useId, useRef } from "react";
15
17
  import { Button as AriaButton, Input, ListBox, ListBoxItem, NumberField as AriaNumberField, Select as AriaSelect, SelectValue, TextArea as AriaTextArea, TextField as AriaTextField, } from "react-aria-components";
16
18
  /* ==================================================================== *
17
19
  * THE CONTROLS — the label is not optional and the error is not a color.
@@ -38,19 +40,24 @@ import { Button as AriaButton, Input, ListBox, ListBoxItem, NumberField as AriaN
38
40
  * control ate a line on every field. The error still goes below, close to
39
41
  * the cursor, and it says what to put.
40
42
  * ==================================================================== */
41
- export function Field({ label, hint, error, required = false, children, htmlFor, layout, action, mode, }) {
43
+ export function Field({ label, hint, error, required = false, children, htmlFor, layout, action, mode, note, copy: copyAs, }) {
42
44
  const d = useDensity();
43
45
  const copy = useUiCopy();
46
+ const copies = useFieldCopy() && copyAs !== false;
47
+ const shown = useRef(null);
44
48
  const auto = useId();
45
49
  const id = htmlFor ?? auto;
46
50
  const sheetLayout = useSheetLayout();
47
51
  const inline = layout === "inline" || (layout === undefined && sheetLayout === "horizontal");
48
52
  const hintId = `${id}-hint`;
49
53
  const errorId = `${id}-error`;
50
- const describedBy = [hint === undefined ? null : hintId, error === undefined ? null : errorId]
51
- .filter((x) => x !== null)
52
- .join(" ") || undefined;
53
- const labelNode = (_jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted ${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 })] }));
54
+ const noteId = `${id}-note`;
55
+ /* 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 = (_jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted ${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 }), copies ? (_jsx(FieldCopy, { label: label, scope: shown, ...(typeof copyAs === "string" ? { value: copyAs } : {}) })) : null] }));
58
+ /* Last in the field so it is read last; absolutely placed, it still sits at the field's
59
+ top — the static position of an absolute child of a flex or grid box is its start. */
60
+ const noteCard = note === undefined ? null : _jsx(NoteCard, { note: note, id: noteId, owner: label });
54
61
  /* The field's `mode` propagates INWARDS and is not used here: what knows how
55
62
  to draw itself as data is the control, not the label. That way a computed
56
63
  field stays in read mode inside a form that is being edited, without the
@@ -63,12 +70,12 @@ export function Field({ label, hint, error, required = false, children, htmlFor,
63
70
  };
64
71
  /* The render prop gets the same context as a control written directly: a system control
65
72
  inside it then also finds the label that names it, not only the id it is handed. */
66
- const content = (_jsx(FieldIdsContext.Provider, { value: ids, children: typeof children === "function" ? children(ids) : children }));
73
+ const content = (_jsx(FieldIdsContext.Provider, { value: ids, children: _jsx("div", { ref: shown, className: "contents", children: typeof children === "function" ? children(ids) : children }) }));
67
74
  const body = (_jsxs(_Fragment, { children: [mode === undefined ? (content) : (_jsx(FormModeProvider, { value: mode, children: content })), error === undefined ? null : (_jsxs("p", { id: errorId, role: "alert", className: "flex items-start gap-1.5 text-micro text-danger", children: [_jsx(AlertCircle, { className: "mt-px size-(--size-icon-sm) shrink-0", strokeWidth: 2, "aria-hidden": true }), _jsx("span", { className: "max-w-prose", children: error })] }))] }));
68
75
  if (inline) {
69
- return (_jsxs("div", { className: "grid-label grid min-w-0 items-start gap-x-4 gap-y-1", children: [labelNode, _jsx("div", { className: "flex min-w-0 flex-col gap-1", children: body }), action === undefined ? null : (_jsx("div", { "data-under-control": true, className: "flex items-center gap-2", children: action }))] }));
76
+ return (_jsxs("div", { className: "grid-label grid min-w-0 items-start gap-x-4 gap-y-1 group/field", children: [labelNode, _jsx("div", { className: "flex min-w-0 flex-col gap-1", children: body }), action === undefined ? null : (_jsx("div", { "data-under-control": true, className: "flex items-center gap-2", children: action })), noteCard] }));
70
77
  }
71
- return (_jsxs("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-1", "gap-0.5")}`, children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [labelNode, action === undefined ? null : _jsx("span", { className: "ml-auto shrink-0", children: action })] }), body] }));
78
+ return (_jsxs("div", { className: `group/field flex min-w-0 flex-col ${byDensity(d, "gap-1", "gap-0.5")}`, children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [labelNode, action === undefined ? null : _jsx("span", { className: "ml-auto shrink-0", children: action })] }), body, noteCard] }));
72
79
  }
73
80
  /**
74
81
  * A label with its VALUE, without a control below. What was missing to show a
@@ -82,18 +89,22 @@ export function Field({ label, hint, error, required = false, children, htmlFor,
82
89
  * It uses the same geometry as `Field`, so a field that goes from being looked
83
90
  * at to being edited does not move anything around it.
84
91
  */
85
- export function DataField({ label, value, hint, layout, mono = false, action, }) {
92
+ export function DataField({ label, value, hint, layout, mono = false, action, note, copy: copyAs, }) {
86
93
  const d = useDensity();
94
+ const shown = useRef(null);
95
+ const allowed = useFieldCopy() && copyAs !== false;
96
+ const noteId = `${useId()}-note`;
87
97
  const sheetLayout = useSheetLayout();
88
98
  const inline = layout === "inline" || (layout === undefined && sheetLayout === "horizontal");
89
99
  const empty = value === undefined || value === null || value === "";
90
100
  const unset = useUiCopy().field.unset;
91
- const labelNode = (_jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted ${inline ? "pt-0.5" : ""}`, children: [_jsx("span", { className: inline ? "min-w-0 truncate" : "", children: label }), hint === undefined ? null : _jsx(Hint, { children: hint })] }));
92
- const valueNode = (_jsx("span", { className: `min-w-0 text-meta ${empty ? "text-muted italic" : "text-ink"} ${mono ? "font-mono tabular-nums" : ""}`, children: empty ? unset : value }));
101
+ const labelNode = (_jsxs("span", { className: `flex min-w-0 items-center gap-1 text-micro font-medium text-muted ${inline ? "pt-0.5" : ""}`, children: [_jsx("span", { className: inline ? "min-w-0 truncate" : "", children: label }), hint === undefined ? null : _jsx(Hint, { children: hint }), note === undefined ? null : _jsx(NoteFlag, { note: note }), allowed && !empty ? (_jsx(FieldCopy, { label: label, scope: shown, ...(typeof copyAs === "string" ? { value: copyAs } : {}) })) : null] }));
102
+ const noteCard = note === undefined ? null : _jsx(NoteCard, { note: note, id: noteId, owner: label });
103
+ const valueNode = (_jsx("span", { ref: shown, ...(note === undefined ? {} : { "aria-describedby": noteId }), className: `min-w-0 text-meta ${empty ? "text-muted italic" : "text-ink"} ${mono ? "font-mono tabular-nums" : ""}`, children: empty ? unset : value }));
93
104
  if (inline) {
94
- return (_jsxs("div", { className: "grid-label grid min-w-0 items-baseline gap-x-4 gap-y-0.5", children: [labelNode, _jsxs("span", { className: "flex min-w-0 items-baseline gap-2", children: [valueNode, action] })] }));
105
+ return (_jsxs("div", { className: "grid-label grid min-w-0 items-baseline gap-x-4 gap-y-0.5 group/field", children: [labelNode, _jsxs("span", { className: "flex min-w-0 items-baseline gap-2", children: [valueNode, action] }), noteCard] }));
95
106
  }
96
- return (_jsxs("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-1", "gap-0.5")}`, children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [labelNode, action === undefined ? null : _jsx("span", { className: "ml-auto shrink-0", children: action })] }), valueNode] }));
107
+ return (_jsxs("div", { className: `group/field flex min-w-0 flex-col ${byDensity(d, "gap-1", "gap-0.5")}`, children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [labelNode, action === undefined ? null : _jsx("span", { className: "ml-auto shrink-0", children: action })] }), valueNode, noteCard] }));
97
108
  }
98
109
  /** What a control draws when the form is LOOKED AT instead of written into.
99
110
  *
@@ -0,0 +1,16 @@
1
+ import { type ReactNode, type RefObject } from "react";
2
+ /** Turns the fields' copy buttons on or off for everything inside. On by default. */
3
+ export declare function FieldCopyProvider({ value, children, }: {
4
+ value: boolean;
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
7
+ export declare function useFieldCopy(): boolean;
8
+ /** The text a value area shows, inputs included, placeholders and named buttons left out. */
9
+ export declare function shownText(root: HTMLElement): string;
10
+ /** The button by a field's label. `scope` is the field's value area. */
11
+ export declare function FieldCopy({ label, scope, value, }: {
12
+ label: string;
13
+ scope: RefObject<HTMLElement | null>;
14
+ /** What to copy instead of what is seen. */
15
+ value?: string;
16
+ }): import("react").JSX.Element;
@@ -0,0 +1,69 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ import { CopyButton } from "../value/Copyable.js";
5
+ import { createContext, useContext } from "react";
6
+ /* ==================================================================== *
7
+ * THE COPY BUTTON OF A FIELD — what is on screen, to the clipboard.
8
+ *
9
+ * Context: half of what a record shows is there to be pasted somewhere else —
10
+ * the customer's name into an email, a NIT into the tax portal, a reference
11
+ * into the bank's site. Selecting it by hand fails on exactly those: a
12
+ * many2one is a link that OPENS when pressed, a date is three segments, a
13
+ * number field holds its value in an input. So every `Field` and `DataField`
14
+ * carries a small copy button next to its label, in read mode and in edit
15
+ * mode alike, revealed by the pointer or the focus (always shown on a touch
16
+ * screen, where there is no hover).
17
+ *
18
+ * ── It copies what is SEEN ──────────────────────────────────────────
19
+ *
20
+ * The field does not know its control's value — the control does — so the
21
+ * button reads the value area at the moment of the press: the text drawn,
22
+ * plus what the inputs hold (the name a picker shows, a formatted amount),
23
+ * skipping placeholders (an empty date's «dd/mm/aaaa»), what is hidden, and
24
+ * the named icon buttons inside (a chip's «remove», a summary's «i»). What
25
+ * the person sees is what they paste.
26
+ *
27
+ * ── On by default, never imposed ────────────────────────────────────
28
+ *
29
+ * This system gives possibilities; it does not enclose. `copy="…"` copies
30
+ * something else (the NIT alone, an id the screen does not draw), `copy={false}`
31
+ * removes the button from one field, and `FieldCopyProvider value={false}`
32
+ * from a whole form — a login, a wizard where copying means nothing.
33
+ * ==================================================================== */
34
+ const FieldCopyContext = createContext(true);
35
+ /** Turns the fields' copy buttons on or off for everything inside. On by default. */
36
+ export function FieldCopyProvider({ value, children, }) {
37
+ return _jsx(FieldCopyContext.Provider, { value: value, children: children });
38
+ }
39
+ export function useFieldCopy() {
40
+ return useContext(FieldCopyContext);
41
+ }
42
+ /** What skipping means: not seen, or not the value. */
43
+ const SKIP = '[hidden], [aria-hidden="true"], [data-placeholder], .sr-only, button[aria-label], [data-copy-skip], select';
44
+ /** The text a value area shows, inputs included, placeholders and named buttons left out. */
45
+ export function shownText(root) {
46
+ const parts = [];
47
+ const walk = (node) => {
48
+ if (node.nodeType === Node.TEXT_NODE) {
49
+ parts.push(node.textContent ?? "");
50
+ return;
51
+ }
52
+ if (!(node instanceof HTMLElement) || node.matches(SKIP))
53
+ return;
54
+ if (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement) {
55
+ if (node.type !== "hidden")
56
+ parts.push(` ${node.value} `);
57
+ return;
58
+ }
59
+ for (const child of node.childNodes)
60
+ walk(child);
61
+ };
62
+ walk(root);
63
+ return parts.join("").replace(/\s+/g, " ").trim();
64
+ }
65
+ /** The button by a field's label. `scope` is the field's value area. */
66
+ export function FieldCopy({ label, scope, value, }) {
67
+ const ui = useUiCopy();
68
+ return (_jsx("span", { "data-copy-skip": "", className: "inline-flex shrink-0 opacity-0 transition-opacity duration-(--duration-instant) group-hover/field:opacity-100 focus-within:opacity-100 pointer-coarse:opacity-100", children: _jsx(CopyButton, { label: fillCopy(ui.copyable.field, { label }), value: () => value ?? (scope.current === null ? "" : shownText(scope.current)) }) }));
69
+ }
@@ -1,7 +1,9 @@
1
1
  export { Checkbox, type CheckItem, CheckList } from "./Checks.js";
2
2
  export { type FieldIds, useFieldIds } from "./context.js";
3
+ export { FieldCopyProvider, shownText, useFieldCopy, } from "./copy.js";
3
4
  export { DropZone, type DropZoneProps } from "./DropZone.js";
4
5
  export { controlClasses, DataField, Field, NumberInput, ReadValue, Select, type SelectOption, TextArea, TextInput, } from "./Field.js";
5
6
  export { type FieldLayout, FieldLayoutProvider, useSheetLayout, } from "./layout.js";
6
7
  export { type FormMode, FormModeProvider, useFormMode, } from "./mode.js";
7
8
  export { Suggest, type SuggestProps } from "./Suggest.js";
9
+ export { type FieldNote } from "./note.js";
@@ -4,8 +4,12 @@
4
4
  a dialog. */
5
5
  export { Checkbox, CheckList } from "./Checks.js";
6
6
  export { useFieldIds } from "./context.js";
7
+ /* The copy button every field carries by its label — on by default, off by field or by form. */
8
+ export { FieldCopyProvider, shownText, useFieldCopy, } from "./copy.js";
7
9
  export { DropZone } from "./DropZone.js";
8
10
  export { controlClasses, DataField, Field, NumberInput, ReadValue, Select, TextArea, TextInput, } from "./Field.js";
9
11
  export { FieldLayoutProvider, useSheetLayout, } from "./layout.js";
10
12
  export { FormModeProvider, useFormMode, } from "./mode.js";
11
13
  export { Suggest } from "./Suggest.js";
14
+ /* A note that belongs to one field: a card in the record's margin, or a flag by its label. */
15
+ export {} from "./note.js";
@@ -0,0 +1,55 @@
1
+ import { type FlagTone } from "../notice/Flag.js";
2
+ import { type ReactNode } from "react";
3
+ /** A note anchored to one field: a sentence, what explains it, and what resolves it. */
4
+ export type FieldNote = {
5
+ /** Without it, `info`. The tone has a shape as well as a colour (`Flag`). */
6
+ tone?: FlagTone;
7
+ /** The sentence: «Vence en 3 días». It names the mark and heads the card. */
8
+ label: string;
9
+ /** What explains it: «NC/2026/0012 por Bs. 1.200,00, sin aplicar». */
10
+ detail?: ReactNode;
11
+ /** What resolves it, if there is something to press: a `Button` with `role="ghost"`
12
+ * and `size="sm"` — the record's primary action is not here. */
13
+ action?: ReactNode;
14
+ };
15
+ /** A note as the record lists it: the note, what it belongs to, and where it is. */
16
+ export type NoteEntry = {
17
+ id: string;
18
+ note: FieldNote;
19
+ /** What it belongs to: «Cliente», «Impresora térmica de tickets 80 mm». */
20
+ owner: string;
21
+ /** The field or row it is anchored to — what «take me there» scrolls to. */
22
+ anchor: () => HTMLElement | null;
23
+ };
24
+ /** The notes mounted in a record. It notifies only when a note appears or goes: a note
25
+ * whose words changed is updated in place, so re-rendering a form (new `note` objects on
26
+ * every render) does not make the record render again, and again. */
27
+ export type NoteRegistry = {
28
+ /** Adds the note, or replaces its words without telling anyone. */
29
+ put: (entry: NoteEntry) => void;
30
+ /** The note went away. */
31
+ drop: (id: string) => void;
32
+ subscribe: (listener: () => void) => () => void;
33
+ snapshot: () => readonly NoteEntry[];
34
+ };
35
+ export declare function createNoteRegistry(): NoteRegistry;
36
+ /** The registry of the record around — `Record` provides it. */
37
+ export declare const NoteRegistryContext: import("react").Context<NoteRegistry | null>;
38
+ /** The notes mounted in the record around, in the order they were mounted. */
39
+ export declare function useNoteEntries(): readonly NoteEntry[];
40
+ /** What a note leaves in its field: its sentence, hidden, for the field's
41
+ * `aria-describedby`, and its entry in the record's activity — where it is listed with
42
+ * what it belongs to and taken back to on press. Nothing is drawn: the mark by the label
43
+ * (`NoteFlag`) says WHERE, the activity says WHAT. */
44
+ export declare function NoteCard({ note, id, owner, }: {
45
+ note: FieldNote;
46
+ id: string;
47
+ /** What it belongs to — the field's label. A row's card reads it from its row. */
48
+ owner?: string;
49
+ }): import("react").JSX.Element;
50
+ /** The mark next to the label: WHERE the note is. It opens the note on press. */
51
+ export declare function NoteFlag({ note }: {
52
+ note: FieldNote;
53
+ }): import("react").JSX.Element;
54
+ /** Joins ids for `aria-describedby`, dropping the absent ones. */
55
+ export declare function describedByOf(...ids: readonly (string | undefined | null)[]): string | undefined;
@@ -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 {};