@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.
- package/README.md +37 -10
- package/dist/components/activity/Activity.d.ts +69 -0
- package/dist/components/activity/Activity.js +279 -0
- package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
- package/dist/components/{chatter → activity}/Messages.js +9 -6
- package/dist/components/{chatter → activity}/index.d.ts +1 -1
- package/dist/components/activity/index.js +4 -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/form/Sheet.d.ts +4 -0
- package/dist/components/form/Sheet.js +11 -4
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +1 -1
- package/dist/components/lines/LineItems.d.ts +42 -5
- package/dist/components/lines/LineItems.js +106 -18
- package/dist/components/listing/Listing.js +1 -1
- package/dist/components/listing/ListingBar.d.ts +41 -2
- package/dist/components/listing/ListingBar.js +43 -4
- package/dist/components/listing/Row.d.ts +5 -1
- package/dist/components/listing/Row.js +3 -2
- package/dist/components/record/RecordHeader.d.ts +21 -17
- package/dist/components/record/RecordHeader.js +46 -14
- package/dist/components/record/RecordNotices.d.ts +37 -0
- package/dist/components/record/RecordNotices.js +75 -0
- package/dist/components/record/ThumbBar.js +1 -1
- package/dist/foundations/copy.d.ts +96 -12
- package/dist/foundations/copy.es.js +63 -10
- package/dist/foundations/copy.js +55 -7
- package/dist/primitives/choice/ComboBox.d.ts +3 -0
- package/dist/primitives/choice/ComboBox.js +2 -2
- package/dist/primitives/choice/SearchField.d.ts +10 -1
- package/dist/primitives/choice/SearchField.js +21 -2
- 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/Drill.d.ts +90 -0
- package/dist/primitives/dialog/Drill.js +172 -0
- package/dist/primitives/dialog/index.d.ts +1 -0
- package/dist/primitives/dialog/index.js +2 -0
- 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 +1 -0
- package/dist/primitives/notice/index.js +1 -0
- package/dist/primitives/overlay/Preview.d.ts +19 -0
- package/dist/primitives/overlay/Preview.js +60 -0
- package/dist/primitives/overlay/index.d.ts +1 -0
- package/dist/primitives/overlay/index.js +3 -1
- package/dist/primitives/picker/Picker.d.ts +41 -0
- package/dist/primitives/picker/Picker.js +62 -8
- 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 +97 -2
- package/dist/primitives/table/EditableTable.js +223 -29
- package/dist/primitives/table/SheetTable.d.ts +4 -1
- package/dist/primitives/table/SheetTable.js +38 -5
- 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/TagsField.d.ts +3 -0
- package/dist/primitives/tag/TagsField.js +6 -2
- 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/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/ProgressBar.d.ts +31 -0
- package/dist/primitives/value/ProgressBar.js +53 -0
- package/dist/primitives/value/Relation.d.ts +6 -1
- package/dist/primitives/value/Relation.js +27 -10
- 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/index.d.ts +4 -3
- package/dist/primitives/value/index.js +4 -3
- package/dist/tokens.css +44 -7
- package/dist/views/list/List.d.ts +8 -2
- package/dist/views/list/List.js +6 -2
- package/dist/views/list/ListView.d.ts +1 -1
- package/dist/views/list/ListView.js +34 -8
- package/dist/views/list/derive.d.ts +12 -1
- package/dist/views/list/derive.js +29 -3
- package/dist/views/list/presentations.d.ts +2 -2
- package/dist/views/list/presentations.js +5 -3
- package/dist/views/list/types.d.ts +13 -1
- package/dist/views/record/Record.d.ts +27 -8
- package/dist/views/record/Record.js +97 -27
- package/dist/views/record/index.d.ts +1 -1
- package/package.json +5 -1
- package/dist/components/chatter/Chatter.d.ts +0 -161
- package/dist/components/chatter/Chatter.js +0 -232
- package/dist/components/chatter/index.js +0 -4
- /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
- /package/dist/components/{chatter → activity}/Timeline.js +0 -0
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { Activity } from "../../components/activity/Activity.js";
|
|
3
4
|
import { Sheet, } from "../../components/form/Sheet.js";
|
|
4
5
|
import { Pager as PagerControl } from "../../components/record/Pager.js";
|
|
5
6
|
import { RecordHeader } from "../../components/record/RecordHeader.js";
|
|
7
|
+
import { RecordBlocking, RecordNoticeIndicator, splitNotices, } from "../../components/record/RecordNotices.js";
|
|
6
8
|
import { RecordSkeleton } from "../../components/record/RecordSkeleton.js";
|
|
7
9
|
import { SmartButton, SmartButtons } from "../../components/record/SmartButtons.js";
|
|
8
10
|
import { StatusBar } from "../../components/record/StatusBar.js";
|
|
9
11
|
import { useUiCopy } from "../../foundations/copy.js";
|
|
10
12
|
import { byDensity, useDensity } from "../../foundations/density.js";
|
|
11
|
-
import { flatten, pick as pickSlot, propsOf, slot, slotOf, } from "../../foundations/slots.js";
|
|
12
|
-
import {
|
|
13
|
+
import { flatten, pick as pickSlot, pickAll, propsOf, slot, slotOf, } from "../../foundations/slots.js";
|
|
14
|
+
import { createNoteRegistry, NoteRegistryContext, } from "../../primitives/field/note.js";
|
|
13
15
|
import { Tabs } from "../../primitives/tabs/Tabs.js";
|
|
16
|
+
import { useEffect, useId, useState, useSyncExternalStore } from "react";
|
|
14
17
|
const pick = (nodes, name) => pickSlot(nodes, name);
|
|
15
18
|
/** El riel: mi lugar delante del registro. Sin él, no hay banda — y eso
|
|
16
19
|
* es exactamente lo que un diálogo quiere. */
|
|
@@ -31,16 +34,26 @@ const Actions = slot("actions", (_props) => null);
|
|
|
31
34
|
* quien la pasaba armada tenía que acordarse de la superficie del riel. Olvidarlo
|
|
32
35
|
* pintaba la paleta del lienzo encima del chrome, y pasó. */
|
|
33
36
|
const Status = slot("status", (_props) => null);
|
|
37
|
+
/** What has to be known about THIS record before acting on it: «este cliente tiene
|
|
38
|
+
* Bs. 1.200 en créditos pendientes · Aplicar», «posible factura duplicada», «vencida
|
|
39
|
+
* hace 12 días», a blocking error from the backend. A `Callout` — the tone, one
|
|
40
|
+
* sentence, an optional `title`, an optional `action` (secondary or ghost: the primary
|
|
41
|
+
* belongs to the rail) and `onDismiss` when it can be put away.
|
|
42
|
+
*
|
|
43
|
+
* Context: written as a child of `Record` or of `Record.Paper`, as many as are true —
|
|
44
|
+
* `{overdue ? <Record.Notice tone="warn">…</Record.Notice> : null}`. Where they go is
|
|
45
|
+
* the record's, and NONE goes among the fields: a `bad` one (blocking) stays visible
|
|
46
|
+
* between the rail and the paper; the rest gather in one indicator in the rail —
|
|
47
|
+
* «⚠ 2 avisos», toned by the most urgent — that opens their list on press. Both in
|
|
48
|
+
* urgency order. A note about ONE field is that field's `note`. See `RecordNotices`.
|
|
49
|
+
*
|
|
50
|
+
* Named `Notice` and not `Alert`: an ARIA `alert` interrupts the screen reader, and a
|
|
51
|
+
* notice that was already true when the record opened interrupts nothing (`Callout`
|
|
52
|
+
* is `role="status"`). */
|
|
53
|
+
const NoticeSlot = slot("notice", (_props) => null);
|
|
34
54
|
/** El papel: el registro. El `<sheet>` blanco sobre el piso. */
|
|
35
55
|
const Paper = slot("paper", (_props) => null);
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
* Va al lado y no abajo porque la historia es el contexto de todo lo demás que
|
|
39
|
-
* está en pantalla — leer «la factura fue rechazada» mirando los datos que la
|
|
40
|
-
* produjeron es justamente para lo que sirve, y una sección más abajo obliga a
|
|
41
|
-
* ver una cosa o la otra. Lo que va adentro lo arma quien compone: el kit no
|
|
42
|
-
* sabe qué es un evento de este producto. */
|
|
43
|
-
const ChatterSlot = slot("chatter", (_props) => null);
|
|
56
|
+
const ActivitySlot = slot("activity", (_props) => null);
|
|
44
57
|
/** Los hechos globales del registro. Están en TODAS las páginas, y por eso van
|
|
45
58
|
* arriba del cuaderno y no adentro de una. */
|
|
46
59
|
const Facts = slot("facts", (_props) => null);
|
|
@@ -54,11 +67,10 @@ export function Record({ identity, error = null, dirty, children }) {
|
|
|
54
67
|
const top = flatten(children);
|
|
55
68
|
const railNode = pick(top, "rail");
|
|
56
69
|
const paperNode = pick(top, "paper");
|
|
57
|
-
const
|
|
58
|
-
const chatter = chatterSlot?.children;
|
|
70
|
+
const activitySlot = propsOf(pick(top, "activity"));
|
|
59
71
|
/* La escotilla: lo que el arquetipo no previó se dibuja igual, en el papel.
|
|
60
72
|
Que aparezca algo acá es la señal de que al arquetipo le falta una pieza. */
|
|
61
|
-
const loose = top.filter((node) => slotOf(node) !== "rail" && slotOf(node) !== "paper" && slotOf(node) !== "
|
|
73
|
+
const loose = top.filter((node) => slotOf(node) !== "rail" && slotOf(node) !== "paper" && slotOf(node) !== "activity");
|
|
62
74
|
const rail = flatten(propsOf(railNode)?.children);
|
|
63
75
|
const crumbs = propsOf(pick(rail, "crumbs"))?.crumbs;
|
|
64
76
|
const pager = propsOf(pick(rail, "pager"));
|
|
@@ -81,6 +93,37 @@ export function Record({ identity, error = null, dirty, children }) {
|
|
|
81
93
|
const paper = propsOf(paperNode);
|
|
82
94
|
const inPaper = [...flatten(paper?.children), ...loose];
|
|
83
95
|
const fields = propsOf(pick(inPaper, "facts"))?.fields;
|
|
96
|
+
/* Written at the top of the record or inside the paper: both are «on the record». */
|
|
97
|
+
const notices = pickAll(inPaper, "notice")
|
|
98
|
+
.map((node) => propsOf(node))
|
|
99
|
+
.filter((notice) => notice !== undefined);
|
|
100
|
+
const { blocking, rest } = splitNotices(notices);
|
|
101
|
+
/* The notes of the record's fields and rows, which the cards register as they mount. */
|
|
102
|
+
const [registry] = useState(createNoteRegistry);
|
|
103
|
+
const notes = useSyncExternalStore(registry.subscribe, registry.snapshot, registry.snapshot);
|
|
104
|
+
/* The panel's open state is the record's, so the rail's indicator can open it. */
|
|
105
|
+
const [ownOpen, setOwnOpen] = useState(activitySlot?.defaultOpen ?? true);
|
|
106
|
+
const activityOpen = activitySlot?.open ?? ownOpen;
|
|
107
|
+
const setActivityOpen = (next) => {
|
|
108
|
+
activitySlot?.onOpenChange?.(next);
|
|
109
|
+
if (activitySlot?.open === undefined)
|
|
110
|
+
setOwnOpen(next);
|
|
111
|
+
};
|
|
112
|
+
const alertsId = useId();
|
|
113
|
+
/* «⚠ 2 avisos» with a panel to go to: open it, then — once it is drawn — bring its
|
|
114
|
+
notices into view and put the focus on them. On a wide record they are beside the
|
|
115
|
+
sheet already; on a narrow one the panel is below and this is the scroll that finds
|
|
116
|
+
it. A counter and not a flag, so pressing it twice moves the focus twice. */
|
|
117
|
+
const [reveal, setReveal] = useState(0);
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (reveal === 0)
|
|
120
|
+
return;
|
|
121
|
+
const section = document.getElementById(alertsId);
|
|
122
|
+
if (section === null)
|
|
123
|
+
return;
|
|
124
|
+
section.scrollIntoView({ block: "start" });
|
|
125
|
+
section.focus({ preventScroll: true });
|
|
126
|
+
}, [reveal, alertsId]);
|
|
84
127
|
const notebook = propsOf(pick(inPaper, "pages"));
|
|
85
128
|
const pages = flatten(notebook?.children).filter((node) => slotOf(node) === "page");
|
|
86
129
|
const tabs = pages
|
|
@@ -113,19 +156,45 @@ export function Record({ identity, error = null, dirty, children }) {
|
|
|
113
156
|
bar (last in it, `mt-auto`) reaches the bottom of the screen even when the record is
|
|
114
157
|
shorter than the screen. The extra height has no paint of its own: on a desk
|
|
115
158
|
nothing changes. */
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
/* The record knows the notes of its fields and rows: their cards register here. */
|
|
160
|
+
_jsx(NoteRegistryContext, { value: registry, children: _jsx("article", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(RecordHeader, { railSurface: surface, ...(dirty === undefined ? {} : { dirty }), ...(crumbs === undefined ? {} : { crumbs }), ...(pager === undefined
|
|
161
|
+
? {}
|
|
162
|
+
: {
|
|
163
|
+
pager: (_jsx(PagerControl, { index: pager.index, total: pager.total, surface: surface, ...(pager.onPrev === undefined ? {} : { onPrev: pager.onPrev }), ...(pager.onNext === undefined ? {} : { onNext: pager.onNext }) })),
|
|
164
|
+
}), ...(actions === undefined ? {} : { actions }), ...(statusBlock === undefined ? {} : { status: statusBlock }), ...(identity.icon === undefined ? {} : { icon: identity.icon }), title: identity.title, ...(identity.subtitle === undefined ? {} : { subtitle: identity.subtitle }), ...(identity.figure === undefined ? {} : { figure: identity.figure }), ...(identity.tag === undefined ? {} : { tag: identity.tag }), ...(smart === undefined || smart.length === 0
|
|
165
|
+
? {}
|
|
166
|
+
: {
|
|
167
|
+
smart: (_jsx(SmartButtons, { children: smart.map((s) => (_jsx(SmartButton, { icon: s.icon, ...(s.count === undefined ? {} : { count: s.count }), label: s.label, layout: onTheRail && d === "comfortable" ? "inline" : "stacked", ...(s.onClick === undefined ? {} : { onClick: s.onClick }) }, s.label))) })),
|
|
168
|
+
}), ...(aspectsNode === undefined || globalSheet ? {} : { aspects: aspectsNode }), ...(activitySlot === undefined
|
|
169
|
+
? {}
|
|
170
|
+
: {
|
|
171
|
+
activity: (_jsx(Activity, { ...activitySlot, alerts: rest, alertsId: alertsId, open: activityOpen, onToggle: () => setActivityOpen(!activityOpen) })),
|
|
172
|
+
activityLayout: activitySlot.layout ?? "auto",
|
|
173
|
+
}), ...(blocking.length === 0 && error === null
|
|
174
|
+
? {}
|
|
175
|
+
: {
|
|
176
|
+
blocking: (_jsx(RecordBlocking, { notices: blocking, error: error, errorHeading: copy.failed.operation })),
|
|
177
|
+
}), ...(rest.length === 0 && (activitySlot === undefined || notes.length === 0)
|
|
178
|
+
? {}
|
|
179
|
+
: {
|
|
180
|
+
indicator: (_jsx(RecordNoticeIndicator, { notices: rest, anchored: notes.map((entry) => ({ tone: entry.note.tone ?? "info" })), ...(activitySlot === undefined
|
|
181
|
+
? {}
|
|
182
|
+
: {
|
|
183
|
+
/* With a panel, the notices live there: the indicator goes to them
|
|
184
|
+
instead of floating a second copy in a popover. */
|
|
185
|
+
reveal: {
|
|
186
|
+
target: alertsId,
|
|
187
|
+
onReveal: () => {
|
|
188
|
+
setActivityOpen(true);
|
|
189
|
+
setReveal((n) => n + 1);
|
|
190
|
+
},
|
|
191
|
+
},
|
|
192
|
+
}) })),
|
|
193
|
+
}), children: _jsx("div", { className: "min-w-0", children: _jsx("div", { className: `flex min-w-0 flex-col ${byDensity(d, "gap-(--spacing-block) px-4 pt-(--spacing-block) pb-4", "gap-2.5 pt-2.5 pb-2")}`, children: _jsxs(Sheet, { layout: paper?.layout ?? "stacked", columns: paper?.columns ?? 2, ...(hasFacts ? { fields } : {}), ...(above.length > 0 || (globalSheet && aspectsNode !== undefined)
|
|
194
|
+
? {
|
|
195
|
+
head: (_jsxs(_Fragment, { children: [above, globalSheet && aspectsNode !== undefined ? (_jsx("div", { className: `border-b border-line-strong ${byDensity(d, "-mx-4 px-4", "")}`, children: aspectsNode })) : null] })),
|
|
196
|
+
}
|
|
197
|
+
: {}), children: [pageBody, below] }) }) }) }) }) }));
|
|
129
198
|
}
|
|
130
199
|
Record.Skeleton = RecordSkeleton;
|
|
131
200
|
Record.Rail = Rail;
|
|
@@ -135,7 +204,8 @@ Record.Smart = Smart;
|
|
|
135
204
|
Record.Actions = Actions;
|
|
136
205
|
Record.Status = Status;
|
|
137
206
|
Record.Paper = Paper;
|
|
138
|
-
Record.
|
|
207
|
+
Record.Activity = ActivitySlot;
|
|
139
208
|
Record.Facts = Facts;
|
|
140
209
|
Record.Pages = Pages;
|
|
141
210
|
Record.Page = Page;
|
|
211
|
+
Record.Notice = NoticeSlot;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Record, type RecordIdentity, type RecordPager, type RecordProps, type SmartLink, } from "./Record.js";
|
|
1
|
+
export { Record, type RecordIdentity, type RecordNotice, type RecordPager, type RecordProps, type SmartLink, } from "./Record.js";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/web-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "An enterprise web design system for business software — ERPs, back offices, management tools — in React and Tailwind: tokens, primitives, components and the two views every management screen is made of.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -99,10 +99,14 @@
|
|
|
99
99
|
"ui",
|
|
100
100
|
"sincpro"
|
|
101
101
|
],
|
|
102
|
+
"homepage": "https://github.com/Sincpro-SRL/sincpro_web_ui#readme",
|
|
102
103
|
"repository": {
|
|
103
104
|
"type": "git",
|
|
104
105
|
"url": "git+https://github.com/Sincpro-SRL/sincpro_web_ui.git"
|
|
105
106
|
},
|
|
107
|
+
"bugs": {
|
|
108
|
+
"url": "https://github.com/Sincpro-SRL/sincpro_web_ui/issues"
|
|
109
|
+
},
|
|
106
110
|
"author": "Gutierrez, Andres <andres.gutierrez@sincpro.com.bo>",
|
|
107
111
|
"license": "MIT",
|
|
108
112
|
"engines": {
|
|
@@ -1,161 +0,0 @@
|
|
|
1
|
-
import type { Message } from "./Messages.js";
|
|
2
|
-
import type { Tone } from "../../foundations/types.js";
|
|
3
|
-
import type { LucideIcon } from "lucide-react";
|
|
4
|
-
import { type ReactNode } from "react";
|
|
5
|
-
/**
|
|
6
|
-
* What a record has been through: what the system logged and what people wrote,
|
|
7
|
-
* in one place.
|
|
8
|
-
*
|
|
9
|
-
* Odoo calls it the chatter and the idea is the one worth taking: a record's
|
|
10
|
-
* history is not one more section of the record — it is the CONTEXT for
|
|
11
|
-
* everything else on the screen. Reading «the assessment failed» while looking
|
|
12
|
-
* at the inputs that produced it is the whole point.
|
|
13
|
-
*
|
|
14
|
-
* ── The merge is the default, and that is the decision ──────────────
|
|
15
|
-
*
|
|
16
|
-
* **Everything together comes first**, because the question somebody actually
|
|
17
|
-
* has is «what happened here», not «what happened here *according to the
|
|
18
|
-
* system*». A payment that bounced and the message somebody left about it are one
|
|
19
|
-
* story; splitting them by who wrote them makes the reader join them back by
|
|
20
|
-
* hand, comparing two timestamps in two lists.
|
|
21
|
-
*
|
|
22
|
-
* The other two tabs are for when you already know which half you want: «who
|
|
23
|
-
* said what» and «what ran». They are a narrowing of the first, never a
|
|
24
|
-
* different content.
|
|
25
|
-
*
|
|
26
|
-
* ── Why the tabs live HERE and not in whoever composes ──────────────
|
|
27
|
-
*
|
|
28
|
-
* They used to be a generic `tabs` array and every screen decided its own: same
|
|
29
|
-
* component, a different chatter on each record. What a record has been through
|
|
30
|
-
* is the same question everywhere, so the answer is the component's, not the
|
|
31
|
-
* screen's. The screen passes what it HAS —messages, events, a composer— and
|
|
32
|
-
* this decides how it is read.
|
|
33
|
-
*
|
|
34
|
-
* A tab with no source behind it is not drawn: with only events there is
|
|
35
|
-
* nothing to merge and nothing to separate, so it shows the stream and no tab
|
|
36
|
-
* bar at all.
|
|
37
|
-
*
|
|
38
|
-
* ── Cut into pages, like any other list ─────────────────────────────
|
|
39
|
-
*
|
|
40
|
-
* A record with six months behind it has hundreds of entries, and a column that
|
|
41
|
-
* only scrolls turns the history into something you give up on. The pages are
|
|
42
|
-
* this component's and not the screen's for the same reason the tabs are: it is
|
|
43
|
-
* the same list on every record.
|
|
44
|
-
*
|
|
45
|
-
* ── Beside on a wide panel, underneath on a narrow one ──────────────
|
|
46
|
-
*
|
|
47
|
-
* It asks `@container`, not `lg:`, for the reason `SectionGrid` already wrote
|
|
48
|
-
* down: with four panels open each one measures 380 px on a 1.600 screen, and a
|
|
49
|
-
* window rule would put a 22rem column inside a 380 px panel. **Whoever mounts
|
|
50
|
-
* it has to declare `@container` on the container** — `Record` does.
|
|
51
|
-
*/
|
|
52
|
-
/**
|
|
53
|
-
* Algo que el sistema anotó.
|
|
54
|
-
*
|
|
55
|
-
* Deliberadamente NO es el evento del producto: lo que una historia necesita
|
|
56
|
-
* para dibujar uno es qué pasó, cuándo, quién, y —cuando las hay— las tres cosas
|
|
57
|
-
* que uno termina yendo a buscar a otro lado.
|
|
58
|
-
*
|
|
59
|
-
* ── Las tres que faltaban, y por qué cada una ───────────────────────
|
|
60
|
-
*
|
|
61
|
-
* by QUIÉN. «se publicó la factura» no dice si la publicó una persona o un
|
|
62
|
-
* cron, y esa es la primera pregunta cuando algo cambió sin aviso.
|
|
63
|
-
* Ausente = lo hizo el sistema, que también es una respuesta.
|
|
64
|
-
*
|
|
65
|
-
* moved DE QUÉ A QUÉ. «el proyecto cambió de estado» obliga a mirar el
|
|
66
|
-
* evento anterior para saber de dónde venía. «borrador →
|
|
67
|
-
* confirmado» se lee de una y es la línea más útil de una historia
|
|
68
|
-
* de gestión.
|
|
69
|
-
*
|
|
70
|
-
* error QUÉ falló, entero y textual. Un evento rojo que dice «falló» sin
|
|
71
|
-
* decir por qué manda a alguien a los logs — y quien mira esta
|
|
72
|
-
* pantalla muchas veces no tiene acceso a los logs.
|
|
73
|
-
*/
|
|
74
|
-
export type ChatterEvent = {
|
|
75
|
-
id: string;
|
|
76
|
-
/** Qué pasó, en una oración y en pasado. */
|
|
77
|
-
label: ReactNode;
|
|
78
|
-
at: Date | string | number;
|
|
79
|
-
tone?: Tone;
|
|
80
|
-
/** Quién lo hizo. Ausente = el sistema. */
|
|
81
|
-
by?: string;
|
|
82
|
-
/** El cambio de estado, cuando lo hubo. `from` ausente = nació así. */
|
|
83
|
-
moved?: {
|
|
84
|
-
from?: string;
|
|
85
|
-
to: string;
|
|
86
|
-
};
|
|
87
|
-
/** Lo que hay que saber además del título, cuando el título no alcanza. */
|
|
88
|
-
detail?: ReactNode;
|
|
89
|
-
/** Lo que falló, textual. Se muestra la PRIMERA línea y el resto se abre: un
|
|
90
|
-
* stack de cuarenta renglones adentro de una historia tapa la historia, y lo
|
|
91
|
-
* que casi siempre alcanza es el primer renglón. */
|
|
92
|
-
error?: string;
|
|
93
|
-
/** El ícono del punto, cuando el tono no dice lo suficiente. Sin esto sale del
|
|
94
|
-
* tono, que es lo que evita un evento rojo con un tilde verde adentro. */
|
|
95
|
-
icon?: LucideIcon;
|
|
96
|
-
};
|
|
97
|
-
export type ChatterProps = {
|
|
98
|
-
/** What people wrote. Absent —not empty— while there is no service behind it:
|
|
99
|
-
* an empty thread says «nobody wrote yet», which is a different claim. */
|
|
100
|
-
messages?: readonly Message[];
|
|
101
|
-
/** What the system logged. */
|
|
102
|
-
events?: readonly ChatterEvent[];
|
|
103
|
-
/** The box to write in, when this record takes messages. It stays on top of
|
|
104
|
-
* the stream: writing is the thing you came to do, reading is what you do
|
|
105
|
-
* next. */
|
|
106
|
-
composer?: ReactNode;
|
|
107
|
-
/** Which tab is showing. Uncontrolled from «everything» when absent. */
|
|
108
|
-
current?: ChatterView;
|
|
109
|
-
onCurrentChange?: (next: ChatterView) => void;
|
|
110
|
-
open: boolean;
|
|
111
|
-
onToggle: () => void;
|
|
112
|
-
label?: string;
|
|
113
|
-
/** Dentro de la hoja del registro: no lleva marco propio, la hoja ya lo
|
|
114
|
-
* tiene. Sin esto es una tarjeta, que es como se ve cuando vive solo. */
|
|
115
|
-
embedded?: boolean;
|
|
116
|
-
/**
|
|
117
|
-
* Dónde vive el chatter respecto del registro.
|
|
118
|
-
*
|
|
119
|
-
* page DEBAJO y a TODO EL ANCHO, como una hoja propia. **Es el
|
|
120
|
-
* default**, y no por comodidad: un formulario necesita su ancho
|
|
121
|
-
* para que los campos no se apilen, y el de al costado se lo come
|
|
122
|
-
* en cuanto la pantalla no sobra. Abajo, la conversación no compite
|
|
123
|
-
* con el registro — es la otra mitad de la página.
|
|
124
|
-
* aside AL COSTADO cuando hay ancho, DEBAJO cuando no. Es para un
|
|
125
|
-
* registro que se LEE más de lo que se edita, donde tener la
|
|
126
|
-
* historia a la vista mientras se mira el dato vale el ancho que
|
|
127
|
-
* cuesta.
|
|
128
|
-
*
|
|
129
|
-
* La diferencia no es cosmética. En `aside` el ancho es escaso, así que el
|
|
130
|
-
* mensaje se lee en una columna de veintipico de caracteres; en `page` hay
|
|
131
|
-
* ancho de sobra y el texto se acota a una medida legible en vez de estirarse
|
|
132
|
-
* de punta a punta, que es donde un párrafo deja de poder seguirse.
|
|
133
|
-
*
|
|
134
|
-
* **Plegar es de pantalla chica.** En `page` el chatter no se pliega cuando
|
|
135
|
-
* hay ancho: plegarlo ahí esconde media pantalla para no ganar nada.
|
|
136
|
-
*/
|
|
137
|
-
layout?: ChatterLayout;
|
|
138
|
-
};
|
|
139
|
-
export type ChatterLayout = "aside" | "page";
|
|
140
|
-
export type ChatterView = "all" | "messages" | "events";
|
|
141
|
-
export declare function Chatter({ messages, events, composer, current, onCurrentChange, open, onToggle, label: ownLabel, layout, embedded, }: ChatterProps): import("react").JSX.Element;
|
|
142
|
-
/** Whether the chatter is open, when the page has no better answer.
|
|
143
|
-
*
|
|
144
|
-
* **Starts folded.** It began the other way round —a history nobody opens is a
|
|
145
|
-
* history nobody reads— and seeing it took the argument apart: open, it holds a
|
|
146
|
-
* 22rem column of context on every record you open, before anybody asked for
|
|
147
|
-
* any. Folded it still says how many there are, which is the half that has to
|
|
148
|
-
* be free; reading them is a click, and a click you chose.
|
|
149
|
-
*
|
|
150
|
-
* It is the same answer for both layouts. A default that depends on the width
|
|
151
|
-
* would mean the same record shows a different screen depending on how many
|
|
152
|
-
* panels happen to be open next to it, and that is not a default, it is a
|
|
153
|
-
* surprise. */
|
|
154
|
-
export declare function useChatter(initial?: boolean): {
|
|
155
|
-
open: boolean;
|
|
156
|
-
toggle: () => void;
|
|
157
|
-
/** Open it without closing it if it already was. What a button from outside
|
|
158
|
-
* needs —«events 12»— when pressing it twice must not hide the very thing
|
|
159
|
-
* it promised to show. */
|
|
160
|
-
show: () => void;
|
|
161
|
-
};
|
|
@@ -1,232 +0,0 @@
|
|
|
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 { Empty } from "../../primitives/notice/Notice.js";
|
|
7
|
-
import { Tabs } from "../../primitives/tabs/Tabs.js";
|
|
8
|
-
import { Tag } from "../../primitives/tag/Tag.js";
|
|
9
|
-
import { DateValue } from "../../primitives/value/Fecha.js";
|
|
10
|
-
import { Avatar } from "../../primitives/value/Person.js";
|
|
11
|
-
import { ArrowRight, ChevronRight, History, MessageSquare, PanelRightClose, PanelRightOpen, Rows3, Zap, } from "lucide-react";
|
|
12
|
-
import { Lock } from "lucide-react";
|
|
13
|
-
import { useState } from "react";
|
|
14
|
-
/* The tab names come from the copy table: the same three words on every record. */
|
|
15
|
-
const TAB_LABEL = {
|
|
16
|
-
all: (copy) => copy.chatter.tabAll,
|
|
17
|
-
messages: (copy) => copy.chatter.tabMessages,
|
|
18
|
-
events: (copy) => copy.chatter.tabEvents,
|
|
19
|
-
};
|
|
20
|
-
/* El ícono de cada pestaña. Con tres, es lo que se reconoce sin leer — y en el
|
|
21
|
-
chatter angosto, lo único que sobrevive cuando el rótulo se trunca. */
|
|
22
|
-
const TAB_ICON = {
|
|
23
|
-
all: Rows3,
|
|
24
|
-
messages: MessageSquare,
|
|
25
|
-
events: History,
|
|
26
|
-
};
|
|
27
|
-
/* UN SOLO ÍCONO para todos los eventos: el rayo, que es lo que pasó — algo se
|
|
28
|
-
ejecutó. Había uno por tono y eso convertía la historia en un muestrario:
|
|
29
|
-
cinco formas y cinco colores para decir cinco veces «pasó algo».
|
|
30
|
-
Lo que SÍ hay que distinguir es si falló, y eso lo dice el contenedor. */
|
|
31
|
-
const EVENT_ICON = Zap;
|
|
32
|
-
/* DOS estados y no seis: pasó, o falló.
|
|
33
|
-
*
|
|
34
|
-
* El rojo de un fallo queda en la fila entera: es el único color, y tiene que
|
|
35
|
-
* verse sin señalar. El hover no lo reemplaza. Es una marca metida adentro,
|
|
36
|
-
* redondeada, para no pintar el riel. */
|
|
37
|
-
const ROW_SKIN = {
|
|
38
|
-
ok: "",
|
|
39
|
-
failed: "bg-bad/6",
|
|
40
|
-
};
|
|
41
|
-
const ROW_HOVER = {
|
|
42
|
-
ok: "group-hover/entry:bg-ink/4",
|
|
43
|
-
failed: "group-hover/entry:bg-bad/10",
|
|
44
|
-
};
|
|
45
|
-
const NODE_SKIN = {
|
|
46
|
-
ok: "bg-ink/8 text-muted",
|
|
47
|
-
failed: "bg-bad/12 text-bad",
|
|
48
|
-
};
|
|
49
|
-
function at(when) {
|
|
50
|
-
return when instanceof Date ? when.getTime() : new Date(when).getTime();
|
|
51
|
-
}
|
|
52
|
-
function MessageEntry({ message }) {
|
|
53
|
-
const copy = useUiCopy();
|
|
54
|
-
/* Mismo relleno que un evento: las dos clases de entrada viven en la misma
|
|
55
|
-
lista y con relleno distinto se leen como dos listas pegadas. Antes el
|
|
56
|
-
mensaje iba a ras del borde y el evento tenía caja, así que el nombre de
|
|
57
|
-
quien escribió quedaba más cerca del filo que el texto de al lado. */
|
|
58
|
-
return (_jsxs("li", { className: "flex min-w-0 gap-2.5 px-2.5 py-2", children: [_jsx(Avatar, { name: message.author, size: "sm" }), _jsxs("div", { className: "min-w-0 flex-1", children: [_jsxs("div", { className: "flex min-w-0 flex-wrap items-baseline gap-x-2 gap-y-0.5", children: [_jsx("span", { className: "text-meta font-medium text-ink", children: message.author }), _jsx(DateValue, { when: message.at, format: "relative" }), message.internal !== true ? null : (_jsxs("span", { className: "inline-flex items-center gap-1 text-micro text-muted", title: copy.chatter.internalNote, children: [_jsx(Lock, { className: "size-3", "aria-hidden": true }), copy.chatter.internal] }))] }), _jsx("p", { className: `mt-0.5 text-body whitespace-pre-line ${message.internal === true
|
|
59
|
-
? "rounded-control border border-warn/40 bg-warn/6 px-2 py-1 text-ink"
|
|
60
|
-
: "text-ink"}`, children: message.body })] })] }));
|
|
61
|
-
}
|
|
62
|
-
/** El cambio de estado, cuando lo hubo: de qué a qué.
|
|
63
|
-
*
|
|
64
|
-
* Es la línea más útil de una historia de gestión y la que peor se contaba: «el
|
|
65
|
-
* proyecto cambió de estado» obliga a buscar el evento anterior para saber de
|
|
66
|
-
* dónde venía. Dos fichas y una flecha se leen sin buscar nada.
|
|
67
|
-
*
|
|
68
|
-
* El ESTADO VIEJO va apagado y el nuevo en tinta: lo que importa es dónde
|
|
69
|
-
* quedó, y el de antes está para dar contexto, no para competir.
|
|
70
|
-
*
|
|
71
|
-
* La cápsula es la de `Tag`: pastilla, pelo, mismo relleno. Dos radios
|
|
72
|
-
* distintos para la misma idea de «una ficha» es una familia de más. */
|
|
73
|
-
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";
|
|
74
|
-
function Moved({ from, to }) {
|
|
75
|
-
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/60", strokeWidth: 2.5, "aria-hidden": true })] })), _jsx("span", { className: `${STATE_CHIP} font-medium text-ink`, children: to })] }));
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* UN EVENTO, como línea de una línea de tiempo.
|
|
79
|
-
*
|
|
80
|
-
* ── Por qué hay un riel y no sólo puntos ────────────────────────────
|
|
81
|
-
*
|
|
82
|
-
* El riel —la línea vertical que une los puntos— es lo que convierte una lista
|
|
83
|
-
* en una CRONOLOGÍA. Sin él son renglones sueltos que casualmente tienen fecha;
|
|
84
|
-
* con él se lee que una cosa pasó después de la otra, que es la pregunta que
|
|
85
|
-
* alguien trae cuando abre esto.
|
|
86
|
-
*
|
|
87
|
-
* ── El color es del punto y de nada más ─────────────────────────────
|
|
88
|
-
*
|
|
89
|
-
* El tono pinta el punto, y el texto se queda en tinta normal. Un renglón entero
|
|
90
|
-
* en rojo no se lee más urgente: se lee peor, y cuando hay tres seguidos deja de
|
|
91
|
-
* significar nada. El punto alcanza para encontrar los rojos de un vistazo, que
|
|
92
|
-
* es para lo que el color sirve en una lista larga.
|
|
93
|
-
*/
|
|
94
|
-
/** El error: la primera línea, y el resto a un clic.
|
|
95
|
-
*
|
|
96
|
-
* Un stack de cuarenta renglones adentro de una historia tapa la historia; el
|
|
97
|
-
* primer renglón casi siempre alcanza. Pero el resto tiene que estar —quien
|
|
98
|
-
* mira esta pantalla muchas veces no tiene acceso a los logs— así que se
|
|
99
|
-
* guarda, no se descarta.
|
|
100
|
-
*
|
|
101
|
-
* Sin color propio: el rojo ya lo puso el nodo. Esto es una cita textual de la
|
|
102
|
-
* máquina, así que se ve como lo que es — monoespaciada sobre el piso. */
|
|
103
|
-
function ErrorLine({ error }) {
|
|
104
|
-
const copy = useUiCopy();
|
|
105
|
-
const [open, setOpen] = useState(false);
|
|
106
|
-
const lineas = error.split("\n");
|
|
107
|
-
const primera = lineas[0] ?? "";
|
|
108
|
-
const hayMas = lineas.length > 1 || primera.length > 120;
|
|
109
|
-
return (_jsxs("div", { className: "min-w-0 rounded-control border border-line bg-ground 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 : primera }), !hayMas ? 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.chatter.less : copy.chatter.more }))] }));
|
|
110
|
-
}
|
|
111
|
-
/**
|
|
112
|
-
* UN EVENTO: una FILA que se aprieta, no un renglón.
|
|
113
|
-
*
|
|
114
|
-
* ── Qué se ve sin abrir ─────────────────────────────────────────────
|
|
115
|
-
*
|
|
116
|
-
* El rótulo, quién —si lo hubo— y cuándo. Nada más. Es lo que alguien lee para
|
|
117
|
-
* recorrer la historia; todo lo demás es para cuando ese renglón no alcanzó.
|
|
118
|
-
*
|
|
119
|
-
* ── Un solo ícono, y el contenedor dice si falló ────────────────────
|
|
120
|
-
*
|
|
121
|
-
* El rayo, porque eso es lo que pasó: algo se ejecutó. Había un ícono y un color
|
|
122
|
-
* por tono, y seis colores no señalan nada — una historia de veinte eventos
|
|
123
|
-
* quedaba como una bolsa de caramelos, y el que falló no saltaba más que los
|
|
124
|
-
* otros. El rojo sirve exactamente mientras sea el único.
|
|
125
|
-
*
|
|
126
|
-
* Lo que distingue un error es el TINTE DE LA FILA, translúcido. Y sirve dos
|
|
127
|
-
* veces: dice que falló, y dice que la fila es una zona que se puede apretar —
|
|
128
|
-
* que era lo que no se notaba.
|
|
129
|
-
*/
|
|
130
|
-
function EventEntry({ event }) {
|
|
131
|
-
const failed = event.tone === "bad" || event.error !== undefined;
|
|
132
|
-
const skin = failed ? "failed" : "ok";
|
|
133
|
-
const Icon = event.icon ?? EVENT_ICON;
|
|
134
|
-
/* Arranca CERRADO: lo de adentro es para cuando el renglón no alcanzó. */
|
|
135
|
-
const [open, setOpen] = useState(false);
|
|
136
|
-
const hayMas = event.moved !== undefined || event.detail !== undefined || event.error !== undefined;
|
|
137
|
-
const fila = (_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 }) }), _jsx("span", { className: "min-w-0 flex-1 truncate text-meta text-ink", children: event.label }), event.by === undefined ? null : (_jsx("span", { className: "hidden shrink-0 truncate text-micro text-muted @xs:inline", children: event.by })), _jsx("span", { className: "shrink-0", children: _jsx(DateValue, { when: event.at, format: "relative" }) }), hayMas ? (_jsx(ChevronRight, { className: `size-3.5 shrink-0 text-muted transition-transform duration-(--duration-instant) ${open ? "rotate-90" : ""}`, strokeWidth: 2.5, "aria-hidden": true })) : (
|
|
138
|
-
/* Hueco del mismo ancho: sin él, las filas que no se abren corren la
|
|
139
|
-
fecha y la columna deja de estar alineada. */
|
|
140
|
-
_jsx("span", { className: "size-3.5 shrink-0", "aria-hidden": true }))] }));
|
|
141
|
-
return (_jsxs("li", { className: "group/event relative @container", children: [_jsx("span", { "aria-hidden": true, className: "absolute top-8 left-5.5 w-px bg-line group-last/event:hidden" }), hayMas ? (_jsxs("button", { type: "button", onClick: () => setOpen(!open), "aria-expanded": open, className: `group/entry relative flex w-full min-w-0 cursor-pointer items-center rounded-box px-2.5 py-2 text-left focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-primary ${ROW_SKIN[skin]}`, children: [_jsx("span", { "aria-hidden": true, className: `pointer-events-none absolute inset-x-1 inset-y-0.5 rounded-control ${ROW_HOVER[skin]}` }), _jsx("span", { className: "relative z-10 flex min-w-0 flex-1 items-center gap-2.5", children: fila })] })) : (_jsx("div", { className: `flex w-full min-w-0 items-center gap-2.5 rounded-box px-2.5 py-2 ${failed ? "bg-bad/6" : ""}`, children: fila })), !open ? null : (
|
|
142
|
-
/* Lo de adentro, alineado bajo el rótulo y sobre el piso: es el DETALLE
|
|
143
|
-
de esa línea, no una línea más de la historia. */
|
|
144
|
-
_jsxs("div", { className: "mt-0.5 mb-1 ml-11 flex min-w-0 flex-col gap-1.5 rounded-box 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.by === undefined ? null : (_jsx("span", { className: "text-micro text-muted @xs:hidden", children: event.by })), 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 })] }))] }));
|
|
145
|
-
}
|
|
146
|
-
export function Chatter({ messages, events, composer, current, onCurrentChange, open, onToggle, label: ownLabel, layout = "page", embedded = false, }) {
|
|
147
|
-
const copy = useUiCopy();
|
|
148
|
-
const label = ownLabel ?? copy.chatter.history;
|
|
149
|
-
const [owned, setOwned] = useState("all");
|
|
150
|
-
const view = current ?? owned;
|
|
151
|
-
const setView = onCurrentChange ?? setOwned;
|
|
152
|
-
const hasMessages = messages !== undefined;
|
|
153
|
-
const hasEvents = events !== undefined;
|
|
154
|
-
/* Las tres pestañas sólo cuando hay DOS fuentes: con una sola, «todo» y esa
|
|
155
|
-
una son la misma lista, y tres solapas donde dos dicen lo mismo es un
|
|
156
|
-
control que no decide nada. */
|
|
157
|
-
const tabs = hasMessages && hasEvents ? ["all", "messages", "events"] : [];
|
|
158
|
-
const showing = tabs.length === 0 ? "all" : view;
|
|
159
|
-
const entries = [
|
|
160
|
-
...(showing === "events" ? [] : (messages ?? [])).map((message) => ({
|
|
161
|
-
kind: "message",
|
|
162
|
-
at: at(message.at),
|
|
163
|
-
message,
|
|
164
|
-
})),
|
|
165
|
-
...(showing === "messages" ? [] : (events ?? [])).map((event) => ({
|
|
166
|
-
kind: "event",
|
|
167
|
-
at: at(event.at),
|
|
168
|
-
event,
|
|
169
|
-
})),
|
|
170
|
-
].sort((a, b) => b.at - a.at);
|
|
171
|
-
const total = (messages?.length ?? 0) + (events?.length ?? 0);
|
|
172
|
-
/* Cortado acá y no por quien compone: es la misma lista en todos los
|
|
173
|
-
registros, y una historia que sólo hace scroll es una historia que se
|
|
174
|
-
abandona a la mitad. */
|
|
175
|
-
const pages = usePagination(entries, { pageSize: 15 });
|
|
176
|
-
const page = layout === "page";
|
|
177
|
-
/* Plegado. Embebido es la última fila de la hoja: el marco lo pone la hoja
|
|
178
|
-
y el hover es una marca redondeada adentro, para no pintar la línea que
|
|
179
|
-
la separa del papel. Suelta, o al costado, sí es una tarjeta. */
|
|
180
|
-
if (!open) {
|
|
181
|
-
const strip = page && embedded;
|
|
182
|
-
const face = (_jsxs(_Fragment, { children: [_jsx(MessageSquare, { className: "size-4 shrink-0 text-muted", "aria-hidden": true }), _jsx("span", { className: page
|
|
183
|
-
? "min-w-0 truncate text-meta text-muted"
|
|
184
|
-
: "min-w-0 truncate text-meta text-muted @4xl:hidden", children: label }), total === 0 ? null : _jsx(Tag, { tone: "neutral", children: total }), _jsx(PanelRightOpen, { className: page
|
|
185
|
-
? "ml-auto size-4 shrink-0 text-muted"
|
|
186
|
-
: "ml-auto size-4 shrink-0 text-muted @4xl:ml-0", "aria-hidden": true })] }));
|
|
187
|
-
return (_jsxs("button", { type: "button", onClick: onToggle, title: fillCopy(copy.chatter.open, { label }), className: page
|
|
188
|
-
? strip
|
|
189
|
-
? "group/strip relative flex w-full shrink-0 cursor-pointer items-center px-3 py-2.5"
|
|
190
|
-
: "flex w-full shrink-0 cursor-pointer items-center gap-2 rounded-box border border-line bg-surface px-3 py-2.5 hover:bg-ground"
|
|
191
|
-
: "flex w-full shrink-0 cursor-pointer items-center gap-2 self-start rounded-box border border-line bg-surface px-2.5 py-2 hover:bg-ground @4xl:w-auto @4xl:flex-col @4xl:px-1.5 @4xl:py-2.5", children: [strip ? (_jsx("span", { "aria-hidden": true, className: "pointer-events-none absolute inset-x-1 inset-y-0.5 rounded-control group-hover/strip:bg-ground" })) : null, strip ? (_jsx("span", { className: "relative z-10 flex min-w-0 flex-1 items-center gap-2", children: face })) : (face)] }));
|
|
192
|
-
}
|
|
193
|
-
return (_jsxs("aside", { className: page
|
|
194
|
-
? embedded
|
|
195
|
-
? "flex w-full flex-col"
|
|
196
|
-
: "flex w-full flex-col rounded-box border border-line bg-surface"
|
|
197
|
-
: "flex w-full shrink-0 flex-col self-start rounded-box border border-line bg-surface @4xl:w-(--container-chatter)", children: [_jsxs("header", { className: `flex min-w-0 items-end gap-1 border-b border-line bg-ground px-2 pt-2 ${page && embedded ? "" : "rounded-t-box"}`, children: [tabs.length === 0 ? (_jsxs("span", { className: "px-2 py-1.5 text-meta font-medium text-ink", children: [label, total === 0 ? null : (_jsx("span", { className: "ml-1.5 text-micro text-muted", children: total }))] })) : (_jsx(Tabs, { label: label, current: showing, onSelect: (id) => setView(id), tabs: tabs.map((tab) => ({
|
|
198
|
-
id: tab,
|
|
199
|
-
label: TAB_LABEL[tab](copy),
|
|
200
|
-
icon: TAB_ICON[tab],
|
|
201
|
-
count: tab === "messages"
|
|
202
|
-
? (messages?.length ?? 0)
|
|
203
|
-
: tab === "events"
|
|
204
|
-
? (events?.length ?? 0)
|
|
205
|
-
: total,
|
|
206
|
-
})) })), _jsx("button", { type: "button", onClick: onToggle, title: fillCopy(copy.chatter.fold, { label }), className: "mb-1.5 ml-auto inline-flex size-(--size-control-sm) shrink-0 cursor-pointer items-center justify-center rounded-pill text-muted transition-colors duration-(--duration-instant) hover:bg-ink/6 hover:text-ink focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary", children: _jsx(PanelRightClose, { className: "size-(--size-icon-md)", "aria-hidden": true }) })] }), _jsxs("div", { className: page
|
|
207
|
-
? "flex w-full min-w-0 max-w-(--container-prose) flex-col gap-3 px-3.5 py-3"
|
|
208
|
-
: "flex min-w-0 flex-col gap-3 px-2.5 py-2.5", children: [composer === undefined || showing === "events" ? null : composer, entries.length === 0 ? (_jsx(Empty, { sentence: copy.chatter.empty })) : (_jsxs(_Fragment, { children: [_jsx("ol", { className: "flex flex-col", children: pages.pageItems.map((entry) => entry.kind === "message" ? (_jsx(MessageEntry, { message: entry.message }, `m:${entry.message.id}`)) : (_jsx(EventEntry, { event: entry.event }, `e:${entry.event.id}`))) }), entries.length <= 15 ? null : _jsx(Pagination, { ...pages.bridge })] }))] })] }));
|
|
209
|
-
}
|
|
210
|
-
/** Whether the chatter is open, when the page has no better answer.
|
|
211
|
-
*
|
|
212
|
-
* **Starts folded.** It began the other way round —a history nobody opens is a
|
|
213
|
-
* history nobody reads— and seeing it took the argument apart: open, it holds a
|
|
214
|
-
* 22rem column of context on every record you open, before anybody asked for
|
|
215
|
-
* any. Folded it still says how many there are, which is the half that has to
|
|
216
|
-
* be free; reading them is a click, and a click you chose.
|
|
217
|
-
*
|
|
218
|
-
* It is the same answer for both layouts. A default that depends on the width
|
|
219
|
-
* would mean the same record shows a different screen depending on how many
|
|
220
|
-
* panels happen to be open next to it, and that is not a default, it is a
|
|
221
|
-
* surprise. */
|
|
222
|
-
export function useChatter(initial = false) {
|
|
223
|
-
const [open, setOpen] = useState(initial);
|
|
224
|
-
return {
|
|
225
|
-
open,
|
|
226
|
-
toggle: () => setOpen((current) => !current),
|
|
227
|
-
/** Open it without closing it if it already was. What a button from outside
|
|
228
|
-
* needs —«events 12»— when pressing it twice must not hide the very thing
|
|
229
|
-
* it promised to show. */
|
|
230
|
-
show: () => setOpen(true),
|
|
231
|
-
};
|
|
232
|
-
}
|
|
File without changes
|
|
File without changes
|