@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.
- package/.turbo/turbo-build.log +20 -12
- package/CHANGELOG.md +9 -0
- package/dist/chunk-D57E6H3M.mjs +36 -0
- package/dist/chunk-D57E6H3M.mjs.map +1 -0
- package/dist/emojis.d.mts +1 -0
- package/dist/emojis.d.ts +1 -0
- package/dist/emojis.js +31 -0
- package/dist/emojis.js.map +1 -0
- package/dist/emojis.mjs +8 -0
- package/dist/emojis.mjs.map +1 -0
- package/dist/index.css +1 -1
- package/dist/index.d.mts +142 -90
- package/dist/index.d.ts +142 -90
- package/dist/index.js +3171 -1745
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3349 -1918
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -7
- package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
- package/src/components/Avatar.tsx +10 -10
- package/src/components/Banner.tsx +1 -1
- package/src/components/BaseToolbar.tsx +1 -1
- package/src/components/Button.tsx +2 -1
- package/src/components/Chip.tsx +1 -1
- package/src/components/ColorSwatchControl.tsx +22 -14
- package/src/components/CommandPalette.tsx +10 -6
- package/src/components/ContextMenu.tsx +249 -38
- package/src/components/Dialog.tsx +70 -67
- package/src/components/Drawer.tsx +56 -17
- package/src/components/DropdownMenu.tsx +305 -46
- package/src/components/EditableText.tsx +1 -1
- package/src/components/EmojiPicker.tsx +645 -0
- package/src/components/GridView.tsx +5 -1
- package/src/components/IVirtualizedList.tsx +21 -1
- package/src/components/InputField.tsx +2 -10
- package/src/components/InspectorContainer.tsx +4 -2
- package/src/components/Message.tsx +5 -16
- package/src/components/Popover.tsx +73 -107
- package/src/components/Progress.tsx +18 -18
- package/src/components/ScrollArea.tsx +66 -31
- package/src/components/ScrollableSidebar.tsx +1 -1
- package/src/components/SegmentedControl.tsx +166 -38
- package/src/components/SelectMenu.tsx +193 -101
- package/src/components/Slider.tsx +40 -38
- package/src/components/Switch.tsx +4 -4
- package/src/components/TextArea.tsx +1 -1
- package/src/components/Toast.tsx +99 -26
- package/src/components/Toolbar.tsx +114 -16
- package/src/components/Tooltip.tsx +18 -8
- package/src/components/Virtualized.tsx +193 -14
- package/src/components/VisuallyHidden.tsx +20 -0
- package/src/components/__tests__/Virtualized.math.test.ts +426 -1
- package/src/components/__tests__/Virtualized.test.tsx +129 -1
- package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
- package/src/components/internal/Menu.tsx +4 -0
- package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
- package/src/components/listView/ListViewRoot.tsx +5 -1
- package/src/components/resizablePanels/Panel.tsx +94 -0
- package/src/components/resizablePanels/PanelGroup.tsx +498 -0
- package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
- package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
- package/src/components/resizablePanels/index.ts +7 -0
- package/src/components/resizablePanels/types.ts +65 -0
- package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
- package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
- package/src/components/workspace/WorkspaceLayout.tsx +28 -58
- package/src/components/workspace/types.ts +6 -4
- package/src/contexts/DialogContext.tsx +15 -24
- package/src/emojis.ts +1 -0
- package/src/hooks/useTriggerToggle.ts +95 -0
- package/src/index.css +2 -0
- package/src/index.tsx +1 -2
- package/src/theme/proseTheme.ts +2 -0
- package/src/utils/mergeProps.ts +57 -0
- package/src/utils/skinTone.ts +90 -0
- package/tsup.config.ts +1 -1
- package/src/__tests__/workspaceLayout.test.ts +0 -281
- package/src/components/ScrollArea2.tsx +0 -76
- package/src/components/internal/MenuViewport.tsx +0 -178
- package/src/components/internal/SelectItem.tsx +0 -138
- 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
|
-
};
|