nexus-shared 3.0.0 → 3.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 (121) hide show
  1. package/CHANGELOG.md +446 -262
  2. package/LICENSE +21 -21
  3. package/README.md +1084 -1000
  4. package/dist/Client.Index.d.ts +3 -0
  5. package/dist/Client.Index.js +3 -0
  6. package/dist/Components/Chats/ChatParts.d.ts +2 -2
  7. package/dist/Components/Chats/ChatParts.js +3 -7
  8. package/dist/Components/Chats/ChatThread.js +3 -3
  9. package/dist/Components/Documents/Button.d.ts +6 -1
  10. package/dist/Components/Documents/Button.js +2 -2
  11. package/dist/Components/Documents/Menu.js +21 -9
  12. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  13. package/dist/Components/Documents/SplitButton.js +7 -5
  14. package/dist/Components/Documents/TabButtons.d.ts +5 -2
  15. package/dist/Components/Documents/TabButtons.js +2 -2
  16. package/dist/Components/Forms/ApiForm.d.ts +1 -1
  17. package/dist/Components/Forms/ApiForm.js +1 -1
  18. package/dist/Components/Forms/Crud.js +177 -58
  19. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  20. package/dist/Components/Forms/CrudParts.js +37 -5
  21. package/dist/Components/Forms/ExcelImport.d.ts +23 -7
  22. package/dist/Components/Forms/ExcelImport.js +189 -60
  23. package/dist/Components/Forms/Form.js +5 -13
  24. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  25. package/dist/Components/Forms/SubmitForm.js +23 -12
  26. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  27. package/dist/Components/Inputs/DropdownParts.js +4 -6
  28. package/dist/Components/Inputs/NumberBox.js +24 -2
  29. package/dist/Components/Inputs/RowsInput.js +2 -2
  30. package/dist/Components/Inputs/Slider.js +4 -1
  31. package/dist/Components/Inputs/TabularForm.js +42 -10
  32. package/dist/Components/Inputs/TimePanel.js +4 -3
  33. package/dist/Components/Layouts/Popup.js +1 -4
  34. package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
  35. package/dist/Components/Layouts/ThemePicker.js +77 -6
  36. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  37. package/dist/Components/Layouts/ThemeScript.js +4 -2
  38. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  39. package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
  40. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  41. package/dist/Components/Viewers/Avatar.js +21 -0
  42. package/dist/Components/Viewers/DataTable.js +192 -44
  43. package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
  44. package/dist/Components/Viewers/DataTableColumns.js +25 -68
  45. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  46. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  47. package/dist/Components/Viewers/DataTableParts.d.ts +2 -2
  48. package/dist/Components/Viewers/DataTableParts.js +3 -3
  49. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  50. package/dist/Helpers/AnimationHelpers.js +20 -2
  51. package/dist/Helpers/CrudBackend.d.ts +7 -4
  52. package/dist/Helpers/CrudBackend.js +4 -2
  53. package/dist/Helpers/ExcelBackend.d.ts +3 -3
  54. package/dist/Helpers/ExcelBackend.js +8 -2
  55. package/dist/Helpers/ExcelHelpers.d.ts +36 -8
  56. package/dist/Helpers/ExcelHelpers.js +55 -13
  57. package/dist/Helpers/FormStore.d.ts +9 -5
  58. package/dist/Helpers/FormStore.js +25 -5
  59. package/dist/Helpers/MessageBuilder.js +1 -0
  60. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  61. package/dist/Helpers/NumberHelpers.js +41 -3
  62. package/dist/Helpers/PopoverHelpers.d.ts +17 -5
  63. package/dist/Helpers/PopoverHelpers.js +52 -17
  64. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  65. package/dist/Helpers/PopoverHooks.js +62 -0
  66. package/dist/Helpers/RowsStore.d.ts +7 -2
  67. package/dist/Helpers/RowsStore.js +15 -3
  68. package/dist/Helpers/TableExport.js +12 -12
  69. package/dist/Helpers/TableFilters.d.ts +25 -0
  70. package/dist/Helpers/TableFilters.js +162 -0
  71. package/dist/Helpers/TableHelpers.d.ts +6 -3
  72. package/dist/Helpers/TableHelpers.js +8 -4
  73. package/dist/Helpers/TableStore.d.ts +11 -3
  74. package/dist/Helpers/TableStore.js +16 -5
  75. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  76. package/dist/Helpers/ThemeHelpers.js +46 -9
  77. package/dist/Helpers/UserPreferences.d.ts +14 -0
  78. package/dist/Helpers/UserPreferences.js +99 -0
  79. package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
  80. package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
  81. package/dist/Interfaces/FormInterfaces.d.ts +33 -5
  82. package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
  83. package/dist/Interfaces/PreferenceInterfaces.js +28 -0
  84. package/dist/Interfaces/TableInterfaces.d.ts +59 -12
  85. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  86. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  87. package/dist/Services/ExcelApi.d.ts +3 -1
  88. package/dist/Services/ExcelApi.js +1 -1
  89. package/dist/Services/ThemeService.d.ts +18 -2
  90. package/dist/Services/ThemeService.js +80 -16
  91. package/dist/Shared.Index.d.ts +2 -0
  92. package/dist/Shared.Index.js +2 -0
  93. package/package.json +85 -85
  94. package/src/Styles/Nexus.Avatar.css +79 -0
  95. package/src/Styles/Nexus.Base.css +117 -70
  96. package/src/Styles/Nexus.Button.css +180 -153
  97. package/src/Styles/Nexus.Calendar.css +273 -275
  98. package/src/Styles/Nexus.Chat.css +1443 -1489
  99. package/src/Styles/Nexus.Choice.css +343 -345
  100. package/src/Styles/Nexus.Crud.css +168 -161
  101. package/src/Styles/Nexus.DateTime.css +81 -81
  102. package/src/Styles/Nexus.Dropdown.css +725 -774
  103. package/src/Styles/Nexus.Editor.css +295 -297
  104. package/src/Styles/Nexus.Excel.css +389 -370
  105. package/src/Styles/Nexus.Form.css +279 -321
  106. package/src/Styles/Nexus.Index.css +37 -36
  107. package/src/Styles/Nexus.Input.css +602 -637
  108. package/src/Styles/Nexus.Markdown.css +237 -265
  109. package/src/Styles/Nexus.Menu.css +237 -259
  110. package/src/Styles/Nexus.Popup.css +724 -731
  111. package/src/Styles/Nexus.RichText.css +2 -32
  112. package/src/Styles/Nexus.Rows.css +637 -596
  113. package/src/Styles/Nexus.Slider.css +276 -278
  114. package/src/Styles/Nexus.Tab.Buttons.css +432 -419
  115. package/src/Styles/Nexus.Table.css +1386 -1296
  116. package/src/Styles/Nexus.Theme.Picker.css +286 -232
  117. package/src/Styles/Nexus.Themes.css +194 -194
  118. package/src/Styles/Nexus.Time.css +144 -147
  119. package/src/Styles/Nexus.Toast.css +263 -265
  120. package/src/Styles/Nexus.Tokens.css +89 -72
  121. package/src/Styles/Nexus.Tree.css +431 -435
@@ -1,6 +1,7 @@
1
1
  import type { CSSProperties, ReactNode, Ref } from "react";
2
2
  import type { TableStore } from "../Helpers/TableStore.ts";
3
3
  import type { ExcelEndpoints } from "./ExcelInterfaces.ts";
4
+ import type { InputParams } from "./FormInterfaces.ts";
4
5
  import type { Size, Tone } from "./TypeInterfaces.ts";
5
6
  /** A row's id: its `rowKey` field, such as `id`. */
6
7
  export type TableKey = string | number;
@@ -81,7 +82,13 @@ export interface TableSort {
81
82
  key: string;
82
83
  direction: TableSortDirection;
83
84
  }
84
- /** What the table shows: the search, the sort, and the page. */
85
+ /**
86
+ * The advanced filter's values, by input name: only the inputs that hold a value, in the order of the inputs, so the
87
+ * same filters always serialize the same way (a cache key, a request). A date range is its pair ["2026-10-01", null],
88
+ * a range slider [2, 7], a multiple choice its list, a ticked checkbox `true`.
89
+ */
90
+ export type TableFilters = Readonly<Record<string, unknown>>;
91
+ /** What the table shows: the search, the filters, the sort, and the page. */
85
92
  export interface TableQuery {
86
93
  /** The search text, trimmed. */
87
94
  search: string;
@@ -89,13 +96,18 @@ export interface TableQuery {
89
96
  /** The page, from 1. */
90
97
  page: number;
91
98
  pageSize: number;
99
+ /**
100
+ * The advanced filter while any input holds a value; absent otherwise. A client table matches its rows; a server
101
+ * table's loader receives the raw values (`filterRequest` gives them in a server's names).
102
+ */
103
+ filters?: TableFilters;
92
104
  }
93
105
  /** One page of rows, as a server sends it. */
94
106
  export interface TablePage<TRow extends TableRow = TableRow> {
95
107
  rows: readonly TRow[];
96
108
  /** Every row there is. Default: the rows given. */
97
109
  total?: number;
98
- /** The rows that match the search. Default: `total`. */
110
+ /** The rows that match the search and the filters. Default: `total`. */
99
111
  matches?: number;
100
112
  }
101
113
  /**
@@ -120,7 +132,7 @@ export interface TableState<TRow extends TableRow = TableRow> {
120
132
  offset: number;
121
133
  /** Every row there is. */
122
134
  total: number;
123
- /** The rows that match the search. */
135
+ /** The rows that match the search and the filters. */
124
136
  matches: number;
125
137
  query: TableQuery;
126
138
  /** The selected rows by key (keys as text), in the order they were picked. */
@@ -175,10 +187,15 @@ export interface TableExcel {
175
187
  subject?: string;
176
188
  /** Values the endpoints' own params need, such as a parent's id. */
177
189
  params?: Record<string, string | number>;
178
- /** The filters the page holds, beside the table's search and sort, read when an export is asked for. */
190
+ /** Filters the page holds outside the table, sent beside the table's own (`filterInputs`); the page's win a clash. */
179
191
  filters?: () => Record<string, unknown>;
180
- /** After an import wrote something. The table reloads itself either way. */
192
+ /** After an import wrote something, before the table reads its rows again: drop here whatever the page keeps of them. */
181
193
  onImported?: () => void;
194
+ /**
195
+ * No **Bulk insert** entry. A sheet with a required list cannot be typed in - its key is picked in the spreadsheet -
196
+ * and its panel says so; this leaves the entry out instead.
197
+ */
198
+ bulk?: false;
182
199
  }
183
200
  /** An item of the table's options menu (⋮): Import, Print, a report of the page's own. */
184
201
  export interface TableMenuAction {
@@ -195,6 +212,12 @@ export interface TableMenuAction {
195
212
  separator?: boolean;
196
213
  onSelect: () => void;
197
214
  }
215
+ /** A way to select rows by what they are, such as "Pinned" or "Not flagged". */
216
+ export interface TableSelectBy<TRow extends TableRow = TableRow> {
217
+ label: string;
218
+ icon?: ReactNode;
219
+ test: (row: TRow) => boolean;
220
+ }
198
221
  /** An action on one row, in its row: an icon button (`inline`) or an item of the row's menu. */
199
222
  export interface TableRowAction<TRow extends TableRow = TableRow> {
200
223
  /** Tells actions apart; a test or a style can find it by `data-action`. */
@@ -247,22 +270,33 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
247
270
  searchable?: boolean;
248
271
  /** Default "Search…". */
249
272
  searchPlaceholder?: string;
250
- /** Before the search box, on the left of the bar: tabs, filters. */
273
+ /** On the left of the bar, after the Add section: tabs, filters. On a narrow table the line under the search. */
251
274
  toolbar?: ReactNode;
252
- /** At the end of the bar: an Add button. */
275
+ /** The page's own buttons, in the Add section after the table's Add: first in the bar, last on a phone. */
253
276
  actions?: ReactNode;
277
+ /**
278
+ * The table's Add, first in the bar. With `excel`, its arrow holds the template, the import and the export, which
279
+ * then leave the options menu.
280
+ */
281
+ add?: {
282
+ label: ReactNode;
283
+ onClick: () => void;
284
+ };
254
285
  /** The Columns panel, where columns are shown, hidden, and dragged into another order. Default true. */
255
286
  columnsMenu?: boolean;
256
287
  /**
257
288
  * The spreadsheet side of this table, against the backend's Excel controller. The options menu (⋮) then offers
258
289
  * **Download template**, **Import from a spreadsheet** (the file is verified, the person goes over the rows, and the
259
- * table reloads once they are written) and, where the endpoints have one, **Export to Excel** - which asks the
260
- * server with the search and the sort on screen instead of writing a file of the rows this browser happens to hold.
290
+ * table reloads once they are written), **Bulk insert** (the same import with no file: the rows are typed into the
291
+ * same editor; offered only with `add`, as it is an Add of many rows) and, where the endpoints have one, **Export to
292
+ * Excel** - which asks the server with the search, the filters and the sort on screen instead of writing a file of
293
+ * the rows this browser happens to hold.
261
294
  *
262
- * It replaces `onImport`, which is the hook for a page that imports its own way.
295
+ * It replaces `onImport`, which is the hook for a page that imports its own way. With `add`, they are under the
296
+ * Add's arrow instead of the options menu.
263
297
  */
264
298
  excel?: TableExcel;
265
- /** A Reload button. Default true when rows are loaded. */
299
+ /** A Reload button (in the options menu on a table under 375px wide). Default true when rows are loaded. */
266
300
  reloadable?: boolean;
267
301
  /** When the user presses Reload, before the rows load again: drop a cache of your own. */
268
302
  onReload?: () => void;
@@ -288,6 +322,11 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
288
322
  maxSelection?: number;
289
323
  /** After the selection changes. */
290
324
  onSelectionChange?: (rows: TRow[]) => void;
325
+ /**
326
+ * While rows are selected, their count opens a menu that selects again by what the rows are: every row on the page,
327
+ * then these, each picking exactly the rows on the page that pass its test.
328
+ */
329
+ selectBy?: readonly TableSelectBy<TRow>[];
291
330
  /** Actions on the selected rows. A function gets the selected rows. */
292
331
  massActions?: readonly TableMassAction<TRow>[] | ((rows: TRow[]) => readonly TableMassAction<TRow>[]);
293
332
  /** Actions of each row. */
@@ -327,9 +366,17 @@ export interface DataTableProps<TRow extends TableRow = TableRow> {
327
366
  stateKey?: string;
328
367
  /** Milliseconds after the last key before a search goes to a server. Default 300; a table with every row searches at once. */
329
368
  searchDelay?: number;
369
+ /**
370
+ * The advanced filter: the inputs the rows can be filtered by, as a form takes them. Keep the array stable.
371
+ * With any, the options menu (⋮) offers **Filters**: a panel of these inputs under its button, applied as they change.
372
+ * Name an input after the column `key` it filters (else a path into the row); that name is also what a server gets.
373
+ * A `number` input named `{key}From` / `{key}To` is a lower / upper bound of column `{key}`.
374
+ * `required`, `defaultValue` and `defaultChecked` are ignored, so the Add form's params can be reused.
375
+ */
376
+ filterInputs?: readonly InputParams[];
330
377
  /** Shown when there are no rows at all. Default "No records yet." */
331
378
  emptyMessage?: ReactNode;
332
- /** Beside the empty message, such as an Add button. */
379
+ /** Beside the empty message. Default: the table's Add (`add`); `null` for none. */
333
380
  emptyAction?: ReactNode;
334
381
  /** The tallest the table grows before its rows scroll under a header that stays: pixels or a CSS length. */
335
382
  maxHeight?: number | string;
@@ -9,9 +9,34 @@ export interface ThemeOption {
9
9
  /** The themes in `nexus-shared/styles.css`. Each has a light and a dark palette. */
10
10
  export declare const NEXUS_THEMES: readonly ThemeOption[];
11
11
  export declare const DEFAULT_THEME = "nexus";
12
+ /** `compact` (default): today's sizes. `cozy`: every control and row a step taller, for touch screens and large monitors. */
13
+ export type Density = "compact" | "cozy";
14
+ /**
15
+ * Whether things move: `on` even when the operating system asks for less motion, `off` never, `system` as the
16
+ * operating system says (`prefers-reduced-motion`). Some people turn animations off for the whole desktop and still
17
+ * want them here, which is why this is a choice of its own.
18
+ */
19
+ export type AnimationMode = "on" | "off" | "system";
20
+ /** The scale grid, a development tool: off, a 4px sheet with a solid line every 48px, or millimetres and centimetres. */
21
+ export type ScaleGrid = "off" | "px" | "cm";
12
22
  /** Browser storage keys for the saved choice. */
13
23
  export declare const THEME_STORAGE_KEY = "nx-theme";
14
24
  export declare const THEME_MODE_STORAGE_KEY = "nx-mode";
25
+ export declare const DENSITY_STORAGE_KEY = "nx-density";
26
+ export declare const ANIMATIONS_STORAGE_KEY = "nx-animations";
27
+ export declare const GRID_STORAGE_KEY = "nx-grid";
15
28
  /** Attributes on `<html>` that select the theme and the mode. Without `data-nx-mode`, the system setting applies. */
16
29
  export declare const THEME_ATTRIBUTE = "data-nx-theme";
17
30
  export declare const THEME_MODE_ATTRIBUTE = "data-nx-mode";
31
+ /** `compact` or `cozy`; the size tokens follow it. */
32
+ export declare const DENSITY_ATTRIBUTE = "data-nx-density";
33
+ /** The visitor's animations choice, `on` or `off`. Without it, the system setting applies. */
34
+ export declare const ANIMATIONS_ATTRIBUTE = "data-nx-animations";
35
+ /**
36
+ * What the stylesheets read: `reduce` while nothing may move, `full` otherwise. `ThemeScript` works it out from the
37
+ * choice and the system setting before the first paint, and again when the system setting changes, so no component
38
+ * asks `prefers-reduced-motion` itself.
39
+ */
40
+ export declare const MOTION_ATTRIBUTE = "data-nx-motion";
41
+ /** `px` or `cm` while the scale grid shows; development builds only. */
42
+ export declare const GRID_ATTRIBUTE = "data-nx-grid";
@@ -12,6 +12,21 @@ export const DEFAULT_THEME = "nexus";
12
12
  /** Browser storage keys for the saved choice. */
13
13
  export const THEME_STORAGE_KEY = "nx-theme";
14
14
  export const THEME_MODE_STORAGE_KEY = "nx-mode";
15
+ export const DENSITY_STORAGE_KEY = "nx-density";
16
+ export const ANIMATIONS_STORAGE_KEY = "nx-animations";
17
+ export const GRID_STORAGE_KEY = "nx-grid";
15
18
  /** Attributes on `<html>` that select the theme and the mode. Without `data-nx-mode`, the system setting applies. */
16
19
  export const THEME_ATTRIBUTE = "data-nx-theme";
17
20
  export const THEME_MODE_ATTRIBUTE = "data-nx-mode";
21
+ /** `compact` or `cozy`; the size tokens follow it. */
22
+ export const DENSITY_ATTRIBUTE = "data-nx-density";
23
+ /** The visitor's animations choice, `on` or `off`. Without it, the system setting applies. */
24
+ export const ANIMATIONS_ATTRIBUTE = "data-nx-animations";
25
+ /**
26
+ * What the stylesheets read: `reduce` while nothing may move, `full` otherwise. `ThemeScript` works it out from the
27
+ * choice and the system setting before the first paint, and again when the system setting changes, so no component
28
+ * asks `prefers-reduced-motion` itself.
29
+ */
30
+ export const MOTION_ATTRIBUTE = "data-nx-motion";
31
+ /** `px` or `cm` while the scale grid shows; development builds only. */
32
+ export const GRID_ATTRIBUTE = "data-nx-grid";
@@ -43,4 +43,6 @@ export declare function verifyExcelFile(endpoints: ExcelEndpoints, file: File, o
43
43
  * they made on screen would be lost. Every check runs again on the way in: what the browser sends is a row, not a
44
44
  * decision.
45
45
  */
46
- export declare function importExcelRows(endpoints: ExcelEndpoints, rows: readonly ExcelSheetRow[], options?: ExcelCallOptions): Promise<ApiResult<ExcelImportAnswer>>;
46
+ export declare function importExcelRows(endpoints: ExcelEndpoints, rows: readonly ExcelSheetRow[], options?: ExcelCallOptions & {
47
+ recordStatus?: number;
48
+ }): Promise<ApiResult<ExcelImportAnswer>>;
@@ -104,7 +104,7 @@ export async function verifyExcelFile(endpoints, file, options = {}) {
104
104
  */
105
105
  export async function importExcelRows(endpoints, rows, options = {}) {
106
106
  const backend = getExcelBackend();
107
- const answer = await api.post(endpoints.import, backend.importRequest(rows), { ...call(options), count: rows.length, toast: options.toast === false ? false : { success: false } });
107
+ const answer = await api.post(endpoints.import, backend.importRequest(rows, options.recordStatus), { ...call(options), count: rows.length, toast: options.toast === false ? false : { success: false } });
108
108
  // An import that wrote nothing at all is the one failure; it still answers with what it refused, where it says so.
109
109
  return { ...answer, result: answer.result === undefined ? undefined : backend.readImport(answer.result) };
110
110
  }
@@ -1,4 +1,4 @@
1
- import { type ThemeMode } from "../Interfaces/ThemeInterfaces.ts";
1
+ import { type AnimationMode, type Density, type ScaleGrid, type ThemeMode } from "../Interfaces/ThemeInterfaces.ts";
2
2
  export interface ThemeState {
3
3
  /** The active theme id. */
4
4
  theme: string;
@@ -6,12 +6,28 @@ export interface ThemeState {
6
6
  mode: ThemeMode;
7
7
  /** The mode actually shown. */
8
8
  resolvedMode: "light" | "dark";
9
+ density: Density;
10
+ /** The chosen animations, which may be `system`. */
11
+ animations: AnimationMode;
12
+ /** Whether things move now: the choice, or the system setting under `system`. */
13
+ motion: boolean;
14
+ /** The scale grid; always `off` outside a development build. */
15
+ grid: ScaleGrid;
9
16
  setTheme: (theme: string) => void;
10
17
  setMode: (mode: ThemeMode) => void;
18
+ setDensity: (density: Density) => void;
19
+ setAnimations: (animations: AnimationMode) => void;
20
+ setGrid: (grid: ScaleGrid) => void;
11
21
  }
12
22
  /** Switches the theme on this page and saves it for the next visit. */
13
23
  export declare function setTheme(theme: string): void;
14
24
  /** Switches light, dark, or system on this page and saves it for the next visit. */
15
25
  export declare function setThemeMode(mode: ThemeMode): void;
16
- /** The current theme and mode, and functions to change them. */
26
+ /** Switches compact or cozy on this page and saves it: every control and row follows at once. */
27
+ export declare function setDensity(density: Density): void;
28
+ /** Turns animations on, off, or back to the system setting on this page, and saves it. */
29
+ export declare function setAnimations(animations: AnimationMode): void;
30
+ /** Shows the scale grid (`px`, `cm`) or hides it (`off`), and saves it. Outside a development build it never shows. */
31
+ export declare function setScaleGrid(grid: ScaleGrid): void;
32
+ /** The current theme, mode, density, animations and scale grid, and functions to change them. */
17
33
  export declare function useTheme(): ThemeState;
@@ -1,11 +1,11 @@
1
1
  "use client";
2
2
  import { useSyncExternalStore } from "react";
3
- import { THEME_ID_PATTERN } from "../Helpers/ThemeHelpers.js";
4
- import { DEFAULT_THEME, THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, THEME_MODE_STORAGE_KEY, THEME_STORAGE_KEY, } from "../Interfaces/ThemeInterfaces.js";
3
+ import { isDevelopmentBuild, motionFor, REDUCED_MOTION_QUERY, THEME_ID_PATTERN } from "../Helpers/ThemeHelpers.js";
4
+ import { ANIMATIONS_ATTRIBUTE, ANIMATIONS_STORAGE_KEY, DEFAULT_THEME, DENSITY_ATTRIBUTE, DENSITY_STORAGE_KEY, GRID_ATTRIBUTE, GRID_STORAGE_KEY, MOTION_ATTRIBUTE, THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, THEME_MODE_STORAGE_KEY, THEME_STORAGE_KEY, } from "../Interfaces/ThemeInterfaces.js";
5
5
  const DARK_QUERY = "(prefers-color-scheme: dark)";
6
6
  // Before hydration there is no document; the real values follow right after.
7
- const SERVER_SNAPSHOT = `${DEFAULT_THEME} system light`;
8
- // Colors switch at once, instead of every button and border running its own transition.
7
+ const SERVER_SNAPSHOT = `${DEFAULT_THEME} system light compact system full off`;
8
+ // Colors and sizes switch at once, instead of every button and border running its own transition.
9
9
  function withoutTransitions(change) {
10
10
  const style = document.createElement("style");
11
11
  style.textContent = "*,*::before,*::after{transition:none!important}";
@@ -22,12 +22,33 @@ function save(key, value) {
22
22
  // Storage blocked (private mode, for example): the choice lasts until the page reloads.
23
23
  }
24
24
  }
25
+ /** Sets an attribute on `<html>`, or takes it away for the choice that means "nothing chosen". */
26
+ function mark(attribute, value, none) {
27
+ const root = document.documentElement;
28
+ if (value === none)
29
+ root.removeAttribute(attribute);
30
+ else
31
+ root.setAttribute(attribute, value);
32
+ }
33
+ const isMode = (value) => value === "light" || value === "dark" || value === "system";
34
+ const isDensity = (value) => value === "compact" || value === "cozy";
35
+ const isAnimations = (value) => value === "on" || value === "off" || value === "system";
36
+ const isGrid = (value) => value === "off" || value === "px" || value === "cm";
25
37
  function applyTheme(theme) {
26
38
  withoutTransitions(() => document.documentElement.setAttribute(THEME_ATTRIBUTE, theme));
27
39
  }
28
40
  function applyMode(mode) {
29
- const root = document.documentElement;
30
- withoutTransitions(() => (mode === "system" ? root.removeAttribute(THEME_MODE_ATTRIBUTE) : root.setAttribute(THEME_MODE_ATTRIBUTE, mode)));
41
+ withoutTransitions(() => mark(THEME_MODE_ATTRIBUTE, mode, "system"));
42
+ }
43
+ function applyDensity(density) {
44
+ withoutTransitions(() => document.documentElement.setAttribute(DENSITY_ATTRIBUTE, density));
45
+ }
46
+ function applyAnimations(animations) {
47
+ mark(ANIMATIONS_ATTRIBUTE, animations, "system");
48
+ document.documentElement.setAttribute(MOTION_ATTRIBUTE, motionFor(animations, window.matchMedia(REDUCED_MOTION_QUERY).matches));
49
+ }
50
+ function applyGrid(grid) {
51
+ mark(GRID_ATTRIBUTE, isDevelopmentBuild() ? grid : "off", "off");
31
52
  }
32
53
  /** Switches the theme on this page and saves it for the next visit. */
33
54
  export function setTheme(theme) {
@@ -41,27 +62,63 @@ export function setThemeMode(mode) {
41
62
  applyMode(mode);
42
63
  save(THEME_MODE_STORAGE_KEY, mode);
43
64
  }
65
+ /** Switches compact or cozy on this page and saves it: every control and row follows at once. */
66
+ export function setDensity(density) {
67
+ if (!isDensity(density))
68
+ return;
69
+ applyDensity(density);
70
+ save(DENSITY_STORAGE_KEY, density);
71
+ }
72
+ /** Turns animations on, off, or back to the system setting on this page, and saves it. */
73
+ export function setAnimations(animations) {
74
+ if (!isAnimations(animations))
75
+ return;
76
+ applyAnimations(animations);
77
+ save(ANIMATIONS_STORAGE_KEY, animations);
78
+ }
79
+ /** Shows the scale grid (`px`, `cm`) or hides it (`off`), and saves it. Outside a development build it never shows. */
80
+ export function setScaleGrid(grid) {
81
+ if (!isGrid(grid))
82
+ return;
83
+ applyGrid(grid);
84
+ save(GRID_STORAGE_KEY, grid);
85
+ }
44
86
  function readSnapshot() {
45
87
  const root = document.documentElement;
46
88
  const theme = root.getAttribute(THEME_ATTRIBUTE) || DEFAULT_THEME;
47
89
  const attribute = root.getAttribute(THEME_MODE_ATTRIBUTE);
48
90
  const mode = attribute === "light" || attribute === "dark" ? attribute : "system";
49
91
  const resolved = mode === "system" ? (window.matchMedia(DARK_QUERY).matches ? "dark" : "light") : mode;
50
- return `${theme} ${mode} ${resolved}`;
92
+ const density = root.getAttribute(DENSITY_ATTRIBUTE) === "cozy" ? "cozy" : "compact";
93
+ const chosen = root.getAttribute(ANIMATIONS_ATTRIBUTE);
94
+ const animations = chosen === "on" || chosen === "off" ? chosen : "system";
95
+ // Read as the stylesheets read it; a page without ThemeScript has no attribute, and its animations run.
96
+ const motion = root.getAttribute(MOTION_ATTRIBUTE) === "reduce" ? "reduce" : "full";
97
+ const grid = root.getAttribute(GRID_ATTRIBUTE);
98
+ return `${theme} ${mode} ${resolved} ${density} ${animations} ${motion} ${grid === "px" || grid === "cm" ? grid : "off"}`;
51
99
  }
52
100
  // The attributes on <html> are the source of truth, so every picker on the page stays in step,
53
101
  // including changes made by the system setting or in another tab.
54
102
  function subscribe(onChange) {
55
103
  const observer = new MutationObserver(onChange);
56
- observer.observe(document.documentElement, { attributes: true, attributeFilter: [THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE] });
104
+ observer.observe(document.documentElement, {
105
+ attributes: true,
106
+ attributeFilter: [THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, DENSITY_ATTRIBUTE, ANIMATIONS_ATTRIBUTE, MOTION_ATTRIBUTE, GRID_ATTRIBUTE],
107
+ });
57
108
  const media = window.matchMedia(DARK_QUERY);
58
109
  media.addEventListener("change", onChange);
59
110
  const onStorage = (event) => {
60
- if (event.key === THEME_STORAGE_KEY && event.newValue && THEME_ID_PATTERN.test(event.newValue))
61
- applyTheme(event.newValue);
62
- if (event.key === THEME_MODE_STORAGE_KEY && (event.newValue === "light" || event.newValue === "dark" || event.newValue === "system")) {
63
- applyMode(event.newValue);
64
- }
111
+ const value = event.newValue;
112
+ if (event.key === THEME_STORAGE_KEY && value && THEME_ID_PATTERN.test(value))
113
+ applyTheme(value);
114
+ else if (event.key === THEME_MODE_STORAGE_KEY && isMode(value))
115
+ applyMode(value);
116
+ else if (event.key === DENSITY_STORAGE_KEY && isDensity(value))
117
+ applyDensity(value);
118
+ else if (event.key === ANIMATIONS_STORAGE_KEY && isAnimations(value))
119
+ applyAnimations(value);
120
+ else if (event.key === GRID_STORAGE_KEY && isGrid(value))
121
+ applyGrid(value);
65
122
  };
66
123
  window.addEventListener("storage", onStorage);
67
124
  return () => {
@@ -70,14 +127,21 @@ function subscribe(onChange) {
70
127
  window.removeEventListener("storage", onStorage);
71
128
  };
72
129
  }
73
- /** The current theme and mode, and functions to change them. */
130
+ /** The current theme, mode, density, animations and scale grid, and functions to change them. */
74
131
  export function useTheme() {
75
- const [theme, mode, resolvedMode] = useSyncExternalStore(subscribe, readSnapshot, () => SERVER_SNAPSHOT).split(" ");
132
+ const [theme, mode, resolvedMode, density, animations, motion, grid] = useSyncExternalStore(subscribe, readSnapshot, () => SERVER_SNAPSHOT).split(" ");
76
133
  return {
77
134
  theme: theme ?? DEFAULT_THEME,
78
- mode: (mode ?? "system"),
135
+ mode: isMode(mode) ? mode : "system",
79
136
  resolvedMode: resolvedMode === "dark" ? "dark" : "light",
137
+ density: density === "cozy" ? "cozy" : "compact",
138
+ animations: isAnimations(animations) ? animations : "system",
139
+ motion: motion !== "reduce",
140
+ grid: isGrid(grid) ? grid : "off",
80
141
  setTheme,
81
142
  setMode: setThemeMode,
143
+ setDensity,
144
+ setAnimations,
145
+ setGrid: setScaleGrid,
82
146
  };
83
147
  }
@@ -32,6 +32,7 @@ export * from "./Helpers/NumberHelpers.ts";
32
32
  export * from "./Helpers/Permissions.ts";
33
33
  export * from "./Helpers/RichTextHelpers.ts";
34
34
  export * from "./Helpers/RowsStore.ts";
35
+ export * from "./Helpers/TableFilters.ts";
35
36
  export * from "./Helpers/TableHelpers.ts";
36
37
  export * from "./Helpers/TableStore.ts";
37
38
  export * from "./Helpers/ThemeHelpers.ts";
@@ -46,6 +47,7 @@ export * from "./Interfaces/ExcelInterfaces.ts";
46
47
  export * from "./Interfaces/FormInterfaces.ts";
47
48
  export * from "./Interfaces/InputInterfaces.ts";
48
49
  export * from "./Interfaces/MessageInterfaces.ts";
50
+ export * from "./Interfaces/PreferenceInterfaces.ts";
49
51
  export * from "./Interfaces/TableInterfaces.ts";
50
52
  export * from "./Interfaces/ThemeInterfaces.ts";
51
53
  export * from "./Interfaces/TypeInterfaces.ts";
@@ -35,6 +35,7 @@ export * from "./Helpers/NumberHelpers.js";
35
35
  export * from "./Helpers/Permissions.js";
36
36
  export * from "./Helpers/RichTextHelpers.js";
37
37
  export * from "./Helpers/RowsStore.js";
38
+ export * from "./Helpers/TableFilters.js";
38
39
  export * from "./Helpers/TableHelpers.js";
39
40
  export * from "./Helpers/TableStore.js";
40
41
  export * from "./Helpers/ThemeHelpers.js";
@@ -50,6 +51,7 @@ export * from "./Interfaces/ExcelInterfaces.js";
50
51
  export * from "./Interfaces/FormInterfaces.js";
51
52
  export * from "./Interfaces/InputInterfaces.js";
52
53
  export * from "./Interfaces/MessageInterfaces.js";
54
+ export * from "./Interfaces/PreferenceInterfaces.js";
53
55
  export * from "./Interfaces/TableInterfaces.js";
54
56
  export * from "./Interfaces/ThemeInterfaces.js";
55
57
  export * from "./Interfaces/TypeInterfaces.js";
package/package.json CHANGED
@@ -1,85 +1,85 @@
1
- {
2
- "name": "nexus-shared",
3
- "version": "3.0.0",
4
- "description": "The Nexus component library for React and Next.js: params-driven inputs and forms, a data table with a CRUD on top, popups and toasts, an API layer with permissions, Markdown and rich text editors, and 7 themes in light and dark.",
5
- "keywords": [
6
- "react",
7
- "nextjs",
8
- "components",
9
- "ui",
10
- "forms",
11
- "datatable",
12
- "crud",
13
- "inputs",
14
- "design-system",
15
- "typescript",
16
- "nepali-calendar",
17
- "nexus"
18
- ],
19
- "license": "MIT",
20
- "author": "Bijay Adhikari",
21
- "type": "module",
22
- "sideEffects": [
23
- "*.css"
24
- ],
25
- "exports": {
26
- ".": {
27
- "types": "./dist/Shared.Index.d.ts",
28
- "default": "./dist/Shared.Index.js"
29
- },
30
- "./client": {
31
- "types": "./dist/Client.Index.d.ts",
32
- "default": "./dist/Client.Index.js"
33
- },
34
- "./server": {
35
- "types": "./dist/Server.Index.d.ts",
36
- "default": "./dist/Server.Index.js"
37
- },
38
- "./styles.css": "./src/Styles/Nexus.Index.css",
39
- "./package.json": "./package.json"
40
- },
41
- "files": [
42
- "dist",
43
- "src/Styles",
44
- "CHANGELOG.md"
45
- ],
46
- "scripts": {
47
- "build": "tsc -p tsconfig.build.json",
48
- "typecheck": "tsc -p tsconfig.json",
49
- "test": "node scripts/test-all.mjs",
50
- "check:themes": "node scripts/check-themes.mjs",
51
- "test:api": "node scripts/test-api.mts",
52
- "test:chat": "node scripts/test-chat.mts",
53
- "test:dates": "node scripts/test-date-helpers.mts",
54
- "test:datetimes": "node scripts/test-datetime-helpers.mts",
55
- "test:drag": "node scripts/test-drag-helpers.mts",
56
- "test:excel": "node scripts/test-excel.mts",
57
- "test:dropdown": "node scripts/test-dropdown-helpers.mts",
58
- "test:forms": "node scripts/test-form-store.mts",
59
- "test:markdown": "node scripts/test-markdown.mts",
60
- "test:numbers": "node scripts/test-number-helpers.mts",
61
- "test:popups": "node scripts/test-popup-rules.mts",
62
- "test:richtext": "node scripts/test-richtext.mts",
63
- "test:rows": "node scripts/test-rows-store.mts",
64
- "test:table": "node scripts/test-table.mts",
65
- "test:times": "node scripts/test-time-helpers.mts",
66
- "test:tree": "node scripts/test-tree-store.mts",
67
- "test:types": "tsc -p scripts/type-checks/tsconfig.json"
68
- },
69
- "dependencies": {
70
- "nexus-icons": "^1.1.0"
71
- },
72
- "peerDependencies": {
73
- "react": ">=19",
74
- "react-dom": ">=19"
75
- },
76
- "devDependencies": {
77
- "@types/react": "19.3.0"
78
- },
79
- "engines": {
80
- "node": ">=20.9"
81
- },
82
- "publishConfig": {
83
- "access": "public"
84
- }
85
- }
1
+ {
2
+ "name": "nexus-shared",
3
+ "version": "3.1.0",
4
+ "description": "The Nexus component library for React and Next.js: params-driven inputs and forms, a data table with a CRUD on top, popups and toasts, an API layer with permissions, Markdown and rich text editors, and 7 themes in light and dark.",
5
+ "keywords": [
6
+ "react",
7
+ "nextjs",
8
+ "components",
9
+ "ui",
10
+ "forms",
11
+ "datatable",
12
+ "crud",
13
+ "inputs",
14
+ "design-system",
15
+ "typescript",
16
+ "nepali-calendar",
17
+ "nexus"
18
+ ],
19
+ "license": "MIT",
20
+ "author": "Bijay Adhikari",
21
+ "type": "module",
22
+ "sideEffects": [
23
+ "*.css"
24
+ ],
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/Shared.Index.d.ts",
28
+ "default": "./dist/Shared.Index.js"
29
+ },
30
+ "./client": {
31
+ "types": "./dist/Client.Index.d.ts",
32
+ "default": "./dist/Client.Index.js"
33
+ },
34
+ "./server": {
35
+ "types": "./dist/Server.Index.d.ts",
36
+ "default": "./dist/Server.Index.js"
37
+ },
38
+ "./styles.css": "./src/Styles/Nexus.Index.css",
39
+ "./package.json": "./package.json"
40
+ },
41
+ "files": [
42
+ "dist",
43
+ "src/Styles",
44
+ "CHANGELOG.md"
45
+ ],
46
+ "scripts": {
47
+ "build": "tsc -p tsconfig.build.json",
48
+ "typecheck": "tsc -p tsconfig.json",
49
+ "test": "node scripts/test-all.mjs",
50
+ "check:themes": "node scripts/check-themes.mjs",
51
+ "test:api": "node scripts/test-api.mts",
52
+ "test:chat": "node scripts/test-chat.mts",
53
+ "test:dates": "node scripts/test-date-helpers.mts",
54
+ "test:datetimes": "node scripts/test-datetime-helpers.mts",
55
+ "test:drag": "node scripts/test-drag-helpers.mts",
56
+ "test:excel": "node scripts/test-excel.mts",
57
+ "test:dropdown": "node scripts/test-dropdown-helpers.mts",
58
+ "test:forms": "node scripts/test-form-store.mts",
59
+ "test:markdown": "node scripts/test-markdown.mts",
60
+ "test:numbers": "node scripts/test-number-helpers.mts",
61
+ "test:popups": "node scripts/test-popup-rules.mts",
62
+ "test:richtext": "node scripts/test-richtext.mts",
63
+ "test:rows": "node scripts/test-rows-store.mts",
64
+ "test:table": "node scripts/test-table.mts",
65
+ "test:times": "node scripts/test-time-helpers.mts",
66
+ "test:tree": "node scripts/test-tree-store.mts",
67
+ "test:types": "tsc -p scripts/type-checks/tsconfig.json"
68
+ },
69
+ "dependencies": {
70
+ "nexus-icons": "^1.1.0"
71
+ },
72
+ "peerDependencies": {
73
+ "react": ">=19",
74
+ "react-dom": ">=19"
75
+ },
76
+ "devDependencies": {
77
+ "@types/react": "19.3.0"
78
+ },
79
+ "engines": {
80
+ "node": ">=20.9"
81
+ },
82
+ "publishConfig": {
83
+ "access": "public"
84
+ }
85
+ }