nexus-shared 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +446 -262
- package/LICENSE +21 -21
- package/README.md +1084 -1000
- package/dist/Client.Index.d.ts +3 -0
- package/dist/Client.Index.js +3 -0
- package/dist/Components/Chats/ChatParts.d.ts +2 -2
- package/dist/Components/Chats/ChatParts.js +3 -7
- package/dist/Components/Chats/ChatThread.js +3 -3
- package/dist/Components/Documents/Button.d.ts +6 -1
- package/dist/Components/Documents/Button.js +2 -2
- package/dist/Components/Documents/Menu.js +21 -9
- package/dist/Components/Documents/SplitButton.d.ts +3 -1
- package/dist/Components/Documents/SplitButton.js +7 -5
- package/dist/Components/Documents/TabButtons.d.ts +5 -2
- package/dist/Components/Documents/TabButtons.js +2 -2
- package/dist/Components/Forms/ApiForm.d.ts +1 -1
- package/dist/Components/Forms/ApiForm.js +1 -1
- package/dist/Components/Forms/Crud.js +177 -58
- package/dist/Components/Forms/CrudParts.d.ts +5 -1
- package/dist/Components/Forms/CrudParts.js +37 -5
- package/dist/Components/Forms/ExcelImport.d.ts +23 -7
- package/dist/Components/Forms/ExcelImport.js +189 -60
- package/dist/Components/Forms/Form.js +5 -13
- package/dist/Components/Forms/SubmitForm.d.ts +11 -2
- package/dist/Components/Forms/SubmitForm.js +23 -12
- package/dist/Components/Inputs/DropdownParts.d.ts +1 -1
- package/dist/Components/Inputs/DropdownParts.js +4 -6
- package/dist/Components/Inputs/NumberBox.js +24 -2
- package/dist/Components/Inputs/RowsInput.js +2 -2
- package/dist/Components/Inputs/Slider.js +4 -1
- package/dist/Components/Inputs/TabularForm.js +42 -10
- package/dist/Components/Inputs/TimePanel.js +4 -3
- package/dist/Components/Layouts/Popup.js +1 -4
- package/dist/Components/Layouts/ThemePicker.d.ts +20 -3
- package/dist/Components/Layouts/ThemePicker.js +77 -6
- package/dist/Components/Layouts/ThemeScript.d.ts +4 -2
- package/dist/Components/Layouts/ThemeScript.js +4 -2
- package/dist/Components/Layouts/ThemeSwitcher.d.ts +6 -3
- package/dist/Components/Layouts/ThemeSwitcher.js +9 -8
- package/dist/Components/Viewers/Avatar.d.ts +21 -0
- package/dist/Components/Viewers/Avatar.js +21 -0
- package/dist/Components/Viewers/DataTable.js +192 -44
- package/dist/Components/Viewers/DataTableColumns.d.ts +7 -3
- package/dist/Components/Viewers/DataTableColumns.js +25 -68
- package/dist/Components/Viewers/DataTableFilters.d.ts +21 -0
- package/dist/Components/Viewers/DataTableFilters.js +68 -0
- package/dist/Components/Viewers/DataTableParts.d.ts +2 -2
- package/dist/Components/Viewers/DataTableParts.js +3 -3
- package/dist/Helpers/AnimationHelpers.d.ts +11 -0
- package/dist/Helpers/AnimationHelpers.js +20 -2
- package/dist/Helpers/CrudBackend.d.ts +7 -4
- package/dist/Helpers/CrudBackend.js +4 -2
- package/dist/Helpers/ExcelBackend.d.ts +3 -3
- package/dist/Helpers/ExcelBackend.js +8 -2
- package/dist/Helpers/ExcelHelpers.d.ts +36 -8
- package/dist/Helpers/ExcelHelpers.js +55 -13
- package/dist/Helpers/FormStore.d.ts +9 -5
- package/dist/Helpers/FormStore.js +25 -5
- package/dist/Helpers/MessageBuilder.js +1 -0
- package/dist/Helpers/NumberHelpers.d.ts +7 -1
- package/dist/Helpers/NumberHelpers.js +41 -3
- package/dist/Helpers/PopoverHelpers.d.ts +17 -5
- package/dist/Helpers/PopoverHelpers.js +52 -17
- package/dist/Helpers/PopoverHooks.d.ts +24 -0
- package/dist/Helpers/PopoverHooks.js +62 -0
- package/dist/Helpers/RowsStore.d.ts +7 -2
- package/dist/Helpers/RowsStore.js +15 -3
- package/dist/Helpers/TableExport.js +12 -12
- package/dist/Helpers/TableFilters.d.ts +25 -0
- package/dist/Helpers/TableFilters.js +162 -0
- package/dist/Helpers/TableHelpers.d.ts +6 -3
- package/dist/Helpers/TableHelpers.js +8 -4
- package/dist/Helpers/TableStore.d.ts +11 -3
- package/dist/Helpers/TableStore.js +16 -5
- package/dist/Helpers/ThemeHelpers.d.ts +21 -4
- package/dist/Helpers/ThemeHelpers.js +46 -9
- package/dist/Helpers/UserPreferences.d.ts +14 -0
- package/dist/Helpers/UserPreferences.js +99 -0
- package/dist/Interfaces/CrudInterfaces.d.ts +20 -1
- package/dist/Interfaces/ExcelInterfaces.d.ts +12 -0
- package/dist/Interfaces/FormInterfaces.d.ts +33 -5
- package/dist/Interfaces/PreferenceInterfaces.d.ts +43 -0
- package/dist/Interfaces/PreferenceInterfaces.js +28 -0
- package/dist/Interfaces/TableInterfaces.d.ts +59 -12
- package/dist/Interfaces/ThemeInterfaces.d.ts +25 -0
- package/dist/Interfaces/ThemeInterfaces.js +15 -0
- package/dist/Services/ExcelApi.d.ts +3 -1
- package/dist/Services/ExcelApi.js +1 -1
- package/dist/Services/ThemeService.d.ts +18 -2
- package/dist/Services/ThemeService.js +80 -16
- package/dist/Shared.Index.d.ts +2 -0
- package/dist/Shared.Index.js +2 -0
- package/package.json +85 -85
- package/src/Styles/Nexus.Avatar.css +79 -0
- package/src/Styles/Nexus.Base.css +117 -70
- package/src/Styles/Nexus.Button.css +180 -153
- package/src/Styles/Nexus.Calendar.css +273 -275
- package/src/Styles/Nexus.Chat.css +1443 -1489
- package/src/Styles/Nexus.Choice.css +343 -345
- package/src/Styles/Nexus.Crud.css +168 -161
- package/src/Styles/Nexus.DateTime.css +81 -81
- package/src/Styles/Nexus.Dropdown.css +725 -774
- package/src/Styles/Nexus.Editor.css +295 -297
- package/src/Styles/Nexus.Excel.css +389 -370
- package/src/Styles/Nexus.Form.css +279 -321
- package/src/Styles/Nexus.Index.css +37 -36
- package/src/Styles/Nexus.Input.css +602 -637
- package/src/Styles/Nexus.Markdown.css +237 -265
- package/src/Styles/Nexus.Menu.css +237 -259
- package/src/Styles/Nexus.Popup.css +724 -731
- package/src/Styles/Nexus.RichText.css +2 -32
- package/src/Styles/Nexus.Rows.css +637 -596
- package/src/Styles/Nexus.Slider.css +276 -278
- package/src/Styles/Nexus.Tab.Buttons.css +432 -419
- package/src/Styles/Nexus.Table.css +1386 -1296
- package/src/Styles/Nexus.Theme.Picker.css +286 -232
- package/src/Styles/Nexus.Themes.css +194 -194
- package/src/Styles/Nexus.Time.css +144 -147
- package/src/Styles/Nexus.Toast.css +263 -265
- package/src/Styles/Nexus.Tokens.css +89 -72
- package/src/Styles/Nexus.Tree.css +431 -435
package/dist/Client.Index.d.ts
CHANGED
|
@@ -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";
|
package/dist/Client.Index.js
CHANGED
|
@@ -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 {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
14
|
+
/** The shared `Avatar`, under the name the chat has always exported it by. */
|
|
16
15
|
export function ChatAvatar({ name, src, size = "md" }) {
|
|
17
|
-
|
|
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
|
-
//
|
|
41
|
-
|
|
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
|
-
|
|
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
|
|
166
|
+
showPanel(panel);
|
|
155
167
|
place();
|
|
156
168
|
if (focus === "panel")
|
|
157
169
|
panel.focus();
|
|
158
170
|
else
|
|
159
|
-
focusItemAt(panel, focus === "first" ?
|
|
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
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
17
|
-
|
|
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
|
|
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
|
|
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
|
|
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");
|