@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
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,
|
|
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
|
|
@@ -116,8 +143,8 @@ theme switch, or open a story with `&globals=theme:dark`.
|
|
|
116
143
|
```bash
|
|
117
144
|
make init # pre-commit + yarn install
|
|
118
145
|
make dev # Storybook on :6006 — the living documentation
|
|
119
|
-
make test #
|
|
120
|
-
make smoke #
|
|
146
|
+
make test # vitest: logic in Node + every story in Chrome (play, a11y), light and dark
|
|
147
|
+
make smoke # only the stories: their play, a11y and screen rules, light and dark
|
|
121
148
|
make build # dist/ (JS + .d.ts + tokens.css)
|
|
122
149
|
```
|
|
123
150
|
|
|
@@ -130,10 +157,10 @@ 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,
|
|
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
|
-
tests/
|
|
163
|
+
tests/ vitest, `model` project: logic and static markup in Node
|
|
137
164
|
```
|
|
138
165
|
|
|
139
166
|
`make doctor` rejects an import that climbs a level (a primitive importing a component, a
|
|
@@ -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`](
|
|
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](
|
|
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](
|
|
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
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type ActivityState = "overdue" | "today" | "planned" | "none";
|
|
3
|
+
/**
|
|
4
|
+
* The record's next planned activity, on its card or its row: a clock in the tone of how late
|
|
5
|
+
* it is — overdue, today, planned — and the word with it.
|
|
6
|
+
*
|
|
7
|
+
* Context: in a pipeline the question is not «what stage» but «who is waiting on us»; Odoo
|
|
8
|
+
* answers it with this clock on every card. A tone alone says nothing to a screen reader or to
|
|
9
|
+
* whoever does not see red, so the state is the name, and `detail` («Llamar · mañana 10:00») is
|
|
10
|
+
* the title. With `onClick` it opens the activities (a popover, the record's activity panel).
|
|
11
|
+
*/
|
|
12
|
+
export declare function ActivityBadge({ state, detail, onClick, }: {
|
|
13
|
+
state: ActivityState;
|
|
14
|
+
detail?: ReactNode;
|
|
15
|
+
onClick?: () => void;
|
|
16
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useUiCopy } from "../../foundations/copy.js";
|
|
4
|
+
import { Clock } from "lucide-react";
|
|
5
|
+
const TONE = {
|
|
6
|
+
overdue: "text-bad",
|
|
7
|
+
today: "text-warn",
|
|
8
|
+
planned: "text-good",
|
|
9
|
+
none: "text-muted",
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* The record's next planned activity, on its card or its row: a clock in the tone of how late
|
|
13
|
+
* it is — overdue, today, planned — and the word with it.
|
|
14
|
+
*
|
|
15
|
+
* Context: in a pipeline the question is not «what stage» but «who is waiting on us»; Odoo
|
|
16
|
+
* answers it with this clock on every card. A tone alone says nothing to a screen reader or to
|
|
17
|
+
* whoever does not see red, so the state is the name, and `detail` («Llamar · mañana 10:00») is
|
|
18
|
+
* the title. With `onClick` it opens the activities (a popover, the record's activity panel).
|
|
19
|
+
*/
|
|
20
|
+
export function ActivityBadge({ state, detail, onClick, }) {
|
|
21
|
+
const copy = useUiCopy();
|
|
22
|
+
const word = copy.activityState[state];
|
|
23
|
+
const title = typeof detail === "string" ? `${word} · ${detail}` : word;
|
|
24
|
+
const body = (_jsxs(_Fragment, { children: [_jsx(Clock, { "aria-hidden": true, className: `size-(--size-icon-sm) ${TONE[state]}`, strokeWidth: 2.25 }), _jsx("span", { className: "sr-only", children: word })] }));
|
|
25
|
+
if (onClick === undefined) {
|
|
26
|
+
return (_jsx("span", { title: title, className: "inline-flex items-center p-1", children: body }));
|
|
27
|
+
}
|
|
28
|
+
return (_jsx("button", { type: "button", title: title, onClick: onClick, className: "relative z-10 inline-flex items-center rounded-pill p-1 transition-colors duration-(--duration-instant) hover:bg-ink/8 focus-visible:outline-2 focus-visible:outline-primary", children: body }));
|
|
29
|
+
}
|
|
@@ -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 `
|
|
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.
|
|
19
|
+
label: copy.activity.writeNote,
|
|
20
20
|
internal: true,
|
|
21
|
-
placeholder: copy.
|
|
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, {
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
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
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Activity, type ActivityAlert, type ActivityEvent, type ActivityLayout, type ActivityProps, useActivity, } from "./Activity.js";
|
|
2
|
+
export { ActivityBadge, type ActivityState, } from "./ActivityBadge.js";
|
|
3
|
+
export { type Message, MessageComposer, type MessageComposerProps, type MessageKind, writeANote, } from "./Messages.js";
|
|
4
|
+
export { Timeline, type TimelineItemProps, type TimelineProps, } from "./Timeline.js";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/* ACTIVITY — everything around a record: its notices, its conversation and its history. */
|
|
2
|
+
export { Activity, useActivity, } from "./Activity.js";
|
|
3
|
+
export { ActivityBadge, } from "./ActivityBadge.js";
|
|
4
|
+
export { MessageComposer, writeANote, } from "./Messages.js";
|
|
5
|
+
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
|
|
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
|
-
}):
|
|
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
|
|
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 `
|
|
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 `
|
|
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.
|