@sincpro/web-ui 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +37 -10
  2. package/dist/components/activity/Activity.d.ts +69 -0
  3. package/dist/components/activity/Activity.js +279 -0
  4. package/dist/components/{chatter → activity}/Messages.d.ts +2 -2
  5. package/dist/components/{chatter → activity}/Messages.js +9 -6
  6. package/dist/components/{chatter → activity}/index.d.ts +1 -1
  7. package/dist/components/activity/index.js +4 -0
  8. package/dist/components/assist/AssistMark.d.ts +3 -3
  9. package/dist/components/assist/AssistMark.js +1 -1
  10. package/dist/components/assist/assist.d.ts +1 -1
  11. package/dist/components/assist/assist.js +1 -1
  12. package/dist/components/form/Sheet.d.ts +4 -0
  13. package/dist/components/form/Sheet.js +11 -4
  14. package/dist/components/index.d.ts +1 -1
  15. package/dist/components/index.js +1 -1
  16. package/dist/components/lines/LineItems.d.ts +42 -5
  17. package/dist/components/lines/LineItems.js +106 -18
  18. package/dist/components/listing/Listing.js +1 -1
  19. package/dist/components/listing/ListingBar.d.ts +41 -2
  20. package/dist/components/listing/ListingBar.js +43 -4
  21. package/dist/components/listing/Row.d.ts +5 -1
  22. package/dist/components/listing/Row.js +3 -2
  23. package/dist/components/record/RecordHeader.d.ts +21 -17
  24. package/dist/components/record/RecordHeader.js +46 -14
  25. package/dist/components/record/RecordNotices.d.ts +37 -0
  26. package/dist/components/record/RecordNotices.js +75 -0
  27. package/dist/components/record/ThumbBar.js +1 -1
  28. package/dist/foundations/copy.d.ts +96 -12
  29. package/dist/foundations/copy.es.js +63 -10
  30. package/dist/foundations/copy.js +55 -7
  31. package/dist/primitives/choice/ComboBox.d.ts +3 -0
  32. package/dist/primitives/choice/ComboBox.js +2 -2
  33. package/dist/primitives/choice/SearchField.d.ts +10 -1
  34. package/dist/primitives/choice/SearchField.js +21 -2
  35. package/dist/primitives/date/Calendar.js +30 -6
  36. package/dist/primitives/date/DatePicker.d.ts +11 -3
  37. package/dist/primitives/date/DatePicker.js +12 -8
  38. package/dist/primitives/date/DateTime.d.ts +35 -0
  39. package/dist/primitives/date/DateTime.js +100 -0
  40. package/dist/primitives/date/index.d.ts +1 -0
  41. package/dist/primitives/date/index.js +4 -2
  42. package/dist/primitives/dialog/Drill.d.ts +90 -0
  43. package/dist/primitives/dialog/Drill.js +172 -0
  44. package/dist/primitives/dialog/index.d.ts +1 -0
  45. package/dist/primitives/dialog/index.js +2 -0
  46. package/dist/primitives/field/DropZone.js +1 -1
  47. package/dist/primitives/field/Field.d.ts +14 -2
  48. package/dist/primitives/field/Field.js +25 -14
  49. package/dist/primitives/field/copy.d.ts +16 -0
  50. package/dist/primitives/field/copy.js +69 -0
  51. package/dist/primitives/field/index.d.ts +2 -0
  52. package/dist/primitives/field/index.js +4 -0
  53. package/dist/primitives/field/note.d.ts +55 -0
  54. package/dist/primitives/field/note.js +117 -0
  55. package/dist/primitives/index.d.ts +1 -0
  56. package/dist/primitives/index.js +1 -0
  57. package/dist/primitives/menu/Menu.d.ts +28 -0
  58. package/dist/primitives/menu/Menu.js +47 -17
  59. package/dist/primitives/notice/Callout.js +1 -1
  60. package/dist/primitives/notice/Flag.d.ts +13 -0
  61. package/dist/primitives/notice/Flag.js +31 -0
  62. package/dist/primitives/notice/index.d.ts +1 -0
  63. package/dist/primitives/notice/index.js +1 -0
  64. package/dist/primitives/overlay/Preview.d.ts +19 -0
  65. package/dist/primitives/overlay/Preview.js +60 -0
  66. package/dist/primitives/overlay/index.d.ts +1 -0
  67. package/dist/primitives/overlay/index.js +3 -1
  68. package/dist/primitives/picker/Picker.d.ts +41 -0
  69. package/dist/primitives/picker/Picker.js +62 -8
  70. package/dist/primitives/table/CellCopy.d.ts +20 -0
  71. package/dist/primitives/table/CellCopy.js +23 -0
  72. package/dist/primitives/table/DataTable.d.ts +55 -1
  73. package/dist/primitives/table/DataTable.js +126 -40
  74. package/dist/primitives/table/EditableTable.d.ts +97 -2
  75. package/dist/primitives/table/EditableTable.js +223 -29
  76. package/dist/primitives/table/SheetTable.d.ts +4 -1
  77. package/dist/primitives/table/SheetTable.js +38 -5
  78. package/dist/primitives/table/index.d.ts +2 -2
  79. package/dist/primitives/table/index.js +1 -1
  80. package/dist/primitives/tabs/Tabs.d.ts +2 -2
  81. package/dist/primitives/tabs/Tabs.js +1 -1
  82. package/dist/primitives/tag/TagsField.d.ts +3 -0
  83. package/dist/primitives/tag/TagsField.js +6 -2
  84. package/dist/primitives/tree/Tree.d.ts +56 -0
  85. package/dist/primitives/tree/Tree.js +74 -0
  86. package/dist/primitives/tree/index.d.ts +1 -0
  87. package/dist/primitives/tree/index.js +3 -0
  88. package/dist/primitives/value/Copyable.d.ts +11 -0
  89. package/dist/primitives/value/Copyable.js +27 -6
  90. package/dist/primitives/value/Fecha.js +10 -1
  91. package/dist/primitives/value/ProgressBar.d.ts +31 -0
  92. package/dist/primitives/value/ProgressBar.js +53 -0
  93. package/dist/primitives/value/Relation.d.ts +6 -1
  94. package/dist/primitives/value/Relation.js +27 -10
  95. package/dist/primitives/value/Totals.d.ts +102 -7
  96. package/dist/primitives/value/Totals.js +52 -2
  97. package/dist/primitives/value/Value.d.ts +0 -37
  98. package/dist/primitives/value/Value.js +0 -39
  99. package/dist/primitives/value/index.d.ts +4 -3
  100. package/dist/primitives/value/index.js +4 -3
  101. package/dist/tokens.css +44 -7
  102. package/dist/views/list/List.d.ts +8 -2
  103. package/dist/views/list/List.js +6 -2
  104. package/dist/views/list/ListView.d.ts +1 -1
  105. package/dist/views/list/ListView.js +34 -8
  106. package/dist/views/list/derive.d.ts +12 -1
  107. package/dist/views/list/derive.js +29 -3
  108. package/dist/views/list/presentations.d.ts +2 -2
  109. package/dist/views/list/presentations.js +5 -3
  110. package/dist/views/list/types.d.ts +13 -1
  111. package/dist/views/record/Record.d.ts +27 -8
  112. package/dist/views/record/Record.js +97 -27
  113. package/dist/views/record/index.d.ts +1 -1
  114. package/package.json +5 -1
  115. package/dist/components/chatter/Chatter.d.ts +0 -161
  116. package/dist/components/chatter/Chatter.js +0 -232
  117. package/dist/components/chatter/index.js +0 -4
  118. /package/dist/components/{chatter → activity}/Timeline.d.ts +0 -0
  119. /package/dist/components/{chatter → activity}/Timeline.js +0 -0
@@ -1,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 { Failed } from "../../primitives/notice/Notice.js";
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
- /** Lo que al registro le PASÓ, al costado: mensajes y eventos en una columna.
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 chatterSlot = propsOf(pick(top, "chatter"));
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) !== "chatter");
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
- _jsx("article", { className: "flex min-w-0 flex-1 flex-col", children: _jsx(RecordHeader, { railSurface: surface, ...(dirty === undefined ? {} : { dirty }), ...(crumbs === undefined ? {} : { crumbs }), ...(pager === undefined
117
- ? {}
118
- : {
119
- pager: (_jsx(PagerControl, { index: pager.index, total: pager.total, surface: surface, ...(pager.onPrev === undefined ? {} : { onPrev: pager.onPrev }), ...(pager.onNext === undefined ? {} : { onNext: pager.onNext }) })),
120
- }), ...(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
121
- ? {}
122
- : {
123
- 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))) })),
124
- }), ...(aspectsNode === undefined || globalSheet ? {} : { aspects: aspectsNode }), ...(chatter === undefined ? {} : { chatter }), ...(chatterSlot?.layout === undefined ? {} : { chatterLayout: chatterSlot.layout }), children: _jsxs("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: [error === null ? null : (_jsx(Failed, { heading: copy.failed.operation, sentence: error })), _jsxs(Sheet, { layout: paper?.layout ?? "stacked", columns: paper?.columns ?? 2, ...(hasFacts ? { fields } : {}), ...(above.length > 0 || (globalSheet && aspectsNode !== undefined)
125
- ? {
126
- 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] })),
127
- }
128
- : {}), children: [pageBody, below] })] }) }) }));
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.Chatter = ChatterSlot;
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.1.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
- }
@@ -1,4 +0,0 @@
1
- /* What a record has been through, beside it: messages and events in one column. */
2
- export { Chatter, useChatter, } from "./Chatter.js";
3
- export { MessageComposer, writeANote, } from "./Messages.js";
4
- export { Timeline, } from "./Timeline.js";