@noya-app/noya-designsystem 0.1.41 → 0.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +10 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +217 -121
  5. package/dist/index.d.ts +217 -121
  6. package/dist/index.js +7435 -6796
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5257 -4660
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -5
  11. package/src/__tests__/combobox.test.ts +578 -0
  12. package/src/components/ActivityIndicator.tsx +4 -4
  13. package/src/components/AnimatePresence.tsx +5 -5
  14. package/src/components/Avatar.tsx +5 -2
  15. package/src/components/BaseToolbar.tsx +61 -0
  16. package/src/components/Button.tsx +1 -1
  17. package/src/components/Checkbox.tsx +6 -5
  18. package/src/components/Combobox.tsx +220 -324
  19. package/src/components/ComboboxMenu.tsx +88 -0
  20. package/src/components/Dialog.tsx +10 -6
  21. package/src/components/DimensionInput.tsx +4 -4
  22. package/src/components/FillPreviewBackground.tsx +51 -44
  23. package/src/components/FloatingWindow.tsx +5 -2
  24. package/src/components/GradientPicker.tsx +14 -14
  25. package/src/components/IconButton.tsx +6 -12
  26. package/src/components/Icons.tsx +3 -3
  27. package/src/components/InputField.tsx +145 -160
  28. package/src/components/Label.tsx +4 -4
  29. package/src/components/LabeledElementView.tsx +35 -41
  30. package/src/components/ListView.tsx +49 -39
  31. package/src/components/Message.tsx +75 -0
  32. package/src/components/Progress.tsx +2 -2
  33. package/src/components/ScrollArea.tsx +7 -5
  34. package/src/components/SegmentedControl.tsx +171 -0
  35. package/src/components/SelectMenu.tsx +61 -10
  36. package/src/components/Slider.tsx +5 -2
  37. package/src/components/Sortable.tsx +17 -27
  38. package/src/components/Spacer.tsx +28 -9
  39. package/src/components/Switch.tsx +8 -8
  40. package/src/components/TextArea.tsx +23 -13
  41. package/src/components/Toolbar.tsx +134 -0
  42. package/src/components/Tooltip.tsx +10 -7
  43. package/src/components/internal/Menu.tsx +5 -4
  44. package/src/contexts/DesignSystemConfiguration.tsx +15 -24
  45. package/src/contexts/DialogContext.tsx +137 -49
  46. package/src/contexts/ImageDataContext.tsx +6 -12
  47. package/src/index.css +1 -3
  48. package/src/index.tsx +12 -3
  49. package/src/utils/combobox.ts +369 -0
  50. package/tailwind.config.ts +1 -2
  51. package/src/components/RadioGroup.tsx +0 -100
  52. package/src/utils/completions.ts +0 -21
@@ -1,50 +1,42 @@
1
- import * as kiwi from 'kiwi.js';
2
- import React, {
3
- Children,
4
- createRef,
5
- Fragment,
6
- isValidElement,
7
- memo,
8
- ReactNode,
9
- useLayoutEffect,
10
- useMemo,
11
- useRef,
12
- } from 'react';
1
+ import * as kiwi from "kiwi.js";
2
+ import * as React from "react";
13
3
 
14
4
  interface ContainerProps {
15
- children: ReactNode;
16
- renderLabel: (provided: { id: string; index: number }) => ReactNode;
5
+ children: React.ReactNode;
6
+ renderLabel: (provided: { id: string; index: number }) => React.ReactNode;
17
7
  }
18
8
 
19
- export const LabeledElementView = memo(function LabeledElementView({
9
+ export const LabeledElementView = React.memo(function LabeledElementView({
20
10
  children,
21
11
  renderLabel,
22
12
  }: ContainerProps) {
23
- const elementIds: string[] = Children.toArray(children)
13
+ const elementIds: string[] = React.Children.toArray(children)
24
14
  .flatMap((child) =>
25
- isValidElement(child) && child.type === Fragment
26
- ? (child.props.children as ReactNode[])
27
- : [child],
15
+ React.isValidElement(child) && child.type === React.Fragment
16
+ ? (child.props.children as React.ReactNode[])
17
+ : [child]
28
18
  )
29
19
  .map((child) =>
30
- isValidElement(child) && 'id' in child.props ? child.props.id : null,
20
+ React.isValidElement(child) && "id" in child.props ? child.props.id : null
31
21
  )
32
22
  .filter((id) => !!id);
33
- const serializedIds = elementIds.join(',');
34
- const containerRef = useRef<HTMLDivElement | null>(null);
23
+ const serializedIds = elementIds.join(",");
24
+ const containerRef = React.useRef<HTMLDivElement | null>(null);
35
25
 
36
- const refs = useMemo(() => {
26
+ const refs = React.useMemo(() => {
37
27
  return Object.fromEntries(
38
- serializedIds.split(',').map((id) => [id, createRef<HTMLSpanElement>()]),
28
+ serializedIds
29
+ .split(",")
30
+ .map((id) => [id, React.createRef<HTMLSpanElement>()])
39
31
  );
40
32
  }, [serializedIds]);
41
33
 
42
- const labelElements = useMemo(() => {
43
- return serializedIds.split(',').map((id, index) => (
34
+ const labelElements = React.useMemo(() => {
35
+ return serializedIds.split(",").map((id, index) => (
44
36
  <span
45
37
  key={id}
46
38
  ref={refs[id]}
47
- style={{ position: 'absolute', left: `var(--x-offset)` }}
39
+ style={{ position: "absolute", left: `var(--x-offset)` }}
48
40
  >
49
41
  {renderLabel({
50
42
  id,
@@ -54,7 +46,7 @@ export const LabeledElementView = memo(function LabeledElementView({
54
46
  ));
55
47
  }, [refs, serializedIds, renderLabel]);
56
48
 
57
- useLayoutEffect(() => {
49
+ React.useLayoutEffect(() => {
58
50
  if (!containerRef.current) return;
59
51
 
60
52
  const containerRect = containerRef.current.getBoundingClientRect();
@@ -87,8 +79,8 @@ export const LabeledElementView = memo(function LabeledElementView({
87
79
  new kiwi.Expression(labelMidX),
88
80
  kiwi.Operator.Eq,
89
81
  new kiwi.Expression(targetMidXValue),
90
- kiwi.Strength.strong,
91
- ),
82
+ kiwi.Strength.strong
83
+ )
92
84
  );
93
85
 
94
86
  if (index === 0) {
@@ -97,8 +89,8 @@ export const LabeledElementView = memo(function LabeledElementView({
97
89
  new kiwi.Expression(labelMidX),
98
90
  kiwi.Operator.Ge,
99
91
  new kiwi.Expression(labelRect.width / 2),
100
- kiwi.Strength.required,
101
- ),
92
+ kiwi.Strength.required
93
+ )
102
94
  );
103
95
  }
104
96
 
@@ -124,8 +116,8 @@ export const LabeledElementView = memo(function LabeledElementView({
124
116
  new kiwi.Expression(labelMidX),
125
117
  kiwi.Operator.Le,
126
118
  new kiwi.Expression(containerRect.width - labelRect.width / 2),
127
- kiwi.Strength.required,
128
- ),
119
+ kiwi.Strength.required
120
+ )
129
121
  );
130
122
  }
131
123
 
@@ -136,7 +128,7 @@ export const LabeledElementView = memo(function LabeledElementView({
136
128
 
137
129
  solver.updateVariables();
138
130
 
139
- Object.entries(refs).forEach(([id, ref], index) => {
131
+ Object.entries(refs).forEach(([_id, ref], index) => {
140
132
  if (!ref.current) return;
141
133
 
142
134
  // console.log(id, variables[index].value());
@@ -144,21 +136,23 @@ export const LabeledElementView = memo(function LabeledElementView({
144
136
  const labelRect = ref.current.getBoundingClientRect();
145
137
 
146
138
  ref.current.style.setProperty(
147
- '--x-offset',
148
- `${variables[index].value() - labelRect.width / 2}px`,
139
+ "--x-offset",
140
+ `${variables[index].value() - labelRect.width / 2}px`
149
141
  );
150
142
  });
151
143
 
152
144
  containerRef.current.style.setProperty(
153
- '--height',
154
- `${Math.max(...heights)}px`,
145
+ "--height",
146
+ `${Math.max(...heights)}px`
155
147
  );
156
148
  }, [refs, labelElements]);
157
149
 
158
150
  return (
159
151
  <div className="flex flex-1 flex-col relative" ref={containerRef}>
160
152
  <div className="flex flex-1 items-center">{children}</div>
161
- <div className="relative overflow-hidden select-none">{labelElements}</div>
153
+ <div className="relative overflow-hidden select-none">
154
+ {labelElements}
155
+ </div>
162
156
  </div>
163
157
  );
164
- });
158
+ });
@@ -38,6 +38,7 @@ import {
38
38
  Sortable,
39
39
  } from "./Sortable";
40
40
  import { Spacer } from "./Spacer";
41
+ import { labelTextStyles } from "./SelectMenu";
41
42
 
42
43
  export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
43
44
  export type ListRowPosition = "only" | "first" | "middle" | "last";
@@ -202,6 +203,7 @@ const RowContainer = forwardRef<
202
203
  $colorScheme: ListColorScheme;
203
204
  $gap: number;
204
205
  $backgroundColor?: CSSProperties["backgroundColor"];
206
+ $clickable?: boolean;
205
207
  }
206
208
  >(
207
209
  (
@@ -221,6 +223,7 @@ const RowContainer = forwardRef<
221
223
  $gap,
222
224
  $backgroundColor,
223
225
  style,
226
+ $clickable,
224
227
  "aria-describedby": _, // Causes hydration warning
225
228
  ...props
226
229
  },
@@ -232,48 +235,46 @@ const RowContainer = forwardRef<
232
235
  <div
233
236
  ref={ref}
234
237
  className={cx(
235
- `${
236
- $isSectionHeader && $sectionHeaderVariant === "label"
237
- ? "font-sans text-label font-medium"
238
- : "font-sans text-heading5 font-normal"
239
- } `,
238
+ $isSectionHeader && $sectionHeaderVariant === "label"
239
+ ? labelTextStyles
240
+ : "font-sans text-heading5 font-normal",
241
+ !$isSectionHeader &&
242
+ $selected &&
243
+ "text-white active:bg-primary-light",
244
+ !$isSectionHeader && $disabled && "text-text-disabled",
245
+ $clickable ? "cursor-pointer" : "cursor-default",
246
+ $isSectionHeader && "bg-listview-raised-background",
247
+ "flex flex-[0_0_auto] select-none items-center relative",
240
248
  className
241
249
  )}
242
250
  style={{
243
- ...($isSectionHeader && { fontWeight: 500 }),
244
251
  gap: $gap,
245
- flex: "0 0 auto",
246
- userSelect: "none",
247
- cursor: "default",
248
252
  ...($variant !== "bare" && {
249
- paddingTop: "6px",
250
- paddingRight: "12px",
251
- paddingBottom: "6px",
252
- paddingLeft: "12px",
253
- ...($variant === "padded" && {
254
- borderRadius: "2px",
255
- marginLeft: "8px",
256
- marginRight: "8px",
257
- marginTop: `${margin.top}px`,
258
- marginBottom: `${margin.bottom}px`,
259
- }),
253
+ paddingTop: $selected ? "4px" : "6px",
254
+ paddingRight: $selected ? "8px" : "12px",
255
+ paddingBottom: $selected ? "4px" : "6px",
256
+ paddingLeft: $selected ? "8px" : "12px",
257
+ borderRadius:
258
+ $variant === "padded" ? "2px" : $selected ? "4px" : "",
259
+ marginLeft: $variant === "padded" ? "8px" : $selected ? "4px" : "",
260
+ marginRight: $variant === "padded" ? "8px" : $selected ? "4px" : "",
261
+ marginTop:
262
+ $variant === "padded"
263
+ ? `${margin.top}px`
264
+ : $selected
265
+ ? "2px"
266
+ : "",
267
+ marginBottom:
268
+ $variant === "padded"
269
+ ? `${margin.bottom}px`
270
+ : $selected
271
+ ? "2px"
272
+ : "",
260
273
  }),
261
- color: cssVars.colors.textMuted,
262
- ...($isSectionHeader && {
263
- backgroundColor: cssVars.colors.listviewRaisedBackground,
264
- ...($sectionHeaderVariant === "label" && {
265
- color: cssVars.colors.textDisabled,
274
+ ...($selected &&
275
+ !$isSectionHeader && {
276
+ backgroundColor: cssVars.colors[$colorScheme],
266
277
  }),
267
- }),
268
- ...($disabled && {
269
- color: cssVars.colors.textDisabled,
270
- }),
271
- ...($selected && {
272
- color: "white",
273
- backgroundColor: cssVars.colors[$colorScheme],
274
- }),
275
- display: "flex",
276
- alignItems: "center",
277
278
  ...($selected &&
278
279
  !$isSectionHeader &&
279
280
  ($selectedPosition === "middle" ||
@@ -288,7 +289,6 @@ const RowContainer = forwardRef<
288
289
  borderBottomRightRadius: "0px",
289
290
  borderBottomLeftRadius: "0px",
290
291
  }),
291
- position: "relative",
292
292
  ...($hovered && {
293
293
  boxShadow: `0 0 0 1px ${cssVars.colors[$colorScheme]} inset`,
294
294
  }),
@@ -549,6 +549,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
549
549
  )}
550
550
  tabIndex={tabIndex}
551
551
  className={className}
552
+ $clickable={!!onPress}
552
553
  >
553
554
  {relativeDropPosition && (
554
555
  <ListViewDragIndicatorElement
@@ -716,7 +717,7 @@ const VirtualizedListInner = forwardRefGeneric(function VirtualizedListInner<T>(
716
717
  itemSize={getItemHeight}
717
718
  estimatedItemSize={ROW_HEIGHT}
718
719
  >
719
- {VirtualizedListRow}
720
+ {VirtualizedListRow as any}
720
721
  </VariableSizeList>
721
722
  </div>
722
723
  ),
@@ -848,10 +849,19 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
848
849
  pressEventName,
849
850
  variant,
850
851
  gap,
851
- sectionHeaderVariant: "normal",
852
+ sectionHeaderVariant,
852
853
  isSectionHeader: false,
853
854
  }),
854
- [colorScheme, sortable, expandable, divider, pressEventName, variant, gap]
855
+ [
856
+ colorScheme,
857
+ sortable,
858
+ expandable,
859
+ divider,
860
+ pressEventName,
861
+ variant,
862
+ gap,
863
+ sectionHeaderVariant,
864
+ ]
855
865
  );
856
866
 
857
867
  const getItemContextValue = useCallback(
@@ -0,0 +1,75 @@
1
+ import React, { forwardRef, memo, useMemo } from "react";
2
+ import { Icons } from "./Icons";
3
+ import { Avatar } from "./Avatar";
4
+ import { cx } from "../utils/classNames";
5
+ import { Text } from "./Text";
6
+ import { MultiplayerUser } from "@noya-app/noya-multiplayer-react";
7
+ import { colorFromString } from "../utils/colorFromString";
8
+ import { cssVars } from "../theme";
9
+
10
+ export type MessageProps = {
11
+ role: "user" | "assistant" | "system" | "data";
12
+ children?: React.ReactNode;
13
+ user?: MultiplayerUser;
14
+ /** @default 27 */
15
+ avatarSize?: number;
16
+ };
17
+
18
+ export const Message = memo(
19
+ forwardRef(function Message(
20
+ { role, children, user, avatarSize = 27 }: MessageProps,
21
+ ref: React.ForwardedRef<HTMLDivElement>
22
+ ) {
23
+ const randomId = crypto.randomUUID();
24
+ const userMessageBackgroundColor = colorFromString(user?.id ?? randomId);
25
+ const styles = useMemo(
26
+ () =>
27
+ role === "user"
28
+ ? {
29
+ backgroundColor: userMessageBackgroundColor,
30
+ }
31
+ : undefined,
32
+ [userMessageBackgroundColor, role]
33
+ );
34
+ return (
35
+ <div
36
+ className={cx(
37
+ "flex gap-2",
38
+ role === "user" ? "flex-row-reverse" : "flex-row"
39
+ )}
40
+ >
41
+ {role === "assistant" ? (
42
+ <Avatar
43
+ name="Assistant"
44
+ size={avatarSize}
45
+ backgroundColor={cssVars.colors.inputBackground}
46
+ >
47
+ <Icons.PersonIcon fontSize={22} color={cssVars.colors.text} />
48
+ </Avatar>
49
+ ) : (
50
+ <Avatar
51
+ userId={!user ? randomId : user?.id}
52
+ name={user?.name ?? "Anonymous"}
53
+ image={user?.image}
54
+ size={avatarSize}
55
+ />
56
+ )}
57
+ <div
58
+ className={cx(
59
+ "flex flex-row gap-2 rounded-lg py-2 px-3 transition-all duration-200 min-w-6 max-w-[70%] items-start",
60
+ role === "user" ? "ml-auto" : "bg-input-background"
61
+ )}
62
+ style={styles}
63
+ ref={ref}
64
+ >
65
+ <Text
66
+ variant="small"
67
+ className={role === "user" ? "text-white" : "text-text"}
68
+ >
69
+ {children}
70
+ </Text>
71
+ </div>
72
+ </div>
73
+ );
74
+ })
75
+ );
@@ -1,6 +1,6 @@
1
1
  import { clamp } from "@noya-app/noya-utils";
2
2
  import * as ProgressPrimitive from "@radix-ui/react-progress";
3
- import React, { useMemo } from "react";
3
+ import * as React from "react";
4
4
  import { cx } from "../utils/classNames";
5
5
 
6
6
  type ProgressVariant = "normal" | "warning" | "primary" | "secondary";
@@ -15,7 +15,7 @@ export function Progress({
15
15
  className?: string;
16
16
  }) {
17
17
  const clampedValue = clamp(value, 0, 100);
18
- const transformStyles = useMemo(
18
+ const transformStyles = React.useMemo(
19
19
  () => ({
20
20
  transform: `translateX(-${100 - clampedValue}%)`,
21
21
  }),
@@ -1,20 +1,22 @@
1
1
  import * as RadixScrollArea from "@radix-ui/react-scroll-area";
2
- import React, { memo, ReactNode, useCallback, useState } from "react";
2
+ import * as React from "react";
3
3
 
4
4
  interface Props {
5
- children?: ReactNode | ((scrollElementRef: HTMLDivElement) => ReactNode);
5
+ children?:
6
+ | React.ReactNode
7
+ | ((scrollElementRef: HTMLDivElement) => React.ReactNode);
6
8
  }
7
9
 
8
- export const ScrollArea = memo(function ScrollArea({ children }: Props) {
10
+ export const ScrollArea = React.memo(function ScrollArea({ children }: Props) {
9
11
  const [scrollElementRef, setScrollElementRef] =
10
- useState<HTMLDivElement | null>(null);
12
+ React.useState<HTMLDivElement | null>(null);
11
13
 
12
14
  return (
13
15
  <div className="flex-1 min-h-0">
14
16
  <RadixScrollArea.Root style={{ width: "100%", height: "100%" }}>
15
17
  <RadixScrollArea.Viewport
16
18
  className="w-full h-full [&>div]:block"
17
- ref={useCallback(
19
+ ref={React.useCallback(
18
20
  (ref: HTMLDivElement | null) => setScrollElementRef(ref),
19
21
  []
20
22
  )}
@@ -0,0 +1,171 @@
1
+ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
2
+ import React, {
3
+ createContext,
4
+ forwardRef,
5
+ memo,
6
+ ReactNode,
7
+ useCallback,
8
+ useContext,
9
+ useMemo,
10
+ } from "react";
11
+ import { cx } from "../utils/classNames";
12
+ import { renderIcon } from "./Icons";
13
+ import { RegularMenuItem } from "./internal/Menu";
14
+ import { Tooltip } from "./Tooltip";
15
+
16
+ type SegmentedControlColorScheme = "primary" | "secondary";
17
+
18
+ type SegmentedControlContextValue = {
19
+ /** @default primary */
20
+ colorScheme?: SegmentedControlColorScheme;
21
+ };
22
+
23
+ const SegmentedControlContext = createContext<SegmentedControlContextValue>({
24
+ colorScheme: "primary",
25
+ });
26
+
27
+ export interface SegmentedControlProps<T extends string> {
28
+ id?: string;
29
+ value?: T;
30
+ onValueChange?: (value: T) => void;
31
+ /** @default primary */
32
+ colorScheme?: SegmentedControlColorScheme;
33
+ allowEmpty?: boolean;
34
+ separator?: boolean;
35
+ items: SegmentedControlItemProps<T>[];
36
+ className?: string;
37
+ style?: React.CSSProperties;
38
+ }
39
+
40
+ export type SegmentedControlItemProps<T extends string> = {
41
+ title?: ReactNode;
42
+ className?: string;
43
+ style?: React.CSSProperties;
44
+ tooltip?: ReactNode;
45
+ } & Pick<RegularMenuItem<T>, "value" | "disabled" | "icon" | "role">;
46
+
47
+ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
48
+ T extends string,
49
+ >(
50
+ {
51
+ value,
52
+ tooltip,
53
+ title,
54
+ disabled = false,
55
+ icon,
56
+ className,
57
+ ...props
58
+ }: SegmentedControlItemProps<T>,
59
+ forwardedRef: React.ForwardedRef<HTMLButtonElement>
60
+ ) {
61
+ const { colorScheme } = useContext(SegmentedControlContext);
62
+
63
+ const itemElement = (
64
+ <ToggleGroupPrimitive.Item
65
+ ref={forwardedRef}
66
+ value={(value ?? "").toString()}
67
+ disabled={disabled}
68
+ className={cx(
69
+ "font-sans text-heading5 font-normal relative flex-1 appearance-none text-segmented-control-item rounded inline-flex gap-1.5 items-center justify-center align-middle text-center focus:outline-none transition-colors",
70
+ colorScheme === "secondary"
71
+ ? "focus:ring-2 focus:ring-secondary focus:ring-offset-1"
72
+ : "focus:ring-2 focus:ring-primary focus:ring-offset-1",
73
+ colorScheme === "secondary"
74
+ ? "aria-checked:bg-secondary aria-checked:text-white"
75
+ : colorScheme === "primary"
76
+ ? "aria-checked:bg-primary aria-checked:text-white"
77
+ : "aria-checked:bg-background aria-checked:text-text",
78
+ "focus:z-interactable",
79
+ "aria-checked:shadow-[0_1px_1px_rgba(0,0,0,0.1)]",
80
+ className
81
+ )}
82
+ {...props}
83
+ >
84
+ {icon && renderIcon(icon)}
85
+ {title}
86
+ </ToggleGroupPrimitive.Item>
87
+ );
88
+
89
+ return tooltip ? (
90
+ <Tooltip content={tooltip}>{itemElement}</Tooltip>
91
+ ) : (
92
+ itemElement
93
+ );
94
+ });
95
+
96
+ const Separator = ({ transparent }: { transparent: boolean }) => (
97
+ <div
98
+ className={cx(
99
+ "w-[1px] my-1 self-stretch bg-divider",
100
+ transparent && "bg-transparent"
101
+ )}
102
+ />
103
+ );
104
+
105
+ export const SegmentedControl = memo(function SegmentedControl<
106
+ T extends string,
107
+ >({
108
+ id,
109
+ value,
110
+ onValueChange,
111
+ colorScheme,
112
+ allowEmpty,
113
+ items,
114
+ separator = true,
115
+ className,
116
+ style,
117
+ }: SegmentedControlProps<T>) {
118
+ const contextValue = useMemo(() => ({ colorScheme }), [colorScheme]);
119
+
120
+ const handleValueChange = useCallback(
121
+ (value: T) => {
122
+ if (!allowEmpty && !value) return;
123
+ onValueChange?.(value);
124
+ },
125
+ [allowEmpty, onValueChange]
126
+ );
127
+
128
+ const selectedIndex = items.findIndex((item) => item?.value === value);
129
+
130
+ return (
131
+ <SegmentedControlContext.Provider value={contextValue}>
132
+ <ToggleGroupPrimitive.Root
133
+ id={id}
134
+ type="single"
135
+ value={value}
136
+ onValueChange={handleValueChange}
137
+ className={cx(
138
+ `flex items-stretch w-full appearance-none relative outline-none min-h-[27px] rounded bg-input-background py-[2px] px-[2px]`,
139
+ className
140
+ )}
141
+ style={style}
142
+ >
143
+ {separator
144
+ ? items.map((item, index) => {
145
+ const isLastItem = index === items.length - 1;
146
+ const currentValue = item?.value;
147
+
148
+ // Show separator if:
149
+ // 1. Current item is not selected AND
150
+ // 2. Current item is not immediately before the selected item (except when selected is 0)
151
+ // 3. Current item is not immediately after the selected item
152
+ const showSeparator =
153
+ currentValue !== value &&
154
+ (selectedIndex === 0 && selectedIndex !== index
155
+ ? true
156
+ : index !== selectedIndex - 1);
157
+
158
+ return (
159
+ <React.Fragment key={index}>
160
+ <SegmentedControlItem key={index} {...item} />
161
+ {!isLastItem && <Separator transparent={!showSeparator} />}
162
+ </React.Fragment>
163
+ );
164
+ })
165
+ : items.map((item, index) => (
166
+ <SegmentedControlItem key={index} {...item} />
167
+ ))}
168
+ </ToggleGroupPrimitive.Root>
169
+ </SegmentedControlContext.Provider>
170
+ );
171
+ });