@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
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@noya-app/noya-designsystem",
3
+ "version": "0.0.2",
4
+ "main": "./dist/index.js",
5
+ "module": "./dist/index.mjs",
6
+ "types": "./dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsup src/index.tsx --format cjs,esm --dts --sourcemap",
9
+ "dev": "npm run build -- --watch"
10
+ },
11
+ "dependencies": {
12
+ "@dnd-kit/core": "3.1.1",
13
+ "@dnd-kit/modifiers": "3.0.0",
14
+ "@dnd-kit/sortable": "4.0.0",
15
+ "@noya-app/noya-colorpicker": "0.0.2",
16
+ "@radix-ui/primitive": "^1.0.0",
17
+ "@radix-ui/react-avatar": "^1.0.1",
18
+ "@radix-ui/react-compose-refs": "^1.0.0",
19
+ "@radix-ui/react-context-menu": "^2.0.1",
20
+ "@radix-ui/react-dialog": "^1.0.2",
21
+ "@radix-ui/react-dropdown-menu": "^2.0.1",
22
+ "@radix-ui/react-popover": "^1.0.2",
23
+ "@radix-ui/react-progress": "^1.0.1",
24
+ "@radix-ui/react-radio-group": "^1.1.0",
25
+ "@radix-ui/react-scroll-area": "^1.0.2",
26
+ "@radix-ui/react-slider": "^1.1.0",
27
+ "@radix-ui/react-switch": "^1.0.1",
28
+ "@radix-ui/react-toast": "^1.1.4",
29
+ "@radix-ui/react-toggle-group": "^1.0.1",
30
+ "@radix-ui/react-tooltip": "^1.0.2",
31
+ "@radix-ui/react-utils": "^0.0.5",
32
+ "immer": "9.0.5",
33
+ "kiwi.js": "^1.1.3",
34
+ "react-use-gesture": "9.1.3",
35
+ "react-virtualized": "^9.22.3",
36
+ "react-window": "^1.8.8",
37
+ "styled-components": "5.3.0",
38
+ "vscode-fuzzy-scorer": "^0.0.4"
39
+ },
40
+ "devDependencies": {
41
+ "@types/react-virtualized": "^9.21.21",
42
+ "@types/react-window": "^1.8.5",
43
+ "@types/styled-components": "5.1.26"
44
+ },
45
+ "peerDependencies": {
46
+ "react": "*",
47
+ "react-dom": "*"
48
+ }
49
+ }
@@ -0,0 +1,41 @@
1
+ // Bun Snapshot v1, https://goo.gl/fbAQLP
2
+
3
+ exports[`filter single match 1`] = `
4
+ [
5
+ {
6
+ "index": 0,
7
+ "labelMatch": [
8
+ {
9
+ "end": 9,
10
+ "start": 5,
11
+ },
12
+ ],
13
+ "score": 32768,
14
+ },
15
+ ]
16
+ `;
17
+
18
+ exports[`filter multiple matches 1`] = `
19
+ [
20
+ {
21
+ "index": 1,
22
+ "labelMatch": [
23
+ {
24
+ "end": 1,
25
+ "start": 0,
26
+ },
27
+ ],
28
+ "score": 131072,
29
+ },
30
+ {
31
+ "index": 0,
32
+ "labelMatch": [
33
+ {
34
+ "end": 6,
35
+ "start": 5,
36
+ },
37
+ ],
38
+ "score": 32768,
39
+ },
40
+ ]
41
+ `;
@@ -0,0 +1,85 @@
1
+ import { expect, test } from "bun:test";
2
+ import { fuzzyFilter, fuzzyScore, fuzzyTokenize } from "../utils/fuzzyScorer";
3
+
4
+ test("prefix match", () => {
5
+ const result = fuzzyScore({ item: "font-bold", query: "font" });
6
+
7
+ expect(result.labelMatch).toEqual([{ start: 0, end: 4 }]);
8
+ });
9
+
10
+ test("suffix match", () => {
11
+ const result = fuzzyScore({ item: "font-bold", query: "bold" });
12
+
13
+ expect(result.labelMatch).toEqual([{ start: 5, end: 9 }]);
14
+ });
15
+
16
+ test("non-contiguous match", () => {
17
+ const result = fuzzyScore({ item: "font-bold", query: "fb" });
18
+
19
+ expect(result.labelMatch).toEqual([
20
+ { start: 0, end: 1 },
21
+ { start: 5, end: 6 },
22
+ ]);
23
+ });
24
+
25
+ test("filter single match", () => {
26
+ const result = fuzzyFilter({
27
+ items: ["font-bold", "font-italic", "font-regular"],
28
+ query: "bold",
29
+ });
30
+
31
+ expect(result).toMatchSnapshot();
32
+ });
33
+
34
+ test("filter multiple matches", () => {
35
+ const result = fuzzyFilter({
36
+ items: ["font-bold", "border"],
37
+ query: "b",
38
+ });
39
+
40
+ expect(result).toMatchSnapshot();
41
+ });
42
+
43
+ test("tokenizes prefix match", () => {
44
+ const item = "font-bold";
45
+ const query = "font";
46
+ const result = fuzzyTokenize({
47
+ item,
48
+ itemScore: fuzzyScore({ item, query }),
49
+ });
50
+
51
+ expect(result).toEqual([
52
+ { type: "match", text: "font" },
53
+ { type: "text", text: "-bold" },
54
+ ]);
55
+ });
56
+
57
+ test("tokenizes suffix match", () => {
58
+ const item = "font-bold";
59
+ const query = "bold";
60
+ const result = fuzzyTokenize({
61
+ item,
62
+ itemScore: fuzzyScore({ item, query }),
63
+ });
64
+
65
+ expect(result).toEqual([
66
+ { type: "text", text: "font-" },
67
+ { type: "match", text: "bold" },
68
+ ]);
69
+ });
70
+
71
+ test("tokenizes non-contiguous match", () => {
72
+ const item = "font-bold";
73
+ const query = "fb";
74
+ const result = fuzzyTokenize({
75
+ item,
76
+ itemScore: fuzzyScore({ item, query }),
77
+ });
78
+
79
+ expect(result).toEqual([
80
+ { type: "match", text: "f" },
81
+ { type: "text", text: "ont-" },
82
+ { type: "match", text: "b" },
83
+ { type: "text", text: "old" },
84
+ ]);
85
+ });
@@ -0,0 +1,44 @@
1
+ import React, { memo } from 'react';
2
+ import styled, { keyframes } from 'styled-components';
3
+
4
+ interface Props {
5
+ size?: number;
6
+ opacity?: number;
7
+ color?: string;
8
+ trackColor?: string;
9
+ }
10
+
11
+ const spin = keyframes`
12
+ 0% { transform: rotate(0deg); }
13
+ 100% { transform: rotate(360deg); }
14
+ `;
15
+
16
+ const Spinner = styled.div`
17
+ display: flex;
18
+ justify-content: center;
19
+ align-items: center;
20
+ opacity: ${({ opacity }: Props) => opacity};
21
+ `;
22
+
23
+ // Use css syntax to support keyframes
24
+ const SpinnerCircle = styled.div<Props>`
25
+ border: 2px solid ${({ trackColor }) => trackColor};
26
+ border-top: 2px solid ${({ color }) => color};
27
+ border-radius: 50%;
28
+ width: ${({ size }) => `${size}px`};
29
+ height: ${({ size }) => `${size}px`};
30
+ animation: ${spin} 1s linear infinite;
31
+ `;
32
+
33
+ export const ActivityIndicator = memo(function ActivityIndicator({
34
+ size = 16,
35
+ opacity = 1,
36
+ color = 'black',
37
+ trackColor = 'rgba(0, 0, 0, 0.1)',
38
+ }: Props) {
39
+ return (
40
+ <Spinner opacity={opacity}>
41
+ <SpinnerCircle size={size} color={color} trackColor={trackColor} />
42
+ </Spinner>
43
+ );
44
+ });
@@ -0,0 +1,64 @@
1
+ import * as AvatarPrimitive from '@radix-ui/react-avatar';
2
+ import React from 'react';
3
+ import styled from 'styled-components';
4
+
5
+ const AvatarRoot = styled(AvatarPrimitive.Root)<{
6
+ size: number;
7
+ overflow: number;
8
+ }>(({ theme, size, overflow }) => ({
9
+ display: 'inline-flex',
10
+ alignItems: 'center',
11
+ justifyContent: 'center',
12
+ verticalAlign: 'middle',
13
+ overflow: 'hidden',
14
+ userSelect: 'none',
15
+ width: size,
16
+ height: size,
17
+ borderRadius: '100%',
18
+ backgroundColor: theme.colors.secondaryLight,
19
+ ...(overflow && {
20
+ margin: `-${overflow}px`,
21
+ }),
22
+ }));
23
+
24
+ const AvatarImage = styled(AvatarPrimitive.Image)({
25
+ width: '100%',
26
+ height: '100%',
27
+ objectFit: 'cover',
28
+ borderRadius: 'inherit',
29
+ });
30
+
31
+ const AvatarFallback = styled(AvatarPrimitive.Fallback)<{
32
+ size: number;
33
+ }>(({ theme, size }) => ({
34
+ fontSize: '0.8rem',
35
+ fontWeight: 500,
36
+ width: '100%',
37
+ height: '100%',
38
+ display: 'flex',
39
+ alignItems: 'center',
40
+ justifyContent: 'center',
41
+ color: 'white',
42
+ lineHeight: `${size}px`,
43
+ }));
44
+
45
+ export function Avatar({
46
+ image,
47
+ name,
48
+ fallback,
49
+ size = 19,
50
+ overflow = 0,
51
+ }: {
52
+ image?: string;
53
+ name?: string;
54
+ fallback?: string;
55
+ size?: number;
56
+ overflow?: number;
57
+ }) {
58
+ return (
59
+ <AvatarRoot size={size} overflow={overflow}>
60
+ <AvatarImage src={image} alt={name} />
61
+ <AvatarFallback size={size}>{fallback}</AvatarFallback>
62
+ </AvatarRoot>
63
+ );
64
+ }
@@ -0,0 +1,209 @@
1
+ import React, {
2
+ CSSProperties,
3
+ ForwardedRef,
4
+ forwardRef,
5
+ ReactNode,
6
+ useCallback,
7
+ } from 'react';
8
+ import styled from 'styled-components';
9
+ import { Tooltip } from './Tooltip';
10
+
11
+ type ButtonVariant =
12
+ | 'normal'
13
+ | 'primary'
14
+ | 'secondary'
15
+ | 'secondaryBright'
16
+ | 'white'
17
+ | 'thin'
18
+ | 'floating'
19
+ | 'none';
20
+
21
+ type ButtonSize = 'normal' | 'large';
22
+
23
+ /* ----------------------------------------------------------------------------
24
+ * Element
25
+ * ------------------------------------------------------------------------- */
26
+
27
+ export const ButtonElement = styled.button<{
28
+ active: boolean;
29
+ variant: ButtonVariant;
30
+ size: ButtonSize;
31
+ flex?: CSSProperties['flex'];
32
+ }>(({ theme, active, disabled, variant, size, flex }) => ({
33
+ ...(size === 'large' ? theme.textStyles.heading4 : theme.textStyles.small),
34
+ textDecoration: 'none',
35
+ lineHeight: '1',
36
+ flex: flex ?? '0 0 auto',
37
+ position: 'relative',
38
+ border: '0',
39
+ outline: 'none',
40
+ WebkitAppRegion: 'no-drag',
41
+ userSelect: 'none',
42
+ minWidth: variant === 'normal' ? '31px' : undefined,
43
+ textAlign: 'left',
44
+ borderRadius: '4px',
45
+ paddingTop: variant === 'none' ? '0px' : '4px',
46
+ paddingRight: variant === 'none' ? '0px' : variant === 'thin' ? '1px' : '6px',
47
+ paddingBottom: variant === 'none' ? '0px' : '4px',
48
+ paddingLeft: variant === 'none' ? '0px' : variant === 'thin' ? '1px' : '6px',
49
+ ...(size === 'large' && {
50
+ paddingTop: '12px',
51
+ paddingRight: '16px',
52
+ paddingBottom: '12px',
53
+ paddingLeft: '16px',
54
+ }),
55
+ background: active
56
+ ? theme.colors.primary
57
+ : variant === 'none' || variant === 'thin'
58
+ ? 'transparent'
59
+ : theme.colors.inputBackground,
60
+ color: active ? 'white' : theme.colors.text,
61
+ opacity: disabled ? 0.25 : 1,
62
+ '&:focus': {
63
+ boxShadow: `0 0 0 1px ${theme.colors.sidebar.background}, 0 0 0 3px ${theme.colors.primary}`,
64
+ },
65
+ '&:hover': {
66
+ background: theme.colors.inputBackgroundLight,
67
+ },
68
+ '&:active': {
69
+ background: theme.colors.activeBackground,
70
+ },
71
+ ...(variant === 'primary' && {
72
+ background: theme.colors.primary,
73
+ color: 'white',
74
+ '&:hover': {
75
+ background: theme.colors.primaryLight,
76
+ },
77
+ '&:active': {
78
+ background: theme.colors.primary,
79
+ },
80
+ }),
81
+ ...(variant === 'secondary' && {
82
+ background: theme.colors.secondary,
83
+ color: 'white',
84
+ '&:hover': {
85
+ background: theme.colors.secondaryLight,
86
+ },
87
+ '&:active': {
88
+ background: theme.colors.secondary,
89
+ },
90
+ }),
91
+ ...(variant === 'secondaryBright' && {
92
+ background: '#0ab557',
93
+ color: 'white',
94
+ '&:hover': {
95
+ opacity: 0.8,
96
+ },
97
+ '&:active': {
98
+ opacity: 0.9,
99
+ },
100
+ }),
101
+ ...(variant === 'white' && {
102
+ background: 'white',
103
+ '&:hover': {
104
+ opacity: 0.8,
105
+ },
106
+ '&:active': {
107
+ opacity: 0.9,
108
+ },
109
+ }),
110
+ ...(variant === 'floating' && {
111
+ background: 'white',
112
+ color: theme.colors.text,
113
+ boxShadow: '0 1px 2px rgba(0, 0, 0, 0.1)',
114
+ fontSize: '12px',
115
+ padding: '0px 4px',
116
+ }),
117
+ display: 'flex',
118
+ alignItems: 'center',
119
+ justifyContent: 'center',
120
+
121
+ '& *': {
122
+ pointerEvents: 'none',
123
+ },
124
+ }));
125
+
126
+ /* ----------------------------------------------------------------------------
127
+ * Content
128
+ * ------------------------------------------------------------------------- */
129
+
130
+ const ButtonContent = styled.span(({ theme }) => ({
131
+ // Line height of small text - maybe figure out better way to ensure
132
+ // icons don't have a smaller height
133
+ minHeight: '19px',
134
+ display: 'flex',
135
+ alignItems: 'center',
136
+ flex: '1',
137
+ justifyContent: 'center',
138
+
139
+ // Helps with alignment with the default small text
140
+ lineHeight: '15px',
141
+ }));
142
+
143
+ /* ----------------------------------------------------------------------------
144
+ * Root
145
+ * ------------------------------------------------------------------------- */
146
+
147
+ export interface ButtonRootProps {
148
+ id?: string;
149
+ flex?: CSSProperties['flex'];
150
+ tabIndex?: number;
151
+ children: ReactNode;
152
+ active?: boolean;
153
+ disabled?: boolean;
154
+ variant?: ButtonVariant;
155
+ size?: ButtonSize;
156
+ tooltip?: ReactNode;
157
+ onClick?: (event: React.MouseEvent) => void;
158
+ onPointerDown?: (event: React.PointerEvent) => void;
159
+ contentStyle?: CSSProperties;
160
+ as?: React.ElementType;
161
+ href?: string;
162
+ target?: string;
163
+ rel?: string;
164
+ }
165
+
166
+ export const Button = forwardRef(function Button(
167
+ {
168
+ id,
169
+ flex,
170
+ tabIndex,
171
+ tooltip,
172
+ active = false,
173
+ disabled = false,
174
+ variant = 'normal',
175
+ size = 'normal',
176
+ contentStyle,
177
+ onClick,
178
+ children,
179
+ ...rest // Propagate any other props so this component works as a Slot
180
+ }: ButtonRootProps,
181
+ forwardedRef: ForwardedRef<HTMLButtonElement>,
182
+ ) {
183
+ const buttonElement = (
184
+ <ButtonElement
185
+ ref={forwardedRef}
186
+ id={id}
187
+ flex={flex}
188
+ tabIndex={tabIndex}
189
+ active={active}
190
+ disabled={disabled}
191
+ variant={variant}
192
+ size={size}
193
+ onClick={onClick}
194
+ // Prevent double clicking a button from triggering any callbacks in ancestors
195
+ onDoubleClick={useCallback((event: React.MouseEvent) => {
196
+ event.stopPropagation();
197
+ }, [])}
198
+ {...rest}
199
+ >
200
+ <ButtonContent style={contentStyle}>{children}</ButtonContent>
201
+ </ButtonElement>
202
+ );
203
+
204
+ return tooltip ? (
205
+ <Tooltip content={tooltip}>{buttonElement}</Tooltip>
206
+ ) : (
207
+ buttonElement
208
+ );
209
+ });
@@ -0,0 +1,214 @@
1
+ import { Cross1Icon, PlusIcon } from '@noya-app/noya-icons';
2
+ import React, { memo } from 'react';
3
+ import styled from 'styled-components';
4
+ import { useDesignSystemTheme } from '../contexts/DesignSystemConfiguration';
5
+ import { useHover } from '../hooks/useHover';
6
+
7
+ type ChipColorScheme = 'primary' | 'secondary' | 'error';
8
+ type ChipSize = 'small' | 'medium';
9
+ type ChipVariant = 'solid' | 'outlined' | 'ghost';
10
+
11
+ const ChipElement = styled.span<{
12
+ colorScheme?: ChipColorScheme;
13
+ size: ChipSize;
14
+ monospace: boolean;
15
+ variant: ChipVariant;
16
+ isInteractive: boolean;
17
+ }>(({ theme, colorScheme, size, variant, monospace, isInteractive }) => {
18
+ const color =
19
+ colorScheme === 'primary'
20
+ ? theme.colors.primary
21
+ : colorScheme === 'secondary'
22
+ ? theme.colors.secondary
23
+ : theme.colors.text;
24
+
25
+ const backgroundColor =
26
+ colorScheme === 'primary'
27
+ ? 'rgb(238, 229, 255)'
28
+ : colorScheme === 'secondary'
29
+ ? 'rgb(205, 238, 231)'
30
+ : colorScheme === 'error'
31
+ ? 'rgb(255, 219, 219)'
32
+ : theme.colors.inputBackground;
33
+
34
+ const subtleColor =
35
+ colorScheme === 'primary'
36
+ ? 'rgba(238, 229, 255, 0.2)'
37
+ : colorScheme === 'secondary'
38
+ ? 'rgba(205, 238, 231, 0.2)'
39
+ : colorScheme === 'error'
40
+ ? 'rgba(255, 219, 219, 0.2)'
41
+ : 'rgba(0, 0, 0, 0.1)';
42
+
43
+ return {
44
+ textTransform: 'initial',
45
+ borderRadius: 4,
46
+ userSelect: 'none',
47
+ display: 'inline-flex',
48
+ alignItems: 'center',
49
+ lineHeight: '1.4',
50
+ ...(size === 'medium'
51
+ ? {
52
+ fontSize: '11px',
53
+ padding: '4px 8px',
54
+ }
55
+ : {
56
+ fontSize: '9px',
57
+ padding: '0px 4px',
58
+ }),
59
+ ...(monospace && {
60
+ fontFamily: theme.fonts.monospace,
61
+ }),
62
+ ...(variant === 'solid'
63
+ ? {
64
+ color,
65
+ backgroundColor,
66
+ }
67
+ : variant === 'outlined'
68
+ ? {
69
+ color,
70
+ backgroundColor: 'transparent',
71
+ boxShadow: `0 0 0 1px ${subtleColor} inset`,
72
+ }
73
+ : {}),
74
+ '&:hover': {
75
+ ...(variant === 'outlined' && {
76
+ backgroundColor: subtleColor,
77
+ boxShadow: 'none',
78
+ }),
79
+ },
80
+ ...(isInteractive && {
81
+ cursor: 'pointer',
82
+ }),
83
+ };
84
+ });
85
+
86
+ const DeleteElement = styled(Cross1Icon)<{
87
+ size: ChipSize;
88
+ }>(({ size }) => ({
89
+ position: 'relative',
90
+ marginRight: '-2px',
91
+ cursor: 'pointer',
92
+ opacity: 0.5,
93
+
94
+ ...(size === 'medium'
95
+ ? {
96
+ marginLeft: '2px',
97
+ // top: '-1px',
98
+ transform: 'scale(0.75)',
99
+ }
100
+ : {
101
+ transform: 'scale(0.6)',
102
+ }),
103
+
104
+ '&:hover': {
105
+ opacity: 0.85,
106
+ },
107
+ }));
108
+
109
+ const ignoredProps = new Set(['size', 'isOnlyChild']);
110
+
111
+ const AddElement = styled(PlusIcon).withConfig({
112
+ shouldForwardProp: (prop) => !ignoredProps.has(prop),
113
+ })<{
114
+ size: ChipSize;
115
+ isOnlyChild: boolean;
116
+ }>(({ size, isOnlyChild }) => ({
117
+ position: 'relative',
118
+ marginRight: '-2px',
119
+ cursor: 'pointer',
120
+ opacity: 0.5,
121
+
122
+ ...(size === 'medium'
123
+ ? {
124
+ marginLeft: '2px',
125
+ top: '-1px',
126
+ transform: 'scale(0.75)',
127
+ }
128
+ : {
129
+ transform: 'scale(0.6)',
130
+ ...(isOnlyChild && { marginLeft: '-2px' }),
131
+ }),
132
+
133
+ '&:hover': {
134
+ opacity: 0.85,
135
+ },
136
+ }));
137
+
138
+ export interface ChipProps {
139
+ colorScheme?: ChipColorScheme;
140
+ variant?: ChipVariant;
141
+ size?: ChipSize;
142
+ children?: React.ReactNode;
143
+ deletable?: boolean;
144
+ addable?: boolean;
145
+ monospace?: boolean;
146
+ onDelete?: () => void;
147
+ onAdd?: () => void;
148
+ onClick?: () => void;
149
+ onHoverDeleteChange?: (hovering: boolean) => void;
150
+ style?: React.CSSProperties;
151
+ tabIndex?: number;
152
+ }
153
+
154
+ export const Chip = memo(function Chip({
155
+ colorScheme,
156
+ children,
157
+ deletable,
158
+ addable,
159
+ style,
160
+ size = 'medium',
161
+ variant = 'solid',
162
+ monospace = false,
163
+ tabIndex,
164
+ onDelete,
165
+ onClick,
166
+ onAdd,
167
+ onHoverDeleteChange,
168
+ }: ChipProps) {
169
+ const theme = useDesignSystemTheme();
170
+
171
+ const { hoverProps: hoverDeleteProps } = useHover({
172
+ onHoverChange: onHoverDeleteChange,
173
+ });
174
+
175
+ const handleClick = !children && !deletable && addable ? onAdd : onClick;
176
+
177
+ const color =
178
+ colorScheme === 'primary'
179
+ ? theme.colors.primary
180
+ : colorScheme === 'secondary'
181
+ ? theme.colors.secondary
182
+ : theme.colors.text;
183
+
184
+ return (
185
+ <ChipElement
186
+ variant={variant}
187
+ colorScheme={colorScheme}
188
+ style={style}
189
+ onClick={handleClick}
190
+ size={size}
191
+ monospace={monospace}
192
+ isInteractive={!!handleClick}
193
+ tabIndex={tabIndex}
194
+ >
195
+ {children}
196
+ {deletable && (
197
+ <DeleteElement
198
+ size={size}
199
+ {...(hoverDeleteProps as any)}
200
+ onClick={onDelete}
201
+ color={color}
202
+ />
203
+ )}
204
+ {addable && (
205
+ <AddElement
206
+ size={size}
207
+ isOnlyChild={!children && !deletable}
208
+ onClick={onAdd}
209
+ color={color}
210
+ />
211
+ )}
212
+ </ChipElement>
213
+ );
214
+ });