@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.
@@ -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 "./SegmentedControl";
2
- export type { SegmentedControlBadgeProp, SegmentedControlOptionProps, SegmentedControlProps, SizeVariant, } from "./SegmentedControl";
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
- export type SliderProps = {
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
- * Callback function invoked when the slider value changes.
61
- *
62
- * @param value - The new value of the slider.
47
+ * Orientation of the slider
48
+ * @default "horizontal"
63
49
  */
64
- onChange: (value: number) => void;
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 SegmentedControlProps<T extends string> = {
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
- * Controls the size of the segmented control
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 segment control, and its options, should be a fully rounded pill shape.
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 SegmentedControl: {
50
- <T extends string>({ value, onChange, children, block, pill, size, gutterSize, className, onClick, ...restProps }: SegmentedControlProps<T>): import("react/jsx-runtime").JSX.Element;
51
- Option: ({ children, icon, badge, ...restProps }: SegmentedControlOptionProps) => import("react/jsx-runtime").JSX.Element;
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 SegmentedControl.Option
70
+ * Badge configuration for Tabs.Tab
55
71
  */
56
- export type SegmentedControlBadgeProp = React.ReactNode | {
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 SegmentedControlOptionProps = {
79
+ export type TabProps = {
64
80
  /**
65
- * Option value
81
+ * Tab value
66
82
  */
67
83
  "value": string;
68
84
  /**
69
- * Text read aloud to screen readers when the option is focused
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 option
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"?: SegmentedControlBadgeProp;
102
+ "badge"?: TabsBadgeProp;
87
103
  /**
88
- * Disable the individual option
104
+ * Disable the individual tab
89
105
  */
90
106
  "disabled"?: boolean;
91
107
  };
@@ -0,0 +1,2 @@
1
+ export { Tabs } from "./Tabs";
2
+ export type { TabsBadgeProp, TabsOrientation, TabsProps, TabsVariant, TabProps, SizeVariant, } from "./Tabs";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexui/ui",
3
- "version": "0.2.1",
3
+ "version": "0.4.0",
4
4
  "description": "Modern design system for building high-quality applications",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -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"]}