@noya-app/noya-designsystem 0.0.2

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 (73) hide show
  1. package/.turbo/turbo-build.log +17 -0
  2. package/CHANGELOG.md +7 -0
  3. package/README.md +1 -0
  4. package/dist/index.d.mts +1178 -0
  5. package/dist/index.d.ts +1178 -0
  6. package/dist/index.js +15651 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/index.mjs +15684 -0
  9. package/dist/index.mjs.map +1 -0
  10. package/package.json +49 -0
  11. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +41 -0
  12. package/src/__tests__/fuzzyScorer.test.ts +85 -0
  13. package/src/components/ActivityIndicator.tsx +44 -0
  14. package/src/components/Avatar.tsx +64 -0
  15. package/src/components/Button.tsx +209 -0
  16. package/src/components/Chip.tsx +214 -0
  17. package/src/components/ContextMenu.tsx +224 -0
  18. package/src/components/Dialog.tsx +143 -0
  19. package/src/components/Divider.tsx +40 -0
  20. package/src/components/DropdownMenu.tsx +208 -0
  21. package/src/components/FillInputField.tsx +43 -0
  22. package/src/components/FillPreviewBackground.tsx +132 -0
  23. package/src/components/GradientPicker.tsx +88 -0
  24. package/src/components/Grid.tsx +54 -0
  25. package/src/components/GridView.tsx +468 -0
  26. package/src/components/IconButton.tsx +46 -0
  27. package/src/components/InputField.tsx +568 -0
  28. package/src/components/InputFieldWithCompletions.tsx +477 -0
  29. package/src/components/Label.tsx +62 -0
  30. package/src/components/LabeledElementView.tsx +185 -0
  31. package/src/components/ListView.tsx +950 -0
  32. package/src/components/Popover.tsx +113 -0
  33. package/src/components/Progress.tsx +57 -0
  34. package/src/components/RadioGroup.tsx +163 -0
  35. package/src/components/ScrollArea.tsx +70 -0
  36. package/src/components/Select.tsx +152 -0
  37. package/src/components/Slider.tsx +82 -0
  38. package/src/components/Sortable.tsx +296 -0
  39. package/src/components/Spacer.tsx +29 -0
  40. package/src/components/Stack.tsx +142 -0
  41. package/src/components/Switch.tsx +67 -0
  42. package/src/components/Text.tsx +164 -0
  43. package/src/components/Toast.tsx +86 -0
  44. package/src/components/Tooltip.tsx +43 -0
  45. package/src/components/TreeView.tsx +85 -0
  46. package/src/components/internal/Menu.tsx +222 -0
  47. package/src/components/internal/TextInput.tsx +296 -0
  48. package/src/components/internal/__tests__/TextInput.test.tsx +144 -0
  49. package/src/contexts/DesignSystemConfiguration.tsx +57 -0
  50. package/src/contexts/DialogContext.tsx +190 -0
  51. package/src/contexts/GlobalInputBlurContext.tsx +61 -0
  52. package/src/contexts/ImageDataContext.tsx +44 -0
  53. package/src/hooks/__tests__/mergeEventHandlers.test.ts +47 -0
  54. package/src/hooks/mergeEventHandlers.ts +55 -0
  55. package/src/hooks/useHover.ts +173 -0
  56. package/src/hooks/useObjectURL.ts +22 -0
  57. package/src/hooks/usePlatform.ts +13 -0
  58. package/src/index.tsx +77 -0
  59. package/src/mediaQuery.ts +13 -0
  60. package/src/theme/dark.ts +37 -0
  61. package/src/theme/index.ts +17 -0
  62. package/src/theme/light.ts +178 -0
  63. package/src/utils/breakpoints.ts +45 -0
  64. package/src/utils/completions.ts +21 -0
  65. package/src/utils/createSectionedMenu.ts +38 -0
  66. package/src/utils/fuzzyScorer.ts +105 -0
  67. package/src/utils/getGradientBackground.tsx +33 -0
  68. package/src/utils/handleNudge.ts +30 -0
  69. package/src/utils/mouseEvent.ts +7 -0
  70. package/src/utils/sketchColor.ts +34 -0
  71. package/src/utils/sketchPattern.ts +29 -0
  72. package/src/utils/withSeparatorElements.ts +31 -0
  73. package/tsconfig.json +3 -0
@@ -0,0 +1,224 @@
1
+ import { CheckIcon, ChevronRightIcon } from '@noya-app/noya-icons';
2
+ import { useKeyboardShortcuts } from '@noya-app/noya-keymap';
3
+ import * as RadixContextMenu from '@radix-ui/react-context-menu';
4
+ import React, {
5
+ ReactElement,
6
+ ReactNode,
7
+ memo,
8
+ useCallback,
9
+ useMemo,
10
+ } from 'react';
11
+ import styled from 'styled-components';
12
+ import { Spacer } from './Spacer';
13
+ import {
14
+ CHECKBOX_RIGHT_INSET,
15
+ CHECKBOX_WIDTH,
16
+ KeyboardShortcut,
17
+ MenuItem,
18
+ SEPARATOR_ITEM,
19
+ getKeyboardShortcutsForMenuItems,
20
+ styles,
21
+ } from './internal/Menu';
22
+
23
+ /* ----------------------------------------------------------------------------
24
+ * Separator
25
+ * ------------------------------------------------------------------------- */
26
+
27
+ const SeparatorElement = styled(RadixContextMenu.Separator)(
28
+ styles.separatorStyle,
29
+ );
30
+
31
+ /* ----------------------------------------------------------------------------
32
+ * Item
33
+ * ------------------------------------------------------------------------- */
34
+
35
+ const ItemElement = styled(RadixContextMenu.Item)(styles.itemStyle);
36
+
37
+ const CheckboxItemElement = styled(RadixContextMenu.CheckboxItem)(
38
+ styles.itemStyle,
39
+ );
40
+
41
+ const StyledItemIndicator = styled(RadixContextMenu.ItemIndicator)(
42
+ styles.itemIndicatorStyle,
43
+ );
44
+
45
+ export interface MenuItemProps<T extends string> {
46
+ value?: T;
47
+ children: ReactNode;
48
+ onSelect: (value: T) => void;
49
+ checked: boolean;
50
+ disabled: boolean;
51
+ indented: boolean;
52
+ shortcut?: string;
53
+ icon?: ReactElement;
54
+ items?: MenuItem<T>[];
55
+ }
56
+
57
+ const ContextMenuItem = memo(function ContextMenuItem<T extends string>({
58
+ value,
59
+ children,
60
+ onSelect,
61
+ checked,
62
+ disabled,
63
+ indented,
64
+ icon,
65
+ items,
66
+ shortcut,
67
+ }: MenuItemProps<T>) {
68
+ // The pointer event within the context menu will bubble outside of the
69
+ // context menu unless we stop it here.
70
+ const handlePointerDown = useCallback(
71
+ (event: React.PointerEvent) => event.stopPropagation(),
72
+ [],
73
+ );
74
+
75
+ const handleSelectItem = useCallback(() => {
76
+ if (!value) return;
77
+
78
+ onSelect(value);
79
+ }, [onSelect, value]);
80
+
81
+ if (checked) {
82
+ return (
83
+ <CheckboxItemElement
84
+ checked={checked}
85
+ disabled={disabled}
86
+ onSelect={handleSelectItem}
87
+ >
88
+ <StyledItemIndicator>
89
+ <CheckIcon />
90
+ </StyledItemIndicator>
91
+ {children}
92
+ </CheckboxItemElement>
93
+ );
94
+ }
95
+
96
+ const element = (
97
+ <ItemElement
98
+ disabled={disabled}
99
+ onSelect={handleSelectItem}
100
+ onPointerDown={handlePointerDown}
101
+ >
102
+ {indented && (
103
+ <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
104
+ )}
105
+ {icon && (
106
+ <>
107
+ {icon}
108
+ <Spacer.Horizontal size={8} />
109
+ </>
110
+ )}
111
+ {children}
112
+ {shortcut && (
113
+ <>
114
+ <Spacer.Horizontal />
115
+ <Spacer.Horizontal size={24} />
116
+ <KeyboardShortcut shortcut={shortcut} />
117
+ </>
118
+ )}
119
+ {items && items.length > 0 && (
120
+ <>
121
+ <Spacer.Horizontal />
122
+ <Spacer.Horizontal size={16} />
123
+ <ChevronRightIcon />
124
+ </>
125
+ )}
126
+ </ItemElement>
127
+ );
128
+
129
+ if (items && items.length > 0) {
130
+ return (
131
+ <ContextMenuRoot isNested items={items} onSelect={onSelect}>
132
+ {element}
133
+ </ContextMenuRoot>
134
+ );
135
+ } else {
136
+ return element;
137
+ }
138
+ });
139
+
140
+ /* ----------------------------------------------------------------------------
141
+ * Root
142
+ * ------------------------------------------------------------------------- */
143
+
144
+ const Content = styled(RadixContextMenu.Content)(styles.contentStyle);
145
+ const SubContent = styled(RadixContextMenu.SubContent)(styles.contentStyle);
146
+
147
+ export interface MenuProps<T extends string> {
148
+ children: ReactNode;
149
+ items: MenuItem<T>[];
150
+ onSelect: (value: T) => void;
151
+ isNested?: boolean;
152
+ shouldBindKeyboardShortcuts?: boolean;
153
+ onOpenChange?: (open: boolean) => void;
154
+ }
155
+
156
+ function ContextMenuRoot<T extends string>({
157
+ items,
158
+ children,
159
+ onSelect,
160
+ isNested,
161
+ shouldBindKeyboardShortcuts,
162
+ onOpenChange,
163
+ }: MenuProps<T>) {
164
+ const hasCheckedItem = items.some(
165
+ (item) => item !== SEPARATOR_ITEM && item.checked,
166
+ );
167
+
168
+ const keymap = useMemo(
169
+ () =>
170
+ isNested || shouldBindKeyboardShortcuts === false
171
+ ? {}
172
+ : getKeyboardShortcutsForMenuItems(items, onSelect),
173
+ [isNested, items, onSelect, shouldBindKeyboardShortcuts],
174
+ );
175
+
176
+ useKeyboardShortcuts(keymap);
177
+
178
+ // We call preventDefault both to:
179
+ // - Disable radix-ui's long-press-to-open behavior
180
+ // https://github.com/radix-ui/primitives/issues/748#issuecomment-869502837
181
+ // - Mark the event as handled, so our ListView root doesn't handle it (a hack)
182
+ const onPointerDown = useCallback((event: React.PointerEvent) => {
183
+ event.preventDefault();
184
+ }, []);
185
+
186
+ const RootComponent = isNested ? RadixContextMenu.Sub : RadixContextMenu.Root;
187
+ const TriggerComponent = isNested
188
+ ? RadixContextMenu.SubTrigger
189
+ : RadixContextMenu.Trigger;
190
+ const ContentComponent: typeof Content = isNested ? SubContent : Content;
191
+
192
+ return (
193
+ <RootComponent onOpenChange={onOpenChange}>
194
+ <TriggerComponent asChild onPointerDown={onPointerDown}>
195
+ {children}
196
+ </TriggerComponent>
197
+ <RadixContextMenu.Portal>
198
+ <ContentComponent>
199
+ {items.map((item, index) =>
200
+ item === SEPARATOR_ITEM ? (
201
+ <SeparatorElement key={index} />
202
+ ) : (
203
+ <ContextMenuItem
204
+ key={item.value ?? index}
205
+ value={item.value}
206
+ indented={hasCheckedItem}
207
+ checked={item.checked ?? false}
208
+ disabled={item.disabled ?? false}
209
+ icon={item.icon}
210
+ onSelect={onSelect}
211
+ items={item.items}
212
+ shortcut={item.shortcut}
213
+ >
214
+ {item.title}
215
+ </ContextMenuItem>
216
+ ),
217
+ )}
218
+ </ContentComponent>
219
+ </RadixContextMenu.Portal>
220
+ </RootComponent>
221
+ );
222
+ }
223
+
224
+ export const ContextMenu = memo(ContextMenuRoot);
@@ -0,0 +1,143 @@
1
+ import * as DialogPrimitive from '@radix-ui/react-dialog';
2
+ import React, {
3
+ ComponentProps,
4
+ ForwardedRef,
5
+ forwardRef,
6
+ ReactNode,
7
+ useImperativeHandle,
8
+ useRef,
9
+ } from 'react';
10
+ import styled from 'styled-components';
11
+ import { IconButton } from './IconButton';
12
+ import { Spacer } from './Spacer';
13
+
14
+ const StyledOverlay = styled(DialogPrimitive.Overlay)({
15
+ backgroundColor: 'rgba(0,0,0,0.5)',
16
+ position: 'fixed',
17
+ inset: 0,
18
+ });
19
+
20
+ const StyledContent = styled(DialogPrimitive.Content)(({ theme }) => ({
21
+ boxShadow:
22
+ 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
23
+ position: 'fixed',
24
+ top: '50%',
25
+ left: '50%',
26
+ transform: 'translate(-50%, -50%)',
27
+ width: '90vw',
28
+ maxWidth: '450px',
29
+ maxHeight: '85vh',
30
+ padding: theme.sizes.dialog.padding,
31
+ borderRadius: 2,
32
+ ...theme.textStyles.small,
33
+ backgroundColor: theme.colors.popover.background,
34
+ overflowY: 'auto',
35
+ color: theme.colors.textMuted,
36
+ '&:focus': { outline: 'none' },
37
+ pointerEvents: 'all',
38
+ zIndex: 1000,
39
+ }));
40
+
41
+ const StyledTitle = styled(DialogPrimitive.Title)(({ theme }) => ({
42
+ margin: 0,
43
+ ...theme.textStyles.body,
44
+ color: theme.colors.text,
45
+ }));
46
+
47
+ const StyledDescription = styled(DialogPrimitive.Description)(({ theme }) => ({
48
+ margin: 0,
49
+ ...theme.textStyles.small,
50
+ color: theme.colors.textMuted,
51
+ }));
52
+
53
+ export const CloseButtonContainer = styled.div(({ theme }) => ({
54
+ position: 'absolute',
55
+ top: theme.sizes.dialog.padding,
56
+ right: theme.sizes.dialog.padding,
57
+ zIndex: 1,
58
+ backgroundColor: theme.colors.popover.background,
59
+ padding: '4px 6px',
60
+ borderRadius: '2px',
61
+ // boxShadow: `0 0 2px rgba(0, 0, 0, 0.2)`,
62
+ border: `1px solid rgba(128,128,128,0.2)`,
63
+ }));
64
+
65
+ export interface IDialog {
66
+ containsElement: (element: HTMLElement) => boolean;
67
+ }
68
+
69
+ interface Props {
70
+ title?: ReactNode;
71
+ description?: ReactNode;
72
+ children?: ReactNode;
73
+ style?: ComponentProps<typeof StyledContent>['style'];
74
+ open: ComponentProps<typeof DialogPrimitive.Root>['open'];
75
+ onOpenChange?: ComponentProps<typeof DialogPrimitive.Root>['onOpenChange'];
76
+ onOpenAutoFocus?: ComponentProps<
77
+ typeof DialogPrimitive.Content
78
+ >['onOpenAutoFocus'];
79
+ closeOnInteractOutside?: boolean;
80
+ }
81
+
82
+ export const Dialog = forwardRef(function Dialog(
83
+ {
84
+ children,
85
+ title,
86
+ description,
87
+ open,
88
+ style,
89
+ onOpenChange,
90
+ onOpenAutoFocus,
91
+ closeOnInteractOutside = true,
92
+ }: Props,
93
+ forwardedRef: ForwardedRef<IDialog>,
94
+ ) {
95
+ const contentRef = useRef<HTMLDivElement>(null);
96
+
97
+ useImperativeHandle(forwardedRef, () => ({
98
+ containsElement(element) {
99
+ if (!contentRef.current) return false;
100
+
101
+ return contentRef.current.contains(element);
102
+ },
103
+ }));
104
+
105
+ return (
106
+ <DialogPrimitive.Root open={open} onOpenChange={onOpenChange}>
107
+ <StyledOverlay />
108
+ <StyledContent
109
+ ref={contentRef}
110
+ onOpenAutoFocus={onOpenAutoFocus}
111
+ style={style}
112
+ {...(closeOnInteractOutside === false && {
113
+ onPointerDownOutside: (event) => {
114
+ event.preventDefault();
115
+ },
116
+ onInteractOutside: (event) => {
117
+ event.preventDefault();
118
+ },
119
+ })}
120
+ >
121
+ <CloseButtonContainer>
122
+ <DialogPrimitive.Close asChild>
123
+ <IconButton iconName="Cross1Icon" />
124
+ </DialogPrimitive.Close>
125
+ </CloseButtonContainer>
126
+ {title && (
127
+ <>
128
+ <StyledTitle>{title}</StyledTitle>
129
+ <Spacer.Vertical size={description ? 10 : 20} />
130
+ </>
131
+ )}
132
+ {description && (
133
+ <>
134
+ <Spacer.Vertical size={10} />
135
+ <StyledDescription>{description}</StyledDescription>
136
+ <Spacer.Vertical size={20} />
137
+ </>
138
+ )}
139
+ {children}
140
+ </StyledContent>
141
+ </DialogPrimitive.Root>
142
+ );
143
+ });
@@ -0,0 +1,40 @@
1
+ import React, { memo } from 'react';
2
+ import styled from 'styled-components';
3
+
4
+ /* ----------------------------------------------------------------------------
5
+ * Divider
6
+ * ------------------------------------------------------------------------- */
7
+
8
+ type DividerVariant = 'normal' | 'strong' | 'subtle';
9
+
10
+ const DividerContainer = styled.div<
11
+ DividerProps & {
12
+ orientation: 'horizontal' | 'vertical';
13
+ }
14
+ >(({ theme, variant = 'normal', orientation, overflow = 0 }) => ({
15
+ ...(orientation === 'horizontal'
16
+ ? { height: '1px', minHeight: '1px', margin: `0px -${overflow}px` }
17
+ : { width: '1px', minWidth: '1px', margin: `-${overflow}px 0px` }),
18
+ background:
19
+ variant === 'strong'
20
+ ? theme.colors.dividerStrong
21
+ : variant === 'subtle'
22
+ ? theme.colors.dividerSubtle
23
+ : theme.colors.divider,
24
+ alignSelf: 'stretch',
25
+ }));
26
+
27
+ interface DividerProps {
28
+ variant?: DividerVariant;
29
+ overflow?: number;
30
+ }
31
+
32
+ export const Divider = memo(function Divider(props: DividerProps) {
33
+ return <DividerContainer orientation="horizontal" {...props} />;
34
+ });
35
+
36
+ export const DividerVertical = memo(function DividerVertical(
37
+ props: DividerProps,
38
+ ) {
39
+ return <DividerContainer orientation="vertical" {...props} />;
40
+ });
@@ -0,0 +1,208 @@
1
+ import { CheckIcon, ChevronRightIcon } from '@noya-app/noya-icons';
2
+ import { useKeyboardShortcuts } from '@noya-app/noya-keymap';
3
+ import * as RadixDropdownMenu from '@radix-ui/react-dropdown-menu';
4
+ import React, {
5
+ ForwardedRef,
6
+ SyntheticEvent,
7
+ forwardRef,
8
+ memo,
9
+ useCallback,
10
+ useMemo,
11
+ } from 'react';
12
+ import styled from 'styled-components';
13
+ import { MenuItemProps, MenuProps } from './ContextMenu';
14
+ import { Spacer } from './Spacer';
15
+ import {
16
+ CHECKBOX_RIGHT_INSET,
17
+ CHECKBOX_WIDTH,
18
+ KeyboardShortcut,
19
+ SEPARATOR_ITEM,
20
+ getKeyboardShortcutsForMenuItems,
21
+ styles,
22
+ } from './internal/Menu';
23
+
24
+ /* ----------------------------------------------------------------------------
25
+ * Separator
26
+ * ------------------------------------------------------------------------- */
27
+
28
+ const SeparatorElement = styled(RadixDropdownMenu.Separator)(
29
+ styles.separatorStyle,
30
+ );
31
+
32
+ /* ----------------------------------------------------------------------------
33
+ * Item
34
+ * ------------------------------------------------------------------------- */
35
+
36
+ const ItemElement = styled(RadixDropdownMenu.Item)(styles.itemStyle);
37
+
38
+ const CheckboxItemElement = styled(RadixDropdownMenu.CheckboxItem)(
39
+ styles.itemStyle,
40
+ );
41
+
42
+ const StyledItemIndicator = styled(RadixDropdownMenu.ItemIndicator)(
43
+ styles.itemIndicatorStyle,
44
+ );
45
+
46
+ const DropdownMenuItem = memo(function ContextMenuItem<T extends string>({
47
+ value,
48
+ children,
49
+ onSelect,
50
+ checked,
51
+ disabled,
52
+ indented,
53
+ icon,
54
+ items,
55
+ shortcut,
56
+ }: MenuItemProps<T>) {
57
+ const handleSelectItem = useCallback(() => {
58
+ if (!value) return;
59
+
60
+ onSelect(value);
61
+ }, [onSelect, value]);
62
+
63
+ if (checked) {
64
+ return (
65
+ <CheckboxItemElement
66
+ checked={checked}
67
+ disabled={disabled}
68
+ onSelect={handleSelectItem}
69
+ >
70
+ <StyledItemIndicator>
71
+ <CheckIcon />
72
+ </StyledItemIndicator>
73
+ {icon && (
74
+ <>
75
+ {icon}
76
+ <Spacer.Horizontal size={8} />
77
+ </>
78
+ )}
79
+ {children}
80
+ </CheckboxItemElement>
81
+ );
82
+ }
83
+
84
+ const element = (
85
+ <ItemElement disabled={disabled} onSelect={handleSelectItem}>
86
+ {indented && (
87
+ <Spacer.Horizontal size={CHECKBOX_WIDTH - CHECKBOX_RIGHT_INSET} />
88
+ )}
89
+ {icon && (
90
+ <>
91
+ {icon}
92
+ <Spacer.Horizontal size={8} />
93
+ </>
94
+ )}
95
+ {children}
96
+ {shortcut && (
97
+ <>
98
+ <Spacer.Horizontal />
99
+ <Spacer.Horizontal size={24} />
100
+ <KeyboardShortcut shortcut={shortcut} />
101
+ </>
102
+ )}
103
+ {items && items.length > 0 && (
104
+ <>
105
+ <Spacer.Horizontal />
106
+ <Spacer.Horizontal size={16} />
107
+ <ChevronRightIcon />
108
+ </>
109
+ )}
110
+ </ItemElement>
111
+ );
112
+
113
+ if (items && items.length > 0) {
114
+ return (
115
+ <DropdownMenuRoot isNested items={items} onSelect={onSelect}>
116
+ {element}
117
+ </DropdownMenuRoot>
118
+ );
119
+ } else {
120
+ return element;
121
+ }
122
+ });
123
+
124
+ /* ----------------------------------------------------------------------------
125
+ * Root
126
+ * ------------------------------------------------------------------------- */
127
+
128
+ const Content = styled(RadixDropdownMenu.Content)(styles.contentStyle);
129
+ const SubContent = styled(RadixDropdownMenu.SubContent)(styles.contentStyle);
130
+
131
+ const DropdownMenuRoot = forwardRef(function DropdownMenuRoot<T extends string>(
132
+ {
133
+ items,
134
+ children,
135
+ onSelect,
136
+ isNested,
137
+ shouldBindKeyboardShortcuts,
138
+ onOpenChange,
139
+ open,
140
+ onCloseAutoFocus,
141
+ }: MenuProps<T> & {
142
+ open?: boolean;
143
+ onCloseAutoFocus?: React.EventHandler<SyntheticEvent<unknown>>;
144
+ },
145
+ forwardedRef: ForwardedRef<HTMLElement>,
146
+ ) {
147
+ const hasCheckedItem = items.some(
148
+ (item) => item !== SEPARATOR_ITEM && item.checked,
149
+ );
150
+
151
+ const keymap = useMemo(
152
+ () =>
153
+ isNested || shouldBindKeyboardShortcuts === false
154
+ ? {}
155
+ : getKeyboardShortcutsForMenuItems(items, onSelect),
156
+ [isNested, items, onSelect, shouldBindKeyboardShortcuts],
157
+ );
158
+
159
+ useKeyboardShortcuts(keymap);
160
+
161
+ const RootComponent = isNested
162
+ ? RadixDropdownMenu.Sub
163
+ : RadixDropdownMenu.Root;
164
+ const TriggerComponent = isNested
165
+ ? RadixDropdownMenu.SubTrigger
166
+ : RadixDropdownMenu.Trigger;
167
+ const ContentComponent = isNested ? SubContent : Content;
168
+
169
+ const contentStyle = useMemo(() => ({ zIndex: 1000 }), []);
170
+
171
+ return (
172
+ <RootComponent onOpenChange={onOpenChange} open={open}>
173
+ <TriggerComponent ref={forwardedRef as ForwardedRef<any>} asChild>
174
+ {children}
175
+ </TriggerComponent>
176
+ <RadixDropdownMenu.Portal>
177
+ <ContentComponent
178
+ sideOffset={4}
179
+ style={contentStyle}
180
+ collisionPadding={8}
181
+ {...({ onCloseAutoFocus } as any)}
182
+ >
183
+ {items.map((item, index) =>
184
+ item === SEPARATOR_ITEM ? (
185
+ <SeparatorElement key={index} />
186
+ ) : (
187
+ <DropdownMenuItem
188
+ key={item.value ?? index}
189
+ value={item.value}
190
+ indented={hasCheckedItem}
191
+ checked={item.checked ?? false}
192
+ disabled={item.disabled ?? false}
193
+ icon={item.icon}
194
+ onSelect={onSelect}
195
+ items={item.items}
196
+ shortcut={item.shortcut}
197
+ >
198
+ {item.title}
199
+ </DropdownMenuItem>
200
+ ),
201
+ )}
202
+ </ContentComponent>
203
+ </RadixDropdownMenu.Portal>
204
+ </RootComponent>
205
+ );
206
+ });
207
+
208
+ export const DropdownMenu = memo(DropdownMenuRoot);
@@ -0,0 +1,43 @@
1
+ import { Sketch } from '@noya-app/noya-file-format';
2
+ import React, { CSSProperties, ForwardedRef, forwardRef, memo } from 'react';
3
+ import styled from 'styled-components';
4
+ import { SketchPattern } from '../utils/sketchPattern';
5
+ import { FillPreviewBackground } from './FillPreviewBackground';
6
+
7
+ const Container = styled.button<{ flex?: CSSProperties['flex'] }>(
8
+ ({ theme, flex }) => ({
9
+ outline: 'none',
10
+ padding: 0,
11
+ width: '50px',
12
+ height: '27px',
13
+ borderRadius: '4px',
14
+ overflow: 'hidden',
15
+ border: 'none',
16
+ boxShadow: `0 0 0 1px ${theme.colors.divider} inset`,
17
+ background: 'transparent',
18
+ '&:focus': {
19
+ boxShadow: `0 0 0 1px ${theme.colors.sidebar.background}, 0 0 0 3px ${theme.colors.primary}`,
20
+ },
21
+ position: 'relative',
22
+ flex,
23
+ }),
24
+ );
25
+
26
+ interface Props {
27
+ id?: string;
28
+ value?: Sketch.Color | Sketch.Gradient | SketchPattern;
29
+ flex?: CSSProperties['flex'];
30
+ }
31
+
32
+ export const FillInputField = memo(
33
+ forwardRef(function FillInputField(
34
+ { id, value, ...rest }: Props,
35
+ ref: ForwardedRef<HTMLButtonElement>,
36
+ ) {
37
+ return (
38
+ <Container ref={ref} id={id} {...rest}>
39
+ <FillPreviewBackground value={value} />
40
+ </Container>
41
+ );
42
+ }),
43
+ );