nexus-shared 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/CHANGELOG.md +446 -262
  2. package/LICENSE +21 -21
  3. package/README.md +1084 -1000
  4. package/dist/Client.Index.d.ts +3 -0
  5. package/dist/Client.Index.js +3 -0
  6. package/dist/Components/Chats/ChatParts.d.ts +2 -2
  7. package/dist/Components/Chats/ChatParts.js +3 -7
  8. package/dist/Components/Chats/ChatThread.js +3 -3
  9. package/dist/Components/Documents/Button.d.ts +6 -1
  10. package/dist/Components/Documents/Button.js +2 -2
  11. package/dist/Components/Documents/Menu.js +21 -9
  12. package/dist/Components/Documents/SplitButton.d.ts +3 -1
  13. package/dist/Components/Documents/SplitButton.js +7 -5
  14. package/dist/Components/Documents/TabButtons.d.ts +5 -2
  15. package/dist/Components/Documents/TabButtons.js +2 -2
  16. package/dist/Components/Forms/ApiForm.d.ts +1 -1
  17. package/dist/Components/Forms/ApiForm.js +1 -1
  18. package/dist/Components/Forms/Crud.js +177 -58
  19. package/dist/Components/Forms/CrudParts.d.ts +5 -1
  20. package/dist/Components/Forms/CrudParts.js +37 -5
  21. package/dist/Components/Forms/ExcelImport.d.ts +23 -7
  22. package/dist/Components/Forms/ExcelImport.js +189 -60
  23. package/dist/Components/Forms/Form.js +5 -13
  24. package/dist/Components/Forms/SubmitForm.d.ts +11 -2
  25. package/dist/Components/Forms/SubmitForm.js +23 -12
  26. package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
  27. package/dist/Components/Inputs/DropdownParts.js +4 -6
  28. package/dist/Components/Inputs/NumberBox.js +24 -2
  29. package/dist/Components/Inputs/RowsInput.js +2 -2
  30. package/dist/Components/Inputs/Slider.js +4 -1
  31. package/dist/Components/Inputs/TabularForm.js +42 -10
  32. package/dist/Components/Inputs/TimePanel.js +4 -3
  33. package/dist/Components/Layouts/Popup.js +1 -4
  34. package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
  35. package/dist/Components/Layouts/ThemePicker.js +77 -6
  36. package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
  37. package/dist/Components/Layouts/ThemeScript.js +4 -2
  38. package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
  39. package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
  40. package/dist/Components/Viewers/Avatar.d.ts +21 -0
  41. package/dist/Components/Viewers/Avatar.js +21 -0
  42. package/dist/Components/Viewers/DataTable.js +192 -44
  43. package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
  44. package/dist/Components/Viewers/DataTableColumns.js +25 -68
  45. package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
  46. package/dist/Components/Viewers/DataTableFilters.js +68 -0
  47. package/dist/Components/Viewers/DataTableParts.d.ts +2 -2
  48. package/dist/Components/Viewers/DataTableParts.js +3 -3
  49. package/dist/Helpers/AnimationHelpers.d.ts +11 -0
  50. package/dist/Helpers/AnimationHelpers.js +20 -2
  51. package/dist/Helpers/CrudBackend.d.ts +7 -4
  52. package/dist/Helpers/CrudBackend.js +4 -2
  53. package/dist/Helpers/ExcelBackend.d.ts +3 -3
  54. package/dist/Helpers/ExcelBackend.js +8 -2
  55. package/dist/Helpers/ExcelHelpers.d.ts +36 -8
  56. package/dist/Helpers/ExcelHelpers.js +55 -13
  57. package/dist/Helpers/FormStore.d.ts +9 -5
  58. package/dist/Helpers/FormStore.js +25 -5
  59. package/dist/Helpers/MessageBuilder.js +1 -0
  60. package/dist/Helpers/NumberHelpers.d.ts +7 -1
  61. package/dist/Helpers/NumberHelpers.js +41 -3
  62. package/dist/Helpers/PopoverHelpers.d.ts +17 -5
  63. package/dist/Helpers/PopoverHelpers.js +52 -17
  64. package/dist/Helpers/PopoverHooks.d.ts +24 -0
  65. package/dist/Helpers/PopoverHooks.js +62 -0
  66. package/dist/Helpers/RowsStore.d.ts +7 -2
  67. package/dist/Helpers/RowsStore.js +15 -3
  68. package/dist/Helpers/TableExport.js +12 -12
  69. package/dist/Helpers/TableFilters.d.ts +25 -0
  70. package/dist/Helpers/TableFilters.js +162 -0
  71. package/dist/Helpers/TableHelpers.d.ts +6 -3
  72. package/dist/Helpers/TableHelpers.js +8 -4
  73. package/dist/Helpers/TableStore.d.ts +11 -3
  74. package/dist/Helpers/TableStore.js +16 -5
  75. package/dist/Helpers/ThemeHelpers.d.ts +21 -4
  76. package/dist/Helpers/ThemeHelpers.js +46 -9
  77. package/dist/Helpers/UserPreferences.d.ts +14 -0
  78. package/dist/Helpers/UserPreferences.js +99 -0
  79. package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
  80. package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
  81. package/dist/Interfaces/FormInterfaces.d.ts +33 -5
  82. package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
  83. package/dist/Interfaces/PreferenceInterfaces.js +28 -0
  84. package/dist/Interfaces/TableInterfaces.d.ts +59 -12
  85. package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
  86. package/dist/Interfaces/ThemeInterfaces.js +15 -0
  87. package/dist/Services/ExcelApi.d.ts +3 -1
  88. package/dist/Services/ExcelApi.js +1 -1
  89. package/dist/Services/ThemeService.d.ts +18 -2
  90. package/dist/Services/ThemeService.js +80 -16
  91. package/dist/Shared.Index.d.ts +2 -0
  92. package/dist/Shared.Index.js +2 -0
  93. package/package.json +85 -85
  94. package/src/Styles/Nexus.Avatar.css +79 -0
  95. package/src/Styles/Nexus.Base.css +117 -70
  96. package/src/Styles/Nexus.Button.css +180 -153
  97. package/src/Styles/Nexus.Calendar.css +273 -275
  98. package/src/Styles/Nexus.Chat.css +1443 -1489
  99. package/src/Styles/Nexus.Choice.css +343 -345
  100. package/src/Styles/Nexus.Crud.css +168 -161
  101. package/src/Styles/Nexus.DateTime.css +81 -81
  102. package/src/Styles/Nexus.Dropdown.css +725 -774
  103. package/src/Styles/Nexus.Editor.css +295 -297
  104. package/src/Styles/Nexus.Excel.css +389 -370
  105. package/src/Styles/Nexus.Form.css +279 -321
  106. package/src/Styles/Nexus.Index.css +37 -36
  107. package/src/Styles/Nexus.Input.css +602 -637
  108. package/src/Styles/Nexus.Markdown.css +237 -265
  109. package/src/Styles/Nexus.Menu.css +237 -259
  110. package/src/Styles/Nexus.Popup.css +724 -731
  111. package/src/Styles/Nexus.RichText.css +2 -32
  112. package/src/Styles/Nexus.Rows.css +637 -596
  113. package/src/Styles/Nexus.Slider.css +276 -278
  114. package/src/Styles/Nexus.Tab.Buttons.css +432 -419
  115. package/src/Styles/Nexus.Table.css +1386 -1296
  116. package/src/Styles/Nexus.Theme.Picker.css +286 -232
  117. package/src/Styles/Nexus.Themes.css +194 -194
  118. package/src/Styles/Nexus.Time.css +144 -147
  119. package/src/Styles/Nexus.Toast.css +263 -265
  120. package/src/Styles/Nexus.Tokens.css +89 -72
  121. package/src/Styles/Nexus.Tree.css +431 -435
@@ -43,12 +43,15 @@ export * from "./Components/Layouts/PopupRules.ts";
43
43
  export * from "./Components/Layouts/ThemePicker.tsx";
44
44
  export * from "./Components/Layouts/ThemeSwitcher.tsx";
45
45
  export * from "./Components/Layouts/Toaster.tsx";
46
+ export * from "./Components/Viewers/Avatar.tsx";
46
47
  export * from "./Components/Viewers/DataTable.tsx";
47
48
  export * from "./Components/Viewers/DataTableParts.tsx";
48
49
  export * from "./Helpers/ChatHooks.ts";
49
50
  export * from "./Helpers/DatePreferences.ts";
50
51
  export * from "./Helpers/DragHelpers.ts";
51
52
  export * from "./Helpers/PopoverHelpers.ts";
53
+ export * from "./Helpers/PopoverHooks.ts";
54
+ export * from "./Helpers/UserPreferences.ts";
52
55
  export * from "./Services/BrowserApi.ts";
53
56
  export * from "./Services/ChatApi.ts";
54
57
  export * from "./Services/ExcelApi.ts";
@@ -46,6 +46,7 @@ export * from "./Components/Layouts/PopupRules.js";
46
46
  export * from "./Components/Layouts/ThemePicker.js";
47
47
  export * from "./Components/Layouts/ThemeSwitcher.js";
48
48
  export * from "./Components/Layouts/Toaster.js";
49
+ export * from "./Components/Viewers/Avatar.js";
49
50
  export * from "./Components/Viewers/DataTable.js";
50
51
  export * from "./Components/Viewers/DataTableParts.js";
51
52
  // Helpers
@@ -53,6 +54,8 @@ export * from "./Helpers/ChatHooks.js";
53
54
  export * from "./Helpers/DatePreferences.js";
54
55
  export * from "./Helpers/DragHelpers.js";
55
56
  export * from "./Helpers/PopoverHelpers.js";
57
+ export * from "./Helpers/PopoverHooks.js";
58
+ export * from "./Helpers/UserPreferences.js";
56
59
  // Services
57
60
  export * from "./Services/BrowserApi.js";
58
61
  export * from "./Services/ChatApi.js";
@@ -1,7 +1,7 @@
1
- import { type ReactNode } from "react";
1
+ import type { ReactNode } from "react";
2
2
  import { type ChatTimeOptions } from "../../Helpers/ChatHelpers.ts";
3
3
  import type { ChatAttachment, ChatId, ChatMessage, ChatParticipant } from "../../Interfaces/ChatInterfaces.ts";
4
- /** A round photo, or the initials of the name on a tone picked from it — the same look and tones as a dropdown's. */
4
+ /** The shared `Avatar`, under the name the chat has always exported it by. */
5
5
  export declare function ChatAvatar({ name, src, size }: {
6
6
  name: string;
7
7
  src?: string | null;
@@ -1,23 +1,19 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconAlertCircle, iconCheck, iconChecks, iconCornerUpLeft, iconFile, iconLoader2, iconRefresh, iconTrash, iconX, InlineIcon } from "nexus-icons";
4
- import { useState } from "react";
5
4
  import { cx } from "../../Helpers/ClassHelpers.js";
6
5
  import { chatDeliveryState, chatFileSize, chatTimeLabel, sameChatId, CHAT_DELIVERY_LABELS } from "../../Helpers/ChatHelpers.js";
7
- import { avatarTone, initials } from "../../Helpers/DropdownHelpers.js";
8
6
  import { markdownToPlainText } from "../../Helpers/MarkdownHelpers.js";
7
+ import { Avatar } from "../Viewers/Avatar.js";
9
8
  import { MarkdownView } from "../Viewers/MarkdownView.js";
10
9
  /*
11
10
  * The pieces a chat is drawn from: the avatar, one message, the lines that break a thread into days and into read and
12
11
  * unread, and the participants. Each one draws what it is given and nothing else — the state is in `ChatStore` — so
13
12
  * the same pieces serve the panel, a page-wide chat, and a demo with messages in memory.
14
13
  */
15
- /** A round photo, or the initials of the name on a tone picked from it — the same look and tones as a dropdown's. */
14
+ /** The shared `Avatar`, under the name the chat has always exported it by. */
16
15
  export function ChatAvatar({ name, src, size = "md" }) {
17
- const url = typeof src === "string" ? src.trim() : "";
18
- const [failed, setFailed] = useState(null);
19
- const photo = url !== "" && failed !== url;
20
- return (_jsx("span", { className: cx("nx-chat-avatar", size === "sm" && "nx-chat-avatar--sm"), "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) }));
16
+ return _jsx(Avatar, { name: name, src: src, size: size });
21
17
  }
22
18
  /** The day a run of messages was written: "Today", "Yesterday", or the date in the user's calendar. */
23
19
  export function ChatDayDivider({ label }) {
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconArrowDown, iconMessageCircle, InlineIcon } from "nexus-icons";
4
4
  import { Fragment, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
5
+ import { smoothScrollAllowed } from "../../Helpers/AnimationHelpers.js";
5
6
  import { cx } from "../../Helpers/ClassHelpers.js";
6
7
  import { chatBadgeText, chatMessageKey, groupChatMessages, sameChatId } from "../../Helpers/ChatHelpers.js";
7
8
  import { useBrowserTimeZone, useDatePreferences } from "../../Helpers/DatePreferences.js";
@@ -37,9 +38,8 @@ export function ChatThread({ store, state, userId, time, replies = true, onReply
37
38
  const list = listRef.current;
38
39
  if (!list)
39
40
  return;
40
- // Windows with its animations off reports reduced motion, and a smooth scroll there may never run: jump instead.
41
- const reduced = typeof window !== "undefined" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
42
- list.scrollTo({ top: list.scrollHeight, behavior: smooth && !reduced ? "smooth" : "auto" });
41
+ // Smooth only when the visitor and the system both allow motion; otherwise it jumps.
42
+ list.scrollTo({ top: list.scrollHeight, behavior: smooth && smoothScrollAllowed() ? "smooth" : "auto" });
43
43
  }, []);
44
44
  // New messages: stay at the bottom when already there, otherwise hold the place and count what came in.
45
45
  useLayoutEffect(() => {
@@ -21,9 +21,14 @@ export interface ButtonProps extends ComponentPropsWithRef<"button"> {
21
21
  loading?: boolean;
22
22
  /** Fills the width of its container. */
23
23
  block?: boolean;
24
+ /**
25
+ * A small dot on the corner. `true` is the red dot of something new, such as unread notifications; `"primary"` marks
26
+ * a state that is on, such as filters.
27
+ */
28
+ dot?: boolean | "primary";
24
29
  }
25
30
  /**
26
31
  * A button. Without children it becomes a square icon button, which then needs an `aria-label`.
27
32
  * Has no client state, so it renders in server and client components alike.
28
33
  */
29
- export declare function Button({ variant, size, hoverTone, icon, iconEnd, loading, block, type, disabled, className, children, ...rest }: ButtonProps): import("react").JSX.Element;
34
+ export declare function Button({ variant, size, hoverTone, icon, iconEnd, loading, block, dot, type, disabled, className, children, ...rest }: ButtonProps): import("react").JSX.Element;
@@ -5,7 +5,7 @@ import { cx } from "../../Helpers/ClassHelpers.js";
5
5
  * A button. Without children it becomes a square icon button, which then needs an `aria-label`.
6
6
  * Has no client state, so it renders in server and client components alike.
7
7
  */
8
- export function Button({ variant = "secondary", size = "md", hoverTone, icon, iconEnd, loading = false, block = false, type = "button", disabled, className, children, ...rest }) {
8
+ export function Button({ variant = "secondary", size = "md", hoverTone, icon, iconEnd, loading = false, block = false, dot = false, type = "button", disabled, className, children, ...rest }) {
9
9
  const hasLabel = children !== undefined && children !== null && children !== false;
10
- return (_jsxs("button", { type: type, className: cx("nx-btn", `nx-btn--${variant}`, `nx-btn--${size}`, hoverTone && `nx-btn--hover-${hoverTone}`, block && "nx-btn--block", !hasLabel && "nx-btn--icon", className), disabled: disabled || loading, "aria-busy": loading || undefined, ...rest, children: [loading ? _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }) : icon, hasLabel && _jsx("span", { className: "nx-btn__label", children: children }), iconEnd] }));
10
+ return (_jsxs("button", { type: type, className: cx("nx-btn", `nx-btn--${variant}`, `nx-btn--${size}`, hoverTone && `nx-btn--hover-${hoverTone}`, block && "nx-btn--block", !hasLabel && "nx-btn--icon", className), disabled: disabled || loading, "aria-busy": loading || undefined, ...rest, children: [loading ? _jsx(InlineIcon, { icon: iconLoader2, className: "nx-spin" }) : icon, hasLabel && _jsx("span", { className: "nx-btn__label", children: children }), iconEnd, dot && _jsx("span", { className: "nx-btn__dot", "data-tone": dot === true ? undefined : dot, "aria-hidden": "true" })] }));
11
11
  }
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { iconCheck, InlineIcon } from "nexus-icons";
4
4
  import { cloneElement, useId, useLayoutEffect, useRef, useState, } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
- import { followAnchor, isPopoverOpen, placeAtPoint, placeUnderAnchor } from "../../Helpers/PopoverHelpers.js";
6
+ import { followAnchor, isPopoverOpen, placeAtPoint, placeUnderAnchor, showPanel } from "../../Helpers/PopoverHelpers.js";
7
7
  export function isMenuItem(entry) {
8
8
  return "value" in entry;
9
9
  }
@@ -12,10 +12,16 @@ const ITEM_ROLE = {
12
12
  radio: "menuitemradio",
13
13
  checkbox: "menuitemcheckbox",
14
14
  };
15
- const ENABLED_ITEM = '.nx-menu__item:not([aria-disabled="true"])';
15
+ // A row, and a control an app put in a heading (the admin's account card has the picture there). The arrow keys walk
16
+ // both: Tab shuts a menu, so a control inside one is reachable no other way.
17
+ const ENABLED_ITEM = '.nx-menu__item:not([aria-disabled="true"]), .nx-menu__heading [role="menuitem"]:not([aria-disabled="true"])';
16
18
  function enabledItems(panel) {
17
19
  return Array.from(panel.querySelectorAll(ENABLED_ITEM));
18
20
  }
21
+ /** Where a menu opened from the keyboard starts: its first row, never a control in a heading above the rows. */
22
+ function firstRowIndex(panel) {
23
+ return Math.max(0, enabledItems(panel).findIndex(item => item.classList.contains("nx-menu__item")));
24
+ }
19
25
  /** Focuses the item at `index`, counting from the end when negative and wrapping past either end. */
20
26
  function focusItemAt(panel, index) {
21
27
  const items = enabledItems(panel);
@@ -87,7 +93,8 @@ export function MenuPanel({ id, items, onSelect, kind = "menu", size = "md", clo
87
93
  event.preventDefault();
88
94
  }
89
95
  // Focus follows the pointer, so the arrow keys carry on from the item under it. Only a pointer that moved counts:
90
- // a menu opened from the keyboard under a resting pointer keeps the focus on its first item.
96
+ // a menu opened from the keyboard under a resting pointer keeps the focus on its first item. It never scrolls the
97
+ // list: an item half under the edge would pull the list up under the pointer, and the heading out of sight with it.
91
98
  const pointerRef = useRef({ x: 0, y: 0 });
92
99
  function onPointerMove(event) {
93
100
  const last = pointerRef.current;
@@ -96,12 +103,17 @@ export function MenuPanel({ id, items, onSelect, kind = "menu", size = "md", clo
96
103
  pointerRef.current = { x: event.clientX, y: event.clientY };
97
104
  const item = event.currentTarget;
98
105
  if (item.getAttribute("aria-disabled") !== "true" && document.activeElement !== item)
99
- item.focus();
106
+ item.focus({ preventScroll: true });
107
+ }
108
+ // A control in a heading is chosen like a row, so it closes the menu like one.
109
+ function onClick(event) {
110
+ if (closeOnSelect && event.target.closest('.nx-menu__heading [role="menuitem"]'))
111
+ hide(event.currentTarget);
100
112
  }
101
113
  const role = ITEM_ROLE[kind];
102
114
  // When any item has an icon, every item keeps the icon column, so the labels line up.
103
115
  const hasIcons = items.some(entry => isMenuItem(entry) && entry.icon);
104
- return (_jsx("div", { ref: ref, id: id, popover: "auto", role: "menu", tabIndex: -1, className: cx("nx-popover", "nx-menu", `nx-menu--${size}`, className), onToggle: onToggle, onKeyDown: onKeyDown, ...labelling, children: _jsx("div", { role: "none", className: "nx-menu__list", children: items.map((entry, index) => {
116
+ return (_jsx("div", { ref: ref, id: id, popover: "auto", role: "menu", tabIndex: -1, className: cx("nx-popover", "nx-menu", `nx-menu--${size}`, className), onToggle: onToggle, onKeyDown: onKeyDown, onClick: onClick, ...labelling, children: _jsx("div", { role: "none", className: "nx-menu__list nx-popover__scroll", children: items.map((entry, index) => {
105
117
  // Items are keyed by their value, so a separator keyed by its index alone collided with an item whose value
106
118
  // was that number ("5" in a menu of projects). The prefix keeps the two apart.
107
119
  if (!isMenuItem(entry)) {
@@ -151,12 +163,12 @@ export function Menu({ children, align = "start", "aria-label": ariaLabel, onOpe
151
163
  const panel = panelRef.current;
152
164
  if (!canShow(panel) || isPopoverOpen(panel))
153
165
  return;
154
- panel.showPopover();
166
+ showPanel(panel);
155
167
  place();
156
168
  if (focus === "panel")
157
169
  panel.focus();
158
170
  else
159
- focusItemAt(panel, focus === "first" ? 0 : -1);
171
+ focusItemAt(panel, focus === "first" ? firstRowIndex(panel) : -1);
160
172
  }
161
173
  // The panel is fixed, so it follows the button while the page scrolls or the window changes size.
162
174
  useLayoutEffect(() => {
@@ -223,7 +235,7 @@ export function ContextMenu({ children, "aria-label": ariaLabel = "Context menu"
223
235
  return;
224
236
  event.preventDefault();
225
237
  hide(panel);
226
- panel.showPopover();
238
+ showPanel(panel);
227
239
  const fromPointer = performance.now() - pressedAtRef.current < 1500;
228
240
  if (fromPointer) {
229
241
  placeAtPoint(panel, event.clientX, event.clientY);
@@ -233,7 +245,7 @@ export function ContextMenu({ children, "aria-label": ariaLabel = "Context menu"
233
245
  // From the keyboard (Shift+F10, the Menu key): browsers report the event at the element or at 0,0, so the
234
246
  // menu opens under the focused element like a dropdown (above it when there is no room), on its first item.
235
247
  placeUnderAnchor(panel, event.target.getBoundingClientRect());
236
- focusItemAt(panel, 0);
248
+ focusItemAt(panel, firstRowIndex(panel));
237
249
  }
238
250
  }
239
251
  return (_jsxs("div", { className: "nx-context-target", onPointerDown: () => {
@@ -29,6 +29,8 @@ export interface SplitButtonProps<T extends string = string> {
29
29
  storageKey?: string;
30
30
  /** Picking an action in the menu also runs it. Default false: it only becomes the main button, as on GitHub. */
31
31
  runOnSelect?: boolean;
32
+ /** The main button never changes: the menu holds other ways to do it, each run when picked, with no check mark. */
33
+ fixed?: boolean;
32
34
  /** Default `primary`. */
33
35
  variant?: ButtonVariant;
34
36
  /** Default `md`. */
@@ -61,4 +63,4 @@ export interface SplitButtonProps<T extends string = string> {
61
63
  * { value: "draft", label: "Create draft pull request", description: "Cannot be merged until marked ready" },
62
64
  * ]} />
63
65
  */
64
- export declare function SplitButton<T extends string>({ options, onAction, defaultValue, value, onValueChange, storageKey, runOnSelect, variant, size, loading, disabled, block, type, form, name, menuLabel, align, className, style, }: SplitButtonProps<T>): import("react").JSX.Element | null;
66
+ export declare function SplitButton<T extends string>({ options, onAction, defaultValue, value, onValueChange, storageKey, runOnSelect, fixed, variant, size, loading, disabled, block, type, form, name, menuLabel, align, className, style, }: SplitButtonProps<T>): import("react").JSX.Element | null;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { iconChevronDown, InlineIcon } from "nexus-icons";
4
4
  import { useId, useLayoutEffect, useRef, useState } from "react";
5
5
  import { cx } from "../../Helpers/ClassHelpers.js";
6
- import { followAnchor, isPopoverOpen, placeUnderAnchor } from "../../Helpers/PopoverHelpers.js";
6
+ import { followAnchor, isPopoverOpen, placeUnderAnchor, showPanel } from "../../Helpers/PopoverHelpers.js";
7
7
  import { Button } from "./Button.js";
8
8
  import { MenuPanel } from "./Menu.js";
9
9
  const STORAGE_PREFIX = "nx-split:";
@@ -37,7 +37,7 @@ function saveChoice(key, value) {
37
37
  * { value: "draft", label: "Create draft pull request", description: "Cannot be merged until marked ready" },
38
38
  * ]} />
39
39
  */
40
- export function SplitButton({ options, onAction, defaultValue, value, onValueChange, storageKey, runOnSelect = false, variant = "primary", size = "md", loading = false, disabled = false, block = false, type = "button", form, name, menuLabel = "More actions", align = "end", className, style, }) {
40
+ export function SplitButton({ options, onAction, defaultValue, value, onValueChange, storageKey, runOnSelect = false, fixed = false, variant = "primary", size = "md", loading = false, disabled = false, block = false, type = "button", form, name, menuLabel = "More actions", align = "end", className, style, }) {
41
41
  const id = useId();
42
42
  const panelId = `${id}-menu`;
43
43
  const toggleRef = useRef(null);
@@ -68,7 +68,7 @@ export function SplitButton({ options, onAction, defaultValue, value, onValueCha
68
68
  const panel = panelRef.current;
69
69
  if (!panel || typeof panel.showPopover !== "function" || isPopoverOpen(panel))
70
70
  return;
71
- panel.showPopover();
71
+ showPanel(panel);
72
72
  place();
73
73
  if (focusFirst)
74
74
  panel.querySelector('.nx-menu__item:not([aria-disabled="true"])')?.focus();
@@ -92,6 +92,8 @@ export function SplitButton({ options, onAction, defaultValue, value, onValueCha
92
92
  const option = usable(next);
93
93
  if (!option || !current)
94
94
  return;
95
+ if (fixed)
96
+ return onAction(next, option);
95
97
  if (value === undefined)
96
98
  setChosen(next);
97
99
  saveChoice(storageKey, next);
@@ -100,7 +102,7 @@ export function SplitButton({ options, onAction, defaultValue, value, onValueCha
100
102
  if (runOnSelect)
101
103
  onAction(next, option);
102
104
  }
103
- const items = options.map(option => ({
105
+ const items = (fixed ? options.filter(option => option.value !== current.value) : options).map(option => ({
104
106
  value: option.value,
105
107
  label: option.label,
106
108
  description: option.description,
@@ -125,5 +127,5 @@ export function SplitButton({ options, onAction, defaultValue, value, onValueCha
125
127
  event.preventDefault();
126
128
  show(true);
127
129
  }
128
- } }), _jsx(MenuPanel, { ref: panelRef, id: panelId, kind: "radio", size: size, items: items, onSelect: pick, "aria-label": menuLabel, onToggle: onToggle, className: "nx-split__menu" })] }));
130
+ } }), _jsx(MenuPanel, { ref: panelRef, id: panelId, kind: fixed ? "menu" : "radio", size: size, items: items, onSelect: pick, "aria-label": menuLabel, onToggle: onToggle, className: "nx-split__menu" })] }));
129
131
  }
@@ -13,8 +13,11 @@ export interface TabButtonOption<T extends string = string> {
13
13
  /** Tooltip. With `labels="selected"`, a text label is the default tooltip. */
14
14
  title?: string;
15
15
  }
16
- /** `all`: every option shows its label. `selected`: options show only their icon, and the selected one adds its label. */
17
- export type TabButtonLabels = "all" | "selected";
16
+ /**
17
+ * `all`: every option shows its label. `selected`: options show only their icon, and the selected one adds its label.
18
+ * `auto`: every label while its container (a data table) is at least 900px wide, `selected` below that.
19
+ */
20
+ export type TabButtonLabels = "all" | "selected" | "auto";
18
21
  /** `nav`: a horizontal track with a sliding indicator. `list`: a vertical list of rows, for settings pages and side panels. */
19
22
  export type TabButtonDesign = "nav" | "list";
20
23
  interface TabButtonsBaseProps<T extends string> {
@@ -16,7 +16,7 @@ export function TabButtons(props) {
16
16
  const generatedName = useId();
17
17
  const groupName = name ?? generatedName;
18
18
  const list = design === "list";
19
- const labelsSelected = !list && labels === "selected";
19
+ const labelsSelected = !list && labels !== "all";
20
20
  const chosen = props.multiple ? new Set(props.value) : null;
21
21
  // One choice slides one indicator; several have no single place for it, so each chosen option draws its own.
22
22
  const index = chosen ? -1 : options.findIndex(option => option.value === props.value);
@@ -34,5 +34,5 @@ export function TabButtons(props) {
34
34
  next.delete(option.value);
35
35
  props.onChange(options.filter(one => next.has(one.value)).map(one => one.value));
36
36
  };
37
- return (_jsxs("div", { role: chosen ? "group" : "radiogroup", "aria-orientation": list && !chosen ? "vertical" : undefined, ...labelling, className: cx("nx-tabs", `nx-tabs--${size}`, list && "nx-tabs--list", tone === "primary" && "nx-tabs--primary", labelsSelected && "nx-tabs--labels-selected", chosen && !list && "nx-tabs--multiple", block && "nx-tabs--block", className), style: list || chosen ? undefined : style, children: [index >= 0 && !list && !labelsSelected && _jsx("span", { className: "nx-tabs__indicator", "aria-hidden": "true" }), options.map(option => (_jsxs("label", { className: "nx-tabs__option", title: option.title ?? (labelsSelected && typeof option.label === "string" ? option.label : undefined), children: [_jsx("input", { type: chosen ? "checkbox" : "radio", className: "nx-visually-hidden", name: groupName, form: form, value: option.value, checked: isChecked(option), disabled: option.disabled, onChange: event => toggle(option, event.currentTarget.checked) }), option.icon, _jsxs("span", { className: "nx-tabs__label", children: [_jsx("span", { className: "nx-tabs__text", children: option.label }), list && option.description && _jsx("span", { className: "nx-tabs__description", children: option.description })] }), list && option.badge !== undefined && option.badge !== null && _jsx("span", { className: "nx-tabs__badge", children: option.badge })] }, option.value)))] }));
37
+ return (_jsxs("div", { role: chosen ? "group" : "radiogroup", "aria-orientation": list && !chosen ? "vertical" : undefined, ...labelling, className: cx("nx-tabs", `nx-tabs--${size}`, list && "nx-tabs--list", tone === "primary" && "nx-tabs--primary", labelsSelected && "nx-tabs--labels-selected", !list && labels === "auto" && "nx-tabs--labels-auto", chosen && !list && "nx-tabs--multiple", block && "nx-tabs--block", className), style: list || chosen ? undefined : style, children: [index >= 0 && !list && !labelsSelected && _jsx("span", { className: "nx-tabs__indicator", "aria-hidden": "true" }), options.map(option => (_jsxs("label", { className: "nx-tabs__option", title: option.title ?? (labelsSelected && typeof option.label === "string" ? option.label : undefined), children: [_jsx("input", { type: chosen ? "checkbox" : "radio", className: "nx-visually-hidden", name: groupName, form: form, value: option.value, checked: isChecked(option), disabled: option.disabled, onChange: event => toggle(option, event.currentTarget.checked) }), option.icon, _jsxs("span", { className: "nx-tabs__label", children: [_jsx("span", { className: "nx-tabs__text", children: option.label }), list && option.description && _jsx("span", { className: "nx-tabs__description", children: option.description })] }), list && option.badge !== undefined && option.badge !== null && _jsx("span", { className: "nx-tabs__badge", children: option.badge })] }, option.value)))] }));
38
38
  }
@@ -7,7 +7,7 @@ import type { ApiFormProps, FormValues } from "../../Interfaces/FormInterfaces.t
7
7
  * details, or any JSON by its status. On success a toast says so and the values become the form's own; on failure the
8
8
  * server's own words show - every message it sent, on the inputs they name and the rest under the form, with our message
9
9
  * by status only for an answer that explains nothing - and the first input with a message takes focus. `loadValue` loads
10
- * the record to edit, with placeholders and Retry.
10
+ * the record to edit: the inputs show disabled until it lands, and a failure offers Retry.
11
11
  *
12
12
  * @example
13
13
  * const ROOMS = apiController(NexusModule.Reservation, "Room", "room");
@@ -20,7 +20,7 @@ import { SubmitForm } from "./SubmitForm.js";
20
20
  * details, or any JSON by its status. On success a toast says so and the values become the form's own; on failure the
21
21
  * server's own words show - every message it sent, on the inputs they name and the rest under the form, with our message
22
22
  * by status only for an answer that explains nothing - and the first input with a message takes focus. `loadValue` loads
23
- * the record to edit, with placeholders and Retry.
23
+ * the record to edit: the inputs show disabled until it lands, and a failure offers Retry.
24
24
  *
25
25
  * @example
26
26
  * const ROOMS = apiController(NexusModule.Reservation, "Room", "room");