@noya-app/noya-designsystem 0.1.77 → 0.1.78
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 +20 -12
- package/CHANGELOG.md +9 -0
- package/dist/chunk-D57E6H3M.mjs +36 -0
- package/dist/chunk-D57E6H3M.mjs.map +1 -0
- package/dist/emojis.d.mts +1 -0
- package/dist/emojis.d.ts +1 -0
- package/dist/emojis.js +31 -0
- package/dist/emojis.js.map +1 -0
- package/dist/emojis.mjs +8 -0
- package/dist/emojis.mjs.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +142 -90
- package/dist/index.d.ts +142 -90
- package/dist/index.js +3171 -1745
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3349 -1918
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -7
- package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
- package/src/components/Avatar.tsx +10 -10
- package/src/components/Banner.tsx +1 -1
- package/src/components/BaseToolbar.tsx +1 -1
- package/src/components/Button.tsx +2 -1
- package/src/components/Chip.tsx +1 -1
- package/src/components/ColorSwatchControl.tsx +22 -14
- package/src/components/CommandPalette.tsx +10 -6
- package/src/components/ContextMenu.tsx +249 -38
- package/src/components/Dialog.tsx +70 -67
- package/src/components/Drawer.tsx +56 -17
- package/src/components/DropdownMenu.tsx +305 -46
- package/src/components/EditableText.tsx +1 -1
- package/src/components/EmojiPicker.tsx +645 -0
- package/src/components/GridView.tsx +5 -1
- package/src/components/IVirtualizedList.tsx +21 -1
- package/src/components/InputField.tsx +2 -10
- package/src/components/InspectorContainer.tsx +4 -2
- package/src/components/Message.tsx +5 -16
- package/src/components/Popover.tsx +73 -107
- package/src/components/Progress.tsx +18 -18
- package/src/components/ScrollArea.tsx +66 -31
- package/src/components/ScrollableSidebar.tsx +1 -1
- package/src/components/SegmentedControl.tsx +166 -38
- package/src/components/SelectMenu.tsx +193 -101
- package/src/components/Slider.tsx +40 -38
- package/src/components/Switch.tsx +4 -4
- package/src/components/TextArea.tsx +1 -1
- package/src/components/Toast.tsx +99 -26
- package/src/components/Toolbar.tsx +114 -16
- package/src/components/Tooltip.tsx +18 -8
- package/src/components/Virtualized.tsx +193 -14
- package/src/components/VisuallyHidden.tsx +20 -0
- package/src/components/__tests__/Virtualized.math.test.ts +426 -1
- package/src/components/__tests__/Virtualized.test.tsx +129 -1
- package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
- package/src/components/internal/Menu.tsx +4 -0
- package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
- package/src/components/listView/ListViewRoot.tsx +5 -1
- package/src/components/resizablePanels/Panel.tsx +94 -0
- package/src/components/resizablePanels/PanelGroup.tsx +498 -0
- package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
- package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
- package/src/components/resizablePanels/index.ts +7 -0
- package/src/components/resizablePanels/types.ts +65 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
- package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
- package/src/components/workspace/WorkspaceLayout.tsx +28 -58
- package/src/components/workspace/types.ts +6 -4
- package/src/contexts/DialogContext.tsx +15 -24
- package/src/emojis.ts +1 -0
- package/src/hooks/useTriggerToggle.ts +95 -0
- package/src/index.css +2 -0
- package/src/index.tsx +1 -2
- package/src/theme/proseTheme.ts +2 -0
- package/src/utils/mergeProps.ts +57 -0
- package/src/utils/skinTone.ts +90 -0
- package/tsup.config.ts +1 -1
- package/src/__tests__/workspaceLayout.test.ts +0 -281
- package/src/components/ScrollArea2.tsx +0 -76
- package/src/components/internal/MenuViewport.tsx +0 -178
- package/src/components/internal/SelectItem.tsx +0 -138
- package/src/components/workspace/panelStorage.ts +0 -216
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { Toggle } from "@base-ui/react/toggle";
|
|
4
|
+
import { ToggleGroup } from "@base-ui/react/toggle-group";
|
|
3
5
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
4
|
-
import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
|
|
5
6
|
import React, {
|
|
6
7
|
createContext,
|
|
7
8
|
forwardRef,
|
|
8
9
|
ReactNode,
|
|
9
10
|
useCallback,
|
|
10
11
|
useContext,
|
|
12
|
+
useLayoutEffect,
|
|
11
13
|
useMemo,
|
|
14
|
+
useRef,
|
|
15
|
+
useState,
|
|
12
16
|
} from "react";
|
|
13
17
|
import { useLabel } from "../hooks/useLabel";
|
|
14
18
|
import { cx } from "../utils/classNames";
|
|
@@ -22,12 +26,16 @@ type SegmentedControlVariant = "default" | "tabs" | "buttons";
|
|
|
22
26
|
type SegmentedControlContextValue = {
|
|
23
27
|
/** @default primary */
|
|
24
28
|
colorScheme?: SegmentedControlColorScheme;
|
|
29
|
+
onSelect?: (value: string) => void;
|
|
30
|
+
itemWidth?: SegmentedControlItemWidth;
|
|
25
31
|
};
|
|
26
32
|
|
|
27
33
|
const SegmentedControlContext = createContext<SegmentedControlContextValue>({
|
|
28
34
|
colorScheme: "primary",
|
|
29
35
|
});
|
|
30
36
|
|
|
37
|
+
type SegmentedControlItemWidth = "auto" | "equal" | "fit";
|
|
38
|
+
|
|
31
39
|
export interface SegmentedControlProps<T extends string> {
|
|
32
40
|
id?: string;
|
|
33
41
|
value?: T;
|
|
@@ -39,6 +47,14 @@ export interface SegmentedControlProps<T extends string> {
|
|
|
39
47
|
className?: string;
|
|
40
48
|
style?: React.CSSProperties;
|
|
41
49
|
variant?: SegmentedControlVariant;
|
|
50
|
+
/**
|
|
51
|
+
* Controls item width distribution.
|
|
52
|
+
* - "auto": Items try to be equal width, but won't shrink below their content
|
|
53
|
+
* - "equal": Items are always equal width (may truncate content)
|
|
54
|
+
* - "fit": Items fit their content
|
|
55
|
+
* @default "auto"
|
|
56
|
+
*/
|
|
57
|
+
itemWidth?: SegmentedControlItemWidth;
|
|
42
58
|
}
|
|
43
59
|
|
|
44
60
|
export type SegmentedControlItemProps<T extends string> = {
|
|
@@ -67,17 +83,31 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
67
83
|
}: SegmentedControlItemProps<T>,
|
|
68
84
|
forwardedRef: React.ForwardedRef<HTMLButtonElement>
|
|
69
85
|
) {
|
|
70
|
-
const { colorScheme } =
|
|
86
|
+
const { colorScheme, onSelect, itemWidth = "auto" } =
|
|
87
|
+
useContext(SegmentedControlContext);
|
|
88
|
+
|
|
89
|
+
const handlePointerDown = useCallback(
|
|
90
|
+
(event: React.PointerEvent) => {
|
|
91
|
+
if (disabled) return;
|
|
92
|
+
onSelect?.((value ?? "").toString());
|
|
93
|
+
// Prevent default to avoid double-triggering on click
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
},
|
|
96
|
+
[disabled, onSelect, value]
|
|
97
|
+
);
|
|
71
98
|
|
|
72
99
|
const itemElement = (
|
|
73
|
-
<
|
|
100
|
+
<Toggle
|
|
74
101
|
ref={forwardedRef}
|
|
75
102
|
value={(value ?? "").toString()}
|
|
76
103
|
disabled={disabled}
|
|
104
|
+
onPointerDown={handlePointerDown}
|
|
77
105
|
className={cx(
|
|
78
106
|
"n-group",
|
|
79
107
|
"n-flex n-items-center n-justify-center",
|
|
80
|
-
"n-font-sans n-text-button n-relative n-
|
|
108
|
+
"n-font-sans n-text-button n-relative n-appearance-none focus:n-outline-none n-transition-colors",
|
|
109
|
+
// item width distribution (only for default variant with fit mode)
|
|
110
|
+
variant === "default" && itemWidth === "fit" && "n-flex-none",
|
|
81
111
|
// text color
|
|
82
112
|
variant === "buttons"
|
|
83
113
|
? "n-text-button-text"
|
|
@@ -88,36 +118,44 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
88
118
|
: variant === "buttons"
|
|
89
119
|
? "n-font-medium"
|
|
90
120
|
: "n-font-normal",
|
|
91
|
-
// focus ring
|
|
121
|
+
// focus ring (visible for keyboard users only)
|
|
92
122
|
variant === "tabs"
|
|
93
123
|
? ""
|
|
94
124
|
: variant === "buttons"
|
|
95
|
-
? "focus:n-ring-2 focus:n-ring-primary focus:n-shadow-[0_0_0_1px_var(--n-popover-background)_inset] n-transition-all"
|
|
125
|
+
? "focus-visible:n-ring-2 focus-visible:n-ring-primary focus-visible:n-shadow-[0_0_0_1px_var(--n-popover-background)_inset] n-transition-all"
|
|
96
126
|
: colorScheme === "secondary"
|
|
97
|
-
? "focus:n-ring-2 focus:n-ring-secondary focus:n-ring-offset-1"
|
|
98
|
-
: "focus:n-ring-2 focus:n-ring-primary focus:n-ring-offset-1",
|
|
127
|
+
? "focus-visible:n-ring-2 focus-visible:n-ring-secondary focus-visible:n-ring-offset-1"
|
|
128
|
+
: "focus-visible:n-ring-2 focus-visible:n-ring-primary focus-visible:n-ring-offset-1",
|
|
99
129
|
// checked
|
|
100
130
|
variant === "tabs"
|
|
101
|
-
? "
|
|
131
|
+
? "data-[pressed]:n-text-primary"
|
|
102
132
|
: variant === "buttons"
|
|
103
|
-
? "active:n-text-button-text-active active:n-bg-button-background-active
|
|
104
|
-
:
|
|
105
|
-
?
|
|
106
|
-
|
|
107
|
-
? "
|
|
108
|
-
:
|
|
109
|
-
|
|
110
|
-
|
|
133
|
+
? "active:n-text-button-text-active active:n-bg-button-background-active data-[pressed]:n-text-button-text-active data-[pressed]:n-bg-button-background-active"
|
|
134
|
+
: variant === "default"
|
|
135
|
+
? // default variant: only text color, background handled by sliding indicator
|
|
136
|
+
colorScheme === "secondary"
|
|
137
|
+
? "data-[pressed]:n-text-white"
|
|
138
|
+
: colorScheme === "primary"
|
|
139
|
+
? "data-[pressed]:n-text-white"
|
|
140
|
+
: "data-[pressed]:n-text-segmented-control-item-active-text"
|
|
141
|
+
: colorScheme === "secondary"
|
|
142
|
+
? "data-[pressed]:n-bg-secondary data-[pressed]:n-text-white"
|
|
143
|
+
: colorScheme === "primary"
|
|
144
|
+
? "data-[pressed]:n-bg-primary data-[pressed]:n-text-white"
|
|
145
|
+
: "data-[pressed]:n-bg-segmented-control-item-active-background data-[pressed]:n-text-segmented-control-item-active-text",
|
|
146
|
+
"focus-visible:n-z-interactable",
|
|
147
|
+
// checked default (all colorSchemes) - hide separators near selected
|
|
111
148
|
variant === "default" &&
|
|
112
|
-
"
|
|
149
|
+
"data-[pressed]:after:n-hidden [&:has(+[data-pressed])]:after:n-hidden",
|
|
113
150
|
// hover
|
|
114
151
|
variant === "buttons" && "hover:n-bg-button-background-hover",
|
|
115
152
|
// spacing and borders
|
|
153
|
+
"n-px-input-padding-x",
|
|
116
154
|
variant === "default"
|
|
117
|
-
? "n-
|
|
155
|
+
? "n-rounded after:n-content-[''] after:n-absolute after:n-right-0 after:n-top-0.5 after:n-bottom-0.5 after:n-w-px after:n-bg-divider last:after:n-hidden"
|
|
118
156
|
: variant === "buttons"
|
|
119
|
-
? "n-
|
|
120
|
-
: "n-
|
|
157
|
+
? "n-rounded-sm n-max-w-fit"
|
|
158
|
+
: "n-border-y-2 n-border-y-transparent data-[pressed]:n-border-b-primary",
|
|
121
159
|
// icon only buttons
|
|
122
160
|
variant === "buttons" && title === "" && icon && "n-min-w-input-height",
|
|
123
161
|
className
|
|
@@ -134,7 +172,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
|
|
|
134
172
|
{icon && renderIcon(icon)}
|
|
135
173
|
{title}
|
|
136
174
|
</span>
|
|
137
|
-
</
|
|
175
|
+
</Toggle>
|
|
138
176
|
);
|
|
139
177
|
|
|
140
178
|
return tooltip ? (
|
|
@@ -156,42 +194,132 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
|
|
|
156
194
|
variant = "default",
|
|
157
195
|
className,
|
|
158
196
|
style,
|
|
197
|
+
itemWidth = "auto",
|
|
159
198
|
}: SegmentedControlProps<T>) {
|
|
160
|
-
const contextValue = useMemo(() => ({ colorScheme }), [colorScheme]);
|
|
161
199
|
const { fieldId: id } = useLabel({ fieldId: idProp });
|
|
162
200
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
201
|
+
// Track if we should animate (skip initial mount)
|
|
202
|
+
const hasInitialized = useRef(false);
|
|
203
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
204
|
+
const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
|
|
205
|
+
|
|
206
|
+
// Indicator position based on measured item dimensions
|
|
207
|
+
const [indicatorStyle, setIndicatorStyle] = useState<{
|
|
208
|
+
left: number;
|
|
209
|
+
width: number;
|
|
210
|
+
} | null>(null);
|
|
211
|
+
|
|
212
|
+
const selectedIndex = useMemo(() => {
|
|
213
|
+
if (!value) return -1;
|
|
214
|
+
return items.findIndex((item) => item.value === value);
|
|
215
|
+
}, [value, items]);
|
|
216
|
+
|
|
217
|
+
useLayoutEffect(() => {
|
|
218
|
+
if (variant !== "default" || selectedIndex < 0) {
|
|
219
|
+
setIndicatorStyle(null);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const container = containerRef.current;
|
|
224
|
+
const selectedItem = itemRefs.current[selectedIndex];
|
|
225
|
+
|
|
226
|
+
if (!container || !selectedItem) {
|
|
227
|
+
setIndicatorStyle(null);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const containerRect = container.getBoundingClientRect();
|
|
232
|
+
const itemRect = selectedItem.getBoundingClientRect();
|
|
233
|
+
|
|
234
|
+
setIndicatorStyle({
|
|
235
|
+
left: itemRect.left - containerRect.left,
|
|
236
|
+
width: itemRect.width,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// Enable animation after first measurement
|
|
240
|
+
hasInitialized.current = true;
|
|
241
|
+
}, [selectedIndex, variant, items.length]);
|
|
242
|
+
|
|
243
|
+
const handleSelect = useCallback(
|
|
244
|
+
(newValue: string) => {
|
|
245
|
+
if (!allowEmpty && !newValue) return;
|
|
246
|
+
onValueChange?.(newValue as T);
|
|
167
247
|
},
|
|
168
248
|
[allowEmpty, onValueChange]
|
|
169
249
|
);
|
|
170
250
|
|
|
251
|
+
const handleGroupValueChange = useCallback(
|
|
252
|
+
(groupValue: string[]) => handleSelect(groupValue[0] ?? ""),
|
|
253
|
+
[handleSelect]
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
const contextValue = useMemo(
|
|
257
|
+
() => ({ colorScheme, onSelect: handleSelect, itemWidth }),
|
|
258
|
+
[colorScheme, handleSelect, itemWidth]
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
const indicatorBgClass =
|
|
262
|
+
colorScheme === "secondary"
|
|
263
|
+
? "n-bg-secondary"
|
|
264
|
+
: colorScheme === "primary"
|
|
265
|
+
? "n-bg-primary"
|
|
266
|
+
: "n-bg-segmented-control-item-active-background";
|
|
267
|
+
|
|
171
268
|
return (
|
|
172
269
|
<SegmentedControlContext.Provider value={contextValue}>
|
|
173
|
-
<
|
|
270
|
+
<ToggleGroup
|
|
271
|
+
ref={containerRef}
|
|
174
272
|
id={id}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
onValueChange={handleValueChange}
|
|
273
|
+
value={value ? [value] : []}
|
|
274
|
+
onValueChange={handleGroupValueChange}
|
|
178
275
|
className={cx(
|
|
179
|
-
|
|
276
|
+
"n-appearance-none n-relative n-outline-none",
|
|
180
277
|
variant === "default" &&
|
|
181
|
-
"
|
|
182
|
-
variant === "
|
|
183
|
-
|
|
278
|
+
(itemWidth === "fit" ? "n-flex n-w-fit" : "n-grid"),
|
|
279
|
+
variant === "default" &&
|
|
280
|
+
"n-min-h-input-height n-bg-input-background n-rounded n-py-0.5 n-px-0.5",
|
|
281
|
+
variant !== "default" && "n-flex",
|
|
282
|
+
variant === "tabs" && "n-gap-1.5 n-min-h-[33px]",
|
|
283
|
+
variant === "buttons" && "n-gap-1.5 n-min-h-input-height",
|
|
184
284
|
className
|
|
185
285
|
)}
|
|
186
286
|
style={{
|
|
187
287
|
...style,
|
|
188
|
-
|
|
288
|
+
...(variant === "default" &&
|
|
289
|
+
itemWidth !== "fit" && {
|
|
290
|
+
gridTemplateColumns:
|
|
291
|
+
itemWidth === "equal"
|
|
292
|
+
? `repeat(${items.length}, 1fr)`
|
|
293
|
+
: `repeat(${items.length}, minmax(min-content, 1fr))`,
|
|
294
|
+
}),
|
|
189
295
|
}}
|
|
190
296
|
>
|
|
297
|
+
{/* Sliding indicator for default variant */}
|
|
298
|
+
{variant === "default" && indicatorStyle && (
|
|
299
|
+
<div
|
|
300
|
+
className={cx(
|
|
301
|
+
"n-absolute n-top-0.5 n-bottom-0.5 n-rounded n-shadow-segment n-pointer-events-none",
|
|
302
|
+
indicatorBgClass,
|
|
303
|
+
hasInitialized.current &&
|
|
304
|
+
"n-transition-[left,width] n-duration-200 n-ease-out"
|
|
305
|
+
)}
|
|
306
|
+
style={{
|
|
307
|
+
left: indicatorStyle.left,
|
|
308
|
+
width: indicatorStyle.width,
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
)}
|
|
191
312
|
{items.map((item, index) => (
|
|
192
|
-
<SegmentedControlItem
|
|
313
|
+
<SegmentedControlItem
|
|
314
|
+
key={index}
|
|
315
|
+
ref={(el) => {
|
|
316
|
+
itemRefs.current[index] = el;
|
|
317
|
+
}}
|
|
318
|
+
{...item}
|
|
319
|
+
variant={variant}
|
|
320
|
+
/>
|
|
193
321
|
))}
|
|
194
|
-
</
|
|
322
|
+
</ToggleGroup>
|
|
195
323
|
</SegmentedControlContext.Provider>
|
|
196
324
|
);
|
|
197
325
|
});
|
|
@@ -4,13 +4,14 @@ import {
|
|
|
4
4
|
ChevronDownIcon,
|
|
5
5
|
ChevronUpIcon,
|
|
6
6
|
DropdownChevronIcon,
|
|
7
|
+
CheckIcon,
|
|
7
8
|
} from "@noya-app/noya-icons";
|
|
8
9
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
9
|
-
import { Select } from "
|
|
10
|
-
import { type SelectProps } from "@radix-ui/react-select";
|
|
10
|
+
import { Select } from "@base-ui/react/select";
|
|
11
11
|
import React, {
|
|
12
12
|
CSSProperties,
|
|
13
13
|
FocusEventHandler,
|
|
14
|
+
useCallback,
|
|
14
15
|
useMemo,
|
|
15
16
|
useState,
|
|
16
17
|
} from "react";
|
|
@@ -23,15 +24,19 @@ import {
|
|
|
23
24
|
} from "../contexts/PortalScopeContext";
|
|
24
25
|
import { getInsetEndStyles } from "../utils/inputs";
|
|
25
26
|
import { Button } from "./Button";
|
|
26
|
-
import { renderIcon } from "./Icons";
|
|
27
|
+
import { renderIcon, Icon, IconName } from "./Icons";
|
|
27
28
|
import {
|
|
28
29
|
isSelectableMenuItem,
|
|
29
30
|
MenuItem,
|
|
30
31
|
SelectableMenuItem,
|
|
31
32
|
styles,
|
|
33
|
+
SectionHeaderMenuItem as SectionHeaderMenuItemComponent,
|
|
34
|
+
CHECKBOX_INDENT_WIDTH,
|
|
35
|
+
CHECKBOX_RIGHT_INSET,
|
|
36
|
+
CHECKBOX_WIDTH,
|
|
32
37
|
} from "./internal/Menu";
|
|
33
|
-
import { MenuComponents, MenuViewport } from "./internal/MenuViewport";
|
|
34
38
|
import { Label } from "./Label";
|
|
39
|
+
import { Spacer } from "./Spacer";
|
|
35
40
|
|
|
36
41
|
type Props<T extends string> = {
|
|
37
42
|
id?: string;
|
|
@@ -47,7 +52,9 @@ type Props<T extends string> = {
|
|
|
47
52
|
onFocus?: FocusEventHandler;
|
|
48
53
|
onBlur?: FocusEventHandler;
|
|
49
54
|
contentStyle?: React.CSSProperties;
|
|
50
|
-
|
|
55
|
+
open?: boolean;
|
|
56
|
+
onOpenChange?: (open: boolean) => void;
|
|
57
|
+
};
|
|
51
58
|
|
|
52
59
|
const readOnlyStyle: CSSProperties = {
|
|
53
60
|
justifyContent: "flex-start",
|
|
@@ -59,75 +66,118 @@ const scrollButtonStyles =
|
|
|
59
66
|
|
|
60
67
|
const insetEndStyles = getInsetEndStyles();
|
|
61
68
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
const itemStyle = (state: { disabled: boolean; highlighted: boolean }) =>
|
|
70
|
+
cx(
|
|
71
|
+
"n-flex-none n-select-none n-cursor-pointer n-rounded",
|
|
72
|
+
"n-py-1.5 n-px-2",
|
|
73
|
+
"n-transition-colors",
|
|
74
|
+
"n-flex n-items-center",
|
|
75
|
+
"n-font-sans n-text-button n-font-medium",
|
|
76
|
+
"focus:n-outline-none",
|
|
77
|
+
state.disabled && "n-text-text-disabled",
|
|
78
|
+
state.highlighted &&
|
|
79
|
+
"n-text-selected-list-item-text n-bg-selected-list-item-background"
|
|
80
|
+
);
|
|
74
81
|
|
|
75
|
-
|
|
76
|
-
style,
|
|
77
|
-
className,
|
|
78
|
-
disabled,
|
|
82
|
+
function SelectItemContent({
|
|
79
83
|
icon,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
90
|
-
const labelType = useLabelType();
|
|
91
|
-
|
|
84
|
+
checked,
|
|
85
|
+
indented,
|
|
86
|
+
children,
|
|
87
|
+
}: {
|
|
88
|
+
icon?: React.ReactNode;
|
|
89
|
+
checked?: boolean;
|
|
90
|
+
indented?: boolean;
|
|
91
|
+
children: React.ReactNode;
|
|
92
|
+
}) {
|
|
92
93
|
return (
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
)}
|
|
102
|
-
disabled={disabled}
|
|
103
|
-
>
|
|
104
|
-
{icon && <span className="n-pr-1.5">{renderIcon(icon)}</span>}
|
|
105
|
-
{label && labelType === "inset" && (
|
|
106
|
-
<div className={insetEndStyles}>
|
|
107
|
-
<Label className="n-pr-4 n-text-text-disabled" htmlFor={id}>
|
|
108
|
-
{label}
|
|
109
|
-
</Label>
|
|
110
|
-
</div>
|
|
111
|
-
)}
|
|
112
|
-
<span className="n-flex-1 n-flex">
|
|
113
|
-
<Select.Value placeholder={placeholder} />
|
|
94
|
+
<>
|
|
95
|
+
{indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
|
|
96
|
+
{checked ? (
|
|
97
|
+
<span
|
|
98
|
+
className="n-flex n-items-center n-relative -n-left-2.5"
|
|
99
|
+
style={{ marginRight: -CHECKBOX_RIGHT_INSET }}
|
|
100
|
+
>
|
|
101
|
+
<CheckIcon />
|
|
114
102
|
</span>
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
103
|
+
) : indented ? (
|
|
104
|
+
<Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
|
|
105
|
+
) : null}
|
|
106
|
+
{icon &&
|
|
107
|
+
(typeof icon === "string" ? (
|
|
108
|
+
<Icon
|
|
109
|
+
name={icon as IconName}
|
|
110
|
+
className="n-mr-2 n-w-4 n-h-4 n-flex-shrink-0"
|
|
111
|
+
/>
|
|
112
|
+
) : (
|
|
113
|
+
<span className="n-mr-2 n-w-4 n-h-4 n-flex-shrink-0 n-flex n-items-center n-justify-center">
|
|
114
|
+
{icon}
|
|
115
|
+
</span>
|
|
116
|
+
))}
|
|
117
|
+
<div className="n-flex n-items-center n-flex-1">{children}</div>
|
|
118
|
+
</>
|
|
123
119
|
);
|
|
124
|
-
}
|
|
120
|
+
}
|
|
125
121
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
function SelectMenuItems<T extends string>({
|
|
123
|
+
items,
|
|
124
|
+
hasCheckedItem,
|
|
125
|
+
}: {
|
|
126
|
+
items: MenuItem<T>[];
|
|
127
|
+
hasCheckedItem: boolean;
|
|
128
|
+
}) {
|
|
129
|
+
return (
|
|
130
|
+
<>
|
|
131
|
+
{items.map((item, index) => {
|
|
132
|
+
switch (item.type) {
|
|
133
|
+
case "separator":
|
|
134
|
+
return (
|
|
135
|
+
<Select.Separator
|
|
136
|
+
key={index}
|
|
137
|
+
className={styles.separatorStyle}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
case "sectionHeader":
|
|
142
|
+
return (
|
|
143
|
+
<SectionHeaderMenuItemComponent
|
|
144
|
+
isFirst={index === 0}
|
|
145
|
+
key={item.id}
|
|
146
|
+
{...item}
|
|
147
|
+
indented={hasCheckedItem}
|
|
148
|
+
/>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
case "popover":
|
|
152
|
+
case "submenu":
|
|
153
|
+
// Select doesn't support submenus/popovers
|
|
154
|
+
return null;
|
|
155
|
+
|
|
156
|
+
default:
|
|
157
|
+
return (
|
|
158
|
+
<div key={item.value} className="n-px-1">
|
|
159
|
+
<Select.Item
|
|
160
|
+
value={item.value}
|
|
161
|
+
disabled={item.disabled}
|
|
162
|
+
className={itemStyle}
|
|
163
|
+
>
|
|
164
|
+
<SelectItemContent
|
|
165
|
+
icon={item.icon}
|
|
166
|
+
checked={item.checked}
|
|
167
|
+
indented={hasCheckedItem}
|
|
168
|
+
>
|
|
169
|
+
<Select.ItemText>
|
|
170
|
+
{item.title ?? item.value}
|
|
171
|
+
</Select.ItemText>
|
|
172
|
+
</SelectItemContent>
|
|
173
|
+
</Select.Item>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
})}
|
|
178
|
+
</>
|
|
179
|
+
);
|
|
180
|
+
}
|
|
131
181
|
|
|
132
182
|
export const SelectMenu = memoGeneric(function SelectMenu<
|
|
133
183
|
T extends string = string,
|
|
@@ -154,19 +204,35 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
154
204
|
isSelectableMenuItem(item) && item.value === value
|
|
155
205
|
);
|
|
156
206
|
const icon = selectedItem?.icon;
|
|
157
|
-
const [internalOpen, setInternalOpen] = useState(open);
|
|
207
|
+
const [internalOpen, setInternalOpen] = useState(open ?? false);
|
|
158
208
|
|
|
159
209
|
const { label, fieldId: id } = useLabel({
|
|
160
210
|
label: props.label,
|
|
161
211
|
fieldId: props.id,
|
|
162
212
|
});
|
|
163
213
|
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
214
|
+
const labelType = useLabelType();
|
|
215
|
+
|
|
216
|
+
const handleOpenChange = useCallback(
|
|
217
|
+
(newOpen: boolean) => {
|
|
218
|
+
onOpenChange?.(newOpen);
|
|
219
|
+
setInternalOpen(newOpen);
|
|
220
|
+
},
|
|
221
|
+
[onOpenChange]
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
const handleValueChange = useCallback(
|
|
225
|
+
(newValue: T | null) => {
|
|
226
|
+
if (newValue !== null) {
|
|
227
|
+
onSelect?.(newValue);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
[onSelect]
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const hasCheckedItem = items.some(
|
|
234
|
+
(item) => isSelectableMenuItem(item) && item.checked
|
|
235
|
+
);
|
|
170
236
|
|
|
171
237
|
const readOnlyButton = useMemo(
|
|
172
238
|
() => (
|
|
@@ -186,7 +252,7 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
186
252
|
|
|
187
253
|
const contentStyle = useMemo(() => {
|
|
188
254
|
return {
|
|
189
|
-
width: "var(--
|
|
255
|
+
width: "var(--anchor-width)",
|
|
190
256
|
minWidth: "max-content",
|
|
191
257
|
maxHeight: "500px",
|
|
192
258
|
...contentStyleProp,
|
|
@@ -196,44 +262,70 @@ export const SelectMenu = memoGeneric(function SelectMenu<
|
|
|
196
262
|
const content = (
|
|
197
263
|
<Select.Root
|
|
198
264
|
value={value}
|
|
199
|
-
onValueChange={
|
|
265
|
+
onValueChange={handleValueChange}
|
|
200
266
|
open={open}
|
|
201
267
|
onOpenChange={handleOpenChange}
|
|
268
|
+
disabled={disabled}
|
|
202
269
|
>
|
|
203
|
-
<
|
|
270
|
+
<Select.Trigger
|
|
204
271
|
id={id}
|
|
205
|
-
style={style}
|
|
206
|
-
className={className}
|
|
207
|
-
disabled={disabled}
|
|
208
|
-
icon={icon}
|
|
209
|
-
placeholder={placeholder}
|
|
210
|
-
isOpen={internalOpen}
|
|
211
272
|
onFocus={onFocus}
|
|
212
273
|
onBlur={onBlur}
|
|
213
|
-
|
|
214
|
-
|
|
274
|
+
render={
|
|
275
|
+
<Button
|
|
276
|
+
style={style}
|
|
277
|
+
className={cx(
|
|
278
|
+
className ?? "n-flex n-w-full",
|
|
279
|
+
"n-flex-1 focus:n-z-interactable n-relative",
|
|
280
|
+
internalOpen && "n-ring-2 n-ring-primary"
|
|
281
|
+
)}
|
|
282
|
+
disabled={disabled}
|
|
283
|
+
/>
|
|
284
|
+
}
|
|
285
|
+
>
|
|
286
|
+
{icon && <span className="n-pr-1.5">{renderIcon(icon)}</span>}
|
|
287
|
+
{label && labelType === "inset" && (
|
|
288
|
+
<div className={insetEndStyles}>
|
|
289
|
+
<Label className="n-pr-4 n-text-text-disabled" htmlFor={id}>
|
|
290
|
+
{label}
|
|
291
|
+
</Label>
|
|
292
|
+
</div>
|
|
293
|
+
)}
|
|
294
|
+
<span className="n-flex-1 n-flex">
|
|
295
|
+
<Select.Value>
|
|
296
|
+
{selectedItem?.title ?? value ?? placeholder}
|
|
297
|
+
</Select.Value>
|
|
298
|
+
</span>
|
|
299
|
+
<Select.Icon>
|
|
300
|
+
<DropdownChevronIcon
|
|
301
|
+
className={cx(
|
|
302
|
+
"n-transition-transform n-duration-100 n-ml-1.5",
|
|
303
|
+
internalOpen ? "n-rotate-180" : "n-rotate-0"
|
|
304
|
+
)}
|
|
305
|
+
/>
|
|
306
|
+
</Select.Icon>
|
|
307
|
+
</Select.Trigger>
|
|
215
308
|
<Select.Portal>
|
|
216
|
-
<Select.
|
|
309
|
+
<Select.Positioner
|
|
217
310
|
{...portalScopeProps(portalScopeId)}
|
|
218
|
-
className={styles.contentStyle}
|
|
219
|
-
position="popper"
|
|
220
311
|
sideOffset={6}
|
|
221
312
|
collisionPadding={8}
|
|
222
313
|
align="center"
|
|
223
314
|
style={contentStyle}
|
|
315
|
+
className="n-z-menu"
|
|
224
316
|
>
|
|
225
|
-
<Select.
|
|
226
|
-
<
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
<
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
</Select.
|
|
236
|
-
</Select.
|
|
317
|
+
<Select.Popup className={styles.contentStyle}>
|
|
318
|
+
<Select.ScrollUpArrow className={scrollButtonStyles}>
|
|
319
|
+
<ChevronUpIcon
|
|
320
|
+
className={cx("n-transition-transform n-duration-100")}
|
|
321
|
+
/>
|
|
322
|
+
</Select.ScrollUpArrow>
|
|
323
|
+
<SelectMenuItems items={items} hasCheckedItem={hasCheckedItem} />
|
|
324
|
+
<Select.ScrollDownArrow className={scrollButtonStyles}>
|
|
325
|
+
<ChevronDownIcon />
|
|
326
|
+
</Select.ScrollDownArrow>
|
|
327
|
+
</Select.Popup>
|
|
328
|
+
</Select.Positioner>
|
|
237
329
|
</Select.Portal>
|
|
238
330
|
</Select.Root>
|
|
239
331
|
);
|