@syscore/ui-library 1.15.4 → 1.17.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/icons/UtilityCompare.tsx +37 -35
- package/client/components/icons/UtilityFeedback.tsx +30 -0
- package/client/components/icons/UtilityRevisionsHide.tsx +1 -1
- package/client/components/icons/UtilityRevisionsShow.tsx +40 -0
- package/client/components/ui/mobile-nav.tsx +286 -0
- package/client/global.css +106 -6
- package/client/ui/MobileNav.stories.tsx +351 -0
- package/dist/index.cjs.js +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.es.js +319 -37
- package/package.json +1 -1
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import {
|
|
3
|
+
MobileNav,
|
|
4
|
+
MobileNavPanel,
|
|
5
|
+
MobileNavBar,
|
|
6
|
+
MobileNavTrigger,
|
|
7
|
+
} from "../components/ui/mobile-nav";
|
|
8
|
+
import {
|
|
9
|
+
Workflow,
|
|
10
|
+
PenLine,
|
|
11
|
+
MessageSquare,
|
|
12
|
+
TriangleAlert,
|
|
13
|
+
X,
|
|
14
|
+
} from "lucide-react";
|
|
15
|
+
import { useState } from "react";
|
|
16
|
+
import { UtilityText } from "../components/icons/UtilityText";
|
|
17
|
+
import { UtilityCompare } from "../components/icons/UtilityCompare";
|
|
18
|
+
import { UtilityRevisionsShow } from "../components/icons/UtilityRevisionsShow";
|
|
19
|
+
import { UtilityFeedback } from "../components/icons/UtilityFeedback";
|
|
20
|
+
|
|
21
|
+
const meta = {
|
|
22
|
+
title: "Review/MobileNav",
|
|
23
|
+
component: MobileNav,
|
|
24
|
+
tags: ["autodocs"],
|
|
25
|
+
parameters: {
|
|
26
|
+
layout: "fullscreen",
|
|
27
|
+
docs: {
|
|
28
|
+
description: {
|
|
29
|
+
component: `
|
|
30
|
+
Mobile navigation with a spring-animated slide-up panel. Built as a **compound component** — state is managed internally via React context, so consumers never need \`useState\` or handlers.
|
|
31
|
+
|
|
32
|
+
## Compound Components
|
|
33
|
+
|
|
34
|
+
| Component | Role |
|
|
35
|
+
|---|---|
|
|
36
|
+
| \`MobileNav\` | Root — holds state, provides context |
|
|
37
|
+
| \`MobileNavPanel\` | Slide-up panel with 3 discrete states: **initial** (content height, max 70%), **full** (viewport), **closed** |
|
|
38
|
+
| \`MobileNavBar\` | Bottom nav bar wrapper |
|
|
39
|
+
| \`MobileNavTrigger\` | Individual nav button — toggles the panel or fires a custom action |
|
|
40
|
+
|
|
41
|
+
## Basic Usage
|
|
42
|
+
|
|
43
|
+
\`\`\`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>
|
|
58
|
+
\`\`\`
|
|
59
|
+
|
|
60
|
+
## Styling Active State
|
|
61
|
+
|
|
62
|
+
\`MobileNavTrigger\` renders a \`<button>\` with the \`group\` class and a \`data-active\` attribute when selected. Use Tailwind's \`group-data-active:\` modifier on children to style based on active state:
|
|
63
|
+
|
|
64
|
+
\`\`\`tsx
|
|
65
|
+
<MobileNavTrigger value="home" label="Home">
|
|
66
|
+
<HomeIcon className="size-6 text-gray-400 group-data-active:text-gray-900" />
|
|
67
|
+
<span className="text-xs hidden group-data-active:block">Home</span>
|
|
68
|
+
</MobileNavTrigger>
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
## Custom Actions (onAction)
|
|
72
|
+
|
|
73
|
+
Use \`onAction\` on a trigger to fire a custom callback instead of opening the panel:
|
|
74
|
+
|
|
75
|
+
\`\`\`tsx
|
|
76
|
+
<MobileNavTrigger value="add" label="Add" onAction={() => openModal()}>
|
|
77
|
+
<PlusIcon className="size-6" />
|
|
78
|
+
</MobileNavTrigger>
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
## Panel Gestures
|
|
82
|
+
|
|
83
|
+
The drag handle supports swipe gestures that snap between discrete states:
|
|
84
|
+
|
|
85
|
+
- **Swipe up** from initial → full screen
|
|
86
|
+
- **Swipe down** from full → initial height
|
|
87
|
+
- **Swipe down** from initial → close
|
|
88
|
+
`,
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
} satisfies Meta<typeof MobileNav>;
|
|
93
|
+
|
|
94
|
+
export default meta;
|
|
95
|
+
|
|
96
|
+
type Story = StoryObj<typeof meta>;
|
|
97
|
+
|
|
98
|
+
type TabKey = "list" | "workflow" | "edit" | "comments";
|
|
99
|
+
|
|
100
|
+
const tabContent: Record<
|
|
101
|
+
TabKey,
|
|
102
|
+
{ title: string; items: { label: string; description: string }[] }
|
|
103
|
+
> = {
|
|
104
|
+
list: {
|
|
105
|
+
title: "Changes",
|
|
106
|
+
items: [
|
|
107
|
+
{
|
|
108
|
+
label: "Reduced scope",
|
|
109
|
+
description:
|
|
110
|
+
"Requirement 2 no longer includes sulfide as a testing parameter.",
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
label: "Clarity edits",
|
|
114
|
+
description: "Requirement 2 has been re-written to maximize clarity.",
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
label: "New threshold",
|
|
118
|
+
description:
|
|
119
|
+
"Requirement 3 adds a 0.05 mg/L threshold for lead in drinking water.",
|
|
120
|
+
},
|
|
121
|
+
],
|
|
122
|
+
},
|
|
123
|
+
workflow: {
|
|
124
|
+
title: "Workflow",
|
|
125
|
+
items: [
|
|
126
|
+
{
|
|
127
|
+
label: "Review pending",
|
|
128
|
+
description: "3 items awaiting team review before finalization.",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
label: "Approved",
|
|
132
|
+
description: "12 strategies have been approved this cycle.",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
edit: {
|
|
137
|
+
title: "Edits",
|
|
138
|
+
items: [
|
|
139
|
+
{
|
|
140
|
+
label: "Draft update",
|
|
141
|
+
description: "Strategy C8.2 has been revised with new language.",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
label: "Formatting fix",
|
|
145
|
+
description:
|
|
146
|
+
"Table headers in Requirement 1 corrected for consistency.",
|
|
147
|
+
},
|
|
148
|
+
],
|
|
149
|
+
},
|
|
150
|
+
comments: {
|
|
151
|
+
title: "Comments",
|
|
152
|
+
items: [
|
|
153
|
+
{
|
|
154
|
+
label: "Team feedback",
|
|
155
|
+
description:
|
|
156
|
+
'Sarah noted: "Consider adding a grace period for compliance."',
|
|
157
|
+
},
|
|
158
|
+
{
|
|
159
|
+
label: "Compliance window",
|
|
160
|
+
description:
|
|
161
|
+
"Suggest extending the compliance window from 30 to 60 days.",
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
label: "Testing methodology",
|
|
165
|
+
description:
|
|
166
|
+
"Lab results should include both grab and composite samples.",
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
label: "Documentation gap",
|
|
170
|
+
description:
|
|
171
|
+
"Section 4.2 is missing referenced appendix for field procedures.",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
label: "Stakeholder input",
|
|
175
|
+
description:
|
|
176
|
+
"Community advisory board requested public comment period extension.",
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
label: "Budget concern",
|
|
180
|
+
description:
|
|
181
|
+
"Monitoring costs may exceed allocated budget by 15% in Q3.",
|
|
182
|
+
},
|
|
183
|
+
{
|
|
184
|
+
label: "Reviewer note",
|
|
185
|
+
description:
|
|
186
|
+
"External reviewer flagged Requirement 4 for further clarification.",
|
|
187
|
+
},
|
|
188
|
+
],
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
const PanelContent = ({ activeKey }: { activeKey: string }) => {
|
|
193
|
+
const content = tabContent[activeKey as TabKey];
|
|
194
|
+
if (!content) return null;
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<div className="mx-auto w-full max-w-md">
|
|
198
|
+
<div className="px-4 pb-2">
|
|
199
|
+
<h2 className="text-lg font-semibold tracking-tight">
|
|
200
|
+
{content.title}
|
|
201
|
+
</h2>
|
|
202
|
+
</div>
|
|
203
|
+
<div className="px-4 pb-4 space-y-3">
|
|
204
|
+
{content.items.map((item) => (
|
|
205
|
+
<div
|
|
206
|
+
key={item.label}
|
|
207
|
+
className="flex items-start justify-between gap-3 rounded-lg border border-gray-100 bg-white p-4"
|
|
208
|
+
>
|
|
209
|
+
<div className="space-y-1">
|
|
210
|
+
<p className="text-sm font-semibold text-gray-900">
|
|
211
|
+
{item.label}
|
|
212
|
+
</p>
|
|
213
|
+
<p className="text-sm text-gray-500">{item.description}</p>
|
|
214
|
+
</div>
|
|
215
|
+
<TriangleAlert className="mt-0.5 size-5 shrink-0 text-teal-400" />
|
|
216
|
+
</div>
|
|
217
|
+
))}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
export const Default: Story = {
|
|
224
|
+
args: { children: null },
|
|
225
|
+
|
|
226
|
+
render: () => (
|
|
227
|
+
<MobileNav className="h-screen w-full bg-gray-50">
|
|
228
|
+
{/* Main content area */}
|
|
229
|
+
<div className="flex-1 p-6 overflow-y-auto">
|
|
230
|
+
<p className="text-sm text-gray-400">
|
|
231
|
+
Tap an icon below to open the panel.
|
|
232
|
+
</p>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
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
|
+
<UtilityFeedback className="size-6 text-gray-500 group-data-active:text-gray-900" />
|
|
253
|
+
</MobileNavTrigger>
|
|
254
|
+
</MobileNavBar>
|
|
255
|
+
</MobileNav>
|
|
256
|
+
),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const WithCustomActionRender = () => {
|
|
260
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
261
|
+
|
|
262
|
+
return (
|
|
263
|
+
<MobileNav className="relative h-screen w-full bg-gray-50">
|
|
264
|
+
<div className="flex-1 p-6 overflow-y-auto">
|
|
265
|
+
<p className="text-sm text-gray-400">
|
|
266
|
+
The “Edits” trigger opens a modal instead of the panel.
|
|
267
|
+
</p>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
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>
|
|
292
|
+
|
|
293
|
+
{modalOpen && (
|
|
294
|
+
<div className="absolute inset-0 z-50 flex items-center justify-center bg-black/20">
|
|
295
|
+
<div className="relative rounded-xl bg-white p-6 shadow-lg w-72">
|
|
296
|
+
<button
|
|
297
|
+
onClick={() => setModalOpen(false)}
|
|
298
|
+
className="absolute top-3 right-3 text-gray-400 hover:text-gray-600"
|
|
299
|
+
aria-label="Close"
|
|
300
|
+
>
|
|
301
|
+
<X className="size-4" />
|
|
302
|
+
</button>
|
|
303
|
+
<p className="text-sm font-semibold text-gray-900">Edit mode</p>
|
|
304
|
+
<p className="mt-1 text-sm text-gray-500">
|
|
305
|
+
This modal was opened via onAction instead of the panel.
|
|
306
|
+
</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
)}
|
|
310
|
+
</MobileNav>
|
|
311
|
+
);
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
export const WithCustomAction: Story = {
|
|
315
|
+
args: { children: null },
|
|
316
|
+
render: () => <WithCustomActionRender />,
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
export const WithDisabledTrigger: Story = {
|
|
320
|
+
args: { children: null },
|
|
321
|
+
|
|
322
|
+
render: () => (
|
|
323
|
+
<MobileNav className="h-screen w-full bg-gray-50">
|
|
324
|
+
<div className="flex-1 p-6 overflow-y-auto">
|
|
325
|
+
<p className="text-sm text-gray-400">
|
|
326
|
+
The “Comments” trigger is disabled because there are no
|
|
327
|
+
comments.
|
|
328
|
+
</p>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<MobileNavPanel>
|
|
332
|
+
{(activeKey) => <PanelContent activeKey={activeKey} />}
|
|
333
|
+
</MobileNavPanel>
|
|
334
|
+
|
|
335
|
+
<MobileNavBar>
|
|
336
|
+
<MobileNavTrigger value="list" label="Changes">
|
|
337
|
+
<UtilityText className="size-6 text-gray-500 group-data-active:text-gray-900" />
|
|
338
|
+
</MobileNavTrigger>
|
|
339
|
+
<MobileNavTrigger value="workflow" label="Workflow">
|
|
340
|
+
<UtilityCompare className="size-6 text-gray-500 group-data-active:text-gray-900" />
|
|
341
|
+
</MobileNavTrigger>
|
|
342
|
+
<MobileNavTrigger value="edit" label="Edits">
|
|
343
|
+
<UtilityRevisionsShow className="size-6 text-gray-500 group-data-active:text-gray-900" />
|
|
344
|
+
</MobileNavTrigger>
|
|
345
|
+
<MobileNavTrigger value="comments" label="Comments" disabled>
|
|
346
|
+
<UtilityFeedback className="size-6 text-gray-500 group-data-active:text-gray-900" />
|
|
347
|
+
</MobileNavTrigger>
|
|
348
|
+
</MobileNavBar>
|
|
349
|
+
</MobileNav>
|
|
350
|
+
),
|
|
351
|
+
};
|