@claralight-design/abweb-navbar 0.1.0 → 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 +42 -66
- package/NavHeader.tsx +13 -217
- package/package.json +1 -1
package/NavHeader.module.css
CHANGED
|
@@ -17,6 +17,10 @@
|
|
|
17
17
|
max-height: 72px;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
.header>* {
|
|
21
|
+
corner-shape: unset;
|
|
22
|
+
}
|
|
23
|
+
|
|
20
24
|
.inner {
|
|
21
25
|
width: calc(100% - 12px);
|
|
22
26
|
max-width: calc(1200px - 12px);
|
|
@@ -91,13 +95,6 @@
|
|
|
91
95
|
flex: 0 1 auto;
|
|
92
96
|
}
|
|
93
97
|
|
|
94
|
-
.track {
|
|
95
|
-
display: flex;
|
|
96
|
-
flex-direction: column;
|
|
97
|
-
will-change: transform;
|
|
98
|
-
transform: translate3d(0, 0, 0);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
98
|
.slide {
|
|
102
99
|
min-height: 40px;
|
|
103
100
|
height: 40px;
|
|
@@ -107,6 +104,12 @@
|
|
|
107
104
|
gap: 10px;
|
|
108
105
|
}
|
|
109
106
|
|
|
107
|
+
.leftContent {
|
|
108
|
+
display: inline-flex;
|
|
109
|
+
align-items: center;
|
|
110
|
+
min-width: 0;
|
|
111
|
+
}
|
|
112
|
+
|
|
110
113
|
.brandName {
|
|
111
114
|
color: var(--color-text);
|
|
112
115
|
font-size: 16px;
|
|
@@ -126,44 +129,18 @@
|
|
|
126
129
|
display: inline-flex;
|
|
127
130
|
align-items: center;
|
|
128
131
|
justify-content: center;
|
|
129
|
-
padding: 4px
|
|
130
|
-
border-radius:
|
|
132
|
+
padding: 4px 10px;
|
|
133
|
+
border-radius: 999px;
|
|
131
134
|
transition: transform 0.25s ease, opacity 0.25s ease;
|
|
132
135
|
color: inherit;
|
|
133
136
|
text-decoration: none;
|
|
134
137
|
white-space: nowrap;
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
.breadcrumb {
|
|
143
|
-
display: inline-flex;
|
|
144
|
-
align-items: center;
|
|
145
|
-
justify-content: center;
|
|
146
|
-
gap: 6px;
|
|
147
|
-
padding: 4px 0;
|
|
148
|
-
white-space: nowrap;
|
|
149
|
-
min-width: 0;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.title {
|
|
153
|
-
margin: 0;
|
|
154
|
-
font-size: 18px;
|
|
155
|
-
line-height: 20px;
|
|
156
|
-
font-weight: 500;
|
|
157
|
-
letter-spacing: 0.01em;
|
|
158
|
-
color: var(--color-text);
|
|
159
|
-
margin-block-end: 0 !important;
|
|
160
|
-
white-space: nowrap;
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
.slash {
|
|
164
|
-
opacity: 0.5;
|
|
165
|
-
font-size: 16px;
|
|
166
|
-
font-weight: 500;
|
|
138
|
+
transition:
|
|
139
|
+
background 0.2s ease,
|
|
140
|
+
color 0.2s ease,
|
|
141
|
+
transform 0.15s ease;
|
|
142
|
+
corner-shape: unset;
|
|
143
|
+
min-height: 40px;
|
|
167
144
|
}
|
|
168
145
|
|
|
169
146
|
.iconButton {
|
|
@@ -218,8 +195,9 @@
|
|
|
218
195
|
}
|
|
219
196
|
|
|
220
197
|
.navLink {
|
|
221
|
-
|
|
222
|
-
|
|
198
|
+
corner-shape: unset;
|
|
199
|
+
min-height: 40px;
|
|
200
|
+
padding: 0 15px;
|
|
223
201
|
border: none;
|
|
224
202
|
border-radius: 999px;
|
|
225
203
|
background: transparent;
|
|
@@ -228,23 +206,24 @@
|
|
|
228
206
|
align-items: center;
|
|
229
207
|
justify-content: center;
|
|
230
208
|
text-decoration: none;
|
|
231
|
-
cursor: pointer;
|
|
232
209
|
transition:
|
|
233
210
|
background 0.2s ease,
|
|
234
211
|
color 0.2s ease,
|
|
235
212
|
transform 0.15s ease;
|
|
236
213
|
white-space: nowrap;
|
|
237
214
|
font-family: inherit;
|
|
238
|
-
font-size:
|
|
239
|
-
font-weight:
|
|
215
|
+
font-size: 15px;
|
|
216
|
+
font-weight: 600;
|
|
240
217
|
}
|
|
241
218
|
|
|
242
|
-
.navLink:hover
|
|
219
|
+
.navLink:hover,
|
|
220
|
+
.logotypeWrapper:hover {
|
|
243
221
|
background: color-mix(in srgb, var(--color-text) 8%, transparent);
|
|
244
222
|
color: var(--color-text);
|
|
245
223
|
}
|
|
246
224
|
|
|
247
|
-
.navLink:active
|
|
225
|
+
.navLink:active,
|
|
226
|
+
.logotypeWrapper:active {
|
|
248
227
|
transform: scale(0.96);
|
|
249
228
|
}
|
|
250
229
|
|
|
@@ -257,10 +236,11 @@
|
|
|
257
236
|
display: inline-flex;
|
|
258
237
|
align-items: center;
|
|
259
238
|
min-width: 0;
|
|
239
|
+
font-weight: 600;
|
|
260
240
|
}
|
|
261
241
|
|
|
262
242
|
.desktopLabel svg {
|
|
263
|
-
max-height:
|
|
243
|
+
max-height: 13px;
|
|
264
244
|
}
|
|
265
245
|
|
|
266
246
|
.mobileMenu {
|
|
@@ -272,8 +252,8 @@ max-height: 12px;
|
|
|
272
252
|
inset: 0;
|
|
273
253
|
z-index: 1001;
|
|
274
254
|
background: rgba(0, 0, 0, 0.52);
|
|
275
|
-
-webkit-backdrop-filter: blur(
|
|
276
|
-
backdrop-filter: blur(
|
|
255
|
+
-webkit-backdrop-filter: blur(16px);
|
|
256
|
+
backdrop-filter: blur(16px);
|
|
277
257
|
}
|
|
278
258
|
|
|
279
259
|
.menuDrawerContent {
|
|
@@ -315,6 +295,8 @@ max-height: 12px;
|
|
|
315
295
|
.menuDrawerHeader {
|
|
316
296
|
width: 100%;
|
|
317
297
|
position: relative;
|
|
298
|
+
display: none;
|
|
299
|
+
visibility: hidden;
|
|
318
300
|
}
|
|
319
301
|
|
|
320
302
|
.menuTitle {
|
|
@@ -402,11 +384,6 @@ max-height: 12px;
|
|
|
402
384
|
color: var(--color-text);
|
|
403
385
|
}
|
|
404
386
|
|
|
405
|
-
.left .slide,
|
|
406
|
-
.right .slide {
|
|
407
|
-
width: 40px;
|
|
408
|
-
}
|
|
409
|
-
|
|
410
387
|
@media (min-width: 960px) {
|
|
411
388
|
.desktopNav {
|
|
412
389
|
display: flex;
|
|
@@ -426,15 +403,6 @@ max-height: 12px;
|
|
|
426
403
|
height: 36px;
|
|
427
404
|
}
|
|
428
405
|
|
|
429
|
-
.breadcrumb {
|
|
430
|
-
padding: 4px 0;
|
|
431
|
-
gap: 4px;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
.title {
|
|
435
|
-
font-size: 14px;
|
|
436
|
-
}
|
|
437
|
-
|
|
438
406
|
.menuDrawerInner {
|
|
439
407
|
padding: 3rem 1.25rem 4.5rem;
|
|
440
408
|
}
|
|
@@ -442,11 +410,19 @@ max-height: 12px;
|
|
|
442
410
|
.menuTitle {
|
|
443
411
|
font-size: 36px;
|
|
444
412
|
}
|
|
413
|
+
|
|
414
|
+
.logotypeWrapper {
|
|
415
|
+
margin: 0 auto;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.centerColumn {
|
|
419
|
+
justify-content: center;
|
|
420
|
+
}
|
|
445
421
|
}
|
|
446
422
|
|
|
447
423
|
@media (prefers-reduced-motion: reduce) {
|
|
424
|
+
|
|
448
425
|
.inner,
|
|
449
|
-
.track,
|
|
450
426
|
.iconButton,
|
|
451
427
|
.logotypeWrapper,
|
|
452
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 (
|
|
@@ -338,7 +188,6 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
338
188
|
open={menuOpen}
|
|
339
189
|
onOpenChange={setMenuOpen}
|
|
340
190
|
shouldScaleBackground
|
|
341
|
-
disablePreventScroll={false}
|
|
342
191
|
direction='top'
|
|
343
192
|
>
|
|
344
193
|
<Drawer.Trigger asChild>
|
|
@@ -347,7 +196,7 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
347
196
|
className={styles.iconButton}
|
|
348
197
|
aria-label={resolvedLabels.menu}
|
|
349
198
|
>
|
|
350
|
-
<
|
|
199
|
+
<DotsNineIcon size={18} weight='bold' />
|
|
351
200
|
</button>
|
|
352
201
|
</Drawer.Trigger>
|
|
353
202
|
|
|
@@ -417,72 +266,19 @@ const NavHeader: React.FC<NavHeaderProps> = ({
|
|
|
417
266
|
</div>
|
|
418
267
|
)}
|
|
419
268
|
<div className={cx(styles.column, styles.left)}>
|
|
420
|
-
<div className={styles.
|
|
421
|
-
{leftStates.map((state, index) => (
|
|
422
|
-
<div
|
|
423
|
-
className={styles.slide}
|
|
424
|
-
data-slide
|
|
425
|
-
key={`left-${state}-${index}`}
|
|
426
|
-
>
|
|
427
|
-
{state === 'back' ? (
|
|
428
|
-
<button
|
|
429
|
-
type='button'
|
|
430
|
-
className={styles.iconButton}
|
|
431
|
-
onClick={handleBack}
|
|
432
|
-
aria-label={resolvedLabels.back}
|
|
433
|
-
>
|
|
434
|
-
<ArrowLeftIcon size={18} weight='bold' />
|
|
435
|
-
</button>
|
|
436
|
-
) : (
|
|
437
|
-
leftSlot
|
|
438
|
-
)}
|
|
439
|
-
</div>
|
|
440
|
-
))}
|
|
441
|
-
</div>
|
|
269
|
+
{leftSlot && <div className={styles.leftContent}>{leftSlot}</div>}
|
|
442
270
|
</div>
|
|
443
271
|
|
|
444
272
|
<div className={styles.centerColumn}>
|
|
445
273
|
<div className={styles.centerTrackWrapper}>
|
|
446
|
-
<div className={styles.
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
<a
|
|
455
|
-
className={styles.logotypeWrapper}
|
|
456
|
-
href={homeHref}
|
|
457
|
-
aria-label={resolvedLogoAriaLabel}
|
|
458
|
-
>
|
|
459
|
-
{brandContent}
|
|
460
|
-
</a>
|
|
461
|
-
)}
|
|
462
|
-
|
|
463
|
-
{state === 'level1' && (
|
|
464
|
-
<div className={styles.breadcrumb}>
|
|
465
|
-
<p className={styles.title}>
|
|
466
|
-
{pageTitle ?? secPageTitle}
|
|
467
|
-
</p>
|
|
468
|
-
</div>
|
|
469
|
-
)}
|
|
470
|
-
|
|
471
|
-
{state === 'level2' && (
|
|
472
|
-
<div className={styles.breadcrumb}>
|
|
473
|
-
{pageTitle && (
|
|
474
|
-
<>
|
|
475
|
-
<p className={styles.title}>
|
|
476
|
-
{pageTitle}
|
|
477
|
-
</p>
|
|
478
|
-
<span className={styles.slash}>/</span>
|
|
479
|
-
</>
|
|
480
|
-
)}
|
|
481
|
-
<p className={styles.title}>{secPageTitle}</p>
|
|
482
|
-
</div>
|
|
483
|
-
)}
|
|
484
|
-
</div>
|
|
485
|
-
))}
|
|
274
|
+
<div className={styles.slide}>
|
|
275
|
+
<a
|
|
276
|
+
className={styles.logotypeWrapper}
|
|
277
|
+
href={homeHref}
|
|
278
|
+
aria-label={resolvedLogoAriaLabel}
|
|
279
|
+
>
|
|
280
|
+
{brandContent}
|
|
281
|
+
</a>
|
|
486
282
|
</div>
|
|
487
283
|
</div>
|
|
488
284
|
|