@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.
Files changed (104) hide show
  1. package/CHANGELOG.md +116 -1
  2. package/README.md +99 -0
  3. package/dist/button/button.d.ts +6 -2
  4. package/dist/button/button.js +5 -1
  5. package/dist/css/avatar.css +6 -0
  6. package/dist/css/badge.css +6 -0
  7. package/dist/css/button.css +81 -6
  8. package/dist/css/checkbox.css +9 -0
  9. package/dist/css/dialog.css +7 -0
  10. package/dist/css/drawer.css +6 -0
  11. package/dist/css/experimental/alert.css +89 -0
  12. package/dist/css/experimental/app-shell.css +258 -0
  13. package/dist/css/experimental/breadcrumb.css +67 -0
  14. package/dist/css/experimental/copy-button.css +13 -0
  15. package/dist/css/experimental/empty-state.css +63 -0
  16. package/dist/css/experimental/otp-input.css +84 -0
  17. package/dist/css/experimental/page-header.css +110 -0
  18. package/dist/css/experimental/password-input.css +21 -0
  19. package/dist/css/experimental/sidebar.css +234 -0
  20. package/dist/css/experimental/stat.css +115 -0
  21. package/dist/css/experimental/top-bar.css +299 -0
  22. package/dist/css/input.css +10 -0
  23. package/dist/css/menu.css +17 -0
  24. package/dist/css/progress.css +14 -0
  25. package/dist/css/scroll-area.css +7 -0
  26. package/dist/css/select.css +16 -0
  27. package/dist/css/separator.css +7 -0
  28. package/dist/css/skeleton.css +6 -0
  29. package/dist/css/slider.css +22 -0
  30. package/dist/css/switch.css +33 -0
  31. package/dist/css/tabs.css +11 -0
  32. package/dist/css/toast.css +7 -0
  33. package/dist/css/toggle.css +13 -0
  34. package/dist/css/tooltip.css +6 -0
  35. package/dist/dialog/dialog.d.ts +4 -1
  36. package/dist/dialog/dialog.js +4 -3
  37. package/dist/drawer/drawer.d.ts +4 -1
  38. package/dist/drawer/drawer.js +5 -4
  39. package/dist/experimental/alert/alert.d.ts +29 -0
  40. package/dist/experimental/alert/alert.js +72 -0
  41. package/dist/experimental/alert/index.d.ts +2 -0
  42. package/dist/experimental/app-shell/app-shell.d.ts +60 -0
  43. package/dist/experimental/app-shell/app-shell.js +224 -0
  44. package/dist/experimental/app-shell/index.d.ts +2 -0
  45. package/dist/experimental/breadcrumb/breadcrumb.d.ts +27 -0
  46. package/dist/experimental/breadcrumb/breadcrumb.js +48 -0
  47. package/dist/experimental/breadcrumb/index.d.ts +2 -0
  48. package/dist/experimental/copy-button/copy-button.d.ts +27 -0
  49. package/dist/experimental/copy-button/copy-button.js +61 -0
  50. package/dist/experimental/copy-button/index.d.ts +2 -0
  51. package/dist/experimental/empty-state/data-state.d.ts +23 -0
  52. package/dist/experimental/empty-state/data-state.js +40 -0
  53. package/dist/experimental/empty-state/empty-state.d.ts +26 -0
  54. package/dist/experimental/empty-state/empty-state.js +46 -0
  55. package/dist/experimental/empty-state/index.d.ts +3 -0
  56. package/dist/experimental/index.d.ts +24 -0
  57. package/dist/experimental/index.js +13 -0
  58. package/dist/experimental/otp-input/index.d.ts +2 -0
  59. package/dist/experimental/otp-input/otp-input.d.ts +27 -0
  60. package/dist/experimental/otp-input/otp-input.js +39 -0
  61. package/dist/experimental/page-header/index.d.ts +2 -0
  62. package/dist/experimental/page-header/page-header.d.ts +32 -0
  63. package/dist/experimental/page-header/page-header.js +73 -0
  64. package/dist/experimental/password-input/index.d.ts +2 -0
  65. package/dist/experimental/password-input/password-input.d.ts +26 -0
  66. package/dist/experimental/password-input/password-input.js +103 -0
  67. package/dist/experimental/sidebar/index.d.ts +2 -0
  68. package/dist/experimental/sidebar/sidebar.d.ts +64 -0
  69. package/dist/experimental/sidebar/sidebar.js +200 -0
  70. package/dist/experimental/stat/index.d.ts +2 -0
  71. package/dist/experimental/stat/stat.d.ts +31 -0
  72. package/dist/experimental/stat/stat.js +90 -0
  73. package/dist/experimental/top-bar/index.d.ts +2 -0
  74. package/dist/experimental/top-bar/top-bar.d.ts +60 -0
  75. package/dist/experimental/top-bar/top-bar.js +162 -0
  76. package/dist/field/field.d.ts +4 -1
  77. package/dist/field/field.js +6 -5
  78. package/dist/format/format.d.ts +12 -0
  79. package/dist/format/format.js +50 -0
  80. package/dist/format/index.d.ts +2 -0
  81. package/dist/format/index.js +1 -0
  82. package/dist/index.d.ts +6 -2
  83. package/dist/index.js +3 -1
  84. package/dist/internal/media.js +15 -0
  85. package/dist/internal/messages.d.ts +75 -0
  86. package/dist/internal/messages.js +44 -5
  87. package/dist/link/link.d.ts +7 -1
  88. package/dist/link/link.js +3 -2
  89. package/dist/locales/hi.d.ts +8 -0
  90. package/dist/locales/hi.js +60 -0
  91. package/dist/progress/progress.d.ts +2 -2
  92. package/dist/progress/progress.js +7 -2
  93. package/dist/provider/index.d.ts +3 -0
  94. package/dist/provider/index.js +1 -0
  95. package/dist/provider/provider.d.ts +46 -0
  96. package/dist/provider/provider.js +90 -0
  97. package/dist/slider/slider.d.ts +5 -2
  98. package/dist/slider/slider.js +10 -3
  99. package/dist/styles.css +1704 -104
  100. package/dist/text-field/text-field.d.ts +2 -2
  101. package/dist/text-field/text-field.js +4 -2
  102. package/dist/toast/toast.d.ts +4 -1
  103. package/dist/toast/toast.js +7 -6
  104. 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,2 @@
1
+ import { Sidebar, SidebarGroup, SidebarGroupProps, SidebarItem, SidebarItemProps, SidebarProps } from "./sidebar.js";
2
+ export { Sidebar, SidebarGroup, type SidebarGroupProps, SidebarItem, type SidebarItemProps, type SidebarProps };
@@ -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,2 @@
1
+ import { Stat, StatGroup, StatGroupProps, StatProps, StatTone } from "./stat.js";
2
+ export { Stat, StatGroup, type StatGroupProps, type StatProps, type StatTone };
@@ -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,2 @@
1
+ import { TopBar, TopBarProps, TopBarSearch, TopBarSearchProps, TopBarStatus, TopBarSwitcher, TopBarSwitcherProps } from "./top-bar.js";
2
+ export { TopBar, type TopBarProps, TopBarSearch, type TopBarSearchProps, type TopBarStatus, TopBarSwitcher, type TopBarSwitcherProps };
@@ -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;