@noya-app/noya-designsystem 0.1.44 → 0.1.45

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 (44) hide show
  1. package/.turbo/turbo-build.log +13 -10
  2. package/CHANGELOG.md +9 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +320 -78
  5. package/dist/index.d.ts +320 -78
  6. package/dist/index.js +2113 -1374
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +2000 -1274
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +3 -3
  11. package/src/components/AnimatePresence.tsx +10 -1
  12. package/src/components/Avatar.tsx +2 -0
  13. package/src/components/Checkbox.tsx +6 -1
  14. package/src/components/Combobox.tsx +6 -4
  15. package/src/components/ComboboxMenu.tsx +30 -15
  16. package/src/components/CommandPalette.tsx +69 -0
  17. package/src/components/ContextMenu.tsx +33 -134
  18. package/src/components/DropdownMenu.tsx +47 -155
  19. package/src/components/Fade.tsx +62 -0
  20. package/src/components/InputField.tsx +109 -133
  21. package/src/components/Label.tsx +81 -7
  22. package/src/components/LabeledField.tsx +112 -0
  23. package/src/components/ListView.tsx +55 -52
  24. package/src/components/Popover.tsx +12 -1
  25. package/src/components/SearchCompletionMenu.tsx +206 -0
  26. package/src/components/SegmentedControl.tsx +5 -2
  27. package/src/components/SelectMenu.tsx +104 -124
  28. package/src/components/SidebarList.tsx +252 -0
  29. package/src/components/Slider.tsx +18 -26
  30. package/src/components/Text.tsx +1 -0
  31. package/src/components/TextArea.tsx +4 -1
  32. package/src/components/Toolbar.tsx +6 -2
  33. package/src/components/TreeView.tsx +3 -0
  34. package/src/components/WorkspaceLayout.tsx +38 -18
  35. package/src/components/internal/Menu.tsx +58 -10
  36. package/src/components/internal/MenuViewport.tsx +151 -0
  37. package/src/components/internal/SelectItem.tsx +111 -0
  38. package/src/hooks/useIndent.ts +12 -0
  39. package/src/hooks/useLabel.ts +51 -0
  40. package/src/hooks/usePreservePanelSize.tsx +50 -19
  41. package/src/index.tsx +12 -5
  42. package/src/utils/combobox.ts +4 -1
  43. package/src/utils/createSectionedMenu.ts +11 -8
  44. package/src/utils/selection.ts +36 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.44",
3
+ "version": "0.1.45",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -23,10 +23,10 @@
23
23
  "@dnd-kit/core": "3.1.1",
24
24
  "@dnd-kit/modifiers": "3.0.0",
25
25
  "@dnd-kit/sortable": "4.0.0",
26
- "@noya-app/noya-colorpicker": "0.1.15",
26
+ "@noya-app/noya-colorpicker": "0.1.16",
27
27
  "@noya-app/noya-utils": "0.1.3",
28
28
  "@noya-app/noya-geometry": "0.1.6",
29
- "@noya-app/noya-icons": "0.1.5",
29
+ "@noya-app/noya-icons": "0.1.6",
30
30
  "@noya-app/noya-keymap": "0.1.3",
31
31
  "@radix-ui/primitive": "^1.0.0",
32
32
  "@radix-ui/react-avatar": "^1.0.1",
@@ -32,6 +32,7 @@ type AnimatePresenceProps = {
32
32
  * "crossfade" - crossfade between the previous and next animation
33
33
  */
34
34
  mode?: "wait" | "crossfade";
35
+ className?: string;
35
36
  };
36
37
 
37
38
  const defaultDuration = 150;
@@ -42,6 +43,7 @@ type PresenceChildProps = {
42
43
  isPresent: boolean;
43
44
  onExitComplete: () => void;
44
45
  waitingToEnter?: boolean;
46
+ className?: string;
45
47
  } & Pick<
46
48
  AnimatePresenceProps,
47
49
  "animationStyles" | "children" | "duration" | "skipInitialAnimation"
@@ -71,6 +73,7 @@ const PresenceChild = ({
71
73
  animationStyles,
72
74
  onExitComplete,
73
75
  skipInitialAnimation,
76
+ className,
74
77
  }: PresenceChildProps) => {
75
78
  const [animationState, setAnimationState] = useState<AnimationState>(() =>
76
79
  skipInitialAnimation && isPresent
@@ -131,7 +134,11 @@ const PresenceChild = ({
131
134
  return null;
132
135
  }
133
136
 
134
- return <div style={mergedAnimationStyles[animationState]}>{children}</div>;
137
+ return (
138
+ <div style={mergedAnimationStyles[animationState]} className={className}>
139
+ {children}
140
+ </div>
141
+ );
135
142
  };
136
143
 
137
144
  type MountedChild = {
@@ -146,6 +153,7 @@ export const AnimatePresence = ({
146
153
  animationStyles,
147
154
  skipInitialAnimation,
148
155
  mode = "wait",
156
+ className,
149
157
  }: AnimatePresenceProps) => {
150
158
  const [mountedChildren, setMountedChildren] = useState<MountedChild[]>([]);
151
159
  const previousChildrenRef = useRef<React.ReactElement[]>([]);
@@ -250,6 +258,7 @@ export const AnimatePresence = ({
250
258
  isPresent={isPresent}
251
259
  skipInitialAnimation={skipInitialAnimation}
252
260
  onExitComplete={() => removeChild(child.key?.toString() ?? "")}
261
+ className={className}
253
262
  >
254
263
  {child}
255
264
  </PresenceChild>
@@ -140,6 +140,7 @@ export const Avatar = React.memo(
140
140
  backgroundColor,
141
141
  variant = "normal",
142
142
  children,
143
+ className,
143
144
  ...props
144
145
  }: AvatarProps,
145
146
  forwardedRef: React.ForwardedRef<HTMLDivElement>
@@ -153,6 +154,7 @@ export const Avatar = React.memo(
153
154
  size={size}
154
155
  backgroundColor={getBackgroundColor}
155
156
  variant={variant}
157
+ className={className}
156
158
  {...props}
157
159
  >
158
160
  {image ? (
@@ -1,4 +1,5 @@
1
1
  import * as React from "react";
2
+ import { useLabel } from "../hooks/useLabel";
2
3
  import { cx } from "../utils/classNames";
3
4
 
4
5
  interface CheckboxProps
@@ -17,6 +18,7 @@ export const Checkbox = React.memo(
17
18
  { className, style, colorScheme = "secondary", ...props },
18
19
  ref
19
20
  ) {
21
+ const { fieldId: id } = useLabel({ fieldId: props.id });
20
22
  return (
21
23
  <div
22
24
  className={cx(
@@ -47,8 +49,10 @@ export const Checkbox = React.memo(
47
49
  focus:ring-offset-1
48
50
  focus:z-interactable
49
51
  transition-all
50
- `}
52
+ `}
53
+ aria-checked={props.checked}
51
54
  {...props}
55
+ id={id}
52
56
  />
53
57
  <svg
54
58
  className={`
@@ -66,6 +70,7 @@ export const Checkbox = React.memo(
66
70
  viewBox="0 0 16 16"
67
71
  fill="none"
68
72
  xmlns="http://www.w3.org/2000/svg"
73
+ aria-hidden="true"
69
74
  >
70
75
  <path
71
76
  d="M3 6L4.5 7.5L8 4"
@@ -11,6 +11,7 @@ import React, {
11
11
  useRef,
12
12
  useState,
13
13
  } from "react";
14
+ import { useLabel } from "../hooks/useLabel";
14
15
  import { cx } from "../utils/classNames";
15
16
  import {
16
17
  ComboboxItem,
@@ -78,14 +79,13 @@ export interface ComboboxRef {
78
79
  export const Combobox = memo(
79
80
  forwardRef(function Combobox(
80
81
  {
81
- id,
82
82
  loading,
83
83
  placeholder,
84
84
  items,
85
85
  size,
86
86
  style,
87
87
  className,
88
- label,
88
+ label: labelProp,
89
89
  children,
90
90
  hideMenuWhenEmptyValue = false,
91
91
  readOnly = false,
@@ -154,6 +154,9 @@ export const Combobox = memo(
154
154
  const [isFocused, setIsFocused] = useState(false);
155
155
  const listRef = useRef<ListView.VirtualizedList>(null);
156
156
  const [shouldBlur, setShouldBlur] = useState(false);
157
+ const { fieldId: id } = useLabel({
158
+ fieldId: rest.id,
159
+ });
157
160
 
158
161
  // Keep state in sync with items prop
159
162
  useEffect(() => {
@@ -500,11 +503,10 @@ export const Combobox = memo(
500
503
  )}
501
504
  {children}
502
505
  {loading && (
503
- <InputField.Label>
506
+ <InputField.Label htmlFor={id}>
504
507
  <ActivityIndicator />
505
508
  </InputField.Label>
506
509
  )}
507
- <InputField.Label>{label}</InputField.Label>
508
510
  {!readOnly && (
509
511
  <InputField.Button variant="floating" onClick={handleChevronClick}>
510
512
  <DropdownChevronIcon
@@ -1,10 +1,13 @@
1
1
  import { Size } from "@noya-app/noya-geometry";
2
+ import { CheckIcon } from "@noya-app/noya-icons";
2
3
  import React, { ForwardedRef, forwardRef, memo } from "react";
3
4
  import { cx } from "../utils/classNames";
4
5
  import { InternalComboboxItem } from "../utils/combobox";
5
6
  import { fuzzyTokenize } from "../utils/fuzzyScorer";
7
+ import { renderIcon } from "./Icons";
6
8
  import { IVirtualizedList, ListView } from "./ListView";
7
9
  import { Spacer } from "./Spacer";
10
+ import { KeyboardShortcut, styles } from "./internal/Menu";
8
11
 
9
12
  interface ComboboxMenuProps {
10
13
  items: InternalComboboxItem[];
@@ -55,28 +58,40 @@ export const ComboboxMenu = memo(
55
58
  <ListView.Row
56
59
  key={item.id}
57
60
  selected={i === selectedIndex}
58
- onPress={() => onSelectItem(item)}
61
+ disabled={item.disabled}
62
+ onPress={() => !item.disabled && onSelectItem(item)}
59
63
  onHoverChange={(hovered) => {
60
- if (hovered) {
64
+ if (hovered && !item.disabled) {
61
65
  onHoverIndex(i);
62
66
  }
63
67
  }}
64
68
  >
65
- {tokens.map((token, j) => (
66
- <span
67
- key={`${item.id}-token-${j}`}
68
- className={cx(
69
- token.type === "match" ? "font-bold" : "font-normal",
70
- "whitespace-pre"
71
- )}
72
- >
73
- {token.text}
74
- </span>
75
- ))}
76
- {item.icon && (
69
+ <div className="flex flex-1 items-center">
70
+ {item.checked && (
71
+ <div className={styles.itemIndicatorStyle}>
72
+ <CheckIcon />
73
+ </div>
74
+ )}
75
+ {tokens.map((token, j) => (
76
+ <span
77
+ key={`${item.id}-token-${j}`}
78
+ className={cx(
79
+ token.type === "match" ? "font-bold" : "font-normal",
80
+ "whitespace-pre"
81
+ )}
82
+ >
83
+ {token.text}
84
+ </span>
85
+ ))}
86
+ </div>
87
+ {(item.shortcut || item.icon) && (
77
88
  <>
78
89
  <Spacer.Horizontal />
79
- {item.icon}
90
+ {item.shortcut ? (
91
+ <KeyboardShortcut shortcut={item.shortcut} />
92
+ ) : (
93
+ item.icon && renderIcon(item.icon)
94
+ )}
80
95
  </>
81
96
  )}
82
97
  </ListView.Row>
@@ -0,0 +1,69 @@
1
+ import { Dialog } from "@noya-app/noya-designsystem";
2
+ import { AutoSizer } from "@noya-app/react-utils";
3
+ import React, { memo } from "react";
4
+ import {
5
+ ISearchCompletionMenu,
6
+ SearchCompletionMenu,
7
+ } from "./SearchCompletionMenu";
8
+
9
+ export const CommandPalette = memo(function CommandPalette({
10
+ showCommandPalette,
11
+ setShowCommandPalette,
12
+ items,
13
+ onSelect,
14
+ onHover,
15
+ }: {
16
+ showCommandPalette: boolean;
17
+ setShowCommandPalette: (value: boolean) => void;
18
+ } & Pick<
19
+ React.ComponentProps<typeof SearchCompletionMenu>,
20
+ "items" | "onSelect" | "onHover"
21
+ >) {
22
+ const menuRef = React.useRef<ISearchCompletionMenu>(null);
23
+
24
+ const handleClose = React.useCallback(() => {
25
+ setShowCommandPalette(false);
26
+ }, [setShowCommandPalette]);
27
+
28
+ return (
29
+ <Dialog
30
+ style={{
31
+ display: "flex",
32
+ flexDirection: "column",
33
+ maxWidth: "500px",
34
+ minHeight: "100px",
35
+ padding: "1px",
36
+ top: "100px",
37
+ left: "50%",
38
+ transform: "translate(-50%, 0)",
39
+ }}
40
+ showCloseButton={false}
41
+ open={showCommandPalette}
42
+ onOpenChange={(value) => {
43
+ setShowCommandPalette(value);
44
+ }}
45
+ closeOnInteractOutside
46
+ onOpenAutoFocus={() => {
47
+ menuRef.current?.focus();
48
+ setTimeout(() => {
49
+ menuRef.current?.focus();
50
+ }, 0);
51
+ }}
52
+ >
53
+ <div className="flex flex-col flex-1">
54
+ <AutoSizer>
55
+ {(size) => (
56
+ <SearchCompletionMenu
57
+ ref={menuRef}
58
+ width={size.width}
59
+ items={items}
60
+ onSelect={onSelect}
61
+ onHover={onHover}
62
+ onClose={handleClose}
63
+ />
64
+ )}
65
+ </AutoSizer>
66
+ </div>
67
+ </Dialog>
68
+ );
69
+ });
@@ -1,19 +1,13 @@
1
- import { CheckIcon, ChevronRightIcon } from "@noya-app/noya-icons";
2
1
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
2
  import { memoGeneric } from "@noya-app/react-utils";
4
3
  import * as RadixContextMenu from "@radix-ui/react-context-menu";
5
4
  import React, { ReactNode, useCallback, useMemo } from "react";
6
- import { renderIcon } from "./Icons";
7
- import { Spacer } from "./Spacer";
8
5
  import {
9
- CHECKBOX_RIGHT_INSET,
10
- CHECKBOX_WIDTH,
11
- KeyboardShortcut,
12
6
  MenuItem,
13
- SEPARATOR_ITEM,
14
7
  getKeyboardShortcutsForMenuItems,
15
8
  styles,
16
9
  } from "./internal/Menu";
10
+ import { MenuViewport } from "./internal/MenuViewport";
17
11
 
18
12
  export interface MenuItemProps<T extends string> {
19
13
  value?: T;
@@ -27,90 +21,17 @@ export interface MenuItemProps<T extends string> {
27
21
  items?: MenuItem<T>[];
28
22
  }
29
23
 
30
- const ContextMenuItem = memoGeneric(function ContextMenuItem<T extends string>({
31
- value,
32
- children,
33
- onSelect,
34
- checked,
35
- disabled,
36
- indented,
37
- icon,
38
- items,
39
- shortcut,
40
- }: MenuItemProps<T>) {
41
- // The pointer event within the context menu will bubble outside of the
42
- // context menu unless we stop it here.
43
- const handlePointerDown = useCallback(
44
- (event: React.PointerEvent) => event.stopPropagation(),
45
- []
46
- );
47
-
48
- const handleSelectItem = useCallback(() => {
49
- if (!value) return;
50
-
51
- onSelect(value);
52
- }, [onSelect, value]);
53
-
54
- if (checked) {
55
- return (
56
- <RadixContextMenu.CheckboxItem
57
- checked={checked}
58
- disabled={disabled}
59
- onSelect={handleSelectItem}
60
- className={styles.itemStyle({ disabled })}
61
- >
62
- <RadixContextMenu.ItemIndicator className={styles.itemIndicatorStyle}>
63
- <CheckIcon />
64
- </RadixContextMenu.ItemIndicator>
65
- {children}
66
- </RadixContextMenu.CheckboxItem>
67
- );
68
- }
69
-
70
- const element = (
71
- <RadixContextMenu.Item
72
- className={styles.itemStyle({ disabled })}
73
- disabled={disabled}
74
- onSelect={handleSelectItem}
75
- onPointerDown={handlePointerDown}
76
- >
77
- {indented && (
78
- <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
79
- )}
80
- {icon && (
81
- <>
82
- {renderIcon(icon)}
83
- <Spacer.Horizontal size={8} />
84
- </>
85
- )}
86
- {children}
87
- {shortcut && (
88
- <>
89
- <Spacer.Horizontal />
90
- <Spacer.Horizontal size={24} />
91
- <KeyboardShortcut shortcut={shortcut} />
92
- </>
93
- )}
94
- {items && items.length > 0 && (
95
- <>
96
- <Spacer.Horizontal />
97
- <Spacer.Horizontal size={16} />
98
- <ChevronRightIcon />
99
- </>
100
- )}
101
- </RadixContextMenu.Item>
102
- );
103
-
104
- if (items && items.length > 0) {
105
- return (
106
- <ContextMenuRoot isNested items={items} onSelect={onSelect}>
107
- {element}
108
- </ContextMenuRoot>
109
- );
110
- } else {
111
- return element;
112
- }
113
- });
24
+ const Components = {
25
+ Separator: RadixContextMenu.Separator,
26
+ ItemText: React.Fragment,
27
+ Item: RadixContextMenu.Item,
28
+ CheckboxItem: RadixContextMenu.CheckboxItem,
29
+ ItemIndicator: RadixContextMenu.ItemIndicator,
30
+ SubTrigger: RadixContextMenu.SubTrigger,
31
+ SubContent: RadixContextMenu.SubContent,
32
+ Sub: RadixContextMenu.Sub,
33
+ Portal: RadixContextMenu.Portal,
34
+ };
114
35
 
115
36
  /* ----------------------------------------------------------------------------
116
37
  * Root
@@ -120,7 +41,6 @@ export interface MenuProps<T extends string> {
120
41
  children: ReactNode;
121
42
  items: MenuItem<T>[];
122
43
  onSelect: (value: T) => void;
123
- isNested?: boolean;
124
44
  shouldBindKeyboardShortcuts?: boolean;
125
45
  onOpenChange?: (open: boolean) => void;
126
46
  }
@@ -129,20 +49,15 @@ function ContextMenuRoot<T extends string>({
129
49
  items,
130
50
  children,
131
51
  onSelect,
132
- isNested,
133
52
  shouldBindKeyboardShortcuts,
134
53
  onOpenChange,
135
54
  }: MenuProps<T>) {
136
- const hasCheckedItem = items.some(
137
- (item) => item !== SEPARATOR_ITEM && item.checked
138
- );
139
-
140
55
  const keymap = useMemo(
141
56
  () =>
142
- isNested || shouldBindKeyboardShortcuts === false
57
+ shouldBindKeyboardShortcuts === false
143
58
  ? {}
144
59
  : getKeyboardShortcutsForMenuItems(items, onSelect),
145
- [isNested, items, onSelect, shouldBindKeyboardShortcuts]
60
+ [items, onSelect, shouldBindKeyboardShortcuts]
146
61
  );
147
62
 
148
63
  useKeyboardShortcuts(keymap);
@@ -155,46 +70,30 @@ function ContextMenuRoot<T extends string>({
155
70
  event.preventDefault();
156
71
  }, []);
157
72
 
158
- const RootComponent = isNested ? RadixContextMenu.Sub : RadixContextMenu.Root;
159
- const TriggerComponent = isNested
160
- ? RadixContextMenu.SubTrigger
161
- : RadixContextMenu.Trigger;
162
- const ContentComponent: typeof RadixContextMenu.Content = isNested
163
- ? RadixContextMenu.SubContent
164
- : RadixContextMenu.Content;
73
+ const handleSelectItem = useCallback(
74
+ (value: T) => {
75
+ if (!value) return;
76
+
77
+ onSelect(value);
78
+ },
79
+ [onSelect]
80
+ );
165
81
 
166
82
  return (
167
- <RootComponent onOpenChange={onOpenChange}>
168
- <TriggerComponent asChild onPointerDown={onPointerDown}>
83
+ <RadixContextMenu.Root onOpenChange={onOpenChange}>
84
+ <RadixContextMenu.Trigger asChild onPointerDown={onPointerDown}>
169
85
  {children}
170
- </TriggerComponent>
86
+ </RadixContextMenu.Trigger>
171
87
  <RadixContextMenu.Portal>
172
- <ContentComponent className={styles.contentStyle}>
173
- {items.map((item, index) =>
174
- item === SEPARATOR_ITEM ? (
175
- <RadixContextMenu.Separator
176
- key={index}
177
- className={styles.separatorStyle}
178
- />
179
- ) : (
180
- <ContextMenuItem
181
- key={item.value ?? index}
182
- value={item.value}
183
- indented={hasCheckedItem}
184
- checked={item.checked ?? false}
185
- disabled={item.disabled ?? false}
186
- icon={item.icon}
187
- onSelect={onSelect}
188
- items={item.items}
189
- shortcut={item.shortcut}
190
- >
191
- {item.title}
192
- </ContextMenuItem>
193
- )
194
- )}
195
- </ContentComponent>
88
+ <RadixContextMenu.Content className={styles.contentStyle}>
89
+ <MenuViewport
90
+ items={items}
91
+ Components={Components}
92
+ onSelect={handleSelectItem}
93
+ />
94
+ </RadixContextMenu.Content>
196
95
  </RadixContextMenu.Portal>
197
- </RootComponent>
96
+ </RadixContextMenu.Root>
198
97
  );
199
98
  }
200
99