@syscore/ui-library 1.15.4 → 1.16.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.
@@ -12,58 +12,60 @@ export const UtilityCompare: React.FC<UtilityCompareProps> = ({
12
12
  <div
13
13
  className={cn(
14
14
  "size-4 flex items-center justify-center text-gray-500",
15
- className
15
+ className,
16
16
  )}
17
17
  >
18
18
  <svg
19
19
  xmlns="http://www.w3.org/2000/svg"
20
- width="15"
21
- height="16"
22
- viewBox="0 0 15 16"
20
+ width="31"
21
+ height="34"
22
+ viewBox="0 0 31 34"
23
23
  fill="none"
24
- className={cn("size-[14px] text-inherit")}
24
+ className={cn("text-inherit w-full h-full")}
25
25
  >
26
26
  <path
27
- d="M4.95001 2.85001C4.95001 4.00981 4.0098 4.95001 2.85 4.95001C1.6902 4.95001 0.75 4.00981 0.75 2.85001C0.75 1.6902 1.6902 0.75 2.85 0.75C4.0098 0.75 4.95001 1.6902 4.95001 2.85001Z"
28
- stroke="currentColor"
29
- strokeWidth="1.5"
30
- strokeLinecap="round"
31
- strokeLinejoin="round"
27
+ 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
+ fill="#E0FBF5"
29
+ stroke="#282A31"
30
+ stroke-width="2"
31
+ stroke-linecap="round"
32
+ stroke-linejoin="round"
32
33
  />
33
34
  <path
34
- d="M2.84961 7.75002L2.84961 11.25C2.84961 11.6213 2.99711 11.9774 3.25966 12.24C3.52221 12.5025 3.87831 12.65 4.24961 12.65L9.14962 12.65"
35
- stroke="currentColor"
36
- strokeWidth="1.5"
37
- strokeLinecap="round"
38
- strokeLinejoin="round"
35
+ 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"
37
+ stroke-width="2"
38
+ stroke-linecap="round"
39
+ stroke-linejoin="round"
39
40
  />
40
41
  <path
41
- d="M4.95001 9.85002L2.85 7.75002L0.75 9.85002"
42
- stroke="currentColor"
43
- strokeWidth="1.5"
44
- strokeLinecap="round"
45
- strokeLinejoin="round"
42
+ d="M10.6002 21.7971L5.80011 16.998L1 21.7971"
43
+ stroke="#282A31"
44
+ stroke-width="2"
45
+ stroke-linecap="round"
46
+ stroke-linejoin="round"
46
47
  />
47
48
  <path
48
- d="M13.3504 12.65C13.3504 13.8098 12.4102 14.75 11.2504 14.75C10.0906 14.75 9.15039 13.8098 9.15039 12.65C9.15039 11.4902 10.0906 10.55 11.2504 10.55C12.4102 10.55 13.3504 11.4902 13.3504 12.65Z"
49
- stroke="currentColor"
50
- strokeWidth="1.5"
51
- strokeLinecap="round"
52
- strokeLinejoin="round"
49
+ 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
+ fill="#EFF5FB"
51
+ stroke="#282A31"
52
+ stroke-width="2"
53
+ stroke-linecap="round"
54
+ stroke-linejoin="round"
53
55
  />
54
56
  <path
55
- d="M11.2492 7.74993L11.2492 4.24992C11.2492 3.87861 11.1017 3.52252 10.8392 3.25997C10.5766 2.99741 10.2205 2.84991 9.84923 2.84991L4.94922 2.84991"
56
- stroke="currentColor"
57
- strokeWidth="1.5"
58
- strokeLinecap="round"
59
- strokeLinejoin="round"
57
+ 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"
59
+ stroke-width="2"
60
+ stroke-linecap="round"
61
+ stroke-linejoin="round"
60
62
  />
61
63
  <path
62
- d="M9.15039 5.6501L11.2504 7.75011L13.3504 5.6501"
63
- stroke="currentColor"
64
- strokeWidth="1.5"
65
- strokeLinecap="round"
66
- strokeLinejoin="round"
64
+ d="M20.1998 12.2012L24.9999 17.0002L29.8001 12.2012"
65
+ stroke="#282A31"
66
+ stroke-width="2"
67
+ stroke-linecap="round"
68
+ stroke-linejoin="round"
67
69
  />
68
70
  </svg>
69
71
  </div>
@@ -0,0 +1,30 @@
1
+ import { cn } from "../../lib/utils";
2
+
3
+ export function UtilityFeedback({ className }: { className?: string }) {
4
+ return (
5
+ <div
6
+ className={cn(
7
+ "size-4 flex items-center justify-center text-gray-500",
8
+ className,
9
+ )}
10
+ >
11
+ <svg
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ width="30"
14
+ height="28"
15
+ viewBox="0 0 30 28"
16
+ fill="none"
17
+ className={cn("text-inherit")}
18
+ >
19
+ <path
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"
21
+ fill="#D6F4FB"
22
+ stroke="#71747D"
23
+ stroke-width="2"
24
+ stroke-linecap="round"
25
+ stroke-linejoin="round"
26
+ />
27
+ </svg>
28
+ </div>
29
+ );
30
+ }
@@ -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("size-[14px] text-inherit")}
17
+ className={cn("text-inherit")}
18
18
  >
19
19
  <path
20
20
  d="M5.48682 6.27734L0.75 11.0142V13.3826H7.85523L10.2236 11.0142"
@@ -0,0 +1,40 @@
1
+ import { cn } from "../../lib/utils";
2
+
3
+ export function UtilityRevisionsShow({ className }: { className?: string }) {
4
+ return (
5
+ <div
6
+ className={cn(
7
+ "size-4 flex items-center justify-center text-gray-500",
8
+ className,
9
+ )}
10
+ >
11
+ <svg
12
+ xmlns="http://www.w3.org/2000/svg"
13
+ width="30"
14
+ height="26"
15
+ viewBox="0 0 30 26"
16
+ fill="none"
17
+ className={cn("text-inherit")}
18
+ >
19
+ <path
20
+ d="M9.84194 11.5L1 20.5006V25.0009H14.2629L18.6839 20.5006"
21
+ fill="#F6EFD0"
22
+ />
23
+ <path
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"
29
+ />
30
+ <path
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"
36
+ />
37
+ </svg>
38
+ </div>
39
+ );
40
+ }
@@ -0,0 +1,278 @@
1
+ import {
2
+ useRef,
3
+ useState,
4
+ useCallback,
5
+ useMemo,
6
+ useEffect,
7
+ ReactNode,
8
+ createContext,
9
+ useContext,
10
+ } from "react";
11
+ import { motion, useMotionValue, animate, type PanInfo } from "motion/react";
12
+ import { cn } from "@/lib/utils";
13
+
14
+ interface MobileNavContextValue {
15
+ activeKey: string | null;
16
+ open: boolean;
17
+ toggle: (key: string) => void;
18
+ close: () => void;
19
+ }
20
+
21
+ interface MobileNavProps {
22
+ className?: string;
23
+ children: ReactNode;
24
+ }
25
+
26
+ interface MobileNavPanelProps {
27
+ className?: string;
28
+ children: (activeKey: string) => ReactNode;
29
+ }
30
+
31
+ interface MobileNavBarProps {
32
+ className?: string;
33
+ children: ReactNode;
34
+ }
35
+
36
+ interface MobileNavTriggerProps {
37
+ value: string;
38
+ children: React.ReactNode;
39
+ label: string;
40
+ /** If provided, fires this action instead of toggling the panel */
41
+ onAction?: () => void;
42
+ className?: string;
43
+ }
44
+
45
+ const SPRING_CONFIG = { stiffness: 300, damping: 30 };
46
+
47
+ const MobileNavContext = createContext<MobileNavContextValue | null>(null);
48
+
49
+ const useMobileNav = () => {
50
+ const ctx = useContext(MobileNavContext);
51
+ if (!ctx)
52
+ throw new Error(
53
+ "MobileNav compound components must be used within <MobileNav>",
54
+ );
55
+ return ctx;
56
+ };
57
+
58
+ const MobileNav = ({ className, children }: MobileNavProps) => {
59
+ const [activeKey, setActiveKey] = useState<string | null>(null);
60
+ const [open, setOpen] = useState(false);
61
+
62
+ const toggle = useCallback(
63
+ (key: string) => {
64
+ if (activeKey === key && open) {
65
+ setOpen(false);
66
+ setActiveKey(null);
67
+ } else {
68
+ setActiveKey(key);
69
+ setOpen(true);
70
+ }
71
+ },
72
+ [activeKey, open],
73
+ );
74
+
75
+ const close = useCallback(() => {
76
+ setOpen(false);
77
+ setActiveKey(null);
78
+ }, []);
79
+
80
+ const value = useMemo(
81
+ () => ({ activeKey, open, toggle, close }),
82
+ [activeKey, open, toggle, close],
83
+ );
84
+
85
+ return (
86
+ <MobileNavContext.Provider value={value}>
87
+ <div className={cn("mobile-nav", className)}>{children}</div>
88
+ </MobileNavContext.Provider>
89
+ );
90
+ };
91
+
92
+ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
93
+ const { activeKey, open, close } = useMobileNav();
94
+
95
+ const panelRef = useRef<HTMLDivElement>(null);
96
+ const contentRef = useRef<HTMLDivElement>(null);
97
+
98
+ const heightMV = useMotionValue(0);
99
+ const stopAnimation = useRef<(() => void) | null>(null);
100
+
101
+ // Track which state the panel is in: "closed" | "initial" | "full"
102
+ const stateRef = useRef<"closed" | "initial" | "full">("closed");
103
+
104
+ // Cache the initial content height so returning from full → initial is consistent
105
+ const cachedInitialHeight = useRef(0);
106
+
107
+ const getNavBarHeight = () => {
108
+ const nav = panelRef.current?.nextElementSibling as HTMLElement | null;
109
+ return nav?.offsetHeight ?? 0;
110
+ };
111
+
112
+ const getAvailableHeight = () => {
113
+ const parent = panelRef.current?.parentElement;
114
+ const container = parent?.clientHeight ?? window.innerHeight;
115
+ return container - getNavBarHeight();
116
+ };
117
+
118
+ const measureContentHeight = () => {
119
+ const content = contentRef.current;
120
+ if (!content) return 0;
121
+ // Measure the first child to get true content height without triggering reflow
122
+ const child = content.firstElementChild as HTMLElement | null;
123
+ const raw = (child?.offsetHeight ?? content.scrollHeight) + 40; // +40 for handle area
124
+ const max = getAvailableHeight() * 0.7;
125
+ return Math.min(raw, max);
126
+ };
127
+
128
+ const springTo = (target: number) => {
129
+ stopAnimation.current?.();
130
+ const controls = animate(heightMV, target, {
131
+ type: "spring",
132
+ ...SPRING_CONFIG,
133
+ });
134
+ stopAnimation.current = () => controls.stop();
135
+ };
136
+
137
+ // Animate open/close
138
+ useEffect(() => {
139
+ let cancelled = false;
140
+ if (open) {
141
+ stateRef.current = "initial";
142
+ requestAnimationFrame(() => {
143
+ if (cancelled) return;
144
+ const h = measureContentHeight();
145
+ cachedInitialHeight.current = h;
146
+ springTo(h);
147
+ });
148
+ } else {
149
+ stateRef.current = "closed";
150
+ cachedInitialHeight.current = 0;
151
+ springTo(0);
152
+ }
153
+ return () => {
154
+ cancelled = true;
155
+ };
156
+ }, [open]);
157
+
158
+ // Re-measure when content changes (e.g. switching tabs)
159
+ useEffect(() => {
160
+ if (!open) return;
161
+ let cancelled = false;
162
+ // Double rAF ensures new children have rendered before measuring
163
+ requestAnimationFrame(() => {
164
+ requestAnimationFrame(() => {
165
+ if (cancelled) return;
166
+ const h = measureContentHeight();
167
+ cachedInitialHeight.current = h;
168
+ if (stateRef.current === "initial" || stateRef.current === "closed") {
169
+ stateRef.current = "initial";
170
+ springTo(h);
171
+ }
172
+ });
173
+ });
174
+ return () => {
175
+ cancelled = true;
176
+ };
177
+ }, [activeKey]);
178
+
179
+ // Track drag direction via ref for reliable gesture detection
180
+ const dragDirection = useRef<"up" | "down" | null>(null);
181
+
182
+ const handlePanStart = () => {
183
+ dragDirection.current = null;
184
+ };
185
+
186
+ const handlePan = (_: unknown, info: PanInfo) => {
187
+ // Continuously track the latest direction during drag
188
+ dragDirection.current = info.offset.y < 0 ? "up" : "down";
189
+ };
190
+
191
+ const handlePanEnd = () => {
192
+ const dir = dragDirection.current;
193
+ if (!dir) return;
194
+
195
+ if (dir === "up") {
196
+ if (stateRef.current === "initial") {
197
+ stateRef.current = "full";
198
+ springTo(getAvailableHeight());
199
+ }
200
+ } else {
201
+ // dir === "down"
202
+ if (stateRef.current === "full") {
203
+ stateRef.current = "initial";
204
+ springTo(cachedInitialHeight.current);
205
+ } else if (stateRef.current === "initial") {
206
+ stateRef.current = "closed";
207
+ springTo(0);
208
+ close();
209
+ }
210
+ }
211
+ };
212
+
213
+ return (
214
+ <motion.div
215
+ ref={panelRef}
216
+ className={cn("mobile-nav-panel", className)}
217
+ data-closed={!open || undefined}
218
+ style={{ height: heightMV }}
219
+ >
220
+ <motion.div
221
+ className="mobile-nav-handle"
222
+ onPanStart={handlePanStart}
223
+ onPan={handlePan}
224
+ onPanEnd={handlePanEnd}
225
+ >
226
+ <div className="mobile-nav-handle-bar" />
227
+ </motion.div>
228
+
229
+ <div ref={contentRef} className="mobile-nav-content">
230
+ {activeKey ? children(activeKey) : null}
231
+ </div>
232
+ </motion.div>
233
+ );
234
+ };
235
+
236
+ const MobileNavBar = ({ className, children }: MobileNavBarProps) => (
237
+ <nav className={cn("mobile-nav-bar", className)}>
238
+ <div className="mobile-nav-bar-inner">{children}</div>
239
+ </nav>
240
+ );
241
+
242
+ const MobileNavTrigger = ({
243
+ value,
244
+ children,
245
+ label,
246
+ onAction,
247
+ className,
248
+ }: MobileNavTriggerProps) => {
249
+ const { activeKey, open, toggle, close } = useMobileNav();
250
+
251
+ const handleClick = () => {
252
+ if (onAction) {
253
+ if (open) close();
254
+ onAction();
255
+ } else {
256
+ toggle(value);
257
+ }
258
+ };
259
+
260
+ return (
261
+ <button
262
+ onClick={handleClick}
263
+ data-active={activeKey === value || undefined}
264
+ className={cn("mobile-nav-trigger group", className)}
265
+ aria-label={label}
266
+ >
267
+ {children}
268
+ </button>
269
+ );
270
+ };
271
+
272
+ export { MobileNav, MobileNavPanel, MobileNavBar, MobileNavTrigger };
273
+ export type {
274
+ MobileNavProps,
275
+ MobileNavPanelProps,
276
+ MobileNavBarProps,
277
+ MobileNavTriggerProps,
278
+ };
package/client/global.css CHANGED
@@ -3786,7 +3786,7 @@ body {
3786
3786
  position: fixed;
3787
3787
  inset: 0;
3788
3788
  z-index: 50;
3789
- background-color: rgba(0, 0, 0, 0.8);
3789
+ background-color: rgba(0, 0, 0, 0.08);
3790
3790
  }
3791
3791
 
3792
3792
  .drawer-content {
@@ -3798,9 +3798,10 @@ body {
3798
3798
  margin-top: 6rem;
3799
3799
  display: flex;
3800
3800
  height: auto;
3801
+ max-height: 70vh;
3801
3802
  flex-direction: column;
3802
- border-top-left-radius: 10px;
3803
- border-top-right-radius: 10px;
3803
+ border-top-left-radius: 32px;
3804
+ border-top-right-radius: 32px;
3804
3805
  border: 1px solid hsl(var(--border));
3805
3806
  background-color: hsl(var(--background));
3806
3807
  }
@@ -3809,10 +3810,10 @@ body {
3809
3810
  margin-left: auto;
3810
3811
  margin-right: auto;
3811
3812
  margin-top: 1rem;
3812
- height: 0.5rem;
3813
- width: 6.25rem;
3813
+ height: 4px;
3814
+ width: 52px;
3814
3815
  border-radius: 9999px;
3815
- background-color: hsl(var(--muted));
3816
+ background-color: var(--color-gray-300);
3816
3817
  }
3817
3818
 
3818
3819
  .drawer-header {
@@ -3849,6 +3850,89 @@ body {
3849
3850
  color: hsl(var(--muted-foreground));
3850
3851
  }
3851
3852
 
3853
+ /* MobileNav Styles */
3854
+ .mobile-nav {
3855
+ display: flex;
3856
+ flex-direction: column;
3857
+ }
3858
+
3859
+ .mobile-nav-panel {
3860
+ background-color: white;
3861
+ border-radius: 32px 32px 0 0;
3862
+ box-shadow:
3863
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
3864
+ 0 4px 6px -4px rgba(0, 0, 0, 0.1);
3865
+ overflow: hidden;
3866
+ }
3867
+
3868
+ .mobile-nav-panel[data-closed] {
3869
+ border-top: 0;
3870
+ }
3871
+
3872
+ .mobile-nav-handle {
3873
+ display: flex;
3874
+ justify-content: center;
3875
+ padding-top: 1rem;
3876
+ padding-bottom: 2rem;
3877
+ cursor: grab;
3878
+ touch-action: none;
3879
+ }
3880
+
3881
+ .mobile-nav-handle:active {
3882
+ cursor: grabbing;
3883
+ }
3884
+
3885
+ .mobile-nav-handle-bar {
3886
+ height: 4px;
3887
+ width: 52px;
3888
+ border-radius: 9999px;
3889
+ background-color: var(--color-gray-300);
3890
+ }
3891
+
3892
+ .mobile-nav-content {
3893
+ overflow-y: auto;
3894
+ height: calc(100% - 40px);
3895
+ }
3896
+
3897
+ .mobile-nav-bar {
3898
+ border-top: 1px solid var(--color-gray-100);
3899
+ background-color: var(--color-gray-50);
3900
+ flex-shrink: 0;
3901
+ min-height: 136px;
3902
+ }
3903
+
3904
+ .mobile-nav-bar-inner {
3905
+ margin-left: auto;
3906
+ margin-right: auto;
3907
+ display: flex;
3908
+ max-width: 28rem;
3909
+ align-items: center;
3910
+ justify-content: space-around;
3911
+ padding-top: 0.5rem;
3912
+ padding-bottom: 2rem;
3913
+ padding-left: 1.5rem;
3914
+ padding-right: 1.5rem;
3915
+ }
3916
+
3917
+ .mobile-nav-trigger {
3918
+ display: flex;
3919
+ align-items: center;
3920
+ justify-content: center;
3921
+ width: 64px;
3922
+ height: 64px;
3923
+ cursor: pointer;
3924
+ transition: color 150ms;
3925
+ color: var(--color-gray-400);
3926
+ }
3927
+
3928
+ .mobile-nav-trigger:hover {
3929
+ color: var(--color-gray-600);
3930
+ }
3931
+
3932
+ .mobile-nav-trigger[data-active] {
3933
+ color: var(--color-gray-900);
3934
+ }
3935
+
3852
3936
  /* Pagination Styles */
3853
3937
  .pagination {
3854
3938
  margin-left: auto;