@brijbyte/agentic-ui 0.0.3 → 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/README.md +132 -35
- package/dist/accordion/accordion.d.ts +14 -0
- package/dist/accordion/accordion.d.ts.map +1 -1
- package/dist/accordion/accordion.js +5 -0
- package/dist/accordion/accordion.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +11 -0
- package/dist/alert-dialog/alert-dialog.d.ts.map +1 -1
- package/dist/alert-dialog/alert-dialog.js +5 -0
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/badge/badge.d.ts +6 -0
- package/dist/badge/badge.d.ts.map +1 -1
- package/dist/badge/badge.js +4 -0
- package/dist/badge/badge.js.map +1 -1
- package/dist/button/button.d.ts +12 -1
- package/dist/button/button.d.ts.map +1 -1
- package/dist/button/button.js +5 -0
- package/dist/button/button.js.map +1 -1
- package/dist/card/card.d.ts +10 -0
- package/dist/card/card.d.ts.map +1 -1
- package/dist/card/card.js +7 -0
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +13 -0
- package/dist/checkbox/checkbox.d.ts.map +1 -1
- package/dist/checkbox/checkbox.js +4 -0
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/collapsible/collapsible.d.ts +11 -0
- package/dist/collapsible/collapsible.d.ts.map +1 -1
- package/dist/collapsible/collapsible.js +5 -0
- package/dist/collapsible/collapsible.js.map +1 -1
- package/dist/context-menu/context-menu.d.ts +5 -0
- package/dist/context-menu/context-menu.d.ts.map +1 -1
- package/dist/context-menu/context-menu.js +4 -0
- package/dist/context-menu/context-menu.js.map +1 -1
- package/dist/dialog/dialog.d.ts +13 -1
- package/dist/dialog/dialog.d.ts.map +1 -1
- package/dist/dialog/dialog.js +6 -0
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/drawer/drawer.d.ts +11 -0
- package/dist/drawer/drawer.d.ts.map +1 -1
- package/dist/drawer/drawer.js +5 -0
- package/dist/drawer/drawer.js.map +1 -1
- package/dist/index.css +1681 -1251
- package/dist/index.d.ts +19 -10
- package/dist/index.js +14 -1
- package/dist/input/input.d.ts +8 -0
- package/dist/input/input.d.ts.map +1 -1
- package/dist/input/input.js +5 -0
- package/dist/input/input.js.map +1 -1
- package/dist/menu/menu.css +3 -8
- package/dist/menu/menu.d.ts +11 -4
- package/dist/menu/menu.d.ts.map +1 -1
- package/dist/menu/menu.js +10 -24
- package/dist/menu/menu.js.map +1 -1
- package/dist/menu/menu.module.js +1 -1
- package/dist/menu/menu.module.js.map +1 -1
- package/dist/meter/circular-meter.d.ts +48 -0
- package/dist/meter/circular-meter.d.ts.map +1 -0
- package/dist/meter/circular-meter.js +86 -0
- package/dist/meter/circular-meter.js.map +1 -0
- package/dist/meter/index.d.ts +4 -0
- package/dist/meter/index.js +5 -0
- package/dist/meter/meter.css +152 -0
- package/dist/meter/meter.d.ts +58 -0
- package/dist/meter/meter.d.ts.map +1 -0
- package/dist/meter/meter.js +50 -0
- package/dist/meter/meter.js.map +1 -0
- package/dist/meter/meter.module.css.d.ts +2 -0
- package/dist/meter/meter.module.js +27 -0
- package/dist/meter/meter.module.js.map +1 -0
- package/dist/meter/meterState.js +18 -0
- package/dist/meter/meterState.js.map +1 -0
- package/dist/meter/parts.d.ts +31 -0
- package/dist/meter/parts.d.ts.map +1 -0
- package/dist/meter/parts.js +56 -0
- package/dist/meter/parts.js.map +1 -0
- package/dist/number-field/number-field.d.ts +16 -0
- package/dist/number-field/number-field.d.ts.map +1 -1
- package/dist/number-field/number-field.js +4 -0
- package/dist/number-field/number-field.js.map +1 -1
- package/dist/popover/index.d.ts +3 -0
- package/dist/popover/index.js +4 -0
- package/dist/popover/parts.d.ts +43 -0
- package/dist/popover/parts.d.ts.map +1 -0
- package/dist/popover/parts.js +96 -0
- package/dist/popover/parts.js.map +1 -0
- package/dist/popover/popover.css +173 -0
- package/dist/popover/popover.d.ts +49 -0
- package/dist/popover/popover.d.ts.map +1 -0
- package/dist/popover/popover.js +68 -0
- package/dist/popover/popover.js.map +1 -0
- package/dist/popover/popover.module.css.d.ts +2 -0
- package/dist/popover/popover.module.js +16 -0
- package/dist/popover/popover.module.js.map +1 -0
- package/dist/progress/progress.d.ts +11 -0
- package/dist/progress/progress.d.ts.map +1 -1
- package/dist/progress/progress.js +5 -0
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio/index.d.ts +3 -0
- package/dist/radio/index.js +4 -0
- package/dist/radio/parts.d.ts +18 -0
- package/dist/radio/parts.d.ts.map +1 -0
- package/dist/radio/parts.js +42 -0
- package/dist/radio/parts.js.map +1 -0
- package/dist/radio/radio.css +84 -0
- package/dist/radio/radio.d.ts +31 -0
- package/dist/radio/radio.d.ts.map +1 -0
- package/dist/radio/radio.js +33 -0
- package/dist/radio/radio.js.map +1 -0
- package/dist/radio/radio.module.css.d.ts +2 -0
- package/dist/radio/radio.module.js +11 -0
- package/dist/radio/radio.module.js.map +1 -0
- package/dist/radio-group/index.d.ts +3 -0
- package/dist/radio-group/index.js +4 -0
- package/dist/radio-group/parts.d.ts +13 -0
- package/dist/radio-group/parts.d.ts.map +1 -0
- package/dist/radio-group/parts.js +31 -0
- package/dist/radio-group/parts.js.map +1 -0
- package/dist/radio-group/radio-group.css +17 -0
- package/dist/radio-group/radio-group.d.ts +37 -0
- package/dist/radio-group/radio-group.d.ts.map +1 -0
- package/dist/radio-group/radio-group.js +28 -0
- package/dist/radio-group/radio-group.js.map +1 -0
- package/dist/radio-group/radio-group.module.css.d.ts +2 -0
- package/dist/radio-group/radio-group.module.js +9 -0
- package/dist/radio-group/radio-group.module.js.map +1 -0
- package/dist/select/select.d.ts +14 -1
- package/dist/select/select.d.ts.map +1 -1
- package/dist/select/select.js +4 -0
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +4 -0
- package/dist/separator/separator.d.ts.map +1 -1
- package/dist/separator/separator.js +4 -0
- package/dist/separator/separator.js.map +1 -1
- package/dist/shared/PopupArrow.js +22 -0
- package/dist/shared/PopupArrow.js.map +1 -0
- package/dist/slider/slider.d.ts +18 -0
- package/dist/slider/slider.d.ts.map +1 -1
- package/dist/slider/slider.js +6 -0
- package/dist/slider/slider.js.map +1 -1
- package/dist/switch/switch.css +11 -2
- package/dist/switch/switch.d.ts +12 -0
- package/dist/switch/switch.d.ts.map +1 -1
- package/dist/switch/switch.js +4 -0
- package/dist/switch/switch.js.map +1 -1
- package/dist/switch/switch.module.js.map +1 -1
- package/dist/tabs/tabs.d.ts +8 -1
- package/dist/tabs/tabs.d.ts.map +1 -1
- package/dist/tabs/tabs.js +4 -0
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/toast/toast.d.ts +11 -0
- package/dist/toast/toast.d.ts.map +1 -1
- package/dist/toast/toast.js +8 -0
- package/dist/toast/toast.js.map +1 -1
- package/dist/tooltip/tooltip.d.ts +9 -0
- package/dist/tooltip/tooltip.d.ts.map +1 -1
- package/dist/tooltip/tooltip.js +4 -0
- package/dist/tooltip/tooltip.js.map +1 -1
- package/package.json +22 -2
- package/src/accordion/accordion.tsx +14 -0
- package/src/alert-dialog/alert-dialog.tsx +11 -0
- package/src/badge/badge.tsx +6 -0
- package/src/button/button.tsx +12 -1
- package/src/card/card.tsx +10 -0
- package/src/checkbox/checkbox.tsx +13 -0
- package/src/collapsible/collapsible.tsx +11 -0
- package/src/context-menu/context-menu.tsx +5 -0
- package/src/dialog/dialog.tsx +13 -1
- package/src/drawer/drawer.tsx +11 -0
- package/src/index.ts +4 -0
- package/src/input/input.tsx +8 -0
- package/src/menu/menu.module.css +3 -10
- package/src/menu/menu.tsx +13 -26
- package/src/meter/circular-meter.tsx +114 -0
- package/src/meter/index.ts +9 -0
- package/src/meter/meter.module.css +162 -0
- package/src/meter/meter.tsx +86 -0
- package/src/meter/meterState.ts +29 -0
- package/src/meter/parts.tsx +72 -0
- package/src/number-field/number-field.tsx +16 -0
- package/src/popover/index.ts +14 -0
- package/src/popover/parts.tsx +120 -0
- package/src/popover/popover.module.css +189 -0
- package/src/popover/popover.tsx +80 -0
- package/src/progress/progress.tsx +11 -0
- package/src/radio/index.ts +6 -0
- package/src/radio/parts.tsx +43 -0
- package/src/radio/radio.module.css +96 -0
- package/src/radio/radio.tsx +37 -0
- package/src/radio-group/index.ts +5 -0
- package/src/radio-group/parts.tsx +32 -0
- package/src/radio-group/radio-group.module.css +17 -0
- package/src/radio-group/radio-group.tsx +63 -0
- package/src/select/select.tsx +14 -1
- package/src/separator/separator.tsx +4 -0
- package/src/shared/PopupArrow.tsx +41 -0
- package/src/slider/slider.tsx +18 -0
- package/src/switch/switch.module.css +11 -2
- package/src/switch/switch.tsx +12 -0
- package/src/tabs/tabs.tsx +8 -1
- package/src/toast/toast.tsx +11 -0
- package/src/tooltip/tooltip.tsx +9 -0
package/src/dialog/dialog.tsx
CHANGED
|
@@ -3,12 +3,17 @@ import { Dialog as BaseDialog } from "@base-ui/react/dialog";
|
|
|
3
3
|
import styles from "./dialog.module.css";
|
|
4
4
|
|
|
5
5
|
export interface DialogProps {
|
|
6
|
+
/** Controlled open state. */
|
|
6
7
|
open?: boolean;
|
|
8
|
+
/** Whether the dialog is initially open (uncontrolled). */
|
|
7
9
|
defaultOpen?: boolean;
|
|
8
|
-
/** `eventDetails` is the base-ui event details object. */
|
|
10
|
+
/** Called when the dialog opens or closes. `eventDetails` is the base-ui event details object. */
|
|
9
11
|
onOpenChange?: (open: boolean, eventDetails: unknown) => void;
|
|
12
|
+
/** Element that opens the dialog when clicked. */
|
|
10
13
|
trigger?: ReactElement;
|
|
14
|
+
/** Heading rendered at the top of the dialog. */
|
|
11
15
|
title?: ReactNode;
|
|
16
|
+
/** Supplementary text below the title. */
|
|
12
17
|
description?: ReactNode;
|
|
13
18
|
children?: ReactNode;
|
|
14
19
|
/** Buttons aligned to the right (cancel, confirm). */
|
|
@@ -20,6 +25,7 @@ export interface DialogProps {
|
|
|
20
25
|
*/
|
|
21
26
|
footerStart?: ReactNode;
|
|
22
27
|
className?: string;
|
|
28
|
+
/** Whether the dialog can be closed by clicking the backdrop or pressing Escape. @default true */
|
|
23
29
|
dismissible?: boolean;
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -31,6 +37,12 @@ function XIcon() {
|
|
|
31
37
|
);
|
|
32
38
|
}
|
|
33
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Modal overlay dialog with title, description, body, and footer slots.
|
|
42
|
+
* Wraps `@base-ui/react` Dialog with a pre-styled backdrop, popup, and
|
|
43
|
+
* close button. Use `footerStart` for a macOS-style left-aligned
|
|
44
|
+
* destructive action.
|
|
45
|
+
*/
|
|
34
46
|
export function Dialog({
|
|
35
47
|
trigger,
|
|
36
48
|
title,
|
package/src/drawer/drawer.tsx
CHANGED
|
@@ -9,17 +9,23 @@ export interface DrawerProps {
|
|
|
9
9
|
trigger?: ReactElement;
|
|
10
10
|
/** Which edge the drawer slides in from. @default "right" */
|
|
11
11
|
side?: DrawerSide;
|
|
12
|
+
/** Heading rendered at the top of the drawer. */
|
|
12
13
|
title?: ReactNode;
|
|
14
|
+
/** Supplementary text below the title. */
|
|
13
15
|
description?: ReactNode;
|
|
14
16
|
children?: ReactNode;
|
|
17
|
+
/** Content rendered at the bottom of the drawer. */
|
|
15
18
|
footer?: ReactNode;
|
|
16
19
|
/** Show a drag handle bar (useful for bottom/top drawers). @default true for bottom/top */
|
|
17
20
|
handleBar?: boolean;
|
|
18
21
|
/** Show a close button in the top-right corner. @default true */
|
|
19
22
|
dismissible?: boolean;
|
|
20
23
|
|
|
24
|
+
/** Controlled open state. */
|
|
21
25
|
open?: boolean;
|
|
26
|
+
/** Whether the drawer is initially open (uncontrolled). */
|
|
22
27
|
defaultOpen?: boolean;
|
|
28
|
+
/** Called when the drawer opens or closes. */
|
|
23
29
|
onOpenChange?: (open: boolean) => void;
|
|
24
30
|
}
|
|
25
31
|
|
|
@@ -47,6 +53,11 @@ const POPUP_CLASS: Record<DrawerSide, string> = {
|
|
|
47
53
|
top: styles["popup-top"] as string,
|
|
48
54
|
};
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* A panel that slides in from any edge of the screen with swipe-to-dismiss
|
|
58
|
+
* gestures. Supports left, right, top, and bottom orientations with an
|
|
59
|
+
* optional drag handle bar.
|
|
60
|
+
*/
|
|
50
61
|
export function Drawer({
|
|
51
62
|
trigger,
|
|
52
63
|
side = "right",
|
package/src/index.ts
CHANGED
|
@@ -14,8 +14,12 @@ export * from "./dialog";
|
|
|
14
14
|
export * from "./drawer";
|
|
15
15
|
export * from "./input";
|
|
16
16
|
export * from "./menu";
|
|
17
|
+
export * from "./meter";
|
|
17
18
|
export * from "./number-field";
|
|
19
|
+
export * from "./popover";
|
|
18
20
|
export * from "./progress";
|
|
21
|
+
export * from "./radio";
|
|
22
|
+
export * from "./radio-group";
|
|
19
23
|
export * from "./select";
|
|
20
24
|
export * from "./separator";
|
|
21
25
|
export * from "./slider";
|
package/src/input/input.tsx
CHANGED
|
@@ -9,11 +9,19 @@ export type InputSize = "sm" | "md" | "lg";
|
|
|
9
9
|
export interface InputProps extends Omit<ComponentPropsWithoutRef<"input">, "size"> {
|
|
10
10
|
/** Visual size. Maps to height + font-size design tokens. Default: `"md"`. */
|
|
11
11
|
size?: InputSize;
|
|
12
|
+
/** Icon or element rendered before the input text. */
|
|
12
13
|
leftAdornment?: ReactNode;
|
|
14
|
+
/** Icon or element rendered after the input text. */
|
|
13
15
|
rightAdornment?: ReactNode;
|
|
16
|
+
/** Applies error styling and sets `aria-invalid`. */
|
|
14
17
|
invalid?: boolean;
|
|
15
18
|
}
|
|
16
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Text input with optional left and right adornment slots. Extends the
|
|
22
|
+
* native `<input>` API — replaces the numeric `size` attribute with a
|
|
23
|
+
* token-based size prop.
|
|
24
|
+
*/
|
|
17
25
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(
|
|
18
26
|
{ size = "md", leftAdornment, rightAdornment, invalid, className, ...props },
|
|
19
27
|
ref,
|
package/src/menu/menu.module.css
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
font-family: var(--font-mono);
|
|
15
15
|
font-size: var(--font-size-sm);
|
|
16
16
|
min-width: 200px;
|
|
17
|
-
transform-origin: var(--transform-origin);
|
|
18
17
|
outline: none;
|
|
18
|
+
transition: opacity 150ms var(--easing-ease-out);
|
|
19
19
|
}
|
|
20
20
|
@supports (backdrop-filter: blur(12px)) {
|
|
21
21
|
.popup {
|
|
@@ -25,17 +25,10 @@
|
|
|
25
25
|
}
|
|
26
26
|
.popup[data-starting-style] {
|
|
27
27
|
opacity: 0;
|
|
28
|
-
transform: scale(0.95);
|
|
29
|
-
transition:
|
|
30
|
-
opacity 150ms var(--easing-ease-out),
|
|
31
|
-
transform 150ms var(--easing-ease-out);
|
|
32
28
|
}
|
|
33
29
|
.popup[data-ending-style] {
|
|
34
30
|
opacity: 0;
|
|
35
|
-
|
|
36
|
-
transition:
|
|
37
|
-
opacity 75ms var(--easing-ease-in),
|
|
38
|
-
transform 75ms var(--easing-ease-in);
|
|
31
|
+
transition: opacity 75ms var(--easing-ease-in);
|
|
39
32
|
}
|
|
40
33
|
/* ─── Item ───────────────────────────────────────────────────────── */
|
|
41
34
|
.item {
|
|
@@ -136,7 +129,7 @@
|
|
|
136
129
|
.arrow-fill {
|
|
137
130
|
fill: var(--color-overlay);
|
|
138
131
|
}
|
|
139
|
-
.arrow-
|
|
132
|
+
.arrow-seam {
|
|
140
133
|
fill: var(--color-line);
|
|
141
134
|
}
|
|
142
135
|
}
|
package/src/menu/menu.tsx
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ReactNode, ReactElement } from "react";
|
|
2
2
|
import { Menu as BaseMenu } from "@base-ui/react/menu";
|
|
3
3
|
import styles from "./menu.module.css";
|
|
4
|
+
import { PopupArrow } from "../shared/PopupArrow";
|
|
4
5
|
|
|
5
6
|
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
6
7
|
|
|
@@ -28,40 +29,21 @@ export type MenuEntry = MenuItemDef | MenuSeparatorDef | MenuGroupDef;
|
|
|
28
29
|
export interface MenuProps {
|
|
29
30
|
/** The button/element that opens the menu. */
|
|
30
31
|
trigger: ReactElement;
|
|
32
|
+
/** Array of menu entries: items, separators, and groups. */
|
|
31
33
|
items: MenuEntry[];
|
|
32
|
-
/** @default "bottom" */
|
|
34
|
+
/** Which edge of the trigger the menu appears on. @default "bottom" */
|
|
33
35
|
side?: "top" | "bottom" | "left" | "right";
|
|
34
|
-
/** @default "start" */
|
|
36
|
+
/** Alignment along the side axis. @default "start" */
|
|
35
37
|
align?: "start" | "center" | "end";
|
|
36
|
-
/** @default 4 */
|
|
38
|
+
/** Gap between the trigger and the menu popup in pixels. @default 4 */
|
|
37
39
|
sideOffset?: number;
|
|
38
|
-
/**
|
|
40
|
+
/** Controlled open state. */
|
|
39
41
|
open?: boolean;
|
|
42
|
+
/** Called when the menu opens or closes. */
|
|
40
43
|
onOpenChange?: (open: boolean) => void;
|
|
41
44
|
className?: string;
|
|
42
45
|
}
|
|
43
46
|
|
|
44
|
-
// ─── Arrow SVG ─────────────────────────────────────────────────────────────
|
|
45
|
-
|
|
46
|
-
function ArrowSvg() {
|
|
47
|
-
return (
|
|
48
|
-
<svg width="20" height="10" viewBox="0 0 20 10" fill="none" aria-hidden>
|
|
49
|
-
<path
|
|
50
|
-
d="M9.66437 2.60207L4.80758 6.97318C4.07308 7.63423 3.11989 8 2.13172 8H0V10H20V8H18.5349C17.5468 8 16.5936 7.63423 15.8591 6.97318L11.0023 2.60207C10.622 2.2598 10.0447 2.25979 9.66437 2.60207Z"
|
|
51
|
-
className={styles["arrow-fill"]}
|
|
52
|
-
/>
|
|
53
|
-
<path
|
|
54
|
-
d="M8.99542 1.85876C9.75604 1.17425 10.9106 1.17422 11.6713 1.85878L16.5281 6.22989C17.0789 6.72568 17.7938 7.00001 18.5349 7.00001L15.89 7L11.0023 2.60207C10.622 2.2598 10.0447 2.2598 9.66436 2.60207L4.77734 7L2.13171 7.00001C2.87284 7.00001 3.58774 6.72568 4.13861 6.22989L8.99542 1.85876Z"
|
|
55
|
-
className={styles["arrow-stroke"]}
|
|
56
|
-
/>
|
|
57
|
-
<path
|
|
58
|
-
d="M10.3333 3.34539L5.47654 7.71648C4.55842 8.54279 3.36693 9 2.13172 9H0V8H2.13172C3.11989 8 4.07308 7.63423 4.80758 6.97318L9.66437 2.60207C10.0447 2.25979 10.622 2.2598 11.0023 2.60207L15.8591 6.97318C16.5936 7.63423 17.5468 8 18.5349 8H20V9H18.5349C17.2998 9 16.1083 8.54278 15.1901 7.71648L10.3333 3.34539Z"
|
|
59
|
-
className={styles["arrow-stroke"]}
|
|
60
|
-
/>
|
|
61
|
-
</svg>
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
47
|
// ─── Item renderer ──────────────────────────────────────────────────────────
|
|
66
48
|
|
|
67
49
|
function renderEntry(entry: MenuEntry, index: number): ReactNode {
|
|
@@ -88,6 +70,11 @@ function renderEntry(entry: MenuEntry, index: number): ReactNode {
|
|
|
88
70
|
|
|
89
71
|
// ─── High-level Menu ────────────────────────────────────────────────────────
|
|
90
72
|
|
|
73
|
+
/**
|
|
74
|
+
* A dropdown list of actions with full keyboard navigation. Supports
|
|
75
|
+
* separators, groups, and keyboard shortcuts. Items can be disabled
|
|
76
|
+
* individually.
|
|
77
|
+
*/
|
|
91
78
|
export function Menu({ trigger, items, side = "bottom", align = "start", sideOffset = 4, className, onOpenChange, ...props }: MenuProps) {
|
|
92
79
|
return (
|
|
93
80
|
<BaseMenu.Root onOpenChange={onOpenChange as never} {...props}>
|
|
@@ -96,7 +83,7 @@ export function Menu({ trigger, items, side = "bottom", align = "start", sideOff
|
|
|
96
83
|
<BaseMenu.Positioner className={`${styles.positioner} ${className ?? ""}`} side={side} align={align} sideOffset={sideOffset}>
|
|
97
84
|
<BaseMenu.Popup className={styles.popup}>
|
|
98
85
|
<BaseMenu.Arrow className={styles.arrow}>
|
|
99
|
-
<
|
|
86
|
+
<PopupArrow fillClass={styles["arrow-fill"]!} seamClass={styles["arrow-seam"]!} />
|
|
100
87
|
</BaseMenu.Arrow>
|
|
101
88
|
{items.map((entry, i) => renderEntry(entry, i))}
|
|
102
89
|
</BaseMenu.Popup>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Meter as BaseMeter } from "@base-ui/react/meter";
|
|
3
|
+
import styles from "./meter.module.css";
|
|
4
|
+
import { getMeterState } from "./meterState";
|
|
5
|
+
|
|
6
|
+
export type CircularMeterSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export interface CircularMeterProps {
|
|
9
|
+
/** Current value. Must be between `min` and `max`. */
|
|
10
|
+
value: number;
|
|
11
|
+
/** @default 0 */
|
|
12
|
+
min?: number;
|
|
13
|
+
/** @default 100 */
|
|
14
|
+
max?: number;
|
|
15
|
+
/** Upper boundary of the low zone. Values ≤ this are "low". */
|
|
16
|
+
low?: number;
|
|
17
|
+
/** Lower boundary of the high zone. Values ≥ this are "high". */
|
|
18
|
+
high?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Which zone is optimal — determines stroke colour:
|
|
21
|
+
* - `"high"` (default) → high=green, mid=amber, low=red (battery, signal)
|
|
22
|
+
* - `"low"` → low=green, mid=amber, high=red (CPU load, disk)
|
|
23
|
+
* - `"mid"` → mid=green, low/high=amber (temperature)
|
|
24
|
+
*/
|
|
25
|
+
optimum?: "low" | "mid" | "high";
|
|
26
|
+
/** Accessible + visible label shown below the value inside the circle. */
|
|
27
|
+
label?: ReactNode;
|
|
28
|
+
/** Show the formatted value inside the circle. @default false */
|
|
29
|
+
showValue?: boolean;
|
|
30
|
+
/** `Intl.NumberFormatOptions` for value formatting. */
|
|
31
|
+
format?: Intl.NumberFormatOptions;
|
|
32
|
+
size?: CircularMeterSize;
|
|
33
|
+
className?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const SIZES: Record<CircularMeterSize, { diameter: number; strokeWidth: number }> = {
|
|
37
|
+
sm: { diameter: 56, strokeWidth: 5 },
|
|
38
|
+
md: { diameter: 80, strokeWidth: 7 },
|
|
39
|
+
lg: { diameter: 112, strokeWidth: 9 },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function CircularMeter({
|
|
43
|
+
value,
|
|
44
|
+
min = 0,
|
|
45
|
+
max = 100,
|
|
46
|
+
low,
|
|
47
|
+
high,
|
|
48
|
+
optimum = "high",
|
|
49
|
+
label,
|
|
50
|
+
showValue = false,
|
|
51
|
+
format,
|
|
52
|
+
size = "md",
|
|
53
|
+
className = "",
|
|
54
|
+
}: CircularMeterProps) {
|
|
55
|
+
const clampedValue = Math.min(Math.max(value, min), max);
|
|
56
|
+
const state = getMeterState(clampedValue, min, max, low, high, optimum);
|
|
57
|
+
|
|
58
|
+
const { diameter, strokeWidth } = SIZES[size];
|
|
59
|
+
const radius = (diameter - strokeWidth) / 2;
|
|
60
|
+
const circumference = 2 * Math.PI * radius;
|
|
61
|
+
const dashOffset = circumference * (1 - (clampedValue - min) / (max - min));
|
|
62
|
+
const center = diameter / 2;
|
|
63
|
+
|
|
64
|
+
return (
|
|
65
|
+
<BaseMeter.Root
|
|
66
|
+
value={clampedValue}
|
|
67
|
+
min={min}
|
|
68
|
+
max={max}
|
|
69
|
+
{...(format !== undefined && { format })}
|
|
70
|
+
className={`${styles["circular-root"]} ${styles[`circular-${size}`]} ${className}`}
|
|
71
|
+
>
|
|
72
|
+
<div style={{ position: "relative", width: diameter, height: diameter }}>
|
|
73
|
+
<svg
|
|
74
|
+
width={diameter}
|
|
75
|
+
height={diameter}
|
|
76
|
+
viewBox={`0 0 ${diameter} ${diameter}`}
|
|
77
|
+
className={styles["circular-svg"]}
|
|
78
|
+
aria-hidden="true"
|
|
79
|
+
>
|
|
80
|
+
{/* Background track ring */}
|
|
81
|
+
<circle cx={center} cy={center} r={radius} strokeWidth={strokeWidth} className={styles["circular-track"]} />
|
|
82
|
+
{/* Value arc — origin at 12 o'clock */}
|
|
83
|
+
<circle
|
|
84
|
+
cx={center}
|
|
85
|
+
cy={center}
|
|
86
|
+
r={radius}
|
|
87
|
+
strokeWidth={strokeWidth}
|
|
88
|
+
strokeDasharray={circumference}
|
|
89
|
+
strokeDashoffset={dashOffset}
|
|
90
|
+
className={styles["circular-indicator"]}
|
|
91
|
+
data-meter-state={state}
|
|
92
|
+
style={{ transform: "rotate(-90deg)", transformOrigin: "center" }}
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
|
|
96
|
+
{(showValue || label != null) && (
|
|
97
|
+
<div
|
|
98
|
+
style={{
|
|
99
|
+
position: "absolute",
|
|
100
|
+
inset: 0,
|
|
101
|
+
display: "flex",
|
|
102
|
+
flexDirection: "column",
|
|
103
|
+
alignItems: "center",
|
|
104
|
+
justifyContent: "center",
|
|
105
|
+
}}
|
|
106
|
+
>
|
|
107
|
+
{showValue && <BaseMeter.Value className={styles["circular-value"]} />}
|
|
108
|
+
{label != null && <BaseMeter.Label className={styles["circular-label"]}>{label}</BaseMeter.Label>}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</BaseMeter.Root>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { Meter } from "./meter";
|
|
2
|
+
export type { MeterProps, MeterSize } from "./meter";
|
|
3
|
+
export { MeterStyles } from "./meter";
|
|
4
|
+
|
|
5
|
+
export { CircularMeter } from "./circular-meter";
|
|
6
|
+
export type { CircularMeterProps, CircularMeterSize } from "./circular-meter";
|
|
7
|
+
|
|
8
|
+
export { MeterTrack, MeterIndicator, MeterLabel, MeterValue } from "./parts";
|
|
9
|
+
export type { MeterTrackProps, MeterIndicatorProps, MeterLabelProps, MeterValueProps } from "./parts";
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
@layer components {
|
|
2
|
+
.root {
|
|
3
|
+
display: flex;
|
|
4
|
+
flex-direction: column;
|
|
5
|
+
gap: var(--space-1-5);
|
|
6
|
+
width: 100%;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/* ── Label row ──────────────────────────────────────────────────── */
|
|
10
|
+
|
|
11
|
+
.label-row {
|
|
12
|
+
display: flex;
|
|
13
|
+
align-items: center;
|
|
14
|
+
justify-content: space-between;
|
|
15
|
+
font-family: var(--font-mono);
|
|
16
|
+
font-size: var(--font-size-xs);
|
|
17
|
+
color: var(--color-secondary);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.label {
|
|
21
|
+
font-family: var(--font-mono);
|
|
22
|
+
font-size: var(--font-size-xs);
|
|
23
|
+
color: var(--color-secondary);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.value {
|
|
27
|
+
font-family: var(--font-mono);
|
|
28
|
+
font-size: var(--font-size-xs);
|
|
29
|
+
color: var(--color-secondary);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/* ── Track (the outer bar) ──────────────────────────────────────── */
|
|
33
|
+
|
|
34
|
+
.track {
|
|
35
|
+
position: relative;
|
|
36
|
+
width: 100%;
|
|
37
|
+
background-color: var(--color-surface-3);
|
|
38
|
+
border-radius: var(--radius-full);
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.track-sm {
|
|
43
|
+
height: 4px;
|
|
44
|
+
}
|
|
45
|
+
.track-md {
|
|
46
|
+
height: 6px;
|
|
47
|
+
}
|
|
48
|
+
.track-lg {
|
|
49
|
+
height: 8px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* ── Indicator (the fill) ───────────────────────────────────────── */
|
|
53
|
+
|
|
54
|
+
.indicator {
|
|
55
|
+
height: 100%;
|
|
56
|
+
border-radius: var(--radius-full);
|
|
57
|
+
background-color: var(--color-accent);
|
|
58
|
+
transition: width var(--duration-slower) var(--easing-standard);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Threshold-based colours — driven by data attributes set in JS */
|
|
62
|
+
.indicator[data-meter-state="optimum"] {
|
|
63
|
+
background-color: var(--color-success-solid);
|
|
64
|
+
}
|
|
65
|
+
.indicator[data-meter-state="suboptimal"] {
|
|
66
|
+
background-color: var(--color-warning-solid);
|
|
67
|
+
}
|
|
68
|
+
.indicator[data-meter-state="critical"] {
|
|
69
|
+
background-color: var(--color-error-solid);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/* ── Tick marks for low / high thresholds ───────────────────────── */
|
|
73
|
+
|
|
74
|
+
.tick {
|
|
75
|
+
position: absolute;
|
|
76
|
+
top: 0;
|
|
77
|
+
bottom: 0;
|
|
78
|
+
width: 1px;
|
|
79
|
+
background-color: var(--color-canvas);
|
|
80
|
+
opacity: 0.6;
|
|
81
|
+
pointer-events: none;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/* ── Circular gauge ─────────────────────────────────────────────── */
|
|
85
|
+
|
|
86
|
+
.circular-root {
|
|
87
|
+
display: inline-flex;
|
|
88
|
+
flex-direction: column;
|
|
89
|
+
align-items: center;
|
|
90
|
+
gap: var(--space-1-5);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.circular-svg {
|
|
94
|
+
overflow: visible;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* Track ring */
|
|
98
|
+
.circular-track {
|
|
99
|
+
fill: none;
|
|
100
|
+
stroke: var(--color-surface-3);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/* Value arc */
|
|
104
|
+
.circular-indicator {
|
|
105
|
+
fill: none;
|
|
106
|
+
stroke: var(--color-accent);
|
|
107
|
+
stroke-linecap: round;
|
|
108
|
+
transition: stroke-dashoffset var(--duration-slower) var(--easing-standard);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.circular-indicator[data-meter-state="optimum"] {
|
|
112
|
+
stroke: var(--color-success-solid);
|
|
113
|
+
}
|
|
114
|
+
.circular-indicator[data-meter-state="suboptimal"] {
|
|
115
|
+
stroke: var(--color-warning-solid);
|
|
116
|
+
}
|
|
117
|
+
.circular-indicator[data-meter-state="critical"] {
|
|
118
|
+
stroke: var(--color-error-solid);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/* Centre text stack */
|
|
122
|
+
.circular-center {
|
|
123
|
+
display: flex;
|
|
124
|
+
flex-direction: column;
|
|
125
|
+
align-items: center;
|
|
126
|
+
justify-content: center;
|
|
127
|
+
gap: var(--space-0-5);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.circular-value {
|
|
131
|
+
font-family: var(--font-mono);
|
|
132
|
+
font-weight: var(--font-weight-semibold);
|
|
133
|
+
color: var(--color-primary);
|
|
134
|
+
line-height: 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.circular-label {
|
|
138
|
+
font-family: var(--font-mono);
|
|
139
|
+
color: var(--color-secondary);
|
|
140
|
+
line-height: 1;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/* Size variants */
|
|
144
|
+
.circular-sm .circular-value {
|
|
145
|
+
font-size: var(--font-size-sm);
|
|
146
|
+
}
|
|
147
|
+
.circular-sm .circular-label {
|
|
148
|
+
font-size: var(--font-size-xs);
|
|
149
|
+
}
|
|
150
|
+
.circular-md .circular-value {
|
|
151
|
+
font-size: var(--font-size-xl);
|
|
152
|
+
}
|
|
153
|
+
.circular-md .circular-label {
|
|
154
|
+
font-size: var(--font-size-xs);
|
|
155
|
+
}
|
|
156
|
+
.circular-lg .circular-value {
|
|
157
|
+
font-size: var(--font-size-2xl);
|
|
158
|
+
}
|
|
159
|
+
.circular-lg .circular-label {
|
|
160
|
+
font-size: var(--font-size-sm);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Meter as BaseMeter } from "@base-ui/react/meter";
|
|
3
|
+
import styles from "./meter.module.css";
|
|
4
|
+
import { getMeterState } from "./meterState";
|
|
5
|
+
|
|
6
|
+
export type MeterSize = "sm" | "md" | "lg";
|
|
7
|
+
|
|
8
|
+
export interface MeterProps {
|
|
9
|
+
/** Current value. Must be between `min` and `max`. */
|
|
10
|
+
value: number;
|
|
11
|
+
/** Minimum value of the range. @default 0 */
|
|
12
|
+
min?: number;
|
|
13
|
+
/** Maximum value of the range. @default 100 */
|
|
14
|
+
max?: number;
|
|
15
|
+
/** Upper boundary of the low zone. Values ≤ this are "low". */
|
|
16
|
+
low?: number;
|
|
17
|
+
/** Lower boundary of the high zone. Values ≥ this are "high". */
|
|
18
|
+
high?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Which zone is optimal — determines fill colour:
|
|
21
|
+
* - `"high"` (default) → high=green, mid=amber, low=red (battery, signal)
|
|
22
|
+
* - `"low"` → low=green, mid=amber, high=red (CPU load, disk)
|
|
23
|
+
* - `"mid"` → mid=green, low/high=amber (temperature)
|
|
24
|
+
*/
|
|
25
|
+
optimum?: "low" | "mid" | "high";
|
|
26
|
+
/** Accessible + visible label. */
|
|
27
|
+
label?: ReactNode;
|
|
28
|
+
/** Show the formatted value next to the label. @default false */
|
|
29
|
+
showValue?: boolean;
|
|
30
|
+
/** `Intl.NumberFormatOptions` for value formatting. */
|
|
31
|
+
format?: Intl.NumberFormatOptions;
|
|
32
|
+
/** Show tick marks at low/high threshold positions. @default false */
|
|
33
|
+
showTicks?: boolean;
|
|
34
|
+
/** Bar thickness. @default "md" */
|
|
35
|
+
size?: MeterSize;
|
|
36
|
+
className?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Displays a scalar measurement within a known range. Use `low` / `high` /
|
|
41
|
+
* `optimum` thresholds to colour the fill based on whether the value is in
|
|
42
|
+
* an optimal, suboptimal, or critical zone.
|
|
43
|
+
*/
|
|
44
|
+
export function Meter({
|
|
45
|
+
value,
|
|
46
|
+
min = 0,
|
|
47
|
+
max = 100,
|
|
48
|
+
low,
|
|
49
|
+
high,
|
|
50
|
+
optimum = "high",
|
|
51
|
+
label,
|
|
52
|
+
showValue = false,
|
|
53
|
+
format,
|
|
54
|
+
showTicks = false,
|
|
55
|
+
size = "md",
|
|
56
|
+
className = "",
|
|
57
|
+
}: MeterProps) {
|
|
58
|
+
const clampedValue = Math.min(Math.max(value, min), max);
|
|
59
|
+
const state = getMeterState(clampedValue, min, max, low, high, optimum);
|
|
60
|
+
const lowPct = low != null ? ((low - min) / (max - min)) * 100 : null;
|
|
61
|
+
const highPct = high != null ? ((high - min) / (max - min)) * 100 : null;
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<BaseMeter.Root
|
|
65
|
+
value={clampedValue}
|
|
66
|
+
min={min}
|
|
67
|
+
max={max}
|
|
68
|
+
{...(format !== undefined && { format })}
|
|
69
|
+
className={`${styles.root} ${className}`}
|
|
70
|
+
>
|
|
71
|
+
{(label != null || showValue) && (
|
|
72
|
+
<div className={styles["label-row"]}>
|
|
73
|
+
{label != null && <BaseMeter.Label className={styles.label}>{label}</BaseMeter.Label>}
|
|
74
|
+
{showValue && <BaseMeter.Value className={styles.value} />}
|
|
75
|
+
</div>
|
|
76
|
+
)}
|
|
77
|
+
<BaseMeter.Track className={`${styles.track} ${styles[`track-${size}`]}`}>
|
|
78
|
+
<BaseMeter.Indicator className={styles.indicator} data-meter-state={state} />
|
|
79
|
+
{showTicks && lowPct != null && <div className={styles.tick} style={{ left: `${lowPct}%` }} />}
|
|
80
|
+
{showTicks && highPct != null && <div className={styles.tick} style={{ left: `${highPct}%` }} />}
|
|
81
|
+
</BaseMeter.Track>
|
|
82
|
+
</BaseMeter.Root>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export { styles as MeterStyles };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derives the semantic zone from value + thresholds,
|
|
3
|
+
* following the HTML <meter> spec colouring algorithm.
|
|
4
|
+
*
|
|
5
|
+
* Returns undefined when no thresholds are defined (plain accent colour).
|
|
6
|
+
*/
|
|
7
|
+
export function getMeterState(
|
|
8
|
+
value: number,
|
|
9
|
+
min: number,
|
|
10
|
+
max: number,
|
|
11
|
+
low: number | undefined,
|
|
12
|
+
high: number | undefined,
|
|
13
|
+
optimum: "low" | "mid" | "high",
|
|
14
|
+
): "optimum" | "suboptimal" | "critical" | undefined {
|
|
15
|
+
if (low === undefined && high === undefined) return undefined;
|
|
16
|
+
|
|
17
|
+
const effectiveLow = low ?? min;
|
|
18
|
+
const effectiveHigh = high ?? max;
|
|
19
|
+
|
|
20
|
+
const zone: "low" | "mid" | "high" = value <= effectiveLow ? "low" : value >= effectiveHigh ? "high" : "mid";
|
|
21
|
+
|
|
22
|
+
if (zone === optimum) return "optimum";
|
|
23
|
+
|
|
24
|
+
// "mid" optimum → no critical zone, extremes are equally suboptimal
|
|
25
|
+
if (optimum === "mid") return "suboptimal";
|
|
26
|
+
|
|
27
|
+
// "low" or "high" optimum → adjacent zone = suboptimal, far end = critical
|
|
28
|
+
return zone === "mid" ? "suboptimal" : "critical";
|
|
29
|
+
}
|