@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.
- package/.turbo/turbo-build.log +17 -0
- package/CHANGELOG.md +7 -0
- package/README.md +1 -0
- package/dist/index.d.mts +1178 -0
- package/dist/index.d.ts +1178 -0
- package/dist/index.js +15651 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +15684 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +49 -0
- package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +41 -0
- package/src/__tests__/fuzzyScorer.test.ts +85 -0
- package/src/components/ActivityIndicator.tsx +44 -0
- package/src/components/Avatar.tsx +64 -0
- package/src/components/Button.tsx +209 -0
- package/src/components/Chip.tsx +214 -0
- package/src/components/ContextMenu.tsx +224 -0
- package/src/components/Dialog.tsx +143 -0
- package/src/components/Divider.tsx +40 -0
- package/src/components/DropdownMenu.tsx +208 -0
- package/src/components/FillInputField.tsx +43 -0
- package/src/components/FillPreviewBackground.tsx +132 -0
- package/src/components/GradientPicker.tsx +88 -0
- package/src/components/Grid.tsx +54 -0
- package/src/components/GridView.tsx +468 -0
- package/src/components/IconButton.tsx +46 -0
- package/src/components/InputField.tsx +568 -0
- package/src/components/InputFieldWithCompletions.tsx +477 -0
- package/src/components/Label.tsx +62 -0
- package/src/components/LabeledElementView.tsx +185 -0
- package/src/components/ListView.tsx +950 -0
- package/src/components/Popover.tsx +113 -0
- package/src/components/Progress.tsx +57 -0
- package/src/components/RadioGroup.tsx +163 -0
- package/src/components/ScrollArea.tsx +70 -0
- package/src/components/Select.tsx +152 -0
- package/src/components/Slider.tsx +82 -0
- package/src/components/Sortable.tsx +296 -0
- package/src/components/Spacer.tsx +29 -0
- package/src/components/Stack.tsx +142 -0
- package/src/components/Switch.tsx +67 -0
- package/src/components/Text.tsx +164 -0
- package/src/components/Toast.tsx +86 -0
- package/src/components/Tooltip.tsx +43 -0
- package/src/components/TreeView.tsx +85 -0
- package/src/components/internal/Menu.tsx +222 -0
- package/src/components/internal/TextInput.tsx +296 -0
- package/src/components/internal/__tests__/TextInput.test.tsx +144 -0
- package/src/contexts/DesignSystemConfiguration.tsx +57 -0
- package/src/contexts/DialogContext.tsx +190 -0
- package/src/contexts/GlobalInputBlurContext.tsx +61 -0
- package/src/contexts/ImageDataContext.tsx +44 -0
- package/src/hooks/__tests__/mergeEventHandlers.test.ts +47 -0
- package/src/hooks/mergeEventHandlers.ts +55 -0
- package/src/hooks/useHover.ts +173 -0
- package/src/hooks/useObjectURL.ts +22 -0
- package/src/hooks/usePlatform.ts +13 -0
- package/src/index.tsx +77 -0
- package/src/mediaQuery.ts +13 -0
- package/src/theme/dark.ts +37 -0
- package/src/theme/index.ts +17 -0
- package/src/theme/light.ts +178 -0
- package/src/utils/breakpoints.ts +45 -0
- package/src/utils/completions.ts +21 -0
- package/src/utils/createSectionedMenu.ts +38 -0
- package/src/utils/fuzzyScorer.ts +105 -0
- package/src/utils/getGradientBackground.tsx +33 -0
- package/src/utils/handleNudge.ts +30 -0
- package/src/utils/mouseEvent.ts +7 -0
- package/src/utils/sketchColor.ts +34 -0
- package/src/utils/sketchPattern.ts +29 -0
- package/src/utils/withSeparatorElements.ts +31 -0
- 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
|
+
);
|