@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
@@ -1,5 +1,5 @@
1
1
  "use client";
2
- import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { Breadcrumb } from "./Breadcrumb.js";
4
4
  import { DirtyActions, } from "./DirtyActions.js";
5
5
  import { isPrimary, ThumbBar, thumbPlan } from "./ThumbBar.js";
@@ -7,8 +7,7 @@ import { byDensity, useDensity } from "../../foundations/density.js";
7
7
  import { SurfaceProvider } from "../../foundations/surface.js";
8
8
  import { Line } from "../../primitives/block/Line.js";
9
9
  import { Tag } from "../../primitives/tag/Tag.js";
10
- import { cloneElement, isValidElement } from "react";
11
- export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, chatter, chatterLayout = "page", children, }) {
10
+ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Icon, title, subtitle, tag, figure, railSurface = "canvas", smart, aspects, dirty, activity, activityLayout = "auto", blocking, indicator, children, }) {
12
11
  const d = useDensity();
13
12
  const hasRail = crumbs !== undefined ||
14
13
  pager !== undefined ||
@@ -24,6 +23,8 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
24
23
  only thing in the rail that belongs to the RECORD and not to my position in
25
24
  front of it. */
26
25
  const withRail = hasRail && d === "comfortable";
26
+ /* The activity column at the RIGHT of the sheet when the page is wide (THE PAGE). */
27
+ const beside = activity !== undefined && activityLayout === "auto";
27
28
  /* The thumb bar's content (see THE THUMB BAR in `ThumbBar`): dirty first, else the primary. */
28
29
  const { rail: railActions, primaries, mode: thumb, } = thumbPlan({ withRail, actions, dirty });
29
30
  /* In compact the TITLE is not drawn either, for the same reason as in
@@ -45,7 +46,7 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
45
46
  pager !== undefined ||
46
47
  actions !== undefined ||
47
48
  dirty !== undefined);
48
- const identity = (_jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-2.5 px-4 pt-3.5", "gap-2 px-0 pt-0")}`, children: [withRail || smart === undefined ? null : _jsx("div", { className: "min-w-0", children: smart }), _jsxs("div", { className: "flex min-w-0 flex-wrap items-start gap-x-5 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2.5", children: [Icon === undefined || d === "compact" ? null : (_jsx("span", { className: "mt-1 flex size-9 shrink-0 items-center justify-center rounded-box bg-ink/5 text-muted", children: _jsx(Icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75 }) })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [kicker === undefined || !withIdentity ? null : (_jsx("span", { className: "text-micro font-medium text-muted", children: kicker })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1", children: [!withIdentity ? null : (
49
+ const identity = (_jsxs("div", { className: `flex flex-col ${byDensity(d, "gap-2.5 px-4 pt-3.5", "gap-2 px-0 pt-0")}`, children: [withRail || (smart === undefined && indicator === undefined) ? null : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-2", children: [smart, indicator] })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-start gap-x-5 gap-y-2", children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2.5", children: [Icon === undefined || d === "compact" ? null : (_jsx("span", { className: "mt-1 flex size-9 shrink-0 items-center justify-center rounded-box bg-ink/5 text-muted", children: _jsx(Icon, { className: "size-(--size-icon-lg)", strokeWidth: 1.75 }) })), _jsxs("div", { className: "flex min-w-0 flex-col", children: [kicker === undefined || !withIdentity ? null : (_jsx("span", { className: "text-micro font-medium text-muted", children: kicker })), _jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-x-3 gap-y-1", children: [!withIdentity ? null : (
49
50
  /* `display`, not `title`: the record's name is the entry point of the
50
51
  screen. At the title size (19 px over a 13 px body, 1.46:1) nothing on
51
52
  the sheet stood out and the eye had nowhere to start. */
@@ -53,20 +54,51 @@ export function RecordHeader({ crumbs, pager, actions, status, kicker, icon: Ico
53
54
  /* The line is the sheet's mouth. In comfortable it walks to the
54
55
  edges; the current tab sits on it and opens into the page below. */
55
56
  _jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspects }))] }));
56
- return (_jsxs(_Fragment, { children: [withRail ? (_jsx(SurfaceProvider, { value: railSurface, children: _jsx("div", { className: "@container/record", children: _jsxs("div", { className: `flex flex-col gap-2 px-3.5 py-2.5 ${railSurface === "chrome" ? "bg-chrome" : "border-b border-line bg-surface"}`, children: [_jsx(Line, { start: crumbs === undefined || crumbs.length === 0 ? null : (_jsx(Breadcrumb, { crumbs: crumbs, surface: railSurface })), center: smart, end: pager }), actions === undefined && status === undefined && dirty === undefined ? null : (
57
+ return (_jsxs(_Fragment, { children: [withRail ? (_jsx(SurfaceProvider, { value: railSurface, children: _jsx("div", { className: "@container/record", children: _jsxs("div", { className: `flex flex-col gap-2 px-3.5 py-2.5 ${railSurface === "chrome" ? "bg-chrome" : "border-b border-line bg-surface"}`, children: [_jsx(Line, { start: crumbs === undefined || crumbs.length === 0 ? null : (_jsx(Breadcrumb, { crumbs: crumbs, surface: railSurface })), center:
58
+ /* The notices' indicator rides with the counters: both are about the
59
+ record and both open something; neither is my position in front of
60
+ it. The line wraps on a phone, so it is never pushed out of reach. */
61
+ smart === undefined && indicator === undefined ? undefined : (_jsxs("div", { className: "flex min-w-0 flex-wrap items-center justify-center gap-2", children: [smart, indicator] })), end: pager }), actions === undefined && status === undefined && dirty === undefined ? null : (
57
62
  /* Row 2 — WHAT I CAN DO and WHERE IT IS AT: actions stuck to the
58
63
  left edge, the history and the state stuck to the right one. No
59
64
  `flex-wrap`: wrapping split the row in two and the state ended up
60
65
  below the actions, which is another hierarchy. */
61
66
  _jsxs("div", { className: "flex min-w-0 items-start gap-x-4", children: [dirty !== undefined ? (_jsx("div", { className: "flex shrink-0 items-center gap-1.5 @max-handheld/record:hidden", children: _jsx(DirtyActions, { ...dirty }) })) : actions === undefined ? null : (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: primaries.length === 0
62
67
  ? actions
63
- : railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)) })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) })) : null, _jsx("div", { className: paper ? "bg-ground px-3 pt-3 pb-3" : "", children: _jsx("div", { className: chatter !== undefined && chatterLayout === "aside" ? "@container" : "", children: _jsxs("div", { className: chatter !== undefined && chatterLayout === "aside"
64
- ? "flex min-w-0 flex-col gap-3 @4xl:flex-row @4xl:items-start"
65
- : "", children: [_jsxs("div", { className: paper
66
- ? `flex w-full max-w-(--container-sheet) min-w-0 flex-1 flex-col overflow-clip rounded-box border border-line bg-surface ${chatter !== undefined && chatterLayout === "aside" ? "" : "mx-auto"}`
67
- : "flex min-w-0 flex-1 flex-col bg-surface", children: [identity, children, chatter !== undefined && chatterLayout === "page" ? (_jsx("div", { className: "border-t border-line", children: isValidElement(chatter)
68
- ? cloneElement(chatter, {
69
- embedded: true,
70
- })
71
- : chatter })) : null] }), chatter !== undefined && chatterLayout === "aside" ? chatter : null] }) }) }), thumb === null ? null : _jsx(ThumbBar, { mode: thumb, dirty: dirty, primaries: primaries })] }));
68
+ : railActions.map((node) => isPrimary(node) ? (_jsx("span", { "data-thumb-moves": "", className: "contents @max-handheld/record:hidden", children: node }, node.key)) : (node)) })), status === undefined ? null : (_jsx("div", { className: "ml-auto flex min-w-0 flex-col items-end gap-1.5", children: status }))] }))] }) }) })) : null, paper ? (
69
+ /* ==================================================================== *
70
+ * THE PAGE — [sheet | Actividad], on the ground.
71
+ *
72
+ * Two columns, joined, placed together and centred: the sheet
73
+ * (≤ --container-sheet, 72rem) and, at its RIGHT, the record's ACTIVITY
74
+ * (--container-activity, 22rem) — no strip of ground between them, so the page
75
+ * reads left to right as one paper: the record, then what it has. The pair is at
76
+ * most --container-record (94rem) wide; on a 1680 px desk both fit whole.
77
+ *
78
+ * A note of a field or a row is not drawn in the column: its mark (`Flag`) stays
79
+ * by its label or at its row — WHERE — and the activity lists it with the record's
80
+ * notices above the timeline — WHAT —, taking you to it on press. Cards aligned in
81
+ * the column were tried and dropped: they and the history fought for one column,
82
+ * and the history is what everybody reads.
83
+ *
84
+ * As the page narrows: below --container-beside (76rem) the activity goes BELOW
85
+ * the sheet as a card at the sheet's width.
86
+ *
87
+ * Folded, the column is a slim strip with its counts; the pair re-centres and the
88
+ * sheet keeps its width. A container query, not the window: with four panels open
89
+ * each is 380 px on a 1,600 screen.
90
+ *
91
+ * The sheet clips with `overflow-clip`, not `overflow-hidden`: hidden would make it
92
+ * a scroll container and the line table header would stick inside it, not under
93
+ * the application bar.
94
+ * ==================================================================== */
95
+ _jsx("div", { className: "bg-ground px-3 pt-3 pb-3", children: _jsx("div", { className: "@container/page", children: _jsxs("div", { className: `mx-auto flex min-w-0 flex-col gap-3 ${beside
96
+ ? "max-w-(--container-record) @beside/page:flex-row @beside/page:items-stretch @beside/page:justify-center @beside/page:gap-0"
97
+ : "max-w-(--container-sheet)"}`, children: [_jsxs("div", { className: `flex min-w-0 flex-1 flex-col gap-3 ${beside ? "mx-auto w-full max-w-(--container-sheet) @beside/page:mx-0" : ""}`, children: [blocking, _jsxs("div", { className: `flex min-w-0 flex-col overflow-clip rounded-box border border-line bg-surface ${beside ? "@beside/page:rounded-r-none" : ""}`, children: [identity, children] }), activity !== undefined && activityLayout === "below" ? activity : null] }), beside ? (
98
+ /* After the sheet: the record first, what is around it second — in the
99
+ reading order and on screen. */
100
+ _jsx("div", { className: "mx-auto w-full max-w-(--container-sheet) min-w-0 @beside/page:mx-0 @beside/page:flex @beside/page:w-auto @beside/page:max-w-none @beside/page:shrink-0", children: activity })) : null] }) }) })) : (
101
+ /* No paper (a dialog, a drawer, a `Drill` context): one surface, no ground beside
102
+ it — the band first, the record, and the activity below. */
103
+ _jsxs("div", { className: "flex min-w-0 flex-1 flex-col bg-surface", children: [blocking === undefined ? null : (_jsx("div", { className: byDensity(d, "px-4 pt-3.5", "pb-2"), children: blocking })), identity, children, activity === undefined ? null : (_jsx("div", { className: byDensity(d, "px-4 pb-4", "pb-2"), children: activity }))] })), thumb === null ? null : _jsx(ThumbBar, { mode: thumb, dirty: dirty, primaries: primaries })] }));
72
104
  }
@@ -0,0 +1,37 @@
1
+ import { type CalloutProps } from "../../primitives/notice/Callout.js";
2
+ import { type ReactNode } from "react";
3
+ export type RecordNoticeSpec = Omit<CalloutProps, "dismissLabel">;
4
+ /** The notices in the order they are drawn: by urgency, written order inside a tone. */
5
+ export declare function orderNotices<T extends {
6
+ tone?: CalloutProps["tone"];
7
+ }>(notices: readonly T[]): T[];
8
+ /** Blocking (`bad`) and the rest, each already in urgency order. */
9
+ export declare function splitNotices(notices: readonly RecordNoticeSpec[]): {
10
+ blocking: RecordNoticeSpec[];
11
+ rest: RecordNoticeSpec[];
12
+ };
13
+ /** The band of what is wrong: the operation error first, then the `bad` notices. */
14
+ export declare function RecordBlocking({ notices, error, errorHeading, }: {
15
+ notices: readonly RecordNoticeSpec[];
16
+ /** The sentence the backend wrote about the last operation. Drawn first, whole. */
17
+ error?: string | null;
18
+ errorHeading?: string;
19
+ }): ReactNode;
20
+ /** The indicator in the rail: the count of the record's notices, toned by the most urgent,
21
+ * that opens their list on press. Nothing is drawn without notices. */
22
+ export declare function RecordNoticeIndicator({ notices, anchored, reveal, }: {
23
+ /** Not blocking, already in urgency order. */
24
+ notices: readonly RecordNoticeSpec[];
25
+ /** The notes of fields and rows: counted too when there is a panel that lists them —
26
+ * without one they are only the marks by their fields, and the popover leaves them out. */
27
+ anchored?: readonly {
28
+ tone?: "bad" | "warn" | "info" | "good";
29
+ }[];
30
+ /** The record has an ACTIVITY panel, and the notices live there: the indicator goes to
31
+ * them (opens the panel, scrolls to its notices, moves the focus) instead of floating
32
+ * a second copy in a popover. `target` is the id of that section. */
33
+ reveal?: {
34
+ target: string;
35
+ onReveal: () => void;
36
+ };
37
+ }): ReactNode;
@@ -0,0 +1,75 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
4
+ import { useSurface } from "../../foundations/surface.js";
5
+ import { focusRing, tv } from "../../foundations/variants.js";
6
+ import { Callout } from "../../primitives/notice/Callout.js";
7
+ import { Failed } from "../../primitives/notice/Notice.js";
8
+ import { Popover } from "../../primitives/overlay/Popover.js";
9
+ import { tagTone } from "../../primitives/tag/Tag.js";
10
+ import { CircleCheck, Info, TriangleAlert } from "lucide-react";
11
+ import { useState } from "react";
12
+ import { Button, Dialog, DialogTrigger, Heading } from "react-aria-components";
13
+ const URGENCY = {
14
+ bad: 0,
15
+ warn: 1,
16
+ info: 2,
17
+ good: 3,
18
+ };
19
+ /** The notices in the order they are drawn: by urgency, written order inside a tone. */
20
+ export function orderNotices(notices) {
21
+ return notices
22
+ .map((notice, at) => ({ notice, at }))
23
+ .sort((a, b) => URGENCY[a.notice.tone ?? "info"] - URGENCY[b.notice.tone ?? "info"] || a.at - b.at)
24
+ .map(({ notice }) => notice);
25
+ }
26
+ /** Blocking (`bad`) and the rest, each already in urgency order. */
27
+ export function splitNotices(notices) {
28
+ const ordered = orderNotices(notices);
29
+ return {
30
+ blocking: ordered.filter((n) => n.tone === "bad"),
31
+ rest: ordered.filter((n) => n.tone !== "bad"),
32
+ };
33
+ }
34
+ /** The band of what is wrong: the operation error first, then the `bad` notices. */
35
+ export function RecordBlocking({ notices, error, errorHeading, }) {
36
+ const copy = useUiCopy();
37
+ const hasError = error !== undefined && error !== null;
38
+ if (notices.length === 0 && !hasError)
39
+ return null;
40
+ return (_jsxs("div", { role: "group", "aria-label": copy.record.blocking, "data-record-blocking": "", className: "flex min-w-0 flex-col gap-1.5", children: [hasError ? (_jsx(Failed, { heading: errorHeading ?? copy.failed.operation, sentence: error })) : null, notices.map(({ children, ...notice }, i) => (_jsx(Callout, { ...notice, dismissLabel: copy.shell.closeNotice, children: children }, i)))] }));
41
+ }
42
+ const ICON = {
43
+ warn: TriangleAlert,
44
+ info: Info,
45
+ good: CircleCheck,
46
+ };
47
+ const chip = tv({
48
+ extend: focusRing,
49
+ base: "inline-flex h-(--size-control-sm) shrink-0 cursor-default items-center gap-1.5 rounded-pill px-2.5 text-micro font-medium whitespace-nowrap transition-[filter] duration-(--duration-instant) hover:brightness-95 aria-expanded:brightness-95",
50
+ });
51
+ /** The indicator in the rail: the count of the record's notices, toned by the most urgent,
52
+ * that opens their list on press. Nothing is drawn without notices. */
53
+ export function RecordNoticeIndicator({ notices, anchored = [], reveal, }) {
54
+ const copy = useUiCopy();
55
+ const surface = useSurface();
56
+ const [open, setOpen] = useState(false);
57
+ const all = reveal === undefined ? notices : [...notices, ...anchored];
58
+ if (all.length === 0)
59
+ return null;
60
+ /* The most urgent of all of them gives the chip its tone; a field's `bad` note warns —
61
+ what blocks the record is the blocking band, not this chip. */
62
+ const order = ["bad", "warn", "info", "good"];
63
+ const worst = order.find((t) => all.some((n) => (n.tone ?? "info") === t)) ?? "info";
64
+ const tone = (worst === "bad" ? "warn" : worst);
65
+ const Icon = ICON[tone];
66
+ const count = fillCopy(all.length === 1 ? copy.record.noticeOne : copy.record.noticeMany, {
67
+ count: all.length.toLocaleString(copy.locale),
68
+ });
69
+ const face = (_jsxs(_Fragment, { children: [_jsx(Icon, { className: "size-(--size-icon-sm) shrink-0", strokeWidth: 2.25, "aria-hidden": true }), count] }));
70
+ const skin = tagTone(tone, surface === "chrome" ? "chrome" : "canvas");
71
+ if (reveal !== undefined) {
72
+ return (_jsx(Button, { "data-record-notices": "", "aria-controls": reveal.target, onPress: reveal.onReveal, className: ({ isFocusVisible }) => `${chip({ isFocusVisible })} ${skin}`, children: face }));
73
+ }
74
+ return (_jsxs(DialogTrigger, { isOpen: open, onOpenChange: setOpen, children: [_jsx(Button, { "data-record-notices": "", className: ({ isFocusVisible }) => `${chip({ isFocusVisible })} ${skin}`, children: face }), _jsx(Popover, { placement: "bottom", children: _jsxs(Dialog, { "aria-label": copy.record.notices, className: "flex max-h-[inherit] w-(--container-toast) flex-col gap-2 overflow-auto p-3 outline-0 max-sm:w-auto", children: [_jsx(Heading, { slot: "title", className: "text-meta font-semibold text-ink", children: copy.record.notices }), notices.map(({ children, ...notice }, i) => (_jsx(Callout, { ...notice, dismissLabel: copy.shell.closeNotice, children: children }, i)))] }) })] }));
75
+ }
@@ -38,7 +38,7 @@ import { cloneElement, isValidElement } from "react";
38
38
  * Same reason as everything else here: a record in a 380 px panel is as
39
39
  * narrow as one on a phone. The query is a NAMED container (`record`) on the
40
40
  * rail and on the bar — never on the paper: an unnamed container around the
41
- * sheet would change what `Sheet`, `SectionGrid` and the chatter measure
41
+ * sheet would change what `Sheet`, `SectionGrid` and the activity panel measure
42
42
  * against, and the desk layout with it.
43
43
  *
44
44
  * ── Why CSS, and why the primary is in the DOM twice ─────────────────
@@ -69,6 +69,13 @@ export type UiCopy = {
69
69
  /** Read aloud next to a stage that failed: the red fill alone says nothing to a
70
70
  * screen reader. */
71
71
  failed: string;
72
+ /** The title of the list of the record's notices, opened from the rail. */
73
+ notices: string;
74
+ /** The rail's indicator: how many notices, singular and plural. */
75
+ noticeOne: string;
76
+ noticeMany: string;
77
+ /** The accessible name of the band of what is wrong with the record. */
78
+ blocking: string;
72
79
  };
73
80
  shell: {
74
81
  modules: string;
@@ -105,6 +112,10 @@ export type UiCopy = {
105
112
  choice: {
106
113
  /** What an open list says when what was typed matches nothing. */
107
114
  noResults: string;
115
+ /** The name of a search box nobody labelled — no `Field`, no `aria-label`, no
116
+ * placeholder. A field a screen reader announces as «edit text» is a field nobody
117
+ * can find. */
118
+ search: string;
108
119
  };
109
120
  search: {
110
121
  tools: string;
@@ -183,6 +194,8 @@ export type UiCopy = {
183
194
  retry: string;
184
195
  };
185
196
  table: {
197
+ /** Under an editable list: write a new record as one more row. */
198
+ add: string;
186
199
  row: string;
187
200
  rows: string;
188
201
  yes: string;
@@ -212,9 +225,15 @@ export type UiCopy = {
212
225
  * filtro. Decir «todavía no hay nada» a quien tiene ciento ochenta y
213
226
  * escribió mal un nombre es contestar otra pregunta. */
214
227
  emptyFiltered: string;
228
+ /** How many records an embedded list holds, on its one-line bar. Two keys and not
229
+ * one: «1 records» is wrong in every language this ships in. */
230
+ countOne: string;
231
+ count: string;
215
232
  };
216
233
  /** La historia de un registro: eventos y mensajes. */
217
- chatter: {
234
+ /** The panel of everything around a record: its notices, its conversation and its
235
+ * history (`Activity`). */
236
+ activity: {
218
237
  /** Quién hizo algo que no hizo nadie. Es una respuesta, no un hueco. */
219
238
  bySystem: string;
220
239
  /** Abrir y cerrar el error completo. */
@@ -232,18 +251,37 @@ export type UiCopy = {
232
251
  noRecipients: string;
233
252
  /** El campo vacío de un mensaje que sí sale. */
234
253
  messagePlaceholder: string;
235
- /** Un hilo donde todavía no pasó nada. */
236
- empty: string;
237
- /** The default name of the chatter, and what its fold and unfold buttons say
238
- * about it. */
239
- history: string;
254
+ /** Nobody wrote yet, and nothing was logged yet: two different answers. */
255
+ emptyMessages: string;
256
+ emptyEvents: string;
257
+ /** The panel's name, and what its fold and unfold buttons say about it. */
258
+ title: string;
240
259
  open: string;
241
260
  fold: string;
242
- /** The three tabs: everything mixed, only what people wrote, only what the
243
- * system logged. */
244
- tabAll: string;
245
- tabMessages: string;
246
- tabEvents: string;
261
+ /** The parts of the timeline: what is true now, what people wrote, what the system
262
+ * logged. */
263
+ alerts: string;
264
+ conversation: string;
265
+ history: string;
266
+ /** The name of the history, under the notices. */
267
+ viewTimeline: string;
268
+ /** The notices beyond the first three, folded: «Ver 4 más». */
269
+ moreAlerts: string;
270
+ fewerAlerts: string;
271
+ /** The timeline's filters: everything, or one kind of entry. */
272
+ filters: string;
273
+ filterAll: string;
274
+ filterConversation: string;
275
+ filterNotes: string;
276
+ filterEvents: string;
277
+ /** The day separators of the timeline that are not a date. */
278
+ today: string;
279
+ yesterday: string;
280
+ /** A filter that leaves nothing. */
281
+ emptyFilter: string;
282
+ /** A note in the timeline takes you to what it belongs to. `{owner}` is that field or
283
+ * line. */
284
+ goTo: string;
247
285
  /** The composer's buttons. */
248
286
  send: string;
249
287
  sending: string;
@@ -255,6 +293,10 @@ export type UiCopy = {
255
293
  done: string;
256
294
  /** Sin portapapeles —contexto no seguro, permiso negado— se dice. */
257
295
  failed: string;
296
+ /** The copy button of a field, named by the field: «Copiar «Cliente»». */
297
+ field: string;
298
+ /** A field with nothing in it: said, not a silent success. */
299
+ empty: string;
258
300
  };
259
301
  /** Ordenar, cuando la presentación no tiene encabezados donde hacer clic. */
260
302
  sorting: {
@@ -326,6 +368,8 @@ export type UiCopy = {
326
368
  };
327
369
  editable: {
328
370
  add: string;
371
+ /** The spreadsheet's: a row, not a line of a document. */
372
+ addRow: string;
329
373
  empty: string;
330
374
  remove: string;
331
375
  };
@@ -338,6 +382,8 @@ export type UiCopy = {
338
382
  refine: string;
339
383
  open: string;
340
384
  browseAll: string;
385
+ /** The quiet row at the end of a list that is fetching its next page. */
386
+ loadingMore: string;
341
387
  };
342
388
  cell: {
343
389
  missing: string;
@@ -349,6 +395,13 @@ export type UiCopy = {
349
395
  invalid: string;
350
396
  noDate: string;
351
397
  };
398
+ /** The dialog that holds a stack of contexts opened from a record (`Drill`). */
399
+ drill: {
400
+ /** Up one level. `{name}` is the context it goes back to. */
401
+ back: string;
402
+ /** The accessible name of the trail of contexts this one was opened from. */
403
+ trail: string;
404
+ };
352
405
  dialog: {
353
406
  expand: string;
354
407
  /** The wizard's own buttons: leave, go back, go on, end — and what the main
@@ -467,6 +520,12 @@ export type UiCopy = {
467
520
  empty: string;
468
521
  /** Closing the line being edited. */
469
522
  done: string;
523
+ /** Sections and notes among the lines (Odoo's `display_type`). */
524
+ addSection: string;
525
+ addNote: string;
526
+ /** The names of their text field, for a screen reader. */
527
+ section: string;
528
+ note: string;
470
529
  };
471
530
  /** `LineItems`, beyond the `lines` keys: counting, the editor of a new line, removing. */
472
531
  lineItems: {
@@ -496,6 +555,23 @@ export type UiCopy = {
496
555
  /** The name of the button that opens it, and of the card. `{name}` is the record. */
497
556
  summary: string;
498
557
  };
558
+ /** The money summary of a document once something is applied against it. */
559
+ totals: {
560
+ /** Read, not shown, before a deducted amount: «Pagado el 12/10/2026, menos Bs. 100». */
561
+ less: string;
562
+ /** The name of a deduction's `i`. `{label}` is the deduction — «Pagado el 12/10/2026». */
563
+ details: string;
564
+ /** Go to the applied record, from its detail. */
565
+ open: string;
566
+ /** Take a deduction back off the document, from its detail. */
567
+ unlink: string;
568
+ /** The title of what could still be applied. */
569
+ credits: string;
570
+ /** The visible word on each available credit's button. */
571
+ apply: string;
572
+ /** Its accessible name, which says WHICH. `{label}` is the credit — «NC/2026/0012». */
573
+ applyCredit: string;
574
+ };
499
575
  /** Changing a value where it is drawn: one more tag of a many2many, one cell of a list. */
500
576
  inlineEdit: {
501
577
  /** The inline input of a tags field, while it holds no tag yet. */
@@ -513,6 +589,11 @@ export type UiCopy = {
513
589
  light: string;
514
590
  dark: string;
515
591
  };
592
+ /** A hierarchy of records — a chart of accounts, product categories, locations. */
593
+ tree: {
594
+ /** What an empty tree says, unless the screen says something closer to its domain. */
595
+ empty: string;
596
+ };
516
597
  /** The assistant acting inside a screen: what it suggests, what it proposes to change,
517
598
  * what it set, and what it is still working on. */
518
599
  assist: {
@@ -589,11 +670,12 @@ export type UiCopyOverride = {
589
670
  ops?: Record<string, string>;
590
671
  sorting?: Partial<UiCopy["sorting"]>;
591
672
  copyable?: Partial<UiCopy["copyable"]>;
592
- chatter?: Partial<UiCopy["chatter"]>;
673
+ activity?: Partial<UiCopy["activity"]>;
593
674
  editable?: Partial<UiCopy["editable"]>;
594
675
  picker?: Partial<UiCopy["picker"]>;
595
676
  cell?: Partial<UiCopy["cell"]>;
596
677
  time?: Partial<UiCopy["time"]>;
678
+ drill?: Partial<UiCopy["drill"]>;
597
679
  dialog?: Partial<UiCopy["dialog"]>;
598
680
  report?: Partial<UiCopy["report"]>;
599
681
  builder?: Partial<UiCopy["builder"]>;
@@ -603,8 +685,10 @@ export type UiCopyOverride = {
603
685
  lineItems?: Partial<UiCopy["lineItems"]>;
604
686
  relation?: Partial<UiCopy["relation"]>;
605
687
  relationCard?: Partial<UiCopy["relationCard"]>;
688
+ totals?: Partial<UiCopy["totals"]>;
606
689
  inlineEdit?: Partial<UiCopy["inlineEdit"]>;
607
690
  theme?: Partial<UiCopy["theme"]>;
691
+ tree?: Partial<UiCopy["tree"]>;
608
692
  assist?: Partial<UiCopy["assist"]>;
609
693
  };
610
694
  export declare function mergeCopy(over?: UiCopyOverride): UiCopy;
@@ -72,6 +72,10 @@ export const SPANISH_COPY = {
72
72
  saving: "Guardando…",
73
73
  discard: "Descartar",
74
74
  failed: "falló",
75
+ notices: "Avisos sobre este registro",
76
+ noticeOne: "{count} aviso",
77
+ noticeMany: "{count} avisos",
78
+ blocking: "Lo que está mal en este registro",
75
79
  },
76
80
  shell: {
77
81
  close: "Cerrar",
@@ -100,6 +104,7 @@ export const SPANISH_COPY = {
100
104
  },
101
105
  choice: {
102
106
  noResults: "Sin resultados",
107
+ search: "Buscar",
103
108
  },
104
109
  dates: {
105
110
  last: "últimos {count} {unit}",
@@ -172,6 +177,7 @@ export const SPANISH_COPY = {
172
177
  retry: "Reintentar",
173
178
  },
174
179
  table: {
180
+ add: "Agregar",
175
181
  row: "fila",
176
182
  rows: "filas",
177
183
  yes: "sí",
@@ -185,8 +191,10 @@ export const SPANISH_COPY = {
185
191
  list: {
186
192
  empty: "Todavía no hay nada acá.",
187
193
  emptyFiltered: "Nada coincide con lo que está puesto.",
194
+ countOne: "1 registro",
195
+ count: "{count} registros",
188
196
  },
189
- chatter: {
197
+ activity: {
190
198
  bySystem: "el sistema",
191
199
  more: "ver el error completo",
192
200
  less: "ver menos",
@@ -197,18 +205,37 @@ export const SPANISH_COPY = {
197
205
  recipients: "Quiénes lo reciben",
198
206
  noRecipients: "todavía no hay a quién mandarlo",
199
207
  messagePlaceholder: "Escribí un mensaje…",
200
- empty: "Todavía no pasó nada acá.",
201
- history: "historial",
202
- open: "Abrir el {label}",
203
- fold: "Plegar el {label}",
204
- tabAll: "todo",
205
- tabMessages: "mensajes",
206
- tabEvents: "eventos",
208
+ emptyMessages: "Todavía nadie escribió acá.",
209
+ emptyEvents: "Todavía no se registró nada.",
210
+ title: "Actividad",
211
+ open: "Abrir {label}",
212
+ fold: "Plegar {label}",
213
+ alerts: "Avisos",
214
+ conversation: "Conversación",
215
+ history: "Historial",
216
+ viewTimeline: "Línea de tiempo",
217
+ moreAlerts: "Ver {count} más",
218
+ fewerAlerts: "Ver menos",
219
+ filters: "Mostrar",
220
+ filterAll: "Todo",
221
+ filterConversation: "Mensajes",
222
+ filterNotes: "Notas",
223
+ filterEvents: "Eventos",
224
+ today: "Hoy",
225
+ yesterday: "Ayer",
226
+ emptyFilter: "Todavía no hay nada de esto.",
227
+ goTo: "Ir a {owner}",
207
228
  send: "Enviar",
208
229
  sending: "Enviando…",
209
230
  cancel: "Cancelar",
210
231
  },
211
- copyable: { label: "Copiar", done: "Copiado", failed: "No se pudo copiar" },
232
+ copyable: {
233
+ label: "Copiar",
234
+ done: "Copiado",
235
+ failed: "No se pudo copiar",
236
+ field: "Copiar «{label}»",
237
+ empty: "No hay nada que copiar",
238
+ },
212
239
  sorting: {
213
240
  label: "Ordenar",
214
241
  by: "ordenar por",
@@ -233,7 +260,12 @@ export const SPANISH_COPY = {
233
260
  clear: "dejar de agrupar",
234
261
  grain: "con qué corte",
235
262
  },
236
- editable: { add: "agregar", empty: "nada todavía", remove: "quitar" },
263
+ editable: {
264
+ add: "agregar",
265
+ addRow: "agregar una fila",
266
+ empty: "nada todavía",
267
+ remove: "quitar",
268
+ },
237
269
  picker: {
238
270
  placeholder: "elegí uno…",
239
271
  empty: "Nada coincide.",
@@ -243,8 +275,13 @@ export const SPANISH_COPY = {
243
275
  refine: "— afiná el filtro",
244
276
  open: "Abrir",
245
277
  browseAll: "ver todos…",
278
+ loadingMore: "cargando más…",
246
279
  },
247
280
  cell: { empty: "—", missing: "falta", emptyBin: "(vacío)" },
281
+ drill: {
282
+ back: "Volver a {name}",
283
+ trail: "Abierto desde",
284
+ },
248
285
  dialog: {
249
286
  expand: "Agrandar a panel",
250
287
  cancel: "Cancelar",
@@ -320,6 +357,10 @@ export const SPANISH_COPY = {
320
357
  remove: "Quitar la línea",
321
358
  empty: "Todavía no hay líneas.",
322
359
  done: "Listo",
360
+ addSection: "Agregar una sección",
361
+ addNote: "Agregar una nota",
362
+ section: "Sección",
363
+ note: "Nota",
323
364
  },
324
365
  lineItems: {
325
366
  one: "{count} línea",
@@ -333,6 +374,15 @@ export const SPANISH_COPY = {
333
374
  },
334
375
  relation: { open: "Abrir", none: "Sin asignar" },
335
376
  relationCard: { summary: "Resumen de {name}" },
377
+ totals: {
378
+ less: "menos",
379
+ details: "Detalle: {label}",
380
+ open: "Abrir",
381
+ unlink: "Desvincular",
382
+ credits: "Créditos pendientes",
383
+ apply: "Aplicar",
384
+ applyCredit: "Aplicar {label}",
385
+ },
336
386
  inlineEdit: {
337
387
  addTag: "Agregar…",
338
388
  createTag: "Crear «{name}»",
@@ -344,6 +394,9 @@ export const SPANISH_COPY = {
344
394
  light: "Claro",
345
395
  dark: "Oscuro",
346
396
  },
397
+ tree: {
398
+ empty: "Todavía no hay nada acá.",
399
+ },
347
400
  assist: {
348
401
  assistant: "Asistente",
349
402
  suggests: "sugiere",