@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.
- package/.turbo/turbo-build.log +13 -10
- package/CHANGELOG.md +18 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +1194 -878
- package/dist/index.d.ts +1194 -878
- package/dist/index.js +11432 -10219
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +11556 -10360
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/combobox.test.ts +137 -89
- package/src/components/AnimatePresence.tsx +10 -1
- package/src/components/Avatar.tsx +2 -0
- package/src/components/Breadcrumbs.tsx +29 -0
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Collection.tsx +74 -0
- package/src/components/Combobox.tsx +75 -56
- package/src/components/ComboboxMenu.tsx +61 -28
- package/src/components/CommandPalette.tsx +69 -0
- package/src/components/ContextMenu.tsx +33 -134
- package/src/components/DropdownMenu.tsx +46 -155
- package/src/components/EditableText.tsx +203 -0
- package/src/components/Fade.tsx +62 -0
- package/src/components/Grid.tsx +243 -0
- package/src/components/GridView.tsx +86 -96
- package/src/components/InputField.tsx +109 -133
- package/src/components/Label.tsx +81 -7
- package/src/components/LabeledField.tsx +112 -0
- package/src/components/List.tsx +268 -0
- package/src/components/ListView.tsx +65 -61
- package/src/components/Popover.tsx +12 -1
- package/src/components/SearchCompletionMenu.tsx +210 -0
- package/src/components/SegmentedControl.tsx +12 -6
- package/src/components/SelectMenu.tsx +110 -126
- package/src/components/Slider.tsx +18 -26
- package/src/components/Text.tsx +1 -0
- package/src/components/TextArea.tsx +4 -1
- package/src/components/Toolbar.tsx +9 -4
- package/src/components/TreeView.tsx +4 -0
- package/src/components/WorkspaceLayout.tsx +64 -20
- package/src/components/internal/Menu.tsx +107 -18
- package/src/components/internal/MenuViewport.tsx +152 -0
- package/src/components/internal/SelectItem.tsx +111 -0
- package/src/hooks/useIndent.ts +12 -0
- package/src/hooks/useLabel.ts +51 -0
- package/src/hooks/usePreservePanelSize.tsx +50 -19
- package/src/index.tsx +15 -6
- package/src/utils/combobox.ts +116 -101
- package/src/utils/createSectionedMenu.ts +11 -14
- package/src/utils/fuzzyScorer.ts +11 -8
- 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 {
|
|
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<
|
|
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 =
|
|
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
|
|
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 {
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
75
|
-
(item): item is
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
|
|
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
|
|
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="
|
|
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
|
});
|
package/src/components/Text.tsx
CHANGED