@noya-app/noya-designsystem 0.1.44 → 0.1.46

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 (51) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +18 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +1194 -878
  5. package/dist/index.d.ts +1194 -878
  6. package/dist/index.js +11432 -10219
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +11556 -10360
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -4
  11. package/src/__tests__/combobox.test.ts +137 -89
  12. package/src/components/AnimatePresence.tsx +10 -1
  13. package/src/components/Avatar.tsx +2 -0
  14. package/src/components/Breadcrumbs.tsx +29 -0
  15. package/src/components/Checkbox.tsx +6 -1
  16. package/src/components/Collection.tsx +74 -0
  17. package/src/components/Combobox.tsx +75 -56
  18. package/src/components/ComboboxMenu.tsx +61 -28
  19. package/src/components/CommandPalette.tsx +69 -0
  20. package/src/components/ContextMenu.tsx +33 -134
  21. package/src/components/DropdownMenu.tsx +46 -155
  22. package/src/components/EditableText.tsx +203 -0
  23. package/src/components/Fade.tsx +62 -0
  24. package/src/components/Grid.tsx +243 -0
  25. package/src/components/GridView.tsx +86 -96
  26. package/src/components/InputField.tsx +109 -133
  27. package/src/components/Label.tsx +81 -7
  28. package/src/components/LabeledField.tsx +112 -0
  29. package/src/components/List.tsx +268 -0
  30. package/src/components/ListView.tsx +65 -61
  31. package/src/components/Popover.tsx +12 -1
  32. package/src/components/SearchCompletionMenu.tsx +210 -0
  33. package/src/components/SegmentedControl.tsx +12 -6
  34. package/src/components/SelectMenu.tsx +110 -126
  35. package/src/components/Slider.tsx +18 -26
  36. package/src/components/Text.tsx +1 -0
  37. package/src/components/TextArea.tsx +4 -1
  38. package/src/components/Toolbar.tsx +9 -4
  39. package/src/components/TreeView.tsx +4 -0
  40. package/src/components/WorkspaceLayout.tsx +64 -20
  41. package/src/components/internal/Menu.tsx +107 -18
  42. package/src/components/internal/MenuViewport.tsx +152 -0
  43. package/src/components/internal/SelectItem.tsx +111 -0
  44. package/src/hooks/useIndent.ts +12 -0
  45. package/src/hooks/useLabel.ts +51 -0
  46. package/src/hooks/usePreservePanelSize.tsx +50 -19
  47. package/src/index.tsx +15 -6
  48. package/src/utils/combobox.ts +116 -101
  49. package/src/utils/createSectionedMenu.ts +11 -14
  50. package/src/utils/fuzzyScorer.ts +11 -8
  51. package/src/utils/selection.ts +48 -0
@@ -0,0 +1,210 @@
1
+ import {
2
+ ComboboxMenu,
3
+ Divider,
4
+ IScoredItem,
5
+ InputField,
6
+ ListView,
7
+ SelectableMenuItem,
8
+ Small,
9
+ fuzzyFilter,
10
+ getNextIndex,
11
+ isSelectableMenuItem,
12
+ } from "@noya-app/noya-designsystem";
13
+ import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
14
+ import React, {
15
+ forwardRef,
16
+ useCallback,
17
+ useEffect,
18
+ useImperativeHandle,
19
+ useMemo,
20
+ useRef,
21
+ useState,
22
+ } from "react";
23
+
24
+ export interface ISearchCompletionMenu {
25
+ focus: () => void;
26
+ }
27
+
28
+ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
29
+ {
30
+ onSelect,
31
+ onHover,
32
+ onClose,
33
+ items,
34
+ width = 200,
35
+ }: {
36
+ onSelect: (item: SelectableMenuItem<string>) => void;
37
+ onHover?: (item: SelectableMenuItem<string>) => void;
38
+ onClose: () => void;
39
+ items: SelectableMenuItem<string>[];
40
+ width?: number;
41
+ },
42
+ forwardedRef: React.ForwardedRef<ISearchCompletionMenu>
43
+ ) {
44
+ const [state, setState] = useState({
45
+ search: "",
46
+ index: 0,
47
+ });
48
+
49
+ const { index, search } = state;
50
+
51
+ const listRef = React.useRef<ListView.VirtualizedList>(null);
52
+
53
+ useEffect(() => {
54
+ listRef.current?.scrollToIndex(index);
55
+ }, [index]);
56
+
57
+ const results = useMemo(
58
+ () =>
59
+ fuzzyFilter({
60
+ items: items.map((item) =>
61
+ typeof item.title === "string" ? item.title : item.value
62
+ ),
63
+ query: search,
64
+ }).map((item) => ({
65
+ ...item,
66
+ ...items[item.index],
67
+ })) as (SelectableMenuItem<string> & IScoredItem)[],
68
+ [items, search]
69
+ );
70
+
71
+ const setSearch = useCallback((search: string) => {
72
+ setState((state) => ({ ...state, search, index: 0 }));
73
+ }, []);
74
+
75
+ const setIndex = useCallback((index: number) => {
76
+ setState((state) => ({ ...state, index }));
77
+ }, []);
78
+
79
+ useEffect(() => {
80
+ onHover?.(results[index]!);
81
+ }, [index, onHover, results]);
82
+
83
+ const searchHeight = 31;
84
+
85
+ const listSize =
86
+ results.length > 0
87
+ ? {
88
+ width,
89
+ height: Math.min(results.length * 31, 31 * 6.5),
90
+ }
91
+ : {
92
+ width,
93
+ height: 60,
94
+ };
95
+
96
+ const elementSize = {
97
+ width: listSize.width,
98
+ height: searchHeight + listSize.height,
99
+ };
100
+
101
+ const selectAndClose = useCallback(
102
+ (item: SelectableMenuItem<string>) => {
103
+ onSelect(item);
104
+ onClose();
105
+ },
106
+ [onSelect, onClose]
107
+ );
108
+
109
+ const handleKeyDown = useCallback(
110
+ (event: React.KeyboardEvent<HTMLInputElement>) => {
111
+ handleKeyboardEvent(event.nativeEvent, getCurrentPlatform(navigator), {
112
+ ArrowUp: () => {
113
+ const nextIndex = getNextIndex(
114
+ results,
115
+ index,
116
+ "previous",
117
+ (item) => item.disabled ?? false
118
+ );
119
+ setIndex(nextIndex);
120
+ },
121
+ ArrowDown: () => {
122
+ const nextIndex = getNextIndex(
123
+ results,
124
+ index,
125
+ "next",
126
+ (item) => item.disabled ?? false
127
+ );
128
+ setIndex(nextIndex);
129
+ },
130
+ Tab: () => selectAndClose(results[index]!),
131
+ Enter: () => selectAndClose(results[index]!),
132
+ Escape: () => onClose(),
133
+ });
134
+ },
135
+ [index, onClose, selectAndClose, results, setIndex]
136
+ );
137
+
138
+ const inputRef = useRef<HTMLInputElement>(null);
139
+
140
+ useImperativeHandle(forwardedRef, () => ({
141
+ focus: () => {
142
+ inputRef.current?.focus();
143
+ },
144
+ }));
145
+
146
+ return (
147
+ <div {...elementSize} className="flex flex-col overflow-hidden">
148
+ <InputField.Root className="flex flex-0">
149
+ <InputField.Input
150
+ ref={inputRef}
151
+ value={search}
152
+ placeholder="Filter..."
153
+ style={{
154
+ height: searchHeight,
155
+ borderRadius: 0,
156
+ outline: "none",
157
+ boxShadow: "none",
158
+ backgroundColor: "white",
159
+ padding: "4px 12px",
160
+ }}
161
+ onChange={setSearch}
162
+ onKeyDown={handleKeyDown}
163
+ // onBlur={(event) => {
164
+ // const isWithinPopover =
165
+ // event.relatedTarget &&
166
+ // event.relatedTarget instanceof HTMLElement &&
167
+ // event.relatedTarget.role === 'dialog';
168
+
169
+ // if (isWithinPopover) {
170
+ // inputRef.current?.focus();
171
+
172
+ // event.stopPropagation();
173
+ // event.preventDefault();
174
+ // }
175
+ // }}
176
+ // onPointerDown={(event) => {
177
+ // event.stopPropagation();
178
+ // event.preventDefault();
179
+ // }}
180
+ // onFocusCapture={(event) => {
181
+ // event.stopPropagation();
182
+ // event.preventDefault();
183
+ // }}
184
+ />
185
+ </InputField.Root>
186
+ <Divider />
187
+ {results.length > 0 ? (
188
+ <ComboboxMenu
189
+ ref={listRef}
190
+ listSize={listSize}
191
+ items={results}
192
+ selectedIndex={index}
193
+ onSelectItem={(item) => {
194
+ if (isSelectableMenuItem(item)) {
195
+ selectAndClose(item);
196
+ }
197
+ }}
198
+ onHoverIndex={setIndex}
199
+ />
200
+ ) : (
201
+ <div
202
+ {...listSize}
203
+ className="flex flex-col p-20 items-center justify-center"
204
+ >
205
+ <Small color="textDisabled">No results</Small>
206
+ </div>
207
+ )}
208
+ </div>
209
+ );
210
+ });
@@ -2,15 +2,16 @@ import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
2
2
  import React, {
3
3
  createContext,
4
4
  forwardRef,
5
- memo,
6
5
  ReactNode,
7
6
  useCallback,
8
7
  useContext,
9
8
  useMemo,
10
9
  } from "react";
10
+ import { memoGeneric } from "../../../noya-react-utils/src/utils/reactGenerics";
11
+ import { useLabel } from "../hooks/useLabel";
11
12
  import { cx } from "../utils/classNames";
12
13
  import { renderIcon } from "./Icons";
13
- import { RegularMenuItem } from "./internal/Menu";
14
+ import { SelectableMenuItem } from "./internal/Menu";
14
15
  import { Tooltip } from "./Tooltip";
15
16
 
16
17
  type SegmentedControlColorScheme = "primary" | "secondary";
@@ -42,7 +43,10 @@ export type SegmentedControlItemProps<T extends string> = {
42
43
  className?: string;
43
44
  style?: React.CSSProperties;
44
45
  tooltip?: ReactNode;
45
- } & Pick<RegularMenuItem<T>, "value" | "disabled" | "icon" | "role">;
46
+ } & Pick<
47
+ SelectableMenuItem<T>,
48
+ "value" | "disabled" | "icon" | "role" | "title"
49
+ >;
46
50
 
47
51
  const SegmentedControlItem = forwardRef(function SegmentedControlItem<
48
52
  T extends string,
@@ -77,6 +81,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
77
81
  : "aria-checked:bg-background aria-checked:text-text",
78
82
  "focus:z-interactable",
79
83
  "aria-checked:shadow-[0_1px_1px_rgba(0,0,0,0.1)]",
84
+ "whitespace-pre",
80
85
  className
81
86
  )}
82
87
  {...props}
@@ -102,10 +107,10 @@ const Separator = ({ transparent }: { transparent: boolean }) => (
102
107
  />
103
108
  );
104
109
 
105
- export const SegmentedControl = memo(function SegmentedControl<
110
+ export const SegmentedControl = memoGeneric(function SegmentedControl<
106
111
  T extends string,
107
112
  >({
108
- id,
113
+ id: idProp,
109
114
  value,
110
115
  onValueChange,
111
116
  colorScheme,
@@ -116,6 +121,7 @@ export const SegmentedControl = memo(function SegmentedControl<
116
121
  style,
117
122
  }: SegmentedControlProps<T>) {
118
123
  const contextValue = useMemo(() => ({ colorScheme }), [colorScheme]);
124
+ const { fieldId: id } = useLabel({ fieldId: idProp });
119
125
 
120
126
  const handleValueChange = useCallback(
121
127
  (value: T) => {
@@ -135,7 +141,7 @@ export const SegmentedControl = memo(function SegmentedControl<
135
141
  value={value}
136
142
  onValueChange={handleValueChange}
137
143
  className={cx(
138
- `flex items-stretch w-full appearance-none relative outline-none min-h-[27px] rounded bg-input-background py-[2px] px-[2px]`,
144
+ `flex items-stretch flex-auto appearance-none relative outline-none min-h-[27px] rounded bg-input-background py-[2px] px-[2px]`,
139
145
  className
140
146
  )}
141
147
  style={style}
@@ -12,17 +12,25 @@ import React, {
12
12
  useMemo,
13
13
  useState,
14
14
  } from "react";
15
+ import { useLabel, useLabelPosition } from "../hooks/useLabel";
15
16
  import { cx } from "../utils/classNames";
17
+
16
18
  import { Button } from "./Button";
17
19
  import { renderIcon } from "./Icons";
18
- import { MenuItem, RegularMenuItem, styles } from "./internal/Menu";
19
- import { Spacer } from "./Spacer";
20
+ import {
21
+ isSelectableMenuItem,
22
+ MenuItem,
23
+ SelectableMenuItem,
24
+ styles,
25
+ } from "./internal/Menu";
26
+ import { MenuComponents, MenuViewport } from "./internal/MenuViewport";
27
+ import { InsetLabel } from "./Label";
20
28
 
21
29
  type Props<T extends string> = {
22
30
  id?: string;
23
31
  style?: React.CSSProperties;
24
32
  className?: string;
25
- menuItems: MenuItem<T>[];
33
+ items: MenuItem<T>[];
26
34
  value: T;
27
35
  onSelect?: (value: T) => void;
28
36
  placeholder?: string;
@@ -39,9 +47,6 @@ const readOnlyStyle: CSSProperties = {
39
47
  textAlign: "left",
40
48
  };
41
49
 
42
- export const labelTextStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%]`;
43
- const labelStyles = `pointer-events-none flex items-center absolute top-0 bottom-0 right-[1.3rem] z-label`;
44
-
45
50
  const scrollButtonStyles = `
46
51
  flex
47
52
  items-center
@@ -52,32 +57,107 @@ const scrollButtonStyles = `
52
57
  hover:text-text
53
58
  `;
54
59
 
60
+ interface SelectMenuTriggerProps {
61
+ id?: string;
62
+ style?: React.CSSProperties;
63
+ className?: string;
64
+ disabled?: boolean;
65
+ icon?: React.ReactNode;
66
+ placeholder?: string;
67
+ isOpen?: boolean;
68
+ onFocus?: FocusEventHandler;
69
+ onBlur?: FocusEventHandler;
70
+ label?: React.ReactNode;
71
+ }
72
+
73
+ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
74
+ style,
75
+ className,
76
+ disabled,
77
+ icon,
78
+ placeholder,
79
+ isOpen,
80
+ onFocus,
81
+ onBlur,
82
+ ...props
83
+ }: SelectMenuTriggerProps) {
84
+ const { label, fieldId: id } = useLabel({
85
+ label: props.label,
86
+ fieldId: props.id,
87
+ });
88
+ const labelPosition = useLabelPosition();
89
+
90
+ return (
91
+ <Select.SelectTrigger asChild onFocus={onFocus} onBlur={onBlur}>
92
+ <Button
93
+ id={id}
94
+ style={style}
95
+ className={cx(
96
+ className ?? "flex w-full gap-1.5",
97
+ "flex-1 focus:z-interactable relative",
98
+ isOpen && "ring-2 ring-primary"
99
+ )}
100
+ disabled={disabled}
101
+ >
102
+ {icon && <span className="pr-1.5">{renderIcon(icon)}</span>}
103
+ {label && labelPosition === "inset" && (
104
+ <InsetLabel className="pr-4" htmlFor={id}>
105
+ {label}
106
+ </InsetLabel>
107
+ )}
108
+ <span className="flex-1 flex">
109
+ <Select.Value placeholder={placeholder} />
110
+ </span>
111
+
112
+ <span className="px-0.5">
113
+ <DropdownChevronIcon
114
+ className={cx(
115
+ "transition-transform duration-100",
116
+ isOpen ? "rotate-180" : "rotate-0"
117
+ )}
118
+ />
119
+ </span>
120
+ </Button>
121
+ </Select.SelectTrigger>
122
+ );
123
+ });
124
+
125
+ const Components: MenuComponents = {
126
+ Item: Select.Item,
127
+ ItemText: Select.ItemText,
128
+ Separator: Select.Separator,
129
+ };
130
+
55
131
  export const SelectMenu = memoGeneric(function SelectMenu<
56
132
  T extends string = string,
57
133
  >({
58
- id,
59
134
  style,
60
135
  className,
61
- menuItems,
136
+ items,
62
137
  value,
63
138
  onSelect,
64
139
  placeholder,
65
140
  disabled,
66
141
  readOnly,
67
- label,
68
142
  open,
69
143
  onFocus,
70
144
  onBlur,
71
145
  onOpenChange,
72
146
  contentStyle,
147
+ ...props
73
148
  }: Props<T>) {
74
- const selectedItem = menuItems.find(
75
- (item): item is RegularMenuItem<T> =>
76
- typeof item !== "string" && item.value === value
149
+ const selectedItem = items.find(
150
+ (item): item is SelectableMenuItem<T> =>
151
+ isSelectableMenuItem(item) && item.value === value
77
152
  );
78
153
  const icon = selectedItem?.icon;
79
154
  const [internalOpen, setInternalOpen] = useState(open);
80
155
 
156
+ const { label, fieldId: id } = useLabel({
157
+ label: props.label,
158
+ fieldId: props.id,
159
+ });
160
+
81
161
  const handleOpenChange = (open: boolean) => {
82
162
  if (onOpenChange) {
83
163
  onOpenChange(open);
@@ -91,92 +171,35 @@ export const SelectMenu = memoGeneric(function SelectMenu<
91
171
  id={id}
92
172
  style={style}
93
173
  contentStyle={readOnlyStyle}
94
- className={cx(`${className} flex-1 focus:z-interactable`)}
174
+ className={cx(`${className} flex-1 focus:z-interactable gap-1.5`)}
95
175
  disabled={disabled}
96
176
  >
97
- {icon && (
98
- <>
99
- {renderIcon(icon)}
100
- <Spacer.Horizontal inline size={6} />
101
- </>
102
- )}
177
+ {icon && renderIcon(icon)}
103
178
  <span className="flex flex-1">{selectedItem?.title ?? value}</span>
104
179
  </Button>
105
180
  ),
106
181
  [icon, id, style, className, disabled, value, selectedItem]
107
182
  );
108
183
 
109
- const trigger = useMemo(
110
- () => (
111
- <Select.SelectTrigger asChild onFocus={onFocus} onBlur={onBlur}>
112
- <Button
113
- id={id}
114
- style={style}
115
- className={cx(
116
- className ?? "w-full",
117
- "flex-1 focus:z-interactable relative",
118
- internalOpen && "ring-2 ring-primary"
119
- )}
120
- disabled={disabled}
121
- >
122
- {icon && (
123
- <>
124
- {renderIcon(icon)}
125
- <Spacer.Horizontal inline size={6} />
126
- </>
127
- )}
128
- <span className="flex flex-1 mr-1.5">
129
- <Select.Value placeholder={placeholder} />
130
- </span>
131
- {label && (
132
- <label htmlFor={id} className={cx(labelStyles, labelTextStyles)}>
133
- {label}
134
- </label>
135
- )}
136
- <DropdownChevronIcon
137
- className={cx(
138
- "transition-transform duration-100",
139
- internalOpen ? "rotate-180" : "rotate-0"
140
- )}
141
- />
142
- </Button>
143
- </Select.SelectTrigger>
144
- ),
145
- [
146
- onFocus,
147
- onBlur,
148
- id,
149
- style,
150
- className,
151
- disabled,
152
- icon,
153
- placeholder,
154
- label,
155
- internalOpen,
156
- ]
157
- );
158
-
159
- if (readOnly) {
160
- return label ? (
161
- <div className="flex flex-col relative">
162
- {readOnlyButton}
163
- <label className={labelStyles} htmlFor={id}>
164
- {label}
165
- </label>
166
- </div>
167
- ) : (
168
- readOnlyButton
169
- );
170
- }
171
-
172
- return (
184
+ const content = (
173
185
  <Select.Root
174
186
  value={value}
175
187
  onValueChange={onSelect}
176
188
  open={open}
177
189
  onOpenChange={handleOpenChange}
178
190
  >
179
- {trigger}
191
+ <SelectMenuTrigger
192
+ id={id}
193
+ style={style}
194
+ className={className}
195
+ disabled={disabled}
196
+ icon={icon}
197
+ placeholder={placeholder}
198
+ isOpen={internalOpen}
199
+ onFocus={onFocus}
200
+ onBlur={onBlur}
201
+ label={label}
202
+ />
180
203
  <Select.Portal>
181
204
  <Select.Content
182
205
  className={styles.contentStyle}
@@ -196,19 +219,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
196
219
  />
197
220
  </Select.ScrollUpButton>
198
221
  <Select.Viewport>
199
- {menuItems.map((menuItem) => {
200
- if (typeof menuItem === "string") {
201
- return <Select.Separator className={styles.separatorStyle} />;
202
- }
203
-
204
- const value = menuItem.value ?? "";
205
-
206
- return (
207
- <SelectItem key={value} value={value} icon={menuItem.icon}>
208
- {menuItem.title ?? value}
209
- </SelectItem>
210
- );
211
- })}
222
+ <MenuViewport items={items} Components={Components} />
212
223
  </Select.Viewport>
213
224
  <Select.ScrollDownButton className={scrollButtonStyles}>
214
225
  <ChevronDownIcon />
@@ -217,33 +228,6 @@ export const SelectMenu = memoGeneric(function SelectMenu<
217
228
  </Select.Portal>
218
229
  </Select.Root>
219
230
  );
220
- });
221
231
 
222
- const SelectItem = React.forwardRef(
223
- (
224
- {
225
- children,
226
- icon,
227
- disabled,
228
- ...props
229
- }: Select.SelectItemProps & { icon?: React.ReactNode },
230
- forwardedRef: React.ForwardedRef<HTMLDivElement>
231
- ) => {
232
- return (
233
- <Select.Item
234
- className={styles.itemStyle({ disabled })}
235
- disabled={disabled}
236
- {...props}
237
- ref={forwardedRef}
238
- >
239
- {icon && (
240
- <>
241
- {renderIcon(icon)}
242
- <Spacer.Horizontal size={8} />
243
- </>
244
- )}
245
- <Select.ItemText>{children}</Select.ItemText>
246
- </Select.Item>
247
- );
248
- }
249
- );
232
+ return readOnly ? readOnlyButton : content;
233
+ });
@@ -1,6 +1,8 @@
1
1
  import * as RadixSlider from "@radix-ui/react-slider";
2
2
  import React, { FocusEventHandler, memo, useCallback, useMemo } from "react";
3
+ import { useLabel, useLabelPosition } from "../hooks/useLabel";
3
4
  import { cx } from "../utils/classNames";
5
+ import { InsetLabel } from "./Label";
4
6
 
5
7
  type ColorScheme = "primary" | "secondary";
6
8
 
@@ -20,8 +22,6 @@ interface Props {
20
22
  readOnly?: boolean;
21
23
  }
22
24
 
23
- const labelStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%] pointer-events-none flex items-center z-label`;
24
-
25
25
  const THUMB_WIDTH = 8; // Width of the thumb in pixels
26
26
 
27
27
  const thumbStyle = {
@@ -29,7 +29,6 @@ const thumbStyle = {
29
29
  };
30
30
 
31
31
  export const Slider = memo(function Slider({
32
- id,
33
32
  style,
34
33
  className,
35
34
  value,
@@ -38,16 +37,22 @@ export const Slider = memo(function Slider({
38
37
  max,
39
38
  step,
40
39
  colorScheme,
41
- label,
42
40
  onFocus,
43
41
  onBlur,
44
42
  readOnly = false,
43
+ ...props
45
44
  }: Props) {
46
45
  const arrayValue = useMemo(
47
46
  () => [Math.min(Math.max(value, min), max)],
48
47
  [value, min, max]
49
48
  );
50
49
 
50
+ const { label, fieldId: id } = useLabel({
51
+ label: props.label,
52
+ fieldId: props.id,
53
+ });
54
+ const labelPosition = useLabelPosition();
55
+
51
56
  const handleValueChange = useCallback(
52
57
  (arrayValue: number[]) => {
53
58
  onValueChange(arrayValue[0]);
@@ -90,7 +95,7 @@ export const Slider = memo(function Slider({
90
95
  value={arrayValue}
91
96
  onValueChange={handleValueChange}
92
97
  className={cx(
93
- "flex relative items-center select-none touch-none h-[27px] rounded overflow-hidden w-full",
98
+ "flex relative items-center select-none touch-none h-[27px] rounded overflow-hidden flex-grow max-h-[27px]",
94
99
  className
95
100
  )}
96
101
  style={style}
@@ -98,25 +103,24 @@ export const Slider = memo(function Slider({
98
103
  onBlur={onBlur}
99
104
  disabled={readOnly}
100
105
  >
101
- <RadixSlider.Track className="relative flex-grow h-full rounded overflow-hidden bg-input-background">
102
- {label && (
103
- <label
104
- htmlFor={id}
105
- className={cx(labelStyles, "absolute top-0 bottom-0 right-2")}
106
- >
107
- {label}
108
- </label>
106
+ <RadixSlider.Track className="flex-grow h-full rounded overflow-hidden bg-input-background">
107
+ {label && labelPosition === "inset" && (
108
+ <InsetLabel htmlFor={id}>{label}</InsetLabel>
109
109
  )}
110
110
  <div
111
111
  style={trackFillStyle}
112
112
  className={cx(
113
113
  "absolute inset-0 w-full h-full rounded overflow-hidden",
114
- colorScheme === undefined && "bg-slider-border",
115
114
  colorScheme === "primary" && "bg-primary",
116
115
  colorScheme === "secondary" && "bg-secondary"
117
116
  )}
118
117
  />
119
118
  </RadixSlider.Track>
119
+ {label && labelPosition === "inset" && colorScheme !== undefined && (
120
+ <InsetLabel className="text-white overflow-hidden" style={labelStyle}>
121
+ {label}
122
+ </InsetLabel>
123
+ )}
120
124
  <RadixSlider.Thumb
121
125
  style={thumbStyle}
122
126
  className={cx(
@@ -126,18 +130,6 @@ export const Slider = memo(function Slider({
126
130
  colorScheme === "secondary" && "border-secondary"
127
131
  )}
128
132
  />
129
- {label && (
130
- <label
131
- htmlFor={id}
132
- className={cx(
133
- labelStyles,
134
- "absolute top-0 bottom-0 left-2 right-2 text-white overflow-hidden flex justify-end"
135
- )}
136
- style={labelStyle}
137
- >
138
- {label}
139
- </label>
140
- )}
141
133
  </RadixSlider.Root>
142
134
  );
143
135
  });
@@ -55,6 +55,7 @@ export type TextProps = {
55
55
  variant: Variant;
56
56
  color?: KebabToCamelCase<ThemeColor>;
57
57
  children: ReactNode;
58
+ id?: string;
58
59
  onClick?: () => void;
59
60
  onDoubleClick?: () => void;
60
61
  onKeyDown?: (event: React.KeyboardEvent<HTMLSpanElement>) => void;