@noya-app/noya-designsystem 0.1.77 → 0.1.78

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.
Files changed (81) hide show
  1. package/.turbo/turbo-build.log +20 -12
  2. package/CHANGELOG.md +9 -0
  3. package/dist/chunk-D57E6H3M.mjs +36 -0
  4. package/dist/chunk-D57E6H3M.mjs.map +1 -0
  5. package/dist/emojis.d.mts +1 -0
  6. package/dist/emojis.d.ts +1 -0
  7. package/dist/emojis.js +31 -0
  8. package/dist/emojis.js.map +1 -0
  9. package/dist/emojis.mjs +8 -0
  10. package/dist/emojis.mjs.map +1 -0
  11. package/dist/index.css +1 -1
  12. package/dist/index.d.mts +142 -90
  13. package/dist/index.d.ts +142 -90
  14. package/dist/index.js +3171 -1745
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3349 -1918
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +11 -7
  19. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
  20. package/src/components/Avatar.tsx +10 -10
  21. package/src/components/Banner.tsx +1 -1
  22. package/src/components/BaseToolbar.tsx +1 -1
  23. package/src/components/Button.tsx +2 -1
  24. package/src/components/Chip.tsx +1 -1
  25. package/src/components/ColorSwatchControl.tsx +22 -14
  26. package/src/components/CommandPalette.tsx +10 -6
  27. package/src/components/ContextMenu.tsx +249 -38
  28. package/src/components/Dialog.tsx +70 -67
  29. package/src/components/Drawer.tsx +56 -17
  30. package/src/components/DropdownMenu.tsx +305 -46
  31. package/src/components/EditableText.tsx +1 -1
  32. package/src/components/EmojiPicker.tsx +645 -0
  33. package/src/components/GridView.tsx +5 -1
  34. package/src/components/IVirtualizedList.tsx +21 -1
  35. package/src/components/InputField.tsx +2 -10
  36. package/src/components/InspectorContainer.tsx +4 -2
  37. package/src/components/Message.tsx +5 -16
  38. package/src/components/Popover.tsx +73 -107
  39. package/src/components/Progress.tsx +18 -18
  40. package/src/components/ScrollArea.tsx +66 -31
  41. package/src/components/ScrollableSidebar.tsx +1 -1
  42. package/src/components/SegmentedControl.tsx +166 -38
  43. package/src/components/SelectMenu.tsx +193 -101
  44. package/src/components/Slider.tsx +40 -38
  45. package/src/components/Switch.tsx +4 -4
  46. package/src/components/TextArea.tsx +1 -1
  47. package/src/components/Toast.tsx +99 -26
  48. package/src/components/Toolbar.tsx +114 -16
  49. package/src/components/Tooltip.tsx +18 -8
  50. package/src/components/Virtualized.tsx +193 -14
  51. package/src/components/VisuallyHidden.tsx +20 -0
  52. package/src/components/__tests__/Virtualized.math.test.ts +426 -1
  53. package/src/components/__tests__/Virtualized.test.tsx +129 -1
  54. package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
  55. package/src/components/internal/Menu.tsx +4 -0
  56. package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
  57. package/src/components/listView/ListViewRoot.tsx +5 -1
  58. package/src/components/resizablePanels/Panel.tsx +94 -0
  59. package/src/components/resizablePanels/PanelGroup.tsx +498 -0
  60. package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
  61. package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
  62. package/src/components/resizablePanels/index.ts +7 -0
  63. package/src/components/resizablePanels/types.ts +65 -0
  64. package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
  65. package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
  66. package/src/components/workspace/WorkspaceLayout.tsx +28 -58
  67. package/src/components/workspace/types.ts +6 -4
  68. package/src/contexts/DialogContext.tsx +15 -24
  69. package/src/emojis.ts +1 -0
  70. package/src/hooks/useTriggerToggle.ts +95 -0
  71. package/src/index.css +2 -0
  72. package/src/index.tsx +1 -2
  73. package/src/theme/proseTheme.ts +2 -0
  74. package/src/utils/mergeProps.ts +57 -0
  75. package/src/utils/skinTone.ts +90 -0
  76. package/tsup.config.ts +1 -1
  77. package/src/__tests__/workspaceLayout.test.ts +0 -281
  78. package/src/components/ScrollArea2.tsx +0 -76
  79. package/src/components/internal/MenuViewport.tsx +0 -178
  80. package/src/components/internal/SelectItem.tsx +0 -138
  81. package/src/components/workspace/panelStorage.ts +0 -216
@@ -0,0 +1,57 @@
1
+ import React from "react";
2
+
3
+ /**
4
+ * Merges trigger props with child element props, composing event handlers
5
+ * instead of overwriting them. This ensures that when a component like
6
+ * Tooltip wraps a Button with an onClick, both handlers get called.
7
+ *
8
+ * @param triggerProps - Props from the trigger component (e.g., Base UI's Trigger render prop)
9
+ * @param childProps - Props from the child element being wrapped
10
+ * @returns Merged props with composed event handlers
11
+ */
12
+ export function mergePropsWithEventComposition(
13
+ triggerProps: Record<string, unknown>,
14
+ childProps: Record<string, unknown>
15
+ ): Record<string, unknown> {
16
+ const mergedProps: Record<string, unknown> = { ...triggerProps };
17
+
18
+ // Compose event handlers to preserve child's handlers
19
+ for (const key of Object.keys(triggerProps)) {
20
+ if (
21
+ key.startsWith("on") &&
22
+ typeof triggerProps[key] === "function" &&
23
+ typeof childProps[key] === "function"
24
+ ) {
25
+ const triggerHandler = triggerProps[key] as (...args: unknown[]) => void;
26
+ const childHandler = childProps[key] as (...args: unknown[]) => void;
27
+ mergedProps[key] = (...args: unknown[]) => {
28
+ childHandler(...args);
29
+ triggerHandler(...args);
30
+ };
31
+ }
32
+ }
33
+
34
+ return mergedProps;
35
+ }
36
+
37
+ /**
38
+ * Clones a React element with trigger props, composing event handlers.
39
+ * If the element is not a valid React element, wraps it in a span.
40
+ *
41
+ * @param element - The element to clone (trigger child)
42
+ * @param triggerProps - Props from the trigger component
43
+ * @returns Cloned element with merged props
44
+ */
45
+ export function cloneElementWithMergedProps(
46
+ element: React.ReactNode,
47
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
48
+ triggerProps: any
49
+ ): React.ReactElement {
50
+ const props = triggerProps as Record<string, unknown>;
51
+ if (React.isValidElement(element)) {
52
+ const childProps = element.props as Record<string, unknown>;
53
+ const mergedProps = mergePropsWithEventComposition(props, childProps);
54
+ return React.cloneElement(element, mergedProps);
55
+ }
56
+ return React.createElement("span", props, element);
57
+ }
@@ -0,0 +1,90 @@
1
+ // Fitzpatrick skin tone modifiers
2
+ export const SKIN_TONES = {
3
+ default: "",
4
+ light: "\u{1F3FB}", // 🏻 Type 1-2
5
+ "medium-light": "\u{1F3FC}", // 🏼 Type 3
6
+ medium: "\u{1F3FD}", // 🏽 Type 4
7
+ "medium-dark": "\u{1F3FE}", // 🏾 Type 5
8
+ dark: "\u{1F3FF}", // 🏿 Type 6
9
+ } as const;
10
+
11
+ export type SkinTone = keyof typeof SKIN_TONES;
12
+
13
+ // Display emojis for the skin tone picker (using hand as example)
14
+ export const SKIN_TONE_DISPLAY: Record<SkinTone, string> = {
15
+ default: "✋",
16
+ light: "✋🏻",
17
+ "medium-light": "✋🏼",
18
+ medium: "✋🏽",
19
+ "medium-dark": "✋🏾",
20
+ dark: "✋🏿",
21
+ };
22
+
23
+ // Emoji presentation selector - takes the same place as skin tone modifier
24
+ // https://unicode.org/reports/tr51/#composing_zwj_seq
25
+ const EMOJI_PRESENTATION_SELECTOR = "\u{FE0F}";
26
+
27
+ // Emojis that don't render correctly with skin tones on some platforms
28
+ // - Family emojis with 2 people (to distinguish from couple, handshake, etc.)
29
+ // - Multi-person activity emojis (don't render correctly on macOS)
30
+ const SKIN_TONE_EXCLUDED_EMOJIS = new Set([
31
+ "👩‍👦",
32
+ "👩‍👧",
33
+ "👨‍👧",
34
+ "👨‍👦",
35
+ "👯", // dancers
36
+ "👯‍♂️", // dancing_men
37
+ "👯‍♀️", // dancing_women
38
+ "🤼", // wrestling
39
+ "🤼‍♂️", // men_wrestling
40
+ "🤼‍♀️", // women_wrestling
41
+ ]);
42
+
43
+ // Regex to match emoji modifier base characters (characters that can have skin tone applied)
44
+ const EMOJI_MODIFIER_BASE_REGEX = /\p{Emoji_Modifier_Base}/gu;
45
+
46
+ // Apply skin tone modifier to an emoji
47
+ // Based on https://github.com/sindresorhus/skin-tone (MIT)
48
+ export function applySkinTone(emoji: string, skinTone: SkinTone): string {
49
+ if (skinTone === "default") {
50
+ return emoji;
51
+ }
52
+
53
+ // Remove any existing skin tone modifiers
54
+ const cleanEmoji = emoji.replaceAll(/[\u{1F3FB}-\u{1F3FF}]/gu, "");
55
+
56
+ // Count how many modifiable components the emoji has
57
+ const modifiableCount =
58
+ cleanEmoji.match(EMOJI_MODIFIER_BASE_REGEX)?.length ?? 0;
59
+
60
+ // Skip if:
61
+ // - No modifiable components
62
+ // - More than 2 modifiable components (family emojis with 3+ people)
63
+ // - Excluded emoji (family emojis, dancers that don't render correctly)
64
+ if (
65
+ modifiableCount === 0 ||
66
+ modifiableCount > 2 ||
67
+ SKIN_TONE_EXCLUDED_EMOJIS.has(cleanEmoji)
68
+ ) {
69
+ return emoji;
70
+ }
71
+
72
+ const tone = SKIN_TONES[skinTone];
73
+ let result = "";
74
+
75
+ for (const codePoint of cleanEmoji) {
76
+ // Skip emoji presentation selector (it's replaced by skin tone modifier)
77
+ if (codePoint === EMOJI_PRESENTATION_SELECTOR) {
78
+ continue;
79
+ }
80
+
81
+ result += codePoint;
82
+
83
+ // Apply skin tone after each modifiable component
84
+ if (EMOJI_MODIFIER_BASE_REGEX.test(codePoint)) {
85
+ result += tone;
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
package/tsup.config.ts CHANGED
@@ -6,7 +6,7 @@ const execAsync = promisify(exec);
6
6
 
7
7
  // "build": "tsup src/index.tsx --format cjs,esm --dts --sourcemap",
8
8
  export default defineConfig({
9
- entry: ["./src/index.tsx"],
9
+ entry: ["./src/index.tsx", "./src/emojis.ts"],
10
10
  format: ["cjs", "esm"],
11
11
  dts: true,
12
12
  sourcemap: true,
@@ -1,281 +0,0 @@
1
- import { expect, it } from "bun:test";
2
- import { MockClientStorage } from "../../../noya-react-utils/src/__tests__/MockClientStorage";
3
- import {
4
- CONTENT_AREA_ID,
5
- LEFT_SIDEBAR_ID,
6
- RIGHT_SIDEBAR_ID,
7
- } from "../components/workspace/constants";
8
- import {
9
- getInitialLayout,
10
- getLayoutCollapsedState,
11
- getLayoutIds,
12
- getPropertyKey,
13
- getStorageKey,
14
- mapWorkspaceLayoutState,
15
- setInitialLayout,
16
- toPercentageSize,
17
- toPixelSize,
18
- } from "../components/workspace/panelStorage";
19
- import { WorkspaceLayoutState } from "../components/workspace/types";
20
-
21
- const TEST_WINDOW_WIDTH = 1000;
22
-
23
- it("gets layout ids", () => {
24
- expect(
25
- getLayoutIds({
26
- hasLeftPanel: true,
27
- hasRightPanel: false,
28
- hasCenterPanel: true,
29
- })
30
- ).toEqual([LEFT_SIDEBAR_ID, CONTENT_AREA_ID]);
31
- });
32
-
33
- it("gets layout ids with all panels", () => {
34
- expect(
35
- getLayoutIds({
36
- hasLeftPanel: true,
37
- hasRightPanel: true,
38
- hasCenterPanel: true,
39
- })
40
- ).toEqual([LEFT_SIDEBAR_ID, CONTENT_AREA_ID, RIGHT_SIDEBAR_ID]);
41
- });
42
-
43
- it("gets layout ids with no panels", () => {
44
- expect(
45
- getLayoutIds({
46
- hasLeftPanel: false,
47
- hasRightPanel: false,
48
- hasCenterPanel: false,
49
- })
50
- ).toEqual([]);
51
- });
52
-
53
- it("converts to pixel size", () => {
54
- expect(toPixelSize(25, TEST_WINDOW_WIDTH)).toBe(250); // 25% of 1000px = 250px
55
- expect(toPixelSize(50, TEST_WINDOW_WIDTH)).toBe(500); // 50% of 1000px = 500px
56
- expect(toPixelSize(75, TEST_WINDOW_WIDTH)).toBe(750); // 75% of 1000px = 750px
57
- });
58
-
59
- it("converts to percentage size", () => {
60
- expect(toPercentageSize(250, TEST_WINDOW_WIDTH)).toBe(25); // 250px of 1000px = 25%
61
- expect(toPercentageSize(500, TEST_WINDOW_WIDTH)).toBe(50); // 500px of 1000px = 50%
62
- expect(toPercentageSize(750, TEST_WINDOW_WIDTH)).toBe(75); // 750px of 1000px = 75%
63
- });
64
-
65
- it("maps workspace layout state", () => {
66
- const layout: WorkspaceLayoutState = {
67
- "left,center": {
68
- layout: [20, 80],
69
- expandToSizes: { left: 30, center: 70 },
70
- },
71
- "left,center,right": {
72
- layout: [20, 60, 20],
73
- expandToSizes: { left: 25, center: 50, right: 25 },
74
- },
75
- };
76
-
77
- const mapped = mapWorkspaceLayoutState(layout, (size) => size * 2);
78
-
79
- expect(mapped).toEqual({
80
- "left,center": {
81
- layout: [40, 160],
82
- expandToSizes: { left: 60, center: 140 },
83
- },
84
- "left,center,right": {
85
- layout: [40, 120, 40],
86
- expandToSizes: { left: 50, center: 100, right: 50 },
87
- },
88
- });
89
- });
90
-
91
- it("gets property key", () => {
92
- expect(getPropertyKey([LEFT_SIDEBAR_ID, CONTENT_AREA_ID])).toBe(
93
- [CONTENT_AREA_ID, LEFT_SIDEBAR_ID].sort().join(",")
94
- );
95
- });
96
-
97
- it("gets storage key", () => {
98
- expect(getStorageKey("test-workspace")).toBe(
99
- "react-resizable-panels:test-workspace"
100
- );
101
- });
102
-
103
- it("gets initial layout with left panel only", () => {
104
- const result = getInitialLayout({
105
- layoutIds: [LEFT_SIDEBAR_ID, CONTENT_AREA_ID],
106
- leftSidebarOptions: {
107
- initialSize: 20,
108
- defaultCollapsed: false,
109
- },
110
- rightSidebarOptions: {
111
- initialSize: 20,
112
- defaultCollapsed: false,
113
- },
114
- centerPanelPercentage: 80,
115
- hasLeftPanel: true,
116
- hasRightPanel: false,
117
- hasCenterPanel: true,
118
- windowWidth: TEST_WINDOW_WIDTH,
119
- });
120
-
121
- expect(result).toEqual([200, 800]); // 20% and 80% of 1000px
122
- });
123
-
124
- it("gets initial layout with collapsed left panel", () => {
125
- const result = getInitialLayout({
126
- layoutIds: [LEFT_SIDEBAR_ID, CONTENT_AREA_ID],
127
- leftSidebarOptions: {
128
- initialSize: 20,
129
- defaultCollapsed: true,
130
- },
131
- rightSidebarOptions: {
132
- initialSize: 20,
133
- defaultCollapsed: false,
134
- },
135
- centerPanelPercentage: 80,
136
- hasLeftPanel: true,
137
- hasRightPanel: false,
138
- hasCenterPanel: true,
139
- windowWidth: TEST_WINDOW_WIDTH,
140
- });
141
-
142
- expect(result).toEqual([0, 1000]); // Collapsed left panel, center takes full width
143
- });
144
-
145
- it("gets initial layout with all panels", () => {
146
- const result = getInitialLayout({
147
- layoutIds: [LEFT_SIDEBAR_ID, CONTENT_AREA_ID, RIGHT_SIDEBAR_ID],
148
- leftSidebarOptions: {
149
- initialSize: 20,
150
- defaultCollapsed: false,
151
- },
152
- rightSidebarOptions: {
153
- initialSize: 20,
154
- defaultCollapsed: false,
155
- },
156
- centerPanelPercentage: 60,
157
- hasLeftPanel: true,
158
- hasRightPanel: true,
159
- hasCenterPanel: true,
160
- windowWidth: TEST_WINDOW_WIDTH,
161
- });
162
-
163
- expect(result).toEqual([200, 600, 200]); // 20%, 60%, 20% of 1000px
164
- });
165
-
166
- it("sets initial layout in storage", () => {
167
- const mockStorage = new MockClientStorage();
168
-
169
- setInitialLayout({
170
- storageKey: "test-key",
171
- propertyKey: "left,center",
172
- calculateLayout: () => [200, 800],
173
- storage: mockStorage,
174
- });
175
-
176
- expect(mockStorage.hasItem("test-key")).toBe(true);
177
- expect(mockStorage.getItemParsed<WorkspaceLayoutState>("test-key")).toEqual({
178
- "left,center": {
179
- layout: [200, 800],
180
- expandToSizes: {},
181
- },
182
- });
183
- });
184
-
185
- it("does not update existing layout property in storage", () => {
186
- const existingLayout: WorkspaceLayoutState = {
187
- center: {
188
- layout: [1000],
189
- expandToSizes: {},
190
- },
191
- // Already has the property we're trying to set
192
- "left,center": {
193
- layout: [100, 900],
194
- expandToSizes: {},
195
- },
196
- };
197
-
198
- const mockStorage = new MockClientStorage();
199
- mockStorage.setItem("test-key", JSON.stringify(existingLayout));
200
-
201
- setInitialLayout({
202
- storageKey: "test-key",
203
- propertyKey: "left,center",
204
- calculateLayout: () => [200, 800],
205
- storage: mockStorage,
206
- });
207
-
208
- // setInitialLayout doesn't update existing properties, so the layout should remain unchanged
209
- expect(mockStorage.getItemParsed<WorkspaceLayoutState>("test-key")).toEqual(
210
- existingLayout
211
- );
212
- });
213
-
214
- it("adds new layout property to existing storage", () => {
215
- const existingLayout: WorkspaceLayoutState = {
216
- center: {
217
- layout: [1000],
218
- expandToSizes: {},
219
- },
220
- };
221
-
222
- const mockStorage = new MockClientStorage();
223
- mockStorage.setItem("test-key", JSON.stringify(existingLayout));
224
-
225
- // The propertyId should be sorted
226
- const sortedPropertyId = [LEFT_SIDEBAR_ID, CONTENT_AREA_ID].sort().join(",");
227
-
228
- setInitialLayout({
229
- storageKey: "test-key",
230
- propertyKey: sortedPropertyId,
231
- calculateLayout: () => [200, 800],
232
- storage: mockStorage,
233
- });
234
-
235
- expect(mockStorage.getItemParsed<WorkspaceLayoutState>("test-key")).toEqual({
236
- center: {
237
- layout: [1000],
238
- expandToSizes: {},
239
- },
240
- [sortedPropertyId]: {
241
- layout: [200, 800],
242
- expandToSizes: {},
243
- },
244
- });
245
- });
246
-
247
- it("gets layout collapsed state", () => {
248
- const layoutGroup = {
249
- layout: [0, 800, 200],
250
- expandToSizes: {},
251
- };
252
-
253
- const result = getLayoutCollapsedState({
254
- layoutGroup,
255
- propertyKey: "left,center,right",
256
- layoutIds: [LEFT_SIDEBAR_ID, CONTENT_AREA_ID, RIGHT_SIDEBAR_ID],
257
- hasLeftPanel: true,
258
- hasRightPanel: true,
259
- });
260
-
261
- expect(result.leftSidebarCollapsed).toBe(true); // size is 0
262
- expect(result.rightSidebarCollapsed).toBe(false); // size is 200
263
- });
264
-
265
- it("handles panels that don't exist", () => {
266
- const layoutGroup = {
267
- layout: [1000],
268
- expandToSizes: {},
269
- };
270
-
271
- const result = getLayoutCollapsedState({
272
- layoutGroup,
273
- propertyKey: "center",
274
- layoutIds: [CONTENT_AREA_ID],
275
- hasLeftPanel: false,
276
- hasRightPanel: false,
277
- });
278
-
279
- expect(result.leftSidebarCollapsed).toBe(false); // Panel doesn't exist
280
- expect(result.rightSidebarCollapsed).toBe(false); // Panel doesn't exist
281
- });
@@ -1,76 +0,0 @@
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
- type ScrollAreaViewportProps = React.HTMLAttributes<HTMLDivElement> & {
21
- ref?: React.Ref<HTMLDivElement>;
22
- };
23
-
24
- export function ScrollArea2({
25
- children,
26
- className,
27
- viewportClassName,
28
- orientation = "vertical",
29
- style,
30
- viewportProps,
31
- }: {
32
- children: React.ReactNode;
33
- className?: string;
34
- viewportClassName?: string;
35
- orientation?: "vertical" | "horizontal" | "both";
36
- style?: React.CSSProperties;
37
- viewportProps?: ScrollAreaViewportProps;
38
- }) {
39
- const {
40
- className: viewportPropsClassName,
41
- ref: viewportRef,
42
- ...viewportRest
43
- } = viewportProps ?? {};
44
-
45
- return (
46
- <ScrollArea.Root className={cx("n-relative", className)} style={style}>
47
- <ScrollArea.Viewport
48
- ref={viewportRef}
49
- className={cx(
50
- "n-h-full n-overscroll-contain",
51
- viewportClassName,
52
- viewportPropsClassName
53
- )}
54
- {...viewportRest}
55
- >
56
- {children}
57
- </ScrollArea.Viewport>
58
- {(orientation === "vertical" || orientation === "both") && (
59
- <ScrollArea.Scrollbar
60
- className={cx(SCROLLBAR_STATE_CLASSES, SCROLLBAR_TRACK_VERTICAL)}
61
- >
62
- <ScrollArea.Thumb className={SCROLLBAR_THUMB_VERTICAL} />
63
- </ScrollArea.Scrollbar>
64
- )}
65
- {(orientation === "horizontal" || orientation === "both") && (
66
- <ScrollArea.Scrollbar
67
- orientation="horizontal"
68
- className={cx(SCROLLBAR_STATE_CLASSES, SCROLLBAR_TRACK_HORIZONTAL)}
69
- >
70
- <ScrollArea.Thumb className={SCROLLBAR_THUMB_HORIZONTAL} />
71
- </ScrollArea.Scrollbar>
72
- )}
73
- {orientation === "both" && <ScrollArea.Corner />}
74
- </ScrollArea.Root>
75
- );
76
- }
@@ -1,178 +0,0 @@
1
- "use client";
2
-
3
- import { ChevronRightIcon } from "@noya-app/noya-icons";
4
- import {
5
- ContextMenu as RadixContextMenu,
6
- DropdownMenu as RadixDropdownMenu,
7
- } from "radix-ui";
8
- import React from "react";
9
- import {
10
- portalScopeProps,
11
- usePortalScopeId,
12
- } from "../../contexts/PortalScopeContext";
13
- import { cx } from "../../utils/classNames";
14
- import {
15
- isSelectableMenuItem,
16
- MenuItem,
17
- SectionHeaderMenuItem,
18
- styles,
19
- } from "./Menu";
20
- import { SelectItem } from "./SelectItem";
21
-
22
- type MenuSeparatorComponent = React.FC<{ className?: string }>;
23
- type MenuItemTextComponent = React.FC<{ children?: React.ReactNode }>;
24
- type MenuItemComponent = React.ForwardRefExoticComponent<
25
- React.RefAttributes<HTMLDivElement> &
26
- React.PropsWithoutRef<{
27
- className?: string;
28
- disabled?: boolean;
29
- children?: React.ReactNode;
30
- value: string;
31
- onSelect?: (event: any) => void;
32
- onClick?: (event: React.MouseEvent<any>) => void;
33
- }>
34
- >;
35
- type MenuCheckboxItemComponent = React.FC<{
36
- checked?: boolean | "indeterminate";
37
- disabled?: boolean;
38
- onSelect?: (event: any) => void;
39
- className?: string;
40
- children?: React.ReactNode;
41
- }>;
42
-
43
- type MenuItemIndicatorComponent = React.FC<{
44
- className?: string;
45
- children?: React.ReactNode;
46
- }>;
47
-
48
- export type MenuComponents = {
49
- Separator: MenuSeparatorComponent;
50
- ItemText: MenuItemTextComponent;
51
- Item: MenuItemComponent;
52
- CheckboxItem?: MenuCheckboxItemComponent;
53
- ItemIndicator?: MenuItemIndicatorComponent;
54
- Sub?: typeof RadixDropdownMenu.Sub | typeof RadixContextMenu.Sub;
55
- SubTrigger?:
56
- | typeof RadixDropdownMenu.SubTrigger
57
- | typeof RadixContextMenu.SubTrigger;
58
- SubContent?:
59
- | typeof RadixDropdownMenu.SubContent
60
- | typeof RadixContextMenu.SubContent;
61
- Portal?: typeof RadixDropdownMenu.Portal | typeof RadixContextMenu.Portal;
62
- };
63
-
64
- type MenuViewportProps<T extends string> = {
65
- items: MenuItem<T>[];
66
- Components: MenuComponents;
67
- onSelect?: (value: any) => void;
68
- };
69
-
70
- export const MenuViewport = <T extends string>({
71
- items,
72
- Components,
73
- onSelect,
74
- }: MenuViewportProps<T>) => {
75
- const portalScopeId = usePortalScopeId();
76
-
77
- const hasCheckedItem = items.some(
78
- (item) => isSelectableMenuItem(item) && item.checked
79
- );
80
-
81
- return (
82
- <>
83
- {items.map((item, index) => {
84
- switch (item.type) {
85
- case "separator":
86
- return (
87
- <Components.Separator
88
- key={index}
89
- className={styles.separatorStyle}
90
- />
91
- );
92
-
93
- case "sectionHeader":
94
- return (
95
- <SectionHeaderMenuItem
96
- isFirst={index === 0}
97
- key={item.id}
98
- {...item}
99
- indented={hasCheckedItem}
100
- />
101
- );
102
- case "popover":
103
- return null;
104
- case "submenu":
105
- if (
106
- item.items &&
107
- Components.Sub &&
108
- Components.SubTrigger &&
109
- Components.SubContent &&
110
- Components.Portal
111
- ) {
112
- return (
113
- <Components.Sub key={item.id}>
114
- <Components.SubTrigger
115
- className={cx(
116
- styles.itemStyle({ disabled: item.disabled }),
117
- styles.selectedItemStyle
118
- )}
119
- asChild
120
- onClick={(e) => {
121
- e.stopPropagation();
122
- e.preventDefault();
123
- }}
124
- disabled={item.disabled}
125
- >
126
- <SelectItem
127
- value={item.id}
128
- icon={item.icon}
129
- checked={item.checked}
130
- onSelect={onSelect}
131
- Components={Components}
132
- indented={hasCheckedItem}
133
- disabled={item.disabled}
134
- >
135
- <div className="n-flex n-items-center n-flex-1">
136
- {item.title}
137
- </div>
138
- <ChevronRightIcon className="-n-mr-1" />
139
- </SelectItem>
140
- </Components.SubTrigger>
141
- <Components.Portal>
142
- <Components.SubContent
143
- {...portalScopeProps(portalScopeId)}
144
- alignOffset={-5}
145
- className={styles.contentStyle}
146
- >
147
- <MenuViewport
148
- items={item.items}
149
- Components={Components}
150
- onSelect={onSelect}
151
- />
152
- </Components.SubContent>
153
- </Components.Portal>
154
- </Components.Sub>
155
- );
156
- } else return null;
157
- default:
158
- return (
159
- <SelectItem
160
- key={item.value}
161
- value={item.value}
162
- icon={item.icon}
163
- checked={item.checked}
164
- shortcut={item.shortcut}
165
- onSelect={onSelect}
166
- Components={Components}
167
- indented={hasCheckedItem}
168
- testSelected={item.testSelected}
169
- disabled={item.disabled}
170
- >
171
- {item.title ?? item.value}
172
- </SelectItem>
173
- );
174
- }
175
- })}
176
- </>
177
- );
178
- };