nexus-shared 3.1.0 → 4.0.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 +2268 -446
- package/LICENSE +21 -21
- package/README.md +1705 -1084
- package/dist/Client.Index.d.ts +17 -0
- package/dist/Client.Index.js +17 -0
- package/dist/Components/Documents/DocumentFrame.d.ts +50 -0
- package/dist/Components/Documents/DocumentFrame.js +83 -0
- package/dist/Components/Documents/Menu.d.ts +4 -0
- package/dist/Components/Documents/Menu.js +40 -24
- package/dist/Components/Documents/PrintDocument.d.ts +95 -0
- package/dist/Components/Documents/PrintDocument.js +62 -0
- package/dist/Components/Documents/Tabs.d.ts +59 -0
- package/dist/Components/Documents/Tabs.js +70 -0
- package/dist/Components/Forms/ApiForm.js +19 -2
- package/dist/Components/Forms/Crud.d.ts +2 -2
- package/dist/Components/Forms/Crud.js +871 -377
- package/dist/Components/Forms/CrudActions.d.ts +136 -0
- package/dist/Components/Forms/CrudActions.js +184 -0
- package/dist/Components/Forms/CrudHistory.d.ts +66 -0
- package/dist/Components/Forms/CrudHistory.js +140 -0
- package/dist/Components/Forms/CrudNarrowPicker.d.ts +31 -0
- package/dist/Components/Forms/CrudNarrowPicker.js +180 -0
- package/dist/Components/Forms/CrudParts.d.ts +30 -2
- package/dist/Components/Forms/CrudParts.js +56 -5
- package/dist/Components/Forms/CrudRowForms.d.ts +26 -0
- package/dist/Components/Forms/CrudRowForms.js +114 -0
- package/dist/Components/Forms/CrudScopes.d.ts +28 -0
- package/dist/Components/Forms/CrudScopes.js +76 -0
- package/dist/Components/Forms/CrudTree.d.ts +90 -0
- package/dist/Components/Forms/CrudTree.js +363 -0
- package/dist/Components/Forms/CrudWorkspace.d.ts +63 -0
- package/dist/Components/Forms/CrudWorkspace.js +169 -0
- package/dist/Components/Forms/ExcelImport.js +3 -3
- package/dist/Components/Forms/Form.d.ts +4 -4
- package/dist/Components/Forms/Form.js +125 -16
- package/dist/Components/Forms/FormLeaveGuard.d.ts +44 -0
- package/dist/Components/Forms/FormLeaveGuard.js +65 -0
- package/dist/Components/Forms/RecordFlows.d.ts +50 -0
- package/dist/Components/Forms/RecordFlows.js +282 -0
- package/dist/Components/Forms/SubmitForm.d.ts +37 -2
- package/dist/Components/Forms/SubmitForm.js +195 -10
- package/dist/Components/Inputs/CheckBox.js +1 -1
- package/dist/Components/Inputs/ChoiceField.d.ts +21 -1
- package/dist/Components/Inputs/ChoiceField.js +28 -2
- package/dist/Components/Inputs/CodeBox.d.ts +69 -0
- package/dist/Components/Inputs/CodeBox.js +316 -0
- package/dist/Components/Inputs/DatePicker.js +4 -1
- package/dist/Components/Inputs/DateRangePicker.js +4 -1
- package/dist/Components/Inputs/DateTimePicker.js +4 -1
- package/dist/Components/Inputs/Dropdown.d.ts +13 -0
- package/dist/Components/Inputs/Dropdown.js +8 -5
- package/dist/Components/Inputs/DropdownParts.d.ts +3 -1
- package/dist/Components/Inputs/DropdownParts.js +16 -4
- package/dist/Components/Inputs/Field.d.ts +7 -1
- package/dist/Components/Inputs/Field.js +2 -2
- package/dist/Components/Inputs/GroupForm.d.ts +6 -6
- package/dist/Components/Inputs/GroupForm.js +10 -11
- package/dist/Components/Inputs/InputRenderer.d.ts +9 -6
- package/dist/Components/Inputs/InputRenderer.js +39 -31
- package/dist/Components/Inputs/NumberBox.js +4 -2
- package/dist/Components/Inputs/PictureInput.d.ts +41 -0
- package/dist/Components/Inputs/PictureInput.js +70 -0
- package/dist/Components/Inputs/RadioGroup.d.ts +16 -3
- package/dist/Components/Inputs/RadioGroup.js +8 -3
- package/dist/Components/Inputs/RowsInput.d.ts +1 -1
- package/dist/Components/Inputs/RowsInput.js +18 -3
- package/dist/Components/Inputs/TabularForm.d.ts +5 -5
- package/dist/Components/Inputs/TabularForm.js +29 -29
- package/dist/Components/Inputs/TextArea.d.ts +6 -0
- package/dist/Components/Inputs/TextArea.js +14 -2
- package/dist/Components/Inputs/TextBox.js +4 -1
- package/dist/Components/Inputs/TextBoxTypes.d.ts +6 -0
- package/dist/Components/Inputs/TextBoxTypes.js +17 -0
- package/dist/Components/Inputs/TimePicker.js +4 -1
- package/dist/Components/Inputs/TreeView.d.ts +107 -6
- package/dist/Components/Inputs/TreeView.js +513 -30
- package/dist/Components/Layouts/MessageBox.d.ts +15 -2
- package/dist/Components/Layouts/MessageBox.js +56 -7
- package/dist/Components/Layouts/Popup.d.ts +13 -6
- package/dist/Components/Layouts/Popup.js +37 -9
- package/dist/Components/Layouts/PopupDock.d.ts +3 -1
- package/dist/Components/Layouts/PopupDock.js +2 -1
- package/dist/Components/Layouts/PopupHost.js +5 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +3 -2
- package/dist/Components/Layouts/ThemePicker.js +16 -4
- package/dist/Components/Viewers/DataTable.js +205 -61
- package/dist/Components/Viewers/DataTableFilters.d.ts +1 -1
- package/dist/Components/Viewers/DataTableFilters.js +17 -2
- package/dist/Components/Viewers/DataTableParts.d.ts +46 -12
- package/dist/Components/Viewers/DataTableParts.js +62 -21
- package/dist/Components/Viewers/OwnerFiles.d.ts +175 -0
- package/dist/Components/Viewers/OwnerFiles.js +589 -0
- package/dist/Components/Viewers/QrCodeImage.d.ts +22 -0
- package/dist/Components/Viewers/QrCodeImage.js +15 -0
- package/dist/Components/Viewers/RecordCards.d.ts +16 -0
- package/dist/Components/Viewers/RecordCards.js +509 -0
- package/dist/Helpers/ApiModules.d.ts +3 -34
- package/dist/Helpers/ApiModules.js +3 -3
- package/dist/Helpers/ApiRoutes.d.ts +68 -26
- package/dist/Helpers/ApiRoutes.js +31 -27
- package/dist/Helpers/ArrayHelpers.d.ts +0 -0
- package/dist/Helpers/ArrayHelpers.js +2 -0
- package/dist/Helpers/BusyHooks.d.ts +6 -0
- package/dist/Helpers/BusyHooks.js +39 -0
- package/dist/Helpers/CodeHelpers.d.ts +97 -0
- package/dist/Helpers/CodeHelpers.js +332 -0
- package/dist/Helpers/CrudBackend.d.ts +114 -6
- package/dist/Helpers/CrudBackend.js +214 -11
- package/dist/Helpers/CrudColumns.d.ts +98 -0
- package/dist/Helpers/CrudColumns.js +160 -0
- package/dist/Helpers/CrudDetailsTabs.d.ts +58 -0
- package/dist/Helpers/CrudDetailsTabs.js +126 -0
- package/dist/Helpers/CrudHelpers.d.ts +130 -6
- package/dist/Helpers/CrudHelpers.js +304 -12
- package/dist/Helpers/CrudNarrowPick.d.ts +83 -0
- package/dist/Helpers/CrudNarrowPick.js +157 -0
- package/dist/Helpers/CrudRowForms.d.ts +15 -0
- package/dist/Helpers/CrudRowForms.js +43 -0
- package/dist/Helpers/CrudScopes.d.ts +32 -0
- package/dist/Helpers/CrudScopes.js +79 -0
- package/dist/Helpers/DropdownHelpers.d.ts +3 -1
- package/dist/Helpers/DropdownHelpers.js +6 -2
- package/dist/Helpers/ExcelHelpers.d.ts +1 -1
- package/dist/Helpers/ExcelHelpers.js +12 -11
- package/dist/Helpers/FileAdapter.d.ts +78 -0
- package/dist/Helpers/FileAdapter.js +102 -0
- package/dist/Helpers/FilePrimaries.d.ts +48 -0
- package/dist/Helpers/FilePrimaries.js +138 -0
- package/dist/Helpers/FormConfirm.d.ts +32 -0
- package/dist/Helpers/FormConfirm.js +40 -0
- package/dist/Helpers/FormStore.d.ts +90 -5
- package/dist/Helpers/FormStore.js +318 -22
- package/dist/Helpers/InputConditions.d.ts +50 -0
- package/dist/Helpers/InputConditions.js +168 -0
- package/dist/Helpers/InputMemory.d.ts +106 -0
- package/dist/Helpers/InputMemory.js +385 -0
- package/dist/Helpers/InputNarrowing.d.ts +18 -0
- package/dist/Helpers/InputNarrowing.js +67 -0
- package/dist/Helpers/InputParamsHelpers.d.ts +44 -1
- package/dist/Helpers/InputParamsHelpers.js +162 -71
- package/dist/Helpers/MessageBuilder.js +2 -0
- package/dist/Helpers/NexusDriveAdapter.d.ts +55 -0
- package/dist/Helpers/NexusDriveAdapter.js +207 -0
- package/dist/Helpers/NexusHelpers.d.ts +0 -0
- package/dist/Helpers/NexusHelpers.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +95 -0
- package/dist/Helpers/NumberHelpers.js +126 -0
- package/dist/Helpers/Permissions.js +2 -1
- package/dist/Helpers/QrCode.d.ts +26 -0
- package/dist/Helpers/QrCode.js +463 -0
- package/dist/Helpers/RecordCards.d.ts +205 -0
- package/dist/Helpers/RecordCards.js +378 -0
- package/dist/Helpers/RequestCache.d.ts +47 -0
- package/dist/Helpers/RequestCache.js +132 -0
- package/dist/Helpers/RowsStore.d.ts +37 -2
- package/dist/Helpers/RowsStore.js +128 -10
- package/dist/Helpers/ServerPreview.d.ts +69 -0
- package/dist/Helpers/ServerPreview.js +99 -0
- package/dist/Helpers/TableColumns.js +4 -3
- package/dist/Helpers/TableExport.d.ts +7 -1
- package/dist/Helpers/TableExport.js +21 -15
- package/dist/Helpers/TableFilters.d.ts +1 -1
- package/dist/Helpers/TableFilters.js +24 -23
- package/dist/Helpers/TableHelpers.d.ts +24 -3
- package/dist/Helpers/TableHelpers.js +58 -12
- package/dist/Helpers/TableJoins.d.ts +83 -0
- package/dist/Helpers/TableJoins.js +284 -0
- package/dist/Helpers/TableStore.d.ts +26 -1
- package/dist/Helpers/TableStore.js +73 -7
- package/dist/Helpers/TreeStore.d.ts +306 -11
- package/dist/Helpers/TreeStore.js +1048 -90
- package/dist/Helpers/UserPreferences.d.ts +8 -0
- package/dist/Helpers/UserPreferences.js +17 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +512 -14
- package/dist/Interfaces/FileInterfaces.d.ts +162 -0
- package/dist/Interfaces/FileInterfaces.js +4 -0
- package/dist/Interfaces/FormInterfaces.d.ts +393 -29
- package/dist/Interfaces/FormInterfaces.js +79 -1
- package/dist/Interfaces/InputInterfaces.d.ts +39 -3
- package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
- package/dist/Interfaces/PreferenceInterfaces.d.ts +8 -0
- package/dist/Interfaces/PreferenceInterfaces.js +2 -5
- package/dist/Interfaces/RecordInterfaces.d.ts +367 -0
- package/dist/Interfaces/RecordInterfaces.js +1 -0
- package/dist/Interfaces/TableInterfaces.d.ts +160 -5
- package/dist/Services/ApiProxy.d.ts +36 -1
- package/dist/Services/ApiProxy.js +182 -52
- package/dist/Services/ApiProxyRules.d.ts +203 -0
- package/dist/Services/ApiProxyRules.js +426 -0
- package/dist/Services/ExcelApi.js +5 -0
- package/dist/Services/FileApi.d.ts +13 -0
- package/dist/Services/FileApi.js +29 -0
- package/dist/Services/ImageEditorLoader.d.ts +14 -0
- package/dist/Services/ImageEditorLoader.js +51 -0
- package/dist/Services/JoinSources.d.ts +80 -0
- package/dist/Services/JoinSources.js +150 -0
- package/dist/Services/ModuleImport.d.ts +7 -0
- package/dist/Services/ModuleImport.js +22 -0
- package/dist/Services/ServerApi.d.ts +13 -2
- package/dist/Services/ServerApi.js +8 -3
- package/dist/Shared.Index.d.ts +20 -0
- package/dist/Shared.Index.js +20 -0
- package/package.json +92 -85
- package/src/Styles/Nexus.Avatar.css +80 -79
- package/src/Styles/Nexus.Base.css +117 -117
- package/src/Styles/Nexus.Button.css +186 -180
- package/src/Styles/Nexus.Calendar.css +273 -273
- package/src/Styles/Nexus.Chat.css +1443 -1443
- package/src/Styles/Nexus.Choice.css +359 -343
- package/src/Styles/Nexus.Crud.css +542 -168
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +733 -725
- package/src/Styles/Nexus.Editor.css +295 -295
- package/src/Styles/Nexus.Excel.css +389 -389
- package/src/Styles/Nexus.Files.css +410 -0
- package/src/Styles/Nexus.Form.css +282 -279
- package/src/Styles/Nexus.Index.css +41 -37
- package/src/Styles/Nexus.Input.css +238 -4
- package/src/Styles/Nexus.Markdown.css +237 -237
- package/src/Styles/Nexus.Menu.css +246 -237
- package/src/Styles/Nexus.Popup.css +800 -724
- package/src/Styles/Nexus.Print.css +288 -0
- package/src/Styles/Nexus.QrCode.css +12 -0
- package/src/Styles/Nexus.Record.css +430 -0
- package/src/Styles/Nexus.Rows.css +637 -637
- package/src/Styles/Nexus.Slider.css +276 -276
- package/src/Styles/Nexus.Tab.Buttons.css +497 -432
- package/src/Styles/Nexus.Table.css +98 -35
- package/src/Styles/Nexus.Theme.Picker.css +59 -3
- package/src/Styles/Nexus.Themes.css +200 -194
- package/src/Styles/Nexus.Time.css +144 -144
- package/src/Styles/Nexus.Toast.css +263 -263
- package/src/Styles/Nexus.Tokens.css +94 -89
- package/src/Styles/Nexus.Tree.css +530 -431
|
@@ -5,16 +5,29 @@ import type { PopupSize } from "./Popup.tsx";
|
|
|
5
5
|
export type MessageTone = "neutral" | "info" | "success" | "warning" | "danger" | "question";
|
|
6
6
|
export interface MessageBoxOptions {
|
|
7
7
|
title: ReactNode;
|
|
8
|
-
/** Text under the title. */
|
|
8
|
+
/** Text under the title, in the box's muted text. */
|
|
9
9
|
message?: ReactNode;
|
|
10
|
+
/**
|
|
11
|
+
* What the question is about, in full, after the message and in the normal text colour: a record's details
|
|
12
|
+
* (`<CrudDetails row columns />`), a list. It scrolls with the box's body while the buttons stay in view; give the box a
|
|
13
|
+
* width that suits it (`size: "lg"` or `"xl"`).
|
|
14
|
+
*/
|
|
15
|
+
details?: ReactNode;
|
|
10
16
|
/** Colors the icon: `info`, `success`, `warning`, `danger`, `question`. `neutral` shows no icon. */
|
|
11
17
|
tone?: MessageTone;
|
|
12
18
|
/** Replaces the tone's icon; `null` shows none. */
|
|
13
19
|
icon?: ReactNode | null;
|
|
14
20
|
/** Default `sm`. */
|
|
15
21
|
size?: PopupSize;
|
|
16
|
-
/** The close button
|
|
22
|
+
/** The close button and Esc dismiss the box. Default true. */
|
|
17
23
|
closable?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* A click outside dismisses the box. Default: as `closable`. `false` keeps a box with much to read (a question with its
|
|
26
|
+
* `details`) open through a stray click, while Esc and the close button still dismiss it.
|
|
27
|
+
*/
|
|
28
|
+
closeOnBackdrop?: boolean;
|
|
29
|
+
/** Dismisses the box when it aborts, as Esc would: the form or the page that asked has gone. */
|
|
30
|
+
signal?: AbortSignal;
|
|
18
31
|
}
|
|
19
32
|
export interface MessageBoxButton<T extends string = string> {
|
|
20
33
|
value: T;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx,
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { iconAlertCircle, iconAlertTriangle, iconCircleCheck, iconHelpCircle, iconInfoCircle, InlineIcon } from "nexus-icons";
|
|
4
4
|
import { cx } from "../../Helpers/ClassHelpers.js";
|
|
5
5
|
import { Button } from "../Documents/Button.js";
|
|
@@ -14,40 +14,85 @@ const TONE_ICONS = {
|
|
|
14
14
|
question: iconHelpCircle,
|
|
15
15
|
};
|
|
16
16
|
let promptCounter = 0;
|
|
17
|
+
let messageCounter = 0;
|
|
18
|
+
/*
|
|
19
|
+
* A held Enter, or one pressed again at once (a double press), never answers a box. The Enter that opened a question (in
|
|
20
|
+
* a form's box) would otherwise press the Confirm it gave the focus to - by the key's repeat, or by the second tap of a
|
|
21
|
+
* double press - before anything in the box was read. Any Enter within this long of the one before it is held.
|
|
22
|
+
*/
|
|
23
|
+
const HASTY_ENTER_MS = 300;
|
|
24
|
+
let lastEnterAt = Number.NEGATIVE_INFINITY;
|
|
25
|
+
function holdHastyEnter(event) {
|
|
26
|
+
if (event.key !== "Enter")
|
|
27
|
+
return;
|
|
28
|
+
const gap = event.timeStamp - lastEnterAt;
|
|
29
|
+
lastEnterAt = event.timeStamp;
|
|
30
|
+
if (!(event.target instanceof Element) || !event.target.closest(".nx-popup--message"))
|
|
31
|
+
return;
|
|
32
|
+
if (event.repeat || gap < HASTY_ENTER_MS)
|
|
33
|
+
event.preventDefault();
|
|
34
|
+
}
|
|
35
|
+
// Listening from the start: the Enter that opens a box comes before the box.
|
|
36
|
+
if (typeof window !== "undefined")
|
|
37
|
+
window.addEventListener("keydown", holdHastyEnter, true);
|
|
17
38
|
function toneIcon(tone, icon) {
|
|
18
39
|
if (icon !== undefined)
|
|
19
40
|
return icon;
|
|
20
41
|
const data = TONE_ICONS[tone];
|
|
21
42
|
return data ? _jsx(InlineIcon, { icon: data }) : null;
|
|
22
43
|
}
|
|
44
|
+
/** The muted message, then the details in the normal text colour; each with an id the box is described by. */
|
|
45
|
+
function messageBody(message, details, id) {
|
|
46
|
+
return (_jsxs(_Fragment, { children: [message && (_jsx("div", { id: `${id}-text`, className: "nx-message__text", children: message })), details && (_jsx("div", { id: `${id}-details`, className: "nx-message__details", children: details }))] }));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* What describes a box to a screen reader: its message and its details, read with the title as the focus enters it, so
|
|
50
|
+
* a question's Confirm is never reached before what it confirms was heard.
|
|
51
|
+
*/
|
|
52
|
+
function describedBy(message, details, id) {
|
|
53
|
+
return cx(message ? `${id}-text` : undefined, details ? `${id}-details` : undefined) || undefined;
|
|
54
|
+
}
|
|
55
|
+
/** Dismisses an open box when the signal aborts; nothing is left listening once it has closed. */
|
|
56
|
+
function closeOnAbort(popup, signal) {
|
|
57
|
+
if (!signal)
|
|
58
|
+
return;
|
|
59
|
+
const dismiss = () => popup.close();
|
|
60
|
+
signal.addEventListener("abort", dismiss, { once: true });
|
|
61
|
+
void popup.closed.then(() => signal.removeEventListener("abort", dismiss));
|
|
62
|
+
}
|
|
23
63
|
/**
|
|
24
64
|
* A message box with the given buttons. Resolves with the clicked button's value, or `null` when the box was
|
|
25
65
|
* dismissed with the close button, Esc, or a click outside.
|
|
26
66
|
*/
|
|
27
67
|
export function showMessageBox(options) {
|
|
28
|
-
const { title, message, tone = "neutral", icon, size = "sm", closable = true, buttons } = options;
|
|
68
|
+
const { title, message, details, tone = "neutral", icon, size = "sm", closable = true, closeOnBackdrop = closable, signal, buttons } = options;
|
|
69
|
+
if (signal?.aborted)
|
|
70
|
+
return Promise.resolve(null);
|
|
29
71
|
const cautious = tone === "danger" || tone === "warning";
|
|
30
72
|
const explicit = buttons.findIndex(button => button.autoFocus);
|
|
31
73
|
const focusIndex = explicit >= 0 ? explicit : cautious ? 0 : buttons.length - 1;
|
|
32
74
|
let result = null;
|
|
75
|
+
const id = `nx-message-${++messageCounter}`;
|
|
33
76
|
const popup = openPopup({
|
|
34
77
|
title,
|
|
35
78
|
icon: toneIcon(tone, icon),
|
|
36
79
|
size,
|
|
37
80
|
closable,
|
|
38
|
-
closeOnBackdrop
|
|
81
|
+
closeOnBackdrop,
|
|
39
82
|
closeOnEscape: closable,
|
|
40
83
|
variant: "dialog",
|
|
41
84
|
minimizable: false,
|
|
42
85
|
maximizable: false,
|
|
43
86
|
className: cx("nx-popup--message", `nx-popup--tone-${tone}`),
|
|
44
87
|
initialFocus: "[data-nx-focus]",
|
|
45
|
-
|
|
88
|
+
"aria-describedby": describedBy(message, details, id),
|
|
89
|
+
content: message || details ? messageBody(message, details, id) : null,
|
|
46
90
|
footer: handle => buttons.map((button, index) => (_jsx(Button, { variant: button.variant ?? (index === buttons.length - 1 ? (tone === "danger" ? "danger" : "primary") : "secondary"), "data-nx-focus": index === focusIndex || undefined, onClick: () => {
|
|
47
91
|
result = button.value;
|
|
48
92
|
handle.close();
|
|
49
93
|
}, children: button.label }, button.value))),
|
|
50
94
|
});
|
|
95
|
+
closeOnAbort(popup, signal);
|
|
51
96
|
return popup.closed.then(() => result);
|
|
52
97
|
}
|
|
53
98
|
function options(input) {
|
|
@@ -72,7 +117,9 @@ function confirm(input) {
|
|
|
72
117
|
}
|
|
73
118
|
/** A text box with Cancel and OK. Resolves with the text, or `null` when cancelled. */
|
|
74
119
|
function prompt(input) {
|
|
75
|
-
const { title, message, tone = "neutral", icon, size = "sm", closable = true, label, defaultValue, placeholder, required = true, inputType = "text", confirmLabel, cancelLabel } = options(input);
|
|
120
|
+
const { title, message, details, tone = "neutral", icon, size = "sm", closable = true, closeOnBackdrop = closable, signal, label, defaultValue, placeholder, required = true, inputType = "text", confirmLabel, cancelLabel } = options(input);
|
|
121
|
+
if (signal?.aborted)
|
|
122
|
+
return Promise.resolve(null);
|
|
76
123
|
const formId = `nx-prompt-${++promptCounter}`;
|
|
77
124
|
let result = null;
|
|
78
125
|
const popup = openPopup({
|
|
@@ -80,20 +127,22 @@ function prompt(input) {
|
|
|
80
127
|
icon: toneIcon(tone, icon),
|
|
81
128
|
size,
|
|
82
129
|
closable,
|
|
83
|
-
closeOnBackdrop
|
|
130
|
+
closeOnBackdrop,
|
|
84
131
|
closeOnEscape: closable,
|
|
85
132
|
variant: "dialog",
|
|
86
133
|
minimizable: false,
|
|
87
134
|
maximizable: false,
|
|
88
135
|
className: cx("nx-popup--message", `nx-popup--tone-${tone}`),
|
|
89
136
|
initialFocus: `#${formId} input`,
|
|
137
|
+
"aria-describedby": describedBy(message, details, formId),
|
|
90
138
|
content: handle => (_jsxs("form", { id: formId, className: "nx-message__form", onSubmit: (event) => {
|
|
91
139
|
event.preventDefault();
|
|
92
140
|
result = String(new FormData(event.currentTarget).get("value") ?? "");
|
|
93
141
|
handle.close();
|
|
94
|
-
}, children: [message
|
|
142
|
+
}, children: [messageBody(message, details, formId), _jsx(TextBox, { name: "value", label: label, type: inputType, defaultValue: defaultValue, placeholder: placeholder, required: required })] })),
|
|
95
143
|
footer: handle => (_jsxs(_Fragment, { children: [_jsx(Button, { onClick: () => handle.close(), children: cancelLabel ?? "Cancel" }), _jsx(Button, { type: "submit", form: formId, variant: "primary", children: confirmLabel ?? "OK" })] })),
|
|
96
144
|
});
|
|
145
|
+
closeOnAbort(popup, signal);
|
|
97
146
|
return popup.closed.then(() => result);
|
|
98
147
|
}
|
|
99
148
|
/**
|
|
@@ -50,8 +50,9 @@ export interface PopupProps {
|
|
|
50
50
|
closeOnBackdrop?: boolean;
|
|
51
51
|
/**
|
|
52
52
|
* The minimize button: the popup steps aside to a bar at the bottom right, keeping everything typed in it, and the
|
|
53
|
-
* page can be used again. Default:
|
|
54
|
-
*
|
|
53
|
+
* page can be used again. Default false: only a popup that asks has it. A CRUD page's Add and Edit forms and its row
|
|
54
|
+
* forms ask (a form put aside while the list behind it is read); message boxes, viewers, galleries, histories and
|
|
55
|
+
* the rest do not. Needs `<PopupHost />`, which shows the bars.
|
|
55
56
|
*/
|
|
56
57
|
minimizable?: boolean;
|
|
57
58
|
/** The maximize button, and a double-click on the title bar: the popup fills the window. Default true. */
|
|
@@ -76,17 +77,23 @@ export interface PopupProps {
|
|
|
76
77
|
initialFocus?: string;
|
|
77
78
|
/** Name for screen readers when there is no title. */
|
|
78
79
|
"aria-label"?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Ids of elements inside the popup that describe it to screen readers, after its `description`: a message box names
|
|
82
|
+
* its message and its details, so they are read with the title as the focus enters.
|
|
83
|
+
*/
|
|
84
|
+
"aria-describedby"?: string;
|
|
79
85
|
className?: string;
|
|
80
86
|
children?: ReactNode;
|
|
81
87
|
}
|
|
82
88
|
/**
|
|
83
|
-
* A modal window on the native <dialog>, with the buttons of a desktop window:
|
|
84
|
-
*
|
|
85
|
-
*
|
|
89
|
+
* A modal window on the native <dialog>, with the buttons of a desktop window: maximize and close, and minimize when
|
|
90
|
+
* it asks (`minimizable`: a CRUD page's Add and Edit). It sits in the top layer above everything, dims and locks the
|
|
91
|
+
* page behind it, traps Tab, closes with Esc or a click outside, and returns focus to where it came from. The title
|
|
92
|
+
* bar moves it; a double-click there maximizes it. Minimized, it
|
|
86
93
|
* waits as a bar at the bottom right with its content kept, while the page can be used. The body scrolls while the
|
|
87
94
|
* title and the footer stay. The content mounts while the popup is open, so a form inside starts fresh every time.
|
|
88
95
|
*
|
|
89
96
|
* @example
|
|
90
97
|
* <Popup open={open} onClose={() => setOpen(false)} title="Edit booking" footer={<Button variant="primary">Save</Button>}>…</Popup>
|
|
91
98
|
*/
|
|
92
|
-
export declare function Popup({ open, onClose, onClosed, title, description, icon, variant, size, footer, closable, closeOnEscape, closeOnBackdrop, minimizable, maximizable, movable, reopen, windowState: windowStateProp, defaultWindowState, onWindowStateChange, flush, initialFocus, "aria-label": ariaLabel, className, children, }: PopupProps): import("react").JSX.Element;
|
|
99
|
+
export declare function Popup({ open, onClose, onClosed, title, description, icon, variant, size, footer, closable, closeOnEscape, closeOnBackdrop, minimizable, maximizable, movable, reopen, windowState: windowStateProp, defaultWindowState, onWindowStateChange, flush, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }: PopupProps): import("react").JSX.Element;
|
|
@@ -17,17 +17,20 @@ function popupPosition(dialog) {
|
|
|
17
17
|
}
|
|
18
18
|
// Each popup's opener and the popup that held it, so focus can find its way back through popups that have gone.
|
|
19
19
|
const openers = new WeakMap();
|
|
20
|
+
// How each popup puts the focus inside itself (its `initialFocus`), for a popup closed over it that left the focus nowhere.
|
|
21
|
+
const homes = new WeakMap();
|
|
20
22
|
/**
|
|
21
|
-
* A modal window on the native <dialog>, with the buttons of a desktop window:
|
|
22
|
-
*
|
|
23
|
-
*
|
|
23
|
+
* A modal window on the native <dialog>, with the buttons of a desktop window: maximize and close, and minimize when
|
|
24
|
+
* it asks (`minimizable`: a CRUD page's Add and Edit). It sits in the top layer above everything, dims and locks the
|
|
25
|
+
* page behind it, traps Tab, closes with Esc or a click outside, and returns focus to where it came from. The title
|
|
26
|
+
* bar moves it; a double-click there maximizes it. Minimized, it
|
|
24
27
|
* waits as a bar at the bottom right with its content kept, while the page can be used. The body scrolls while the
|
|
25
28
|
* title and the footer stay. The content mounts while the popup is open, so a form inside starts fresh every time.
|
|
26
29
|
*
|
|
27
30
|
* @example
|
|
28
31
|
* <Popup open={open} onClose={() => setOpen(false)} title="Edit booking" footer={<Button variant="primary">Save</Button>}>…</Popup>
|
|
29
32
|
*/
|
|
30
|
-
export function Popup({ open, onClose, onClosed, title, description, icon, variant = "window", size = "md", footer, closable = true, closeOnEscape = true, closeOnBackdrop = true, minimizable, maximizable = true, movable = true, reopen = "replace", windowState: windowStateProp, defaultWindowState = "normal", onWindowStateChange, flush = false, initialFocus, "aria-label": ariaLabel, className, children, }) {
|
|
33
|
+
export function Popup({ open, onClose, onClosed, title, description, icon, variant = "window", size = "md", footer, closable = true, closeOnEscape = true, closeOnBackdrop = true, minimizable, maximizable = true, movable = true, reopen = "replace", windowState: windowStateProp, defaultWindowState = "normal", onWindowStateChange, flush = false, initialFocus, "aria-label": ariaLabel, "aria-describedby": ariaDescribedBy, className, children, }) {
|
|
31
34
|
const id = useId();
|
|
32
35
|
const dialogRef = useRef(null);
|
|
33
36
|
// Content stays mounted through the exit animation, and while minimized.
|
|
@@ -41,7 +44,8 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
41
44
|
const maximized = windowState === "maximized";
|
|
42
45
|
const dockReady = useDockReady();
|
|
43
46
|
const hosted = useContext(HostedPopupContext);
|
|
44
|
-
|
|
47
|
+
// Only a popup that asks: a CRUD page's Add and Edit, and its row forms.
|
|
48
|
+
const canMinimize = minimizable === true && dockReady;
|
|
45
49
|
const downOnBackdropRef = useRef(false);
|
|
46
50
|
// The element that opened the popup, where focus goes back when the browser has nowhere better.
|
|
47
51
|
const openerRef = useRef(null);
|
|
@@ -65,19 +69,22 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
65
69
|
onWindowStateChange?.(next);
|
|
66
70
|
}
|
|
67
71
|
// The latest props and callbacks, for the dock bar and the drag, which outlive a render.
|
|
68
|
-
const latest = useRef({ windowState, defaultWindowState, reopen, title, icon, closable, ariaLabel, onClose, changeWindowState });
|
|
72
|
+
const latest = useRef({ windowState, defaultWindowState, reopen, title, description, icon, closable, ariaLabel, onClose, changeWindowState });
|
|
69
73
|
useLayoutEffect(() => {
|
|
70
|
-
latest.current = { windowState, defaultWindowState, reopen, title, icon, closable, ariaLabel, onClose, changeWindowState };
|
|
74
|
+
latest.current = { windowState, defaultWindowState, reopen, title, description, icon, closable, ariaLabel, onClose, changeWindowState };
|
|
71
75
|
if (!minimized)
|
|
72
76
|
restoreToRef.current = windowState;
|
|
73
77
|
});
|
|
74
78
|
function dock() {
|
|
75
79
|
const current = latest.current;
|
|
76
80
|
dockedRef.current = true;
|
|
81
|
+
const label = typeof current.title === "string" ? current.title : current.ariaLabel;
|
|
77
82
|
dockWindow({
|
|
78
83
|
id: dockId,
|
|
79
84
|
title: current.title ?? current.ariaLabel ?? "Window",
|
|
80
|
-
|
|
85
|
+
// Under the title, so two bars of one kind (two Edit forms, two Adds) read apart: the record, the place.
|
|
86
|
+
description: current.description,
|
|
87
|
+
label: label && typeof current.description === "string" ? `${label} · ${current.description}` : label,
|
|
81
88
|
icon: current.icon,
|
|
82
89
|
restore: () => latest.current.changeWindowState(restoreToRef.current),
|
|
83
90
|
close: current.closable ? () => latest.current.onClose("close") : undefined,
|
|
@@ -102,11 +109,26 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
102
109
|
link = openers.get(link.within);
|
|
103
110
|
if (link?.opener?.isConnected)
|
|
104
111
|
link.opener.focus({ preventScroll: true });
|
|
112
|
+
// An opener that cannot take the focus - hidden in a menu that closed, disabled while its action runs - or none at
|
|
113
|
+
// all never leaves it on the page while another popup is still open: the popup now on top takes it, where it puts
|
|
114
|
+
// it on opening (a record's workspace: its chosen tab).
|
|
115
|
+
const now = document.activeElement;
|
|
116
|
+
if (now && now !== document.body && !dialog.contains(now))
|
|
117
|
+
return;
|
|
118
|
+
const below = Array.from(document.querySelectorAll("dialog.nx-popup")).filter(other => other !== dialog && other.open);
|
|
119
|
+
const top = below[below.length - 1];
|
|
120
|
+
if (top)
|
|
121
|
+
(homes.get(top) ?? (() => top.focus()))();
|
|
105
122
|
}
|
|
106
123
|
function focusInside(dialog) {
|
|
107
124
|
const target = initialFocus ? dialog.querySelector(initialFocus) : dialog.querySelector(`:is(.nx-popup__body, .nx-popup__foot) :is(${FOCUSABLE})`);
|
|
108
125
|
(target ?? dialog).focus();
|
|
109
126
|
}
|
|
127
|
+
useLayoutEffect(() => {
|
|
128
|
+
const dialog = dialogRef.current;
|
|
129
|
+
if (dialog)
|
|
130
|
+
homes.set(dialog, () => focusInside(dialog));
|
|
131
|
+
});
|
|
110
132
|
function showDialog(dialog) {
|
|
111
133
|
// The first modal locks the page; the space of its scrollbar is kept as padding, so nothing shifts.
|
|
112
134
|
if (!document.querySelector(".nx-popup:modal"))
|
|
@@ -317,6 +339,12 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
317
339
|
function onPointerDown(event) {
|
|
318
340
|
downOnBackdropRef.current = event.target === event.currentTarget;
|
|
319
341
|
}
|
|
342
|
+
// A press outside a popup that stays open through it (`closeOnBackdrop: false`) leaves the focus where it was, as
|
|
343
|
+
// the press does nothing: a question's Confirm keeps it, so Enter still answers, and a field keeps its caret.
|
|
344
|
+
function onMouseDown(event) {
|
|
345
|
+
if (event.target === event.currentTarget && !closeOnBackdrop)
|
|
346
|
+
event.preventDefault();
|
|
347
|
+
}
|
|
320
348
|
function onClick(event) {
|
|
321
349
|
const onBackdrop = event.target === event.currentTarget && downOnBackdropRef.current;
|
|
322
350
|
downOnBackdropRef.current = false;
|
|
@@ -369,5 +397,5 @@ export function Popup({ open, onClose, onClosed, title, description, icon, varia
|
|
|
369
397
|
const titleId = `${id}-title`;
|
|
370
398
|
const descriptionId = `${id}-description`;
|
|
371
399
|
const hasControls = canMinimize || maximizable || closable;
|
|
372
|
-
return (_jsx("dialog", { ref: dialogRef, className: cx("nx-popup", `nx-popup--${variant}`, `nx-popup--${size}`, flush && "nx-popup--flush", maximized && "nx-popup--maximized", movable && "nx-popup--movable", closing && "nx-popup--closing", className), "data-window-state": windowState, "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : ariaLabel, "aria-describedby": description ? descriptionId : undefined, onCancel: onCancel, onClose: onNativeClose, onPointerDown: onPointerDown, onClick: onClick, children: shown && (_jsxs(HostedPopupContext, { value: false, children: [(title || icon || hasControls) && (_jsxs("header", { className: "nx-popup__head", onPointerDown: onHeadPointerDown, onDoubleClick: onHeadDoubleClick, children: [icon && _jsx("span", { className: "nx-popup__icon", children: icon }), _jsxs("div", { className: "nx-popup__heading", children: [title && (_jsx("h2", { id: titleId, className: "nx-popup__title", children: title })), description && (_jsx("p", { id: descriptionId, className: "nx-popup__description", children: description }))] }), hasControls && (_jsxs("div", { className: "nx-popup__controls", children: [canMinimize && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__minimize", "aria-label": "Minimize", title: "Minimize", onMouseDown: keepFocus, onClick: () => changeWindowState("minimized"), children: _jsx(InlineIcon, { icon: iconMinus }) })), maximizable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__maximize", "aria-label": maximized ? "Restore down" : "Maximize", title: maximized ? "Restore down" : "Maximize", onMouseDown: keepFocus, onClick: toggleMaximize, children: _jsx(InlineIcon, { icon: maximized ? iconSquares : iconSquare }) })), closable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__close", "aria-label": "Close", title: "Close", onClick: () => onClose("close"), children: _jsx(InlineIcon, { icon: iconX }) }))] }))] })), _jsx("div", { className: "nx-popup__body", children: children }), footer && _jsx("footer", { className: "nx-popup__foot", children: footer })] }, round)) }));
|
|
400
|
+
return (_jsx("dialog", { ref: dialogRef, className: cx("nx-popup", `nx-popup--${variant}`, `nx-popup--${size}`, flush && "nx-popup--flush", maximized && "nx-popup--maximized", movable && "nx-popup--movable", closing && "nx-popup--closing", className), "data-window-state": windowState, "aria-labelledby": title ? titleId : undefined, "aria-label": title ? undefined : ariaLabel, "aria-describedby": cx(description ? descriptionId : undefined, ariaDescribedBy) || undefined, onCancel: onCancel, onClose: onNativeClose, onPointerDown: onPointerDown, onMouseDown: onMouseDown, onClick: onClick, children: shown && (_jsxs(HostedPopupContext, { value: false, children: [(title || icon || hasControls) && (_jsxs("header", { className: "nx-popup__head", onPointerDown: onHeadPointerDown, onDoubleClick: onHeadDoubleClick, children: [icon && _jsx("span", { className: "nx-popup__icon", children: icon }), _jsxs("div", { className: "nx-popup__heading", children: [title && (_jsx("h2", { id: titleId, className: "nx-popup__title", children: title })), description && (_jsx("p", { id: descriptionId, className: "nx-popup__description", children: description }))] }), hasControls && (_jsxs("div", { className: "nx-popup__controls", children: [canMinimize && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__minimize", "aria-label": "Minimize", title: "Minimize", onMouseDown: keepFocus, onClick: () => changeWindowState("minimized"), children: _jsx(InlineIcon, { icon: iconMinus }) })), maximizable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__maximize", "aria-label": maximized ? "Restore down" : "Maximize", title: maximized ? "Restore down" : "Maximize", onMouseDown: keepFocus, onClick: toggleMaximize, children: _jsx(InlineIcon, { icon: maximized ? iconSquares : iconSquare }) })), closable && (_jsx("button", { type: "button", className: "nx-popup__control nx-popup__close", "aria-label": "Close", title: "Close", onClick: () => onClose("close"), children: _jsx(InlineIcon, { icon: iconX }) }))] }))] })), _jsx("div", { className: "nx-popup__body", children: children }), footer && _jsx("footer", { className: "nx-popup__foot", children: footer })] }, round)) }));
|
|
373
401
|
}
|
|
@@ -3,7 +3,9 @@ import { type ReactNode } from "react";
|
|
|
3
3
|
export interface DockedWindow {
|
|
4
4
|
id: string;
|
|
5
5
|
title: ReactNode;
|
|
6
|
-
/**
|
|
6
|
+
/** A second line under the title: the record or the place a window is about, so two of one kind read apart. */
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
/** Plain text of the title, and of the description, for the tooltip. */
|
|
7
9
|
label?: string;
|
|
8
10
|
/** The window's icon; a window icon when left out. */
|
|
9
11
|
icon?: ReactNode;
|
|
@@ -113,9 +113,10 @@ export function PopupDock() {
|
|
|
113
113
|
function DockBar({ item, onRestore }) {
|
|
114
114
|
const id = useId();
|
|
115
115
|
const titleId = `${id}-title`;
|
|
116
|
+
const descriptionId = `${id}-description`;
|
|
116
117
|
const closeId = `${id}-close`;
|
|
117
118
|
return (_jsxs("li", { className: "nx-dock__item", children: [_jsxs("button", { type: "button", className: "nx-dock__window", title: item.label ? `Restore: ${item.label}` : "Restore", onClick: () => {
|
|
118
119
|
onRestore();
|
|
119
120
|
item.restore();
|
|
120
|
-
}, children: [_jsx("span", { className: "nx-dock__icon", "aria-hidden": "true", children: item.icon ?? _jsx(InlineIcon, { icon: iconAppWindow }) }), _jsx("span", { className: "nx-visually-hidden", children: "Restore " }), _jsx("span", { id: titleId, className: "nx-dock__title", children: item.title }), _jsx(InlineIcon, { className: "nx-dock__restore", icon: iconChevronUp })] }), item.close && (_jsxs("button", { type: "button", className: "nx-dock__close", "aria-labelledby": `${closeId} ${titleId}`, title: "Close", onClick: item.close, children: [_jsx("span", { id: closeId, hidden: true, children: "Close" }), _jsx(InlineIcon, { icon: iconX })] }))] }));
|
|
121
|
+
}, children: [_jsx("span", { className: "nx-dock__icon", "aria-hidden": "true", children: item.icon ?? _jsx(InlineIcon, { icon: iconAppWindow }) }), _jsx("span", { className: "nx-visually-hidden", children: "Restore " }), _jsxs("span", { className: "nx-dock__text", children: [_jsx("span", { id: titleId, className: "nx-dock__title", children: item.title }), item.description ? (_jsx("span", { id: descriptionId, className: "nx-dock__description", children: item.description })) : null] }), _jsx(InlineIcon, { className: "nx-dock__restore", icon: iconChevronUp })] }), item.close && (_jsxs("button", { type: "button", className: "nx-dock__close", "aria-labelledby": item.description ? `${closeId} ${titleId} ${descriptionId}` : `${closeId} ${titleId}`, title: "Close", onClick: item.close, children: [_jsx("span", { id: closeId, hidden: true, children: "Close" }), _jsx(InlineIcon, { icon: iconX })] }))] }));
|
|
121
122
|
}
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
3
3
|
import { useEffect, useLayoutEffect, useSyncExternalStore } from "react";
|
|
4
4
|
import { Popup } from "./Popup.js";
|
|
5
5
|
import { HostedPopupContext } from "./PopupContext.js";
|
|
6
|
+
import { PageLeaveGuardContext } from "../Forms/FormLeaveGuard.js";
|
|
6
7
|
import { PopupDock } from "./PopupDock.js";
|
|
7
8
|
import { withPopupRule } from "./PopupRules.js";
|
|
8
9
|
const EMPTY = [];
|
|
@@ -156,8 +157,8 @@ export function PopupHost() {
|
|
|
156
157
|
for (const entry of list)
|
|
157
158
|
rendered.add(entry.id);
|
|
158
159
|
});
|
|
159
|
-
return (_jsxs(HostedPopupContext, { value: true, children: [list.map(entry => {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
160
|
+
return (_jsx(PageLeaveGuardContext, { value: null, children: _jsxs(HostedPopupContext, { value: true, children: [list.map(entry => {
|
|
161
|
+
const { content, footer, onClose: _onClose, beforeClose: _beforeClose, onWindowStateChange: _onWindowStateChange, key: _key, ...rest } = entry.options;
|
|
162
|
+
return (_jsx(Popup, { ...rest, open: entry.open, windowState: entry.windowState, onWindowStateChange: state => setWindowState(entry.id, state), footer: typeof footer === "function" ? footer(entry.handle) : footer, onClose: reason => requestClose(entry.id, reason), onClosed: () => removeEntry(entry.id), children: typeof content === "function" ? content(entry.handle) : content }, entry.id));
|
|
163
|
+
}), _jsx(PopupDock, {})] }) }));
|
|
163
164
|
}
|
|
@@ -13,8 +13,9 @@ export interface ThemePickerProps {
|
|
|
13
13
|
className?: string;
|
|
14
14
|
}
|
|
15
15
|
/**
|
|
16
|
-
* Light / Dark / System
|
|
17
|
-
* development build, the scale grid. Each choice applies at once
|
|
16
|
+
* Two sections: the theme (Light / Dark / System and a card per theme with a live preview of its colors) and the
|
|
17
|
+
* appearance (the density, the animations and, in a development build, the scale grid). Each choice applies at once
|
|
18
|
+
* and is saved in the browser.
|
|
18
19
|
*/
|
|
19
20
|
export declare function ThemePicker({ themes, showMode, showDensity, showAnimations, showGrid, className }: ThemePickerProps): import("react").JSX.Element;
|
|
20
21
|
export interface PreferencesPickerProps {
|
|
@@ -15,6 +15,7 @@ import { useTheme } from "../../Services/ThemeService.js";
|
|
|
15
15
|
import { TabButtons } from "../Documents/TabButtons.js";
|
|
16
16
|
import { Switch } from "../Inputs/CheckBox.js";
|
|
17
17
|
import { Dropdown } from "../Inputs/Dropdown.js";
|
|
18
|
+
import { TextBox } from "../Inputs/TextBox.js";
|
|
18
19
|
const MODES = [
|
|
19
20
|
{ value: "light", label: "Light", icon: _jsx(InlineIcon, { icon: iconSun }) },
|
|
20
21
|
{ value: "dark", label: "Dark", icon: _jsx(InlineIcon, { icon: iconMoon }) },
|
|
@@ -38,14 +39,20 @@ const GRIDS = [
|
|
|
38
39
|
function Choice({ id, legend, options, value, onChange, children }) {
|
|
39
40
|
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
|
+
/** A group of settings under its name: one section of the panel, a line above it. */
|
|
43
|
+
function Section({ title, wide, children }) {
|
|
44
|
+
const id = useId();
|
|
45
|
+
return (_jsxs("section", { className: cx("nx-theme-picker__block", wide && "nx-theme-picker__block--wide"), "aria-labelledby": id, children: [_jsx("h3", { id: id, className: "nx-theme-picker__section", children: title }), children] }));
|
|
46
|
+
}
|
|
41
47
|
/**
|
|
42
|
-
* Light / Dark / System
|
|
43
|
-
* development build, the scale grid. Each choice applies at once
|
|
48
|
+
* Two sections: the theme (Light / Dark / System and a card per theme with a live preview of its colors) and the
|
|
49
|
+
* appearance (the density, the animations and, in a development build, the scale grid). Each choice applies at once
|
|
50
|
+
* and is saved in the browser.
|
|
44
51
|
*/
|
|
45
52
|
export function ThemePicker({ themes = NEXUS_THEMES, showMode = true, showDensity = true, showAnimations = true, showGrid = isDevelopmentBuild(), className }) {
|
|
46
53
|
const { theme, mode, density, animations, grid, setTheme, setMode, setDensity, setAnimations, setGrid } = useTheme();
|
|
47
54
|
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: "
|
|
55
|
+
return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [_jsxs(Section, { title: "Theme", wide: true, 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: "Colors" }), _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 || showAnimations || showGrid) && (_jsxs(Section, { title: "Appearance", children: [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
56
|
}
|
|
50
57
|
/* The quick preferences: language, dates, numbers and tables, beside the theme. */
|
|
51
58
|
const LANGUAGES = [
|
|
@@ -75,6 +82,11 @@ const SEPARATORS = [
|
|
|
75
82
|
{ value: "none", label: "1200", title: "None" },
|
|
76
83
|
];
|
|
77
84
|
const PAGE_SIZES = DEFAULT_PAGE_SIZES.map(size => ({ value: size, label: `${size} rows` }));
|
|
85
|
+
const DESIGNS = [
|
|
86
|
+
{ value: "standard", label: "Standard", title: "The label above the box" },
|
|
87
|
+
{ value: "outlined", label: "Outlined", title: "The label on the box's edge" },
|
|
88
|
+
{ value: "inbox", label: "Inbox", title: "The label inside the top of the box" },
|
|
89
|
+
];
|
|
78
90
|
// A fixed day, so the server and the browser write the same examples, and one past the 12th, so day and month differ.
|
|
79
91
|
const SAMPLE_DATE = "2026-09-27";
|
|
80
92
|
/**
|
|
@@ -89,5 +101,5 @@ export function PreferencesPicker({ className }) {
|
|
|
89
101
|
const preferences = useUserPreferences();
|
|
90
102
|
const name = useId();
|
|
91
103
|
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: [
|
|
104
|
+
return (_jsxs("div", { className: cx("nx-theme-picker", className), children: [_jsxs(Section, { title: "Language and region", children: [_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 }) })] }), _jsxs(Section, { title: "Dates and times", children: [_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 }) }) })] }), _jsxs(Section, { title: "Numbers", children: [_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 }) })] }), _jsxs(Section, { title: "Tables", children: [_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 }) })] }), _jsxs(Section, { title: "Forms", children: [_jsx(Choice, { id: `${name}-design`, legend: "Input design", options: DESIGNS, value: preferences.inputDesign ?? "standard", onChange: inputDesign => setUserPreferences({ inputDesign }) }), _jsx(TextBox, { label: "Example", placeholder: "Type to try it" })] })] }));
|
|
93
105
|
}
|