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.
Files changed (163) hide show
  1. package/CHANGELOG.md +795 -0
  2. package/README.md +220 -14
  3. package/dist/Client.Index.d.ts +13 -0
  4. package/dist/Client.Index.js +13 -0
  5. package/dist/Components/Chats/ChatComposer.d.ts +7 -1
  6. package/dist/Components/Chats/ChatComposer.js +9 -6
  7. package/dist/Components/Chats/ChatParts.d.ts +15 -2
  8. package/dist/Components/Chats/ChatParts.js +15 -4
  9. package/dist/Components/Chats/ChatPopup.d.ts +46 -0
  10. package/dist/Components/Chats/ChatPopup.js +34 -0
  11. package/dist/Components/Chats/ChatThread.d.ts +9 -1
  12. package/dist/Components/Chats/ChatThread.js +54 -6
  13. package/dist/Components/Dashboards/DashboardBoard.d.ts +62 -0
  14. package/dist/Components/Dashboards/DashboardBoard.js +594 -0
  15. package/dist/Components/Dashboards/DashboardWidgets.d.ts +155 -0
  16. package/dist/Components/Dashboards/DashboardWidgets.js +159 -0
  17. package/dist/Components/Dashboards/WidgetFrame.d.ts +51 -0
  18. package/dist/Components/Dashboards/WidgetFrame.js +45 -0
  19. package/dist/Components/Documents/Button.d.ts +6 -1
  20. package/dist/Components/Documents/Button.js +2 -2
  21. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  22. package/dist/Components/Documents/TabButtons.js +5 -4
  23. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  24. package/dist/Components/Domains/DomainCard.js +182 -0
  25. package/dist/Components/Forms/ApiForm.js +5 -1
  26. package/dist/Components/Forms/Crud.js +72 -24
  27. package/dist/Components/Forms/CrudActions.d.ts +5 -4
  28. package/dist/Components/Forms/CrudActions.js +42 -12
  29. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  30. package/dist/Components/Forms/CrudHistory.js +14 -6
  31. package/dist/Components/Forms/CrudParts.d.ts +11 -4
  32. package/dist/Components/Forms/CrudParts.js +76 -10
  33. package/dist/Components/Forms/CrudRowForms.js +65 -5
  34. package/dist/Components/Forms/CrudTree.js +7 -7
  35. package/dist/Components/Forms/CrudWorkspace.d.ts +15 -5
  36. package/dist/Components/Forms/CrudWorkspace.js +19 -11
  37. package/dist/Components/Forms/Form.d.ts +3 -2
  38. package/dist/Components/Forms/Form.js +33 -26
  39. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  40. package/dist/Components/Forms/FormQuestion.js +28 -0
  41. package/dist/Components/Forms/SubmitForm.js +2 -15
  42. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  43. package/dist/Components/Inputs/ComputedInput.js +179 -0
  44. package/dist/Components/Inputs/Dropdown.js +4 -2
  45. package/dist/Components/Inputs/GroupForm.js +3 -2
  46. package/dist/Components/Inputs/InputRenderer.d.ts +5 -1
  47. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  48. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  49. package/dist/Components/Inputs/PictureInput.js +8 -3
  50. package/dist/Components/Inputs/RowsInput.d.ts +14 -1
  51. package/dist/Components/Inputs/RowsInput.js +78 -5
  52. package/dist/Components/Inputs/TabularForm.js +16 -3
  53. package/dist/Components/Inputs/TreeView.d.ts +24 -1
  54. package/dist/Components/Inputs/TreeView.js +84 -7
  55. package/dist/Components/Layouts/Popup.d.ts +22 -2
  56. package/dist/Components/Layouts/Popup.js +108 -6
  57. package/dist/Components/Messages/MailClient.d.ts +50 -0
  58. package/dist/Components/Messages/MailClient.js +410 -0
  59. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  60. package/dist/Components/Messages/MailColumns.js +92 -0
  61. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  62. package/dist/Components/Messages/MailCompose.js +154 -0
  63. package/dist/Components/Messages/MailReader.d.ts +43 -0
  64. package/dist/Components/Messages/MailReader.js +207 -0
  65. package/dist/Components/Messages/MailThread.d.ts +71 -0
  66. package/dist/Components/Messages/MailThread.js +185 -0
  67. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  68. package/dist/Components/Messages/MessageBell.js +76 -0
  69. package/dist/Components/Viewers/DataTable.js +7 -5
  70. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  71. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  72. package/dist/Components/Viewers/DataTableParts.d.ts +3 -1
  73. package/dist/Components/Viewers/DataTableParts.js +2 -2
  74. package/dist/Components/Viewers/OwnerFiles.d.ts +57 -5
  75. package/dist/Components/Viewers/OwnerFiles.js +142 -31
  76. package/dist/Helpers/ApiModules.js +4 -0
  77. package/dist/Helpers/AuditEntries.d.ts +50 -0
  78. package/dist/Helpers/AuditEntries.js +96 -0
  79. package/dist/Helpers/ChatBackend.d.ts +8 -1
  80. package/dist/Helpers/ChatBackend.js +18 -0
  81. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  82. package/dist/Helpers/ChatHelpers.js +4 -1
  83. package/dist/Helpers/CrudColumns.d.ts +44 -6
  84. package/dist/Helpers/CrudColumns.js +63 -13
  85. package/dist/Helpers/CrudHelpers.d.ts +36 -1
  86. package/dist/Helpers/CrudHelpers.js +61 -0
  87. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  88. package/dist/Helpers/CrudPickerPlan.js +46 -0
  89. package/dist/Helpers/CrudRowForms.d.ts +18 -1
  90. package/dist/Helpers/CrudRowForms.js +35 -2
  91. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  92. package/dist/Helpers/DashboardHelpers.js +305 -0
  93. package/dist/Helpers/DomainCards.d.ts +140 -0
  94. package/dist/Helpers/DomainCards.js +370 -0
  95. package/dist/Helpers/FileAdapter.d.ts +82 -2
  96. package/dist/Helpers/FileAdapter.js +72 -1
  97. package/dist/Helpers/FormStore.d.ts +11 -1
  98. package/dist/Helpers/FormStore.js +66 -19
  99. package/dist/Helpers/InputConditions.d.ts +6 -0
  100. package/dist/Helpers/InputConditions.js +24 -0
  101. package/dist/Helpers/InputMemory.js +40 -30
  102. package/dist/Helpers/InputNarrowing.d.ts +38 -6
  103. package/dist/Helpers/InputNarrowing.js +102 -21
  104. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  105. package/dist/Helpers/MailClientHelpers.js +211 -0
  106. package/dist/Helpers/MessageBackend.d.ts +152 -0
  107. package/dist/Helpers/MessageBackend.js +329 -0
  108. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  109. package/dist/Helpers/MessageHelpers.js +573 -0
  110. package/dist/Helpers/MessageUnread.d.ts +95 -0
  111. package/dist/Helpers/MessageUnread.js +245 -0
  112. package/dist/Helpers/NexusDriveAdapter.d.ts +17 -1
  113. package/dist/Helpers/NexusDriveAdapter.js +32 -3
  114. package/dist/Helpers/NumberHelpers.d.ts +61 -0
  115. package/dist/Helpers/NumberHelpers.js +124 -8
  116. package/dist/Helpers/PagedPicker.d.ts +42 -0
  117. package/dist/Helpers/PagedPicker.js +85 -0
  118. package/dist/Helpers/RowsStore.d.ts +24 -0
  119. package/dist/Helpers/RowsStore.js +79 -10
  120. package/dist/Helpers/TableHelpers.d.ts +15 -2
  121. package/dist/Helpers/TableHelpers.js +28 -1
  122. package/dist/Helpers/TableStore.d.ts +7 -3
  123. package/dist/Helpers/TableStore.js +9 -4
  124. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  125. package/dist/Helpers/ToolbarFit.js +30 -0
  126. package/dist/Helpers/TreeStore.d.ts +75 -6
  127. package/dist/Helpers/TreeStore.js +182 -18
  128. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  129. package/dist/Interfaces/ApiInterfaces.js +10 -1
  130. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  131. package/dist/Interfaces/CrudInterfaces.d.ts +78 -5
  132. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  133. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  134. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  135. package/dist/Interfaces/DomainInterfaces.js +1 -0
  136. package/dist/Interfaces/FileInterfaces.d.ts +21 -0
  137. package/dist/Interfaces/FileInterfaces.js +2 -0
  138. package/dist/Interfaces/FormInterfaces.d.ts +78 -11
  139. package/dist/Interfaces/InputInterfaces.d.ts +12 -5
  140. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  141. package/dist/Interfaces/MailInterfaces.js +1 -0
  142. package/dist/Interfaces/TableInterfaces.d.ts +28 -2
  143. package/dist/Services/JoinSources.d.ts +47 -6
  144. package/dist/Services/JoinSources.js +68 -33
  145. package/dist/Services/MessageApi.d.ts +18 -0
  146. package/dist/Services/MessageApi.js +64 -0
  147. package/dist/Shared.Index.d.ts +12 -0
  148. package/dist/Shared.Index.js +12 -0
  149. package/package.json +7 -1
  150. package/src/Styles/Nexus.Button.css +29 -0
  151. package/src/Styles/Nexus.Chat.css +114 -22
  152. package/src/Styles/Nexus.Crud.css +119 -28
  153. package/src/Styles/Nexus.Dashboard.css +763 -0
  154. package/src/Styles/Nexus.Domain.css +384 -0
  155. package/src/Styles/Nexus.Editor.css +20 -0
  156. package/src/Styles/Nexus.Files.css +27 -0
  157. package/src/Styles/Nexus.Index.css +3 -0
  158. package/src/Styles/Nexus.Input.css +31 -0
  159. package/src/Styles/Nexus.Mail.css +1062 -0
  160. package/src/Styles/Nexus.Popup.css +58 -0
  161. package/src/Styles/Nexus.Table.css +1458 -1449
  162. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  163. package/src/Styles/Nexus.Tree.css +10 -0
@@ -0,0 +1,46 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ChatId, ChatMessage, ChatSource, ChatSummary } from "../../Interfaces/ChatInterfaces.ts";
3
+ import { type PopupSize, type PopupWindowState } from "../Layouts/Popup.tsx";
4
+ export interface ChatPopupProps {
5
+ open: boolean;
6
+ /** Asked to close: its close button, Esc inside it, or its bar on the dock. */
7
+ onClose: () => void;
8
+ /** Where the lines come from: a thread's discussion (`mailDiscussionSource(mail.threadSource(id))`), a chat's source. */
9
+ source: ChatSource;
10
+ /** The conversation: keys its lines, so two popups on one conversation never share a store by accident. */
11
+ conversationId: ChatId;
12
+ /** Who is reading: their lines sit on the right. */
13
+ userId: ChatId;
14
+ /** The title bar: the chat's name, "Discussion: Cable stock". */
15
+ title: ReactNode;
16
+ /** Muted after the title, and under it on the dock's bar: the thread it belongs to, the people. */
17
+ description?: ReactNode;
18
+ /** Its icon. Default the chat's. */
19
+ icon?: ReactNode;
20
+ /** What an empty conversation says. Default "No messages yet." */
21
+ emptyMessage?: string;
22
+ /** The reply box's placeholder. Default "Write a message…". */
23
+ placeholder?: string;
24
+ /** Where the unsent text is kept. Default `chat-popup:<conversation>:<reader>`. */
25
+ draftKey?: string;
26
+ /** Width: `xs` 360, `sm` 440 (default), `md` 560. */
27
+ size?: Extract<PopupSize, "xs" | "sm" | "md">;
28
+ /** Height, a CSS length. Default `30rem`, never taller than the window. */
29
+ height?: string;
30
+ /** The minimise button. Default true. */
31
+ minimizable?: boolean;
32
+ /** The window state, when the page controls it. */
33
+ windowState?: PopupWindowState;
34
+ onWindowStateChange?: (state: PopupWindowState) => void;
35
+ /** Every time the counts change, a read included: a list's row follows it. */
36
+ onSummary?: (summary: ChatSummary) => void;
37
+ /** After a line of the reader's own is saved. */
38
+ onSent?: (message: ChatMessage) => void;
39
+ /** Lines per page. Default 30. */
40
+ pageSize?: number;
41
+ /** Milliseconds between checks for new lines. 0 turns it off. Default 15,000. */
42
+ pollInterval?: number;
43
+ className?: string;
44
+ }
45
+ /** A conversation in a docked, minimisable window. */
46
+ export declare function ChatPopup({ open, onClose, source, conversationId, userId, title, description, icon, emptyMessage, placeholder, draftKey, size, height, minimizable, windowState, onWindowStateChange, onSummary, onSent, pageSize, pollInterval, className, }: ChatPopupProps): import("react").JSX.Element;
@@ -0,0 +1,34 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconMessageCircle, InlineIcon } from "nexus-icons";
4
+ import { useEffect, useMemo, useRef, useState } from "react";
5
+ import { useChat } from "../../Helpers/ChatHooks.js";
6
+ import { Popup } from "../Layouts/Popup.js";
7
+ import { ChatComposer } from "./ChatComposer.js";
8
+ import { ChatThread } from "./ChatThread.js";
9
+ /** A conversation in a docked, minimisable window. */
10
+ export function ChatPopup({ open, onClose, source, conversationId, userId, title, description, icon, emptyMessage, placeholder = "Write a message…", draftKey, size = "sm", height = "30rem", minimizable = true, windowState, onWindowStateChange, onSummary, onSent, pageSize, pollInterval, className, }) {
11
+ const [replyTo, setReplyTo] = useState(null);
12
+ // Its own place for the store: the thread drawn as mail behind it reads every line, the popup its discussion.
13
+ const scope = useMemo(() => ({ domain: "chat-popup", parentId: conversationId, userId }), [conversationId, userId]);
14
+ const { store, state } = useChat({ scope, conversationId, source, pageSize, pollInterval, enabled: open });
15
+ const latest = useRef({ onSummary, onSent });
16
+ latest.current = { onSummary, onSent };
17
+ useEffect(() => {
18
+ if (state.loaded)
19
+ latest.current.onSummary?.(state.summary);
20
+ }, [state.loaded, state.summary]);
21
+ // A line of the reader's own, saved: the page may count it (a Discuss button's number).
22
+ const known = useRef(new Set());
23
+ useEffect(() => {
24
+ for (const message of state.messages) {
25
+ const key = String(message.clientId ?? message.id);
26
+ if (message.status !== "sent" || known.current.has(key))
27
+ continue;
28
+ known.current.add(key);
29
+ if (message.clientId && String(message.authorId) === String(userId))
30
+ latest.current.onSent?.(message);
31
+ }
32
+ }, [state.messages, userId]);
33
+ return (_jsx(Popup, { open: open, onClose: onClose, title: title, description: description, icon: icon ?? _jsx(InlineIcon, { icon: iconMessageCircle }), size: size, height: height, docked: true, minimizable: minimizable, movable: false, reopen: "restore", flush: true, windowState: windowState, onWindowStateChange: onWindowStateChange, className: className, children: _jsxs("div", { className: "nx-chat-panel nx-chat-popup", children: [_jsx("div", { className: "nx-chat-panel__body", children: _jsx(ChatThread, { store: store, state: state, userId: userId, onReply: setReplyTo, emptyMessage: emptyMessage }) }), _jsx(ChatComposer, { store: store, state: state, draftKey: draftKey ?? `chat-popup:${String(conversationId)}:${String(userId)}`, placeholder: placeholder, replyTo: replyTo, onCancelReply: () => setReplyTo(null), autoFocus: true })] }) }));
34
+ }
@@ -1,3 +1,4 @@
1
+ import { type ReactNode } from "react";
1
2
  import { type ChatTimeOptions } from "../../Helpers/ChatHelpers.ts";
2
3
  import type { ChatSnapshot, ChatStore } from "../../Helpers/ChatStore.ts";
3
4
  import type { ChatId, ChatMessage } from "../../Interfaces/ChatInterfaces.ts";
@@ -16,7 +17,14 @@ export interface ChatThreadProps {
16
17
  marksRead?: boolean;
17
18
  /** Shown when there is not a single message. */
18
19
  emptyMessage?: string;
20
+ /**
21
+ * What the conversation opens with, above its first message and scrolling with it - a mail thread's subject, people
22
+ * and record - so a tall head is read in the thread's one scroll, never in a box of its own. With one, the thread opens
23
+ * at its top, or at the first unread line when that would be out of view, and older pages load from the "Earlier
24
+ * messages" button only, so the lead stays where the reader scrolled it.
25
+ */
26
+ lead?: ReactNode;
19
27
  className?: string;
20
28
  }
21
29
  /** The messages of a conversation, in days and runs, with the lines that break them up. */
22
- export declare function ChatThread({ store, state, userId, time, replies, onReply, marksRead, emptyMessage, className }: ChatThreadProps): import("react").JSX.Element;
30
+ export declare function ChatThread({ store, state, userId, time, replies, onReply, marksRead, emptyMessage, lead, className }: ChatThreadProps): import("react").JSX.Element;
@@ -7,7 +7,7 @@ import { cx } from "../../Helpers/ClassHelpers.js";
7
7
  import { chatBadgeText, chatMessageKey, groupChatMessages, sameChatId } from "../../Helpers/ChatHelpers.js";
8
8
  import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
9
9
  import { Button } from "../Documents/Button.js";
10
- import { ChatAvatar, ChatDayDivider, ChatEmpty, ChatFailure, ChatLoading, ChatMessageItem, ChatSystemNote, ChatUnreadLine } from "./ChatParts.js";
10
+ import { ChatAvatar, ChatDayDivider, ChatEmpty, ChatEventNote, ChatFailure, ChatLoading, ChatMessageItem, ChatSystemNote, ChatUnreadLine } from "./ChatParts.js";
11
11
  /*
12
12
  * The thread: the conversation itself, scrolling. It opens at the newest message, keeps itself at the bottom while the
13
13
  * reader is already there, and when they are not, it leaves the scroll alone and offers a button down to the newest
@@ -21,7 +21,7 @@ const EDGE = 32;
21
21
  /** How near the top loading the page above starts. */
22
22
  const TOP_EDGE = 120;
23
23
  /** The messages of a conversation, in days and runs, with the lines that break them up. */
24
- export function ChatThread({ store, state, userId, time, replies = true, onReply, marksRead = true, emptyMessage, className }) {
24
+ export function ChatThread({ store, state, userId, time, replies = true, onReply, marksRead = true, emptyMessage, lead, className }) {
25
25
  const preferences = useDatePreferences();
26
26
  const zone = useBrowserTimeZone();
27
27
  const settings = useMemo(() => ({ ...preferences, ...(zone && { timeZone: zone }), ...time }), [preferences, zone, time]);
@@ -32,6 +32,8 @@ export function ChatThread({ store, state, userId, time, replies = true, onReply
32
32
  const anchor = useRef(null);
33
33
  const lastKey = useRef(null);
34
34
  const count = useRef(0);
35
+ // With a lead, the first messages place the thread once: at its top, or at the first unread line.
36
+ const placed = useRef(false);
35
37
  const days = useMemo(() => groupChatMessages(state.messages, { userId, unreadFrom: state.unreadFrom, ...settings }), [state.messages, state.unreadFrom, userId, settings]);
36
38
  const byId = useMemo(() => new Map(state.messages.map(message => [String(message.id), message])), [state.messages]);
37
39
  const scrollToEnd = useCallback((smooth) => {
@@ -41,11 +43,44 @@ export function ChatThread({ store, state, userId, time, replies = true, onReply
41
43
  // Smooth only when the visitor and the system both allow motion; otherwise it jumps.
42
44
  list.scrollTo({ top: list.scrollHeight, behavior: smooth && smoothScrollAllowed() ? "smooth" : "auto" });
43
45
  }, []);
46
+ /** Whether the newest message is on screen: the reader is at the bottom. */
47
+ const measureBottom = useCallback(() => {
48
+ const list = listRef.current;
49
+ if (!list)
50
+ return;
51
+ const bottom = list.scrollHeight - list.scrollTop - list.clientHeight <= EDGE;
52
+ setAtBottom(bottom);
53
+ if (bottom)
54
+ setMissed(0);
55
+ }, []);
56
+ /**
57
+ * A thread with a lead opens at its top - the lead and the first messages - unless its first unread line would be out of
58
+ * view there; then that line opens at the top. The reader is at the bottom only if the newest message shows.
59
+ */
60
+ const placeFirst = useCallback(() => {
61
+ const list = listRef.current;
62
+ if (!list)
63
+ return;
64
+ const unread = list.querySelector(".nx-chat-unread-line");
65
+ const top = list.getBoundingClientRect().top;
66
+ const within = (element) => element.getBoundingClientRect().bottom - top + list.scrollTop;
67
+ const run = unread?.nextElementSibling instanceof HTMLElement ? unread.nextElementSibling : null;
68
+ list.scrollTop = unread && run && within(run) > list.clientHeight ? Math.max(0, within(unread) - unread.offsetHeight - 8) : 0;
69
+ measureBottom();
70
+ }, [measureBottom]);
44
71
  // New messages: stay at the bottom when already there, otherwise hold the place and count what came in.
45
72
  useLayoutEffect(() => {
46
73
  const list = listRef.current;
47
74
  if (!list)
48
75
  return;
76
+ if (lead && !placed.current && state.messages.length > 0 && !state.loading) {
77
+ placed.current = true;
78
+ count.current = state.messages.length;
79
+ const newest = state.messages[state.messages.length - 1];
80
+ lastKey.current = newest ? chatMessageKey(newest) : null;
81
+ placeFirst();
82
+ return;
83
+ }
49
84
  if (anchor.current) {
50
85
  // A page of older messages: the scroll keeps the message it was on, rather than jumping to the new top.
51
86
  list.scrollTop = anchor.current.top + (list.scrollHeight - anchor.current.height);
@@ -67,7 +102,18 @@ export function ChatThread({ store, state, userId, time, replies = true, onReply
67
102
  setMissed(value => value + 1);
68
103
  // `missed` is read, not depended on: a second message must not re-run the first one's scroll.
69
104
  // eslint-disable-next-line react-hooks/exhaustive-deps
70
- }, [state.messages, atBottom, userId, scrollToEnd]);
105
+ }, [state.messages, state.loading, atBottom, userId, scrollToEnd, lead, placeFirst]);
106
+ // A lead that grows (a record's card landing, its details unfolded) moves the newest message down: whether the reader is
107
+ // at the bottom is worked out again, so a line that arrives next does not pull the lead out of view.
108
+ const leadRef = useRef(null);
109
+ useLayoutEffect(() => {
110
+ const element = leadRef.current;
111
+ if (!element)
112
+ return;
113
+ const observer = new ResizeObserver(() => measureBottom());
114
+ observer.observe(element);
115
+ return () => observer.disconnect();
116
+ }, [lead !== undefined, measureBottom]); // eslint-disable-line react-hooks/exhaustive-deps
71
117
  // The conversation counts as read once its newest message has been on screen.
72
118
  useEffect(() => {
73
119
  if (!marksRead || !atBottom || state.loading || state.summary.unreadCount === 0)
@@ -82,13 +128,15 @@ export function ChatThread({ store, state, userId, time, replies = true, onReply
82
128
  setAtBottom(bottom);
83
129
  if (bottom)
84
130
  setMissed(0);
85
- if (list.scrollTop <= TOP_EDGE && state.hasMore && !state.loadingOlder) {
131
+ // Under a lead the top is the lead's: earlier pages come from their button, so the lead is not pushed away.
132
+ if (!lead && list.scrollTop <= TOP_EDGE && state.hasMore && !state.loadingOlder) {
86
133
  anchor.current = { height: list.scrollHeight, top: list.scrollTop };
87
134
  void store.loadOlder();
88
135
  }
89
- }, [state.hasMore, state.loadingOlder, store]);
136
+ }, [state.hasMore, state.loadingOlder, store, lead]);
90
137
  const quiet = !state.loading && state.messages.length === 0;
91
- return (_jsxs("div", { className: cx("nx-chat-thread", className), children: [_jsxs("div", { className: "nx-chat-thread__list", ref: listRef, onScroll: onScroll, role: "log", "aria-live": "polite", "aria-relevant": "additions", tabIndex: 0, children: [state.loadingOlder && _jsx(ChatLoading, { label: "Loading earlier messages\u2026" }), !state.loadingOlder && state.hasMore && (_jsx("div", { className: "nx-chat-thread__older", children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => void store.loadOlder(), children: "Earlier messages" }) })), state.loading && state.messages.length === 0 && _jsx(ChatLoading, { label: "Loading the conversation\u2026" }), state.error && _jsx(ChatFailure, { message: state.error, onRetry: () => void store.refresh() }), quiet && !state.error && _jsx(ChatEmpty, { message: emptyMessage ?? "No messages yet.", hint: _jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconMessageCircle }), " Write the first one below."] }) }), days.map(day => (_jsxs("section", { className: "nx-chat-thread__day", children: [_jsx(ChatDayDivider, { label: day.label }), day.runs.map((run, runIndex) => (_jsxs(Fragment, { children: [run.startsUnread && _jsx(ChatUnreadLine, { count: state.summary.unreadCount }), _jsx("div", { className: cx("nx-chat-run", run.mine && "nx-chat-run--mine", run.system && "nx-chat-run--system"), children: run.system ? (run.messages.map(message => _jsx(ChatSystemNote, { message: message, time: settings }, chatMessageKey(message)))) : (_jsxs(_Fragment, { children: [!run.mine && _jsx(ChatAvatarColumn, { name: run.authorName, src: run.authorAvatar }), _jsx("div", { className: "nx-chat-run__messages", children: run.messages.map((message, index) => (_jsx(ChatMessageItem, { message: message, mine: run.mine, first: index === 0, last: index === run.messages.length - 1, time: settings, participants: state.participants, userId: userId, quoted: message.replyToId ? byId.get(String(message.replyToId)) : undefined, onReply: replies && onReply ? onReply : undefined, onRemove: run.mine && state.features.remove ? message => void store.remove(message.id) : undefined, onRetry: store.retry, onDiscard: store.discard }, chatMessageKey(message)))) })] })) })] }, `${day.date}-${runIndex}`)))] }, day.date)))] }), !atBottom && (_jsxs("button", { type: "button", className: "nx-chat-thread__jump", onClick: () => scrollToEnd(true), "aria-label": missed > 0 ? `${missed} new, go to the newest` : "Go to the newest message", children: [_jsx(InlineIcon, { icon: iconArrowDown }), missed > 0 && _jsx("span", { className: "nx-chat-thread__jump-count", children: chatBadgeText(missed) })] }))] }));
138
+ const lines = (_jsxs(_Fragment, { children: [state.loadingOlder && _jsx(ChatLoading, { label: "Loading earlier messages\u2026" }), !state.loadingOlder && state.hasMore && (_jsx("div", { className: "nx-chat-thread__older", children: _jsx(Button, { size: "sm", variant: "ghost", onClick: () => void store.loadOlder(), children: "Earlier messages" }) })), state.loading && state.messages.length === 0 && _jsx(ChatLoading, { label: "Loading the conversation\u2026" }), state.error && _jsx(ChatFailure, { message: state.error, onRetry: () => void store.refresh() }), quiet && !state.error && _jsx(ChatEmpty, { message: emptyMessage ?? "No messages yet.", hint: _jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconMessageCircle }), " Write the first one below."] }) }), days.map(day => (_jsxs("section", { className: "nx-chat-thread__day", children: [_jsx(ChatDayDivider, { label: day.label }), day.runs.map((run, runIndex) => (_jsxs(Fragment, { children: [run.startsUnread && _jsx(ChatUnreadLine, { count: state.summary.unreadCount }), _jsx("div", { className: cx("nx-chat-run", run.mine && "nx-chat-run--mine", (run.system || run.notification) && "nx-chat-run--system"), children: run.system ? (run.messages.map(message => _jsx(ChatSystemNote, { message: message, time: settings }, chatMessageKey(message)))) : run.notification ? (run.messages.map(message => _jsx(ChatEventNote, { message: message, time: settings }, chatMessageKey(message)))) : (_jsxs(_Fragment, { children: [!run.mine && _jsx(ChatAvatarColumn, { name: run.authorName, src: run.authorAvatar }), _jsx("div", { className: "nx-chat-run__messages", children: run.messages.map((message, index) => (_jsx(ChatMessageItem, { message: message, mine: run.mine, first: index === 0, last: index === run.messages.length - 1, time: settings, participants: state.participants, userId: userId, quoted: message.replyToId ? byId.get(String(message.replyToId)) : undefined, onReply: replies && onReply ? onReply : undefined, onRemove: run.mine && state.features.remove ? message => void store.remove(message.id) : undefined, onRetry: store.retry, onDiscard: store.discard }, chatMessageKey(message)))) })] })) })] }, `${day.date}-${runIndex}`)))] }, day.date)))] }));
139
+ return (_jsxs("div", { className: cx("nx-chat-thread", className), children: [lead ? (_jsxs("div", { className: "nx-chat-thread__list", ref: listRef, onScroll: onScroll, tabIndex: 0, role: "region", "aria-label": "Thread", children: [_jsx("div", { ref: leadRef, className: "nx-chat-thread__lead", children: lead }), _jsx("div", { className: "nx-chat-thread__log", role: "log", "aria-live": "polite", "aria-relevant": "additions", children: lines })] })) : (_jsx("div", { className: "nx-chat-thread__list", ref: listRef, onScroll: onScroll, role: "log", "aria-live": "polite", "aria-relevant": "additions", tabIndex: 0, children: lines })), !atBottom && (_jsxs("button", { type: "button", className: "nx-chat-thread__jump", onClick: () => scrollToEnd(true), "aria-label": missed > 0 ? `${missed} new, go to the newest` : "Go to the newest message", children: [_jsx(InlineIcon, { icon: iconArrowDown }), missed > 0 && _jsx("span", { className: "nx-chat-thread__jump-count", children: chatBadgeText(missed) })] }))] }));
92
140
  }
93
141
  /** The avatar beside a run of somebody else's messages, kept out of the bubbles so they line up under each other. */
94
142
  function ChatAvatarColumn({ name, src }) {
@@ -0,0 +1,62 @@
1
+ import { type ReactNode } from "react";
2
+ import type { DashboardLayout, DashboardLayoutItem, DashboardSaveResult, DashboardVersion, WidgetDefinition, WidgetModuleLoader } from "../../Interfaces/DashboardInterfaces.ts";
3
+ export interface DashboardBoardProps {
4
+ /** The place this board is: `home`, or a module's dashboard (`message`, `staffing`). */
5
+ place: string;
6
+ /** The module whose `own` widgets may go here. Default: `place` (the Home has no module of that name, so none). */
7
+ placeModule?: string | null;
8
+ /** Every widget the app declares; the board draws those allowed here that the person may see. */
9
+ widgets: readonly WidgetDefinition<any>[];
10
+ /** Whether the person may see a widget now: the company holds its module, the person its key. Default: every widget. */
11
+ canSee?: (widget: WidgetDefinition<any>) => boolean;
12
+ /** The person's saved layout for this place, or `null` when they saved none (the default is drawn). */
13
+ layout?: DashboardLayout | null;
14
+ /** The saved layout's version, given back with a save so a stale one is refused. */
15
+ version?: DashboardVersion;
16
+ /** The layout drawn when the person has none, and what Reset goes back to: the company's for this place, else the code's. */
17
+ defaultLayout: DashboardLayout;
18
+ /**
19
+ * Saves the person's layout (the whole of it: widgets they cannot see now keep their places), with the version it was
20
+ * edited from. Answer `stale` with the saved layout when the version is old (the server's 409). Without it the board's
21
+ * edits last as long as the page.
22
+ */
23
+ onSave?: (layout: DashboardLayoutItem[], version: DashboardVersion) => Promise<DashboardSaveResult>;
24
+ /** Forgets the person's layout: Reset then Save. Without it, Reset saves the default as theirs through `onSave`. */
25
+ onReset?: (version: DashboardVersion) => Promise<DashboardSaveResult>;
26
+ /** Shows Customise. Default true. */
27
+ canCustomise?: boolean;
28
+ /** Offers "Save as the company's default" beside Save, with `onSaveCompanyDefault`. */
29
+ canSaveCompanyDefault?: boolean;
30
+ onSaveCompanyDefault?: (layout: DashboardLayoutItem[]) => Promise<DashboardSaveResult | void>;
31
+ /** The one call per module per draw, for widgets with `batch`. */
32
+ loadModule?: WidgetModuleLoader;
33
+ /** Data read with the page (on the server), by widget code: drawn at once, kept as if asked now. */
34
+ initialData?: Readonly<Record<string, unknown>>;
35
+ /** A module's name in the Add list. Default: the module's code with a capital. */
36
+ moduleLabel?: (module: string) => ReactNode;
37
+ /** Moves a link inside the board through the app's router (`router.push`). Default: the browser follows it. */
38
+ onNavigate?: (href: string) => void;
39
+ /** The board's heading. */
40
+ title?: ReactNode;
41
+ /** More controls in the board's bar, before Refresh and Customise. */
42
+ actions?: ReactNode;
43
+ /** How near the screen a widget starts to load, as a margin. Default `200px`. */
44
+ loadMargin?: string;
45
+ className?: string;
46
+ }
47
+ /**
48
+ * A dashboard of widgets on a 12-column grid, for one place (the Home, a module's dashboard). Each widget takes a size
49
+ * preset (S 3×1, M 6×2, L 6×4, W 12×2, T 12×4), the grid fills every gap (`grid-auto-flow: dense`), and a layout is
50
+ * an ordered list of `{ code, size }`; on a phone every widget is the full width, in order. Widgets load only when near
51
+ * the screen, each module asked once per draw (`loadModule`), kept for the page's life or their `refresh` seconds.
52
+ *
53
+ * Customise turns on edit mode: move by dragging a widget's handle or with the arrow keys on a focused widget, Enter or
54
+ * Space for its size menu (only its sizes), Delete to remove it; Add widget opens a drawer of the widgets this place
55
+ * may hold, by module; Save, Cancel, Reset to default. A save another window beat (`stale`) shows the layout saved
56
+ * there and says so.
57
+ *
58
+ * @example
59
+ * <DashboardBoard place="home" widgets={WIDGETS} canSee={canSee} layout={saved?.items} version={saved?.version}
60
+ * defaultLayout={HOME_DEFAULT} loadModule={loadWidgets} onSave={saveLayout} onReset={resetLayout} />
61
+ */
62
+ export declare function DashboardBoard({ place, placeModule, widgets, canSee, layout, version, defaultLayout, onSave, onReset, canCustomise, canSaveCompanyDefault, onSaveCompanyDefault, loadModule, initialData, moduleLabel, onNavigate, title, actions, loadMargin, className, }: DashboardBoardProps): import("react").JSX.Element;