@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,90 @@
1
+ import { type DialogContext, type DialogWidth } from "./Dialog.js";
2
+ import { type ReactNode, type RefObject } from "react";
3
+ /** Where the drill was opened from: the page's record. The first crumb of the trail. */
4
+ export type DrillOrigin = DialogContext;
5
+ /** Which contexts are open, in order, and the ways to move between them. */
6
+ export interface DrillState {
7
+ /** The ids of the open contexts, from the first opened to the one in front. */
8
+ readonly path: readonly string[];
9
+ /** One level deeper. An id already on the path goes back to it instead: a product
10
+ * that leads to its supplier that leads to the same product is a loop, not depth. */
11
+ open: (id: string) => void;
12
+ /** Up one level. On the first one, it closes. */
13
+ back: () => void;
14
+ /** Back to the context with this id, closing the ones in front of it. */
15
+ to: (id: string) => void;
16
+ /** Close every level: back to the page. */
17
+ close: () => void;
18
+ /** Internal: the control that opened each level, where focus returns when the level
19
+ * above it closes. Written by `open`, read by `Drill`. */
20
+ readonly focusMarks: RefObject<(HTMLElement | null)[]>;
21
+ }
22
+ /** One level deeper: push the id — or, when it is already open, go back to it. Internal,
23
+ * exported for the test. */
24
+ export declare function openPath(path: readonly string[], id: string): readonly string[];
25
+ /** Back to the context with this id; an id not on the path changes nothing. */
26
+ export declare function toPath(path: readonly string[], id: string): readonly string[];
27
+ /** The written contexts that are open, in the order of the path. A context on the path
28
+ * that is not written is skipped: there is nothing to draw for it. */
29
+ export declare function framesOf(children: ReactNode, path: readonly string[]): DrillContextProps[];
30
+ /** The state of a `Drill`: the path of open contexts. Pass it to `<Drill state={…}>` and
31
+ * call `open(id)` from wherever the context is opened — a relation's «Abrir», a
32
+ * `Picker`'s open button, a rail action, a line's cell. */
33
+ export declare function useDrill(initial?: readonly string[]): DrillState;
34
+ export interface DrillContextProps {
35
+ /** What `open(id)` names. Unique inside the drill. */
36
+ id: string;
37
+ /** The context's name: the title while it is in front, its crumb when it is behind.
38
+ * Short — «Línea 3», «Impresora térmica 80 mm», «Registrar pago». */
39
+ title: string;
40
+ /** One sentence under the title; also the dialog's description while in front. */
41
+ subtitle?: string;
42
+ /** This context's actions — Cancel and ONE primary. Drawn only while it is in front. */
43
+ footer?: ReactNode;
44
+ /** No padding of its own: for a child that is already a surface with its own margins
45
+ * (a `Record`). A form or a wizard keeps the padding. */
46
+ flush?: boolean;
47
+ children: ReactNode;
48
+ }
49
+ export interface DrillProps {
50
+ state: DrillState;
51
+ /** The page's record, as the first crumb: «pedido · S00142». Pressing it closes. */
52
+ origin?: DrillOrigin;
53
+ /** One width for every level: a window that changes width at each step jumps under the
54
+ * pointer. `lg` by default — a record with pages and lines needs it. */
55
+ width?: DialogWidth;
56
+ /** The `Drill.Context`s. Which ones are open is the state's path. */
57
+ children: ReactNode;
58
+ }
59
+ /**
60
+ * The inside of the window: the trail, the context in front (and the ones behind it,
61
+ * hidden), and the footer of the one in front.
62
+ *
63
+ * Internal — exported only so a test can read its markup, which the modal (a portal)
64
+ * does not produce on the server.
65
+ */
66
+ export declare function DrillSurface({ frames, origin, onBack, onTo, onClose, focusMarks, }: {
67
+ frames: readonly DrillContextProps[];
68
+ origin?: DrillOrigin;
69
+ onBack: () => void;
70
+ onTo: (id: string) => void;
71
+ onClose: () => void;
72
+ focusMarks?: RefObject<(HTMLElement | null)[]>;
73
+ }): import("react").JSX.Element | null;
74
+ /**
75
+ * ONE dialog that holds a stack of contexts opened from a record — and from each other —
76
+ * with a trail back to the page. See the header of this file.
77
+ *
78
+ * const drill = useDrill();
79
+ * …<RelationValue name={product} onOpen={() => drill.open("product")} />…
80
+ * <Drill state={drill} origin={{ kind: "pedido", label: "S00142" }}>
81
+ * <Drill.Context id="product" title={product} flush>
82
+ * <Record identity={…}>…</Record>
83
+ * </Drill.Context>
84
+ * <Drill.Context id="supplier" title={supplier} footer={…}>…</Drill.Context>
85
+ * </Drill>
86
+ */
87
+ export declare function Drill({ state, origin, width, children }: DrillProps): import("react").JSX.Element;
88
+ export declare namespace Drill {
89
+ var Context: ((props: DrillContextProps) => ReactNode) & import("../../foundations/slots.js").Slotted<"context">;
90
+ }
@@ -0,0 +1,172 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ import { flatten, propsOf, slot, slotOf } from "../../foundations/slots.js";
5
+ import { cn, focusRing, tv } from "../../foundations/variants.js";
6
+ import { IconButton } from "../button/Button.js";
7
+ import { dialogStyles, } from "./Dialog.js";
8
+ import { ArrowLeft, ChevronRight, X } from "lucide-react";
9
+ import { useEffect, useId, useRef, useState } from "react";
10
+ import { useKeyboard } from "react-aria";
11
+ import { Dialog as AriaDialog, Heading, Link, Modal, ModalOverlay, } from "react-aria-components";
12
+ function focused() {
13
+ if (typeof document === "undefined")
14
+ return null;
15
+ return document.activeElement instanceof HTMLElement ? document.activeElement : null;
16
+ }
17
+ /** One level deeper: push the id — or, when it is already open, go back to it. Internal,
18
+ * exported for the test. */
19
+ export function openPath(path, id) {
20
+ const at = path.indexOf(id);
21
+ return at === -1 ? [...path, id] : path.slice(0, at + 1);
22
+ }
23
+ /** Back to the context with this id; an id not on the path changes nothing. */
24
+ export function toPath(path, id) {
25
+ const at = path.indexOf(id);
26
+ return at === -1 ? path : path.slice(0, at + 1);
27
+ }
28
+ /** The written contexts that are open, in the order of the path. A context on the path
29
+ * that is not written is skipped: there is nothing to draw for it. */
30
+ export function framesOf(children, path) {
31
+ const written = flatten(children)
32
+ .filter((node) => slotOf(node) === "context")
33
+ .map((node) => propsOf(node))
34
+ .filter((props) => props !== undefined);
35
+ return path
36
+ .map((id) => written.find((context) => context.id === id))
37
+ .filter((frame) => frame !== undefined);
38
+ }
39
+ /** The state of a `Drill`: the path of open contexts. Pass it to `<Drill state={…}>` and
40
+ * call `open(id)` from wherever the context is opened — a relation's «Abrir», a
41
+ * `Picker`'s open button, a rail action, a line's cell. */
42
+ export function useDrill(initial = []) {
43
+ const [path, setPath] = useState(initial);
44
+ const focusMarks = useRef([]);
45
+ return {
46
+ path,
47
+ open: (id) => {
48
+ if (!path.includes(id))
49
+ focusMarks.current[path.length] = focused();
50
+ setPath(openPath(path, id));
51
+ },
52
+ back: () => setPath(path.slice(0, -1)),
53
+ to: (id) => setPath(toPath(path, id)),
54
+ close: () => setPath([]),
55
+ focusMarks,
56
+ };
57
+ }
58
+ const crumb = tv({
59
+ extend: focusRing,
60
+ base: "block max-w-(--container-crumb) min-w-0 cursor-pointer truncate rounded-mark text-muted transition-colors duration-(--duration-instant) hover:text-ink hover:underline hover:underline-offset-4",
61
+ });
62
+ /**
63
+ * The inside of the window: the trail, the context in front (and the ones behind it,
64
+ * hidden), and the footer of the one in front.
65
+ *
66
+ * Internal — exported only so a test can read its markup, which the modal (a portal)
67
+ * does not produce on the server.
68
+ */
69
+ export function DrillSurface({ frames, origin, onBack, onTo, onClose, focusMarks, }) {
70
+ const copy = useUiCopy();
71
+ const depth = frames.length;
72
+ const top = frames[depth - 1];
73
+ const parent = depth > 1 ? frames[depth - 2] : undefined;
74
+ const bodies = useRef(new Map());
75
+ const was = useRef(depth);
76
+ const subtitleId = useId();
77
+ const { keyboardProps } = useKeyboard({
78
+ onKeyDown: (e) => {
79
+ if (e.key !== "Escape") {
80
+ e.continuePropagation();
81
+ return;
82
+ }
83
+ if (depth > 1)
84
+ onBack();
85
+ else
86
+ onClose();
87
+ },
88
+ });
89
+ /* Focus follows the level. Down: into the new context, so the keyboard does not stay on
90
+ a control that is now hidden. Up: back to what opened the level that closed. */
91
+ const topId = top?.id;
92
+ useEffect(() => {
93
+ const before = was.current;
94
+ was.current = depth;
95
+ if (before === depth || depth === 0 || topId === undefined)
96
+ return;
97
+ const box = bodies.current.get(topId);
98
+ if (depth > before) {
99
+ box?.focus();
100
+ return;
101
+ }
102
+ const mark = focusMarks?.current[depth];
103
+ (mark?.isConnected === true ? mark : box)?.focus();
104
+ }, [depth, topId, focusMarks]);
105
+ if (top === undefined)
106
+ return null;
107
+ const trail = [
108
+ ...(origin === undefined
109
+ ? []
110
+ : [
111
+ {
112
+ key: "\u0000origin",
113
+ label: (_jsxs(_Fragment, { children: [origin.kind === undefined ? null : (_jsxs("span", { className: "font-medium", children: [origin.kind, " "] })), _jsx("span", { className: "text-ink", children: origin.label })] })),
114
+ onPress: onClose,
115
+ },
116
+ ]),
117
+ ...frames.slice(0, -1).map((frame) => ({
118
+ key: frame.id,
119
+ label: frame.title,
120
+ onPress: () => onTo(frame.id),
121
+ })),
122
+ ];
123
+ return (_jsx(AriaDialog, { ...(top.subtitle === undefined ? {} : { "aria-describedby": subtitleId }), className: "flex max-h-[inherit] min-h-0 flex-1 flex-col outline-0", children: _jsxs("div", { ...keyboardProps, className: "flex max-h-[inherit] min-h-0 flex-1 flex-col", children: [_jsxs("header", { className: "flex shrink-0 items-start gap-2 border-b border-line px-4 py-3", children: [parent === undefined ? null : (_jsx("span", { className: "-ml-1.5 shrink-0", children: _jsx(IconButton, { icon: ArrowLeft, label: fillCopy(copy.drill.back, { name: parent.title }), onClick: onBack }) })), _jsxs("div", { className: "min-w-0 flex-1", children: [trail.length === 0 ? null : (
124
+ /* Where this was opened from, small and on top — the same place `Dialog`
125
+ says what it is about. Every crumb is a way back; the one in front is
126
+ not a crumb, it is the title. */
127
+ _jsx("nav", { "aria-label": copy.drill.trail, className: "mb-0.5 flex min-w-0", children: _jsx("ol", { className: "flex min-w-0 flex-wrap items-center gap-x-1 gap-y-0.5 text-micro", children: trail.map((one, i) => (_jsxs("li", { className: "flex min-w-0 items-center gap-1", children: [i === 0 ? null : (_jsx(ChevronRight, { className: "size-(--size-icon-sm) shrink-0 text-line-strong", strokeWidth: 2, "aria-hidden": true })), _jsx(Link, { onPress: one.onPress, className: ({ isFocusVisible }) => crumb({ isFocusVisible }), children: one.label })] }, one.key))) }) })), _jsx(Heading, { slot: "title", className: "truncate text-title font-semibold text-ink", children: top.title }), top.subtitle === undefined ? null : (_jsx("p", { id: subtitleId, className: "mt-1 max-w-prose text-meta text-muted", children: top.subtitle }))] }), _jsx(IconButton, { icon: X, label: copy.shell.close, onClick: onClose })] }), frames.map((frame) => {
128
+ const front = frame.id === top.id;
129
+ return (
130
+ /* Each level scrolls on its own and the ones behind are `hidden`, not gone:
131
+ their state — a half-typed payment, the open tab of a product — is still
132
+ there when the level above closes. */
133
+ _jsx("div", { ref: (node) => {
134
+ if (node === null)
135
+ bodies.current.delete(frame.id);
136
+ else
137
+ bodies.current.set(frame.id, node);
138
+ }, hidden: !front, tabIndex: -1, "data-drill-context": frame.id, className: cn("min-h-0 flex-1 overflow-y-auto outline-0", frame.flush !== true && "px-4 py-4", front && frame.footer === undefined && "max-sm:safe-bottom"), children: frame.children }, frame.id));
139
+ }), top.footer === undefined ? null : (_jsx("footer", { className: "flex shrink-0 flex-wrap items-center justify-end gap-2 border-t border-line bg-ground px-4 py-3 max-sm:safe-bottom", children: top.footer }))] }) }));
140
+ }
141
+ /** One context of a drill: a title, what goes inside, and its own footer. It does not
142
+ * draw where it is written — the `Drill` draws it when its id is on the path. */
143
+ const DrillContextSlot = slot("context", (_props) => null);
144
+ /**
145
+ * ONE dialog that holds a stack of contexts opened from a record — and from each other —
146
+ * with a trail back to the page. See the header of this file.
147
+ *
148
+ * const drill = useDrill();
149
+ * …<RelationValue name={product} onOpen={() => drill.open("product")} />…
150
+ * <Drill state={drill} origin={{ kind: "pedido", label: "S00142" }}>
151
+ * <Drill.Context id="product" title={product} flush>
152
+ * <Record identity={…}>…</Record>
153
+ * </Drill.Context>
154
+ * <Drill.Context id="supplier" title={supplier} footer={…}>…</Drill.Context>
155
+ * </Drill>
156
+ */
157
+ export function Drill({ state, origin, width = "lg", children }) {
158
+ /* While the window fades out the path is already empty: it keeps drawing the last levels
159
+ it had, so the content does not vanish before the window does. */
160
+ const [lastPath, setLastPath] = useState(state.path);
161
+ if (state.path.length > 0 && state.path !== lastPath)
162
+ setLastPath(state.path);
163
+ const path = state.path.length > 0 ? state.path : lastPath;
164
+ const frames = framesOf(children, path);
165
+ return (_jsx(ModalOverlay, { isOpen: state.path.length > 0, onOpenChange: (next) => {
166
+ if (!next)
167
+ state.close();
168
+ }, isDismissable: true,
169
+ /* Escape is ours: up one level, not out of everything. See the header. */
170
+ isKeyboardDismissDisabled: true, className: dialogStyles.overlay, children: _jsx(Modal, { className: (s) => dialogStyles.modal({ ...s, width }), children: _jsx(DrillSurface, { frames: frames, ...(origin === undefined ? {} : { origin }), onBack: state.back, onTo: state.to, onClose: state.close, focusMarks: state.focusMarks }) }) }));
171
+ }
172
+ Drill.Context = DrillContextSlot;
@@ -1 +1,2 @@
1
1
  export { Dialog, type DialogContext, type DialogProps, type DialogWidth, Wizard, type WizardProps, type WizardStep, } from "./Dialog.js";
2
+ export { Drill, type DrillContextProps, type DrillOrigin, type DrillProps, type DrillState, useDrill, } from "./Drill.js";
@@ -1,2 +1,4 @@
1
1
  /* What has to be answered before moving on. */
2
2
  export { Dialog, Wizard, } from "./Dialog.js";
3
+ /* One dialog that holds a stack of contexts opened from a record, and from each other. */
4
+ export { Drill, useDrill, } from "./Drill.js";
@@ -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;