@varialkit/segmentcontrol 0.1.1
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/docs.md +88 -0
- package/examples.tsx +218 -0
- package/package.json +26 -0
- package/src/SegmentControl.scss +213 -0
- package/src/SegmentControl.tsx +297 -0
- package/src/SegmentControl.types.ts +82 -0
- package/src/index.ts +2 -0
package/docs.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# SegmentControl
|
|
2
|
+
|
|
3
|
+
SegmentControl lets users switch between related views or data sets with a single, compact control.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { SegmentControl } from "@solara/segmentcontrol";
|
|
9
|
+
|
|
10
|
+
export function ViewSwitcher() {
|
|
11
|
+
const [value, setValue] = React.useState("overview");
|
|
12
|
+
|
|
13
|
+
return (
|
|
14
|
+
<SegmentControl value={value} onChange={setValue} size="medium">
|
|
15
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
16
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
17
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
18
|
+
</SegmentControl>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Notes
|
|
24
|
+
|
|
25
|
+
- Provide an `ariaLabel` when using icon-only segments.
|
|
26
|
+
- Use `fullWidth` when the control should span its container.
|
|
27
|
+
- Prefer 2-5 segments to keep labels readable.
|
|
28
|
+
- By default, segments size to their content for stable label alignment; `fullWidth` stretches them evenly.
|
|
29
|
+
|
|
30
|
+
## Segment Props
|
|
31
|
+
|
|
32
|
+
Each `SegmentControl.Segment` accepts the following props:
|
|
33
|
+
|
|
34
|
+
| Prop | Type | Description |
|
|
35
|
+
| --- | --- | --- |
|
|
36
|
+
| `value` | `string` | Segment value used for selection. |
|
|
37
|
+
| `icon` | `ReactNode` | Optional leading icon rendered before the label. |
|
|
38
|
+
| `ariaLabel` | `string` | Required when the segment has no visible label. |
|
|
39
|
+
| `disabled` | `boolean` | Disable the segment. |
|
|
40
|
+
|
|
41
|
+
### Icons
|
|
42
|
+
|
|
43
|
+
Use the `icon` prop with an icon name or full `IconProps`. The SegmentControl renders
|
|
44
|
+
the `Icon` component internally for consistent sizing and theming. Icons inherit the
|
|
45
|
+
segment text color via `currentColor`.
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
<SegmentControl value={value} onChange={setValue}>
|
|
49
|
+
<SegmentControl.Segment value="overview" icon="data_spreadsheet_search_24">
|
|
50
|
+
Overview
|
|
51
|
+
</SegmentControl.Segment>
|
|
52
|
+
<SegmentControl.Segment value="activity" icon="arrow_line_up_16">
|
|
53
|
+
Activity
|
|
54
|
+
</SegmentControl.Segment>
|
|
55
|
+
</SegmentControl>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Icon-Only Segments
|
|
59
|
+
|
|
60
|
+
Provide `ariaLabel` and pass an `Icon` as the segment content.
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
<SegmentControl value={value} onChange={setValue}>
|
|
64
|
+
<SegmentControl.Segment value="overview" ariaLabel="Overview" icon="data_spreadsheet_search_24" />
|
|
65
|
+
<SegmentControl.Segment value="activity" ariaLabel="Activity" icon="arrow_line_up_16" />
|
|
66
|
+
<SegmentControl.Segment value="settings" ariaLabel="Settings" icon="arrow_swap_16" />
|
|
67
|
+
</SegmentControl>
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Moving Background
|
|
71
|
+
|
|
72
|
+
Set `movingBackground` to render a single animated background that follows hover/focus and then returns to the active segment.
|
|
73
|
+
|
|
74
|
+
- The background is positioned and sized to the hovered segment.
|
|
75
|
+
- On pointer leave or blur, it aligns to the active value.
|
|
76
|
+
- The indicator respects responsive sizing and density changes.
|
|
77
|
+
- Hover and keyboard focus behave the same way for accessibility parity.
|
|
78
|
+
- The active segment still drives selection state and `onChange` behavior.
|
|
79
|
+
|
|
80
|
+
### How It Works
|
|
81
|
+
|
|
82
|
+
`SegmentControl` measures the hovered (or focused) segment and writes its position and size into CSS
|
|
83
|
+
variables. A single indicator element uses those variables to animate between segments, keeping the
|
|
84
|
+
DOM light and avoiding per-segment background transitions.
|
|
85
|
+
|
|
86
|
+
- A `ResizeObserver` updates the indicator when segment sizes change.
|
|
87
|
+
- When no segment is hovered, the indicator re-centers on the active value.
|
|
88
|
+
- If `movingBackground` is `false`, segments use per-button hover and active backgrounds.
|
package/examples.tsx
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { iconNames } from "@solara/icons";
|
|
3
|
+
import type { SolaraIconName } from "@solara/icons";
|
|
4
|
+
import { SegmentControl } from "./src/SegmentControl";
|
|
5
|
+
import type { SegmentControlSize } from "./src/SegmentControl.types";
|
|
6
|
+
|
|
7
|
+
type PlaygroundProps = {
|
|
8
|
+
size: SegmentControlSize;
|
|
9
|
+
fullWidth: boolean;
|
|
10
|
+
withIcons: boolean;
|
|
11
|
+
movingBackground: boolean;
|
|
12
|
+
disabledValue: "none" | "overview" | "activity" | "settings";
|
|
13
|
+
iconName: SolaraIconName;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const SegmentControlPlayground = ({
|
|
17
|
+
size,
|
|
18
|
+
fullWidth,
|
|
19
|
+
withIcons,
|
|
20
|
+
movingBackground,
|
|
21
|
+
disabledValue,
|
|
22
|
+
iconName,
|
|
23
|
+
}: PlaygroundProps) => {
|
|
24
|
+
const [value, setValue] = React.useState("overview");
|
|
25
|
+
|
|
26
|
+
React.useEffect(() => {
|
|
27
|
+
if (disabledValue === "none" || disabledValue !== value) return;
|
|
28
|
+
const fallback = ["overview", "activity", "settings"].find(
|
|
29
|
+
(option) => option !== disabledValue
|
|
30
|
+
);
|
|
31
|
+
if (fallback) setValue(fallback);
|
|
32
|
+
}, [disabledValue, value]);
|
|
33
|
+
|
|
34
|
+
const segments = [
|
|
35
|
+
{ value: "overview", label: "Overview" },
|
|
36
|
+
{ value: "activity", label: "Activity" },
|
|
37
|
+
{ value: "settings", label: "Settings" },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<SegmentControl
|
|
42
|
+
value={value}
|
|
43
|
+
onChange={setValue}
|
|
44
|
+
size={size}
|
|
45
|
+
fullWidth={fullWidth}
|
|
46
|
+
movingBackground={movingBackground}
|
|
47
|
+
>
|
|
48
|
+
{segments.map((segment) => (
|
|
49
|
+
<SegmentControl.Segment
|
|
50
|
+
key={segment.value}
|
|
51
|
+
value={segment.value}
|
|
52
|
+
disabled={disabledValue === segment.value}
|
|
53
|
+
icon={withIcons ? iconName : undefined}
|
|
54
|
+
>
|
|
55
|
+
{segment.label}
|
|
56
|
+
</SegmentControl.Segment>
|
|
57
|
+
))}
|
|
58
|
+
</SegmentControl>
|
|
59
|
+
);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const stories = {
|
|
63
|
+
playground: {
|
|
64
|
+
title: "Playground",
|
|
65
|
+
description: "Explore size, width, and disabled segment states.",
|
|
66
|
+
render: (props: PlaygroundProps) => <SegmentControlPlayground {...props} />,
|
|
67
|
+
controls: [
|
|
68
|
+
{
|
|
69
|
+
name: "size",
|
|
70
|
+
type: "select",
|
|
71
|
+
options: ["small", "medium", "large"],
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "fullWidth",
|
|
75
|
+
type: "boolean",
|
|
76
|
+
label: "Full Width",
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "withIcons",
|
|
80
|
+
type: "boolean",
|
|
81
|
+
label: "With Icons",
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
name: "movingBackground",
|
|
85
|
+
type: "boolean",
|
|
86
|
+
label: "Moving Background",
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "disabledValue",
|
|
90
|
+
type: "select",
|
|
91
|
+
options: ["none", "overview", "activity", "settings"],
|
|
92
|
+
label: "Disabled Segment",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "iconName",
|
|
96
|
+
type: "select",
|
|
97
|
+
options: iconNames,
|
|
98
|
+
label: "Icon Name",
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
initialProps: {
|
|
102
|
+
size: "medium",
|
|
103
|
+
fullWidth: false,
|
|
104
|
+
withIcons: false,
|
|
105
|
+
movingBackground: false,
|
|
106
|
+
disabledValue: "none",
|
|
107
|
+
iconName: (iconNames[0] ?? "data_spreadsheet_search_24") as SolaraIconName,
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
sizes: {
|
|
111
|
+
title: "Sizes",
|
|
112
|
+
description: "Small, medium, and large segment controls.",
|
|
113
|
+
showProps: false,
|
|
114
|
+
render: () => (
|
|
115
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
116
|
+
<SegmentControl value="overview" onChange={() => undefined} size="small">
|
|
117
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
118
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
119
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
120
|
+
</SegmentControl>
|
|
121
|
+
<SegmentControl value="overview" onChange={() => undefined} size="medium">
|
|
122
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
123
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
124
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
125
|
+
</SegmentControl>
|
|
126
|
+
<SegmentControl value="overview" onChange={() => undefined} size="large">
|
|
127
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
128
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
129
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
130
|
+
</SegmentControl>
|
|
131
|
+
</div>
|
|
132
|
+
),
|
|
133
|
+
code: `import { SegmentControl } from "@solara/segmentcontrol";
|
|
134
|
+
|
|
135
|
+
export function Example() {
|
|
136
|
+
return (
|
|
137
|
+
<div style={{ display: "flex", flexDirection: "column", gap: "1rem" }}>
|
|
138
|
+
<SegmentControl value="overview" onChange={() => undefined} size="small">
|
|
139
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
140
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
141
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
142
|
+
</SegmentControl>
|
|
143
|
+
<SegmentControl value="overview" onChange={() => undefined} size="medium">
|
|
144
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
145
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
146
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
147
|
+
</SegmentControl>
|
|
148
|
+
<SegmentControl value="overview" onChange={() => undefined} size="large">
|
|
149
|
+
<SegmentControl.Segment value="overview">Overview</SegmentControl.Segment>
|
|
150
|
+
<SegmentControl.Segment value="activity">Activity</SegmentControl.Segment>
|
|
151
|
+
<SegmentControl.Segment value="settings">Settings</SegmentControl.Segment>
|
|
152
|
+
</SegmentControl>
|
|
153
|
+
</div>
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
`,
|
|
157
|
+
},
|
|
158
|
+
iconOnly: {
|
|
159
|
+
title: "Icon Only",
|
|
160
|
+
description: "Provide aria-labels when using icons without text.",
|
|
161
|
+
showProps: false,
|
|
162
|
+
render: () => (
|
|
163
|
+
<SegmentControl value="overview" onChange={() => undefined}>
|
|
164
|
+
<SegmentControl.Segment value="overview" ariaLabel="Overview" icon="data_spreadsheet_search_24" />
|
|
165
|
+
<SegmentControl.Segment value="activity" ariaLabel="Activity" icon="arrow_line_up_16" />
|
|
166
|
+
<SegmentControl.Segment value="settings" ariaLabel="Settings" icon="arrow_swap_16" />
|
|
167
|
+
</SegmentControl>
|
|
168
|
+
),
|
|
169
|
+
code: `import { SegmentControl } from "@solara/segmentcontrol";
|
|
170
|
+
|
|
171
|
+
export function Example() {
|
|
172
|
+
return (
|
|
173
|
+
<SegmentControl value="overview" onChange={() => undefined}>
|
|
174
|
+
<SegmentControl.Segment value="overview" ariaLabel="Overview" icon="data_spreadsheet_search_24" />
|
|
175
|
+
<SegmentControl.Segment value="activity" ariaLabel="Activity" icon="arrow_line_up_16" />
|
|
176
|
+
<SegmentControl.Segment value="settings" ariaLabel="Settings" icon="arrow_swap_16" />
|
|
177
|
+
</SegmentControl>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
`,
|
|
181
|
+
},
|
|
182
|
+
icons: {
|
|
183
|
+
title: "Icons",
|
|
184
|
+
description: "Use leading icons alongside labels.",
|
|
185
|
+
showProps: false,
|
|
186
|
+
render: () => (
|
|
187
|
+
<SegmentControl value="overview" onChange={() => undefined}>
|
|
188
|
+
<SegmentControl.Segment value="overview" icon="data_spreadsheet_search_24">
|
|
189
|
+
Overview
|
|
190
|
+
</SegmentControl.Segment>
|
|
191
|
+
<SegmentControl.Segment value="activity" icon="arrow_line_up_16">
|
|
192
|
+
Activity
|
|
193
|
+
</SegmentControl.Segment>
|
|
194
|
+
<SegmentControl.Segment value="settings" icon="arrow_swap_16">
|
|
195
|
+
Settings
|
|
196
|
+
</SegmentControl.Segment>
|
|
197
|
+
</SegmentControl>
|
|
198
|
+
),
|
|
199
|
+
code: `import { SegmentControl } from "@solara/segmentcontrol";
|
|
200
|
+
|
|
201
|
+
export function Example() {
|
|
202
|
+
return (
|
|
203
|
+
<SegmentControl value="overview" onChange={() => undefined}>
|
|
204
|
+
<SegmentControl.Segment value="overview" icon="data_spreadsheet_search_24">
|
|
205
|
+
Overview
|
|
206
|
+
</SegmentControl.Segment>
|
|
207
|
+
<SegmentControl.Segment value="activity" icon="arrow_line_up_16">
|
|
208
|
+
Activity
|
|
209
|
+
</SegmentControl.Segment>
|
|
210
|
+
<SegmentControl.Segment value="settings" icon="arrow_swap_16">
|
|
211
|
+
Settings
|
|
212
|
+
</SegmentControl.Segment>
|
|
213
|
+
</SegmentControl>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
`,
|
|
217
|
+
},
|
|
218
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@varialkit/segmentcontrol",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "src/index.ts",
|
|
6
|
+
"types": "src/index.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.ts",
|
|
9
|
+
"./examples": "./examples.tsx"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@varialkit/icons": "0.1.1"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"docs.md",
|
|
17
|
+
"examples.tsx"
|
|
18
|
+
],
|
|
19
|
+
"peerDependencies": {
|
|
20
|
+
"react": "^19.0.0"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/react": "19.0.10",
|
|
24
|
+
"react": "19.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
.solara-segment-control {
|
|
2
|
+
--segment-control-bg: var(--color-surface-100);
|
|
3
|
+
--segment-control-surface-color: var(--color-surface-100);
|
|
4
|
+
--segment-control-surface-color-rgb: var(--color-surface-100-rgb);
|
|
5
|
+
--surface-border-color: var(--color-divider-secondary);
|
|
6
|
+
--surface-border-color-rgb: var(--color-divider-secondary-rgb);
|
|
7
|
+
--surface-opacity: 1;
|
|
8
|
+
--surface-blur: 0px;
|
|
9
|
+
--surface-shadow: var(--elevation-1);
|
|
10
|
+
--segment-control-text: var(--color-text-secondary);
|
|
11
|
+
--segment-control-text-hover: var(--color-text-primary);
|
|
12
|
+
--segment-control-text-active: var(--color-text-primary);
|
|
13
|
+
--segment-control-text-disabled: var(--color-text-secondary);
|
|
14
|
+
--segment-control-bg-active: var(--color-surface-0);
|
|
15
|
+
--segment-control-bg-hover: var(--color-surface-200);
|
|
16
|
+
--segment-control-bg-indicator: var(--segment-control-bg-active);
|
|
17
|
+
--segment-control-shadow: var(--elevation-1);
|
|
18
|
+
--segment-padding-y: var(--space-2);
|
|
19
|
+
--segment-padding-x: var(--space-4);
|
|
20
|
+
--segment-font-size: var(--font-size-body-scaled);
|
|
21
|
+
--segment-line-height: var(--line-height-body-scaled);
|
|
22
|
+
--segment-icon-size: 16px;
|
|
23
|
+
--segment-gap: var(--space-2);
|
|
24
|
+
--segment-height: var(--space-9);
|
|
25
|
+
|
|
26
|
+
position: relative;
|
|
27
|
+
display: inline-flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
gap: calc(var(--space-1) * var(--spacing-multiplier));
|
|
30
|
+
border-radius: var(--radius-pill);
|
|
31
|
+
background-color: rgba(var(--segment-control-surface-color-rgb), var(--surface-opacity));
|
|
32
|
+
padding: calc(var(--space-1) * var(--spacing-multiplier));
|
|
33
|
+
box-shadow: transparent;
|
|
34
|
+
border: 1px solid var(--surface-border-color);
|
|
35
|
+
width: auto;
|
|
36
|
+
height: calc(var(--segment-height) * var(--spacing-multiplier));
|
|
37
|
+
font-family: var(--font-body);
|
|
38
|
+
backdrop-filter: blur(var(--surface-blur));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.solara-segment-control__indicator {
|
|
42
|
+
// The moving background is positioned via CSS variables set in JS.
|
|
43
|
+
position: absolute;
|
|
44
|
+
left: 0;
|
|
45
|
+
top: 0;
|
|
46
|
+
width: var(--segment-indicator-width, 0px);
|
|
47
|
+
height: var(--segment-indicator-height, 0px);
|
|
48
|
+
transform: translate(
|
|
49
|
+
var(--segment-indicator-left, 0px),
|
|
50
|
+
var(--segment-indicator-top, 0px)
|
|
51
|
+
);
|
|
52
|
+
background-color: var(--segment-control-bg-indicator);
|
|
53
|
+
border-radius: var(--radius-pill);
|
|
54
|
+
box-shadow: var(--segment-control-shadow);
|
|
55
|
+
opacity: var(--segment-indicator-opacity, 0);
|
|
56
|
+
transition: transform 0.2s ease, width 0.2s ease, height 0.2s ease, opacity 0.2s ease;
|
|
57
|
+
pointer-events: none;
|
|
58
|
+
z-index: 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.solara-segment-control--moving-bg {
|
|
62
|
+
.solara-segment-control__segment {
|
|
63
|
+
background-color: transparent;
|
|
64
|
+
box-shadow: none;
|
|
65
|
+
position: relative;
|
|
66
|
+
z-index: 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.solara-segment-control__segment:not(:disabled):hover {
|
|
70
|
+
background-color: transparent;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.solara-segment-control__segment[data-state="active"] {
|
|
74
|
+
background-color: transparent;
|
|
75
|
+
box-shadow: none;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.solara-segment-control--full-width {
|
|
80
|
+
// Full width keeps the control stretched evenly across its container.
|
|
81
|
+
width: 100%;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.solara-segment-control--size-small {
|
|
85
|
+
--segment-padding-y: var(--space-1);
|
|
86
|
+
--segment-padding-x: var(--space-3);
|
|
87
|
+
--segment-font-size: var(--font-size-caption-scaled);
|
|
88
|
+
--segment-line-height: var(--line-height-caption-scaled);
|
|
89
|
+
--segment-icon-size: 12px;
|
|
90
|
+
--segment-height: var(--space-7);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.solara-segment-control--size-large {
|
|
94
|
+
--segment-padding-y: var(--space-2);
|
|
95
|
+
--segment-padding-x: var(--space-5);
|
|
96
|
+
--segment-font-size: var(--font-size-h6-scaled);
|
|
97
|
+
--segment-line-height: var(--line-height-h6-scaled);
|
|
98
|
+
--segment-icon-size: 20px;
|
|
99
|
+
--segment-height: var(--space-10);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.solara-segment-control__segment {
|
|
103
|
+
position: relative;
|
|
104
|
+
display: inline-flex;
|
|
105
|
+
align-items: center;
|
|
106
|
+
justify-content: center;
|
|
107
|
+
gap: calc(var(--segment-gap) * var(--spacing-multiplier));
|
|
108
|
+
padding: calc(var(--segment-padding-y) * var(--spacing-multiplier))
|
|
109
|
+
calc(var(--segment-padding-x) * var(--spacing-multiplier));
|
|
110
|
+
margin: 0;
|
|
111
|
+
font-size: var(--segment-font-size);
|
|
112
|
+
font-weight: 500;
|
|
113
|
+
line-height: var(--segment-line-height);
|
|
114
|
+
color: var(--segment-control-text);
|
|
115
|
+
background-color: transparent;
|
|
116
|
+
border: none;
|
|
117
|
+
border-radius: var(--radius-pill);
|
|
118
|
+
cursor: pointer;
|
|
119
|
+
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
|
120
|
+
white-space: nowrap;
|
|
121
|
+
height: 100%;
|
|
122
|
+
// Default to content-sized segments for stable label alignment.
|
|
123
|
+
flex: 0 0 auto;
|
|
124
|
+
min-width: 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.solara-segment-control--full-width {
|
|
128
|
+
.solara-segment-control__segment {
|
|
129
|
+
// Override content sizing when full width is requested.
|
|
130
|
+
flex: 1 1 0%;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.solara-segment-control__segment:focus-visible {
|
|
135
|
+
outline: none;
|
|
136
|
+
box-shadow: 0 0 0 3px var(--color-focus-halo);
|
|
137
|
+
z-index: 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.solara-segment-control__segment:not(:disabled):hover {
|
|
141
|
+
color: var(--segment-control-text-hover);
|
|
142
|
+
background-color: var(--segment-control-bg-hover);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.solara-segment-control__segment[data-state="active"] {
|
|
146
|
+
color: var(--segment-control-text-active);
|
|
147
|
+
background-color: var(--segment-control-bg-active);
|
|
148
|
+
box-shadow: var(--elevation-1);
|
|
149
|
+
font-weight: 600;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.solara-segment-control__segment:disabled,
|
|
153
|
+
.solara-segment-control__segment[data-disabled="true"] {
|
|
154
|
+
color: var(--segment-control-text-disabled);
|
|
155
|
+
cursor: not-allowed;
|
|
156
|
+
opacity: 0.6;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.solara-segment-control__icon {
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
width: var(--segment-icon-size);
|
|
164
|
+
height: var(--segment-icon-size);
|
|
165
|
+
flex-shrink: 0;
|
|
166
|
+
|
|
167
|
+
.solara-icon,
|
|
168
|
+
> svg {
|
|
169
|
+
width: 100%;
|
|
170
|
+
height: 100%;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.solara-segment-control__icon .solara-icon [stroke]:not([stroke="none"]) {
|
|
175
|
+
stroke: currentColor;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.solara-segment-control__icon .solara-icon [fill]:not([fill="none"]) {
|
|
179
|
+
fill: currentColor;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.solara-segment-control__label {
|
|
183
|
+
display: inline-flex;
|
|
184
|
+
align-items: center;
|
|
185
|
+
min-width: 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
:root[data-surface-default="translucent"] .solara-segment-control,
|
|
189
|
+
:root[data-surface-segment-control="translucent"] .solara-segment-control,
|
|
190
|
+
.solara-segment-control[data-surface-style="translucent"] {
|
|
191
|
+
--surface-opacity: var(--opacity-translucent-medium);
|
|
192
|
+
--surface-blur: 0px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
:root[data-surface-default="glass"] .solara-segment-control,
|
|
196
|
+
:root[data-surface-segment-control="glass"] .solara-segment-control,
|
|
197
|
+
.solara-segment-control[data-surface-style="glass"] {
|
|
198
|
+
--surface-opacity: var(--opacity-translucent-heavy);
|
|
199
|
+
--surface-blur: var(--blur-medium);
|
|
200
|
+
--surface-border-color: rgba(var(--surface-border-color-rgb), var(--surface-border-alpha-glass));
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.solara-segment-control[data-surface-style="solid"] {
|
|
204
|
+
--surface-opacity: 1;
|
|
205
|
+
--surface-blur: 0px;
|
|
206
|
+
--surface-border-color: var(--color-divider-secondary);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
:root[data-surface-segment-control="solid"] .solara-segment-control {
|
|
210
|
+
--surface-opacity: 1;
|
|
211
|
+
--surface-blur: 0px;
|
|
212
|
+
--surface-border-color: var(--color-divider-secondary);
|
|
213
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import React, { Children, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { Icon } from "@solara/icons";
|
|
3
|
+
import type { IconProps } from "@solara/icons";
|
|
4
|
+
import type { SegmentControlProps, SegmentControlSize, SegmentProps } from "./SegmentControl.types";
|
|
5
|
+
import "./SegmentControl.scss";
|
|
6
|
+
|
|
7
|
+
const sizeAliasMap: Record<SegmentControlSize, "small" | "medium" | "large"> = {
|
|
8
|
+
sm: "small",
|
|
9
|
+
md: "medium",
|
|
10
|
+
lg: "large",
|
|
11
|
+
small: "small",
|
|
12
|
+
medium: "medium",
|
|
13
|
+
large: "large",
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const classNames = (...classes: Array<string | undefined | false | null>) =>
|
|
17
|
+
classes.filter(Boolean).join(" ");
|
|
18
|
+
|
|
19
|
+
const normalizeIconProps = (icon: NonNullable<SegmentProps["icon"]>): IconProps =>
|
|
20
|
+
typeof icon === "string" ? { name: icon } : icon;
|
|
21
|
+
|
|
22
|
+
const resolveIconProps = (icon: NonNullable<SegmentProps["icon"]>): IconProps => {
|
|
23
|
+
const iconProps = normalizeIconProps(icon);
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
...iconProps,
|
|
27
|
+
style: {
|
|
28
|
+
...iconProps.style,
|
|
29
|
+
color: "currentColor",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const Segment = ({
|
|
35
|
+
value,
|
|
36
|
+
disabled = false,
|
|
37
|
+
children,
|
|
38
|
+
className,
|
|
39
|
+
...props
|
|
40
|
+
}: SegmentProps) => {
|
|
41
|
+
void value;
|
|
42
|
+
void disabled;
|
|
43
|
+
void children;
|
|
44
|
+
void className;
|
|
45
|
+
void props;
|
|
46
|
+
// This component is used as a type guard and for prop documentation.
|
|
47
|
+
// The actual rendering is handled by the parent SegmentControl.
|
|
48
|
+
return null;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type SegmentControlComponent = React.FC<SegmentControlProps> & {
|
|
52
|
+
Segment: typeof Segment;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* A segmented control for toggling between related views or data sets.
|
|
57
|
+
*/
|
|
58
|
+
export const SegmentControl: SegmentControlComponent = ({
|
|
59
|
+
value,
|
|
60
|
+
onChange,
|
|
61
|
+
fullWidth = false,
|
|
62
|
+
size = "medium",
|
|
63
|
+
movingBackground = false,
|
|
64
|
+
surfaceStyle,
|
|
65
|
+
children,
|
|
66
|
+
className,
|
|
67
|
+
style,
|
|
68
|
+
...props
|
|
69
|
+
}: SegmentControlProps) => {
|
|
70
|
+
const resolvedSize = sizeAliasMap[size];
|
|
71
|
+
// Container ref is used to compute relative offsets for the moving indicator.
|
|
72
|
+
const containerRef = useRef<HTMLDivElement | null>(null);
|
|
73
|
+
// Each segment stores its DOM node so we can measure width/position on demand.
|
|
74
|
+
const segmentRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
|
|
75
|
+
// Tracks the segment currently under pointer/focus so the indicator can follow it.
|
|
76
|
+
const hoverValueRef = useRef<string | null>(null);
|
|
77
|
+
const [indicatorMetrics, setIndicatorMetrics] = useState<{
|
|
78
|
+
left: number;
|
|
79
|
+
top: number;
|
|
80
|
+
width: number;
|
|
81
|
+
height: number;
|
|
82
|
+
opacity: number;
|
|
83
|
+
} | null>(null);
|
|
84
|
+
|
|
85
|
+
const validChildren = useMemo(
|
|
86
|
+
() =>
|
|
87
|
+
Children.toArray(children).filter(
|
|
88
|
+
(child) =>
|
|
89
|
+
React.isValidElement(child) &&
|
|
90
|
+
(child.type === Segment || child.type === SegmentControl.Segment)
|
|
91
|
+
) as React.ReactElement<SegmentProps>[],
|
|
92
|
+
[children]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Keep indicator positioning logic centralized so hover, focus, and resize share it.
|
|
96
|
+
const setIndicatorFromValue = useCallback(
|
|
97
|
+
(segmentValue: string | null) => {
|
|
98
|
+
if (!movingBackground) return;
|
|
99
|
+
|
|
100
|
+
const container = containerRef.current;
|
|
101
|
+
if (!container || !segmentValue) {
|
|
102
|
+
// Hide the indicator if there's no valid target (e.g. during teardown).
|
|
103
|
+
setIndicatorMetrics((previous) =>
|
|
104
|
+
previous ? { ...previous, opacity: 0 } : null
|
|
105
|
+
);
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const target = segmentRefs.current.get(segmentValue);
|
|
110
|
+
if (!target) {
|
|
111
|
+
setIndicatorMetrics((previous) =>
|
|
112
|
+
previous ? { ...previous, opacity: 0 } : null
|
|
113
|
+
);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const containerRect = container.getBoundingClientRect();
|
|
118
|
+
const targetRect = target.getBoundingClientRect();
|
|
119
|
+
|
|
120
|
+
// Translate segment coordinates into container-local offsets for CSS vars.
|
|
121
|
+
setIndicatorMetrics({
|
|
122
|
+
left: targetRect.left - containerRect.left,
|
|
123
|
+
top: targetRect.top - containerRect.top,
|
|
124
|
+
width: targetRect.width,
|
|
125
|
+
height: targetRect.height,
|
|
126
|
+
opacity: 1,
|
|
127
|
+
});
|
|
128
|
+
},
|
|
129
|
+
[movingBackground]
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
// When selection changes (or on first mount), align the indicator to the active segment.
|
|
133
|
+
useEffect(() => {
|
|
134
|
+
if (!movingBackground) return;
|
|
135
|
+
if (hoverValueRef.current) return;
|
|
136
|
+
setIndicatorFromValue(value);
|
|
137
|
+
}, [movingBackground, value, validChildren, resolvedSize, setIndicatorFromValue]);
|
|
138
|
+
|
|
139
|
+
// ResizeObserver keeps the indicator aligned if segment sizes change responsively.
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
if (!movingBackground) return;
|
|
142
|
+
const container = containerRef.current;
|
|
143
|
+
if (!container || typeof ResizeObserver === "undefined") return;
|
|
144
|
+
|
|
145
|
+
const observer = new ResizeObserver(() => {
|
|
146
|
+
// If the user is hovering, keep the indicator on that segment.
|
|
147
|
+
const targetValue = hoverValueRef.current ?? value;
|
|
148
|
+
setIndicatorFromValue(targetValue);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
observer.observe(container);
|
|
152
|
+
return () => observer.disconnect();
|
|
153
|
+
}, [movingBackground, value, setIndicatorFromValue]);
|
|
154
|
+
|
|
155
|
+
const handleSegmentRef = useCallback(
|
|
156
|
+
(segmentValue: string) => (node: HTMLButtonElement | null) => {
|
|
157
|
+
if (node) {
|
|
158
|
+
segmentRefs.current.set(segmentValue, node);
|
|
159
|
+
} else {
|
|
160
|
+
segmentRefs.current.delete(segmentValue);
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
[]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (validChildren.length === 0) {
|
|
167
|
+
console.warn("SegmentControl requires at least one Segment child");
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Surface styling is applied via data attributes so theme defaults remain centralized.
|
|
172
|
+
const surfaceAttributes: Record<string, string | undefined> = {};
|
|
173
|
+
const surfaceStyleVars: React.CSSProperties = {};
|
|
174
|
+
|
|
175
|
+
if (surfaceStyle) {
|
|
176
|
+
if (typeof surfaceStyle === "string") {
|
|
177
|
+
surfaceAttributes["data-surface-style"] = surfaceStyle;
|
|
178
|
+
} else {
|
|
179
|
+
surfaceAttributes["data-surface-style"] = "custom";
|
|
180
|
+
if (surfaceStyle.opacity !== undefined) {
|
|
181
|
+
surfaceStyleVars["--surface-opacity"] = surfaceStyle.opacity.toString();
|
|
182
|
+
}
|
|
183
|
+
if (surfaceStyle.blur !== undefined) {
|
|
184
|
+
surfaceStyleVars["--surface-blur"] = `${surfaceStyle.blur}px`;
|
|
185
|
+
}
|
|
186
|
+
if (surfaceStyle.borderColor !== undefined) {
|
|
187
|
+
surfaceStyleVars["--surface-border-color"] = surfaceStyle.borderColor;
|
|
188
|
+
}
|
|
189
|
+
if (surfaceStyle.shadow !== undefined) {
|
|
190
|
+
surfaceStyleVars["--surface-shadow"] = surfaceStyle.shadow;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return (
|
|
196
|
+
<div
|
|
197
|
+
ref={containerRef}
|
|
198
|
+
className={classNames(
|
|
199
|
+
"solara-segment-control",
|
|
200
|
+
`solara-segment-control--size-${resolvedSize}`,
|
|
201
|
+
fullWidth ? "solara-segment-control--full-width" : null,
|
|
202
|
+
movingBackground ? "solara-segment-control--moving-bg" : null,
|
|
203
|
+
className
|
|
204
|
+
)}
|
|
205
|
+
role="tablist"
|
|
206
|
+
style={{ ...surfaceStyleVars, ...style }}
|
|
207
|
+
onMouseLeave={() => {
|
|
208
|
+
// When the pointer leaves the control, snap back to the active segment.
|
|
209
|
+
hoverValueRef.current = null;
|
|
210
|
+
setIndicatorFromValue(value);
|
|
211
|
+
}}
|
|
212
|
+
{...surfaceAttributes}
|
|
213
|
+
{...props}
|
|
214
|
+
>
|
|
215
|
+
{movingBackground ? (
|
|
216
|
+
// Single moving background element sized and positioned by CSS variables.
|
|
217
|
+
<span
|
|
218
|
+
className="solara-segment-control__indicator"
|
|
219
|
+
aria-hidden="true"
|
|
220
|
+
style={
|
|
221
|
+
indicatorMetrics
|
|
222
|
+
? ({
|
|
223
|
+
"--segment-indicator-left": `${indicatorMetrics.left}px`,
|
|
224
|
+
"--segment-indicator-top": `${indicatorMetrics.top}px`,
|
|
225
|
+
"--segment-indicator-width": `${indicatorMetrics.width}px`,
|
|
226
|
+
"--segment-indicator-height": `${indicatorMetrics.height}px`,
|
|
227
|
+
"--segment-indicator-opacity": `${indicatorMetrics.opacity}`,
|
|
228
|
+
} as React.CSSProperties)
|
|
229
|
+
: undefined
|
|
230
|
+
}
|
|
231
|
+
/>
|
|
232
|
+
) : null}
|
|
233
|
+
{validChildren.map((child) => {
|
|
234
|
+
const isActive = value === child.props.value;
|
|
235
|
+
const isDisabled = child.props.disabled;
|
|
236
|
+
const icon = child.props.icon;
|
|
237
|
+
const hasLabel = Boolean(child.props.children);
|
|
238
|
+
const ariaLabel =
|
|
239
|
+
child.props.ariaLabel ??
|
|
240
|
+
(typeof child.props.children === "string" ? child.props.children : undefined);
|
|
241
|
+
|
|
242
|
+
return (
|
|
243
|
+
<button
|
|
244
|
+
key={child.props.value}
|
|
245
|
+
type="button"
|
|
246
|
+
role="tab"
|
|
247
|
+
aria-selected={isActive}
|
|
248
|
+
aria-disabled={isDisabled}
|
|
249
|
+
aria-label={ariaLabel}
|
|
250
|
+
title={child.props.title}
|
|
251
|
+
disabled={isDisabled}
|
|
252
|
+
onClick={() => !isDisabled && onChange(child.props.value)}
|
|
253
|
+
onMouseEnter={() => {
|
|
254
|
+
if (!movingBackground || isDisabled) return;
|
|
255
|
+
// Hovering should move the background even if the segment isn't active.
|
|
256
|
+
hoverValueRef.current = child.props.value;
|
|
257
|
+
setIndicatorFromValue(child.props.value);
|
|
258
|
+
}}
|
|
259
|
+
onFocus={() => {
|
|
260
|
+
if (!movingBackground || isDisabled) return;
|
|
261
|
+
// Keyboard focus should behave like hover for accessibility parity.
|
|
262
|
+
hoverValueRef.current = child.props.value;
|
|
263
|
+
setIndicatorFromValue(child.props.value);
|
|
264
|
+
}}
|
|
265
|
+
onBlur={() => {
|
|
266
|
+
if (!movingBackground) return;
|
|
267
|
+
// Restore indicator when focus moves away.
|
|
268
|
+
hoverValueRef.current = null;
|
|
269
|
+
setIndicatorFromValue(value);
|
|
270
|
+
}}
|
|
271
|
+
className={classNames(
|
|
272
|
+
"solara-segment-control__segment",
|
|
273
|
+
child.props.className
|
|
274
|
+
)}
|
|
275
|
+
data-state={isActive ? "active" : "inactive"}
|
|
276
|
+
data-disabled={isDisabled ? "true" : undefined}
|
|
277
|
+
ref={handleSegmentRef(child.props.value)}
|
|
278
|
+
>
|
|
279
|
+
{icon ? (
|
|
280
|
+
<span className="solara-segment-control__icon" aria-hidden="true">
|
|
281
|
+
<Icon {...resolveIconProps(icon)} />
|
|
282
|
+
</span>
|
|
283
|
+
) : null}
|
|
284
|
+
{hasLabel ? (
|
|
285
|
+
<span className="solara-segment-control__label">{child.props.children}</span>
|
|
286
|
+
) : null}
|
|
287
|
+
</button>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
};
|
|
293
|
+
|
|
294
|
+
SegmentControl.Segment = Segment;
|
|
295
|
+
SegmentControl.displayName = "SegmentControl";
|
|
296
|
+
|
|
297
|
+
export { Segment };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import type { IconProps } from "@solara/icons";
|
|
3
|
+
|
|
4
|
+
export type SegmentControlSize = "small" | "medium" | "large" | "sm" | "md" | "lg";
|
|
5
|
+
|
|
6
|
+
export type SegmentControlSurfaceStyle =
|
|
7
|
+
| "solid"
|
|
8
|
+
| "translucent"
|
|
9
|
+
| "glass"
|
|
10
|
+
| {
|
|
11
|
+
opacity?: number;
|
|
12
|
+
blur?: number;
|
|
13
|
+
borderColor?: string;
|
|
14
|
+
shadow?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export interface SegmentControlProps
|
|
18
|
+
extends Omit<React.HTMLAttributes<HTMLDivElement>, "onChange"> {
|
|
19
|
+
/**
|
|
20
|
+
* The currently active segment value.
|
|
21
|
+
*/
|
|
22
|
+
value: string;
|
|
23
|
+
/**
|
|
24
|
+
* Callback when the active segment changes.
|
|
25
|
+
*/
|
|
26
|
+
onChange: (value: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Whether the segment control should take up the full width of its container.
|
|
29
|
+
*/
|
|
30
|
+
fullWidth?: boolean;
|
|
31
|
+
/**
|
|
32
|
+
* Size of the segment control.
|
|
33
|
+
*/
|
|
34
|
+
size?: SegmentControlSize;
|
|
35
|
+
/**
|
|
36
|
+
* Controls surface material treatment without changing layout tokens.
|
|
37
|
+
*/
|
|
38
|
+
surfaceStyle?: SegmentControlSurfaceStyle;
|
|
39
|
+
/**
|
|
40
|
+
* When true, uses a single animated background that moves between segments.
|
|
41
|
+
*/
|
|
42
|
+
movingBackground?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* The segments to render.
|
|
45
|
+
*/
|
|
46
|
+
children: React.ReactNode;
|
|
47
|
+
/**
|
|
48
|
+
* Additional class name.
|
|
49
|
+
*/
|
|
50
|
+
className?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface SegmentProps {
|
|
54
|
+
/**
|
|
55
|
+
* The value of the segment.
|
|
56
|
+
*/
|
|
57
|
+
value: string;
|
|
58
|
+
/**
|
|
59
|
+
* Whether the segment is disabled.
|
|
60
|
+
*/
|
|
61
|
+
disabled?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Optional leading icon. Accepts an icon name or full IconProps.
|
|
64
|
+
*/
|
|
65
|
+
icon?: IconProps | IconProps["name"];
|
|
66
|
+
/**
|
|
67
|
+
* Optional title used for accessibility (tooltip).
|
|
68
|
+
*/
|
|
69
|
+
title?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Optional aria-label for icon-only segments.
|
|
72
|
+
*/
|
|
73
|
+
ariaLabel?: string;
|
|
74
|
+
/**
|
|
75
|
+
* The content of the segment.
|
|
76
|
+
*/
|
|
77
|
+
children?: React.ReactNode;
|
|
78
|
+
/**
|
|
79
|
+
* Additional class name.
|
|
80
|
+
*/
|
|
81
|
+
className?: string;
|
|
82
|
+
}
|
package/src/index.ts
ADDED