@noya-app/noya-designsystem 0.1.87 → 0.1.89

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 (33) hide show
  1. package/.turbo/turbo-build.log +10 -10
  2. package/CHANGELOG.md +14 -0
  3. package/dist/index.css +1 -1
  4. package/dist/index.d.mts +181 -120
  5. package/dist/index.d.ts +181 -120
  6. package/dist/index.js +2147 -1915
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +1936 -1711
  9. package/dist/index.mjs.map +1 -1
  10. package/package.json +2 -2
  11. package/src/__tests__/SharedDragProvider.test.ts +39 -0
  12. package/src/components/AppToolbar.tsx +464 -0
  13. package/src/components/Card.tsx +20 -0
  14. package/src/components/Dialog.tsx +12 -5
  15. package/src/components/DialogNavigator.tsx +154 -0
  16. package/src/components/InputField.tsx +9 -7
  17. package/src/components/Navigator.tsx +2 -0
  18. package/src/components/SegmentedControl.tsx +32 -4
  19. package/src/components/SelectionToolbar.tsx +5 -10
  20. package/src/components/StatCard.tsx +82 -0
  21. package/src/components/Toolbar.tsx +32 -459
  22. package/src/components/__tests__/Card.test.tsx +71 -0
  23. package/src/components/__tests__/DialogNavigator.test.tsx +88 -0
  24. package/src/components/__tests__/InputField.test.tsx +227 -3
  25. package/src/components/__tests__/SegmentedControl.test.ts +46 -0
  26. package/src/components/__tests__/Toolbar.test.tsx +56 -0
  27. package/src/components/connected-users-menu/ConnectedUsersMenuLayout.tsx +0 -16
  28. package/src/components/sorting/SharedDragProvider.tsx +37 -1
  29. package/src/components/workspace/DrawerWorkspaceLayout.tsx +2 -2
  30. package/src/components/workspace/VerticalTabMenu.tsx +1 -1
  31. package/src/components/workspace/__tests__/DrawerWorkspaceLayout.test.tsx +2 -0
  32. package/src/index.css +6 -0
  33. package/src/index.tsx +4 -0
@@ -0,0 +1,154 @@
1
+ "use client";
2
+
3
+ import React, { ReactNode } from "react";
4
+ import { cx } from "../utils/classNames";
5
+ import { BaseToolbar } from "./BaseToolbar";
6
+ import { Button } from "./Button";
7
+ import { Dialog, DialogProps } from "./Dialog";
8
+ import { IconButton } from "./IconButton";
9
+ import {
10
+ Navigator,
11
+ NavigatorParams,
12
+ NavigatorProps,
13
+ NavigatorRoute,
14
+ NavigatorRoutesConfig,
15
+ NavigatorScreenProps,
16
+ useNavigator,
17
+ useNavigatorRoute,
18
+ } from "./Navigator";
19
+ import { StackNavigatorHeaderProps } from "./StackNavigator";
20
+ import { Body } from "./Text";
21
+
22
+ export type DialogNavigatorProps<
23
+ Routes extends Record<string, NavigatorRoute<any>>,
24
+ Name extends keyof Routes,
25
+ > = Omit<
26
+ NavigatorProps<Routes, Name>,
27
+ "children" | "className" | "style" | "renderHeader" | "showHeader"
28
+ > &
29
+ Pick<DialogProps, "closeOnInteractOutside" | "open"> & {
30
+ children: ReactNode;
31
+ title: ReactNode;
32
+ onOpenChange: (open: boolean) => void;
33
+ className?: string;
34
+ style?: React.CSSProperties;
35
+ navigatorClassName?: string;
36
+ navigatorStyle?: React.CSSProperties;
37
+ showCloseButton?: boolean;
38
+ };
39
+
40
+ export function DialogNavigator<
41
+ Routes extends Record<string, NavigatorRoute<any>>,
42
+ Name extends keyof Routes,
43
+ >({
44
+ children,
45
+ title,
46
+ open,
47
+ onOpenChange,
48
+ closeOnInteractOutside,
49
+ className,
50
+ style,
51
+ navigatorClassName,
52
+ navigatorStyle,
53
+ showCloseButton = true,
54
+ ...navigatorProps
55
+ }: DialogNavigatorProps<Routes, Name>) {
56
+ return (
57
+ <Dialog
58
+ title={title}
59
+ titleVisibility="hidden"
60
+ open={open}
61
+ onOpenChange={onOpenChange}
62
+ closeOnInteractOutside={closeOnInteractOutside}
63
+ showCloseButton={false}
64
+ className={cx("n-p-0 n-overflow-hidden", className)}
65
+ style={style}
66
+ >
67
+ <Navigator
68
+ {...navigatorProps}
69
+ className={navigatorClassName}
70
+ style={navigatorStyle}
71
+ renderHeader={(headerProps) => (
72
+ <DialogNavigatorHeader
73
+ {...headerProps}
74
+ showCloseButton={showCloseButton}
75
+ onClose={() => onOpenChange(false)}
76
+ />
77
+ )}
78
+ >
79
+ {children}
80
+ </Navigator>
81
+ </Dialog>
82
+ );
83
+ }
84
+
85
+ function DialogNavigatorHeader({
86
+ showDivider,
87
+ showBackButton,
88
+ onBack,
89
+ right,
90
+ title,
91
+ showCloseButton,
92
+ onClose,
93
+ }: StackNavigatorHeaderProps & {
94
+ showCloseButton: boolean;
95
+ onClose: () => void;
96
+ }) {
97
+ return (
98
+ <BaseToolbar
99
+ innerClassName="n-px-1"
100
+ showDivider={showDivider}
101
+ enableAbsoluteBreakpoint={false}
102
+ enableContainerQuery={false}
103
+ left={
104
+ showBackButton && (
105
+ <Button
106
+ className="-n-mx-2"
107
+ icon="ArrowLeftIcon"
108
+ onClick={onBack}
109
+ aria-label="Back"
110
+ />
111
+ )
112
+ }
113
+ right={
114
+ (right || showCloseButton) && (
115
+ <div className="n-flex n-items-center n-gap-1">
116
+ {right}
117
+ {showCloseButton && (
118
+ <IconButton
119
+ iconName="Cross1Icon"
120
+ aria-label="Close"
121
+ onClick={onClose}
122
+ />
123
+ )}
124
+ </div>
125
+ )
126
+ }
127
+ >
128
+ {typeof title === "string" || typeof title === "number" ? (
129
+ <Body>{title}</Body>
130
+ ) : (
131
+ title
132
+ )}
133
+ </BaseToolbar>
134
+ );
135
+ }
136
+
137
+ export function createDialogNavigator<
138
+ RoutesConfig extends Record<string, NavigatorParams | null>,
139
+ >() {
140
+ return Object.assign(
141
+ DialogNavigator as <Name extends keyof RoutesConfig>(
142
+ props: DialogNavigatorProps<NavigatorRoutesConfig<RoutesConfig>, Name>
143
+ ) => ReactNode,
144
+ {
145
+ Screen: Navigator.Screen as <Name extends keyof RoutesConfig>(
146
+ props: NavigatorScreenProps<NavigatorRoutesConfig<RoutesConfig>, Name>
147
+ ) => ReactNode,
148
+ useNavigator: useNavigator<NavigatorRoutesConfig<RoutesConfig>>,
149
+ useNavigatorRoute: useNavigatorRoute as <Name extends keyof RoutesConfig>(
150
+ name: Name
151
+ ) => NavigatorRoute<NavigatorRoutesConfig<RoutesConfig>[Name]["params"]>,
152
+ }
153
+ );
154
+ }
@@ -434,23 +434,25 @@ function InputFieldNumberInput(props: InputFieldNumberInputProps) {
434
434
  );
435
435
 
436
436
  const handleChange = useCallback(
437
- (value: string) => {
438
- if (value === "" || value === "-" || value === ".") {
439
- setInternalValue(value);
437
+ (inputValue: string) => {
438
+ if (inputValue === "" || inputValue === "-" || inputValue === ".") {
439
+ setInternalValue(inputValue);
440
440
  if (triggersOnEmpty) {
441
441
  onChange?.(0, { isEmpty: true });
442
442
  }
443
443
  return;
444
444
  }
445
445
 
446
- if (value.endsWith(".")) {
447
- setInternalValue(value);
446
+ if (inputValue.endsWith(".")) {
447
+ setInternalValue(inputValue);
448
448
  return;
449
449
  }
450
450
 
451
- setInternalValue(undefined);
451
+ // Keep the text being edited stable if the parent rejects or normalizes
452
+ // an intermediate numeric value. The accepted prop value takes over on blur.
453
+ setInternalValue(inputValue);
452
454
 
453
- const newValue = parseNumber(value);
455
+ const newValue = parseNumber(inputValue);
454
456
 
455
457
  if (!isNaN(newValue)) {
456
458
  onChange?.(newValue, { isEmpty: false });
@@ -71,6 +71,7 @@ export type NavigatorHeaderProps<
71
71
  title: React.ReactNode;
72
72
  right: React.ReactNode;
73
73
  showBackButton: boolean;
74
+ showDivider: boolean;
74
75
  onBack?: () => void;
75
76
  };
76
77
 
@@ -406,6 +407,7 @@ function NavigatorImpl<
406
407
  title,
407
408
  right,
408
409
  showBackButton: headerProps.showBackButton,
410
+ showDivider: headerProps.showDivider,
409
411
  onBack: headerProps.onBack,
410
412
  }),
411
413
  renderContent: () => {
@@ -87,6 +87,30 @@ export type SegmentedControlItemProps<T extends string> = {
87
87
  "value" | "disabled" | "icon" | "role" | "title"
88
88
  >;
89
89
 
90
+ export function snapIndicatorEdgesToDevicePixels({
91
+ containerLeft,
92
+ itemLeft,
93
+ itemRight,
94
+ devicePixelRatio,
95
+ }: {
96
+ containerLeft: number;
97
+ itemLeft: number;
98
+ itemRight: number;
99
+ devicePixelRatio: number;
100
+ }) {
101
+ const scale =
102
+ Number.isFinite(devicePixelRatio) && devicePixelRatio > 0
103
+ ? devicePixelRatio
104
+ : 1;
105
+ const snappedLeft = Math.round(itemLeft * scale) / scale;
106
+ const snappedRight = Math.round(itemRight * scale) / scale;
107
+
108
+ return {
109
+ left: snappedLeft - containerLeft,
110
+ width: Math.max(0, snappedRight - snappedLeft),
111
+ };
112
+ }
113
+
90
114
  const SegmentedControlItem = forwardRef(function SegmentedControlItem<
91
115
  T extends string,
92
116
  >(
@@ -256,10 +280,14 @@ function useIndicatorPosition({
256
280
  const containerRect = container.getBoundingClientRect();
257
281
  const itemRect = selectedItem.getBoundingClientRect();
258
282
 
259
- setIndicatorStyle({
260
- left: itemRect.left - containerRect.left,
261
- width: itemRect.width,
262
- });
283
+ setIndicatorStyle(
284
+ snapIndicatorEdgesToDevicePixels({
285
+ containerLeft: containerRect.left,
286
+ itemLeft: itemRect.left,
287
+ itemRight: itemRect.right,
288
+ devicePixelRatio: window.devicePixelRatio,
289
+ })
290
+ );
263
291
 
264
292
  hasInitialized.current = true;
265
293
  }, [containerRef, itemRefs, selectedIndex, variant, itemCount, isCompact]);
@@ -1,8 +1,6 @@
1
1
  "use client";
2
2
 
3
3
  import {
4
- cssVarNames,
5
- cx,
6
4
  portalScopeProps,
7
5
  usePortalScopeId,
8
6
  } from "@noya-app/noya-designsystem";
@@ -18,6 +16,7 @@ import {
18
16
  type VirtualElement,
19
17
  } from "@floating-ui/react";
20
18
  import React, { memo, useCallback, useEffect, useMemo, useState } from "react";
19
+ import { Toolbar } from "./Toolbar";
21
20
 
22
21
  type Side = "top" | "right" | "bottom" | "left";
23
22
  type Align = "start" | "center" | "end";
@@ -106,17 +105,13 @@ export const SelectionToolbarContainer = memo(
106
105
  ...floatingStyles,
107
106
  opacity: size ? 1 : 0,
108
107
  transition: `opacity ${transitionDuration}s ease-in-out`,
109
- [cssVarNames.colors.inputBackground]: "transparent",
110
- [cssVarNames.colors.buttonBackground]: "transparent",
111
108
  }}
112
109
  className="n-z-menu"
113
110
  >
114
- <div
111
+ <Toolbar
115
112
  ref={containerRef}
116
- className={cx(
117
- "n-flex n-gap-1 n-bg-popover-background n-rounded n-shadow-popover n-p-1",
118
- containerClassName
119
- )}
113
+ size="small"
114
+ className={containerClassName}
120
115
  style={containerStyle}
121
116
  onClick={(e) => e.stopPropagation()}
122
117
  onMouseDown={(e) => e.stopPropagation()}
@@ -124,7 +119,7 @@ export const SelectionToolbarContainer = memo(
124
119
  onWheel={(e) => e.stopPropagation()}
125
120
  >
126
121
  {children}
127
- </div>
122
+ </Toolbar>
128
123
  </div>
129
124
  </FloatingPortal>
130
125
  );
@@ -0,0 +1,82 @@
1
+ import { ArrowDownIcon, ArrowUpIcon } from "@noya-app/noya-icons";
2
+ import React, { forwardRef } from "react";
3
+ import { cx } from "../utils/classNames";
4
+ import { Card, CardProps } from "./Card";
5
+ import { Chip, ChipProps } from "./Chip";
6
+ import { IconProp, renderIcon } from "./Icons";
7
+ import { Heading1, Small } from "./Text";
8
+
9
+ export type StatCardColorScheme = NonNullable<ChipProps["colorScheme"]>;
10
+ export type StatCardTrend = "up" | "down";
11
+
12
+ export type StatCardProps = Omit<CardProps, "children"> & {
13
+ label: React.ReactNode;
14
+ value: React.ReactNode;
15
+ icon?: IconProp;
16
+ change?: React.ReactNode;
17
+ trend?: StatCardTrend;
18
+ colorScheme?: StatCardColorScheme;
19
+ };
20
+
21
+ const ICON_COLOR_STYLES: Record<StatCardColorScheme, string> = {
22
+ primary: "n-bg-primary",
23
+ secondary: "n-bg-secondary",
24
+ info: "n-bg-banner-info-text",
25
+ error: "n-bg-banner-error-text",
26
+ };
27
+
28
+ export const StatCard = forwardRef<HTMLDivElement, StatCardProps>(
29
+ function StatCard(
30
+ {
31
+ label,
32
+ value,
33
+ icon,
34
+ change,
35
+ trend,
36
+ colorScheme = "primary",
37
+ className,
38
+ ...props
39
+ },
40
+ forwardedRef
41
+ ) {
42
+ const TrendIcon = trend === "down" ? ArrowDownIcon : ArrowUpIcon;
43
+
44
+ return (
45
+ <Card
46
+ ref={forwardedRef}
47
+ className={cx("n-flex n-items-center n-gap-4", className)}
48
+ {...props}
49
+ >
50
+ {icon && (
51
+ <div
52
+ aria-hidden="true"
53
+ className={cx(
54
+ "n-flex n-h-16 n-w-16 n-flex-none n-items-center n-justify-center n-rounded-md n-text-white [&>svg]:n-h-8 [&>svg]:n-w-8",
55
+ ICON_COLOR_STYLES[colorScheme]
56
+ )}
57
+ >
58
+ {renderIcon(icon)}
59
+ </div>
60
+ )}
61
+ <div className="n-flex n-min-w-0 n-flex-1 n-self-stretch n-flex-col n-justify-center n-gap-1">
62
+ <div className="n-flex n-items-center n-gap-4">
63
+ <Small className="n-min-w-0 n-flex-1 n-opacity-70">{label}</Small>
64
+ {change !== undefined && (
65
+ <Chip
66
+ className="n-flex-none n-gap-1"
67
+ colorScheme={colorScheme}
68
+ size="large"
69
+ >
70
+ {trend && <TrendIcon aria-hidden="true" />}
71
+ {change}
72
+ </Chip>
73
+ )}
74
+ </div>
75
+ <Heading1 as="div" className="!n-font-bold n-leading-none">
76
+ {value}
77
+ </Heading1>
78
+ </div>
79
+ </Card>
80
+ );
81
+ }
82
+ );