nexus-shared 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/CHANGELOG.md +442 -0
  2. package/README.md +1123 -1000
  3. package/dist/Client.Index.d.ts +6 -0
  4. package/dist/Client.Index.js +6 -0
  5. package/dist/Components/Chats/ChatParts.d.ts +2 -2
  6. package/dist/Components/Chats/ChatParts.js +3 -7
  7. package/dist/Components/Chats/ChatThread.js +3 -3
  8. package/dist/Components/Documents/Button.d.ts +6 -1
  9. package/dist/Components/Documents/Button.js +2 -2
  10. package/dist/Components/Documents/Menu.js +21 -9
  11. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  12. package/dist/Components/Documents/SplitButton.js +7 -5
  13. package/dist/Components/Documents/TabButtons.d.ts +5 -2
  14. package/dist/Components/Documents/TabButtons.js +2 -2
  15. package/dist/Components/Forms/ApiForm.d.ts +1 -1
  16. package/dist/Components/Forms/ApiForm.js +1 -1
  17. package/dist/Components/Forms/Crud.js +486 -206
  18. package/dist/Components/Forms/CrudHistory.d.ts +66 -0
  19. package/dist/Components/Forms/CrudHistory.js +140 -0
  20. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  21. package/dist/Components/Forms/CrudParts.js +38 -5
  22. package/dist/Components/Forms/ExcelImport.d.ts +23 -7
  23. package/dist/Components/Forms/ExcelImport.js +189 -60
  24. package/dist/Components/Forms/Form.js +9 -14
  25. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  26. package/dist/Components/Forms/SubmitForm.js +23 -12
  27. package/dist/Components/Inputs/CodeBox.d.ts +69 -0
  28. package/dist/Components/Inputs/CodeBox.js +316 -0
  29. package/dist/Components/Inputs/DatePicker.js +4 -1
  30. package/dist/Components/Inputs/DateRangePicker.js +4 -1
  31. package/dist/Components/Inputs/DateTimePicker.js +4 -1
  32. package/dist/Components/Inputs/Dropdown.js +4 -1
  33. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  34. package/dist/Components/Inputs/DropdownParts.js +4 -6
  35. package/dist/Components/Inputs/InputRenderer.js +3 -0
  36. package/dist/Components/Inputs/NumberBox.js +26 -2
  37. package/dist/Components/Inputs/RowsInput.js +2 -2
  38. package/dist/Components/Inputs/Slider.js +4 -1
  39. package/dist/Components/Inputs/TabularForm.js +42 -10
  40. package/dist/Components/Inputs/TextArea.js +4 -1
  41. package/dist/Components/Inputs/TextBox.js +4 -1
  42. package/dist/Components/Inputs/TimePanel.js +4 -3
  43. package/dist/Components/Inputs/TimePicker.js +4 -1
  44. package/dist/Components/Inputs/TreeView.d.ts +55 -5
  45. package/dist/Components/Inputs/TreeView.js +372 -23
  46. package/dist/Components/Layouts/Popup.js +1 -4
  47. package/dist/Components/Layouts/ThemePicker.d.ts +21 -3
  48. package/dist/Components/Layouts/ThemePicker.js +89 -6
  49. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  50. package/dist/Components/Layouts/ThemeScript.js +4 -2
  51. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  52. package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
  53. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  54. package/dist/Components/Viewers/Avatar.js +21 -0
  55. package/dist/Components/Viewers/DataTable.js +252 -62
  56. package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
  57. package/dist/Components/Viewers/DataTableColumns.js +25 -68
  58. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  59. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  60. package/dist/Components/Viewers/DataTableParts.d.ts +28 -10
  61. package/dist/Components/Viewers/DataTableParts.js +42 -13
  62. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  63. package/dist/Helpers/AnimationHelpers.js +20 -2
  64. package/dist/Helpers/ApiModules.d.ts +3 -34
  65. package/dist/Helpers/ApiModules.js +3 -3
  66. package/dist/Helpers/ApiRoutes.d.ts +68 -26
  67. package/dist/Helpers/ApiRoutes.js +31 -27
  68. package/dist/Helpers/ArrayHelpers.d.ts +0 -0
  69. package/dist/Helpers/ArrayHelpers.js +2 -0
  70. package/dist/Helpers/CodeHelpers.d.ts +97 -0
  71. package/dist/Helpers/CodeHelpers.js +332 -0
  72. package/dist/Helpers/CrudBackend.d.ts +101 -8
  73. package/dist/Helpers/CrudBackend.js +172 -11
  74. package/dist/Helpers/CrudColumns.d.ts +51 -0
  75. package/dist/Helpers/CrudColumns.js +69 -0
  76. package/dist/Helpers/CrudHelpers.d.ts +50 -5
  77. package/dist/Helpers/CrudHelpers.js +109 -12
  78. package/dist/Helpers/ExcelBackend.d.ts +3 -3
  79. package/dist/Helpers/ExcelBackend.js +8 -2
  80. package/dist/Helpers/ExcelHelpers.d.ts +36 -8
  81. package/dist/Helpers/ExcelHelpers.js +55 -13
  82. package/dist/Helpers/FormStore.d.ts +9 -5
  83. package/dist/Helpers/FormStore.js +25 -5
  84. package/dist/Helpers/InputParamsHelpers.js +8 -0
  85. package/dist/Helpers/MessageBuilder.js +3 -0
  86. package/dist/Helpers/NexusHelpers.d.ts +0 -0
  87. package/dist/Helpers/NexusHelpers.js +1 -0
  88. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  89. package/dist/Helpers/NumberHelpers.js +41 -3
  90. package/dist/Helpers/PopoverHelpers.d.ts +17 -5
  91. package/dist/Helpers/PopoverHelpers.js +52 -17
  92. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  93. package/dist/Helpers/PopoverHooks.js +62 -0
  94. package/dist/Helpers/RequestCache.d.ts +35 -0
  95. package/dist/Helpers/RequestCache.js +113 -0
  96. package/dist/Helpers/RowsStore.d.ts +7 -2
  97. package/dist/Helpers/RowsStore.js +15 -3
  98. package/dist/Helpers/TableFilters.d.ts +25 -0
  99. package/dist/Helpers/TableFilters.js +162 -0
  100. package/dist/Helpers/TableHelpers.d.ts +7 -4
  101. package/dist/Helpers/TableHelpers.js +15 -6
  102. package/dist/Helpers/TableJoins.d.ts +70 -0
  103. package/dist/Helpers/TableJoins.js +250 -0
  104. package/dist/Helpers/TableStore.d.ts +16 -3
  105. package/dist/Helpers/TableStore.js +25 -5
  106. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  107. package/dist/Helpers/ThemeHelpers.js +46 -9
  108. package/dist/Helpers/TreeStore.d.ts +225 -11
  109. package/dist/Helpers/TreeStore.js +683 -90
  110. package/dist/Helpers/UserPreferences.d.ts +22 -0
  111. package/dist/Helpers/UserPreferences.js +116 -0
  112. package/dist/Interfaces/CrudInterfaces.d.ts +67 -10
  113. package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
  114. package/dist/Interfaces/FormInterfaces.d.ts +37 -6
  115. package/dist/Interfaces/InputInterfaces.d.ts +24 -0
  116. package/dist/Interfaces/MessageInterfaces.d.ts +1 -1
  117. package/dist/Interfaces/PreferenceInterfaces.d.ts +51 -0
  118. package/dist/Interfaces/PreferenceInterfaces.js +25 -0
  119. package/dist/Interfaces/TableInterfaces.d.ts +145 -16
  120. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  121. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  122. package/dist/Services/ApiProxy.d.ts +36 -1
  123. package/dist/Services/ApiProxy.js +182 -52
  124. package/dist/Services/ApiProxyRules.d.ts +203 -0
  125. package/dist/Services/ApiProxyRules.js +426 -0
  126. package/dist/Services/ExcelApi.d.ts +3 -1
  127. package/dist/Services/ExcelApi.js +1 -1
  128. package/dist/Services/JoinSources.d.ts +48 -0
  129. package/dist/Services/JoinSources.js +74 -0
  130. package/dist/Services/ServerApi.d.ts +13 -2
  131. package/dist/Services/ServerApi.js +8 -3
  132. package/dist/Services/ThemeService.d.ts +18 -2
  133. package/dist/Services/ThemeService.js +80 -16
  134. package/dist/Shared.Index.d.ts +6 -0
  135. package/dist/Shared.Index.js +6 -0
  136. package/package.json +4 -2
  137. package/src/Styles/Nexus.Avatar.css +79 -0
  138. package/src/Styles/Nexus.Base.css +51 -4
  139. package/src/Styles/Nexus.Button.css +27 -0
  140. package/src/Styles/Nexus.Calendar.css +4 -6
  141. package/src/Styles/Nexus.Chat.css +5 -51
  142. package/src/Styles/Nexus.Choice.css +4 -6
  143. package/src/Styles/Nexus.Crud.css +16 -9
  144. package/src/Styles/Nexus.Dropdown.css +12 -61
  145. package/src/Styles/Nexus.Editor.css +4 -6
  146. package/src/Styles/Nexus.Excel.css +34 -15
  147. package/src/Styles/Nexus.Form.css +25 -67
  148. package/src/Styles/Nexus.Index.css +1 -0
  149. package/src/Styles/Nexus.Input.css +794 -637
  150. package/src/Styles/Nexus.Markdown.css +0 -28
  151. package/src/Styles/Nexus.Menu.css +8 -30
  152. package/src/Styles/Nexus.Popup.css +22 -29
  153. package/src/Styles/Nexus.RichText.css +2 -32
  154. package/src/Styles/Nexus.Rows.css +51 -10
  155. package/src/Styles/Nexus.Slider.css +3 -5
  156. package/src/Styles/Nexus.Tab.Buttons.css +20 -7
  157. package/src/Styles/Nexus.Table.css +1387 -1296
  158. package/src/Styles/Nexus.Theme.Picker.css +342 -232
  159. package/src/Styles/Nexus.Time.css +9 -12
  160. package/src/Styles/Nexus.Toast.css +6 -8
  161. package/src/Styles/Nexus.Tokens.css +17 -0
  162. package/src/Styles/Nexus.Tree.css +76 -11
@@ -0,0 +1,69 @@
1
+ import { type ChangeEvent, type ComponentPropsWithRef, type ReactNode } from "react";
2
+ import type { CodeRules, InputDesign, ValidateOn } from "../../Interfaces/InputInterfaces.ts";
3
+ import type { Size } from "../../Interfaces/TypeInterfaces.ts";
4
+ type NativeInputProps = Omit<ComponentPropsWithRef<"input">, "type" | "size" | "value" | "defaultValue" | "pattern" | "minLength" | "maxLength" | "prefix" | "placeholder" | "children" | keyof CodeRules>;
5
+ export interface CodeBoxProps extends NativeInputProps, CodeRules {
6
+ label?: ReactNode;
7
+ /** Help text under the boxes. */
8
+ hint?: ReactNode;
9
+ /** An error from outside, such as the server's "That code is wrong". Shown before the field's own message. */
10
+ error?: string | null;
11
+ /** Accepted for forms that set one on every input; the label of a code always sits above its boxes. */
12
+ design?: InputDesign;
13
+ /** Default `md`. */
14
+ size?: Size;
15
+ /** The boxes share the field's whole width, as on a sign-in card. Default: each box keeps its size. */
16
+ block?: boolean;
17
+ /** Shown between groups, such as "-" for an SSN. Default: a wider gap only. */
18
+ separator?: string;
19
+ /** A dot in each filled box instead of its character, for a PIN or an SSN. The browser treats it as a password. */
20
+ masked?: boolean;
21
+ /** Faint text in the empty boxes: one character for every box ("0"), or one per box ("MMDDYYYY"). */
22
+ slotPlaceholder?: string;
23
+ /**
24
+ * The code: only the characters it takes, without separators ("123456789" for an SSN). A saved code loses only its
25
+ * separators and case (`loadCode`); one the boxes cannot hold - a character the code does not take, or more characters
26
+ * than boxes - is kept as it is and shown invalid at once, with its message, never cut.
27
+ */
28
+ value?: string;
29
+ defaultValue?: string;
30
+ /** What a read-only field says when someone tries to change it. Default "Read-only: this code cannot be changed." */
31
+ readOnlyMessage?: string;
32
+ /** Name used in messages, e.g. "The code needs all 6 digits." Default: the label, when it is text. */
33
+ fieldName?: string;
34
+ /** When the field shows its own message. Default `blur`; submitting a form always shows it. */
35
+ validateOn?: ValidateOn;
36
+ /** The code after each change. */
37
+ onValueChange?: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
38
+ /**
39
+ * Once every box is filled, with the code, so a one-time code can go without a button press. Once per code, and again
40
+ * once the boxes were short, however they got so: typing, a form's reset, or the page emptying a controlled `value`.
41
+ */
42
+ onComplete?: (value: string) => void;
43
+ /** Class of the outer field. `inputClassName` goes on the input. */
44
+ className?: string;
45
+ inputClassName?: string;
46
+ }
47
+ /**
48
+ * A code typed one character to a box: a one-time code, a PIN, an SSN, a licence key, a hex colour. Digits, letters,
49
+ * both, hex, or any one-character pattern; boxes in groups with a separator; dots for a secret.
50
+ *
51
+ * It is one real input under the boxes, so a phone offers the code from its message (`autoComplete="one-time-code"`),
52
+ * a pasted "123-45-6789" fills every box, and a screen reader reads one field. The boxes are drawn from that input
53
+ * straight in the DOM, so typing re-renders nothing. Uncontrolled by default, like the text box; `value` also works.
54
+ *
55
+ * Typing follows one model, however the text comes - a key, a phone's keyboard, an input method, a paste, autofill
56
+ * (`typeCode`): a whole code replaces everything; a character goes over a selected box, or into the box with the ring -
57
+ * inserted while the code has an empty box, so a gap Backspace or Delete left takes back what was there, and over that
58
+ * box once the code is full. A click or an arrow key selects a filled box; a full code's caret selects its last box. An
59
+ * input method's word goes in once, when it commits.
60
+ *
61
+ * A saved code loses only its separators and case (`loadCode`); one the boxes cannot hold is kept and shown invalid at
62
+ * once, never cut. A screen reader hears the label, then what the boxes show: "9 digits, in groups of 3, 2 and 4", or
63
+ * "8 digits: DD/MM/YYYY" with a hint in each box (`describeCode`). A read-only code rings every box while it has focus.
64
+ *
65
+ * @example <CodeBox label="Verification code" autoComplete="one-time-code" required onComplete={code => verify(code)} />
66
+ * @example <CodeBox label="SSN" groups={[3, 2, 4]} separator="-" masked />
67
+ */
68
+ export declare function CodeBox(props: CodeBoxProps): import("react").JSX.Element;
69
+ export {};
@@ -0,0 +1,316 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Fragment, useEffect, useEffectEvent, useId, useLayoutEffect, useRef, useState, } from "react";
4
+ import { cx } from "../../Helpers/ClassHelpers.js";
5
+ import { codeAfterInput, codeFits, codeGroups, codeLength, describeCode, loadCode, moveCode, typeCode, validateCode, } from "../../Helpers/CodeHelpers.js";
6
+ import { Field } from "./Field.js";
7
+ import { useInputField } from "./InputField.js";
8
+ import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
9
+ /**
10
+ * A code typed one character to a box: a one-time code, a PIN, an SSN, a licence key, a hex colour. Digits, letters,
11
+ * both, hex, or any one-character pattern; boxes in groups with a separator; dots for a secret.
12
+ *
13
+ * It is one real input under the boxes, so a phone offers the code from its message (`autoComplete="one-time-code"`),
14
+ * a pasted "123-45-6789" fills every box, and a screen reader reads one field. The boxes are drawn from that input
15
+ * straight in the DOM, so typing re-renders nothing. Uncontrolled by default, like the text box; `value` also works.
16
+ *
17
+ * Typing follows one model, however the text comes - a key, a phone's keyboard, an input method, a paste, autofill
18
+ * (`typeCode`): a whole code replaces everything; a character goes over a selected box, or into the box with the ring -
19
+ * inserted while the code has an empty box, so a gap Backspace or Delete left takes back what was there, and over that
20
+ * box once the code is full. A click or an arrow key selects a filled box; a full code's caret selects its last box. An
21
+ * input method's word goes in once, when it commits.
22
+ *
23
+ * A saved code loses only its separators and case (`loadCode`); one the boxes cannot hold is kept and shown invalid at
24
+ * once, never cut. A screen reader hears the label, then what the boxes show: "9 digits, in groups of 3, 2 and 4", or
25
+ * "8 digits: DD/MM/YYYY" with a hint in each box (`describeCode`). A read-only code rings every box while it has focus.
26
+ *
27
+ * @example <CodeBox label="Verification code" autoComplete="one-time-code" required onComplete={code => verify(code)} />
28
+ * @example <CodeBox label="SSN" groups={[3, 2, 4]} separator="-" masked />
29
+ */
30
+ export function CodeBox(props) {
31
+ const { label, hint, error, design: _design, size = "md", block = false, separator, masked = false, slotPlaceholder, length, groups, characters = "digits", allowed, letterCase = "upper", required, validate, value, defaultValue, readOnlyMessage = "Read-only: this code cannot be changed.", fieldName, validateOn, onValueChange, onComplete, className, inputClassName, style, disabled, readOnly, ref, autoComplete = "off", inputMode, onChange, onBlur, onInvalid, onKeyDown, onClick, onPaste, onFocus, onSelect, ...inputProps } = props;
32
+ const rules = { length, groups, characters, allowed, letterCase, required, validate };
33
+ const shape = { ...rules, separator, slotPlaceholder };
34
+ const sizes = codeGroups(rules);
35
+ const total = codeLength(rules);
36
+ const editable = !disabled && !readOnly;
37
+ // What the boxes say to the eye - how many, of what, in which groups, the hint in each - said to a screen reader.
38
+ const shapeId = useId();
39
+ const rowRef = useRef(null);
40
+ // The last code `onComplete` was told, so moving the caret or retyping the same code does not tell it again. It is
41
+ // forgotten whenever the boxes are short of a whole code, however they got so: typing, a form's reset, the page.
42
+ const completedRef = useRef(null);
43
+ // The input as its last change left it, for a change no `beforeinput` announced (autofill).
44
+ const lastRef = useRef(null);
45
+ // The input just before the browser changes it, from `beforeinput`.
46
+ const beforeRef = useRef(null);
47
+ // While an input method composes: the input as it was before. Its word is applied once, when it commits.
48
+ const composingRef = useRef(null);
49
+ // Set while this box announces its own change, which the browser-change listener leaves alone.
50
+ const announcingRef = useRef(false);
51
+ // A controlled box's text while an input method composes, so a render then does not put the code back over the word.
52
+ const [composed, setComposed] = useState(null);
53
+ const field = useInputField({
54
+ ...props,
55
+ "aria-describedby": [shapeId, props["aria-describedby"]].filter(Boolean).join(" "),
56
+ onChange: event => {
57
+ const code = event.currentTarget.value;
58
+ onChange?.(event);
59
+ onValueChange?.(code, event);
60
+ if ([...code].length < total)
61
+ completedRef.current = null;
62
+ else if (completedRef.current !== code) {
63
+ completedRef.current = code;
64
+ onComplete?.(code);
65
+ }
66
+ },
67
+ }, {
68
+ check: (element, name) => validateCode(element.value, rules, name),
69
+ checkKeys: [value, required, total, sizes.join(), characters, allowed, letterCase, validate],
70
+ onReset: element => {
71
+ forgetIfShort(element.value);
72
+ lastRef.current = readEdit(element);
73
+ paint();
74
+ },
75
+ });
76
+ const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage);
77
+ function forgetIfShort(code) {
78
+ if ([...code].length < total)
79
+ completedRef.current = null;
80
+ }
81
+ /**
82
+ * Draws the boxes from the input: each one's character (or a dot), and the caret, the selection, and focus. While an
83
+ * input method composes, they show its word as the code will hold it.
84
+ */
85
+ function paint() {
86
+ const input = field.elementRef.current;
87
+ const row = rowRef.current;
88
+ if (!input || !row)
89
+ return;
90
+ const composing = composingRef.current;
91
+ const shown = composing ? codeAfterInput(composing, readEdit(input), rules) : readEdit(input);
92
+ const characters = [...shown.value];
93
+ const hasFocus = !disabled && input.ownerDocument.activeElement === input;
94
+ const focused = editable && hasFocus;
95
+ // A read-only code has no caret: focus marks every box, which shows the accent border alone, as a read-only box does.
96
+ const lookedAt = hasFocus && !editable;
97
+ const { start, end } = shown;
98
+ row.querySelectorAll(".nx-code__slot").forEach((slot, index, slots) => {
99
+ const character = characters[index];
100
+ const text = character === undefined ? "" : masked ? "•" : character;
101
+ if (slot.textContent !== text)
102
+ slot.textContent = text;
103
+ const selected = focused && end > start && index >= start && index < end;
104
+ // A caret is drawn on the box after it; at the end of a full code, on the last box.
105
+ const caret = focused && start === end && index === Math.min(start, slots.length - 1);
106
+ mark(slot, "data-active", lookedAt || selected || caret);
107
+ mark(slot, "data-selected", selected);
108
+ mark(slot, "data-caret", caret && character === undefined);
109
+ });
110
+ }
111
+ // A value from the parent, a new shape, or the first render: the input holds the code as the boxes take it. Not
112
+ // while an input method composes: its word goes in when it commits. A saved code the boxes cannot hold says so at once.
113
+ useLayoutEffect(() => {
114
+ const input = field.elementRef.current;
115
+ if (!input)
116
+ return;
117
+ if (!composingRef.current) {
118
+ if (value !== undefined) {
119
+ const code = loadCode(value, shape);
120
+ if (input.value !== code)
121
+ input.value = code;
122
+ }
123
+ lastRef.current = readEdit(input);
124
+ if (editable && input.value && !codeFits(input.value, rules))
125
+ field.touch();
126
+ }
127
+ forgetIfShort(input.value);
128
+ paint();
129
+ });
130
+ // The caret moves without a change (a click, the browser's own keys): follow it while the input has focus.
131
+ const follow = useEffectEvent(() => {
132
+ const input = field.elementRef.current;
133
+ if (!input || input.ownerDocument.activeElement !== input)
134
+ return;
135
+ if (!composingRef.current)
136
+ lastRef.current = readEdit(input);
137
+ paint();
138
+ });
139
+ useEffect(() => {
140
+ const document = field.elementRef.current?.ownerDocument;
141
+ document?.addEventListener("selectionchange", follow);
142
+ return () => document?.removeEventListener("selectionchange", follow);
143
+ }, []);
144
+ /* Text the browser puts in itself: a phone's keyboard, an input method, autofill, a drop, Backspace and Delete */
145
+ const beforeInput = useEffectEvent(() => {
146
+ const input = field.elementRef.current;
147
+ if (input && !composingRef.current)
148
+ beforeRef.current = readEdit(input);
149
+ });
150
+ const compositionStart = useEffectEvent(() => {
151
+ const input = field.elementRef.current;
152
+ if (input)
153
+ composingRef.current = readEdit(input);
154
+ beforeRef.current = null;
155
+ });
156
+ // The input method committed (or gave up): its word goes in once, as typed text does, and is announced.
157
+ const compositionEnd = useEffectEvent(() => {
158
+ const input = field.elementRef.current;
159
+ const composing = composingRef.current;
160
+ composingRef.current = null;
161
+ if (value !== undefined)
162
+ setComposed(null);
163
+ if (input && composing)
164
+ apply(input, codeAfterInput(composing, readEdit(input), rules), true);
165
+ });
166
+ const browserInput = useEffectEvent((event) => {
167
+ const input = field.elementRef.current;
168
+ if (!input || announcingRef.current)
169
+ return;
170
+ if (!composingRef.current && event.isComposing)
171
+ composingRef.current = lastRef.current;
172
+ if (composingRef.current) {
173
+ // Mid-composition the input keeps the input method's text: rewriting it now makes the input method type its whole
174
+ // word again. React does not hear of it either, so nothing checks or reports a word half typed.
175
+ event.stopPropagation();
176
+ if (value !== undefined)
177
+ setComposed(input.value);
178
+ paint();
179
+ return;
180
+ }
181
+ // Before React's onChange: the input holds the code the model makes of the change, and React reports that.
182
+ const before = beforeRef.current ?? lastRef.current ?? readEdit(input);
183
+ beforeRef.current = null;
184
+ apply(input, codeAfterInput(before, readEdit(input), rules), false);
185
+ });
186
+ useLayoutEffect(() => {
187
+ const input = field.elementRef.current;
188
+ if (!input)
189
+ return;
190
+ const listeners = [
191
+ ["beforeinput", () => beforeInput()],
192
+ ["compositionstart", () => compositionStart()],
193
+ ["compositionend", () => compositionEnd()],
194
+ ["input", event => browserInput(event)],
195
+ ];
196
+ for (const [name, listener] of listeners)
197
+ input.addEventListener(name, listener);
198
+ return () => {
199
+ for (const [name, listener] of listeners)
200
+ input.removeEventListener(name, listener);
201
+ };
202
+ }, []);
203
+ /** Puts an edit into the input and draws it; `announce` reports it as typing does, so React and every listener hear. */
204
+ function apply(input, edit, announce) {
205
+ // setRangeText leaves React's own copy of the value alone, so React sees the change and calls onChange.
206
+ if (input.value !== edit.value)
207
+ input.setRangeText(edit.value, 0, input.value.length, "end");
208
+ input.setSelectionRange(edit.start, edit.end);
209
+ lastRef.current = edit;
210
+ if (announce) {
211
+ announcingRef.current = true;
212
+ try {
213
+ input.dispatchEvent(new Event("input", { bubbles: true }));
214
+ }
215
+ finally {
216
+ announcingRef.current = false;
217
+ }
218
+ }
219
+ paint();
220
+ }
221
+ function handleChange(event) {
222
+ lastRef.current = readEdit(event.currentTarget);
223
+ paint();
224
+ field.controlProps.onChange(event);
225
+ }
226
+ function handleKeyDown(event) {
227
+ readOnlyNotice.onKeyDown(event);
228
+ onKeyDown?.(event);
229
+ // An input method's keys are its own until it commits (229 is a key it holds, in every browser).
230
+ const composing = event.nativeEvent.isComposing || event.keyCode === 229;
231
+ if (event.defaultPrevented || !editable || composing || event.ctrlKey || event.metaKey || event.altKey)
232
+ return;
233
+ const input = event.currentTarget;
234
+ if (isCodeMove(event.key) && !event.shiftKey) {
235
+ event.preventDefault();
236
+ apply(input, moveCode(readEdit(input), event.key, rules), false);
237
+ return;
238
+ }
239
+ if ([...event.key].length !== 1)
240
+ return;
241
+ // One character from a keyboard goes in by the model; one the code does not take is not typed at all.
242
+ event.preventDefault();
243
+ const next = typeCode(readEdit(input), event.key, rules);
244
+ if (next)
245
+ apply(input, next, true);
246
+ }
247
+ function handlePaste(event) {
248
+ onPaste?.(event);
249
+ if (event.defaultPrevented || !editable)
250
+ return;
251
+ event.preventDefault();
252
+ const input = event.currentTarget;
253
+ // A whole code replaces what is there, a part of one goes in at the caret, and a message is read for its code.
254
+ const next = typeCode(readEdit(input), event.clipboardData.getData("text"), rules);
255
+ if (next)
256
+ apply(input, next, true);
257
+ }
258
+ function handleClick(event) {
259
+ readOnlyNotice.onClick(event);
260
+ onClick?.(event);
261
+ const input = event.currentTarget;
262
+ const slots = rowRef.current?.querySelectorAll(".nx-code__slot");
263
+ if (!editable || !slots?.length)
264
+ return;
265
+ // The box under the pointer: a filled one is selected, to type over it; past the code, the caret goes to its end.
266
+ let nearest = 0;
267
+ let distance = Infinity;
268
+ slots.forEach((slot, index) => {
269
+ const box = slot.getBoundingClientRect();
270
+ const away = Math.abs(event.clientX - (box.left + box.right) / 2);
271
+ if (away < distance) {
272
+ nearest = index;
273
+ distance = away;
274
+ }
275
+ });
276
+ const filled = [...input.value].length;
277
+ if (nearest < filled)
278
+ input.setSelectionRange(nearest, nearest + 1);
279
+ else
280
+ input.setSelectionRange(filled, filled);
281
+ paint();
282
+ }
283
+ function handleFocus(event) {
284
+ onFocus?.(event);
285
+ paint();
286
+ }
287
+ function handleBlur(event) {
288
+ readOnlyNotice.hide();
289
+ field.controlProps.onBlur(event);
290
+ paint();
291
+ }
292
+ const hints = [...(slotPlaceholder ?? "")];
293
+ let next = 0;
294
+ const boxes = sizes.map((count, group) => (_jsxs(Fragment, { children: [group > 0 && _jsx("span", { className: "nx-code__separator", children: separator }), _jsx("span", { className: "nx-code__group", children: Array.from({ length: count }, () => {
295
+ const index = next++;
296
+ return _jsx("span", { className: "nx-code__slot", "data-placeholder": (hints.length === 1 ? hints[0] : hints[index]) || undefined }, index);
297
+ }) })] }, group)));
298
+ return (_jsxs(_Fragment, { children: [_jsx(Field, { controlId: field.controlId, messageId: field.messageId, label: label, required: required, size: size, hint: hint, error: field.error, actions: readOnlyNotice.lock, disabled: disabled, readOnly: readOnly, className: cx("nx-field--code", className), style: style, children: _jsxs("span", { className: "nx-code", "data-block": block || undefined, children: [_jsx("span", { ref: rowRef, className: "nx-code__slots", "aria-hidden": "true", children: boxes }), _jsx("span", { id: shapeId, className: "nx-visually-hidden", children: describeCode(shape) }), _jsx("input", { autoCorrect: "off", spellCheck: false, autoCapitalize: characters === "digits" && !allowed ? "off" : "characters", ...inputProps, ...field.controlProps, type: masked ? "password" : "text", inputMode: inputMode ?? (characters === "digits" && !allowed ? "numeric" : "text"), autoComplete: autoComplete, className: cx("nx-code__input", inputClassName), value: value === undefined ? undefined : (composed ?? loadCode(value, shape)), defaultValue: value === undefined ? loadCode(defaultValue, shape) : undefined, disabled: disabled, readOnly: readOnly, onChange: handleChange, onKeyDown: handleKeyDown, onPaste: handlePaste, onClick: handleClick, onFocus: handleFocus, onBlur: handleBlur, onSelect: event => {
299
+ onSelect?.(event);
300
+ paint();
301
+ } })] }) }), readOnlyNotice.notice] }));
302
+ }
303
+ function mark(element, name, on) {
304
+ if (on)
305
+ element.setAttribute(name, "");
306
+ else
307
+ element.removeAttribute(name);
308
+ }
309
+ /** The input's code and selection, as the typing model takes them. */
310
+ function readEdit(input) {
311
+ const value = input.value;
312
+ const start = input.selectionStart ?? value.length;
313
+ return { value, start, end: input.selectionEnd ?? start };
314
+ }
315
+ const CODE_MOVES = new Set(["ArrowLeft", "ArrowRight", "Home", "End"]);
316
+ const isCodeMove = (key) => CODE_MOVES.has(key);
@@ -10,6 +10,7 @@ import { Calendar } from "./Calendar.js";
10
10
  import { Field, hasContent } from "./Field.js";
11
11
  import { setInputValue, useInputField } from "./InputField.js";
12
12
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
13
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
13
14
  const GAP = 4;
14
15
  const MARGIN = 8;
15
16
  /**
@@ -20,7 +21,9 @@ const MARGIN = 8;
20
21
  * @example <DatePicker name="checkIn" label="Check-in" required min={todayIsoDate()} />
21
22
  */
22
23
  export function DatePicker(props) {
23
- const { label, hint, error, design = "standard", size = "md", icon, prefix, clearable = true, closeOnSelect = true, fieldName, readOnlyMessage = "Read-only: this date cannot be changed.", validateOn, value, defaultValue, onValueChange, weekStart, weekendDays, isDateDisabled, showOutsideDays, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
24
+ const { label, hint, error, design: ownDesign, size = "md", icon, prefix, clearable = true, closeOnSelect = true, fieldName, readOnlyMessage = "Read-only: this date cannot be changed.", validateOn, value, defaultValue, onValueChange, weekStart, weekendDays, isDateDisabled, showOutsideDays, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
25
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
26
+ const design = useInputDesign(ownDesign, Boolean(label));
24
27
  const preferences = useDatePreferences();
25
28
  const settings = {
26
29
  calendar: props.calendar ?? preferences.calendar,
@@ -10,6 +10,7 @@ import { Calendar } from "./Calendar.js";
10
10
  import { Field, hasContent } from "./Field.js";
11
11
  import { setInputValue, useInputField } from "./InputField.js";
12
12
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
13
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
13
14
  const GAP = 4;
14
15
  const MARGIN = 8;
15
16
  // Two months of the small calendar are about 480px wide; a narrower window gets one month.
@@ -26,7 +27,9 @@ const sameRange = (a, b) => (a === null || b === null ? a === b : a[0] === b[0]
26
27
  * @example <DateRangePicker startName="checkIn" endName="checkOut" label="Stay" required min={todayIsoDate()} minDistance={1} />
27
28
  */
28
29
  export function DateRangePicker(props) {
29
- const { label, hint, error, design = "standard", size = "md", icon, prefix, months = 2, clearable = true, closeOnSelect = true, fieldName, readOnlyMessage = "Read-only: these dates cannot be changed.", validateOn, value, defaultValue, onValueChange, startName, endName, weekStart, weekendDays, isDateDisabled, showOutsideDays, className, inputClassName, style, required, min, max, minDistance, maxDistance, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
30
+ const { label, hint, error, design: ownDesign, size = "md", icon, prefix, months = 2, clearable = true, closeOnSelect = true, fieldName, readOnlyMessage = "Read-only: these dates cannot be changed.", validateOn, value, defaultValue, onValueChange, startName, endName, weekStart, weekendDays, isDateDisabled, showOutsideDays, className, inputClassName, style, required, min, max, minDistance, maxDistance, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
31
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
32
+ const design = useInputDesign(ownDesign, Boolean(label));
30
33
  const preferences = useDatePreferences();
31
34
  const settings = {
32
35
  calendar: props.calendar ?? preferences.calendar,
@@ -15,6 +15,7 @@ import { Field, hasContent } from "./Field.js";
15
15
  import { setInputValue, useInputField } from "./InputField.js";
16
16
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
17
17
  import { TimePanel } from "./TimePanel.js";
18
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
18
19
  const PANEL_GAP = 4;
19
20
  const PANEL_MARGIN = 8;
20
21
  /**
@@ -27,7 +28,9 @@ const PANEL_MARGIN = 8;
27
28
  * @example <DateTimePicker name="pickupAt" label="Pickup" required min={new Date()} step={900} />
28
29
  */
29
30
  export function DateTimePicker(props) {
30
- const { label, hint, error, design = "standard", size = "md", icon, prefix, calendar: calendarProp, format: formatProp, timeFormat: timeFormatProp, script: scriptProp, timeZone: timeZoneProp, showTimeZone = Boolean(timeZoneProp), view = "columns", defaultTime, clearable = true, fieldName, readOnlyMessage = "Read-only: this date and time cannot be changed.", validateOn, value, defaultValue, onValueChange, isDateDisabled, isTimeDisabled, weekStart, weekendDays, showOutsideDays, className, inputClassName, style, required, min, max, minTime, maxTime, step, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
31
+ const { label, hint, error, design: ownDesign, size = "md", icon, prefix, calendar: calendarProp, format: formatProp, timeFormat: timeFormatProp, script: scriptProp, timeZone: timeZoneProp, showTimeZone = Boolean(timeZoneProp), view = "columns", defaultTime, clearable = true, fieldName, readOnlyMessage = "Read-only: this date and time cannot be changed.", validateOn, value, defaultValue, onValueChange, isDateDisabled, isTimeDisabled, weekStart, weekendDays, showOutsideDays, className, inputClassName, style, required, min, max, minTime, maxTime, step, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onClick, ...inputProps } = props;
32
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
33
+ const design = useInputDesign(ownDesign, Boolean(label));
31
34
  const preferences = useDatePreferences();
32
35
  const browserZone = useBrowserTimeZone();
33
36
  // Unknown on the server and while the page hydrates: the box stays empty until then rather than show another zone's time.
@@ -11,6 +11,7 @@ import { DropdownChip, DropdownMedia, DropdownOption, DropdownTag, hasIcon } fro
11
11
  import { Field } from "./Field.js";
12
12
  import { useInputField } from "./InputField.js";
13
13
  import { useReadOnlyNotice } from "./ReadOnlyNotice.js";
14
+ import { useInputDesign } from "../../Helpers/UserPreferences.js";
14
15
  /** Rows a local list renders at first, and adds each time it scrolls near its end, so 10,000 options open at once. */
15
16
  const RENDER_STEP = 100;
16
17
  /** Closer to the end of the list than this, in pixels, renders or loads more. */
@@ -27,7 +28,9 @@ const NAMES_LISTED = 20;
27
28
  const SKELETON_WIDTHS = ["72%", "54%", "64%", "46%"];
28
29
  const IDLE_LIST = { search: "", loadKey: undefined, items: [], page: 0, hasMore: false, total: undefined, status: "idle", errorPage: 1, firstKeys: new Set() };
29
30
  export function Dropdown(props) {
30
- const { label, hint, error, design = "standard", size = "md", icon, options, loadOptions, loadKey, pageSize = 50, selectedOptions, resolveOptions, valueKey = "value", labelKey = "label", descriptionKey = "description", iconKey = "icon", columns, searchKeys, isOptionDisabled = isDisabledItem, isOptionInactive = isInactiveItem, searchable: searchableProp, clearable = true, loading = false, emptyMessage = "No options", renderValue, panelWidth, readOnlyMessage, fieldName, validateOn, className, inputClassName, style, required, validate, name, form, placeholder, disabled, readOnly, ref, onBlur, onFocus, onKeyDown, onClick, onInvalid, multiple: multipleProp, value: valueProp, defaultValue, onValueChange, min, max, selectAll = true, maxChips = 10, ...inputProps } = props;
31
+ const { label, hint, error, design: ownDesign, size = "md", icon, options, loadOptions, loadKey, pageSize = 50, selectedOptions, resolveOptions, valueKey = "value", labelKey = "label", descriptionKey = "description", iconKey = "icon", columns, searchKeys, isOptionDisabled = isDisabledItem, isOptionInactive = isInactiveItem, searchable: searchableProp, clearable = true, loading = false, emptyMessage = "No options", renderValue, panelWidth, readOnlyMessage, fieldName, validateOn, className, inputClassName, style, required, validate, name, form, placeholder, disabled, readOnly, ref, onBlur, onFocus, onKeyDown, onClick, onInvalid, multiple: multipleProp, value: valueProp, defaultValue, onValueChange, min, max, selectAll = true, maxChips = 10, ...inputProps } = props;
32
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
33
+ const design = useInputDesign(ownDesign, Boolean(label));
31
34
  const multiple = multipleProp === true;
32
35
  const controlled = valueProp !== undefined;
33
36
  const editable = !disabled && !readOnly;
@@ -11,7 +11,7 @@ export declare const DROPDOWN_STATE_LABELS: Record<DropdownValueState, string>;
11
11
  /** The tooltip of a current value that cannot be picked again. */
12
12
  export declare const DROPDOWN_STATE_NOTES: Record<DropdownValueState, string>;
13
13
  type AnyColumn = DropdownColumn<never>;
14
- /** A round photo, or the initials of the name on a soft tone when there is no photo or it fails to load. */
14
+ /** The shared `Avatar`, sized to the dropdown's pictures by its class. */
15
15
  export declare function DropdownAvatar({ src, name }: {
16
16
  src: unknown;
17
17
  name: string;
@@ -3,7 +3,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { Icon, iconCheck, iconX, InlineIcon } from "nexus-icons";
4
4
  import { isValidElement, memo, useState } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
- import { avatarTone, initials, isMediaColumn, readField } from "../../Helpers/DropdownHelpers.js";
6
+ import { isMediaColumn, readField } from "../../Helpers/DropdownHelpers.js";
7
+ import { Avatar } from "../Viewers/Avatar.js";
7
8
  export const DROPDOWN_STATE_LABELS = {
8
9
  inactive: "Inactive",
9
10
  unavailable: "Unavailable",
@@ -15,12 +16,9 @@ export const DROPDOWN_STATE_NOTES = {
15
16
  unavailable: "No longer in the list: kept as the current value. Once removed, it cannot be chosen again.",
16
17
  unknown: "Not found in the list: kept as the current value. Once removed, it cannot be chosen again.",
17
18
  };
18
- /** A round photo, or the initials of the name on a soft tone when there is no photo or it fails to load. */
19
+ /** The shared `Avatar`, sized to the dropdown's pictures by its class. */
19
20
  export function DropdownAvatar({ src, name }) {
20
- const url = typeof src === "string" ? src.trim() : "";
21
- const [failed, setFailed] = useState(null);
22
- const photo = url !== "" && failed !== url;
23
- return (_jsx("span", { className: "nx-dropdown-avatar", "data-tone": photo ? undefined : avatarTone(name), "aria-hidden": "true", children: photo ? _jsx("img", { src: url, alt: "", loading: "lazy", decoding: "async", onError: () => setFailed(url) }) : initials(name) }));
21
+ return _jsx(Avatar, { name: name, src: src, className: "nx-dropdown-avatar" });
24
22
  }
25
23
  /** A small 4:3 picture, such as a flag or a logo; an empty frame when there is none or it fails to load. */
26
24
  export function DropdownImage({ src }) {
@@ -2,6 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { isBooleanInput } from "../../Helpers/InputParamsHelpers.js";
3
3
  import { CheckBox, RadioButton, Switch } from "./CheckBox.js";
4
4
  import { CheckBoxGroup } from "./CheckBoxGroup.js";
5
+ import { CodeBox } from "./CodeBox.js";
5
6
  import { DatePicker } from "./DatePicker.js";
6
7
  import { DateRangePicker } from "./DateRangePicker.js";
7
8
  import { DateTimePicker } from "./DateTimePicker.js";
@@ -58,6 +59,8 @@ export function renderInput(params, overrides = {}) {
58
59
  return _jsx(TextArea, { ...props });
59
60
  case "number":
60
61
  return _jsx(NumberBox, { ...props });
62
+ case "code":
63
+ return _jsx(CodeBox, { ...props });
61
64
  case "checkbox":
62
65
  return _jsx(CheckBox, { ...props });
63
66
  case "switch":
@@ -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, DEFAULT_NUMBER_LOCALE, formatNumber, getNumberSymbols, isPartialNumber, parseNumber, readPastedNumber, roundDecimal, stepNumber, } from "../../Helpers/NumberHelpers.js";
6
+ import { clampNumber, cleanNumberText, formatNumber, getNumberSymbols, isPartialNumber, parseNumber, readPastedNumber, roundDecimal, stepNumber, } from "../../Helpers/NumberHelpers.js";
7
+ import { useInputDesign, 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,12 @@ 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 = DEFAULT_NUMBER_LOCALE, className, inputClassName, style, required, min, max, validate, name, form, placeholder, disabled, readOnly, ref, onChange, onBlur, onFocus, onInvalid, onKeyDown, onPaste, onClick, ...inputProps } = props;
27
+ const { label, hint, error, design: ownDesign, 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
+ // The person's input design (the quick preferences) when this page and its form name none, for a labelled input.
29
+ const design = useInputDesign(ownDesign, Boolean(label));
30
+ // Without a locale of its own, the box writes and reads numbers the way the person chose (Number scale, Thousands).
31
+ const preferred = useNumberLocale();
32
+ const locale = ownLocale ?? preferred;
27
33
  const format = { decimals, fixedDecimals, grouping, locale };
28
34
  const { decimal, group } = getNumberSymbols(locale);
29
35
  const textRules = { decimals, negative: min === undefined || min < 0 };
@@ -55,8 +61,11 @@ export function NumberBox(props) {
55
61
  const readOnlyNotice = useReadOnlyNotice(Boolean(readOnly) && !disabled, field.elementRef, readOnlyMessage, {
56
62
  keys: ["ArrowUp", "ArrowDown", "PageUp", "PageDown"],
57
63
  });
64
+ // The number the box holds, as last read in the marks it was written in.
65
+ const numberRef = useRef(initial);
58
66
  /** Writes what depends on the value straight to the DOM, so typing re-renders nothing: the hidden input and the button limits. */
59
67
  function sync(number) {
68
+ numberRef.current = number;
60
69
  if (hiddenRef.current)
61
70
  hiddenRef.current.value = isNumber(number) ? String(number) : "";
62
71
  for (const [button, direction] of [
@@ -69,6 +78,21 @@ export function NumberBox(props) {
69
78
  button?.removeAttribute("aria-disabled");
70
79
  }
71
80
  }
81
+ // The person changed how numbers are written (Number scale, Thousands): the number the box held is written again in
82
+ // the new marks. Its text is not read again, because by now it may be either: React has already rewritten an untouched
83
+ // box's default text, and "1,250.5" read with a decimal comma would be 1.2505. Runs before the effects below read it.
84
+ const localeRef = useRef(locale);
85
+ useLayoutEffect(() => {
86
+ const previous = localeRef.current;
87
+ localeRef.current = locale;
88
+ const input = field.elementRef.current;
89
+ const number = numberRef.current;
90
+ if (previous === locale || !input || !input.value || !isNumber(number))
91
+ return;
92
+ input.value = document.activeElement === input ? editText(number) : displayText(number);
93
+ sync(number);
94
+ // eslint-disable-next-line react-hooks/exhaustive-deps
95
+ }, [locale]);
72
96
  // A controlled value that differs from the box's text replaces it. Text that already means this number ("12.") stays.
73
97
  useLayoutEffect(() => {
74
98
  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
- const text = (value) => (format ? format(value) : `${prefix ?? ""}${formatNumber(value, { decimals, fixedDecimals, grouping, locale })}${suffix ?? ""}`);
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);