@syscore/ui-library 1.16.0 → 1.18.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.
@@ -4,11 +4,12 @@ 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";
13
14
 
14
15
  interface MobileNavContextValue {
@@ -39,6 +40,7 @@ interface MobileNavTriggerProps {
39
40
  label: string;
40
41
  /** If provided, fires this action instead of toggling the panel */
41
42
  onAction?: () => void;
43
+ disabled?: boolean;
42
44
  className?: string;
43
45
  }
44
46
 
@@ -98,10 +100,8 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
98
100
  const heightMV = useMotionValue(0);
99
101
  const stopAnimation = useRef<(() => void) | null>(null);
100
102
 
101
- // Track which state the panel is in: "closed" | "initial" | "full"
103
+ // Panel state: "closed" | "initial" | "full"
102
104
  const stateRef = useRef<"closed" | "initial" | "full">("closed");
103
-
104
- // Cache the initial content height so returning from full → initial is consistent
105
105
  const cachedInitialHeight = useRef(0);
106
106
 
107
107
  const getNavBarHeight = () => {
@@ -110,15 +110,12 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
110
110
  };
111
111
 
112
112
  const getAvailableHeight = () => {
113
- const parent = panelRef.current?.parentElement;
114
- const container = parent?.clientHeight ?? window.innerHeight;
115
- return container - getNavBarHeight();
113
+ return window.innerHeight - getNavBarHeight();
116
114
  };
117
115
 
118
116
  const measureContentHeight = () => {
119
117
  const content = contentRef.current;
120
118
  if (!content) return 0;
121
- // Measure the first child to get true content height without triggering reflow
122
119
  const child = content.firstElementChild as HTMLElement | null;
123
120
  const raw = (child?.offsetHeight ?? content.scrollHeight) + 40; // +40 for handle area
124
121
  const max = getAvailableHeight() * 0.7;
@@ -135,101 +132,140 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
135
132
  };
136
133
 
137
134
  // Animate open/close
138
- useEffect(() => {
139
- let cancelled = false;
135
+ useLayoutEffect(() => {
140
136
  if (open) {
141
137
  stateRef.current = "initial";
142
- requestAnimationFrame(() => {
143
- if (cancelled) return;
144
- const h = measureContentHeight();
145
- cachedInitialHeight.current = h;
146
- springTo(h);
147
- });
138
+ const h = measureContentHeight();
139
+ cachedInitialHeight.current = h;
140
+ springTo(h);
148
141
  } else {
149
142
  stateRef.current = "closed";
150
143
  cachedInitialHeight.current = 0;
151
144
  springTo(0);
152
145
  }
153
- return () => {
154
- cancelled = true;
155
- };
156
146
  }, [open]);
157
147
 
158
- // Re-measure when content changes (e.g. switching tabs)
159
- useEffect(() => {
148
+ // Re-measure when switching tabs while panel is already open
149
+ useLayoutEffect(() => {
160
150
  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
- };
151
+ const h = measureContentHeight();
152
+ cachedInitialHeight.current = h;
153
+ if (stateRef.current !== "full") {
154
+ stateRef.current = "initial";
155
+ springTo(h);
156
+ }
177
157
  }, [activeKey]);
178
158
 
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;
159
+ // Close on Escape key
160
+ useEffect(() => {
161
+ if (!open) return;
162
+ const handleKeyDown = (e: KeyboardEvent) => {
163
+ if (e.key === "Escape") close();
164
+ };
165
+ document.addEventListener("keydown", handleKeyDown);
166
+ return () => document.removeEventListener("keydown", handleKeyDown);
167
+ }, [open, close]);
168
+
169
+ // --- Pointer-based drag (bypasses motion's gesture detection) ---
170
+ const isDragging = useRef(false);
171
+ const lastY = useRef(0);
172
+ const dragStartY = useRef(0);
173
+ const dragStartTime = useRef(0);
174
+ const dragMaxHeight = useRef(0);
175
+
176
+ const handlePointerDown = (e: React.PointerEvent) => {
177
+ (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
178
+ stopAnimation.current?.();
179
+ isDragging.current = true;
180
+ lastY.current = e.clientY;
181
+ dragStartY.current = e.clientY;
182
+ dragStartTime.current = Date.now();
183
+ dragMaxHeight.current = getAvailableHeight();
184
184
  };
185
185
 
186
- const handlePan = (_: unknown, info: PanInfo) => {
187
- // Continuously track the latest direction during drag
188
- dragDirection.current = info.offset.y < 0 ? "up" : "down";
186
+ const handlePointerMove = (e: React.PointerEvent) => {
187
+ if (!isDragging.current) return;
188
+ const dy = e.clientY - lastY.current;
189
+ lastY.current = e.clientY;
190
+ const current = heightMV.get();
191
+ const next = Math.max(0, current - dy);
192
+ heightMV.set(Math.min(next, dragMaxHeight.current));
189
193
  };
190
194
 
191
- const handlePanEnd = () => {
192
- const dir = dragDirection.current;
193
- if (!dir) return;
195
+ const handlePointerUp = (e: React.PointerEvent) => {
196
+ if (!isDragging.current) return;
197
+ isDragging.current = false;
194
198
 
195
- if (dir === "up") {
196
- if (stateRef.current === "initial") {
197
- stateRef.current = "full";
198
- springTo(getAvailableHeight());
199
- }
200
- } else {
201
- // dir === "down"
199
+ const currentHeight = heightMV.get();
200
+ const initial = cachedInitialHeight.current;
201
+ const full = dragMaxHeight.current;
202
+
203
+ // Calculate velocity (px/s) — positive = downward
204
+ const elapsed = Date.now() - dragStartTime.current;
205
+ const totalDy = e.clientY - dragStartY.current;
206
+ const velocity = elapsed > 0 ? (totalDy / elapsed) * 1000 : 0;
207
+
208
+ const VELOCITY_THRESHOLD = 500;
209
+
210
+ if (velocity > VELOCITY_THRESHOLD) {
211
+ // Fast downward flick
202
212
  if (stateRef.current === "full") {
203
213
  stateRef.current = "initial";
204
- springTo(cachedInitialHeight.current);
205
- } else if (stateRef.current === "initial") {
214
+ springTo(initial);
215
+ } else {
206
216
  stateRef.current = "closed";
207
217
  springTo(0);
208
218
  close();
209
219
  }
220
+ } else if (velocity < -VELOCITY_THRESHOLD) {
221
+ // Fast upward flick
222
+ stateRef.current = "full";
223
+ springTo(full);
224
+ } else {
225
+ // Slow drag — snap to nearest
226
+ const midInitial = initial / 2;
227
+ const midFull = (initial + full) / 2;
228
+
229
+ if (currentHeight < midInitial) {
230
+ stateRef.current = "closed";
231
+ springTo(0);
232
+ close();
233
+ } else if (currentHeight < midFull) {
234
+ stateRef.current = "initial";
235
+ springTo(initial);
236
+ } else {
237
+ stateRef.current = "full";
238
+ springTo(full);
239
+ }
210
240
  }
211
241
  };
212
242
 
213
243
  return (
214
- <motion.div
215
- ref={panelRef}
216
- className={cn("mobile-nav-panel", className)}
217
- data-closed={!open || undefined}
218
- style={{ height: heightMV }}
219
- >
244
+ <>
245
+ {open && (
246
+ <div className="mobile-nav-overlay" onClick={close} aria-hidden />
247
+ )}
220
248
  <motion.div
221
- className="mobile-nav-handle"
222
- onPanStart={handlePanStart}
223
- onPan={handlePan}
224
- onPanEnd={handlePanEnd}
249
+ ref={panelRef}
250
+ className={cn("mobile-nav-panel", className)}
251
+ data-closed={!open || undefined}
252
+ style={{ height: heightMV }}
225
253
  >
226
- <div className="mobile-nav-handle-bar" />
254
+ <div
255
+ className="mobile-nav-handle"
256
+ onPointerDown={handlePointerDown}
257
+ onPointerMove={handlePointerMove}
258
+ onPointerUp={handlePointerUp}
259
+ onPointerCancel={handlePointerUp}
260
+ >
261
+ <div className="mobile-nav-handle-bar" />
262
+ </div>
263
+
264
+ <div ref={contentRef} className="mobile-nav-content">
265
+ {activeKey ? children(activeKey) : null}
266
+ </div>
227
267
  </motion.div>
228
-
229
- <div ref={contentRef} className="mobile-nav-content">
230
- {activeKey ? children(activeKey) : null}
231
- </div>
232
- </motion.div>
268
+ </>
233
269
  );
234
270
  };
235
271
 
@@ -244,6 +280,7 @@ const MobileNavTrigger = ({
244
280
  children,
245
281
  label,
246
282
  onAction,
283
+ disabled,
247
284
  className,
248
285
  }: MobileNavTriggerProps) => {
249
286
  const { activeKey, open, toggle, close } = useMobileNav();
@@ -260,7 +297,9 @@ const MobileNavTrigger = ({
260
297
  return (
261
298
  <button
262
299
  onClick={handleClick}
300
+ disabled={disabled}
263
301
  data-active={activeKey === value || undefined}
302
+ aria-expanded={activeKey === value && open}
264
303
  className={cn("mobile-nav-trigger group", className)}
265
304
  aria-label={label}
266
305
  >
package/client/global.css CHANGED
@@ -3852,11 +3852,25 @@ body {
3852
3852
 
3853
3853
  /* MobileNav Styles */
3854
3854
  .mobile-nav {
3855
+ position: fixed;
3856
+ bottom: 0;
3857
+ left: 0;
3858
+ right: 0;
3859
+ z-index: 50;
3855
3860
  display: flex;
3856
3861
  flex-direction: column;
3857
3862
  }
3858
3863
 
3864
+ .mobile-nav-overlay {
3865
+ position: fixed;
3866
+ inset: 0;
3867
+ background-color: rgba(0, 0, 0, 0.08);
3868
+ z-index: 40;
3869
+ }
3870
+
3859
3871
  .mobile-nav-panel {
3872
+ position: relative;
3873
+ z-index: 50;
3860
3874
  background-color: white;
3861
3875
  border-radius: 32px 32px 0 0;
3862
3876
  box-shadow:
@@ -3895,10 +3909,11 @@ body {
3895
3909
  }
3896
3910
 
3897
3911
  .mobile-nav-bar {
3912
+ position: relative;
3913
+ z-index: 50;
3898
3914
  border-top: 1px solid var(--color-gray-100);
3899
3915
  background-color: var(--color-gray-50);
3900
3916
  flex-shrink: 0;
3901
- min-height: 136px;
3902
3917
  }
3903
3918
 
3904
3919
  .mobile-nav-bar-inner {
@@ -3909,7 +3924,7 @@ body {
3909
3924
  align-items: center;
3910
3925
  justify-content: space-around;
3911
3926
  padding-top: 0.5rem;
3912
- padding-bottom: 2rem;
3927
+ padding-bottom: 0.5rem;
3913
3928
  padding-left: 1.5rem;
3914
3929
  padding-right: 1.5rem;
3915
3930
  }
@@ -3933,6 +3948,11 @@ body {
3933
3948
  color: var(--color-gray-900);
3934
3949
  }
3935
3950
 
3951
+ .mobile-nav-trigger:disabled {
3952
+ pointer-events: none;
3953
+ opacity: 0.4;
3954
+ }
3955
+
3936
3956
  /* Pagination Styles */
3937
3957
  .pagination {
3938
3958
  margin-left: auto;
@@ -6976,6 +6996,38 @@ body {
6976
6996
  text-underline-offset: 2px;
6977
6997
  }
6978
6998
 
6999
+ /* Responsive Typography - Mobile */
7000
+ @media (max-width: 640px) {
7001
+ .heading-xlarge {
7002
+ font-size: 36px;
7003
+ line-height: 40px;
7004
+ }
7005
+ .heading-large {
7006
+ font-size: 28px;
7007
+ line-height: 32px;
7008
+ }
7009
+ .heading-medium {
7010
+ font-size: 26px;
7011
+ line-height: 30px;
7012
+ }
7013
+ .heading-small {
7014
+ font-size: 24px;
7015
+ line-height: 28px;
7016
+ }
7017
+ .heading-xsmall {
7018
+ font-size: 20px;
7019
+ line-height: 24px;
7020
+ }
7021
+ .heading-xxsmall {
7022
+ font-size: 18px;
7023
+ line-height: 22px;
7024
+ }
7025
+ .body-large {
7026
+ font-size: 16px;
7027
+ line-height: 22.4px;
7028
+ }
7029
+ }
7030
+
6979
7031
  /* Standard Table Styles */
6980
7032
  .standard-table {
6981
7033
  border: 1px solid var(--color-blue-200, #cbe0f1);
@@ -7089,6 +7141,14 @@ body {
7089
7141
  gap: 1rem;
7090
7142
  }
7091
7143
 
7144
+ @media (max-width: 640px) {
7145
+ .standard-table-row-header,
7146
+ .standard-table-list-row {
7147
+ height: auto;
7148
+ min-height: 5rem;
7149
+ }
7150
+ }
7151
+
7092
7152
  /* PageHeader Styles */
7093
7153
  .page-header {
7094
7154
  margin-bottom: 1.5rem;
@@ -7126,6 +7186,12 @@ body {
7126
7186
  padding-right: 10rem;
7127
7187
  }
7128
7188
 
7189
+ @media (max-width: 640px) {
7190
+ .page-header-description {
7191
+ padding-right: 0;
7192
+ }
7193
+ }
7194
+
7129
7195
  /* Navigation Styles */
7130
7196
  .navigation {
7131
7197
  position: absolute;
@@ -17,6 +17,7 @@ import { UtilityText } from "../components/icons/UtilityText";
17
17
  import { UtilityCompare } from "../components/icons/UtilityCompare";
18
18
  import { UtilityRevisionsShow } from "../components/icons/UtilityRevisionsShow";
19
19
  import { UtilityFeedback } from "../components/icons/UtilityFeedback";
20
+ import { Button } from "@/components/ui/button";
20
21
 
21
22
  const meta = {
22
23
  title: "Review/MobileNav",
@@ -24,6 +25,9 @@ const meta = {
24
25
  tags: ["autodocs"],
25
26
  parameters: {
26
27
  layout: "fullscreen",
28
+ viewport: {
29
+ defaultViewport: "mobile1",
30
+ },
27
31
  docs: {
28
32
  description: {
29
33
  component: `
@@ -41,20 +45,26 @@ Mobile navigation with a spring-animated slide-up panel. Built as a **compound c
41
45
  ## Basic Usage
42
46
 
43
47
  \`\`\`tsx
44
- <MobileNav className="h-screen">
45
- <MobileNavPanel>
46
- {(activeKey) => <MyContent tab={activeKey} />}
47
- </MobileNavPanel>
48
-
49
- <MobileNavBar>
50
- <MobileNavTrigger value="home" label="Home">
51
- <HomeIcon className="size-6" />
52
- </MobileNavTrigger>
53
- <MobileNavTrigger value="search" label="Search">
54
- <SearchIcon className="size-6" />
55
- </MobileNavTrigger>
56
- </MobileNavBar>
57
- </MobileNav>
48
+ <section>
49
+ {/* Your page content */}
50
+ <MyPageContent />
51
+
52
+ {/* Fixed bottom nav */}
53
+ <MobileNav>
54
+ <MobileNavPanel>
55
+ {(activeKey) => <MyContent tab={activeKey} />}
56
+ </MobileNavPanel>
57
+
58
+ <MobileNavBar>
59
+ <MobileNavTrigger value="home" label="Home">
60
+ <HomeIcon className="size-6" />
61
+ </MobileNavTrigger>
62
+ <MobileNavTrigger value="search" label="Search">
63
+ <SearchIcon className="size-6" />
64
+ </MobileNavTrigger>
65
+ </MobileNavBar>
66
+ </MobileNav>
67
+ </section>
58
68
  \`\`\`
59
69
 
60
70
  ## Styling Active State
@@ -224,35 +234,43 @@ export const Default: Story = {
224
234
  args: { children: null },
225
235
 
226
236
  render: () => (
227
- <MobileNav className="h-screen w-full bg-gray-50">
237
+ <div className="h-screen w-full bg-white">
228
238
  {/* Main content area */}
229
- <div className="flex-1 p-6 overflow-y-auto">
239
+ <div className="p-6">
230
240
  <p className="text-sm text-gray-400">
231
241
  Tap an icon below to open the panel.
232
242
  </p>
243
+ <Button
244
+ size="large"
245
+ variant="general-primary"
246
+ onClick={() => alert("Hello")}
247
+ >
248
+ Open panel
249
+ </Button>
233
250
  </div>
234
251
 
235
- {/* Panel slides up above the nav */}
236
- <MobileNavPanel>
237
- {(activeKey) => <PanelContent activeKey={activeKey} />}
238
- </MobileNavPanel>
239
-
240
- {/* Nav — always visible */}
241
- <MobileNavBar>
242
- <MobileNavTrigger value="list" label="Changes">
243
- <UtilityText className="size-6 text-gray-500 group-data-active:text-gray-900" />
244
- </MobileNavTrigger>
245
- <MobileNavTrigger value="workflow" label="Workflow">
246
- <UtilityCompare className="size-6 text-gray-500 group-data-active:text-gray-900" />
247
- </MobileNavTrigger>
248
- <MobileNavTrigger value="edit" label="Edits">
249
- <UtilityRevisionsShow className="size-6 text-gray-500 group-data-active:text-gray-900" />
250
- </MobileNavTrigger>
251
- <MobileNavTrigger value="comments" label="Comments">
252
- <MessageSquare className="size-6 text-gray-500 group-data-active:text-gray-900" />
253
- </MobileNavTrigger>
254
- </MobileNavBar>
255
- </MobileNav>
252
+ {/* Fixed bottom nav */}
253
+ <MobileNav>
254
+ <MobileNavPanel>
255
+ {(activeKey) => <PanelContent activeKey={activeKey} />}
256
+ </MobileNavPanel>
257
+
258
+ <MobileNavBar>
259
+ <MobileNavTrigger value="list" label="Changes">
260
+ <UtilityText className="size-6 text-gray-500 group-data-active:text-gray-900" />
261
+ </MobileNavTrigger>
262
+ <MobileNavTrigger value="workflow" label="Workflow">
263
+ <UtilityCompare className="size-6 text-gray-500 group-data-active:text-gray-900" />
264
+ </MobileNavTrigger>
265
+ <MobileNavTrigger value="edit" label="Edits">
266
+ <UtilityRevisionsShow className="size-6 text-gray-500 group-data-active:text-gray-900" />
267
+ </MobileNavTrigger>
268
+ <MobileNavTrigger value="comments" label="Comments">
269
+ <UtilityFeedback className="size-6 text-gray-500 group-data-active:text-gray-900" />
270
+ </MobileNavTrigger>
271
+ </MobileNavBar>
272
+ </MobileNav>
273
+ </div>
256
274
  ),
257
275
  };
258
276
 
@@ -260,38 +278,40 @@ const WithCustomActionRender = () => {
260
278
  const [modalOpen, setModalOpen] = useState(false);
261
279
 
262
280
  return (
263
- <MobileNav className="relative h-screen w-full bg-gray-50">
264
- <div className="flex-1 p-6 overflow-y-auto">
281
+ <div className="relative h-screen w-full bg-white">
282
+ <div className="p-6">
265
283
  <p className="text-sm text-gray-400">
266
284
  The &ldquo;Edits&rdquo; trigger opens a modal instead of the panel.
267
285
  </p>
268
286
  </div>
269
287
 
270
- <MobileNavPanel>
271
- {(activeKey) => <PanelContent activeKey={activeKey} />}
272
- </MobileNavPanel>
273
-
274
- <MobileNavBar>
275
- <MobileNavTrigger value="list" label="Changes">
276
- <UtilityText className="size-6 text-gray-500 group-data-active:text-gray-900" />
277
- </MobileNavTrigger>
278
- <MobileNavTrigger value="workflow" label="Workflow">
279
- <UtilityCompare className="size-8 text-gray-500 group-data-active:text-gray-900" />
280
- </MobileNavTrigger>
281
- <MobileNavTrigger
282
- value="edit"
283
- label="Edits"
284
- onAction={() => setModalOpen(true)}
285
- >
286
- <UtilityRevisionsShow className="size-6 text-gray-500" />
287
- </MobileNavTrigger>
288
- <MobileNavTrigger value="comments" label="Comments">
289
- <UtilityFeedback className="size-6 text-gray-500 group-data-active:text-gray-900" />
290
- </MobileNavTrigger>
291
- </MobileNavBar>
288
+ <MobileNav>
289
+ <MobileNavPanel>
290
+ {(activeKey) => <PanelContent activeKey={activeKey} />}
291
+ </MobileNavPanel>
292
+
293
+ <MobileNavBar>
294
+ <MobileNavTrigger value="list" label="Changes">
295
+ <UtilityText className="size-6 text-gray-500 group-data-active:text-gray-900" />
296
+ </MobileNavTrigger>
297
+ <MobileNavTrigger value="workflow" label="Workflow">
298
+ <UtilityCompare className="size-8 text-gray-500 group-data-active:text-gray-900" />
299
+ </MobileNavTrigger>
300
+ <MobileNavTrigger
301
+ value="edit"
302
+ label="Edits"
303
+ onAction={() => setModalOpen(true)}
304
+ >
305
+ <UtilityRevisionsShow className="size-6 text-gray-500" />
306
+ </MobileNavTrigger>
307
+ <MobileNavTrigger value="comments" label="Comments">
308
+ <UtilityFeedback className="size-6 text-gray-500 group-data-active:text-gray-900" />
309
+ </MobileNavTrigger>
310
+ </MobileNavBar>
311
+ </MobileNav>
292
312
 
293
313
  {modalOpen && (
294
- <div className="absolute inset-0 z-50 flex items-center justify-center bg-black/20">
314
+ <div className="fixed inset-0 z-60 flex items-center justify-center bg-black/20">
295
315
  <div className="relative rounded-xl bg-white p-6 shadow-lg w-72">
296
316
  <button
297
317
  onClick={() => setModalOpen(false)}
@@ -307,7 +327,7 @@ const WithCustomActionRender = () => {
307
327
  </div>
308
328
  </div>
309
329
  )}
310
- </MobileNav>
330
+ </div>
311
331
  );
312
332
  };
313
333
 
@@ -315,3 +335,39 @@ export const WithCustomAction: Story = {
315
335
  args: { children: null },
316
336
  render: () => <WithCustomActionRender />,
317
337
  };
338
+
339
+ export const WithDisabledTrigger: Story = {
340
+ args: { children: null },
341
+
342
+ render: () => (
343
+ <div className="h-screen w-full bg-white">
344
+ <div className="p-6">
345
+ <p className="text-sm text-gray-400">
346
+ The &ldquo;Comments&rdquo; trigger is disabled because there are no
347
+ comments.
348
+ </p>
349
+ </div>
350
+
351
+ <MobileNav>
352
+ <MobileNavPanel>
353
+ {(activeKey) => <PanelContent activeKey={activeKey} />}
354
+ </MobileNavPanel>
355
+
356
+ <MobileNavBar>
357
+ <MobileNavTrigger value="list" label="Changes">
358
+ <UtilityText className="size-6 text-gray-500 group-data-active:text-gray-900" />
359
+ </MobileNavTrigger>
360
+ <MobileNavTrigger value="workflow" label="Workflow">
361
+ <UtilityCompare className="size-6 text-gray-500 group-data-active:text-gray-900" />
362
+ </MobileNavTrigger>
363
+ <MobileNavTrigger value="edit" label="Edits">
364
+ <UtilityRevisionsShow className="size-6 text-gray-500 group-data-active:text-gray-900" />
365
+ </MobileNavTrigger>
366
+ <MobileNavTrigger value="comments" label="Comments" disabled>
367
+ <UtilityFeedback className="size-6 text-gray-500 group-data-active:text-gray-900" />
368
+ </MobileNavTrigger>
369
+ </MobileNavBar>
370
+ </MobileNav>
371
+ </div>
372
+ ),
373
+ };