@noya-app/noya-designsystem 0.1.76 → 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 (86) hide show
  1. package/.turbo/turbo-build.log +20 -12
  2. package/CHANGELOG.md +19 -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 +192 -93
  13. package/dist/index.d.ts +192 -93
  14. package/dist/index.js +3355 -1763
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3622 -2030
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +13 -9
  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 +20 -6
  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 +307 -46
  31. package/src/components/EditableText.tsx +1 -1
  32. package/src/components/EmojiPicker.tsx +645 -0
  33. package/src/components/GridBackground.tsx +40 -0
  34. package/src/components/GridView.tsx +5 -1
  35. package/src/components/IVirtualizedList.tsx +21 -1
  36. package/src/components/InputField.tsx +2 -10
  37. package/src/components/InspectorContainer.tsx +4 -2
  38. package/src/components/Message.tsx +5 -16
  39. package/src/components/OverlayToolbar.tsx +97 -0
  40. package/src/components/Popover.tsx +73 -107
  41. package/src/components/Progress.tsx +18 -18
  42. package/src/components/ScrollArea.tsx +66 -31
  43. package/src/components/ScrollableSidebar.tsx +1 -1
  44. package/src/components/SegmentedControl.tsx +166 -38
  45. package/src/components/SelectMenu.tsx +193 -101
  46. package/src/components/Slider.tsx +40 -38
  47. package/src/components/StackNavigator.tsx +1 -0
  48. package/src/components/Switch.tsx +4 -4
  49. package/src/components/TextArea.tsx +1 -1
  50. package/src/components/Toast.tsx +99 -26
  51. package/src/components/Toolbar.tsx +133 -24
  52. package/src/components/Tooltip.tsx +18 -8
  53. package/src/components/UserPointer.tsx +5 -3
  54. package/src/components/Virtualized.tsx +193 -14
  55. package/src/components/VisuallyHidden.tsx +20 -0
  56. package/src/components/__tests__/Virtualized.math.test.ts +426 -1
  57. package/src/components/__tests__/Virtualized.test.tsx +129 -1
  58. package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
  59. package/src/components/internal/Menu.tsx +4 -0
  60. package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
  61. package/src/components/listView/ListViewRoot.tsx +5 -1
  62. package/src/components/resizablePanels/Panel.tsx +94 -0
  63. package/src/components/resizablePanels/PanelGroup.tsx +498 -0
  64. package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
  65. package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
  66. package/src/components/resizablePanels/index.ts +7 -0
  67. package/src/components/resizablePanels/types.ts +65 -0
  68. package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
  69. package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
  70. package/src/components/workspace/WorkspaceLayout.tsx +82 -76
  71. package/src/components/workspace/types.ts +6 -4
  72. package/src/contexts/DialogContext.tsx +15 -24
  73. package/src/emojis.ts +1 -0
  74. package/src/hooks/useTriggerToggle.ts +95 -0
  75. package/src/index.css +2 -0
  76. package/src/index.tsx +3 -2
  77. package/src/theme/proseTheme.ts +22 -0
  78. package/src/utils/mergeProps.ts +57 -0
  79. package/src/utils/skinTone.ts +90 -0
  80. package/tailwind.config.ts +6 -1
  81. package/tsup.config.ts +1 -1
  82. package/src/__tests__/workspaceLayout.test.ts +0 -281
  83. package/src/components/ScrollArea2.tsx +0 -76
  84. package/src/components/internal/MenuViewport.tsx +0 -178
  85. package/src/components/internal/SelectItem.tsx +0 -138
  86. package/src/components/workspace/panelStorage.ts +0 -216
@@ -1,33 +1,47 @@
1
1
  "use client";
2
2
 
3
+ import { Menu } from "@base-ui/react/menu";
4
+ import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
3
5
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
4
6
  import {
5
7
  forwardRefGeneric,
6
8
  memoGeneric,
7
9
  useControlledOrUncontrolled,
8
10
  } from "@noya-app/react-utils";
9
- import { DropdownMenu as RadixDropdownMenu } from "radix-ui";
10
11
  import React, {
11
12
  ComponentProps,
12
13
  ForwardedRef,
13
14
  ReactNode,
14
- SyntheticEvent,
15
15
  useMemo,
16
+ useState,
16
17
  } from "react";
17
18
  import {
18
19
  portalScopeProps,
19
20
  usePortalScopeId,
20
21
  } from "../contexts/PortalScopeContext";
22
+ import { cx } from "../utils/classNames";
21
23
  import { Button, ButtonProps } from "./Button";
22
24
  import { MenuProps } from "./ContextMenu";
23
- import { getKeyboardShortcutsForMenuItems, styles } from "./internal/Menu";
24
- import { MenuViewport } from "./internal/MenuViewport";
25
+ import { Icon, IconName } from "./Icons";
26
+ import {
27
+ CHECKBOX_INDENT_WIDTH,
28
+ CHECKBOX_RIGHT_INSET,
29
+ CHECKBOX_WIDTH,
30
+ getKeyboardShortcutsForMenuItems,
31
+ isSelectableMenuItem,
32
+ KeyboardShortcut,
33
+ MenuItem,
34
+ MenuItemIcon,
35
+ SectionHeaderMenuItem as SectionHeaderMenuItemComponent,
36
+ styles,
37
+ } from "./internal/Menu";
38
+ import { Spacer } from "./Spacer";
25
39
 
26
40
  /* ----------------------------------------------------------------------------
27
41
  * Root
28
42
  * ------------------------------------------------------------------------- */
29
43
 
30
- export type DropdownRootProps<T extends string> = Omit<
44
+ export type DropdownMenuProps<T extends string> = Omit<
31
45
  MenuProps<T>,
32
46
  "children"
33
47
  > & {
@@ -35,29 +49,228 @@ export type DropdownRootProps<T extends string> = Omit<
35
49
  title?: ReactNode;
36
50
  icon?: ButtonProps["icon"];
37
51
  open?: boolean;
38
- onCloseAutoFocus?: React.EventHandler<SyntheticEvent<unknown>>;
39
52
  emptyState?: React.ReactNode;
40
53
  contentStyle?: React.CSSProperties;
54
+ modal?: boolean;
41
55
  } & Pick<
42
- ComponentProps<typeof RadixDropdownMenu.Content>,
56
+ ComponentProps<typeof Menu.Positioner>,
43
57
  "side" | "sideOffset" | "align" | "alignOffset" | "collisionPadding"
44
58
  >;
45
59
 
46
- const Components = {
47
- Separator: RadixDropdownMenu.Separator,
48
- ItemText: React.Fragment,
49
- Item: RadixDropdownMenu.Item,
50
- CheckboxItem: RadixDropdownMenu.CheckboxItem,
51
- ItemIndicator: RadixDropdownMenu.ItemIndicator,
52
- SubTrigger: RadixDropdownMenu.SubTrigger,
53
- SubContent: RadixDropdownMenu.SubContent,
54
- Sub: RadixDropdownMenu.Sub,
55
- Portal: RadixDropdownMenu.Portal,
56
- };
60
+ const itemStyle = (state: { disabled: boolean; highlighted: boolean }) =>
61
+ cx(
62
+ "n-flex-none n-select-none n-cursor-pointer n-rounded",
63
+ "n-py-1.5 n-px-2",
64
+ "n-transition-colors",
65
+ "n-flex n-items-center",
66
+ "n-font-sans n-text-button n-font-medium",
67
+ "focus:n-outline-none",
68
+ state.disabled && "n-text-text-disabled",
69
+ state.highlighted &&
70
+ "n-text-selected-list-item-text n-bg-selected-list-item-background"
71
+ );
72
+
73
+ function MenuItemContent({
74
+ icon,
75
+ checked,
76
+ indented,
77
+ children,
78
+ shortcut,
79
+ isSubmenuTrigger,
80
+ }: {
81
+ icon?: MenuItemIcon;
82
+ checked?: boolean;
83
+ indented?: boolean;
84
+ children: ReactNode;
85
+ shortcut?: string;
86
+ isSubmenuTrigger?: boolean;
87
+ }) {
88
+ return (
89
+ <>
90
+ {indented && <Spacer.Horizontal size={CHECKBOX_INDENT_WIDTH} />}
91
+ {checked ? (
92
+ <span
93
+ className="n-flex n-items-center n-relative -n-left-2.5"
94
+ style={{ marginRight: -CHECKBOX_RIGHT_INSET }}
95
+ >
96
+ <CheckIcon />
97
+ </span>
98
+ ) : indented ? (
99
+ <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
100
+ ) : null}
101
+ {icon &&
102
+ (typeof icon === "string" ? (
103
+ <Icon
104
+ name={icon as IconName}
105
+ className="n-mr-2 n-w-4 n-h-4 n-flex-shrink-0"
106
+ />
107
+ ) : (
108
+ <span className="n-mr-2 n-w-4 n-h-4 n-flex-shrink-0 n-flex n-items-center n-justify-center">
109
+ {icon}
110
+ </span>
111
+ ))}
112
+ <div className="n-flex n-items-center n-flex-1">{children}</div>
113
+ {shortcut && (
114
+ <>
115
+ <Spacer.Horizontal size={16} />
116
+ <KeyboardShortcut shortcut={shortcut} />
117
+ </>
118
+ )}
119
+ {isSubmenuTrigger && <ChevronRightIcon className="-n-mr-1" />}
120
+ </>
121
+ );
122
+ }
123
+
124
+ // Render submenu inline to preserve React context from parent Menu.Root
125
+ function SubmenuItem<T extends string>({
126
+ item,
127
+ onSelect,
128
+ hasCheckedItem,
129
+ portalScopeId,
130
+ }: {
131
+ item: Extract<MenuItem<T>, { type: "submenu" }>;
132
+ onSelect: ((value: T) => void) | undefined;
133
+ hasCheckedItem: boolean;
134
+ portalScopeId: string | undefined;
135
+ }) {
136
+ const [open, setOpen] = useState(false);
137
+
138
+ const submenuHasCheckedItem = item.items.some(
139
+ (subItem) => isSelectableMenuItem(subItem) && subItem.checked
140
+ );
141
+
142
+ return (
143
+ <Menu.SubmenuRoot
144
+ key={item.id}
145
+ open={open}
146
+ onOpenChange={(newOpen, event) => {
147
+ // Ignore focus-out events that cause premature submenu closing
148
+ if (event.reason === "focus-out") {
149
+ return;
150
+ }
151
+ setOpen(newOpen);
152
+ }}
153
+ >
154
+ <div className="n-px-1">
155
+ <Menu.SubmenuTrigger
156
+ disabled={item.disabled}
157
+ className={itemStyle}
158
+ openOnHover
159
+ delay={0}
160
+ closeDelay={150}
161
+ nativeButton={false}
162
+ >
163
+ <MenuItemContent
164
+ icon={item.icon}
165
+ checked={item.checked}
166
+ indented={hasCheckedItem}
167
+ isSubmenuTrigger
168
+ >
169
+ {item.title}
170
+ </MenuItemContent>
171
+ </Menu.SubmenuTrigger>
172
+ </div>
173
+ <Menu.Portal>
174
+ <Menu.Positioner
175
+ {...portalScopeProps(portalScopeId)}
176
+ alignOffset={-5}
177
+ className="n-z-menu n-outline-none"
178
+ >
179
+ <Menu.Popup className={styles.contentStyle}>
180
+ <MenuViewport
181
+ items={item.items}
182
+ onSelect={onSelect}
183
+ hasCheckedItem={submenuHasCheckedItem}
184
+ />
185
+ </Menu.Popup>
186
+ </Menu.Positioner>
187
+ </Menu.Portal>
188
+ </Menu.SubmenuRoot>
189
+ );
190
+ }
191
+
192
+ function MenuViewport<T extends string>({
193
+ items,
194
+ onSelect,
195
+ hasCheckedItem,
196
+ }: {
197
+ items: MenuItem<T>[];
198
+ onSelect?: (value: T) => void;
199
+ hasCheckedItem: boolean;
200
+ }) {
201
+ const portalScopeId = usePortalScopeId();
202
+
203
+ return (
204
+ <>
205
+ {items.map((item, index) => {
206
+ switch (item.type) {
207
+ case "separator":
208
+ return (
209
+ <Menu.Separator key={index} className={styles.separatorStyle} />
210
+ );
211
+
212
+ case "sectionHeader":
213
+ return (
214
+ <SectionHeaderMenuItemComponent
215
+ isFirst={index === 0}
216
+ key={item.id}
217
+ {...item}
218
+ indented={hasCheckedItem}
219
+ />
220
+ );
221
+
222
+ case "popover":
223
+ return null;
224
+
225
+ case "submenu":
226
+ if (item.items) {
227
+ return (
228
+ <SubmenuItem
229
+ key={item.id}
230
+ item={item as Extract<MenuItem<string>, { type: "submenu" }>}
231
+ onSelect={onSelect as ((value: string) => void) | undefined}
232
+ hasCheckedItem={hasCheckedItem}
233
+ portalScopeId={portalScopeId}
234
+ />
235
+ );
236
+ }
237
+ return null;
238
+
239
+ default:
240
+ return (
241
+ <div key={item.value} className="n-px-1">
242
+ <Menu.Item
243
+ disabled={item.disabled}
244
+ className={itemStyle}
245
+ onClick={() => {
246
+ if (onSelect && item.value) {
247
+ // Delay onSelect to allow menu to close and focus to settle
248
+ // This fixes issues like rename not working because focus
249
+ // returns to trigger after menu closes
250
+ setTimeout(() => onSelect(item.value), 0);
251
+ }
252
+ }}
253
+ >
254
+ <MenuItemContent
255
+ icon={item.icon}
256
+ checked={item.checked}
257
+ indented={hasCheckedItem}
258
+ shortcut={item.shortcut}
259
+ >
260
+ {item.title ?? item.value}
261
+ </MenuItemContent>
262
+ </Menu.Item>
263
+ </div>
264
+ );
265
+ }
266
+ })}
267
+ </>
268
+ );
269
+ }
57
270
 
58
271
  const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
59
272
  T extends string,
60
- >(props: DropdownRootProps<T>, forwardedRef: ForwardedRef<HTMLElement>) {
273
+ >(props: DropdownMenuProps<T>, forwardedRef: ForwardedRef<HTMLElement>) {
61
274
  const {
62
275
  items,
63
276
  children,
@@ -65,7 +278,6 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
65
278
  shouldBindKeyboardShortcuts,
66
279
  onOpenChange,
67
280
  open: openProp,
68
- onCloseAutoFocus,
69
281
  side,
70
282
  sideOffset = 4,
71
283
  align,
@@ -75,6 +287,7 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
75
287
  contentStyle,
76
288
  title,
77
289
  icon,
290
+ modal = true,
78
291
  } = props;
79
292
 
80
293
  const [open, setOpen] = useControlledOrUncontrolled({
@@ -104,41 +317,89 @@ const DropdownMenuRoot = forwardRefGeneric(function DropdownMenuRoot<
104
317
  [contentStyle]
105
318
  );
106
319
 
320
+ const hasCheckedItem = items.some(
321
+ (item) => isSelectableMenuItem(item) && item.checked
322
+ );
323
+
324
+ const childContent =
325
+ typeof children === "function"
326
+ ? children({ open })
327
+ : children || (
328
+ <Button icon={icon} active={open} iconRight="DropdownChevronIcon">
329
+ {title}
330
+ </Button>
331
+ );
332
+
333
+ // Detect if the child renders as a native button element
334
+ const isNativeButton =
335
+ React.isValidElement(childContent) &&
336
+ (childContent.type === "button" ||
337
+ (childContent.type === Button &&
338
+ (childContent.props as ButtonProps).as !== "a"));
339
+
107
340
  return (
108
- <RadixDropdownMenu.Root onOpenChange={setOpen} open={open}>
109
- <RadixDropdownMenu.Trigger
341
+ <Menu.Root
342
+ open={open}
343
+ onOpenChange={(newOpen, event) => {
344
+ // Ignore sibling-open events - these are incorrectly fired when submenus open
345
+ // This is a workaround for Base UI treating SubmenuRoot as a sibling menu
346
+ if (event.reason === "sibling-open" && newOpen === false) {
347
+ return;
348
+ }
349
+ setOpen(newOpen);
350
+ }}
351
+ modal={modal}
352
+ >
353
+ <Menu.Trigger
110
354
  ref={forwardedRef as ForwardedRef<any>}
111
- asChild
112
- >
113
- {typeof children === "function"
114
- ? children({ open })
115
- : children || (
116
- <Button icon={icon} active={open} iconRight="DropdownChevronIcon">
117
- {title}
118
- </Button>
119
- )}
120
- </RadixDropdownMenu.Trigger>
121
- <RadixDropdownMenu.Portal>
122
- <RadixDropdownMenu.Content
355
+ nativeButton={isNativeButton}
356
+ render={(triggerProps) => {
357
+ // Clone the child element and merge props
358
+ // triggerProps must come last to ensure dropdown handlers work
359
+ // Wrap onClick to stop propagation so parent click handlers don't fire
360
+ const wrappedProps = {
361
+ ...triggerProps,
362
+ onClick: (e: React.MouseEvent) => {
363
+ e.stopPropagation();
364
+ triggerProps.onClick?.(e);
365
+ },
366
+ onPointerDown: (e: React.PointerEvent) => {
367
+ e.stopPropagation();
368
+ triggerProps.onPointerDown?.(e);
369
+ },
370
+ };
371
+
372
+ if (React.isValidElement(childContent)) {
373
+ return React.cloneElement(childContent, {
374
+ ...(childContent.props as any),
375
+ ...wrappedProps,
376
+ });
377
+ }
378
+
379
+ return <span {...wrappedProps}>{childContent}</span>;
380
+ }}
381
+ />
382
+ <Menu.Portal>
383
+ <Menu.Positioner
123
384
  {...portalScopeProps(portalScopeId)}
124
- className={styles.contentStyle}
125
385
  side={side}
126
386
  sideOffset={sideOffset}
127
387
  align={align}
128
388
  alignOffset={alignOffset}
129
- style={contentStyles}
130
389
  collisionPadding={collisionPadding}
131
- {...({ onCloseAutoFocus } as any)}
390
+ className="n-z-menu n-outline-none"
132
391
  >
133
- {items.length === 0 ? emptyState : null}
134
- <MenuViewport
135
- items={items}
136
- Components={Components}
137
- onSelect={onSelect}
138
- />
139
- </RadixDropdownMenu.Content>
140
- </RadixDropdownMenu.Portal>
141
- </RadixDropdownMenu.Root>
392
+ <Menu.Popup className={styles.contentStyle} style={contentStyles}>
393
+ {items.length === 0 ? emptyState : null}
394
+ <MenuViewport
395
+ items={items}
396
+ onSelect={onSelect}
397
+ hasCheckedItem={hasCheckedItem}
398
+ />
399
+ </Menu.Popup>
400
+ </Menu.Positioner>
401
+ </Menu.Portal>
402
+ </Menu.Root>
142
403
  );
143
404
  });
144
405
 
@@ -155,7 +155,7 @@ export const EditableText = memo(
155
155
  });
156
156
 
157
157
  const input = (
158
- <div className="n-flex n-absolute n-inset-0 -n-mx-1.5 -n-my-1">
158
+ <div className="n-flex n-absolute n-inset-0 -n-mx-input-padding-x -n-my-1">
159
159
  <InputField.Root onFocusChange={setFocused}>
160
160
  <InputField.Input
161
161
  ref={inputRef}