nexus-shared 4.0.0 → 4.1.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/CHANGELOG.md +795 -0
- package/README.md +220 -14
- package/dist/Client.Index.d.ts +13 -0
- package/dist/Client.Index.js +13 -0
- package/dist/Components/Chats/ChatComposer.d.ts +7 -1
- package/dist/Components/Chats/ChatComposer.js +9 -6
- package/dist/Components/Chats/ChatParts.d.ts +15 -2
- package/dist/Components/Chats/ChatParts.js +15 -4
- package/dist/Components/Chats/ChatPopup.d.ts +46 -0
- package/dist/Components/Chats/ChatPopup.js +34 -0
- package/dist/Components/Chats/ChatThread.d.ts +9 -1
- package/dist/Components/Chats/ChatThread.js +54 -6
- package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
- package/dist/Components/Dashboards/DashboardBoard.js +594 -0
- package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
- package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
- package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
- package/dist/Components/Dashboards/WidgetFrame.js +45 -0
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/TabButtons.d.ts +6 -0
- package/dist/Components/Documents/TabButtons.js +5 -4
- package/dist/Components/Domains/DomainCard.d.ts +98 -0
- package/dist/Components/Domains/DomainCard.js +182 -0
- package/dist/Components/Forms/ApiForm.js +5 -1
- package/dist/Components/Forms/Crud.js +72 -24
- package/dist/Components/Forms/CrudActions.d.ts +5 -4
- package/dist/Components/Forms/CrudActions.js +42 -12
- package/dist/Components/Forms/CrudHistory.d.ts +6 -1
- package/dist/Components/Forms/CrudHistory.js +14 -6
- package/dist/Components/Forms/CrudParts.d.ts +11 -4
- package/dist/Components/Forms/CrudParts.js +76 -10
- package/dist/Components/Forms/CrudRowForms.js +65 -5
- package/dist/Components/Forms/CrudTree.js +7 -7
- package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
- package/dist/Components/Forms/CrudWorkspace.js +19 -11
- package/dist/Components/Forms/Form.d.ts +3 -2
- package/dist/Components/Forms/Form.js +33 -26
- package/dist/Components/Forms/FormQuestion.d.ts +6 -0
- package/dist/Components/Forms/FormQuestion.js +28 -0
- package/dist/Components/Forms/SubmitForm.js +2 -15
- package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
- package/dist/Components/Inputs/ComputedInput.js +179 -0
- package/dist/Components/Inputs/Dropdown.js +4 -2
- package/dist/Components/Inputs/GroupForm.js +3 -2
- package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
- package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
- package/dist/Components/Inputs/MarkdownEditor.js +32 -6
- package/dist/Components/Inputs/PictureInput.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +14 -1
- package/dist/Components/Inputs/RowsInput.js +78 -5
- package/dist/Components/Inputs/TabularForm.js +16 -3
- package/dist/Components/Inputs/TreeView.d.ts +24 -1
- package/dist/Components/Inputs/TreeView.js +84 -7
- package/dist/Components/Layouts/Popup.d.ts +22 -2
- package/dist/Components/Layouts/Popup.js +108 -6
- package/dist/Components/Messages/MailClient.d.ts +50 -0
- package/dist/Components/Messages/MailClient.js +410 -0
- package/dist/Components/Messages/MailColumns.d.ts +21 -0
- package/dist/Components/Messages/MailColumns.js +92 -0
- package/dist/Components/Messages/MailCompose.d.ts +46 -0
- package/dist/Components/Messages/MailCompose.js +154 -0
- package/dist/Components/Messages/MailReader.d.ts +43 -0
- package/dist/Components/Messages/MailReader.js +207 -0
- package/dist/Components/Messages/MailThread.d.ts +71 -0
- package/dist/Components/Messages/MailThread.js +185 -0
- package/dist/Components/Messages/MessageBell.d.ts +41 -0
- package/dist/Components/Messages/MessageBell.js +76 -0
- package/dist/Components/Viewers/DataTable.js +7 -5
- package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
- package/dist/Components/Viewers/DataTableColumns.js +2 -1
- package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
- package/dist/Components/Viewers/DataTableParts.js +2 -2
- package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
- package/dist/Components/Viewers/OwnerFiles.js +142 -31
- package/dist/Helpers/ApiModules.js +4 -0
- package/dist/Helpers/AuditEntries.d.ts +50 -0
- package/dist/Helpers/AuditEntries.js +96 -0
- package/dist/Helpers/ChatBackend.d.ts +8 -1
- package/dist/Helpers/ChatBackend.js +18 -0
- package/dist/Helpers/ChatHelpers.d.ts +2 -0
- package/dist/Helpers/ChatHelpers.js +4 -1
- package/dist/Helpers/CrudColumns.d.ts +44 -6
- package/dist/Helpers/CrudColumns.js +63 -13
- package/dist/Helpers/CrudHelpers.d.ts +36 -1
- package/dist/Helpers/CrudHelpers.js +61 -0
- package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
- package/dist/Helpers/CrudPickerPlan.js +46 -0
- package/dist/Helpers/CrudRowForms.d.ts +18 -1
- package/dist/Helpers/CrudRowForms.js +35 -2
- package/dist/Helpers/DashboardHelpers.d.ts +115 -0
- package/dist/Helpers/DashboardHelpers.js +305 -0
- package/dist/Helpers/DomainCards.d.ts +140 -0
- package/dist/Helpers/DomainCards.js +370 -0
- package/dist/Helpers/FileAdapter.d.ts +82 -2
- package/dist/Helpers/FileAdapter.js +72 -1
- package/dist/Helpers/FormStore.d.ts +11 -1
- package/dist/Helpers/FormStore.js +66 -19
- package/dist/Helpers/InputConditions.d.ts +6 -0
- package/dist/Helpers/InputConditions.js +24 -0
- package/dist/Helpers/InputMemory.js +40 -30
- package/dist/Helpers/InputNarrowing.d.ts +38 -6
- package/dist/Helpers/InputNarrowing.js +102 -21
- package/dist/Helpers/MailClientHelpers.d.ts +96 -0
- package/dist/Helpers/MailClientHelpers.js +211 -0
- package/dist/Helpers/MessageBackend.d.ts +152 -0
- package/dist/Helpers/MessageBackend.js +329 -0
- package/dist/Helpers/MessageHelpers.d.ts +160 -0
- package/dist/Helpers/MessageHelpers.js +573 -0
- package/dist/Helpers/MessageUnread.d.ts +95 -0
- package/dist/Helpers/MessageUnread.js +245 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
- package/dist/Helpers/NexusDriveAdapter.js +32 -3
- package/dist/Helpers/NumberHelpers.d.ts +61 -0
- package/dist/Helpers/NumberHelpers.js +124 -8
- package/dist/Helpers/PagedPicker.d.ts +42 -0
- package/dist/Helpers/PagedPicker.js +85 -0
- package/dist/Helpers/RowsStore.d.ts +24 -0
- package/dist/Helpers/RowsStore.js +79 -10
- package/dist/Helpers/TableHelpers.d.ts +15 -2
- package/dist/Helpers/TableHelpers.js +28 -1
- package/dist/Helpers/TableStore.d.ts +7 -3
- package/dist/Helpers/TableStore.js +9 -4
- package/dist/Helpers/ToolbarFit.d.ts +23 -0
- package/dist/Helpers/ToolbarFit.js +30 -0
- package/dist/Helpers/TreeStore.d.ts +75 -6
- package/dist/Helpers/TreeStore.js +182 -18
- package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
- package/dist/Interfaces/ApiInterfaces.js +10 -1
- package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
- package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
- package/dist/Interfaces/DashboardInterfaces.js +1 -0
- package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
- package/dist/Interfaces/DomainInterfaces.js +1 -0
- package/dist/Interfaces/FileInterfaces.d.ts +21 -0
- package/dist/Interfaces/FileInterfaces.js +2 -0
- package/dist/Interfaces/FormInterfaces.d.ts +78 -11
- package/dist/Interfaces/InputInterfaces.d.ts +12 -5
- package/dist/Interfaces/MailInterfaces.d.ts +192 -0
- package/dist/Interfaces/MailInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +28 -2
- package/dist/Services/JoinSources.d.ts +47 -6
- package/dist/Services/JoinSources.js +68 -33
- package/dist/Services/MessageApi.d.ts +18 -0
- package/dist/Services/MessageApi.js +64 -0
- package/dist/Shared.Index.d.ts +12 -0
- package/dist/Shared.Index.js +12 -0
- package/package.json +7 -1
- package/src/Styles/Nexus.Button.css +29 -0
- package/src/Styles/Nexus.Chat.css +114 -22
- package/src/Styles/Nexus.Crud.css +119 -28
- package/src/Styles/Nexus.Dashboard.css +763 -0
- package/src/Styles/Nexus.Domain.css +384 -0
- package/src/Styles/Nexus.Editor.css +20 -0
- package/src/Styles/Nexus.Files.css +27 -0
- package/src/Styles/Nexus.Index.css +3 -0
- package/src/Styles/Nexus.Input.css +31 -0
- package/src/Styles/Nexus.Mail.css +1062 -0
- package/src/Styles/Nexus.Popup.css +58 -0
- package/src/Styles/Nexus.Table.css +1458 -1449
- package/src/Styles/Nexus.Theme.Picker.css +1 -0
- package/src/Styles/Nexus.Tree.css +10 -0
package/README.md
CHANGED
|
@@ -196,6 +196,13 @@ on this device, and reaches other open tabs:
|
|
|
196
196
|
| `CrudTree` | `nexus-shared/client` | A controller's records as a lazy tree, with the `Crud`'s own Add and Edit forms and its trash, recover and delete calls: icons on each row, a toolbar Add New, and the picked record's fields and buttons for a panel the page draws; every change written into the tree and its kept pages, nothing read again |
|
|
197
197
|
| `CrudTabFooter` | `nexus-shared/client` | The footer of a tab in a record's workspace: its words and Save in one bar that stays at the bottom of the tab while a long form scrolls, and sits in its place at the end; a plain bar elsewhere |
|
|
198
198
|
| `RecordCards` | `nexus-shared/client` | One record that is not a list (a profile, my company) as a hero and cards of fields, of items with icon actions on the right, and of the page's own; every change in a popup (a form, steps such as a code sent then typed, a confirm), its answer drawn at once, and no Save on the page; loading held, a failed read per card with Try again, read-only, an ended session |
|
|
199
|
+
| `MailThread`, `MailPeople`, `mailInboxColumns`, `mailRowUnread` | `nexus-shared/client` | A mail thread: its subject and people (From, To, Cc, Bcc as the backend shows them, folded to three names and "+2"), the record it is about, and the chat's own lines and reply box; a notification as an event line; the note a blind copy reads before replying; an inbox's columns and its strong unread row ([Mail thread](#mail-thread)) |
|
|
200
|
+
| `MessageSource`, `apiMessageSource`, `localMessageSource`, `MessageBackend`, `NexusMessageBackend`, `defineMessageBackend` | `nexus-shared` (`apiMessageSource`: `/client`) | Where a mailbox's data comes from: the app's API through its backend (the Nexus Message module's `Mail` controller by default), or a mailbox in memory that keeps a backend's rules, for demos and tests |
|
|
201
|
+
| `DomainCard`, `DomainCardFrame`, `DomainLine`, `useDomainCard` | `nexus-shared/client` | A record a message is about, as a card read live from its module: the kind an app gave a `card` on its `defineChatDomains` row (`domainCard({ load, view })`), drawn from params (title, code, state, fields, figures, lines, link) in every state - a placeholder that keeps its height, refused for 403 and 404, failed with Try again, read again when the record changes ([A record's card in a message](#a-records-card-in-a-message)) |
|
|
202
|
+
| `MessageBell`, `useMessageUnread` | `nexus-shared/client` | The unread count on a top bar's button, one size whatever the count ("99+"), and the newest unread a click away; every bell of a page on one mailbox shares one read a minute, none while the tab is hidden ([Message bell](#message-bell)) |
|
|
203
|
+
| `MessageUnreadPoller`, `messageUnreadPoller`, `refreshMessageUnread`, `mailUnreadWords` | `nexus-shared` | The page's one poller per mailbox, outside React, and the words a bell says |
|
|
204
|
+
| `domainCardReads`, `refreshDomainCards`, `DomainCardRun`, `formatDays` | `nexus-shared` | One read of a record at a time, kept 30 seconds per reader; every card of a record read again when the page changed it; a card's read outside React; days written "1 day", "2.5 days" |
|
|
205
|
+
| `pagedPicker`, `holdPickerItems`, `mailPeoplePicker`, `mailComposeDefaults` | `nexus-shared` | A dropdown over a server's search, paged, that names every value it holds (seen, handed over, looked up, or found in the first pages): people to send to; a new message's defaults for a CRUD's `form.createDefaults` |
|
|
199
206
|
| `Permissions`, `NexusPermissions`, `definePermissions`, `canCall` | `nexus-shared` | May the user call this endpoint? The app registers its rules once; `NexusPermissions` reads the Nexus backend's access actions. Everything is allowed until then |
|
|
200
207
|
| `CrudBackend`, `NexusCrudBackend`, `defineCrudBackend` | `nexus-shared` | How a CRUD talks to a backend beyond its endpoints: page requests and answers, mass action bodies, flag and pin marks. Nexus's by default |
|
|
201
208
|
| `configureCrud`, `getCrudSettings` | `nexus-shared` | What every CRUD in the app follows: how many records a user may pin (100 by default) |
|
|
@@ -374,8 +381,9 @@ definePopupRules({
|
|
|
374
381
|
backdrop, or code asked; the page decides by setting `open`. `closable`, `closeOnEscape`, `closeOnBackdrop` turn
|
|
375
382
|
those off for a popup that must be answered.
|
|
376
383
|
- `size`: `xs` 360, `sm` 440, `md` 560, `lg` 720, `xl` 960, `full`. The body scrolls while the title and footer
|
|
377
|
-
stay; `flush` removes the body padding for tables. `initialFocus` picks the first focused element (default:
|
|
378
|
-
|
|
384
|
+
stay; `flush` removes the body padding for tables. `initialFocus` picks the first focused element (default: an
|
|
385
|
+
element of the body or footer marked `data-nx-focus` - a mail thread marks its subject, so a long read opens at its
|
|
386
|
+
top - else the first control there). The content mounts only while open, so forms start fresh.
|
|
379
387
|
- Popups open other popups and message boxes above themselves; Esc closes the top one.
|
|
380
388
|
- A **title bar** like a desktop window's: 32px high (40px on touch screens), the icon, the title, and the description
|
|
381
389
|
on one line (cut with …), and the window buttons filling its top right corner, 46 x 32 each: **minimize** (only when it asks),
|
|
@@ -450,6 +458,9 @@ const choice = await messageBox.show({ title: "Unsaved changes", tone: "warning"
|
|
|
450
458
|
scrolls with the body while the buttons stay: `details: <CrudDetails row={order} columns={ORDER_COLUMNS} />` with a
|
|
451
459
|
`size` of `lg` or `xl`. `closeOnBackdrop: false` keeps a box with much to read open through a stray click (Esc and ×
|
|
452
460
|
still dismiss it, and the focus stays where it was); `signal` dismisses it when the page or form that asked goes.
|
|
461
|
+
- The message and the details describe the box to a screen reader (`aria-describedby`), so they are read with the
|
|
462
|
+
title as the focus enters. A held Enter, or one within 300 ms of the Enter before it (the one that opened the box,
|
|
463
|
+
pressed twice), never answers a box; the next deliberate Enter does.
|
|
453
464
|
|
|
454
465
|
### Button hover tones
|
|
455
466
|
|
|
@@ -776,6 +787,9 @@ import { MarkdownEditor } from "nexus-shared/client";
|
|
|
776
787
|
(Ctrl+Shift+.), code (Ctrl+E), code block, link (Ctrl+K), image, bulleted, numbered, and task lists (Ctrl+Shift+8,
|
|
777
788
|
7, L), table, and horizontal line. Marks wrap the selection or the word at the caret and come off when applied
|
|
778
789
|
again. Edits go through the browser's own editing, so Ctrl+Z undoes them. `toolbar` picks and orders the buttons.
|
|
790
|
+
Under 560 px they take a row of their own; `toolbarOverflow="menu"` keeps one row at any width instead, the buttons
|
|
791
|
+
that do not fit in a "More formatting" menu at its end (`fitToolbar` works out which) - a chat's reply box does, so a
|
|
792
|
+
phone keeps its room for the lines.
|
|
779
793
|
- **While typing:** Enter continues a list (the next number, an empty task box) and ends it on an empty item; a URL
|
|
780
794
|
pasted over selected text links it; a task ticked in the preview is ticked in the text.
|
|
781
795
|
- **Height handle:** the status bar under the editor drags the height between `minHeight` and `maxHeight`, showing
|
|
@@ -959,7 +973,10 @@ const ROOM_INPUTS: InputParams[] = [
|
|
|
959
973
|
change; a form reset after a save opens on the new choice. No `viewer`, nothing kept.
|
|
960
974
|
`{ type: EInputTypes.RadioGroup, name: "paidBy", ..., remember: { key: "counter-paid-by", viewer: `${userId}|${companyId}`, preferred: settings.defaultPaymentMode } }`.
|
|
961
975
|
- **A picker** (`...crudPicker(...)`, options from `loadOptions`) confirms a kept value by its key before it opens -
|
|
962
|
-
found, not inactive, inside its `narrowedBy` narrowing - with its spinner meanwhile
|
|
976
|
+
found, not inactive, inside its `narrowedBy` narrowing - with its spinner meanwhile (the box `aria-busy`, described
|
|
977
|
+
"Loading"; a person in the box keeps the focus when the value opens); never over the person's own pick. A picker
|
|
978
|
+
that cannot name a record by its key is looked through page by page in its own `pageSize`: a value not met in 20
|
|
979
|
+
pages, on a list that goes on, is kept for later but not opened - only the list's end makes it gone.
|
|
963
980
|
- **`per: "subscriptionGroupId"`** keeps one value per value of that input (`nx-input:{key}-{group}:{viewer}`): a
|
|
964
981
|
package per group. **`when: { name: "id", filled: false }`** remembers on a CRUD's Add only (Edit holds the key).
|
|
965
982
|
- **In rows** (a tabular or group form): a new row's input opens on the kept value (give its first choice as the
|
|
@@ -968,8 +985,9 @@ const ROOM_INPUTS: InputParams[] = [
|
|
|
968
985
|
checks, a question with what will be saved in full - `{ title: "Place this order?", message, details: (values, body) =>
|
|
969
986
|
({ row: body, columns, currency }), confirmLabel: "Place order", size: "xl", when }`. `body` is what is sent: an
|
|
970
987
|
ApiForm's `transform` runs once, before the question, and that very body is sent after Confirm. The details are drawn
|
|
971
|
-
as a record's (`CrudDetails`), a section under a heading for each run of columns with one `group
|
|
972
|
-
|
|
988
|
+
as a record's (`CrudDetails`), a section under a heading for each run of columns with one `group`, and are read out
|
|
989
|
+
with the title. Confirm has the focus, so a deliberate Enter saves (the Enter that asked, held or pressed twice,
|
|
990
|
+
does not); Back to the form, Esc and × send nothing and leave the form as it was, the focus back where it
|
|
973
991
|
was; a click outside leaves it open. While it asks nothing else submits and nothing says "Saving…"; a throw while it
|
|
974
992
|
is prepared shows under the form and asks nothing. `when` limits it to some saves.
|
|
975
993
|
- **One save at a time.** Save waits with a spinner; another click, Enter, or Ctrl+S does nothing until it ends. A save
|
|
@@ -1054,6 +1072,8 @@ export const { GET, HEAD, POST, PUT, PATCH, DELETE } = createApiProxy({ token: r
|
|
|
1054
1072
|
that disagrees with the backend by one bit grants or denies the wrong module in silence. They changed after
|
|
1055
1073
|
2.0.0: `Chat` took bit 6 and `Accounting` through `Portfolio` each moved up one. Code that uses the names needs
|
|
1056
1074
|
nothing; anything holding a stored or hard-coded **number** does. [CHANGELOG.md](CHANGELOG.md) has the table.
|
|
1075
|
+
`Inventory` (`API_INVENTORY`) was added at bit 17 in 4.0.0, after `Portfolio`, and moved nothing: a new module takes
|
|
1076
|
+
the next bit at the end.
|
|
1057
1077
|
- **Module types from one place.** An app names its enum once, next to `defineApiModules`:
|
|
1058
1078
|
`declare module "nexus-shared" { interface ApiRegister { module: HrModule } }`. Every endpoint, call, `ApiForm`,
|
|
1059
1079
|
and CRUD then takes only those modules (`AppModule`), so `{ module: NexusModule.Reservation, … }` in an HR app is a
|
|
@@ -1176,7 +1196,8 @@ table.upsert(saved); table.patch(7, { paid: true }, { flash: true }); table.remo
|
|
|
1176
1196
|
`value`, `render`, `text`, `width`, `minWidth`, `align`, `wrap`, `sortable`, `sortKey`, `searchable`, `hidden`,
|
|
1177
1197
|
`hideable`, `movable`, `resizable`, `pinnable`, `pinned`. Numbers line up at the end in tabular figures; dates and
|
|
1178
1198
|
times follow the user's settings; moments show in the user's zone once the page has hydrated; long text ends in … at
|
|
1179
|
-
the column's width.
|
|
1199
|
+
the column's width. `table` (sub-columns) marks a list of rows: a record's details draw it as a small table across the line
|
|
1200
|
+
(numbered rows, an option's tone as its tag, a yes or no in words, "None" for an empty list), the table its count.
|
|
1180
1201
|
- **Money in the company's currency.** `<DataTable currency="Rs." />` (a `Crud`'s `table.currency`) writes every
|
|
1181
1202
|
`money` column "Rs. 1,250.00" - in the cells, the details, the CSV and the print - with no `render` of the page's; a
|
|
1182
1203
|
column's own `currency` (a text, or `row => row.currencyText`) wins. `formatMoney(value, { currency, locale })` writes
|
|
@@ -1300,7 +1321,9 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
1300
1321
|
pins, and saved values change in place; a new record goes first; a server page loads again in the background to fill up. When a mass
|
|
1301
1322
|
answer lists the records it changed, only those change.
|
|
1302
1323
|
- **A save is never read back** (Nexus: an Add is one Create, an Edit one Update). The row is what was sent under what the
|
|
1303
|
-
answer said
|
|
1324
|
+
answer said (`crudSavedRecord`): the body `transform` made over the form's values, so a field sent as null is empty on
|
|
1325
|
+
the row; and a value `showWhen` hid at the save shows only as the answer (a record) or else the body gives it, never
|
|
1326
|
+
the hidden box's own copy. The fields only the server keeps are written by `CrudBackend.savedRow` (Nexus: Active, no marks, added
|
|
1304
1327
|
now; an edit keeps them and was changed now), and the names the database joins written again by `crudJoinedNames` for
|
|
1305
1328
|
each `crudColumns.dbJoin` column given `from` (the key's field): from the record the user picked in the form, one the
|
|
1306
1329
|
page holds (`peekOption`), or the name the row had when the key did not change. An edit takes the status, the marks
|
|
@@ -1315,8 +1338,13 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
1315
1338
|
button, Esc, the dock) asks "Discard your changes?". Edit opens with the row, the `details` endpoint's record
|
|
1316
1339
|
(`form.load: "details"`, for fields a list leaves out), or a loader of your own.
|
|
1317
1340
|
- **Details** (View, or a double-click) shows every column, hidden ones too, and the full record once `details` answers,
|
|
1318
|
-
with Edit in its footer.
|
|
1319
|
-
|
|
1341
|
+
with Edit in its footer. Each field is a label and its value in a box, two pairs to a line where the details are
|
|
1342
|
+
wide and one where they are not. Columns that name a `group` (a table's band) are drawn in sections under a numbered
|
|
1343
|
+
heading, "01. Contact", "02. Account" (`CrudDetails`, `crudDetailsRuns`); with no group anywhere the list is one
|
|
1344
|
+
grid. In a workspace (below) this footer is the Details tab's alone: every other tab runs to the popup's foot. A
|
|
1345
|
+
section may change on its own: `details.sections: [{ group: "KYC details", edit: "edit" }, { group: "Contacts", edit:
|
|
1346
|
+
"contacts" }]` (the Edit form, or a `rowForms` id) puts an Edit at the end of its heading; once saved the record is
|
|
1347
|
+
read again and the focus comes back to the button.
|
|
1320
1348
|
- **A record's workspace** (`details.profile`, `details.tabs`, `details.remember`): View, and a click on a row under
|
|
1321
1349
|
`table.openOnClick`, open the record as a wide popup (`full` unless `size` says otherwise) - who it is and its tabs in
|
|
1322
1350
|
a 20rem column on the left, the chosen tab on the right, scrolling inside itself. A page passes it in a few lines:
|
|
@@ -1345,8 +1373,9 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
1345
1373
|
the chosen tab is drawn, and a switch never changes the popup's size.
|
|
1346
1374
|
- `profile.fields` puts a few of the record's fields under the tags, a label and a value each, drawn as the table
|
|
1347
1375
|
draws them (the person's date format, an option's tone, the column's `tag` after the value). A field is a column
|
|
1348
|
-
given whole or a key of the details' `columns`; one with no value for the record is left out.
|
|
1349
|
-
|
|
1376
|
+
given whole or a key of the details' `columns`; one with no value for the record is left out. A field the list's
|
|
1377
|
+
row does not carry keeps its line while the record is read (held) and says "Not loaded" if that read fails; it is
|
|
1378
|
+
left out only once read and empty. The Details tab leaves out every key named there, so no field shows twice. Beside the tabs they are a small grid in the 20rem column; on a
|
|
1350
1379
|
phone they follow the tags as one wrapped line.
|
|
1351
1380
|
|
|
1352
1381
|
```tsx
|
|
@@ -1409,6 +1438,12 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
1409
1438
|
once and searched in the browser, and a saved value it does not offer looked up by key through `findListing`. A
|
|
1410
1439
|
`valueKey` other than `id` - a person's account rather than their row's key - is looked up through `find`, an endpoint
|
|
1411
1440
|
of its own that takes those values. Without it such a value is never looked up, since `findListing` takes keys.
|
|
1441
|
+
**`list`** reads another listing in place of `activeListing` - a controller with none, such as a pay run's CRUD
|
|
1442
|
+
endpoints (`crudPicker(PAY_RUNS, { list: PAY_RUNS.lists.normal, ... })`; with no `findListing`, nothing is looked
|
|
1443
|
+
up) - **`usable`** says which records count as usable in place of the backend's rule (under 30): `run =>
|
|
1444
|
+
run.recordStatus < NEXUS_TRASH_STATUS` offers a draft run (31); and **`option`** makes each record's option, keeping its
|
|
1445
|
+
fields (`run => ({ ...run, label: run.monthName + " · " + run.runCode, state: STATES[run.payRunState] })`). A CRUD's
|
|
1446
|
+
change on the controller marks the list old, as it does the active one.
|
|
1412
1447
|
- **A record nobody may change here** (`rowLock={row => row.isLocked ? row.lockReason : null}`): its Edit, Flag, Move to
|
|
1413
1448
|
trash, Recover and the deletes are listed switched off with that sentence under each, its details have no Edit, the
|
|
1414
1449
|
handle's `edit` opens its details, and a selection holding it has those mass actions switched off. View, History, Pin
|
|
@@ -1456,8 +1491,18 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
1456
1491
|
record. `writes` names the answer's fields that change the record (or is a function of the answer): only those, merged
|
|
1457
1492
|
over the record as the CRUD holds it when the answer lands (an Edit made while the popup was minimized stays), written
|
|
1458
1493
|
into the table, every list kept and the pinned sections with no list read again. The handle's `openRowForm(id, row)`
|
|
1459
|
-
opens one.
|
|
1460
|
-
-
|
|
1494
|
+
opens one. **`confirm`** asks before the save, as a form's `confirm` does - the details in full, Confirm focused, read
|
|
1495
|
+
out, a hasty Enter held - its params or a function of the record and what was read (`confirm: run => ({ title:
|
|
1496
|
+
"Lock " + run.runCode + "?", details: () => ({ row: run, columns: TOTALS }) })`). **A row form with no `inputs` is its
|
|
1497
|
+
question alone** (a pay run's Lock): no popup, the question (`confirm`, else "{label} {name}?", buttons the action's
|
|
1498
|
+
word and "Cancel"), then the record as the list has it - or what `load` read - sent through `transform`; the API's
|
|
1499
|
+
toasts say it was done or why not, and a refusal writes nothing.
|
|
1500
|
+
- **A record's history** (`api.audit`, so every row offers History; or `AuditHistory`/`openAuditHistory` on a page):
|
|
1501
|
+
each entry says what was done, when and by whom - "by Sita Rai" when the answer carries `creatorName`, else the start
|
|
1502
|
+
of the person's token ("User 3fA9b2…"), which still tells two people apart - and the columns it changed. An app whose
|
|
1503
|
+
backend sends no names sets `setAuditActorNames(async (tokens, signal) => ({ [token]: "Sita Rai (sita.rai)" }))` once:
|
|
1504
|
+
the history asks it once per page shown, for the tokens it has not had an answer for, and keeps the answers.
|
|
1505
|
+
- **Options narrowed by another input** (`narrowedBy: { name: "subscriptionGroupId" }` on an input, in a Form and its rows): a
|
|
1461
1506
|
Package list only of the Group picked. Static `options` are filtered by the records' `key` field (default the other
|
|
1462
1507
|
input's name); `loadOptions` is asked with `request.narrow`, which `crudPicker` answers from its kept list, or from
|
|
1463
1508
|
the server with `crudPicker(PACKAGES, { labelKey, narrowQuery: "subscriptionGroupId" })`. While the other input is
|
|
@@ -1465,6 +1510,12 @@ export const GUESTS = MODULES.controller(NexusModule.Reservation, "Guest", "gues
|
|
|
1465
1510
|
**Or by a fixed value** the page knows when the form opens (`narrowedBy: { key: "subscriptionGroupId", value:
|
|
1466
1511
|
subscriber.subscriptionGroupId }`): nothing is followed or cleared, and a `null` value offers nothing. `orUnset: true`
|
|
1467
1512
|
(both forms) also offers the options whose field is empty: the Fee list of the group, and the fees of every group.
|
|
1513
|
+
**Several at once, and in rows**: `narrowedBy: [{ name: "storeId", from: "form" }, { name: "itemId" }]` on a line
|
|
1514
|
+
table's Tags offers the units of the line's Item in the form's Store - the first empty one asked for, a loader told
|
|
1515
|
+
every value (`request.narrows`), and `crudPicker({}, { list: UNITS, valueKey: "tagNumber", labelKey: "tagNumber" })`
|
|
1516
|
+
over `StockBalance/Units/{storeId}/{itemId}` reads each pair once, the values in its path. A person's new Item clears
|
|
1517
|
+
that line's tags, a new Store every line's; a saved document loading keeps them. `dependsOn: ["quantity"]` on a row
|
|
1518
|
+
input redraws its row as the quantity changes, so `cellParams` can cap the picks (`{ max: row.quantity }`).
|
|
1468
1519
|
- **Cache.** Lists are kept for 5 minutes while the page is open (`cache={{ lifetime }}`, or `false`): going back to a
|
|
1469
1520
|
list paints it at once. After a change, the list in view keeps its rows and the others load again when shown; Reload
|
|
1470
1521
|
and the handle's `reload()` go past the cache. The cache goes when the page does.
|
|
@@ -1596,7 +1647,7 @@ owner's `module`, the `category`, the `ownerId`, and for a picture its `aspect`,
|
|
|
1596
1647
|
|---|---|---|
|
|
1597
1648
|
| `EInputTypes.Picture` (`PictureInput`) | `nexus-shared` / `/client` | In a form: the owner's main picture, Upload, Edit, Remove, All pictures (a click on the picture opens them too); in a category that keeps one file, Replace and Delete. Changed at once: the form keeps no field for it (`holdsValue`), so a save sends nothing under its name, and it takes no `required`. `ownerId` null: "Save the record first". |
|
|
1598
1649
|
| `PictureGallery`, `openPictureGallery` | `nexus-shared/client` | An owner's pictures in two panes: the main picture large with Upload and Remove and the count, then a grid of rounded squares, each with View, Make main, Edit and Delete inside it (on hover, on a keyboard's focus, always by touch; neither Make main nor Delete on the main one). Upload several, up to the category's count; edits are saved as new pictures. Under 560px the main picture sits small over the grid |
|
|
1599
|
-
| `DocumentList` | `nexus-shared/client` | The same as a list, for a category that takes PDF (the gallery picks it itself): kind, size, date, open, rename, delete |
|
|
1650
|
+
| `DocumentList` | `nexus-shared/client` | The same as a list, for a category that takes PDF (the gallery picks it itself): kind, size, date, open, rename, delete; `changes` for a record whose files are only added to |
|
|
1600
1651
|
| `pictureColumn` | `nexus-shared/client` | A table column with each row's picture: the page's rows in one call (`primaryPictures`), a click opens the row's gallery |
|
|
1601
1652
|
| `OwnerPicture`, `usePrimaryPicture`, `useOwnerFiles` | `nexus-shared/client` | The small picture, the kept main picture, and the files with every change, for a page's own layout |
|
|
1602
1653
|
| `loadImageEditor`, `preloadImageEditor` | `nexus-shared/client` | The image editor, downloaded from the file backend the first time it is needed; never in the bundle |
|
|
@@ -1642,6 +1693,12 @@ columns: [pictureColumn({ ...LOGO, rowTitle: row => row.companyName }), ...];
|
|
|
1642
1693
|
- **Read only, and why.** `lockReason: "The branch is closed."` makes the files read only with that sentence under
|
|
1643
1694
|
them; a person the backend lets only look is told the backend's words (`OwnerFiles.readOnlyReason`), and the files
|
|
1644
1695
|
of an owner not in use (trashed, ended) say so (`OWNER_NOT_IN_USE_REASON`).
|
|
1696
|
+
- **Only some changes.** `changes: receipt.isPosted ? ["add"] : undefined` with `changesReason: "Posted: a document can
|
|
1697
|
+
be added; none is removed."` keeps Upload and offers no Rename, Delete, Make main or Remove (nor a Replace or an Edit
|
|
1698
|
+
that deletes or marks) for a record whose files may only be added to; the reason is said under the files. The changes
|
|
1699
|
+
are `FileChange` words, the core's `EFileChanges` one for one. A refusal that says so - the Nexus `API_X084`, read by
|
|
1700
|
+
`FileAdapter.changesLeft` - narrows them the same way for that owner under the server's words, so a page read before
|
|
1701
|
+
the record was posted stops offering what the server refuses. `ownerFileActions` is the rule, for a page's own buttons.
|
|
1645
1702
|
- **Initials.** `pictureColumn({ ..., nameKey: "fullname" })` (or `OwnerPicture name`) shows an owner with no picture
|
|
1646
1703
|
as the name's initials, on the shared `Avatar`'s tone for that name.
|
|
1647
1704
|
- **The keyboard's focus stays.** While a change saves, the buttons stay focusable (`aria-disabled`, the press ignored:
|
|
@@ -1703,3 +1760,152 @@ follows, the hero too, with nothing read again).
|
|
|
1703
1760
|
- **States** are the piece's: the cards held as they will be while they load, a failed read in each of its cards with
|
|
1704
1761
|
Try again, `readOnly` with no button anywhere, and `signedOut` - an answer that ended the session closes the popups
|
|
1705
1762
|
and leaves the page's words and its way back in.
|
|
1763
|
+
|
|
1764
|
+
### Mail thread
|
|
1765
|
+
|
|
1766
|
+
Mail inside the app: a **thread** with a subject, a sender and the people it went to (To, Cc, Bcc), whose first line is
|
|
1767
|
+
the mail and the rest are replies, drawn by the chat's own thread and reply box. A thread can be about a record (a
|
|
1768
|
+
leave request), and an app can write into it when something happens - a **notification**, drawn as a quiet event line
|
|
1769
|
+
among the replies. Everything comes through one interface, `MessageSource`, so a page passes params and the package
|
|
1770
|
+
does the rest.
|
|
1771
|
+
|
|
1772
|
+
```tsx
|
|
1773
|
+
const mail = apiMessageSource(); // the app's MessageBackend: Nexus's Message module, Mail controller, by default
|
|
1774
|
+
const people = mailPeoplePicker(mail); // To, Cc and Bcc boxes that page the server and name what they hold
|
|
1775
|
+
|
|
1776
|
+
<Crud<MailInboxRow>
|
|
1777
|
+
api={{ pages: { normal: INBOX }, create: MAIL_CREATE }}
|
|
1778
|
+
paging="server"
|
|
1779
|
+
actions={["create", "view"]}
|
|
1780
|
+
columns={mailInboxColumnList()}
|
|
1781
|
+
inputs={[
|
|
1782
|
+
{ type: EInputTypes.Dropdown, name: "to", label: "To", multiple: true, required: true, ...people },
|
|
1783
|
+
{ type: EInputTypes.Dropdown, name: "cc", label: "Cc", multiple: true, ...people },
|
|
1784
|
+
{ type: EInputTypes.Dropdown, name: "bcc", label: "Bcc", multiple: true, ...people },
|
|
1785
|
+
{ type: EInputTypes.Text, name: "subject", label: "Subject", required: true, maxLength: 100 },
|
|
1786
|
+
{ type: EInputTypes.Markdown, name: "body", label: "Message", required: true, maxLength: 4000 },
|
|
1787
|
+
]}
|
|
1788
|
+
form={{ newValues: () => ({ clientId: newMailClientId() }), createDefaults: mailComposeDefaults(mail, people) }}
|
|
1789
|
+
details={{ size: "xl", render: row => <MailThread source={mail} threadId={row.id} userId={me.id} /> }}
|
|
1790
|
+
table={{ rowStrong: mailRowUnread, openOnClick: true }}
|
|
1791
|
+
/>
|
|
1792
|
+
```
|
|
1793
|
+
|
|
1794
|
+
- **The head** is a band at the top of the thread, scrolling with its lines (`ChatThread` `lead`), never in a box of
|
|
1795
|
+
its own; the thread opens at its top. It holds the subject, the time, From, To and Cc, and Bcc **only as the source gives
|
|
1796
|
+
it** (the sender sees every person copied unseen; a person copied unseen sees their own row; nobody else sees any).
|
|
1797
|
+
Lists of people fold to three names and "+2", a button that opens the rest in place. A record the thread is about
|
|
1798
|
+
is drawn as its card ([below](#a-records-card-in-a-message)) when its kind has one, read again when a notification
|
|
1799
|
+
arrives, and folded to its title and figures while the thread is narrower than 480px (its real width, followed as
|
|
1800
|
+
it changes, until the reader folds or unfolds it); otherwise as its kind (named by `defineChatDomains`) and its words.
|
|
1801
|
+
`renderDomain` draws it instead.
|
|
1802
|
+
- **The lines** are a `ChatStore` on `source.threadSource(id)`: paging up, a reply on screen at once, Retry, read marks
|
|
1803
|
+
(reading to the bottom marks the thread read; `onSummary` hands the page the new counts), polling.
|
|
1804
|
+
`ChatMessage.kind` says what a line is: `chat`, `mail`, or `notification` (an event line with who acted, no bubble).
|
|
1805
|
+
- **A person copied unseen** reads "You were sent a blind copy. Replying shows you to everyone." over the reply box
|
|
1806
|
+
(`mailBccNote`), and the reply box is described by it; once they reply the head is read again and shows them as a Cc.
|
|
1807
|
+
- **When someone else writes**, the head is read again (the people may have changed) and `onChanged` tells the page;
|
|
1808
|
+
`reason: "notification"` means the record may have changed, and the record's slot is told too (`revision`).
|
|
1809
|
+
- **States**: the head held by bones while it loads, a failure with Try again in the head and in the thread, "This
|
|
1810
|
+
message is not available to you." for a thread the reader is not on (403 or 404), a closed thread that is read and not
|
|
1811
|
+
answered (`canSend: false`).
|
|
1812
|
+
- **`MessageBackend`** maps each action to an endpoint and reads each answer; `NexusMessageBackend` is the Message
|
|
1813
|
+
module's `Mail` controller (POST `Inbox`, `History`, `Create`, `Reply`, `People`; GET `Thread/{id}`, `Unread`,
|
|
1814
|
+
`Defaults`; PUT `Read`, `Archive`, `Unarchive`), reading a role (1 From, 2 To, 3 Cc, 4 Bcc) and a line's type (0 chat,
|
|
1815
|
+
1 mail, 2 notification) as this package's words. Override `module` or any reader for another backend.
|
|
1816
|
+
- **`localMessageSource({ userId, people, threads })`** keeps a mailbox in memory with a backend's rules - who sees
|
|
1817
|
+
whom, a blind copy's reply, archive and unread per person, a retried send written once - for a demo or a test;
|
|
1818
|
+
`as(userId)` reads it as somebody else and `post` writes a module's notification.
|
|
1819
|
+
- **Three params any page can use**: `pagedPicker` (a dropdown over any server search, naming the values it holds),
|
|
1820
|
+
`Crud` `form.createDefaults` (an Add form's values read as it opens; a failed read opens it on `newValues`), and
|
|
1821
|
+
`DataTable` `rowStrong` (a row read strong while a field says so).
|
|
1822
|
+
|
|
1823
|
+
### A record's card in a message
|
|
1824
|
+
|
|
1825
|
+
A message about a record - a leave request, a requisition - shows that record as a **card**, read live from the module
|
|
1826
|
+
that owns it each time it is drawn, never a copy kept in the message. The package knows no module and no record kind:
|
|
1827
|
+
an app gives each kind it names with `defineChatDomains` a `card` - how to read the record and what to show of it - and
|
|
1828
|
+
every thread about that kind draws it at its head. Every kind looks alike, because a card is params.
|
|
1829
|
+
|
|
1830
|
+
```tsx
|
|
1831
|
+
defineChatDomains([
|
|
1832
|
+
{
|
|
1833
|
+
domain: EDomainTypes.LeaveRequest,
|
|
1834
|
+
key: "leave-request",
|
|
1835
|
+
label: "Leave request",
|
|
1836
|
+
card: domainCard({
|
|
1837
|
+
// The owning module's card door, which checks the reader. 403 and 404 read "You cannot open this leave request."
|
|
1838
|
+
load: (id, signal) => api.get<LeaveRequestCard>(REQUEST_CARD.leave, { params: { id }, signal, toast: false }).then(unwrapApiResult),
|
|
1839
|
+
// `view` is typed by what `load` answers.
|
|
1840
|
+
view: leave => ({
|
|
1841
|
+
title: `${leave.leaveTypeName} · ${formatDays(leave.days)}`,
|
|
1842
|
+
code: leave.requestCode,
|
|
1843
|
+
state: { label: "In approval", tone: "info" },
|
|
1844
|
+
fields: [
|
|
1845
|
+
{ label: "From", value: leave.startDate, kind: "date" },
|
|
1846
|
+
{ label: "To", value: leave.endDate, kind: "date", note: "half day" },
|
|
1847
|
+
],
|
|
1848
|
+
figures: [
|
|
1849
|
+
{ label: "Available", value: leave.balance.availableDays, kind: "days" },
|
|
1850
|
+
{ label: "Held", value: leave.balance.heldDays, kind: "days", tone: "warning" },
|
|
1851
|
+
],
|
|
1852
|
+
link: { href: "/admin/requisition/my-approvals", label: "Open My approvals" },
|
|
1853
|
+
}),
|
|
1854
|
+
held: { subtitle: true, fields: 2, figures: 2 }, // what its placeholder holds the first time
|
|
1855
|
+
}),
|
|
1856
|
+
},
|
|
1857
|
+
]);
|
|
1858
|
+
|
|
1859
|
+
<DomainCard domain={EDomainTypes.LeaveRequest} id={token} label="LR2082-0007 · Puja Shrestha" viewer={me.id} />
|
|
1860
|
+
```
|
|
1861
|
+
|
|
1862
|
+
- **The card** (`DomainCardParams`): the kind as a tag and the title; a quiet line under it with the code, a subtitle and
|
|
1863
|
+
the link at its end; the state as a tag in its tone; figures first (large values in a row, two to a row on a phone,
|
|
1864
|
+
a tone mark beside each, a hint under one keeping two lines); then its details: fields (a label over each value, as
|
|
1865
|
+
many to a row as fit) and lines (a short table, numbers and
|
|
1866
|
+
money at the end, the first column one line cut with "..."; "and 3 more"). Values are written by their kind with the
|
|
1867
|
+
reader's settings: `date`, `datetime` (their zone), `days` (`formatDays`: "1 day", "2.5 days"), `money` (the card's
|
|
1868
|
+
`currency`, `formatMoney`), `number`, `person`, `text`. A field with no value is left out unless it names `empty`.
|
|
1869
|
+
`extra(data)` adds one more piece of this package's parts after the lines. A band, never a box: a mail thread's head
|
|
1870
|
+
draws a line above it.
|
|
1871
|
+
The fold button folds the details and keeps the figures (folded, a leave's card is its title, code and balance);
|
|
1872
|
+
`collapsed` with `onCollapsedChange` lets a page hold the fold, `defaultCollapsed` starts it.
|
|
1873
|
+
- **Every state**: a placeholder of the card's own parts, in the shape of the kind's last card (or its `held`), so the
|
|
1874
|
+
record lands without moving what is under it, at any width; the card; "You cannot open this leave request." for 403
|
|
1875
|
+
and 404 alike (a stranger learns nothing from the difference); "Could not load this leave request." with Try again
|
|
1876
|
+
for anything else; the kind and the record's words alone for a kind with no card or a message with no id. `view`
|
|
1877
|
+
throwing on an answer is a failure too, never a broken thread.
|
|
1878
|
+
- **Read again**: when `revision` grows (a mail thread's notification), `ref.reload()`, or `refreshDomainCards(kind,
|
|
1879
|
+
id)` (the page changed the record itself). The card stays while it is read, says "Updated 2:45 PM" when the record
|
|
1880
|
+
turned out different, and "Not up to date" with Try again when the read failed; a refusal on a read again drops it.
|
|
1881
|
+
A screen reader is told each.
|
|
1882
|
+
- **Reads** (`DomainCardReads`, `domainCardReads`): one read of a record at a time however many cards draw it, an
|
|
1883
|
+
answer kept 30 seconds per reader (`lifetime`), a failure never kept, a read nobody waits for aborted, a read again
|
|
1884
|
+
taking only a read started after the change and forgetting what other readers kept. `useDomainCard` and
|
|
1885
|
+
`DomainCardRun` give a page the same read for a look of its own; `DomainCardFrame` draws a card from params a page
|
|
1886
|
+
already holds, with no read.
|
|
1887
|
+
|
|
1888
|
+
### Message bell
|
|
1889
|
+
|
|
1890
|
+
How much internal mail waits, on a top bar's button: the count over its corner in the chat's badge (one size whatever
|
|
1891
|
+
the count, "99+" from 100 on) and the newest unread threads a click away, each a link into the inbox.
|
|
1892
|
+
|
|
1893
|
+
```tsx
|
|
1894
|
+
const mail = apiMessageSource(); // one object for the page: every bell on it shares one poller
|
|
1895
|
+
|
|
1896
|
+
<AdminLayout
|
|
1897
|
+
utilities={[{ code: "notifications", label: "Messages", icon: "bell" }]}
|
|
1898
|
+
renderUtility={item => (item.code === "notifications" ? <MessageBell source={mail} href="/admin/message/inbox" label={item.label} /> : undefined)}
|
|
1899
|
+
/>
|
|
1900
|
+
```
|
|
1901
|
+
|
|
1902
|
+
- **One read however many bells.** `useMessageUnread(source)` joins the page's poller for that source: it reads when
|
|
1903
|
+
the first bell appears, then every minute while the page is in front, at once when the window takes the focus or the
|
|
1904
|
+
tab comes back, and never while the tab is hidden. A failure keeps the count read before. Call
|
|
1905
|
+
`refreshMessageUnread(source)` after the page reads or archives threads, and every bell on it reads at once - or
|
|
1906
|
+
right after the read on its way, which may have counted before the change (one more read, however many are asked).
|
|
1907
|
+
- **The menu**: the newest unread (subject; who wrote last, the record, when), a row opening `href?thread=<id>` (or
|
|
1908
|
+
`threadHref(row)`), "Open inbox", and Try again after a failure. Opening it reads the count again.
|
|
1909
|
+
- **A screen reader** hears the button's name ("Messages: 3 unread messages") and, when the count goes up, the new
|
|
1910
|
+
count once from one bell of the page - never on every read, never as the page loads.
|
|
1911
|
+
- **`Button` `badge`** is the piece underneath: a count over any button's corner that leaves its size alone.
|
package/dist/Client.Index.d.ts
CHANGED
|
@@ -3,15 +3,21 @@ export * from "./Components/Chats/ChatButton.tsx";
|
|
|
3
3
|
export * from "./Components/Chats/ChatComposer.tsx";
|
|
4
4
|
export * from "./Components/Chats/ChatConversations.tsx";
|
|
5
5
|
export * from "./Components/Chats/ChatPanel.tsx";
|
|
6
|
+
export * from "./Components/Chats/ChatPopup.tsx";
|
|
6
7
|
export * from "./Components/Chats/ChatParts.tsx";
|
|
7
8
|
export * from "./Components/Chats/ChatThread.tsx";
|
|
9
|
+
export * from "./Components/Dashboards/DashboardBoard.tsx";
|
|
10
|
+
export * from "./Components/Dashboards/DashboardWidgets.tsx";
|
|
11
|
+
export * from "./Components/Dashboards/WidgetFrame.tsx";
|
|
8
12
|
export * from "./Components/Documents/DocumentFrame.tsx";
|
|
13
|
+
export * from "./Components/Domains/DomainCard.tsx";
|
|
9
14
|
export * from "./Components/Documents/Menu.tsx";
|
|
10
15
|
export * from "./Components/Documents/SplitButton.tsx";
|
|
11
16
|
export * from "./Components/Documents/TabButtons.tsx";
|
|
12
17
|
export * from "./Components/Documents/Tabs.tsx";
|
|
13
18
|
export * from "./Components/Forms/ApiForm.tsx";
|
|
14
19
|
export * from "./Components/Forms/Crud.tsx";
|
|
20
|
+
export { openCrudForm, type CrudFormRequest } from "./Components/Forms/CrudActions.tsx";
|
|
15
21
|
export * from "./Components/Forms/CrudHistory.tsx";
|
|
16
22
|
export * from "./Components/Forms/CrudNarrowPicker.tsx";
|
|
17
23
|
export * from "./Components/Forms/CrudParts.tsx";
|
|
@@ -52,6 +58,12 @@ export * from "./Components/Layouts/PopupRules.ts";
|
|
|
52
58
|
export * from "./Components/Layouts/ThemePicker.tsx";
|
|
53
59
|
export * from "./Components/Layouts/ThemeSwitcher.tsx";
|
|
54
60
|
export * from "./Components/Layouts/Toaster.tsx";
|
|
61
|
+
export * from "./Components/Messages/MailClient.tsx";
|
|
62
|
+
export * from "./Components/Messages/MailColumns.tsx";
|
|
63
|
+
export * from "./Components/Messages/MailCompose.tsx";
|
|
64
|
+
export * from "./Components/Messages/MailReader.tsx";
|
|
65
|
+
export * from "./Components/Messages/MailThread.tsx";
|
|
66
|
+
export * from "./Components/Messages/MessageBell.tsx";
|
|
55
67
|
export * from "./Components/Viewers/Avatar.tsx";
|
|
56
68
|
export * from "./Components/Viewers/DataTable.tsx";
|
|
57
69
|
export * from "./Components/Viewers/DataTableParts.tsx";
|
|
@@ -72,4 +84,5 @@ export * from "./Services/FileApi.ts";
|
|
|
72
84
|
export * from "./Services/ImageEditorLoader.ts";
|
|
73
85
|
export * from "./Services/ModuleImport.ts";
|
|
74
86
|
export * from "./Services/JoinSources.ts";
|
|
87
|
+
export * from "./Services/MessageApi.ts";
|
|
75
88
|
export * from "./Services/ThemeService.ts";
|
package/dist/Client.Index.js
CHANGED
|
@@ -6,15 +6,21 @@ export * from "./Components/Chats/ChatButton.js";
|
|
|
6
6
|
export * from "./Components/Chats/ChatComposer.js";
|
|
7
7
|
export * from "./Components/Chats/ChatConversations.js";
|
|
8
8
|
export * from "./Components/Chats/ChatPanel.js";
|
|
9
|
+
export * from "./Components/Chats/ChatPopup.js";
|
|
9
10
|
export * from "./Components/Chats/ChatParts.js";
|
|
10
11
|
export * from "./Components/Chats/ChatThread.js";
|
|
12
|
+
export * from "./Components/Dashboards/DashboardBoard.js";
|
|
13
|
+
export * from "./Components/Dashboards/DashboardWidgets.js";
|
|
14
|
+
export * from "./Components/Dashboards/WidgetFrame.js";
|
|
11
15
|
export * from "./Components/Documents/DocumentFrame.js";
|
|
16
|
+
export * from "./Components/Domains/DomainCard.js";
|
|
12
17
|
export * from "./Components/Documents/Menu.js";
|
|
13
18
|
export * from "./Components/Documents/SplitButton.js";
|
|
14
19
|
export * from "./Components/Documents/TabButtons.js";
|
|
15
20
|
export * from "./Components/Documents/Tabs.js";
|
|
16
21
|
export * from "./Components/Forms/ApiForm.js";
|
|
17
22
|
export * from "./Components/Forms/Crud.js";
|
|
23
|
+
export { openCrudForm } from "./Components/Forms/CrudActions.js";
|
|
18
24
|
export * from "./Components/Forms/CrudHistory.js";
|
|
19
25
|
export * from "./Components/Forms/CrudNarrowPicker.js";
|
|
20
26
|
export * from "./Components/Forms/CrudParts.js";
|
|
@@ -55,6 +61,12 @@ export * from "./Components/Layouts/PopupRules.js";
|
|
|
55
61
|
export * from "./Components/Layouts/ThemePicker.js";
|
|
56
62
|
export * from "./Components/Layouts/ThemeSwitcher.js";
|
|
57
63
|
export * from "./Components/Layouts/Toaster.js";
|
|
64
|
+
export * from "./Components/Messages/MailClient.js";
|
|
65
|
+
export * from "./Components/Messages/MailColumns.js";
|
|
66
|
+
export * from "./Components/Messages/MailCompose.js";
|
|
67
|
+
export * from "./Components/Messages/MailReader.js";
|
|
68
|
+
export * from "./Components/Messages/MailThread.js";
|
|
69
|
+
export * from "./Components/Messages/MessageBell.js";
|
|
58
70
|
export * from "./Components/Viewers/Avatar.js";
|
|
59
71
|
export * from "./Components/Viewers/DataTable.js";
|
|
60
72
|
export * from "./Components/Viewers/DataTableParts.js";
|
|
@@ -77,4 +89,5 @@ export * from "./Services/FileApi.js";
|
|
|
77
89
|
export * from "./Services/ImageEditorLoader.js";
|
|
78
90
|
export * from "./Services/ModuleImport.js";
|
|
79
91
|
export * from "./Services/JoinSources.js";
|
|
92
|
+
export * from "./Services/MessageApi.js";
|
|
80
93
|
export * from "./Services/ThemeService.js";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
1
2
|
import type { ChatSnapshot, ChatStore } from "../../Helpers/ChatStore.ts";
|
|
2
3
|
import type { ChatMessage, ChatParticipant } from "../../Interfaces/ChatInterfaces.ts";
|
|
3
4
|
import { type MarkdownToolbarItem } from "../Inputs/MarkdownEditor.tsx";
|
|
@@ -32,10 +33,15 @@ export interface ChatComposerProps {
|
|
|
32
33
|
height?: number;
|
|
33
34
|
/** Why the user may not write here. */
|
|
34
35
|
readOnlyMessage?: string;
|
|
36
|
+
/**
|
|
37
|
+
* What the writer should know before sending, in a line over the writing area that the writing area is described by:
|
|
38
|
+
* "You were sent a blind copy. Replying shows you to everyone."
|
|
39
|
+
*/
|
|
40
|
+
notice?: ReactNode;
|
|
35
41
|
/** Most characters in one message. Default 4,000. */
|
|
36
42
|
maxLength?: number;
|
|
37
43
|
autoFocus?: boolean;
|
|
38
44
|
className?: string;
|
|
39
45
|
}
|
|
40
46
|
/** Writes and sends a message, in Markdown. */
|
|
41
|
-
export declare function ChatComposer({ store, state, draftKey, placeholder, files, replyTo, onCancelReply, sendKey, mentions, mentionHref, toolbar, height, readOnlyMessage, maxLength, autoFocus, className, }: ChatComposerProps): import("react").JSX.Element;
|
|
47
|
+
export declare function ChatComposer({ store, state, draftKey, placeholder, files, replyTo, onCancelReply, sendKey, mentions, mentionHref, toolbar, height, readOnlyMessage, notice, maxLength, autoFocus, className, }: ChatComposerProps): import("react").JSX.Element;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { iconPaperclip, iconSend, iconX, InlineIcon } from "nexus-icons";
|
|
4
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
import { iconInfoCircle, iconPaperclip, iconSend, iconX, InlineIcon } from "nexus-icons";
|
|
4
|
+
import { useCallback, useEffect, useId, useRef, useState } from "react";
|
|
5
5
|
import { applyChatMention, chatFileSize, findChatMention, matchChatMentions, readChatMentions } from "../../Helpers/ChatHelpers.js";
|
|
6
6
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
7
7
|
import { continueMarkdownList } from "../../Helpers/MarkdownEditing.js";
|
|
@@ -54,8 +54,9 @@ function insideCodeFence(value, caret) {
|
|
|
54
54
|
return (before.match(/^```/gm) ?? []).length % 2 === 1;
|
|
55
55
|
}
|
|
56
56
|
/** Writes and sends a message, in Markdown. */
|
|
57
|
-
export function ChatComposer({ store, state, draftKey, placeholder = "Write a message…", files = false, replyTo, onCancelReply, sendKey = "enter", mentions, mentionHref, toolbar = CHAT_TOOLBAR, height = 84, readOnlyMessage = "You can read this conversation, but not write in it.", maxLength = 4000, autoFocus = false, className, }) {
|
|
57
|
+
export function ChatComposer({ store, state, draftKey, placeholder = "Write a message…", files = false, replyTo, onCancelReply, sendKey = "enter", mentions, mentionHref, toolbar = CHAT_TOOLBAR, height = 84, readOnlyMessage = "You can read this conversation, but not write in it.", notice, maxLength = 4000, autoFocus = false, className, }) {
|
|
58
58
|
const boxRef = useRef(null);
|
|
59
|
+
const noticeId = useId();
|
|
59
60
|
const fileRef = useRef(null);
|
|
60
61
|
// The text lives here so Send, the draft, and clearing after a send all read the same thing. A message is short, so
|
|
61
62
|
// a render per keystroke costs nothing — unlike the long documents the editor is otherwise built for.
|
|
@@ -169,9 +170,11 @@ export function ChatComposer({ store, state, draftKey, placeholder = "Write a me
|
|
|
169
170
|
return (_jsx("div", { className: cx("nx-chat-composer nx-chat-composer--readonly", className), children: _jsx("p", { children: readOnlyMessage }) }));
|
|
170
171
|
}
|
|
171
172
|
const empty = text.trim() === "" && picked.length === 0;
|
|
172
|
-
return (_jsxs("div", { className: cx("nx-chat-composer", className), children: [replyTo && (_jsxs("div", { className: "nx-chat-composer__reply", children: [_jsxs("span", { className: "nx-chat-composer__reply-text", children: [_jsx("span", { className: "nx-chat-composer__reply-author", children: replyTo.authorName || "Unknown" }), _jsx("span", { children: markdownToPlainText(replyTo.body) })] }), _jsx("button", { type: "button", onClick: onCancelReply, "aria-label": "Stop replying", children: _jsx(InlineIcon, { icon: iconX }) })] })), picked.length > 0 && (_jsx("ul", { className: "nx-chat-composer__files", children: picked.map((file, index) => (_jsxs("li", { children: [_jsx("span", { className: "nx-chat-composer__file-name", children: file.name }), _jsx("span", { className: "nx-chat-composer__file-size", children: chatFileSize(file.size) }), _jsx("button", { type: "button", onClick: () => setPicked(held => held.filter((_, at) => at !== index)), "aria-label": `Remove ${file.name}`, children: _jsx(InlineIcon, { icon: iconX }) })] }, `${file.name}-${index}`))) })), picking && (_jsx("ul", { className: "nx-chat-mentions", role: "listbox", "aria-label": "People", children: offered.map((person, index) => (_jsx("li", { children: _jsxs("button", { type: "button", className: cx("nx-chat-mentions__item", index === at && "is-at"), role: "option", "aria-selected": index === at,
|
|
173
|
+
return (_jsxs("div", { className: cx("nx-chat-composer", className), children: [notice && (_jsxs("p", { className: "nx-chat-composer__notice", id: noticeId, children: [_jsx(InlineIcon, { icon: iconInfoCircle }), _jsx("span", { children: notice })] })), replyTo && (_jsxs("div", { className: "nx-chat-composer__reply", children: [_jsxs("span", { className: "nx-chat-composer__reply-text", children: [_jsx("span", { className: "nx-chat-composer__reply-author", children: replyTo.authorName || "Unknown" }), _jsx("span", { children: markdownToPlainText(replyTo.body) })] }), _jsx("button", { type: "button", onClick: onCancelReply, "aria-label": "Stop replying", children: _jsx(InlineIcon, { icon: iconX }) })] })), picked.length > 0 && (_jsx("ul", { className: "nx-chat-composer__files", children: picked.map((file, index) => (_jsxs("li", { children: [_jsx("span", { className: "nx-chat-composer__file-name", children: file.name }), _jsx("span", { className: "nx-chat-composer__file-size", children: chatFileSize(file.size) }), _jsx("button", { type: "button", onClick: () => setPicked(held => held.filter((_, at) => at !== index)), "aria-label": `Remove ${file.name}`, children: _jsx(InlineIcon, { icon: iconX }) })] }, `${file.name}-${index}`))) })), picking && (_jsx("ul", { className: "nx-chat-mentions", role: "listbox", "aria-label": "People", children: offered.map((person, index) => (_jsx("li", { children: _jsxs("button", { type: "button", className: cx("nx-chat-mentions__item", index === at && "is-at"), role: "option", "aria-selected": index === at,
|
|
173
174
|
// The editor must not lose the caret before the name goes in.
|
|
174
|
-
onMouseDown: event => event.preventDefault(), onMouseEnter: () => setAt(index), onClick: () => putMention(person), children: [_jsx(ChatAvatar, { name: person.name, src: person.avatar, size: "sm" }), _jsx("span", { className: "nx-chat-mentions__name", children: person.name }), person.role && _jsx("span", { className: "nx-chat-mentions__role", children: person.role })] }) }, String(person.id)))) })), _jsx(MarkdownEditor, { ref: boxRef, className: "nx-chat-composer__editor", "aria-label": "Message", placeholder: placeholder, value: text, onValueChange: change, onKeyDown: keyDown,
|
|
175
|
+
onMouseDown: event => event.preventDefault(), onMouseEnter: () => setAt(index), onClick: () => putMention(person), children: [_jsx(ChatAvatar, { name: person.name, src: person.avatar, size: "sm" }), _jsx("span", { className: "nx-chat-mentions__name", children: person.name }), person.role && _jsx("span", { className: "nx-chat-mentions__role", children: person.role })] }) }, String(person.id)))) })), _jsx(MarkdownEditor, { ref: boxRef, className: "nx-chat-composer__editor", "aria-label": "Message", "aria-describedby": notice ? noticeId : undefined, placeholder: placeholder, value: text, onValueChange: change, onKeyDown: keyDown,
|
|
175
176
|
// Moving the caret away from an `@…` closes the list, as clicking elsewhere in the text does.
|
|
176
|
-
onSelect: () => setMention(mentions ? findChatMention(boxRef.current?.value ?? "", boxRef.current?.selectionStart ?? 0) : null), onBlur: () => setMention(null), modes: ["write", "preview"], toolbar: toolbar,
|
|
177
|
+
onSelect: () => setMention(mentions ? findChatMention(boxRef.current?.value ?? "", boxRef.current?.selectionStart ?? 0) : null), onBlur: () => setMention(null), modes: ["write", "preview"], toolbar: toolbar,
|
|
178
|
+
// One row on a phone too: the buttons that do not fit go into a menu, so the thread above keeps its room.
|
|
179
|
+
toolbarOverflow: "menu", height: height, minHeight: 72, maxHeight: 360, heightKey: "nx-chat", showCount: false, maxLength: maxLength }), _jsxs("div", { className: "nx-chat-composer__row", children: [_jsx("span", { className: "nx-chat-composer__hint", children: sendKey === "enter" ? "Enter sends · Shift+Enter for a line" : "Ctrl+Enter sends" }), files && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", className: "nx-chat-composer__attach", onClick: () => fileRef.current?.click(), "aria-label": "Attach a file", title: "Attach a file", children: _jsx(InlineIcon, { icon: iconPaperclip }) }), _jsx("input", { ref: fileRef, type: "file", multiple: true, hidden: true, onChange: addFiles })] })), _jsxs("button", { type: "button", className: "nx-chat-composer__send", onClick: send, disabled: empty, "aria-label": "Send", title: sendKey === "enter" ? "Send (Enter)" : "Send (Ctrl+Enter)", children: [_jsx(InlineIcon, { icon: iconSend }), _jsx("span", { children: "Send" })] })] })] }));
|
|
177
180
|
}
|
|
@@ -40,6 +40,15 @@ export declare function ChatSystemNote({ message, time }: {
|
|
|
40
40
|
message: ChatMessage;
|
|
41
41
|
time: ChatTimeOptions;
|
|
42
42
|
}): import("react").JSX.Element;
|
|
43
|
+
/**
|
|
44
|
+
* A notification (`kind: "notification"`): what an app wrote about an event, under the name of the person who acted -
|
|
45
|
+
* "Approved at Supervisor. Now with Accounts." It is a quiet line across the thread, not a bubble on a side: the bell,
|
|
46
|
+
* who acted and when, and the words under them, so a thread of replies reads around its events.
|
|
47
|
+
*/
|
|
48
|
+
export declare function ChatEventNote({ message, time }: {
|
|
49
|
+
message: ChatMessage;
|
|
50
|
+
time: ChatTimeOptions;
|
|
51
|
+
}): import("react").JSX.Element;
|
|
43
52
|
export interface ChatMessageProps {
|
|
44
53
|
message: ChatMessage;
|
|
45
54
|
/** This user wrote it: it sits on the right. */
|
|
@@ -76,10 +85,14 @@ export declare function ChatEmpty({ message, hint }: {
|
|
|
76
85
|
message?: string;
|
|
77
86
|
hint?: ReactNode;
|
|
78
87
|
}): import("react").JSX.Element;
|
|
79
|
-
/**
|
|
80
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Something that did not load, with a way to try again. `flat`: its tint without an edge, for a failure that already
|
|
90
|
+
* sits in a framed panel (a dashboard widget).
|
|
91
|
+
*/
|
|
92
|
+
export declare function ChatFailure({ message, onRetry, flat }: {
|
|
81
93
|
message: string;
|
|
82
94
|
onRetry?: () => void;
|
|
95
|
+
flat?: boolean;
|
|
83
96
|
}): import("react").JSX.Element;
|
|
84
97
|
/** Messages on their way in. */
|
|
85
98
|
export declare function ChatLoading({ label }: {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { iconAlertCircle, iconCheck, iconChecks, iconCornerUpLeft, iconFile, iconLoader2, iconRefresh, iconTrash, iconX, InlineIcon } from "nexus-icons";
|
|
3
|
+
import { iconAlertCircle, iconBell, iconCheck, iconChecks, iconCornerUpLeft, iconFile, iconLoader2, iconRefresh, iconTrash, iconX, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
5
|
import { chatDeliveryState, chatFileSize, chatTimeLabel, sameChatId, CHAT_DELIVERY_LABELS } from "../../Helpers/ChatHelpers.js";
|
|
6
6
|
import { markdownToPlainText } from "../../Helpers/MarkdownHelpers.js";
|
|
@@ -57,6 +57,14 @@ export function ChatQuote({ message, onOpen }) {
|
|
|
57
57
|
export function ChatSystemNote({ message, time }) {
|
|
58
58
|
return (_jsxs("div", { className: "nx-chat-note", children: [_jsx("span", { children: markdownToPlainText(message.body) }), _jsx("time", { dateTime: message.createdAt, children: chatTimeLabel(message.createdAt, time) })] }));
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A notification (`kind: "notification"`): what an app wrote about an event, under the name of the person who acted -
|
|
62
|
+
* "Approved at Supervisor. Now with Accounts." It is a quiet line across the thread, not a bubble on a side: the bell,
|
|
63
|
+
* who acted and when, and the words under them, so a thread of replies reads around its events.
|
|
64
|
+
*/
|
|
65
|
+
export function ChatEventNote({ message, time }) {
|
|
66
|
+
return (_jsxs("div", { className: "nx-chat-event", children: [_jsx(InlineIcon, { icon: iconBell, className: "nx-chat-event__icon" }), _jsxs("div", { className: "nx-chat-event__text", children: [_jsxs("span", { className: "nx-chat-event__meta", children: [_jsx("span", { className: "nx-chat-event__author", children: message.authorName || "Unknown" }), _jsx("time", { dateTime: message.createdAt, children: chatTimeLabel(message.createdAt, time) })] }), message.body && _jsx(MarkdownView, { className: "nx-chat-event__body", value: message.body })] })] }));
|
|
67
|
+
}
|
|
60
68
|
/**
|
|
61
69
|
* One message. Its own messages sit on the right in the primary tint, everyone else's on the left on a surface. The
|
|
62
70
|
* actions are one row of icon buttons that appear on hover or keyboard focus — and never move the message, so a long
|
|
@@ -86,9 +94,12 @@ export function ChatParticipantList({ participants, userId, empty = "Nobody else
|
|
|
86
94
|
export function ChatEmpty({ message = "No messages yet.", hint }) {
|
|
87
95
|
return (_jsxs("div", { className: "nx-chat-empty", children: [_jsx("p", { children: message }), hint && _jsx("p", { className: "nx-chat-empty__hint", children: hint })] }));
|
|
88
96
|
}
|
|
89
|
-
/**
|
|
90
|
-
|
|
91
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Something that did not load, with a way to try again. `flat`: its tint without an edge, for a failure that already
|
|
99
|
+
* sits in a framed panel (a dashboard widget).
|
|
100
|
+
*/
|
|
101
|
+
export function ChatFailure({ message, onRetry, flat = false }) {
|
|
102
|
+
return (_jsxs("div", { className: flat ? "nx-chat-failure nx-chat-failure--flat" : "nx-chat-failure", role: "alert", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: message }), onRetry && (_jsxs("button", { type: "button", onClick: onRetry, children: [_jsx(InlineIcon, { icon: iconRefresh }), " Try again"] }))] }));
|
|
92
103
|
}
|
|
93
104
|
/** Messages on their way in. */
|
|
94
105
|
export function ChatLoading({ label = "Loading…" }) {
|