@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,113 @@
|
|
|
1
|
+
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
2
|
+
import React, { ComponentProps } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
import { IconButton } from './IconButton';
|
|
5
|
+
|
|
6
|
+
type PopoverVariant = 'normal' | 'large';
|
|
7
|
+
|
|
8
|
+
const ContentElement = styled(PopoverPrimitive.Content)<{
|
|
9
|
+
variant?: PopoverVariant;
|
|
10
|
+
}>(({ theme, variant }) => ({
|
|
11
|
+
borderRadius: 4,
|
|
12
|
+
fontSize: 14,
|
|
13
|
+
backgroundColor: theme.colors.popover.background,
|
|
14
|
+
boxShadow: '0 2px 4px rgba(0,0,0,0.2), 0 0 12px rgba(0,0,0,0.1)',
|
|
15
|
+
maxHeight: '600px',
|
|
16
|
+
overflowY: 'auto',
|
|
17
|
+
color: theme.colors.textMuted,
|
|
18
|
+
...(variant === 'large' && { width: '680px' }),
|
|
19
|
+
...(variant === 'normal' && { width: '240px' }),
|
|
20
|
+
zIndex: 1000,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const ArrowElement = styled(PopoverPrimitive.Arrow)(({ theme }) => ({
|
|
24
|
+
fill: theme.colors.popover.background,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
const PopoverClose = styled(PopoverPrimitive.Close)(({ theme }) => ({
|
|
28
|
+
all: 'unset',
|
|
29
|
+
fontFamily: 'inherit',
|
|
30
|
+
borderRadius: '100%',
|
|
31
|
+
height: 25,
|
|
32
|
+
width: 25,
|
|
33
|
+
display: 'inline-flex',
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
justifyContent: 'center',
|
|
36
|
+
position: 'absolute',
|
|
37
|
+
top: 5,
|
|
38
|
+
right: 5,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
interface Props
|
|
42
|
+
extends Pick<
|
|
43
|
+
ComponentProps<typeof PopoverPrimitive['Content']>,
|
|
44
|
+
| 'onOpenAutoFocus'
|
|
45
|
+
| 'onCloseAutoFocus'
|
|
46
|
+
| 'onPointerDownOutside'
|
|
47
|
+
| 'onInteractOutside'
|
|
48
|
+
| 'onFocusOutside'
|
|
49
|
+
| 'side'
|
|
50
|
+
> {
|
|
51
|
+
children: React.ReactNode;
|
|
52
|
+
trigger: React.ReactNode;
|
|
53
|
+
variant?: PopoverVariant;
|
|
54
|
+
closable?: boolean;
|
|
55
|
+
open?: boolean;
|
|
56
|
+
onOpenChange?: (open: boolean) => void;
|
|
57
|
+
sideOffset?: number;
|
|
58
|
+
onClickClose?: () => void;
|
|
59
|
+
showArrow?: boolean;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function Popover({
|
|
63
|
+
children,
|
|
64
|
+
trigger,
|
|
65
|
+
variant,
|
|
66
|
+
closable,
|
|
67
|
+
open,
|
|
68
|
+
side,
|
|
69
|
+
sideOffset = 4,
|
|
70
|
+
showArrow = true,
|
|
71
|
+
onOpenChange,
|
|
72
|
+
onOpenAutoFocus,
|
|
73
|
+
onCloseAutoFocus,
|
|
74
|
+
onPointerDownOutside,
|
|
75
|
+
onInteractOutside,
|
|
76
|
+
onFocusOutside,
|
|
77
|
+
onClickClose,
|
|
78
|
+
}: Props) {
|
|
79
|
+
return (
|
|
80
|
+
<PopoverPrimitive.Root open={open} onOpenChange={onOpenChange}>
|
|
81
|
+
<PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
|
|
82
|
+
<PopoverPrimitive.Portal>
|
|
83
|
+
<ContentElement
|
|
84
|
+
variant={variant}
|
|
85
|
+
side={side}
|
|
86
|
+
align="center"
|
|
87
|
+
sideOffset={sideOffset}
|
|
88
|
+
onOpenAutoFocus={onOpenAutoFocus}
|
|
89
|
+
onCloseAutoFocus={onCloseAutoFocus}
|
|
90
|
+
onInteractOutside={onInteractOutside}
|
|
91
|
+
onFocusOutside={onFocusOutside}
|
|
92
|
+
onPointerDownOutside={onPointerDownOutside}
|
|
93
|
+
collisionPadding={8}
|
|
94
|
+
// Don't propagate pointer down events to the canvas
|
|
95
|
+
onPointerDown={(event) => {
|
|
96
|
+
event.stopPropagation();
|
|
97
|
+
}}
|
|
98
|
+
onWheel={(event) => {
|
|
99
|
+
event.stopPropagation();
|
|
100
|
+
}}
|
|
101
|
+
>
|
|
102
|
+
{children}
|
|
103
|
+
{showArrow && <ArrowElement />}
|
|
104
|
+
{closable && (
|
|
105
|
+
<PopoverClose>
|
|
106
|
+
<IconButton iconName="Cross2Icon" onClick={onClickClose} />
|
|
107
|
+
</PopoverClose>
|
|
108
|
+
)}
|
|
109
|
+
</ContentElement>
|
|
110
|
+
</PopoverPrimitive.Portal>
|
|
111
|
+
</PopoverPrimitive.Root>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { clamp } from '@noya-app/noya-utils';
|
|
2
|
+
import * as ProgressPrimitive from '@radix-ui/react-progress';
|
|
3
|
+
import React, { CSSProperties } from 'react';
|
|
4
|
+
import styled from 'styled-components';
|
|
5
|
+
|
|
6
|
+
type ProgressVariant = 'normal' | 'warning' | 'primary' | 'secondary';
|
|
7
|
+
|
|
8
|
+
export function Progress({
|
|
9
|
+
value,
|
|
10
|
+
width,
|
|
11
|
+
height = 5,
|
|
12
|
+
flex,
|
|
13
|
+
variant = 'normal',
|
|
14
|
+
}: {
|
|
15
|
+
value: number;
|
|
16
|
+
height?: CSSProperties['height'];
|
|
17
|
+
width?: CSSProperties['width'];
|
|
18
|
+
flex?: CSSProperties['flex'];
|
|
19
|
+
variant?: ProgressVariant;
|
|
20
|
+
}) {
|
|
21
|
+
const clampedValue = clamp(value, 0, 100);
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<ProgressRoot value={clampedValue} style={{ width, flex, height }}>
|
|
25
|
+
<ProgressIndicator
|
|
26
|
+
variant={variant}
|
|
27
|
+
style={{ transform: `translateX(-${100 - clampedValue}%)` }}
|
|
28
|
+
/>
|
|
29
|
+
</ProgressRoot>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const ProgressRoot = styled(ProgressPrimitive.Root)(({ theme }) => ({
|
|
34
|
+
position: 'relative',
|
|
35
|
+
overflow: 'hidden',
|
|
36
|
+
background: theme.colors.inputBackground,
|
|
37
|
+
|
|
38
|
+
// Fix overflow clipping in Safari
|
|
39
|
+
// https://gist.github.com/domske/b66047671c780a238b51c51ffde8d3a0
|
|
40
|
+
transform: 'translateZ(0)',
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const ProgressIndicator = styled(ProgressPrimitive.Indicator)<{
|
|
44
|
+
variant: ProgressVariant;
|
|
45
|
+
}>(({ theme, variant }) => ({
|
|
46
|
+
backgroundColor:
|
|
47
|
+
variant === 'primary'
|
|
48
|
+
? theme.colors.primary
|
|
49
|
+
: variant === 'secondary'
|
|
50
|
+
? theme.colors.secondary
|
|
51
|
+
: variant === 'warning'
|
|
52
|
+
? theme.colors.warning
|
|
53
|
+
: theme.colors.text,
|
|
54
|
+
width: '100%',
|
|
55
|
+
height: '100%',
|
|
56
|
+
transition: 'transform 660ms cubic-bezier(0.65, 0, 0.35, 1)',
|
|
57
|
+
}));
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as ToggleGroupPrimitive from '@radix-ui/react-toggle-group';
|
|
2
|
+
import React, {
|
|
3
|
+
ComponentProps,
|
|
4
|
+
createContext,
|
|
5
|
+
forwardRef,
|
|
6
|
+
memo,
|
|
7
|
+
ReactNode,
|
|
8
|
+
useCallback,
|
|
9
|
+
useContext,
|
|
10
|
+
useMemo,
|
|
11
|
+
} from 'react';
|
|
12
|
+
import styled from 'styled-components';
|
|
13
|
+
import { Tooltip } from './Tooltip';
|
|
14
|
+
|
|
15
|
+
type RadioGroupColorScheme = 'primary' | 'secondary';
|
|
16
|
+
|
|
17
|
+
const ignoredProps = new Set(['colorScheme']);
|
|
18
|
+
|
|
19
|
+
const StyledRoot = styled(ToggleGroupPrimitive.Root).withConfig({
|
|
20
|
+
shouldForwardProp: (prop) => !ignoredProps.has(prop),
|
|
21
|
+
})<{
|
|
22
|
+
colorScheme?: RadioGroupColorScheme;
|
|
23
|
+
}>(({ theme, colorScheme }) => ({
|
|
24
|
+
appearance: 'none',
|
|
25
|
+
width: '0px', // Reset intrinsic width
|
|
26
|
+
flex: '1 1 0px',
|
|
27
|
+
position: 'relative',
|
|
28
|
+
border: '0',
|
|
29
|
+
outline: 'none',
|
|
30
|
+
minWidth: '0',
|
|
31
|
+
textAlign: 'left',
|
|
32
|
+
alignSelf: 'stretch',
|
|
33
|
+
borderRadius: '4px',
|
|
34
|
+
background: theme.colors.inputBackground,
|
|
35
|
+
'&:focus': {
|
|
36
|
+
boxShadow: `0 0 0 1px ${theme.colors.sidebar.background}, 0 0 0 3px ${
|
|
37
|
+
theme.colors[colorScheme ?? 'primary']
|
|
38
|
+
}`,
|
|
39
|
+
},
|
|
40
|
+
display: 'flex',
|
|
41
|
+
alignItems: 'stretch',
|
|
42
|
+
minHeight: '27px',
|
|
43
|
+
padding: colorScheme === undefined ? '2px' : 0,
|
|
44
|
+
}));
|
|
45
|
+
|
|
46
|
+
const StyledItem = styled(ToggleGroupPrimitive.Item).withConfig({
|
|
47
|
+
shouldForwardProp: (prop) => !ignoredProps.has(prop),
|
|
48
|
+
})<{
|
|
49
|
+
colorScheme?: RadioGroupColorScheme;
|
|
50
|
+
}>(({ theme, colorScheme }) => ({
|
|
51
|
+
position: 'relative',
|
|
52
|
+
flex: '1 1 0',
|
|
53
|
+
appearance: 'none',
|
|
54
|
+
border: 'none',
|
|
55
|
+
background: 'none',
|
|
56
|
+
color: 'rgb(139, 139, 139)',
|
|
57
|
+
padding: 0,
|
|
58
|
+
margin: 0,
|
|
59
|
+
borderRadius: '4px',
|
|
60
|
+
display: 'inline-flex',
|
|
61
|
+
alignItems: 'center',
|
|
62
|
+
justifyContent: 'center',
|
|
63
|
+
verticalAlign: 'middle',
|
|
64
|
+
'&[aria-checked="true"]': {
|
|
65
|
+
backgroundColor: colorScheme
|
|
66
|
+
? theme.colors[colorScheme]
|
|
67
|
+
: theme.colors.radioGroup.background,
|
|
68
|
+
color: colorScheme ? theme.colors.radioGroup.background : theme.colors.text,
|
|
69
|
+
boxShadow: colorScheme ? undefined : `0 1px 1px rgba(0,0,0,0.1)`,
|
|
70
|
+
},
|
|
71
|
+
'&:focus': {
|
|
72
|
+
outline: 'none',
|
|
73
|
+
boxShadow: `0 0 0 1px ${theme.colors.sidebar.background}, 0 0 0 3px ${
|
|
74
|
+
theme.colors[colorScheme ?? 'primary']
|
|
75
|
+
}`,
|
|
76
|
+
},
|
|
77
|
+
}));
|
|
78
|
+
|
|
79
|
+
interface ItemProps {
|
|
80
|
+
value: string;
|
|
81
|
+
tooltip?: ReactNode;
|
|
82
|
+
children: ReactNode;
|
|
83
|
+
disabled?: boolean;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const ToggleGroupItem = forwardRef(function ToggleGroupItem(
|
|
87
|
+
{ value, tooltip, children, disabled = false }: ItemProps,
|
|
88
|
+
forwardedRef: React.ForwardedRef<HTMLButtonElement>,
|
|
89
|
+
) {
|
|
90
|
+
const { colorScheme } = useContext(RadioGroupContext);
|
|
91
|
+
|
|
92
|
+
const itemElement = (
|
|
93
|
+
<StyledItem
|
|
94
|
+
ref={forwardedRef}
|
|
95
|
+
value={value}
|
|
96
|
+
disabled={disabled}
|
|
97
|
+
colorScheme={colorScheme}
|
|
98
|
+
>
|
|
99
|
+
{children}
|
|
100
|
+
</StyledItem>
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
return tooltip ? (
|
|
104
|
+
<Tooltip content={tooltip}>{itemElement}</Tooltip>
|
|
105
|
+
) : (
|
|
106
|
+
itemElement
|
|
107
|
+
);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
type RadioGroupContextValue = {
|
|
111
|
+
colorScheme?: RadioGroupColorScheme;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const RadioGroupContext = createContext<RadioGroupContextValue>({
|
|
115
|
+
colorScheme: 'primary',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
interface Props {
|
|
119
|
+
id?: string;
|
|
120
|
+
value?: string;
|
|
121
|
+
onValueChange?: ComponentProps<typeof StyledRoot>['onValueChange'];
|
|
122
|
+
colorScheme?: RadioGroupColorScheme;
|
|
123
|
+
allowEmpty?: boolean;
|
|
124
|
+
children: ReactNode;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function ToggleGroupRoot({
|
|
128
|
+
id,
|
|
129
|
+
value,
|
|
130
|
+
onValueChange,
|
|
131
|
+
colorScheme,
|
|
132
|
+
allowEmpty,
|
|
133
|
+
children,
|
|
134
|
+
}: Props) {
|
|
135
|
+
const contextValue = useMemo(() => ({ colorScheme }), [colorScheme]);
|
|
136
|
+
|
|
137
|
+
const handleValueChange = useCallback(
|
|
138
|
+
(value: string) => {
|
|
139
|
+
if (!allowEmpty && !value) return;
|
|
140
|
+
onValueChange?.(value);
|
|
141
|
+
},
|
|
142
|
+
[allowEmpty, onValueChange],
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return (
|
|
146
|
+
<RadioGroupContext.Provider value={contextValue}>
|
|
147
|
+
<StyledRoot
|
|
148
|
+
id={id}
|
|
149
|
+
type="single"
|
|
150
|
+
value={value}
|
|
151
|
+
colorScheme={colorScheme}
|
|
152
|
+
onValueChange={handleValueChange}
|
|
153
|
+
>
|
|
154
|
+
{children}
|
|
155
|
+
</StyledRoot>
|
|
156
|
+
</RadioGroupContext.Provider>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export namespace RadioGroup {
|
|
161
|
+
export const Root = memo(ToggleGroupRoot);
|
|
162
|
+
export const Item = memo(ToggleGroupItem);
|
|
163
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as RadixScrollArea from "@radix-ui/react-scroll-area";
|
|
2
|
+
import React, { memo, ReactNode, useCallback, useState } from "react";
|
|
3
|
+
import styled from "styled-components";
|
|
4
|
+
|
|
5
|
+
const SCROLLBAR_SIZE = 10;
|
|
6
|
+
|
|
7
|
+
const StyledScrollArea = styled(RadixScrollArea.Root)({
|
|
8
|
+
width: "100%",
|
|
9
|
+
height: "100%",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const StyledViewport = styled(RadixScrollArea.Viewport)({
|
|
13
|
+
width: "100%",
|
|
14
|
+
height: "100%",
|
|
15
|
+
// Override the `display: table` in the child, since this allows
|
|
16
|
+
// elements to expand beyond the width of the viewport.
|
|
17
|
+
"& > div": {
|
|
18
|
+
display: "block !important",
|
|
19
|
+
},
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const StyledScrollbar = styled(RadixScrollArea.Scrollbar)({
|
|
23
|
+
display: "flex",
|
|
24
|
+
padding: "3px",
|
|
25
|
+
'&[data-orientation="vertical"]': {
|
|
26
|
+
width: SCROLLBAR_SIZE,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const StyledThumb = styled(RadixScrollArea.Thumb)(({ theme }) => ({
|
|
31
|
+
flex: 1,
|
|
32
|
+
borderRadius: SCROLLBAR_SIZE,
|
|
33
|
+
backgroundColor: theme.colors.scrollbar,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const Container = styled.div({
|
|
37
|
+
flex: "1 1 0px",
|
|
38
|
+
minHeight: 0,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
children?: ReactNode | ((scrollElementRef: HTMLDivElement) => ReactNode);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export const ScrollArea = memo(function ScrollArea({ children }: Props) {
|
|
46
|
+
const [scrollElementRef, setScrollElementRef] =
|
|
47
|
+
useState<HTMLDivElement | null>(null);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<Container>
|
|
51
|
+
<StyledScrollArea>
|
|
52
|
+
<StyledViewport
|
|
53
|
+
ref={useCallback(
|
|
54
|
+
(ref: HTMLDivElement | null) => setScrollElementRef(ref),
|
|
55
|
+
[]
|
|
56
|
+
)}
|
|
57
|
+
>
|
|
58
|
+
{typeof children === "function"
|
|
59
|
+
? scrollElementRef
|
|
60
|
+
? children(scrollElementRef)
|
|
61
|
+
: null
|
|
62
|
+
: children}
|
|
63
|
+
</StyledViewport>
|
|
64
|
+
<StyledScrollbar orientation="vertical" className="scroll-component">
|
|
65
|
+
<StyledThumb className="scroll-component" />
|
|
66
|
+
</StyledScrollbar>
|
|
67
|
+
</StyledScrollArea>
|
|
68
|
+
</Container>
|
|
69
|
+
);
|
|
70
|
+
});
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { memoize } from "@noya-app/noya-utils";
|
|
2
|
+
import React, {
|
|
3
|
+
createContext,
|
|
4
|
+
CSSProperties,
|
|
5
|
+
memo,
|
|
6
|
+
ReactNode,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useEffect,
|
|
10
|
+
useMemo,
|
|
11
|
+
useRef,
|
|
12
|
+
} from "react";
|
|
13
|
+
import styled from "styled-components";
|
|
14
|
+
|
|
15
|
+
type SelectContextValue = {
|
|
16
|
+
addListener: (value: string, listener: () => void) => void;
|
|
17
|
+
removeListener: (value: string) => void;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const SelectContext = createContext<SelectContextValue | undefined>(undefined);
|
|
21
|
+
|
|
22
|
+
interface SelectOptionProps<T extends string> {
|
|
23
|
+
value: T;
|
|
24
|
+
title?: string;
|
|
25
|
+
onSelect?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const SelectOption = memo(function SelectOption<T extends string>({
|
|
29
|
+
value,
|
|
30
|
+
title,
|
|
31
|
+
onSelect,
|
|
32
|
+
}: SelectOptionProps<T>) {
|
|
33
|
+
const { addListener, removeListener } = useContext(SelectContext)!;
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (!onSelect) return;
|
|
37
|
+
|
|
38
|
+
addListener(value, onSelect);
|
|
39
|
+
|
|
40
|
+
return () => removeListener(value);
|
|
41
|
+
}, [addListener, onSelect, removeListener, value]);
|
|
42
|
+
|
|
43
|
+
return <option value={value}>{title ?? value}</option>;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const createChevronSVGString = memoize(
|
|
47
|
+
(color: string) => `
|
|
48
|
+
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 15 15' fill='${color}'>
|
|
49
|
+
<path d='M3.13523 6.15803C3.3241 5.95657 3.64052 5.94637 3.84197 6.13523L7.5 9.56464L11.158 6.13523C11.3595 5.94637 11.6759 5.95657 11.8648 6.15803C12.0536 6.35949 12.0434 6.67591 11.842 6.86477L7.84197 10.6148C7.64964 10.7951 7.35036 10.7951 7.15803 10.6148L3.15803 6.86477C2.95657 6.67591 2.94637 6.35949 3.13523 6.15803Z'></path>
|
|
50
|
+
</svg>
|
|
51
|
+
`
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
const SelectElement = styled.select<{ flex: CSSProperties["flex"] }>(
|
|
55
|
+
({ theme, flex }) => ({
|
|
56
|
+
appearance: "none",
|
|
57
|
+
...theme.textStyles.small,
|
|
58
|
+
color: theme.colors.text,
|
|
59
|
+
width: "0px", // Reset intrinsic width
|
|
60
|
+
flex: flex ?? "1 1 0px",
|
|
61
|
+
position: "relative",
|
|
62
|
+
border: "0",
|
|
63
|
+
outline: "none",
|
|
64
|
+
minWidth: "0",
|
|
65
|
+
textAlign: "left",
|
|
66
|
+
alignSelf: "stretch",
|
|
67
|
+
borderRadius: "4px",
|
|
68
|
+
paddingTop: "4px",
|
|
69
|
+
paddingBottom: "4px",
|
|
70
|
+
paddingLeft: "8px",
|
|
71
|
+
paddingRight: "23px",
|
|
72
|
+
background: [
|
|
73
|
+
`calc(100% - 6px) / 15px url("data:image/svg+xml;utf8,${createChevronSVGString(
|
|
74
|
+
theme.colors.icon
|
|
75
|
+
)}") no-repeat`,
|
|
76
|
+
theme.colors.inputBackground,
|
|
77
|
+
].join(","),
|
|
78
|
+
"&:focus": {
|
|
79
|
+
boxShadow: `0 0 0 2px ${theme.colors.primary}`,
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
type ChildrenProps<T> =
|
|
85
|
+
| {
|
|
86
|
+
children: ReactNode;
|
|
87
|
+
}
|
|
88
|
+
| {
|
|
89
|
+
options: T[];
|
|
90
|
+
getTitle?: (option: T, index: number) => string;
|
|
91
|
+
onChange: (value: T) => void;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
type Props<T extends string> = ChildrenProps<T> & {
|
|
95
|
+
id: string;
|
|
96
|
+
flex?: CSSProperties["flex"];
|
|
97
|
+
value: T;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const Select = memo(function Select<T extends string>({
|
|
101
|
+
id,
|
|
102
|
+
flex,
|
|
103
|
+
value,
|
|
104
|
+
...rest
|
|
105
|
+
}: Props<T>) {
|
|
106
|
+
const options = "options" in rest ? rest.options : undefined;
|
|
107
|
+
const getTitle = "options" in rest ? rest.getTitle : undefined;
|
|
108
|
+
const onChange = "options" in rest ? rest.onChange : undefined;
|
|
109
|
+
const children = "options" in rest ? undefined : rest.children;
|
|
110
|
+
|
|
111
|
+
const optionElements = useMemo(
|
|
112
|
+
() =>
|
|
113
|
+
options
|
|
114
|
+
? options.map((option, index) => (
|
|
115
|
+
<SelectOption
|
|
116
|
+
key={option}
|
|
117
|
+
value={option}
|
|
118
|
+
title={getTitle?.(option, index)}
|
|
119
|
+
onSelect={() => onChange?.(option)}
|
|
120
|
+
/>
|
|
121
|
+
))
|
|
122
|
+
: children,
|
|
123
|
+
[children, getTitle, onChange, options]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const listeners = useRef<Map<string, () => void>>(new Map());
|
|
127
|
+
|
|
128
|
+
const contextValue: SelectContextValue = useMemo(
|
|
129
|
+
() => ({
|
|
130
|
+
addListener: (value, listener) => listeners.current.set(value, listener),
|
|
131
|
+
removeListener: (value) => listeners.current.delete(value),
|
|
132
|
+
}),
|
|
133
|
+
[]
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<SelectContext.Provider value={contextValue}>
|
|
138
|
+
<SelectElement
|
|
139
|
+
id={id}
|
|
140
|
+
flex={flex}
|
|
141
|
+
value={value}
|
|
142
|
+
onChange={useCallback(
|
|
143
|
+
(event: React.ChangeEvent<HTMLSelectElement>) =>
|
|
144
|
+
listeners.current.get(event.target.value)?.(),
|
|
145
|
+
[listeners]
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
{optionElements}
|
|
149
|
+
</SelectElement>
|
|
150
|
+
</SelectContext.Provider>
|
|
151
|
+
);
|
|
152
|
+
});
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import * as RadixSlider from '@radix-ui/react-slider';
|
|
2
|
+
import React, { useCallback, useMemo } from 'react';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
const StyledSlider = styled(RadixSlider.Root)({
|
|
6
|
+
flex: '1',
|
|
7
|
+
position: 'relative',
|
|
8
|
+
display: 'flex',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
userSelect: 'none',
|
|
11
|
+
touchAction: 'none',
|
|
12
|
+
height: '16px',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const StyledTrack = styled(RadixSlider.Track)(({ theme }) => ({
|
|
16
|
+
backgroundColor: theme.colors.divider,
|
|
17
|
+
position: 'relative',
|
|
18
|
+
flexGrow: 1,
|
|
19
|
+
height: '2px',
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
const StyledRange = styled(RadixSlider.Range)(({ theme }) => ({
|
|
23
|
+
position: 'absolute',
|
|
24
|
+
backgroundColor: theme.colors.primary,
|
|
25
|
+
borderRadius: '9999px',
|
|
26
|
+
height: '100%',
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const StyledThumb = styled(RadixSlider.Thumb)(({ theme }) => ({
|
|
30
|
+
display: 'block',
|
|
31
|
+
width: '12px',
|
|
32
|
+
height: '12px',
|
|
33
|
+
backgroundColor: theme.colors.slider.background,
|
|
34
|
+
border: `1px solid ${theme.colors.slider.border}`,
|
|
35
|
+
borderRadius: '20px',
|
|
36
|
+
':focus': {
|
|
37
|
+
outline: 'none',
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
interface Props {
|
|
42
|
+
id?: string;
|
|
43
|
+
value: number;
|
|
44
|
+
onValueChange: (value: number) => void;
|
|
45
|
+
min: number;
|
|
46
|
+
max: number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const Slider = function Slider({
|
|
50
|
+
id,
|
|
51
|
+
value,
|
|
52
|
+
onValueChange,
|
|
53
|
+
min,
|
|
54
|
+
max,
|
|
55
|
+
}: Props) {
|
|
56
|
+
const arrayValue = useMemo(
|
|
57
|
+
() => [Math.min(Math.max(value, min), max)],
|
|
58
|
+
[value, min, max],
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const handleValueChange = useCallback(
|
|
62
|
+
(arrayValue: number[]) => {
|
|
63
|
+
onValueChange(arrayValue[0]);
|
|
64
|
+
},
|
|
65
|
+
[onValueChange],
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<StyledSlider
|
|
70
|
+
min={min}
|
|
71
|
+
max={max}
|
|
72
|
+
id={id}
|
|
73
|
+
value={arrayValue}
|
|
74
|
+
onValueChange={handleValueChange}
|
|
75
|
+
>
|
|
76
|
+
<StyledTrack>
|
|
77
|
+
<StyledRange />
|
|
78
|
+
</StyledTrack>
|
|
79
|
+
<StyledThumb />
|
|
80
|
+
</StyledSlider>
|
|
81
|
+
);
|
|
82
|
+
};
|