@arronqzy/ui 1.0.0 → 1.0.3

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.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # @arronqzy/ui
2
+
3
+ Abuilder 的**共享 UI 组件库**,基于 [shadcn/ui](https://ui.shadcn.com/) + Radix UI + Tailwind CSS,为编辑器与蓝图提供统一视觉与交互。
4
+
5
+ ## 功能概览
6
+
7
+ - **基础组件**:Button、Input、Dialog、Dropdown、Tabs、Tooltip、Toast 等
8
+ - **表单**:与 `react-hook-form`、`zod` 集成的表单控件
9
+ - **布局**:Resizable Panels、ScrollArea、Separator
10
+ - **主题**:`ThemeProvider` 支持亮/暗色与 `class` 策略
11
+ - **层级管理**:`UI_Z_INDEX` / `MODAL_Z_INDEX` 统一浮层 z-index
12
+ - **图表**:封装 Recharts 常用图表(按需使用)
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ pnpm add @arronqzy/ui
18
+ ```
19
+
20
+ ```ts
21
+ import "@arronqzy/ui/styles.css";
22
+ ```
23
+
24
+ ## 使用
25
+
26
+ ```tsx
27
+ import { ThemeProvider, Button, toast } from "@arronqzy/ui";
28
+ import "@arronqzy/ui/styles.css";
29
+
30
+ function App() {
31
+ return (
32
+ <ThemeProvider defaultTheme="dark">
33
+ <Button onClick={() => toast({ title: "Hello" })}>点击</Button>
34
+ </ThemeProvider>
35
+ );
36
+ }
37
+ ```
38
+
39
+ ### 工具函数
40
+
41
+ ```ts
42
+ import { cn } from "@arronqzy/ui";
43
+ import { UI_Z_INDEX } from "@arronqzy/ui";
44
+ ```
45
+
46
+ ## 导出说明
47
+
48
+ - `@arronqzy/ui` — 全部组件、`cn`、`ThemeProvider`、`useToast`
49
+ - `@arronqzy/ui/styles.css` — 编译后的 Tailwind 样式
50
+
51
+ 组件源码位于 `src/ui/`,遵循 shadcn 可组合模式,可按需在本包内扩展。
52
+
53
+ ## 构建样式
54
+
55
+ ```bash
56
+ pnpm -C packages/ui build:css
57
+ ```
58
+
59
+ 依赖 `@arronqzy/tailwind` 预设保持与设计令牌一致。
60
+
61
+ ## 许可证
62
+
63
+ MIT
package/dist/styles.css CHANGED
@@ -657,6 +657,11 @@ body {
657
657
  right: 0px;
658
658
  }
659
659
 
660
+ .inset-x-10 {
661
+ left: 2.5rem;
662
+ right: 2.5rem;
663
+ }
664
+
660
665
  .inset-y-0 {
661
666
  top: 0px;
662
667
  bottom: 0px;
@@ -682,6 +687,10 @@ body {
682
687
  bottom: 0px;
683
688
  }
684
689
 
690
+ .bottom-2 {
691
+ bottom: 0.5rem;
692
+ }
693
+
685
694
  .left-0 {
686
695
  left: 0px;
687
696
  }
@@ -706,6 +715,10 @@ body {
706
715
  right: 0.5rem;
707
716
  }
708
717
 
718
+ .right-3 {
719
+ right: 0.75rem;
720
+ }
721
+
709
722
  .right-4 {
710
723
  right: 1rem;
711
724
  }
@@ -722,6 +735,10 @@ body {
722
735
  top: 0.5rem;
723
736
  }
724
737
 
738
+ .top-3 {
739
+ top: 0.75rem;
740
+ }
741
+
725
742
  .top-4 {
726
743
  top: 1rem;
727
744
  }
@@ -746,6 +763,14 @@ body {
746
763
  z-index: 10;
747
764
  }
748
765
 
766
+ .z-20 {
767
+ z-index: 20;
768
+ }
769
+
770
+ .z-30 {
771
+ z-index: 30;
772
+ }
773
+
749
774
  .z-50 {
750
775
  z-index: 50;
751
776
  }
@@ -754,14 +779,14 @@ body {
754
779
  z-index: 10000;
755
780
  }
756
781
 
757
- .z-\[100\] {
758
- z-index: 100;
759
- }
760
-
761
782
  .z-\[1\] {
762
783
  z-index: 1;
763
784
  }
764
785
 
786
+ .z-toast {
787
+ z-index: 99999;
788
+ }
789
+
765
790
  .-mx-1 {
766
791
  margin-left: -0.25rem;
767
792
  margin-right: -0.25rem;
@@ -876,6 +901,10 @@ body {
876
901
  height: var(--cell-size);
877
902
  }
878
903
 
904
+ .h-1 {
905
+ height: 0.25rem;
906
+ }
907
+
879
908
  .h-1\.5 {
880
909
  height: 0.375rem;
881
910
  }
@@ -972,6 +1001,10 @@ body {
972
1001
  max-height: var(--radix-dropdown-menu-content-available-height);
973
1002
  }
974
1003
 
1004
+ .max-h-none {
1005
+ max-height: none;
1006
+ }
1007
+
975
1008
  .max-h-screen {
976
1009
  max-height: 100vh;
977
1010
  }
@@ -988,6 +1021,10 @@ body {
988
1021
  width: 0.25rem;
989
1022
  }
990
1023
 
1024
+ .w-1\.5 {
1025
+ width: 0.375rem;
1026
+ }
1027
+
991
1028
  .w-10 {
992
1029
  width: 2.5rem;
993
1030
  }
@@ -996,6 +1033,10 @@ body {
996
1033
  width: 2.75rem;
997
1034
  }
998
1035
 
1036
+ .w-12 {
1037
+ width: 3rem;
1038
+ }
1039
+
999
1040
  .w-2 {
1000
1041
  width: 0.5rem;
1001
1042
  }
@@ -1177,11 +1218,21 @@ body {
1177
1218
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1178
1219
  }
1179
1220
 
1221
+ .translate-x-0 {
1222
+ --tw-translate-x: 0px;
1223
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1224
+ }
1225
+
1180
1226
  .translate-x-\[-50\%\] {
1181
1227
  --tw-translate-x: -50%;
1182
1228
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1183
1229
  }
1184
1230
 
1231
+ .translate-y-0 {
1232
+ --tw-translate-y: 0px;
1233
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1234
+ }
1235
+
1185
1236
  .translate-y-\[-50\%\] {
1186
1237
  --tw-translate-y: -50%;
1187
1238
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
@@ -1201,6 +1252,10 @@ body {
1201
1252
  transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1202
1253
  }
1203
1254
 
1255
+ .animate-none {
1256
+ animation: none;
1257
+ }
1258
+
1204
1259
  @keyframes pulse {
1205
1260
  50% {
1206
1261
  opacity: .5;
@@ -1225,10 +1280,46 @@ body {
1225
1280
  cursor: default;
1226
1281
  }
1227
1282
 
1283
+ .cursor-e-resize {
1284
+ cursor: e-resize;
1285
+ }
1286
+
1287
+ .cursor-grab {
1288
+ cursor: grab;
1289
+ }
1290
+
1291
+ .cursor-n-resize {
1292
+ cursor: n-resize;
1293
+ }
1294
+
1295
+ .cursor-ne-resize {
1296
+ cursor: ne-resize;
1297
+ }
1298
+
1299
+ .cursor-nw-resize {
1300
+ cursor: nw-resize;
1301
+ }
1302
+
1228
1303
  .cursor-pointer {
1229
1304
  cursor: pointer;
1230
1305
  }
1231
1306
 
1307
+ .cursor-s-resize {
1308
+ cursor: s-resize;
1309
+ }
1310
+
1311
+ .cursor-se-resize {
1312
+ cursor: se-resize;
1313
+ }
1314
+
1315
+ .cursor-sw-resize {
1316
+ cursor: sw-resize;
1317
+ }
1318
+
1319
+ .cursor-w-resize {
1320
+ cursor: w-resize;
1321
+ }
1322
+
1232
1323
  .touch-none {
1233
1324
  touch-action: none;
1234
1325
  }
@@ -1239,6 +1330,14 @@ body {
1239
1330
  user-select: none;
1240
1331
  }
1241
1332
 
1333
+ .\!resize {
1334
+ resize: both !important;
1335
+ }
1336
+
1337
+ .resize {
1338
+ resize: both;
1339
+ }
1340
+
1242
1341
  .list-none {
1243
1342
  list-style-type: none;
1244
1343
  }
@@ -1531,6 +1630,10 @@ body {
1531
1630
  background-color: hsl(var(--muted));
1532
1631
  }
1533
1632
 
1633
+ .bg-muted-foreground\/40 {
1634
+ background-color: hsl(var(--muted-foreground) / 0.4);
1635
+ }
1636
+
1534
1637
  .bg-muted\/50 {
1535
1638
  background-color: hsl(var(--muted) / 0.5);
1536
1639
  }
@@ -1677,6 +1780,10 @@ body {
1677
1780
  padding-right: 0.25rem;
1678
1781
  }
1679
1782
 
1783
+ .pr-10 {
1784
+ padding-right: 2.5rem;
1785
+ }
1786
+
1680
1787
  .pr-2 {
1681
1788
  padding-right: 0.5rem;
1682
1789
  }
@@ -1701,6 +1808,10 @@ body {
1701
1808
  padding-top: 1rem;
1702
1809
  }
1703
1810
 
1811
+ .pt-8 {
1812
+ padding-top: 2rem;
1813
+ }
1814
+
1704
1815
  .text-left {
1705
1816
  text-align: left;
1706
1817
  }
@@ -1930,6 +2041,10 @@ body {
1930
2041
  transition-duration: 150ms;
1931
2042
  }
1932
2043
 
2044
+ .transition-none {
2045
+ transition-property: none;
2046
+ }
2047
+
1933
2048
  .transition-opacity {
1934
2049
  transition-property: opacity;
1935
2050
  transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
@@ -2183,6 +2298,10 @@ body {
2183
2298
  --tw-ring-offset-color: hsl(var(--background));
2184
2299
  }
2185
2300
 
2301
+ .active\:cursor-grabbing:active {
2302
+ cursor: grabbing;
2303
+ }
2304
+
2186
2305
  .disabled\:pointer-events-none:disabled {
2187
2306
  pointer-events: none;
2188
2307
  }
@@ -2300,6 +2419,10 @@ body {
2300
2419
  pointer-events: none;
2301
2420
  }
2302
2421
 
2422
+ .data-\[state\=closed\]\:pointer-events-none[data-state="closed"] {
2423
+ pointer-events: none;
2424
+ }
2425
+
2303
2426
  .data-\[panel-group-direction\=vertical\]\:h-px[data-panel-group-direction="vertical"] {
2304
2427
  height: 1px;
2305
2428
  }
@@ -2493,10 +2616,6 @@ body {
2493
2616
  color: hsl(var(--accent-foreground));
2494
2617
  }
2495
2618
 
2496
- .data-\[state\=open\]\:text-muted-foreground[data-state="open"] {
2497
- color: hsl(var(--muted-foreground));
2498
- }
2499
-
2500
2619
  .data-\[disabled\=true\]\:opacity-50[data-disabled="true"] {
2501
2620
  opacity: 0.5;
2502
2621
  }
@@ -2625,6 +2744,10 @@ body {
2625
2744
  --tw-exit-scale: .95;
2626
2745
  }
2627
2746
 
2747
+ .data-\[state\=open\]\:zoom-in-100[data-state="open"] {
2748
+ --tw-enter-scale: 1;
2749
+ }
2750
+
2628
2751
  .data-\[state\=open\]\:zoom-in-90[data-state="open"] {
2629
2752
  --tw-enter-scale: .9;
2630
2753
  }
@@ -2701,6 +2824,10 @@ body {
2701
2824
  --tw-enter-translate-x: -100%;
2702
2825
  }
2703
2826
 
2827
+ .data-\[state\=open\]\:slide-in-from-left-0[data-state="open"] {
2828
+ --tw-enter-translate-x: -0px;
2829
+ }
2830
+
2704
2831
  .data-\[state\=open\]\:slide-in-from-left-1\/2[data-state="open"] {
2705
2832
  --tw-enter-translate-x: -50%;
2706
2833
  }
@@ -2713,6 +2840,10 @@ body {
2713
2840
  --tw-enter-translate-y: -100%;
2714
2841
  }
2715
2842
 
2843
+ .data-\[state\=open\]\:slide-in-from-top-0[data-state="open"] {
2844
+ --tw-enter-translate-y: -0px;
2845
+ }
2846
+
2716
2847
  .data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state="open"] {
2717
2848
  --tw-enter-translate-y: -48%;
2718
2849
  }
@@ -2769,18 +2900,6 @@ body {
2769
2900
  }
2770
2901
 
2771
2902
  @media (min-width: 640px) {
2772
- .sm\:bottom-0 {
2773
- bottom: 0px;
2774
- }
2775
-
2776
- .sm\:right-0 {
2777
- right: 0px;
2778
- }
2779
-
2780
- .sm\:top-auto {
2781
- top: auto;
2782
- }
2783
-
2784
2903
  .sm\:mt-0 {
2785
2904
  margin-top: 0px;
2786
2905
  }
@@ -2793,10 +2912,6 @@ body {
2793
2912
  flex-direction: row;
2794
2913
  }
2795
2914
 
2796
- .sm\:flex-col {
2797
- flex-direction: column;
2798
- }
2799
-
2800
2915
  .sm\:justify-end {
2801
2916
  justify-content: flex-end;
2802
2917
  }
@@ -2818,10 +2933,6 @@ body {
2818
2933
  .sm\:text-left {
2819
2934
  text-align: left;
2820
2935
  }
2821
-
2822
- .data-\[state\=open\]\:sm\:slide-in-from-bottom-full[data-state="open"] {
2823
- --tw-enter-translate-y: 100%;
2824
- }
2825
2936
  }
2826
2937
 
2827
2938
  @media (min-width: 768px) {
package/index.ts CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  export * from "./src/ui/index";
4
4
  export * from "./src/lib/utils";
5
- export { MODAL_Z_INDEX } from "./src/lib/overlay-z-index";
5
+ export { MODAL_Z_INDEX, UI_Z_INDEX } from "./src/lib/overlay-z-index";
6
6
  export * from "./src/hooks/use-toast";
7
7
  export * from "./src/theme-provider";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/ui",
3
- "version": "1.0.0",
3
+ "version": "1.0.3",
4
4
  "exports": {
5
5
  ".": "./index.ts",
6
6
  "./counter": "./components/counter.ts",
@@ -64,7 +64,7 @@
64
64
  "typescript": "^5.5.4",
65
65
  "@arronqzy/eslint-config": "0.0.0",
66
66
  "@arronqzy/typescript-config": "1.0.0",
67
- "@arronqzy/tailwind": "1.0.1"
67
+ "@arronqzy/tailwind": "1.0.2"
68
68
  },
69
69
  "scripts": {
70
70
  "lint": "eslint \"**/*.{ts,tsx}\"",
@@ -1,8 +1,25 @@
1
1
  import type { CSSProperties } from "react";
2
2
 
3
- /** 弹窗层级:高于画布 Ruler(999)、工具栏(1200)、Menubar(10100) 等 */
4
- export const MODAL_Z_INDEX = 20000;
5
- export const MODAL_CONTENT_Z_INDEX = 20001;
3
+ /** 全局浮层 z-index 刻度 */
4
+ export const UI_Z_INDEX = {
5
+ tooltip: 10000,
6
+ /** 普通页面下拉;弹窗内下拉须高于 modalContent */
7
+ dropdown: 20150,
8
+ popover: 20170,
9
+ alertDialog: 10150,
10
+ /** 弹窗:高于画布 Ruler、工具栏、Menubar 等 */
11
+ modal: 20000,
12
+ modalContent: 20001,
13
+ /** Toast 通知:始终置于最顶层 */
14
+ toast: 99999,
15
+ } as const;
6
16
 
7
- export const modalOverlayStyle: CSSProperties = { zIndex: MODAL_Z_INDEX };
8
- export const modalContentStyle: CSSProperties = { zIndex: MODAL_CONTENT_Z_INDEX };
17
+ /** @deprecated 使用 UI_Z_INDEX.modal */
18
+ export const MODAL_Z_INDEX = UI_Z_INDEX.modal;
19
+
20
+ /** @deprecated 使用 UI_Z_INDEX.modalContent */
21
+ export const MODAL_CONTENT_Z_INDEX = UI_Z_INDEX.modalContent;
22
+
23
+ export const modalOverlayStyle: CSSProperties = { zIndex: UI_Z_INDEX.modal };
24
+ export const modalContentStyle: CSSProperties = { zIndex: UI_Z_INDEX.modalContent };
25
+ export const dropdownStyle: CSSProperties = { zIndex: UI_Z_INDEX.dropdown };
@@ -9,6 +9,12 @@ import {
9
9
  modalOverlayStyle,
10
10
  } from "../lib/overlay-z-index"
11
11
  import { buttonVariants } from "./button"
12
+ import {
13
+ FloatingWindowChrome,
14
+ FloatingWindowProvider,
15
+ useFloatingWindow,
16
+ useFloatingWindowDrag,
17
+ } from "./floating-window"
12
18
 
13
19
  const AlertDialog = AlertDialogPrimitive.Root
14
20
 
@@ -32,39 +38,107 @@ const AlertDialogOverlay = React.forwardRef<
32
38
  ))
33
39
  AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
34
40
 
41
+ type AlertDialogContentProps = React.ComponentPropsWithoutRef<
42
+ typeof AlertDialogPrimitive.Content
43
+ > & {
44
+ overlayClassName?: string
45
+ movable?: boolean
46
+ resizable?: boolean
47
+ minWidth?: number
48
+ minHeight?: number
49
+ }
50
+
35
51
  const AlertDialogContent = React.forwardRef<
36
52
  React.ElementRef<typeof AlertDialogPrimitive.Content>,
37
- React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content> & {
38
- overlayClassName?: string
53
+ AlertDialogContentProps
54
+ >(
55
+ (
56
+ {
57
+ className,
58
+ style,
59
+ overlayClassName,
60
+ children,
61
+ movable = true,
62
+ resizable = true,
63
+ minWidth,
64
+ minHeight,
65
+ ...props
66
+ },
67
+ ref
68
+ ) => {
69
+ const floatingEnabled = movable || resizable
70
+ const {
71
+ setNodeRef,
72
+ floatingStyle,
73
+ onDragPointerDown,
74
+ onResizePointerDown,
75
+ dragContext,
76
+ } = useFloatingWindow({
77
+ enabled: floatingEnabled,
78
+ minWidth,
79
+ minHeight,
80
+ })
81
+
82
+ const setRefs = React.useCallback(
83
+ (node: HTMLDivElement | null) => {
84
+ setNodeRef(node)
85
+ if (typeof ref === "function") ref(node)
86
+ else if (ref) ref.current = node
87
+ },
88
+ [ref, setNodeRef]
89
+ )
90
+
91
+ return (
92
+ <AlertDialogPortal>
93
+ <AlertDialogOverlay className={overlayClassName} />
94
+ <AlertDialogPrimitive.Content
95
+ ref={setRefs}
96
+ style={{ ...modalContentStyle, ...floatingStyle, ...style }}
97
+ className={cn(
98
+ "fixed left-[50%] top-[50%] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 pt-8 shadow-lg transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
99
+ floatingStyle &&
100
+ "max-h-none translate-x-0 translate-y-0 animate-none overflow-auto transition-none data-[state=open]:zoom-in-100 data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-0",
101
+ className
102
+ )}
103
+ {...props}
104
+ >
105
+ <FloatingWindowProvider value={dragContext}>
106
+ <FloatingWindowChrome
107
+ enabled={floatingEnabled}
108
+ onDragPointerDown={movable ? onDragPointerDown : () => undefined}
109
+ onResizePointerDown={
110
+ resizable ? onResizePointerDown : () => () => undefined
111
+ }
112
+ />
113
+ {children}
114
+ </FloatingWindowProvider>
115
+ </AlertDialogPrimitive.Content>
116
+ </AlertDialogPortal>
117
+ )
39
118
  }
40
- >(({ className, style, overlayClassName, ...props }, ref) => (
41
- <AlertDialogPortal>
42
- <AlertDialogOverlay className={overlayClassName} />
43
- <AlertDialogPrimitive.Content
44
- ref={ref}
45
- style={{ ...modalContentStyle, ...style }}
46
- className={cn(
47
- "fixed left-[50%] top-[50%] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
48
- className
49
- )}
50
- {...props}
51
- />
52
- </AlertDialogPortal>
53
- ))
119
+ )
54
120
  AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
55
121
 
56
122
  const AlertDialogHeader = ({
57
123
  className,
124
+ onPointerDown,
58
125
  ...props
59
- }: React.HTMLAttributes<HTMLDivElement>) => (
60
- <div
61
- className={cn(
62
- "flex flex-col space-y-2 text-center sm:text-left",
63
- className
64
- )}
65
- {...props}
66
- />
67
- )
126
+ }: React.HTMLAttributes<HTMLDivElement>) => {
127
+ const drag = useFloatingWindowDrag()
128
+ return (
129
+ <div
130
+ className={cn(
131
+ "flex cursor-grab flex-col space-y-2 text-center active:cursor-grabbing sm:text-left",
132
+ className
133
+ )}
134
+ onPointerDown={(event) => {
135
+ drag?.onDragPointerDown(event)
136
+ onPointerDown?.(event)
137
+ }}
138
+ {...props}
139
+ />
140
+ )
141
+ }
68
142
  AlertDialogHeader.displayName = "AlertDialogHeader"
69
143
 
70
144
  const AlertDialogFooter = ({
package/src/ui/dialog.tsx CHANGED
@@ -9,6 +9,12 @@ import {
9
9
  modalContentStyle,
10
10
  modalOverlayStyle,
11
11
  } from "../lib/overlay-z-index"
12
+ import {
13
+ FloatingWindowChrome,
14
+ FloatingWindowProvider,
15
+ useFloatingWindow,
16
+ useFloatingWindowDrag,
17
+ } from "./floating-window"
12
18
 
13
19
  const Dialog = DialogPrimitive.Root
14
20
 
@@ -34,45 +40,127 @@ const DialogOverlay = React.forwardRef<
34
40
  ))
35
41
  DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
36
42
 
43
+ type DialogContentProps = React.ComponentPropsWithoutRef<
44
+ typeof DialogPrimitive.Content
45
+ > & {
46
+ overlayClassName?: string
47
+ /** 默认 true;AI 浮窗等场景可关掉,避免整屏黑遮罩 */
48
+ showOverlay?: boolean
49
+ /** 默认 true:显示右上角关闭按钮 */
50
+ showClose?: boolean
51
+ /** 默认 true:支持拖动标题条移动 */
52
+ movable?: boolean
53
+ /** 默认 true:支持边缘/角落缩放 */
54
+ resizable?: boolean
55
+ minWidth?: number
56
+ minHeight?: number
57
+ }
58
+
37
59
  const DialogContent = React.forwardRef<
38
60
  React.ElementRef<typeof DialogPrimitive.Content>,
39
- React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> & {
40
- overlayClassName?: string
61
+ DialogContentProps
62
+ >(
63
+ (
64
+ {
65
+ className,
66
+ style,
67
+ children,
68
+ overlayClassName,
69
+ showOverlay = true,
70
+ showClose = true,
71
+ movable = true,
72
+ resizable = true,
73
+ minWidth,
74
+ minHeight,
75
+ ...props
76
+ },
77
+ ref
78
+ ) => {
79
+ const floatingEnabled = movable || resizable
80
+ const {
81
+ setNodeRef,
82
+ floatingStyle,
83
+ onDragPointerDown,
84
+ onResizePointerDown,
85
+ dragContext,
86
+ } = useFloatingWindow({
87
+ enabled: floatingEnabled,
88
+ minWidth,
89
+ minHeight,
90
+ })
91
+
92
+ const setRefs = React.useCallback(
93
+ (node: HTMLDivElement | null) => {
94
+ setNodeRef(node)
95
+ if (typeof ref === "function") ref(node)
96
+ else if (ref) ref.current = node
97
+ },
98
+ [ref, setNodeRef]
99
+ )
100
+
101
+ return (
102
+ <DialogPortal>
103
+ {showOverlay ? (
104
+ <DialogOverlay className={overlayClassName} />
105
+ ) : null}
106
+ <DialogPrimitive.Content
107
+ ref={setRefs}
108
+ style={{ ...modalContentStyle, ...floatingStyle, ...style }}
109
+ className={cn(
110
+ "fixed left-[50%] top-[50%] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
111
+ floatingStyle &&
112
+ "max-h-none translate-x-0 translate-y-0 animate-none overflow-auto transition-none data-[state=open]:zoom-in-100 data-[state=open]:slide-in-from-left-0 data-[state=open]:slide-in-from-top-0",
113
+ className
114
+ )}
115
+ {...props}
116
+ >
117
+ <FloatingWindowProvider value={dragContext}>
118
+ <FloatingWindowChrome
119
+ enabled={floatingEnabled}
120
+ onDragPointerDown={movable ? onDragPointerDown : () => undefined}
121
+ onResizePointerDown={
122
+ resizable ? onResizePointerDown : () => () => undefined
123
+ }
124
+ />
125
+ {children}
126
+ </FloatingWindowProvider>
127
+ {showClose ? (
128
+ <DialogPrimitive.Close
129
+ data-no-drag
130
+ className="absolute right-3 top-3 z-50 inline-flex h-8 w-8 items-center justify-center rounded-lg text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none"
131
+ >
132
+ <X className="h-4 w-4" />
133
+ <span className="sr-only">Close</span>
134
+ </DialogPrimitive.Close>
135
+ ) : null}
136
+ </DialogPrimitive.Content>
137
+ </DialogPortal>
138
+ )
41
139
  }
42
- >(({ className, style, children, overlayClassName, ...props }, ref) => (
43
- <DialogPortal>
44
- <DialogOverlay className={overlayClassName} />
45
- <DialogPrimitive.Content
46
- ref={ref}
47
- style={{ ...modalContentStyle, ...style }}
48
- className={cn(
49
- "fixed left-[50%] top-[50%] grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
50
- className
51
- )}
52
- {...props}
53
- >
54
- {children}
55
- <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
56
- <X className="h-4 w-4" />
57
- <span className="sr-only">Close</span>
58
- </DialogPrimitive.Close>
59
- </DialogPrimitive.Content>
60
- </DialogPortal>
61
- ))
140
+ )
62
141
  DialogContent.displayName = DialogPrimitive.Content.displayName
63
142
 
64
143
  const DialogHeader = ({
65
144
  className,
145
+ onPointerDown,
66
146
  ...props
67
- }: React.HTMLAttributes<HTMLDivElement>) => (
68
- <div
69
- className={cn(
70
- "flex flex-col space-y-1.5 text-center sm:text-left",
71
- className
72
- )}
73
- {...props}
74
- />
75
- )
147
+ }: React.HTMLAttributes<HTMLDivElement>) => {
148
+ const drag = useFloatingWindowDrag()
149
+ return (
150
+ <div
151
+ className={cn(
152
+ // Reserve space for the absolute close button so titles/actions never sit under it
153
+ "flex cursor-grab flex-col space-y-1.5 pr-10 text-center active:cursor-grabbing sm:text-left",
154
+ className
155
+ )}
156
+ onPointerDown={(event) => {
157
+ drag?.onDragPointerDown(event)
158
+ onPointerDown?.(event)
159
+ }}
160
+ {...props}
161
+ />
162
+ )
163
+ }
76
164
  DialogHeader.displayName = "DialogHeader"
77
165
 
78
166
  const DialogFooter = ({
@@ -0,0 +1,365 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { cn } from "../lib/utils"
5
+
6
+ export type FloatingBounds = {
7
+ x: number
8
+ y: number
9
+ width: number
10
+ height: number
11
+ }
12
+
13
+ type ResizeEdge = "n" | "s" | "e" | "w" | "ne" | "nw" | "se" | "sw"
14
+
15
+ const DEFAULT_MIN_WIDTH = 320
16
+ const DEFAULT_MIN_HEIGHT = 220
17
+
18
+ function clamp(n: number, min: number, max: number) {
19
+ return Math.min(max, Math.max(min, n))
20
+ }
21
+
22
+ function isInteractiveDragTarget(target: EventTarget | null) {
23
+ if (!(target instanceof Element)) return false
24
+ return Boolean(
25
+ target.closest(
26
+ "button,a,input,textarea,select,option,[role='button'],[role='combobox'],[role='menuitem'],[data-no-drag],[data-radix-collection-item]"
27
+ )
28
+ )
29
+ }
30
+
31
+ export type FloatingWindowContextValue = {
32
+ onDragPointerDown: (event: React.PointerEvent) => void
33
+ }
34
+
35
+ const FloatingWindowContext =
36
+ React.createContext<FloatingWindowContextValue | null>(null)
37
+
38
+ export function useFloatingWindowDrag() {
39
+ return React.useContext(FloatingWindowContext)
40
+ }
41
+
42
+ export function FloatingWindowProvider({
43
+ value,
44
+ children,
45
+ }: {
46
+ value: FloatingWindowContextValue
47
+ children: React.ReactNode
48
+ }) {
49
+ return (
50
+ <FloatingWindowContext.Provider value={value}>
51
+ {children}
52
+ </FloatingWindowContext.Provider>
53
+ )
54
+ }
55
+
56
+ export type UseFloatingWindowOptions = {
57
+ enabled?: boolean
58
+ minWidth?: number
59
+ minHeight?: number
60
+ }
61
+
62
+ function applyBoundsToDom(el: HTMLElement | null, bounds: FloatingBounds) {
63
+ if (!el) return
64
+ el.style.setProperty("left", `${bounds.x}px`)
65
+ el.style.setProperty("top", `${bounds.y}px`)
66
+ el.style.setProperty("width", `${bounds.width}px`)
67
+ el.style.setProperty("height", `${bounds.height}px`)
68
+ el.style.setProperty("max-width", "none")
69
+ el.style.setProperty("transform", "none")
70
+ el.style.setProperty("margin", "0")
71
+ }
72
+
73
+ export function useFloatingWindow({
74
+ enabled = true,
75
+ minWidth = DEFAULT_MIN_WIDTH,
76
+ minHeight = DEFAULT_MIN_HEIGHT,
77
+ }: UseFloatingWindowOptions = {}) {
78
+ const nodeRef = React.useRef<HTMLElement | null>(null)
79
+ const liveBoundsRef = React.useRef<FloatingBounds | null>(null)
80
+ const [bounds, setBounds] = React.useState<FloatingBounds | null>(null)
81
+ const dragRef = React.useRef<{
82
+ pointerId: number
83
+ offsetX: number
84
+ offsetY: number
85
+ } | null>(null)
86
+ const resizeRef = React.useRef<{
87
+ pointerId: number
88
+ edge: ResizeEdge
89
+ startX: number
90
+ startY: number
91
+ origin: FloatingBounds
92
+ } | null>(null)
93
+ const rafRef = React.useRef<number | null>(null)
94
+ const pendingBoundsRef = React.useRef<FloatingBounds | null>(null)
95
+
96
+ const measure = React.useCallback((): FloatingBounds | null => {
97
+ const el = nodeRef.current
98
+ if (!el) return null
99
+ const rect = el.getBoundingClientRect()
100
+ return {
101
+ x: rect.left,
102
+ y: rect.top,
103
+ width: rect.width,
104
+ height: rect.height,
105
+ }
106
+ }, [])
107
+
108
+ const paintBounds = React.useCallback((next: FloatingBounds) => {
109
+ liveBoundsRef.current = next
110
+ pendingBoundsRef.current = next
111
+ if (rafRef.current != null) return
112
+ rafRef.current = window.requestAnimationFrame(() => {
113
+ rafRef.current = null
114
+ const pending = pendingBoundsRef.current
115
+ if (!pending) return
116
+ applyBoundsToDom(nodeRef.current, pending)
117
+ })
118
+ }, [])
119
+
120
+ const commitBounds = React.useCallback((next: FloatingBounds) => {
121
+ liveBoundsRef.current = next
122
+ applyBoundsToDom(nodeRef.current, next)
123
+ setBounds(next)
124
+ }, [])
125
+
126
+ const ensureBounds = React.useCallback((): FloatingBounds | null => {
127
+ if (liveBoundsRef.current) return liveBoundsRef.current
128
+ const measured = measure()
129
+ if (!measured) return null
130
+ // Switch from centered transform layout to absolute coords immediately (no React lag).
131
+ commitBounds(measured)
132
+ return measured
133
+ }, [commitBounds, measure])
134
+
135
+ const setNodeRef = React.useCallback((node: HTMLElement | null) => {
136
+ nodeRef.current = node
137
+ if (node && liveBoundsRef.current) {
138
+ applyBoundsToDom(node, liveBoundsRef.current)
139
+ }
140
+ }, [])
141
+
142
+ React.useEffect(() => {
143
+ if (!enabled) return
144
+
145
+ const onPointerMove = (event: PointerEvent) => {
146
+ const drag = dragRef.current
147
+ if (drag && event.pointerId === drag.pointerId) {
148
+ event.preventDefault()
149
+ const prev = liveBoundsRef.current
150
+ if (!prev) return
151
+ const maxX = Math.max(0, window.innerWidth - prev.width)
152
+ const maxY = Math.max(0, window.innerHeight - 48)
153
+ paintBounds({
154
+ ...prev,
155
+ x: clamp(event.clientX - drag.offsetX, 0, maxX),
156
+ y: clamp(event.clientY - drag.offsetY, 0, maxY),
157
+ })
158
+ return
159
+ }
160
+
161
+ const resize = resizeRef.current
162
+ if (!resize || event.pointerId !== resize.pointerId) return
163
+ event.preventDefault()
164
+ const { edge, startX, startY, origin } = resize
165
+ const dx = event.clientX - startX
166
+ const dy = event.clientY - startY
167
+ let { x, y, width, height } = origin
168
+
169
+ if (edge.includes("e")) {
170
+ width = clamp(origin.width + dx, minWidth, window.innerWidth - origin.x)
171
+ }
172
+ if (edge.includes("s")) {
173
+ height = clamp(
174
+ origin.height + dy,
175
+ minHeight,
176
+ window.innerHeight - origin.y
177
+ )
178
+ }
179
+ if (edge.includes("w")) {
180
+ const nextWidth = clamp(
181
+ origin.width - dx,
182
+ minWidth,
183
+ origin.x + origin.width
184
+ )
185
+ x = origin.x + (origin.width - nextWidth)
186
+ width = nextWidth
187
+ }
188
+ if (edge.includes("n")) {
189
+ const nextHeight = clamp(
190
+ origin.height - dy,
191
+ minHeight,
192
+ origin.y + origin.height
193
+ )
194
+ y = origin.y + (origin.height - nextHeight)
195
+ height = nextHeight
196
+ }
197
+
198
+ paintBounds({
199
+ x: clamp(x, 0, Math.max(0, window.innerWidth - width)),
200
+ y: clamp(y, 0, Math.max(0, window.innerHeight - 48)),
201
+ width,
202
+ height,
203
+ })
204
+ }
205
+
206
+ const onPointerUp = (event: PointerEvent) => {
207
+ const wasDragging =
208
+ dragRef.current?.pointerId === event.pointerId ||
209
+ resizeRef.current?.pointerId === event.pointerId
210
+ if (dragRef.current?.pointerId === event.pointerId) dragRef.current = null
211
+ if (resizeRef.current?.pointerId === event.pointerId) {
212
+ resizeRef.current = null
213
+ }
214
+ if (wasDragging && liveBoundsRef.current) {
215
+ // Final sync into React state after gesture ends.
216
+ setBounds({ ...liveBoundsRef.current })
217
+ }
218
+ }
219
+
220
+ window.addEventListener("pointermove", onPointerMove, { passive: false })
221
+ window.addEventListener("pointerup", onPointerUp)
222
+ window.addEventListener("pointercancel", onPointerUp)
223
+ return () => {
224
+ window.removeEventListener("pointermove", onPointerMove)
225
+ window.removeEventListener("pointerup", onPointerUp)
226
+ window.removeEventListener("pointercancel", onPointerUp)
227
+ if (rafRef.current != null) {
228
+ window.cancelAnimationFrame(rafRef.current)
229
+ rafRef.current = null
230
+ }
231
+ }
232
+ }, [enabled, minHeight, minWidth, paintBounds])
233
+
234
+ const onDragPointerDown = React.useCallback(
235
+ (event: React.PointerEvent) => {
236
+ if (!enabled || event.button !== 0) return
237
+ if (isInteractiveDragTarget(event.target)) return
238
+ const current = ensureBounds()
239
+ if (!current) return
240
+ event.preventDefault()
241
+ event.stopPropagation()
242
+ try {
243
+ ;(event.currentTarget as HTMLElement).setPointerCapture?.(event.pointerId)
244
+ } catch {
245
+ // ignore
246
+ }
247
+ dragRef.current = {
248
+ pointerId: event.pointerId,
249
+ offsetX: event.clientX - current.x,
250
+ offsetY: event.clientY - current.y,
251
+ }
252
+ },
253
+ [enabled, ensureBounds]
254
+ )
255
+
256
+ const onResizePointerDown = React.useCallback(
257
+ (edge: ResizeEdge) => (event: React.PointerEvent) => {
258
+ if (!enabled || event.button !== 0) return
259
+ const current = ensureBounds()
260
+ if (!current) return
261
+ event.preventDefault()
262
+ event.stopPropagation()
263
+ try {
264
+ ;(event.currentTarget as HTMLElement).setPointerCapture?.(
265
+ event.pointerId
266
+ )
267
+ } catch {
268
+ // ignore
269
+ }
270
+ resizeRef.current = {
271
+ pointerId: event.pointerId,
272
+ edge,
273
+ startX: event.clientX,
274
+ startY: event.clientY,
275
+ origin: { ...current },
276
+ }
277
+ },
278
+ [enabled, ensureBounds]
279
+ )
280
+
281
+ const floatingStyle: React.CSSProperties | undefined = bounds
282
+ ? {
283
+ left: bounds.x,
284
+ top: bounds.y,
285
+ width: bounds.width,
286
+ height: bounds.height,
287
+ maxWidth: "none",
288
+ transform: "none",
289
+ margin: 0,
290
+ }
291
+ : undefined
292
+
293
+ const resetBounds = React.useCallback(() => {
294
+ liveBoundsRef.current = null
295
+ setBounds(null)
296
+ }, [])
297
+
298
+ const dragContext = React.useMemo(
299
+ () => ({ onDragPointerDown }),
300
+ [onDragPointerDown]
301
+ )
302
+
303
+ return {
304
+ bounds,
305
+ setNodeRef,
306
+ floatingStyle,
307
+ onDragPointerDown,
308
+ onResizePointerDown,
309
+ resetBounds,
310
+ dragContext,
311
+ }
312
+ }
313
+
314
+ const RESIZE_HANDLES: Array<{
315
+ edge: ResizeEdge
316
+ className: string
317
+ }> = [
318
+ { edge: "n", className: "left-2 right-2 top-0 h-1.5 cursor-n-resize" },
319
+ { edge: "s", className: "left-2 right-2 bottom-0 h-1.5 cursor-s-resize" },
320
+ { edge: "e", className: "top-2 bottom-2 right-0 w-1.5 cursor-e-resize" },
321
+ { edge: "w", className: "top-2 bottom-2 left-0 w-1.5 cursor-w-resize" },
322
+ { edge: "ne", className: "right-0 top-0 h-3 w-3 cursor-ne-resize" },
323
+ { edge: "nw", className: "left-0 top-0 h-3 w-3 cursor-nw-resize" },
324
+ { edge: "se", className: "right-0 bottom-0 h-3 w-3 cursor-se-resize" },
325
+ { edge: "sw", className: "left-0 bottom-0 h-3 w-3 cursor-sw-resize" },
326
+ ]
327
+
328
+ export function FloatingWindowChrome({
329
+ enabled = true,
330
+ onDragPointerDown,
331
+ onResizePointerDown,
332
+ className,
333
+ }: {
334
+ enabled?: boolean
335
+ onDragPointerDown: (event: React.PointerEvent) => void
336
+ onResizePointerDown: (edge: ResizeEdge) => (event: React.PointerEvent) => void
337
+ className?: string
338
+ }) {
339
+ if (!enabled) return null
340
+
341
+ return (
342
+ <>
343
+ <div
344
+ data-floating-drag-handle
345
+ onPointerDown={onDragPointerDown}
346
+ className={cn(
347
+ "absolute inset-x-10 top-0 z-20 flex h-9 cursor-grab touch-none items-center justify-center active:cursor-grabbing",
348
+ className
349
+ )}
350
+ aria-hidden
351
+ >
352
+ <div className="h-1 w-12 rounded-full bg-muted-foreground/40" />
353
+ </div>
354
+ {RESIZE_HANDLES.map((handle) => (
355
+ <div
356
+ key={handle.edge}
357
+ data-floating-resize={handle.edge}
358
+ onPointerDown={onResizePointerDown(handle.edge)}
359
+ className={cn("absolute z-30 touch-none", handle.className)}
360
+ aria-hidden
361
+ />
362
+ ))}
363
+ </>
364
+ )
365
+ }
package/src/ui/index.ts CHANGED
@@ -18,6 +18,7 @@ export * from "./dialog";
18
18
  export * from "./drawer";
19
19
  export * from "./dropdown-menu";
20
20
  export * from "./empty";
21
+ export * from "./floating-window";
21
22
  export * from "./form";
22
23
  export * from "./hover-card";
23
24
  export * from "./input";
package/src/ui/select.tsx CHANGED
@@ -5,11 +5,10 @@ import * as SelectPrimitive from "@radix-ui/react-select"
5
5
  import { Check, ChevronDown, ChevronUp } from "lucide-react"
6
6
 
7
7
  import { cn } from "../lib/utils"
8
+ import { dropdownStyle } from "../lib/overlay-z-index"
8
9
 
9
10
  const Select = SelectPrimitive.Root
10
-
11
11
  const SelectGroup = SelectPrimitive.Group
12
-
13
12
  const SelectValue = SelectPrimitive.Value
14
13
 
15
14
  const SelectTrigger = React.forwardRef<
@@ -70,12 +69,13 @@ SelectScrollDownButton.displayName =
70
69
  const SelectContent = React.forwardRef<
71
70
  React.ElementRef<typeof SelectPrimitive.Content>,
72
71
  React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
73
- >(({ className, children, position = "popper", ...props }, ref) => (
72
+ >(({ className, children, position = "popper", style, ...props }, ref) => (
74
73
  <SelectPrimitive.Portal>
75
74
  <SelectPrimitive.Content
76
75
  ref={ref}
76
+ style={{ ...dropdownStyle, ...style }}
77
77
  className={cn(
78
- "relative z-50 max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
78
+ "relative max-h-[--radix-select-content-available-height] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-select-content-transform-origin]",
79
79
  position === "popper" &&
80
80
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
81
81
  className
package/src/ui/sonner.tsx CHANGED
@@ -10,6 +10,8 @@ import {
10
10
  import { useTheme } from "next-themes"
11
11
  import { Toaster as Sonner } from "sonner"
12
12
 
13
+ import { UI_Z_INDEX } from "../lib/overlay-z-index"
14
+
13
15
  type ToasterProps = React.ComponentProps<typeof Sonner>
14
16
 
15
17
  const Toaster = ({ ...props }: ToasterProps) => {
@@ -19,6 +21,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
19
21
  <Sonner
20
22
  theme={theme as ToasterProps["theme"]}
21
23
  className="toaster group"
24
+ style={{ zIndex: UI_Z_INDEX.toast }}
22
25
  icons={{
23
26
  success: <CircleCheck className="h-4 w-4" />,
24
27
  info: <Info className="h-4 w-4" />,
package/src/ui/toast.tsx CHANGED
@@ -6,6 +6,7 @@ import { cva, type VariantProps } from "class-variance-authority"
6
6
  import { X } from "lucide-react"
7
7
 
8
8
  import { cn } from "../lib/utils"
9
+ import { UI_Z_INDEX } from "../lib/overlay-z-index"
9
10
 
10
11
  const ToastProvider = ToastPrimitives.Provider
11
12
 
@@ -15,8 +16,9 @@ const ToastViewport = React.forwardRef<
15
16
  >(({ className, ...props }, ref) => (
16
17
  <ToastPrimitives.Viewport
17
18
  ref={ref}
19
+ style={{ zIndex: UI_Z_INDEX.toast }}
18
20
  className={cn(
19
- "fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
21
+ "pointer-events-none fixed top-0 right-0 z-toast flex max-h-screen w-full flex-col gap-2 p-4 md:max-w-[420px]",
20
22
  className
21
23
  )}
22
24
  {...props}
@@ -25,7 +27,7 @@ const ToastViewport = React.forwardRef<
25
27
  ToastViewport.displayName = ToastPrimitives.Viewport.displayName
26
28
 
27
29
  const toastVariants = cva(
28
- "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
30
+ "group pointer-events-auto data-[state=closed]:pointer-events-none relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full",
29
31
  {
30
32
  variants: {
31
33
  variant: {
@@ -1,5 +1,7 @@
1
1
  "use client"
2
2
 
3
+ import { createPortal } from "react-dom"
4
+
3
5
  import { useToast } from "../hooks/use-toast"
4
6
  import {
5
7
  Toast,
@@ -13,8 +15,10 @@ import {
13
15
  export function Toaster() {
14
16
  const { toasts } = useToast()
15
17
 
16
- return (
17
- <ToastProvider>
18
+ if (typeof document === "undefined") return null
19
+
20
+ return createPortal(
21
+ <ToastProvider swipeDirection="right">
18
22
  {toasts.map(function ({ id, title, description, action, ...props }) {
19
23
  return (
20
24
  <Toast key={id} {...props}>
@@ -30,6 +34,7 @@ export function Toaster() {
30
34
  )
31
35
  })}
32
36
  <ToastViewport />
33
- </ToastProvider>
37
+ </ToastProvider>,
38
+ document.body
34
39
  )
35
40
  }