@noya-app/noya-designsystem 0.1.61 → 0.1.63
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/.eslintrc.js +1 -5
- package/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-lint.log +0 -1
- package/CHANGELOG.md +16 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +147 -69
- package/dist/index.d.ts +147 -69
- package/dist/index.js +1347 -1063
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +756 -473
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -4
- package/src/components/Button.tsx +28 -20
- package/src/components/DimensionInput.tsx +1 -1
- package/src/components/DraggableMenuButton.tsx +20 -4
- package/src/components/DropdownMenu.tsx +31 -5
- package/src/components/IconButton.tsx +2 -2
- package/src/components/InputField.tsx +5 -5
- package/src/components/Label.tsx +0 -2
- package/src/components/LabeledField.tsx +36 -34
- package/src/components/Popover.tsx +12 -3
- package/src/components/SegmentedControl.tsx +1 -1
- package/src/components/SelectMenu.tsx +3 -3
- package/src/components/SelectionToolbar.tsx +28 -7
- package/src/components/Slider.tsx +1 -1
- package/src/components/Spacer.tsx +0 -1
- package/src/components/Tabs.tsx +46 -0
- package/src/components/Toolbar.tsx +106 -11
- package/src/components/Tooltip.tsx +8 -8
- package/src/components/file-explorer/FileExplorerLayout.tsx +12 -6
- package/src/components/internal/Menu.tsx +19 -2
- package/src/components/internal/MenuViewport.tsx +4 -3
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +7 -3
- package/src/components/workspace/PanelWorkspaceLayout.tsx +103 -3
- package/src/components/workspace/WorkspaceLayout.tsx +96 -15
- package/src/components/workspace/renderPanelChildren.tsx +12 -0
- package/src/components/workspace/types.ts +19 -3
- package/src/hooks/useLabel.ts +10 -10
- package/src/hooks/usePreservePanelSize.tsx +1 -1
- package/src/index.css +10 -6
- package/src/index.tsx +2 -0
- package/src/utils/editableBlockStyles.ts +10 -0
- package/src/hooks/useWindowSize.tsx +0 -26
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { useControlledOrUncontrolled } from "@noya-app/react-utils";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { DetailedHTMLProps, HTMLAttributes } from "react";
|
|
4
|
+
import { cx } from "../utils/classNames";
|
|
5
|
+
import { Divider } from "./Divider";
|
|
6
|
+
import { isSelectableMenuItem, MenuItem } from "./internal/Menu";
|
|
7
|
+
import { SegmentedControl } from "./SegmentedControl";
|
|
8
|
+
|
|
9
|
+
export type TabsProps<T extends string = string> = {
|
|
10
|
+
value?: T;
|
|
11
|
+
onValueChange?: (value: T) => void;
|
|
12
|
+
items: MenuItem<T>[];
|
|
13
|
+
renderContent: ({ value }: { value: T }) => React.ReactNode;
|
|
14
|
+
} & DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>;
|
|
15
|
+
|
|
16
|
+
export function Tabs<T extends string = string>({
|
|
17
|
+
value,
|
|
18
|
+
onValueChange,
|
|
19
|
+
items,
|
|
20
|
+
renderContent,
|
|
21
|
+
...props
|
|
22
|
+
}: TabsProps<T>) {
|
|
23
|
+
const selectableItems = items.filter(isSelectableMenuItem);
|
|
24
|
+
|
|
25
|
+
const [activeTab, setActiveTab] = useControlledOrUncontrolled({
|
|
26
|
+
defaultValue: selectableItems.at(0)?.value ?? ("" as T),
|
|
27
|
+
value,
|
|
28
|
+
onChange: onValueChange,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div {...props} className={cx("flex flex-col", props.className)}>
|
|
33
|
+
<div className="flex flex-row px-3">
|
|
34
|
+
<SegmentedControl
|
|
35
|
+
className="flex-1"
|
|
36
|
+
value={activeTab}
|
|
37
|
+
onValueChange={setActiveTab}
|
|
38
|
+
variant="tabs"
|
|
39
|
+
items={selectableItems}
|
|
40
|
+
/>
|
|
41
|
+
</div>
|
|
42
|
+
<Divider />
|
|
43
|
+
{renderContent({ value: activeTab })}
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
|
|
2
|
-
import React, { ComponentProps } from "react";
|
|
2
|
+
import React, { ComponentProps, ReactNode } from "react";
|
|
3
3
|
|
|
4
4
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
5
5
|
import { BaseToolbar } from "./BaseToolbar";
|
|
@@ -9,17 +9,17 @@ import { DropdownMenu } from "./DropdownMenu";
|
|
|
9
9
|
import {
|
|
10
10
|
getKeyboardShortcutsForMenuItems,
|
|
11
11
|
isSelectableMenuItem,
|
|
12
|
+
KeyboardShortcut,
|
|
12
13
|
MenuItem,
|
|
14
|
+
PopoverMenuItem,
|
|
13
15
|
SelectableMenuItem,
|
|
14
16
|
SubMenuItem,
|
|
15
17
|
} from "./internal/Menu";
|
|
18
|
+
import { Popover } from "./Popover";
|
|
19
|
+
import { Spacer } from "./Spacer";
|
|
16
20
|
import { Tooltip } from "./Tooltip";
|
|
17
21
|
|
|
18
|
-
const
|
|
19
|
-
height: 27,
|
|
20
|
-
width: 27,
|
|
21
|
-
minWidth: 27,
|
|
22
|
-
};
|
|
22
|
+
const SIDE_OFFSET = 6;
|
|
23
23
|
|
|
24
24
|
export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
|
|
25
25
|
T extends string,
|
|
@@ -55,24 +55,101 @@ export const ToolbarMenuDropdown = memoGeneric(function ToolbarMenuDropdown<
|
|
|
55
55
|
);
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
export const ToolbarShortcut = ({
|
|
59
|
+
shortcut,
|
|
60
|
+
label,
|
|
61
|
+
}: {
|
|
62
|
+
shortcut: string;
|
|
63
|
+
label?: ReactNode;
|
|
64
|
+
}) => {
|
|
65
|
+
return (
|
|
66
|
+
<div className="flex items-center">
|
|
67
|
+
{label}
|
|
68
|
+
<Spacer.Horizontal size={6} />
|
|
69
|
+
<KeyboardShortcut shortcut={shortcut} />
|
|
70
|
+
</div>
|
|
71
|
+
);
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const ToolbarMenuPopover = memoGeneric(function ToolbarMenuPopover({
|
|
75
|
+
item,
|
|
76
|
+
portalContainer,
|
|
77
|
+
}: {
|
|
78
|
+
item: PopoverMenuItem;
|
|
79
|
+
portalContainer?: HTMLElement | null;
|
|
80
|
+
}) {
|
|
81
|
+
const {
|
|
82
|
+
disabled,
|
|
83
|
+
icon,
|
|
84
|
+
title,
|
|
85
|
+
tooltip,
|
|
86
|
+
content: itemContent,
|
|
87
|
+
checked,
|
|
88
|
+
} = item;
|
|
89
|
+
const [open, setOpen] = React.useState(false);
|
|
90
|
+
|
|
91
|
+
const content = (
|
|
92
|
+
<span>
|
|
93
|
+
<Popover
|
|
94
|
+
open={open}
|
|
95
|
+
onOpenChange={setOpen}
|
|
96
|
+
side="top"
|
|
97
|
+
showArrow={false}
|
|
98
|
+
sideOffset={SIDE_OFFSET}
|
|
99
|
+
portalContainer={portalContainer}
|
|
100
|
+
trigger={
|
|
101
|
+
<Button
|
|
102
|
+
disabled={disabled}
|
|
103
|
+
active={checked || open}
|
|
104
|
+
icon={icon}
|
|
105
|
+
iconRight="DropdownChevronIcon"
|
|
106
|
+
>
|
|
107
|
+
{title}
|
|
108
|
+
</Button>
|
|
109
|
+
}
|
|
110
|
+
>
|
|
111
|
+
{typeof itemContent === "function"
|
|
112
|
+
? itemContent({ close: () => setOpen(false) })
|
|
113
|
+
: itemContent}
|
|
114
|
+
</Popover>
|
|
115
|
+
</span>
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
return tooltip && !open ? (
|
|
119
|
+
<Tooltip
|
|
120
|
+
sideOffset={SIDE_OFFSET}
|
|
121
|
+
side="top"
|
|
122
|
+
content={tooltip}
|
|
123
|
+
portalContainer={portalContainer}
|
|
124
|
+
>
|
|
125
|
+
{content}
|
|
126
|
+
</Tooltip>
|
|
127
|
+
) : (
|
|
128
|
+
content
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
|
|
58
132
|
export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
59
133
|
T extends string,
|
|
60
134
|
>({
|
|
61
135
|
item,
|
|
62
136
|
onSelectMenuItem,
|
|
63
137
|
as,
|
|
138
|
+
activeValue,
|
|
139
|
+
tooltipSide,
|
|
64
140
|
}: {
|
|
65
141
|
item: SelectableMenuItem<T>;
|
|
66
142
|
onSelectMenuItem?: (value: T) => void;
|
|
67
143
|
as?: ComponentProps<typeof Button>["as"];
|
|
144
|
+
activeValue?: T;
|
|
145
|
+
tooltipSide?: ComponentProps<typeof Tooltip>["side"];
|
|
68
146
|
}) {
|
|
69
147
|
const content = (
|
|
70
148
|
<Button
|
|
71
149
|
as={as}
|
|
72
150
|
disabled={item.disabled}
|
|
73
|
-
active={item.checked}
|
|
151
|
+
active={item.checked || activeValue === item.value}
|
|
74
152
|
icon={item.icon}
|
|
75
|
-
style={item.icon && !item.title ? iconButtonStyle : undefined}
|
|
76
153
|
{...(as === "a" && { href: item.value })}
|
|
77
154
|
onClick={() => {
|
|
78
155
|
if (onSelectMenuItem && item.value) {
|
|
@@ -85,7 +162,7 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
|
85
162
|
);
|
|
86
163
|
|
|
87
164
|
return item.tooltip ? (
|
|
88
|
-
<Tooltip sideOffset={
|
|
165
|
+
<Tooltip sideOffset={SIDE_OFFSET} content={item.tooltip} side={tooltipSide}>
|
|
89
166
|
{content}
|
|
90
167
|
</Tooltip>
|
|
91
168
|
) : (
|
|
@@ -99,23 +176,32 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
99
176
|
item,
|
|
100
177
|
onSelectMenuItem,
|
|
101
178
|
buttonAs,
|
|
179
|
+
activeValue,
|
|
180
|
+
tooltipSide,
|
|
181
|
+
portalContainer,
|
|
102
182
|
}: {
|
|
103
183
|
item: MenuItem<T>;
|
|
104
184
|
onSelectMenuItem?: (value: T) => void;
|
|
105
185
|
buttonAs?: ComponentProps<typeof Button>["as"];
|
|
186
|
+
activeValue?: T;
|
|
187
|
+
tooltipSide?: ComponentProps<typeof Tooltip>["side"];
|
|
188
|
+
portalContainer?: HTMLElement | null;
|
|
106
189
|
}) {
|
|
107
190
|
if (item.type === "sectionHeader") return null;
|
|
108
|
-
|
|
109
191
|
if (item.type === "separator") {
|
|
110
192
|
return <DividerVertical overflow={4} />;
|
|
111
193
|
}
|
|
112
|
-
|
|
194
|
+
if (item.type === "popover") {
|
|
195
|
+
return <ToolbarMenuPopover item={item} portalContainer={portalContainer} />;
|
|
196
|
+
}
|
|
113
197
|
if (isSelectableMenuItem(item)) {
|
|
114
198
|
return (
|
|
115
199
|
<ToolbarMenuButton
|
|
116
200
|
item={item}
|
|
117
201
|
onSelectMenuItem={onSelectMenuItem}
|
|
118
202
|
as={buttonAs}
|
|
203
|
+
activeValue={activeValue}
|
|
204
|
+
tooltipSide={tooltipSide}
|
|
119
205
|
/>
|
|
120
206
|
);
|
|
121
207
|
}
|
|
@@ -129,10 +215,16 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
|
129
215
|
items,
|
|
130
216
|
onSelectMenuItem,
|
|
131
217
|
buttonAs,
|
|
218
|
+
activeValue,
|
|
219
|
+
tooltipSide,
|
|
220
|
+
portalContainer,
|
|
132
221
|
}: {
|
|
133
222
|
items: MenuItem<T>[];
|
|
134
223
|
onSelectMenuItem?: (value: T) => void;
|
|
135
224
|
buttonAs?: ComponentProps<typeof Button>["as"];
|
|
225
|
+
activeValue?: T;
|
|
226
|
+
tooltipSide?: ComponentProps<typeof Tooltip>["side"];
|
|
227
|
+
portalContainer?: HTMLElement | null;
|
|
136
228
|
}) {
|
|
137
229
|
return (
|
|
138
230
|
<>
|
|
@@ -143,6 +235,9 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
|
143
235
|
item={item}
|
|
144
236
|
onSelectMenuItem={onSelectMenuItem}
|
|
145
237
|
buttonAs={buttonAs}
|
|
238
|
+
activeValue={activeValue}
|
|
239
|
+
tooltipSide={tooltipSide}
|
|
240
|
+
portalContainer={portalContainer}
|
|
146
241
|
/>
|
|
147
242
|
);
|
|
148
243
|
})}
|
|
@@ -1,25 +1,26 @@
|
|
|
1
1
|
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
|
|
2
2
|
import * as React from "react";
|
|
3
|
+
import { ComponentProps } from "react";
|
|
3
4
|
import {
|
|
4
5
|
portalScopeProps,
|
|
5
6
|
usePortalScopeId,
|
|
6
7
|
} from "../contexts/PortalScopeContext";
|
|
7
8
|
import { textStyles } from "./Text";
|
|
8
9
|
|
|
9
|
-
// const Arrow = styled(TooltipPrimitive.Arrow)(({ theme }) => ({
|
|
10
|
-
// fill: theme.colors.popover.background,
|
|
11
|
-
// }));
|
|
12
|
-
|
|
13
10
|
interface Props {
|
|
14
11
|
children: React.ReactNode;
|
|
15
12
|
content: React.ReactNode;
|
|
16
13
|
sideOffset?: number;
|
|
14
|
+
side?: ComponentProps<typeof TooltipPrimitive.Content>["side"];
|
|
15
|
+
portalContainer?: HTMLElement | null;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export const Tooltip = React.memo(function Tooltip({
|
|
20
19
|
children,
|
|
21
20
|
content,
|
|
22
21
|
sideOffset = 2,
|
|
22
|
+
side = "bottom",
|
|
23
|
+
portalContainer,
|
|
23
24
|
}: Props) {
|
|
24
25
|
const portalScopeId = usePortalScopeId();
|
|
25
26
|
|
|
@@ -27,17 +28,16 @@ export const Tooltip = React.memo(function Tooltip({
|
|
|
27
28
|
<TooltipPrimitive.Provider>
|
|
28
29
|
<TooltipPrimitive.Root>
|
|
29
30
|
<TooltipPrimitive.Trigger asChild>{children}</TooltipPrimitive.Trigger>
|
|
30
|
-
<TooltipPrimitive.Portal>
|
|
31
|
+
<TooltipPrimitive.Portal container={portalContainer}>
|
|
31
32
|
<TooltipPrimitive.Content
|
|
32
33
|
{...portalScopeProps(portalScopeId)}
|
|
33
|
-
side=
|
|
34
|
+
side={side}
|
|
34
35
|
align="center"
|
|
35
36
|
sideOffset={sideOffset}
|
|
36
37
|
collisionPadding={8}
|
|
37
|
-
className={`${textStyles.small} text-text rounded-
|
|
38
|
+
className={`${textStyles.small} text-text rounded-sm p-2 bg-popover-background shadow-tooltip z-menu`}
|
|
38
39
|
>
|
|
39
40
|
{content}
|
|
40
|
-
{/* <Arrow /> */}
|
|
41
41
|
</TooltipPrimitive.Content>
|
|
42
42
|
</TooltipPrimitive.Portal>
|
|
43
43
|
</TooltipPrimitive.Root>
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import {
|
|
2
|
+
ActivityIndicator,
|
|
2
3
|
Banner,
|
|
3
4
|
BannerProps,
|
|
4
|
-
|
|
5
|
+
Button,
|
|
5
6
|
Section,
|
|
6
7
|
SectionProps,
|
|
7
8
|
} from "@noya-app/noya-designsystem";
|
|
@@ -28,20 +29,25 @@ export const FileExplorerLayout = ({
|
|
|
28
29
|
export const FileExplorerUploadButton = ({
|
|
29
30
|
showUploadButton,
|
|
30
31
|
onUpload,
|
|
32
|
+
isUploading,
|
|
31
33
|
children,
|
|
32
34
|
}: {
|
|
33
35
|
showUploadButton?: boolean;
|
|
34
36
|
onUpload?: () => void;
|
|
37
|
+
isUploading?: boolean;
|
|
35
38
|
children?: React.ReactNode;
|
|
36
39
|
}) => (
|
|
37
40
|
<div className="flex items-center gap-2">
|
|
38
41
|
{showUploadButton && (
|
|
39
|
-
<
|
|
40
|
-
|
|
42
|
+
<Button
|
|
43
|
+
icon={isUploading ? <ActivityIndicator size={15} /> : "UploadIcon"}
|
|
41
44
|
onClick={onUpload}
|
|
42
|
-
aria-label="Upload
|
|
43
|
-
tooltip="Upload
|
|
44
|
-
|
|
45
|
+
aria-label="Upload file"
|
|
46
|
+
tooltip="Upload file"
|
|
47
|
+
disabled={isUploading}
|
|
48
|
+
>
|
|
49
|
+
Upload
|
|
50
|
+
</Button>
|
|
45
51
|
)}
|
|
46
52
|
{children}
|
|
47
53
|
</div>
|
|
@@ -101,16 +101,26 @@ export type SubMenuItem<T extends string> = BaseMenuItem & {
|
|
|
101
101
|
id: string;
|
|
102
102
|
};
|
|
103
103
|
|
|
104
|
+
export type PopoverMenuItem = BaseMenuItem & {
|
|
105
|
+
type: "popover";
|
|
106
|
+
id: string;
|
|
107
|
+
content: ReactNode | ((props: { close: () => void }) => ReactNode);
|
|
108
|
+
shortcut?: string;
|
|
109
|
+
tooltip?: ReactNode;
|
|
110
|
+
};
|
|
111
|
+
|
|
104
112
|
export type NonSelectableMenuItem<T extends string> =
|
|
105
113
|
| SeparatorItem
|
|
106
114
|
| SectionHeaderMenuItem
|
|
107
|
-
| SubMenuItem<T
|
|
115
|
+
| SubMenuItem<T>
|
|
116
|
+
| PopoverMenuItem;
|
|
108
117
|
|
|
109
118
|
export type MenuItem<T extends string> =
|
|
110
119
|
| SeparatorItem
|
|
111
120
|
| SelectableMenuItem<T>
|
|
112
121
|
| SectionHeaderMenuItem
|
|
113
|
-
| SubMenuItem<T
|
|
122
|
+
| SubMenuItem<T>
|
|
123
|
+
| PopoverMenuItem;
|
|
114
124
|
|
|
115
125
|
type ScoredSelectableMenuItem<T extends string> = SelectableMenuItem<T> &
|
|
116
126
|
IScoredItem;
|
|
@@ -142,6 +152,10 @@ export const isSelectableMenuItemWithScore = (
|
|
|
142
152
|
): item is ScoredSelectableMenuItem<string> =>
|
|
143
153
|
isSelectableMenuItem(item) && "index" in item && "score" in item;
|
|
144
154
|
|
|
155
|
+
export const isPopoverMenuItem = (
|
|
156
|
+
item: MenuItem<string>
|
|
157
|
+
): item is PopoverMenuItem => item.type === "popover";
|
|
158
|
+
|
|
145
159
|
export const getMenuItemKey = <T extends string>(
|
|
146
160
|
item: MenuItem<T>,
|
|
147
161
|
index: number
|
|
@@ -153,6 +167,8 @@ export const getMenuItemKey = <T extends string>(
|
|
|
153
167
|
return item.id;
|
|
154
168
|
case "submenu":
|
|
155
169
|
return item.id;
|
|
170
|
+
case "popover":
|
|
171
|
+
return item.id;
|
|
156
172
|
default:
|
|
157
173
|
return item.value;
|
|
158
174
|
}
|
|
@@ -191,6 +207,7 @@ function getKeyboardShortcuts<T extends string>(
|
|
|
191
207
|
): [string, T][] {
|
|
192
208
|
if (item.type === "separator") return [];
|
|
193
209
|
if (item.type === "sectionHeader") return [];
|
|
210
|
+
if (item.type === "popover") return [];
|
|
194
211
|
|
|
195
212
|
if (item.type === "submenu") return item.items.flatMap(getKeyboardShortcuts);
|
|
196
213
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
usePortalScopeId,
|
|
8
8
|
} from "../../contexts/PortalScopeContext";
|
|
9
9
|
import { cx } from "../../utils/classNames";
|
|
10
|
-
import { MenuItem, SectionHeader, styles } from "./Menu";
|
|
10
|
+
import { isSelectableMenuItem, MenuItem, SectionHeader, styles } from "./Menu";
|
|
11
11
|
import { SelectItem } from "./SelectItem";
|
|
12
12
|
|
|
13
13
|
type MenuSeparatorComponent = React.FC<{ className?: string }>;
|
|
@@ -66,8 +66,7 @@ export const MenuViewport = <T extends string>({
|
|
|
66
66
|
const portalScopeId = usePortalScopeId();
|
|
67
67
|
|
|
68
68
|
const hasCheckedItem = items.some(
|
|
69
|
-
(item) =>
|
|
70
|
-
item.type !== "separator" && item.type !== "sectionHeader" && item.checked
|
|
69
|
+
(item) => isSelectableMenuItem(item) && item.checked
|
|
71
70
|
);
|
|
72
71
|
|
|
73
72
|
return (
|
|
@@ -91,6 +90,8 @@ export const MenuViewport = <T extends string>({
|
|
|
91
90
|
indented={hasCheckedItem}
|
|
92
91
|
/>
|
|
93
92
|
);
|
|
93
|
+
case "popover":
|
|
94
|
+
return null;
|
|
94
95
|
case "submenu":
|
|
95
96
|
if (
|
|
96
97
|
item.items &&
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memoGeneric } from "@noya-app/react-utils";
|
|
2
|
+
import React, { useLayoutEffect, useRef, useState } from "react";
|
|
2
3
|
import {
|
|
3
4
|
EDITOR_PANEL_GROUP_ID,
|
|
4
5
|
LEFT_SIDEBAR_ID,
|
|
@@ -7,7 +8,10 @@ import {
|
|
|
7
8
|
import { Drawer } from "../Drawer";
|
|
8
9
|
import { LayoutProps } from "./types";
|
|
9
10
|
|
|
10
|
-
export const DrawerWorkspaceLayout =
|
|
11
|
+
export const DrawerWorkspaceLayout = memoGeneric(function DrawerWorkspaceLayout<
|
|
12
|
+
LeftTab extends string = string,
|
|
13
|
+
RightTab extends string = string,
|
|
14
|
+
>({
|
|
11
15
|
leftPanel,
|
|
12
16
|
leftSidebarRef,
|
|
13
17
|
rightPanel,
|
|
@@ -15,7 +19,7 @@ export const DrawerWorkspaceLayout = memo(function DrawerWorkspaceLayout({
|
|
|
15
19
|
centerPanel,
|
|
16
20
|
internalLayoutState,
|
|
17
21
|
handleChangeLayoutState,
|
|
18
|
-
}: LayoutProps) {
|
|
22
|
+
}: LayoutProps<LeftTab, RightTab>) {
|
|
19
23
|
const portalContainer = useRef<HTMLDivElement>(null);
|
|
20
24
|
const [mounted, setMounted] = useState(false);
|
|
21
25
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { memoGeneric } from "@noya-app/react-utils";
|
|
2
|
+
import React, { ComponentProps, useRef } from "react";
|
|
2
3
|
import {
|
|
3
4
|
ImperativePanelGroupHandle,
|
|
4
5
|
ImperativePanelHandle,
|
|
@@ -13,9 +14,17 @@ import {
|
|
|
13
14
|
RIGHT_SIDEBAR_ID,
|
|
14
15
|
usePreservePanelSize,
|
|
15
16
|
} from "../../hooks/usePreservePanelSize";
|
|
17
|
+
import { cssVarNames } from "../../theme";
|
|
18
|
+
import { DividerVertical } from "../Divider";
|
|
19
|
+
import { MenuItem } from "../internal/Menu";
|
|
20
|
+
import { ToolbarMenu } from "../Toolbar";
|
|
21
|
+
import { Tooltip } from "../Tooltip";
|
|
16
22
|
import { LayoutProps } from "./types";
|
|
17
23
|
|
|
18
|
-
export const PanelWorkspaceLayout =
|
|
24
|
+
export const PanelWorkspaceLayout = memoGeneric(function PanelWorkspaceLayout<
|
|
25
|
+
LeftTab extends string,
|
|
26
|
+
RightTab extends string,
|
|
27
|
+
>({
|
|
19
28
|
leftPanel,
|
|
20
29
|
rightPanel,
|
|
21
30
|
centerPanel,
|
|
@@ -26,8 +35,14 @@ export const PanelWorkspaceLayout = memo(function PanelWorkspaceLayout({
|
|
|
26
35
|
internalLayoutState,
|
|
27
36
|
leftSidebarRef,
|
|
28
37
|
rightSidebarRef,
|
|
38
|
+
leftTabItems,
|
|
39
|
+
rightTabItems,
|
|
40
|
+
leftTabValue,
|
|
41
|
+
rightTabValue,
|
|
42
|
+
onChangeLeftTab,
|
|
43
|
+
onChangeRightTab,
|
|
29
44
|
handleChangeLayoutState,
|
|
30
|
-
}: LayoutProps) {
|
|
45
|
+
}: LayoutProps<LeftTab, RightTab>) {
|
|
31
46
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
|
32
47
|
|
|
33
48
|
usePreservePanelSize(panelGroupRef, handleChangeLayoutState);
|
|
@@ -46,6 +61,33 @@ export const PanelWorkspaceLayout = memo(function PanelWorkspaceLayout({
|
|
|
46
61
|
>
|
|
47
62
|
{leftPanel && (
|
|
48
63
|
<>
|
|
64
|
+
{leftTabItems && (
|
|
65
|
+
<VerticalTabMenu
|
|
66
|
+
tooltipSide="right"
|
|
67
|
+
items={leftTabItems}
|
|
68
|
+
activeValue={leftTabValue}
|
|
69
|
+
isSidebarCollapsed={
|
|
70
|
+
internalLayoutState?.leftSidebarCollapsed ?? false
|
|
71
|
+
}
|
|
72
|
+
onChange={(value) => {
|
|
73
|
+
if (value === leftTabValue) {
|
|
74
|
+
if (leftSidebarRef.current?.isExpanded()) {
|
|
75
|
+
leftSidebarRef.current?.collapse();
|
|
76
|
+
} else {
|
|
77
|
+
leftSidebarRef.current?.expand();
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
if (!leftSidebarRef.current?.isExpanded()) {
|
|
81
|
+
leftSidebarRef.current?.expand();
|
|
82
|
+
}
|
|
83
|
+
onChangeLeftTab?.(value);
|
|
84
|
+
}
|
|
85
|
+
}}
|
|
86
|
+
/>
|
|
87
|
+
)}
|
|
88
|
+
{leftTabItems && !internalLayoutState?.leftSidebarCollapsed && (
|
|
89
|
+
<DividerVertical className="h-full" />
|
|
90
|
+
)}
|
|
49
91
|
<Panel
|
|
50
92
|
id={LEFT_SIDEBAR_ID}
|
|
51
93
|
className="relative"
|
|
@@ -95,8 +137,66 @@ export const PanelWorkspaceLayout = memo(function PanelWorkspaceLayout({
|
|
|
95
137
|
>
|
|
96
138
|
{internalLayoutState?.rightSidebarCollapsed ? null : rightPanel}
|
|
97
139
|
</Panel>
|
|
140
|
+
{rightTabItems && !internalLayoutState?.rightSidebarCollapsed && (
|
|
141
|
+
<DividerVertical className="h-full" />
|
|
142
|
+
)}
|
|
143
|
+
{rightTabItems && (
|
|
144
|
+
<VerticalTabMenu
|
|
145
|
+
tooltipSide="left"
|
|
146
|
+
items={rightTabItems}
|
|
147
|
+
activeValue={rightTabValue}
|
|
148
|
+
isSidebarCollapsed={
|
|
149
|
+
internalLayoutState?.rightSidebarCollapsed ?? false
|
|
150
|
+
}
|
|
151
|
+
onChange={(value) => {
|
|
152
|
+
if (value === rightTabValue) {
|
|
153
|
+
if (rightSidebarRef.current?.isExpanded()) {
|
|
154
|
+
rightSidebarRef.current?.collapse();
|
|
155
|
+
} else {
|
|
156
|
+
rightSidebarRef.current?.expand();
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
if (!rightSidebarRef.current?.isExpanded()) {
|
|
160
|
+
rightSidebarRef.current?.expand();
|
|
161
|
+
}
|
|
162
|
+
onChangeRightTab?.(value);
|
|
163
|
+
}
|
|
164
|
+
}}
|
|
165
|
+
/>
|
|
166
|
+
)}
|
|
98
167
|
</>
|
|
99
168
|
)}
|
|
100
169
|
</PanelGroup>
|
|
101
170
|
);
|
|
102
171
|
});
|
|
172
|
+
|
|
173
|
+
function VerticalTabMenu<TabValue extends string>({
|
|
174
|
+
items,
|
|
175
|
+
activeValue,
|
|
176
|
+
isSidebarCollapsed,
|
|
177
|
+
onChange,
|
|
178
|
+
tooltipSide,
|
|
179
|
+
}: {
|
|
180
|
+
items: MenuItem<TabValue>[];
|
|
181
|
+
activeValue: TabValue | undefined;
|
|
182
|
+
isSidebarCollapsed: boolean;
|
|
183
|
+
onChange: (value: TabValue) => void;
|
|
184
|
+
tooltipSide?: ComponentProps<typeof Tooltip>["side"];
|
|
185
|
+
}) {
|
|
186
|
+
return (
|
|
187
|
+
<div
|
|
188
|
+
className="w-[47px] h-full bg-sidebar-background flex flex-col items-center py-3 gap-3"
|
|
189
|
+
style={{
|
|
190
|
+
[cssVarNames.colors.inputBackground]: "transparent",
|
|
191
|
+
[cssVarNames.colors.buttonBackground]: "transparent",
|
|
192
|
+
}}
|
|
193
|
+
>
|
|
194
|
+
<ToolbarMenu
|
|
195
|
+
items={items}
|
|
196
|
+
activeValue={isSidebarCollapsed ? undefined : activeValue}
|
|
197
|
+
onSelectMenuItem={onChange}
|
|
198
|
+
tooltipSide={tooltipSide}
|
|
199
|
+
/>
|
|
200
|
+
</div>
|
|
201
|
+
);
|
|
202
|
+
}
|