@syscore/ui-library 1.17.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 {
@@ -99,10 +100,8 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
99
100
  const heightMV = useMotionValue(0);
100
101
  const stopAnimation = useRef<(() => void) | null>(null);
101
102
 
102
- // Track which state the panel is in: "closed" | "initial" | "full"
103
+ // Panel state: "closed" | "initial" | "full"
103
104
  const stateRef = useRef<"closed" | "initial" | "full">("closed");
104
-
105
- // Cache the initial content height so returning from full → initial is consistent
106
105
  const cachedInitialHeight = useRef(0);
107
106
 
108
107
  const getNavBarHeight = () => {
@@ -111,15 +110,12 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
111
110
  };
112
111
 
113
112
  const getAvailableHeight = () => {
114
- const parent = panelRef.current?.parentElement;
115
- const container = parent?.clientHeight ?? window.innerHeight;
116
- return container - getNavBarHeight();
113
+ return window.innerHeight - getNavBarHeight();
117
114
  };
118
115
 
119
116
  const measureContentHeight = () => {
120
117
  const content = contentRef.current;
121
118
  if (!content) return 0;
122
- // Measure the first child to get true content height without triggering reflow
123
119
  const child = content.firstElementChild as HTMLElement | null;
124
120
  const raw = (child?.offsetHeight ?? content.scrollHeight) + 40; // +40 for handle area
125
121
  const max = getAvailableHeight() * 0.7;
@@ -136,78 +132,111 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
136
132
  };
137
133
 
138
134
  // Animate open/close
139
- useEffect(() => {
140
- let cancelled = false;
135
+ useLayoutEffect(() => {
141
136
  if (open) {
142
137
  stateRef.current = "initial";
143
- requestAnimationFrame(() => {
144
- if (cancelled) return;
145
- const h = measureContentHeight();
146
- cachedInitialHeight.current = h;
147
- springTo(h);
148
- });
138
+ const h = measureContentHeight();
139
+ cachedInitialHeight.current = h;
140
+ springTo(h);
149
141
  } else {
150
142
  stateRef.current = "closed";
151
143
  cachedInitialHeight.current = 0;
152
144
  springTo(0);
153
145
  }
154
- return () => {
155
- cancelled = true;
156
- };
157
146
  }, [open]);
158
147
 
159
- // Re-measure when content changes (e.g. switching tabs)
160
- useEffect(() => {
148
+ // Re-measure when switching tabs while panel is already open
149
+ useLayoutEffect(() => {
161
150
  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
- };
151
+ const h = measureContentHeight();
152
+ cachedInitialHeight.current = h;
153
+ if (stateRef.current !== "full") {
154
+ stateRef.current = "initial";
155
+ springTo(h);
156
+ }
178
157
  }, [activeKey]);
179
158
 
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;
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();
185
184
  };
186
185
 
187
- const handlePan = (_: unknown, info: PanInfo) => {
188
- // Continuously track the latest direction during drag
189
- 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));
190
193
  };
191
194
 
192
- const handlePanEnd = () => {
193
- const dir = dragDirection.current;
194
- if (!dir) return;
195
+ const handlePointerUp = (e: React.PointerEvent) => {
196
+ if (!isDragging.current) return;
197
+ isDragging.current = false;
195
198
 
196
- if (dir === "up") {
197
- if (stateRef.current === "initial") {
198
- stateRef.current = "full";
199
- springTo(getAvailableHeight());
200
- }
201
- } else {
202
- // 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
203
212
  if (stateRef.current === "full") {
204
213
  stateRef.current = "initial";
205
- springTo(cachedInitialHeight.current);
206
- } else if (stateRef.current === "initial") {
214
+ springTo(initial);
215
+ } else {
207
216
  stateRef.current = "closed";
208
217
  springTo(0);
209
218
  close();
210
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
+ }
211
240
  }
212
241
  };
213
242
 
@@ -222,14 +251,15 @@ const MobileNavPanel = ({ className, children }: MobileNavPanelProps) => {
222
251
  data-closed={!open || undefined}
223
252
  style={{ height: heightMV }}
224
253
  >
225
- <motion.div
254
+ <div
226
255
  className="mobile-nav-handle"
227
- onPanStart={handlePanStart}
228
- onPan={handlePan}
229
- onPanEnd={handlePanEnd}
256
+ onPointerDown={handlePointerDown}
257
+ onPointerMove={handlePointerMove}
258
+ onPointerUp={handlePointerUp}
259
+ onPointerCancel={handlePointerUp}
230
260
  >
231
261
  <div className="mobile-nav-handle-bar" />
232
- </motion.div>
262
+ </div>
233
263
 
234
264
  <div ref={contentRef} className="mobile-nav-content">
235
265
  {activeKey ? children(activeKey) : null}
@@ -269,6 +299,7 @@ const MobileNavTrigger = ({
269
299
  onClick={handleClick}
270
300
  disabled={disabled}
271
301
  data-active={activeKey === value || undefined}
302
+ aria-expanded={activeKey === value && open}
272
303
  className={cn("mobile-nav-trigger group", className)}
273
304
  aria-label={label}
274
305
  >
package/client/global.css CHANGED
@@ -3852,21 +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
- position: relative;
3858
3862
  }
3859
3863
 
3860
3864
  .mobile-nav-overlay {
3861
- position: absolute;
3865
+ position: fixed;
3862
3866
  inset: 0;
3863
3867
  background-color: rgba(0, 0, 0, 0.08);
3864
- z-index: 10;
3868
+ z-index: 40;
3865
3869
  }
3866
3870
 
3867
3871
  .mobile-nav-panel {
3868
3872
  position: relative;
3869
- z-index: 20;
3873
+ z-index: 50;
3870
3874
  background-color: white;
3871
3875
  border-radius: 32px 32px 0 0;
3872
3876
  box-shadow:
@@ -3906,7 +3910,7 @@ body {
3906
3910
 
3907
3911
  .mobile-nav-bar {
3908
3912
  position: relative;
3909
- z-index: 20;
3913
+ z-index: 50;
3910
3914
  border-top: 1px solid var(--color-gray-100);
3911
3915
  background-color: var(--color-gray-50);
3912
3916
  flex-shrink: 0;
@@ -3920,7 +3924,7 @@ body {
3920
3924
  align-items: center;
3921
3925
  justify-content: space-around;
3922
3926
  padding-top: 0.5rem;
3923
- padding-bottom: 2rem;
3927
+ padding-bottom: 0.5rem;
3924
3928
  padding-left: 1.5rem;
3925
3929
  padding-right: 1.5rem;
3926
3930
  }
@@ -6992,6 +6996,38 @@ body {
6992
6996
  text-underline-offset: 2px;
6993
6997
  }
6994
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
+
6995
7031
  /* Standard Table Styles */
6996
7032
  .standard-table {
6997
7033
  border: 1px solid var(--color-blue-200, #cbe0f1);
@@ -7105,6 +7141,14 @@ body {
7105
7141
  gap: 1rem;
7106
7142
  }
7107
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
+
7108
7152
  /* PageHeader Styles */
7109
7153
  .page-header {
7110
7154
  margin-bottom: 1.5rem;
@@ -7142,6 +7186,12 @@ body {
7142
7186
  padding-right: 10rem;
7143
7187
  }
7144
7188
 
7189
+ @media (max-width: 640px) {
7190
+ .page-header-description {
7191
+ padding-right: 0;
7192
+ }
7193
+ }
7194
+
7145
7195
  /* Navigation Styles */
7146
7196
  .navigation {
7147
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
- <UtilityFeedback 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
 
@@ -320,32 +340,34 @@ export const WithDisabledTrigger: Story = {
320
340
  args: { children: null },
321
341
 
322
342
  render: () => (
323
- <MobileNav className="h-screen w-full bg-gray-50">
324
- <div className="flex-1 p-6 overflow-y-auto">
343
+ <div className="h-screen w-full bg-white">
344
+ <div className="p-6">
325
345
  <p className="text-sm text-gray-400">
326
346
  The &ldquo;Comments&rdquo; trigger is disabled because there are no
327
347
  comments.
328
348
  </p>
329
349
  </div>
330
350
 
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>
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>
350
372
  ),
351
373
  };