@noya-app/noya-designsystem 0.1.48 → 0.1.49
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 +10 -10
- package/CHANGELOG.md +8 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +213 -164
- package/dist/index.d.ts +213 -164
- package/dist/index.js +3583 -3290
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3617 -3329
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -2
- package/src/__tests__/validateDropIndicator.test.ts +4 -4
- package/src/components/ActionMenu.tsx +15 -4
- package/src/components/BaseToolbar.tsx +2 -2
- package/src/components/Checkbox.tsx +2 -2
- package/src/components/Chip.tsx +7 -6
- package/src/components/Collection.tsx +19 -1
- package/src/components/Combobox.tsx +22 -23
- package/src/components/ComboboxMenu.tsx +1 -1
- package/src/components/DimensionInput.tsx +14 -12
- package/src/components/EditableText.tsx +3 -1
- package/src/components/FillInputField.tsx +2 -2
- package/src/components/Grid.tsx +133 -113
- package/src/components/GridView.tsx +36 -18
- package/src/components/InputField.tsx +116 -171
- package/src/components/Label.tsx +4 -68
- package/src/components/LabeledField.tsx +7 -1
- package/src/components/List.tsx +103 -44
- package/src/components/ListView.tsx +42 -26
- package/src/components/MediaThumbnail.tsx +3 -3
- package/src/components/Message.tsx +8 -8
- package/src/components/NoyaLogo.tsx +41 -0
- package/src/components/SegmentedControl.tsx +1 -1
- package/src/components/SelectMenu.tsx +9 -4
- package/src/components/Slider.tsx +16 -7
- package/src/components/Sortable.tsx +62 -25
- package/src/components/internal/Menu.tsx +8 -3
- package/src/components/internal/SelectItem.tsx +3 -2
- package/src/index.css +7 -1
- package/src/index.tsx +2 -0
- package/src/theme/index.ts +2 -0
- package/src/utils/classNames.ts +51 -3
- package/src/utils/inputs.ts +21 -0
- package/tailwind.config.ts +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.49",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@dnd-kit/sortable": "10.0.0",
|
|
25
25
|
"@noya-app/noya-colorpicker": "0.1.18",
|
|
26
26
|
"@noya-app/noya-utils": "0.1.4",
|
|
27
|
-
"@noya-app/noya-geometry": "0.1.
|
|
27
|
+
"@noya-app/noya-geometry": "0.1.9",
|
|
28
28
|
"@noya-app/noya-icons": "0.1.6",
|
|
29
29
|
"@noya-app/noya-keymap": "0.1.3",
|
|
30
30
|
"@radix-ui/primitive": "^1.0.0",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"vscode-fuzzy-scorer": "^0.0.4"
|
|
51
51
|
},
|
|
52
52
|
"devDependencies": {
|
|
53
|
+
"@tailwindcss/container-queries": "^0.1.1",
|
|
53
54
|
"@types/react": "*",
|
|
54
55
|
"@types/react-dom": "*",
|
|
55
56
|
"@types/react-virtualized": "^9.21.21",
|
|
@@ -49,7 +49,7 @@ describe("defaultAcceptsDrop", () => {
|
|
|
49
49
|
describe("validateDropIndicator", () => {
|
|
50
50
|
const mockAcceptsDrop = (
|
|
51
51
|
sourceIndex: number,
|
|
52
|
-
|
|
52
|
+
targetIndex: number,
|
|
53
53
|
position: "above" | "below" | "inside"
|
|
54
54
|
) => {
|
|
55
55
|
// Mock that only allows dropping inside
|
|
@@ -59,7 +59,7 @@ describe("validateDropIndicator", () => {
|
|
|
59
59
|
|
|
60
60
|
const mockAcceptsDropAboveBelow = (
|
|
61
61
|
sourceIndex: number,
|
|
62
|
-
|
|
62
|
+
targetIndex: number,
|
|
63
63
|
position: "above" | "below" | "inside"
|
|
64
64
|
) => {
|
|
65
65
|
// Mock that only allows dropping above/below
|
|
@@ -69,7 +69,7 @@ describe("validateDropIndicator", () => {
|
|
|
69
69
|
|
|
70
70
|
const mockAcceptsAll = (
|
|
71
71
|
sourceIndex: number,
|
|
72
|
-
|
|
72
|
+
targetIndex: number,
|
|
73
73
|
position: "above" | "below" | "inside"
|
|
74
74
|
) => {
|
|
75
75
|
// Mock that allows all drop positions
|
|
@@ -78,7 +78,7 @@ describe("validateDropIndicator", () => {
|
|
|
78
78
|
|
|
79
79
|
const mockAcceptsAboveAndInside = (
|
|
80
80
|
sourceIndex: number,
|
|
81
|
-
|
|
81
|
+
targetIndex: number,
|
|
82
82
|
position: "above" | "below" | "inside"
|
|
83
83
|
) => {
|
|
84
84
|
// Mock that allows dropping above and inside, but not below
|
|
@@ -10,19 +10,29 @@ type ActionMenuProps<TMenu extends string> = {
|
|
|
10
10
|
onSelect: (action: TMenu) => void;
|
|
11
11
|
selected: boolean;
|
|
12
12
|
onOpenChange: (open: boolean) => void;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: React.CSSProperties;
|
|
13
15
|
};
|
|
14
16
|
|
|
15
17
|
export const ActionMenu = memoGeneric(function ActionMenu<
|
|
16
18
|
TMenu extends string,
|
|
17
|
-
>({
|
|
18
|
-
|
|
19
|
+
>({
|
|
20
|
+
menuItems,
|
|
21
|
+
onSelect,
|
|
22
|
+
selected,
|
|
23
|
+
onOpenChange,
|
|
24
|
+
className,
|
|
25
|
+
style,
|
|
26
|
+
}: ActionMenuProps<TMenu>) {
|
|
27
|
+
const internalStyle = useMemo(() => {
|
|
19
28
|
return {
|
|
20
29
|
backgroundColor: selected
|
|
21
30
|
? cssVars.colors.primaryPastel
|
|
22
31
|
: cssVars.colors.inputBackground,
|
|
23
32
|
padding: "4px",
|
|
33
|
+
...style,
|
|
24
34
|
};
|
|
25
|
-
}, [selected]);
|
|
35
|
+
}, [selected, style]);
|
|
26
36
|
|
|
27
37
|
return (
|
|
28
38
|
<DropdownMenu
|
|
@@ -31,7 +41,8 @@ export const ActionMenu = memoGeneric(function ActionMenu<
|
|
|
31
41
|
onOpenChange={onOpenChange}
|
|
32
42
|
>
|
|
33
43
|
<IconButton
|
|
34
|
-
style={
|
|
44
|
+
style={internalStyle}
|
|
45
|
+
className={className}
|
|
35
46
|
iconName="DotsVerticalIcon"
|
|
36
47
|
color={selected ? cssVars.colors.primary : undefined}
|
|
37
48
|
/>
|
|
@@ -21,7 +21,7 @@ export function BaseToolbar({
|
|
|
21
21
|
return (
|
|
22
22
|
<div className="flex flex-col">
|
|
23
23
|
<div
|
|
24
|
-
className="flex items-center pr-2.5 bg-sidebar-background flex-none relative"
|
|
24
|
+
className="flex items-center pr-2.5 bg-sidebar-background flex-none relative @container/toolbar"
|
|
25
25
|
style={{
|
|
26
26
|
flex: `0 0 ${cssVars.spacing.toolbarHeight}`,
|
|
27
27
|
}}
|
|
@@ -39,7 +39,7 @@ export function BaseToolbar({
|
|
|
39
39
|
<Spacer.Horizontal size={10} />
|
|
40
40
|
</>
|
|
41
41
|
)}
|
|
42
|
-
<div className="flex items-center justify-center text-text-muted pointer-events-none
|
|
42
|
+
<div className="flex items-center justify-center text-text-muted pointer-events-none @2xl/toolbar:absolute inset-0">
|
|
43
43
|
<div
|
|
44
44
|
style={{
|
|
45
45
|
display: "flex",
|
package/src/components/Chip.tsx
CHANGED
|
@@ -52,16 +52,16 @@ const ICON_BASE_STYLES = "relative opacity-50 cursor-pointer hover:opacity-85";
|
|
|
52
52
|
|
|
53
53
|
const ICON_SIZE_STYLES = {
|
|
54
54
|
large: {
|
|
55
|
-
add: "ml-[-2px] mr-[2px] -top-[1px] scale-75",
|
|
56
|
-
delete: "mr-[-2px] ml-[2px] scale-75",
|
|
55
|
+
add: "ml-[-2px] mr-[2px] -top-[1px] scale-[0.75]",
|
|
56
|
+
delete: "mr-[-2px] ml-[2px] scale-[0.75]",
|
|
57
57
|
},
|
|
58
58
|
medium: {
|
|
59
|
-
add: "ml-[-2px] mr-[-2px] scale-60",
|
|
60
|
-
delete: "mr-[-2px] scale-60",
|
|
59
|
+
add: "ml-[-2px] mr-[-2px] scale-[0.60]",
|
|
60
|
+
delete: "mr-[-2px] scale-[0.60]",
|
|
61
61
|
},
|
|
62
62
|
small: {
|
|
63
|
-
add: "ml-[-2px] mr-[-2px] scale-60",
|
|
64
|
-
delete: "mr-[-2px] scale-60",
|
|
63
|
+
add: "ml-[-2px] mr-[-2px] scale-[0.60]",
|
|
64
|
+
delete: "mr-[-2px] scale-[0.60]",
|
|
65
65
|
},
|
|
66
66
|
} as const;
|
|
67
67
|
|
|
@@ -82,6 +82,7 @@ export interface ChipProps {
|
|
|
82
82
|
onHoverDeleteChange?: (hovering: boolean) => void;
|
|
83
83
|
style?: React.CSSProperties;
|
|
84
84
|
tabIndex?: number;
|
|
85
|
+
role?: string;
|
|
85
86
|
}
|
|
86
87
|
|
|
87
88
|
export const Chip = memo(
|
|
@@ -7,6 +7,8 @@ import { List } from "./List";
|
|
|
7
7
|
|
|
8
8
|
export type CollectionViewType = "grid" | "list";
|
|
9
9
|
|
|
10
|
+
export type CollectionThumbnailSize = "auto" | "custom";
|
|
11
|
+
|
|
10
12
|
export type CollectionRef = {
|
|
11
13
|
editName: (id: string) => void;
|
|
12
14
|
};
|
|
@@ -16,12 +18,19 @@ export type CollectionThumbnailProps<T> = {
|
|
|
16
18
|
selected: boolean;
|
|
17
19
|
};
|
|
18
20
|
|
|
21
|
+
export type CollectionRenderActionProps<T> = {
|
|
22
|
+
item: T;
|
|
23
|
+
selected: boolean;
|
|
24
|
+
onOpenChange: (open: boolean) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
19
27
|
export interface CollectionProps<T, M extends string = string> {
|
|
20
28
|
className?: string;
|
|
21
29
|
items: T[];
|
|
22
30
|
getId: (item: T) => string;
|
|
23
31
|
getName: (item: T) => string;
|
|
24
32
|
getExpanded?: (item: T) => boolean | undefined;
|
|
33
|
+
getPlaceholder?: (item: T) => string;
|
|
25
34
|
/**
|
|
26
35
|
* Whether directories should be expandable.
|
|
27
36
|
* @default true
|
|
@@ -39,7 +48,9 @@ export interface CollectionProps<T, M extends string = string> {
|
|
|
39
48
|
item,
|
|
40
49
|
selected,
|
|
41
50
|
}: CollectionThumbnailProps<T>) => React.ReactNode;
|
|
42
|
-
renderAction?:
|
|
51
|
+
renderAction?:
|
|
52
|
+
| "menu"
|
|
53
|
+
| ((props: CollectionRenderActionProps<T>) => React.ReactNode);
|
|
43
54
|
renderDetail?: (item: T, selected: boolean) => React.ReactNode;
|
|
44
55
|
/** Callback when selection changes. If not provided, selection will be disabled. */
|
|
45
56
|
onSelectionChange?: (
|
|
@@ -52,13 +63,18 @@ export interface CollectionProps<T, M extends string = string> {
|
|
|
52
63
|
detailPosition?: "end" | "below";
|
|
53
64
|
/** Size of the list items. Defaults to 'medium'. */
|
|
54
65
|
size?: GridViewSize;
|
|
66
|
+
/** Size of the thumbnail. Defaults to 'auto', which will be based on the size prop. */
|
|
67
|
+
thumbnailSize?: CollectionThumbnailSize;
|
|
55
68
|
/** For testing: Override the hover state with a specific item ID */
|
|
56
69
|
testHoveredId?: string;
|
|
57
70
|
/** For testing: Override the renaming state with a specific item ID */
|
|
58
71
|
testRenamingId?: string;
|
|
72
|
+
/** For testing: Override the drop indicator state with a specific item ID */
|
|
73
|
+
testShowDropIndicatorId?: string;
|
|
59
74
|
setExpanded?: (item: T, expanded: boolean) => void;
|
|
60
75
|
acceptsDrop?: ListViewRootProps["acceptsDrop"];
|
|
61
76
|
onMoveItem?: ListViewRootProps["onMoveItem"];
|
|
77
|
+
getDropTargetParentIndex?: ListViewRootProps["getDropTargetParentIndex"];
|
|
62
78
|
getDepth?: (item: T) => number;
|
|
63
79
|
onFilesDrop?: (event: React.DragEvent<Element>) => void;
|
|
64
80
|
getRenamable?: (item: T) => boolean;
|
|
@@ -68,6 +84,8 @@ export interface CollectionProps<T, M extends string = string> {
|
|
|
68
84
|
onClickItem?: (itemId: string) => void;
|
|
69
85
|
onDoubleClickItem?: (itemId: string) => void;
|
|
70
86
|
renderEmptyState?: () => React.ReactElement;
|
|
87
|
+
/** @default false */
|
|
88
|
+
sortable?: boolean;
|
|
71
89
|
}
|
|
72
90
|
|
|
73
91
|
export const Collection = forwardRefGeneric(function Collection<
|
|
@@ -27,10 +27,6 @@ import {
|
|
|
27
27
|
import { ListView } from "./ListView";
|
|
28
28
|
import { Small } from "./Text";
|
|
29
29
|
|
|
30
|
-
const comboboxButtonStyle = {
|
|
31
|
-
right: "2px",
|
|
32
|
-
};
|
|
33
|
-
|
|
34
30
|
export type BaseComboboxProps<T extends string> = {
|
|
35
31
|
id?: string;
|
|
36
32
|
loading?: boolean;
|
|
@@ -454,6 +450,28 @@ export const Combobox = memoGeneric(
|
|
|
454
450
|
id={id}
|
|
455
451
|
size={size}
|
|
456
452
|
sideOffset={6}
|
|
453
|
+
end={
|
|
454
|
+
<>
|
|
455
|
+
{loading && (
|
|
456
|
+
<div className="pr-1.5">
|
|
457
|
+
<ActivityIndicator />
|
|
458
|
+
</div>
|
|
459
|
+
)}
|
|
460
|
+
{!readOnly && (
|
|
461
|
+
<InputField.Button
|
|
462
|
+
variant="floating"
|
|
463
|
+
onClick={handleChevronClick}
|
|
464
|
+
>
|
|
465
|
+
<DropdownChevronIcon
|
|
466
|
+
className={cx(
|
|
467
|
+
"transition-transform duration-100",
|
|
468
|
+
shouldShowMenu ? "rotate-180" : "rotate-0"
|
|
469
|
+
)}
|
|
470
|
+
/>
|
|
471
|
+
</InputField.Button>
|
|
472
|
+
)}
|
|
473
|
+
</>
|
|
474
|
+
}
|
|
457
475
|
renderPopoverContent={({ width }) => {
|
|
458
476
|
const height = Math.min(
|
|
459
477
|
ListView.calculateHeight({
|
|
@@ -528,25 +546,6 @@ export const Combobox = memoGeneric(
|
|
|
528
546
|
/>
|
|
529
547
|
)}
|
|
530
548
|
{children}
|
|
531
|
-
{loading && (
|
|
532
|
-
<InputField.Label htmlFor={id}>
|
|
533
|
-
<ActivityIndicator />
|
|
534
|
-
</InputField.Label>
|
|
535
|
-
)}
|
|
536
|
-
{!readOnly && (
|
|
537
|
-
<InputField.Button
|
|
538
|
-
variant="floating"
|
|
539
|
-
onClick={handleChevronClick}
|
|
540
|
-
style={comboboxButtonStyle}
|
|
541
|
-
>
|
|
542
|
-
<DropdownChevronIcon
|
|
543
|
-
className={cx(
|
|
544
|
-
"transition-transform duration-100",
|
|
545
|
-
shouldShowMenu ? "rotate-180" : "rotate-0"
|
|
546
|
-
)}
|
|
547
|
-
/>
|
|
548
|
-
</InputField.Button>
|
|
549
|
-
)}
|
|
550
549
|
</InputField.Root>
|
|
551
550
|
);
|
|
552
551
|
})
|
|
@@ -92,7 +92,7 @@ export const ComboboxMenu = memoGeneric(
|
|
|
92
92
|
<div className="flex flex-1 min-w-0 items-center">
|
|
93
93
|
{indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
|
|
94
94
|
{item.checked ? (
|
|
95
|
-
<div
|
|
95
|
+
<div {...styles.itemIndicator}>
|
|
96
96
|
<CheckIcon />
|
|
97
97
|
</div>
|
|
98
98
|
) : indented ? (
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { round } from "@noya-app/noya-utils";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { InputField } from "./InputField";
|
|
4
|
+
import { LabeledField } from "./LabeledField";
|
|
4
5
|
|
|
5
6
|
export type SetNumberMode = "replace" | "adjust";
|
|
6
7
|
|
|
@@ -50,17 +51,18 @@ export const DimensionInput = React.memo(function DimensionInput({
|
|
|
50
51
|
);
|
|
51
52
|
|
|
52
53
|
return (
|
|
53
|
-
<
|
|
54
|
-
<InputField.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
54
|
+
<LabeledField label={label} labelPosition="inset">
|
|
55
|
+
<InputField.Root id={id} width={size}>
|
|
56
|
+
<InputField.NumberInput
|
|
57
|
+
value={value === undefined ? value : round(value, 2)}
|
|
58
|
+
placeholder={value === undefined ? placeholder : undefined}
|
|
59
|
+
onNudge={handleNudgeValue}
|
|
60
|
+
disabled={disabled}
|
|
61
|
+
{...(trigger === "change"
|
|
62
|
+
? { onChange: handleSetValue }
|
|
63
|
+
: { onSubmit: handleSetValue })}
|
|
64
|
+
/>
|
|
65
|
+
</InputField.Root>
|
|
66
|
+
</LabeledField>
|
|
65
67
|
);
|
|
66
68
|
});
|
|
@@ -35,6 +35,7 @@ type Props = {
|
|
|
35
35
|
textClassName?: string;
|
|
36
36
|
readOnly?: boolean;
|
|
37
37
|
onBlur?: () => void;
|
|
38
|
+
tabIndex?: number;
|
|
38
39
|
/**
|
|
39
40
|
* If true, will render an unfocused input. This should only be used for visual regression tests.
|
|
40
41
|
*/
|
|
@@ -66,6 +67,7 @@ export const EditableText = memo(
|
|
|
66
67
|
readOnly,
|
|
67
68
|
onBlur,
|
|
68
69
|
testFocused = false,
|
|
70
|
+
tabIndex = 0,
|
|
69
71
|
...props
|
|
70
72
|
}: Props,
|
|
71
73
|
ref: React.ForwardedRef<EditableTextRef>
|
|
@@ -136,7 +138,7 @@ export const EditableText = memo(
|
|
|
136
138
|
children: displayValue,
|
|
137
139
|
onClick: () => setFocused(true),
|
|
138
140
|
ref: previewRef,
|
|
139
|
-
tabIndex
|
|
141
|
+
tabIndex,
|
|
140
142
|
onKeyDown: (e: React.KeyboardEvent) => {
|
|
141
143
|
if (e.key === "Enter" || e.key === " ") {
|
|
142
144
|
setFocused(true);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Sketch } from "@noya-app/noya-file-format";
|
|
2
2
|
import React, { forwardRef, memo } from "react";
|
|
3
|
+
import { cx } from "../utils/classNames";
|
|
3
4
|
import { SketchPattern } from "../utils/sketchPattern";
|
|
4
5
|
import { FillPreviewBackground } from "./FillPreviewBackground";
|
|
5
|
-
import { cx } from "../utils/classNames";
|
|
6
6
|
|
|
7
7
|
const FillButton = forwardRef<
|
|
8
8
|
HTMLButtonElement,
|
|
@@ -11,7 +11,7 @@ const FillButton = forwardRef<
|
|
|
11
11
|
<button
|
|
12
12
|
ref={ref}
|
|
13
13
|
className={cx(
|
|
14
|
-
`outline-none p-0 w-[50px] h-
|
|
14
|
+
`outline-none p-0 w-[50px] h-input-height rounded overflow-hidden border-none shadow-[0_0_0_1px_var(--divider)_inset] bg-transparent relative focus:shadow-[0_0_0_1px_var(--sidebar-background),0_0_0_3px_var(--primary)] `,
|
|
15
15
|
className
|
|
16
16
|
)}
|
|
17
17
|
{...props}
|
package/src/components/Grid.tsx
CHANGED
|
@@ -82,6 +82,7 @@ export const Grid = memoGeneric(
|
|
|
82
82
|
size = "medium",
|
|
83
83
|
testHoveredId,
|
|
84
84
|
testRenamingId,
|
|
85
|
+
testShowDropIndicatorId,
|
|
85
86
|
scrollable = false,
|
|
86
87
|
// acceptsDrop,
|
|
87
88
|
// onMoveItem,
|
|
@@ -90,6 +91,7 @@ export const Grid = memoGeneric(
|
|
|
90
91
|
onClickItem,
|
|
91
92
|
onDoubleClickItem,
|
|
92
93
|
renderEmptyState,
|
|
94
|
+
// getDropTargetParentIndex,
|
|
93
95
|
} = props;
|
|
94
96
|
|
|
95
97
|
const [hoveredId, setHoveredId] = useState<string>();
|
|
@@ -143,130 +145,148 @@ export const Grid = memoGeneric(
|
|
|
143
145
|
|
|
144
146
|
return (
|
|
145
147
|
<GridView.Root
|
|
148
|
+
role="listbox"
|
|
149
|
+
aria-orientation="horizontal"
|
|
150
|
+
aria-multiselectable={true}
|
|
146
151
|
minColumnWidth={minColumnWidth}
|
|
147
|
-
scrollable={scrollable}
|
|
152
|
+
scrollable={items.length > 0 && scrollable}
|
|
153
|
+
contentClassName={cx(items.length === 0 && "flex flex-1")}
|
|
148
154
|
gap={gap}
|
|
149
155
|
className={cx(isDropTargetActive && "bg-primary-pastel", className)}
|
|
150
156
|
{...dropTargetProps}
|
|
151
157
|
ref={fileDropTargetRef}
|
|
152
158
|
>
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
menuItems && (
|
|
174
|
-
<ActionMenu
|
|
175
|
-
menuItems={menuItems}
|
|
176
|
-
onSelect={(action) => handleMenuAction(action, item)}
|
|
177
|
-
selected={isSelected}
|
|
178
|
-
onOpenChange={(open) => {
|
|
179
|
-
setDropdownOpenId(open ? id : undefined);
|
|
159
|
+
{items.length > 0 ? (
|
|
160
|
+
<GridView.Section>
|
|
161
|
+
{items.map((item) => {
|
|
162
|
+
const id = getId(item);
|
|
163
|
+
const isDropdownOpen = dropdownOpenId === id;
|
|
164
|
+
const isHovered =
|
|
165
|
+
testHoveredId === id ||
|
|
166
|
+
currentHoveredId === id ||
|
|
167
|
+
isDropdownOpen;
|
|
168
|
+
const itemIsRenamable = getRenamable?.(item) ?? renamable;
|
|
169
|
+
const isSelected = selectedIds.includes(id);
|
|
170
|
+
const isRenaming =
|
|
171
|
+
itemIsRenamable && onRename && renamingId === id;
|
|
172
|
+
const isTestingRenaming = testRenamingId === id;
|
|
173
|
+
const thumbnail = renderThumbnail?.({
|
|
174
|
+
item,
|
|
175
|
+
selected: isSelected,
|
|
176
|
+
});
|
|
177
|
+
const onOpenChange = (open: boolean) => {
|
|
178
|
+
setDropdownOpenId(open ? id : undefined);
|
|
180
179
|
|
|
181
|
-
|
|
182
|
-
|
|
180
|
+
if (open && !isSelected) {
|
|
181
|
+
handleSelect(id);
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
const action =
|
|
185
|
+
(isHovered || isSelected) &&
|
|
186
|
+
!isRenaming &&
|
|
187
|
+
(typeof renderAction === "function"
|
|
188
|
+
? renderAction({ item, selected: isSelected, onOpenChange })
|
|
189
|
+
: renderAction === "menu" &&
|
|
190
|
+
menuItems && (
|
|
191
|
+
<ActionMenu
|
|
192
|
+
menuItems={menuItems}
|
|
193
|
+
onSelect={(action) => handleMenuAction(action, item)}
|
|
194
|
+
selected={isSelected}
|
|
195
|
+
onOpenChange={onOpenChange}
|
|
196
|
+
/>
|
|
197
|
+
));
|
|
198
|
+
|
|
199
|
+
return (
|
|
200
|
+
<GridView.Item<M>
|
|
201
|
+
key={id}
|
|
202
|
+
id={id}
|
|
203
|
+
role="option"
|
|
204
|
+
aria-selected={isSelected}
|
|
205
|
+
aria-label={getName(item)}
|
|
206
|
+
hovered={isHovered}
|
|
207
|
+
testShowInsideDropIndicator={testShowDropIndicatorId === id}
|
|
208
|
+
onHoverChange={(isHovering) => {
|
|
209
|
+
setHoveredId(isHovering ? id : undefined);
|
|
210
|
+
}}
|
|
211
|
+
title={
|
|
212
|
+
<EditableText
|
|
213
|
+
tabIndex={isRenaming ? undefined : -1}
|
|
214
|
+
value={getName(item)}
|
|
215
|
+
focused={isRenaming}
|
|
216
|
+
testFocused={isTestingRenaming}
|
|
217
|
+
readOnly={!itemIsRenamable || !onRename}
|
|
218
|
+
onSubmit={(value) => {
|
|
219
|
+
if (value !== getName(item)) {
|
|
220
|
+
onRename?.(item, value);
|
|
183
221
|
}
|
|
222
|
+
setRenamingId(undefined);
|
|
184
223
|
}}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
readOnly={!itemIsRenamable || !onRename}
|
|
202
|
-
onSubmit={(value) => {
|
|
203
|
-
if (value !== getName(item)) {
|
|
204
|
-
onRename?.(item, value);
|
|
205
|
-
}
|
|
206
|
-
setRenamingId(undefined);
|
|
207
|
-
}}
|
|
208
|
-
onBlur={() => {
|
|
209
|
-
setRenamingId(undefined);
|
|
210
|
-
}}
|
|
211
|
-
// max height ensures preview text and input are one line
|
|
212
|
-
textClassName="bg-listview-editing-background max-h-[27px]"
|
|
213
|
-
>
|
|
214
|
-
{(props) => (
|
|
215
|
-
<span
|
|
216
|
-
{...props}
|
|
217
|
-
className={cx(
|
|
218
|
-
isSelected && "text-primary",
|
|
219
|
-
"truncate select-none"
|
|
220
|
-
)}
|
|
221
|
-
/>
|
|
222
|
-
)}
|
|
223
|
-
</EditableText>
|
|
224
|
-
}
|
|
225
|
-
subtitle={
|
|
226
|
-
<span className={cx(isSelected && "text-primary")}>
|
|
227
|
-
{renderDetail?.(item, isSelected)}
|
|
228
|
-
</span>
|
|
229
|
-
}
|
|
230
|
-
loading={false}
|
|
231
|
-
selected={isSelected}
|
|
232
|
-
onClick={() => {
|
|
233
|
-
onClickItem?.(id);
|
|
234
|
-
}}
|
|
235
|
-
onSelect={(event) => {
|
|
236
|
-
handleSelect(id, event);
|
|
237
|
-
}}
|
|
238
|
-
onKeyDown={(event) => {
|
|
239
|
-
if (event.key === "Enter") {
|
|
240
|
-
setRenamingId(id);
|
|
224
|
+
onBlur={() => {
|
|
225
|
+
setRenamingId(undefined);
|
|
226
|
+
}}
|
|
227
|
+
// max height ensures preview text and input are one line
|
|
228
|
+
textClassName="bg-listview-editing-background max-h-input-height"
|
|
229
|
+
>
|
|
230
|
+
{(props) => (
|
|
231
|
+
<span
|
|
232
|
+
{...props}
|
|
233
|
+
className={cx(
|
|
234
|
+
isSelected && "text-primary",
|
|
235
|
+
"truncate select-none"
|
|
236
|
+
)}
|
|
237
|
+
/>
|
|
238
|
+
)}
|
|
239
|
+
</EditableText>
|
|
241
240
|
}
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
menuItems={menuItems}
|
|
247
|
-
onSelectMenuItem={(action) => handleMenuAction(action, item)}
|
|
248
|
-
action={action}
|
|
249
|
-
onMenuOpenChange={(open: boolean) => {
|
|
250
|
-
if (open && !isSelected) {
|
|
251
|
-
handleSelect(id);
|
|
241
|
+
subtitle={
|
|
242
|
+
<span className={cx(isSelected && "text-primary")}>
|
|
243
|
+
{renderDetail?.(item, isSelected)}
|
|
244
|
+
</span>
|
|
252
245
|
}
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
246
|
+
loading={false}
|
|
247
|
+
selected={isSelected}
|
|
248
|
+
onClick={() => {
|
|
249
|
+
onClickItem?.(id);
|
|
250
|
+
}}
|
|
251
|
+
onSelect={(event) => {
|
|
252
|
+
handleSelect(id, event);
|
|
253
|
+
}}
|
|
254
|
+
onKeyDown={(event) => {
|
|
255
|
+
if (event.key === "Enter") {
|
|
256
|
+
setRenamingId(id);
|
|
257
|
+
}
|
|
258
|
+
}}
|
|
259
|
+
onDoubleClick={() => {
|
|
260
|
+
onDoubleClickItem?.(id);
|
|
261
|
+
}}
|
|
262
|
+
menuItems={menuItems}
|
|
263
|
+
onSelectMenuItem={(action) => handleMenuAction(action, item)}
|
|
264
|
+
action={action}
|
|
265
|
+
onMenuOpenChange={(open: boolean) => {
|
|
266
|
+
if (open && !isSelected) {
|
|
267
|
+
handleSelect(id);
|
|
268
|
+
}
|
|
269
|
+
}}
|
|
270
|
+
// getDropTargetParentIndex={getDropTargetParentIndex}
|
|
271
|
+
>
|
|
272
|
+
{thumbnail && (
|
|
273
|
+
<div
|
|
274
|
+
className={cx(
|
|
275
|
+
"rounded overflow-hidden flex-none flex w-full",
|
|
276
|
+
isSelected && "text-primary"
|
|
277
|
+
)}
|
|
278
|
+
tabIndex={-1}
|
|
279
|
+
>
|
|
280
|
+
{thumbnail}
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
</GridView.Item>
|
|
284
|
+
);
|
|
285
|
+
})}
|
|
286
|
+
</GridView.Section>
|
|
287
|
+
) : (
|
|
288
|
+
renderEmptyState?.()
|
|
289
|
+
)}
|
|
270
290
|
</GridView.Root>
|
|
271
291
|
);
|
|
272
292
|
})
|