@noya-app/noya-designsystem 0.1.66 → 0.1.68
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 +11 -11
- package/CHANGELOG.md +14 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +81 -5
- package/dist/index.d.ts +81 -5
- package/dist/index.js +1205 -857
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1047 -698
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -2
- package/src/components/Anchor.tsx +30 -0
- package/src/components/BaseToolbar.tsx +11 -8
- package/src/components/Chip.tsx +16 -2
- package/src/components/Collection.tsx +10 -0
- package/src/components/Combobox.tsx +30 -21
- package/src/components/ComboboxMenu.tsx +49 -18
- package/src/components/Dialog.tsx +7 -1
- package/src/components/EditableText.tsx +7 -3
- package/src/components/Grid.tsx +2 -0
- package/src/components/GridView.tsx +52 -9
- package/src/components/List.tsx +54 -9
- package/src/components/ListView.tsx +44 -0
- package/src/components/ScrollArea2.tsx +56 -0
- package/src/components/SearchCompletionMenu.tsx +6 -2
- package/src/components/TreeView.tsx +4 -0
- package/src/components/UserPointer.tsx +34 -7
- package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +1 -3
- package/src/components/listView/ListViewEditableRowTitle.tsx +23 -1
- package/src/components/listView/ListViewRoot.tsx +45 -13
- package/src/components/sorting/Sortable.tsx +22 -10
- package/src/components/workspace/WorkspaceLayout.tsx +62 -4
- package/src/components/workspace/WorkspaceSideContext.tsx +18 -0
- package/src/components/workspace/renderPanelChildren.tsx +2 -2
- package/src/components/workspace/types.ts +8 -1
- package/src/contexts/DialogContext.tsx +22 -10
- package/src/index.css +143 -1
- package/src/index.tsx +4 -0
- package/src/theme/proseTheme.ts +2 -0
- package/src/utils/combobox.ts +17 -2
- package/src/utils/detailLineClamp.ts +33 -0
- package/tailwind.config.ts +3 -1
package/src/components/List.tsx
CHANGED
|
@@ -15,6 +15,10 @@ import React, {
|
|
|
15
15
|
import { useLinkComponent } from "../contexts/LinkComponentContext";
|
|
16
16
|
import { cssVars } from "../theme";
|
|
17
17
|
import { cx } from "../utils/classNames";
|
|
18
|
+
import {
|
|
19
|
+
DETAIL_LINE_CLAMP_CLASSES,
|
|
20
|
+
normalizeDetailLineClamp,
|
|
21
|
+
} from "../utils/detailLineClamp";
|
|
18
22
|
import { updateSelection } from "../utils/selection";
|
|
19
23
|
import { ActionMenu } from "./ActionMenu";
|
|
20
24
|
import {
|
|
@@ -55,6 +59,7 @@ export const List = memoGeneric(
|
|
|
55
59
|
renderAction,
|
|
56
60
|
renderDetail,
|
|
57
61
|
renderRight,
|
|
62
|
+
detailLineClamp,
|
|
58
63
|
onSelectionChange,
|
|
59
64
|
itemStyle: itemStyleProp,
|
|
60
65
|
itemClassName: itemClassNameProp,
|
|
@@ -82,6 +87,7 @@ export const List = memoGeneric(
|
|
|
82
87
|
sortableId,
|
|
83
88
|
sharedDragProps,
|
|
84
89
|
virtualized,
|
|
90
|
+
renameSelectsBeforeDot,
|
|
85
91
|
} = props;
|
|
86
92
|
|
|
87
93
|
const [internalHoveredId, setHoveredId] = useState<string>();
|
|
@@ -100,6 +106,8 @@ export const List = memoGeneric(
|
|
|
100
106
|
[itemStyleProp]
|
|
101
107
|
);
|
|
102
108
|
|
|
109
|
+
const normalizedDetailLineClamp = normalizeDetailLineClamp(detailLineClamp);
|
|
110
|
+
|
|
103
111
|
const handleSelect = (itemId: string, event?: ListView.ClickInfo) => {
|
|
104
112
|
if (!onSelectionChange) return;
|
|
105
113
|
|
|
@@ -220,12 +228,27 @@ export const List = memoGeneric(
|
|
|
220
228
|
/>
|
|
221
229
|
));
|
|
222
230
|
|
|
223
|
-
const
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
const detailContent = renderDetail?.(item, isSelected);
|
|
232
|
+
|
|
233
|
+
const end = action || (detailPosition === "end" && detailContent);
|
|
226
234
|
const right = renderRight?.(item, isSelected);
|
|
227
|
-
|
|
228
|
-
|
|
235
|
+
let below = detailPosition === "below" ? detailContent : undefined;
|
|
236
|
+
|
|
237
|
+
if (below && normalizedDetailLineClamp) {
|
|
238
|
+
const clampClassName =
|
|
239
|
+
DETAIL_LINE_CLAMP_CLASSES[normalizedDetailLineClamp];
|
|
240
|
+
|
|
241
|
+
below = (
|
|
242
|
+
<div
|
|
243
|
+
className={cx(
|
|
244
|
+
"n-overflow-hidden n-w-full n-max-w-full",
|
|
245
|
+
clampClassName
|
|
246
|
+
)}
|
|
247
|
+
>
|
|
248
|
+
{below}
|
|
249
|
+
</div>
|
|
250
|
+
);
|
|
251
|
+
}
|
|
229
252
|
return (
|
|
230
253
|
<ViewComponent.Row
|
|
231
254
|
as={typeof href === "string" ? LinkComponent : undefined}
|
|
@@ -242,6 +265,7 @@ export const List = memoGeneric(
|
|
|
242
265
|
testShowDropIndicatorId === id ? "inside" : undefined
|
|
243
266
|
}
|
|
244
267
|
chevronClassName={cx(expandable && "n-relative n-left-2")}
|
|
268
|
+
chevronAs={href ? "div" : undefined}
|
|
245
269
|
style={itemStyle}
|
|
246
270
|
hovered={isHovered}
|
|
247
271
|
selected={isSelected}
|
|
@@ -281,7 +305,10 @@ export const List = memoGeneric(
|
|
|
281
305
|
dragIndicatorStyle={dragIndicatorStyle}
|
|
282
306
|
>
|
|
283
307
|
<div
|
|
284
|
-
className={cx(
|
|
308
|
+
className={cx(
|
|
309
|
+
"n-flex n-flex-1 n-items-center n-px-2",
|
|
310
|
+
rowGapMap[size]
|
|
311
|
+
)}
|
|
285
312
|
>
|
|
286
313
|
{thumbnail && (
|
|
287
314
|
<div
|
|
@@ -298,13 +325,17 @@ export const List = memoGeneric(
|
|
|
298
325
|
)}
|
|
299
326
|
<div className="n-flex n-flex-col n-flex-1 n-w-0">
|
|
300
327
|
<div className="n-flex n-items-center n-min-h-input-height n-flex-1 n-gap-2">
|
|
301
|
-
<div
|
|
328
|
+
<div
|
|
329
|
+
className="n-flex-1 n-w-0 n-flex n-items-center"
|
|
330
|
+
tabIndex={-1}
|
|
331
|
+
>
|
|
302
332
|
{isRenaming ? (
|
|
303
333
|
<ViewComponent.EditableRowTitle
|
|
304
334
|
value={getName(item)}
|
|
305
335
|
placeholder={getPlaceholder?.(item) ?? "Enter name"}
|
|
306
336
|
className={fontStyleMap[size]}
|
|
307
337
|
autoFocus={!isTestingRenaming}
|
|
338
|
+
selectBeforeDot={renameSelectsBeforeDot}
|
|
308
339
|
onSubmitEditing={(value: string) => {
|
|
309
340
|
onRename?.(item, value);
|
|
310
341
|
setRenamingId(undefined);
|
|
@@ -337,7 +368,15 @@ export const List = memoGeneric(
|
|
|
337
368
|
)}
|
|
338
369
|
</div>
|
|
339
370
|
{below && (
|
|
340
|
-
<DetailContainer
|
|
371
|
+
<DetailContainer
|
|
372
|
+
selected={isSelected}
|
|
373
|
+
align={
|
|
374
|
+
normalizedDetailLineClamp &&
|
|
375
|
+
normalizedDetailLineClamp > 1
|
|
376
|
+
? "start"
|
|
377
|
+
: "center"
|
|
378
|
+
}
|
|
379
|
+
>
|
|
341
380
|
{below}
|
|
342
381
|
</DetailContainer>
|
|
343
382
|
)}
|
|
@@ -363,13 +402,19 @@ export const List = memoGeneric(
|
|
|
363
402
|
const DetailContainer = memo(function DetailContainer({
|
|
364
403
|
children,
|
|
365
404
|
selected,
|
|
405
|
+
align = "center",
|
|
366
406
|
}: {
|
|
367
407
|
children: React.ReactNode;
|
|
368
408
|
selected: boolean;
|
|
409
|
+
align?: "center" | "start";
|
|
369
410
|
}) {
|
|
370
411
|
return (
|
|
371
412
|
<div
|
|
372
|
-
className={cx(
|
|
413
|
+
className={cx(
|
|
414
|
+
"n-flex n-gap-2",
|
|
415
|
+
align === "center" ? "n-items-center" : "n-items-start",
|
|
416
|
+
selected && "n-text-primary"
|
|
417
|
+
)}
|
|
373
418
|
tabIndex={-1}
|
|
374
419
|
>
|
|
375
420
|
{children}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { IVirtualizedList } from "./IVirtualizedList";
|
|
4
|
+
import {
|
|
5
|
+
isMenuItemSectionHeader,
|
|
6
|
+
isSelectableMenuItem,
|
|
7
|
+
MenuItem,
|
|
8
|
+
ScoredMenuItem,
|
|
9
|
+
} from "./internal/Menu";
|
|
4
10
|
import { ListRowContext } from "./listView/ListViewContexts";
|
|
5
11
|
import { ListViewEditableRowTitle } from "./listView/ListViewEditableRowTitle";
|
|
6
12
|
import {
|
|
@@ -8,6 +14,7 @@ import {
|
|
|
8
14
|
ListViewRoot,
|
|
9
15
|
ROW_HEIGHT,
|
|
10
16
|
SECTION_HEADER_LABEL_HEIGHT,
|
|
17
|
+
TALL_ROW_HEIGHT,
|
|
11
18
|
} from "./listView/ListViewRoot";
|
|
12
19
|
import {
|
|
13
20
|
ListViewClickInfo,
|
|
@@ -32,6 +39,7 @@ export namespace ListView {
|
|
|
32
39
|
export const DragIndicator = ListViewDragIndicatorElement;
|
|
33
40
|
export const rowHeight = ROW_HEIGHT;
|
|
34
41
|
export const sectionHeaderLabelHeight = SECTION_HEADER_LABEL_HEIGHT;
|
|
42
|
+
export const tallRowHeight = TALL_ROW_HEIGHT;
|
|
35
43
|
export const calculateHeight = ({
|
|
36
44
|
menuItemsCount,
|
|
37
45
|
sectionHeaderCount,
|
|
@@ -43,5 +51,41 @@ export namespace ListView {
|
|
|
43
51
|
menuItemsCount * rowHeight + sectionHeaderCount * sectionHeaderLabelHeight
|
|
44
52
|
);
|
|
45
53
|
};
|
|
54
|
+
|
|
55
|
+
export const calculateHeightWithTallRows = ({
|
|
56
|
+
menuItemsCount,
|
|
57
|
+
sectionHeaderCount,
|
|
58
|
+
tallMenuItemsCount,
|
|
59
|
+
maxBaseRows,
|
|
60
|
+
}: {
|
|
61
|
+
menuItemsCount: number;
|
|
62
|
+
sectionHeaderCount: number;
|
|
63
|
+
tallMenuItemsCount: number;
|
|
64
|
+
/** Maximum number of base-height rows to show before capping */
|
|
65
|
+
maxBaseRows: number;
|
|
66
|
+
}) => {
|
|
67
|
+
const normalRows = menuItemsCount - tallMenuItemsCount;
|
|
68
|
+
const totalHeight =
|
|
69
|
+
normalRows * rowHeight +
|
|
70
|
+
tallMenuItemsCount * tallRowHeight +
|
|
71
|
+
sectionHeaderCount * sectionHeaderLabelHeight;
|
|
72
|
+
const maxHeight = rowHeight * maxBaseRows;
|
|
73
|
+
return Math.min(totalHeight, maxHeight);
|
|
74
|
+
};
|
|
75
|
+
export const getMenuMetrics = <T extends string>(
|
|
76
|
+
items: Array<MenuItem<T> | ScoredMenuItem<T>>
|
|
77
|
+
) => {
|
|
78
|
+
const sectionHeaderCount = items.filter((item) =>
|
|
79
|
+
isMenuItemSectionHeader(item)
|
|
80
|
+
).length;
|
|
81
|
+
const menuItemsCount = items.filter((item) =>
|
|
82
|
+
isSelectableMenuItem(item)
|
|
83
|
+
).length;
|
|
84
|
+
const tallMenuItemsCount = items.filter((item) => {
|
|
85
|
+
return isSelectableMenuItem(item) && item.tooltip !== undefined;
|
|
86
|
+
}).length;
|
|
87
|
+
|
|
88
|
+
return { sectionHeaderCount, menuItemsCount, tallMenuItemsCount };
|
|
89
|
+
};
|
|
46
90
|
export const normalizeTargetIndex = normalizeListTargetIndex;
|
|
47
91
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { ScrollArea } from "@base-ui-components/react/scroll-area";
|
|
4
|
+
import * as React from "react";
|
|
5
|
+
import { cx } from "../utils/classNames";
|
|
6
|
+
|
|
7
|
+
const SCROLLBAR_STATE_CLASSES =
|
|
8
|
+
"scroll-component n-opacity-0 n-transition-opacity n-delay-300 n-pointer-events-none data-[hovering]:n-opacity-100 data-[hovering]:n-delay-0 data-[hovering]:n-duration-75 data-[hovering]:n-pointer-events-auto data-[scrolling]:n-opacity-100 data-[scrolling]:n-delay-0 data-[scrolling]:n-duration-75 data-[scrolling]:n-pointer-events-auto";
|
|
9
|
+
|
|
10
|
+
const SCROLLBAR_TRACK_VERTICAL =
|
|
11
|
+
"n-m-1 n-w-1 flex n-justify-center n-rounded-md n-bg-divider-subtle";
|
|
12
|
+
const SCROLLBAR_TRACK_HORIZONTAL =
|
|
13
|
+
"n-m-1 n-h-1 flex n-justify-center n-rounded-md n-bg-divider-subtle";
|
|
14
|
+
|
|
15
|
+
const SCROLLBAR_THUMB_VERTICAL =
|
|
16
|
+
"scroll-component n-w-full n-rounded n-bg-scrollbar";
|
|
17
|
+
const SCROLLBAR_THUMB_HORIZONTAL =
|
|
18
|
+
"scroll-component n-h-full n-rounded n-bg-scrollbar";
|
|
19
|
+
|
|
20
|
+
export function ScrollArea2({
|
|
21
|
+
children,
|
|
22
|
+
className,
|
|
23
|
+
viewportClassName,
|
|
24
|
+
orientation = "vertical",
|
|
25
|
+
}: {
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
className?: string;
|
|
28
|
+
viewportClassName?: string;
|
|
29
|
+
orientation?: "vertical" | "horizontal" | "both";
|
|
30
|
+
}) {
|
|
31
|
+
return (
|
|
32
|
+
<ScrollArea.Root className={cx("n-relative", className)}>
|
|
33
|
+
<ScrollArea.Viewport
|
|
34
|
+
className={cx("n-h-full n-overscroll-contain", viewportClassName)}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</ScrollArea.Viewport>
|
|
38
|
+
{(orientation === "vertical" || orientation === "both") && (
|
|
39
|
+
<ScrollArea.Scrollbar
|
|
40
|
+
className={cx(SCROLLBAR_STATE_CLASSES, SCROLLBAR_TRACK_VERTICAL)}
|
|
41
|
+
>
|
|
42
|
+
<ScrollArea.Thumb className={SCROLLBAR_THUMB_VERTICAL} />
|
|
43
|
+
</ScrollArea.Scrollbar>
|
|
44
|
+
)}
|
|
45
|
+
{(orientation === "horizontal" || orientation === "both") && (
|
|
46
|
+
<ScrollArea.Scrollbar
|
|
47
|
+
orientation="horizontal"
|
|
48
|
+
className={cx(SCROLLBAR_STATE_CLASSES, SCROLLBAR_TRACK_HORIZONTAL)}
|
|
49
|
+
>
|
|
50
|
+
<ScrollArea.Thumb className={SCROLLBAR_THUMB_HORIZONTAL} />
|
|
51
|
+
</ScrollArea.Scrollbar>
|
|
52
|
+
)}
|
|
53
|
+
{orientation === "both" && <ScrollArea.Corner />}
|
|
54
|
+
</ScrollArea.Root>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -91,13 +91,17 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
|
|
|
91
91
|
onHover?.(results[index]!);
|
|
92
92
|
}, [index, onHover, results]);
|
|
93
93
|
|
|
94
|
-
const
|
|
94
|
+
const baseRowHeight = ListView.rowHeight;
|
|
95
|
+
const searchHeight = baseRowHeight;
|
|
95
96
|
|
|
96
97
|
const listSize =
|
|
97
98
|
results.length > 0
|
|
98
99
|
? {
|
|
99
100
|
width,
|
|
100
|
-
height: Math.min(
|
|
101
|
+
height: Math.min(
|
|
102
|
+
results.length * baseRowHeight,
|
|
103
|
+
baseRowHeight * 6.5
|
|
104
|
+
),
|
|
101
105
|
}
|
|
102
106
|
: {
|
|
103
107
|
width,
|
|
@@ -20,6 +20,7 @@ type TreeRowBaseProps = {
|
|
|
20
20
|
icon?: Exclude<ReactNode, string> | IconName;
|
|
21
21
|
expanded?: boolean;
|
|
22
22
|
chevronClassName?: string;
|
|
23
|
+
chevronAs?: React.ElementType;
|
|
23
24
|
onClickChevron?: ({ altKey }: { altKey: boolean }) => void;
|
|
24
25
|
onDoubleClick?: () => void;
|
|
25
26
|
};
|
|
@@ -33,6 +34,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
33
34
|
expanded,
|
|
34
35
|
onClickChevron,
|
|
35
36
|
chevronClassName,
|
|
37
|
+
chevronAs,
|
|
36
38
|
children,
|
|
37
39
|
...rest
|
|
38
40
|
}: TreeViewRowProps<MenuItemType>,
|
|
@@ -45,6 +47,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
45
47
|
const handleClickChevron = useCallback(
|
|
46
48
|
(event: React.MouseEvent) => {
|
|
47
49
|
event.stopPropagation();
|
|
50
|
+
event.preventDefault();
|
|
48
51
|
stableOnClickChevron?.({ altKey: event.altKey });
|
|
49
52
|
},
|
|
50
53
|
[stableOnClickChevron]
|
|
@@ -58,6 +61,7 @@ const TreeRow = forwardRefGeneric(function TreeRow<MenuItemType extends string>(
|
|
|
58
61
|
<Spacer.Horizontal size={19} />
|
|
59
62
|
) : (
|
|
60
63
|
<IconButton
|
|
64
|
+
as={chevronAs}
|
|
61
65
|
className={chevronClassName}
|
|
62
66
|
iconName={expanded ? "ChevronDownIcon2" : "ChevronRightIcon2"}
|
|
63
67
|
onClick={handleClickChevron}
|
|
@@ -42,16 +42,25 @@ const UserNameTag = memo(function UserNameTag({
|
|
|
42
42
|
className,
|
|
43
43
|
style,
|
|
44
44
|
children,
|
|
45
|
+
variant = "rounded",
|
|
46
|
+
size = "medium",
|
|
47
|
+
bordered = true,
|
|
45
48
|
}: {
|
|
46
49
|
backgroundColor: string;
|
|
47
50
|
className?: string;
|
|
48
51
|
style?: React.CSSProperties;
|
|
49
52
|
children: React.ReactNode;
|
|
53
|
+
variant?: "rounded" | "rectangle";
|
|
54
|
+
size?: "small" | "medium";
|
|
55
|
+
bordered?: boolean;
|
|
50
56
|
}) {
|
|
51
57
|
return (
|
|
52
58
|
<span
|
|
53
59
|
className={cx(
|
|
54
|
-
"n-relative n-inline-block n-
|
|
60
|
+
"n-relative n-inline-block n-text-xs n-font-medium n-leading-none n-text-white n-shadow-sm",
|
|
61
|
+
bordered && "n-border n-border-background",
|
|
62
|
+
size === "small" ? "n-p-1" : "n-px-2 n-py-1.5",
|
|
63
|
+
variant === "rounded" ? "n-rounded-full" : "n-rounded-none",
|
|
55
64
|
className
|
|
56
65
|
)}
|
|
57
66
|
style={{
|
|
@@ -113,6 +122,15 @@ export type UserPointerProps = {
|
|
|
113
122
|
backgroundColor?: string;
|
|
114
123
|
style?: React.CSSProperties;
|
|
115
124
|
className?: string;
|
|
125
|
+
/**
|
|
126
|
+
* Whether to render the pointer icon. Useful for contexts that only need the
|
|
127
|
+
* name tag styling/positioning without the icon.
|
|
128
|
+
*/
|
|
129
|
+
showPointerIcon?: boolean;
|
|
130
|
+
/** Controls the shape of the name tag */
|
|
131
|
+
labelVariant?: "rounded" | "rectangle";
|
|
132
|
+
labelSize?: "small" | "medium";
|
|
133
|
+
bordered?: boolean;
|
|
116
134
|
};
|
|
117
135
|
|
|
118
136
|
const POINTER_SIZE = 12;
|
|
@@ -126,6 +144,10 @@ export const UserPointer = memo(function UserPointer({
|
|
|
126
144
|
backgroundColor,
|
|
127
145
|
style,
|
|
128
146
|
className,
|
|
147
|
+
showPointerIcon = true,
|
|
148
|
+
labelVariant = "rounded",
|
|
149
|
+
labelSize = "medium",
|
|
150
|
+
bordered = true,
|
|
129
151
|
}: UserPointerProps) {
|
|
130
152
|
const userBackgroundColor = useMemo(
|
|
131
153
|
() => backgroundColor ?? colorFromString(userId ?? name ?? ""),
|
|
@@ -154,14 +176,19 @@ export const UserPointer = memo(function UserPointer({
|
|
|
154
176
|
>
|
|
155
177
|
{name && (
|
|
156
178
|
<div className="n-relative">
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
179
|
+
{showPointerIcon && (
|
|
180
|
+
<UserPointerIcon
|
|
181
|
+
size={POINTER_SIZE}
|
|
182
|
+
color={userBackgroundColor}
|
|
183
|
+
style={pointerOverlapStyle}
|
|
184
|
+
/>
|
|
185
|
+
)}
|
|
162
186
|
<UserNameTag
|
|
163
187
|
backgroundColor={userBackgroundColor}
|
|
164
|
-
style={nameTagOverlapStyle}
|
|
188
|
+
style={showPointerIcon ? nameTagOverlapStyle : undefined}
|
|
189
|
+
variant={labelVariant}
|
|
190
|
+
size={labelSize}
|
|
191
|
+
bordered={bordered}
|
|
165
192
|
>
|
|
166
193
|
{name}
|
|
167
194
|
</UserNameTag>
|
|
@@ -33,9 +33,7 @@ export const ConnectedUsersMenuLayout = ({
|
|
|
33
33
|
}: ConnectedUsersMenuLayoutProps) => {
|
|
34
34
|
return (
|
|
35
35
|
<div className="n-flex n-gap-1.5">
|
|
36
|
-
<AvatarStack size={INPUT_HEIGHT}
|
|
37
|
-
{renderUsers()}
|
|
38
|
-
</AvatarStack>
|
|
36
|
+
<AvatarStack size={INPUT_HEIGHT}>{renderUsers()}</AvatarStack>
|
|
39
37
|
{launchAIAssistant && (
|
|
40
38
|
<Tooltip content="AI Assistant">
|
|
41
39
|
<Button
|
|
@@ -11,6 +11,12 @@ export interface EditableRowTitleProps {
|
|
|
11
11
|
placeholder?: string;
|
|
12
12
|
className?: string;
|
|
13
13
|
onKeyDown?: (e: React.KeyboardEvent) => void;
|
|
14
|
+
/**
|
|
15
|
+
* When true, selects only the portion of the value
|
|
16
|
+
* before the last '.' on autofocus (useful for renaming files
|
|
17
|
+
* without selecting the extension). Defaults to false.
|
|
18
|
+
*/
|
|
19
|
+
selectBeforeDot?: boolean;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export const ListViewEditableRowTitle = memo(function ListViewEditableRowTitle({
|
|
@@ -20,6 +26,7 @@ export const ListViewEditableRowTitle = memo(function ListViewEditableRowTitle({
|
|
|
20
26
|
placeholder,
|
|
21
27
|
className,
|
|
22
28
|
onKeyDown,
|
|
29
|
+
selectBeforeDot = false,
|
|
23
30
|
}: EditableRowTitleProps) {
|
|
24
31
|
const inputRef = useRef<HTMLInputElement | null>(null);
|
|
25
32
|
|
|
@@ -34,9 +41,24 @@ export const ListViewEditableRowTitle = memo(function ListViewEditableRowTitle({
|
|
|
34
41
|
|
|
35
42
|
// must be a delay of 1ms or more to ensure any radix ui components have finished any focus behaviors
|
|
36
43
|
setTimeout(() => {
|
|
44
|
+
if (selectBeforeDot) {
|
|
45
|
+
const index = value.lastIndexOf(".");
|
|
46
|
+
if (index > 0) {
|
|
47
|
+
// Some browsers require calling select() before setSelectionRange
|
|
48
|
+
// to ensure the selection is applied on first focus.
|
|
49
|
+
element.select();
|
|
50
|
+
try {
|
|
51
|
+
element.setSelectionRange(0, index, "forward");
|
|
52
|
+
} catch {
|
|
53
|
+
// Fallback in case setSelectionRange throws (e.g., unsupported type)
|
|
54
|
+
element.select();
|
|
55
|
+
}
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
37
59
|
element.select();
|
|
38
60
|
}, 1);
|
|
39
|
-
}, [autoFocus]);
|
|
61
|
+
}, [autoFocus, selectBeforeDot, value]);
|
|
40
62
|
|
|
41
63
|
return (
|
|
42
64
|
<InputField.Input
|
|
@@ -19,7 +19,6 @@ import React, {
|
|
|
19
19
|
} from "react";
|
|
20
20
|
import { WindowScroller, WindowScrollerChildProps } from "react-virtualized";
|
|
21
21
|
import { ListChildComponentProps, VariableSizeList } from "react-window";
|
|
22
|
-
import { INPUT_HEIGHT } from "../../theme";
|
|
23
22
|
import { mergeConflictingClassNames } from "../../utils/classNames";
|
|
24
23
|
import { IVirtualizedList } from "../IVirtualizedList";
|
|
25
24
|
import { ScrollArea } from "../ScrollArea";
|
|
@@ -49,7 +48,13 @@ import {
|
|
|
49
48
|
} from "./types";
|
|
50
49
|
|
|
51
50
|
export const ROW_HEIGHT = 31;
|
|
52
|
-
|
|
51
|
+
// Section headers with variant "label" render as 12px line-height with 6px
|
|
52
|
+
// padding above and below, totaling 24px. Keep this constant in sync with
|
|
53
|
+
// ListViewRow styles (n-leading-3 with 6px vertical padding).
|
|
54
|
+
export const SECTION_HEADER_LABEL_HEIGHT = 24;
|
|
55
|
+
// Some list rows render an extra line (e.g., tooltip/description). Those rows
|
|
56
|
+
// use a taller height.
|
|
57
|
+
export const TALL_ROW_HEIGHT = 50;
|
|
53
58
|
|
|
54
59
|
const VirtualizedListRow = memo(function VirtualizedListRow({
|
|
55
60
|
index,
|
|
@@ -99,9 +104,10 @@ const VirtualizedListInner = forwardRefGeneric(function VirtualizedListInner<T>(
|
|
|
99
104
|
useLayoutEffect(() => {
|
|
100
105
|
listRef.current?.resetAfterIndex(0);
|
|
101
106
|
}, [
|
|
102
|
-
// When items
|
|
103
|
-
// since
|
|
107
|
+
// When items or the height resolver change, re-render the virtualized list
|
|
108
|
+
// since VariableSizeList caches sizes per index.
|
|
104
109
|
items,
|
|
110
|
+
getItemHeight,
|
|
105
111
|
]);
|
|
106
112
|
|
|
107
113
|
// Internally, react-virtualized updates these properties. We always want
|
|
@@ -223,6 +229,7 @@ export type ListViewRootProps<T> = {
|
|
|
223
229
|
sharedDragProps?: SharedDragProps;
|
|
224
230
|
sortableId?: string;
|
|
225
231
|
itemHeight?: number;
|
|
232
|
+
getItemHeightForItem?: (item: T, index: number) => number;
|
|
226
233
|
};
|
|
227
234
|
|
|
228
235
|
export const ListViewRoot = memoGeneric(
|
|
@@ -262,6 +269,7 @@ export const ListViewRoot = memoGeneric(
|
|
|
262
269
|
sharedDragProps,
|
|
263
270
|
sortableId,
|
|
264
271
|
itemHeight,
|
|
272
|
+
getItemHeightForItem,
|
|
265
273
|
}: ListViewRootProps<T>,
|
|
266
274
|
forwardedRef: ForwardedRef<IVirtualizedList>
|
|
267
275
|
) {
|
|
@@ -299,8 +307,19 @@ export const ListViewRoot = memoGeneric(
|
|
|
299
307
|
|
|
300
308
|
const getItemContextValue = useCallback(
|
|
301
309
|
(i: number): ListRowContextValue | undefined => {
|
|
302
|
-
|
|
303
|
-
|
|
310
|
+
// Always detect section headers for height calculations.
|
|
311
|
+
// For "bare" and "normal" variants we keep default margins/selection
|
|
312
|
+
// but still set isSectionHeader based on the rendered child.
|
|
313
|
+
if (variant === "bare" || variant === "normal") {
|
|
314
|
+
const current = renderChild(i);
|
|
315
|
+
|
|
316
|
+
if (!isValidElement(current)) return defaultContextValue;
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
...defaultContextValue,
|
|
320
|
+
isSectionHeader: !!(current as any).props.isSectionHeader,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
304
323
|
|
|
305
324
|
const current = renderChild(i);
|
|
306
325
|
|
|
@@ -419,16 +438,29 @@ export const ListViewRoot = memoGeneric(
|
|
|
419
438
|
|
|
420
439
|
const getItemHeight = useCallback(
|
|
421
440
|
(index: number) => {
|
|
441
|
+
// Section header height takes precedence
|
|
442
|
+
const child = getItemContextValue(index);
|
|
443
|
+
if (child?.isSectionHeader && sectionHeaderVariant === "label") {
|
|
444
|
+
return SECTION_HEADER_LABEL_HEIGHT;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// Fixed height for all items if provided
|
|
422
448
|
if (itemHeight) return itemHeight;
|
|
423
449
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
return
|
|
450
|
+
// Per-item height callback
|
|
451
|
+
if (getItemHeightForItem) {
|
|
452
|
+
return getItemHeightForItem(data[index], index);
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
return ROW_HEIGHT;
|
|
430
456
|
},
|
|
431
|
-
[
|
|
457
|
+
[
|
|
458
|
+
getItemContextValue,
|
|
459
|
+
itemHeight,
|
|
460
|
+
sectionHeaderVariant,
|
|
461
|
+
getItemHeightForItem,
|
|
462
|
+
data,
|
|
463
|
+
]
|
|
432
464
|
);
|
|
433
465
|
|
|
434
466
|
const getKey = useCallback(
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from "@dnd-kit/sortable";
|
|
10
10
|
import { Insets } from "@noya-app/noya-geometry";
|
|
11
11
|
import * as React from "react";
|
|
12
|
+
import { useMemo } from "react";
|
|
12
13
|
import {
|
|
13
14
|
ActiveDragContext,
|
|
14
15
|
ActiveDragContextType,
|
|
@@ -145,7 +146,7 @@ function SortableItem<TElement extends HTMLElement>({
|
|
|
145
146
|
disabled?: boolean;
|
|
146
147
|
children: (
|
|
147
148
|
props: ItemChildrenProps<TElement>,
|
|
148
|
-
extraProps:
|
|
149
|
+
extraProps: ItemChildrenExtraProps
|
|
149
150
|
) => React.ReactNode;
|
|
150
151
|
}) {
|
|
151
152
|
const { acceptsDrop, listId, getDropTargetParentIndex } =
|
|
@@ -180,21 +181,35 @@ function SortableItem<TElement extends HTMLElement>({
|
|
|
180
181
|
getDropTargetParentIndex,
|
|
181
182
|
});
|
|
182
183
|
|
|
183
|
-
|
|
184
|
-
{
|
|
184
|
+
const props = useMemo(
|
|
185
|
+
() => ({
|
|
185
186
|
ref,
|
|
186
187
|
...attributes,
|
|
187
188
|
...listeners,
|
|
188
189
|
style: { opacity: isDragging ? 0.5 : 1 },
|
|
189
|
-
},
|
|
190
|
-
|
|
190
|
+
}),
|
|
191
|
+
[ref, attributes, listeners, isDragging]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const extraProps = useMemo(
|
|
195
|
+
() => ({
|
|
191
196
|
isDragging,
|
|
197
|
+
isOverlay: false,
|
|
192
198
|
setActivatorNodeRef,
|
|
193
199
|
setDraggableNodeRef,
|
|
194
200
|
setDroppableNodeRef,
|
|
195
201
|
relativeDropPosition,
|
|
196
|
-
}
|
|
202
|
+
}),
|
|
203
|
+
[
|
|
204
|
+
isDragging,
|
|
205
|
+
setActivatorNodeRef,
|
|
206
|
+
setDraggableNodeRef,
|
|
207
|
+
setDroppableNodeRef,
|
|
208
|
+
relativeDropPosition,
|
|
209
|
+
]
|
|
197
210
|
);
|
|
211
|
+
|
|
212
|
+
return children(props, extraProps);
|
|
198
213
|
}
|
|
199
214
|
|
|
200
215
|
function SortableRoot_({
|
|
@@ -396,10 +411,7 @@ const Sortable_ = function Sortable_<TItem, TElement extends HTMLElement>({
|
|
|
396
411
|
{keys.map((key, index) => (
|
|
397
412
|
<SortableItem<TElement> key={key} id={key}>
|
|
398
413
|
{(props, extraProps) => {
|
|
399
|
-
return renderItem(items[index], props,
|
|
400
|
-
isOverlay: false,
|
|
401
|
-
...extraProps,
|
|
402
|
-
});
|
|
414
|
+
return renderItem(items[index], props, extraProps);
|
|
403
415
|
}}
|
|
404
416
|
</SortableItem>
|
|
405
417
|
))}
|