@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.
@@ -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 * as PopperPrimitive from "@radix-ui/react-popper";
12
- import type { MeasurableElement } from "@radix-ui/utils";
13
- import React, { ComponentProps, memo, useMemo } from "react";
14
- import { createPortal } from "react-dom";
15
-
16
- // Create a Measurable from a Rect
17
- const createVirtualElement = (rect: Rect): MeasurableElement => ({
18
- getBoundingClientRect: () => ({
19
- width: rect.width,
20
- height: rect.height,
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 PopperPrimitiveContentProps = ComponentProps<
32
- typeof PopperPrimitive.Content
33
- >;
22
+ type Side = "top" | "right" | "bottom" | "left";
23
+ type Align = "start" | "center" | "end";
34
24
 
35
- type SelectionToolbarContainerProps = Pick<
36
- PopperPrimitiveContentProps,
37
- "side" | "sideOffset" | "align" | "avoidCollisions"
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 containerRef = React.useRef<HTMLDivElement>(null);
62
- const size = useSize(containerRef, "width");
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 virtualRef = useMemo(
66
- () => ({
67
- current: createVirtualElement(rect),
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 popperContent = (
73
- <PopperPrimitive.Content
74
- {...portalScopeProps(portalScopeId)}
75
- side={side}
76
- sideOffset={sideOffset}
77
- align={align}
78
- avoidCollisions={avoidCollisions}
79
- collisionPadding={10}
80
- contentEditable={false}
81
- style={{
82
- zIndex: 10,
83
- opacity: size ? 1 : 0,
84
- transition: `opacity ${transitionDuration}s ease-in-out`,
85
- [cssVarNames.colors.inputBackground]: "transparent",
86
- [cssVarNames.colors.buttonBackground]: "transparent",
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={containerRef}
91
- className={cx(
92
- "n-flex n-gap-1 n-bg-popover-background n-rounded n-shadow-popover n-p-1",
93
- containerClassName
94
- )}
95
- style={containerStyle}
96
- onClick={(e) => e.stopPropagation()}
97
- onMouseDown={(e) => e.stopPropagation()}
98
- onPointerDown={(e) => e.stopPropagation()}
99
- onWheel={(e) => e.stopPropagation()}
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
- {children}
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
- </PopperPrimitive.Content>
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
- { id: propId, order, className, defaultSize, minSize, maxSize, collapsible, defaultCollapsed, children },
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
- }, [id, order, defaultSize, minSize, maxSize, collapsible, defaultCollapsed, registerPanel, unregisterPanel]);
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
- const collapsed = hasSavedSize
152
- ? savedSize === 0 && !!panel.collapsible
153
- : !!panel.defaultCollapsed && !!panel.collapsible;
154
- const initialSize = hasSavedSize
155
- ? savedSize
156
- : collapsed
157
- ? 0
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, useRef, useState } from "react";
5
- import { Button } from "../Button";
6
- import { DividerVertical } from "../Divider";
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 { EDITOR_PANEL_GROUP_ID, RIGHT_SIDEBAR_ID } from "./constants";
10
- import { LayoutProps, PanelLayoutState, SidebarRef } from "./types";
11
- import { VerticalTabMenu } from "./VerticalTabMenu";
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
- const useCompactInToolbar =
55
- !!compactDrawerMenu && allSelectableTabItems.length <= 1;
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
- if (leftSelectableTabItems.some((item) => item.value === value)) {
84
- if (value === leftTabValue) {
85
- toggleSidebar(leftSidebarRef);
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
- ensureExpanded(leftSidebarRef);
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 (rightSelectableTabItems.some((item) => item.value === value)) {
91
- if (value === rightTabValue) {
92
- toggleSidebar(rightSidebarRef);
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
- ensureExpanded(rightSidebarRef);
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
- const { activeValue, isSidebarCollapsed } =
101
- leftSelectableTabItems.length > 0 && !leftSidebarCollapsed
102
- ? { activeValue: leftTabValue, isSidebarCollapsed: leftSidebarCollapsed }
103
- : rightSelectableTabItems.length > 0 && !rightSidebarCollapsed
104
- ? {
105
- activeValue: rightTabValue,
106
- isSidebarCollapsed: rightSidebarCollapsed,
107
- }
108
- : { activeValue: undefined, isSidebarCollapsed: true };
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
- {hasTabs && !useCompactInToolbar && (
159
+ {/* Horizontal toggle bar - secondary toolbar row */}
160
+ {hasTabs && (
117
161
  <>
118
- <VerticalTabMenu
119
- tooltipSide="right"
120
- items={allTabItems}
121
- activeValue={activeValue}
122
- isSidebarCollapsed={isSidebarCollapsed}
123
- onChange={(value) => handleSelectTab(value as LeftTab | RightTab)}
124
- />
125
- <DividerVertical className="n-h-full" />
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
- {useCompactInToolbar && (
130
- <div className="n-absolute n-top-0 n-left-0 n-h-[calc(var(--n-toolbar-height)+1px)] n-w-[calc(var(--n-toolbar-height)+1px)] n-bg-sidebar-background n-border-r n-border-b n-border-divider-strong n-z-10 n-flex n-items-center n-justify-center">
131
- {allSelectableTabItems[0] && (
132
- <Button
133
- variant="none"
134
- className="n-rounded"
135
- style={{
136
- width: "var(--n-input-height)",
137
- height: "var(--n-input-height)",
138
- }}
139
- icon={allSelectableTabItems[0].icon}
140
- tooltip={
141
- allSelectableTabItems[0].tooltip ??
142
- allSelectableTabItems[0].title
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
- </div>
169
- )}
197
+ </div>
198
+ )}
170
199
 
171
- <div className="n-flex-1 n-flex n-relative">{centerPanel}</div>
172
-
173
- {leftPanel && (
174
- <Drawer
175
- modal={false}
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
+ });