@claralight-design/abweb-navbar 0.1.2 → 0.1.3
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/NavHeader.module.css +21 -59
- package/NavHeader.tsx +12 -215
- package/package.json +1 -1
package/NavHeader.module.css
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
max-height: 72px;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
.header
|
|
20
|
+
.header>* {
|
|
21
21
|
corner-shape: unset;
|
|
22
22
|
}
|
|
23
23
|
|
|
@@ -95,13 +95,6 @@
|
|
|
95
95
|
flex: 0 1 auto;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
.track {
|
|
99
|
-
display: flex;
|
|
100
|
-
flex-direction: column;
|
|
101
|
-
will-change: transform;
|
|
102
|
-
transform: translate3d(0, 0, 0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
98
|
.slide {
|
|
106
99
|
min-height: 40px;
|
|
107
100
|
height: 40px;
|
|
@@ -111,6 +104,12 @@
|
|
|
111
104
|
gap: 10px;
|
|
112
105
|
}
|
|
113
106
|
|
|
107
|
+
.leftContent {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
min-width: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
114
113
|
.brandName {
|
|
115
114
|
color: var(--color-text);
|
|
116
115
|
font-size: 16px;
|
|
@@ -130,44 +129,18 @@
|
|
|
130
129
|
display: inline-flex;
|
|
131
130
|
align-items: center;
|
|
132
131
|
justify-content: center;
|
|
133
|
-
padding: 4px
|
|
132
|
+
padding: 4px 10px;
|
|
134
133
|
border-radius: 999px;
|
|
135
134
|
transition: transform 0.25s ease, opacity 0.25s ease;
|
|
136
135
|
color: inherit;
|
|
137
136
|
text-decoration: none;
|
|
138
137
|
white-space: nowrap;
|
|
139
|
-
min-height: 36px;
|
|
140
138
|
transition:
|
|
141
139
|
background 0.2s ease,
|
|
142
140
|
color 0.2s ease,
|
|
143
141
|
transform 0.15s ease;
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
.breadcrumb {
|
|
147
|
-
display: inline-flex;
|
|
148
|
-
align-items: center;
|
|
149
|
-
justify-content: center;
|
|
150
|
-
gap: 6px;
|
|
151
|
-
padding: 4px 0;
|
|
152
|
-
white-space: nowrap;
|
|
153
|
-
min-width: 0;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
.title {
|
|
157
|
-
margin: 0;
|
|
158
|
-
font-size: 18px;
|
|
159
|
-
line-height: 20px;
|
|
160
|
-
font-weight: 500;
|
|
161
|
-
letter-spacing: 0.01em;
|
|
162
|
-
color: var(--color-text);
|
|
163
|
-
margin-block-end: 0 !important;
|
|
164
|
-
white-space: nowrap;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
.slash {
|
|
168
|
-
opacity: 0.5;
|
|
169
|
-
font-size: 16px;
|
|
170
|
-
font-weight: 500;
|
|
142
|
+
corner-shape: unset;
|
|
143
|
+
min-height: 40px;
|
|
171
144
|
}
|
|
172
145
|
|
|
173
146
|
.iconButton {
|
|
@@ -222,8 +195,9 @@
|
|
|
222
195
|
}
|
|
223
196
|
|
|
224
197
|
.navLink {
|
|
225
|
-
|
|
226
|
-
|
|
198
|
+
corner-shape: unset;
|
|
199
|
+
min-height: 40px;
|
|
200
|
+
padding: 0 15px;
|
|
227
201
|
border: none;
|
|
228
202
|
border-radius: 999px;
|
|
229
203
|
background: transparent;
|
|
@@ -238,16 +212,18 @@
|
|
|
238
212
|
transform 0.15s ease;
|
|
239
213
|
white-space: nowrap;
|
|
240
214
|
font-family: inherit;
|
|
241
|
-
font-size:
|
|
242
|
-
font-weight:
|
|
215
|
+
font-size: 15px;
|
|
216
|
+
font-weight: 600;
|
|
243
217
|
}
|
|
244
218
|
|
|
245
|
-
.navLink:hover,
|
|
219
|
+
.navLink:hover,
|
|
220
|
+
.logotypeWrapper:hover {
|
|
246
221
|
background: color-mix(in srgb, var(--color-text) 8%, transparent);
|
|
247
222
|
color: var(--color-text);
|
|
248
223
|
}
|
|
249
224
|
|
|
250
|
-
.navLink:active,
|
|
225
|
+
.navLink:active,
|
|
226
|
+
.logotypeWrapper:active {
|
|
251
227
|
transform: scale(0.96);
|
|
252
228
|
}
|
|
253
229
|
|
|
@@ -264,7 +240,7 @@
|
|
|
264
240
|
}
|
|
265
241
|
|
|
266
242
|
.desktopLabel svg {
|
|
267
|
-
max-height:
|
|
243
|
+
max-height: 13px;
|
|
268
244
|
}
|
|
269
245
|
|
|
270
246
|
.mobileMenu {
|
|
@@ -408,11 +384,6 @@ max-height: 12px;
|
|
|
408
384
|
color: var(--color-text);
|
|
409
385
|
}
|
|
410
386
|
|
|
411
|
-
.left .slide,
|
|
412
|
-
.right .slide {
|
|
413
|
-
width: 40px;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
387
|
@media (min-width: 960px) {
|
|
417
388
|
.desktopNav {
|
|
418
389
|
display: flex;
|
|
@@ -432,15 +403,6 @@ max-height: 12px;
|
|
|
432
403
|
height: 36px;
|
|
433
404
|
}
|
|
434
405
|
|
|
435
|
-
.breadcrumb {
|
|
436
|
-
padding: 4px 0;
|
|
437
|
-
gap: 4px;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
.title {
|
|
441
|
-
font-size: 14px;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
406
|
.menuDrawerInner {
|
|
445
407
|
padding: 3rem 1.25rem 4.5rem;
|
|
446
408
|
}
|
|
@@ -459,8 +421,8 @@ max-height: 12px;
|
|
|
459
421
|
}
|
|
460
422
|
|
|
461
423
|
@media (prefers-reduced-motion: reduce) {
|
|
424
|
+
|
|
462
425
|
.inner,
|
|
463
|
-
.track,
|
|
464
426
|
.iconButton,
|
|
465
427
|
.logotypeWrapper,
|
|
466
428
|
.navLink,
|
package/NavHeader.tsx
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
import React, {
|
|
1
|
+
import React, { useState } from 'react'
|
|
2
2
|
import styles from './NavHeader.module.css'
|
|
3
|
-
import {
|
|
3
|
+
import { DotsNineIcon, XIcon } from '@phosphor-icons/react'
|
|
4
4
|
import { Drawer } from 'vaul'
|
|
5
5
|
import BlurEffect from 'react-progressive-blur'
|
|
6
6
|
|
|
7
7
|
export type NavHeaderLabels = {
|
|
8
8
|
menu: string
|
|
9
9
|
close: string
|
|
10
|
-
back: string
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export type NavHeaderItem = {
|
|
@@ -26,8 +25,6 @@ export type NavHeaderItem = {
|
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export type NavHeaderProps = {
|
|
29
|
-
pageTitle?: string
|
|
30
|
-
secPageTitle?: string
|
|
31
28
|
currentPath?: string
|
|
32
29
|
variant?: 'default' | 'docs' | (string & {})
|
|
33
30
|
showHeaderBlur?: boolean
|
|
@@ -40,17 +37,11 @@ export type NavHeaderProps = {
|
|
|
40
37
|
logoAriaLabel?: string
|
|
41
38
|
labels?: Partial<NavHeaderLabels>
|
|
42
39
|
className?: string
|
|
43
|
-
scrollTransitionDistance?: number
|
|
44
|
-
onBack?: () => void
|
|
45
40
|
}
|
|
46
41
|
|
|
47
|
-
type HeaderState = 'home' | 'level1' | 'level2'
|
|
48
|
-
type LeftState = 'slot' | 'back'
|
|
49
|
-
|
|
50
42
|
const DEFAULT_LABELS: NavHeaderLabels = {
|
|
51
43
|
menu: '菜单',
|
|
52
|
-
close: '关闭'
|
|
53
|
-
back: '返回'
|
|
44
|
+
close: '关闭'
|
|
54
45
|
}
|
|
55
46
|
|
|
56
47
|
const normalizePath = (path: string): string => {
|
|
@@ -75,40 +66,6 @@ const isMenuItemActive = (currentPath: string, itemPath: string): boolean => {
|
|
|
75
66
|
)
|
|
76
67
|
}
|
|
77
68
|
|
|
78
|
-
const useHeaderStates = (pageTitle?: string, secPageTitle?: string) => {
|
|
79
|
-
return useMemo<HeaderState[]>(() => {
|
|
80
|
-
if (!pageTitle && !secPageTitle) {
|
|
81
|
-
return ['home']
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (!pageTitle && secPageTitle) {
|
|
85
|
-
return ['home', 'level1']
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (pageTitle && !secPageTitle) {
|
|
89
|
-
return ['home', 'level1']
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
return ['level1', 'level2']
|
|
93
|
-
}, [pageTitle, secPageTitle])
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const useLeftStates = (leftSlot?: React.ReactNode, secPageTitle?: string) => {
|
|
97
|
-
return useMemo<LeftState[]>(() => {
|
|
98
|
-
const states: LeftState[] = []
|
|
99
|
-
|
|
100
|
-
if (leftSlot) {
|
|
101
|
-
states.push('slot')
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
if (secPageTitle) {
|
|
105
|
-
states.push('back')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return states
|
|
109
|
-
}, [leftSlot, secPageTitle])
|
|
110
|
-
}
|
|
111
|
-
|
|
112
69
|
const cx = (...classNames: Array<string | false | null | undefined>) =>
|
|
113
70
|
classNames.filter(Boolean).join(' ')
|
|
114
71
|
|
|
@@ -196,8 +153,6 @@ const renderMobileItem = (
|
|
|
196
153
|
}
|
|
197
154
|
|
|
198
155
|
const NavHeader: React.FC<NavHeaderProps> = ({
|
|
199
|
-
pageTitle,
|
|
200
|
-
secPageTitle,
|
|
201
156
|
currentPath = '/',
|
|
202
157
|
variant = 'default',
|
|
203
158
|
showHeaderBlur = true,
|
|
@@ -210,118 +165,13 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
210
165
|
logoAriaLabel,
|
|
211
166
|
labels,
|
|
212
167
|
className,
|
|
213
|
-
scrollTransitionDistance = 72,
|
|
214
|
-
onBack
|
|
215
168
|
}) => {
|
|
216
|
-
const leftTrackRef = useRef<HTMLDivElement>(null)
|
|
217
|
-
const centerTrackRef = useRef<HTMLDivElement>(null)
|
|
218
169
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
219
170
|
|
|
220
171
|
const resolvedLabels = { ...DEFAULT_LABELS, ...labels }
|
|
221
|
-
const states = useHeaderStates(pageTitle, secPageTitle)
|
|
222
|
-
const leftStates = useLeftStates(leftSlot, secPageTitle)
|
|
223
|
-
const hasTransition = states.length > 1
|
|
224
172
|
const hasNavItems = navItems.length > 0
|
|
225
173
|
const resolvedLogoAriaLabel = logoAriaLabel ?? brandName
|
|
226
174
|
|
|
227
|
-
useEffect(() => {
|
|
228
|
-
if (typeof window === 'undefined') return
|
|
229
|
-
|
|
230
|
-
const leftTrack = leftTrackRef.current
|
|
231
|
-
const centerTrack = centerTrackRef.current
|
|
232
|
-
const animatableTracks = [
|
|
233
|
-
leftStates.length > 1 ? leftTrack : null,
|
|
234
|
-
states.length > 1 ? centerTrack : null
|
|
235
|
-
].filter(Boolean) as HTMLDivElement[]
|
|
236
|
-
|
|
237
|
-
if (!animatableTracks.length) return
|
|
238
|
-
|
|
239
|
-
let slideHeight = 0
|
|
240
|
-
let centerOffset = 0
|
|
241
|
-
let leftOffset = 0
|
|
242
|
-
let rafId = 0
|
|
243
|
-
let nextProgress = 0
|
|
244
|
-
let lastProgress = -1
|
|
245
|
-
|
|
246
|
-
const refreshOffsets = () => {
|
|
247
|
-
const el =
|
|
248
|
-
centerTrack?.querySelector<HTMLElement>('[data-slide]') ??
|
|
249
|
-
leftTrack?.querySelector<HTMLElement>('[data-slide]')
|
|
250
|
-
slideHeight = el?.offsetHeight ?? 0
|
|
251
|
-
centerOffset = hasTransition ? -(states.length - 1) * slideHeight : 0
|
|
252
|
-
leftOffset =
|
|
253
|
-
leftStates.length > 1 ? -((leftStates.length - 1) * slideHeight) : 0
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const applyProgress = (progress: number) => {
|
|
257
|
-
if (leftTrack && leftStates.length > 1) {
|
|
258
|
-
leftTrack.style.transform = `translate3d(0, ${leftOffset * progress}px, 0)`
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
if (centerTrack && hasTransition) {
|
|
262
|
-
centerTrack.style.transform = `translate3d(0, ${centerOffset * progress}px, 0)`
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
const queueApply = (progress: number) => {
|
|
267
|
-
nextProgress = progress
|
|
268
|
-
if (rafId) return
|
|
269
|
-
|
|
270
|
-
rafId = window.requestAnimationFrame(() => {
|
|
271
|
-
rafId = 0
|
|
272
|
-
if (nextProgress === lastProgress) return
|
|
273
|
-
lastProgress = nextProgress
|
|
274
|
-
applyProgress(nextProgress)
|
|
275
|
-
})
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
const syncFromScroll = () => {
|
|
279
|
-
if (!slideHeight) refreshOffsets()
|
|
280
|
-
if (!slideHeight || (!hasTransition && leftStates.length <= 1)) return
|
|
281
|
-
const progress = Math.max(
|
|
282
|
-
0,
|
|
283
|
-
Math.min(1, window.scrollY / Math.max(scrollTransitionDistance, 1))
|
|
284
|
-
)
|
|
285
|
-
queueApply(progress)
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
refreshOffsets()
|
|
289
|
-
syncFromScroll()
|
|
290
|
-
|
|
291
|
-
window.addEventListener('scroll', syncFromScroll, { passive: true })
|
|
292
|
-
|
|
293
|
-
const ro = new ResizeObserver(() => {
|
|
294
|
-
refreshOffsets()
|
|
295
|
-
syncFromScroll()
|
|
296
|
-
})
|
|
297
|
-
|
|
298
|
-
animatableTracks.forEach((track) => ro.observe(track))
|
|
299
|
-
|
|
300
|
-
return () => {
|
|
301
|
-
if (rafId) window.cancelAnimationFrame(rafId)
|
|
302
|
-
window.removeEventListener('scroll', syncFromScroll)
|
|
303
|
-
ro.disconnect()
|
|
304
|
-
if (leftTrack) leftTrack.style.transform = ''
|
|
305
|
-
if (centerTrack) centerTrack.style.transform = ''
|
|
306
|
-
}
|
|
307
|
-
}, [hasTransition, leftStates.length, scrollTransitionDistance, states])
|
|
308
|
-
|
|
309
|
-
const handleBack = () => {
|
|
310
|
-
if (typeof window === 'undefined') return
|
|
311
|
-
|
|
312
|
-
if (onBack) {
|
|
313
|
-
onBack()
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
if (window.history.length > 1) {
|
|
318
|
-
window.history.back()
|
|
319
|
-
return
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
window.location.href = homeHref
|
|
323
|
-
}
|
|
324
|
-
|
|
325
175
|
const brandContent = logo ?? <span className={styles.brandName}>{brandName}</span>
|
|
326
176
|
|
|
327
177
|
return (
|
|
@@ -416,72 +266,19 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
416
266
|
</div>
|
|
417
267
|
)}
|
|
418
268
|
<div className={cx(styles.column, styles.left)}>
|
|
419
|
-
<div className={styles.
|
|
420
|
-
{leftStates.map((state, index) => (
|
|
421
|
-
<div
|
|
422
|
-
className={styles.slide}
|
|
423
|
-
data-slide
|
|
424
|
-
key={`left-${state}-${index}`}
|
|
425
|
-
>
|
|
426
|
-
{state === 'back' ? (
|
|
427
|
-
<button
|
|
428
|
-
type='button'
|
|
429
|
-
className={styles.iconButton}
|
|
430
|
-
onClick={handleBack}
|
|
431
|
-
aria-label={resolvedLabels.back}
|
|
432
|
-
>
|
|
433
|
-
<ArrowLeftIcon size={18} weight='bold' />
|
|
434
|
-
</button>
|
|
435
|
-
) : (
|
|
436
|
-
leftSlot
|
|
437
|
-
)}
|
|
438
|
-
</div>
|
|
439
|
-
))}
|
|
440
|
-
</div>
|
|
269
|
+
{leftSlot && <div className={styles.leftContent}>{leftSlot}</div>}
|
|
441
270
|
</div>
|
|
442
271
|
|
|
443
272
|
<div className={styles.centerColumn}>
|
|
444
273
|
<div className={styles.centerTrackWrapper}>
|
|
445
|
-
<div className={styles.
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
<a
|
|
454
|
-
className={styles.logotypeWrapper}
|
|
455
|
-
href={homeHref}
|
|
456
|
-
aria-label={resolvedLogoAriaLabel}
|
|
457
|
-
>
|
|
458
|
-
{brandContent}
|
|
459
|
-
</a>
|
|
460
|
-
)}
|
|
461
|
-
|
|
462
|
-
{state === 'level1' && (
|
|
463
|
-
<div className={styles.breadcrumb}>
|
|
464
|
-
<p className={styles.title}>
|
|
465
|
-
{pageTitle ?? secPageTitle}
|
|
466
|
-
</p>
|
|
467
|
-
</div>
|
|
468
|
-
)}
|
|
469
|
-
|
|
470
|
-
{state === 'level2' && (
|
|
471
|
-
<div className={styles.breadcrumb}>
|
|
472
|
-
{pageTitle && (
|
|
473
|
-
<>
|
|
474
|
-
<p className={styles.title}>
|
|
475
|
-
{pageTitle}
|
|
476
|
-
</p>
|
|
477
|
-
<span className={styles.slash}>/</span>
|
|
478
|
-
</>
|
|
479
|
-
)}
|
|
480
|
-
<p className={styles.title}>{secPageTitle}</p>
|
|
481
|
-
</div>
|
|
482
|
-
)}
|
|
483
|
-
</div>
|
|
484
|
-
))}
|
|
274
|
+
<div className={styles.slide}>
|
|
275
|
+
<a
|
|
276
|
+
className={styles.logotypeWrapper}
|
|
277
|
+
href={homeHref}
|
|
278
|
+
aria-label={resolvedLogoAriaLabel}
|
|
279
|
+
>
|
|
280
|
+
{brandContent}
|
|
281
|
+
</a>
|
|
485
282
|
</div>
|
|
486
283
|
</div>
|
|
487
284
|
|