@noya-app/noya-designsystem 0.1.76 → 0.1.78

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 (86) hide show
  1. package/.turbo/turbo-build.log +20 -12
  2. package/CHANGELOG.md +19 -0
  3. package/dist/chunk-D57E6H3M.mjs +36 -0
  4. package/dist/chunk-D57E6H3M.mjs.map +1 -0
  5. package/dist/emojis.d.mts +1 -0
  6. package/dist/emojis.d.ts +1 -0
  7. package/dist/emojis.js +31 -0
  8. package/dist/emojis.js.map +1 -0
  9. package/dist/emojis.mjs +8 -0
  10. package/dist/emojis.mjs.map +1 -0
  11. package/dist/index.css +1 -1
  12. package/dist/index.d.mts +192 -93
  13. package/dist/index.d.ts +192 -93
  14. package/dist/index.js +3355 -1763
  15. package/dist/index.js.map +1 -1
  16. package/dist/index.mjs +3622 -2030
  17. package/dist/index.mjs.map +1 -1
  18. package/package.json +13 -9
  19. package/src/__tests__/__snapshots__/fuzzyScorer.test.ts.snap +1 -1
  20. package/src/components/Avatar.tsx +10 -10
  21. package/src/components/Banner.tsx +1 -1
  22. package/src/components/BaseToolbar.tsx +20 -6
  23. package/src/components/Button.tsx +2 -1
  24. package/src/components/Chip.tsx +1 -1
  25. package/src/components/ColorSwatchControl.tsx +22 -14
  26. package/src/components/CommandPalette.tsx +10 -6
  27. package/src/components/ContextMenu.tsx +249 -38
  28. package/src/components/Dialog.tsx +70 -67
  29. package/src/components/Drawer.tsx +56 -17
  30. package/src/components/DropdownMenu.tsx +307 -46
  31. package/src/components/EditableText.tsx +1 -1
  32. package/src/components/EmojiPicker.tsx +645 -0
  33. package/src/components/GridBackground.tsx +40 -0
  34. package/src/components/GridView.tsx +5 -1
  35. package/src/components/IVirtualizedList.tsx +21 -1
  36. package/src/components/InputField.tsx +2 -10
  37. package/src/components/InspectorContainer.tsx +4 -2
  38. package/src/components/Message.tsx +5 -16
  39. package/src/components/OverlayToolbar.tsx +97 -0
  40. package/src/components/Popover.tsx +73 -107
  41. package/src/components/Progress.tsx +18 -18
  42. package/src/components/ScrollArea.tsx +66 -31
  43. package/src/components/ScrollableSidebar.tsx +1 -1
  44. package/src/components/SegmentedControl.tsx +166 -38
  45. package/src/components/SelectMenu.tsx +193 -101
  46. package/src/components/Slider.tsx +40 -38
  47. package/src/components/StackNavigator.tsx +1 -0
  48. package/src/components/Switch.tsx +4 -4
  49. package/src/components/TextArea.tsx +1 -1
  50. package/src/components/Toast.tsx +99 -26
  51. package/src/components/Toolbar.tsx +133 -24
  52. package/src/components/Tooltip.tsx +18 -8
  53. package/src/components/UserPointer.tsx +5 -3
  54. package/src/components/Virtualized.tsx +193 -14
  55. package/src/components/VisuallyHidden.tsx +20 -0
  56. package/src/components/__tests__/Virtualized.math.test.ts +426 -1
  57. package/src/components/__tests__/Virtualized.test.tsx +129 -1
  58. package/src/components/ai-assistant/AIAssistantLayout.tsx +11 -6
  59. package/src/components/internal/Menu.tsx +4 -0
  60. package/src/components/listView/ListViewEditableRowTitle.tsx +1 -1
  61. package/src/components/listView/ListViewRoot.tsx +5 -1
  62. package/src/components/resizablePanels/Panel.tsx +94 -0
  63. package/src/components/resizablePanels/PanelGroup.tsx +498 -0
  64. package/src/components/resizablePanels/PanelGroupContext.tsx +14 -0
  65. package/src/components/resizablePanels/PanelResizeHandle.tsx +61 -0
  66. package/src/components/resizablePanels/index.ts +7 -0
  67. package/src/components/resizablePanels/types.ts +65 -0
  68. package/src/components/workspace/DrawerWorkspaceLayout.tsx +22 -7
  69. package/src/components/workspace/PanelWorkspaceLayout.tsx +30 -84
  70. package/src/components/workspace/WorkspaceLayout.tsx +82 -76
  71. package/src/components/workspace/types.ts +6 -4
  72. package/src/contexts/DialogContext.tsx +15 -24
  73. package/src/emojis.ts +1 -0
  74. package/src/hooks/useTriggerToggle.ts +95 -0
  75. package/src/index.css +2 -0
  76. package/src/index.tsx +3 -2
  77. package/src/theme/proseTheme.ts +22 -0
  78. package/src/utils/mergeProps.ts +57 -0
  79. package/src/utils/skinTone.ts +90 -0
  80. package/tailwind.config.ts +6 -1
  81. package/tsup.config.ts +1 -1
  82. package/src/__tests__/workspaceLayout.test.ts +0 -281
  83. package/src/components/ScrollArea2.tsx +0 -76
  84. package/src/components/internal/MenuViewport.tsx +0 -178
  85. package/src/components/internal/SelectItem.tsx +0 -138
  86. package/src/components/workspace/panelStorage.ts +0 -216
@@ -1,5 +1,25 @@
1
1
  "use client";
2
2
 
3
+ export type ScrollToIndexAlign = "auto" | "start" | "center" | "end";
4
+
5
+ export type ScrollToIndexPadding =
6
+ | number
7
+ | {
8
+ top?: number;
9
+ bottom?: number;
10
+ };
11
+
12
+ export interface ScrollToIndexOptions {
13
+ align?: ScrollToIndexAlign;
14
+ /**
15
+ * Extra padding to include when scrolling. When scrolling to "start",
16
+ * this adds space above the item. When scrolling to "end", this adds
17
+ * space below. For "auto", it applies the appropriate padding based
18
+ * on scroll direction. A number applies to both directions.
19
+ */
20
+ padding?: ScrollToIndexPadding;
21
+ }
22
+
3
23
  export interface IVirtualizedList {
4
- scrollToIndex(index: number): void;
24
+ scrollToIndex(index: number, options?: ScrollToIndexOptions): void;
5
25
  }
@@ -61,8 +61,8 @@ export const getFieldSpacing = ({
61
61
  startWidth?: number;
62
62
  variant?: InputFieldVariant;
63
63
  }) => ({
64
- paddingLeft: `${variant === "bare" ? startWidth : startWidth ? startWidth + 12 : 6}px`,
65
- paddingRight: `${variant === "bare" ? endWidth : endWidth ? endWidth + 12 : 6}px`,
64
+ paddingLeft: `${variant === "bare" ? startWidth : startWidth ? startWidth + 12 : 8}px`,
65
+ paddingRight: `${variant === "bare" ? endWidth : endWidth ? endWidth + 12 : 8}px`,
66
66
  });
67
67
 
68
68
  /* ----------------------------------------------------------------------------
@@ -663,14 +663,6 @@ function InputFieldRoot({
663
663
  trigger={rootElement}
664
664
  sideOffset={sideOffset}
665
665
  showArrow={false}
666
- onOpenAutoFocus={(event) => {
667
- event.preventDefault();
668
- event.stopPropagation();
669
- }}
670
- onCloseAutoFocus={(event) => {
671
- event.preventDefault();
672
- event.stopPropagation();
673
- }}
674
666
  >
675
667
  {measuredWidthObject && (
676
668
  <div
@@ -42,7 +42,7 @@ export const InspectorContainer = memo(
42
42
  >
43
43
  {header}
44
44
  {children ? (
45
- <ScrollArea>
45
+ <ScrollArea className="n-flex-1 n-min-h-0">
46
46
  <div className="n-flex n-flex-col n-relative">
47
47
  {showDividers
48
48
  ? withSeparatorElements(children, <Divider />)
@@ -50,7 +50,9 @@ export const InspectorContainer = memo(
50
50
  </div>
51
51
  </ScrollArea>
52
52
  ) : fallback ? (
53
- <div className="n-flex n-flex-col n-relative n-h-full">{fallback}</div>
53
+ <div className="n-flex n-flex-col n-relative n-h-full">
54
+ {fallback}
55
+ </div>
54
56
  ) : null}
55
57
  </div>
56
58
  );
@@ -5,7 +5,6 @@ import { uuid } from "@noya-app/noya-utils";
5
5
  import React, { forwardRef, memo, useMemo } from "react";
6
6
  import { cssVars, INPUT_HEIGHT } from "../theme";
7
7
  import { cx } from "../utils/classNames";
8
- import { colorFromString } from "../utils/colorFromString";
9
8
  import { Avatar } from "./Avatar";
10
9
  import { Logo } from "./NoyaLogo";
11
10
 
@@ -22,18 +21,8 @@ export const Message = memo(
22
21
  { role, children, user, avatarSize = INPUT_HEIGHT }: MessageProps,
23
22
  ref: React.ForwardedRef<HTMLDivElement>
24
23
  ) {
25
- const randomId = uuid();
24
+ const randomId = useMemo(() => uuid(), []);
26
25
  const resolvedUserId = user?.authId ?? user?.connectionId ?? randomId;
27
- const userMessageBackgroundColor = colorFromString(resolvedUserId);
28
- const styles = useMemo(
29
- () =>
30
- role === "user"
31
- ? {
32
- backgroundColor: userMessageBackgroundColor,
33
- }
34
- : undefined,
35
- [userMessageBackgroundColor, role]
36
- );
37
26
  return (
38
27
  <div
39
28
  className={cx(
@@ -60,11 +49,11 @@ export const Message = memo(
60
49
  <div
61
50
  className={cx(
62
51
  "markdown-message",
63
- "n-flex n-flex-row n-gap-2 n-rounded-lg n-py-2 n-px-3 n-transition-all n-duration-200 n-max-w-[70%] n-text-sm",
64
- role === "user" ? "n-text-white" : "n-text-text",
65
- role === "user" ? "n-ml-auto" : "n-bg-input-background"
52
+ "n-rounded-lg n-py-2 n-px-3 n-transition-all n-duration-200 n-max-w-[70%] n-min-w-0 n-text-sm [&_pre]:n-whitespace-pre-wrap [&_code]:n-whitespace-pre-wrap",
53
+ role === "user"
54
+ ? "n-text-white n-ml-auto n-bg-primary"
55
+ : "n-text-text n-bg-input-background"
66
56
  )}
67
- style={styles}
68
57
  ref={ref}
69
58
  >
70
59
  {children}
@@ -0,0 +1,97 @@
1
+ "use client";
2
+
3
+ import React, { useMemo } from "react";
4
+ import { cx } from "../utils/classNames";
5
+ import { toolbarTransparentStyle } from "./BaseToolbar";
6
+
7
+ export type OverlayLayout = "bottomCenterTopRight" | "topLeftTopRight";
8
+
9
+ export interface OverlayToolbarProps {
10
+ left?: React.ReactNode;
11
+ right?: React.ReactNode;
12
+ overlayLayout?: OverlayLayout;
13
+ className?: string;
14
+ style?: React.CSSProperties;
15
+ }
16
+
17
+ const panelClassName =
18
+ "n-pointer-events-auto n-inline-flex n-items-center n-gap-toolbar-separator n-rounded-3xl n-border n-border-divider n-bg-sidebar-background n-px-3 n-py-2.5 n-shadow-2xl";
19
+
20
+ export function OverlayToolbar({
21
+ left,
22
+ right,
23
+ overlayLayout = "bottomCenterTopRight",
24
+ className,
25
+ style,
26
+ }: OverlayToolbarProps) {
27
+ const resolvedLayout =
28
+ overlayLayout === "topLeftTopRight"
29
+ ? "topLeftTopRight"
30
+ : "bottomCenterTopRight";
31
+
32
+ // Merge transparent backgrounds with custom style
33
+ const panelStyle = useMemo(
34
+ () => ({
35
+ ...toolbarTransparentStyle,
36
+ ...style,
37
+ }),
38
+ [style]
39
+ );
40
+
41
+ const renderLeftPanel = (overlayId: string) => {
42
+ if (!left) return null;
43
+
44
+ return (
45
+ <div
46
+ className={panelClassName}
47
+ data-embedded-overlay-id={overlayId}
48
+ style={panelStyle}
49
+ >
50
+ {left}
51
+ </div>
52
+ );
53
+ };
54
+
55
+ return (
56
+ <>
57
+ {/* Left items panel */}
58
+ {resolvedLayout === "bottomCenterTopRight" ? (
59
+ <div
60
+ className={cx(
61
+ "n-pointer-events-none n-absolute n-bottom-0 n-left-0 n-right-0 n-flex n-justify-center n-px-6 n-pb-6",
62
+ className
63
+ )}
64
+ >
65
+ {renderLeftPanel("noya-toolbar-bottom-center")}
66
+ </div>
67
+ ) : (
68
+ <div
69
+ className={cx(
70
+ "n-pointer-events-none n-absolute n-top-0 n-left-0 n-px-4 sm:!n-px-6 n-pt-4 sm:!n-pt-6",
71
+ className
72
+ )}
73
+ >
74
+ {renderLeftPanel("noya-toolbar-top-left")}
75
+ </div>
76
+ )}
77
+
78
+ {/* Right items panel */}
79
+ {right && (
80
+ <div
81
+ className={cx(
82
+ "n-pointer-events-none n-absolute n-top-0 n-right-0 n-px-4 sm:!n-px-6 n-pt-4 sm:!n-pt-6",
83
+ className
84
+ )}
85
+ >
86
+ <div
87
+ className={panelClassName}
88
+ data-embedded-overlay-id="noya-toolbar-top-right"
89
+ style={panelStyle}
90
+ >
91
+ {right}
92
+ </div>
93
+ </div>
94
+ )}
95
+ </>
96
+ );
97
+ }
@@ -1,39 +1,21 @@
1
1
  "use client";
2
2
 
3
+ import { Popover as PopoverPrimitive } from "@base-ui/react/popover";
3
4
  import { useControlledOrUncontrolled } from "@noya-app/react-utils";
4
- import { Popover as PopoverPrimitive } from "radix-ui";
5
- import React, {
6
- ComponentProps,
7
- useCallback,
8
- useEffect,
9
- useId,
10
- useRef,
11
- } from "react";
5
+ import React, { useCallback, useEffect, useId } from "react";
12
6
  import { useOpenPortalsControls } from "../contexts/OpenPortalsContext";
13
7
  import {
14
8
  portalScopeProps,
15
9
  usePortalScopeId,
16
10
  } from "../contexts/PortalScopeContext";
11
+ import { useTriggerToggle } from "../hooks/useTriggerToggle";
17
12
  import { cx } from "../utils/classNames";
13
+ import { mergePropsWithEventComposition } from "../utils/mergeProps";
18
14
  import { IconButton } from "./IconButton";
19
15
 
20
- const closeStyles: React.CSSProperties = {
21
- all: "unset",
22
- fontFamily: "inherit",
23
- };
24
-
25
16
  type PopoverVariant = "normal" | "large";
26
17
 
27
- interface Props
28
- extends Pick<
29
- ComponentProps<(typeof PopoverPrimitive)["Content"]>,
30
- | "onOpenAutoFocus"
31
- | "onCloseAutoFocus"
32
- | "onPointerDownOutside"
33
- | "onInteractOutside"
34
- | "onFocusOutside"
35
- | "side"
36
- > {
18
+ interface Props {
37
19
  children:
38
20
  | React.ReactNode
39
21
  | ((props: { close: () => void }) => React.ReactNode);
@@ -48,11 +30,16 @@ interface Props
48
30
  className?: string;
49
31
  style?: React.CSSProperties;
50
32
  portalContainer?: HTMLElement | null;
33
+ side?: "top" | "bottom" | "left" | "right";
34
+ /**
35
+ * Called when an interaction outside the popover occurs.
36
+ * Return false to prevent the popover from closing.
37
+ */
38
+ shouldCloseOnInteractOutside?: (event: PointerEvent) => boolean;
51
39
  }
52
40
 
53
41
  export const popoverStyle = {
54
- base:
55
- "n-rounded n-font-[14px] n-bg-popover-background n-shadow-popover n-max-h-[600px] n-overflow-y-auto n-text-text-muted n-z-[1000]",
42
+ base: "n-rounded n-font-[14px] n-bg-popover-background n-shadow-popover n-max-h-[600px] n-overflow-y-auto n-text-text-muted n-z-[1000]",
56
43
  large: "n-w-[680px]",
57
44
  normal: "n-w-[240px]",
58
45
  };
@@ -67,19 +54,14 @@ export function Popover({
67
54
  sideOffset = 4,
68
55
  showArrow = true,
69
56
  onOpenChange,
70
- onOpenAutoFocus,
71
- onCloseAutoFocus,
72
- onPointerDownOutside,
73
- onInteractOutside,
74
- onFocusOutside,
75
57
  onClickClose,
76
58
  className,
77
59
  portalContainer,
78
60
  style,
61
+ shouldCloseOnInteractOutside,
79
62
  }: Props) {
80
63
  const defaultId = useId();
81
64
  const portalScopeId = usePortalScopeId();
82
- const contentRef = useRef<HTMLDivElement>(null);
83
65
  const [isOpen, setIsOpen] = useControlledOrUncontrolled({
84
66
  value: open,
85
67
  onChange: onOpenChange,
@@ -87,6 +69,9 @@ export function Popover({
87
69
  });
88
70
  const close = useCallback(() => setIsOpen(false), [setIsOpen]);
89
71
 
72
+ const { anchorElement, setAnchorElement, popupRef, handleOpenChange } =
73
+ useTriggerToggle(isOpen, setIsOpen, { shouldCloseOnInteractOutside });
74
+
90
75
  const { addOpenPortal, removeOpenPortal } = useOpenPortalsControls();
91
76
 
92
77
  useEffect(() => {
@@ -100,91 +85,72 @@ export function Popover({
100
85
  }, [addOpenPortal, isOpen, portalScopeId, removeOpenPortal, defaultId]);
101
86
 
102
87
  return (
103
- <PopoverPrimitive.Root open={isOpen} onOpenChange={setIsOpen}>
104
- <PopoverPrimitive.Trigger asChild>{trigger}</PopoverPrimitive.Trigger>
88
+ <PopoverPrimitive.Root open={isOpen} onOpenChange={handleOpenChange}>
89
+ <PopoverPrimitive.Trigger
90
+ render={(triggerProps) => {
91
+ const element = React.isValidElement(trigger) ? (
92
+ trigger
93
+ ) : (
94
+ <span>{trigger}</span>
95
+ );
96
+ const childProps = element.props as Record<string, unknown>;
97
+ const mergedProps = mergePropsWithEventComposition(
98
+ triggerProps as Record<string, unknown>,
99
+ childProps
100
+ );
101
+
102
+ // Custom ref handling for anchor tracking
103
+ mergedProps.ref = (node: Element | null) => {
104
+ // Only update state when we get a valid node to prevent anchor loss during rerenders
105
+ if (node) setAnchorElement(node);
106
+ };
107
+
108
+ return React.cloneElement(
109
+ element as React.ReactElement<any>,
110
+ mergedProps
111
+ );
112
+ }}
113
+ />
105
114
  <PopoverPrimitive.Portal container={portalContainer}>
106
- <PopoverPrimitive.Content
115
+ <PopoverPrimitive.Positioner
107
116
  {...portalScopeProps(portalScopeId)}
108
- ref={contentRef}
109
- style={style}
110
- className={cx(
111
- popoverStyle.base,
112
- variant === "large" && popoverStyle.large,
113
- variant === "normal" && popoverStyle.normal,
114
- className
115
- )}
117
+ anchor={anchorElement}
116
118
  side={side}
117
119
  align="center"
118
120
  sideOffset={sideOffset}
119
- onOpenAutoFocus={onOpenAutoFocus}
120
- onCloseAutoFocus={onCloseAutoFocus}
121
- onInteractOutside={onInteractOutside}
122
- onFocusOutside={onFocusOutside}
123
- onPointerDownOutside={(event) => {
124
- if (
125
- isEventInsideElement(
126
- event.detail.originalEvent,
127
- contentRef.current
128
- )
129
- ) {
130
- event.preventDefault();
131
- }
132
-
133
- onPointerDownOutside?.(event);
134
- }}
135
121
  collisionPadding={8}
136
122
  arrowPadding={2}
137
- // Don't propagate pointer down events to the canvas
138
- // onPointerDown={(event) => {
139
- // event.stopPropagation();
140
- // }}
141
- // onWheel={(event) => {
142
- // event.stopPropagation();
143
- // }}
123
+ className="n-z-menu"
144
124
  >
145
- {typeof children === "function" ? children({ close }) : children}
146
- {showArrow && (
147
- <PopoverPrimitive.Arrow className="n-fill-popover-background" />
148
- )}
149
- {closable && (
150
- <PopoverPrimitive.Close
151
- className="n-rounded-full n-h-[25px] n-w-[25px] n-inline-flex n-items-center n-justify-center n-absolute n-top-[5px] n-right-[5px]"
152
- style={closeStyles}
153
- >
154
- <IconButton iconName="Cross2Icon" onClick={onClickClose} />
155
- </PopoverPrimitive.Close>
156
- )}
157
- </PopoverPrimitive.Content>
125
+ <PopoverPrimitive.Popup
126
+ ref={popupRef}
127
+ style={style}
128
+ className={cx(
129
+ popoverStyle.base,
130
+ variant === "large" && popoverStyle.large,
131
+ variant === "normal" && popoverStyle.normal,
132
+ className
133
+ )}
134
+ >
135
+ {typeof children === "function" ? children({ close }) : children}
136
+ {showArrow && (
137
+ <PopoverPrimitive.Arrow className="n-fill-popover-background" />
138
+ )}
139
+ {closable && (
140
+ <PopoverPrimitive.Close
141
+ className="n-rounded-full n-h-[25px] n-w-[25px] n-inline-flex n-items-center n-justify-center n-absolute n-top-[5px] n-right-[5px]"
142
+ render={
143
+ <IconButton
144
+ iconName="Cross2Icon"
145
+ onClick={onClickClose}
146
+ style={{ all: "unset", fontFamily: "inherit" }}
147
+ />
148
+ }
149
+ />
150
+ )}
151
+ </PopoverPrimitive.Popup>
152
+ </PopoverPrimitive.Positioner>
158
153
  </PopoverPrimitive.Portal>
159
154
  </PopoverPrimitive.Root>
160
155
  );
161
156
  }
162
-
163
- /**
164
- * Radix ignores pointer-events on the body when a popover is open.
165
- * This function checks if an event is inside an element by temporarily
166
- * setting pointer-events to auto and then restoring it afterward.
167
- */
168
- function isEventInsideElement(
169
- event: PointerEvent,
170
- element: HTMLElement | null
171
- ) {
172
- if (!element) return false;
173
-
174
- const body = document.body;
175
- const originalPointerEvents = body.style.pointerEvents;
176
- body.style.pointerEvents = "auto";
177
-
178
- // Get all elements at the point
179
- const elementsAtPoint = document.elementsFromPoint(
180
- event.clientX,
181
- event.clientY
182
- );
183
-
184
- // Restore original pointer-events
185
- body.style.pointerEvents = originalPointerEvents;
186
-
187
- const target = elementsAtPoint.at(0);
188
-
189
- return target && element.contains(target);
190
- }
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
 
3
3
  import { clamp } from "@noya-app/noya-utils";
4
- import { Progress as ProgressPrimitive } from "radix-ui";
4
+ import { Progress as ProgressPrimitive } from "@base-ui/react/progress";
5
5
  import * as React from "react";
6
6
  import { cx } from "../utils/classNames";
7
7
 
@@ -23,19 +23,17 @@ export function Progress({
23
23
  style?: React.CSSProperties;
24
24
  }) {
25
25
  const clampedValue = clamp(value, 0, 100);
26
- const transformStyles = React.useMemo(
27
- () => ({
28
- transform: `translateX(-${100 - clampedValue}%)`,
29
- backgroundColor: color,
30
- }),
31
- [color, clampedValue]
32
- );
33
26
 
34
27
  const style = React.useMemo(
35
28
  () => ({ transform: "translateZ(0)", ...styleProp }),
36
29
  [styleProp]
37
30
  );
38
31
 
32
+ const indicatorStyle = React.useMemo(
33
+ () => ({ backgroundColor: color }),
34
+ [color]
35
+ );
36
+
39
37
  return (
40
38
  <ProgressPrimitive.Root
41
39
  className={cx(
@@ -45,16 +43,18 @@ export function Progress({
45
43
  style={style}
46
44
  value={clampedValue}
47
45
  >
48
- <ProgressPrimitive.Indicator
49
- className={cx(
50
- "n-transition-[transform_660ms_cubic-bezier(0.65,0,0.35,1)] n-w-full n-h-full",
51
- colorScheme === "primary" && "n-bg-primary",
52
- colorScheme === "secondary" && "n-bg-secondary",
53
- colorScheme === "warning" && "n-bg-warning",
54
- colorScheme === "normal" && "n-bg-text"
55
- )}
56
- style={transformStyles}
57
- />
46
+ <ProgressPrimitive.Track className="n-w-full n-h-full">
47
+ <ProgressPrimitive.Indicator
48
+ className={cx(
49
+ "n-transition-[width_660ms_cubic-bezier(0.65,0,0.35,1)] n-h-full",
50
+ colorScheme === "primary" && "n-bg-primary",
51
+ colorScheme === "secondary" && "n-bg-secondary",
52
+ colorScheme === "warning" && "n-bg-warning",
53
+ colorScheme === "normal" && "n-bg-text"
54
+ )}
55
+ style={indicatorStyle}
56
+ />
57
+ </ProgressPrimitive.Track>
58
58
  </ProgressPrimitive.Root>
59
59
  );
60
60
  }
@@ -1,41 +1,76 @@
1
1
  "use client";
2
2
 
3
- import { ScrollArea as RadixScrollArea } from "radix-ui";
3
+ import { ScrollArea as BScrollArea } from "@base-ui/react/scroll-area";
4
4
  import * as React from "react";
5
+ import { cx } from "../utils/classNames";
5
6
 
6
- interface Props {
7
- children?:
8
- | React.ReactNode
9
- | ((scrollElementRef: HTMLDivElement) => React.ReactNode);
10
- }
7
+ const SCROLLBAR_STATE_CLASSES =
8
+ "scroll-component n-opacity-0 n-transition-opacity n-delay-300 n-pointer-events-none data-[hovering]:n-opacity-100 data-[hovering]:n-delay-0 data-[hovering]:n-duration-75 data-[hovering]:n-pointer-events-auto data-[scrolling]:n-opacity-100 data-[scrolling]:n-delay-0 data-[scrolling]:n-duration-75 data-[scrolling]:n-pointer-events-auto";
9
+
10
+ const SCROLLBAR_TRACK_VERTICAL =
11
+ "n-m-1 n-w-1 flex n-justify-center n-rounded-md n-bg-divider-subtle";
12
+ const SCROLLBAR_TRACK_HORIZONTAL =
13
+ "n-m-1 n-h-1 flex n-justify-center n-rounded-md n-bg-divider-subtle";
14
+
15
+ const SCROLLBAR_THUMB_VERTICAL =
16
+ "scroll-component n-w-full n-rounded n-bg-scrollbar";
17
+ const SCROLLBAR_THUMB_HORIZONTAL =
18
+ "scroll-component n-h-full n-rounded n-bg-scrollbar";
11
19
 
12
- export const ScrollArea = React.memo(function ScrollArea({ children }: Props) {
13
- const [scrollElementRef, setScrollElementRef] =
14
- React.useState<HTMLDivElement | null>(null);
20
+ type ScrollAreaViewportProps = React.HTMLAttributes<HTMLDivElement> & {
21
+ ref?: React.Ref<HTMLDivElement>;
22
+ };
23
+
24
+ export function ScrollArea({
25
+ children,
26
+ className,
27
+ viewportClassName,
28
+ orientation = "vertical",
29
+ style,
30
+ viewportProps,
31
+ }: {
32
+ children: React.ReactNode;
33
+ className?: string;
34
+ viewportClassName?: string;
35
+ orientation?: "vertical" | "horizontal" | "both";
36
+ style?: React.CSSProperties;
37
+ viewportProps?: ScrollAreaViewportProps;
38
+ }) {
39
+ const {
40
+ className: viewportPropsClassName,
41
+ ref: viewportRef,
42
+ ...viewportRest
43
+ } = viewportProps ?? {};
15
44
 
16
45
  return (
17
- <div className="n-flex-1 n-min-h-0">
18
- <RadixScrollArea.Root style={{ width: "100%", height: "100%" }}>
19
- <RadixScrollArea.Viewport
20
- className="n-w-full n-h-full [&>div]:n-block"
21
- ref={React.useCallback(
22
- (ref: HTMLDivElement | null) => setScrollElementRef(ref),
23
- []
24
- )}
46
+ <BScrollArea.Root className={cx("n-relative", className)} style={style}>
47
+ <BScrollArea.Viewport
48
+ ref={viewportRef}
49
+ className={cx(
50
+ "n-h-full n-overscroll-contain",
51
+ viewportClassName,
52
+ viewportPropsClassName
53
+ )}
54
+ {...viewportRest}
55
+ >
56
+ {children}
57
+ </BScrollArea.Viewport>
58
+ {(orientation === "vertical" || orientation === "both") && (
59
+ <BScrollArea.Scrollbar
60
+ className={cx(SCROLLBAR_STATE_CLASSES, SCROLLBAR_TRACK_VERTICAL)}
25
61
  >
26
- {typeof children === "function"
27
- ? scrollElementRef
28
- ? children(scrollElementRef)
29
- : null
30
- : children}
31
- </RadixScrollArea.Viewport>
32
- <RadixScrollArea.Scrollbar
33
- orientation="vertical"
34
- className="n-scroll-component n-flex n-p-[3px] data-[orientation=vertical]:n-w-[10px]"
62
+ <BScrollArea.Thumb className={SCROLLBAR_THUMB_VERTICAL} />
63
+ </BScrollArea.Scrollbar>
64
+ )}
65
+ {(orientation === "horizontal" || orientation === "both") && (
66
+ <BScrollArea.Scrollbar
67
+ orientation="horizontal"
68
+ className={cx(SCROLLBAR_STATE_CLASSES, SCROLLBAR_TRACK_HORIZONTAL)}
35
69
  >
36
- <RadixScrollArea.Thumb className="n-scroll-component n-flex-1 n-rounded-[10px] n-bg-scrollbar" />
37
- </RadixScrollArea.Scrollbar>
38
- </RadixScrollArea.Root>
39
- </div>
70
+ <BScrollArea.Thumb className={SCROLLBAR_THUMB_HORIZONTAL} />
71
+ </BScrollArea.Scrollbar>
72
+ )}
73
+ {orientation === "both" && <BScrollArea.Corner />}
74
+ </BScrollArea.Root>
40
75
  );
41
- });
76
+ }
@@ -4,7 +4,7 @@ import { ScrollArea } from "./ScrollArea";
4
4
  export function ScrollableSidebar({ children }: { children: React.ReactNode }) {
5
5
  return (
6
6
  <div className="n-relative n-flex n-flex-col n-flex-1 n-max-h-full">
7
- <ScrollArea>{children}</ScrollArea>
7
+ <ScrollArea className="n-flex-1 n-min-h-0">{children}</ScrollArea>
8
8
  </div>
9
9
  );
10
10
  }