@claralight-design/abweb-navbar 0.1.2 → 0.1.4
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 +26 -73
- package/NavHeader.tsx +12 -220
- package/package.json +1 -1
package/NavHeader.module.css
CHANGED
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
max-height: 72px;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
.header
|
|
20
|
+
.header>* {
|
|
21
21
|
corner-shape: unset;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
.inner {
|
|
25
|
-
width: calc(100%
|
|
26
|
-
max-width: calc(1200px
|
|
25
|
+
width: calc(100% + 32px);
|
|
26
|
+
max-width: calc(1200px + 32px);
|
|
27
27
|
position: relative;
|
|
28
28
|
display: grid;
|
|
29
29
|
grid-template-columns: auto minmax(0, 1fr) auto;
|
|
@@ -70,17 +70,6 @@
|
|
|
70
70
|
order: 3;
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
.right {
|
|
74
|
-
gap: 8px;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.rightSlot {
|
|
78
|
-
display: inline-flex;
|
|
79
|
-
align-items: center;
|
|
80
|
-
justify-content: center;
|
|
81
|
-
min-width: 0;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
73
|
.centerColumn {
|
|
85
74
|
min-width: 0;
|
|
86
75
|
display: flex;
|
|
@@ -95,13 +84,6 @@
|
|
|
95
84
|
flex: 0 1 auto;
|
|
96
85
|
}
|
|
97
86
|
|
|
98
|
-
.track {
|
|
99
|
-
display: flex;
|
|
100
|
-
flex-direction: column;
|
|
101
|
-
will-change: transform;
|
|
102
|
-
transform: translate3d(0, 0, 0);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
87
|
.slide {
|
|
106
88
|
min-height: 40px;
|
|
107
89
|
height: 40px;
|
|
@@ -111,6 +93,12 @@
|
|
|
111
93
|
gap: 10px;
|
|
112
94
|
}
|
|
113
95
|
|
|
96
|
+
.leftContent {
|
|
97
|
+
display: inline-flex;
|
|
98
|
+
align-items: center;
|
|
99
|
+
min-width: 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
114
102
|
.brandName {
|
|
115
103
|
color: var(--color-text);
|
|
116
104
|
font-size: 16px;
|
|
@@ -130,44 +118,18 @@
|
|
|
130
118
|
display: inline-flex;
|
|
131
119
|
align-items: center;
|
|
132
120
|
justify-content: center;
|
|
133
|
-
padding: 4px
|
|
121
|
+
padding: 4px 10px;
|
|
134
122
|
border-radius: 999px;
|
|
135
123
|
transition: transform 0.25s ease, opacity 0.25s ease;
|
|
136
124
|
color: inherit;
|
|
137
125
|
text-decoration: none;
|
|
138
126
|
white-space: nowrap;
|
|
139
|
-
min-height: 36px;
|
|
140
127
|
transition:
|
|
141
128
|
background 0.2s ease,
|
|
142
129
|
color 0.2s ease,
|
|
143
130
|
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;
|
|
131
|
+
corner-shape: unset;
|
|
132
|
+
min-height: 40px;
|
|
171
133
|
}
|
|
172
134
|
|
|
173
135
|
.iconButton {
|
|
@@ -222,8 +184,9 @@
|
|
|
222
184
|
}
|
|
223
185
|
|
|
224
186
|
.navLink {
|
|
225
|
-
|
|
226
|
-
|
|
187
|
+
corner-shape: unset;
|
|
188
|
+
min-height: 40px;
|
|
189
|
+
padding: 0 15px;
|
|
227
190
|
border: none;
|
|
228
191
|
border-radius: 999px;
|
|
229
192
|
background: transparent;
|
|
@@ -238,16 +201,18 @@
|
|
|
238
201
|
transform 0.15s ease;
|
|
239
202
|
white-space: nowrap;
|
|
240
203
|
font-family: inherit;
|
|
241
|
-
font-size:
|
|
242
|
-
font-weight:
|
|
204
|
+
font-size: 15px;
|
|
205
|
+
font-weight: 600;
|
|
243
206
|
}
|
|
244
207
|
|
|
245
|
-
.navLink:hover,
|
|
208
|
+
.navLink:hover,
|
|
209
|
+
.logotypeWrapper:hover {
|
|
246
210
|
background: color-mix(in srgb, var(--color-text) 8%, transparent);
|
|
247
211
|
color: var(--color-text);
|
|
248
212
|
}
|
|
249
213
|
|
|
250
|
-
.navLink:active,
|
|
214
|
+
.navLink:active,
|
|
215
|
+
.logotypeWrapper:active {
|
|
251
216
|
transform: scale(0.96);
|
|
252
217
|
}
|
|
253
218
|
|
|
@@ -264,7 +229,7 @@
|
|
|
264
229
|
}
|
|
265
230
|
|
|
266
231
|
.desktopLabel svg {
|
|
267
|
-
max-height:
|
|
232
|
+
max-height: 13px;
|
|
268
233
|
}
|
|
269
234
|
|
|
270
235
|
.mobileMenu {
|
|
@@ -408,11 +373,6 @@ max-height: 12px;
|
|
|
408
373
|
color: var(--color-text);
|
|
409
374
|
}
|
|
410
375
|
|
|
411
|
-
.left .slide,
|
|
412
|
-
.right .slide {
|
|
413
|
-
width: 40px;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
376
|
@media (min-width: 960px) {
|
|
417
377
|
.desktopNav {
|
|
418
378
|
display: flex;
|
|
@@ -425,22 +385,14 @@ max-height: 12px;
|
|
|
425
385
|
|
|
426
386
|
@media (max-width: 959px) {
|
|
427
387
|
.inner {
|
|
428
|
-
|
|
388
|
+
display: flex;
|
|
389
|
+
width: calc(100% - 14px);
|
|
429
390
|
}
|
|
430
391
|
|
|
431
392
|
.slide {
|
|
432
393
|
height: 36px;
|
|
433
394
|
}
|
|
434
395
|
|
|
435
|
-
.breadcrumb {
|
|
436
|
-
padding: 4px 0;
|
|
437
|
-
gap: 4px;
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
.title {
|
|
441
|
-
font-size: 14px;
|
|
442
|
-
}
|
|
443
|
-
|
|
444
396
|
.menuDrawerInner {
|
|
445
397
|
padding: 3rem 1.25rem 4.5rem;
|
|
446
398
|
}
|
|
@@ -455,12 +407,13 @@ max-height: 12px;
|
|
|
455
407
|
|
|
456
408
|
.centerColumn {
|
|
457
409
|
justify-content: center;
|
|
410
|
+
width: 100%;
|
|
458
411
|
}
|
|
459
412
|
}
|
|
460
413
|
|
|
461
414
|
@media (prefers-reduced-motion: reduce) {
|
|
415
|
+
|
|
462
416
|
.inner,
|
|
463
|
-
.track,
|
|
464
417
|
.iconButton,
|
|
465
418
|
.logotypeWrapper,
|
|
466
419
|
.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,31 +25,22 @@ 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
|
|
34
31
|
navItems?: NavHeaderItem[]
|
|
35
32
|
leftSlot?: React.ReactNode
|
|
36
|
-
rightSlot?: React.ReactNode
|
|
37
33
|
logo?: React.ReactNode
|
|
38
34
|
brandName?: string
|
|
39
35
|
homeHref?: string
|
|
40
36
|
logoAriaLabel?: string
|
|
41
37
|
labels?: Partial<NavHeaderLabels>
|
|
42
38
|
className?: string
|
|
43
|
-
scrollTransitionDistance?: number
|
|
44
|
-
onBack?: () => void
|
|
45
39
|
}
|
|
46
40
|
|
|
47
|
-
type HeaderState = 'home' | 'level1' | 'level2'
|
|
48
|
-
type LeftState = 'slot' | 'back'
|
|
49
|
-
|
|
50
41
|
const DEFAULT_LABELS: NavHeaderLabels = {
|
|
51
42
|
menu: '菜单',
|
|
52
|
-
close: '关闭'
|
|
53
|
-
back: '返回'
|
|
43
|
+
close: '关闭'
|
|
54
44
|
}
|
|
55
45
|
|
|
56
46
|
const normalizePath = (path: string): string => {
|
|
@@ -75,40 +65,6 @@ const isMenuItemActive = (currentPath: string, itemPath: string): boolean => {
|
|
|
75
65
|
)
|
|
76
66
|
}
|
|
77
67
|
|
|
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
68
|
const cx = (...classNames: Array<string | false | null | undefined>) =>
|
|
113
69
|
classNames.filter(Boolean).join(' ')
|
|
114
70
|
|
|
@@ -196,132 +152,24 @@ const renderMobileItem = (
|
|
|
196
152
|
}
|
|
197
153
|
|
|
198
154
|
const NavHeader: React.FC<NavHeaderProps> = ({
|
|
199
|
-
pageTitle,
|
|
200
|
-
secPageTitle,
|
|
201
155
|
currentPath = '/',
|
|
202
156
|
variant = 'default',
|
|
203
157
|
showHeaderBlur = true,
|
|
204
158
|
navItems = [],
|
|
205
159
|
leftSlot,
|
|
206
|
-
rightSlot,
|
|
207
160
|
logo,
|
|
208
161
|
brandName = 'Brand',
|
|
209
162
|
homeHref = '/',
|
|
210
163
|
logoAriaLabel,
|
|
211
164
|
labels,
|
|
212
165
|
className,
|
|
213
|
-
scrollTransitionDistance = 72,
|
|
214
|
-
onBack
|
|
215
166
|
}) => {
|
|
216
|
-
const leftTrackRef = useRef<HTMLDivElement>(null)
|
|
217
|
-
const centerTrackRef = useRef<HTMLDivElement>(null)
|
|
218
167
|
const [menuOpen, setMenuOpen] = useState(false)
|
|
219
168
|
|
|
220
169
|
const resolvedLabels = { ...DEFAULT_LABELS, ...labels }
|
|
221
|
-
const states = useHeaderStates(pageTitle, secPageTitle)
|
|
222
|
-
const leftStates = useLeftStates(leftSlot, secPageTitle)
|
|
223
|
-
const hasTransition = states.length > 1
|
|
224
170
|
const hasNavItems = navItems.length > 0
|
|
225
171
|
const resolvedLogoAriaLabel = logoAriaLabel ?? brandName
|
|
226
172
|
|
|
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
173
|
const brandContent = logo ?? <span className={styles.brandName}>{brandName}</span>
|
|
326
174
|
|
|
327
175
|
return (
|
|
@@ -416,72 +264,19 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
416
264
|
</div>
|
|
417
265
|
)}
|
|
418
266
|
<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>
|
|
267
|
+
{leftSlot && <div className={styles.leftContent}>{leftSlot}</div>}
|
|
441
268
|
</div>
|
|
442
269
|
|
|
443
270
|
<div className={styles.centerColumn}>
|
|
444
271
|
<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
|
-
))}
|
|
272
|
+
<div className={styles.slide}>
|
|
273
|
+
<a
|
|
274
|
+
className={styles.logotypeWrapper}
|
|
275
|
+
href={homeHref}
|
|
276
|
+
aria-label={resolvedLogoAriaLabel}
|
|
277
|
+
>
|
|
278
|
+
{brandContent}
|
|
279
|
+
</a>
|
|
485
280
|
</div>
|
|
486
281
|
</div>
|
|
487
282
|
|
|
@@ -518,9 +313,6 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
518
313
|
</nav>
|
|
519
314
|
)}
|
|
520
315
|
</div>
|
|
521
|
-
<div className={cx(styles.column, styles.right)}>
|
|
522
|
-
{rightSlot && <div className={styles.rightSlot}>{rightSlot}</div>}
|
|
523
|
-
</div>
|
|
524
316
|
</header>
|
|
525
317
|
</div>
|
|
526
318
|
)
|