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.
- package/CHANGELOG.md +446 -262
- package/LICENSE +21 -21
- package/README.md +1084 -1000
- package/dist/Client.Index.d.ts +3 -0
- package/dist/Client.Index.js +3 -0
- package/dist/Components/Chats/ChatParts.d.ts +2 -2
- package/dist/Components/Chats/ChatParts.js +3 -7
- package/dist/Components/Chats/ChatThread.js +3 -3
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +21 -9
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +7 -5
- package/dist/Components/Documents/TabButtons.d.ts +5 -2
- package/dist/Components/Documents/TabButtons.js +2 -2
- package/dist/Components/Forms/ApiForm.d.ts +1 -1
- package/dist/Components/Forms/ApiForm.js +1 -1
- package/dist/Components/Forms/Crud.js +177 -58
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +5 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/NumberBox.js +24 -2
- package/dist/Components/Inputs/RowsInput.js +2 -2
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.js +42 -10
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
- package/dist/Components/Layouts/ThemePicker.js +77 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +192 -44
- package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
- package/dist/Components/Viewers/DataTableColumns.js +25 -68
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +2 -2
- package/dist/Components/Viewers/DataTableParts.js +3 -3
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/CrudBackend.d.ts +7 -4
- package/dist/Helpers/CrudBackend.js +4 -2
- package/dist/Helpers/ExcelBackend.d.ts +3 -3
- package/dist/Helpers/ExcelBackend.js +8 -2
- package/dist/Helpers/ExcelHelpers.d.ts +36 -8
- package/dist/Helpers/ExcelHelpers.js +55 -13
- package/dist/Helpers/FormStore.d.ts +9 -5
- package/dist/Helpers/FormStore.js +25 -5
- package/dist/Helpers/MessageBuilder.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +17 -5
- package/dist/Helpers/PopoverHelpers.js +52 -17
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +6 -3
- package/dist/Helpers/TableHelpers.js +8 -4
- package/dist/Helpers/TableStore.d.ts +11 -3
- package/dist/Helpers/TableStore.js +16 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/UserPreferences.d.ts +14 -0
- package/dist/Helpers/UserPreferences.js +99 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +33 -5
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +59 -12
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +2 -0
- package/dist/Shared.Index.js +2 -0
- package/package.json +85 -85
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -153
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -1489
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -161
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -774
- package/src/Styles/Nexus.Editor.css +295 -297
- package/src/Styles/Nexus.Excel.css +389 -370
- package/src/Styles/Nexus.Form.css +279 -321
- package/src/Styles/Nexus.Index.css +37 -36
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -259
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -596
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -419
- package/src/Styles/Nexus.Table.css +1386 -1296
- package/src/Styles/Nexus.Theme.Picker.css +286 -232
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -147
- package/src/Styles/Nexus.Toast.css +263 -265
- package/src/Styles/Nexus.Tokens.css +89 -72
- package/src/Styles/Nexus.Tree.css +431 -435
|
@@ -2,6 +2,7 @@ import { formatDate } from "./DateHelpers.js";
|
|
|
2
2
|
import { formatDateTime } from "./DateTimeHelpers.js";
|
|
3
3
|
import { foldSearchText, searchWords } from "./DropdownHelpers.js";
|
|
4
4
|
import { formatNumber } from "./NumberHelpers.js";
|
|
5
|
+
import { filterMatcher } from "./TableFilters.js";
|
|
5
6
|
import { formatTime } from "./TimeHelpers.js";
|
|
6
7
|
/*
|
|
7
8
|
* Data table helpers, with no React: reading a column's value, its text, searching, sorting, and paging rows, and the
|
|
@@ -83,7 +84,7 @@ export function formatCellText(column, value, options = {}) {
|
|
|
83
84
|
if (number === null)
|
|
84
85
|
return String(value);
|
|
85
86
|
const decimals = column.decimals ?? (column.format === "money" ? 2 : undefined);
|
|
86
|
-
const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true });
|
|
87
|
+
const text = formatNumber(number, { decimals, fixedDecimals: column.format === "money" || column.decimals !== undefined, grouping: true, locale: options.locale });
|
|
87
88
|
return `${prefix}${text}${column.format === "percent" && !suffix ? "%" : suffix}`;
|
|
88
89
|
}
|
|
89
90
|
case "date":
|
|
@@ -176,10 +177,10 @@ export function readTablePage(answer) {
|
|
|
176
177
|
return { rows, total, matches };
|
|
177
178
|
}
|
|
178
179
|
/**
|
|
179
|
-
* Searches, sorts, and pages rows the way the table shows them. `texts` caches each row's folded search text,
|
|
180
|
-
* search of 10,000 rows looks at each row once per keystroke.
|
|
180
|
+
* Searches, filters, sorts, and pages rows the way the table shows them. `texts` caches each row's folded search text,
|
|
181
|
+
* so a search of 10,000 rows looks at each row once per keystroke. `query.filters` are matched by `filterInputs`.
|
|
181
182
|
*/
|
|
182
|
-
export function queryRows(rows, columns, query, texts) {
|
|
183
|
+
export function queryRows(rows, columns, query, texts, filterInputs = []) {
|
|
183
184
|
let list = rows;
|
|
184
185
|
const words = searchWords(query.search);
|
|
185
186
|
if (words.length) {
|
|
@@ -193,6 +194,9 @@ export function queryRows(rows, columns, query, texts) {
|
|
|
193
194
|
return words.every(word => text.includes(word));
|
|
194
195
|
});
|
|
195
196
|
}
|
|
197
|
+
const test = filterMatcher(columns, filterInputs, query.filters);
|
|
198
|
+
if (test)
|
|
199
|
+
list = list.filter(test);
|
|
196
200
|
const sortColumn = query.sort ? columns.find(column => column.key === query.sort.key) : undefined;
|
|
197
201
|
if (sortColumn && query.sort)
|
|
198
202
|
list = sortRows(list, sortColumn, query.sort.direction);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InputParams } from "../Interfaces/FormInterfaces.ts";
|
|
1
2
|
import type { TableColumn, TableKey, TableLoader, TablePaging, TablePinSide, TableRow, TableSort, TableState } from "../Interfaces/TableInterfaces.ts";
|
|
2
3
|
/** What the table component tells its store on every render. */
|
|
3
4
|
export interface TableStoreOptions<TRow extends TableRow = TableRow> {
|
|
@@ -9,6 +10,8 @@ export interface TableStoreOptions<TRow extends TableRow = TableRow> {
|
|
|
9
10
|
paging: TablePaging;
|
|
10
11
|
maxSelection?: number;
|
|
11
12
|
onSelectionChange?: (rows: TRow[]) => void;
|
|
13
|
+
/** The advanced filter's inputs, as the table prepared them (`asFilterInput`). */
|
|
14
|
+
filterInputs?: readonly InputParams[];
|
|
12
15
|
}
|
|
13
16
|
/** How long a changed or added row stays highlighted. */
|
|
14
17
|
export declare const TABLE_FLASH_MS = 1600;
|
|
@@ -52,8 +55,8 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
52
55
|
getRow(key: TableKey): TRow | undefined;
|
|
53
56
|
getSelected(): TRow[];
|
|
54
57
|
/**
|
|
55
|
-
* Every row the table would show for its query, in order: the whole searched and sorted list for a client
|
|
56
|
-
* page a server sent for a server table. An export and a print write these.
|
|
58
|
+
* Every row the table would show for its query, in order: the whole searched, filtered and sorted list for a client
|
|
59
|
+
* table, the page a server sent for a server table. An export and a print write these.
|
|
57
60
|
*/
|
|
58
61
|
getViewRows(): TRow[];
|
|
59
62
|
isSelected(row: TRow): boolean;
|
|
@@ -78,11 +81,16 @@ export declare class TableStore<TRow extends TableRow = TableRow> {
|
|
|
78
81
|
reload({ quiet, reset }?: TableReloadOptions): Promise<void>;
|
|
79
82
|
private take;
|
|
80
83
|
private fail;
|
|
81
|
-
/** Searches, sorts, and pages every row (client tables), then tells the table. */
|
|
84
|
+
/** Searches, filters, sorts, and pages every row (client tables), then tells the table. */
|
|
82
85
|
private view;
|
|
83
86
|
/** Changes the query: a client table shows it at once, a server table loads it. */
|
|
84
87
|
private query;
|
|
85
88
|
setSearch(search: string): void;
|
|
89
|
+
/**
|
|
90
|
+
* Filters the rows by these values, from the first page; `{}` clears. Values are normalized with `activeFilters`;
|
|
91
|
+
* the same filters change nothing. A client table shows the result at once; a server table loads it.
|
|
92
|
+
*/
|
|
93
|
+
setFilters(values: Readonly<Record<string, unknown>>): void;
|
|
86
94
|
setSort(sort: TableSort | null): void;
|
|
87
95
|
/** Sorts by a column: ascending, then descending, then back to no sort. */
|
|
88
96
|
toggleSort(key: string): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { errorMessage, isAbortError } from "./ApiResponses.js";
|
|
2
|
+
import { activeFilters } from "./TableFilters.js";
|
|
2
3
|
import { DEFAULT_MAX_SELECTION, DEFAULT_PAGE_SIZE, MIN_COLUMN_WIDTH, clampPage, moveColumnKey, nextSort, queryRows, readTablePage, rowKeyText } from "./TableHelpers.js";
|
|
3
4
|
/** How long a changed or added row stays highlighted. */
|
|
4
5
|
export const TABLE_FLASH_MS = 1600;
|
|
@@ -76,13 +77,13 @@ export class TableStore {
|
|
|
76
77
|
return [...this.state.selected.values()];
|
|
77
78
|
}
|
|
78
79
|
/**
|
|
79
|
-
* Every row the table would show for its query, in order: the whole searched and sorted list for a client
|
|
80
|
-
* page a server sent for a server table. An export and a print write these.
|
|
80
|
+
* Every row the table would show for its query, in order: the whole searched, filtered and sorted list for a client
|
|
81
|
+
* table, the page a server sent for a server table. An export and a print write these.
|
|
81
82
|
*/
|
|
82
83
|
getViewRows() {
|
|
83
84
|
if (this.options.paging === "server")
|
|
84
85
|
return [...this.state.rows];
|
|
85
|
-
return queryRows(this.all, this.options.columns, { ...this.state.query, page: 1, pageSize: Number.MAX_SAFE_INTEGER }, this.texts).rows;
|
|
86
|
+
return queryRows(this.all, this.options.columns, { ...this.state.query, page: 1, pageSize: Number.MAX_SAFE_INTEGER }, this.texts, this.options.filterInputs).rows;
|
|
86
87
|
}
|
|
87
88
|
isSelected(row) {
|
|
88
89
|
return this.state.selected.has(this.keyOf(row));
|
|
@@ -220,14 +221,14 @@ export class TableStore {
|
|
|
220
221
|
this.controller = null;
|
|
221
222
|
this.set({ status: "error", refreshing: false, error: errorMessage(error, "The rows could not be loaded.") });
|
|
222
223
|
}
|
|
223
|
-
/** Searches, sorts, and pages every row (client tables), then tells the table. */
|
|
224
|
+
/** Searches, filters, sorts, and pages every row (client tables), then tells the table. */
|
|
224
225
|
view(extra = {}) {
|
|
225
226
|
if (this.options.paging === "server") {
|
|
226
227
|
this.set(extra);
|
|
227
228
|
return;
|
|
228
229
|
}
|
|
229
230
|
const query = { ...this.state.query, ...extra.query };
|
|
230
|
-
const result = queryRows(this.all, this.options.columns, query, this.texts);
|
|
231
|
+
const result = queryRows(this.all, this.options.columns, query, this.texts, this.options.filterInputs);
|
|
231
232
|
const page = result.page;
|
|
232
233
|
this.set({
|
|
233
234
|
...extra,
|
|
@@ -254,6 +255,16 @@ export class TableStore {
|
|
|
254
255
|
return;
|
|
255
256
|
this.query({ search: text, page: 1 });
|
|
256
257
|
}
|
|
258
|
+
/**
|
|
259
|
+
* Filters the rows by these values, from the first page; `{}` clears. Values are normalized with `activeFilters`;
|
|
260
|
+
* the same filters change nothing. A client table shows the result at once; a server table loads it.
|
|
261
|
+
*/
|
|
262
|
+
setFilters(values) {
|
|
263
|
+
const filters = activeFilters(this.options.filterInputs ?? [], values);
|
|
264
|
+
if (JSON.stringify(filters) === JSON.stringify(this.state.query.filters))
|
|
265
|
+
return;
|
|
266
|
+
this.query({ filters, page: 1 });
|
|
267
|
+
}
|
|
257
268
|
setSort(sort) {
|
|
258
269
|
this.query({ sort, page: 1 });
|
|
259
270
|
}
|
|
@@ -1,5 +1,14 @@
|
|
|
1
|
-
import { type ThemeMode, type ThemeOption } from "../Interfaces/ThemeInterfaces.ts";
|
|
1
|
+
import { type AnimationMode, type Density, type ThemeMode, type ThemeOption } from "../Interfaces/ThemeInterfaces.ts";
|
|
2
2
|
export declare const THEME_ID_PATTERN: RegExp;
|
|
3
|
+
/** The operating system's "less motion" setting, which `system` animations follow. */
|
|
4
|
+
export declare const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
5
|
+
/** What `data-nx-motion` says for an animations choice, given whether the operating system asks for less motion. */
|
|
6
|
+
export declare function motionFor(animations: AnimationMode, systemReduces: boolean): "reduce" | "full";
|
|
7
|
+
/**
|
|
8
|
+
* Whether this is a development build. Bundlers (Next.js, Vite, webpack) write the build's mode in place of
|
|
9
|
+
* `process.env.NODE_ENV`, and a server reads its own; a page with neither counts as production.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isDevelopmentBuild(): boolean;
|
|
3
12
|
export interface ThemeScriptOptions {
|
|
4
13
|
/** Themes a saved choice may name. Default: every built-in theme. */
|
|
5
14
|
themes?: readonly ThemeOption[];
|
|
@@ -7,9 +16,17 @@ export interface ThemeScriptOptions {
|
|
|
7
16
|
defaultTheme?: string;
|
|
8
17
|
/** Mode before the visitor chooses one. Default `system`. */
|
|
9
18
|
defaultMode?: ThemeMode;
|
|
19
|
+
/** Density before the visitor chooses one. Default `compact`. */
|
|
20
|
+
defaultDensity?: Density;
|
|
21
|
+
/** Animations before the visitor chooses. Default `system`, which follows `prefers-reduced-motion`. */
|
|
22
|
+
defaultAnimations?: AnimationMode;
|
|
23
|
+
/** Shows the saved scale grid. Default: in a development build only, so a grid left on never reaches production. */
|
|
24
|
+
grid?: boolean;
|
|
10
25
|
}
|
|
11
26
|
/**
|
|
12
|
-
* JavaScript that sets the saved theme and
|
|
13
|
-
* the
|
|
27
|
+
* JavaScript that sets the saved theme, mode, density, animations and scale grid on `<html>` before the page paints,
|
|
28
|
+
* so a page never flashes the defaults first. It also works out `data-nx-motion` from the animations choice and the
|
|
29
|
+
* system setting, and keeps it right when the system setting changes. `ThemeScript` renders it; use this directly
|
|
30
|
+
* only outside React.
|
|
14
31
|
*/
|
|
15
|
-
export declare function themeScript({ themes, defaultTheme, defaultMode }?: ThemeScriptOptions): string;
|
|
32
|
+
export declare function themeScript({ themes, defaultTheme, defaultMode, defaultDensity, defaultAnimations, grid, }?: ThemeScriptOptions): string;
|
|
@@ -1,16 +1,53 @@
|
|
|
1
|
-
import { DEFAULT_THEME, NEXUS_THEMES, THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, THEME_MODE_STORAGE_KEY, THEME_STORAGE_KEY, } from "../Interfaces/ThemeInterfaces.js";
|
|
1
|
+
import { ANIMATIONS_ATTRIBUTE, ANIMATIONS_STORAGE_KEY, DEFAULT_THEME, DENSITY_ATTRIBUTE, DENSITY_STORAGE_KEY, GRID_ATTRIBUTE, GRID_STORAGE_KEY, MOTION_ATTRIBUTE, NEXUS_THEMES, THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, THEME_MODE_STORAGE_KEY, THEME_STORAGE_KEY, } from "../Interfaces/ThemeInterfaces.js";
|
|
2
2
|
export const THEME_ID_PATTERN = /^[a-z0-9-]+$/;
|
|
3
|
+
/** The operating system's "less motion" setting, which `system` animations follow. */
|
|
4
|
+
export const REDUCED_MOTION_QUERY = "(prefers-reduced-motion: reduce)";
|
|
5
|
+
/** What `data-nx-motion` says for an animations choice, given whether the operating system asks for less motion. */
|
|
6
|
+
export function motionFor(animations, systemReduces) {
|
|
7
|
+
return animations === "off" || (animations !== "on" && systemReduces) ? "reduce" : "full";
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Whether this is a development build. Bundlers (Next.js, Vite, webpack) write the build's mode in place of
|
|
11
|
+
* `process.env.NODE_ENV`, and a server reads its own; a page with neither counts as production.
|
|
12
|
+
*/
|
|
13
|
+
export function isDevelopmentBuild() {
|
|
14
|
+
try {
|
|
15
|
+
return process.env.NODE_ENV !== "production";
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
3
21
|
/**
|
|
4
|
-
* JavaScript that sets the saved theme and
|
|
5
|
-
* the
|
|
22
|
+
* JavaScript that sets the saved theme, mode, density, animations and scale grid on `<html>` before the page paints,
|
|
23
|
+
* so a page never flashes the defaults first. It also works out `data-nx-motion` from the animations choice and the
|
|
24
|
+
* system setting, and keeps it right when the system setting changes. `ThemeScript` renders it; use this directly
|
|
25
|
+
* only outside React.
|
|
6
26
|
*/
|
|
7
|
-
export function themeScript({ themes = NEXUS_THEMES, defaultTheme = DEFAULT_THEME, defaultMode = "system" } = {}) {
|
|
8
|
-
const
|
|
9
|
-
|
|
27
|
+
export function themeScript({ themes = NEXUS_THEMES, defaultTheme = DEFAULT_THEME, defaultMode = "system", defaultDensity = "compact", defaultAnimations = "system", grid = isDevelopmentBuild(), } = {}) {
|
|
28
|
+
const config = {
|
|
29
|
+
ids: themes.map(theme => theme.id).filter(id => THEME_ID_PATTERN.test(id)),
|
|
30
|
+
theme: defaultTheme,
|
|
31
|
+
mode: defaultMode,
|
|
32
|
+
density: defaultDensity,
|
|
33
|
+
animations: defaultAnimations,
|
|
34
|
+
grid,
|
|
35
|
+
keys: [THEME_STORAGE_KEY, THEME_MODE_STORAGE_KEY, DENSITY_STORAGE_KEY, ANIMATIONS_STORAGE_KEY, GRID_STORAGE_KEY],
|
|
36
|
+
attributes: [THEME_ATTRIBUTE, THEME_MODE_ATTRIBUTE, DENSITY_ATTRIBUTE, ANIMATIONS_ATTRIBUTE, GRID_ATTRIBUTE, MOTION_ATTRIBUTE],
|
|
37
|
+
reduce: REDUCED_MOTION_QUERY,
|
|
38
|
+
};
|
|
10
39
|
// "<" is escaped so no value can close the <script> element.
|
|
11
40
|
const json = JSON.stringify(config).replace(/</g, "\\u003c");
|
|
12
|
-
return ("(function(c){var d=document.documentElement,t=c
|
|
13
|
-
"try{
|
|
14
|
-
"if(c
|
|
41
|
+
return ("(function(c){var d=document.documentElement,k=c.keys,a=c.attributes,t=c.theme,m=c.mode,n=c.density,o=c.animations,g='off',v;" +
|
|
42
|
+
"function r(i){try{return localStorage.getItem(k[i])}catch(e){return null}}" +
|
|
43
|
+
"v=r(0);if(c.ids.indexOf(v)>-1)t=v;v=r(1);if(v==='light'||v==='dark'||v==='system')m=v;" +
|
|
44
|
+
"v=r(2);if(v==='compact'||v==='cozy')n=v;v=r(3);if(v==='on'||v==='off'||v==='system')o=v;" +
|
|
45
|
+
"if(c.grid){v=r(4);if(v==='px'||v==='cm')g=v}" +
|
|
46
|
+
"if(c.ids.indexOf(t)>-1)d.setAttribute(a[0],t);if(m==='light'||m==='dark')d.setAttribute(a[1],m);" +
|
|
47
|
+
"d.setAttribute(a[2],n==='cozy'?n:'compact');if(o==='on'||o==='off')d.setAttribute(a[3],o);if(g!=='off')d.setAttribute(a[4],g);" +
|
|
48
|
+
// Motion follows the system setting live, read against whatever the animations choice is by then.
|
|
49
|
+
"var q=window.matchMedia?matchMedia(c.reduce):null;" +
|
|
50
|
+
"function f(){var x=d.getAttribute(a[3]);d.setAttribute(a[5],x==='off'||(x!=='on'&&q&&q.matches)?'reduce':'full')}" +
|
|
51
|
+
"f();if(q&&q.addEventListener)q.addEventListener('change',f)" +
|
|
15
52
|
`})(${json})`);
|
|
16
53
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type UserPreferences } from "../Interfaces/PreferenceInterfaces.ts";
|
|
2
|
+
/** The saved settings, with the defaults for anything missing or invalid. Safe to call on the server. */
|
|
3
|
+
export declare function readUserPreferences(): UserPreferences;
|
|
4
|
+
/** Saves settings and updates every component that shows them. Settings left out keep their value. */
|
|
5
|
+
export declare function setUserPreferences(changes: Partial<UserPreferences>): void;
|
|
6
|
+
/** Calls back when the settings change here or in another tab. Returns the function that stops listening. */
|
|
7
|
+
export declare function subscribeUserPreferences(listener: () => void): () => void;
|
|
8
|
+
/** The current settings; the component renders again when they change. */
|
|
9
|
+
export declare function useUserPreferences(): UserPreferences;
|
|
10
|
+
/**
|
|
11
|
+
* The locale for the person's number settings (`numberLocale`): what a number box, a slider and a table's number cells
|
|
12
|
+
* use when they are given no `locale` of their own.
|
|
13
|
+
*/
|
|
14
|
+
export declare function useNumberLocale(): string;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useSyncExternalStore } from "react";
|
|
3
|
+
import { DEFAULT_USER_PREFERENCES, ENumberScales, PREFERENCE_STORAGE_KEYS } from "../Interfaces/PreferenceInterfaces.js";
|
|
4
|
+
import { numberLocale } from "./NumberHelpers.js";
|
|
5
|
+
/*
|
|
6
|
+
* The person's settings for the interface language, numbers and tables, saved in local storage under the
|
|
7
|
+
* PREFERENCE_STORAGE_KEYS and read by the components that show them: a change here redraws every number and table on
|
|
8
|
+
* the page at once, and in other open tabs. The same pattern as the date settings (DatePreferences.ts). On the server
|
|
9
|
+
* there is no storage, so pages render with the defaults and take the saved settings right after hydration.
|
|
10
|
+
*/
|
|
11
|
+
const listeners = new Set();
|
|
12
|
+
let snapshot = DEFAULT_USER_PREFERENCES;
|
|
13
|
+
const SEPARATORS = ["comma", "period", "space", "none"];
|
|
14
|
+
function read(key) {
|
|
15
|
+
try {
|
|
16
|
+
return localStorage.getItem(key);
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** The saved settings, with the defaults for anything missing or invalid. Safe to call on the server. */
|
|
23
|
+
export function readUserPreferences() {
|
|
24
|
+
if (typeof localStorage === "undefined")
|
|
25
|
+
return DEFAULT_USER_PREFERENCES;
|
|
26
|
+
const language = read(PREFERENCE_STORAGE_KEYS.language);
|
|
27
|
+
const scale = Number(read(PREFERENCE_STORAGE_KEYS.numberScale));
|
|
28
|
+
const thousands = read(PREFERENCE_STORAGE_KEYS.thousands);
|
|
29
|
+
const pageSize = Number(read(PREFERENCE_STORAGE_KEYS.pageSize));
|
|
30
|
+
const rowClick = read(PREFERENCE_STORAGE_KEYS.rowClick);
|
|
31
|
+
const next = {
|
|
32
|
+
language: language === "ne" ? "ne" : DEFAULT_USER_PREFERENCES.language,
|
|
33
|
+
numberScale: scale === ENumberScales.SouthAsian ? ENumberScales.SouthAsian : DEFAULT_USER_PREFERENCES.numberScale,
|
|
34
|
+
thousands: thousands && SEPARATORS.includes(thousands) ? thousands : DEFAULT_USER_PREFERENCES.thousands,
|
|
35
|
+
pageSize: Number.isInteger(pageSize) && pageSize > 0 ? pageSize : DEFAULT_USER_PREFERENCES.pageSize,
|
|
36
|
+
rowClick: rowClick === "1" ? true : rowClick === "0" ? false : DEFAULT_USER_PREFERENCES.rowClick,
|
|
37
|
+
};
|
|
38
|
+
// The same object while nothing changed, so React sees a stable snapshot.
|
|
39
|
+
const keys = Object.keys(next);
|
|
40
|
+
if (keys.some(key => next[key] !== snapshot[key]))
|
|
41
|
+
snapshot = next;
|
|
42
|
+
return snapshot;
|
|
43
|
+
}
|
|
44
|
+
/** Saves settings and updates every component that shows them. Settings left out keep their value. */
|
|
45
|
+
export function setUserPreferences(changes) {
|
|
46
|
+
try {
|
|
47
|
+
if (changes.language !== undefined)
|
|
48
|
+
localStorage.setItem(PREFERENCE_STORAGE_KEYS.language, changes.language);
|
|
49
|
+
if (changes.numberScale !== undefined)
|
|
50
|
+
localStorage.setItem(PREFERENCE_STORAGE_KEYS.numberScale, String(changes.numberScale));
|
|
51
|
+
if (changes.thousands !== undefined)
|
|
52
|
+
localStorage.setItem(PREFERENCE_STORAGE_KEYS.thousands, changes.thousands);
|
|
53
|
+
// `null` goes back to each table's own.
|
|
54
|
+
if (changes.pageSize === null)
|
|
55
|
+
localStorage.removeItem(PREFERENCE_STORAGE_KEYS.pageSize);
|
|
56
|
+
else if (changes.pageSize !== undefined)
|
|
57
|
+
localStorage.setItem(PREFERENCE_STORAGE_KEYS.pageSize, String(changes.pageSize));
|
|
58
|
+
if (changes.rowClick === null)
|
|
59
|
+
localStorage.removeItem(PREFERENCE_STORAGE_KEYS.rowClick);
|
|
60
|
+
else if (changes.rowClick !== undefined)
|
|
61
|
+
localStorage.setItem(PREFERENCE_STORAGE_KEYS.rowClick, changes.rowClick ? "1" : "0");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Storage blocked (private mode, for example): the components keep what they have.
|
|
65
|
+
}
|
|
66
|
+
for (const listener of listeners)
|
|
67
|
+
listener();
|
|
68
|
+
}
|
|
69
|
+
// One storage listener for the page, however many components are mounted.
|
|
70
|
+
const STORAGE_KEY_LIST = Object.values(PREFERENCE_STORAGE_KEYS);
|
|
71
|
+
const onStorage = (event) => {
|
|
72
|
+
if (event.key === null || STORAGE_KEY_LIST.includes(event.key))
|
|
73
|
+
for (const listener of listeners)
|
|
74
|
+
listener();
|
|
75
|
+
};
|
|
76
|
+
/** Calls back when the settings change here or in another tab. Returns the function that stops listening. */
|
|
77
|
+
export function subscribeUserPreferences(listener) {
|
|
78
|
+
if (listeners.size === 0)
|
|
79
|
+
window.addEventListener("storage", onStorage);
|
|
80
|
+
listeners.add(listener);
|
|
81
|
+
return () => {
|
|
82
|
+
listeners.delete(listener);
|
|
83
|
+
if (listeners.size === 0)
|
|
84
|
+
window.removeEventListener("storage", onStorage);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
const serverSnapshot = () => DEFAULT_USER_PREFERENCES;
|
|
88
|
+
/** The current settings; the component renders again when they change. */
|
|
89
|
+
export function useUserPreferences() {
|
|
90
|
+
return useSyncExternalStore(subscribeUserPreferences, readUserPreferences, serverSnapshot);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The locale for the person's number settings (`numberLocale`): what a number box, a slider and a table's number cells
|
|
94
|
+
* use when they are given no `locale` of their own.
|
|
95
|
+
*/
|
|
96
|
+
export function useNumberLocale() {
|
|
97
|
+
const { numberScale, thousands } = useUserPreferences();
|
|
98
|
+
return numberLocale(numberScale, thousands);
|
|
99
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode, Ref } from "react";
|
|
2
2
|
import type { PopupSize } from "../Components/Layouts/Popup.tsx";
|
|
3
|
+
import type { PopupHandle } from "../Components/Layouts/PopupHost.tsx";
|
|
3
4
|
import type { FormStore } from "../Helpers/FormStore.ts";
|
|
4
5
|
import type { TableStore } from "../Helpers/TableStore.ts";
|
|
5
6
|
import type { ApiEndpoint } from "./ApiInterfaces.ts";
|
|
@@ -128,6 +129,24 @@ export interface CrudDetailsOptions<TRow extends FormValues = FormValues> {
|
|
|
128
129
|
* "{Subject} {label}". Default `details`. A popup whose panel is the point says so: `schema`, `preview`.
|
|
129
130
|
*/
|
|
130
131
|
label?: string;
|
|
132
|
+
/** Buttons of the page's own in the footer, before Edit and Close: "View history". */
|
|
133
|
+
actions?: readonly CrudDetailsAction<TRow>[];
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* A button in the details popup's footer. One that names the endpoint it calls shows only to a user who may call
|
|
137
|
+
* it, by the same rule every built-in action follows - so it appears and disappears with the user's rights, with
|
|
138
|
+
* nothing to change here when they come from the backend.
|
|
139
|
+
*/
|
|
140
|
+
export interface CrudDetailsAction<TRow extends FormValues = FormValues> {
|
|
141
|
+
id: string;
|
|
142
|
+
label: string;
|
|
143
|
+
icon?: ReactNode;
|
|
144
|
+
/** What it calls: the button shows only when the user may call it. */
|
|
145
|
+
endpoint?: ApiEndpoint;
|
|
146
|
+
/** Hidden for this record, by a rule of the page's own. */
|
|
147
|
+
hidden?: (row: TRow) => boolean;
|
|
148
|
+
/** On a click, with the record and the popup (close it, or leave it open beside what this opens). */
|
|
149
|
+
onSelect: (row: TRow, popup: PopupHandle) => void;
|
|
131
150
|
}
|
|
132
151
|
/** What changed, for `onChange`. */
|
|
133
152
|
export interface CrudChange<TRow extends FormValues = FormValues> {
|
|
@@ -137,7 +156,7 @@ export interface CrudChange<TRow extends FormValues = FormValues> {
|
|
|
137
156
|
mode: CrudMode;
|
|
138
157
|
}
|
|
139
158
|
/** Table settings a CRUD passes on to its `DataTable`. */
|
|
140
|
-
export type CrudTableOptions<TRow extends FormValues = FormValues> = Pick<DataTableProps<TRow>, "pageSize" | "pageSizes" | "defaultSort" | "numbered" | "maxHeight" | "fill" | "excel" | "size" | "searchPlaceholder" | "maxSelection" | "emptyMessage" | "columnsMenu" | "stateKey" | "searchDelay" | "reorderable" | "resizable" | "pinnable" | "moreActions" | "exportable" | "printable" | "exportName" | "onImport" | "openOnClick">;
|
|
159
|
+
export type CrudTableOptions<TRow extends FormValues = FormValues> = Pick<DataTableProps<TRow>, "pageSize" | "pageSizes" | "defaultSort" | "numbered" | "maxHeight" | "fill" | "excel" | "size" | "searchPlaceholder" | "maxSelection" | "emptyMessage" | "columnsMenu" | "stateKey" | "searchDelay" | "filterInputs" | "reorderable" | "resizable" | "pinnable" | "moreActions" | "exportable" | "printable" | "exportName" | "onImport" | "openOnClick">;
|
|
141
160
|
/** Props of `Crud`. */
|
|
142
161
|
export interface CrudProps<TRow extends FormValues = FormValues> {
|
|
143
162
|
/** The endpoints: `MODULES.controller(NexusModule.Reservation, "Guest", "guest")`, or an object of your own. */
|
|
@@ -84,6 +84,18 @@ export interface ExcelSettings {
|
|
|
84
84
|
* what the editors draw from until it is not.
|
|
85
85
|
*/
|
|
86
86
|
inputs?: readonly unknown[];
|
|
87
|
+
/**
|
|
88
|
+
* The statuses a new record may start in when it is imported, each with its words: the import offers them as a
|
|
89
|
+
* choice. Empty, or absent, when the model offers none, and then no choice is shown.
|
|
90
|
+
*/
|
|
91
|
+
recordStatuses?: ExcelRecordStatus[];
|
|
92
|
+
/** The status picked unless the person picks another: the backend's "Imported". */
|
|
93
|
+
defaultRecordStatus?: number;
|
|
94
|
+
}
|
|
95
|
+
/** One status an imported record may start in: the backend's number, and the words for it. */
|
|
96
|
+
export interface ExcelRecordStatus {
|
|
97
|
+
value: number;
|
|
98
|
+
label: string;
|
|
87
99
|
}
|
|
88
100
|
/** One thing wrong with one cell, told so the person can find it. */
|
|
89
101
|
export interface ExcelCellProblem {
|
|
@@ -213,8 +213,13 @@ export interface RowsInputHandle<TRow extends RowValue = RowValue> {
|
|
|
213
213
|
setValue: (rows: readonly TRow[]) => void;
|
|
214
214
|
/** Adds a row with these values (over `newRow`), at `index` or at the end, and focuses its first input. */
|
|
215
215
|
add: (values?: Partial<TRow>, index?: number) => void;
|
|
216
|
-
/**
|
|
217
|
-
|
|
216
|
+
/**
|
|
217
|
+
* Removes the row at `index`, with an Undo under the rows - or none with `undo: false`, for a row the code drops
|
|
218
|
+
* rather than the person, such as one nothing was typed in.
|
|
219
|
+
*/
|
|
220
|
+
remove: (index: number, options?: {
|
|
221
|
+
undo?: boolean;
|
|
222
|
+
}) => void;
|
|
218
223
|
/** Checks every row, shows every message, and focuses the first problem. Returns whether all is valid. */
|
|
219
224
|
validate: () => boolean;
|
|
220
225
|
/** Brings the row at `index` into view and focuses its input named `name`, or its first input. */
|
|
@@ -228,6 +233,17 @@ export interface TabularFormProps<TRow extends RowValue = RowValue> extends Rows
|
|
|
228
233
|
maxHeight?: number | string;
|
|
229
234
|
/** The row number column. Default true; it is also the button of each row's menu. */
|
|
230
235
|
numbered?: boolean;
|
|
236
|
+
/**
|
|
237
|
+
* Where each row's actions sit. `"number"` (the default): a menu beside the row's number. `"end"`: a column fixed at
|
|
238
|
+
* the right edge, as a data table has them - Remove and Lock as buttons of their own, any other action in a menu
|
|
239
|
+
* after them.
|
|
240
|
+
*/
|
|
241
|
+
actionsAt?: "number" | "end";
|
|
242
|
+
/**
|
|
243
|
+
* The person can lock a row, and unlock it: a locked row's inputs are read-only and it cannot be removed. The locks
|
|
244
|
+
* are the screen's, never part of the value - a locked row is still sent. Its button is with the row's actions.
|
|
245
|
+
*/
|
|
246
|
+
lockable?: boolean;
|
|
231
247
|
}
|
|
232
248
|
/** `GroupForm`: each row as a section of inputs, laid out on a 12-column grid by each input's `span`. */
|
|
233
249
|
export interface GroupFormProps<TRow extends RowValue = RowValue> extends RowsInputProps<TRow> {
|
|
@@ -292,7 +308,10 @@ export interface FormProps<TValues extends FormValues = FormValues> {
|
|
|
292
308
|
readOnly?: boolean;
|
|
293
309
|
/** Every input is disabled, and nothing is checked. */
|
|
294
310
|
disabled?: boolean;
|
|
295
|
-
/**
|
|
311
|
+
/**
|
|
312
|
+
* The values are loading: the inputs show at once, disabled, so the form has its final size from the first frame, and
|
|
313
|
+
* nothing typed is taken (even with `disabled` taken off by hand) until it ends. A submit waits too.
|
|
314
|
+
*/
|
|
296
315
|
loading?: boolean;
|
|
297
316
|
id?: string;
|
|
298
317
|
className?: string;
|
|
@@ -325,6 +344,13 @@ export type FormSubmitResult<TValues extends FormValues = FormValues> = void | u
|
|
|
325
344
|
};
|
|
326
345
|
/** Where `SubmitForm` puts its buttons: at the end with the status line before them, at the start, or nowhere (`FormButtons` elsewhere). */
|
|
327
346
|
export type FormButtonsPlacement = "end" | "start" | false;
|
|
347
|
+
/**
|
|
348
|
+
* What a form's status line reports. `full`: loading, saving, what needs attention, unsaved changes, and when it saved.
|
|
349
|
+
* `errors`: only an error that belongs to no field, such as the server's "The sign-in took too long", for a short form
|
|
350
|
+
* that keeps nothing (a sign-in, a search, a code to verify): "Unsaved changes" would be untrue there, the button's
|
|
351
|
+
* spinner already says it is working, and every field is in view with its own message under it.
|
|
352
|
+
*/
|
|
353
|
+
export type FormStatusLineMode = "full" | "errors";
|
|
328
354
|
/** Props of `SubmitForm`: a `Form` in a `<form>`, checked on submit, with Save, Cancel, and Reset. */
|
|
329
355
|
export interface SubmitFormProps<TValues extends FormValues = FormValues> extends FormProps<TValues> {
|
|
330
356
|
/**
|
|
@@ -353,6 +379,8 @@ export interface SubmitFormProps<TValues extends FormValues = FormValues> extend
|
|
|
353
379
|
resetOnSuccess?: boolean;
|
|
354
380
|
/** Where the buttons go. Default `end`. With `false`, put a `FormButtons` with the same `form` elsewhere, such as a popup's footer. */
|
|
355
381
|
buttons?: FormButtonsPlacement;
|
|
382
|
+
/** What the status line reports. Default `full`; `errors` for a short form that keeps nothing, such as a sign-in or a search. */
|
|
383
|
+
statusLine?: FormStatusLineMode;
|
|
356
384
|
/** Asks before the page closes or reloads with unsaved changes. Default: true without a `draftKey` (a draft keeps them). */
|
|
357
385
|
confirmLeave?: boolean;
|
|
358
386
|
/** Name for screen readers. */
|
|
@@ -394,8 +422,8 @@ export interface ApiFormProps<TValues extends FormValues = FormValues> extends O
|
|
|
394
422
|
/** The toast after a save: text, or `false` for none. Default: the server's message, else the endpoint's ("Room saved."), else "Saved." */
|
|
395
423
|
successMessage?: ReactNode | false;
|
|
396
424
|
/**
|
|
397
|
-
* Loads the values to edit, e.g. a record by id.
|
|
398
|
-
* once; give the form a new `key` to load another record.
|
|
425
|
+
* Loads the values to edit, e.g. a record by id. The inputs show disabled while it runs, and a failure offers Retry.
|
|
426
|
+
* It runs once; give the form a new `key` to load another record.
|
|
399
427
|
*/
|
|
400
428
|
loadValue?: (signal: AbortSignal) => Promise<Partial<TValues> | null | undefined>;
|
|
401
429
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** The words of the app itself. Data keeps the language it was entered in. */
|
|
2
|
+
export type InterfaceLanguage = "en" | "ne";
|
|
3
|
+
/**
|
|
4
|
+
* The core's `ENumberScales`, by member value: where the groups of a number fall. `International` writes 1,234,567
|
|
5
|
+
* (thousand, million, billion); `SouthAsian` writes 12,34,567 (thousand, lakh, crore).
|
|
6
|
+
*/
|
|
7
|
+
export declare const ENumberScales: {
|
|
8
|
+
readonly International: 0;
|
|
9
|
+
readonly SouthAsian: 1;
|
|
10
|
+
};
|
|
11
|
+
export type ENumberScales = (typeof ENumberScales)[keyof typeof ENumberScales];
|
|
12
|
+
/**
|
|
13
|
+
* The mark between groups of digits: 1,200, 1.200, 1 200 (a no-break space) or 1200. A period between the groups
|
|
14
|
+
* takes a comma for the decimal mark, as it does in much of Europe; every other choice keeps the period.
|
|
15
|
+
*/
|
|
16
|
+
export type ThousandsSeparator = "comma" | "period" | "space" | "none";
|
|
17
|
+
export interface UserPreferences {
|
|
18
|
+
/** Default `en`. */
|
|
19
|
+
language: InterfaceLanguage;
|
|
20
|
+
/** Default `International`. */
|
|
21
|
+
numberScale: ENumberScales;
|
|
22
|
+
/** Default `comma`. */
|
|
23
|
+
thousands: ThousandsSeparator;
|
|
24
|
+
/**
|
|
25
|
+
* The rows every table opens with, over the size the table names itself, when the table offers it; a size picked in a
|
|
26
|
+
* table with a `stateKey` still wins there. `null` (the default): not chosen, each table's own.
|
|
27
|
+
*/
|
|
28
|
+
pageSize: number | null;
|
|
29
|
+
/**
|
|
30
|
+
* A single click on a row opens its record (`true`) or only a double-click does (`false`), in every table, over what
|
|
31
|
+
* the table says itself. `null` (the default): not chosen, each table's own `openOnClick`.
|
|
32
|
+
*/
|
|
33
|
+
rowClick: boolean | null;
|
|
34
|
+
}
|
|
35
|
+
export declare const DEFAULT_USER_PREFERENCES: UserPreferences;
|
|
36
|
+
/** Local storage keys of the settings. A number scale is kept as its member value ("0", "1"), a row click as "1" or "0". */
|
|
37
|
+
export declare const PREFERENCE_STORAGE_KEYS: {
|
|
38
|
+
readonly language: "nx-language";
|
|
39
|
+
readonly numberScale: "nx-number-scale";
|
|
40
|
+
readonly thousands: "nx-thousands";
|
|
41
|
+
readonly pageSize: "nx-page-size";
|
|
42
|
+
readonly rowClick: "nx-row-click";
|
|
43
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The person's own settings beyond the theme and the dates: the interface language, how numbers are written, and what a
|
|
3
|
+
* table does before anyone touches its own controls. Saved on this device and read by the components that show them,
|
|
4
|
+
* so a change applies to everything on the page at once. The date settings are `DatePreferences` (DateInterfaces.ts).
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* The core's `ENumberScales`, by member value: where the groups of a number fall. `International` writes 1,234,567
|
|
8
|
+
* (thousand, million, billion); `SouthAsian` writes 12,34,567 (thousand, lakh, crore).
|
|
9
|
+
*/
|
|
10
|
+
export const ENumberScales = {
|
|
11
|
+
International: 0,
|
|
12
|
+
SouthAsian: 1,
|
|
13
|
+
};
|
|
14
|
+
export const DEFAULT_USER_PREFERENCES = {
|
|
15
|
+
language: "en",
|
|
16
|
+
numberScale: ENumberScales.International,
|
|
17
|
+
thousands: "comma",
|
|
18
|
+
pageSize: null,
|
|
19
|
+
rowClick: null,
|
|
20
|
+
};
|
|
21
|
+
/** Local storage keys of the settings. A number scale is kept as its member value ("0", "1"), a row click as "1" or "0". */
|
|
22
|
+
export const PREFERENCE_STORAGE_KEYS = {
|
|
23
|
+
language: "nx-language",
|
|
24
|
+
numberScale: "nx-number-scale",
|
|
25
|
+
thousands: "nx-thousands",
|
|
26
|
+
pageSize: "nx-page-size",
|
|
27
|
+
rowClick: "nx-row-click",
|
|
28
|
+
};
|