@noya-app/noya-designsystem 0.1.43 → 0.1.45
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 +12 -9
- package/CHANGELOG.md +15 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +323 -80
- package/dist/index.d.ts +323 -80
- package/dist/index.js +2126 -1373
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2012 -1272
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/components/AnimatePresence.tsx +10 -1
- package/src/components/Avatar.tsx +2 -0
- package/src/components/Checkbox.tsx +6 -1
- package/src/components/Combobox.tsx +28 -14
- package/src/components/ComboboxMenu.tsx +30 -15
- package/src/components/CommandPalette.tsx +69 -0
- package/src/components/ContextMenu.tsx +33 -134
- package/src/components/DropdownMenu.tsx +47 -155
- package/src/components/Fade.tsx +62 -0
- package/src/components/InputField.tsx +109 -133
- package/src/components/Label.tsx +81 -7
- package/src/components/LabeledField.tsx +112 -0
- package/src/components/ListView.tsx +55 -52
- package/src/components/Popover.tsx +12 -1
- package/src/components/SearchCompletionMenu.tsx +206 -0
- package/src/components/SegmentedControl.tsx +5 -2
- package/src/components/SelectMenu.tsx +108 -124
- package/src/components/SidebarList.tsx +252 -0
- 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 +6 -2
- package/src/components/TreeView.tsx +3 -0
- package/src/components/WorkspaceLayout.tsx +38 -18
- package/src/components/internal/Menu.tsx +58 -10
- package/src/components/internal/MenuViewport.tsx +151 -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 +12 -5
- package/src/utils/combobox.ts +4 -1
- package/src/utils/createSectionedMenu.ts +11 -8
- package/src/utils/selection.ts +36 -0
|
@@ -12,17 +12,19 @@ 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";
|
|
16
17
|
import { Button } from "./Button";
|
|
17
18
|
import { renderIcon } from "./Icons";
|
|
18
19
|
import { MenuItem, RegularMenuItem, styles } from "./internal/Menu";
|
|
19
|
-
import {
|
|
20
|
+
import { MenuComponents, MenuViewport } from "./internal/MenuViewport";
|
|
21
|
+
import { InsetLabel } from "./Label";
|
|
20
22
|
|
|
21
23
|
type Props<T extends string> = {
|
|
22
24
|
id?: string;
|
|
23
25
|
style?: React.CSSProperties;
|
|
24
26
|
className?: string;
|
|
25
|
-
|
|
27
|
+
items: MenuItem<T>[];
|
|
26
28
|
value: T;
|
|
27
29
|
onSelect?: (value: T) => void;
|
|
28
30
|
placeholder?: string;
|
|
@@ -31,6 +33,7 @@ type Props<T extends string> = {
|
|
|
31
33
|
label?: React.ReactNode;
|
|
32
34
|
onFocus?: FocusEventHandler;
|
|
33
35
|
onBlur?: FocusEventHandler;
|
|
36
|
+
contentStyle?: React.CSSProperties;
|
|
34
37
|
} & Pick<SelectProps, "open" | "onOpenChange">;
|
|
35
38
|
|
|
36
39
|
const readOnlyStyle: CSSProperties = {
|
|
@@ -38,9 +41,6 @@ const readOnlyStyle: CSSProperties = {
|
|
|
38
41
|
textAlign: "left",
|
|
39
42
|
};
|
|
40
43
|
|
|
41
|
-
export const labelTextStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%]`;
|
|
42
|
-
const labelStyles = `pointer-events-none flex items-center absolute top-0 bottom-0 right-[1.3rem] z-label`;
|
|
43
|
-
|
|
44
44
|
const scrollButtonStyles = `
|
|
45
45
|
flex
|
|
46
46
|
items-center
|
|
@@ -51,31 +51,109 @@ const scrollButtonStyles = `
|
|
|
51
51
|
hover:text-text
|
|
52
52
|
`;
|
|
53
53
|
|
|
54
|
+
interface SelectMenuTriggerProps {
|
|
55
|
+
id?: string;
|
|
56
|
+
style?: React.CSSProperties;
|
|
57
|
+
className?: string;
|
|
58
|
+
disabled?: boolean;
|
|
59
|
+
icon?: React.ReactNode;
|
|
60
|
+
placeholder?: string;
|
|
61
|
+
isOpen?: boolean;
|
|
62
|
+
onFocus?: FocusEventHandler;
|
|
63
|
+
onBlur?: FocusEventHandler;
|
|
64
|
+
label?: React.ReactNode;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
|
|
68
|
+
style,
|
|
69
|
+
className,
|
|
70
|
+
disabled,
|
|
71
|
+
icon,
|
|
72
|
+
placeholder,
|
|
73
|
+
isOpen,
|
|
74
|
+
onFocus,
|
|
75
|
+
onBlur,
|
|
76
|
+
...props
|
|
77
|
+
}: SelectMenuTriggerProps) {
|
|
78
|
+
const { label, fieldId: id } = useLabel({
|
|
79
|
+
label: props.label,
|
|
80
|
+
fieldId: props.id,
|
|
81
|
+
});
|
|
82
|
+
const labelPosition = useLabelPosition();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Select.SelectTrigger asChild onFocus={onFocus} onBlur={onBlur}>
|
|
86
|
+
<Button
|
|
87
|
+
id={id}
|
|
88
|
+
style={style}
|
|
89
|
+
className={cx(
|
|
90
|
+
className ?? "flex w-full gap-1.5",
|
|
91
|
+
"flex-1 focus:z-interactable relative",
|
|
92
|
+
isOpen && "ring-2 ring-primary"
|
|
93
|
+
)}
|
|
94
|
+
disabled={disabled}
|
|
95
|
+
>
|
|
96
|
+
{icon && <span className="pr-1.5">{renderIcon(icon)}</span>}
|
|
97
|
+
{label && labelPosition === "inset" && (
|
|
98
|
+
<InsetLabel className="pr-4" htmlFor={id}>
|
|
99
|
+
{label}
|
|
100
|
+
</InsetLabel>
|
|
101
|
+
)}
|
|
102
|
+
<span className="flex-1 flex">
|
|
103
|
+
<Select.Value placeholder={placeholder} />
|
|
104
|
+
</span>
|
|
105
|
+
|
|
106
|
+
<span className="px-0.5">
|
|
107
|
+
<DropdownChevronIcon
|
|
108
|
+
className={cx(
|
|
109
|
+
"transition-transform duration-100",
|
|
110
|
+
isOpen ? "rotate-180" : "rotate-0"
|
|
111
|
+
)}
|
|
112
|
+
/>
|
|
113
|
+
</span>
|
|
114
|
+
</Button>
|
|
115
|
+
</Select.SelectTrigger>
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
const Components: MenuComponents = {
|
|
120
|
+
Item: Select.Item,
|
|
121
|
+
ItemText: Select.ItemText,
|
|
122
|
+
Separator: Select.Separator,
|
|
123
|
+
};
|
|
124
|
+
|
|
54
125
|
export const SelectMenu = memoGeneric(function SelectMenu<
|
|
55
126
|
T extends string = string,
|
|
56
127
|
>({
|
|
57
|
-
id,
|
|
58
128
|
style,
|
|
59
129
|
className,
|
|
60
|
-
|
|
130
|
+
items,
|
|
61
131
|
value,
|
|
62
132
|
onSelect,
|
|
63
133
|
placeholder,
|
|
64
134
|
disabled,
|
|
65
135
|
readOnly,
|
|
66
|
-
label,
|
|
67
136
|
open,
|
|
68
137
|
onFocus,
|
|
69
138
|
onBlur,
|
|
70
139
|
onOpenChange,
|
|
140
|
+
contentStyle,
|
|
141
|
+
...props
|
|
71
142
|
}: Props<T>) {
|
|
72
|
-
const selectedItem =
|
|
143
|
+
const selectedItem = items.find(
|
|
73
144
|
(item): item is RegularMenuItem<T> =>
|
|
74
|
-
|
|
145
|
+
item.type !== "separator" &&
|
|
146
|
+
item.type !== "sectionHeader" &&
|
|
147
|
+
item.value === value
|
|
75
148
|
);
|
|
76
149
|
const icon = selectedItem?.icon;
|
|
77
150
|
const [internalOpen, setInternalOpen] = useState(open);
|
|
78
151
|
|
|
152
|
+
const { label, fieldId: id } = useLabel({
|
|
153
|
+
label: props.label,
|
|
154
|
+
fieldId: props.id,
|
|
155
|
+
});
|
|
156
|
+
|
|
79
157
|
const handleOpenChange = (open: boolean) => {
|
|
80
158
|
if (onOpenChange) {
|
|
81
159
|
onOpenChange(open);
|
|
@@ -89,92 +167,35 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
89
167
|
id={id}
|
|
90
168
|
style={style}
|
|
91
169
|
contentStyle={readOnlyStyle}
|
|
92
|
-
className={cx(`${className} flex-1 focus:z-interactable`)}
|
|
170
|
+
className={cx(`${className} flex-1 focus:z-interactable gap-1.5`)}
|
|
93
171
|
disabled={disabled}
|
|
94
172
|
>
|
|
95
|
-
{icon && (
|
|
96
|
-
<>
|
|
97
|
-
{renderIcon(icon)}
|
|
98
|
-
<Spacer.Horizontal inline size={6} />
|
|
99
|
-
</>
|
|
100
|
-
)}
|
|
173
|
+
{icon && renderIcon(icon)}
|
|
101
174
|
<span className="flex flex-1">{selectedItem?.title ?? value}</span>
|
|
102
175
|
</Button>
|
|
103
176
|
),
|
|
104
177
|
[icon, id, style, className, disabled, value, selectedItem]
|
|
105
178
|
);
|
|
106
179
|
|
|
107
|
-
const
|
|
108
|
-
() => (
|
|
109
|
-
<Select.SelectTrigger asChild onFocus={onFocus} onBlur={onBlur}>
|
|
110
|
-
<Button
|
|
111
|
-
id={id}
|
|
112
|
-
style={style}
|
|
113
|
-
className={cx(
|
|
114
|
-
className ?? "w-full",
|
|
115
|
-
"flex-1 focus:z-interactable relative",
|
|
116
|
-
internalOpen && "ring-2 ring-primary"
|
|
117
|
-
)}
|
|
118
|
-
disabled={disabled}
|
|
119
|
-
>
|
|
120
|
-
{icon && (
|
|
121
|
-
<>
|
|
122
|
-
{renderIcon(icon)}
|
|
123
|
-
<Spacer.Horizontal inline size={6} />
|
|
124
|
-
</>
|
|
125
|
-
)}
|
|
126
|
-
<span className="flex flex-1 mr-1.5">
|
|
127
|
-
<Select.Value placeholder={placeholder} />
|
|
128
|
-
</span>
|
|
129
|
-
{label && (
|
|
130
|
-
<label htmlFor={id} className={cx(labelStyles, labelTextStyles)}>
|
|
131
|
-
{label}
|
|
132
|
-
</label>
|
|
133
|
-
)}
|
|
134
|
-
<DropdownChevronIcon
|
|
135
|
-
className={cx(
|
|
136
|
-
"transition-transform duration-100",
|
|
137
|
-
internalOpen ? "rotate-180" : "rotate-0"
|
|
138
|
-
)}
|
|
139
|
-
/>
|
|
140
|
-
</Button>
|
|
141
|
-
</Select.SelectTrigger>
|
|
142
|
-
),
|
|
143
|
-
[
|
|
144
|
-
onFocus,
|
|
145
|
-
onBlur,
|
|
146
|
-
id,
|
|
147
|
-
style,
|
|
148
|
-
className,
|
|
149
|
-
disabled,
|
|
150
|
-
icon,
|
|
151
|
-
placeholder,
|
|
152
|
-
label,
|
|
153
|
-
internalOpen,
|
|
154
|
-
]
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
if (readOnly) {
|
|
158
|
-
return label ? (
|
|
159
|
-
<div className="flex flex-col relative">
|
|
160
|
-
{readOnlyButton}
|
|
161
|
-
<label className={labelStyles} htmlFor={id}>
|
|
162
|
-
{label}
|
|
163
|
-
</label>
|
|
164
|
-
</div>
|
|
165
|
-
) : (
|
|
166
|
-
readOnlyButton
|
|
167
|
-
);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return (
|
|
180
|
+
const content = (
|
|
171
181
|
<Select.Root
|
|
172
182
|
value={value}
|
|
173
183
|
onValueChange={onSelect}
|
|
174
184
|
open={open}
|
|
175
185
|
onOpenChange={handleOpenChange}
|
|
176
186
|
>
|
|
177
|
-
|
|
187
|
+
<SelectMenuTrigger
|
|
188
|
+
id={id}
|
|
189
|
+
style={style}
|
|
190
|
+
className={className}
|
|
191
|
+
disabled={disabled}
|
|
192
|
+
icon={icon}
|
|
193
|
+
placeholder={placeholder}
|
|
194
|
+
isOpen={internalOpen}
|
|
195
|
+
onFocus={onFocus}
|
|
196
|
+
onBlur={onBlur}
|
|
197
|
+
label={label}
|
|
198
|
+
/>
|
|
178
199
|
<Select.Portal>
|
|
179
200
|
<Select.Content
|
|
180
201
|
className={styles.contentStyle}
|
|
@@ -184,6 +205,8 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
184
205
|
align="end"
|
|
185
206
|
style={{
|
|
186
207
|
width: "var(--radix-select-trigger-width)",
|
|
208
|
+
maxHeight: "500px",
|
|
209
|
+
...contentStyle,
|
|
187
210
|
}}
|
|
188
211
|
>
|
|
189
212
|
<Select.ScrollUpButton className={scrollButtonStyles}>
|
|
@@ -192,19 +215,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
192
215
|
/>
|
|
193
216
|
</Select.ScrollUpButton>
|
|
194
217
|
<Select.Viewport>
|
|
195
|
-
{
|
|
196
|
-
if (typeof menuItem === "string") {
|
|
197
|
-
return <Select.Separator className={styles.separatorStyle} />;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
const value = menuItem.value ?? "";
|
|
201
|
-
|
|
202
|
-
return (
|
|
203
|
-
<SelectItem key={value} value={value} icon={menuItem.icon}>
|
|
204
|
-
{menuItem.title ?? value}
|
|
205
|
-
</SelectItem>
|
|
206
|
-
);
|
|
207
|
-
})}
|
|
218
|
+
<MenuViewport items={items} Components={Components} />
|
|
208
219
|
</Select.Viewport>
|
|
209
220
|
<Select.ScrollDownButton className={scrollButtonStyles}>
|
|
210
221
|
<ChevronDownIcon />
|
|
@@ -213,33 +224,6 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
213
224
|
</Select.Portal>
|
|
214
225
|
</Select.Root>
|
|
215
226
|
);
|
|
216
|
-
});
|
|
217
227
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
{
|
|
221
|
-
children,
|
|
222
|
-
icon,
|
|
223
|
-
disabled,
|
|
224
|
-
...props
|
|
225
|
-
}: Select.SelectItemProps & { icon?: React.ReactNode },
|
|
226
|
-
forwardedRef: React.ForwardedRef<HTMLDivElement>
|
|
227
|
-
) => {
|
|
228
|
-
return (
|
|
229
|
-
<Select.Item
|
|
230
|
-
className={styles.itemStyle({ disabled })}
|
|
231
|
-
disabled={disabled}
|
|
232
|
-
{...props}
|
|
233
|
-
ref={forwardedRef}
|
|
234
|
-
>
|
|
235
|
-
{icon && (
|
|
236
|
-
<>
|
|
237
|
-
{renderIcon(icon)}
|
|
238
|
-
<Spacer.Horizontal size={8} />
|
|
239
|
-
</>
|
|
240
|
-
)}
|
|
241
|
-
<Select.ItemText>{children}</Select.ItemText>
|
|
242
|
-
</Select.Item>
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
);
|
|
228
|
+
return readOnly ? readOnlyButton : content;
|
|
229
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { useFileDropTarget } from "@noya-app/react-utils";
|
|
2
|
+
import React, { useRef, useState } from "react";
|
|
3
|
+
import { cssVars } from "../theme";
|
|
4
|
+
import { cx } from "../utils/classNames";
|
|
5
|
+
import { updateSelection } from "../utils/selection";
|
|
6
|
+
import { MenuItem } from "./internal/Menu";
|
|
7
|
+
import type { ListViewRootProps } from "./ListView";
|
|
8
|
+
import { ListView } from "./ListView";
|
|
9
|
+
import { TreeView } from "./TreeView";
|
|
10
|
+
|
|
11
|
+
interface SidebarListProps<T, M extends string = string> {
|
|
12
|
+
className?: string;
|
|
13
|
+
items: T[];
|
|
14
|
+
getId: (item: T) => string;
|
|
15
|
+
getName: (item: T) => string;
|
|
16
|
+
getExpanded?: (item: T) => boolean | undefined;
|
|
17
|
+
expandable?: boolean;
|
|
18
|
+
menuItems?: MenuItem<M>[];
|
|
19
|
+
onSelectMenuItem?: (action: M, selectedItems: T[]) => void;
|
|
20
|
+
onRename?: (itemId: string, newName: string) => void;
|
|
21
|
+
renderThumbnail?: (item: T) => React.ReactNode;
|
|
22
|
+
renderHoverAction?: (item: T, selected: boolean) => React.ReactNode;
|
|
23
|
+
renderDetail?: (item: T) => React.ReactNode;
|
|
24
|
+
onSelectionChange?: (selectedItems: T[]) => void;
|
|
25
|
+
scrollable?: boolean;
|
|
26
|
+
itemRoleDescription?: string;
|
|
27
|
+
/** Position of the detail content. Defaults to 'end'. */
|
|
28
|
+
detailPosition?: "end" | "below";
|
|
29
|
+
/** Size of the list items. Defaults to 'medium'. */
|
|
30
|
+
size?: "medium" | "large";
|
|
31
|
+
/** For testing: Override the hover state with a specific item ID */
|
|
32
|
+
testHoveredId?: string;
|
|
33
|
+
/** For testing: Override the renaming state with a specific item ID */
|
|
34
|
+
testRenamingId?: string;
|
|
35
|
+
setExpanded?: (item: T, expanded: boolean) => void;
|
|
36
|
+
acceptsDrop?: ListViewRootProps["acceptsDrop"];
|
|
37
|
+
onMoveItem?: ListViewRootProps["onMoveItem"];
|
|
38
|
+
getDepth?: (item: T) => number;
|
|
39
|
+
onFilesDrop?: (event: React.DragEvent<Element>) => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function SidebarList<T, M extends string = string>({
|
|
43
|
+
className,
|
|
44
|
+
items,
|
|
45
|
+
getId,
|
|
46
|
+
getName,
|
|
47
|
+
expandable,
|
|
48
|
+
getExpanded,
|
|
49
|
+
getDepth,
|
|
50
|
+
setExpanded,
|
|
51
|
+
menuItems,
|
|
52
|
+
onSelectMenuItem,
|
|
53
|
+
onRename,
|
|
54
|
+
renderThumbnail,
|
|
55
|
+
renderHoverAction,
|
|
56
|
+
renderDetail,
|
|
57
|
+
onSelectionChange,
|
|
58
|
+
itemRoleDescription = "clickable item",
|
|
59
|
+
detailPosition = "end",
|
|
60
|
+
size = "medium",
|
|
61
|
+
testHoveredId,
|
|
62
|
+
testRenamingId,
|
|
63
|
+
scrollable,
|
|
64
|
+
acceptsDrop,
|
|
65
|
+
onMoveItem,
|
|
66
|
+
onFilesDrop,
|
|
67
|
+
}: SidebarListProps<T, M>) {
|
|
68
|
+
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
|
69
|
+
const [internalHoveredId, setHoveredId] = useState<string>();
|
|
70
|
+
const [internalRenamingId, setRenamingId] = useState<string>();
|
|
71
|
+
|
|
72
|
+
const hoveredId = internalHoveredId ?? testHoveredId;
|
|
73
|
+
const renamingId = testRenamingId ?? internalRenamingId;
|
|
74
|
+
|
|
75
|
+
const handleSelect = (itemId: string, event?: ListView.ClickInfo) => {
|
|
76
|
+
const newSelectedIds = updateSelection(
|
|
77
|
+
items.map(getId),
|
|
78
|
+
selectedIds,
|
|
79
|
+
itemId,
|
|
80
|
+
event
|
|
81
|
+
);
|
|
82
|
+
setSelectedIds(newSelectedIds);
|
|
83
|
+
|
|
84
|
+
if (onSelectionChange) {
|
|
85
|
+
const selectedItems = items.filter((item) =>
|
|
86
|
+
newSelectedIds.includes(getId(item))
|
|
87
|
+
);
|
|
88
|
+
onSelectionChange(selectedItems);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const handleMenuAction = (action: M) => {
|
|
93
|
+
if (!onSelectMenuItem) return;
|
|
94
|
+
|
|
95
|
+
const selectedItems = items.filter((item) =>
|
|
96
|
+
selectedIds.includes(getId(item))
|
|
97
|
+
);
|
|
98
|
+
onSelectMenuItem(action, selectedItems);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const ViewComponent = expandable ? TreeView : ListView;
|
|
102
|
+
|
|
103
|
+
const fileDropTargetRef = useRef<HTMLDivElement>(null);
|
|
104
|
+
const { isDropTargetActive, dropTargetProps } = useFileDropTarget(
|
|
105
|
+
fileDropTargetRef,
|
|
106
|
+
onFilesDrop
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
<ViewComponent.Root
|
|
111
|
+
variant="bare"
|
|
112
|
+
className={cx(className, isDropTargetActive && "bg-primary-pastel")}
|
|
113
|
+
containerRef={fileDropTargetRef}
|
|
114
|
+
scrollable={scrollable}
|
|
115
|
+
data={items}
|
|
116
|
+
keyExtractor={getId}
|
|
117
|
+
role="listbox"
|
|
118
|
+
aria-orientation="vertical"
|
|
119
|
+
aria-multiselectable={true}
|
|
120
|
+
expandable={expandable}
|
|
121
|
+
sortable={expandable}
|
|
122
|
+
acceptsDrop={acceptsDrop}
|
|
123
|
+
onMoveItem={onMoveItem}
|
|
124
|
+
{...dropTargetProps}
|
|
125
|
+
renderItem={(
|
|
126
|
+
item: T,
|
|
127
|
+
_: number,
|
|
128
|
+
{ isDragging }: { isDragging: boolean }
|
|
129
|
+
) => {
|
|
130
|
+
const id = getId(item);
|
|
131
|
+
const isHovered = hoveredId === id && !isDragging;
|
|
132
|
+
const expanded = getExpanded?.(item);
|
|
133
|
+
const isSelected = selectedIds.includes(id);
|
|
134
|
+
const isRenaming = renamingId === id;
|
|
135
|
+
|
|
136
|
+
const thumbnail = renderThumbnail?.(item);
|
|
137
|
+
const action =
|
|
138
|
+
isHovered && !isRenaming && renderHoverAction?.(item, isSelected);
|
|
139
|
+
const end =
|
|
140
|
+
action ?? (detailPosition === "end" && renderDetail?.(item));
|
|
141
|
+
const below = detailPosition === "below" && renderDetail?.(item);
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<ViewComponent.Row
|
|
145
|
+
id={id}
|
|
146
|
+
key={id}
|
|
147
|
+
role="option"
|
|
148
|
+
sortable={expandable}
|
|
149
|
+
aria-roledescription={itemRoleDescription}
|
|
150
|
+
aria-label={getName(item)}
|
|
151
|
+
aria-selected={isSelected}
|
|
152
|
+
className="cursor-pointer p-1"
|
|
153
|
+
chevronClassName={expandable ? "relative left-2" : undefined}
|
|
154
|
+
hovered={isHovered}
|
|
155
|
+
selected={isSelected}
|
|
156
|
+
expanded={expanded}
|
|
157
|
+
depth={getDepth?.(item)}
|
|
158
|
+
onHoverChange={(hovered: boolean) =>
|
|
159
|
+
setHoveredId(hovered ? id : undefined)
|
|
160
|
+
}
|
|
161
|
+
onPress={(e: ListView.ClickInfo) => handleSelect(id, e)}
|
|
162
|
+
onKeyDown={(e: React.KeyboardEvent) => {
|
|
163
|
+
if (e.key !== "Enter" && e.key !== " ") return;
|
|
164
|
+
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
handleSelect(id, e);
|
|
167
|
+
}}
|
|
168
|
+
menuItems={menuItems}
|
|
169
|
+
onSelectMenuItem={handleMenuAction}
|
|
170
|
+
onMenuOpenChange={(open: boolean) => {
|
|
171
|
+
if (open && !isSelected) {
|
|
172
|
+
handleSelect(id, {
|
|
173
|
+
shiftKey: false,
|
|
174
|
+
altKey: false,
|
|
175
|
+
metaKey: false,
|
|
176
|
+
ctrlKey: false,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
}}
|
|
180
|
+
tabIndex={0}
|
|
181
|
+
onClickChevron={() => {
|
|
182
|
+
if (expanded === undefined) return;
|
|
183
|
+
setExpanded?.(item, !expanded);
|
|
184
|
+
}}
|
|
185
|
+
>
|
|
186
|
+
<div className="flex flex-1 items-center px-2 gap-3">
|
|
187
|
+
{thumbnail && (
|
|
188
|
+
<div
|
|
189
|
+
className={cx(
|
|
190
|
+
"rounded overflow-hidden flex-none flex items-center justify-center",
|
|
191
|
+
size === "medium" ? "w-8 h-8" : "w-16 h-16"
|
|
192
|
+
)}
|
|
193
|
+
style={{
|
|
194
|
+
backgroundColor: cssVars.colors.inputBackground,
|
|
195
|
+
}}
|
|
196
|
+
tabIndex={-1}
|
|
197
|
+
>
|
|
198
|
+
{thumbnail}
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
<div className="flex flex-col flex-1 w-0">
|
|
202
|
+
<div className="flex items-center min-h-[27px] flex-1 gap-2">
|
|
203
|
+
<div className="flex-1 w-0 flex items-center" tabIndex={-1}>
|
|
204
|
+
{isRenaming ? (
|
|
205
|
+
<ViewComponent.EditableRowTitle
|
|
206
|
+
value={getName(item)}
|
|
207
|
+
placeholder="Enter name"
|
|
208
|
+
className={cx(
|
|
209
|
+
"font-medium",
|
|
210
|
+
testRenamingId && "bg-primary-pastel"
|
|
211
|
+
)}
|
|
212
|
+
autoFocus
|
|
213
|
+
onSubmitEditing={(value: string) => {
|
|
214
|
+
if (value !== getName(item) && onRename) {
|
|
215
|
+
onRename(id, value);
|
|
216
|
+
}
|
|
217
|
+
setRenamingId(undefined);
|
|
218
|
+
}}
|
|
219
|
+
/>
|
|
220
|
+
) : (
|
|
221
|
+
<ViewComponent.RowTitle
|
|
222
|
+
className="font-medium"
|
|
223
|
+
onDoubleClick={(e: React.MouseEvent) => {
|
|
224
|
+
e.stopPropagation();
|
|
225
|
+
if (onRename) {
|
|
226
|
+
setRenamingId(id);
|
|
227
|
+
}
|
|
228
|
+
}}
|
|
229
|
+
>
|
|
230
|
+
{getName(item)}
|
|
231
|
+
</ViewComponent.RowTitle>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
{end && (
|
|
235
|
+
<div className="flex items-center gap-2" tabIndex={-1}>
|
|
236
|
+
{end}
|
|
237
|
+
</div>
|
|
238
|
+
)}
|
|
239
|
+
</div>
|
|
240
|
+
{below && (
|
|
241
|
+
<div className="flex items-center" tabIndex={-1}>
|
|
242
|
+
{below}
|
|
243
|
+
</div>
|
|
244
|
+
)}
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
</ViewComponent.Row>
|
|
248
|
+
);
|
|
249
|
+
}}
|
|
250
|
+
/>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
@@ -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