@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.
- package/client/components/ui/mobile-nav.tsx +112 -73
- package/client/global.css +68 -2
- package/client/ui/MobileNav.stories.tsx +119 -63
- package/dist/index.cjs.js +1 -1
- package/dist/index.es.js +100 -69
- package/package.json +1 -1
|
@@ -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
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
139
|
-
let cancelled = false;
|
|
135
|
+
useLayoutEffect(() => {
|
|
140
136
|
if (open) {
|
|
141
137
|
stateRef.current = "initial";
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
159
|
-
|
|
148
|
+
// Re-measure when switching tabs while panel is already open
|
|
149
|
+
useLayoutEffect(() => {
|
|
160
150
|
if (!open) return;
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
|
187
|
-
|
|
188
|
-
|
|
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
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
const handlePointerUp = (e: React.PointerEvent) => {
|
|
196
|
+
if (!isDragging.current) return;
|
|
197
|
+
isDragging.current = false;
|
|
194
198
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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(
|
|
205
|
-
} else
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
style={{ height: heightMV }}
|
|
219
|
-
>
|
|
244
|
+
<>
|
|
245
|
+
{open && (
|
|
246
|
+
<div className="mobile-nav-overlay" onClick={close} aria-hidden />
|
|
247
|
+
)}
|
|
220
248
|
<motion.div
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
249
|
+
ref={panelRef}
|
|
250
|
+
className={cn("mobile-nav-panel", className)}
|
|
251
|
+
data-closed={!open || undefined}
|
|
252
|
+
style={{ height: heightMV }}
|
|
225
253
|
>
|
|
226
|
-
<div
|
|
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:
|
|
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
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
<
|
|
52
|
-
</
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
</
|
|
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
|
-
<
|
|
237
|
+
<div className="h-screen w-full bg-white">
|
|
228
238
|
{/* Main content area */}
|
|
229
|
-
<div className="
|
|
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
|
-
{/*
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
<
|
|
264
|
-
<div className="
|
|
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 “Edits” trigger opens a modal instead of the panel.
|
|
267
285
|
</p>
|
|
268
286
|
</div>
|
|
269
287
|
|
|
270
|
-
<
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
<
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
<
|
|
290
|
-
|
|
291
|
-
|
|
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="
|
|
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
|
-
</
|
|
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 “Comments” 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
|
+
};
|