@qeetrix/react 0.0.2 → 0.0.4
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 +116 -1
- package/README.md +99 -0
- package/dist/button/button.d.ts +6 -2
- package/dist/button/button.js +5 -1
- package/dist/css/avatar.css +6 -0
- package/dist/css/badge.css +6 -0
- package/dist/css/button.css +81 -6
- package/dist/css/checkbox.css +9 -0
- package/dist/css/dialog.css +7 -0
- package/dist/css/drawer.css +6 -0
- package/dist/css/experimental/alert.css +89 -0
- package/dist/css/experimental/app-shell.css +258 -0
- package/dist/css/experimental/breadcrumb.css +67 -0
- package/dist/css/experimental/copy-button.css +13 -0
- package/dist/css/experimental/empty-state.css +63 -0
- package/dist/css/experimental/otp-input.css +84 -0
- package/dist/css/experimental/page-header.css +110 -0
- package/dist/css/experimental/password-input.css +21 -0
- package/dist/css/experimental/sidebar.css +234 -0
- package/dist/css/experimental/stat.css +115 -0
- package/dist/css/experimental/top-bar.css +299 -0
- package/dist/css/input.css +10 -0
- package/dist/css/menu.css +17 -0
- package/dist/css/progress.css +14 -0
- package/dist/css/scroll-area.css +7 -0
- package/dist/css/select.css +16 -0
- package/dist/css/separator.css +7 -0
- package/dist/css/skeleton.css +6 -0
- package/dist/css/slider.css +22 -0
- package/dist/css/switch.css +33 -0
- package/dist/css/tabs.css +11 -0
- package/dist/css/toast.css +7 -0
- package/dist/css/toggle.css +13 -0
- package/dist/css/tooltip.css +6 -0
- package/dist/dialog/dialog.d.ts +4 -1
- package/dist/dialog/dialog.js +4 -3
- package/dist/drawer/drawer.d.ts +4 -1
- package/dist/drawer/drawer.js +5 -4
- package/dist/experimental/alert/alert.d.ts +29 -0
- package/dist/experimental/alert/alert.js +72 -0
- package/dist/experimental/alert/index.d.ts +2 -0
- package/dist/experimental/app-shell/app-shell.d.ts +60 -0
- package/dist/experimental/app-shell/app-shell.js +224 -0
- package/dist/experimental/app-shell/index.d.ts +2 -0
- package/dist/experimental/breadcrumb/breadcrumb.d.ts +27 -0
- package/dist/experimental/breadcrumb/breadcrumb.js +48 -0
- package/dist/experimental/breadcrumb/index.d.ts +2 -0
- package/dist/experimental/copy-button/copy-button.d.ts +27 -0
- package/dist/experimental/copy-button/copy-button.js +61 -0
- package/dist/experimental/copy-button/index.d.ts +2 -0
- package/dist/experimental/empty-state/data-state.d.ts +23 -0
- package/dist/experimental/empty-state/data-state.js +40 -0
- package/dist/experimental/empty-state/empty-state.d.ts +26 -0
- package/dist/experimental/empty-state/empty-state.js +46 -0
- package/dist/experimental/empty-state/index.d.ts +3 -0
- package/dist/experimental/index.d.ts +24 -0
- package/dist/experimental/index.js +13 -0
- package/dist/experimental/otp-input/index.d.ts +2 -0
- package/dist/experimental/otp-input/otp-input.d.ts +27 -0
- package/dist/experimental/otp-input/otp-input.js +39 -0
- package/dist/experimental/page-header/index.d.ts +2 -0
- package/dist/experimental/page-header/page-header.d.ts +32 -0
- package/dist/experimental/page-header/page-header.js +73 -0
- package/dist/experimental/password-input/index.d.ts +2 -0
- package/dist/experimental/password-input/password-input.d.ts +26 -0
- package/dist/experimental/password-input/password-input.js +103 -0
- package/dist/experimental/sidebar/index.d.ts +2 -0
- package/dist/experimental/sidebar/sidebar.d.ts +64 -0
- package/dist/experimental/sidebar/sidebar.js +200 -0
- package/dist/experimental/stat/index.d.ts +2 -0
- package/dist/experimental/stat/stat.d.ts +31 -0
- package/dist/experimental/stat/stat.js +90 -0
- package/dist/experimental/top-bar/index.d.ts +2 -0
- package/dist/experimental/top-bar/top-bar.d.ts +60 -0
- package/dist/experimental/top-bar/top-bar.js +162 -0
- package/dist/field/field.d.ts +4 -1
- package/dist/field/field.js +6 -5
- package/dist/format/format.d.ts +12 -0
- package/dist/format/format.js +50 -0
- package/dist/format/index.d.ts +2 -0
- package/dist/format/index.js +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +3 -1
- package/dist/internal/media.js +15 -0
- package/dist/internal/messages.d.ts +75 -0
- package/dist/internal/messages.js +44 -5
- package/dist/link/link.d.ts +7 -1
- package/dist/link/link.js +3 -2
- package/dist/locales/hi.d.ts +8 -0
- package/dist/locales/hi.js +60 -0
- package/dist/progress/progress.d.ts +2 -2
- package/dist/progress/progress.js +7 -2
- package/dist/provider/index.d.ts +3 -0
- package/dist/provider/index.js +1 -0
- package/dist/provider/provider.d.ts +46 -0
- package/dist/provider/provider.js +90 -0
- package/dist/slider/slider.d.ts +5 -2
- package/dist/slider/slider.js +10 -3
- package/dist/styles.css +1704 -104
- package/dist/text-field/text-field.d.ts +2 -2
- package/dist/text-field/text-field.js +4 -2
- package/dist/toast/toast.d.ts +4 -1
- package/dist/toast/toast.js +7 -6
- package/package.json +18 -2
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { IconButton } from "../../button/button.js";
|
|
3
|
+
import { useMessages } from "../../provider/provider.js";
|
|
4
|
+
import { mergeRefs } from "../../internal/refs.js";
|
|
5
|
+
import { Input } from "../../input/input.js";
|
|
6
|
+
import { VisuallyHidden } from "../../visually-hidden/visually-hidden.js";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
|
+
import { useEffect, useEffectEvent, useRef, useState } from "react";
|
|
9
|
+
import { ArrowBigUpIcon } from "@qeetrix/icons/icons/arrow-big-up";
|
|
10
|
+
import { EyeIcon } from "@qeetrix/icons/icons/eye";
|
|
11
|
+
import { EyeOffIcon } from "@qeetrix/icons/icons/eye-off";
|
|
12
|
+
// src/experimental/password-input/password-input.tsx
|
|
13
|
+
/**
|
|
14
|
+
* A password or secret field (07-components.md, C2): an Input with a Show password toggle. It
|
|
15
|
+
* hides again when its form is submitted, so the browser never stores it as plain text; spelling
|
|
16
|
+
* and capitalisation tools stay off; paste always works, and there's no length cap.
|
|
17
|
+
*/
|
|
18
|
+
function PasswordInput({ autoComplete = "current-password", visible, defaultVisible = false, onVisibleChange, capsLockWarning = true, messages, endAdornment, disabled, ref, onKeyDown, onKeyUp, onBlur, className, ...props }) {
|
|
19
|
+
const strings = useMessages("passwordInput", messages);
|
|
20
|
+
const input = useRef(null);
|
|
21
|
+
const [own, setOwn] = useState(defaultVisible);
|
|
22
|
+
const shown = visible ?? own;
|
|
23
|
+
const [announcement, setAnnouncement] = useState("");
|
|
24
|
+
const [capsLock, setCapsLock] = useState(false);
|
|
25
|
+
const setShown = (next) => {
|
|
26
|
+
if (visible === void 0) setOwn(next);
|
|
27
|
+
onVisibleChange?.(next);
|
|
28
|
+
};
|
|
29
|
+
const hideOnSubmit = useEffectEvent(() => {
|
|
30
|
+
if (input.current) input.current.type = "password";
|
|
31
|
+
if (shown) setShown(false);
|
|
32
|
+
});
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
const form = input.current?.form;
|
|
35
|
+
if (!form) return;
|
|
36
|
+
const onSubmit = () => hideOnSubmit();
|
|
37
|
+
form.addEventListener("submit", onSubmit);
|
|
38
|
+
return () => form.removeEventListener("submit", onSubmit);
|
|
39
|
+
}, []);
|
|
40
|
+
const toggle = () => {
|
|
41
|
+
const field = input.current;
|
|
42
|
+
const selection = field ? [field.selectionStart, field.selectionEnd] : [null, null];
|
|
43
|
+
setShown(!shown);
|
|
44
|
+
setAnnouncement(shown ? strings.hidden : strings.shown);
|
|
45
|
+
requestAnimationFrame(() => {
|
|
46
|
+
const [start, end] = selection;
|
|
47
|
+
if (field && start != null && end != null) field.setSelectionRange(start, end);
|
|
48
|
+
});
|
|
49
|
+
};
|
|
50
|
+
const readCapsLock = (event) => {
|
|
51
|
+
if (capsLockWarning) setCapsLock(event.getModifierState("CapsLock"));
|
|
52
|
+
};
|
|
53
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
54
|
+
...props,
|
|
55
|
+
ref: mergeRefs(ref, input),
|
|
56
|
+
type: shown ? "text" : "password",
|
|
57
|
+
autoComplete,
|
|
58
|
+
spellCheck: false,
|
|
59
|
+
autoCapitalize: "none",
|
|
60
|
+
autoCorrect: "off",
|
|
61
|
+
disabled,
|
|
62
|
+
className: className ? `qx-password-input ${className}` : "qx-password-input",
|
|
63
|
+
onKeyDown: (event) => {
|
|
64
|
+
readCapsLock(event);
|
|
65
|
+
onKeyDown?.(event);
|
|
66
|
+
},
|
|
67
|
+
onKeyUp: (event) => {
|
|
68
|
+
readCapsLock(event);
|
|
69
|
+
onKeyUp?.(event);
|
|
70
|
+
},
|
|
71
|
+
onBlur: (event) => {
|
|
72
|
+
setCapsLock(false);
|
|
73
|
+
onBlur?.(event);
|
|
74
|
+
},
|
|
75
|
+
endAdornment: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
76
|
+
capsLock && /* @__PURE__ */ jsxs("span", {
|
|
77
|
+
className: "qx-password-input-caps",
|
|
78
|
+
"data-slot": "password-input-caps",
|
|
79
|
+
children: [/* @__PURE__ */ jsx(ArrowBigUpIcon, {
|
|
80
|
+
shape: "sharp",
|
|
81
|
+
size: 14,
|
|
82
|
+
"aria-hidden": true
|
|
83
|
+
}), strings.capsLock]
|
|
84
|
+
}),
|
|
85
|
+
endAdornment,
|
|
86
|
+
/* @__PURE__ */ jsx(IconButton, {
|
|
87
|
+
icon: shown ? EyeOffIcon : EyeIcon,
|
|
88
|
+
"aria-label": strings.show,
|
|
89
|
+
"aria-pressed": shown,
|
|
90
|
+
size: "sm",
|
|
91
|
+
tone: "neutral",
|
|
92
|
+
disabled,
|
|
93
|
+
onClick: toggle,
|
|
94
|
+
className: "qx-password-input-toggle"
|
|
95
|
+
}),
|
|
96
|
+
/* @__PURE__ */ jsx(VisuallyHidden, {
|
|
97
|
+
"aria-live": "polite",
|
|
98
|
+
children: announcement
|
|
99
|
+
})
|
|
100
|
+
] })
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
export { PasswordInput };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RenderProp } from "../../internal/render.js";
|
|
2
|
+
import { QeetrixIcon } from "../../internal/control.js";
|
|
3
|
+
import { QeetrixMessages } from "../../internal/messages.js";
|
|
4
|
+
import { CSSProperties, ComponentProps, MouseEvent, ReactElement, ReactNode } from "react";
|
|
5
|
+
export interface SidebarProps extends Omit<ComponentProps<"div">, "children" | "title"> {
|
|
6
|
+
/** Names the navigation landmark, e.g. the product: "Qeet Pay". */
|
|
7
|
+
label: string;
|
|
8
|
+
/** The area's heading at the top, e.g. "Identity". */
|
|
9
|
+
title?: ReactNode;
|
|
10
|
+
/** Add "Filter navigation", which narrows the items by label as you type. */
|
|
11
|
+
filter?: boolean;
|
|
12
|
+
/** Below the items, e.g. a link to what's new. */
|
|
13
|
+
footer?: ReactNode;
|
|
14
|
+
/** SidebarGroups. */
|
|
15
|
+
children?: ReactNode;
|
|
16
|
+
/** The filter's strings, over QeetrixProvider's and the English defaults. */
|
|
17
|
+
messages?: Partial<QeetrixMessages["sidebar"]>;
|
|
18
|
+
}
|
|
19
|
+
export interface SidebarGroupProps extends Omit<ComponentProps<"div">, "children"> {
|
|
20
|
+
/** The group's label; without one, the group is separated by space alone. */
|
|
21
|
+
label?: ReactNode;
|
|
22
|
+
/** SidebarItems. */
|
|
23
|
+
children?: ReactNode;
|
|
24
|
+
}
|
|
25
|
+
export interface SidebarItemProps {
|
|
26
|
+
/** The item's text. In the rail it becomes the tooltip and stays the accessible name. */
|
|
27
|
+
label: string;
|
|
28
|
+
/** Where it goes. A category (an item with children) takes no `href`. */
|
|
29
|
+
href?: string;
|
|
30
|
+
/** A router link instead of `<a>`, e.g. `render={<Link to="/users" />}`. */
|
|
31
|
+
render?: RenderProp;
|
|
32
|
+
/**
|
|
33
|
+
* A 16px icon from `@qeetrix/icons`, in its round shape: navigation is the one place Qeetrix uses
|
|
34
|
+
* round icons. Icons represent destinations, not actions.
|
|
35
|
+
*/
|
|
36
|
+
icon?: QeetrixIcon;
|
|
37
|
+
/** Show the icon's filled drawing while current. Only for icons that have one. */
|
|
38
|
+
filledWhenCurrent?: boolean;
|
|
39
|
+
/** This item is the current page: `aria-current="page"` and the Ember bar. */
|
|
40
|
+
current?: boolean;
|
|
41
|
+
/** At the end: a count or a Badge. Hidden in the rail. */
|
|
42
|
+
badge?: ReactNode;
|
|
43
|
+
/** A category's initial state. By default it starts open when it holds the current page. */
|
|
44
|
+
defaultOpen?: boolean;
|
|
45
|
+
/** Called when the link is followed. */
|
|
46
|
+
onClick?: (event: MouseEvent<HTMLElement>) => void;
|
|
47
|
+
/** One level of nested SidebarItems: the item becomes a category. */
|
|
48
|
+
children?: ReactNode;
|
|
49
|
+
className?: string;
|
|
50
|
+
style?: CSSProperties;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A console's navigation (07-components.md, C1): links only, one level of nesting, every item in
|
|
54
|
+
* the Tab order. Inside an AppShell it follows the shell: a rail when collapsed, a drawer on narrow
|
|
55
|
+
* screens.
|
|
56
|
+
*/
|
|
57
|
+
export declare function Sidebar({ label, title, filter, footer, messages, className, children, ...props }: SidebarProps): ReactElement;
|
|
58
|
+
/** Items under one label. */
|
|
59
|
+
export declare function SidebarGroup({ label, className, children, ...props }: SidebarGroupProps): ReactElement;
|
|
60
|
+
/**
|
|
61
|
+
* A destination, or a category of destinations. A closed category carries the current mark for its
|
|
62
|
+
* current child, so the current page is always marked.
|
|
63
|
+
*/
|
|
64
|
+
export declare function SidebarItem({ label, href, render, icon: Icon, filledWhenCurrent, current, badge, defaultOpen, onClick, className, style, children }: SidebarItemProps): ReactElement | null;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cx } from "../../internal/cx.js";
|
|
3
|
+
import { renderElement } from "../../internal/render.js";
|
|
4
|
+
import { Tooltip } from "../../tooltip/tooltip.js";
|
|
5
|
+
import { IconButton } from "../../button/button.js";
|
|
6
|
+
import { useMessages } from "../../provider/provider.js";
|
|
7
|
+
import { Input } from "../../input/input.js";
|
|
8
|
+
import { useShellState } from "../app-shell/app-shell.js";
|
|
9
|
+
import { ChevronDownIcon } from "@qeetrix/icons/icons/chevron-down";
|
|
10
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
import { Children, createContext, isValidElement, use, useId, useRef, useState } from "react";
|
|
12
|
+
import { SearchIcon } from "@qeetrix/icons/icons/search";
|
|
13
|
+
// src/experimental/sidebar/sidebar.tsx
|
|
14
|
+
/** The filter's text, lower-cased; and whether an enclosing category matched it by its own label. */
|
|
15
|
+
const FilterContext = createContext("");
|
|
16
|
+
const ParentMatchContext = createContext(false);
|
|
17
|
+
/**
|
|
18
|
+
* A console's navigation (07-components.md, C1): links only, one level of nesting, every item in
|
|
19
|
+
* the Tab order. Inside an AppShell it follows the shell: a rail when collapsed, a drawer on narrow
|
|
20
|
+
* screens.
|
|
21
|
+
*/
|
|
22
|
+
function Sidebar({ label, title, filter, footer, messages, className, children, ...props }) {
|
|
23
|
+
const shell = useShellState();
|
|
24
|
+
const rail = shell.collapsed && !shell.inDrawer;
|
|
25
|
+
const strings = useMessages("sidebar", messages);
|
|
26
|
+
const [query, setQuery] = useState("");
|
|
27
|
+
const filterRef = useRef(null);
|
|
28
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
29
|
+
...props,
|
|
30
|
+
className: cx("qx-sidebar", className),
|
|
31
|
+
"data-slot": "sidebar",
|
|
32
|
+
"data-rail": rail ? "" : void 0,
|
|
33
|
+
children: [
|
|
34
|
+
(title != null || filter) && /* @__PURE__ */ jsxs("div", {
|
|
35
|
+
className: "qx-sidebar-header",
|
|
36
|
+
"data-slot": "sidebar-header",
|
|
37
|
+
children: [
|
|
38
|
+
filter && rail && /* @__PURE__ */ jsx(IconButton, {
|
|
39
|
+
icon: SearchIcon,
|
|
40
|
+
"aria-label": strings.filter,
|
|
41
|
+
tone: "neutral",
|
|
42
|
+
className: "qx-sidebar-rail-search",
|
|
43
|
+
onClick: () => {
|
|
44
|
+
shell.expand();
|
|
45
|
+
requestAnimationFrame(() => filterRef.current?.focus());
|
|
46
|
+
}
|
|
47
|
+
}),
|
|
48
|
+
title != null && /* @__PURE__ */ jsx("div", {
|
|
49
|
+
className: "qx-sidebar-title",
|
|
50
|
+
children: title
|
|
51
|
+
}),
|
|
52
|
+
filter && /* @__PURE__ */ jsx(Input, {
|
|
53
|
+
ref: filterRef,
|
|
54
|
+
type: "search",
|
|
55
|
+
icon: SearchIcon,
|
|
56
|
+
value: query,
|
|
57
|
+
onValueChange: setQuery,
|
|
58
|
+
"aria-label": strings.filter,
|
|
59
|
+
placeholder: strings.filter,
|
|
60
|
+
className: "qx-sidebar-filter"
|
|
61
|
+
})
|
|
62
|
+
]
|
|
63
|
+
}),
|
|
64
|
+
/* @__PURE__ */ jsxs("nav", {
|
|
65
|
+
"aria-label": label,
|
|
66
|
+
className: "qx-sidebar-nav",
|
|
67
|
+
"data-slot": "sidebar-nav",
|
|
68
|
+
children: [/* @__PURE__ */ jsx(FilterContext, {
|
|
69
|
+
value: query.trim().toLocaleLowerCase(),
|
|
70
|
+
children
|
|
71
|
+
}), /* @__PURE__ */ jsx("p", {
|
|
72
|
+
className: "qx-sidebar-empty",
|
|
73
|
+
role: "status",
|
|
74
|
+
children: query.trim() ? strings.noMatches : ""
|
|
75
|
+
})]
|
|
76
|
+
}),
|
|
77
|
+
footer != null && /* @__PURE__ */ jsx("div", {
|
|
78
|
+
className: "qx-sidebar-footer",
|
|
79
|
+
"data-slot": "sidebar-footer",
|
|
80
|
+
children: footer
|
|
81
|
+
})
|
|
82
|
+
]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
/** Items under one label. */
|
|
86
|
+
function SidebarGroup({ label, className, children, ...props }) {
|
|
87
|
+
const id = useId();
|
|
88
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
89
|
+
...props,
|
|
90
|
+
className: cx("qx-sidebar-group", className),
|
|
91
|
+
"data-slot": "sidebar-group",
|
|
92
|
+
children: [label != null && /* @__PURE__ */ jsx("div", {
|
|
93
|
+
id,
|
|
94
|
+
className: "qx-sidebar-group-label",
|
|
95
|
+
"data-slot": "sidebar-group-label",
|
|
96
|
+
children: label
|
|
97
|
+
}), /* @__PURE__ */ jsx("ul", {
|
|
98
|
+
className: "qx-sidebar-list",
|
|
99
|
+
"aria-labelledby": label != null ? id : void 0,
|
|
100
|
+
children
|
|
101
|
+
})]
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* A destination, or a category of destinations. A closed category carries the current mark for its
|
|
106
|
+
* current child, so the current page is always marked.
|
|
107
|
+
*/
|
|
108
|
+
function SidebarItem({ label, href, render, icon: Icon, filledWhenCurrent, current, badge, defaultOpen, onClick, className, style, children }) {
|
|
109
|
+
const shell = useShellState();
|
|
110
|
+
const query = use(FilterContext);
|
|
111
|
+
const parentMatched = use(ParentMatchContext);
|
|
112
|
+
const rail = shell.collapsed && !shell.inDrawer;
|
|
113
|
+
const panelId = useId();
|
|
114
|
+
const nested = Children.toArray(children).filter(isValidElement);
|
|
115
|
+
const isCategory = nested.length > 0;
|
|
116
|
+
const holdsCurrent = nested.some((child) => child.props.current);
|
|
117
|
+
const [open, setOpen] = useState(defaultOpen ?? holdsCurrent);
|
|
118
|
+
const matches = (text) => text.toLocaleLowerCase().includes(query);
|
|
119
|
+
const selfMatch = query === "" || parentMatched || matches(label);
|
|
120
|
+
const childMatch = query !== "" && nested.some((child) => matches(String(child.props.label)));
|
|
121
|
+
if (!selfMatch && !childMatch) return null;
|
|
122
|
+
const expanded = (query !== "" ? true : open) && !rail;
|
|
123
|
+
const marked = current || isCategory && holdsCurrent && !expanded;
|
|
124
|
+
const Filled = Icon;
|
|
125
|
+
const content = /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
126
|
+
Icon && /* @__PURE__ */ jsx(Icon, {
|
|
127
|
+
shape: "round",
|
|
128
|
+
size: 16,
|
|
129
|
+
"aria-hidden": true,
|
|
130
|
+
className: "qx-sidebar-icon qx-sidebar-icon-outline"
|
|
131
|
+
}),
|
|
132
|
+
Filled && filledWhenCurrent && /* @__PURE__ */ jsx(Filled, {
|
|
133
|
+
shape: "round",
|
|
134
|
+
variant: "filled",
|
|
135
|
+
size: 16,
|
|
136
|
+
"aria-hidden": true,
|
|
137
|
+
className: "qx-sidebar-icon qx-sidebar-icon-filled"
|
|
138
|
+
}),
|
|
139
|
+
/* @__PURE__ */ jsx("span", {
|
|
140
|
+
className: "qx-sidebar-label",
|
|
141
|
+
children: label
|
|
142
|
+
}),
|
|
143
|
+
badge != null && /* @__PURE__ */ jsx("span", {
|
|
144
|
+
className: "qx-sidebar-badge",
|
|
145
|
+
children: badge
|
|
146
|
+
}),
|
|
147
|
+
isCategory && /* @__PURE__ */ jsx(ChevronDownIcon, {
|
|
148
|
+
shape: "round",
|
|
149
|
+
size: 16,
|
|
150
|
+
"aria-hidden": true,
|
|
151
|
+
className: "qx-sidebar-chevron"
|
|
152
|
+
})
|
|
153
|
+
] });
|
|
154
|
+
let control = isCategory ? /* @__PURE__ */ jsx("button", {
|
|
155
|
+
type: "button",
|
|
156
|
+
className: "qx-sidebar-item",
|
|
157
|
+
"aria-expanded": expanded,
|
|
158
|
+
"aria-controls": panelId,
|
|
159
|
+
"data-current": marked ? "" : void 0,
|
|
160
|
+
"data-open": expanded ? "" : void 0,
|
|
161
|
+
onClick: () => {
|
|
162
|
+
if (rail) {
|
|
163
|
+
shell.expand();
|
|
164
|
+
setOpen(true);
|
|
165
|
+
} else setOpen(!open);
|
|
166
|
+
},
|
|
167
|
+
children: content
|
|
168
|
+
}) : renderElement("a", render, {
|
|
169
|
+
href,
|
|
170
|
+
className: "qx-sidebar-item",
|
|
171
|
+
"aria-current": current ? "page" : void 0,
|
|
172
|
+
"data-current": current ? "" : void 0,
|
|
173
|
+
onClick: (event) => {
|
|
174
|
+
onClick?.(event);
|
|
175
|
+
if (shell.inDrawer) shell.closeDrawer();
|
|
176
|
+
},
|
|
177
|
+
children: content
|
|
178
|
+
});
|
|
179
|
+
control = /* @__PURE__ */ jsx(Tooltip, {
|
|
180
|
+
content: label,
|
|
181
|
+
side: "inline-end",
|
|
182
|
+
disabled: !rail,
|
|
183
|
+
children: control
|
|
184
|
+
});
|
|
185
|
+
return /* @__PURE__ */ jsxs("li", {
|
|
186
|
+
className: cx("qx-sidebar-entry", className),
|
|
187
|
+
style,
|
|
188
|
+
"data-slot": "sidebar-item",
|
|
189
|
+
children: [control, isCategory && /* @__PURE__ */ jsx("ul", {
|
|
190
|
+
id: panelId,
|
|
191
|
+
className: "qx-sidebar-list qx-sidebar-sublist",
|
|
192
|
+
hidden: !expanded,
|
|
193
|
+
children: /* @__PURE__ */ jsx(ParentMatchContext, {
|
|
194
|
+
value: query !== "" && matches(label),
|
|
195
|
+
children: nested
|
|
196
|
+
})
|
|
197
|
+
})]
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
export { Sidebar, SidebarGroup, SidebarItem };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ComponentProps, ReactElement, ReactNode } from "react";
|
|
2
|
+
/** What the note says about the number: `positive`, `negative` or `warning` colour it. */
|
|
3
|
+
export type StatTone = "neutral" | "positive" | "negative" | "warning";
|
|
4
|
+
export interface StatGroupProps extends ComponentProps<"dl"> {
|
|
5
|
+
/** Stats, side by side in equal ruled cells. */
|
|
6
|
+
children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
export interface StatProps extends Omit<ComponentProps<"div">, "children"> {
|
|
9
|
+
/** What's counted: "Total users". */
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
/** The number. A `number` is formatted for `locale`; pass a string for units or percentages. */
|
|
12
|
+
value: ReactNode;
|
|
13
|
+
/** One line under the value: the change or the context, e.g. "+38 this week". */
|
|
14
|
+
note?: ReactNode;
|
|
15
|
+
/** Colours the note. Default `neutral`. */
|
|
16
|
+
tone?: StatTone;
|
|
17
|
+
/**
|
|
18
|
+
* Recent values, oldest first, drawn as a small line beside the number. It's decorative: say what
|
|
19
|
+
* matters about the trend in `note`.
|
|
20
|
+
*/
|
|
21
|
+
trend?: readonly number[];
|
|
22
|
+
/** The locale for a numeric `value`. Default `en-IN`. */
|
|
23
|
+
locale?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Key numbers in a ruled strip (07-components.md, C1): a `<dl>` of Stats in equal cells with rules
|
|
27
|
+
* between them. In a PageHeader's `summary` it runs edge to edge. Server-safe.
|
|
28
|
+
*/
|
|
29
|
+
export declare function StatGroup({ className, ...props }: StatGroupProps): ReactElement;
|
|
30
|
+
/** One number with its label, a note and an optional trend. Use inside a StatGroup. Server-safe. */
|
|
31
|
+
export declare function Stat({ label, value, note, tone, trend, locale, className, ...props }: StatProps): ReactElement;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { cx } from "../../internal/cx.js";
|
|
2
|
+
import { formatNumber } from "../../format/format.js";
|
|
3
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
// src/experimental/stat/stat.tsx
|
|
5
|
+
const TREND = {
|
|
6
|
+
width: 88,
|
|
7
|
+
height: 28,
|
|
8
|
+
pad: 3
|
|
9
|
+
};
|
|
10
|
+
/** The trend line and its area, scaled into the box; the last point is marked. */
|
|
11
|
+
function trendGeometry(values) {
|
|
12
|
+
const { width, height, pad } = TREND;
|
|
13
|
+
const min = Math.min(...values);
|
|
14
|
+
const span = Math.max(...values) - min;
|
|
15
|
+
const points = values.map((value, index) => ({
|
|
16
|
+
x: index / (values.length - 1) * width,
|
|
17
|
+
y: span === 0 ? height / 2 : height - pad - (value - min) / span * (height - 2 * pad)
|
|
18
|
+
}));
|
|
19
|
+
const line = points.map(({ x, y }, i) => `${i === 0 ? "M" : "L"}${x.toFixed(1)} ${y.toFixed(1)}`);
|
|
20
|
+
return {
|
|
21
|
+
line: line.join(" "),
|
|
22
|
+
area: `${line.join(" ")} L${width} ${height} L0 ${height} Z`,
|
|
23
|
+
end: points[points.length - 1]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Key numbers in a ruled strip (07-components.md, C1): a `<dl>` of Stats in equal cells with rules
|
|
28
|
+
* between them. In a PageHeader's `summary` it runs edge to edge. Server-safe.
|
|
29
|
+
*/
|
|
30
|
+
function StatGroup({ className, ...props }) {
|
|
31
|
+
return /* @__PURE__ */ jsx("dl", {
|
|
32
|
+
...props,
|
|
33
|
+
className: cx("qx-stat-group", className),
|
|
34
|
+
"data-slot": "stat-group"
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
/** One number with its label, a note and an optional trend. Use inside a StatGroup. Server-safe. */
|
|
38
|
+
function Stat({ label, value, note, tone = "neutral", trend, locale, className, ...props }) {
|
|
39
|
+
const geometry = trend && trend.length > 1 ? trendGeometry(trend) : void 0;
|
|
40
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
41
|
+
...props,
|
|
42
|
+
className: cx("qx-stat", className),
|
|
43
|
+
"data-slot": "stat",
|
|
44
|
+
children: [
|
|
45
|
+
/* @__PURE__ */ jsx("dt", {
|
|
46
|
+
className: "qx-stat-label",
|
|
47
|
+
"data-slot": "stat-label",
|
|
48
|
+
children: label
|
|
49
|
+
}),
|
|
50
|
+
/* @__PURE__ */ jsx("dd", {
|
|
51
|
+
className: "qx-stat-value",
|
|
52
|
+
"data-slot": "stat-value",
|
|
53
|
+
children: typeof value === "number" ? formatNumber(value, locale) : value
|
|
54
|
+
}),
|
|
55
|
+
geometry && /* @__PURE__ */ jsx("dd", {
|
|
56
|
+
className: "qx-stat-trend-cell",
|
|
57
|
+
children: /* @__PURE__ */ jsxs("svg", {
|
|
58
|
+
className: "qx-stat-trend",
|
|
59
|
+
"data-slot": "stat-trend",
|
|
60
|
+
viewBox: `0 0 ${TREND.width} ${TREND.height}`,
|
|
61
|
+
"aria-hidden": "true",
|
|
62
|
+
focusable: "false",
|
|
63
|
+
children: [
|
|
64
|
+
/* @__PURE__ */ jsx("path", {
|
|
65
|
+
className: "qx-stat-trend-area",
|
|
66
|
+
d: geometry.area
|
|
67
|
+
}),
|
|
68
|
+
/* @__PURE__ */ jsx("path", {
|
|
69
|
+
className: "qx-stat-trend-line",
|
|
70
|
+
d: geometry.line
|
|
71
|
+
}),
|
|
72
|
+
/* @__PURE__ */ jsx("circle", {
|
|
73
|
+
className: "qx-stat-trend-end",
|
|
74
|
+
cx: geometry.end.x,
|
|
75
|
+
cy: geometry.end.y,
|
|
76
|
+
r: 2.5
|
|
77
|
+
})
|
|
78
|
+
]
|
|
79
|
+
})
|
|
80
|
+
}),
|
|
81
|
+
note != null && /* @__PURE__ */ jsx("dd", {
|
|
82
|
+
className: "qx-stat-note",
|
|
83
|
+
"data-slot": "stat-note",
|
|
84
|
+
"data-tone": tone,
|
|
85
|
+
children: note
|
|
86
|
+
})
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
export { Stat, StatGroup };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RenderProp } from "../../internal/render.js";
|
|
2
|
+
import { ComponentProps, ReactElement, ReactNode } from "react";
|
|
3
|
+
import { Button } from "@base-ui/react/button";
|
|
4
|
+
export interface TopBarProps extends Omit<ComponentProps<"header">, "children"> {
|
|
5
|
+
/** The suite launcher: a Menu of Qeet products behind a neutral IconButton. */
|
|
6
|
+
launcher?: ReactNode;
|
|
7
|
+
/** The product's mark, e.g. `<QeetLogo variant="dark" height={20} />`. */
|
|
8
|
+
logo?: ReactNode;
|
|
9
|
+
/** The product's name, beside the mark: "Qeet ID". */
|
|
10
|
+
product: string;
|
|
11
|
+
/** The identity's link home. */
|
|
12
|
+
href?: string;
|
|
13
|
+
/** A router link for the identity instead of `<a>`. */
|
|
14
|
+
render?: RenderProp;
|
|
15
|
+
/**
|
|
16
|
+
* The scope, outermost first: organisation, then project or environment. Usually
|
|
17
|
+
* TopBarSwitchers; a slash is drawn before each.
|
|
18
|
+
*/
|
|
19
|
+
context?: ReactNode;
|
|
20
|
+
/** The global search, centred: a TopBarSearch. Hidden below 840px. */
|
|
21
|
+
search?: ReactNode;
|
|
22
|
+
/** Utility IconButtons in the neutral tone, after the AppShell's panel triggers. */
|
|
23
|
+
actions?: ReactNode;
|
|
24
|
+
/** The account menu: an Avatar trigger, after a rule. */
|
|
25
|
+
account?: ReactNode;
|
|
26
|
+
}
|
|
27
|
+
/** An environment's colour: production, staging, development… The word says which. */
|
|
28
|
+
export type TopBarStatus = "neutral" | "success" | "info" | "warning" | "danger";
|
|
29
|
+
export interface TopBarSwitcherProps extends Omit<Button.Props, "children" | "className"> {
|
|
30
|
+
/** The current organisation, project or environment. */
|
|
31
|
+
label: ReactNode;
|
|
32
|
+
/** A small square before the label: the organisation's initials or logo. */
|
|
33
|
+
mark?: ReactNode;
|
|
34
|
+
/** A dot before the label, for an environment. Never colour alone: the label names it. */
|
|
35
|
+
status?: TopBarStatus;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
export interface TopBarSearchProps extends Omit<Button.Props, "children" | "className"> {
|
|
39
|
+
/** The prompt, which is also the button's name: "Search or jump to…". */
|
|
40
|
+
placeholder: string;
|
|
41
|
+
/** Keys shown at the end, e.g. `["⌘", "K"]`. Wire the shortcut itself in the product. */
|
|
42
|
+
shortcut?: readonly string[];
|
|
43
|
+
className?: string;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The scope bar of a Qeet console (07-components.md, C1): launcher, product, the scope path,
|
|
47
|
+
* search, tools and account across the full width. It's black in light and dark alike (it renders
|
|
48
|
+
* the dark scheme on itself), topped by a 2px Ember line.
|
|
49
|
+
*/
|
|
50
|
+
export declare function TopBar({ launcher, logo, product, href, render, context, search, actions, account, className, ...props }: TopBarProps): ReactElement;
|
|
51
|
+
/**
|
|
52
|
+
* A step of the TopBar's scope path: the current organisation or environment, which opens a Menu
|
|
53
|
+
* to switch it. Use it as the trigger: `<MenuTrigger render={<TopBarSwitcher label="Acme" />} />`.
|
|
54
|
+
*/
|
|
55
|
+
export declare function TopBarSwitcher({ label, mark, status, className, ...props }: TopBarSwitcherProps): ReactElement;
|
|
56
|
+
/**
|
|
57
|
+
* The global search in the TopBar: a field-shaped button that opens the product's search or command
|
|
58
|
+
* palette, with its shortcut shown at the end.
|
|
59
|
+
*/
|
|
60
|
+
export declare function TopBarSearch({ placeholder, shortcut, className, ...props }: TopBarSearchProps): ReactElement;
|