@noya-app/noya-designsystem 0.1.50 → 0.1.52

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 (56) hide show
  1. package/.turbo/turbo-build.log +10 -13
  2. package/CHANGELOG.md +19 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +314 -203
  5. package/dist/index.d.ts +314 -203
  6. package/dist/index.js +6473 -12656
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +8399 -14605
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +15 -12
  11. package/postcss.config.js +7 -0
  12. package/src/components/ActionMenu.tsx +12 -5
  13. package/src/components/Banner.tsx +2 -2
  14. package/src/components/BaseToolbar.tsx +6 -2
  15. package/src/components/Breadcrumbs.tsx +32 -2
  16. package/src/components/Button.tsx +30 -19
  17. package/src/components/Chip.tsx +3 -0
  18. package/src/components/Collection.tsx +12 -3
  19. package/src/components/ColorSwatch.tsx +42 -0
  20. package/src/components/ColorSwatchControl.tsx +92 -0
  21. package/src/components/ComboboxMenu.tsx +10 -1
  22. package/src/components/Dialog.tsx +1 -1
  23. package/src/components/FillInputField.tsx +1 -1
  24. package/src/components/Grid.tsx +22 -12
  25. package/src/components/GridView.tsx +1 -1
  26. package/src/components/LabeledField.tsx +4 -1
  27. package/src/components/List.tsx +37 -26
  28. package/src/components/ListView.tsx +7 -4
  29. package/src/components/MediaThumbnail.tsx +63 -15
  30. package/src/components/Popover.tsx +1 -1
  31. package/src/components/SearchCompletionMenu.tsx +12 -12
  32. package/src/components/SegmentedControl.tsx +2 -2
  33. package/src/components/SelectionToolbar.tsx +70 -0
  34. package/src/components/Switch.tsx +1 -1
  35. package/src/components/Text.tsx +2 -2
  36. package/src/components/Toast.tsx +13 -7
  37. package/src/components/Toolbar.tsx +33 -11
  38. package/src/components/Tooltip.tsx +4 -2
  39. package/src/components/catppuccin/fileIcons.ts +2430 -0
  40. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +3 -1
  41. package/src/components/file-explorer/FileExplorerLayout.tsx +12 -2
  42. package/src/components/internal/Menu.tsx +5 -4
  43. package/src/components/internal/MenuViewport.tsx +2 -0
  44. package/src/components/internal/TextInput.tsx +7 -2
  45. package/src/components/pipeline/PipelineResultLayout.tsx +18 -0
  46. package/src/contexts/DialogContext.tsx +20 -5
  47. package/src/index.css +103 -34
  48. package/src/index.tsx +3 -0
  49. package/src/theme/index.ts +7 -2
  50. package/src/theme/themeUtils.ts +10 -3
  51. package/src/utils/classNames.ts +4 -3
  52. package/src/utils/combobox.ts +18 -13
  53. package/src/utils/moveTreeItem.ts +4 -4
  54. package/tailwind.config.ts +2 -239
  55. package/tailwind.d.ts +1 -1
  56. package/tsup.config.ts +1 -4
@@ -29,11 +29,13 @@ export const LabeledField = memo(function LabeledField({
29
29
  minWidth = 100,
30
30
  className,
31
31
  style,
32
+ labelStyle: labelStyleProp,
32
33
  ...props
33
34
  }: {
34
35
  children: React.ReactNode;
35
36
  label: React.ReactNode;
36
37
  labelPosition?: LabelPosition;
38
+ labelStyle?: React.CSSProperties;
37
39
  /** Used for both the `id` of the field and the `htmlFor` of the label, unless `id` and `htmlFor` are explicitly provided */
38
40
  fieldId?: string;
39
41
  className?: string;
@@ -79,8 +81,9 @@ export const LabeledField = memo(function LabeledField({
79
81
  const labelStyle = useMemo(
80
82
  () => ({
81
83
  minWidth: `calc(${labelWidth}px - ${indentLevel * 8}px)`,
84
+ ...labelStyleProp,
82
85
  }),
83
- [labelWidth, indentLevel]
86
+ [labelWidth, indentLevel, labelStyleProp]
84
87
  );
85
88
 
86
89
  return (
@@ -8,13 +8,16 @@ import { cssVars } from "../theme";
8
8
  import { cx } from "../utils/classNames";
9
9
  import { updateSelection } from "../utils/selection";
10
10
  import { ActionMenu } from "./ActionMenu";
11
- import { CollectionProps, CollectionRef } from "./Collection";
12
- import { GridViewSize } from "./Grid";
11
+ import {
12
+ CollectionItemSize,
13
+ CollectionProps,
14
+ CollectionRef,
15
+ } from "./Collection";
13
16
  import { ListView, ListViewItemInfo } from "./ListView";
14
17
  import { TreeView } from "./TreeView";
15
18
 
16
19
  const cssVarOverrides = {
17
- "--n-icon-selected": cssVars.colors.primary,
20
+ "--n-icon-selected": cssVars.colors.selectedListItemText,
18
21
  };
19
22
 
20
23
  const emptyArray: string[] = [];
@@ -114,7 +117,7 @@ export const List = memoGeneric(
114
117
  variant="bare"
115
118
  className={cx(
116
119
  className,
117
- isDropTargetActive && "bg-primary-pastel",
120
+ isDropTargetActive && "bg-selected-list-item-background",
118
121
  scrollable && "basis-0 min-h-0"
119
122
  )}
120
123
  containerRef={fileDropTargetRef}
@@ -149,6 +152,7 @@ export const List = memoGeneric(
149
152
  const thumbnail = renderThumbnail?.({
150
153
  item,
151
154
  selected: isSelected,
155
+ size,
152
156
  });
153
157
  const onOpenChange = (open: boolean) => {
154
158
  setDropdownOpenId(open ? id : undefined);
@@ -169,6 +173,15 @@ export const List = memoGeneric(
169
173
  onSelect={(action) => handleMenuAction(action, item)}
170
174
  selected={isSelected}
171
175
  onOpenChange={onOpenChange}
176
+ variant="bare"
177
+ style={{
178
+ backgroundColor: isSelected
179
+ ? cssVars.colors.selectedListItemBackground
180
+ : "transparent",
181
+ color: isSelected
182
+ ? cssVars.colors.selectedListItemText
183
+ : undefined,
184
+ }}
172
185
  />
173
186
  ));
174
187
 
@@ -188,9 +201,6 @@ export const List = memoGeneric(
188
201
  aria-label={getName(item)}
189
202
  aria-selected={isSelected}
190
203
  className={cx("cursor-pointer rounded px-1", pyMap[size])}
191
- backgroundColor={
192
- isSelected ? cssVars.colors.primaryPastel : undefined
193
- }
194
204
  testRelativeDropPosition={
195
205
  testShowDropIndicatorId === id ? "inside" : undefined
196
206
  }
@@ -238,9 +248,10 @@ export const List = memoGeneric(
238
248
  {thumbnail && (
239
249
  <div
240
250
  className={cx(
241
- "rounded overflow-hidden flex-none flex items-center justify-center",
251
+ "overflow-hidden flex-none flex items-center justify-center",
242
252
  thumbnailSize === "auto" && thumbnailSizeMap[size],
243
- isSelected && "text-primary"
253
+ isSelected && "text-selected-list-item-text",
254
+ size === "small" ? "rounded-sm" : "rounded"
244
255
  )}
245
256
  tabIndex={-1}
246
257
  >
@@ -268,7 +279,7 @@ export const List = memoGeneric(
268
279
  <ViewComponent.RowTitle
269
280
  className={cx(
270
281
  fontStyleMap[size],
271
- isSelected && "text-primary"
282
+ isSelected && "text-selected-list-item-text"
272
283
  )}
273
284
  >
274
285
  {getName(item)}
@@ -316,37 +327,37 @@ const DetailContainer = memo(function DetailContainer({
316
327
  );
317
328
  });
318
329
 
319
- const pyMap: Record<GridViewSize, string> = {
320
- xxs: "py-1",
321
- xs: "py-1",
322
- small: "py-1",
330
+ const pyMap: Record<CollectionItemSize, string> = {
331
+ xxs: "py-0.5",
332
+ xs: "py-0.5",
333
+ small: "py-0.5",
323
334
  medium: "py-1",
324
335
  large: "py-2",
325
336
  xl: "py-3",
326
337
  };
327
338
 
328
- const thumbnailSizeMap: Record<GridViewSize, string> = {
329
- xxs: "w-4 h-4",
330
- xs: "w-4 h-4",
331
- small: "w-6 h-6",
339
+ const thumbnailSizeMap: Record<CollectionItemSize, string> = {
340
+ xxs: "w-5 h-5",
341
+ xs: "w-5 h-5",
342
+ small: "w-5 h-5",
332
343
  medium: "w-8 h-8",
333
344
  large: "w-16 h-16",
334
345
  xl: "w-24 h-24",
335
346
  };
336
347
 
337
- const rowGapMap: Record<GridViewSize, string> = {
338
- xxs: "gap-3",
339
- xs: "gap-3",
340
- small: "gap-3",
348
+ const rowGapMap: Record<CollectionItemSize, string> = {
349
+ xxs: "gap-2",
350
+ xs: "gap-2",
351
+ small: "gap-2",
341
352
  medium: "gap-3",
342
353
  large: "gap-3",
343
354
  xl: "gap-4",
344
355
  };
345
356
 
346
- const fontStyleMap: Record<GridViewSize, string> = {
347
- xxs: "font-medium",
348
- xs: "font-medium",
349
- small: "font-medium",
357
+ const fontStyleMap: Record<CollectionItemSize, string> = {
358
+ xxs: "font-medium text-list-small",
359
+ xs: "font-medium text-list-small",
360
+ small: "font-medium text-list-small",
350
361
  medium: "font-medium",
351
362
  large: "font-medium",
352
363
  xl: "font-medium text-heading4 text-text",
@@ -232,7 +232,7 @@ const RowContainer = forwardRef<
232
232
  : "font-sans text-heading5 font-normal",
233
233
  !$isSectionHeader &&
234
234
  $selected &&
235
- "text-white active:bg-primary-light",
235
+ "text-selected-list-item-text active:bg-selected-list-item-background",
236
236
  !$isSectionHeader && $disabled && "text-text-disabled",
237
237
  $clickable ? "cursor-pointer" : "cursor-default",
238
238
  $isSectionHeader && "bg-listview-raised-background",
@@ -254,7 +254,10 @@ const RowContainer = forwardRef<
254
254
  }),
255
255
  ...($selected &&
256
256
  !$isSectionHeader && {
257
- backgroundColor: cssVars.colors[$colorScheme],
257
+ backgroundColor:
258
+ $colorScheme === "primary"
259
+ ? cssVars.colors["selectedListItemBackground"]
260
+ : cssVars.colors[$colorScheme],
258
261
  }),
259
262
  ...($selected &&
260
263
  !$isSectionHeader &&
@@ -279,7 +282,7 @@ const RowContainer = forwardRef<
279
282
  backgroundColor: $selected
280
283
  ? $colorScheme === "secondary"
281
284
  ? cssVars.colors.secondaryLight
282
- : cssVars.colors.primaryLight
285
+ : cssVars.colors.selectedListItemBackground
283
286
  : cssVars.colors.activeBackground,
284
287
  },
285
288
  }),
@@ -358,7 +361,7 @@ const ListViewDragIndicatorElement = forwardRef<
358
361
  backgroundColor:
359
362
  $colorScheme === "secondary"
360
363
  ? cssVars.colors.secondary
361
- : cssVars.colors.primary,
364
+ : cssVars.colors.selectedListItemBackground,
362
365
  border: `2px solid white`,
363
366
  boxShadow: "0 0 2px rgba(0,0,0,0.5)",
364
367
  }),
@@ -1,8 +1,15 @@
1
- import { ImageIcon, SpeakerLoudIcon, VideoIcon } from "@noya-app/noya-icons";
1
+ import {
2
+ FileIcon,
3
+ ImageIcon,
4
+ SpeakerLoudIcon,
5
+ VideoIcon,
6
+ } from "@noya-app/noya-icons";
2
7
  import React, { memo, useMemo } from "react";
3
8
  import { cssVars } from "../theme";
4
9
  import { cx } from "../utils/classNames";
10
+ import { CollectionItemSize } from "./Collection";
5
11
  import { IconName, Icons } from "./Icons";
12
+ import { fileExtensions, fileNames } from "./catppuccin/fileIcons";
6
13
 
7
14
  type ColorScheme = "primary" | "secondary" | "icon" | "warning";
8
15
 
@@ -29,6 +36,9 @@ type MediaThumbnailProps = {
29
36
  * If provided, use this icon instead of the default one for the asset type
30
37
  */
31
38
  iconName?: IconName;
39
+ size?: CollectionItemSize;
40
+
41
+ fileName?: string;
32
42
  };
33
43
 
34
44
  export const getThumbnailColors = (
@@ -37,8 +47,8 @@ export const getThumbnailColors = (
37
47
  ) => {
38
48
  if (selected) {
39
49
  return {
40
- icon: cssVars.colors.primary,
41
- background: cssVars.colors.primaryPastel,
50
+ icon: cssVars.colors.selectedListItemText,
51
+ background: cssVars.colors.selectedListItemIconBackground,
42
52
  };
43
53
  }
44
54
 
@@ -67,29 +77,34 @@ export const getThumbnailColors = (
67
77
  }
68
78
  };
69
79
 
70
- const iconStyles = {
71
- width: "max(15px, 20%)",
72
- height: "max(15px, 20%)",
73
- };
74
-
75
80
  export const MediaThumbnail = memo(function MediaThumbnail({
76
81
  contentType: contentTypeProp,
77
82
  selected = false,
78
83
  className,
79
84
  colorScheme = "icon",
80
85
  url,
81
- iconName = "FileIcon",
86
+ iconName,
87
+ size = "medium",
88
+ fileName,
82
89
  }: MediaThumbnailProps) {
83
90
  const { icon: iconColor, background: backgroundColor } = getThumbnailColors(
84
91
  colorScheme,
85
92
  selected
86
93
  );
87
94
  const contentType = getAssetType(contentTypeProp);
95
+ const iconStyles = useMemo(
96
+ () => ({
97
+ width: iconSizeMap[size],
98
+ height: iconSizeMap[size],
99
+ }),
100
+ [size]
101
+ );
88
102
 
89
103
  const content = useMemo(() => {
90
104
  switch (contentType) {
91
105
  case "image":
92
106
  if (!url) return <ImageIcon color={iconColor} style={iconStyles} />;
107
+
93
108
  return (
94
109
  <img
95
110
  src={url}
@@ -98,18 +113,32 @@ export const MediaThumbnail = memo(function MediaThumbnail({
98
113
  tabIndex={-1}
99
114
  />
100
115
  );
101
-
102
116
  case "video":
103
117
  return <VideoIcon color={iconColor} style={iconStyles} />;
104
-
105
118
  case "audio":
106
119
  return <SpeakerLoudIcon color={iconColor} style={iconStyles} />;
107
-
108
120
  default:
109
- const Icon = Icons[iconName];
110
- return <Icon color={iconColor} style={iconStyles} />;
121
+ if (iconName) {
122
+ const Icon = Icons[iconName];
123
+ return <Icon color={iconColor} style={iconStyles} />;
124
+ }
125
+
126
+ if (fileName) {
127
+ const fileIcon = findFileIcon(fileName);
128
+ if (fileIcon) {
129
+ return (
130
+ <img
131
+ src={`https://api.iconify.design/catppuccin/${fileIcon}.svg?height=none&box=1`}
132
+ alt=""
133
+ className="w-4 h-4"
134
+ />
135
+ );
136
+ }
137
+ }
138
+
139
+ return <FileIcon color={iconColor} style={iconStyles} />;
111
140
  }
112
- }, [contentType, url, iconColor, iconName]);
141
+ }, [contentType, url, iconColor, iconStyles, iconName, fileName]);
113
142
 
114
143
  return (
115
144
  <div
@@ -123,3 +152,22 @@ export const MediaThumbnail = memo(function MediaThumbnail({
123
152
  </div>
124
153
  );
125
154
  });
155
+
156
+ const iconSizeMap: Record<CollectionItemSize, number | string> = {
157
+ xxs: 12,
158
+ xs: 12,
159
+ small: 12,
160
+ medium: "max(15px, 20%)",
161
+ large: "max(15px, 20%)",
162
+ xl: "max(15px, 20%)",
163
+ };
164
+
165
+ function findFileIcon(fileName: string) {
166
+ const fileIcon = fileNames[fileName];
167
+ if (fileIcon) return fileIcon;
168
+ const fileExtensionIndex = fileName.lastIndexOf(".");
169
+ if (fileExtensionIndex === -1) return undefined;
170
+ const fileExtension = fileName.slice(fileExtensionIndex + 1);
171
+ const fileExtensionIcon = fileExtensions[fileExtension];
172
+ return fileExtensionIcon;
173
+ }
@@ -33,7 +33,7 @@ interface Props
33
33
  }
34
34
 
35
35
  export const popoverStyle = {
36
- base: "rounded font-[14px] bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
36
+ base: "rounded font-[14px] bg-popover-background shadow-popover max-h-[600px] overflow-y-auto text-text-muted z-[1000]",
37
37
  large: "w-[680px]",
38
38
  normal: "w-[240px]",
39
39
  };
@@ -119,21 +119,21 @@ export const SearchCompletionMenu = forwardRef(function SearchCompletionMenu(
119
119
  (event: React.KeyboardEvent<HTMLInputElement>) => {
120
120
  handleKeyboardEvent(event.nativeEvent, getCurrentPlatform(navigator), {
121
121
  ArrowUp: () => {
122
- const nextIndex = getNextIndex(
123
- results,
124
- index,
125
- "previous",
126
- (item) => item.disabled ?? false
127
- );
122
+ const nextIndex = getNextIndex({
123
+ items: results,
124
+ currentIndex: index,
125
+ direction: "previous",
126
+ isDisabled: (item) => item.disabled ?? false,
127
+ });
128
128
  setIndex(nextIndex);
129
129
  },
130
130
  ArrowDown: () => {
131
- const nextIndex = getNextIndex(
132
- results,
133
- index,
134
- "next",
135
- (item) => item.disabled ?? false
136
- );
131
+ const nextIndex = getNextIndex({
132
+ items: results,
133
+ currentIndex: index,
134
+ direction: "next",
135
+ isDisabled: (item) => item.disabled ?? false,
136
+ });
137
137
  setIndex(nextIndex);
138
138
  },
139
139
  Tab: () => selectAndClose(results[index]!),
@@ -1,3 +1,4 @@
1
+ import { memoGeneric } from "@noya-app/react-utils";
1
2
  import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
2
3
  import React, {
3
4
  createContext,
@@ -7,7 +8,6 @@ import React, {
7
8
  useContext,
8
9
  useMemo,
9
10
  } from "react";
10
- import { memoGeneric } from "../../../noya-react-utils/src/utils/reactGenerics";
11
11
  import { useLabel } from "../hooks/useLabel";
12
12
  import { cx } from "../utils/classNames";
13
13
  import { renderIcon } from "./Icons";
@@ -80,7 +80,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
80
80
  ? "aria-checked:bg-primary aria-checked:text-white"
81
81
  : "aria-checked:bg-background aria-checked:text-text",
82
82
  "focus:z-interactable",
83
- "aria-checked:shadow-[0_1px_1px_rgba(0,0,0,0.1)]",
83
+ "aria-checked:shadow-segment",
84
84
  "whitespace-pre",
85
85
  className
86
86
  )}
@@ -0,0 +1,70 @@
1
+ import { MenuItem, ToolbarMenu } from "@noya-app/noya-designsystem";
2
+ import { Rect } from "@noya-app/noya-geometry";
3
+ import { memoGeneric, useSize } from "@noya-app/react-utils";
4
+ import * as PopperPrimitive from "@radix-ui/react-popper";
5
+ import type { MeasurableElement } from "@radix-ui/utils";
6
+ import React, { CSSProperties, useMemo } from "react";
7
+
8
+ // Create a Measurable from a Rect
9
+ const createVirtualElement = (rect: Rect): MeasurableElement => ({
10
+ getBoundingClientRect: () => ({
11
+ width: rect.width,
12
+ height: rect.height,
13
+ x: rect.x,
14
+ y: rect.y,
15
+ top: rect.y,
16
+ right: rect.x + rect.width,
17
+ bottom: rect.y + rect.height,
18
+ left: rect.x,
19
+ toJSON: () => null,
20
+ }),
21
+ });
22
+
23
+ type SelectionToolbarProps<T extends string> = {
24
+ rect: Rect;
25
+ menuItems: MenuItem<T>[];
26
+ onSelectMenuItem: (item: T) => void;
27
+ };
28
+
29
+ export const SelectionToolbar = memoGeneric(function SelectionToolbar<
30
+ T extends string,
31
+ >({ rect, menuItems, onSelectMenuItem }: SelectionToolbarProps<T>) {
32
+ const containerRef = React.useRef<HTMLDivElement>(null);
33
+ const size = useSize(containerRef, "width");
34
+
35
+ // Create a virtual reference element from the rect
36
+ const virtualRef = useMemo(
37
+ () => ({
38
+ current: createVirtualElement(rect),
39
+ }),
40
+ [rect]
41
+ );
42
+
43
+ return (
44
+ <PopperPrimitive.Root>
45
+ <PopperPrimitive.Anchor virtualRef={virtualRef} />
46
+ <PopperPrimitive.Content
47
+ side="top"
48
+ sideOffset={10}
49
+ align="center"
50
+ avoidCollisions
51
+ collisionPadding={10}
52
+ data-editor-overlay
53
+ contentEditable={false}
54
+ style={{
55
+ zIndex: 1000,
56
+ opacity: size ? 1 : 0,
57
+ transition: "opacity 0.2s ease-in-out",
58
+ ["--n-input-background" as keyof CSSProperties]: "transparent",
59
+ }}
60
+ >
61
+ <div
62
+ ref={containerRef}
63
+ className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
64
+ >
65
+ <ToolbarMenu items={menuItems} onSelectMenuItem={onSelectMenuItem} />
66
+ </div>
67
+ </PopperPrimitive.Content>
68
+ </PopperPrimitive.Root>
69
+ );
70
+ });
@@ -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 ring-offset-background [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",
41
+ "all-unset w-8 h-[19px] bg-active-background rounded-full relative cursor-pointer ring-offset-background [-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
@@ -33,8 +33,8 @@ const elements: Record<Variant, keyof ReactHTML> = {
33
33
  };
34
34
 
35
35
  export const textStyles: Record<Variant, string> = {
36
- title: "font-sans text-title font-bold sm:text-[36px]",
37
- subtitle: "font-sans text-subtitle font-medium sm:text-[18px]",
36
+ title: "font-sans text-title font-bold",
37
+ subtitle: "font-sans text-subtitle font-medium",
38
38
  heading1: "font-sans text-heading1 font-semibold",
39
39
  heading2: "font-sans text-heading2 font-medium",
40
40
  heading3: "font-sans text-heading3 font-normal",
@@ -1,7 +1,7 @@
1
- import * as ToastPrimitive from '@radix-ui/react-toast';
2
- import React, { ReactNode } from 'react';
3
- import { IconButton } from './IconButton';
4
- import { textStyles } from './Text';
1
+ import * as ToastPrimitive from "@radix-ui/react-toast";
2
+ import React, { ReactNode } from "react";
3
+ import { IconButton } from "./IconButton";
4
+ import { textStyles } from "./Text";
5
5
 
6
6
  export const Toast = ({
7
7
  title,
@@ -15,15 +15,21 @@ export const Toast = ({
15
15
  }) => {
16
16
  return (
17
17
  <ToastPrimitive.Root
18
- className="px-2 py-2.5 grid grid-cols-[auto_max-content] gap-x-2.5 items-center rounded text-sm bg-popover-background shadow-[0_2px_4px_rgba(0,0,0,0.2),0_0_12px_rgba(0,0,0,0.1)] text-text-muted"
18
+ className="px-2 py-2.5 grid grid-cols-[auto_max-content] gap-x-2.5 items-center rounded text-sm bg-popover-background shadow-popover text-text-muted"
19
19
  {...props}
20
20
  >
21
21
  {title && (
22
- <ToastPrimitive.Title className={`mb-[5px] ${textStyles.label} font-bold text-text`}>
22
+ <ToastPrimitive.Title
23
+ className={`mb-[5px] ${textStyles.label} font-bold text-text`}
24
+ >
23
25
  {title}
24
26
  </ToastPrimitive.Title>
25
27
  )}
26
- <ToastPrimitive.Description className={`${textStyles.small} text-text-muted`}>{content}</ToastPrimitive.Description>
28
+ <ToastPrimitive.Description
29
+ className={`${textStyles.small} text-text-muted`}
30
+ >
31
+ {content}
32
+ </ToastPrimitive.Description>
27
33
  {children && (
28
34
  <ToastPrimitive.Action asChild altText="">
29
35
  {children}
@@ -2,8 +2,11 @@ import { DropdownChevronIcon } from "@noya-app/noya-icons";
2
2
  import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
3
3
  import React from "react";
4
4
 
5
+ import { memoGeneric } from "@noya-app/react-utils";
6
+ import { cssVars } from "../theme";
5
7
  import { BaseToolbar } from "./BaseToolbar";
6
8
  import { Button } from "./Button";
9
+ import { DividerVertical } from "./Divider";
7
10
  import { DropdownMenu } from "./DropdownMenu";
8
11
  import { renderIcon } from "./Icons";
9
12
  import {
@@ -12,6 +15,7 @@ import {
12
15
  MenuItem,
13
16
  } from "./internal/Menu";
14
17
  import { Spacer } from "./Spacer";
18
+ import { Tooltip } from "./Tooltip";
15
19
 
16
20
  export interface ToolbarProps<T extends string = string> {
17
21
  children?: React.ReactNode;
@@ -76,7 +80,12 @@ export function Toolbar<T extends string = string>({
76
80
  );
77
81
  }
78
82
 
79
- export function ToolbarMenu<T extends string>({
83
+ const activeButtonStyle = {
84
+ backgroundColor: cssVars.colors.primaryPastel,
85
+ color: cssVars.colors.primary,
86
+ };
87
+
88
+ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
80
89
  items,
81
90
  onSelectMenuItem,
82
91
  }: {
@@ -86,14 +95,18 @@ export function ToolbarMenu<T extends string>({
86
95
  return (
87
96
  <>
88
97
  {items.map((item, i) => {
89
- if (item.type === "separator") return null;
90
98
  if (item.type === "sectionHeader") return null;
91
99
 
100
+ if (item.type === "separator") {
101
+ return <DividerVertical key={i} overflow={4} />;
102
+ }
103
+
92
104
  if (isSelectableMenuItem(item)) {
93
- return (
105
+ const content = (
94
106
  <Button
95
107
  key={i}
96
108
  disabled={item.disabled}
109
+ style={item.checked ? activeButtonStyle : undefined}
97
110
  onClick={() => {
98
111
  if (onSelectMenuItem && item.value) {
99
112
  onSelectMenuItem(item.value);
@@ -101,14 +114,18 @@ export function ToolbarMenu<T extends string>({
101
114
  }}
102
115
  >
103
116
  {item.title}
104
- {item.icon && (
105
- <>
106
- <Spacer.Horizontal inline size={6} />
107
- {renderIcon(item.icon)}
108
- </>
109
- )}
117
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
118
+ {item.icon && renderIcon(item.icon)}
110
119
  </Button>
111
120
  );
121
+
122
+ return item.tooltip ? (
123
+ <Tooltip sideOffset={10} content={item.tooltip} key={i}>
124
+ {content}
125
+ </Tooltip>
126
+ ) : (
127
+ content
128
+ );
112
129
  }
113
130
 
114
131
  return (
@@ -121,8 +138,13 @@ export function ToolbarMenu<T extends string>({
121
138
  }
122
139
  }}
123
140
  >
124
- <Button disabled={item.disabled}>
141
+ <Button
142
+ disabled={item.disabled}
143
+ style={item.checked ? activeButtonStyle : undefined}
144
+ >
125
145
  {item.title}
146
+ {item.title && item.icon && <Spacer.Horizontal inline size={6} />}
147
+ {item.icon && renderIcon(item.icon)}
126
148
  <Spacer.Horizontal size={6} />
127
149
  <DropdownChevronIcon />
128
150
  </Button>
@@ -131,4 +153,4 @@ export function ToolbarMenu<T extends string>({
131
153
  })}
132
154
  </>
133
155
  );
134
- }
156
+ });
@@ -9,11 +9,13 @@ import { textStyles } from "./Text";
9
9
  interface Props {
10
10
  children: React.ReactNode;
11
11
  content: React.ReactNode;
12
+ sideOffset?: number;
12
13
  }
13
14
 
14
15
  export const Tooltip = React.memo(function Tooltip({
15
16
  children,
16
17
  content,
18
+ sideOffset = 2,
17
19
  }: Props) {
18
20
  return (
19
21
  <TooltipPrimitive.Provider>
@@ -23,9 +25,9 @@ export const Tooltip = React.memo(function Tooltip({
23
25
  <TooltipPrimitive.Content
24
26
  side="bottom"
25
27
  align="center"
26
- sideOffset={2}
28
+ sideOffset={sideOffset}
27
29
  collisionPadding={8}
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`}
30
+ className={`${textStyles.small} text-text rounded-[3px] py-small px-medium bg-popover-background shadow-popover border border-solid border-divider-strong z-menu`}
29
31
  >
30
32
  {content}
31
33
  {/* <Arrow /> */}