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
|
@@ -3,7 +3,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
3
3
|
import { InlineIcon, iconMinus, iconPlus } from "nexus-icons";
|
|
4
4
|
import { useEffect, useLayoutEffect, useRef, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
-
import { clampNumber, cleanNumberText,
|
|
6
|
+
import { clampNumber, cleanNumberText, formatNumber, getNumberSymbols, isPartialNumber, parseNumber, readPastedNumber, roundDecimal, stepNumber, } from "../../Helpers/NumberHelpers.js";
|
|
7
|
+
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
7
8
|
import { validateNumber } from "../../Helpers/ValidationHelpers.js";
|
|
8
9
|
import { Field, hasContent } from "./Field.js";
|
|
9
10
|
import { setInputValue, useInputField } from "./InputField.js";
|
|
@@ -23,7 +24,10 @@ const isNumber = (value) => value !== null && !Number.isNaN(value);
|
|
|
23
24
|
* @example <NumberBox label="Amount" prefix="Rs" min={0} decimals={2} fixedDecimals grouping required />
|
|
24
25
|
*/
|
|
25
26
|
export function NumberBox(props) {
|
|
26
|
-
const { label, hint, error, design = "standard", size = "md", icon, iconEnd, prefix, suffix, step = 1, stepper = false, clamp = false, readOnlyMessage = "Read-only: this number cannot be changed.", fieldName, validateOn, value, defaultValue, onValueChange, decimals, fixedDecimals = false, grouping = false, locale
|
|
27
|
+
const { label, hint, error, design = "standard", size = "md", icon, iconEnd, prefix, suffix, step = 1, stepper = false, clamp = false, readOnlyMessage = "Read-only: this number cannot be changed.", fieldName, validateOn, value, defaultValue, onValueChange, decimals, fixedDecimals = false, grouping = false, locale: ownLocale, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onPaste, onClick, ...inputProps } = props;
|
|
28
|
+
// Without a locale of its own, the box writes and reads numbers the way the person chose (Number scale, Thousands).
|
|
29
|
+
const preferred = useNumberLocale();
|
|
30
|
+
const locale = ownLocale ?? preferred;
|
|
27
31
|
const format = { decimals, fixedDecimals, grouping, locale };
|
|
28
32
|
const { decimal, group } = getNumberSymbols(locale);
|
|
29
33
|
const textRules = { decimals, negative: min === undefined || min < 0 };
|
|
@@ -55,8 +59,11 @@ export function NumberBox(props) {
|
|
|
55
59
|
const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, {
|
|
56
60
|
keys: ["ArrowUp", "ArrowDown", "PageUp", "PageDown"],
|
|
57
61
|
});
|
|
62
|
+
// The number the box holds, as last read in the marks it was written in.
|
|
63
|
+
const numberRef = useRef(initial);
|
|
58
64
|
/** Writes what depends on the value straight to the DOM, so typing re-renders nothing: the hidden input and the button limits. */
|
|
59
65
|
function sync(number) {
|
|
66
|
+
numberRef.current = number;
|
|
60
67
|
if (hiddenRef.current)
|
|
61
68
|
hiddenRef.current.value = isNumber(number) ? String(number) : "";
|
|
62
69
|
for (const [button, direction] of [
|
|
@@ -69,6 +76,21 @@ export function NumberBox(props) {
|
|
|
69
76
|
button?.removeAttribute("aria-disabled");
|
|
70
77
|
}
|
|
71
78
|
}
|
|
79
|
+
// The person changed how numbers are written (Number scale, Thousands): the number the box held is written again in
|
|
80
|
+
// the new marks. Its text is not read again, because by now it may be either: React has already rewritten an untouched
|
|
81
|
+
// box's default text, and "1,250.5" read with a decimal comma would be 1.2505. Runs before the effects below read it.
|
|
82
|
+
const localeRef = useRef(locale);
|
|
83
|
+
useLayoutEffect(() => {
|
|
84
|
+
const previous = localeRef.current;
|
|
85
|
+
localeRef.current = locale;
|
|
86
|
+
const input = field.elementRef.current;
|
|
87
|
+
const number = numberRef.current;
|
|
88
|
+
if (previous === locale || !input || !input.value || !isNumber(number))
|
|
89
|
+
return;
|
|
90
|
+
input.value = document.activeElement === input ? editText(number) : displayText(number);
|
|
91
|
+
sync(number);
|
|
92
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
93
|
+
}, [locale]);
|
|
72
94
|
// A controlled value that differs from the box's text replaces it. Text that already means this number ("12.") stays.
|
|
73
95
|
useLayoutEffect(() => {
|
|
74
96
|
const input = field.elementRef.current;
|
|
@@ -135,10 +135,10 @@ export function useRowsInput(props, fallbackItem, layoutRef) {
|
|
|
135
135
|
add: (values, index) => {
|
|
136
136
|
actions.add(values, index);
|
|
137
137
|
},
|
|
138
|
-
remove: index => {
|
|
138
|
+
remove: (index, options) => {
|
|
139
139
|
const key = store.keyAt(index);
|
|
140
140
|
if (key)
|
|
141
|
-
store.remove(key);
|
|
141
|
+
store.remove(key, options);
|
|
142
142
|
},
|
|
143
143
|
validate: () => {
|
|
144
144
|
const place = store.validateAll();
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { Fragment, useEffect, useEffectEvent, useRef, } from "react";
|
|
4
4
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
5
|
import { clampNumber, countDecimals, formatNumber, roundDecimal } from "../../Helpers/NumberHelpers.js";
|
|
6
|
+
import { useNumberLocale } from "../../Helpers/UserPreferences.js";
|
|
6
7
|
import { GroupFooter, leavesField, OPTION_ATTRIBUTE, useChoiceField } from "./ChoiceField.js";
|
|
7
8
|
import { hasContent } from "./Field.js";
|
|
8
9
|
import { setInputValue } from "./InputField.js";
|
|
@@ -54,7 +55,9 @@ function SliderField(props) {
|
|
|
54
55
|
const { readOnlyMessage = range ? "Read-only: this range cannot be changed." : "Read-only: this value cannot be changed." } = props;
|
|
55
56
|
const span = max - min || 1;
|
|
56
57
|
const initial = (values ?? defaultValues).map(value => clampNumber(value, min, max));
|
|
57
|
-
|
|
58
|
+
// Without a locale of its own, the values are written the way the person chose.
|
|
59
|
+
const numbers = useNumberLocale();
|
|
60
|
+
const text = (value) => (format ? format(value) : `${prefix ?? ""}${formatNumber(value, { decimals, fixedDecimals, grouping, locale: locale ?? numbers })}${suffix ?? ""}`);
|
|
58
61
|
const fraction = (value) => (clampNumber(value, min, max) - min) / span;
|
|
59
62
|
const controlRef = useRef(null);
|
|
60
63
|
const valueRef = useRef(null);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconLock, iconRowInsertBottom, iconRowInsertTop, iconTrash, InlineIcon } from "nexus-icons";
|
|
3
|
+
import { iconArrowDown, iconArrowUp, iconCopy, iconDotsVertical, iconLock, iconLockOpen, iconRowInsertBottom, iconRowInsertTop, iconTrash, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { memo, useCallback, useLayoutEffect, useMemo, useRef, useState, } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
6
|
import { Menu } from "../Documents/Menu.js";
|
|
@@ -35,6 +35,8 @@ const COLUMN_WIDTH = {
|
|
|
35
35
|
};
|
|
36
36
|
/** Columns that keep their width instead of sharing the spare room. */
|
|
37
37
|
const FIXED_TYPES = new Set(["checkbox", "switch", "radio"]);
|
|
38
|
+
/** What a locked row's cells say when one is used. */
|
|
39
|
+
const LOCKED_ROW = "Locked: unlock the row to change it.";
|
|
38
40
|
const ENTER_MOVES = new Set(["text", "email", "tel", "url", "search", "password", "number"]);
|
|
39
41
|
const cssLength = (value) => (typeof value === "number" ? `${value}px` : value);
|
|
40
42
|
/** How a cell lines up: numbers at the end, checkboxes and radios in the middle, the rest at the start. */
|
|
@@ -64,11 +66,17 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
|
|
|
64
66
|
items.push({ separator: true });
|
|
65
67
|
items.push({ value: "move-up", label: "Move up", icon: _jsx(InlineIcon, { icon: iconArrowUp }), disabled: index === 0 || Boolean(context.rowLock(store.keyAt(index - 1), index - 1)) }, { value: "move-down", label: "Move down", icon: _jsx(InlineIcon, { icon: iconArrowDown }), disabled: index === count - 1 });
|
|
66
68
|
}
|
|
67
|
-
|
|
69
|
+
// At the end of the row, Remove and Lock are buttons of their own, as a data table's are; the menu holds the rest.
|
|
70
|
+
const atEnd = context.actionsAt === "end";
|
|
71
|
+
const locked = context.isLocked(rowKey);
|
|
72
|
+
const removable = context.removable && !fixed;
|
|
73
|
+
const lockable = context.lockable && !fixed;
|
|
74
|
+
if (removable && !atEnd) {
|
|
68
75
|
if (items.length)
|
|
69
76
|
items.push({ separator: true });
|
|
70
77
|
items.push({ value: "remove", label: "Remove", icon: _jsx(InlineIcon, { icon: iconTrash }), tone: "danger" });
|
|
71
78
|
}
|
|
79
|
+
const menu = items.length > 0 && (_jsx(Menu, { items: items, align: atEnd ? "end" : undefined, onSelect: value => onAction(value, rowKey), "aria-label": `${title} actions`, children: _jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${title} actions`, title: `${title} actions`, "data-nx-row-actions": "", children: _jsx(InlineIcon, { icon: iconDotsVertical }) }) }));
|
|
72
80
|
// Leaving an input counts as using it: a repeated value shows its message from then on.
|
|
73
81
|
function onBlur(event) {
|
|
74
82
|
const cell = event.target.closest("[data-nx-cell]");
|
|
@@ -76,7 +84,7 @@ const TabularRow = memo(function TabularRow({ store, rowKey, index, count, colum
|
|
|
76
84
|
return;
|
|
77
85
|
store.touch(rowKey, cell.dataset.nxCell ?? "");
|
|
78
86
|
}
|
|
79
|
-
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }),
|
|
87
|
+
return (_jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": index + 2, "data-nx-row": rowKey, "data-problem": invalid || undefined, onBlur: onBlur, children: [context.numbered && (_jsxs("div", { role: "cell", className: "nx-tabular__num", children: [_jsx("span", { className: "nx-tabular__num-text", children: index + 1 }), !atEnd && menu] })), columns.map(params => (_jsx("div", { role: "cell", className: cx("nx-tabular__cell", `nx-tabular__cell--${cellKind(params)}`), "data-nx-cell": params.name, children: _jsx(RowCell, { store: store, rowKey: rowKey, params: context.cellParams(rowKey, params, index), index: index, error: store.shownError(rowKey, params.name), layout: "tabular", idPrefix: context.idPrefix, size: context.size, readOnly: context.readOnly, disabled: context.disabled, lock: context.cellLock(rowKey, params.name, index) }, store.revision(rowKey, params.name)) }, params.name))), atEnd && (_jsxs("div", { role: "cell", className: "nx-tabular__actions", children: [removable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `Remove ${title}`, title: locked ? "Unlock the row to remove it" : "Remove", "data-tone": "danger", "data-nx-row-actions": "", disabled: locked || context.disabled, onClick: () => onAction("remove", rowKey), children: _jsx(InlineIcon, { icon: iconTrash }) })), lockable && (_jsx("button", { type: "button", className: "nx-tabular__num-button", "aria-label": `${locked ? "Unlock" : "Lock"} ${title}`, "aria-pressed": locked, title: locked ? "Locked: unlock to change it" : "Lock, so it cannot be changed", "data-nx-row-actions": "", disabled: context.disabled, onClick: () => onAction(locked ? "unlock" : "lock", rowKey), children: _jsx(InlineIcon, { icon: locked ? iconLock : iconLockOpen }) })), menu] }))] }));
|
|
80
88
|
});
|
|
81
89
|
/**
|
|
82
90
|
* Rows of inputs in a table, one column per input: a person's emails or addresses, invoice lines, a salary sheet. The
|
|
@@ -108,6 +116,10 @@ export function TabularForm(props) {
|
|
|
108
116
|
const addable = props.addable !== false && !readOnly;
|
|
109
117
|
const removable = props.removable !== false && !readOnly && !disabled;
|
|
110
118
|
const sortable = Boolean(props.sortable) && !readOnly && !disabled;
|
|
119
|
+
const actionsAt = props.actionsAt ?? "number";
|
|
120
|
+
const lockable = Boolean(props.lockable) && !readOnly;
|
|
121
|
+
/** The rows the person locked, by key: the screen's, never the value's. */
|
|
122
|
+
const [lockedKeys, setLockedKeys] = useState(() => new Set());
|
|
111
123
|
const layoutRef = useRef(null);
|
|
112
124
|
const rows = useRowsInput(props, "Row", layoutRef);
|
|
113
125
|
const store = rows.store;
|
|
@@ -238,9 +250,22 @@ export function TabularForm(props) {
|
|
|
238
250
|
run.move(key, -1);
|
|
239
251
|
else if (action === "move-down")
|
|
240
252
|
run.move(key, 1);
|
|
253
|
+
else if (action === "lock" || action === "unlock") {
|
|
254
|
+
setLockedKeys(current => {
|
|
255
|
+
const next = new Set(current);
|
|
256
|
+
if (action === "lock")
|
|
257
|
+
next.add(key);
|
|
258
|
+
else
|
|
259
|
+
next.delete(key);
|
|
260
|
+
return next;
|
|
261
|
+
});
|
|
262
|
+
}
|
|
241
263
|
else
|
|
242
264
|
run.remove(key);
|
|
243
265
|
}, [store]);
|
|
266
|
+
const isLocked = useCallback((key) => lockedKeys.has(key), [lockedKeys]);
|
|
267
|
+
// A locked row's cells are read-only with the lock's reason, as a row nobody may change has its own.
|
|
268
|
+
const cellLock = useCallback((key, name, index) => (lockedKeys.has(key) ? LOCKED_ROW : rows.cellLock(key, name, index)), [lockedKeys, rows.cellLock]);
|
|
244
269
|
/** The message or hint of the input with focus, shown under the table: cells have no room for their own. */
|
|
245
270
|
function readStatus() {
|
|
246
271
|
const active = document.activeElement;
|
|
@@ -286,7 +311,7 @@ export function TabularForm(props) {
|
|
|
286
311
|
layoutRef.current?.focusRow(key, name);
|
|
287
312
|
}
|
|
288
313
|
}
|
|
289
|
-
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock
|
|
314
|
+
const context = useMemo(() => ({ idPrefix: frame.id, size, readOnly, disabled, numbered, removable, sortable, canAdd: rows.canAdd, showAll: snapshot.showAll, cellLock, cellParams: rows.cellParams, rowLock: rows.rowLock, actionsAt, lockable, isLocked }), [frame.id, size, readOnly, disabled, numbered, removable, sortable, rows.canAdd, snapshot.showAll, cellLock, rows.cellParams, rows.rowLock, actionsAt, lockable, isLocked]);
|
|
290
315
|
/**
|
|
291
316
|
* Which columns are generated for **every** row on the page. The lock is then said once, on the column header,
|
|
292
317
|
* instead of on each of its cells: eighteen locks down one column is eighteen times the ink for one fact. The cells
|
|
@@ -307,7 +332,7 @@ export function TabularForm(props) {
|
|
|
307
332
|
let reason;
|
|
308
333
|
for (const [offset, key] of shownKeys.entries()) {
|
|
309
334
|
// A locked row says nothing about its columns: the rows around it decide whether a column is generated.
|
|
310
|
-
if (rows.rowLock(key, start + offset))
|
|
335
|
+
if (lockedKeys.has(key) || rows.rowLock(key, start + offset))
|
|
311
336
|
continue;
|
|
312
337
|
const lock = rows.cellLock(key, params.name, start + offset);
|
|
313
338
|
if (!lock) {
|
|
@@ -323,9 +348,13 @@ export function TabularForm(props) {
|
|
|
323
348
|
return locked;
|
|
324
349
|
// `shownKeys` is a fresh array on every render; the work is a call per visible cell, and nothing memoised is
|
|
325
350
|
// handed to the rows, so recomputing it costs a map and never a re-render of a row.
|
|
326
|
-
}, [columns, readOnly, rows.cellLock, rows.rowLock, shownKeys, start]);
|
|
351
|
+
}, [columns, readOnly, rows.cellLock, rows.rowLock, shownKeys, start, lockedKeys]);
|
|
327
352
|
// The number and the row's menu, side by side, plus the frame around them. Both are always there, so nothing moves.
|
|
328
|
-
const
|
|
353
|
+
const atEnd = actionsAt === "end";
|
|
354
|
+
const numberWidth = atEnd ? `calc(${Math.max(2, String(count).length)}ch + 16px)` : `calc(${Math.max(2, String(count).length)}ch + 18px + var(--nx-tabular-menu))`;
|
|
355
|
+
// At the end: a button for Remove, one for Lock, and one for the menu when there is anything else in it.
|
|
356
|
+
const buttons = atEnd ? Number(removable) + Number(lockable) + Number(addable || sortable) : 0;
|
|
357
|
+
const actionsWidth = buttons ? `calc(${buttons} * var(--nx-tabular-menu) + ${buttons - 1} * 2px + 9px)` : null;
|
|
329
358
|
const widths = columns.map(columnWidth);
|
|
330
359
|
const template = [
|
|
331
360
|
numbered ? numberWidth : null,
|
|
@@ -333,16 +362,19 @@ export function TabularForm(props) {
|
|
|
333
362
|
const grow = params.grow ?? (FIXED_TYPES.has(params.type) ? 0 : 1);
|
|
334
363
|
return grow > 0 ? `minmax(${widths[index]}, ${grow}fr)` : widths[index];
|
|
335
364
|
}),
|
|
365
|
+
actionsWidth,
|
|
336
366
|
]
|
|
337
367
|
.filter(Boolean)
|
|
338
368
|
.join(" ");
|
|
339
|
-
const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths].join(" + ")})`;
|
|
369
|
+
const minWidth = `calc(${[numbered ? numberWidth : "0px", ...widths, actionsWidth ?? "0px"].join(" + ")})`;
|
|
340
370
|
const tableStyle = { "--nx-tabular-columns": template, "--nx-tabular-num": numbered ? numberWidth : "0px", minWidth };
|
|
341
371
|
const invalid = Boolean(error || snapshot.message || snapshot.invalidRows);
|
|
342
|
-
return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1,
|
|
372
|
+
return (_jsxs("fieldset", { id: frame.id, className: cx("nx-rows", "nx-tabular", `nx-tabular--${size}`, className), style: style, disabled: disabled, "data-problem": invalid || undefined, "data-readonly": readOnly || undefined, "data-actions-at": atEnd ? "end" : undefined, "aria-label": props["aria-label"], "aria-describedby": frame.messageId, children: [_jsx(RowsLegend, { id: frame.legendId, label: label, required: required || (props.min ?? 0) > 0, lock: frame.lock }), _jsx("input", { ref: rows.proxyRef, className: "nx-visually-hidden", tabIndex: -1, "aria-hidden": "true", form: props.form, onInvalid: rows.onInvalid }), _jsx("div", { ref: viewportRef, className: "nx-tabular__viewport", tabIndex: -1,
|
|
373
|
+
// A focused cell scrolls clear of the actions fixed at the end, as it does of the number at the start.
|
|
374
|
+
style: { maxHeight: viewportMax ?? cssLength(maxHeight), scrollPaddingRight: actionsWidth ?? undefined }, onScroll: updateRange, children: _jsxs("div", { role: "table", className: "nx-tabular__table", style: tableStyle, "aria-labelledby": label ? frame.legendId : undefined, "aria-label": label ? undefined : props["aria-label"], "aria-rowcount": count + 1, children: [_jsx("div", { ref: headRef, role: "rowgroup", className: "nx-tabular__head", children: _jsxs("div", { role: "row", className: "nx-tabular__row", "aria-rowindex": 1, children: [numbered && (_jsxs("div", { role: "columnheader", className: "nx-tabular__num", children: [_jsx("span", { "aria-hidden": "true", children: "#" }), _jsx("span", { className: "nx-visually-hidden", children: store.names.title })] })), columns.map(params => {
|
|
343
375
|
const columnHint = params.hint;
|
|
344
376
|
const locked = lockedColumns.get(params.name);
|
|
345
377
|
const reason = typeof locked === "string" ? locked : locked ? "Read-only" : undefined;
|
|
346
378
|
return (_jsxs("div", { role: "columnheader", className: cx("nx-tabular__head-cell", `nx-tabular__cell--${cellKind(params)}`), title: reason ?? (typeof columnHint === "string" ? columnHint : undefined), "data-nx-locked": locked ? "" : undefined, children: [_jsx("span", { className: "nx-tabular__head-label", children: params.label ?? params.name }), params.required && !locked && (_jsx("span", { className: "nx-field__required", "aria-hidden": "true", children: "*" })), locked && _jsx(InlineIcon, { icon: iconLock, className: "nx-tabular__head-lock", "aria-hidden": "true" })] }, params.name));
|
|
347
|
-
})] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
|
|
379
|
+
}), actionsWidth && (_jsx("div", { role: "columnheader", className: "nx-tabular__actions", children: _jsx("span", { className: "nx-visually-hidden", children: "Actions" }) }))] }) }), _jsx("div", { ref: bodyRef, role: "rowgroup", className: "nx-tabular__body", style: { paddingTop: start * rowHeight, paddingBottom: Math.max(0, count - end) * rowHeight }, onKeyDown: onBodyKeyDown, onFocus: readStatus, onBlur: readStatusSoon, onInput: readStatusSoon, children: count === 0 ? (_jsx("div", { role: "row", className: "nx-tabular__row nx-tabular__row--empty", children: _jsx("div", { role: "cell", className: "nx-tabular__empty", children: frame.emptyMessage }) })) : (shownKeys.map((key, offset) => (_jsx(TabularRow, { store: store, rowKey: key, index: start + offset, count: count, columns: columns, context: context, onAction: onAction }, `${snapshot.generation}:${key}`)))) })] }) }), _jsx(RowsFooter, { messageId: frame.messageId, store: store, snapshot: snapshot, error: error, hint: hint, status: status, addLabel: frame.addLabel, addable: addable, canAdd: rows.canAdd, max: max, maxMessage: frame.maxMessage, size: size, onAdd: () => rows.actions.add(), onUndo: () => rows.actions.undo() })] }));
|
|
348
380
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
4
|
+
import { smoothScrollAllowed } from "../../Helpers/AnimationHelpers.js";
|
|
4
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
6
|
import { toDevanagariDigits } from "../../Helpers/DateHelpers.js";
|
|
6
7
|
import { formatTime, getMeridiemNames, isSecondsInRange, isTwelveHourFormat, listTimeSlots, normalizeTimeStep, nowIsoTime, SECONDS_PER_DAY, secondsToIsoTime, timeToSeconds, toIsoTime, } from "../../Helpers/TimeHelpers.js";
|
|
@@ -157,15 +158,15 @@ export function TimePanel(props) {
|
|
|
157
158
|
return (slots.find(slot => !slot.disabled) ?? slots[0]).value;
|
|
158
159
|
}, [view, slots, selected]);
|
|
159
160
|
// Chosen items scroll to the middle row of their column, so they line up across columns with their neighbours above
|
|
160
|
-
// and below; the list centers its slot the same way. At once when the panel opens, smoothly after that, unless
|
|
161
|
-
//
|
|
161
|
+
// and below; the list centers its slot the same way. At once when the panel opens, smoothly after that, unless motion
|
|
162
|
+
// is off. (Columns are positioned, so an item's offsetTop is measured from the column.)
|
|
162
163
|
// With `autoFocus`, focus then moves in once.
|
|
163
164
|
useLayoutEffect(() => {
|
|
164
165
|
const root = rootRef.current;
|
|
165
166
|
if (!root)
|
|
166
167
|
return;
|
|
167
168
|
const align = () => {
|
|
168
|
-
const smooth = scrolledRef.current &&
|
|
169
|
+
const smooth = scrolledRef.current && smoothScrollAllowed();
|
|
169
170
|
for (const column of root.querySelectorAll("[data-column]")) {
|
|
170
171
|
const target = column.querySelector(view === "list" ? "[data-anchor]" : '[aria-selected="true"]');
|
|
171
172
|
if (!target)
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { iconMinus, iconSquare, iconSquares, iconX, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { useContext, useEffect, useId, useLayoutEffect, useRef, useState } from "react";
|
|
5
5
|
import { flushSync } from "react-dom";
|
|
6
|
-
import { afterAnimation } from "../../Helpers/AnimationHelpers.js";
|
|
6
|
+
import { afterAnimation, motionAllowed } from "../../Helpers/AnimationHelpers.js";
|
|
7
7
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
8
8
|
import { cssVariablePosition, DRAG_CANCEL_SELECTOR, moveWithin, startDrag, viewportBounds } from "../../Helpers/DragHelpers.js";
|
|
9
9
|
import { HostedPopupContext } from "./PopupContext.js";
|
|
@@ -12,9 +12,6 @@ const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]):not([t
|
|
|
12
12
|
// Where a minimized popup flies to: the newest bar of the dock, at the bottom right.
|
|
13
13
|
const DOCK_EDGE = 16;
|
|
14
14
|
const DOCK_BAR = { width: 300, height: 44 };
|
|
15
|
-
function motionAllowed() {
|
|
16
|
-
return !window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
17
|
-
}
|
|
18
15
|
function popupPosition(dialog) {
|
|
19
16
|
return cssVariablePosition(dialog, "--nx-popup-x", "--nx-popup-y");
|
|
20
17
|
}
|
|
@@ -4,10 +4,27 @@ export interface ThemePickerProps {
|
|
|
4
4
|
themes?: readonly ThemeOption[];
|
|
5
5
|
/** Show the Light / Dark / System choice. Default `true`. */
|
|
6
6
|
showMode?: boolean;
|
|
7
|
+
/** Show Compact / Cozy. Default `true`. */
|
|
8
|
+
showDensity?: boolean;
|
|
9
|
+
/** Show Animations: On / Off / System. Default `true`. */
|
|
10
|
+
showAnimations?: boolean;
|
|
11
|
+
/** Show the scale grid, a development tool. Default: in a development build only. */
|
|
12
|
+
showGrid?: boolean;
|
|
7
13
|
className?: string;
|
|
8
14
|
}
|
|
9
15
|
/**
|
|
10
|
-
* Light / Dark / System,
|
|
11
|
-
*
|
|
16
|
+
* Light / Dark / System, a card per theme with a live preview of its colors, the density, the animations and, in a
|
|
17
|
+
* development build, the scale grid. Each choice applies at once and is saved in the browser.
|
|
12
18
|
*/
|
|
13
|
-
export declare function ThemePicker({ themes, showMode, className }: ThemePickerProps): import("react").JSX.Element;
|
|
19
|
+
export declare function ThemePicker({ themes, showMode, showDensity, showAnimations, showGrid, className }: ThemePickerProps): import("react").JSX.Element;
|
|
20
|
+
export interface PreferencesPickerProps {
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The person's quick preferences, each applied at once and saved on this device: the interface language and the
|
|
25
|
+
* calendar; the date picker's language, the clock and the date format (the date settings every date component reads);
|
|
26
|
+
* the number scale and the thousands separator (every number box, slider and table number without a locale of its
|
|
27
|
+
* own); and the page size tables open with and whether a row click opens a record (every table, once chosen, over what
|
|
28
|
+
* the table names itself). `ThemeSwitcher` shows it under the theme.
|
|
29
|
+
*/
|
|
30
|
+
export declare function PreferencesPicker({ className }: PreferencesPickerProps): import("react").JSX.Element;
|
|
@@ -1,22 +1,93 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { iconCheck, iconDeviceDesktop, iconMoon, iconSun, InlineIcon } from "nexus-icons";
|
|
3
|
+
import { iconArrowsMaximize, iconArrowsMinimize, iconCheck, iconCircleCheck, iconCircleX, iconDeviceDesktop, iconEyeOff, iconLayoutGrid, iconMoon, iconSun, iconTable, InlineIcon, } from "nexus-icons";
|
|
4
4
|
import { useId } from "react";
|
|
5
5
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { formatDate } from "../../Helpers/DateHelpers.js";
|
|
7
|
+
import { setDatePreferences, useDatePreferences } from "../../Helpers/DatePreferences.js";
|
|
8
|
+
import { DEFAULT_PAGE_SIZE, DEFAULT_PAGE_SIZES } from "../../Helpers/TableHelpers.js";
|
|
9
|
+
import { isDevelopmentBuild } from "../../Helpers/ThemeHelpers.js";
|
|
10
|
+
import { setUserPreferences, useUserPreferences } from "../../Helpers/UserPreferences.js";
|
|
11
|
+
import { DATE_FORMATS, DEFAULT_TIME_FORMAT } from "../../Interfaces/DateInterfaces.js";
|
|
12
|
+
import { ENumberScales } from "../../Interfaces/PreferenceInterfaces.js";
|
|
6
13
|
import { NEXUS_THEMES } from "../../Interfaces/ThemeInterfaces.js";
|
|
7
14
|
import { useTheme } from "../../Services/ThemeService.js";
|
|
8
15
|
import { TabButtons } from "../Documents/TabButtons.js";
|
|
16
|
+
import { Switch } from "../Inputs/CheckBox.js";
|
|
17
|
+
import { Dropdown } from "../Inputs/Dropdown.js";
|
|
9
18
|
const MODES = [
|
|
10
19
|
{ value: "light", label: "Light", icon: _jsx(InlineIcon, { icon: iconSun }) },
|
|
11
20
|
{ value: "dark", label: "Dark", icon: _jsx(InlineIcon, { icon: iconMoon }) },
|
|
12
21
|
{ value: "system", label: "System", icon: _jsx(InlineIcon, { icon: iconDeviceDesktop }), title: "Follow the operating system" },
|
|
13
22
|
];
|
|
23
|
+
const DENSITIES = [
|
|
24
|
+
{ value: "compact", label: "Compact", icon: _jsx(InlineIcon, { icon: iconArrowsMinimize }) },
|
|
25
|
+
{ value: "cozy", label: "Cozy", icon: _jsx(InlineIcon, { icon: iconArrowsMaximize }), title: "Taller controls and rows, for touch screens and large monitors" },
|
|
26
|
+
];
|
|
27
|
+
const ANIMATIONS = [
|
|
28
|
+
{ value: "on", label: "On", icon: _jsx(InlineIcon, { icon: iconCircleCheck }), title: "Animate, even when the operating system asks for less motion" },
|
|
29
|
+
{ value: "off", label: "Off", icon: _jsx(InlineIcon, { icon: iconCircleX }), title: "Nothing moves" },
|
|
30
|
+
{ value: "system", label: "System", icon: _jsx(InlineIcon, { icon: iconDeviceDesktop }), title: "Follow the operating system's animations setting" },
|
|
31
|
+
];
|
|
32
|
+
const GRIDS = [
|
|
33
|
+
{ value: "off", label: "Off", icon: _jsx(InlineIcon, { icon: iconEyeOff }) },
|
|
34
|
+
{ value: "px", label: "px", icon: _jsx(InlineIcon, { icon: iconLayoutGrid }), title: "A line every 4px, a solid one every 48px" },
|
|
35
|
+
{ value: "cm", label: "mm", icon: _jsx(InlineIcon, { icon: iconTable }), title: "A line every millimetre, a solid one every centimetre" },
|
|
36
|
+
];
|
|
37
|
+
/** A small heading and what it names: a row of tab buttons by default, or the control given. */
|
|
38
|
+
function Choice({ id, legend, options, value, onChange, children }) {
|
|
39
|
+
return (_jsxs("div", { className: "nx-theme-picker__group", children: [_jsx("p", { id: id, className: "nx-theme-picker__legend", children: legend }), children ?? (options && value !== undefined && onChange && _jsx(TabButtons, { "aria-labelledby": id, name: id, options: options, value: value, onChange: onChange, block: true }))] }));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Light / Dark / System, a card per theme with a live preview of its colors, the density, the animations and, in a
|
|
43
|
+
* development build, the scale grid. Each choice applies at once and is saved in the browser.
|
|
44
|
+
*/
|
|
45
|
+
export function ThemePicker({ themes = NEXUS_THEMES, showMode = true, showDensity = true, showAnimations = true, showGrid = isDevelopmentBuild(), className }) {
|
|
46
|
+
const { theme, mode, density, animations, grid, setTheme, setMode, setDensity, setAnimations, setGrid } = useTheme();
|
|
47
|
+
const name = useId();
|
|
48
|
+
return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [showMode && _jsx(Choice, { id: `${name}-mode`, legend: "Mode", options: MODES, value: mode, onChange: setMode }), _jsxs("fieldset", { className: "nx-theme-picker__group", children: [_jsx("legend", { className: "nx-theme-picker__legend", children: "Theme" }), _jsx("div", { className: "nx-theme-picker__grid", children: themes.map(option => (_jsxs("label", { className: "nx-theme-option", title: option.description, children: [_jsx("input", { type: "radio", className: "nx-visually-hidden", name: `${name}-theme`, value: option.id, checked: theme === option.id, onChange: () => setTheme(option.id) }), _jsx("span", { className: "nx-theme-option__preview", "data-nx-theme": option.id, "aria-hidden": "true", children: _jsxs("span", { className: "nx-theme-option__window", children: [_jsx("span", { className: "nx-theme-option__line" }), _jsx("span", { className: "nx-theme-option__line nx-theme-option__line--muted" }), _jsxs("span", { className: "nx-theme-option__swatches", children: [_jsx("span", { className: "nx-theme-option__pill" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--success" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--warning" }), _jsx("span", { className: "nx-theme-option__dot nx-theme-option__dot--danger" })] })] }) }), _jsxs("span", { className: "nx-theme-option__name", children: [option.name, _jsx(InlineIcon, { icon: iconCheck, className: "nx-theme-option__check" })] })] }, option.id))) })] }), showDensity && _jsx(Choice, { id: `${name}-density`, legend: "Density", options: DENSITIES, value: density, onChange: setDensity }), showAnimations && _jsx(Choice, { id: `${name}-animations`, legend: "Animations", options: ANIMATIONS, value: animations, onChange: setAnimations }), showGrid && _jsx(Choice, { id: `${name}-grid`, legend: "Scale grid", options: GRIDS, value: grid, onChange: setGrid })] }));
|
|
49
|
+
}
|
|
50
|
+
/* The quick preferences: language, dates, numbers and tables, beside the theme. */
|
|
51
|
+
const LANGUAGES = [
|
|
52
|
+
{ value: "en", label: "English" },
|
|
53
|
+
{ value: "ne", label: _jsx("span", { lang: "ne", children: "\u0928\u0947\u092A\u093E\u0932\u0940" }), title: "Nepali" },
|
|
54
|
+
];
|
|
55
|
+
const CALENDARS = [
|
|
56
|
+
{ value: "ad", label: "AD", title: "Gregorian (AD)" },
|
|
57
|
+
{ value: "bs", label: "BS", title: "Bikram Sambat (BS)" },
|
|
58
|
+
];
|
|
59
|
+
const SCRIPTS = [
|
|
60
|
+
{ value: "latin", label: "English", title: "Month names and digits in English" },
|
|
61
|
+
{ value: "devanagari", label: _jsx("span", { lang: "ne", children: "\u0928\u0947\u092A\u093E\u0932\u0940" }), title: "Month names and digits in Nepali" },
|
|
62
|
+
];
|
|
63
|
+
const CLOCKS = [
|
|
64
|
+
{ value: "12", label: "12-hour", title: "2:30 PM" },
|
|
65
|
+
{ value: "24", label: "24-hour", title: "14:30" },
|
|
66
|
+
];
|
|
67
|
+
const SCALES = [
|
|
68
|
+
{ value: String(ENumberScales.International), label: "International", title: "1,234,567: thousand, million, billion" },
|
|
69
|
+
{ value: String(ENumberScales.SouthAsian), label: "South Asian", title: "12,34,567: thousand, lakh, crore" },
|
|
70
|
+
];
|
|
71
|
+
const SEPARATORS = [
|
|
72
|
+
{ value: "comma", label: "1,200", title: "Comma" },
|
|
73
|
+
{ value: "period", label: "1.200", title: "Period, with a comma for decimals" },
|
|
74
|
+
{ value: "space", label: "1\u00a0200", title: "Space" },
|
|
75
|
+
{ value: "none", label: "1200", title: "None" },
|
|
76
|
+
];
|
|
77
|
+
const PAGE_SIZES = DEFAULT_PAGE_SIZES.map(size => ({ value: size, label: `${size} rows` }));
|
|
78
|
+
// A fixed day, so the server and the browser write the same examples, and one past the 12th, so day and month differ.
|
|
79
|
+
const SAMPLE_DATE = "2026-09-27";
|
|
14
80
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
81
|
+
* The person's quick preferences, each applied at once and saved on this device: the interface language and the
|
|
82
|
+
* calendar; the date picker's language, the clock and the date format (the date settings every date component reads);
|
|
83
|
+
* the number scale and the thousands separator (every number box, slider and table number without a locale of its
|
|
84
|
+
* own); and the page size tables open with and whether a row click opens a record (every table, once chosen, over what
|
|
85
|
+
* the table names itself). `ThemeSwitcher` shows it under the theme.
|
|
17
86
|
*/
|
|
18
|
-
export function
|
|
19
|
-
const
|
|
87
|
+
export function PreferencesPicker({ className }) {
|
|
88
|
+
const dates = useDatePreferences();
|
|
89
|
+
const preferences = useUserPreferences();
|
|
20
90
|
const name = useId();
|
|
21
|
-
|
|
91
|
+
const formats = DATE_FORMATS.map(format => ({ value: format, label: formatDate(SAMPLE_DATE, { ...dates, format }), description: format }));
|
|
92
|
+
return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [_jsx("h3", { className: "nx-theme-picker__section", children: "Language and region" }), _jsx(Choice, { id: `${name}-language`, legend: "Interface language", options: LANGUAGES, value: preferences.language, onChange: language => setUserPreferences({ language }) }), _jsx(Choice, { id: `${name}-calendar`, legend: "Calendar", options: CALENDARS, value: dates.calendar, onChange: calendar => setDatePreferences({ calendar }) }), _jsx("h3", { className: "nx-theme-picker__section", children: "Dates and times" }), _jsx(Choice, { id: `${name}-script`, legend: "Date picker language", options: SCRIPTS, value: dates.script, onChange: script => setDatePreferences({ script }) }), _jsx(Choice, { id: `${name}-clock`, legend: "Clock", options: CLOCKS, value: /H/.test(dates.timeFormat) ? "24" : "12", onChange: clock => setDatePreferences({ timeFormat: clock === "24" ? "HH:mm" : DEFAULT_TIME_FORMAT }) }), _jsx(Choice, { id: `${name}-format`, legend: "Display date format", children: _jsx(Dropdown, { "aria-labelledby": `${name}-format`, options: formats, value: dates.format, clearable: false, searchable: false, onValueChange: format => format && setDatePreferences({ format }) }) }), _jsx("h3", { className: "nx-theme-picker__section", children: "Numbers" }), _jsx(Choice, { id: `${name}-scale`, legend: "Number scale", options: SCALES, value: String(preferences.numberScale), onChange: scale => setUserPreferences({ numberScale: Number(scale) === ENumberScales.SouthAsian ? ENumberScales.SouthAsian : ENumberScales.International }) }), _jsx(Choice, { id: `${name}-thousands`, legend: "Thousands separator", options: SEPARATORS, value: preferences.thousands, onChange: thousands => setUserPreferences({ thousands }) }), _jsx("h3", { className: "nx-theme-picker__section", children: "Tables" }), _jsx(Choice, { id: `${name}-page-size`, legend: "Default page size", children: _jsx(Dropdown, { "aria-labelledby": `${name}-page-size`, options: PAGE_SIZES, value: preferences.pageSize ?? DEFAULT_PAGE_SIZE, clearable: false, searchable: false, onValueChange: pageSize => pageSize && setUserPreferences({ pageSize }) }) }), _jsx(Switch, { label: "A row click opens the record", labelPosition: "start", checked: preferences.rowClick ?? false, onValueChange: rowClick => setUserPreferences({ rowClick }) })] }));
|
|
22
93
|
}
|
|
@@ -4,8 +4,10 @@ export interface ThemeScriptProps extends ThemeScriptOptions {
|
|
|
4
4
|
nonce?: string;
|
|
5
5
|
}
|
|
6
6
|
/**
|
|
7
|
-
* Applies the saved theme
|
|
8
|
-
* and
|
|
7
|
+
* Applies the saved theme, mode, density, animations and (in a development build) scale grid before the first paint,
|
|
8
|
+
* and keeps `data-nx-motion` in step with the system's reduced-motion setting: the stylesheets read that attribute,
|
|
9
|
+
* not the media query, so an app without this script animates whatever the system says. Render it in `<head>` of the
|
|
10
|
+
* root layout, and put `suppressHydrationWarning` on `<html>`, because the script sets attributes there.
|
|
9
11
|
*
|
|
10
12
|
* @example
|
|
11
13
|
* <html lang="en" suppressHydrationWarning>
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { themeScript } from "../../Helpers/ThemeHelpers.js";
|
|
3
3
|
/**
|
|
4
|
-
* Applies the saved theme
|
|
5
|
-
* and
|
|
4
|
+
* Applies the saved theme, mode, density, animations and (in a development build) scale grid before the first paint,
|
|
5
|
+
* and keeps `data-nx-motion` in step with the system's reduced-motion setting: the stylesheets read that attribute,
|
|
6
|
+
* not the media query, so an app without this script animates whatever the system says. Render it in `<head>` of the
|
|
7
|
+
* root layout, and put `suppressHydrationWarning` on `<html>`, because the script sets attributes there.
|
|
6
8
|
*
|
|
7
9
|
* @example
|
|
8
10
|
* <html lang="en" suppressHydrationWarning>
|
|
@@ -5,9 +5,12 @@ export interface ThemeSwitcherProps extends ThemePickerProps {
|
|
|
5
5
|
label?: string;
|
|
6
6
|
/** Default `ghost`. */
|
|
7
7
|
variant?: ButtonVariant;
|
|
8
|
+
/** The quick preferences under the theme: language, dates, numbers and tables (`PreferencesPicker`). Default true. */
|
|
9
|
+
preferences?: boolean;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
|
-
* A palette button that opens
|
|
11
|
-
*
|
|
12
|
+
* A palette button that opens a quick preference panel below it, with the panel's arrow on the button: the theme
|
|
13
|
+
* (`ThemePicker`) and, under it, the language, dates, numbers and tables (`PreferencesPicker`). The panel is a native
|
|
14
|
+
* popover: it closes with Esc or a click outside, stays inside the window and scrolls inside itself.
|
|
12
15
|
*/
|
|
13
|
-
export declare function ThemeSwitcher({ label, variant, className, ...pickerProps }: ThemeSwitcherProps): import("react").JSX.Element;
|
|
16
|
+
export declare function ThemeSwitcher({ label, variant, className, preferences, ...pickerProps }: ThemeSwitcherProps): import("react").JSX.Element;
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { iconPalette, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { useId, useLayoutEffect, useRef, useState } from "react";
|
|
5
|
-
import { followAnchor, isPopoverOpen, placeUnderAnchor } from "../../Helpers/PopoverHelpers.js";
|
|
5
|
+
import { followAnchor, isPopoverOpen, placeUnderAnchor, showPanel } from "../../Helpers/PopoverHelpers.js";
|
|
6
6
|
import { Button } from "../Documents/Button.js";
|
|
7
|
-
import { ThemePicker } from "./ThemePicker.js";
|
|
7
|
+
import { PreferencesPicker, ThemePicker } from "./ThemePicker.js";
|
|
8
8
|
/**
|
|
9
|
-
* A palette button that opens
|
|
10
|
-
*
|
|
9
|
+
* A palette button that opens a quick preference panel below it, with the panel's arrow on the button: the theme
|
|
10
|
+
* (`ThemePicker`) and, under it, the language, dates, numbers and tables (`PreferencesPicker`). The panel is a native
|
|
11
|
+
* popover: it closes with Esc or a click outside, stays inside the window and scrolls inside itself.
|
|
11
12
|
*/
|
|
12
|
-
export function ThemeSwitcher({ label = "Theme", variant = "ghost", className, ...pickerProps }) {
|
|
13
|
+
export function ThemeSwitcher({ label = "Theme", variant = "ghost", className, preferences = true, ...pickerProps }) {
|
|
13
14
|
const panelId = useId();
|
|
14
15
|
const buttonRef = useRef(null);
|
|
15
16
|
const panelRef = useRef(null);
|
|
@@ -42,8 +43,8 @@ export function ThemeSwitcher({ label = "Theme", variant = "ghost", className, .
|
|
|
42
43
|
else if (isPopoverOpen(panel))
|
|
43
44
|
panel?.hidePopover();
|
|
44
45
|
else if (panel && typeof panel.showPopover === "function") {
|
|
45
|
-
panel
|
|
46
|
+
showPanel(panel);
|
|
46
47
|
setOpen(true);
|
|
47
48
|
}
|
|
48
|
-
} }), _jsx("div", { ref: panelRef, id: panelId, popover: "auto", role: "dialog", "aria-label": label, className: "nx-popover nx-theme-switcher__panel", onToggle: onToggle, children: _jsx(ThemePicker, { ...pickerProps }) })] }));
|
|
49
|
+
} }), _jsx("div", { ref: panelRef, id: panelId, popover: "auto", role: "dialog", "aria-label": label, className: "nx-popover nx-theme-switcher__panel", onToggle: onToggle, children: _jsxs("div", { className: "nx-popover__scroll nx-theme-switcher__body", children: [_jsx(ThemePicker, { ...pickerProps }), preferences && _jsx(PreferencesPicker, {})] }) })] }));
|
|
49
50
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface AvatarProps {
|
|
2
|
+
/** Whose picture it is: the initials stand in without one, and the name picks their tone. */
|
|
3
|
+
name: string;
|
|
4
|
+
/** Address of the picture. Anything but a non-empty string is no picture. */
|
|
5
|
+
src?: unknown;
|
|
6
|
+
/** `sm` 22px, `md` 28px, `lg` 44px. Default `md`; `--nx-avatar-size` sets any other. */
|
|
7
|
+
size?: "sm" | "md" | "lg";
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Makes the picture a button that offers to change it: a camera shows over it on hover, and a click calls this. */
|
|
10
|
+
onChangePicture?: () => void;
|
|
11
|
+
/** The button's name. Default "Change picture". */
|
|
12
|
+
changeLabel?: string;
|
|
13
|
+
/** For the button inside a menu: `role="menuitem"` and `tabIndex={-1}` let the arrow keys reach it. */
|
|
14
|
+
role?: string;
|
|
15
|
+
tabIndex?: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A person: a round picture, or their initials on a soft tone when there is none or it fails to load. The chat,
|
|
19
|
+
* the dropdown's avatar column and the admin bar all draw this one.
|
|
20
|
+
*/
|
|
21
|
+
export declare function Avatar({ name, src, size, className, onChangePicture, changeLabel, role, tabIndex }: AvatarProps): import("react").JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Icon } from "nexus-icons";
|
|
4
|
+
import { useState } from "react";
|
|
5
|
+
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
6
|
+
import { avatarTone, initials } from "../../Helpers/DropdownHelpers.js";
|
|
7
|
+
/**
|
|
8
|
+
* A person: a round picture, or their initials on a soft tone when there is none or it fails to load. The chat,
|
|
9
|
+
* the dropdown's avatar column and the admin bar all draw this one.
|
|
10
|
+
*/
|
|
11
|
+
export function Avatar({ name, src, size = "md", className, onChangePicture, changeLabel = "Change picture", role, tabIndex }) {
|
|
12
|
+
const url = typeof src === "string" ? src.trim() : "";
|
|
13
|
+
const [failed, setFailed] = useState(null);
|
|
14
|
+
const photo = url !== "" && failed !== url;
|
|
15
|
+
const face = photo ? _jsx("img", { src: url, alt: "", loading: "lazy", decoding: "async", onError: () => setFailed(url) }) : initials(name);
|
|
16
|
+
const shared = { className: cx("nx-avatar", size !== "md" && `nx-avatar--${size}`, className), "data-tone": photo ? undefined : avatarTone(name) };
|
|
17
|
+
if (!onChangePicture) {
|
|
18
|
+
return (_jsx("span", { ...shared, "aria-hidden": "true", children: face }));
|
|
19
|
+
}
|
|
20
|
+
return (_jsxs("button", { ...shared, type: "button", role: role, tabIndex: tabIndex, "aria-label": changeLabel, title: changeLabel, onClick: onChangePicture, children: [face, _jsx("span", { className: "nx-avatar__veil", "aria-hidden": "true", children: _jsx(Icon, { name: "camera", size: "40%" }) })] }));
|
|
21
|
+
}
|