@noya-app/noya-designsystem 0.1.44 → 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 +13 -10
- package/CHANGELOG.md +9 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +320 -78
- package/dist/index.d.ts +320 -78
- package/dist/index.js +2113 -1374
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2000 -1274
- 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 +6 -4
- 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 +104 -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;
|
|
@@ -39,9 +41,6 @@ const readOnlyStyle: CSSProperties = {
|
|
|
39
41
|
textAlign: "left",
|
|
40
42
|
};
|
|
41
43
|
|
|
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
44
|
const scrollButtonStyles = `
|
|
46
45
|
flex
|
|
47
46
|
items-center
|
|
@@ -52,32 +51,109 @@ const scrollButtonStyles = `
|
|
|
52
51
|
hover:text-text
|
|
53
52
|
`;
|
|
54
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
|
+
|
|
55
125
|
export const SelectMenu = memoGeneric(function SelectMenu<
|
|
56
126
|
T extends string = string,
|
|
57
127
|
>({
|
|
58
|
-
id,
|
|
59
128
|
style,
|
|
60
129
|
className,
|
|
61
|
-
|
|
130
|
+
items,
|
|
62
131
|
value,
|
|
63
132
|
onSelect,
|
|
64
133
|
placeholder,
|
|
65
134
|
disabled,
|
|
66
135
|
readOnly,
|
|
67
|
-
label,
|
|
68
136
|
open,
|
|
69
137
|
onFocus,
|
|
70
138
|
onBlur,
|
|
71
139
|
onOpenChange,
|
|
72
140
|
contentStyle,
|
|
141
|
+
...props
|
|
73
142
|
}: Props<T>) {
|
|
74
|
-
const selectedItem =
|
|
143
|
+
const selectedItem = items.find(
|
|
75
144
|
(item): item is RegularMenuItem<T> =>
|
|
76
|
-
|
|
145
|
+
item.type !== "separator" &&
|
|
146
|
+
item.type !== "sectionHeader" &&
|
|
147
|
+
item.value === value
|
|
77
148
|
);
|
|
78
149
|
const icon = selectedItem?.icon;
|
|
79
150
|
const [internalOpen, setInternalOpen] = useState(open);
|
|
80
151
|
|
|
152
|
+
const { label, fieldId: id } = useLabel({
|
|
153
|
+
label: props.label,
|
|
154
|
+
fieldId: props.id,
|
|
155
|
+
});
|
|
156
|
+
|
|
81
157
|
const handleOpenChange = (open: boolean) => {
|
|
82
158
|
if (onOpenChange) {
|
|
83
159
|
onOpenChange(open);
|
|
@@ -91,92 +167,35 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
91
167
|
id={id}
|
|
92
168
|
style={style}
|
|
93
169
|
contentStyle={readOnlyStyle}
|
|
94
|
-
className={cx(`${className} flex-1 focus:z-interactable`)}
|
|
170
|
+
className={cx(`${className} flex-1 focus:z-interactable gap-1.5`)}
|
|
95
171
|
disabled={disabled}
|
|
96
172
|
>
|
|
97
|
-
{icon && (
|
|
98
|
-
<>
|
|
99
|
-
{renderIcon(icon)}
|
|
100
|
-
<Spacer.Horizontal inline size={6} />
|
|
101
|
-
</>
|
|
102
|
-
)}
|
|
173
|
+
{icon && renderIcon(icon)}
|
|
103
174
|
<span className="flex flex-1">{selectedItem?.title ?? value}</span>
|
|
104
175
|
</Button>
|
|
105
176
|
),
|
|
106
177
|
[icon, id, style, className, disabled, value, selectedItem]
|
|
107
178
|
);
|
|
108
179
|
|
|
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 (
|
|
180
|
+
const content = (
|
|
173
181
|
<Select.Root
|
|
174
182
|
value={value}
|
|
175
183
|
onValueChange={onSelect}
|
|
176
184
|
open={open}
|
|
177
185
|
onOpenChange={handleOpenChange}
|
|
178
186
|
>
|
|
179
|
-
|
|
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
|
+
/>
|
|
180
199
|
<Select.Portal>
|
|
181
200
|
<Select.Content
|
|
182
201
|
className={styles.contentStyle}
|
|
@@ -196,19 +215,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
196
215
|
/>
|
|
197
216
|
</Select.ScrollUpButton>
|
|
198
217
|
<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
|
-
})}
|
|
218
|
+
<MenuViewport items={items} Components={Components} />
|
|
212
219
|
</Select.Viewport>
|
|
213
220
|
<Select.ScrollDownButton className={scrollButtonStyles}>
|
|
214
221
|
<ChevronDownIcon />
|
|
@@ -217,33 +224,6 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
217
224
|
</Select.Portal>
|
|
218
225
|
</Select.Root>
|
|
219
226
|
);
|
|
220
|
-
});
|
|
221
227
|
|
|
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
|
-
);
|
|
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
|
@@ -7,6 +7,7 @@ import React, {
|
|
|
7
7
|
useEffect,
|
|
8
8
|
useRef,
|
|
9
9
|
} from "react";
|
|
10
|
+
import { useLabel } from "../hooks/useLabel";
|
|
10
11
|
import { cx } from "../utils/classNames";
|
|
11
12
|
|
|
12
13
|
export const useAutoResize = (value: string | undefined) => {
|
|
@@ -39,6 +40,7 @@ export const TextArea = memo(
|
|
|
39
40
|
} & React.TextareaHTMLAttributes<HTMLTextAreaElement>,
|
|
40
41
|
forwardedRef: React.ForwardedRef<HTMLTextAreaElement>
|
|
41
42
|
) {
|
|
43
|
+
const { fieldId: id } = useLabel({ fieldId: rest.id });
|
|
42
44
|
const autoResizeRef = useAutoResize(
|
|
43
45
|
autoResize ? value || rest.placeholder || "" : undefined
|
|
44
46
|
);
|
|
@@ -67,8 +69,9 @@ export const TextArea = memo(
|
|
|
67
69
|
`font-sans text-heading5 font-normal text-text bg-input-background flex-1 py-1 px-1.5 border-none outline-none min-h-6 w-full rounded resize-none placeholder:text-text-disabled focus:ring-2 focus:ring-primary read-only:text-text-disabled focus:z-interactable transition-all`,
|
|
68
70
|
className
|
|
69
71
|
)}
|
|
70
|
-
ref={handleRef}
|
|
71
72
|
{...rest}
|
|
73
|
+
id={id}
|
|
74
|
+
ref={handleRef}
|
|
72
75
|
onChange={handleChange}
|
|
73
76
|
value={value}
|
|
74
77
|
/>
|