@arronqzy/ui 1.0.2 → 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/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
  }
@@ -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
  }
@@ -2497,10 +2616,6 @@ body {
2497
2616
  color: hsl(var(--accent-foreground));
2498
2617
  }
2499
2618
 
2500
- .data-\[state\=open\]\:text-muted-foreground[data-state="open"] {
2501
- color: hsl(var(--muted-foreground));
2502
- }
2503
-
2504
2619
  .data-\[disabled\=true\]\:opacity-50[data-disabled="true"] {
2505
2620
  opacity: 0.5;
2506
2621
  }
@@ -2629,6 +2744,10 @@ body {
2629
2744
  --tw-exit-scale: .95;
2630
2745
  }
2631
2746
 
2747
+ .data-\[state\=open\]\:zoom-in-100[data-state="open"] {
2748
+ --tw-enter-scale: 1;
2749
+ }
2750
+
2632
2751
  .data-\[state\=open\]\:zoom-in-90[data-state="open"] {
2633
2752
  --tw-enter-scale: .9;
2634
2753
  }
@@ -2705,6 +2824,10 @@ body {
2705
2824
  --tw-enter-translate-x: -100%;
2706
2825
  }
2707
2826
 
2827
+ .data-\[state\=open\]\:slide-in-from-left-0[data-state="open"] {
2828
+ --tw-enter-translate-x: -0px;
2829
+ }
2830
+
2708
2831
  .data-\[state\=open\]\:slide-in-from-left-1\/2[data-state="open"] {
2709
2832
  --tw-enter-translate-x: -50%;
2710
2833
  }
@@ -2717,6 +2840,10 @@ body {
2717
2840
  --tw-enter-translate-y: -100%;
2718
2841
  }
2719
2842
 
2843
+ .data-\[state\=open\]\:slide-in-from-top-0[data-state="open"] {
2844
+ --tw-enter-translate-y: -0px;
2845
+ }
2846
+
2720
2847
  .data-\[state\=open\]\:slide-in-from-top-\[48\%\][data-state="open"] {
2721
2848
  --tw-enter-translate-y: -48%;
2722
2849
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arronqzy/ui",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "exports": {
5
5
  ".": "./index.ts",
6
6
  "./counter": "./components/counter.ts",
@@ -63,8 +63,8 @@
63
63
  "eslint": "^8.57.0",
64
64
  "typescript": "^5.5.4",
65
65
  "@arronqzy/eslint-config": "0.0.0",
66
- "@arronqzy/tailwind": "1.0.2",
67
- "@arronqzy/typescript-config": "1.0.0"
66
+ "@arronqzy/typescript-config": "1.0.0",
67
+ "@arronqzy/tailwind": "1.0.2"
68
68
  },
69
69
  "scripts": {
70
70
  "lint": "eslint \"**/*.{ts,tsx}\"",
@@ -3,8 +3,9 @@ import type { CSSProperties } from "react";
3
3
  /** 全局浮层 z-index 刻度 */
4
4
  export const UI_Z_INDEX = {
5
5
  tooltip: 10000,
6
- dropdown: 10100,
7
- popover: 10120,
6
+ /** 普通页面下拉;弹窗内下拉须高于 modalContent */
7
+ dropdown: 20150,
8
+ popover: 20170,
8
9
  alertDialog: 10150,
9
10
  /** 弹窗:高于画布 Ruler、工具栏、Menubar 等 */
10
11
  modal: 20000,
@@ -21,3 +22,4 @@ export const MODAL_CONTENT_Z_INDEX = UI_Z_INDEX.modalContent;
21
22
 
22
23
  export const modalOverlayStyle: CSSProperties = { zIndex: UI_Z_INDEX.modal };
23
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