@noya-app/noya-designsystem 0.1.86 → 0.1.88

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.
@@ -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
+ }
@@ -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
+ );