@noya-app/noya-designsystem 0.1.21 → 0.1.22

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noya-app/noya-designsystem",
3
- "version": "0.1.21",
3
+ "version": "0.1.22",
4
4
  "main": "./dist/index.js",
5
5
  "module": "./dist/index.mjs",
6
6
  "types": "./dist/index.d.ts",
@@ -13,9 +13,9 @@
13
13
  "@dnd-kit/core": "3.1.1",
14
14
  "@dnd-kit/modifiers": "3.0.0",
15
15
  "@dnd-kit/sortable": "4.0.0",
16
- "@noya-app/noya-colorpicker": "0.1.9",
17
- "@noya-app/noya-utils": "0.1.1",
18
- "@noya-app/noya-geometry": "0.1.3",
16
+ "@noya-app/noya-colorpicker": "0.1.10",
17
+ "@noya-app/noya-utils": "0.1.2",
18
+ "@noya-app/noya-geometry": "0.1.4",
19
19
  "@noya-app/noya-icons": "0.1.4",
20
20
  "@noya-app/noya-keymap": "0.1.2",
21
21
  "@radix-ui/primitive": "^1.0.0",
@@ -34,6 +34,7 @@
34
34
  "@radix-ui/react-toggle-group": "^1.0.1",
35
35
  "@radix-ui/react-tooltip": "^1.0.2",
36
36
  "@radix-ui/react-utils": "^0.0.5",
37
+ "@radix-ui/react-select": "^2.1.1",
37
38
  "immer": "9.0.5",
38
39
  "kiwi.js": "^1.1.3",
39
40
  "react-resizable-panels": "2.0.22",
@@ -1,5 +1,5 @@
1
1
  import { Cross1Icon, PlusIcon } from "@noya-app/noya-icons";
2
- import React, { memo } from "react";
2
+ import React, { forwardRef, memo } from "react";
3
3
  import styled from "styled-components";
4
4
  import { useDesignSystemTheme } from "../contexts/DesignSystemConfiguration";
5
5
  import { useHover } from "../hooks/useHover";
@@ -141,10 +141,13 @@ const AddElement = styled(PlusIcon).withConfig({
141
141
  }));
142
142
 
143
143
  export interface ChipProps {
144
+ id?: string;
145
+ className?: string;
144
146
  colorScheme?: ChipColorScheme;
145
147
  variant?: ChipVariant;
146
148
  size?: ChipSize;
147
149
  children?: React.ReactNode;
150
+ clickable?: boolean;
148
151
  deletable?: boolean;
149
152
  addable?: boolean;
150
153
  monospace?: boolean;
@@ -156,78 +159,87 @@ export interface ChipProps {
156
159
  tabIndex?: number;
157
160
  }
158
161
 
159
- export const Chip = memo(function Chip({
160
- colorScheme,
161
- children,
162
- deletable,
163
- addable,
164
- style,
165
- size = "medium",
166
- variant = "solid",
167
- monospace = false,
168
- tabIndex,
169
- onDelete,
170
- onClick,
171
- onAdd,
172
- onHoverDeleteChange,
173
- }: ChipProps) {
174
- const theme = useDesignSystemTheme();
175
-
176
- const { hoverProps: hoverDeleteProps } = useHover({
177
- onHoverChange: onHoverDeleteChange,
178
- });
179
-
180
- const handleClick = !children && !deletable && addable ? onAdd : onClick;
181
-
182
- const color =
183
- colorScheme === "primary"
184
- ? theme.colors.primary
185
- : colorScheme === "secondary"
186
- ? theme.colors.secondary
187
- : theme.colors.text;
188
-
189
- return (
190
- <ChipElement
191
- $variant={variant}
192
- $colorScheme={colorScheme}
193
- style={style}
194
- onClick={handleClick}
195
- $size={size}
196
- $monospace={monospace}
197
- $isInteractive={!!handleClick}
198
- tabIndex={tabIndex}
199
- >
200
- {addable && (
201
- <AddElement
202
- size={size}
203
- isOnlyChild={!children && !deletable}
204
- onClick={
205
- onAdd
206
- ? (event) => {
207
- event.stopPropagation();
208
- onAdd?.();
209
- }
210
- : undefined
211
- }
212
- color={color}
213
- />
214
- )}
215
- {children}
216
- {deletable && (
217
- <DeleteElement
218
- size={size}
219
- {...(hoverDeleteProps as any)}
220
- onClick={
221
- onDelete
222
- ? (event) => {
223
- event.stopPropagation();
224
- onDelete?.();
225
- }
226
- : undefined
227
- }
228
- color={color}
229
- />
230
- )}
231
- </ChipElement>
232
- );
233
- });
162
+ export const Chip = memo(
163
+ forwardRef(function Chip(
164
+ {
165
+ colorScheme,
166
+ children,
167
+ deletable,
168
+ addable,
169
+ clickable,
170
+ style,
171
+ size = "medium",
172
+ variant = "solid",
173
+ monospace = false,
174
+ tabIndex,
175
+ onDelete,
176
+ onClick,
177
+ onAdd,
178
+ onHoverDeleteChange,
179
+ ...rest
180
+ }: ChipProps,
181
+ forwardedRef: React.ForwardedRef<HTMLSpanElement>
182
+ ) {
183
+ const theme = useDesignSystemTheme();
184
+
185
+ const { hoverProps: hoverDeleteProps } = useHover({
186
+ onHoverChange: onHoverDeleteChange,
187
+ });
188
+
189
+ const handleClick = !children && !deletable && addable ? onAdd : onClick;
190
+
191
+ const color =
192
+ colorScheme === "primary"
193
+ ? theme.colors.primary
194
+ : colorScheme === "secondary"
195
+ ? theme.colors.secondary
196
+ : theme.colors.text;
197
+
198
+ return (
199
+ <ChipElement
200
+ ref={forwardedRef}
201
+ $variant={variant}
202
+ $colorScheme={colorScheme}
203
+ style={style}
204
+ onClick={handleClick}
205
+ $size={size}
206
+ $monospace={monospace}
207
+ $isInteractive={!!handleClick || clickable === true}
208
+ tabIndex={tabIndex}
209
+ {...rest}
210
+ >
211
+ {addable && (
212
+ <AddElement
213
+ size={size}
214
+ isOnlyChild={!children && !deletable}
215
+ onClick={
216
+ onAdd
217
+ ? (event) => {
218
+ event.stopPropagation();
219
+ onAdd?.();
220
+ }
221
+ : undefined
222
+ }
223
+ color={color}
224
+ />
225
+ )}
226
+ {children}
227
+ {deletable && (
228
+ <DeleteElement
229
+ size={size}
230
+ {...(hoverDeleteProps as any)}
231
+ onClick={
232
+ onDelete
233
+ ? (event) => {
234
+ event.stopPropagation();
235
+ onDelete?.();
236
+ }
237
+ : undefined
238
+ }
239
+ color={color}
240
+ />
241
+ )}
242
+ </ChipElement>
243
+ );
244
+ })
245
+ );
@@ -1,15 +1,9 @@
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';
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, { ReactNode, memo, useCallback, useMemo } from "react";
5
+ import styled from "styled-components";
6
+ import { Spacer } from "./Spacer";
13
7
  import {
14
8
  CHECKBOX_RIGHT_INSET,
15
9
  CHECKBOX_WIDTH,
@@ -18,14 +12,14 @@ import {
18
12
  SEPARATOR_ITEM,
19
13
  getKeyboardShortcutsForMenuItems,
20
14
  styles,
21
- } from './internal/Menu';
15
+ } from "./internal/Menu";
22
16
 
23
17
  /* ----------------------------------------------------------------------------
24
18
  * Separator
25
19
  * ------------------------------------------------------------------------- */
26
20
 
27
21
  const SeparatorElement = styled(RadixContextMenu.Separator)(
28
- styles.separatorStyle,
22
+ styles.separatorStyle
29
23
  );
30
24
 
31
25
  /* ----------------------------------------------------------------------------
@@ -35,11 +29,11 @@ const SeparatorElement = styled(RadixContextMenu.Separator)(
35
29
  const ItemElement = styled(RadixContextMenu.Item)(styles.itemStyle);
36
30
 
37
31
  const CheckboxItemElement = styled(RadixContextMenu.CheckboxItem)(
38
- styles.itemStyle,
32
+ styles.itemStyle
39
33
  );
40
34
 
41
35
  const StyledItemIndicator = styled(RadixContextMenu.ItemIndicator)(
42
- styles.itemIndicatorStyle,
36
+ styles.itemIndicatorStyle
43
37
  );
44
38
 
45
39
  export interface MenuItemProps<T extends string> {
@@ -50,7 +44,7 @@ export interface MenuItemProps<T extends string> {
50
44
  disabled: boolean;
51
45
  indented: boolean;
52
46
  shortcut?: string;
53
- icon?: ReactElement;
47
+ icon?: ReactNode;
54
48
  items?: MenuItem<T>[];
55
49
  }
56
50
 
@@ -69,7 +63,7 @@ const ContextMenuItem = memo(function ContextMenuItem<T extends string>({
69
63
  // context menu unless we stop it here.
70
64
  const handlePointerDown = useCallback(
71
65
  (event: React.PointerEvent) => event.stopPropagation(),
72
- [],
66
+ []
73
67
  );
74
68
 
75
69
  const handleSelectItem = useCallback(() => {
@@ -162,7 +156,7 @@ function ContextMenuRoot<T extends string>({
162
156
  onOpenChange,
163
157
  }: MenuProps<T>) {
164
158
  const hasCheckedItem = items.some(
165
- (item) => item !== SEPARATOR_ITEM && item.checked,
159
+ (item) => item !== SEPARATOR_ITEM && item.checked
166
160
  );
167
161
 
168
162
  const keymap = useMemo(
@@ -170,7 +164,7 @@ function ContextMenuRoot<T extends string>({
170
164
  isNested || shouldBindKeyboardShortcuts === false
171
165
  ? {}
172
166
  : getKeyboardShortcutsForMenuItems(items, onSelect),
173
- [isNested, items, onSelect, shouldBindKeyboardShortcuts],
167
+ [isNested, items, onSelect, shouldBindKeyboardShortcuts]
174
168
  );
175
169
 
176
170
  useKeyboardShortcuts(keymap);
@@ -213,7 +207,7 @@ function ContextMenuRoot<T extends string>({
213
207
  >
214
208
  {item.title}
215
209
  </ContextMenuItem>
216
- ),
210
+ )
217
211
  )}
218
212
  </ContentComponent>
219
213
  </RadixContextMenu.Portal>
@@ -15,6 +15,7 @@ import React, {
15
15
  useLayoutEffect,
16
16
  useMemo,
17
17
  useRef,
18
+ useState,
18
19
  } from "react";
19
20
  import styled from "styled-components";
20
21
  import handleNudge from "../utils/handleNudge";
@@ -431,10 +432,15 @@ function parseNumber(value: string) {
431
432
  }
432
433
 
433
434
  function InputFieldNumberInput(props: InputFieldNumberInputProps) {
434
- const { value, placeholder, onNudge, ...rest } = props;
435
+ const { value, placeholder, onNudge, onBlur, ...rest } = props;
435
436
  const onSubmit = "onSubmit" in props ? props.onSubmit : undefined;
436
437
  const onChange = "onChange" in props ? props.onChange : undefined;
437
438
 
439
+ // Internal value is used to handle empty string as a valid state,
440
+ // e.g. when the user deletes the value before entering a new one.
441
+ // This is only used for in the onChange case.
442
+ const [internalValue, setInternalValue] = useState<string | undefined>();
443
+
438
444
  const handleSubmit = useCallback(
439
445
  (value: string) => {
440
446
  const newValue = parseNumber(value);
@@ -453,6 +459,7 @@ function InputFieldNumberInput(props: InputFieldNumberInputProps) {
453
459
  if (!amount) return;
454
460
 
455
461
  onNudge?.(amount);
462
+ setInternalValue(undefined);
456
463
 
457
464
  event.preventDefault();
458
465
  event.stopPropagation();
@@ -462,17 +469,48 @@ function InputFieldNumberInput(props: InputFieldNumberInputProps) {
462
469
 
463
470
  const handleChange = useCallback(
464
471
  (value: string) => {
465
- onChange?.(parseNumber(value));
472
+ if (value === "" || value === "-" || value === ".") {
473
+ setInternalValue(value);
474
+ return;
475
+ }
476
+
477
+ if (value.endsWith(".")) {
478
+ setInternalValue(value);
479
+ return;
480
+ }
481
+
482
+ setInternalValue(undefined);
483
+
484
+ const newValue = parseNumber(value);
485
+
486
+ if (!isNaN(newValue)) {
487
+ onChange?.(newValue);
488
+ }
466
489
  },
467
490
  [onChange]
468
491
  );
469
492
 
493
+ const handleBlur = useCallback(
494
+ (event: React.FocusEvent<HTMLInputElement>) => {
495
+ setInternalValue(undefined);
496
+ onBlur?.(event);
497
+ },
498
+ [onBlur]
499
+ );
500
+
470
501
  return (
471
502
  <InputFieldInput
472
503
  {...rest}
473
- value={value === undefined ? "" : String(value)}
504
+ value={
505
+ internalValue !== undefined
506
+ ? String(internalValue)
507
+ : value === undefined
508
+ ? ""
509
+ : String(value)
510
+ }
474
511
  placeholder={placeholder}
475
512
  onKeyDown={handleKeyDown}
513
+ onBlur={handleBlur}
476
514
  {...("onChange" in props
477
515
  ? { onChange: handleChange }
478
516
  : { onSubmit: handleSubmit })}
@@ -676,6 +676,9 @@ type ListViewVariant = "normal" | "padded" | "bare";
676
676
  type ListViewSectionHeaderVariant = "normal" | "label";
677
677
 
678
678
  type ListViewRootProps = {
679
+ id?: string;
680
+ className?: string;
681
+ style?: CSSProperties;
679
682
  onPress?: () => void;
680
683
  scrollable?: boolean;
681
684
  expandable?: boolean;
@@ -696,6 +699,9 @@ type ListViewRootProps = {
696
699
 
697
700
  const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
698
701
  {
702
+ id,
703
+ className,
704
+ style,
699
705
  onPress,
700
706
  scrollable = false,
701
707
  expandable = true,
@@ -895,6 +901,9 @@ const ListViewRootInner = forwardRef(function ListViewRootInner<T>(
895
901
  return (
896
902
  <DraggingContext.Provider value={draggingContextValue}>
897
903
  <RootContainer
904
+ id={id}
905
+ className={className}
906
+ style={style}
898
907
  {...{
899
908
  [pressEventName]: handleClick,
900
909
  }}
@@ -93,12 +93,12 @@ export function Popover({
93
93
  collisionPadding={8}
94
94
  arrowPadding={2}
95
95
  // Don't propagate pointer down events to the canvas
96
- onPointerDown={(event) => {
97
- event.stopPropagation();
98
- }}
99
- onWheel={(event) => {
100
- event.stopPropagation();
101
- }}
96
+ // onPointerDown={(event) => {
97
+ // event.stopPropagation();
98
+ // }}
99
+ // onWheel={(event) => {
100
+ // event.stopPropagation();
101
+ // }}
102
102
  >
103
103
  {children}
104
104
  {showArrow && <ArrowElement />}
@@ -120,7 +120,7 @@ type Props<T extends string> = ChildrenProps<T> & {
120
120
  id: string;
121
121
  flex?: CSSProperties["flex"];
122
122
  value: T;
123
- label?: string;
123
+ label?: ReactNode;
124
124
  };
125
125
 
126
126
  export const Select = memo(function Select<T extends string>({
@@ -0,0 +1,100 @@
1
+ import { DropdownChevronIcon } from "@noya-app/noya-icons";
2
+ import * as Select from "@radix-ui/react-select";
3
+ import React, { memo } from "react";
4
+ import { styled } from "styled-components";
5
+ import { Button } from "./Button";
6
+ import { Spacer } from "./Spacer";
7
+ import { MenuItem, RegularMenuItem, styles } from "./internal/Menu";
8
+
9
+ type Props<T extends string> = {
10
+ id?: string;
11
+ style?: React.CSSProperties;
12
+ className?: string;
13
+ menuItems: MenuItem<T>[];
14
+ value: T;
15
+ onSelect?: (value: T) => void;
16
+ placeholder?: string;
17
+ };
18
+
19
+ export const SelectMenu = memo(function SelectMenu<T extends string = string>({
20
+ id,
21
+ style,
22
+ className,
23
+ menuItems,
24
+ value,
25
+ onSelect,
26
+ placeholder,
27
+ }: Props<T>) {
28
+ const selectedItem = menuItems.find(
29
+ (item): item is RegularMenuItem<T> =>
30
+ typeof item !== "string" && item.value === value
31
+ );
32
+ const icon = selectedItem?.icon;
33
+
34
+ return (
35
+ <Select.Root value={value} onValueChange={onSelect}>
36
+ <Select.SelectTrigger asChild>
37
+ <Button id={id} style={style} className={className}>
38
+ {icon && (
39
+ <>
40
+ {icon}
41
+ <Spacer.Horizontal size={8} />
42
+ </>
43
+ )}
44
+ <Select.Value placeholder={placeholder} />
45
+ <Spacer.Horizontal />
46
+ <DropdownChevronIcon />
47
+ </Button>
48
+ </Select.SelectTrigger>
49
+ <Select.Portal>
50
+ <SelectContent>
51
+ <SelectViewport>
52
+ {menuItems.map((menuItem) => {
53
+ if (typeof menuItem === "string") {
54
+ return <StyledSeparator />;
55
+ }
56
+
57
+ const value = menuItem.value ?? "";
58
+
59
+ return (
60
+ <SelectItem key={value} value={value} icon={menuItem.icon}>
61
+ {menuItem.title ?? value}
62
+ </SelectItem>
63
+ );
64
+ })}
65
+ </SelectViewport>
66
+ </SelectContent>
67
+ </Select.Portal>
68
+ </Select.Root>
69
+ );
70
+ });
71
+
72
+ const SelectContent = styled(Select.Content)(styles.contentStyle);
73
+
74
+ const SelectViewport = styled(Select.Viewport)({});
75
+
76
+ const SelectItem = React.forwardRef(
77
+ (
78
+ {
79
+ children,
80
+ icon,
81
+ ...props
82
+ }: Select.SelectItemProps & { icon?: React.ReactNode },
83
+ forwardedRef: React.ForwardedRef<HTMLDivElement>
84
+ ) => {
85
+ return (
86
+ <StyledItem {...props} ref={forwardedRef}>
87
+ {icon && (
88
+ <>
89
+ {icon}
90
+ <Spacer.Horizontal size={8} />
91
+ </>
92
+ )}
93
+ <Select.ItemText>{children}</Select.ItemText>
94
+ </StyledItem>
95
+ );
96
+ }
97
+ );
98
+
99
+ const StyledItem = styled(Select.Item)(styles.itemStyle);
100
+ const StyledSeparator = styled(Select.Separator)(styles.separatorStyle);
@@ -89,7 +89,17 @@ const Element = styled.div<{
89
89
  }));
90
90
 
91
91
  const StackBase = forwardRef(function StackBase(
92
- { id, as, children, separator, breakpoints, tabIndex, href, ...rest }: Props,
92
+ {
93
+ id,
94
+ className,
95
+ as,
96
+ children,
97
+ separator,
98
+ breakpoints,
99
+ tabIndex,
100
+ href,
101
+ ...rest
102
+ }: Props,
93
103
  forwardedRef: ForwardedRef<HTMLElement>
94
104
  ) {
95
105
  const elements = separator
@@ -107,6 +117,7 @@ const StackBase = forwardRef(function StackBase(
107
117
  <Element
108
118
  ref={forwardedRef as any}
109
119
  id={id}
120
+ className={className}
110
121
  as={as}
111
122
  $styleProps={styleProps}
112
123
  $breakpoints={breakpoints}
@@ -61,6 +61,8 @@ interface Props extends StyleProps {
61
61
  children: ReactNode;
62
62
  onClick?: () => void;
63
63
  onDoubleClick?: () => void;
64
+ onKeyDown?: (event: React.KeyboardEvent<HTMLSpanElement>) => void;
65
+ tabIndex?: number;
64
66
  }
65
67
 
66
68
  const StyledElement = styled.span<{
@@ -84,7 +86,9 @@ export const Text = forwardRef(function Text(
84
86
  className,
85
87
  color,
86
88
  href,
89
+ tabIndex,
87
90
  onClick,
91
+ onKeyDown,
88
92
  ...rest
89
93
  }: Props,
90
94
  forwardedRef: ForwardedRef<HTMLElement>
@@ -96,11 +100,13 @@ export const Text = forwardRef(function Text(
96
100
  ref={forwardedRef}
97
101
  as={element}
98
102
  className={className}
103
+ tabIndex={tabIndex}
99
104
  $variant={variant}
100
105
  $breakpoints={breakpoints}
101
106
  $styleProps={rest}
102
107
  $color={color}
103
108
  onClick={onClick}
109
+ onKeyDown={onKeyDown}
104
110
  {...((href && { href }) as any)}
105
111
  >
106
112
  {children}
@@ -5,10 +5,10 @@ import React, {
5
5
  ReactNode,
6
6
  useCallback,
7
7
  useContext,
8
- } from 'react';
9
- import { IconButton } from './IconButton';
10
- import { ListView } from './ListView';
11
- import { Spacer } from './Spacer';
8
+ } from "react";
9
+ import { IconButton } from "./IconButton";
10
+ import { ListView } from "./ListView";
11
+ import { Spacer } from "./Spacer";
12
12
 
13
13
  /* ----------------------------------------------------------------------------
14
14
  * Row
@@ -31,7 +31,7 @@ const TreeRow = forwardRef(function TreeRow<MenuItemType extends string>(
31
31
  children,
32
32
  ...rest
33
33
  }: TreeViewRowProps<MenuItemType>,
34
- forwardedRef: ForwardedRef<HTMLLIElement>,
34
+ forwardedRef: ForwardedRef<HTMLLIElement>
35
35
  ) {
36
36
  const { expandable } = useContext(ListView.RowContext);
37
37
 
@@ -40,7 +40,7 @@ const TreeRow = forwardRef(function TreeRow<MenuItemType extends string>(
40
40
  event.stopPropagation();
41
41
  onClickChevron?.({ altKey: event.altKey });
42
42
  },
43
- [onClickChevron],
43
+ [onClickChevron]
44
44
  );
45
45
 
46
46
  return (
@@ -51,7 +51,7 @@ const TreeRow = forwardRef(function TreeRow<MenuItemType extends string>(
51
51
  <Spacer.Horizontal size={19} />
52
52
  ) : (
53
53
  <IconButton
54
- iconName={expanded ? 'ChevronDownIcon' : 'ChevronRightIcon'}
54
+ iconName={expanded ? "ChevronDownIcon2" : "ChevronRightIcon2"}
55
55
  onClick={handleClickChevron}
56
56
  selected={rest.selected}
57
57
  />
@@ -201,6 +201,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
201
201
  style={{
202
202
  display: "flex",
203
203
  flexDirection: "row",
204
+ position: "relative",
204
205
  }}
205
206
  >
206
207
  {leftPanelContent}
@@ -223,6 +224,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
223
224
  style={{
224
225
  display: "flex",
225
226
  flexDirection: "row",
227
+ position: "relative",
226
228
  }}
227
229
  >
228
230
  {centerPanelContent}
@@ -247,6 +249,7 @@ export const WorkspaceLayout = forwardRef(function WorkspaceLayout(
247
249
  style={{
248
250
  display: "flex",
249
251
  flexDirection: "row",
252
+ position: "relative",
250
253
  }}
251
254
  >
252
255
  {rightPanelContent}