@noya-app/noya-designsystem 0.1.62 → 0.1.64

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 (118) hide show
  1. package/.eslintrc.js +1 -5
  2. package/.turbo/turbo-build.log +10 -10
  3. package/.turbo/turbo-lint.log +0 -1
  4. package/CHANGELOG.md +23 -0
  5. package/dist/index.css +1 -1
  6. package/dist/index.d.mts +299 -179
  7. package/dist/index.d.ts +299 -179
  8. package/dist/index.js +3772 -2999
  9. package/dist/index.js.map +1 -1
  10. package/dist/index.mjs +3485 -2720
  11. package/dist/index.mjs.map +1 -1
  12. package/package.json +10 -22
  13. package/src/__tests__/validateDropIndicator.test.ts +26 -41
  14. package/src/__tests__/workspaceLayout.test.ts +281 -0
  15. package/src/components/ActionMenu.tsx +2 -0
  16. package/src/components/ActivityIndicator.tsx +5 -3
  17. package/src/components/AnimatePresence.tsx +6 -4
  18. package/src/components/Avatar.tsx +4 -2
  19. package/src/components/BaseToolbar.tsx +45 -34
  20. package/src/components/Breadcrumbs.tsx +2 -0
  21. package/src/components/Button.tsx +35 -21
  22. package/src/components/Chip.tsx +2 -0
  23. package/src/components/Collection.tsx +4 -1
  24. package/src/components/ColorSwatch.tsx +42 -22
  25. package/src/components/ColorSwatchControl.tsx +5 -3
  26. package/src/components/Combobox.tsx +2 -0
  27. package/src/components/CommandPalette.tsx +2 -0
  28. package/src/components/ContextMenu.tsx +3 -1
  29. package/src/components/Dialog.tsx +3 -1
  30. package/src/components/DimensionInput.tsx +3 -1
  31. package/src/components/Divider.tsx +10 -6
  32. package/src/components/DraggableMenuButton.tsx +22 -4
  33. package/src/components/Drawer.tsx +11 -3
  34. package/src/components/DropdownMenu.tsx +34 -6
  35. package/src/components/EditableText.tsx +2 -0
  36. package/src/components/FileUploadIndicator.tsx +108 -0
  37. package/src/components/FillPreviewBackground.tsx +2 -0
  38. package/src/components/FloatingWindow.tsx +2 -0
  39. package/src/components/GradientPicker.tsx +2 -0
  40. package/src/components/Grid.tsx +47 -38
  41. package/src/components/GridView.tsx +33 -11
  42. package/src/components/IconButton.tsx +4 -2
  43. package/src/components/Icons.tsx +1 -1
  44. package/src/components/InputField.tsx +7 -5
  45. package/src/components/Label.tsx +1 -3
  46. package/src/components/LabeledElementView.tsx +6 -2
  47. package/src/components/LabeledField.tsx +38 -34
  48. package/src/components/List.tsx +10 -2
  49. package/src/components/ListView.tsx +27 -14
  50. package/src/components/MediaThumbnail.tsx +31 -2
  51. package/src/components/Message.tsx +8 -9
  52. package/src/components/Popover.tsx +15 -4
  53. package/src/components/Progress.tsx +20 -7
  54. package/src/components/ScrollArea.tsx +3 -1
  55. package/src/components/SearchCompletionMenu.tsx +2 -0
  56. package/src/components/Section.tsx +2 -0
  57. package/src/components/SegmentedControl.tsx +43 -17
  58. package/src/components/SelectMenu.tsx +6 -4
  59. package/src/components/SelectionToolbar.tsx +31 -8
  60. package/src/components/Slider.tsx +4 -2
  61. package/src/components/Spacer.tsx +0 -1
  62. package/src/components/Switch.tsx +3 -1
  63. package/src/components/Tabs.tsx +48 -0
  64. package/src/components/Text.tsx +6 -5
  65. package/src/components/TextArea.tsx +2 -0
  66. package/src/components/Toast.tsx +1 -1
  67. package/src/components/Toolbar.tsx +113 -12
  68. package/src/components/ToolbarDrawer.tsx +29 -0
  69. package/src/components/Tooltip.tsx +9 -9
  70. package/src/components/TreeView.tsx +2 -0
  71. package/src/components/UserPointer.tsx +2 -0
  72. package/src/components/ai-assistant/AIAssistantLayout.tsx +2 -0
  73. package/src/components/blocks/ImageBlockComponent.tsx +156 -0
  74. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +2 -0
  75. package/src/components/file-explorer/FileExplorerLayout.tsx +14 -6
  76. package/src/components/internal/Checkmark.tsx +24 -0
  77. package/src/components/internal/Menu.tsx +21 -3
  78. package/src/components/internal/MenuViewport.tsx +8 -5
  79. package/src/components/internal/SelectItem.tsx +4 -23
  80. package/src/components/internal/TextInput.tsx +2 -0
  81. package/src/components/sorting/DragRegistration.tsx +8 -4
  82. package/src/components/sorting/SharedDragProvider.tsx +43 -18
  83. package/src/components/sorting/Sortable.tsx +19 -10
  84. package/src/components/sorting/createSharedDrag.tsx +7 -6
  85. package/src/components/sorting/sorting.ts +6 -9
  86. package/src/components/workspace/DrawerWorkspaceLayout.tsx +113 -28
  87. package/src/components/workspace/PanelWorkspaceLayout.tsx +149 -16
  88. package/src/components/workspace/VerticalTabMenu.tsx +36 -0
  89. package/src/components/workspace/WorkspaceLayout.tsx +132 -47
  90. package/src/components/workspace/constants.ts +4 -0
  91. package/src/components/workspace/panelStorage.ts +216 -0
  92. package/src/components/workspace/renderPanelChildren.tsx +12 -0
  93. package/src/components/workspace/types.ts +30 -7
  94. package/src/contexts/DesignSystemConfiguration.tsx +12 -5
  95. package/src/contexts/DialogContext.tsx +2 -0
  96. package/src/contexts/FloatingWindowContext.tsx +2 -0
  97. package/src/contexts/GlobalInputBlurContext.tsx +2 -0
  98. package/src/contexts/ImageDataContext.tsx +2 -0
  99. package/src/contexts/LinkComponentContext.tsx +17 -0
  100. package/src/contexts/OpenPortalsContext.tsx +2 -0
  101. package/src/contexts/PortalScopeContext.tsx +2 -0
  102. package/src/hooks/mergeEventHandlers.ts +1 -1
  103. package/src/hooks/useHover.ts +30 -17
  104. package/src/hooks/useIndent.ts +2 -0
  105. package/src/hooks/useLabel.ts +12 -10
  106. package/src/hooks/useObjectURL.ts +5 -3
  107. package/src/hooks/usePlatform.ts +5 -3
  108. package/src/hooks/useTheme.ts +3 -1
  109. package/src/index.css +25 -4
  110. package/src/index.tsx +7 -2
  111. package/src/utils/classNames.ts +4 -3
  112. package/src/utils/combobox.ts +2 -0
  113. package/src/utils/editableBlockStyles.ts +10 -0
  114. package/src/utils/formatByteSize.ts +8 -0
  115. package/src/utils/withSeparatorElements.ts +1 -1
  116. package/src/hooks/usePreservePanelSize.tsx +0 -183
  117. package/src/hooks/useWindowSize.tsx +0 -26
  118. package/src/mediaQuery.ts +0 -13
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { Size } from "@noya-app/noya-geometry";
2
4
  import { range } from "@noya-app/noya-utils";
3
5
  import { forwardRefGeneric, memoGeneric } from "@noya-app/react-utils";
@@ -192,10 +194,14 @@ const RowContainer = forwardRef<
192
194
  $gap: number;
193
195
  $backgroundColor?: CSSProperties["backgroundColor"];
194
196
  $clickable?: boolean;
197
+ href?: string;
198
+ as?: React.ElementType;
195
199
  }
196
200
  >(
197
201
  (
198
202
  {
203
+ as: Component = "div",
204
+ href,
199
205
  className,
200
206
  $marginType,
201
207
  $selected,
@@ -222,8 +228,9 @@ const RowContainer = forwardRef<
222
228
  ref
223
229
  ) => {
224
230
  return (
225
- <div
231
+ <Component
226
232
  ref={ref}
233
+ href={href}
227
234
  role={role}
228
235
  aria-label={ariaLabel}
229
236
  aria-roledescription={ariaRoleDescription}
@@ -363,7 +370,7 @@ const ListViewDragIndicatorElement = forwardRef<
363
370
  backgroundColor:
364
371
  $colorScheme === "secondary"
365
372
  ? cssVars.colors.secondary
366
- : cssVars.colors.selectedListItemBackground,
373
+ : cssVars.colors.dragOutline,
367
374
  border: `2px solid white`,
368
375
  boxShadow: "0 0 2px rgba(0,0,0,0.5)",
369
376
  }),
@@ -410,6 +417,8 @@ export interface ListViewRowProps<MenuItemType extends string = string> {
410
417
  | CSSProperties
411
418
  | ((props: DragIndicatorStyleProps) => CSSProperties);
412
419
  testRelativeDropPosition?: RelativeDropPosition;
420
+ href?: string;
421
+ as?: React.ElementType;
413
422
  }
414
423
 
415
424
  export type DragIndicatorStyleProps = {
@@ -446,6 +455,8 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
446
455
  dragIndicatorStyle,
447
456
  className,
448
457
  testRelativeDropPosition,
458
+ href,
459
+ as,
449
460
  }: ListViewRowProps<MenuItemType>,
450
461
  forwardedRef: ForwardedRef<HTMLElement>
451
462
  ) {
@@ -472,7 +483,7 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
472
483
  // We use preventDefault as a hack to mark this event as handled. We check for
473
484
  // this in the ListView.Root. We can't stopPropagation here or existing ContextMenus
474
485
  // won't close (onPointerDownOutside won't fire).
475
- event.preventDefault();
486
+ // event.preventDefault();
476
487
 
477
488
  // We only want to handle left clicks
478
489
  if (event.button !== 0) return;
@@ -517,6 +528,8 @@ const ListViewRow = forwardRefGeneric(function ListViewRow<
517
528
 
518
529
  const element = (
519
530
  <Component
531
+ as={as}
532
+ href={href}
520
533
  ref={ref as any}
521
534
  $colorScheme={colorScheme}
522
535
  onContextMenu={onContextMenu}
@@ -787,7 +800,7 @@ export type ListViewRootProps = {
787
800
  divider?: boolean;
788
801
  gap?: number;
789
802
  colorScheme?: ListColorScheme;
790
- containerRef?: React.RefObject<HTMLDivElement>;
803
+ containerRef?: React.RefObject<HTMLDivElement | null>;
791
804
  onDragOver?: React.DragEventHandler<HTMLDivElement>;
792
805
  onDragEnter?: React.DragEventHandler<HTMLDivElement>;
793
806
  onDragLeave?: React.DragEventHandler<HTMLDivElement>;
@@ -897,18 +910,18 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
897
910
  const prevChild = i - 1 >= 0 && renderChild(i - 1);
898
911
  const nextChild = i + 1 < data.length && renderChild(i + 1);
899
912
 
900
- const next: ReactElement | undefined = isValidElement(nextChild)
913
+ const next: ReactElement<any> | undefined = isValidElement(nextChild)
901
914
  ? nextChild
902
915
  : undefined;
903
- const prev: ReactElement | undefined = isValidElement(prevChild)
916
+ const prev: ReactElement<any> | undefined = isValidElement(prevChild)
904
917
  ? prevChild
905
918
  : undefined;
906
919
 
907
920
  const hasMarginTop = !prev;
908
921
  const hasMarginBottom =
909
922
  !next ||
910
- current.props.isSectionHeader ||
911
- (next && next.props.isSectionHeader);
923
+ (current as any).props.isSectionHeader ||
924
+ (next && (next as any).props.isSectionHeader);
912
925
 
913
926
  let marginType: ListRowMarginType;
914
927
 
@@ -924,11 +937,11 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
924
937
 
925
938
  let selectedPosition: ListRowPosition = "only";
926
939
 
927
- if (current.props.selected) {
940
+ if ((current as any).props.selected) {
928
941
  const nextSelected =
929
- next && !next.props.isSectionHeader && next.props.selected;
942
+ next && !next.props.isSectionHeader && (next as any).props.selected;
930
943
  const prevSelected =
931
- prev && !prev.props.isSectionHeader && prev.props.selected;
944
+ prev && !prev.props.isSectionHeader && (prev as any).props.selected;
932
945
 
933
946
  if (nextSelected && prevSelected) {
934
947
  selectedPosition = "middle";
@@ -949,7 +962,7 @@ const ListViewRootInner = forwardRefGeneric(function ListViewRootInner<T>(
949
962
  pressEventName,
950
963
  variant,
951
964
  sectionHeaderVariant,
952
- isSectionHeader: current.props.isSectionHeader,
965
+ isSectionHeader: (current as any).props.isSectionHeader,
953
966
  gap,
954
967
  };
955
968
  },
@@ -1100,7 +1113,7 @@ const ChildrenListViewInner = forwardRef(function ChildrenListViewInner(
1100
1113
  { children, ...rest }: ChildrenProps & ListViewRootProps,
1101
1114
  forwardedRef: ForwardedRef<IVirtualizedList>
1102
1115
  ) {
1103
- const items: ReactElement[] = useMemo(
1116
+ const items: ReactElement<any>[] = useMemo(
1104
1117
  () =>
1105
1118
  Children.toArray(children).flatMap((child) =>
1106
1119
  isValidElement(child) ? [child] : []
@@ -1118,7 +1131,7 @@ const ChildrenListViewInner = forwardRef(function ChildrenListViewInner(
1118
1131
  typeof key === "string" ? key : (key ?? index).toString(),
1119
1132
  []
1120
1133
  )}
1121
- renderItem={useCallback((item: ReactElement) => item, [])}
1134
+ renderItem={useCallback((item: ReactElement<any>) => item, [])}
1122
1135
  />
1123
1136
  );
1124
1137
  });
@@ -1,6 +1,9 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  FileIcon,
3
5
  ImageIcon,
6
+ NoyaIcon,
4
7
  SpeakerLoudIcon,
5
8
  VideoIcon,
6
9
  } from "@noya-app/noya-icons";
@@ -23,7 +26,12 @@ const getAssetType = (contentType?: string): AssetType | undefined => {
23
26
  return undefined;
24
27
  };
25
28
 
26
- type MediaThumbnailProps = {
29
+ export type RenderThumbnailIconProps = {
30
+ fileName?: string;
31
+ contentType?: string;
32
+ };
33
+
34
+ export type MediaThumbnailProps = {
27
35
  contentType?: string;
28
36
  url?: string;
29
37
  selected?: boolean;
@@ -39,6 +47,7 @@ type MediaThumbnailProps = {
39
47
  size?: CollectionItemSize;
40
48
 
41
49
  fileName?: string;
50
+ renderThumbnailIcon?: (props: RenderThumbnailIconProps) => React.ReactNode;
42
51
  };
43
52
 
44
53
  export const getThumbnailColors = (
@@ -86,6 +95,7 @@ export const MediaThumbnail = memo(function MediaThumbnail({
86
95
  iconName,
87
96
  size = "medium",
88
97
  fileName,
98
+ renderThumbnailIcon,
89
99
  }: MediaThumbnailProps) {
90
100
  const { icon: iconColor, background: backgroundColor } = getThumbnailColors(
91
101
  colorScheme,
@@ -101,6 +111,12 @@ export const MediaThumbnail = memo(function MediaThumbnail({
101
111
  );
102
112
 
103
113
  const content = useMemo(() => {
114
+ if (renderThumbnailIcon) {
115
+ const rendered = renderThumbnailIcon({ fileName, contentType });
116
+
117
+ if (rendered) return rendered;
118
+ }
119
+
104
120
  switch (contentType) {
105
121
  case "image":
106
122
  if (!url) return <ImageIcon color={iconColor} style={iconStyles} />;
@@ -124,7 +140,12 @@ export const MediaThumbnail = memo(function MediaThumbnail({
124
140
  }
125
141
 
126
142
  if (fileName) {
143
+ if (fileName.endsWith(".noya")) {
144
+ return <NoyaIcon color={iconColor} style={iconStyles} />;
145
+ }
146
+
127
147
  const fileIcon = findFileIcon(fileName);
148
+
128
149
  if (fileIcon) {
129
150
  return (
130
151
  <img
@@ -138,7 +159,15 @@ export const MediaThumbnail = memo(function MediaThumbnail({
138
159
 
139
160
  return <FileIcon color={iconColor} style={iconStyles} />;
140
161
  }
141
- }, [contentType, url, iconColor, iconStyles, iconName, fileName]);
162
+ }, [
163
+ contentType,
164
+ url,
165
+ iconColor,
166
+ iconStyles,
167
+ iconName,
168
+ renderThumbnailIcon,
169
+ fileName,
170
+ ]);
142
171
 
143
172
  return (
144
173
  <div
@@ -1,11 +1,13 @@
1
+ "use client";
2
+
1
3
  import { MultiplayerUser } from "@noya-app/noya-multiplayer-react";
4
+ import { uuid } from "@noya-app/noya-utils";
2
5
  import React, { forwardRef, memo, useMemo } from "react";
3
6
  import { cssVars, INPUT_HEIGHT } from "../theme";
4
7
  import { cx } from "../utils/classNames";
5
8
  import { colorFromString } from "../utils/colorFromString";
6
9
  import { Avatar } from "./Avatar";
7
10
  import { Logo } from "./NoyaLogo";
8
- import { Text } from "./Text";
9
11
 
10
12
  export type MessageProps = {
11
13
  role: "user" | "assistant" | "system" | "data";
@@ -20,7 +22,7 @@ export const Message = memo(
20
22
  { role, children, user, avatarSize = INPUT_HEIGHT }: MessageProps,
21
23
  ref: React.ForwardedRef<HTMLDivElement>
22
24
  ) {
23
- const randomId = crypto.randomUUID();
25
+ const randomId = uuid();
24
26
  const userMessageBackgroundColor = colorFromString(user?.id ?? randomId);
25
27
  const styles = useMemo(
26
28
  () =>
@@ -56,18 +58,15 @@ export const Message = memo(
56
58
  )}
57
59
  <div
58
60
  className={cx(
59
- "flex flex-row gap-2 rounded-lg py-2 px-3 transition-all duration-200 min-w-6 max-w-[70%] items-start",
61
+ "markdown-message",
62
+ "flex flex-row gap-2 rounded-lg py-2 px-3 transition-all duration-200 max-w-[70%] text-sm",
63
+ role === "user" ? "text-white" : "text-text",
60
64
  role === "user" ? "ml-auto" : "bg-input-background"
61
65
  )}
62
66
  style={styles}
63
67
  ref={ref}
64
68
  >
65
- <Text
66
- variant="small"
67
- className={role === "user" ? "text-white" : "text-text"}
68
- >
69
- {children}
70
- </Text>
69
+ {children}
71
70
  </div>
72
71
  </div>
73
72
  );
@@ -1,6 +1,14 @@
1
+ "use client";
2
+
1
3
  import { useControlledOrUncontrolled } from "@noya-app/react-utils";
2
- import * as PopoverPrimitive from "@radix-ui/react-popover";
3
- import React, { ComponentProps, useEffect, useId, useRef } from "react";
4
+ import { Popover as PopoverPrimitive } from "radix-ui";
5
+ import React, {
6
+ ComponentProps,
7
+ useCallback,
8
+ useEffect,
9
+ useId,
10
+ useRef,
11
+ } from "react";
4
12
  import { useOpenPortalsControls } from "../contexts/OpenPortalsContext";
5
13
  import {
6
14
  portalScopeProps,
@@ -26,7 +34,9 @@ interface Props
26
34
  | "onFocusOutside"
27
35
  | "side"
28
36
  > {
29
- children: React.ReactNode;
37
+ children:
38
+ | React.ReactNode
39
+ | ((props: { close: () => void }) => React.ReactNode);
30
40
  trigger: React.ReactNode;
31
41
  variant?: PopoverVariant;
32
42
  closable?: boolean;
@@ -72,6 +82,7 @@ export function Popover({
72
82
  onChange: onOpenChange,
73
83
  defaultValue: false,
74
84
  });
85
+ const close = useCallback(() => setIsOpen(false), [setIsOpen]);
75
86
 
76
87
  const { addOpenPortal, removeOpenPortal } = useOpenPortalsControls();
77
88
 
@@ -127,7 +138,7 @@ export function Popover({
127
138
  // event.stopPropagation();
128
139
  // }}
129
140
  >
130
- {children}
141
+ {typeof children === "function" ? children({ close }) : children}
131
142
  {showArrow && (
132
143
  <PopoverPrimitive.Arrow className="fill-popover-background" />
133
144
  )}
@@ -1,18 +1,22 @@
1
+ "use client";
2
+
1
3
  import { clamp } from "@noya-app/noya-utils";
2
- import * as ProgressPrimitive from "@radix-ui/react-progress";
4
+ import { Progress as ProgressPrimitive } from "radix-ui";
3
5
  import * as React from "react";
4
6
  import { cx } from "../utils/classNames";
5
7
 
6
- type ProgressVariant = "normal" | "warning" | "primary" | "secondary";
8
+ type ColorScheme = "normal" | "warning" | "primary" | "secondary";
7
9
 
8
10
  export function Progress({
9
11
  value,
10
- variant = "normal",
12
+ colorScheme = "normal",
11
13
  className,
14
+ style,
12
15
  }: {
13
16
  value: number;
14
- variant?: ProgressVariant;
17
+ colorScheme?: ColorScheme;
15
18
  className?: string;
19
+ style?: React.CSSProperties;
16
20
  }) {
17
21
  const clampedValue = clamp(value, 0, 100);
18
22
  const transformStyles = React.useMemo(
@@ -24,12 +28,21 @@ export function Progress({
24
28
 
25
29
  return (
26
30
  <ProgressPrimitive.Root
27
- className={cx(`relative hidden bg-input-background h-[5px] `, className)}
28
- style={{ transform: "translateZ(0)" }}
31
+ className={cx(
32
+ `relative overflow-hidden bg-input-background h-[5px]`,
33
+ className
34
+ )}
35
+ style={{ transform: "translateZ(0)", ...style }}
29
36
  value={clampedValue}
30
37
  >
31
38
  <ProgressPrimitive.Indicator
32
- className={`${variant === "primary" ? "bg-primary" : variant === "secondary" ? "bg-secondary" : variant === "warning" ? "bg-warning" : "bg-text"} transition-[transform_660ms_cubic-bezier(0.65,0,0.35,1) w-full h-full`}
39
+ className={cx(
40
+ "transition-[transform_660ms_cubic-bezier(0.65,0,0.35,1) w-full h-full",
41
+ colorScheme === "primary" && "bg-primary",
42
+ colorScheme === "secondary" && "bg-secondary",
43
+ colorScheme === "warning" && "bg-warning",
44
+ colorScheme === "normal" && "bg-text"
45
+ )}
33
46
  style={transformStyles}
34
47
  />
35
48
  </ProgressPrimitive.Root>
@@ -1,4 +1,6 @@
1
- import * as RadixScrollArea from "@radix-ui/react-scroll-area";
1
+ "use client";
2
+
3
+ import { ScrollArea as RadixScrollArea } from "radix-ui";
2
4
  import * as React from "react";
3
5
 
4
6
  interface Props {
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import { getCurrentPlatform, handleKeyboardEvent } from "@noya-app/noya-keymap";
2
4
  import React, {
3
5
  forwardRef,
@@ -1,3 +1,5 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  Button,
3
5
  IconButton,
@@ -1,5 +1,7 @@
1
+ "use client";
2
+
1
3
  import { memoGeneric } from "@noya-app/react-utils";
2
- import * as ToggleGroupPrimitive from "@radix-ui/react-toggle-group";
4
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui";
3
5
  import React, {
4
6
  createContext,
5
7
  forwardRef,
@@ -15,7 +17,7 @@ import { SelectableMenuItem } from "./internal/Menu";
15
17
  import { Tooltip } from "./Tooltip";
16
18
 
17
19
  type SegmentedControlColorScheme = "primary" | "secondary";
18
- type SegmentedControlVariant = "default" | "tabs";
20
+ type SegmentedControlVariant = "default" | "tabs" | "buttons";
19
21
 
20
22
  type SegmentedControlContextValue = {
21
23
  /** @default primary */
@@ -75,26 +77,49 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
75
77
  className={cx(
76
78
  "group",
77
79
  "flex items-center justify-center",
78
- "font-sans text-button relative flex-1 appearance-none text-segmented-control-item focus:outline-none transition-colors",
79
- variant === "tabs" ? "font-semibold" : "font-normal",
80
+ "font-sans text-button relative flex-1 appearance-none focus:outline-none transition-colors",
81
+ // text color
82
+ variant === "buttons"
83
+ ? "text-button-text"
84
+ : "text-segmented-control-item",
85
+ // font weight
86
+ variant === "tabs"
87
+ ? "font-semibold"
88
+ : variant === "buttons"
89
+ ? "font-medium"
90
+ : "font-normal",
91
+ // focus ring
80
92
  variant === "tabs"
81
93
  ? ""
82
- : colorScheme === "secondary"
83
- ? "focus:ring-2 focus:ring-secondary focus:ring-offset-1"
84
- : "focus:ring-2 focus:ring-primary focus:ring-offset-1",
94
+ : variant === "buttons"
95
+ ? "focus-visible:ring-2 focus-visible:ring-primary focus:shadow-[0_0_0_1px_var(--n-popover-background)_inset] transition-all"
96
+ : colorScheme === "secondary"
97
+ ? "focus:ring-2 focus:ring-secondary focus:ring-offset-1"
98
+ : "focus:ring-2 focus:ring-primary focus:ring-offset-1",
99
+ // checked
85
100
  variant === "tabs"
86
101
  ? "aria-checked:text-primary"
87
- : colorScheme === "secondary"
88
- ? "aria-checked:bg-secondary aria-checked:text-white"
89
- : colorScheme === "primary"
90
- ? "aria-checked:bg-primary aria-checked:text-white"
91
- : "aria-checked:bg-segmented-control-item-active-background aria-checked:text-segmented-control-item-active-text",
102
+ : variant === "buttons"
103
+ ? "active:text-button-text-active active:bg-button-background-active aria-checked:text-button-text-active aria-checked:bg-button-background-active"
104
+ : colorScheme === "secondary"
105
+ ? "aria-checked:bg-secondary aria-checked:text-white"
106
+ : colorScheme === "primary"
107
+ ? "aria-checked:bg-primary aria-checked:text-white"
108
+ : "aria-checked:bg-segmented-control-item-active-background aria-checked:text-segmented-control-item-active-text",
92
109
  "focus:z-interactable",
93
- variant === "default"
94
- ? "px-1 rounded aria-checked:shadow-segment"
95
- : "px-1.5 border-y-2 border-y-transparent aria-checked:border-b-primary",
110
+ // checked default (all colorSchemes)
96
111
  variant === "default" &&
97
- "border-r border-divider last:border-r-0 aria-checked:border-r-transparent [&:has(+[aria-checked=true])]:border-r-transparent",
112
+ "aria-checked:border-r-transparent [&:has(+[aria-checked=true])]:border-r-transparent aria-checked:shadow-segment",
113
+ // hover
114
+ variant === "buttons" && "hover:bg-button-background-hover",
115
+ // spacing and borders
116
+ variant === "default"
117
+ ? "px-1 rounded border-r border-divider last:border-r-0"
118
+ : variant === "buttons"
119
+ ? "px-1 rounded-sm max-w-fit"
120
+ : "px-1.5 border-y-2 border-y-transparent aria-checked:border-b-primary",
121
+ // icon only buttons
122
+ variant === "buttons" && title === "" && icon && "min-w-input-height",
98
123
  className
99
124
  )}
100
125
  {...props}
@@ -103,7 +128,7 @@ const SegmentedControlItem = forwardRef(function SegmentedControlItem<
103
128
  className={cx(
104
129
  "inline-flex whitespace-pre flex-nowrap gap-1.5 items-center justify-center align-middle text-center",
105
130
  variant === "tabs" &&
106
- "group-focus:ring-2 group-focus:ring-primary-pastel group-focus:ring-offset-2"
131
+ "group-focus-visible:ring-2 group-focus-visible:ring-primary-pastel group-focus-visible:ring-offset-2"
107
132
  )}
108
133
  >
109
134
  {icon && renderIcon(icon)}
@@ -155,6 +180,7 @@ export const SegmentedControl = memoGeneric(function SegmentedControl<
155
180
  variant === "default" &&
156
181
  "grid min-h-input-height bg-input-background rounded py-0.5 px-0.5",
157
182
  variant === "tabs" && "flex gap-1.5 min-h-[33px]",
183
+ variant === "buttons" && "flex gap-1.5 min-h-input-height",
158
184
  className
159
185
  )}
160
186
  style={{
@@ -1,10 +1,12 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  ChevronDownIcon,
3
5
  ChevronUpIcon,
4
6
  DropdownChevronIcon,
5
7
  } from "@noya-app/noya-icons";
6
8
  import { memoGeneric } from "@noya-app/react-utils";
7
- import * as Select from "@radix-ui/react-select";
9
+ import { Select } from "radix-ui";
8
10
  import { type SelectProps } from "@radix-ui/react-select";
9
11
  import React, {
10
12
  CSSProperties,
@@ -12,7 +14,7 @@ import React, {
12
14
  useMemo,
13
15
  useState,
14
16
  } from "react";
15
- import { useLabel, useLabelPosition } from "../hooks/useLabel";
17
+ import { useLabel, useLabelType } from "../hooks/useLabel";
16
18
  import { cx } from "../utils/classNames";
17
19
 
18
20
  import {
@@ -92,7 +94,7 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
92
94
  label: props.label,
93
95
  fieldId: props.id,
94
96
  });
95
- const labelPosition = useLabelPosition();
97
+ const labelType = useLabelType();
96
98
 
97
99
  return (
98
100
  <Select.SelectTrigger asChild onFocus={onFocus} onBlur={onBlur}>
@@ -107,7 +109,7 @@ const SelectMenuTrigger = memoGeneric(function SelectMenuTrigger({
107
109
  disabled={disabled}
108
110
  >
109
111
  {icon && <span className="pr-1.5">{renderIcon(icon)}</span>}
110
- {label && labelPosition === "inset" && (
112
+ {label && labelType === "inset" && (
111
113
  <div className={insetEndStyles}>
112
114
  <Label className="pr-4 text-text-disabled" htmlFor={id}>
113
115
  {label}
@@ -1,5 +1,8 @@
1
+ "use client";
2
+
1
3
  import {
2
4
  cssVarNames,
5
+ cx,
3
6
  portalScopeProps,
4
7
  usePortalScopeId,
5
8
  } from "@noya-app/noya-designsystem";
@@ -7,7 +10,7 @@ import { Rect } from "@noya-app/noya-geometry";
7
10
  import { useSize } from "@noya-app/react-utils";
8
11
  import * as PopperPrimitive from "@radix-ui/react-popper";
9
12
  import type { MeasurableElement } from "@radix-ui/utils";
10
- import React, { memo, useMemo } from "react";
13
+ import React, { ComponentProps, memo, useMemo } from "react";
11
14
  import { createPortal } from "react-dom";
12
15
 
13
16
  // Create a Measurable from a Rect
@@ -25,10 +28,20 @@ const createVirtualElement = (rect: Rect): MeasurableElement => ({
25
28
  }),
26
29
  });
27
30
 
28
- type SelectionToolbarContainerProps = {
31
+ type PopperPrimitiveContentProps = ComponentProps<
32
+ typeof PopperPrimitive.Content
33
+ >;
34
+
35
+ type SelectionToolbarContainerProps = Pick<
36
+ PopperPrimitiveContentProps,
37
+ "side" | "sideOffset" | "align"
38
+ > & {
29
39
  rect: Rect;
30
40
  children: React.ReactNode;
31
41
  portalContainer?: HTMLElement | null;
42
+ containerClassName?: string;
43
+ containerStyle?: React.CSSProperties;
44
+ transitionDuration?: number;
32
45
  };
33
46
 
34
47
  export const SelectionToolbarContainer = memo(
@@ -36,6 +49,12 @@ export const SelectionToolbarContainer = memo(
36
49
  rect,
37
50
  children,
38
51
  portalContainer,
52
+ side = "top",
53
+ sideOffset = 10,
54
+ align = "center",
55
+ containerClassName,
56
+ containerStyle,
57
+ transitionDuration = 0.2,
39
58
  }: SelectionToolbarContainerProps) {
40
59
  const portalScopeId = usePortalScopeId();
41
60
  const containerRef = React.useRef<HTMLDivElement>(null);
@@ -52,23 +71,27 @@ export const SelectionToolbarContainer = memo(
52
71
  const popperContent = (
53
72
  <PopperPrimitive.Content
54
73
  {...portalScopeProps(portalScopeId)}
55
- side="top"
56
- sideOffset={10}
57
- align="center"
74
+ side={side}
75
+ sideOffset={sideOffset}
76
+ align={align}
58
77
  avoidCollisions
59
78
  collisionPadding={10}
60
79
  contentEditable={false}
61
80
  style={{
62
- zIndex: 1000,
81
+ zIndex: 10,
63
82
  opacity: size ? 1 : 0,
64
- transition: "opacity 0.2s ease-in-out",
83
+ transition: `opacity ${transitionDuration}s ease-in-out`,
65
84
  [cssVarNames.colors.inputBackground]: "transparent",
66
85
  [cssVarNames.colors.buttonBackground]: "transparent",
67
86
  }}
68
87
  >
69
88
  <div
70
89
  ref={containerRef}
71
- className="flex gap-1 bg-popover-background rounded shadow-popover p-1"
90
+ className={cx(
91
+ "flex gap-1 bg-popover-background rounded shadow-popover p-1",
92
+ containerClassName
93
+ )}
94
+ style={containerStyle}
72
95
  onClick={(e) => e.stopPropagation()}
73
96
  onMouseDown={(e) => e.stopPropagation()}
74
97
  onPointerDown={(e) => e.stopPropagation()}
@@ -1,4 +1,6 @@
1
- import * as RadixSlider from "@radix-ui/react-slider";
1
+ "use client";
2
+
3
+ import { Slider as RadixSlider } from "radix-ui";
2
4
  import React, { FocusEventHandler, memo, useCallback, useMemo } from "react";
3
5
  import { useLabel } from "../hooks/useLabel";
4
6
  import { cx } from "../utils/classNames";
@@ -78,7 +80,7 @@ export const Slider = memo(function Slider({
78
80
  >
79
81
  <div className="relative flex-grow h-[3px] overflow-hidden">
80
82
  <div
81
- className="absolute h-full rounded-l bg-primary"
83
+ className="absolute h-full rounded-l bg-slider-active-background"
82
84
  style={{
83
85
  left: 0,
84
86
  width: `calc(${ratio * 100}% - ${ratio * 8}px - 2px)`,
@@ -1,4 +1,3 @@
1
- /* eslint-disable react/jsx-pascal-case */
2
1
  import type { ForwardedRef } from "react";
3
2
  import * as React from "react";
4
3
 
@@ -1,4 +1,6 @@
1
- import * as SwitchPrimitive from "@radix-ui/react-switch";
1
+ "use client";
2
+
3
+ import { Switch as SwitchPrimitive } from "radix-ui";
2
4
  import * as React from "react";
3
5
  import { cx } from "../utils/classNames";
4
6