@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.
- package/README.md +40 -13
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/activity/ActivityBadge.d.ts +16 -0
- package/dist/components/activity/ActivityBadge.js +29 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/activity/index.d.ts +4 -0
- package/dist/components/activity/index.js +5 -0
- package/dist/components/assist/AssistMark.d.ts +3 -3
- package/dist/components/assist/AssistMark.js +1 -1
- package/dist/components/assist/assist.d.ts +1 -1
- package/dist/components/assist/assist.js +1 -1
- package/dist/components/calendar/RecordCalendar.d.ts +48 -0
- package/dist/components/calendar/RecordCalendar.js +101 -0
- package/dist/components/calendar/index.d.ts +1 -0
- package/dist/components/calendar/index.js +1 -0
- package/dist/components/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +16 -5
- package/dist/components/gantt/RecordGantt.d.ts +44 -0
- package/dist/components/gantt/RecordGantt.js +110 -0
- package/dist/components/gantt/index.d.ts +1 -0
- package/dist/components/gantt/index.js +1 -0
- package/dist/components/index.d.ts +4 -1
- package/dist/components/index.js +4 -1
- package/dist/components/lines/LineItems.d.ts +45 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/BoardColumns.d.ts +39 -0
- package/dist/components/listing/BoardColumns.js +138 -0
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +46 -2
- package/dist/components/listing/ListingBar.js +47 -5
- package/dist/components/listing/PivotExplorer.d.ts +34 -0
- package/dist/components/listing/PivotExplorer.js +86 -0
- package/dist/components/listing/RecordCard.d.ts +6 -8
- package/dist/components/listing/RecordCard.js +13 -7
- package/dist/components/listing/Row.d.ts +25 -2
- package/dist/components/listing/Row.js +30 -13
- package/dist/components/listing/index.d.ts +2 -0
- package/dist/components/listing/index.js +2 -0
- package/dist/components/listing/set.d.ts +1 -1
- package/dist/components/navigation/index.d.ts +0 -2
- package/dist/components/navigation/index.js +2 -3
- package/dist/components/record/RecordAudit.d.ts +17 -0
- package/dist/components/record/RecordAudit.js +27 -0
- package/dist/components/record/RecordHeader.d.ts +27 -17
- package/dist/components/record/RecordHeader.js +64 -20
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/RecordRibbon.d.ts +13 -0
- package/dist/components/record/RecordRibbon.js +22 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/components/record/index.d.ts +2 -0
- package/dist/components/record/index.js +2 -0
- package/dist/components/search/CriteriaChips.d.ts +6 -1
- package/dist/components/search/CriteriaChips.js +71 -20
- package/dist/components/search/SearchBar.d.ts +4 -1
- package/dist/components/search/SearchBar.js +15 -7
- package/dist/components/timeline/RecordTimeline.d.ts +60 -0
- package/dist/components/timeline/RecordTimeline.js +214 -0
- package/dist/components/timeline/TimelineToolbar.d.ts +30 -0
- package/dist/components/timeline/TimelineToolbar.js +31 -0
- package/dist/components/timeline/index.d.ts +3 -0
- package/dist/components/timeline/index.js +3 -0
- package/dist/components/timeline/time.d.ts +43 -0
- package/dist/components/timeline/time.js +135 -0
- package/dist/foundations/copy.d.ts +194 -18
- package/dist/foundations/copy.es.js +142 -15
- package/dist/foundations/copy.js +133 -11
- package/dist/foundations/index.d.ts +1 -1
- package/dist/foundations/match.d.ts +23 -0
- package/dist/foundations/match.js +63 -0
- package/dist/foundations/slots.d.ts +1 -1
- package/dist/foundations/slots.js +8 -3
- package/dist/foundations/types.d.ts +7 -0
- package/dist/foundations/types.js +0 -36
- package/dist/primitives/block/index.d.ts +1 -1
- package/dist/primitives/block/index.js +1 -1
- package/dist/primitives/cell/CellStack.d.ts +7 -1
- package/dist/primitives/cell/CellStack.js +5 -2
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +10 -6
- package/dist/primitives/choice/KanbanState.d.ts +14 -0
- package/dist/primitives/choice/KanbanState.js +37 -0
- package/dist/primitives/choice/Priority.d.ts +20 -0
- package/dist/primitives/choice/Priority.js +41 -0
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- package/dist/primitives/choice/Slider.d.ts +6 -1
- package/dist/primitives/choice/Slider.js +2 -2
- package/dist/primitives/choice/index.d.ts +2 -0
- package/dist/primitives/choice/index.js +2 -0
- package/dist/primitives/date/Calendar.js +30 -6
- package/dist/primitives/date/DatePicker.d.ts +11 -3
- package/dist/primitives/date/DatePicker.js +12 -8
- package/dist/primitives/date/DateTime.d.ts +35 -0
- package/dist/primitives/date/DateTime.js +100 -0
- package/dist/primitives/date/index.d.ts +1 -0
- package/dist/primitives/date/index.js +4 -2
- package/dist/primitives/dialog/Dialog.d.ts +9 -3
- package/dist/primitives/dialog/Dialog.js +41 -5
- package/dist/primitives/field/DropZone.js +1 -1
- package/dist/primitives/field/Field.d.ts +14 -2
- package/dist/primitives/field/Field.js +25 -14
- package/dist/primitives/field/copy.d.ts +16 -0
- package/dist/primitives/field/copy.js +69 -0
- package/dist/primitives/field/index.d.ts +2 -0
- package/dist/primitives/field/index.js +4 -0
- package/dist/primitives/field/note.d.ts +55 -0
- package/dist/primitives/field/note.js +117 -0
- package/dist/primitives/index.d.ts +1 -0
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/menu/Menu.d.ts +28 -0
- package/dist/primitives/menu/Menu.js +47 -17
- package/dist/primitives/notice/Callout.js +1 -1
- package/dist/primitives/notice/Flag.d.ts +13 -0
- package/dist/primitives/notice/Flag.js +31 -0
- package/dist/primitives/notice/index.d.ts +2 -1
- package/dist/primitives/notice/index.js +2 -1
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +69 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/MatchedText.d.ts +10 -0
- package/dist/primitives/picker/MatchedText.js +12 -0
- package/dist/primitives/picker/Picker.d.ts +67 -4
- package/dist/primitives/picker/Picker.js +142 -23
- package/dist/primitives/table/CellCopy.d.ts +20 -0
- package/dist/primitives/table/CellCopy.js +23 -0
- package/dist/primitives/table/DataTable.d.ts +55 -1
- package/dist/primitives/table/DataTable.js +126 -40
- package/dist/primitives/table/EditableTable.d.ts +102 -2
- package/dist/primitives/table/EditableTable.js +287 -32
- package/dist/primitives/table/ReportTable.js +2 -1
- package/dist/primitives/table/RowActions.js +7 -2
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- package/dist/primitives/table/Table.d.ts +5 -0
- package/dist/primitives/table/Table.js +17 -8
- package/dist/primitives/table/index.d.ts +2 -2
- package/dist/primitives/table/index.js +1 -1
- package/dist/primitives/tabs/Tabs.d.ts +2 -2
- package/dist/primitives/tabs/Tabs.js +1 -1
- package/dist/primitives/tag/Tags.d.ts +2 -0
- package/dist/primitives/tag/Tags.js +2 -1
- package/dist/primitives/tag/TagsField.d.ts +13 -0
- package/dist/primitives/tag/TagsField.js +39 -10
- package/dist/primitives/tag/choices.d.ts +2 -0
- package/dist/primitives/tree/Tree.d.ts +56 -0
- package/dist/primitives/tree/Tree.js +74 -0
- package/dist/primitives/tree/index.d.ts +1 -0
- package/dist/primitives/tree/index.js +3 -0
- package/dist/primitives/value/Avatar.d.ts +16 -0
- package/dist/primitives/value/Avatar.js +91 -0
- package/dist/primitives/value/Copyable.d.ts +11 -0
- package/dist/primitives/value/Copyable.js +27 -6
- package/dist/primitives/value/Fecha.js +10 -1
- package/dist/primitives/value/Person.d.ts +6 -51
- package/dist/primitives/value/Person.js +9 -148
- package/dist/primitives/value/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +22 -5
- package/dist/primitives/value/Relation.js +42 -14
- package/dist/primitives/value/Totals.d.ts +102 -7
- package/dist/primitives/value/Totals.js +52 -2
- package/dist/primitives/value/Value.d.ts +0 -37
- package/dist/primitives/value/Value.js +0 -39
- package/dist/primitives/value/contact.d.ts +30 -0
- package/dist/primitives/value/contact.js +51 -0
- package/dist/primitives/value/index.d.ts +5 -4
- package/dist/primitives/value/index.js +5 -4
- package/dist/tokens.css +179 -7
- package/dist/views/list/List.d.ts +32 -3
- package/dist/views/list/List.js +51 -7
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +70 -13
- package/dist/views/list/columns.js +8 -1
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +5 -2
- package/dist/views/list/presentations.js +35 -3
- package/dist/views/list/types.d.ts +57 -1
- package/dist/views/list/useNarrowList.d.ts +10 -0
- package/dist/views/list/useNarrowList.js +20 -6
- package/dist/views/record/Record.d.ts +41 -8
- package/dist/views/record/Record.js +113 -28
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +15 -4
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.d.ts +0 -3
- package/dist/components/chatter/index.js +0 -4
- package/dist/components/navigation/CommandPalette.d.ts +0 -40
- package/dist/components/navigation/CommandPalette.js +0 -114
- package/dist/components/navigation/commands.d.ts +0 -30
- package/dist/components/navigation/commands.js +0 -30
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /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 }),
|
|
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/
|
|
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
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
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
|
+
}
|
package/dist/primitives/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type ButtonRole, type ButtonSize } from "../button/Button.js";
|
|
2
2
|
import type { LucideIcon } from "lucide-react";
|
|
3
3
|
import type { ReactNode } from "react";
|
|
4
|
+
import { type Key } from "react-aria-components";
|
|
4
5
|
export interface MenuOption {
|
|
5
6
|
id: string;
|
|
6
7
|
label: string;
|
|
@@ -27,6 +28,16 @@ export interface MenuOption {
|
|
|
27
28
|
* menu built from a registry —twenty-five operations, each one with its own
|
|
28
29
|
* action— has no reasonable `switch` on the other side. */
|
|
29
30
|
onSelect?: () => void;
|
|
31
|
+
/** The keys that run this option without opening the menu, as the user reads them
|
|
32
|
+
* —«Ctrl+P», «⌘D»—. Drawn at the end of the row and linked to it for a screen reader.
|
|
33
|
+
* It is a HINT: binding the keys is the application's job, because the menu does not
|
|
34
|
+
* exist until it is opened. In an ERP the frequent verbs have a shortcut, and the menu
|
|
35
|
+
* is where people learn it. */
|
|
36
|
+
shortcut?: string;
|
|
37
|
+
/** The option opens a SUBMENU with these options instead of running. Its own `onSelect`
|
|
38
|
+
* and `checked` are ignored — a row that opens something does not also do something.
|
|
39
|
+
* Children may carry `items` too, but a third level is usually a dialog in disguise. */
|
|
40
|
+
items?: readonly MenuOption[];
|
|
30
41
|
}
|
|
31
42
|
export interface MenuGroup {
|
|
32
43
|
/** The group's label, in the brand voice. Without a label, the group is just
|
|
@@ -51,6 +62,9 @@ declare const WIDTH: {
|
|
|
51
62
|
readonly md: "min-w-56 max-w-80";
|
|
52
63
|
readonly lg: "min-w-80 max-w-(--container-short)";
|
|
53
64
|
};
|
|
65
|
+
/** Every option of the tree by id, children included: a submenu is a separate React Aria
|
|
66
|
+
* menu with its own `onAction`, and both have to reach the same handlers. */
|
|
67
|
+
export declare function indexOptions(options: readonly MenuOption[], into?: Map<string, MenuOption>): Map<string, MenuOption>;
|
|
54
68
|
export declare function Menu({ label, groups, onSelect, surface, align, icon: Trigger,
|
|
55
69
|
/** The menu as a control of a list ("filters ▾") carries the chevron; the
|
|
56
70
|
* actions `⋯` does not, because the icon already says there is more. */
|
|
@@ -97,4 +111,18 @@ chevron, active, iconOnly, variant, searchPlaceholder, emptyText, width, role, s
|
|
|
97
111
|
* each click turns three hides into three open-close round trips. */
|
|
98
112
|
closeOnSelect?: boolean;
|
|
99
113
|
}): import("react").JSX.Element;
|
|
114
|
+
/**
|
|
115
|
+
* The panel of a `Menu`: its groups and rows, and the submenus hanging from them. Internal —
|
|
116
|
+
* it is not exported from the family — and separate from the trigger for two reasons: a
|
|
117
|
+
* submenu is this same panel one level down, and a panel can be rendered on the server
|
|
118
|
+
* without the trigger's popover, which is how the tests see the rows.
|
|
119
|
+
*/
|
|
120
|
+
export declare function MenuList({ label, groups, onAction, closeOnSelect, emptyText, width, }: {
|
|
121
|
+
label: string;
|
|
122
|
+
groups: readonly MenuGroup[];
|
|
123
|
+
onAction: (key: Key) => void;
|
|
124
|
+
closeOnSelect: boolean;
|
|
125
|
+
emptyText: string;
|
|
126
|
+
width?: keyof typeof WIDTH;
|
|
127
|
+
}): import("react").JSX.Element;
|
|
100
128
|
export {};
|