@noya-app/noya-designsystem 0.1.63 → 0.1.65

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 (143) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +25 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +648 -333
  5. package/dist/index.d.ts +648 -333
  6. package/dist/index.js +13813 -9157
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +13949 -9269
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +10 -22
  11. package/src/__tests__/validateDropIndicator.test.ts +26 -41
  12. package/src/__tests__/workspaceLayout.test.ts +281 -0
  13. package/src/components/ActionMenu.tsx +2 -0
  14. package/src/components/ActivityIndicator.tsx +13 -6
  15. package/src/components/ActivityLog.tsx +197 -0
  16. package/src/components/AnimatePresence.tsx +43 -26
  17. package/src/components/Avatar.tsx +45 -26
  18. package/src/components/Banner.tsx +57 -11
  19. package/src/components/BaseToolbar.tsx +67 -34
  20. package/src/components/Breadcrumbs.tsx +4 -2
  21. package/src/components/Button.tsx +28 -23
  22. package/src/components/Checkbox.tsx +6 -35
  23. package/src/components/Chip.tsx +47 -33
  24. package/src/components/Collection.tsx +20 -12
  25. package/src/components/ColorSwatch.tsx +42 -22
  26. package/src/components/ColorSwatchControl.tsx +7 -5
  27. package/src/components/Combobox.tsx +8 -6
  28. package/src/components/ComboboxMenu.tsx +6 -5
  29. package/src/components/CommandPalette.tsx +3 -1
  30. package/src/components/ContextMenu.tsx +3 -1
  31. package/src/components/Dialog.tsx +9 -13
  32. package/src/components/DimensionInput.tsx +2 -0
  33. package/src/components/Divider.tsx +18 -14
  34. package/src/components/DraggableMenuButton.tsx +11 -5
  35. package/src/components/Drawer.tsx +14 -6
  36. package/src/components/DropdownMenu.tsx +4 -2
  37. package/src/components/EditableText.tsx +4 -2
  38. package/src/components/Fade.tsx +13 -13
  39. package/src/components/FileUploadIndicator.tsx +108 -0
  40. package/src/components/FloatingWindow.tsx +4 -2
  41. package/src/components/Grid.tsx +58 -42
  42. package/src/components/GridView.tsx +65 -36
  43. package/src/components/IVirtualizedList.tsx +5 -0
  44. package/src/components/IconButton.tsx +2 -0
  45. package/src/components/Icons.tsx +24 -4
  46. package/src/components/InputField.tsx +21 -19
  47. package/src/components/InspectorContainer.tsx +2 -2
  48. package/src/components/InspectorPrimitives.tsx +15 -9
  49. package/src/components/Label.tsx +4 -3
  50. package/src/components/LabeledElementView.tsx +9 -5
  51. package/src/components/LabeledField.tsx +8 -6
  52. package/src/components/List.tsx +112 -46
  53. package/src/components/ListMenu.tsx +91 -0
  54. package/src/components/ListNavigator.tsx +106 -0
  55. package/src/components/ListView.tsx +22 -1147
  56. package/src/components/MediaThumbnail.tsx +84 -14
  57. package/src/components/Message.tsx +11 -12
  58. package/src/components/Navigator.tsx +461 -0
  59. package/src/components/NoyaLogo.tsx +1 -1
  60. package/src/components/Popover.tsx +12 -6
  61. package/src/components/Progress.tsx +31 -8
  62. package/src/components/ResizableContainer.tsx +187 -0
  63. package/src/components/RingProgress.tsx +128 -0
  64. package/src/components/ScrollArea.tsx +7 -5
  65. package/src/components/ScrollableSidebar.tsx +10 -0
  66. package/src/components/SearchCompletionMenu.tsx +5 -3
  67. package/src/components/Section.tsx +109 -57
  68. package/src/components/SegmentedControl.tsx +51 -25
  69. package/src/components/SelectMenu.tsx +16 -21
  70. package/src/components/SelectionToolbar.tsx +7 -4
  71. package/src/components/Slider.tsx +12 -10
  72. package/src/components/Spacer.tsx +2 -2
  73. package/src/components/StackNavigator.tsx +378 -0
  74. package/src/components/Stepper.tsx +88 -0
  75. package/src/components/Switch.tsx +7 -5
  76. package/src/components/Tabs.tsx +6 -4
  77. package/src/components/Text.tsx +29 -18
  78. package/src/components/TextArea.tsx +5 -3
  79. package/src/components/Toast.tsx +5 -5
  80. package/src/components/Toolbar.tsx +57 -10
  81. package/src/components/ToolbarDrawer.tsx +29 -0
  82. package/src/components/Tooltip.tsx +2 -2
  83. package/src/components/TreeView.tsx +13 -4
  84. package/src/components/UserPointer.tsx +6 -4
  85. package/src/components/ai-assistant/AIAssistantLayout.tsx +10 -8
  86. package/src/components/blocks/ImageBlockComponent.tsx +162 -0
  87. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +6 -4
  88. package/src/components/file-explorer/FileExplorerLayout.tsx +22 -8
  89. package/src/components/internal/Checkmark.tsx +24 -0
  90. package/src/components/internal/Menu.tsx +24 -19
  91. package/src/components/internal/MenuViewport.tsx +15 -6
  92. package/src/components/internal/SelectItem.tsx +7 -26
  93. package/src/components/internal/TextInput.tsx +7 -1
  94. package/src/components/listView/ListViewContexts.tsx +68 -0
  95. package/src/components/listView/ListViewEditableRowTitle.tsx +55 -0
  96. package/src/components/listView/ListViewRoot.tsx +521 -0
  97. package/src/components/listView/ListViewRow.tsx +475 -0
  98. package/src/components/listView/ListViewRowTitle.tsx +21 -0
  99. package/src/components/listView/types.ts +11 -0
  100. package/src/components/pipeline/PipelineResultLayout.tsx +6 -6
  101. package/src/components/sorting/DragRegistration.tsx +10 -5
  102. package/src/components/sorting/SharedDragProvider.tsx +134 -27
  103. package/src/components/sorting/Sortable.tsx +90 -27
  104. package/src/components/sorting/createSharedDrag.tsx +7 -6
  105. package/src/components/sorting/sorting.ts +6 -9
  106. package/src/components/workspace/DrawerWorkspaceLayout.tsx +110 -29
  107. package/src/components/workspace/PanelWorkspaceLayout.tsx +148 -99
  108. package/src/components/workspace/VerticalTabMenu.tsx +46 -0
  109. package/src/components/workspace/WorkspaceLayout.tsx +76 -63
  110. package/src/components/workspace/constants.ts +4 -0
  111. package/src/components/workspace/panelStorage.ts +216 -0
  112. package/src/components/workspace/types.ts +11 -4
  113. package/src/contexts/DesignSystemConfiguration.tsx +12 -5
  114. package/src/contexts/DialogContext.tsx +19 -5
  115. package/src/contexts/FloatingWindowContext.tsx +2 -0
  116. package/src/contexts/GlobalInputBlurContext.tsx +2 -0
  117. package/src/contexts/ImageDataContext.tsx +2 -0
  118. package/src/contexts/LinkComponentContext.tsx +17 -0
  119. package/src/contexts/OpenPortalsContext.tsx +2 -0
  120. package/src/contexts/PortalScopeContext.tsx +2 -0
  121. package/src/hooks/mergeEventHandlers.ts +1 -1
  122. package/src/hooks/useHover.ts +30 -17
  123. package/src/hooks/useIndent.ts +5 -8
  124. package/src/hooks/useLabel.ts +2 -0
  125. package/src/hooks/useObjectURL.ts +5 -3
  126. package/src/hooks/usePlatform.ts +5 -3
  127. package/src/hooks/useTheme.ts +3 -1
  128. package/src/index.css +43 -10
  129. package/src/index.tsx +15 -7
  130. package/src/utils/classNames.ts +56 -9
  131. package/src/utils/combobox.ts +2 -0
  132. package/src/utils/editableBlockStyles.ts +2 -2
  133. package/src/utils/formatByteSize.ts +9 -0
  134. package/src/utils/inputs.ts +8 -6
  135. package/src/utils/sketchColor.ts +0 -34
  136. package/src/utils/withSeparatorElements.ts +1 -1
  137. package/tailwind.config.ts +1 -0
  138. package/src/components/FillInputField.tsx +0 -37
  139. package/src/components/FillPreviewBackground.tsx +0 -132
  140. package/src/components/GradientPicker.tsx +0 -88
  141. package/src/hooks/usePreservePanelSize.tsx +0 -183
  142. package/src/mediaQuery.ts +0 -13
  143. package/src/utils/getGradientBackground.tsx +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.63",
3
+ "version": "0.1.65",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -22,33 +22,21 @@
22
22
  "dependencies": {
23
23
  "@dnd-kit/core": "6.3.1",
24
24
  "@dnd-kit/sortable": "10.0.0",
25
- "@noya-app/noya-colorpicker": "0.1.26",
26
- "@noya-app/noya-utils": "0.1.5",
27
- "@noya-app/noya-geometry": "0.1.12",
28
- "@noya-app/noya-icons": "0.1.11",
29
- "@noya-app/noya-keymap": "0.1.3",
30
- "@noya-app/noya-tailwind-config": "0.1.5",
31
- "@radix-ui/primitive": "^1.0.0",
32
- "@radix-ui/react-avatar": "^1.0.1",
25
+ "@noya-app/noya-colorpicker": "0.1.28",
26
+ "@noya-app/noya-utils": "0.1.7",
27
+ "@noya-app/noya-geometry": "0.1.14",
28
+ "@noya-app/noya-icons": "0.1.13",
29
+ "@noya-app/noya-keymap": "0.1.4",
30
+ "@noya-app/noya-tailwind-config": "0.1.7",
31
+ "radix-ui": "1.4.2",
33
32
  "@radix-ui/react-compose-refs": "^1.0.0",
34
- "@radix-ui/react-context-menu": "^2.0.1",
35
- "@radix-ui/react-dialog": "^1.0.2",
36
- "@radix-ui/react-dropdown-menu": "^2.0.1",
37
- "@radix-ui/react-popover": "^1.0.2",
38
- "@radix-ui/react-progress": "^1.0.1",
39
- "@radix-ui/react-scroll-area": "^1.0.2",
40
- "@radix-ui/react-slider": "^1.2.2",
41
- "@radix-ui/react-switch": "^1.0.1",
42
- "@radix-ui/react-toast": "^1.1.4",
43
- "@radix-ui/react-toggle-group": "^1.0.1",
44
- "@radix-ui/react-tooltip": "^1.0.2",
33
+ "@radix-ui/primitive": "^1.0.1",
45
34
  "@radix-ui/react-utils": "^0.0.5",
46
- "@radix-ui/react-select": "^2.1.1",
47
35
  "@radix-ui/react-popper": "1.2.3",
48
36
  "@radix-ui/utils": "0.0.3",
49
37
  "immer": "9.0.5",
50
38
  "kiwi.js": "^1.1.3",
51
- "react-resizable-panels": "2.0.22",
39
+ "react-resizable-panels": "3.0.3",
52
40
  "react-window": "^1.8.8",
53
41
  "vscode-fuzzy-scorer": "^0.0.4",
54
42
  "react-virtualized": "9.22.6"
@@ -118,15 +118,12 @@ describe("validateDropIndicator", () => {
118
118
  return parameters.position !== "below";
119
119
  };
120
120
 
121
- const keys = ["item1", "item2", "item3"];
122
-
123
121
  describe("with above and inside positions allowed, but not below", () => {
124
122
  it("should return 'inside' when cursor is in the middle third", () => {
125
123
  const result = validateDropIndicator({
126
124
  acceptsDrop: mockAcceptsAboveAndInside,
127
- keys,
128
- activeId: "item1",
129
- overId: "item2",
125
+ activeIndex: 0,
126
+ overIndex: 1,
130
127
  offsetStart: 50, // offsetStart in middle third
131
128
  elementStart: 0, // elementStart
132
129
  elementSize: 100, // elementSize
@@ -139,9 +136,8 @@ describe("validateDropIndicator", () => {
139
136
  it("should return 'above' when cursor is in the top third", () => {
140
137
  const result = validateDropIndicator({
141
138
  acceptsDrop: mockAcceptsAboveAndInside,
142
- keys,
143
- activeId: "item1",
144
- overId: "item2",
139
+ activeIndex: 0,
140
+ overIndex: 1,
145
141
  offsetStart: 20, // offsetStart in top third
146
142
  elementStart: 0, // elementStart
147
143
  elementSize: 100, // elementSize
@@ -154,9 +150,8 @@ describe("validateDropIndicator", () => {
154
150
  it("should fallback to 'inside' when cursor is in bottom third since 'below' is not allowed", () => {
155
151
  const result = validateDropIndicator({
156
152
  acceptsDrop: mockAcceptsAboveAndInside,
157
- keys,
158
- activeId: "item1",
159
- overId: "item2",
153
+ activeIndex: 0,
154
+ overIndex: 1,
160
155
  offsetStart: 80, // offsetStart in bottom third
161
156
  elementStart: 0, // elementStart
162
157
  elementSize: 100, // elementSize
@@ -171,9 +166,8 @@ describe("validateDropIndicator", () => {
171
166
  it("should prefer 'inside' when cursor is in the middle third", () => {
172
167
  const result = validateDropIndicator({
173
168
  acceptsDrop: mockAcceptsAll,
174
- keys,
175
- activeId: "item1",
176
- overId: "item2",
169
+ activeIndex: 0,
170
+ overIndex: 1,
177
171
  offsetStart: 50, // offsetStart in middle third
178
172
  elementStart: 0, // elementStart
179
173
  elementSize: 100, // elementSize
@@ -186,9 +180,8 @@ describe("validateDropIndicator", () => {
186
180
  it("should prefer 'above' when cursor is in the top third", () => {
187
181
  const result = validateDropIndicator({
188
182
  acceptsDrop: mockAcceptsAll,
189
- keys,
190
- activeId: "item1",
191
- overId: "item2",
183
+ activeIndex: 0,
184
+ overIndex: 1,
192
185
  offsetStart: 20, // offsetStart in top third
193
186
  elementStart: 0, // elementStart
194
187
  elementSize: 100, // elementSize
@@ -201,9 +194,8 @@ describe("validateDropIndicator", () => {
201
194
  it("should prefer 'below' when cursor is in the bottom third", () => {
202
195
  const result = validateDropIndicator({
203
196
  acceptsDrop: mockAcceptsAll,
204
- keys,
205
- activeId: "item1",
206
- overId: "item2",
197
+ activeIndex: 0,
198
+ overIndex: 1,
207
199
  offsetStart: 80, // offsetStart in bottom third
208
200
  elementStart: 0, // elementStart
209
201
  elementSize: 100, // elementSize
@@ -216,9 +208,8 @@ describe("validateDropIndicator", () => {
216
208
  it("should handle non-zero elementStart position", () => {
217
209
  const result = validateDropIndicator({
218
210
  acceptsDrop: mockAcceptsAll,
219
- keys,
220
- activeId: "item1",
221
- overId: "item2",
211
+ activeIndex: 0,
212
+ overIndex: 1,
222
213
  offsetStart: 120, // offsetStart in middle third relative to elementStart
223
214
  elementStart: 100, // elementStart
224
215
  elementSize: 60, // elementSize
@@ -232,9 +223,8 @@ describe("validateDropIndicator", () => {
232
223
  it("should return 'inside' when cursor is in the middle third and inside drops are allowed", () => {
233
224
  const result = validateDropIndicator({
234
225
  acceptsDrop: mockAcceptsDrop,
235
- keys,
236
- activeId: "item1",
237
- overId: "item2",
226
+ activeIndex: 0,
227
+ overIndex: 1,
238
228
  offsetStart: 50, // offsetStart in middle third
239
229
  elementStart: 0, // elementStart
240
230
  elementSize: 100, // elementSize
@@ -247,9 +237,8 @@ describe("validateDropIndicator", () => {
247
237
  it("should return 'above' when cursor is in top half and above drops are allowed", () => {
248
238
  const result = validateDropIndicator({
249
239
  acceptsDrop: mockAcceptsDropAboveBelow,
250
- keys,
251
- activeId: "item1",
252
- overId: "item2",
240
+ activeIndex: 0,
241
+ overIndex: 1,
253
242
  offsetStart: 20, // offsetStart in top half
254
243
  elementStart: 0, // elementStart
255
244
  elementSize: 100, // elementSize
@@ -262,9 +251,8 @@ describe("validateDropIndicator", () => {
262
251
  it("should return 'below' when cursor is in bottom half and below drops are allowed", () => {
263
252
  const result = validateDropIndicator({
264
253
  acceptsDrop: mockAcceptsDropAboveBelow,
265
- keys,
266
- activeId: "item1",
267
- overId: "item2",
254
+ activeIndex: 0,
255
+ overIndex: 1,
268
256
  offsetStart: 70, // offsetStart in bottom half
269
257
  elementStart: 0, // elementStart
270
258
  elementSize: 100, // elementSize
@@ -277,9 +265,8 @@ describe("validateDropIndicator", () => {
277
265
  it("should fallback to inside when above/below not allowed and inside is allowed", () => {
278
266
  const result = validateDropIndicator({
279
267
  acceptsDrop: mockAcceptsDrop,
280
- keys,
281
- activeId: "item1",
282
- overId: "item2",
268
+ activeIndex: 0,
269
+ overIndex: 1,
283
270
  offsetStart: 20, // offsetStart in top half
284
271
  elementStart: 0, // elementStart
285
272
  elementSize: 100, // elementSize
@@ -293,9 +280,8 @@ describe("validateDropIndicator", () => {
293
280
  const neverAllowDrop = () => false;
294
281
  const result = validateDropIndicator({
295
282
  acceptsDrop: neverAllowDrop,
296
- keys,
297
- activeId: "item1",
298
- overId: "item2",
283
+ activeIndex: 0,
284
+ overIndex: 1,
299
285
  offsetStart: 50,
300
286
  elementStart: 0,
301
287
  elementSize: 100,
@@ -308,9 +294,8 @@ describe("validateDropIndicator", () => {
308
294
  it("should ignore items not in the keys array", () => {
309
295
  const result = validateDropIndicator({
310
296
  acceptsDrop: mockAcceptsDrop,
311
- keys,
312
- activeId: "nonexistent",
313
- overId: "item2",
297
+ activeIndex: -1,
298
+ overIndex: 1,
314
299
  offsetStart: 50,
315
300
  elementStart: 0,
316
301
  elementSize: 100,
@@ -0,0 +1,281 @@
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,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { memoGeneric } from "@noya-app/react-utils";
2
4
  import React, { useMemo } from "react";
3
5
  import { cssVars } from "../theme";
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import * as React from "react";
2
4
  import { cx } from "../utils/classNames";
3
5
 
@@ -7,6 +9,7 @@ interface Props {
7
9
  color?: string;
8
10
  trackColor?: string;
9
11
  className?: string;
12
+ style?: React.CSSProperties;
10
13
  }
11
14
 
12
15
  export const ActivityIndicator = React.memo(function ActivityIndicator({
@@ -15,8 +18,12 @@ export const ActivityIndicator = React.memo(function ActivityIndicator({
15
18
  color = "black",
16
19
  trackColor = "rgba(0, 0, 0, 0.1)",
17
20
  className,
21
+ style,
18
22
  }: Props) {
19
- const opacityStyle = React.useMemo(() => ({ opacity }), [opacity]);
23
+ const containerStyle = React.useMemo(
24
+ () => ({ opacity, ...style }),
25
+ [opacity, style]
26
+ );
20
27
  const dynamicStyles = React.useMemo(
21
28
  () => ({
22
29
  border: `2px solid ${trackColor}`,
@@ -27,11 +34,11 @@ export const ActivityIndicator = React.memo(function ActivityIndicator({
27
34
  [size, color, trackColor]
28
35
  );
29
36
  return (
30
- <div
31
- className={cx("flex justify-center items-center", className)}
32
- style={opacityStyle}
37
+ <span
38
+ className={cx("n-flex n-justify-center n-items-center", className)}
39
+ style={containerStyle}
33
40
  >
34
- <div style={dynamicStyles} className="animate-spin rounded-[50%]" />
35
- </div>
41
+ <span style={dynamicStyles} className="n-animate-spin n-rounded-[50%]" />
42
+ </span>
36
43
  );
37
44
  });