@noya-app/noya-designsystem 0.1.81 → 0.1.83
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 +37 -38
- package/.turbo/turbo-lint.log +1 -15
- package/CHANGELOG.md +15 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +43 -13
- package/dist/index.d.ts +43 -13
- package/dist/index.js +682 -469
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +608 -389
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -5
- package/src/components/Drawer.tsx +8 -5
- package/src/components/EmojiPicker.tsx +13 -1
- package/src/components/Popover.tsx +8 -6
- package/src/components/SegmentedControl.tsx +237 -42
- package/src/components/SelectionToolbar.tsx +88 -71
- package/src/components/resizablePanels/Panel.tsx +22 -2
- package/src/components/resizablePanels/PanelGroup.tsx +15 -8
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +131 -152
- package/src/components/workspace/HorizontalTabBar.tsx +43 -0
- package/src/components/workspace/PanelWorkspaceLayout.tsx +112 -54
- package/src/components/workspace/WorkspaceLayout.tsx +19 -5
- package/src/components/workspace/types.ts +17 -5
- package/src/index.css +12 -2
- package/src/utils/mergeProps.ts +38 -0
- package/dist/chunk-D57E6H3M.mjs +0 -36
- package/dist/chunk-D57E6H3M.mjs.map +0 -1
- package/dist/chunk-FJ6ZGZIA.mjs +0 -43
- package/dist/chunk-FJ6ZGZIA.mjs.map +0 -1
|
@@ -8,47 +8,44 @@ import {
|
|
|
8
8
|
} from "@noya-app/noya-designsystem";
|
|
9
9
|
import { Rect } from "@noya-app/noya-geometry";
|
|
10
10
|
import { useSize } from "@noya-app/react-utils";
|
|
11
|
-
import
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
x: rect.x,
|
|
22
|
-
y: rect.y,
|
|
23
|
-
top: rect.y,
|
|
24
|
-
right: rect.x + rect.width,
|
|
25
|
-
bottom: rect.y + rect.height,
|
|
26
|
-
left: rect.x,
|
|
27
|
-
toJSON: () => null,
|
|
28
|
-
}),
|
|
29
|
-
});
|
|
11
|
+
import {
|
|
12
|
+
useFloating,
|
|
13
|
+
offset,
|
|
14
|
+
flip,
|
|
15
|
+
shift,
|
|
16
|
+
FloatingPortal,
|
|
17
|
+
type Placement,
|
|
18
|
+
type VirtualElement,
|
|
19
|
+
} from "@floating-ui/react";
|
|
20
|
+
import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
|
|
30
21
|
|
|
31
|
-
type
|
|
32
|
-
|
|
33
|
-
>;
|
|
22
|
+
type Side = "top" | "right" | "bottom" | "left";
|
|
23
|
+
type Align = "start" | "center" | "end";
|
|
34
24
|
|
|
35
|
-
type SelectionToolbarContainerProps =
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
25
|
+
type SelectionToolbarContainerProps = {
|
|
26
|
+
side?: Side;
|
|
27
|
+
sideOffset?: number;
|
|
28
|
+
align?: Align;
|
|
29
|
+
avoidCollisions?: boolean;
|
|
39
30
|
rect: Rect;
|
|
40
31
|
children: React.ReactNode;
|
|
41
|
-
portalContainer?: HTMLElement | null;
|
|
42
32
|
containerClassName?: string;
|
|
43
33
|
containerStyle?: React.CSSProperties;
|
|
44
34
|
transitionDuration?: number;
|
|
45
35
|
};
|
|
46
36
|
|
|
37
|
+
// Map side + align to floating-ui placement
|
|
38
|
+
function getPlacement(side: Side, align: Align): Placement {
|
|
39
|
+
if (align === "center") {
|
|
40
|
+
return side;
|
|
41
|
+
}
|
|
42
|
+
return `${side}-${align}` as Placement;
|
|
43
|
+
}
|
|
44
|
+
|
|
47
45
|
export const SelectionToolbarContainer = memo(
|
|
48
46
|
function SelectionToolbarContainer({
|
|
49
47
|
rect,
|
|
50
48
|
children,
|
|
51
|
-
portalContainer,
|
|
52
49
|
side = "top",
|
|
53
50
|
sideOffset = 10,
|
|
54
51
|
align = "center",
|
|
@@ -58,58 +55,78 @@ export const SelectionToolbarContainer = memo(
|
|
|
58
55
|
transitionDuration = 0.2,
|
|
59
56
|
}: SelectionToolbarContainerProps) {
|
|
60
57
|
const portalScopeId = usePortalScopeId();
|
|
61
|
-
const
|
|
62
|
-
|
|
58
|
+
const [containerElement, setContainerElement] =
|
|
59
|
+
useState<HTMLDivElement | null>(null);
|
|
60
|
+
const containerRef = useCallback((el: HTMLDivElement | null) => {
|
|
61
|
+
setContainerElement(el);
|
|
62
|
+
}, []);
|
|
63
|
+
const containerRefObject = useMemo(
|
|
64
|
+
() => ({ current: containerElement }),
|
|
65
|
+
[containerElement]
|
|
66
|
+
);
|
|
67
|
+
const size = useSize(containerRefObject, "width");
|
|
63
68
|
|
|
64
69
|
// Create a virtual reference element from the rect
|
|
65
|
-
const
|
|
66
|
-
() => ({
|
|
67
|
-
|
|
70
|
+
const virtualElement = useMemo(
|
|
71
|
+
(): VirtualElement => ({
|
|
72
|
+
getBoundingClientRect: () => ({
|
|
73
|
+
width: rect.width,
|
|
74
|
+
height: rect.height,
|
|
75
|
+
x: rect.x,
|
|
76
|
+
y: rect.y,
|
|
77
|
+
top: rect.y,
|
|
78
|
+
right: rect.x + rect.width,
|
|
79
|
+
bottom: rect.y + rect.height,
|
|
80
|
+
left: rect.x,
|
|
81
|
+
}),
|
|
68
82
|
}),
|
|
69
83
|
[rect]
|
|
70
84
|
);
|
|
71
85
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}}
|
|
88
|
-
>
|
|
86
|
+
const placement = getPlacement(side, align);
|
|
87
|
+
|
|
88
|
+
const { refs, floatingStyles } = useFloating({
|
|
89
|
+
placement,
|
|
90
|
+
middleware: avoidCollisions
|
|
91
|
+
? [offset(sideOffset), flip(), shift({ padding: 10 })]
|
|
92
|
+
: [offset(sideOffset)],
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
refs.setPositionReference(virtualElement);
|
|
97
|
+
}, [refs, virtualElement]);
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<FloatingPortal>
|
|
89
101
|
<div
|
|
90
|
-
ref={
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
102
|
+
ref={refs.setFloating}
|
|
103
|
+
{...portalScopeProps(portalScopeId)}
|
|
104
|
+
contentEditable={false}
|
|
105
|
+
style={{
|
|
106
|
+
...floatingStyles,
|
|
107
|
+
opacity: size ? 1 : 0,
|
|
108
|
+
transition: `opacity ${transitionDuration}s ease-in-out`,
|
|
109
|
+
[cssVarNames.colors.inputBackground]: "transparent",
|
|
110
|
+
[cssVarNames.colors.buttonBackground]: "transparent",
|
|
111
|
+
}}
|
|
112
|
+
className="n-z-menu"
|
|
100
113
|
>
|
|
101
|
-
|
|
114
|
+
<div
|
|
115
|
+
ref={containerRef}
|
|
116
|
+
className={cx(
|
|
117
|
+
"n-flex n-gap-1 n-bg-popover-background n-rounded n-shadow-popover n-p-1",
|
|
118
|
+
containerClassName
|
|
119
|
+
)}
|
|
120
|
+
style={containerStyle}
|
|
121
|
+
onClick={(e) => e.stopPropagation()}
|
|
122
|
+
onMouseDown={(e) => e.stopPropagation()}
|
|
123
|
+
onPointerDown={(e) => e.stopPropagation()}
|
|
124
|
+
onWheel={(e) => e.stopPropagation()}
|
|
125
|
+
>
|
|
126
|
+
{children}
|
|
127
|
+
</div>
|
|
102
128
|
</div>
|
|
103
|
-
</
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
return (
|
|
107
|
-
<PopperPrimitive.Root>
|
|
108
|
-
<PopperPrimitive.Anchor virtualRef={virtualRef} />
|
|
109
|
-
{portalContainer
|
|
110
|
-
? createPortal(popperContent, portalContainer)
|
|
111
|
-
: popperContent}
|
|
112
|
-
</PopperPrimitive.Root>
|
|
129
|
+
</FloatingPortal>
|
|
113
130
|
);
|
|
114
131
|
}
|
|
115
132
|
);
|
|
@@ -25,7 +25,17 @@ interface PanelProps {
|
|
|
25
25
|
|
|
26
26
|
export const Panel = forwardRef<ImperativePanelHandle, PanelProps>(
|
|
27
27
|
function Panel(
|
|
28
|
-
{
|
|
28
|
+
{
|
|
29
|
+
id: propId,
|
|
30
|
+
order,
|
|
31
|
+
className,
|
|
32
|
+
defaultSize,
|
|
33
|
+
minSize,
|
|
34
|
+
maxSize,
|
|
35
|
+
collapsible,
|
|
36
|
+
defaultCollapsed,
|
|
37
|
+
children,
|
|
38
|
+
},
|
|
29
39
|
ref
|
|
30
40
|
) {
|
|
31
41
|
const generatedId = useId();
|
|
@@ -56,7 +66,17 @@ export const Panel = forwardRef<ImperativePanelHandle, PanelProps>(
|
|
|
56
66
|
return () => {
|
|
57
67
|
unregisterPanel(id);
|
|
58
68
|
};
|
|
59
|
-
}, [
|
|
69
|
+
}, [
|
|
70
|
+
id,
|
|
71
|
+
order,
|
|
72
|
+
defaultSize,
|
|
73
|
+
minSize,
|
|
74
|
+
maxSize,
|
|
75
|
+
collapsible,
|
|
76
|
+
defaultCollapsed,
|
|
77
|
+
registerPanel,
|
|
78
|
+
unregisterPanel,
|
|
79
|
+
]);
|
|
60
80
|
|
|
61
81
|
const size = getPanelSize(id);
|
|
62
82
|
const isFlexPanel = defaultSize === undefined;
|
|
@@ -144,17 +144,24 @@ export const PanelGroup = forwardRef<ImperativePanelGroupHandle, PanelGroupProps
|
|
|
144
144
|
const registerPanel = useCallback(
|
|
145
145
|
(panel: PanelRegistration) => {
|
|
146
146
|
setPanels((prev) => {
|
|
147
|
+
// Check saved sizes first, then use defaultCollapsed
|
|
147
148
|
const savedSizes = loadSavedSizes(autoSaveId);
|
|
148
149
|
const savedSize = savedSizes?.[panel.id];
|
|
149
|
-
// Use saved size if available, otherwise check defaultCollapsed, then use defaultSize
|
|
150
150
|
const hasSavedSize = savedSize !== undefined;
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
151
|
+
|
|
152
|
+
let collapsed: boolean;
|
|
153
|
+
if (hasSavedSize) {
|
|
154
|
+
// Use saved state
|
|
155
|
+
collapsed = savedSize === 0 && !!panel.collapsible;
|
|
156
|
+
} else {
|
|
157
|
+
// Use defaultCollapsed
|
|
158
|
+
collapsed = !!panel.defaultCollapsed && !!panel.collapsible;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const initialSize = collapsed
|
|
162
|
+
? 0
|
|
163
|
+
: hasSavedSize
|
|
164
|
+
? savedSize
|
|
158
165
|
: panel.defaultSize ?? 0;
|
|
159
166
|
|
|
160
167
|
const newPanels = new Map(prev);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
4
|
-
import React, { useMemo,
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import { Drawer } from "../Drawer";
|
|
4
|
+
import React, { useMemo, useState } from "react";
|
|
5
|
+
import { cssVarNames } from "../../theme";
|
|
6
|
+
import { Divider } from "../Divider";
|
|
8
7
|
import { isSelectableMenuItem, MenuItem } from "../internal/Menu";
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
8
|
+
import { ToolbarMenu } from "../Toolbar";
|
|
9
|
+
import { EDITOR_PANEL_GROUP_ID } from "./constants";
|
|
10
|
+
import { LayoutProps, PanelLayoutState } from "./types";
|
|
12
11
|
|
|
13
12
|
export const DrawerWorkspaceLayout = memoGeneric(function DrawerWorkspaceLayout<
|
|
14
13
|
LeftTab extends string = string,
|
|
@@ -25,16 +24,46 @@ export const DrawerWorkspaceLayout = memoGeneric(function DrawerWorkspaceLayout<
|
|
|
25
24
|
rightTabValue,
|
|
26
25
|
onChangeRightTab,
|
|
27
26
|
onChangeLeftTab,
|
|
28
|
-
compactDrawerMenu,
|
|
29
27
|
}: LayoutProps<LeftTab, RightTab>) {
|
|
30
|
-
const portalContainer = useRef<HTMLDivElement>(null);
|
|
31
|
-
|
|
32
28
|
const [{ leftSidebarCollapsed, rightSidebarCollapsed }, setLayoutState] =
|
|
33
29
|
useState<PanelLayoutState>({
|
|
34
30
|
leftSidebarCollapsed: true,
|
|
35
31
|
rightSidebarCollapsed: true,
|
|
36
32
|
});
|
|
37
33
|
|
|
34
|
+
// Expose expand/collapse methods via refs
|
|
35
|
+
React.useImperativeHandle(
|
|
36
|
+
leftSidebarRef,
|
|
37
|
+
() => ({
|
|
38
|
+
expand: () =>
|
|
39
|
+
setLayoutState((prev) => ({
|
|
40
|
+
...prev,
|
|
41
|
+
leftSidebarCollapsed: false,
|
|
42
|
+
rightSidebarCollapsed: true,
|
|
43
|
+
})),
|
|
44
|
+
collapse: () =>
|
|
45
|
+
setLayoutState((prev) => ({ ...prev, leftSidebarCollapsed: true })),
|
|
46
|
+
isExpanded: () => !leftSidebarCollapsed,
|
|
47
|
+
}),
|
|
48
|
+
[leftSidebarCollapsed]
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
React.useImperativeHandle(
|
|
52
|
+
rightSidebarRef,
|
|
53
|
+
() => ({
|
|
54
|
+
expand: () =>
|
|
55
|
+
setLayoutState((prev) => ({
|
|
56
|
+
...prev,
|
|
57
|
+
rightSidebarCollapsed: false,
|
|
58
|
+
leftSidebarCollapsed: true,
|
|
59
|
+
})),
|
|
60
|
+
collapse: () =>
|
|
61
|
+
setLayoutState((prev) => ({ ...prev, rightSidebarCollapsed: true })),
|
|
62
|
+
isExpanded: () => !rightSidebarCollapsed,
|
|
63
|
+
}),
|
|
64
|
+
[rightSidebarCollapsed]
|
|
65
|
+
);
|
|
66
|
+
|
|
38
67
|
const allTabItems = useMemo(
|
|
39
68
|
() =>
|
|
40
69
|
[
|
|
@@ -44,15 +73,12 @@ export const DrawerWorkspaceLayout = memoGeneric(function DrawerWorkspaceLayout<
|
|
|
44
73
|
[leftTabItems, rightTabItems, leftPanel, rightPanel]
|
|
45
74
|
);
|
|
46
75
|
|
|
47
|
-
const hasTabs = allTabItems.length > 0;
|
|
48
|
-
|
|
49
|
-
// Determine if we should use the compact top-row menu variant.
|
|
50
76
|
const allSelectableTabItems = useMemo(
|
|
51
77
|
() => allTabItems.filter(isSelectableMenuItem),
|
|
52
78
|
[allTabItems]
|
|
53
79
|
);
|
|
54
|
-
|
|
55
|
-
|
|
80
|
+
|
|
81
|
+
const hasTabs = allSelectableTabItems.length > 0;
|
|
56
82
|
|
|
57
83
|
const leftSelectableTabItems = useMemo(
|
|
58
84
|
() => (leftTabItems ?? []).filter(isSelectableMenuItem),
|
|
@@ -64,165 +90,118 @@ export const DrawerWorkspaceLayout = memoGeneric(function DrawerWorkspaceLayout<
|
|
|
64
90
|
[rightTabItems]
|
|
65
91
|
);
|
|
66
92
|
|
|
67
|
-
// Shared helpers for expanding/collapsing drawers
|
|
68
|
-
const toggleSidebar = (ref: React.RefObject<SidebarRef | null>) => {
|
|
69
|
-
if (ref.current?.isExpanded()) {
|
|
70
|
-
ref.current?.collapse();
|
|
71
|
-
} else {
|
|
72
|
-
ref.current?.expand();
|
|
73
|
-
}
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
const ensureExpanded = (ref: React.RefObject<SidebarRef | null>) => {
|
|
77
|
-
if (!ref.current?.isExpanded()) {
|
|
78
|
-
ref.current?.expand();
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
93
|
const handleSelectTab = (value: LeftTab | RightTab) => {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
94
|
+
const isLeftItem = leftSelectableTabItems.some(
|
|
95
|
+
(item) => item.value === value
|
|
96
|
+
);
|
|
97
|
+
const isRightItem = rightSelectableTabItems.some(
|
|
98
|
+
(item) => item.value === value
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (isLeftItem) {
|
|
102
|
+
if (value === leftTabValue && !leftSidebarCollapsed) {
|
|
103
|
+
// Toggle off - clicking same tab that's already open
|
|
104
|
+
setLayoutState((prev) => ({ ...prev, leftSidebarCollapsed: true }));
|
|
86
105
|
} else {
|
|
87
|
-
|
|
106
|
+
// Open this tab, close other
|
|
107
|
+
setLayoutState({
|
|
108
|
+
leftSidebarCollapsed: false,
|
|
109
|
+
rightSidebarCollapsed: true,
|
|
110
|
+
});
|
|
88
111
|
onChangeLeftTab?.(value as LeftTab);
|
|
89
112
|
}
|
|
90
|
-
} else if (
|
|
91
|
-
if (value === rightTabValue) {
|
|
92
|
-
|
|
113
|
+
} else if (isRightItem) {
|
|
114
|
+
if (value === rightTabValue && !rightSidebarCollapsed) {
|
|
115
|
+
// Toggle off - clicking same tab that's already open
|
|
116
|
+
setLayoutState((prev) => ({ ...prev, rightSidebarCollapsed: true }));
|
|
93
117
|
} else {
|
|
94
|
-
|
|
118
|
+
// Open this tab, close other
|
|
119
|
+
setLayoutState({
|
|
120
|
+
leftSidebarCollapsed: true,
|
|
121
|
+
rightSidebarCollapsed: false,
|
|
122
|
+
});
|
|
95
123
|
onChangeRightTab?.(value as RightTab);
|
|
96
124
|
}
|
|
97
125
|
}
|
|
98
126
|
};
|
|
99
127
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
128
|
+
// Determine which panel is currently showing
|
|
129
|
+
const activeDrawerPanel = !leftSidebarCollapsed
|
|
130
|
+
? leftPanel
|
|
131
|
+
: !rightSidebarCollapsed
|
|
132
|
+
? rightPanel
|
|
133
|
+
: null;
|
|
134
|
+
|
|
135
|
+
const isDrawerOpen = activeDrawerPanel !== null;
|
|
136
|
+
|
|
137
|
+
// Determine active tab value for highlighting
|
|
138
|
+
const activeTabValue = !leftSidebarCollapsed
|
|
139
|
+
? leftTabValue
|
|
140
|
+
: !rightSidebarCollapsed
|
|
141
|
+
? rightTabValue
|
|
142
|
+
: undefined;
|
|
143
|
+
|
|
144
|
+
// Create toolbar menu items with checked state
|
|
145
|
+
const toolbarMenuItems = useMemo(
|
|
146
|
+
() =>
|
|
147
|
+
allSelectableTabItems.map((item) => ({
|
|
148
|
+
...item,
|
|
149
|
+
checked: activeTabValue === item.value,
|
|
150
|
+
})),
|
|
151
|
+
[allSelectableTabItems, activeTabValue]
|
|
152
|
+
);
|
|
109
153
|
|
|
110
154
|
return (
|
|
111
155
|
<div
|
|
112
|
-
ref={portalContainer}
|
|
113
156
|
id={EDITOR_PANEL_GROUP_ID}
|
|
114
|
-
className="n-flex n-flex-1 n-relative focus:n-outline-none"
|
|
157
|
+
className="n-flex n-flex-col n-flex-1 n-relative focus:n-outline-none"
|
|
115
158
|
>
|
|
116
|
-
{
|
|
159
|
+
{/* Horizontal toggle bar - secondary toolbar row */}
|
|
160
|
+
{hasTabs && (
|
|
117
161
|
<>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
162
|
+
<div
|
|
163
|
+
className="n-flex n-flex-row n-items-center n-gap-1 n-px-2 n-py-1.5 n-bg-sidebar-background n-flex-none"
|
|
164
|
+
style={{
|
|
165
|
+
[cssVarNames.colors.inputBackground]: "transparent",
|
|
166
|
+
[cssVarNames.colors.buttonBackground]: "transparent",
|
|
167
|
+
}}
|
|
168
|
+
>
|
|
169
|
+
<ToolbarMenu
|
|
170
|
+
items={toolbarMenuItems}
|
|
171
|
+
onSelectMenuItem={handleSelectTab}
|
|
172
|
+
/>
|
|
173
|
+
</div>
|
|
174
|
+
<Divider />
|
|
126
175
|
</>
|
|
127
176
|
)}
|
|
128
177
|
|
|
129
|
-
{
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
178
|
+
{/* Main content area with drawer overlay */}
|
|
179
|
+
<div className="n-flex-1 n-flex n-flex-col n-relative n-min-h-0">
|
|
180
|
+
{/* Drawer panel - positioned absolutely over content */}
|
|
181
|
+
{isDrawerOpen && (
|
|
182
|
+
<div className="n-absolute n-inset-0 n-z-20 n-flex n-flex-col">
|
|
183
|
+
{/* Drawer content */}
|
|
184
|
+
<div className="n-flex-none n-max-h-[50vh] n-overflow-auto n-bg-sidebar-background n-border-b n-border-divider-strong n-shadow-lg">
|
|
185
|
+
{activeDrawerPanel}
|
|
186
|
+
</div>
|
|
187
|
+
{/* Backdrop - click to close */}
|
|
188
|
+
<div
|
|
189
|
+
className="n-flex-1 n-bg-black/30"
|
|
190
|
+
onClick={() =>
|
|
191
|
+
setLayoutState({
|
|
192
|
+
leftSidebarCollapsed: true,
|
|
193
|
+
rightSidebarCollapsed: true,
|
|
194
|
+
})
|
|
143
195
|
}
|
|
144
|
-
active={(() => {
|
|
145
|
-
const value = allSelectableTabItems[0].value as
|
|
146
|
-
| LeftTab
|
|
147
|
-
| RightTab;
|
|
148
|
-
const isLeftItem = leftSelectableTabItems.some(
|
|
149
|
-
(i) => i.value === value
|
|
150
|
-
);
|
|
151
|
-
const isRightItem = rightSelectableTabItems.some(
|
|
152
|
-
(i) => i.value === value
|
|
153
|
-
);
|
|
154
|
-
return isLeftItem
|
|
155
|
-
? !leftSidebarCollapsed
|
|
156
|
-
: isRightItem
|
|
157
|
-
? !rightSidebarCollapsed
|
|
158
|
-
: false;
|
|
159
|
-
})()}
|
|
160
|
-
onClick={() => {
|
|
161
|
-
const value = allSelectableTabItems[0].value as
|
|
162
|
-
| LeftTab
|
|
163
|
-
| RightTab;
|
|
164
|
-
handleSelectTab(value);
|
|
165
|
-
}}
|
|
166
196
|
/>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
)}
|
|
197
|
+
</div>
|
|
198
|
+
)}
|
|
170
199
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
ref={leftSidebarRef}
|
|
177
|
-
className="n-flex-1"
|
|
178
|
-
style={
|
|
179
|
-
useCompactInToolbar
|
|
180
|
-
? undefined
|
|
181
|
-
: { left: 48, maxWidth: "calc(100% - 48px)" }
|
|
182
|
-
}
|
|
183
|
-
overlayStyle={useCompactInToolbar ? undefined : { left: 48 }}
|
|
184
|
-
open={!leftSidebarCollapsed}
|
|
185
|
-
positioning="absolute"
|
|
186
|
-
side="left"
|
|
187
|
-
trigger={null}
|
|
188
|
-
portalContainer={portalContainer.current}
|
|
189
|
-
onOpenChange={(open) => {
|
|
190
|
-
setLayoutState({
|
|
191
|
-
leftSidebarCollapsed: !open,
|
|
192
|
-
rightSidebarCollapsed: rightSidebarCollapsed,
|
|
193
|
-
});
|
|
194
|
-
}}
|
|
195
|
-
>
|
|
196
|
-
{leftPanel}
|
|
197
|
-
</Drawer>
|
|
198
|
-
)}
|
|
199
|
-
{rightPanel && (
|
|
200
|
-
<Drawer
|
|
201
|
-
id={RIGHT_SIDEBAR_ID}
|
|
202
|
-
modal={false}
|
|
203
|
-
ref={rightSidebarRef}
|
|
204
|
-
className="n-flex-1"
|
|
205
|
-
style={
|
|
206
|
-
useCompactInToolbar
|
|
207
|
-
? undefined
|
|
208
|
-
: { left: 48, maxWidth: "calc(100% - 48px)" }
|
|
209
|
-
}
|
|
210
|
-
overlayStyle={useCompactInToolbar ? undefined : { left: 48 }}
|
|
211
|
-
open={!rightSidebarCollapsed}
|
|
212
|
-
positioning="absolute"
|
|
213
|
-
side="left"
|
|
214
|
-
trigger={null}
|
|
215
|
-
portalContainer={portalContainer.current}
|
|
216
|
-
onOpenChange={(open) => {
|
|
217
|
-
setLayoutState({
|
|
218
|
-
leftSidebarCollapsed: leftSidebarCollapsed,
|
|
219
|
-
rightSidebarCollapsed: !open,
|
|
220
|
-
});
|
|
221
|
-
}}
|
|
222
|
-
>
|
|
223
|
-
{rightPanel}
|
|
224
|
-
</Drawer>
|
|
225
|
-
)}
|
|
200
|
+
{/* Center content */}
|
|
201
|
+
<div className="n-flex-1 n-flex n-relative n-min-h-0">
|
|
202
|
+
{centerPanel}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
226
205
|
</div>
|
|
227
206
|
);
|
|
228
207
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { memoGeneric } from "@noya-app/react-utils";
|
|
2
|
+
import React from "react";
|
|
3
|
+
import { isSelectableMenuItem, MenuItem } from "../internal/Menu";
|
|
4
|
+
import { SegmentedControl } from "../SegmentedControl";
|
|
5
|
+
|
|
6
|
+
type HorizontalTabBarProps<TabValue extends string> = {
|
|
7
|
+
items: MenuItem<TabValue>[];
|
|
8
|
+
activeValue: TabValue | undefined;
|
|
9
|
+
onChange: (value: TabValue) => void;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const HorizontalTabBar = memoGeneric(function HorizontalTabBar<
|
|
13
|
+
TabValue extends string,
|
|
14
|
+
>({ items, activeValue, onChange }: HorizontalTabBarProps<TabValue>) {
|
|
15
|
+
const selectableItems = items.filter(isSelectableMenuItem);
|
|
16
|
+
|
|
17
|
+
// Don't render if there's nothing to switch between
|
|
18
|
+
if (selectableItems.length <= 1) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="n-flex n-flex-col n-bg-sidebar-background n-flex-none">
|
|
24
|
+
<div className="n-flex n-flex-row n-px-3 n-py-3">
|
|
25
|
+
<SegmentedControl
|
|
26
|
+
enableCompactMode
|
|
27
|
+
className="n-flex-1"
|
|
28
|
+
value={activeValue}
|
|
29
|
+
onValueChange={onChange}
|
|
30
|
+
variant="default"
|
|
31
|
+
items={selectableItems.map((item) => ({
|
|
32
|
+
value: item.value,
|
|
33
|
+
title: item.title,
|
|
34
|
+
icon: item.icon,
|
|
35
|
+
tooltip: item.tooltip,
|
|
36
|
+
disabled: item.disabled,
|
|
37
|
+
}))}
|
|
38
|
+
/>
|
|
39
|
+
</div>
|
|
40
|
+
{/* <Divider /> */}
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
});
|