nexus-shared 3.2.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 (271) hide show
  1. package/CHANGELOG.md +2359 -0
  2. package/README.md +1911 -1123
  3. package/dist/Client.Index.d.ts +27 -0
  4. package/dist/Client.Index.js +27 -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/DocumentFrame.d.ts +50 -0
  22. package/dist/Components/Documents/DocumentFrame.js +83 -0
  23. package/dist/Components/Documents/Menu.d.ts +4 -0
  24. package/dist/Components/Documents/Menu.js +40 -24
  25. package/dist/Components/Documents/PrintDocument.d.ts +95 -0
  26. package/dist/Components/Documents/PrintDocument.js +62 -0
  27. package/dist/Components/Documents/TabButtons.d.ts +6 -0
  28. package/dist/Components/Documents/TabButtons.js +5 -4
  29. package/dist/Components/Documents/Tabs.d.ts +59 -0
  30. package/dist/Components/Documents/Tabs.js +70 -0
  31. package/dist/Components/Domains/DomainCard.d.ts +98 -0
  32. package/dist/Components/Domains/DomainCard.js +182 -0
  33. package/dist/Components/Forms/ApiForm.js +24 -3
  34. package/dist/Components/Forms/Crud.d.ts +2 -2
  35. package/dist/Components/Forms/Crud.js +628 -247
  36. package/dist/Components/Forms/CrudActions.d.ts +137 -0
  37. package/dist/Components/Forms/CrudActions.js +214 -0
  38. package/dist/Components/Forms/CrudHistory.d.ts +6 -1
  39. package/dist/Components/Forms/CrudHistory.js +14 -6
  40. package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
  41. package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
  42. package/dist/Components/Forms/CrudParts.d.ts +37 -2
  43. package/dist/Components/Forms/CrudParts.js +123 -7
  44. package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
  45. package/dist/Components/Forms/CrudRowForms.js +174 -0
  46. package/dist/Components/Forms/CrudScopes.d.ts +28 -0
  47. package/dist/Components/Forms/CrudScopes.js +76 -0
  48. package/dist/Components/Forms/CrudTree.d.ts +90 -0
  49. package/dist/Components/Forms/CrudTree.js +363 -0
  50. package/dist/Components/Forms/CrudWorkspace.d.ts +73 -0
  51. package/dist/Components/Forms/CrudWorkspace.js +177 -0
  52. package/dist/Components/Forms/ExcelImport.js +3 -3
  53. package/dist/Components/Forms/Form.d.ts +6 -5
  54. package/dist/Components/Forms/Form.js +134 -21
  55. package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
  56. package/dist/Components/Forms/FormLeaveGuard.js +65 -0
  57. package/dist/Components/Forms/FormQuestion.d.ts +6 -0
  58. package/dist/Components/Forms/FormQuestion.js +28 -0
  59. package/dist/Components/Forms/RecordFlows.d.ts +50 -0
  60. package/dist/Components/Forms/RecordFlows.js +282 -0
  61. package/dist/Components/Forms/SubmitForm.d.ts +37 -2
  62. package/dist/Components/Forms/SubmitForm.js +182 -10
  63. package/dist/Components/Inputs/CheckBox.js +1 -1
  64. package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
  65. package/dist/Components/Inputs/ChoiceField.js +28 -2
  66. package/dist/Components/Inputs/ComputedInput.d.ts +51 -0
  67. package/dist/Components/Inputs/ComputedInput.js +179 -0
  68. package/dist/Components/Inputs/Dropdown.d.ts +13 -0
  69. package/dist/Components/Inputs/Dropdown.js +8 -6
  70. package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
  71. package/dist/Components/Inputs/DropdownParts.js +16 -4
  72. package/dist/Components/Inputs/Field.d.ts +7 -1
  73. package/dist/Components/Inputs/Field.js +2 -2
  74. package/dist/Components/Inputs/GroupForm.d.ts +6 -6
  75. package/dist/Components/Inputs/GroupForm.js +12 -12
  76. package/dist/Components/Inputs/InputRenderer.d.ts +13 -6
  77. package/dist/Components/Inputs/InputRenderer.js +37 -32
  78. package/dist/Components/Inputs/MarkdownEditor.d.ts +6 -0
  79. package/dist/Components/Inputs/MarkdownEditor.js +32 -6
  80. package/dist/Components/Inputs/PictureInput.d.ts +41 -0
  81. package/dist/Components/Inputs/PictureInput.js +75 -0
  82. package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
  83. package/dist/Components/Inputs/RadioGroup.js +8 -3
  84. package/dist/Components/Inputs/RowsInput.d.ts +15 -2
  85. package/dist/Components/Inputs/RowsInput.js +96 -8
  86. package/dist/Components/Inputs/TabularForm.d.ts +5 -5
  87. package/dist/Components/Inputs/TabularForm.js +45 -32
  88. package/dist/Components/Inputs/TextArea.d.ts +6 -0
  89. package/dist/Components/Inputs/TextArea.js +11 -2
  90. package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
  91. package/dist/Components/Inputs/TextBoxTypes.js +17 -0
  92. package/dist/Components/Inputs/TreeView.d.ts +76 -2
  93. package/dist/Components/Inputs/TreeView.js +227 -16
  94. package/dist/Components/Layouts/MessageBox.d.ts +15 -2
  95. package/dist/Components/Layouts/MessageBox.js +56 -7
  96. package/dist/Components/Layouts/Popup.d.ts +34 -7
  97. package/dist/Components/Layouts/Popup.js +143 -13
  98. package/dist/Components/Layouts/PopupDock.d.ts +3 -1
  99. package/dist/Components/Layouts/PopupDock.js +2 -1
  100. package/dist/Components/Layouts/PopupHost.js +5 -4
  101. package/dist/Components/Messages/MailClient.d.ts +50 -0
  102. package/dist/Components/Messages/MailClient.js +410 -0
  103. package/dist/Components/Messages/MailColumns.d.ts +21 -0
  104. package/dist/Components/Messages/MailColumns.js +92 -0
  105. package/dist/Components/Messages/MailCompose.d.ts +46 -0
  106. package/dist/Components/Messages/MailCompose.js +154 -0
  107. package/dist/Components/Messages/MailReader.d.ts +43 -0
  108. package/dist/Components/Messages/MailReader.js +207 -0
  109. package/dist/Components/Messages/MailThread.d.ts +71 -0
  110. package/dist/Components/Messages/MailThread.js +185 -0
  111. package/dist/Components/Messages/MessageBell.d.ts +41 -0
  112. package/dist/Components/Messages/MessageBell.js +76 -0
  113. package/dist/Components/Viewers/DataTable.js +153 -49
  114. package/dist/Components/Viewers/DataTableColumns.d.ts +2 -2
  115. package/dist/Components/Viewers/DataTableColumns.js +2 -1
  116. package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
  117. package/dist/Components/Viewers/DataTableFilters.js +17 -2
  118. package/dist/Components/Viewers/DataTableParts.d.ts +23 -5
  119. package/dist/Components/Viewers/DataTableParts.js +26 -14
  120. package/dist/Components/Viewers/OwnerFiles.d.ts +227 -0
  121. package/dist/Components/Viewers/OwnerFiles.js +700 -0
  122. package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
  123. package/dist/Components/Viewers/QrCodeImage.js +15 -0
  124. package/dist/Components/Viewers/RecordCards.d.ts +16 -0
  125. package/dist/Components/Viewers/RecordCards.js +509 -0
  126. package/dist/Helpers/ApiModules.js +4 -0
  127. package/dist/Helpers/AuditEntries.d.ts +50 -0
  128. package/dist/Helpers/AuditEntries.js +96 -0
  129. package/dist/Helpers/BusyHooks.d.ts +6 -0
  130. package/dist/Helpers/BusyHooks.js +39 -0
  131. package/dist/Helpers/ChatBackend.d.ts +8 -1
  132. package/dist/Helpers/ChatBackend.js +18 -0
  133. package/dist/Helpers/ChatHelpers.d.ts +2 -0
  134. package/dist/Helpers/ChatHelpers.js +4 -1
  135. package/dist/Helpers/CrudBackend.d.ts +20 -2
  136. package/dist/Helpers/CrudBackend.js +46 -2
  137. package/dist/Helpers/CrudColumns.d.ts +91 -6
  138. package/dist/Helpers/CrudColumns.js +148 -7
  139. package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
  140. package/dist/Helpers/CrudDetailsTabs.js +126 -0
  141. package/dist/Helpers/CrudHelpers.d.ts +116 -2
  142. package/dist/Helpers/CrudHelpers.js +256 -0
  143. package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
  144. package/dist/Helpers/CrudNarrowPick.js +157 -0
  145. package/dist/Helpers/CrudPickerPlan.d.ts +58 -0
  146. package/dist/Helpers/CrudPickerPlan.js +46 -0
  147. package/dist/Helpers/CrudRowForms.d.ts +32 -0
  148. package/dist/Helpers/CrudRowForms.js +76 -0
  149. package/dist/Helpers/CrudScopes.d.ts +32 -0
  150. package/dist/Helpers/CrudScopes.js +79 -0
  151. package/dist/Helpers/DashboardHelpers.d.ts +115 -0
  152. package/dist/Helpers/DashboardHelpers.js +305 -0
  153. package/dist/Helpers/DomainCards.d.ts +140 -0
  154. package/dist/Helpers/DomainCards.js +370 -0
  155. package/dist/Helpers/DropdownHelpers.d.ts +3 -1
  156. package/dist/Helpers/DropdownHelpers.js +6 -2
  157. package/dist/Helpers/ExcelHelpers.d.ts +1 -1
  158. package/dist/Helpers/ExcelHelpers.js +12 -11
  159. package/dist/Helpers/FileAdapter.d.ts +158 -0
  160. package/dist/Helpers/FileAdapter.js +173 -0
  161. package/dist/Helpers/FilePrimaries.d.ts +48 -0
  162. package/dist/Helpers/FilePrimaries.js +138 -0
  163. package/dist/Helpers/FormConfirm.d.ts +32 -0
  164. package/dist/Helpers/FormConfirm.js +40 -0
  165. package/dist/Helpers/FormStore.d.ts +100 -5
  166. package/dist/Helpers/FormStore.js +368 -25
  167. package/dist/Helpers/InputConditions.d.ts +56 -0
  168. package/dist/Helpers/InputConditions.js +192 -0
  169. package/dist/Helpers/InputMemory.d.ts +106 -0
  170. package/dist/Helpers/InputMemory.js +395 -0
  171. package/dist/Helpers/InputNarrowing.d.ts +50 -0
  172. package/dist/Helpers/InputNarrowing.js +148 -0
  173. package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
  174. package/dist/Helpers/InputParamsHelpers.js +157 -74
  175. package/dist/Helpers/MailClientHelpers.d.ts +96 -0
  176. package/dist/Helpers/MailClientHelpers.js +211 -0
  177. package/dist/Helpers/MessageBackend.d.ts +152 -0
  178. package/dist/Helpers/MessageBackend.js +329 -0
  179. package/dist/Helpers/MessageHelpers.d.ts +160 -0
  180. package/dist/Helpers/MessageHelpers.js +573 -0
  181. package/dist/Helpers/MessageUnread.d.ts +95 -0
  182. package/dist/Helpers/MessageUnread.js +245 -0
  183. package/dist/Helpers/NexusDriveAdapter.d.ts +71 -0
  184. package/dist/Helpers/NexusDriveAdapter.js +236 -0
  185. package/dist/Helpers/NumberHelpers.d.ts +156 -0
  186. package/dist/Helpers/NumberHelpers.js +242 -0
  187. package/dist/Helpers/PagedPicker.d.ts +42 -0
  188. package/dist/Helpers/PagedPicker.js +85 -0
  189. package/dist/Helpers/Permissions.js +2 -1
  190. package/dist/Helpers/QrCode.d.ts +26 -0
  191. package/dist/Helpers/QrCode.js +463 -0
  192. package/dist/Helpers/RecordCards.d.ts +205 -0
  193. package/dist/Helpers/RecordCards.js +378 -0
  194. package/dist/Helpers/RequestCache.d.ts +14 -2
  195. package/dist/Helpers/RequestCache.js +22 -3
  196. package/dist/Helpers/RowsStore.d.ts +61 -2
  197. package/dist/Helpers/RowsStore.js +203 -16
  198. package/dist/Helpers/ServerPreview.d.ts +69 -0
  199. package/dist/Helpers/ServerPreview.js +99 -0
  200. package/dist/Helpers/TableColumns.js +4 -3
  201. package/dist/Helpers/TableExport.d.ts +7 -1
  202. package/dist/Helpers/TableExport.js +9 -3
  203. package/dist/Helpers/TableFilters.d.ts +1 -1
  204. package/dist/Helpers/TableFilters.js +24 -23
  205. package/dist/Helpers/TableHelpers.d.ts +38 -4
  206. package/dist/Helpers/TableHelpers.js +78 -10
  207. package/dist/Helpers/TableJoins.d.ts +13 -0
  208. package/dist/Helpers/TableJoins.js +34 -0
  209. package/dist/Helpers/TableStore.d.ts +28 -4
  210. package/dist/Helpers/TableStore.js +73 -11
  211. package/dist/Helpers/ToolbarFit.d.ts +23 -0
  212. package/dist/Helpers/ToolbarFit.js +30 -0
  213. package/dist/Helpers/TreeStore.d.ts +156 -6
  214. package/dist/Helpers/TreeStore.js +547 -18
  215. package/dist/Interfaces/ApiInterfaces.d.ts +10 -1
  216. package/dist/Interfaces/ApiInterfaces.js +10 -1
  217. package/dist/Interfaces/ChatInterfaces.d.ts +22 -0
  218. package/dist/Interfaces/CrudInterfaces.d.ts +539 -6
  219. package/dist/Interfaces/DashboardInterfaces.d.ts +123 -0
  220. package/dist/Interfaces/DashboardInterfaces.js +1 -0
  221. package/dist/Interfaces/DomainInterfaces.d.ts +145 -0
  222. package/dist/Interfaces/DomainInterfaces.js +1 -0
  223. package/dist/Interfaces/FileInterfaces.d.ts +183 -0
  224. package/dist/Interfaces/FileInterfaces.js +6 -0
  225. package/dist/Interfaces/FormInterfaces.d.ts +460 -32
  226. package/dist/Interfaces/FormInterfaces.js +79 -1
  227. package/dist/Interfaces/InputInterfaces.d.ts +22 -3
  228. package/dist/Interfaces/MailInterfaces.d.ts +192 -0
  229. package/dist/Interfaces/MailInterfaces.js +1 -0
  230. package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
  231. package/dist/Interfaces/RecordInterfaces.js +1 -0
  232. package/dist/Interfaces/TableInterfaces.d.ts +102 -3
  233. package/dist/Services/ExcelApi.js +5 -0
  234. package/dist/Services/FileApi.d.ts +13 -0
  235. package/dist/Services/FileApi.js +29 -0
  236. package/dist/Services/ImageEditorLoader.d.ts +14 -0
  237. package/dist/Services/ImageEditorLoader.js +51 -0
  238. package/dist/Services/JoinSources.d.ts +79 -6
  239. package/dist/Services/JoinSources.js +132 -21
  240. package/dist/Services/MessageApi.d.ts +18 -0
  241. package/dist/Services/MessageApi.js +64 -0
  242. package/dist/Services/ModuleImport.d.ts +7 -0
  243. package/dist/Services/ModuleImport.js +22 -0
  244. package/dist/Shared.Index.d.ts +28 -0
  245. package/dist/Shared.Index.js +28 -0
  246. package/package.json +13 -2
  247. package/src/Styles/Nexus.Avatar.css +6 -5
  248. package/src/Styles/Nexus.Button.css +39 -4
  249. package/src/Styles/Nexus.Chat.css +114 -22
  250. package/src/Styles/Nexus.Choice.css +16 -0
  251. package/src/Styles/Nexus.Crud.css +486 -21
  252. package/src/Styles/Nexus.Dashboard.css +763 -0
  253. package/src/Styles/Nexus.Domain.css +384 -0
  254. package/src/Styles/Nexus.Dropdown.css +8 -0
  255. package/src/Styles/Nexus.Editor.css +20 -0
  256. package/src/Styles/Nexus.Files.css +437 -0
  257. package/src/Styles/Nexus.Form.css +5 -2
  258. package/src/Styles/Nexus.Index.css +7 -0
  259. package/src/Styles/Nexus.Input.css +73 -0
  260. package/src/Styles/Nexus.Mail.css +1062 -0
  261. package/src/Styles/Nexus.Menu.css +10 -1
  262. package/src/Styles/Nexus.Popup.css +135 -1
  263. package/src/Styles/Nexus.Print.css +288 -0
  264. package/src/Styles/Nexus.QrCode.css +12 -0
  265. package/src/Styles/Nexus.Record.css +430 -0
  266. package/src/Styles/Nexus.Tab.Buttons.css +87 -22
  267. package/src/Styles/Nexus.Table.css +1458 -1387
  268. package/src/Styles/Nexus.Theme.Picker.css +1 -0
  269. package/src/Styles/Nexus.Themes.css +12 -6
  270. package/src/Styles/Nexus.Tokens.css +8 -3
  271. package/src/Styles/Nexus.Tree.css +41 -1
@@ -0,0 +1,155 @@
1
+ import { type ReactNode } from "react";
2
+ import { type CalendarMark } from "../../Helpers/DashboardHelpers.ts";
3
+ import type { CalendarType } from "../../Interfaces/DateInterfaces.ts";
4
+ import type { Tone } from "../../Interfaces/TypeInterfaces.ts";
5
+ export interface StatWidgetItem {
6
+ /** What the figure counts: "Unread". */
7
+ label: ReactNode;
8
+ /** The figure, written as the page wants it: 12, "NPR 4.2M", "92%". */
9
+ value: ReactNode;
10
+ /** A line under it: what it is compared with, or made of. */
11
+ note?: ReactNode;
12
+ /** The page that lists what it counts: the whole figure is the link. */
13
+ href?: string;
14
+ /** A dot of this tone before the label (a second cue beside the words, never the only one). */
15
+ tone?: Tone;
16
+ }
17
+ export interface StatWidgetProps {
18
+ items: readonly StatWidgetItem[];
19
+ className?: string;
20
+ }
21
+ /**
22
+ * One or several figures in a widget, side by side as they fit and wrapping as the widget narrows; each may link to
23
+ * the list behind it. Plain on the widget's panel: no card in the card.
24
+ *
25
+ * @example <StatWidget items={[{ label: "Unread", value: 4, href: "/admin/message?list=2" }, { label: "Sent", value: 31 }]} />
26
+ */
27
+ export declare function StatWidget({ items, className }: StatWidgetProps): import("react").JSX.Element;
28
+ export interface ListWidgetRow {
29
+ id: string | number;
30
+ /** The row's words: a subject, a person, a request. */
31
+ title: ReactNode;
32
+ /** A second line, muted: who, what, from where. */
33
+ secondary?: ReactNode;
34
+ /** At the end: a time, a date, already written. */
35
+ time?: ReactNode;
36
+ /** A tag at the end: a type, a state. */
37
+ chip?: {
38
+ label: ReactNode;
39
+ tone?: Tone;
40
+ };
41
+ /** A person's picture (initials without one). */
42
+ avatar?: {
43
+ name: string;
44
+ src?: string | null;
45
+ };
46
+ /** An icon in the picture's place. */
47
+ icon?: ReactNode;
48
+ /** The row opens this page. */
49
+ href?: string;
50
+ /** Strong title: not read yet. */
51
+ unread?: boolean;
52
+ }
53
+ export interface ListWidgetProps {
54
+ rows: readonly ListWidgetRow[];
55
+ /** How many there are in all, when `rows` are the first of them (a count the server gave). Default: the rows'. */
56
+ total?: number;
57
+ /**
58
+ * A line under the rows linking to the whole list, shown while some are not: its label is "N more" by default, or
59
+ * the page's words (a function is given N).
60
+ */
61
+ more?: {
62
+ href: string;
63
+ label?: ReactNode | ((hidden: number) => ReactNode);
64
+ };
65
+ /** Shows only the rows that fit whole in the widget's height, never a half row. Default true. */
66
+ fit?: boolean;
67
+ className?: string;
68
+ }
69
+ /**
70
+ * A few rows in a widget: a picture or an icon, the words and a muted second line, a time and a tag at the end. A row
71
+ * with `href` is a link. Rows are one height, and only those that fit whole show (`fit`), so the widget's size says how
72
+ * many: the rest are counted on the more line.
73
+ *
74
+ * @example <ListWidget rows={mails.map(m => ({ id: m.id, title: m.subject, secondary: m.from, time: m.when, avatar: { name: m.from } }))} />
75
+ */
76
+ export declare function ListWidget({ rows, total, more, fit, className }: ListWidgetProps): import("react").JSX.Element;
77
+ /** A kind of calendar mark: its name in the legend and its tone. */
78
+ export interface CalendarMarkKind {
79
+ label: string;
80
+ tone: Tone;
81
+ }
82
+ /** The kinds a calendar widget knows without being told: holidays, leave, events. */
83
+ export declare const CALENDAR_MARK_KINDS: Readonly<Record<string, CalendarMarkKind>>;
84
+ export interface CalendarWidgetProps {
85
+ marks: readonly CalendarMark[];
86
+ /** The kinds of mark and their tones, added to (or replacing) holiday, leave and event. */
87
+ kinds?: Readonly<Record<string, CalendarMarkKind>>;
88
+ /** The calendar shown: `ad` or `bs`. Default: the person's setting. */
89
+ calendar?: CalendarType;
90
+ /** Today, an ISO date. Default: today where the browser is. */
91
+ today?: string;
92
+ /** After the month shown changes, with its first day: read that month's marks. */
93
+ onMonthChange?: (firstDay: string) => void;
94
+ /** First column of the week: 0 Sunday (default), 1 Monday. */
95
+ weekStart?: number;
96
+ className?: string;
97
+ }
98
+ /**
99
+ * A month in a widget with typed marks on its days - holidays, leave, events, or the app's own kinds - a dot of each
100
+ * kind's tone under the day's number, today marked, a legend counting the month's marks by kind, and the picked day's
101
+ * marks listed beside the month (under it when the widget is narrow). In the English or the Nepali calendar.
102
+ *
103
+ * @example <CalendarWidget marks={[{ date: "2026-11-12", kind: "holiday", label: "Tihar" }, { date: "2026-11-16", until: "2026-11-18", kind: "leave", label: "Sita Rai" }]} />
104
+ */
105
+ export declare function CalendarWidget({ marks, kinds, calendar, today, onMonthChange, weekStart, className }: CalendarWidgetProps): import("react").JSX.Element;
106
+ /** A release note: `released` (out now) or `coming-soon` (announced). */
107
+ export interface NewsItem {
108
+ id: string | number;
109
+ title: ReactNode;
110
+ /** "4.2.0". */
111
+ version?: string;
112
+ /** The release day, an ISO date. */
113
+ date?: string;
114
+ kind?: "released" | "coming-soon";
115
+ /** Which application: "Nexus Web". */
116
+ application?: ReactNode;
117
+ /** The notes, in Markdown: the first lines show. */
118
+ notes?: string;
119
+ /** The whole note's page: "Read more". */
120
+ href?: string;
121
+ }
122
+ export interface NewsWidgetProps {
123
+ items: readonly NewsItem[];
124
+ /** "Read more" without `href`: open the note in a popup of the page's own. */
125
+ onReadMore?: (item: NewsItem) => void;
126
+ /** Lines of notes shown. Default 3. */
127
+ excerptLines?: number;
128
+ className?: string;
129
+ }
130
+ /**
131
+ * Release notes in a widget, newest first as given: the title, the version and the kind (Released, Coming soon) as
132
+ * tags, the day, the first lines of the Markdown notes and "Read more".
133
+ *
134
+ * @example <NewsWidget items={releases} onReadMore={item => openNote(item)} />
135
+ */
136
+ export declare function NewsWidget({ items, onReadMore, excerptLines, className }: NewsWidgetProps): import("react").JSX.Element;
137
+ export interface VersionWidgetProps {
138
+ /** The application: "Nexus Web". */
139
+ name: ReactNode;
140
+ /** "4.2.0". */
141
+ version: string;
142
+ /** When it was built, an ISO date or date and time. */
143
+ build?: string;
144
+ /** A tag beside the version: "Preview", "Stable". */
145
+ channel?: ReactNode;
146
+ /** Its release notes. */
147
+ href?: string;
148
+ className?: string;
149
+ }
150
+ /**
151
+ * The application's name, version and build day in a widget: what a person reads out when they ask for help.
152
+ *
153
+ * @example <VersionWidget name="Nexus Web" version={BUILD.version} build={BUILD.time} href="/admin/tenant/releases" />
154
+ */
155
+ export declare function VersionWidget({ name, version, build, channel, href, className }: VersionWidgetProps): import("react").JSX.Element;
@@ -0,0 +1,159 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { iconArrowRight, InlineIcon } from "nexus-icons";
4
+ import { useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { marksByDay } from "../../Helpers/DashboardHelpers.js";
7
+ import { formatDate, todayIsoDate, toLatinDigits, usesDevanagari, toDevanagariDigits } from "../../Helpers/DateHelpers.js";
8
+ import { useDatePreferences } from "../../Helpers/DatePreferences.js";
9
+ import { Calendar } from "../Inputs/Calendar.js";
10
+ import { MarkdownView } from "../Viewers/MarkdownView.js";
11
+ import { Avatar } from "../Viewers/Avatar.js";
12
+ import { WidgetLink } from "./WidgetFrame.js";
13
+ /**
14
+ * One or several figures in a widget, side by side as they fit and wrapping as the widget narrows; each may link to
15
+ * the list behind it. Plain on the widget's panel: no card in the card.
16
+ *
17
+ * @example <StatWidget items={[{ label: "Unread", value: 4, href: "/admin/message?list=2" }, { label: "Sent", value: 31 }]} />
18
+ */
19
+ export function StatWidget({ items, className }) {
20
+ return (_jsx("ul", { className: cx("nx-wstat", className), children: items.map((item, index) => {
21
+ const body = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "nx-wstat__label", children: [item.tone && _jsx("span", { className: "nx-wstat__dot", "data-tone": item.tone, "aria-hidden": "true" }), item.label] }), _jsx("span", { className: "nx-wstat__value", children: item.value }), item.note && _jsx("span", { className: "nx-wstat__note", children: item.note })] }));
22
+ return (_jsx("li", { className: "nx-wstat__item", children: item.href ? (_jsx(WidgetLink, { className: "nx-wstat__figure nx-wstat__figure--link", href: item.href, children: body })) : (_jsx("span", { className: "nx-wstat__figure", children: body })) }, index));
23
+ }) }));
24
+ }
25
+ // The more line's height before it is drawn: a line of small text and its padding.
26
+ const MORE_LINE = 24;
27
+ /**
28
+ * A few rows in a widget: a picture or an icon, the words and a muted second line, a time and a tag at the end. A row
29
+ * with `href` is a link. Rows are one height, and only those that fit whole show (`fit`), so the widget's size says how
30
+ * many: the rest are counted on the more line.
31
+ *
32
+ * @example <ListWidget rows={mails.map(m => ({ id: m.id, title: m.subject, secondary: m.from, time: m.when, avatar: { name: m.from } }))} />
33
+ */
34
+ export function ListWidget({ rows, total = rows.length, more, fit = true, className }) {
35
+ const rootRef = useRef(null);
36
+ const [shown, setShown] = useState(rows.length);
37
+ // As many rows as fit whole in what holds the list (a widget's body), less the more line when some stay out.
38
+ useLayoutEffect(() => {
39
+ const root = rootRef.current;
40
+ const holder = root?.parentElement;
41
+ if (!fit || !root || !holder) {
42
+ setShown(rows.length);
43
+ return;
44
+ }
45
+ const measure = () => {
46
+ const first = root.querySelector(".nx-wlist__row");
47
+ if (!first)
48
+ return;
49
+ const style = getComputedStyle(holder);
50
+ const room = holder.clientHeight - parseFloat(style.paddingTop) - parseFloat(style.paddingBottom);
51
+ const row = first.offsetHeight;
52
+ if (row <= 0 || room <= 0)
53
+ return;
54
+ const line = root.querySelector(".nx-wlist__more")?.offsetHeight || MORE_LINE;
55
+ const all = rows.length * row <= room - (more && total > rows.length ? line : 0);
56
+ const next = all ? rows.length : Math.max(1, Math.floor((room - (more ? line : 0)) / row));
57
+ setShown(previous => (previous === next ? previous : next));
58
+ };
59
+ measure();
60
+ const observer = new ResizeObserver(measure);
61
+ observer.observe(holder);
62
+ return () => observer.disconnect();
63
+ }, [fit, rows.length, total, more]);
64
+ const count = Math.min(shown, rows.length);
65
+ const hidden = total - count;
66
+ const moreLabel = more && (typeof more.label === "function" ? more.label(hidden) : (more.label ?? `${hidden} more`));
67
+ return (_jsxs("div", { ref: rootRef, className: cx("nx-wlist", className), children: [_jsx("ul", { className: "nx-wlist__rows", children: rows.slice(0, count).map(row => {
68
+ const body = (_jsxs(_Fragment, { children: [row.avatar ? _jsx(Avatar, { name: row.avatar.name, src: row.avatar.src, size: "md" }) : row.icon ? _jsx("span", { className: "nx-wlist__icon", children: row.icon }) : null, _jsxs("span", { className: "nx-wlist__text", children: [_jsx("span", { className: "nx-wlist__title", "data-unread": row.unread || undefined, children: row.title }), row.secondary && _jsx("span", { className: "nx-wlist__secondary", children: row.secondary })] }), (row.time || row.chip) && (_jsxs("span", { className: "nx-wlist__end", children: [row.time && _jsx("span", { className: "nx-wlist__time", children: row.time }), row.chip && (_jsx("span", { className: "nx-table__tag nx-wlist__chip", "data-tone": row.chip.tone, children: row.chip.label }))] }))] }));
69
+ return (_jsx("li", { className: "nx-wlist__row", "data-lead": row.avatar || row.icon ? "" : undefined, children: row.href ? (_jsx(WidgetLink, { className: "nx-wlist__item nx-wlist__item--link", href: row.href, children: body })) : (_jsx("span", { className: "nx-wlist__item", children: body })) }, row.id));
70
+ }) }), more && hidden > 0 && (_jsxs(WidgetLink, { className: "nx-wlist__more", href: more.href, children: [moreLabel, _jsx(InlineIcon, { icon: iconArrowRight })] }))] }));
71
+ }
72
+ /** The kinds a calendar widget knows without being told: holidays, leave, events. */
73
+ export const CALENDAR_MARK_KINDS = {
74
+ holiday: { label: "Holiday", tone: "danger" },
75
+ leave: { label: "Leave", tone: "warning" },
76
+ event: { label: "Event", tone: "primary" },
77
+ };
78
+ const MAX_DOTS = 3;
79
+ /**
80
+ * A month in a widget with typed marks on its days - holidays, leave, events, or the app's own kinds - a dot of each
81
+ * kind's tone under the day's number, today marked, a legend counting the month's marks by kind, and the picked day's
82
+ * marks listed beside the month (under it when the widget is narrow). In the English or the Nepali calendar.
83
+ *
84
+ * @example <CalendarWidget marks={[{ date: "2026-11-12", kind: "holiday", label: "Tihar" }, { date: "2026-11-16", until: "2026-11-18", kind: "leave", label: "Sita Rai" }]} />
85
+ */
86
+ export function CalendarWidget({ marks, kinds, calendar, today, onMonthChange, weekStart, className }) {
87
+ const preferences = useDatePreferences();
88
+ const shown = calendar ?? preferences.calendar;
89
+ const day0 = today ?? todayIsoDate();
90
+ const [picked, setPicked] = useState(day0);
91
+ const [month, setMonth] = useState(null);
92
+ const known = useMemo(() => ({ ...CALENDAR_MARK_KINDS, ...kinds }), [kinds]);
93
+ const byDay = useMemo(() => marksByDay(marks), [marks]);
94
+ const kindOf = (kind) => known[kind] ?? {
95
+ label: kind.charAt(0).toUpperCase() + kind.slice(1),
96
+ tone: "neutral",
97
+ };
98
+ const devanagari = usesDevanagari(shown, preferences.script);
99
+ const digits = (value) => (devanagari ? toDevanagariDigits(String(value)) : String(value));
100
+ const dateOptions = {
101
+ ...preferences,
102
+ calendar: shown,
103
+ format: "dddd, MMM D",
104
+ };
105
+ // The month's marks by kind, for the legend. A mark over several days counts once.
106
+ const monthMarks = useMemo(() => {
107
+ const first = month ?? day0;
108
+ const counts = new Map();
109
+ const seen = new Set();
110
+ const prefix = (iso) => formatDate(iso, { calendar: shown, format: "YYYY-MM", script: "latin" });
111
+ const wanted = prefix(first);
112
+ for (const [iso, list] of byDay)
113
+ if (prefix(iso) === wanted)
114
+ for (const mark of list)
115
+ if (!seen.has(mark)) {
116
+ seen.add(mark);
117
+ counts.set(mark.kind, (counts.get(mark.kind) ?? 0) + 1);
118
+ }
119
+ return counts;
120
+ }, [byDay, month, day0, shown]);
121
+ const legendKinds = [...new Set([...Object.keys(CALENDAR_MARK_KINDS), ...Object.keys(kinds ?? {}), ...monthMarks.keys()])];
122
+ const dayMarks = byDay.get(picked) ?? [];
123
+ function renderDay(day) {
124
+ const list = byDay.get(day.iso) ?? [];
125
+ const kindsOnDay = [...new Set(list.map(mark => mark.kind))];
126
+ return (_jsxs("span", { className: "nx-wcal__day", "data-holiday": kindsOnDay.includes("holiday") || undefined, children: [_jsx("span", { className: "nx-cal__num", children: digits(day.parts.day) }), kindsOnDay.length > 0 && (_jsx("span", { className: "nx-wcal__dots", "aria-hidden": "true", children: kindsOnDay.slice(0, MAX_DOTS).map(kind => (_jsx("span", { className: "nx-wcal__dot", "data-tone": kindOf(kind).tone }, kind))) }))] }));
127
+ }
128
+ return (_jsxs("div", { className: cx("nx-wcal", className), children: [_jsx(Calendar, { size: "sm", className: "nx-wcal__month", calendar: shown, value: picked, onValueChange: value => value && setPicked(value), today: day0, footer: false, weekStart: weekStart, renderDay: renderDay, onMonthChange: first => {
129
+ setMonth(first);
130
+ onMonthChange?.(first);
131
+ }, "aria-label": "Calendar" }), _jsxs("div", { className: "nx-wcal__side", children: [_jsx("ul", { className: "nx-wcal__legend", "aria-label": "This month", children: legendKinds.map(kind => (_jsxs("li", { className: "nx-wcal__key", children: [_jsx("span", { className: "nx-wcal__dot", "data-tone": kindOf(kind).tone, "aria-hidden": "true" }), kindOf(kind).label, _jsx("span", { className: "nx-wcal__count", children: digits(monthMarks.get(kind) ?? 0) })] }, kind))) }), _jsxs("section", { "aria-live": "polite", children: [_jsxs("h4", { className: "nx-wcal__date", children: [picked === day0 ? "Today · " : "", formatDate(picked, dateOptions)] }), dayMarks.length > 0 ? (_jsx("ul", { className: "nx-wcal__marks", children: dayMarks.map((mark, index) => (_jsxs("li", { className: "nx-wcal__mark", children: [_jsx("span", { className: "nx-table__tag", "data-tone": kindOf(mark.kind).tone, children: kindOf(mark.kind).label }), _jsx("span", { className: "nx-wcal__label", children: mark.label })] }, index))) })) : (_jsx("p", { className: "nx-wcal__none", children: "Nothing on this day." }))] })] })] }));
132
+ }
133
+ const NEWS_KINDS = {
134
+ released: { label: "Released", tone: "success" },
135
+ "coming-soon": { label: "Coming soon", tone: "info" },
136
+ };
137
+ /**
138
+ * Release notes in a widget, newest first as given: the title, the version and the kind (Released, Coming soon) as
139
+ * tags, the day, the first lines of the Markdown notes and "Read more".
140
+ *
141
+ * @example <NewsWidget items={releases} onReadMore={item => openNote(item)} />
142
+ */
143
+ export function NewsWidget({ items, onReadMore, excerptLines = 3, className }) {
144
+ const preferences = useDatePreferences();
145
+ return (_jsx("ol", { className: cx("nx-wnews", className), children: items.map(item => (_jsxs("li", { className: "nx-wnews__item", children: [_jsxs("div", { className: "nx-wnews__head", children: [_jsx("span", { className: "nx-wnews__title", children: item.title }), item.version && (_jsx("span", { className: "nx-table__tag", "data-tone": "primary", children: item.version })), item.kind && (_jsx("span", { className: "nx-table__tag", "data-tone": NEWS_KINDS[item.kind].tone, children: NEWS_KINDS[item.kind].label }))] }), (item.application || item.date) && (_jsxs("p", { className: "nx-wnews__meta", children: [item.application, item.application && item.date ? " · " : "", item.date && (_jsx("time", { dateTime: item.date, children: formatDate(item.date, {
146
+ ...preferences,
147
+ format: "MMM D, YYYY",
148
+ }) }))] })), item.notes && _jsx(MarkdownView, { className: "nx-wnews__notes", value: item.notes, style: { WebkitLineClamp: excerptLines } }), item.href ? (_jsxs(WidgetLink, { className: "nx-wnews__more", href: item.href, children: ["Read more", _jsx(InlineIcon, { icon: iconArrowRight })] })) : (onReadMore && (_jsxs("button", { type: "button", className: "nx-wnews__more", onClick: () => onReadMore(item), children: ["Read more", _jsx(InlineIcon, { icon: iconArrowRight })] })))] }, item.id))) }));
149
+ }
150
+ /**
151
+ * The application's name, version and build day in a widget: what a person reads out when they ask for help.
152
+ *
153
+ * @example <VersionWidget name="Nexus Web" version={BUILD.version} build={BUILD.time} href="/admin/tenant/releases" />
154
+ */
155
+ export function VersionWidget({ name, version, build, channel, href, className }) {
156
+ const preferences = useDatePreferences();
157
+ const day = build ? toLatinDigits(build).slice(0, 10) : undefined;
158
+ return (_jsxs("div", { className: cx("nx-wversion", className), children: [_jsxs("span", { className: "nx-wversion__line", children: [_jsx("span", { className: "nx-wversion__version", children: version }), channel && (_jsx("span", { className: "nx-table__tag", "data-tone": "info", children: channel }))] }), _jsxs("span", { className: "nx-wversion__meta", children: [name, day && (_jsxs(_Fragment, { children: [" · Built ", _jsx("time", { dateTime: build, children: formatDate(day, { ...preferences, format: "MMM D, YYYY" }) })] }))] }), href && (_jsxs(WidgetLink, { className: "nx-wversion__link", href: href, children: ["What's new", _jsx(InlineIcon, { icon: iconArrowRight })] }))] }));
159
+ }
@@ -0,0 +1,51 @@
1
+ import { type ComponentPropsWithRef, type ReactNode } from "react";
2
+ import type { WidgetSize } from "../../Interfaces/DashboardInterfaces.ts";
3
+ /**
4
+ * How a link inside a board moves: the app's router (`router.push`) for a plain click, so a dashboard's links stay in
5
+ * the app; a click with a modifier key, or no router, is the browser's own. `DashboardBoard` gives it from `onNavigate`.
6
+ */
7
+ export declare const WidgetNavigateContext: import("react").Context<((href: string) => void) | null>;
8
+ export interface WidgetLinkProps extends Omit<ComponentPropsWithRef<"a">, "href"> {
9
+ href: string;
10
+ }
11
+ /** A link inside a widget: an `<a>`, moved through the board's `onNavigate` on a plain click. */
12
+ export declare function WidgetLink({ href, onClick, ...rest }: WidgetLinkProps): import("react").JSX.Element;
13
+ /** What a frame shows: bones while it loads, a sentence when empty, the failure with Try again, or its content. */
14
+ export type WidgetFrameState = "loading" | "ready" | "empty" | "failed";
15
+ export interface WidgetFrameProps extends Omit<ComponentPropsWithRef<"article">, "title" | "children"> {
16
+ title: ReactNode;
17
+ /** A link in the heading to the page its data comes from: "View all" by default. */
18
+ link?: {
19
+ href: string;
20
+ label?: ReactNode;
21
+ };
22
+ /** Its size on a board's grid; it sets the frame's spans. Default `M`. */
23
+ size?: WidgetSize;
24
+ /** Default `ready`. */
25
+ state?: WidgetFrameState;
26
+ /** `empty`: what it says (default "Nothing to show yet."); `failed`: the failure (default "This could not be read."). */
27
+ message?: ReactNode;
28
+ /** `failed`: Try again. */
29
+ onRetry?: () => void;
30
+ /** Its own refresh button in the heading. */
31
+ onRefresh?: () => void;
32
+ /** Its data is being read again behind what it shows: the refresh button turns. */
33
+ refreshing?: boolean;
34
+ /** Controls at the end of the heading (a board's edit tools). */
35
+ tools?: ReactNode;
36
+ /** Before the title (a board's move handle). */
37
+ lead?: ReactNode;
38
+ /** The body takes no clicks or focus (a board in edit mode). */
39
+ inertBody?: boolean;
40
+ children?: ReactNode;
41
+ }
42
+ /**
43
+ * A widget's frame on a dashboard: the title, an optional link, its own refresh, and its body in one of four states -
44
+ * bones of its own size while it loads (it is the size it will be, so nothing moves when the data lands), a sentence
45
+ * when there is nothing to show, the failure with Try again (never more than the frame: the board goes on), or its
46
+ * content. Its size sets its spans on the board's grid.
47
+ *
48
+ * @example
49
+ * <WidgetFrame title="Mail" size="S" state={loading ? "loading" : "ready"} link={{ href: "/admin/message" }}>…</WidgetFrame>
50
+ */
51
+ export declare function WidgetFrame({ title, link, size, state, message, onRetry, onRefresh, refreshing, tools, lead, inertBody, className, style, children, ...rest }: WidgetFrameProps): import("react").JSX.Element;
@@ -0,0 +1,45 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { iconArrowRight, iconRefresh, InlineIcon } from "nexus-icons";
4
+ import { createContext, useContext, useId } from "react";
5
+ import { cx } from "../../Helpers/ClassHelpers.js";
6
+ import { WIDGET_SPANS } from "../../Helpers/DashboardHelpers.js";
7
+ import { ChatFailure } from "../Chats/ChatParts.js";
8
+ import { Button } from "../Documents/Button.js";
9
+ /**
10
+ * How a link inside a board moves: the app's router (`router.push`) for a plain click, so a dashboard's links stay in
11
+ * the app; a click with a modifier key, or no router, is the browser's own. `DashboardBoard` gives it from `onNavigate`.
12
+ */
13
+ export const WidgetNavigateContext = createContext(null);
14
+ /** A link inside a widget: an `<a>`, moved through the board's `onNavigate` on a plain click. */
15
+ export function WidgetLink({ href, onClick, ...rest }) {
16
+ const navigate = useContext(WidgetNavigateContext);
17
+ function click(event) {
18
+ onClick?.(event);
19
+ if (event.defaultPrevented || !navigate || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey || rest.target === "_blank")
20
+ return;
21
+ event.preventDefault();
22
+ navigate(href);
23
+ }
24
+ return _jsx("a", { ...rest, href: href, onClick: click });
25
+ }
26
+ /** How many lines of bones stand in for a size's content while it loads. */
27
+ const BONE_LINES = { S: 1, M: 3, L: 7, W: 3, T: 7 };
28
+ /**
29
+ * A widget's frame on a dashboard: the title, an optional link, its own refresh, and its body in one of four states -
30
+ * bones of its own size while it loads (it is the size it will be, so nothing moves when the data lands), a sentence
31
+ * when there is nothing to show, the failure with Try again (never more than the frame: the board goes on), or its
32
+ * content. Its size sets its spans on the board's grid.
33
+ *
34
+ * @example
35
+ * <WidgetFrame title="Mail" size="S" state={loading ? "loading" : "ready"} link={{ href: "/admin/message" }}>…</WidgetFrame>
36
+ */
37
+ export function WidgetFrame({ title, link, size = "M", state = "ready", message, onRetry, onRefresh, refreshing = false, tools, lead, inertBody = false, className, style, children, ...rest }) {
38
+ const titleId = `${useId()}-title`;
39
+ const span = WIDGET_SPANS[size];
40
+ return (_jsxs("article", { ...rest, className: cx("nx-widget", className), "data-size": size, "data-state": state, "aria-labelledby": rest["aria-label"] ? undefined : titleId, "aria-busy": state === "loading" || refreshing || undefined, style: {
41
+ "--nx-widget-columns": span.columns,
42
+ "--nx-widget-rows": span.rows,
43
+ ...style,
44
+ }, children: [_jsxs("header", { className: "nx-widget__head", children: [lead, _jsx("h3", { id: titleId, className: "nx-widget__title", children: title }), link && (_jsxs(WidgetLink, { className: "nx-widget__link", href: link.href, tabIndex: inertBody ? -1 : undefined, children: [link.label ?? "View all", _jsx(InlineIcon, { icon: iconArrowRight })] })), onRefresh && !tools && (_jsx(Button, { variant: "ghost", size: "sm", className: "nx-widget__refresh", icon: _jsx(InlineIcon, { icon: iconRefresh, className: refreshing ? "nx-spin" : undefined }), "aria-label": typeof title === "string" ? `Refresh ${title}` : "Refresh", title: "Refresh", disabled: state === "loading" || refreshing, onClick: onRefresh })), tools] }), _jsx("div", { className: "nx-widget__body", inert: inertBody || undefined, children: state === "loading" ? (_jsx("div", { className: "nx-widget__bones", "aria-hidden": "true", children: Array.from({ length: BONE_LINES[size] }, (_, index) => (_jsx("span", { className: "nx-table__bone nx-widget__bone" }, index))) })) : state === "failed" ? (_jsx("div", { className: "nx-widget__failure", children: _jsx(ChatFailure, { message: typeof message === "string" && message ? message : "This could not be read.", onRetry: onRetry, flat: true }) })) : state === "empty" ? (_jsx("p", { className: "nx-widget__empty", children: message ?? "Nothing to show yet." })) : (children) })] }));
45
+ }
@@ -26,9 +26,14 @@ export interface ButtonProps extends ComponentPropsWithRef<"button"> {
26
26
  * a state that is on, such as filters.
27
27
  */
28
28
  dot?: boolean | "primary";
29
+ /**
30
+ * A count over the corner, in the chat's badge: "3", "99+". The button keeps its size whatever the count, so a bar of
31
+ * buttons does not move as it arrives. Say the count in `aria-label` too: the badge is not read.
32
+ */
33
+ badge?: number | string | null;
29
34
  }
30
35
  /**
31
36
  * A button. Without children it becomes a square icon button, which then needs an `aria-label`.
32
37
  * Has no client state, so it renders in server and client components alike.
33
38
  */
34
- export declare function Button({ variant, size, hoverTone, icon, iconEnd, loading, block, dot, type, disabled, className, children, ...rest }: ButtonProps): import("react").JSX.Element;
39
+ export declare function Button({ variant, size, hoverTone, icon, iconEnd, loading, block, dot, badge, type, disabled, className, children, ...rest }: ButtonProps): import("react").JSX.Element;
@@ -5,7 +5,7 @@ import { cx } from "../../Helpers/ClassHelpers.js";
5
5
  * A button. Without children it becomes a square icon button, which then needs an `aria-label`.
6
6
  * Has no client state, so it renders in server and client components alike.
7
7
  */
8
- export function Button({ variant = "secondary", size = "md", hoverTone, icon, iconEnd, loading = false, block = false, dot = false, type = "button", disabled, className, children, ...rest }) {
8
+ export function Button({ variant = "secondary", size = "md", hoverTone, icon, iconEnd, loading = false, block = false, dot = false, badge, type = "button", disabled, className, children, ...rest }) {
9
9
  const hasLabel = children !== undefined && children !== null && children !== false;
10
- return (_jsxs("button", { type: type, className: cx("nx-btn", `nx-btn--${variant}`, `nx-btn--${size}`, hoverTone && `nx-btn--hover-${hoverTone}`, block && "nx-btn--block", !hasLabel && "nx-btn--icon", className), disabled: disabled || loading, "aria-busy": loading || undefined, ...rest, children: [loading ? _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }) : icon, hasLabel && _jsx("span", { className: "nx-btn__label", children: children }), iconEnd, dot && _jsx("span", { className: "nx-btn__dot", "data-tone": dot === true ? undefined : dot, "aria-hidden": "true" })] }));
10
+ return (_jsxs("button", { type: type, className: cx("nx-btn", `nx-btn--${variant}`, `nx-btn--${size}`, hoverTone && `nx-btn--hover-${hoverTone}`, block && "nx-btn--block", !hasLabel && "nx-btn--icon", className), disabled: disabled || loading, "aria-busy": loading || undefined, ...rest, children: [loading ? _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }) : icon, hasLabel && _jsx("span", { className: "nx-btn__label", children: children }), iconEnd, dot && _jsx("span", { className: "nx-btn__dot", "data-tone": dot === true ? undefined : dot, "aria-hidden": "true" }), badge !== undefined && badge !== null && badge !== "" && (_jsx("span", { className: "nx-btn__badge", "aria-hidden": "true", children: badge }))] }));
11
11
  }
@@ -0,0 +1,50 @@
1
+ import { type CSSProperties, type ReactNode, type Ref } from "react";
2
+ /** What a page shown in a `DocumentFrame` can be asked to do. */
3
+ export interface DocumentFrameHandle {
4
+ /** Opens the print dialog for the page shown; `false` when it cannot (not `printable`, no page yet, frame not loaded). */
5
+ print: () => boolean;
6
+ }
7
+ export interface DocumentFrameProps {
8
+ /** The whole page to show, or null/undefined (or "") while there is none yet. */
9
+ html: string | null | undefined;
10
+ /** The frame's name for screen readers and its title attribute: "Voucher preview". Required. */
11
+ title: string;
12
+ /**
13
+ * A read is under way: the page shown stays, and a read that lasts dims it and says `loadingMessage` in the bar (a
14
+ * quick one says nothing, as a form's status line). The page area is `aria-busy` from the start. While loading, or
15
+ * with an `error`, an `html` of null keeps the last page in view.
16
+ */
17
+ loading?: boolean;
18
+ /** What the bar says while a read lasts. Default "Loading…". */
19
+ loadingMessage?: ReactNode;
20
+ /** Why the page could not be shown: a line with `role="alert"` in the bar above the page, which stays in view under it. */
21
+ error?: ReactNode;
22
+ /** Shown in the page area before any page arrives. Default "Nothing to show yet." */
23
+ emptyMessage?: ReactNode;
24
+ /**
25
+ * Buttons the page puts in the frame's bar, at its end beside the status line ("Print", "Reset to default"); on a
26
+ * narrow frame they wrap under the line. The bar shows only when there are actions, or a status to say.
27
+ */
28
+ actions?: ReactNode;
29
+ /**
30
+ * The page may be printed through the handle's `print()`: only then is the frame sandboxed
31
+ * "allow-same-origin allow-modals" (same origin so the parent can call the frame's print(); modals so print() opens
32
+ * the dialog). Never allow-scripts. Without it the sandbox is "" (an opaque origin, nothing allowed).
33
+ */
34
+ printable?: boolean;
35
+ className?: string;
36
+ style?: CSSProperties;
37
+ ref?: Ref<DocumentFrameHandle>;
38
+ }
39
+ /**
40
+ * A whole HTML page from a server, in a frame that fills the height its container gives it (at least 22rem) and
41
+ * scrolls inside itself: an A4 sheet wider than the frame scrolls sideways there, never the page. Above it, a bar holds
42
+ * the status line and the page's `actions`. A page read again keeps its place while the title stays the same (a
43
+ * `printable` frame; the sandbox of another lets nobody look in).
44
+ *
45
+ * @example
46
+ * const frame = useRef<DocumentFrameHandle>(null);
47
+ * <DocumentFrame ref={frame} title="Voucher preview" html={preview} loading={reading} error={problem} printable
48
+ * actions={<Button onClick={() => frame.current?.print()}>Print</Button>} />
49
+ */
50
+ export declare function DocumentFrame({ html, title, loading, loadingMessage, error, emptyMessage, actions, printable, className, style, ref, }: DocumentFrameProps): import("react").JSX.Element;
@@ -0,0 +1,83 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { iconAlertCircle, iconFileText, iconLoader2, InlineIcon } from "nexus-icons";
4
+ import { useImperativeHandle, useLayoutEffect, useRef } from "react";
5
+ import { useSlowBusy } from "../../Helpers/BusyHooks.js";
6
+ import { cx } from "../../Helpers/ClassHelpers.js";
7
+ /*
8
+ * A whole HTML page a server wrote - a print layout's preview, a document to print - shown in a sandboxed frame. The
9
+ * page is the server's, from `<!DOCTYPE html>` to `</html>`, with its own `@page` sheet and screen style, so it is never
10
+ * mixed into this page's DOM and styles: it goes into an `<iframe srcdoc>`, and **no script ever runs in it**, whatever
11
+ * the page holds. The sandbox is "" (an opaque origin: nothing allowed, the parent cannot reach in either), or, for a
12
+ * page that may be printed, "allow-same-origin allow-modals": the parent may call the frame's print(), and print() may
13
+ * open its dialog. Never allow-scripts, allow-forms, allow-popups or allow-top-navigation.
14
+ */
15
+ /** A page that may be printed: the parent reaches the frame's window, and its print() may open the dialog. */
16
+ const PRINTABLE_SANDBOX = "allow-same-origin allow-modals";
17
+ const hasContent = (node) => node !== undefined && node !== null && node !== false && node !== "";
18
+ /**
19
+ * A whole HTML page from a server, in a frame that fills the height its container gives it (at least 22rem) and
20
+ * scrolls inside itself: an A4 sheet wider than the frame scrolls sideways there, never the page. Above it, a bar holds
21
+ * the status line and the page's `actions`. A page read again keeps its place while the title stays the same (a
22
+ * `printable` frame; the sandbox of another lets nobody look in).
23
+ *
24
+ * @example
25
+ * const frame = useRef<DocumentFrameHandle>(null);
26
+ * <DocumentFrame ref={frame} title="Voucher preview" html={preview} loading={reading} error={problem} printable
27
+ * actions={<Button onClick={() => frame.current?.print()}>Print</Button>} />
28
+ */
29
+ export function DocumentFrame({ html, title, loading = false, loadingMessage = "Loading…", error, emptyMessage = "Nothing to show yet.", actions, printable = false, className, style, ref, }) {
30
+ const frameRef = useRef(null);
31
+ const failed = hasContent(error);
32
+ // The page shown: the one given, else the last one while a read is under way or has failed.
33
+ const last = useRef(null);
34
+ const page = html ? html : loading || failed ? last.current : null;
35
+ const slow = useSlowBusy(loading);
36
+ const said = failed || slow;
37
+ const shownTitle = useRef(title);
38
+ const place = useRef(null);
39
+ // A new page replaces the one shown: note where the old one was scrolled to, while it is still there (the frame
40
+ // loads the new one a task later). Only a printable frame's page can be read; another's throws, and keeps nothing.
41
+ useLayoutEffect(() => {
42
+ try {
43
+ const view = frameRef.current?.contentWindow;
44
+ place.current = view && view.location.href === "about:srcdoc" ? { title: shownTitle.current, x: view.scrollX, y: view.scrollY } : null;
45
+ }
46
+ catch {
47
+ place.current = null;
48
+ }
49
+ last.current = page;
50
+ shownTitle.current = title;
51
+ }, [page, title]);
52
+ function keepPlace() {
53
+ const kept = place.current;
54
+ if (!kept || kept.title !== title || (!kept.x && !kept.y))
55
+ return;
56
+ try {
57
+ frameRef.current?.contentWindow?.scrollTo(kept.x, kept.y);
58
+ }
59
+ catch {
60
+ // The frame is not ours to reach (not printable): it opens at the top.
61
+ }
62
+ }
63
+ useImperativeHandle(ref, () => ({
64
+ print() {
65
+ const frame = frameRef.current;
66
+ if (!printable || !frame)
67
+ return false;
68
+ try {
69
+ const view = frame.contentWindow;
70
+ const document_ = frame.contentDocument;
71
+ if (!view || !document_ || document_.URL !== "about:srcdoc" || document_.readyState !== "complete")
72
+ return false;
73
+ view.focus();
74
+ view.print();
75
+ return true;
76
+ }
77
+ catch {
78
+ return false;
79
+ }
80
+ },
81
+ }), [printable]);
82
+ return (_jsxs("div", { className: cx("nx-document-frame", className), style: style, children: [_jsxs("div", { className: cx("nx-form-buttons nx-form-buttons--end nx-document-frame__bar", !said && !hasContent(actions) && "nx-visually-hidden"), children: [failed ? (_jsxs("p", { className: "nx-form-status nx-form-status--error", role: "alert", children: [_jsx(InlineIcon, { icon: iconAlertCircle }), _jsx("span", { children: error })] }, "error")) : (_jsx("p", { className: "nx-form-status", role: "status", children: slow && (_jsxs(_Fragment, { children: [_jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }), _jsx("span", { children: loadingMessage })] })) }, "status")), hasContent(actions) && _jsx("div", { className: "nx-form-buttons__actions", children: actions })] }), _jsx("div", { className: cx("nx-document-frame__view", slow && "nx-document-frame__view--busy"), "aria-busy": loading || undefined, children: page ? (_jsx("iframe", { ref: frameRef, className: "nx-document-frame__page", title: title, sandbox: printable ? PRINTABLE_SANDBOX : "", srcDoc: page, referrerPolicy: "no-referrer", onLoad: keepPlace }, printable ? "printable" : "sealed")) : (!loading && (_jsxs("p", { className: "nx-form-status nx-document-frame__empty", children: [_jsx(InlineIcon, { icon: iconFileText }), _jsx("span", { children: emptyMessage })] }))) })] }));
83
+ }
@@ -12,6 +12,10 @@ export interface MenuItem<T extends string = string> {
12
12
  shortcut?: string;
13
13
  /** `danger` draws the item in the danger color, for Delete and the like. */
14
14
  tone?: "danger";
15
+ /**
16
+ * Switched off. With a `description` it is held, saying why: still reached by the arrow keys, and a press shows the
17
+ * description in a note beside it, as a held Add and a read-only field do.
18
+ */
15
19
  disabled?: boolean;
16
20
  /** Renders the item as a link. */
17
21
  href?: string;