@syscore/ui-library 1.17.0 → 1.19.0

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.
@@ -1,13 +1,6 @@
1
- import React from "react";
2
1
  import { cn } from "../../lib/utils";
3
2
 
4
- interface UtilityCompareProps {
5
- className?: string;
6
- }
7
-
8
- export const UtilityCompare: React.FC<UtilityCompareProps> = ({
9
- className,
10
- }) => {
3
+ export function UtilityCompare({ className }: { className?: string }) {
11
4
  return (
12
5
  <div
13
6
  className={cn(
@@ -26,21 +19,21 @@ export const UtilityCompare: React.FC<UtilityCompareProps> = ({
26
19
  <path
27
20
  d="M10.6002 5.79905C10.6002 8.44949 8.45114 10.5981 5.80011 10.5981C3.14908 10.5981 1 8.44949 1 5.79905C1 3.14861 3.14908 1 5.80011 1C8.45114 1 10.6002 3.14861 10.6002 5.79905Z"
28
21
  fill="#E0FBF5"
29
- stroke="#282A31"
22
+ stroke="currentColor"
30
23
  stroke-width="2"
31
24
  stroke-linecap="round"
32
25
  stroke-linejoin="round"
33
26
  />
34
27
  <path
35
28
  d="M5.80078 16.998L5.80078 24.9965C5.80078 25.845 6.13793 26.6588 6.73806 27.2587C7.33819 27.8587 8.15214 28.1958 9.00086 28.1958L20.2011 28.1958"
36
- stroke="#282A31"
29
+ stroke="currentColor"
37
30
  stroke-width="2"
38
31
  stroke-linecap="round"
39
32
  stroke-linejoin="round"
40
33
  />
41
34
  <path
42
35
  d="M10.6002 21.7971L5.80011 16.998L1 21.7971"
43
- stroke="#282A31"
36
+ stroke="currentColor"
44
37
  stroke-width="2"
45
38
  stroke-linecap="round"
46
39
  stroke-linejoin="round"
@@ -48,21 +41,21 @@ export const UtilityCompare: React.FC<UtilityCompareProps> = ({
48
41
  <path
49
42
  d="M29.8001 28.2014C29.8001 30.8518 27.651 33.0004 24.9999 33.0004C22.3489 33.0004 20.1998 30.8518 20.1998 28.2014C20.1998 25.551 22.3489 23.4023 24.9999 23.4023C27.651 23.4023 29.8001 25.5509 29.8001 28.2014Z"
50
43
  fill="#EFF5FB"
51
- stroke="#282A31"
44
+ stroke="currentColor"
52
45
  stroke-width="2"
53
46
  stroke-linecap="round"
54
47
  stroke-linejoin="round"
55
48
  />
56
49
  <path
57
50
  d="M25.0002 16.9946V8.99624C25.0002 8.14771 24.663 7.33394 24.0629 6.73395C23.4628 6.13395 22.6488 5.79687 21.8001 5.79687L10.5999 5.79688"
58
- stroke="#282A31"
51
+ stroke="currentColor"
59
52
  stroke-width="2"
60
53
  stroke-linecap="round"
61
54
  stroke-linejoin="round"
62
55
  />
63
56
  <path
64
57
  d="M20.1998 12.2012L24.9999 17.0002L29.8001 12.2012"
65
- stroke="#282A31"
58
+ stroke="currentColor"
66
59
  stroke-width="2"
67
60
  stroke-linecap="round"
68
61
  stroke-linejoin="round"
@@ -70,4 +63,4 @@ export const UtilityCompare: React.FC<UtilityCompareProps> = ({
70
63
  </svg>
71
64
  </div>
72
65
  );
73
- };
66
+ }
@@ -14,7 +14,7 @@ export function UtilityFeedback({ className }: { className?: string }) {
14
14
  height="28"
15
15
  viewBox="0 0 30 28"
16
16
  fill="none"
17
- className={cn("text-inherit")}
17
+ className={cn("text-inherit w-full h-full")}
18
18
  >
19
19
  <path
20
20
  d="M29 20.162C29 20.888 28.705 21.5843 28.1799 22.0977C27.6548 22.611 26.9426 22.8995 26.2 22.8995H7.7592C7.01665 22.8996 6.30458 23.1881 5.7796 23.7015L2.6968 26.7154C2.55779 26.8513 2.38068 26.9438 2.18788 26.9813C1.99508 27.0188 1.79524 26.9996 1.61363 26.926C1.43202 26.8525 1.27678 26.728 1.16756 26.5682C1.05834 26.4084 1.00003 26.2205 1 26.0283V3.73743C1 3.01142 1.295 2.31514 1.8201 1.80178C2.3452 1.28841 3.05739 1 3.8 1H26.2C26.9426 1 27.6548 1.28841 28.1799 1.80178C28.705 2.31514 29 3.01142 29 3.73743V20.162Z"
@@ -14,7 +14,7 @@ export function UtilityRevisionsHide({ className }: { className?: string }) {
14
14
  height="15"
15
15
  viewBox="0 0 17 15"
16
16
  fill="none"
17
- className={cn("text-inherit")}
17
+ className={cn("text-inherit w-full h-full")}
18
18
  >
19
19
  <path
20
20
  d="M5.48682 6.27734L0.75 11.0142V13.3826H7.85523L10.2236 11.0142"
@@ -14,7 +14,7 @@ export function UtilityRevisionsShow({ className }: { className?: string }) {
14
14
  height="26"
15
15
  viewBox="0 0 30 26"
16
16
  fill="none"
17
- className={cn("text-inherit")}
17
+ className={cn("text-inherit w-full h-full")}
18
18
  >
19
19
  <path
20
20
  d="M9.84194 11.5L1 20.5006V25.0009H14.2629L18.6839 20.5006"
@@ -22,17 +22,17 @@ export function UtilityRevisionsShow({ className }: { className?: string }) {
22
22
  />
23
23
  <path
24
24
  d="M9.84194 11.5L1 20.5006V25.0009H14.2629L18.6839 20.5006"
25
- stroke="#282A31"
26
- stroke-width="2"
27
- stroke-linecap="round"
28
- stroke-linejoin="round"
25
+ stroke="currentColor"
26
+ strokeWidth="2"
27
+ strokeLinecap="round"
28
+ strokeLinejoin="round"
29
29
  />
30
30
  <path
31
31
  d="M28.9999 13.0008L22.2211 19.9013C21.6702 20.451 20.9295 20.7589 20.158 20.7589C19.3865 20.7589 18.6458 20.451 18.0949 19.9013L10.4319 12.1008C9.89184 11.5399 9.58936 10.7859 9.58936 10.0006C9.58936 9.21531 9.89184 8.4613 10.4319 7.90048L17.2107 1"
32
- stroke="#282A31"
33
- stroke-width="2"
34
- stroke-linecap="round"
35
- stroke-linejoin="round"
32
+ stroke="currentColor"
33
+ strokeWidth="2"
34
+ strokeLinecap="round"
35
+ strokeLinejoin="round"
36
36
  />
37
37
  </svg>
38
38
  </div>
@@ -1,34 +1,43 @@
1
+ import { cn } from "@/lib/utils";
2
+
1
3
  export function UtilityText({ className }: { className?: string }) {
2
4
  return (
3
- <svg
4
- className={className}
5
- xmlns="http://www.w3.org/2000/svg"
6
- width="16"
7
- height="14"
8
- viewBox="0 0 16 14"
9
- fill="none"
5
+ <div
6
+ className={cn(
7
+ "size-4 flex items-center justify-center text-gray-500",
8
+ className,
9
+ )}
10
10
  >
11
- <path
12
- d="M8.75 12.75H0.75"
13
- stroke="currentColor"
14
- strokeWidth="1.5"
15
- strokeLinecap="round"
16
- stroke-linejoin="round"
17
- />
18
- <path
19
- d="M10.75 0.75H0.75"
20
- stroke="currentColor"
21
- strokeWidth="1.5"
22
- strokeLinecap="round"
23
- strokeLinejoin="round"
24
- />
25
- <path
26
- d="M14.75 6.75H0.75"
27
- stroke="currentColor"
28
- strokeWidth="1.5"
29
- strokeLinecap="round"
30
- strokeLinejoin="round"
31
- />
32
- </svg>
11
+ <svg
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ width="16"
14
+ height="14"
15
+ viewBox="0 0 16 14"
16
+ fill="none"
17
+ className={cn("text-inherit w-full h-full")}
18
+ >
19
+ <path
20
+ d="M8.75 12.75H0.75"
21
+ stroke="currentColor"
22
+ strokeWidth="1.5"
23
+ strokeLinecap="round"
24
+ stroke-linejoin="round"
25
+ />
26
+ <path
27
+ d="M10.75 0.75H0.75"
28
+ stroke="currentColor"
29
+ strokeWidth="1.5"
30
+ strokeLinecap="round"
31
+ strokeLinejoin="round"
32
+ />
33
+ <path
34
+ d="M14.75 6.75H0.75"
35
+ stroke="currentColor"
36
+ strokeWidth="1.5"
37
+ strokeLinecap="round"
38
+ strokeLinejoin="round"
39
+ />
40
+ </svg>
41
+ </div>
33
42
  );
34
43
  }
@@ -4,6 +4,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog";
4
4
  import { cn } from "@/lib/utils";
5
5
  import { UtilityClose } from "../icons/UtilityClose";
6
6
 
7
+
7
8
  function Dialog({
8
9
  ...props
9
10
  }: React.ComponentProps<typeof DialogPrimitive.Root>) {
@@ -46,30 +47,39 @@ function DialogOverlay({
46
47
 
47
48
  function DialogContent({
48
49
  className,
50
+ innerClassName,
51
+ closeClassName,
49
52
  children,
50
53
  showCloseButton = true,
51
54
  ...props
52
55
  }: React.ComponentProps<typeof DialogPrimitive.Content> & {
53
56
  showCloseButton?: boolean;
57
+ innerClassName?: string;
58
+ closeClassName?: string;
54
59
  }) {
55
60
  return (
56
61
  <DialogPortal data-slot="dialog-portal">
57
- <DialogOverlay>
62
+ <DialogOverlay className="overflow-y-auto">
58
63
  <DialogPrimitive.Content
59
64
  data-slot="dialog-content"
60
65
  className={cn("dialog-content", className)}
61
66
  {...props}
62
67
  >
63
- <div className="dialog-content-inner">{children}</div>
64
- {showCloseButton && (
65
- <DialogPrimitive.Close
66
- data-slot="dialog-close"
67
- className="dialog-close"
68
- >
69
- <UtilityClose />
70
- <span className="sr-only">Close</span>
71
- </DialogPrimitive.Close>
72
- )}
68
+ <div className="dialog-content-wrapper">
69
+ <div className={cn("dialog-content-inner", innerClassName)}>
70
+ {children}
71
+ </div>
72
+ {showCloseButton && (
73
+ <div className="dialog-close">
74
+ <DialogPrimitive.Close data-slot="dialog-close">
75
+ <UtilityClose
76
+ className={cn("dialog-close-icon", closeClassName)}
77
+ />
78
+ <span className="sr-only">Close</span>
79
+ </DialogPrimitive.Close>
80
+ </div>
81
+ )}
82
+ </div>
73
83
  </DialogPrimitive.Content>
74
84
  </DialogOverlay>
75
85
  </DialogPortal>
@@ -4,12 +4,14 @@ import {
4
4
  useCallback,
5
5
  useMemo,
6
6
  useEffect,
7
+ useLayoutEffect,
7
8
  ReactNode,
8
9
  createContext,
9
10
  useContext,
10
11
  } from "react";
11
- import { motion, useMotionValue, animate, type PanInfo } from "motion/react";
12
+ import { motion, useMotionValue, animate } from "motion/react";
12
13
  import { cn } from "@/lib/utils";
14
+ import { Button } from "./button";
13
15
 
14
16
  interface MobileNavContextValue {
15
17
  activeKey: string | null;
@@ -99,10 +101,8 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
99
101
  const heightMV = useMotionValue(0);
100
102
  const stopAnimation = useRef<(() => void) | null>(null);
101
103
 
102
- // Track which state the panel is in: "closed" | "initial" | "full"
104
+ // Panel state: "closed" | "initial" | "full"
103
105
  const stateRef = useRef<"closed" | "initial" | "full">("closed");
104
-
105
- // Cache the initial content height so returning from full → initial is consistent
106
106
  const cachedInitialHeight = useRef(0);
107
107
 
108
108
  const getNavBarHeight = () => {
@@ -111,15 +111,12 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
111
111
  };
112
112
 
113
113
  const getAvailableHeight = () => {
114
- const parent = panelRef.current?.parentElement;
115
- const container = parent?.clientHeight ?? window.innerHeight;
116
- return container - getNavBarHeight();
114
+ return window.innerHeight - getNavBarHeight();
117
115
  };
118
116
 
119
117
  const measureContentHeight = () => {
120
118
  const content = contentRef.current;
121
119
  if (!content) return 0;
122
- // Measure the first child to get true content height without triggering reflow
123
120
  const child = content.firstElementChild as HTMLElement | null;
124
121
  const raw = (child?.offsetHeight ?? content.scrollHeight) + 40; // +40 for handle area
125
122
  const max = getAvailableHeight() * 0.7;
@@ -136,78 +133,111 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
136
133
  };
137
134
 
138
135
  // Animate open/close
139
- useEffect(() => {
140
- let cancelled = false;
136
+ useLayoutEffect(() => {
141
137
  if (open) {
142
138
  stateRef.current = "initial";
143
- requestAnimationFrame(() => {
144
- if (cancelled) return;
145
- const h = measureContentHeight();
146
- cachedInitialHeight.current = h;
147
- springTo(h);
148
- });
139
+ const h = measureContentHeight();
140
+ cachedInitialHeight.current = h;
141
+ springTo(h);
149
142
  } else {
150
143
  stateRef.current = "closed";
151
144
  cachedInitialHeight.current = 0;
152
145
  springTo(0);
153
146
  }
154
- return () => {
155
- cancelled = true;
156
- };
157
147
  }, [open]);
158
148
 
159
- // Re-measure when content changes (e.g. switching tabs)
160
- useEffect(() => {
149
+ // Re-measure when switching tabs while panel is already open
150
+ useLayoutEffect(() => {
161
151
  if (!open) return;
162
- let cancelled = false;
163
- // Double rAF ensures new children have rendered before measuring
164
- requestAnimationFrame(() => {
165
- requestAnimationFrame(() => {
166
- if (cancelled) return;
167
- const h = measureContentHeight();
168
- cachedInitialHeight.current = h;
169
- if (stateRef.current === "initial" || stateRef.current === "closed") {
170
- stateRef.current = "initial";
171
- springTo(h);
172
- }
173
- });
174
- });
175
- return () => {
176
- cancelled = true;
177
- };
152
+ const h = measureContentHeight();
153
+ cachedInitialHeight.current = h;
154
+ if (stateRef.current !== "full") {
155
+ stateRef.current = "initial";
156
+ springTo(h);
157
+ }
178
158
  }, [activeKey]);
179
159
 
180
- // Track drag direction via ref for reliable gesture detection
181
- const dragDirection = useRef<"up" | "down" | null>(null);
182
-
183
- const handlePanStart = () => {
184
- dragDirection.current = null;
160
+ // Close on Escape key
161
+ useEffect(() => {
162
+ if (!open) return;
163
+ const handleKeyDown = (e: KeyboardEvent) => {
164
+ if (e.key === "Escape") close();
165
+ };
166
+ document.addEventListener("keydown", handleKeyDown);
167
+ return () => document.removeEventListener("keydown", handleKeyDown);
168
+ }, [open, close]);
169
+
170
+ // --- Pointer-based drag (bypasses motion's gesture detection) ---
171
+ const isDragging = useRef(false);
172
+ const lastY = useRef(0);
173
+ const dragStartY = useRef(0);
174
+ const dragStartTime = useRef(0);
175
+ const dragMaxHeight = useRef(0);
176
+
177
+ const handlePointerDown = (e: React.PointerEvent) => {
178
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
179
+ stopAnimation.current?.();
180
+ isDragging.current = true;
181
+ lastY.current = e.clientY;
182
+ dragStartY.current = e.clientY;
183
+ dragStartTime.current = Date.now();
184
+ dragMaxHeight.current = getAvailableHeight();
185
185
  };
186
186
 
187
- const handlePan = (_: unknown, info: PanInfo) => {
188
- // Continuously track the latest direction during drag
189
- dragDirection.current = info.offset.y < 0 ? "up" : "down";
187
+ const handlePointerMove = (e: React.PointerEvent) => {
188
+ if (!isDragging.current) return;
189
+ const dy = e.clientY - lastY.current;
190
+ lastY.current = e.clientY;
191
+ const current = heightMV.get();
192
+ const next = Math.max(0, current - dy);
193
+ heightMV.set(Math.min(next, dragMaxHeight.current));
190
194
  };
191
195
 
192
- const handlePanEnd = () => {
193
- const dir = dragDirection.current;
194
- if (!dir) return;
196
+ const handlePointerUp = (e: React.PointerEvent) => {
197
+ if (!isDragging.current) return;
198
+ isDragging.current = false;
195
199
 
196
- if (dir === "up") {
197
- if (stateRef.current === "initial") {
198
- stateRef.current = "full";
199
- springTo(getAvailableHeight());
200
- }
201
- } else {
202
- // dir === "down"
200
+ const currentHeight = heightMV.get();
201
+ const initial = cachedInitialHeight.current;
202
+ const full = dragMaxHeight.current;
203
+
204
+ // Calculate velocity (px/s) — positive = downward
205
+ const elapsed = Date.now() - dragStartTime.current;
206
+ const totalDy = e.clientY - dragStartY.current;
207
+ const velocity = elapsed > 0 ? (totalDy / elapsed) * 1000 : 0;
208
+
209
+ const VELOCITY_THRESHOLD = 500;
210
+
211
+ if (velocity > VELOCITY_THRESHOLD) {
212
+ // Fast downward flick
203
213
  if (stateRef.current === "full") {
204
214
  stateRef.current = "initial";
205
- springTo(cachedInitialHeight.current);
206
- } else if (stateRef.current === "initial") {
215
+ springTo(initial);
216
+ } else {
207
217
  stateRef.current = "closed";
208
218
  springTo(0);
209
219
  close();
210
220
  }
221
+ } else if (velocity < -VELOCITY_THRESHOLD) {
222
+ // Fast upward flick
223
+ stateRef.current = "full";
224
+ springTo(full);
225
+ } else {
226
+ // Slow drag — snap to nearest
227
+ const midInitial = initial / 2;
228
+ const midFull = (initial + full) / 2;
229
+
230
+ if (currentHeight < midInitial) {
231
+ stateRef.current = "closed";
232
+ springTo(0);
233
+ close();
234
+ } else if (currentHeight < midFull) {
235
+ stateRef.current = "initial";
236
+ springTo(initial);
237
+ } else {
238
+ stateRef.current = "full";
239
+ springTo(full);
240
+ }
211
241
  }
212
242
  };
213
243
 
@@ -222,14 +252,15 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
222
252
  data-closed={!open || undefined}
223
253
  style={{ height: heightMV }}
224
254
  >
225
- <motion.div
255
+ <div
226
256
  className="mobile-nav-handle"
227
- onPanStart={handlePanStart}
228
- onPan={handlePan}
229
- onPanEnd={handlePanEnd}
257
+ onPointerDown={handlePointerDown}
258
+ onPointerMove={handlePointerMove}
259
+ onPointerUp={handlePointerUp}
260
+ onPointerCancel={handlePointerUp}
230
261
  >
231
262
  <div className="mobile-nav-handle-bar" />
232
- </motion.div>
263
+ </div>
233
264
 
234
265
  <div ref={contentRef} className="mobile-nav-content">
235
266
  {activeKey ? children(activeKey) : null}
@@ -265,15 +296,19 @@ const MobileNavTrigger = ({
265
296
  };
266
297
 
267
298
  return (
268
- <button
299
+ <Button
300
+ variant="clear"
301
+ size="icon"
302
+ type="button"
269
303
  onClick={handleClick}
270
304
  disabled={disabled}
271
305
  data-active={activeKey === value || undefined}
306
+ aria-expanded={activeKey === value && open}
272
307
  className={cn("mobile-nav-trigger group", className)}
273
308
  aria-label={label}
274
309
  >
275
310
  {children}
276
- </button>
311
+ </Button>
277
312
  );
278
313
  };
279
314