@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
package/README.md CHANGED
@@ -8,18 +8,39 @@ Most design systems are built for marketing sites and consumer apps, then stretc
8
8
  business software. This one starts from the other end: dense data, long sessions, keyboard
9
9
  users, numbers that must be legible, and screens that are always about _records_.
10
10
 
11
+ ## A UI that evolves — a method that holds
12
+
13
+ `@sincpro/web-ui` **will keep changing.** Its look, the set of components and their props grow
14
+ and get corrected as real screens ask for them. It is `0.x`: a minor release may break an API,
15
+ and each release says what changed in
16
+ [GitHub Releases](https://github.com/Sincpro-SRL/sincpro_web_ui/releases).
17
+
18
+ What does not change is **how the components are managed** — and that is what this project is
19
+ about:
20
+
21
+ | Stable — the contract | Evolving — the content |
22
+ | ----------------------------------------------------------- | --------------------------------- |
23
+ | Every piece belongs to one of four levels, and imports down | Which components exist |
24
+ | Values come from tokens; contrast is measured | The palette, sizes, density steps |
25
+ | Interface copy is data, never written in a component | The wording |
26
+ | Every piece ships with a test and a story | Props, their names and defaults |
27
+ | Whole pages are built from the public API alone | Visual details of each piece |
28
+
29
+ Each rule on the left is checked by a machine — `make test`, `make doctor`, `make smoke` — not
30
+ by anyone's memory, so the catalogue can grow fast without rotting.
31
+
11
32
  ## The idea
12
33
 
13
34
  **Every management screen is one of two things: one record, or N records.** That is the
14
35
  whole product surface, and the system is organised around it in four levels — each one
15
36
  defined by what it _knows_, and each one importing only from the levels below:
16
37
 
17
- | Level | What it knows | Examples |
18
- | --------------- | --------------------------------------- | ------------------------------------------------------- |
19
- | **Foundations** | The decisions. No components. | color, typography, scales, motion, surfaces |
20
- | **Primitives** | Nothing about the screen it is on. | `Button`, `Field`, `Menu`, `Tag`, `Table`, `Dialog` |
21
- | **Components** | One question of the product, by family. | data tables, search and criteria, record parts, chatter |
22
- | **Views** | The two macros, and nothing else. | `Record` (one) · `List` (N) |
38
+ | Level | What it knows | Examples |
39
+ | --------------- | --------------------------------------- | -------------------------------------------------------- |
40
+ | **Foundations** | The decisions. No components. | color, typography, scales, motion, surfaces |
41
+ | **Primitives** | Nothing about the screen it is on. | `Button`, `Field`, `Menu`, `Tag`, `Table`, `Dialog` |
42
+ | **Components** | One question of the product, by family. | data tables, search and criteria, record parts, activity |
43
+ | **Views** | The two macros, and nothing else. | `Record` (one) · `List` (N) |
23
44
 
24
45
  If something does not fit one of the two views, it is not a view — it is a component in
25
46
  disguise. And if something needs to know what an invoice, a dataset or a customer _is_, it
@@ -40,6 +61,12 @@ does not belong here: that is domain, and it lives in the application.
40
61
  (`installCopy(SPANISH_COPY)`, `UiCopyProvider`) — never hard-coded in a component.
41
62
  - **Accessible by default.** Storybook runs the a11y checks as errors, not warnings.
42
63
 
64
+ The full set — hierarchy, one primary action, color that means something, progressive
65
+ disclosure, undo over confirm… — each with the piece that applies it and the check that
66
+ enforces it, is in
67
+ [`docs/PRINCIPLES.md`](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/docs/PRINCIPLES.md)
68
+ (in Spanish).
69
+
43
70
  ## Using it
44
71
 
45
72
  ```bash
@@ -130,7 +157,7 @@ src/
130
157
  ├── index.ts the public surface: the four levels below
131
158
  ├── foundations/ density, surface, copy, slots, calendar, pivot, xlsx
132
159
  ├── primitives/ one folder per family: button, field, menu, table, tabs, …
133
- ├── components/ one folder per family: listing, record, search, form, chatter, frame, shell
160
+ ├── components/ one folder per family: listing, record, search, form, activity, frame, shell
134
161
  └── views/ list (N records) · record (one record)
135
162
  stories/ Storybook, by level — and templates/: whole pages built from the public API
136
163
  tests/ node --test over the TypeScript sources
@@ -142,7 +169,7 @@ component importing a view), so the layering holds without anyone remembering it
142
169
  ### Iterating with AI
143
170
 
144
171
  This system is developed together with AI agents, and it is set up for it: the rules an
145
- agent must follow live in [`AGENTS.md`](./AGENTS.md), the guards that catch what an agent
172
+ agent must follow live in [`AGENTS.md`](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/AGENTS.md), the guards that catch what an agent
146
173
  gets wrong (tokens, contrast, import direction, client boundaries, stories that blow up)
147
174
  run in `make test`, `make doctor` and `make smoke`, and
148
175
  while `make dev` runs, Storybook serves an MCP endpoint at `/mcp` so an agent reads the
@@ -150,9 +177,9 @@ stories that already exist instead of guessing them.
150
177
 
151
178
  ## Contributing
152
179
 
153
- See [CONTRIBUTING.md](./CONTRIBUTING.md). Issues and pull requests are welcome — especially
180
+ See [CONTRIBUTING.md](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/CONTRIBUTING.md). Issues and pull requests are welcome — especially
154
181
  from people who build business software for a living.
155
182
 
156
183
  ## License
157
184
 
158
- [MIT](./LICENSE) © SINCPRO S.R.L.
185
+ [MIT](https://github.com/Sincpro-SRL/sincpro_web_ui/blob/main/LICENSE) © SINCPRO S.R.L.
@@ -0,0 +1,69 @@
1
+ import type { Message } from "./Messages.js";
2
+ import type { Tone } from "../../foundations/types.js";
3
+ import { type CalloutProps } from "../../primitives/notice/Callout.js";
4
+ import type { LucideIcon } from "lucide-react";
5
+ import { type ReactNode } from "react";
6
+ /**
7
+ * Algo que el sistema anotó.
8
+ *
9
+ * Deliberadamente NO es el evento del producto: lo que una historia necesita
10
+ * para dibujar uno es qué pasó, cuándo, quién, y —cuando las hay— las tres cosas
11
+ * que uno termina yendo a buscar a otro lado.
12
+ *
13
+ * by QUIÉN. Ausente = lo hizo el sistema, que también es una respuesta.
14
+ * moved DE QUÉ A QUÉ. «borrador → confirmado» se lee de una.
15
+ * error QUÉ falló, entero y textual: quien mira esta pantalla muchas
16
+ * veces no tiene acceso a los logs.
17
+ */
18
+ export type ActivityEvent = {
19
+ id: string;
20
+ /** Qué pasó, en una oración y en pasado. */
21
+ label: ReactNode;
22
+ at: Date | string | number;
23
+ tone?: Tone;
24
+ /** Quién lo hizo. Ausente = el sistema. */
25
+ by?: string;
26
+ /** El cambio de estado, cuando lo hubo. `from` ausente = nació así. */
27
+ moved?: {
28
+ from?: string;
29
+ to: string;
30
+ };
31
+ /** Lo que hay que saber además del título, cuando el título no alcanza. */
32
+ detail?: ReactNode;
33
+ /** Lo que falló, textual. Se muestra la PRIMERA línea y el resto se abre. */
34
+ error?: string;
35
+ /** El ícono del punto, cuando el rayo no dice lo suficiente. */
36
+ icon?: LucideIcon;
37
+ };
38
+ /** A notice of the record, as the panel lists it: a `Callout` without its × label. */
39
+ export type ActivityAlert = Omit<CalloutProps, "dismissLabel">;
40
+ /** `auto`: beside the sheet when the record is wide, below when not. `below`: always below. */
41
+ export type ActivityLayout = "auto" | "below";
42
+ export type ActivityProps = {
43
+ /** What people wrote. Absent —not empty— while there is no service behind it:
44
+ * an empty thread says «nobody wrote yet», which is a different claim. */
45
+ messages?: readonly Message[];
46
+ /** What the system logged. */
47
+ events?: readonly ActivityEvent[];
48
+ /** What is true now about the record, in urgency order — `Record` passes its notices. */
49
+ alerts?: readonly ActivityAlert[];
50
+ /** The id of the notices section: where the rail's indicator sends the focus. */
51
+ alertsId?: string;
52
+ /** The box to write in, when this record takes messages. It heads the conversation:
53
+ * writing is the thing you came to do, reading is what you do next. */
54
+ composer?: ReactNode;
55
+ open: boolean;
56
+ onToggle: () => void;
57
+ /** The panel's name. «Actividad» by default. */
58
+ label?: string;
59
+ layout?: ActivityLayout;
60
+ };
61
+ export declare function Activity({ messages, events, alerts, alertsId: ownAlertsId, composer, open, onToggle, label: ownLabel, layout, }: ActivityProps): import("react").JSX.Element;
62
+ /** Whether a standalone activity panel is open. Inside a `Record`, `Record.Activity`
63
+ * keeps this state itself (open by default: on a big screen you see what the record has). */
64
+ export declare function useActivity(initial?: boolean): {
65
+ open: boolean;
66
+ toggle: () => void;
67
+ /** Open it without closing it if it already was. */
68
+ show: () => void;
69
+ };
@@ -0,0 +1,279 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { usePagination } from "../listing/pages.js";
4
+ import { Pagination } from "../listing/Pagination.js";
5
+ import { fillCopy, useUiCopy } from "../../foundations/copy.js";
6
+ import { cn } from "../../foundations/variants.js";
7
+ import { IconButton } from "../../primitives/button/Button.js";
8
+ import { SegmentedControl } from "../../primitives/choice/SegmentedControl.js";
9
+ import { useNoteEntries } from "../../primitives/field/note.js";
10
+ import { Callout } from "../../primitives/notice/Callout.js";
11
+ import { Tag } from "../../primitives/tag/Tag.js";
12
+ import { DateValue } from "../../primitives/value/Fecha.js";
13
+ import { Avatar } from "../../primitives/value/Person.js";
14
+ import { Activity as ActivityIcon, ArrowRight, CheckCircle2, ChevronDown, ChevronRight, ChevronUp, CornerDownRight, History, Info, Lock, MessageSquare, OctagonAlert, PanelRightClose, PanelRightOpen, TriangleAlert, Zap, } from "lucide-react";
15
+ import { Fragment, useId, useState } from "react";
16
+ /** The day an instant falls on, here — a `YYYY-MM-DD` key in local time. */
17
+ function dayKey(time) {
18
+ const d = new Date(time);
19
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
20
+ }
21
+ /** How a day separator reads: «Hoy», «Ayer», or the date — with the year only when it is
22
+ * not this one, since «13 de octubre» is all a recent history needs. */
23
+ function dayLabel(key, locale, words, now = new Date()) {
24
+ const today = dayKey(now.getTime());
25
+ const yesterday = dayKey(now.getTime() - 86_400_000);
26
+ if (key === today)
27
+ return words.today;
28
+ if (key === yesterday)
29
+ return words.yesterday;
30
+ const [y, m, d] = key.split("-").map(Number);
31
+ const date = new Date(y, m - 1, d);
32
+ return new Intl.DateTimeFormat(locale, {
33
+ weekday: "long",
34
+ day: "numeric",
35
+ month: "long",
36
+ ...(y === now.getFullYear() ? {} : { year: "numeric" }),
37
+ }).format(date);
38
+ }
39
+ /* The most urgent tone first: the alerts' count takes the tone of the first. */
40
+ const URGENCY = {
41
+ bad: 0,
42
+ warn: 1,
43
+ info: 2,
44
+ good: 3,
45
+ };
46
+ function mostUrgent(alerts) {
47
+ return alerts.reduce((best, one) => {
48
+ const tone = one.tone ?? "info";
49
+ return URGENCY[tone] < URGENCY[best] ? tone : best;
50
+ }, "good");
51
+ }
52
+ /* The tone's shape, the same as `Callout`'s: a triangle warns, an «i» informs. */
53
+ const ALERT_ICON = {
54
+ bad: OctagonAlert,
55
+ warn: TriangleAlert,
56
+ info: Info,
57
+ good: CheckCircle2,
58
+ };
59
+ /* ONE icon for every event: the bolt, which is what happened — something ran. One per
60
+ tone turned the history into a sample book; what has to stand out is a failure, and
61
+ that is said by the row's tint. */
62
+ const EVENT_ICON = Zap;
63
+ const NODE_SKIN = {
64
+ ok: "bg-ink/8 text-muted",
65
+ failed: "bg-bad/12 text-bad",
66
+ };
67
+ function at(when) {
68
+ return when instanceof Date ? when.getTime() : new Date(when).getTime();
69
+ }
70
+ /* The stream's two kinds of entry share one GEOMETRY: a 1.5rem disc (the face, or the
71
+ event's node) on the left, the text beside it, the meta line under it. Two geometries
72
+ in one list read as two lists glued together. */
73
+ function MessageEntry({ message }) {
74
+ const copy = useUiCopy();
75
+ const internal = message.internal === true;
76
+ return (_jsxs("li", { className: "flex min-w-0 gap-2.5 px-2 py-2", children: [_jsx(Avatar, { name: message.author, size: "md" }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-1", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5", children: [_jsx("span", { className: "truncate text-meta font-medium text-ink", children: message.author }), _jsx("span", { className: "text-micro text-muted", children: _jsx(DateValue, { when: message.at, format: "relative" }) }), internal ? (_jsxs("span", { className: "inline-flex items-center gap-1 text-micro text-muted", title: copy.activity.internalNote, children: [_jsx(Lock, { className: "size-3", "aria-hidden": true }), copy.activity.internal] })) : null] }), _jsx("p", { className: cn("max-w-(--container-prose) rounded-box px-2.5 py-1.5 text-body whitespace-pre-line text-ink", internal ? "border border-warn/35 bg-warn/6" : "bg-ground"), children: message.body })] })] }));
77
+ }
78
+ /** The change of state: from what to what. The old one quiet, the new one in ink. */
79
+ const STATE_CHIP = "inline-flex max-w-32 min-w-0 items-center truncate rounded-pill border border-line-strong bg-ink/5 px-2 py-0.5 text-micro";
80
+ function Moved({ from, to }) {
81
+ return (_jsxs("span", { className: "inline-flex min-w-0 items-center gap-1", children: [from === undefined ? null : (_jsxs(_Fragment, { children: [_jsx("span", { className: `${STATE_CHIP} text-muted`, children: from }), _jsx(ArrowRight, { className: "size-3 shrink-0 text-muted", strokeWidth: 2.5, "aria-hidden": true })] })), _jsx("span", { className: `${STATE_CHIP} font-medium text-ink`, children: to })] }));
82
+ }
83
+ /** The error: the first line, and the rest one press away — kept, not dropped. */
84
+ function ErrorLine({ error }) {
85
+ const copy = useUiCopy();
86
+ const [open, setOpen] = useState(false);
87
+ const lines = error.split("\n");
88
+ const first = lines[0] ?? "";
89
+ const more = lines.length > 1 || first.length > 120;
90
+ return (_jsxs("div", { className: "min-w-0 rounded-control border border-line bg-surface px-2 py-1", children: [_jsx("p", { className: `min-w-0 font-mono text-micro text-ink ${open ? "break-words whitespace-pre-wrap" : "truncate"}`, children: open ? error : first }), !more ? null : (_jsx("button", { type: "button", onClick: () => setOpen(!open), className: "mt-0.5 cursor-pointer text-micro text-muted underline-offset-2 hover:text-ink hover:underline", children: open ? copy.activity.less : copy.activity.more }))] }));
91
+ }
92
+ /**
93
+ * UN EVENTO: a row that opens, not a line.
94
+ *
95
+ * Closed it says what happened —in up to two lines, never cut to «se envió …»— and
96
+ * under it who and when, in the quiet size. That second line is what made the narrow
97
+ * column legible: side by side, the name and the date took half the width and the
98
+ * label was clipped to three words. A failure tints the row; the rest stays ink.
99
+ */
100
+ function EventEntry({ event }) {
101
+ const copy = useUiCopy();
102
+ const failed = event.tone === "bad" || event.error !== undefined;
103
+ const skin = failed ? "failed" : "ok";
104
+ const Icon = event.icon ?? EVENT_ICON;
105
+ const [open, setOpen] = useState(false);
106
+ const more = event.moved !== undefined || event.detail !== undefined || event.error !== undefined;
107
+ const row = (_jsxs(_Fragment, { children: [_jsx("span", { className: `relative z-10 flex size-6 shrink-0 items-center justify-center rounded-pill ring-2 ring-surface ${NODE_SKIN[skin]}`, children: _jsx(Icon, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "line-clamp-2 text-meta text-ink", children: event.label }), _jsxs("span", { className: "flex min-w-0 flex-wrap items-baseline gap-x-1.5 text-micro text-muted", children: [_jsx("span", { className: "truncate", children: event.by ?? copy.activity.bySystem }), _jsx("span", { "aria-hidden": true, children: "\u00B7" }), _jsx(DateValue, { when: event.at, format: "relative" })] })] }), more ? (_jsx(ChevronRight, { className: `mt-1 size-3.5 shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2.5, "aria-hidden": true })) : null] }));
108
+ const box = `flex w-full min-w-0 items-start gap-2.5 rounded-control px-2 py-2 text-left ${failed ? "bg-bad/6" : ""}`;
109
+ return (_jsxs("li", { className: "group/event relative", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-8 bottom-0 left-5 w-px bg-line group-last/event:hidden" }), more ? (_jsx("button", { type: "button", onClick: () => setOpen(!open), "aria-expanded": open, className: `${box} cursor-pointer transition-colors duration-(--duration-instant) focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${failed ? "hover:bg-bad/10" : "hover:bg-ink/4"}`, children: row })) : (_jsx("div", { className: box, children: row })), !open ? null : (_jsxs("div", { className: "mb-1 ml-10.5 flex min-w-0 flex-col gap-1.5 rounded-control bg-ground px-2.5 py-2", children: [event.moved === undefined ? null : (_jsx(Moved, { ...(event.moved.from === undefined ? {} : { from: event.moved.from }), to: event.moved.to })), event.detail === undefined ? null : (_jsx("div", { className: "min-w-0 text-micro text-muted", children: event.detail })), event.error === undefined ? null : _jsx(ErrorLine, { error: event.error })] }))] }));
110
+ }
111
+ /** Takes you to the field or row a note belongs to, and lights it for a moment so the eye
112
+ * lands on it. The light is the system's focus colour, faded out; with reduced motion it
113
+ * is shown and removed without the fade. */
114
+ function goTo(entry) {
115
+ const target = entry.anchor();
116
+ if (target === null)
117
+ return;
118
+ const still = typeof window !== "undefined" &&
119
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
120
+ target.scrollIntoView({ block: "center", behavior: still ? "auto" : "smooth" });
121
+ target.animate([
122
+ {
123
+ boxShadow: "0 0 0 0.125rem var(--color-primary)",
124
+ borderRadius: "var(--radius-control)",
125
+ },
126
+ { boxShadow: "0 0 0 0.125rem transparent", borderRadius: "var(--radius-control)" },
127
+ ], { duration: still ? 1 : 1800, easing: "ease-out", delay: still ? 1200 : 300 });
128
+ }
129
+ /** A field note or row flag in the timeline: its sentence, and WHAT it belongs to — a link
130
+ * that takes you there. */
131
+ function NoteLine({ entry }) {
132
+ const copy = useUiCopy();
133
+ const tone = entry.note.tone ?? "info";
134
+ const Icon = ALERT_ICON[tone];
135
+ return (_jsx("li", { className: "min-w-0", children: _jsxs("button", { type: "button", onClick: () => goTo(entry), title: entry.owner === ""
136
+ ? undefined
137
+ : fillCopy(copy.activity.goTo, { owner: entry.owner }), className: "flex w-full min-w-0 cursor-pointer items-start gap-2 rounded-control px-2 py-1.5 text-left transition-colors duration-(--duration-instant) hover:bg-ink/4 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary", children: [_jsx("span", { className: `mt-0.5 shrink-0 ${TONE_TEXT[tone]}`, children: _jsx(Icon, { className: "size-(--size-icon-sm)", strokeWidth: 2.25, "aria-hidden": true }) }), _jsxs("span", { className: "flex min-w-0 flex-1 flex-col gap-0.5", children: [_jsx("span", { className: "text-meta text-ink", children: entry.note.label }), entry.owner === "" ? null : (_jsxs("span", { className: "flex min-w-0 items-center gap-1 text-micro text-muted", children: [_jsx(CornerDownRight, { className: "size-3 shrink-0", "aria-hidden": true }), _jsx("span", { className: "truncate", children: entry.owner })] }))] })] }) }));
138
+ }
139
+ const TONE_TEXT = {
140
+ bad: "text-bad",
141
+ warn: "text-warn",
142
+ info: "text-info",
143
+ good: "text-good",
144
+ };
145
+ export function Activity({ messages, events, alerts = [], alertsId: ownAlertsId, composer, open, onToggle, label: ownLabel, layout = "auto", }) {
146
+ const copy = useUiCopy();
147
+ const label = ownLabel ?? copy.activity.title;
148
+ const titleId = useId();
149
+ const autoAlertsId = useId();
150
+ const alertsId = ownAlertsId ?? autoAlertsId;
151
+ const auto = layout === "auto";
152
+ /* The notes of the fields and rows of the record around — `Record` keeps them. */
153
+ const notes = useNoteEntries();
154
+ const byTime = (items) => [...items].sort((x, y) => at(y.at) - at(x.at));
155
+ const said = byTime(messages ?? []);
156
+ const logged = byTime(events ?? []);
157
+ const notices = alerts.length + notes.length;
158
+ const tones = [
159
+ ...alerts.map((a) => a.tone ?? "info"),
160
+ ...notes.map((n) => n.note.tone ?? "info"),
161
+ ];
162
+ const alertTone = tones.length === 0 ? undefined : mostUrgent(tones.map((tone) => ({ tone })));
163
+ const AlertIcon = ALERT_ICON[alertTone ?? "info"];
164
+ /* ONE stream, by time: what happened before what is the whole point of a history. */
165
+ const [filter, setFilter] = useState("all");
166
+ const stream = [
167
+ ...said.map((message) => ({
168
+ kind: "message",
169
+ at: at(message.at),
170
+ id: `m-${message.id}`,
171
+ message,
172
+ })),
173
+ ...logged.map((event) => ({
174
+ kind: "event",
175
+ at: at(event.at),
176
+ id: `e-${event.id}`,
177
+ event,
178
+ })),
179
+ ].sort((x, y) => y.at - x.at);
180
+ const kept = stream.filter((entry) => filter === "all"
181
+ ? true
182
+ : filter === "events"
183
+ ? entry.kind === "event"
184
+ : entry.kind === "message" &&
185
+ (filter === "notes") === (entry.message.internal === true));
186
+ const pages = usePagination(kept, { pageSize: 20 });
187
+ const counts = {
188
+ conversation: said.filter((m) => m.internal !== true).length,
189
+ notes: said.filter((m) => m.internal === true).length,
190
+ events: logged.length,
191
+ };
192
+ /* The notices, compact: the first three, the rest one press away. */
193
+ const [allAlerts, setAllAlerts] = useState(false);
194
+ const SHOWN = 3;
195
+ /* A count with its shape and, for a screen reader, its word: «💬 1» is «Conversación 1». */
196
+ const counted = (Icon, n, word) => (_jsxs("span", { className: "inline-flex shrink-0 items-center gap-1 text-micro text-muted tabular-nums", children: [_jsx(Icon, { className: "size-3.5", strokeWidth: 2, "aria-hidden": true }), n, _jsx("span", { className: "sr-only", children: word })] }));
197
+ /* ── Folded: a strip that still says how much there is ──────────── */
198
+ if (!open) {
199
+ return (_jsxs("button", { type: "button", onClick: onToggle,
200
+ /* Named by what it shows — «Actividad, 2 avisos, 1 Conversación, 5 Historial» —
201
+ so the counts are heard too; the tooltip says what pressing it does. */
202
+ title: fillCopy(copy.activity.open, { label }), "aria-expanded": false, "data-activity": "folded", className: cn("flex w-full min-w-0 cursor-pointer items-center gap-2.5 rounded-box border border-line bg-surface px-3 py-2.5 text-left transition-colors duration-(--duration-instant) hover:bg-ink/3 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary",
203
+ /* Beside the sheet it folds to a slim column JOINED to it: the toggle on top
204
+ where the eye starts, and under it the counts, stacked — nothing hidden
205
+ silently. The sheet's own right edge is the hairline between the two. */
206
+ auto &&
207
+ "@beside/page:w-auto @beside/page:flex-col @beside/page:justify-start @beside/page:gap-3 @beside/page:rounded-l-none @beside/page:border-l-0 @beside/page:px-2 @beside/page:py-3"), children: [_jsx("span", { className: cn("hidden shrink-0 text-muted", auto && "@beside/page:block"), children: _jsx(PanelRightOpen, { className: "size-(--size-icon-md)", "aria-hidden": true }) }), _jsx(ActivityIcon, { className: cn("size-(--size-icon-md) shrink-0 text-muted", auto && "@beside/page:hidden"), "aria-hidden": true }), _jsx("span", { className: cn("min-w-0 truncate text-meta font-medium text-ink", auto && "@beside/page:sr-only"), children: label }), alertTone === undefined ? null : (_jsxs(Tag, { tone: alertTone, children: [_jsxs("span", { className: cn("hidden items-center gap-1", auto && "@beside/page:inline-flex"), children: [_jsx(AlertIcon, { className: "size-3", strokeWidth: 2.5, "aria-hidden": true }), notices] }), _jsx("span", { className: cn(auto && "@beside/page:sr-only"), children: fillCopy(notices === 1 ? copy.record.noticeOne : copy.record.noticeMany, {
208
+ count: notices,
209
+ }) })] })), messages === undefined
210
+ ? null
211
+ : counted(MessageSquare, said.length, copy.activity.conversation), events === undefined ? null : counted(History, logged.length, copy.activity.history), _jsx("span", { className: cn("ml-auto shrink-0 text-muted", auto && "@beside/page:hidden"), children: _jsx(ChevronDown, { className: "size-(--size-icon-md)", "aria-hidden": true }) })] }));
212
+ }
213
+ return (_jsxs("section", { "aria-labelledby": titleId, "data-activity": "open", className: cn("flex w-full min-w-0 flex-col rounded-box border border-line bg-surface",
214
+ /* Beside the sheet: its column, joined to it — the sheet's right edge is the line. */
215
+ auto &&
216
+ "@beside/page:w-(--container-activity) @beside/page:rounded-l-none @beside/page:border-l-0"), children: [_jsxs("header", { className: "flex min-w-0 items-center gap-2 border-b border-line py-1.5 pr-1.5 pl-3", children: [_jsx(ActivityIcon, { className: "size-(--size-icon-md) shrink-0 text-muted", "aria-hidden": true }), _jsx("h2", { id: titleId, className: "min-w-0 truncate text-meta font-semibold text-ink", children: label }), _jsxs("span", { className: "ml-auto flex shrink-0", children: [_jsx("span", { className: cn("flex", auto && "@beside/page:hidden"), children: _jsx(IconButton, { icon: ChevronUp, label: fillCopy(copy.activity.fold, { label }), onClick: onToggle }) }), auto ? (_jsx("span", { className: "hidden @beside/page:flex", children: _jsx(IconButton, { icon: PanelRightClose, label: fillCopy(copy.activity.fold, { label }), onClick: onToggle }) })) : null] })] }), notices === 0 ? null : (
217
+ /* NOW, before what happened. The rail's indicator sends the focus here, so it is
218
+ focusable without being a tab stop. */
219
+ _jsxs("section", { id: alertsId, tabIndex: -1, "aria-label": copy.activity.alerts, "data-activity-alerts": "", className: "flex min-w-0 scroll-mt-(--size-appbar) flex-col gap-2 border-b border-line px-3 py-3 outline-0 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-primary", children: [_jsxs("h3", { className: "flex items-center gap-2 px-1 text-micro font-medium text-muted", children: [copy.activity.alerts, _jsx(Tag, { tone: alertTone ?? "info", children: notices })] }), (() => {
220
+ const rows = [
221
+ ...alerts.map(({ children, ...alert }, i) => ({
222
+ key: `a${i}`,
223
+ node: (_jsx(Callout, { ...alert, dismissLabel: copy.shell.closeNotice, children: children })),
224
+ })),
225
+ ...notes.map((entry) => ({
226
+ key: entry.id,
227
+ node: (_jsx("ul", { className: "-mx-1 flex min-w-0 flex-col", children: _jsx(NoteLine, { entry: entry }) })),
228
+ })),
229
+ ];
230
+ const shown = allAlerts ? rows : rows.slice(0, SHOWN);
231
+ return (_jsxs(_Fragment, { children: [shown.map((row) => (_jsx(Fragment, { children: row.node }, row.key))), rows.length <= SHOWN ? null : (_jsx("button", { type: "button", "aria-expanded": allAlerts, onClick: () => setAllAlerts(!allAlerts), className: "self-start rounded-control px-1 text-micro font-medium text-primary hover:underline focus-visible:outline-2 focus-visible:outline-primary", children: allAlerts
232
+ ? copy.activity.fewerAlerts
233
+ : fillCopy(copy.activity.moreAlerts, { count: rows.length - SHOWN }) }))] }));
234
+ })()] })), _jsxs("section", { "aria-label": copy.activity.viewTimeline, className: "flex min-w-0 flex-col gap-3 px-3 py-3", children: [_jsx("h3", { className: "px-1 text-micro font-medium text-muted", children: copy.activity.viewTimeline }), composer, stream.length === 0 ? null : (_jsx(SegmentedControl, { size: "sm", "aria-label": copy.activity.filters, value: filter, onChange: (next) => setFilter(next), options: [
235
+ { value: "all", label: copy.activity.filterAll },
236
+ ...(messages === undefined
237
+ ? []
238
+ : [
239
+ {
240
+ value: "conversation",
241
+ label: `${copy.activity.filterConversation} · ${counts.conversation}`,
242
+ },
243
+ {
244
+ value: "notes",
245
+ label: `${copy.activity.filterNotes} · ${counts.notes}`,
246
+ },
247
+ ]),
248
+ ...(events === undefined
249
+ ? []
250
+ : [
251
+ {
252
+ value: "events",
253
+ label: `${copy.activity.filterEvents} · ${counts.events}`,
254
+ },
255
+ ]),
256
+ ] })), stream.length === 0 ? (_jsx("p", { className: "px-1 text-meta text-muted", children: messages === undefined ? copy.activity.emptyEvents : copy.activity.emptyMessages })) : kept.length === 0 ? (_jsx("p", { className: "px-1 text-meta text-muted", children: copy.activity.emptyFilter })) : (_jsxs(_Fragment, { children: [_jsx("ol", { "aria-label": copy.activity.viewTimeline, className: "-mx-1 flex min-w-0 flex-col", children: pages.pageItems.map((entry, i) => {
257
+ const day = dayKey(entry.at);
258
+ const before = pages.pageItems[i - 1];
259
+ const newDay = before === undefined || dayKey(before.at) !== day;
260
+ return (_jsxs(Fragment, { children: [newDay ? (
261
+ /* The day: a quiet rule with its name, so «what happened on the 13th»
262
+ is found by scanning, not by reading every date. */
263
+ _jsxs("li", { "aria-hidden": true, className: "flex items-center gap-2 px-1 pt-2 pb-1 text-micro font-medium text-muted first:pt-0", children: [_jsx("span", { className: "first-letter:uppercase", children: dayLabel(day, copy.locale, {
264
+ today: copy.activity.today,
265
+ yesterday: copy.activity.yesterday,
266
+ }) }), _jsx("span", { className: "h-px flex-1 bg-line" })] })) : null, entry.kind === "message" ? (_jsx(MessageEntry, { message: entry.message })) : (_jsx(EventEntry, { event: entry.event }))] }, entry.id));
267
+ }) }), kept.length <= 20 ? null : _jsx(Pagination, { ...pages.bridge })] }))] })] }));
268
+ }
269
+ /** Whether a standalone activity panel is open. Inside a `Record`, `Record.Activity`
270
+ * keeps this state itself (open by default: on a big screen you see what the record has). */
271
+ export function useActivity(initial = true) {
272
+ const [open, setOpen] = useState(initial);
273
+ return {
274
+ open,
275
+ toggle: () => setOpen((current) => !current),
276
+ /** Open it without closing it if it already was. */
277
+ show: () => setOpen(true),
278
+ };
279
+ }
@@ -1,9 +1,9 @@
1
1
  import { type ReactNode } from "react";
2
2
  /**
3
- * The box to write in. What gets written is drawn by `Chatter`, together with
3
+ * The box to write in. What gets written is drawn by `Activity`, together with
4
4
  * what the system logged — see its note on why the merge is the default.
5
5
  *
6
- * Odoo's chatter is two things sharing a column — a log the system writes and a
6
+ * Odoo's chatter —our activity— is two things sharing a column — a log the system writes and a
7
7
  * thread people write — and they are usually confused because they sit
8
8
  * together. They are not the same: an event is a fact that happened, and a
9
9
  * message is somebody's account of it. The system's half never needs a reply
@@ -16,9 +16,9 @@ export function writeANote() {
16
16
  const copy = currentCopy();
17
17
  return {
18
18
  id: "note",
19
- label: copy.chatter.writeNote,
19
+ label: copy.activity.writeNote,
20
20
  internal: true,
21
- placeholder: copy.chatter.notePlaceholder,
21
+ placeholder: copy.activity.notePlaceholder,
22
22
  };
23
23
  }
24
24
  export function MessageComposer({ kinds, onSend, sending = false, footer, }) {
@@ -34,10 +34,13 @@ export function MessageComposer({ kinds, onSend, sending = false, footer, }) {
34
34
  setWriting(undefined);
35
35
  setBody("");
36
36
  }
37
- return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: [ways.map((kind) => (_jsx(Button, { role: writing?.id === kind.id ? "primary" : "ghost", size: "sm", onClick: () => (writing?.id === kind.id ? close() : setWriting(kind)), children: kind.label }, kind.id))), footer === undefined ? null : _jsx("div", { className: "ml-auto min-w-0", children: footer })] }), writing === undefined ? null : (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [writing.internal === true || writing.recipients === undefined ? null : (_jsxs("p", { className: "flex min-w-0 items-center gap-1.5 text-micro text-muted", title: copy.chatter.recipients, children: [_jsx(Users, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx("span", { className: "min-w-0 truncate", children: writing.recipients.length === 0
38
- ? copy.chatter.noRecipients
39
- : writing.recipients.join(" · ") })] })), _jsx(TextArea, { value: body, onChange: setBody, placeholder: writing.placeholder ?? copy.chatter.messagePlaceholder, rows: 3 }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Button, { role: "primary", size: "sm", icon: Send, disabled: !ready, onClick: () => {
37
+ return (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-center gap-1.5", children: [ways.map((kind) => (_jsx(Button, {
38
+ /* Chosen is `secondary`, not `primary`: the gold is the record's one action,
39
+ and a way of writing that is merely selected is not an action. */
40
+ role: writing?.id === kind.id ? "secondary" : "ghost", size: "sm", onClick: () => (writing?.id === kind.id ? close() : setWriting(kind)), children: kind.label }, kind.id))), footer === undefined ? null : _jsx("div", { className: "ml-auto min-w-0", children: footer })] }), writing === undefined ? null : (_jsxs("div", { className: "flex min-w-0 flex-col gap-2", children: [writing.internal === true || writing.recipients === undefined ? null : (_jsxs("p", { className: "flex min-w-0 items-center gap-1.5 text-micro text-muted", title: copy.activity.recipients, children: [_jsx(Users, { className: "size-3.5 shrink-0", "aria-hidden": true }), _jsx("span", { className: "min-w-0 truncate", children: writing.recipients.length === 0
41
+ ? copy.activity.noRecipients
42
+ : writing.recipients.join(" · ") })] })), _jsx(TextArea, { value: body, onChange: setBody, placeholder: writing.placeholder ?? copy.activity.messagePlaceholder, rows: 3 }), _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [_jsx(Button, { role: "secondary", size: "sm", icon: Send, disabled: !ready, onClick: () => {
40
43
  onSend(body.trim(), writing);
41
44
  close();
42
- }, children: sending ? copy.chatter.sending : copy.chatter.send }), _jsx(Button, { role: "ghost", size: "sm", icon: X, onClick: close, children: copy.chatter.cancel })] })] }))] }));
45
+ }, children: sending ? copy.activity.sending : copy.activity.send }), _jsx(Button, { role: "ghost", size: "sm", icon: X, onClick: close, children: copy.activity.cancel })] })] }))] }));
43
46
  }
@@ -1,3 +1,3 @@
1
- export { Chatter, type ChatterEvent, type ChatterLayout, type ChatterProps, type ChatterView, useChatter, } from "./Chatter.js";
1
+ export { Activity, type ActivityAlert, type ActivityEvent, type ActivityLayout, type ActivityProps, useActivity, } from "./Activity.js";
2
2
  export { type Message, MessageComposer, type MessageComposerProps, type MessageKind, writeANote, } from "./Messages.js";
3
3
  export { Timeline, type TimelineItemProps, type TimelineProps, } from "./Timeline.js";
@@ -0,0 +1,4 @@
1
+ /* ACTIVITY — everything around a record: its notices, its conversation and its history. */
2
+ export { Activity, useActivity, } from "./Activity.js";
3
+ export { MessageComposer, writeANote, } from "./Messages.js";
4
+ export { Timeline, } from "./Timeline.js";
@@ -1,5 +1,5 @@
1
+ import type { ActivityEvent } from "../activity/Activity.js";
1
2
  import { type AssistOrigin, type AssistSource } from "./assist.js";
2
- import type { ChatterEvent } from "../chatter/Chatter.js";
3
3
  import type { ReactNode } from "react";
4
4
  /** The robot and the assistant's name: the header of a suggestion, a review, a pending
5
5
  * state. `children` replaces the name — «Asistente sugiere». */
@@ -43,7 +43,7 @@ export declare function AssistedValue({ children, origin, }: {
43
43
  origin?: AssistOrigin;
44
44
  }): import("react").JSX.Element;
45
45
  /**
46
- * The assistant's entry in a record's history: the chatter's event, with the robot as its
46
+ * The assistant's entry in a record's history: the activity's event, with the robot as its
47
47
  * node, the assistant (and who accepted) as its author, and the source as its detail.
48
48
  *
49
49
  * Context: provenance has two places. The mark answers «where did THIS value come from»
@@ -61,4 +61,4 @@ export declare function assistEvent({ id, label, at, source, acceptedBy, }: {
61
61
  at: Date | string | number;
62
62
  source?: AssistSource;
63
63
  acceptedBy?: string;
64
- }): ChatterEvent;
64
+ }): ActivityEvent;
@@ -81,7 +81,7 @@ export function AssistedValue({ children, origin, }) {
81
81
  return (_jsxs("span", { className: "inline-flex max-w-full min-w-0 items-center gap-0.5", children: [_jsx("span", { className: "min-w-0 truncate", children: children }), _jsx(AssistMark, { ...(origin === undefined ? {} : { origin }) })] }));
82
82
  }
83
83
  /**
84
- * The assistant's entry in a record's history: the chatter's event, with the robot as its
84
+ * The assistant's entry in a record's history: the activity's event, with the robot as its
85
85
  * node, the assistant (and who accepted) as its author, and the source as its detail.
86
86
  *
87
87
  * Context: provenance has two places. The mark answers «where did THIS value come from»
@@ -7,7 +7,7 @@ import type { ReactNode } from "react";
7
7
  * automated assistant did this», which is a fact, not a celebration.
8
8
  *
9
9
  * Exported so a screen that composes the assistant into a piece of its own — a `Callout`,
10
- * a `ChatterEvent` — uses the same mark and not a second one. */
10
+ * a `ActivityEvent` — uses the same mark and not a second one. */
11
11
  export declare const ASSIST_ICON: LucideIcon;
12
12
  /** Where a suggestion or a change comes from: «orden de compra PO/2026/0042», «las últimas
13
13
  * 5 facturas del cliente». With `href` it is a real link; with `onOpen`, a button that the
@@ -24,7 +24,7 @@ import { Bot } from "lucide-react";
24
24
  * automated assistant did this», which is a fact, not a celebration.
25
25
  *
26
26
  * Exported so a screen that composes the assistant into a piece of its own — a `Callout`,
27
- * a `ChatterEvent` — uses the same mark and not a second one. */
27
+ * a `ActivityEvent` — uses the same mark and not a second one. */
28
28
  export const ASSIST_ICON = Bot;
29
29
  /**
30
30
  * Runs `apply` and, when it returns an undo and there is a notice stack, offers it.
@@ -1,4 +1,5 @@
1
1
  import { type SheetLayout } from "../../primitives/field/layout.js";
2
+ import { type FieldNote } from "../../primitives/field/note.js";
2
3
  import type { LucideIcon } from "lucide-react";
3
4
  import { type ReactNode } from "react";
4
5
  export type { SheetLayout };
@@ -8,6 +9,9 @@ export type SheetField = {
8
9
  /** Help on the label, as a `?`. Hover or click. */
9
10
  hint?: string;
10
11
  mono?: boolean;
12
+ /** A note that belongs to this fact — the same as `Field`'s: a flag by the label, and an
13
+ * entry in the record's activity. */
14
+ note?: FieldNote;
11
15
  };
12
16
  export declare function Sheet({ fields, columns, layout, head, children, }: {
13
17
  fields?: readonly SheetField[];
@@ -2,10 +2,12 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { byDensity, useDensity } from "../../foundations/density.js";
4
4
  import { FieldLayoutProvider, } from "../../primitives/field/layout.js";
5
+ import { NoteCard, NoteFlag } from "../../primitives/field/note.js";
5
6
  import { Hint } from "../../primitives/hint/Hint.js";
6
- import { Children } from "react";
7
+ import { Children, useId } from "react";
7
8
  function Facts({ fields, columns, layout, }) {
8
9
  const d = useDensity();
10
+ const idBase = useId();
9
11
  const horizontal = layout === "horizontal";
10
12
  /* Horizontal facts are a run of pairs, not a grid stretched across the
11
13
  paper. A two-column grid put «Total Bs 12.480» on the left and «Vence
@@ -20,10 +22,15 @@ function Facts({ fields, columns, layout, }) {
20
22
  ? "flex min-w-0 flex-wrap items-baseline gap-x-6 gap-y-1.5"
21
23
  : asGrid
22
24
  ? `@container grid min-w-0 ${grid} ${byDensity(d, "gap-x-8 gap-y-2.5", "gap-x-6 gap-y-2")}`
23
- : `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: fields.map((field) => {
25
+ : `flex min-w-0 flex-col ${byDensity(d, "gap-2.5", "gap-2")}`, children: fields.map((field, at) => {
26
+ const noteId = field.note === undefined ? undefined : `${idBase}-${at}-note`;
24
27
  const empty = field.value === undefined || field.value === null || field.value === "";
25
- const value = (_jsx("dd", { className: `min-w-0 text-meta ${empty ? "text-muted italic" : "text-ink"} ${field.mono === true ? "font-mono tabular-nums" : ""}`, children: empty ? "—" : field.value }));
26
- const name = (_jsxs("dt", { className: `flex items-center gap-1 text-micro text-muted ${horizontal ? "shrink-0" : "min-w-0"}`, children: [_jsx("span", { children: field.label }), field.hint === undefined ? null : _jsx(Hint, { children: field.hint })] }));
28
+ const card = field.note === undefined || noteId === undefined ? null : (_jsx(NoteCard, { note: field.note, id: noteId, owner: field.label }));
29
+ /* The note's card goes INSIDE the value: a `<dl>` holds only its terms and values.
30
+ With a note the value is a flex box, so the absolute card keeps the value's top
31
+ as its static position while being read after it. */
32
+ const value = (_jsxs("dd", { ...(noteId === undefined ? {} : { "aria-describedby": noteId }), className: `min-w-0 text-meta ${card === null ? "" : "flex"} ${empty ? "text-muted italic" : "text-ink"} ${field.mono === true ? "font-mono tabular-nums" : ""}`, children: [empty ? "—" : field.value, card] }));
33
+ const name = (_jsxs("dt", { className: `flex items-center gap-1 text-micro text-muted ${horizontal ? "shrink-0" : "min-w-0"}`, children: [_jsx("span", { children: field.label }), field.hint === undefined ? null : _jsx(Hint, { children: field.hint }), field.note === undefined ? null : _jsx(NoteFlag, { note: field.note })] }));
27
34
  if (horizontal) {
28
35
  /* A header fact is a pair: the number sits next to its name. The
29
36
  8rem label column is for a form, where twenty values have to
@@ -1,5 +1,5 @@
1
+ export * from "./activity/index.js";
1
2
  export * from "./assist/index.js";
2
- export * from "./chatter/index.js";
3
3
  export * from "./form/index.js";
4
4
  export * from "./frame/index.js";
5
5
  export * from "./lines/index.js";