@plexui/ui 0.2.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/components/Field/Field.js +34 -0
- package/dist/es/components/Field/Field.js.map +1 -0
- package/dist/es/components/Field/Field.module.css +97 -0
- package/dist/es/components/Field/index.js +2 -0
- package/dist/es/components/Field/index.js.map +1 -0
- package/dist/es/components/SegmentedControl/index.js +1 -1
- package/dist/es/components/SegmentedControl/index.js.map +1 -1
- package/dist/es/components/Slider/Slider.js +68 -37
- package/dist/es/components/Slider/Slider.js.map +1 -1
- package/dist/es/components/Slider/Slider.module.css +36 -3
- package/dist/es/components/Tabs/Tabs.js +153 -0
- package/dist/es/components/Tabs/Tabs.js.map +1 -0
- package/dist/es/components/{SegmentedControl/SegmentedControl.module.css → Tabs/Tabs.module.css} +232 -98
- package/dist/es/components/Tabs/index.js +2 -0
- package/dist/es/components/Tabs/index.js.map +1 -0
- package/dist/es/styles/variables-components.css +12 -0
- package/dist/types/components/Field/Field.d.ts +58 -0
- package/dist/types/components/Field/index.d.ts +1 -0
- package/dist/types/components/SegmentedControl/index.d.ts +2 -2
- package/dist/types/components/Slider/Slider.d.ts +47 -18
- package/dist/types/components/{SegmentedControl/SegmentedControl.d.ts → Tabs/Tabs.d.ts} +30 -14
- package/dist/types/components/Tabs/index.d.ts +2 -0
- package/package.json +1 -1
- package/dist/es/components/SegmentedControl/SegmentedControl.js +0 -118
- package/dist/es/components/SegmentedControl/SegmentedControl.js.map +0 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { ControlSize } from "../../types";
|
|
2
|
+
export type FieldChildProps = {
|
|
3
|
+
id: string;
|
|
4
|
+
"aria-describedby"?: string;
|
|
5
|
+
"aria-invalid"?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type FieldProps = {
|
|
8
|
+
/**
|
|
9
|
+
* Label text for the field
|
|
10
|
+
*/
|
|
11
|
+
label: React.ReactNode;
|
|
12
|
+
/**
|
|
13
|
+
* Helper/description text displayed below the label.
|
|
14
|
+
* Automatically linked via aria-describedby.
|
|
15
|
+
*/
|
|
16
|
+
description?: React.ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Error message displayed below the control.
|
|
19
|
+
* When provided, the child control receives aria-invalid="true".
|
|
20
|
+
* Uses the existing FieldError component internally.
|
|
21
|
+
*/
|
|
22
|
+
errorMessage?: React.ReactNode;
|
|
23
|
+
/**
|
|
24
|
+
* Controls the font size of the label to visually match the child control's size.
|
|
25
|
+
* Matches the ControlSize scale used by Input, Select, etc.
|
|
26
|
+
* @default "md"
|
|
27
|
+
*/
|
|
28
|
+
size?: ControlSize;
|
|
29
|
+
/**
|
|
30
|
+
* Display a required indicator (asterisk) after the label.
|
|
31
|
+
* This is purely visual — it does not add the `required` HTML attribute.
|
|
32
|
+
* @default false
|
|
33
|
+
*/
|
|
34
|
+
required?: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Layout direction of label and control.
|
|
37
|
+
* - "vertical": label stacked above control (default)
|
|
38
|
+
* - "horizontal": label beside control
|
|
39
|
+
* @default "vertical"
|
|
40
|
+
*/
|
|
41
|
+
orientation?: "vertical" | "horizontal";
|
|
42
|
+
/**
|
|
43
|
+
* Allows overriding the auto-generated `id`. When provided, this becomes
|
|
44
|
+
* the id set on the child control and the `htmlFor` on the label.
|
|
45
|
+
*/
|
|
46
|
+
id?: string;
|
|
47
|
+
/**
|
|
48
|
+
* CSS class applied to the root wrapper
|
|
49
|
+
*/
|
|
50
|
+
className?: string;
|
|
51
|
+
/**
|
|
52
|
+
* The form control(s) to render.
|
|
53
|
+
* - If a single ReactElement, Field clones it with { id, aria-describedby, aria-invalid }.
|
|
54
|
+
* - If a function (render prop), it is called with { id, "aria-describedby", "aria-invalid" }.
|
|
55
|
+
*/
|
|
56
|
+
children: React.ReactElement | ((fieldProps: FieldChildProps) => React.ReactNode);
|
|
57
|
+
};
|
|
58
|
+
export declare function Field({ label, description, errorMessage, size, required, orientation, id: idProp, className, children, }: FieldProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Field, type FieldProps, type FieldChildProps } from "./Field";
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { SegmentedControl } from "
|
|
2
|
-
export type { SegmentedControlBadgeProp, SegmentedControlOptionProps, SegmentedControlProps, SizeVariant, } from "
|
|
1
|
+
export { Tabs as SegmentedControl } from "../Tabs";
|
|
2
|
+
export type { TabsBadgeProp as SegmentedControlBadgeProp, TabProps as SegmentedControlOptionProps, TabsProps as SegmentedControlProps, SizeVariant, } from "../Tabs";
|
|
@@ -4,11 +4,7 @@ export type SliderMark = {
|
|
|
4
4
|
value: number;
|
|
5
5
|
label: string;
|
|
6
6
|
};
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The current value of the slider
|
|
10
|
-
*/
|
|
11
|
-
value: number;
|
|
7
|
+
type SliderBaseProps = {
|
|
12
8
|
/**
|
|
13
9
|
* The minimum value the slider can have
|
|
14
10
|
*/
|
|
@@ -21,15 +17,6 @@ export type SliderProps = {
|
|
|
21
17
|
* The step increment between slider values
|
|
22
18
|
*/
|
|
23
19
|
step: number;
|
|
24
|
-
/**
|
|
25
|
-
* Value that will be offered as a "reset to default" option
|
|
26
|
-
*/
|
|
27
|
-
resetValue?: number;
|
|
28
|
-
/**
|
|
29
|
-
* String that will be displayed in the tooltip
|
|
30
|
-
* @default Reset to default
|
|
31
|
-
*/
|
|
32
|
-
resetTooltip?: string;
|
|
33
20
|
/**
|
|
34
21
|
* Unit to display next to the slider value (e.g., ms, px)
|
|
35
22
|
*/
|
|
@@ -57,13 +44,55 @@ export type SliderProps = {
|
|
|
57
44
|
className?: string;
|
|
58
45
|
disabled?: boolean;
|
|
59
46
|
/**
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
* @param value - The new value of the slider.
|
|
47
|
+
* Orientation of the slider
|
|
48
|
+
* @default "horizontal"
|
|
63
49
|
*/
|
|
64
|
-
|
|
50
|
+
orientation?: "horizontal" | "vertical";
|
|
65
51
|
onBlur?: FocusEventHandler<HTMLInputElement>;
|
|
66
52
|
onFocus?: FocusEventHandler<HTMLInputElement>;
|
|
67
53
|
ref?: React.Ref<ElementRef<typeof RadixSlider.Root> | null>;
|
|
68
54
|
};
|
|
55
|
+
type SingleSliderProps = SliderBaseProps & {
|
|
56
|
+
/**
|
|
57
|
+
* When false or omitted, the slider has a single thumb
|
|
58
|
+
*/
|
|
59
|
+
range?: false;
|
|
60
|
+
/**
|
|
61
|
+
* The current value of the slider
|
|
62
|
+
*/
|
|
63
|
+
value: number;
|
|
64
|
+
/**
|
|
65
|
+
* Callback function invoked when the slider value changes.
|
|
66
|
+
*/
|
|
67
|
+
onChange: (value: number) => void;
|
|
68
|
+
/**
|
|
69
|
+
* Value that will be offered as a "reset to default" option
|
|
70
|
+
*/
|
|
71
|
+
resetValue?: number;
|
|
72
|
+
/**
|
|
73
|
+
* String that will be displayed in the tooltip
|
|
74
|
+
* @default Reset to default
|
|
75
|
+
*/
|
|
76
|
+
resetTooltip?: string;
|
|
77
|
+
};
|
|
78
|
+
type RangeSliderProps = SliderBaseProps & {
|
|
79
|
+
/**
|
|
80
|
+
* When true, the slider supports multiple thumbs
|
|
81
|
+
*/
|
|
82
|
+
range: true;
|
|
83
|
+
/**
|
|
84
|
+
* Array of values, one per thumb
|
|
85
|
+
*/
|
|
86
|
+
value: number[];
|
|
87
|
+
/**
|
|
88
|
+
* Callback function invoked when slider values change.
|
|
89
|
+
*/
|
|
90
|
+
onChange: (value: number[]) => void;
|
|
91
|
+
/**
|
|
92
|
+
* Minimum number of steps between thumbs
|
|
93
|
+
*/
|
|
94
|
+
minStepsBetweenThumbs?: number;
|
|
95
|
+
};
|
|
96
|
+
export type SliderProps = SingleSliderProps | RangeSliderProps;
|
|
69
97
|
export declare const Slider: import("react").MemoExoticComponent<(props: SliderProps) => import("react/jsx-runtime").JSX.Element>;
|
|
98
|
+
export {};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { type ControlSize, type SemanticColors, type Sizes, type Variants } from "../../types";
|
|
2
2
|
export type SizeVariant = "2xs" | "xs" | "sm" | "md" | "lg" | "xl";
|
|
3
|
-
export type
|
|
3
|
+
export type TabsVariant = "segmented" | "underline";
|
|
4
|
+
export type TabsOrientation = "horizontal" | "vertical";
|
|
5
|
+
export type TabsProps<T extends string> = {
|
|
4
6
|
/**
|
|
5
7
|
* Controlled value for the group
|
|
6
8
|
*/
|
|
@@ -14,7 +16,19 @@ export type SegmentedControlProps<T extends string> = {
|
|
|
14
16
|
*/
|
|
15
17
|
"aria-label": string;
|
|
16
18
|
/**
|
|
17
|
-
*
|
|
19
|
+
* Visual variant of the tab group
|
|
20
|
+
* - `"segmented"` — background container with sliding highlight (default)
|
|
21
|
+
* - `"underline"` — no background, animated line indicator under active tab
|
|
22
|
+
* @default "segmented"
|
|
23
|
+
*/
|
|
24
|
+
"variant"?: TabsVariant;
|
|
25
|
+
/**
|
|
26
|
+
* Orientation of the tab layout
|
|
27
|
+
* @default "horizontal"
|
|
28
|
+
*/
|
|
29
|
+
"orientation"?: TabsOrientation;
|
|
30
|
+
/**
|
|
31
|
+
* Controls the size of the tabs
|
|
18
32
|
*
|
|
19
33
|
* | 3xs | 2xs | xs | sm | md | lg | xl | 2xl | 3xl |
|
|
20
34
|
* | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |
|
|
@@ -39,38 +53,40 @@ export type SegmentedControlProps<T extends string> = {
|
|
|
39
53
|
*/
|
|
40
54
|
"block"?: boolean;
|
|
41
55
|
/**
|
|
42
|
-
* Determines if the
|
|
56
|
+
* Determines if the tabs should be a fully rounded pill shape.
|
|
57
|
+
* Only applies to the `"segmented"` variant.
|
|
43
58
|
* @default true
|
|
44
59
|
*/
|
|
45
60
|
"pill"?: boolean;
|
|
46
61
|
"className"?: string;
|
|
47
62
|
"children": React.ReactNode;
|
|
48
63
|
};
|
|
49
|
-
export declare const
|
|
50
|
-
<T extends string>({ value, onChange, children, block, pill, size, gutterSize, className, onClick, ...restProps }:
|
|
51
|
-
|
|
64
|
+
export declare const Tabs: {
|
|
65
|
+
<T extends string>({ value, onChange, children, variant, orientation, block, pill, size, gutterSize, className, onClick, ...restProps }: TabsProps<T>): import("react/jsx-runtime").JSX.Element;
|
|
66
|
+
Tab: ({ children, icon, badge, ...restProps }: TabProps) => import("react/jsx-runtime").JSX.Element;
|
|
67
|
+
Option: ({ children, icon, badge, ...restProps }: TabProps) => import("react/jsx-runtime").JSX.Element;
|
|
52
68
|
};
|
|
53
69
|
/**
|
|
54
|
-
* Badge configuration for
|
|
70
|
+
* Badge configuration for Tabs.Tab
|
|
55
71
|
*/
|
|
56
|
-
export type
|
|
72
|
+
export type TabsBadgeProp = React.ReactNode | {
|
|
57
73
|
content: React.ReactNode;
|
|
58
74
|
color?: SemanticColors<"secondary" | "success" | "danger" | "warning" | "info" | "discovery" | "caution">;
|
|
59
75
|
variant?: Variants<"soft" | "solid">;
|
|
60
76
|
pill?: boolean;
|
|
61
77
|
loading?: boolean;
|
|
62
78
|
};
|
|
63
|
-
export type
|
|
79
|
+
export type TabProps = {
|
|
64
80
|
/**
|
|
65
|
-
*
|
|
81
|
+
* Tab value
|
|
66
82
|
*/
|
|
67
83
|
"value": string;
|
|
68
84
|
/**
|
|
69
|
-
* Text read aloud to screen readers when the
|
|
85
|
+
* Text read aloud to screen readers when the tab is focused
|
|
70
86
|
*/
|
|
71
87
|
"aria-label"?: string;
|
|
72
88
|
/**
|
|
73
|
-
* Text content to render in the
|
|
89
|
+
* Text content to render in the tab
|
|
74
90
|
*/
|
|
75
91
|
"children"?: React.ReactNode;
|
|
76
92
|
/**
|
|
@@ -83,9 +99,9 @@ export type SegmentedControlOptionProps = {
|
|
|
83
99
|
* @example badge={5}
|
|
84
100
|
* @example badge={{ content: 5, color: "danger" }}
|
|
85
101
|
*/
|
|
86
|
-
"badge"?:
|
|
102
|
+
"badge"?: TabsBadgeProp;
|
|
87
103
|
/**
|
|
88
|
-
* Disable the individual
|
|
104
|
+
* Disable the individual tab
|
|
89
105
|
*/
|
|
90
106
|
"disabled"?: boolean;
|
|
91
107
|
};
|
package/package.json
CHANGED
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import clsx from "clsx";
|
|
4
|
-
import { ToggleGroup } from "radix-ui";
|
|
5
|
-
import { useCallback, useLayoutEffect, useRef } from "react";
|
|
6
|
-
import { useResizeObserver } from "usehooks-ts";
|
|
7
|
-
import { handlePressableMouseEnter, waitForAnimationFrame } from "../../lib/helpers";
|
|
8
|
-
import {} from "../../types";
|
|
9
|
-
import { LoadingIndicator } from "../Indicator";
|
|
10
|
-
import s from "./SegmentedControl.module.css";
|
|
11
|
-
export const SegmentedControl = ({ value, onChange, children, block, pill = true, size = "md", gutterSize, className, onClick, ...restProps }) => {
|
|
12
|
-
const rootRef = useRef(null);
|
|
13
|
-
const thumbRef = useRef(null);
|
|
14
|
-
const prevSizeRef = useRef(size);
|
|
15
|
-
const applyThumbSizing = useCallback((attemptScroll) => {
|
|
16
|
-
const root = rootRef.current;
|
|
17
|
-
const thumb = thumbRef.current;
|
|
18
|
-
if (!root || !thumb) {
|
|
19
|
-
return;
|
|
20
|
-
}
|
|
21
|
-
// Get selected node
|
|
22
|
-
const activeNode = root?.querySelector('[data-state="on"]');
|
|
23
|
-
// Impossible
|
|
24
|
-
if (!activeNode) {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
const rootWidth = root.clientWidth;
|
|
28
|
-
let targetWidth = Math.floor(activeNode.clientWidth);
|
|
29
|
-
const targetOffset = activeNode.offsetLeft;
|
|
30
|
-
// Detect if the thumb is moving too far to the edge of the container.
|
|
31
|
-
// This would most commonly be due to subpixel widths adding up to excessive distance.
|
|
32
|
-
if (rootWidth - (targetWidth + targetOffset) < 2) {
|
|
33
|
-
targetWidth = targetWidth - 1;
|
|
34
|
-
}
|
|
35
|
-
thumb.style.width = `${Math.floor(targetWidth)}px`;
|
|
36
|
-
thumb.style.transform = `translateX(${targetOffset}px)`;
|
|
37
|
-
// If the control is scrollable, ensure the active option is visible
|
|
38
|
-
if (root.scrollWidth > rootWidth) {
|
|
39
|
-
// Only scroll items near the edge, but not the inner 2/3.
|
|
40
|
-
const buffer = rootWidth * 0.15;
|
|
41
|
-
const scrollLeft = root.scrollLeft;
|
|
42
|
-
const left = activeNode.offsetLeft;
|
|
43
|
-
const right = left + targetWidth;
|
|
44
|
-
if (left < scrollLeft + buffer || right > scrollLeft + rootWidth - buffer) {
|
|
45
|
-
// Cheap trick to avoid unintentional scroll on mount - transition is set after mounting
|
|
46
|
-
if (attemptScroll) {
|
|
47
|
-
activeNode.scrollIntoView({ block: "nearest", inline: "center", behavior: "smooth" });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}, []);
|
|
52
|
-
useResizeObserver({
|
|
53
|
-
// @ts-expect-error(2322) -- bug in types: https://github.com/juliencrn/usehooks-ts/issues/663
|
|
54
|
-
ref: rootRef,
|
|
55
|
-
onResize: () => {
|
|
56
|
-
const thumb = thumbRef.current;
|
|
57
|
-
if (!thumb) {
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
// Perform the size update instantly
|
|
61
|
-
const currentTransition = thumb.style.transition;
|
|
62
|
-
thumb.style.transition = "";
|
|
63
|
-
applyThumbSizing(false);
|
|
64
|
-
thumb.style.transition = currentTransition;
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
useLayoutEffect(() => {
|
|
68
|
-
const root = rootRef.current;
|
|
69
|
-
const thumb = thumbRef.current;
|
|
70
|
-
if (!root || !thumb) {
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
const sizeChanged = prevSizeRef.current !== size;
|
|
74
|
-
prevSizeRef.current = size;
|
|
75
|
-
if (sizeChanged) {
|
|
76
|
-
// Size changed - disable transition, wait for CSS, then apply sizing
|
|
77
|
-
const currentTransition = thumb.style.transition;
|
|
78
|
-
thumb.style.transition = "";
|
|
79
|
-
waitForAnimationFrame(() => {
|
|
80
|
-
applyThumbSizing(false);
|
|
81
|
-
waitForAnimationFrame(() => {
|
|
82
|
-
thumb.style.transition = currentTransition;
|
|
83
|
-
});
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
// Normal update (value change, etc.)
|
|
88
|
-
waitForAnimationFrame(() => {
|
|
89
|
-
applyThumbSizing(!!thumb.style.transition);
|
|
90
|
-
// Apply transition after initial calculation is set
|
|
91
|
-
if (!thumb.style.transition) {
|
|
92
|
-
waitForAnimationFrame(() => {
|
|
93
|
-
thumb.style.transition =
|
|
94
|
-
"width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)";
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
}, [applyThumbSizing, value, size, gutterSize, pill]);
|
|
100
|
-
const handleValueChange = (nextValue) => {
|
|
101
|
-
// Only trigger onChange when a value exists
|
|
102
|
-
// Disallow toggling off enabled items
|
|
103
|
-
if (nextValue && onChange)
|
|
104
|
-
onChange(nextValue);
|
|
105
|
-
};
|
|
106
|
-
return (_jsxs(ToggleGroup.Root, { ref: rootRef, className: clsx(s.SegmentedControl, className), type: "single", value: value, loop: false, onValueChange: handleValueChange, onClick: onClick, "data-block": block ? "" : undefined, "data-pill": pill ? "" : undefined, "data-size": size, "data-gutter-size": gutterSize, ...restProps, children: [_jsx("div", { className: s.SegmentedControlThumb, ref: thumbRef }), children] }));
|
|
107
|
-
};
|
|
108
|
-
// Type guard for badge object form
|
|
109
|
-
const isBadgeObject = (badge) => {
|
|
110
|
-
return badge != null && typeof badge === "object" && "content" in badge;
|
|
111
|
-
};
|
|
112
|
-
const Segment = ({ children, icon, badge, ...restProps }) => {
|
|
113
|
-
// Normalize badge prop
|
|
114
|
-
const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null;
|
|
115
|
-
return (_jsx(ToggleGroup.Item, { className: s.SegmentedControlOption, ...restProps, onPointerEnter: handlePressableMouseEnter, children: _jsxs("span", { className: s.SegmentedControlOptionContent, children: [icon, children && _jsx("span", { children: children }), badgeProps && (_jsx("span", { className: s.OptionBadge, "data-color": badgeProps.color ?? "secondary", "data-variant": badgeProps.variant ?? "soft", "data-pill": badgeProps.pill ? "" : undefined, children: badgeProps.loading ? _jsx(LoadingIndicator, {}) : badgeProps.content }))] }) }));
|
|
116
|
-
};
|
|
117
|
-
SegmentedControl.Option = Segment;
|
|
118
|
-
//# sourceMappingURL=SegmentedControl.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SegmentedControl.js","sourceRoot":"","sources":["../../../../src/components/SegmentedControl/SegmentedControl.tsx"],"names":[],"mappings":"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","sourcesContent":["\"use client\"\n\nimport clsx from \"clsx\"\nimport { ToggleGroup } from \"radix-ui\"\nimport { useCallback, useLayoutEffect, useRef } from \"react\"\nimport { useResizeObserver } from \"usehooks-ts\"\nimport { handlePressableMouseEnter, waitForAnimationFrame } from \"../../lib/helpers\"\nimport { type ControlSize, type SemanticColors, type Sizes, type Variants } from \"../../types\"\nimport { LoadingIndicator } from \"../Indicator\"\nimport s from \"./SegmentedControl.module.css\"\n\nexport type SizeVariant = \"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\"\n\nexport type SegmentedControlProps<T extends string> = {\n /**\n * Controlled value for the group\n */\n \"value\": T\n /** Callback for when a new value is selected */\n \"onChange\"?: (nextValue: T) => void\n /** Callback any time the control is clicked (even if a new value was not selected) */\n \"onClick\"?: () => void\n /**\n * Text read aloud to screen readers when the control is focused\n */\n \"aria-label\": string\n /**\n * Controls the size of the segmented control\n *\n * | 3xs | 2xs | xs | sm | md | lg | xl | 2xl | 3xl |\n * | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- | ------- |\n * | `22px` | `24px` | `26px` | `28px` | `32px` | `36px` | `40px` | `44px` | `48px` |\n *\n * @default md\n */\n \"size\"?: ControlSize\n /**\n * Controls gutter on the edges of the button, defaults to value from `size`.\n *\n * | 2xs | xs | sm | md | lg | xl |\n * | ------ | ------ | ------ | ------ | ------ | ------ |\n * | `6px` | `8px` | `10px` | `12px` | `14px` | `16px` |\n */\n \"gutterSize\"?: Sizes<\"2xs\" | \"xs\" | \"sm\" | \"md\" | \"lg\" | \"xl\">\n /** Disable the entire group */\n \"disabled\"?: boolean\n /**\n * Display the control as a block element with equal width segments\n * @default false\n */\n \"block\"?: boolean\n /**\n * Determines if the segment control, and its options, should be a fully rounded pill shape.\n * @default true\n */\n \"pill\"?: boolean\n \"className\"?: string\n \"children\": React.ReactNode\n}\n\nexport const SegmentedControl = <T extends string>({\n value,\n onChange,\n children,\n block,\n pill = true,\n size = \"md\",\n gutterSize,\n className,\n onClick,\n ...restProps\n}: SegmentedControlProps<T>) => {\n const rootRef = useRef<HTMLDivElement>(null)\n const thumbRef = useRef<HTMLDivElement>(null)\n const prevSizeRef = useRef(size)\n\n const applyThumbSizing = useCallback((attemptScroll: boolean) => {\n const root = rootRef.current\n const thumb = thumbRef.current\n\n if (!root || !thumb) {\n return\n }\n\n // Get selected node\n const activeNode = root?.querySelector<HTMLDivElement>('[data-state=\"on\"]')\n\n // Impossible\n if (!activeNode) {\n return\n }\n\n const rootWidth = root.clientWidth\n let targetWidth = Math.floor(activeNode.clientWidth)\n const targetOffset = activeNode.offsetLeft\n\n // Detect if the thumb is moving too far to the edge of the container.\n // This would most commonly be due to subpixel widths adding up to excessive distance.\n if (rootWidth - (targetWidth + targetOffset) < 2) {\n targetWidth = targetWidth - 1\n }\n\n thumb.style.width = `${Math.floor(targetWidth)}px`\n thumb.style.transform = `translateX(${targetOffset}px)`\n\n // If the control is scrollable, ensure the active option is visible\n if (root.scrollWidth > rootWidth) {\n // Only scroll items near the edge, but not the inner 2/3.\n const buffer = rootWidth * 0.15\n const scrollLeft = root.scrollLeft\n const left = activeNode.offsetLeft\n const right = left + targetWidth\n if (left < scrollLeft + buffer || right > scrollLeft + rootWidth - buffer) {\n // Cheap trick to avoid unintentional scroll on mount - transition is set after mounting\n if (attemptScroll) {\n activeNode.scrollIntoView({ block: \"nearest\", inline: \"center\", behavior: \"smooth\" })\n }\n }\n }\n }, [])\n\n useResizeObserver({\n // @ts-expect-error(2322) -- bug in types: https://github.com/juliencrn/usehooks-ts/issues/663\n ref: rootRef,\n onResize: () => {\n const thumb = thumbRef.current\n\n if (!thumb) {\n return\n }\n\n // Perform the size update instantly\n const currentTransition = thumb.style.transition\n thumb.style.transition = \"\"\n applyThumbSizing(false)\n thumb.style.transition = currentTransition\n },\n })\n\n useLayoutEffect(() => {\n const root = rootRef.current\n const thumb = thumbRef.current\n\n if (!root || !thumb) {\n return\n }\n\n const sizeChanged = prevSizeRef.current !== size\n prevSizeRef.current = size\n\n if (sizeChanged) {\n // Size changed - disable transition, wait for CSS, then apply sizing\n const currentTransition = thumb.style.transition\n thumb.style.transition = \"\"\n\n waitForAnimationFrame(() => {\n applyThumbSizing(false)\n waitForAnimationFrame(() => {\n thumb.style.transition = currentTransition\n })\n })\n } else {\n // Normal update (value change, etc.)\n waitForAnimationFrame(() => {\n applyThumbSizing(!!thumb.style.transition)\n\n // Apply transition after initial calculation is set\n if (!thumb.style.transition) {\n waitForAnimationFrame(() => {\n thumb.style.transition =\n \"width 300ms var(--cubic-enter), transform 300ms var(--cubic-enter)\"\n })\n }\n })\n }\n }, [applyThumbSizing, value, size, gutterSize, pill])\n\n const handleValueChange = (nextValue: T) => {\n // Only trigger onChange when a value exists\n // Disallow toggling off enabled items\n if (nextValue && onChange) onChange(nextValue)\n }\n\n return (\n <ToggleGroup.Root\n ref={rootRef}\n className={clsx(s.SegmentedControl, className)}\n type=\"single\"\n value={value}\n loop={false}\n onValueChange={handleValueChange}\n onClick={onClick}\n data-block={block ? \"\" : undefined}\n data-pill={pill ? \"\" : undefined}\n data-size={size}\n data-gutter-size={gutterSize}\n {...restProps}\n >\n <div className={s.SegmentedControlThumb} ref={thumbRef} />\n {children}\n </ToggleGroup.Root>\n )\n}\n\n/**\n * Badge configuration for SegmentedControl.Option\n */\nexport type SegmentedControlBadgeProp =\n | React.ReactNode\n | {\n content: React.ReactNode\n color?: SemanticColors<\n \"secondary\" | \"success\" | \"danger\" | \"warning\" | \"info\" | \"discovery\" | \"caution\"\n >\n variant?: Variants<\"soft\" | \"solid\">\n pill?: boolean\n loading?: boolean\n }\n\nexport type SegmentedControlOptionProps = {\n /**\n * Option value\n */\n \"value\": string\n /**\n * Text read aloud to screen readers when the option is focused\n */\n \"aria-label\"?: string\n /**\n * Text content to render in the option\n */\n \"children\"?: React.ReactNode\n /**\n * Icon to render before the text content\n */\n \"icon\"?: React.ReactNode\n /**\n * Badge to render after the text content.\n * Can be a simple value or an object with content, color, variant, and loading options.\n * @example badge={5}\n * @example badge={{ content: 5, color: \"danger\" }}\n */\n \"badge\"?: SegmentedControlBadgeProp\n /**\n * Disable the individual option\n */\n \"disabled\"?: boolean\n}\n\n// Type guard for badge object form\nconst isBadgeObject = (\n badge: SegmentedControlBadgeProp,\n): badge is Exclude<SegmentedControlBadgeProp, React.ReactNode> & { content: React.ReactNode } => {\n return badge != null && typeof badge === \"object\" && \"content\" in badge\n}\n\nconst Segment = ({ children, icon, badge, ...restProps }: SegmentedControlOptionProps) => {\n // Normalize badge prop\n const badgeProps = badge != null ? (isBadgeObject(badge) ? badge : { content: badge }) : null\n\n return (\n <ToggleGroup.Item\n className={s.SegmentedControlOption}\n {...restProps}\n onPointerEnter={handlePressableMouseEnter}\n >\n <span className={s.SegmentedControlOptionContent}>\n {icon}\n {children && <span>{children}</span>}\n {badgeProps && (\n <span\n className={s.OptionBadge}\n data-color={badgeProps.color ?? \"secondary\"}\n data-variant={badgeProps.variant ?? \"soft\"}\n data-pill={badgeProps.pill ? \"\" : undefined}\n >\n {badgeProps.loading ? <LoadingIndicator /> : badgeProps.content}\n </span>\n )}\n </span>\n </ToggleGroup.Item>\n )\n}\n\nSegmentedControl.Option = Segment\n"]}
|