@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,468 @@
1
+ import { KeyModifiers } from '@noya-app/noya-keymap';
2
+ import React, {
3
+ createContext,
4
+ CSSProperties,
5
+ ForwardedRef,
6
+ forwardRef,
7
+ memo,
8
+ ReactNode,
9
+ useCallback,
10
+ useContext,
11
+ useMemo,
12
+ } from 'react';
13
+ import styled, { keyframes } from 'styled-components';
14
+ import { useHover } from '../hooks/useHover';
15
+ import withSeparatorElements from '../utils/withSeparatorElements';
16
+ import { ActivityIndicator } from './ActivityIndicator';
17
+ import { ContextMenu } from './ContextMenu';
18
+ import { MenuItem } from './internal/Menu';
19
+ import { ScrollArea } from './ScrollArea';
20
+ import { Spacer } from './Spacer';
21
+ import { Stack } from './Stack';
22
+ import { Tooltip } from './Tooltip';
23
+
24
+ export type GridViewSize = 'xxs' | 'xs' | 'small' | 'large';
25
+
26
+ const sizes: Record<
27
+ GridViewSize,
28
+ {
29
+ itemWidth: number;
30
+ itemHeight: number;
31
+ gap: number;
32
+ }
33
+ > = {
34
+ xxs: {
35
+ itemWidth: 36,
36
+ itemHeight: 36,
37
+ gap: 5,
38
+ },
39
+ xs: {
40
+ itemWidth: 87,
41
+ itemHeight: 87,
42
+ gap: 9,
43
+ },
44
+ small: {
45
+ itemWidth: 160,
46
+ itemHeight: 170,
47
+ gap: 20,
48
+ },
49
+ large: {
50
+ itemWidth: 280,
51
+ itemHeight: 280,
52
+ gap: 20,
53
+ },
54
+ };
55
+
56
+ const Grid = styled.div<{
57
+ size: GridViewSize;
58
+ padding: CSSProperties['padding'];
59
+ }>(({ theme, size, padding }) => {
60
+ return {
61
+ color: theme.colors.text,
62
+ display: 'grid',
63
+ gridTemplateColumns: `repeat(auto-fill, minmax(${sizes[size].itemWidth}px, 1fr))`,
64
+ // gridAutoRows: `${sizes[size].itemHeight}px`,
65
+ gap: `${sizes[size].gap}px`,
66
+ // gridTemplateColumns: `repeat(auto-fill, minmax(
67
+ // ${size === 'large' ? '280px' : size === 'small' ? '160px' : '116px'}
68
+ // , 1fr))`,
69
+ // gridAutoRows:
70
+ // size === 'large' ? '280px' : size === 'small' ? '170px' : '116px',
71
+ // gap: size === 'large' || size === 'small' ? `20px` : `12px`,
72
+ padding,
73
+ };
74
+ });
75
+
76
+ const Container = styled.div<{ scrollable: boolean }>(
77
+ ({ theme, scrollable }) => ({
78
+ flex: scrollable ? '1' : '0 0 auto',
79
+ display: 'flex',
80
+ flexDirection: 'column',
81
+ }),
82
+ );
83
+
84
+ const ContentContainer = styled.div<{
85
+ selected?: boolean;
86
+ hovered?: boolean;
87
+ bordered: boolean;
88
+ disabled: boolean;
89
+ }>(({ theme, selected, hovered, bordered, disabled }) => ({
90
+ display: 'flex',
91
+ flex: '1',
92
+ backgroundColor: theme.colors.sidebar.background,
93
+ alignItems: 'center',
94
+ justifyContent: 'center',
95
+ borderRadius: '2px',
96
+ border: `1px solid ${
97
+ selected
98
+ ? theme.colors.primary
99
+ : // : hovered
100
+ // ? `rgb(132, 63, 255, 0.5)`
101
+ bordered
102
+ ? theme.colors.divider
103
+ : 'transparent'
104
+ }`,
105
+ overflow: 'hidden',
106
+
107
+ cursor: 'pointer',
108
+ ...(disabled
109
+ ? { opacity: 0.5 }
110
+ : {
111
+ '&:hover': { opacity: 0.85 },
112
+ '&:active': { opacity: 0.7 },
113
+ }),
114
+ }));
115
+
116
+ const ItemContainer = styled.div(({ theme }) => ({
117
+ display: 'flex',
118
+ flexDirection: 'column',
119
+ position: 'relative',
120
+
121
+ '&:focus': {
122
+ boxShadow: `0 0 0 1px ${theme.colors.sidebar.background}, 0 0 0 3px ${theme.colors.primary}`,
123
+ outline: 'none',
124
+ },
125
+ }));
126
+
127
+ const ItemTitle = styled.span<{ showBackground: boolean }>(
128
+ ({ theme, showBackground }) => ({
129
+ ...theme.textStyles.small,
130
+ lineHeight: '1',
131
+ color: theme.colors.text,
132
+ fontWeight: 500,
133
+ userSelect: 'none',
134
+ whiteSpace: 'pre',
135
+ overflow: 'hidden',
136
+ textOverflow: 'ellipsis',
137
+
138
+ ...(showBackground && {
139
+ background: theme.colors.sidebar.background,
140
+ // border: `1px solid ${theme.colors.dividerSubtle}`,
141
+ padding: '2px 4px',
142
+ borderRadius: '2px',
143
+ backdropFilter: 'blur(4px)',
144
+ }),
145
+ }),
146
+ );
147
+
148
+ const ItemDescription = styled.span<{ showBackground: boolean }>(
149
+ ({ theme, showBackground }) => ({
150
+ ...theme.textStyles.small,
151
+ fontSize: '0.7rem',
152
+ lineHeight: '1',
153
+ color: theme.colors.textMuted,
154
+ userSelect: 'none',
155
+ whiteSpace: 'pre',
156
+ overflow: 'hidden',
157
+ textOverflow: 'ellipsis',
158
+
159
+ ...(showBackground && {
160
+ background: theme.colors.sidebar.background,
161
+ // border: `1px solid ${theme.colors.dividerSubtle}`,
162
+ padding: '2px 4px',
163
+ borderRadius: '2px',
164
+ backdropFilter: 'blur(4px)',
165
+ }),
166
+ }),
167
+ );
168
+
169
+ const SectionTitle = styled.span<{ last?: boolean }>(
170
+ ({ theme, last = false }) => ({
171
+ ...theme.textStyles.heading3,
172
+ color: last ? theme.colors.text : theme.colors.textMuted,
173
+ fontWeight: 500,
174
+ userSelect: 'none',
175
+ whiteSpace: 'pre',
176
+ overflow: 'hidden',
177
+ textOverflow: 'ellipsis',
178
+ lineHeight: '1',
179
+ }),
180
+ );
181
+
182
+ const SectionHeaderContainer = styled.div(({ theme }) => ({
183
+ padding: '0 20px',
184
+ }));
185
+
186
+ const TextOverlay = styled.div({
187
+ position: 'absolute',
188
+ inset: 0,
189
+ display: 'flex',
190
+ flexDirection: 'column',
191
+ justifyContent: 'end',
192
+ alignItems: 'start',
193
+ padding: '4px',
194
+ pointerEvents: 'none',
195
+ overflow: 'hidden',
196
+ textOverflow: 'ellipsis',
197
+ whiteSpace: 'nowrap',
198
+ gap: '2px',
199
+ });
200
+
201
+ const shimmer = keyframes({
202
+ '0%': {
203
+ backgroundPosition: '-200% 0',
204
+ },
205
+ '100%': {
206
+ backgroundPosition: '200% 0',
207
+ },
208
+ });
209
+
210
+ const Shimmer = styled.div`
211
+ pointer-events: none;
212
+ position: absolute;
213
+ inset: 0;
214
+ animation: ${shimmer} 6s infinite linear;
215
+ background: linear-gradient(
216
+ 90deg,
217
+ rgba(255, 255, 255, 0),
218
+ rgba(226, 232, 240, 0.5),
219
+ rgba(255, 255, 255, 0)
220
+ );
221
+ background-size: 200% 100%;
222
+ display: flex;
223
+ flex-direction: column;
224
+ justify-content: start;
225
+ align-items: end;
226
+ padding: 4px;
227
+ `;
228
+
229
+ interface ItemProps<MenuItemType extends string = string> {
230
+ id: string;
231
+ title?: ReactNode;
232
+ subtitle?: ReactNode;
233
+ loading?: boolean;
234
+ selected?: boolean;
235
+ onClick?: (event: React.MouseEvent) => void;
236
+ onPress?: (options: KeyModifiers) => void;
237
+ onDoubleClick?: () => void;
238
+ onHoverChange?: (isHovering: boolean) => void;
239
+ children?: ReactNode;
240
+ menuItems?: MenuItem<MenuItemType>[];
241
+ onSelectMenuItem?: (value: MenuItemType) => void;
242
+ onContextMenu?: () => void;
243
+ }
244
+
245
+ const GridViewItem = forwardRef(function GridViewItem<
246
+ MenuItemType extends string,
247
+ >(
248
+ {
249
+ id,
250
+ title,
251
+ subtitle,
252
+ loading,
253
+ selected,
254
+ onClick,
255
+ onPress,
256
+ onDoubleClick,
257
+ onHoverChange,
258
+ children,
259
+ menuItems,
260
+ onSelectMenuItem,
261
+ onContextMenu,
262
+ }: ItemProps<MenuItemType>,
263
+ forwardedRef: ForwardedRef<HTMLDivElement>,
264
+ ) {
265
+ const [hovered, setHovered] = React.useState(false);
266
+
267
+ const { hoverProps } = useHover({
268
+ onHoverChange: (isHovering) => {
269
+ onHoverChange?.(isHovering);
270
+ setHovered(isHovering);
271
+ },
272
+ });
273
+
274
+ const { textPosition, bordered, disabled } = useContext(GridViewContext);
275
+
276
+ const handleClick = useCallback(
277
+ (event: React.MouseEvent) => {
278
+ event.stopPropagation();
279
+ event.preventDefault();
280
+
281
+ onClick?.(event);
282
+ onPress?.(event);
283
+ },
284
+ [onClick, onPress],
285
+ );
286
+
287
+ const handleKeyDown = useCallback(
288
+ (event: React.KeyboardEvent) => {
289
+ if (event.key === 'Enter' || event.key === ' ') {
290
+ event.stopPropagation();
291
+ event.preventDefault();
292
+
293
+ onPress?.(event);
294
+ } else {
295
+ return;
296
+ }
297
+ },
298
+ [onPress],
299
+ );
300
+
301
+ let element = (
302
+ <ItemContainer
303
+ id={id}
304
+ ref={forwardedRef}
305
+ {...hoverProps}
306
+ tabIndex={disabled ? undefined : 0}
307
+ onKeyDown={handleKeyDown}
308
+ >
309
+ <ContentContainer
310
+ disabled={disabled}
311
+ bordered={bordered}
312
+ selected={selected}
313
+ hovered={!disabled && hovered}
314
+ onClick={handleClick}
315
+ onDoubleClick={onDoubleClick}
316
+ onContextMenu={onContextMenu}
317
+ >
318
+ {children}
319
+ </ContentContainer>
320
+ {textPosition === 'below' && (
321
+ <>
322
+ <Spacer.Vertical size={8} />
323
+ <ItemTitle showBackground={false}>{title || ' '}</ItemTitle>
324
+ <ItemDescription showBackground={false}>
325
+ {subtitle || ' '}
326
+ </ItemDescription>
327
+ </>
328
+ )}
329
+ {textPosition === 'overlay' && hovered && (title || subtitle) && (
330
+ <TextOverlay>
331
+ {title && <ItemTitle showBackground>{title}</ItemTitle>}
332
+ {subtitle && (
333
+ <ItemDescription showBackground>{subtitle}</ItemDescription>
334
+ )}
335
+ </TextOverlay>
336
+ )}
337
+ {loading && (
338
+ <Shimmer>
339
+ <ActivityIndicator opacity={0.5} size={13} />
340
+ </Shimmer>
341
+ )}
342
+ </ItemContainer>
343
+ );
344
+
345
+ if (menuItems && onSelectMenuItem) {
346
+ element = (
347
+ <ContextMenu<MenuItemType> items={menuItems} onSelect={onSelectMenuItem}>
348
+ {element}
349
+ </ContextMenu>
350
+ );
351
+ }
352
+
353
+ if (textPosition === 'toolip') {
354
+ element = (
355
+ <Tooltip
356
+ content={
357
+ <Stack.V gap={2}>
358
+ <ItemTitle showBackground={false}>{title}</ItemTitle>
359
+ <ItemDescription showBackground={false}>{subtitle}</ItemDescription>
360
+ </Stack.V>
361
+ }
362
+ >
363
+ {element}
364
+ </Tooltip>
365
+ );
366
+ }
367
+
368
+ return element;
369
+ });
370
+
371
+ type GridViewContextValue = {
372
+ size: GridViewSize;
373
+ textPosition: 'overlay' | 'below' | 'toolip';
374
+ bordered: boolean;
375
+ disabled: boolean;
376
+ };
377
+
378
+ const GridViewContext = createContext<GridViewContextValue>({
379
+ size: 'small',
380
+ textPosition: 'below',
381
+ bordered: false,
382
+ disabled: false,
383
+ });
384
+
385
+ interface GridViewRootProps extends Partial<GridViewContextValue> {
386
+ children: ReactNode;
387
+ onClick?: () => void;
388
+ scrollable?: boolean;
389
+ }
390
+
391
+ function GridViewRoot({
392
+ size = 'small',
393
+ children,
394
+ scrollable = true,
395
+ onClick,
396
+ textPosition = 'below',
397
+ bordered = false,
398
+ disabled = false,
399
+ }: GridViewRootProps) {
400
+ const handleClick = useCallback(
401
+ (event: React.MouseEvent) => {
402
+ event.stopPropagation();
403
+ event.preventDefault();
404
+
405
+ onClick?.();
406
+ },
407
+ [onClick],
408
+ );
409
+
410
+ const contextValue = useMemo(
411
+ () => ({
412
+ size,
413
+ textPosition,
414
+ bordered,
415
+ disabled,
416
+ }),
417
+ [bordered, disabled, size, textPosition],
418
+ );
419
+
420
+ return (
421
+ <GridViewContext.Provider value={contextValue}>
422
+ <Container onClick={handleClick} scrollable={scrollable}>
423
+ {scrollable ? <ScrollArea>{children}</ScrollArea> : children}
424
+ </Container>
425
+ </GridViewContext.Provider>
426
+ );
427
+ }
428
+
429
+ function GridViewSection({
430
+ children,
431
+ padding = '20px',
432
+ }: {
433
+ children?: ReactNode;
434
+ padding?: CSSProperties['padding'];
435
+ }) {
436
+ const { size } = useContext(GridViewContext);
437
+
438
+ return (
439
+ <Grid size={size} padding={padding}>
440
+ {children}
441
+ </Grid>
442
+ );
443
+ }
444
+
445
+ function GridViewSectionHeader({ title }: { title: string }) {
446
+ const grouped = title.split('/');
447
+
448
+ return (
449
+ <SectionHeaderContainer>
450
+ <Spacer.Vertical size={24} />
451
+ {withSeparatorElements(
452
+ grouped.map((title, index) => (
453
+ <SectionTitle last={index === grouped.length - 1}>
454
+ {title}
455
+ </SectionTitle>
456
+ )),
457
+ <SectionTitle> / </SectionTitle>,
458
+ )}
459
+ </SectionHeaderContainer>
460
+ );
461
+ }
462
+
463
+ export namespace GridView {
464
+ export const Root = memo(GridViewRoot);
465
+ export const Item = memo(GridViewItem);
466
+ export const Section = memo(GridViewSection);
467
+ export const SectionHeader = memo(GridViewSectionHeader);
468
+ }
@@ -0,0 +1,46 @@
1
+ import * as Icons from '@noya-app/noya-icons';
2
+ import React, {
3
+ CSSProperties,
4
+ ForwardedRef,
5
+ forwardRef,
6
+ memo,
7
+ useMemo,
8
+ } from 'react';
9
+ import { useTheme } from 'styled-components';
10
+ import { Button, ButtonRootProps } from './Button';
11
+
12
+ type Props = Omit<ButtonRootProps, 'children' | 'variant' | 'flex' | 'size'> & {
13
+ iconName: keyof typeof Icons;
14
+ selected?: boolean;
15
+ color?: string;
16
+ size?: number;
17
+ };
18
+
19
+ export const IconButton = memo(
20
+ forwardRef(function IconButton(
21
+ { selected, iconName, color, size, contentStyle, ...props }: Props,
22
+ forwardedRef: ForwardedRef<HTMLButtonElement>,
23
+ ) {
24
+ const { icon: iconColor, iconSelected: iconSelectedColor } =
25
+ useTheme().colors;
26
+
27
+ const Icon = Icons[iconName];
28
+
29
+ const style = useMemo((): CSSProperties => {
30
+ return {
31
+ padding: '0 2px',
32
+ ...(size && { minHeight: size }),
33
+ ...contentStyle,
34
+ };
35
+ }, [contentStyle, size]);
36
+
37
+ return (
38
+ <Button ref={forwardedRef} {...props} variant="none" contentStyle={style}>
39
+ <Icon
40
+ color={color ?? (selected ? iconSelectedColor : iconColor)}
41
+ {...(size && { width: size, height: size })}
42
+ />
43
+ </Button>
44
+ );
45
+ }),
46
+ );