@noya-app/noya-designsystem 0.1.41 → 0.1.43

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 (53) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +16 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +268 -147
  5. package/dist/index.d.ts +268 -147
  6. package/dist/index.js +7618 -6908
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +6161 -5487
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +4 -5
  11. package/src/__tests__/combobox.test.ts +578 -0
  12. package/src/components/ActivityIndicator.tsx +4 -4
  13. package/src/components/AnimatePresence.tsx +5 -5
  14. package/src/components/Avatar.tsx +5 -2
  15. package/src/components/BaseToolbar.tsx +61 -0
  16. package/src/components/Button.tsx +1 -1
  17. package/src/components/Checkbox.tsx +6 -5
  18. package/src/components/Combobox.tsx +327 -337
  19. package/src/components/ComboboxMenu.tsx +88 -0
  20. package/src/components/Dialog.tsx +10 -6
  21. package/src/components/DimensionInput.tsx +4 -4
  22. package/src/components/FillPreviewBackground.tsx +51 -44
  23. package/src/components/FloatingWindow.tsx +5 -2
  24. package/src/components/GradientPicker.tsx +14 -14
  25. package/src/components/IconButton.tsx +6 -12
  26. package/src/components/Icons.tsx +3 -3
  27. package/src/components/InputField.tsx +145 -160
  28. package/src/components/Label.tsx +4 -4
  29. package/src/components/LabeledElementView.tsx +35 -41
  30. package/src/components/ListView.tsx +49 -39
  31. package/src/components/Message.tsx +75 -0
  32. package/src/components/Progress.tsx +2 -2
  33. package/src/components/ScrollArea.tsx +7 -5
  34. package/src/components/SegmentedControl.tsx +171 -0
  35. package/src/components/SelectMenu.tsx +61 -10
  36. package/src/components/Slider.tsx +5 -2
  37. package/src/components/Sortable.tsx +17 -27
  38. package/src/components/Spacer.tsx +28 -9
  39. package/src/components/Switch.tsx +8 -8
  40. package/src/components/TextArea.tsx +59 -12
  41. package/src/components/Toolbar.tsx +129 -0
  42. package/src/components/Tooltip.tsx +10 -7
  43. package/src/components/WorkspaceLayout.tsx +145 -152
  44. package/src/components/internal/Menu.tsx +5 -4
  45. package/src/contexts/DesignSystemConfiguration.tsx +15 -24
  46. package/src/contexts/DialogContext.tsx +137 -49
  47. package/src/contexts/ImageDataContext.tsx +6 -12
  48. package/src/index.css +1 -3
  49. package/src/index.tsx +12 -3
  50. package/src/utils/combobox.ts +369 -0
  51. package/tailwind.config.ts +1 -2
  52. package/src/components/RadioGroup.tsx +0 -100
  53. package/src/utils/completions.ts +0 -21
@@ -6,7 +6,12 @@ import {
6
6
  import { memoGeneric } from "@noya-app/react-utils";
7
7
  import * as Select from "@radix-ui/react-select";
8
8
  import { type SelectProps } from "@radix-ui/react-select";
9
- import React, { CSSProperties, FocusEventHandler, useMemo } from "react";
9
+ import React, {
10
+ CSSProperties,
11
+ FocusEventHandler,
12
+ useMemo,
13
+ useState,
14
+ } from "react";
10
15
  import { cx } from "../utils/classNames";
11
16
  import { Button } from "./Button";
12
17
  import { renderIcon } from "./Icons";
@@ -26,14 +31,15 @@ type Props<T extends string> = {
26
31
  label?: React.ReactNode;
27
32
  onFocus?: FocusEventHandler;
28
33
  onBlur?: FocusEventHandler;
29
- } & Pick<SelectProps, "open">;
34
+ } & Pick<SelectProps, "open" | "onOpenChange">;
30
35
 
31
36
  const readOnlyStyle: CSSProperties = {
32
37
  justifyContent: "flex-start",
33
38
  textAlign: "left",
34
39
  };
35
40
 
36
- const labelStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%] pointer-events-none flex items-center absolute top-0 bottom-0 right-[1.3rem] z-label`;
41
+ export const labelTextStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%]`;
42
+ const labelStyles = `pointer-events-none flex items-center absolute top-0 bottom-0 right-[1.3rem] z-label`;
37
43
 
38
44
  const scrollButtonStyles = `
39
45
  flex
@@ -61,12 +67,21 @@ export const SelectMenu = memoGeneric(function SelectMenu<
61
67
  open,
62
68
  onFocus,
63
69
  onBlur,
70
+ onOpenChange,
64
71
  }: Props<T>) {
65
72
  const selectedItem = menuItems.find(
66
73
  (item): item is RegularMenuItem<T> =>
67
74
  typeof item !== "string" && item.value === value
68
75
  );
69
76
  const icon = selectedItem?.icon;
77
+ const [internalOpen, setInternalOpen] = useState(open);
78
+
79
+ const handleOpenChange = (open: boolean) => {
80
+ if (onOpenChange) {
81
+ onOpenChange(open);
82
+ }
83
+ setInternalOpen(open);
84
+ };
70
85
 
71
86
  const readOnlyButton = useMemo(
72
87
  () => (
@@ -95,7 +110,11 @@ export const SelectMenu = memoGeneric(function SelectMenu<
95
110
  <Button
96
111
  id={id}
97
112
  style={style}
98
- className={`${className ?? "w-full"} flex-1 focus:z-interactable relative`}
113
+ className={cx(
114
+ className ?? "w-full",
115
+ "flex-1 focus:z-interactable relative",
116
+ internalOpen && "ring-2 ring-primary"
117
+ )}
99
118
  disabled={disabled}
100
119
  >
101
120
  {icon && (
@@ -108,15 +127,31 @@ export const SelectMenu = memoGeneric(function SelectMenu<
108
127
  <Select.Value placeholder={placeholder} />
109
128
  </span>
110
129
  {label && (
111
- <label htmlFor={id} className={labelStyles}>
130
+ <label htmlFor={id} className={cx(labelStyles, labelTextStyles)}>
112
131
  {label}
113
132
  </label>
114
133
  )}
115
- <DropdownChevronIcon />
134
+ <DropdownChevronIcon
135
+ className={cx(
136
+ "transition-transform duration-100",
137
+ internalOpen ? "rotate-180" : "rotate-0"
138
+ )}
139
+ />
116
140
  </Button>
117
141
  </Select.SelectTrigger>
118
142
  ),
119
- [onFocus, onBlur, id, style, className, disabled, icon, placeholder, label]
143
+ [
144
+ onFocus,
145
+ onBlur,
146
+ id,
147
+ style,
148
+ className,
149
+ disabled,
150
+ icon,
151
+ placeholder,
152
+ label,
153
+ internalOpen,
154
+ ]
120
155
  );
121
156
 
122
157
  if (readOnly) {
@@ -133,12 +168,28 @@ export const SelectMenu = memoGeneric(function SelectMenu<
133
168
  }
134
169
 
135
170
  return (
136
- <Select.Root value={value} onValueChange={onSelect} open={open}>
171
+ <Select.Root
172
+ value={value}
173
+ onValueChange={onSelect}
174
+ open={open}
175
+ onOpenChange={handleOpenChange}
176
+ >
137
177
  {trigger}
138
178
  <Select.Portal>
139
- <Select.Content className={styles.contentStyle}>
179
+ <Select.Content
180
+ className={styles.contentStyle}
181
+ position="popper"
182
+ sideOffset={6}
183
+ collisionPadding={8}
184
+ align="end"
185
+ style={{
186
+ width: "var(--radix-select-trigger-width)",
187
+ }}
188
+ >
140
189
  <Select.ScrollUpButton className={scrollButtonStyles}>
141
- <ChevronUpIcon />
190
+ <ChevronUpIcon
191
+ className={cx("transition-transform duration-100")}
192
+ />
142
193
  </Select.ScrollUpButton>
143
194
  <Select.Viewport>
144
195
  {menuItems.map((menuItem) => {
@@ -17,6 +17,7 @@ interface Props {
17
17
  label?: React.ReactNode;
18
18
  onFocus?: FocusEventHandler;
19
19
  onBlur?: FocusEventHandler;
20
+ readOnly?: boolean;
20
21
  }
21
22
 
22
23
  const labelStyles = `font-sans text-label uppercase text-text-disabled leading-[19px] font-bold text-[60%] pointer-events-none flex items-center z-label`;
@@ -40,6 +41,7 @@ export const Slider = memo(function Slider({
40
41
  label,
41
42
  onFocus,
42
43
  onBlur,
44
+ readOnly = false,
43
45
  }: Props) {
44
46
  const arrayValue = useMemo(
45
47
  () => [Math.min(Math.max(value, min), max)],
@@ -88,12 +90,13 @@ export const Slider = memo(function Slider({
88
90
  value={arrayValue}
89
91
  onValueChange={handleValueChange}
90
92
  className={cx(
91
- "flex relative items-center select-none touch-none h-[27px] rounded overflow-hidden",
93
+ "flex relative items-center select-none touch-none h-[27px] rounded overflow-hidden w-full",
92
94
  className
93
95
  )}
94
96
  style={style}
95
97
  onFocus={onFocus}
96
98
  onBlur={onBlur}
99
+ disabled={readOnly}
97
100
  >
98
101
  <RadixSlider.Track className="relative flex-grow h-full rounded overflow-hidden bg-input-background">
99
102
  {label && (
@@ -117,7 +120,7 @@ export const Slider = memo(function Slider({
117
120
  <RadixSlider.Thumb
118
121
  style={thumbStyle}
119
122
  className={cx(
120
- "block h-[27px] rounded border border-solid bg-slider-thumb-background focus:outline-none cursor-ew-resize",
123
+ "block h-[27px] rounded border border-solid bg-slider-thumb-background transition-colors focus:border-primary focus:border-2 outline-none",
121
124
  colorScheme === undefined && "border-slider-border",
122
125
  colorScheme === "primary" && "border-primary",
123
126
  colorScheme === "secondary" && "border-secondary"
@@ -16,17 +16,7 @@ import {
16
16
  verticalListSortingStrategy,
17
17
  } from "@dnd-kit/sortable";
18
18
  import { memoGeneric } from "@noya-app/react-utils";
19
- import React, {
20
- createContext,
21
- ReactNode,
22
- Ref,
23
- useCallback,
24
- useContext,
25
- useEffect,
26
- useMemo,
27
- useRef,
28
- useState,
29
- } from "react";
19
+ import * as React from "react";
30
20
  import { createPortal } from "react-dom";
31
21
 
32
22
  export type RelativeDropPosition = "above" | "below" | "inside";
@@ -60,7 +50,7 @@ const defaultAcceptsDrop: DropValidator = (
60
50
  return true;
61
51
  };
62
52
 
63
- const SortableItemContext = createContext<{
53
+ const SortableItemContext = React.createContext<{
64
54
  keys: string[];
65
55
  position: Translate;
66
56
  acceptsDrop: DropValidator;
@@ -119,7 +109,7 @@ interface ItemProps<T> {
119
109
  disabled?: boolean;
120
110
  children: (
121
111
  props: {
122
- ref: Ref<T>;
112
+ ref: React.Ref<T>;
123
113
  relativeDropPosition?: RelativeDropPosition;
124
114
  [key: string]: any;
125
115
  } & UseSortableReturnType["attributes"]
@@ -132,7 +122,7 @@ function SortableItem<T extends HTMLElement>({
132
122
  children,
133
123
  }: ItemProps<T>) {
134
124
  const { keys, position, acceptsDrop, setActivatorEvent, axis } =
135
- useContext(SortableItemContext);
125
+ React.useContext(SortableItemContext);
136
126
  const sortable = useSortable({ id, disabled });
137
127
 
138
128
  const {
@@ -156,7 +146,7 @@ function SortableItem<T extends HTMLElement>({
156
146
  const offsetLeft = eventX + position.x;
157
147
  const offsetTop = eventY + position.y;
158
148
 
159
- const ref = useCallback((node: T) => setNodeRef(node), [setNodeRef]);
149
+ const ref = React.useCallback((node: T) => setNodeRef(node), [setNodeRef]);
160
150
 
161
151
  return children({
162
152
  ref,
@@ -183,8 +173,8 @@ function SortableItem<T extends HTMLElement>({
183
173
 
184
174
  interface RootProps {
185
175
  keys: string[];
186
- children: ReactNode;
187
- renderOverlay?: (index: number) => ReactNode;
176
+ children: React.ReactNode;
177
+ renderOverlay?: (index: number) => React.ReactNode;
188
178
  onMoveItem?: (
189
179
  sourceIndex: number,
190
180
  destinationIndex: number,
@@ -210,27 +200,27 @@ function SortableRoot({
210
200
  })
211
201
  );
212
202
 
213
- const [activeIndex, setActiveIndex] = useState<number | undefined>();
214
- const activatorEvent = useRef<PointerEvent | null>(null);
203
+ const [activeIndex, setActiveIndex] = React.useState<number | undefined>();
204
+ const activatorEvent = React.useRef<PointerEvent | null>(null);
215
205
 
216
- const setActivatorEvent = useCallback((event: PointerEvent) => {
206
+ const setActivatorEvent = React.useCallback((event: PointerEvent) => {
217
207
  activatorEvent.current = event;
218
208
  }, []);
219
209
 
220
- const [position, setPosition] = useState<Translate>({ x: 0, y: 0 });
210
+ const [position, setPosition] = React.useState<Translate>({ x: 0, y: 0 });
221
211
 
222
- const handleDragStart = useCallback(
212
+ const handleDragStart = React.useCallback(
223
213
  (event: DragStartEvent) => {
224
214
  setActiveIndex(keys.indexOf(event.active.id));
225
215
  },
226
216
  [keys]
227
217
  );
228
218
 
229
- const handleDragMove = useCallback((event: DragMoveEvent) => {
219
+ const handleDragMove = React.useCallback((event: DragMoveEvent) => {
230
220
  setPosition({ ...event.delta });
231
221
  }, []);
232
222
 
233
- const handleDragEnd = useCallback(
223
+ const handleDragEnd = React.useCallback(
234
224
  (event: DragEndEvent) => {
235
225
  const { active, over } = event;
236
226
 
@@ -263,15 +253,15 @@ function SortableRoot({
263
253
  [acceptsDrop, axis, keys, onMoveItem, position.x, position.y]
264
254
  );
265
255
 
266
- const [mounted, setMounted] = useState(false);
256
+ const [mounted, setMounted] = React.useState(false);
267
257
 
268
- useEffect(() => {
258
+ React.useEffect(() => {
269
259
  setMounted(true);
270
260
  }, []);
271
261
 
272
262
  return (
273
263
  <SortableItemContext.Provider
274
- value={useMemo(
264
+ value={React.useMemo(
275
265
  () => ({
276
266
  keys,
277
267
  acceptsDrop,
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable react/jsx-pascal-case */
2
- import React, { ForwardedRef, forwardRef } from "react";
2
+ import type { ForwardedRef } from "react";
3
+ import * as React from "react";
3
4
 
4
5
  interface Props {
5
6
  size?: number | string;
@@ -10,12 +11,21 @@ interface Props {
10
11
  * Vertical
11
12
  * ------------------------------------------------------------------------- */
12
13
 
13
- const SpacerVertical = forwardRef<HTMLSpanElement, Props>(
14
+ const SpacerVertical = React.forwardRef<HTMLSpanElement, Props>(
14
15
  ({ size, inline, ...props }, ref: ForwardedRef<HTMLSpanElement>) => {
15
16
  return (
16
- <span className={`${inline ? "inline-block" : "block"} ${size === undefined ? "flex flex-1" : ""}`} style={size ? {
17
- minHeight: `${size}px`,
18
- } : undefined} {...props} ref={ref} />
17
+ <span
18
+ className={`${inline ? "inline-block" : "block"} ${size === undefined ? "flex flex-1" : ""}`}
19
+ style={
20
+ size
21
+ ? {
22
+ minHeight: `${size}px`,
23
+ }
24
+ : undefined
25
+ }
26
+ {...props}
27
+ ref={ref}
28
+ />
19
29
  );
20
30
  }
21
31
  );
@@ -24,12 +34,21 @@ const SpacerVertical = forwardRef<HTMLSpanElement, Props>(
24
34
  * Horizontal
25
35
  * ------------------------------------------------------------------------- */
26
36
 
27
- const SpacerHorizontal = forwardRef<HTMLSpanElement, Props>(
37
+ const SpacerHorizontal = React.forwardRef<HTMLSpanElement, Props>(
28
38
  ({ size, inline, ...props }, ref: ForwardedRef<HTMLSpanElement>) => {
29
39
  return (
30
- <span className={`${inline ? "inline-block" : "block"} ${size === undefined ? "flex flex-1" : ""}`} style={size ? {
31
- minWidth: `${size}px`,
32
- } : undefined} {...props} ref={ref} />
40
+ <span
41
+ className={`${inline ? "inline-block" : "block"} ${size === undefined ? "flex flex-1" : ""}`}
42
+ style={
43
+ size
44
+ ? {
45
+ minWidth: `${size}px`,
46
+ }
47
+ : undefined
48
+ }
49
+ {...props}
50
+ ref={ref}
51
+ />
33
52
  );
34
53
  }
35
54
  );
@@ -1,8 +1,8 @@
1
1
  import * as SwitchPrimitive from "@radix-ui/react-switch";
2
- import React, { CSSProperties, FocusEventHandler } from "react";
2
+ import * as React from "react";
3
3
  import { cx } from "../utils/classNames";
4
4
 
5
- type SwitchColorScheme = "normal" | "primary" | "secondary";
5
+ type SwitchColorScheme = "primary" | "secondary";
6
6
 
7
7
  interface Props {
8
8
  id?: string;
@@ -11,17 +11,17 @@ interface Props {
11
11
  /** @default normal */
12
12
  colorScheme?: SwitchColorScheme;
13
13
  disabled?: boolean;
14
- onFocus?: FocusEventHandler;
15
- onBlur?: FocusEventHandler;
14
+ onFocus?: React.FocusEventHandler;
15
+ onBlur?: React.FocusEventHandler;
16
16
  className?: string;
17
- style?: CSSProperties;
17
+ style?: React.CSSProperties;
18
18
  }
19
19
 
20
20
  export const Switch = function Switch({
21
21
  id,
22
22
  value,
23
23
  onChange,
24
- colorScheme = "normal",
24
+ colorScheme = "primary",
25
25
  disabled,
26
26
  onFocus,
27
27
  onBlur,
@@ -38,7 +38,7 @@ export const Switch = function Switch({
38
38
  onChange(newValue);
39
39
  }}
40
40
  className={cx(
41
- "all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer [webkit-tap-highlight-color:rgba(0,0,0,0)] focus:ring-2 focus:ring-primary focus:ring-offset-[1px] outline-none data-[state=checked]:bg-primary",
41
+ "all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer [webkit-tap-highlight-color:rgba(0,0,0,0)] focus:ring-2 focus:ring-primary focus:ring-offset-[1px] outline-none data-[state=checked]:bg-primary transition-all",
42
42
  colorScheme === "secondary" && "data-[state=checked]:bg-secondary",
43
43
  "focus:z-interactable",
44
44
  className
@@ -46,7 +46,7 @@ export const Switch = function Switch({
46
46
  onFocus={onFocus}
47
47
  onBlur={onBlur}
48
48
  >
49
- <SwitchPrimitive.Thumb className="block w-[15px] h-[15px] bg-white rounded-full transition-transform translate-x-[2px] data-[state=checked]:translate-x-[15px]" />
49
+ <SwitchPrimitive.Thumb className="block w-[15px] h-[15px] bg-background rounded-full transition-transform translate-x-[2px] data-[state=checked]:translate-x-[15px]" />
50
50
  </SwitchPrimitive.Root>
51
51
  );
52
52
  };
@@ -1,11 +1,19 @@
1
1
  import { assignRef } from "@noya-app/react-utils";
2
- import React, { forwardRef, memo, useCallback, useEffect, useRef } from "react";
2
+ import React, {
3
+ ComponentProps,
4
+ forwardRef,
5
+ memo,
6
+ useCallback,
7
+ useEffect,
8
+ useRef,
9
+ } from "react";
3
10
  import { cx } from "../utils/classNames";
4
11
 
5
- export const useAutoResize = (value: string) => {
12
+ export const useAutoResize = (value: string | undefined) => {
6
13
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
7
14
 
8
15
  useEffect(() => {
16
+ if (value === undefined) return;
9
17
  if (!textareaRef.current) return;
10
18
 
11
19
  textareaRef.current.style.height = "auto"; // Reset the height
@@ -15,39 +23,48 @@ export const useAutoResize = (value: string) => {
15
23
  return textareaRef;
16
24
  };
17
25
 
18
- export const AutoResizingTextArea = memo(
19
- forwardRef(function AutoResizingTextArea(
26
+ export const TextArea = memo(
27
+ forwardRef(function TextArea(
20
28
  {
21
29
  value,
22
30
  onChangeText,
23
31
  className,
32
+ onChange,
33
+ autoResize = false,
24
34
  ...rest
25
35
  }: {
26
- onChangeText: (value: string) => void;
36
+ onChangeText?: (value: string) => void;
27
37
  value?: string;
38
+ autoResize?: boolean;
28
39
  } & React.TextareaHTMLAttributes<HTMLTextAreaElement>,
29
40
  forwardedRef: React.ForwardedRef<HTMLTextAreaElement>
30
41
  ) {
31
- const ref = useAutoResize(value || rest.placeholder || "");
42
+ const autoResizeRef = useAutoResize(
43
+ autoResize ? value || rest.placeholder || "" : undefined
44
+ );
32
45
 
33
46
  const handleChange = useCallback(
34
- (event: React.ChangeEvent<HTMLTextAreaElement>) =>
35
- onChangeText(event.target.value),
36
- [onChangeText]
47
+ (event: React.ChangeEvent<HTMLTextAreaElement>) => {
48
+ onChangeText?.(event.target.value);
49
+ onChange?.(event);
50
+ },
51
+ [onChangeText, onChange]
37
52
  );
38
53
 
39
54
  const handleRef = useCallback(
40
55
  (value: HTMLTextAreaElement | null) => {
41
- ref.current = value;
56
+ if (autoResize) {
57
+ autoResizeRef.current = value;
58
+ }
42
59
  assignRef(forwardedRef, value);
43
60
  },
44
- [ref, forwardedRef]
61
+ [autoResize, autoResizeRef, forwardedRef]
45
62
  );
46
63
 
47
64
  return (
48
65
  <textarea
49
66
  className={cx(
50
- `font-sans text-heading5 font-normal text-text bg-input-background w-0 flex-1 py-1 px-1.5 border-none outline-none h-[100px] rounded-4 resize-none placeholder:text-text-disabled focus:ring-2 focus:ring-primary read-only:text-text-disabled focus:z-interactable `,
67
+ `font-sans text-heading5 font-normal text-text bg-input-background flex-1 py-1 px-1.5 border-none outline-none min-h-6 w-full rounded resize-none placeholder:text-text-disabled focus:ring-2 focus:ring-primary read-only:text-text-disabled focus:z-interactable transition-all`,
51
68
  className
52
69
  )}
53
70
  ref={handleRef}
@@ -58,3 +75,33 @@ export const AutoResizingTextArea = memo(
58
75
  );
59
76
  })
60
77
  );
78
+
79
+ export const TextAreaRow = memo(
80
+ forwardRef(function TextAreaRow(
81
+ {
82
+ className,
83
+ textAreaClassName,
84
+ textAreaRef,
85
+ end,
86
+ endClassName,
87
+ ...rest
88
+ }: {
89
+ className?: string;
90
+ textAreaClassName?: string;
91
+ textAreaRef?: React.Ref<HTMLTextAreaElement>;
92
+ end?: React.ReactNode;
93
+ endClassName?: string;
94
+ } & ComponentProps<typeof TextArea>,
95
+ forwardedRef: React.ForwardedRef<HTMLDivElement>
96
+ ) {
97
+ return (
98
+ <div
99
+ ref={forwardedRef}
100
+ className={cx("relative w-full h-full", className)}
101
+ >
102
+ <TextArea className={textAreaClassName} {...rest} ref={textAreaRef} />
103
+ <div className={cx("absolute right-1 top-4", endClassName)}>{end}</div>
104
+ </div>
105
+ );
106
+ })
107
+ );
@@ -0,0 +1,129 @@
1
+ import { DropdownChevronIcon } from "@noya-app/noya-icons";
2
+ import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
+ import React from "react";
4
+ import { BaseToolbar } from "./BaseToolbar";
5
+ import { Button } from "./Button";
6
+ import { DropdownMenu } from "./DropdownMenu";
7
+ import { renderIcon } from "./Icons";
8
+ import type { MenuItem } from "./internal/Menu";
9
+ import { getKeyboardShortcutsForMenuItems } from "./internal/Menu";
10
+ import { Spacer } from "./Spacer";
11
+
12
+ export interface ToolbarProps<T extends string = string> {
13
+ children?: React.ReactNode;
14
+ logo?: React.ReactNode;
15
+ leftMenuItems?: MenuItem<T>[];
16
+ rightMenuItems?: MenuItem<T>[];
17
+ onSelectMenuItem?: (value: T) => void;
18
+ /** Whether to bind keyboard shortcuts for menu items. @default true */
19
+ shouldBindKeyboardShortcuts?: boolean;
20
+ }
21
+
22
+ export function Toolbar<T extends string = string>({
23
+ children,
24
+ logo,
25
+ leftMenuItems = [],
26
+ rightMenuItems = [],
27
+ onSelectMenuItem,
28
+ shouldBindKeyboardShortcuts = true,
29
+ }: ToolbarProps<T>) {
30
+ // Register keyboard shortcuts for all menu items
31
+ const allMenuItems = React.useMemo(
32
+ () => [...leftMenuItems, ...rightMenuItems],
33
+ [leftMenuItems, rightMenuItems]
34
+ );
35
+
36
+ useKeyboardShortcuts(
37
+ React.useMemo(
38
+ () =>
39
+ onSelectMenuItem && shouldBindKeyboardShortcuts
40
+ ? getKeyboardShortcutsForMenuItems(allMenuItems, onSelectMenuItem)
41
+ : {},
42
+ [allMenuItems, onSelectMenuItem, shouldBindKeyboardShortcuts]
43
+ )
44
+ );
45
+
46
+ return (
47
+ <BaseToolbar
48
+ logo={logo}
49
+ left={
50
+ leftMenuItems.length > 0 && (
51
+ <div className="flex gap-2">
52
+ <ToolbarMenu
53
+ items={leftMenuItems}
54
+ onSelectMenuItem={onSelectMenuItem}
55
+ />
56
+ </div>
57
+ )
58
+ }
59
+ right={
60
+ rightMenuItems.length > 0 && (
61
+ <div className="flex gap-2">
62
+ <ToolbarMenu
63
+ items={rightMenuItems}
64
+ onSelectMenuItem={onSelectMenuItem}
65
+ />
66
+ </div>
67
+ )
68
+ }
69
+ >
70
+ {children}
71
+ </BaseToolbar>
72
+ );
73
+ }
74
+
75
+ export function ToolbarMenu<T extends string>({
76
+ items,
77
+ onSelectMenuItem,
78
+ }: {
79
+ items: MenuItem<T>[];
80
+ onSelectMenuItem?: (value: T) => void;
81
+ }) {
82
+ return (
83
+ <>
84
+ {items.map((item, i) => {
85
+ if (typeof item === "string") return null;
86
+
87
+ if (!item.items) {
88
+ return (
89
+ <Button
90
+ key={i}
91
+ disabled={item.disabled}
92
+ onClick={() => {
93
+ if (onSelectMenuItem && item.value) {
94
+ onSelectMenuItem(item.value);
95
+ }
96
+ }}
97
+ >
98
+ {item.title}
99
+ {item.icon && (
100
+ <>
101
+ <Spacer.Horizontal inline size={6} />
102
+ {renderIcon(item.icon)}
103
+ </>
104
+ )}
105
+ </Button>
106
+ );
107
+ }
108
+
109
+ return (
110
+ <DropdownMenu
111
+ key={i}
112
+ items={item.items}
113
+ onSelect={(value) => {
114
+ if (onSelectMenuItem && value) {
115
+ onSelectMenuItem(value);
116
+ }
117
+ }}
118
+ >
119
+ <Button disabled={item.disabled}>
120
+ {item.title}
121
+ <Spacer.Horizontal size={6} />
122
+ <DropdownChevronIcon />
123
+ </Button>
124
+ </DropdownMenu>
125
+ );
126
+ })}
127
+ </>
128
+ );
129
+ }
@@ -1,17 +1,20 @@
1
- import * as TooltipPrimitive from '@radix-ui/react-tooltip';
2
- import React, { memo, ReactNode } from 'react';
3
- import { textStyles } from './Text';
1
+ import * as TooltipPrimitive from "@radix-ui/react-tooltip";
2
+ import * as React from "react";
3
+ import { textStyles } from "./Text";
4
4
 
5
5
  // const Arrow = styled(TooltipPrimitive.Arrow)(({ theme }) => ({
6
6
  // fill: theme.colors.popover.background,
7
7
  // }));
8
8
 
9
9
  interface Props {
10
- children: ReactNode;
11
- content: ReactNode;
10
+ children: React.ReactNode;
11
+ content: React.ReactNode;
12
12
  }
13
13
 
14
- export const Tooltip = memo(function Tooltip({ children, content }: Props) {
14
+ export const Tooltip = React.memo(function Tooltip({
15
+ children,
16
+ content,
17
+ }: Props) {
15
18
  return (
16
19
  <TooltipPrimitive.Provider>
17
20
  <TooltipPrimitive.Root>
@@ -22,7 +25,7 @@ export const Tooltip = memo(function Tooltip({ children, content }: Props) {
22
25
  align="center"
23
26
  sideOffset={2}
24
27
  collisionPadding={8}
25
- className={`${textStyles.small} text-text rounded-[3px] py-small px-medium bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2)_0_0_12px_rgba(0,0,0,0.1)] border border-solid border-divider-strong`}
28
+ className={`${textStyles.small} text-text rounded-[3px] py-small px-medium bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2)_0_0_12px_rgba(0,0,0,0.1)] border border-solid border-divider-strong z-menu`}
26
29
  >
27
30
  {content}
28
31
  {/* <Arrow /> */}