@sofidevo/astro-dynamic-header 2.0.1 → 3.0.1
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/README.md +177 -74
- package/package.json +3 -3
- package/src/HamburgerButton.astro +19 -14
- package/src/Header.astro +87 -110
- package/src/MobileNav.astro +120 -64
- package/src/NavMenu.astro +37 -6
- package/src/defaults.ts +36 -0
- package/src/index.ts +136 -14
package/README.md
CHANGED
|
@@ -3,7 +3,13 @@
|
|
|
3
3
|
A dynamic, responsive header component for Astro projects that can switch between floating and fullscreen styles with multi-level dropdown navigation support.
|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
|
-
> **Breaking Changes
|
|
6
|
+
> **Breaking Changes — v2.2+**: The `logo` prop has been removed in favor of a much more flexible `logo` slot. The `color` prop has also been removed from the `HamburgerButton` component to ensure flawless dark mode synchronization. The `theme` property behavior was also refactored to prioritize native CSS variables, reducing DOM bloat and enabling purely CSS-based customization. See the [Changelog](#changelog) section below.
|
|
7
|
+
|
|
8
|
+
> [!WARNING]
|
|
9
|
+
> **Breaking Changes — v2.0+**: The `logo` and `navigation` props were restructured from strings/arrays to configuration objects. See the [Component Props](#component-props) and [Comprehensive Example](#comprehensive-example) to migrate.
|
|
10
|
+
|
|
11
|
+
> [!NOTE]
|
|
12
|
+
> **What's new — v2.1**: `CustomClassNames` has been renamed `HeaderClassNames` (the old name still works as an alias). New nested class props added to `LogoConfig` and `NavConfig`. New `mobileNav` slot in `HeaderClassNames`. `defaultThemes` is now exported for use outside the component. See the [Changelog](#changelog) section below.
|
|
7
13
|
|
|
8
14
|
## Features
|
|
9
15
|
|
|
@@ -12,7 +18,8 @@ A dynamic, responsive header component for Astro projects that can switch betwee
|
|
|
12
18
|
- **Multi-level Dropdowns**: Support for nested navigation menus
|
|
13
19
|
- **Slot Support**: Customizable slots for desktop header and mobile panel content
|
|
14
20
|
- **TypeScript Support**: Full type safety and IntelliSense
|
|
15
|
-
- **
|
|
21
|
+
- **Two-layer customization**: High-level `classNames` prop + fine-grained nested class props inside `navigation` and `logo`
|
|
22
|
+
- **Exportable defaults**: Import and extend `defaultThemes` from your own code
|
|
16
23
|
- **Astro Optimized**: Built specifically for Astro framework
|
|
17
24
|
|
|
18
25
|
### Live demo
|
|
@@ -45,11 +52,9 @@ By default, the header uses `preset="auto"`, which automatically detects the the
|
|
|
45
52
|
---
|
|
46
53
|
import Header from '@sofidevo/astro-dynamic-header/Header';
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
]
|
|
52
|
-
|
|
55
|
+
const menuItems = [
|
|
56
|
+
{ link: '/about', text: 'About' },
|
|
57
|
+
];
|
|
53
58
|
---
|
|
54
59
|
|
|
55
60
|
<!-- Detects .dark class on root automatically -->
|
|
@@ -92,14 +97,13 @@ const theme = {
|
|
|
92
97
|
|
|
93
98
|
### Header Component
|
|
94
99
|
|
|
95
|
-
| Prop | Type | Default | Description
|
|
96
|
-
| ------------ | ----------------------------- | ------------ |
|
|
97
|
-
| `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Header layout style
|
|
98
|
-
| `preset` | `"light" \| "dark" \| "auto"` | `"auto"` | Theme behavior. `auto` follows root class.
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `classNames` | `CustomClassNames` | `{}` | Custom class names for CSS Modules |
|
|
100
|
+
| Prop | Type | Default | Description |
|
|
101
|
+
| ------------ | ----------------------------- | ------------ | ---------------------------------------------- |
|
|
102
|
+
| `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Header layout style |
|
|
103
|
+
| `preset` | `"light" \| "dark" \| "auto"` | `"auto"` | Theme behavior. `auto` follows root class. |
|
|
104
|
+
| `navigation` | `NavConfig` | `{}` | Navigation configuration object |
|
|
105
|
+
| `theme` | `DualThemeConfig` | `{}` | Custom theme overrides for light/dark |
|
|
106
|
+
| `classNames` | `HeaderClassNames` | `{}` | High-level class overrides for layout elements |
|
|
103
107
|
|
|
104
108
|
### Config Objects
|
|
105
109
|
|
|
@@ -135,33 +139,61 @@ const theme = {
|
|
|
135
139
|
}} />
|
|
136
140
|
```
|
|
137
141
|
|
|
138
|
-
#### CustomClassNames
|
|
142
|
+
#### CustomClassNames → HeaderClassNames
|
|
143
|
+
|
|
144
|
+
> [!NOTE]
|
|
145
|
+
> `CustomClassNames` was renamed to `HeaderClassNames` in v2.1. The old name still works as a type alias — no migration required.
|
|
146
|
+
|
|
147
|
+
The `classNames` prop targets the **structural wrapper elements** of the Header. For fine-grained control of individual nav links or the logo internals, use the nested `xxx__class` props inside the `navigation` or `logo` config objects instead.
|
|
148
|
+
|
|
149
|
+
| Property | Target Element | Purpose & Common Use Cases |
|
|
150
|
+
| ----------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
|
|
151
|
+
| `container` | Outer `div` wrapping the header | **Positioning & Layout**: `top-0`, `z-50`, `fixed`, adjusting `max-width` and `mx-auto` logic. |
|
|
152
|
+
| `header` | Inner `<header>` element | **Appearance**: Shadows (`shadow-md`), borders (`border-b`), or custom transition durations. |
|
|
153
|
+
| `logo` | `<a>` tag surrounding the logo | **Interactions**: Hover states, custom focus rings, or adjusting the flex alignment of the logo group. |
|
|
154
|
+
| `logoText` | `<span>` tag containing the logo text | **Typography**: Font weights, text shadows, or specific tracking/leading classes. |
|
|
155
|
+
| `nav` | `<div>` wrapping the desktop navigation items | **Desktop Layout**: Spacing between the logo and the menu, or responsive visibility classes (`hidden md:flex`). |
|
|
156
|
+
| `mobileNav` | Root `<nav>` of the mobile slide-in panel | **Mobile Panel**: Extra backdrop blur, custom z-index, slide-in overrides. |
|
|
157
|
+
|
|
158
|
+
##### Usage Examples
|
|
159
|
+
|
|
160
|
+
**Premium Shadow & Border:**
|
|
161
|
+
|
|
162
|
+
```astro
|
|
163
|
+
<Header
|
|
164
|
+
classNames={{
|
|
165
|
+
header: "shadow-xl border-b border-black/5 dark:border-white/10 transition-all duration-500",
|
|
166
|
+
container: "top-4 px-6",
|
|
167
|
+
mobileNav: "backdrop-blur-md",
|
|
168
|
+
}}
|
|
169
|
+
/>
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
**Custom Typography & Nav Spacing:**
|
|
173
|
+
|
|
174
|
+
```astro
|
|
175
|
+
<Header
|
|
176
|
+
classNames={{
|
|
177
|
+
logoText: "tracking-tighter font-black italic uppercase",
|
|
178
|
+
nav: "ml-auto gap-8",
|
|
179
|
+
}}
|
|
180
|
+
/>
|
|
181
|
+
```
|
|
139
182
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
| `container` | `string` | Main container class |
|
|
143
|
-
| `header` | `string` | Header element class |
|
|
144
|
-
| `logo` | `string` | Logo link container class |
|
|
145
|
-
| `logoText` | `string` | Logo text class |
|
|
146
|
-
| `nav` | `string` | Desktop navigation wrapper class |
|
|
183
|
+
> [!TIP]
|
|
184
|
+
> Since these classes are injected using Astro's `class:list`, you can also pass objects or arrays if you need conditional logic for your custom classes.
|
|
147
185
|
|
|
148
|
-
#### LogoConfig
|
|
149
186
|
|
|
150
|
-
| Property | Type | Description |
|
|
151
|
-
| ----------- | -------- | ------------------------------------------------ |
|
|
152
|
-
| `src` | `string` | URL of the logo image |
|
|
153
|
-
| `alt` | `string` | Alternative text for the logo image |
|
|
154
|
-
| `width` | `string` | Width of the logo (e.g., "50px", "5rem") |
|
|
155
|
-
| `text` | `string` | Text to display next to or instead of logo image |
|
|
156
|
-
| `textSize` | `string` | Font size for the logo text |
|
|
157
|
-
| `textColor` | `string` | Color for the logo text |
|
|
158
187
|
|
|
159
188
|
#### NavConfig
|
|
160
189
|
|
|
161
|
-
| Property
|
|
162
|
-
|
|
|
163
|
-
| `homeUrl`
|
|
164
|
-
| `menuItems`
|
|
190
|
+
| Property | Type | Description |
|
|
191
|
+
| --------------------- | ------------ | ------------------------------------------------------- |
|
|
192
|
+
| `homeUrl` | `string` | URL for the home link (defaults to `/`) |
|
|
193
|
+
| `menuItems` | `MenuItem[]` | Array of navigation menu items |
|
|
194
|
+
| `header__menu__class` | `string` | Extra CSS class(es) for the desktop `<nav>` element |
|
|
195
|
+
| `header__item__class` | `string` | Extra CSS class(es) for each top-level `<li>` menu item |
|
|
196
|
+
| `menu__link__class` | `string` | Extra CSS class(es) for each top-level `<a>` link |
|
|
165
197
|
|
|
166
198
|
#### MenuItem
|
|
167
199
|
|
|
@@ -179,6 +211,7 @@ The Header component provides a flexible slot system that allows you to add addi
|
|
|
179
211
|
|
|
180
212
|
| Slot Name | Location | Visibility | Description |
|
|
181
213
|
| --------- | --------------------- | --------------------- | ---------------------------------------- |
|
|
214
|
+
| `logo` | Header Desktop/Mobile | Always | Add your custom logo HTML/Components |
|
|
182
215
|
| `actions` | Header & Mobile panel | Responsive visibility | Add action buttons (login, signup, etc.) |
|
|
183
216
|
|
|
184
217
|
### Example with Slots
|
|
@@ -229,32 +262,38 @@ const menuItems = [
|
|
|
229
262
|
{ link: "/contact", text: "Contact" },
|
|
230
263
|
];
|
|
231
264
|
|
|
232
|
-
|
|
233
|
-
light: {
|
|
234
|
-
accentColor: "#ff0000",
|
|
235
|
-
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
236
|
-
},
|
|
237
|
-
dark: {
|
|
238
|
-
accentColor: "#00ffff",
|
|
239
|
-
backgroundColor: "rgba(20, 20, 20, 0.9)",
|
|
240
|
-
},
|
|
241
|
-
};
|
|
265
|
+
];
|
|
242
266
|
---
|
|
243
267
|
|
|
268
|
+
<style is:inline>
|
|
269
|
+
:root {
|
|
270
|
+
/* Pure CSS Theming Configuration */
|
|
271
|
+
--l-accent: #ff0000;
|
|
272
|
+
--l-bg: rgba(255, 255, 255, 0.8);
|
|
273
|
+
|
|
274
|
+
--d-accent: #00ffff;
|
|
275
|
+
--d-bg: rgba(20, 20, 20, 0.9);
|
|
276
|
+
}
|
|
277
|
+
</style>
|
|
278
|
+
|
|
244
279
|
<Header
|
|
245
280
|
headerType="floating"
|
|
246
281
|
preset="dark"
|
|
247
|
-
logo={{
|
|
248
|
-
src: "https://itssofi.dev/img/icons/sofi-icon.webp",
|
|
249
|
-
alt: "My Site Logo",
|
|
250
|
-
width: "44px",
|
|
251
|
-
}}
|
|
252
282
|
navigation={{
|
|
253
283
|
homeUrl: "/",
|
|
254
284
|
menuItems: menuItems,
|
|
285
|
+
header__menu__class: "flex gap-6",
|
|
286
|
+
menu__link__class: "font-medium",
|
|
287
|
+
}}
|
|
288
|
+
classNames={{
|
|
289
|
+
header: "shadow-xl",
|
|
290
|
+
mobileNav: "backdrop-blur-md",
|
|
255
291
|
}}
|
|
256
|
-
theme={theme}
|
|
257
292
|
>
|
|
293
|
+
<a slot="logo" href="/" style="display: flex; align-items: center; gap: 10px; color: inherit; text-decoration: none;">
|
|
294
|
+
<img src="https://sofidev.blog/img/branding/logo.webp" alt="My Site Logo" width="44" />
|
|
295
|
+
<span style="font-weight: bold; font-size: 1.2rem;">SofiDev</span>
|
|
296
|
+
</a>
|
|
258
297
|
<button slot="actions">Login</button>
|
|
259
298
|
</Header>
|
|
260
299
|
```
|
|
@@ -294,9 +333,11 @@ The package provides full TypeScript support. You can import types to ensure you
|
|
|
294
333
|
```astro
|
|
295
334
|
---
|
|
296
335
|
import Header from '@sofidevo/astro-dynamic-header/Header';
|
|
336
|
+
import { defaultThemes } from '@sofidevo/astro-dynamic-header';
|
|
297
337
|
import type {
|
|
298
338
|
NavConfig,
|
|
299
339
|
DualThemeConfig,
|
|
340
|
+
HeaderClassNames,
|
|
300
341
|
MenuItem,
|
|
301
342
|
SecondaryMenuItem
|
|
302
343
|
} from '@sofidevo/astro-dynamic-header';
|
|
@@ -311,42 +352,43 @@ const navigation: NavConfig = {
|
|
|
311
352
|
{ link: '/hardware', text: 'Hardware' }
|
|
312
353
|
]
|
|
313
354
|
}
|
|
314
|
-
]
|
|
355
|
+
],
|
|
356
|
+
header__menu__class: "flex gap-6",
|
|
315
357
|
};
|
|
316
358
|
|
|
359
|
+
// Prefer CSS custom properties globally rather than passing the `theme` object.
|
|
360
|
+
// But you can still use the theme prop if needed:
|
|
317
361
|
const theme: DualThemeConfig = {
|
|
318
|
-
light: {
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
backgroundColor: "rgba(10, 10, 10, 0.9)",
|
|
326
|
-
backgroundColorOpaque: "#0a0a0a"
|
|
327
|
-
}
|
|
362
|
+
light: { ...defaultThemes.light, accentColor: "#3e1c71" },
|
|
363
|
+
dark: { ...defaultThemes.dark, accentColor: "#00ffff", backgroundColor: "rgba(10, 10, 10, 0.9)" },
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
const classNames: HeaderClassNames = {
|
|
367
|
+
header: "shadow-xl",
|
|
368
|
+
mobileNav: "backdrop-blur-md",
|
|
328
369
|
};
|
|
329
370
|
---
|
|
330
371
|
|
|
331
372
|
<Header
|
|
332
373
|
navigation={navigation}
|
|
333
|
-
|
|
374
|
+
classNames={classNames}
|
|
334
375
|
preset="auto"
|
|
335
376
|
/>
|
|
336
377
|
```
|
|
337
378
|
|
|
338
379
|
### Available Types
|
|
339
380
|
|
|
340
|
-
| Type | Description
|
|
341
|
-
| ------------------- |
|
|
342
|
-
| `MenuItem` | Top-level menu item with optional properties
|
|
343
|
-
| `SecondaryMenuItem` | Second-level menu item
|
|
344
|
-
| `TertiaryMenuItem` | Third-level menu item
|
|
345
|
-
| `NavConfig` |
|
|
346
|
-
| `ThemeConfig` | Individual theme settings (colors, blur, etc.)
|
|
347
|
-
| `DualThemeConfig` | Combined settings for light and dark modes
|
|
348
|
-
| `
|
|
349
|
-
| `
|
|
381
|
+
| Type | Description |
|
|
382
|
+
| ------------------- | ----------------------------------------------- |
|
|
383
|
+
| `MenuItem` | Top-level menu item with optional properties |
|
|
384
|
+
| `SecondaryMenuItem` | Second-level menu item |
|
|
385
|
+
| `TertiaryMenuItem` | Third-level menu item |
|
|
386
|
+
| `NavConfig` | Navigation config (items + nested class props) |
|
|
387
|
+
| `ThemeConfig` | Individual theme settings (colors, blur, etc.) |
|
|
388
|
+
| `DualThemeConfig` | Combined settings for light and dark modes |
|
|
389
|
+
| `HeaderClassNames` | Class overrides for structural wrapper elements |
|
|
390
|
+
| `CustomClassNames` | **Deprecated alias** for `HeaderClassNames` |
|
|
391
|
+
| `HeaderProps` | Main props for the Header component |
|
|
350
392
|
|
|
351
393
|
## Browser Support
|
|
352
394
|
|
|
@@ -400,3 +442,64 @@ MIT License - see the [LICENSE](./LICENSE) file for details.
|
|
|
400
442
|
## Support
|
|
401
443
|
|
|
402
444
|
If you find this package helpful, please consider giving it a star on GitHub!
|
|
445
|
+
|
|
446
|
+
---
|
|
447
|
+
|
|
448
|
+
## Changelog
|
|
449
|
+
|
|
450
|
+
### v2.2 — Performance & DX Optimization
|
|
451
|
+
|
|
452
|
+
#### Breaking changes
|
|
453
|
+
|
|
454
|
+
- **Removed `logo` Object Configuration prop**: The `logo` property and `LogoConfig` interface have been removed. You should now use `<slot name="logo" />` to render your logo exactly as you need, passing native HTML or Astro components.
|
|
455
|
+
- **Removed `color` prop from `HamburgerButton`**: The `HamburgerButton` now inherits `--text-color` natively via CSS, which fixes a bug where the button would not properly update its color when switching to dark mode. Any direct uses of `<HamburgerButton color="..." />` will fail to compile in TypeScript and should be updated to rely on global CSS variables or context inheritance.
|
|
456
|
+
|
|
457
|
+
#### New features and enhancements
|
|
458
|
+
|
|
459
|
+
- **Native CSS Variable Theming (Better DX)**: Redesigned how default styles are injected into the DOM. `Header.astro` no longer injects all default properties into `style={...}` attributes on load, solving the "DOM bloat" issue. You can now deeply customize the component purely by setting CSS variables like `--l-bg`, `--d-bg`, `--l-accent`, and `--d-accent` inside your `:root` style tag, removing the necessity to parse massive `theme` JS objects.
|
|
460
|
+
- **Dead Code Elimination**: Cleaned up repetitive prop destructuring inside `MobileNav.astro`.
|
|
461
|
+
|
|
462
|
+
### v2.1 — Style & Customization Refactor
|
|
463
|
+
|
|
464
|
+
#### New features
|
|
465
|
+
|
|
466
|
+
- **`HeaderClassNames`** replaces `CustomClassNames` (alias kept — no migration required).
|
|
467
|
+
New `mobileNav` slot targets the mobile slide-in `<nav>` panel.
|
|
468
|
+
|
|
469
|
+
- **Nested class props** on `LogoConfig`:
|
|
470
|
+
| Prop | Targets |
|
|
471
|
+
|------|---------|
|
|
472
|
+
| `logo__container__class` | Logo `<a>` wrapper |
|
|
473
|
+
| `logo__text__class` | Logo text `<span>` |
|
|
474
|
+
|
|
475
|
+
- **Nested class props** on `NavConfig`:
|
|
476
|
+
| Prop | Targets |
|
|
477
|
+
|------|---------|
|
|
478
|
+
| `header__menu__class` | Desktop `<nav>` element |
|
|
479
|
+
| `header__item__class` | Each top-level `<li>` |
|
|
480
|
+
| `menu__link__class` | Each top-level `<a>` |
|
|
481
|
+
|
|
482
|
+
- **`defaultThemes` exported** — import and spread the built-in tokens to extend them:
|
|
483
|
+
```ts
|
|
484
|
+
import { defaultThemes } from "@sofidevo/astro-dynamic-header";
|
|
485
|
+
const theme = { dark: { ...defaultThemes.dark, accentColor: "#f43f5e" } };
|
|
486
|
+
```
|
|
487
|
+
|
|
488
|
+
#### Internal improvements
|
|
489
|
+
|
|
490
|
+
- Default theme tokens extracted to `src/defaults.ts` — easier to read and maintain.
|
|
491
|
+
- `HamburgerButton` color is now explicitly wired from the resolved theme (no visual change).
|
|
492
|
+
|
|
493
|
+
#### Deprecations
|
|
494
|
+
|
|
495
|
+
- `CustomClassNames` — use `HeaderClassNames` instead. The alias will remain until the next major version.
|
|
496
|
+
|
|
497
|
+
### v2.0 — Object-based Configuration API
|
|
498
|
+
|
|
499
|
+
> [!WARNING]
|
|
500
|
+
> Breaking change: `logo` changed from `string` to `LogoConfig`, `navigation` changed from `MenuItem[]` to `NavConfig`.
|
|
501
|
+
|
|
502
|
+
- `logo` prop restructured to `LogoConfig` object (`src`, `alt`, `width`, `text`, `textSize`, `textColor`).
|
|
503
|
+
- `navigation` prop restructured to `NavConfig` object (`homeUrl`, `menuItems`).
|
|
504
|
+
- `classNames` prop introduced for CSS class injection (`CustomClassNames`).
|
|
505
|
+
- Dual-theme support via `DualThemeConfig` (`light` + `dark`).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sofidevo/astro-dynamic-header",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A dynamic Astro header component that switches between floating and fullscreen styles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"astro": "^6.0.6"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"@astrojs/check": "^0.9.
|
|
56
|
+
"@astrojs/check": "^0.9.9",
|
|
57
57
|
"@types/jsdom": "^21.1.7",
|
|
58
58
|
"@vitest/coverage-v8": "^3.2.4",
|
|
59
|
-
"astro": "^6.
|
|
59
|
+
"astro": "^6.3.7",
|
|
60
60
|
"jsdom": "^26.1.0",
|
|
61
61
|
"typescript": "^5.0.0",
|
|
62
62
|
"vitest": "^3.2.4"
|
|
@@ -1,16 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
export interface Props {
|
|
3
|
-
color?: string;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
2
|
+
export interface Props {}
|
|
7
3
|
---
|
|
8
4
|
|
|
9
5
|
<button
|
|
10
6
|
class="hamburger hamburger--collapse"
|
|
11
7
|
type="button"
|
|
12
8
|
id="hamburger-btn"
|
|
13
|
-
style={`--hamburger-color: ${color};`}
|
|
14
9
|
>
|
|
15
10
|
<span class="hamburger-box">
|
|
16
11
|
<span class="hamburger-inner"></span>
|
|
@@ -23,14 +18,16 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
23
18
|
font: inherit;
|
|
24
19
|
overflow: visible;
|
|
25
20
|
margin: 0;
|
|
26
|
-
padding:
|
|
21
|
+
padding: 10px;
|
|
27
22
|
cursor: pointer;
|
|
28
23
|
transition-timing-function: linear;
|
|
29
24
|
transition-duration: 0.15s;
|
|
30
|
-
transition-property: opacity, filter;
|
|
25
|
+
transition-property: opacity, filter, border-color;
|
|
31
26
|
text-transform: none;
|
|
32
27
|
color: inherit;
|
|
33
|
-
border:
|
|
28
|
+
border: 1px solid
|
|
29
|
+
color-mix(in srgb, var(--text-color, #fff) 25%, transparent);
|
|
30
|
+
border-radius: 8px;
|
|
34
31
|
background-color: transparent;
|
|
35
32
|
|
|
36
33
|
@media screen and (max-width: 768px) {
|
|
@@ -61,7 +58,7 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
61
58
|
.hamburger-inner,
|
|
62
59
|
.hamburger-inner::before,
|
|
63
60
|
.hamburger-inner::after {
|
|
64
|
-
background-color: var(--
|
|
61
|
+
background-color: var(--text-color, inherit);
|
|
65
62
|
position: absolute;
|
|
66
63
|
width: 40px;
|
|
67
64
|
height: 4px;
|
|
@@ -88,12 +85,16 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
88
85
|
}
|
|
89
86
|
|
|
90
87
|
.hamburger--collapse .hamburger-inner:before {
|
|
91
|
-
transition:
|
|
88
|
+
transition:
|
|
89
|
+
top 0.12s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s,
|
|
90
|
+
transform 0.13s cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
|
92
91
|
}
|
|
93
92
|
|
|
94
93
|
.hamburger--collapse .hamburger-inner:after {
|
|
95
94
|
top: -20px;
|
|
96
|
-
transition:
|
|
95
|
+
transition:
|
|
96
|
+
top 0.2s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s,
|
|
97
|
+
opacity 0.1s linear;
|
|
97
98
|
}
|
|
98
99
|
|
|
99
100
|
/* Animation */
|
|
@@ -105,13 +106,17 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
105
106
|
|
|
106
107
|
.hamburger--collapse.is-active .hamburger-inner:before {
|
|
107
108
|
top: 0;
|
|
108
|
-
transition:
|
|
109
|
+
transition:
|
|
110
|
+
top 0.1s cubic-bezier(0.33333, 0, 0.66667, 0.33333) 0.16s,
|
|
111
|
+
transform 0.13s cubic-bezier(0.215, 0.61, 0.355, 1) 0.25s;
|
|
109
112
|
transform: rotate(-90deg);
|
|
110
113
|
}
|
|
111
114
|
|
|
112
115
|
.hamburger--collapse.is-active .hamburger-inner:after {
|
|
113
116
|
top: 0;
|
|
114
|
-
transition:
|
|
117
|
+
transition:
|
|
118
|
+
top 0.2s cubic-bezier(0.33333, 0, 0.66667, 0.33333),
|
|
119
|
+
opacity 0.1s linear 0.22s;
|
|
115
120
|
opacity: 0;
|
|
116
121
|
}
|
|
117
122
|
</style>
|
package/src/Header.astro
CHANGED
|
@@ -4,37 +4,52 @@ import NavMenu from "./NavMenu.astro";
|
|
|
4
4
|
import MobileNav from "./MobileNav.astro";
|
|
5
5
|
|
|
6
6
|
import type {
|
|
7
|
-
LogoConfig,
|
|
8
7
|
NavConfig,
|
|
9
8
|
DualThemeConfig,
|
|
10
|
-
|
|
9
|
+
HeaderClassNames,
|
|
11
10
|
} from "./index.js";
|
|
12
11
|
|
|
13
12
|
export interface Props {
|
|
13
|
+
/**
|
|
14
|
+
* Layout style.
|
|
15
|
+
* - "floating": Centered with max-width and rounded corners.
|
|
16
|
+
* - "fullscreen": Full width with no border radius.
|
|
17
|
+
* @default "floating"
|
|
18
|
+
*/
|
|
14
19
|
headerType?: "floating" | "fullscreen";
|
|
20
|
+
/**
|
|
21
|
+
* Theme behavior.
|
|
22
|
+
* - "light": Force light mode.
|
|
23
|
+
* - "dark": Force dark mode.
|
|
24
|
+
* - "auto": Detects .dark class on the root element.
|
|
25
|
+
* @default "auto"
|
|
26
|
+
*/
|
|
15
27
|
preset?: "light" | "dark" | "auto";
|
|
16
|
-
|
|
28
|
+
|
|
29
|
+
/** Navigation links and structure. */
|
|
17
30
|
navigation?: NavConfig;
|
|
31
|
+
/** Custom theme overrides for light and dark modes. */
|
|
18
32
|
theme?: DualThemeConfig;
|
|
19
|
-
|
|
33
|
+
/**
|
|
34
|
+
* High-level CSS class overrides for structural wrapper elements.
|
|
35
|
+
* For fine-grained nav/logo element classes, use the nested `xxx__class`
|
|
36
|
+
* props inside `navigation` or `logo` instead.
|
|
37
|
+
* @example { header: "shadow-lg", container: "top-4" }
|
|
38
|
+
*/
|
|
39
|
+
classNames?: HeaderClassNames;
|
|
20
40
|
}
|
|
21
41
|
|
|
22
42
|
const {
|
|
23
43
|
headerType = "floating",
|
|
24
44
|
preset = "auto",
|
|
25
|
-
|
|
45
|
+
|
|
26
46
|
navigation = {},
|
|
27
47
|
theme = {},
|
|
28
48
|
classNames = {},
|
|
29
49
|
} = Astro.props;
|
|
30
50
|
|
|
31
51
|
if (import.meta.env.DEV) {
|
|
32
|
-
|
|
33
|
-
if (typeof logo === "string") {
|
|
34
|
-
console.warn(
|
|
35
|
-
"[@sofidevo/astro-dynamic-header] BREAKING CHANGE: The 'logo' prop now expects an object. Please use logo={{ src: '...' }} instead.",
|
|
36
|
-
);
|
|
37
|
-
}
|
|
52
|
+
|
|
38
53
|
if (Array.isArray(navigation)) {
|
|
39
54
|
console.warn(
|
|
40
55
|
"[@sofidevo/astro-dynamic-header] BREAKING CHANGE: The 'navigation' prop now expects an object with 'menuItems'. Please use navigation={{ menuItems: [...] }} instead.",
|
|
@@ -42,42 +57,36 @@ if (import.meta.env.DEV) {
|
|
|
42
57
|
}
|
|
43
58
|
}
|
|
44
59
|
|
|
45
|
-
//
|
|
46
|
-
const
|
|
47
|
-
light: {
|
|
48
|
-
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
49
|
-
backgroundColorOpaque: "rgb(255, 255, 255)",
|
|
50
|
-
backdropBlur: "blur(20px)",
|
|
51
|
-
zIndex: 10,
|
|
52
|
-
textColor: "#1a1a1a",
|
|
53
|
-
accentColor: "#3e1c71",
|
|
54
|
-
},
|
|
55
|
-
dark: {
|
|
56
|
-
backgroundColor: "#0d0d0dcc",
|
|
57
|
-
backgroundColorOpaque: "#0d0d0d",
|
|
58
|
-
backdropBlur: "blur(20px)",
|
|
59
|
-
zIndex: 10,
|
|
60
|
-
textColor: "#ffffff",
|
|
61
|
-
accentColor: "#00ffff",
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
// Merge with user overrides
|
|
66
|
-
const lightTheme = { ...defaultThemes.light, ...theme.light };
|
|
67
|
-
const darkTheme = { ...defaultThemes.dark, ...theme.dark };
|
|
60
|
+
// Map user theme overrides to CSS custom properties
|
|
61
|
+
const inlineStyles: Record<string, string> = {};
|
|
68
62
|
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
if (theme.light) {
|
|
64
|
+
if (theme.light.backgroundColor) inlineStyles["--l-bg"] = theme.light.backgroundColor;
|
|
65
|
+
if (theme.light.backgroundColorOpaque) inlineStyles["--l-bg-opaque"] = theme.light.backgroundColorOpaque;
|
|
66
|
+
if (theme.light.textColor) inlineStyles["--l-text"] = theme.light.textColor;
|
|
67
|
+
if (theme.light.accentColor) inlineStyles["--l-accent"] = theme.light.accentColor;
|
|
68
|
+
if (theme.light.backdropBlur) inlineStyles["--l-blur"] = theme.light.backdropBlur;
|
|
69
|
+
}
|
|
70
|
+
if (theme.dark) {
|
|
71
|
+
if (theme.dark.backgroundColor) inlineStyles["--d-bg"] = theme.dark.backgroundColor;
|
|
72
|
+
if (theme.dark.backgroundColorOpaque) inlineStyles["--d-bg-opaque"] = theme.dark.backgroundColorOpaque;
|
|
73
|
+
if (theme.dark.textColor) inlineStyles["--d-text"] = theme.dark.textColor;
|
|
74
|
+
if (theme.dark.accentColor) inlineStyles["--d-accent"] = theme.dark.accentColor;
|
|
75
|
+
if (theme.dark.backdropBlur) inlineStyles["--d-blur"] = theme.dark.backdropBlur;
|
|
76
|
+
}
|
|
71
77
|
|
|
72
|
-
|
|
78
|
+
const zIndex = theme.light?.zIndex ?? 10;
|
|
79
|
+
|
|
80
|
+
// Navigation configuration
|
|
73
81
|
const {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
homeUrl = "/",
|
|
83
|
+
menuItems = [],
|
|
84
|
+
header__menu__class,
|
|
85
|
+
header__item__class,
|
|
86
|
+
menu__link__class,
|
|
87
|
+
} = navigation;
|
|
88
|
+
|
|
89
|
+
|
|
81
90
|
|
|
82
91
|
const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
83
92
|
---
|
|
@@ -130,7 +139,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
130
139
|
`header__container--${headerType}`,
|
|
131
140
|
classNames.container,
|
|
132
141
|
]}
|
|
133
|
-
style={{ zIndex
|
|
142
|
+
style={{ zIndex }}
|
|
134
143
|
>
|
|
135
144
|
<header
|
|
136
145
|
class:list={[
|
|
@@ -139,40 +148,17 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
139
148
|
forcedClass,
|
|
140
149
|
classNames.header,
|
|
141
150
|
]}
|
|
142
|
-
style={
|
|
143
|
-
"--l-bg": lightTheme.backgroundColor,
|
|
144
|
-
"--l-bg-opaque": lightTheme.backgroundColorOpaque,
|
|
145
|
-
"--l-text": lightTheme.textColor,
|
|
146
|
-
"--l-accent": lightTheme.accentColor,
|
|
147
|
-
"--l-blur": lightTheme.backdropBlur,
|
|
148
|
-
"--d-bg": darkTheme.backgroundColor,
|
|
149
|
-
"--d-bg-opaque": darkTheme.backgroundColorOpaque,
|
|
150
|
-
"--d-text": darkTheme.textColor,
|
|
151
|
-
"--d-accent": darkTheme.accentColor,
|
|
152
|
-
"--d-blur": darkTheme.backdropBlur,
|
|
153
|
-
}}
|
|
151
|
+
style={Object.keys(inlineStyles).length > 0 ? inlineStyles : undefined}
|
|
154
152
|
>
|
|
155
|
-
<
|
|
156
|
-
<img
|
|
157
|
-
class="header__logo"
|
|
158
|
-
src={logoSrc}
|
|
159
|
-
alt={logoAlt}
|
|
160
|
-
style={{ width: logoWidth }}
|
|
161
|
-
/>
|
|
162
|
-
{
|
|
163
|
-
logoText && (
|
|
164
|
-
<span
|
|
165
|
-
class:list={["header__logo-text", classNames.logoText]}
|
|
166
|
-
style={{ color: logoTextColor, fontSize: logoTextSize }}
|
|
167
|
-
>
|
|
168
|
-
{logoText}
|
|
169
|
-
</span>
|
|
170
|
-
)
|
|
171
|
-
}
|
|
172
|
-
</a>
|
|
153
|
+
<slot name="logo" />
|
|
173
154
|
|
|
174
155
|
<div class:list={["nav-menu-wrapper", classNames.nav]}>
|
|
175
|
-
<NavMenu
|
|
156
|
+
<NavMenu
|
|
157
|
+
menuItems={menuItems}
|
|
158
|
+
header__menu__class={header__menu__class}
|
|
159
|
+
header__item__class={header__item__class}
|
|
160
|
+
menu__link__class={menu__link__class}
|
|
161
|
+
/>
|
|
176
162
|
</div>
|
|
177
163
|
|
|
178
164
|
{
|
|
@@ -185,7 +171,11 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
185
171
|
|
|
186
172
|
<HamburgerButton />
|
|
187
173
|
|
|
188
|
-
<MobileNav
|
|
174
|
+
<MobileNav
|
|
175
|
+
menuItems={menuItems}
|
|
176
|
+
type={headerType}
|
|
177
|
+
mobileNav__class={classNames.mobileNav}
|
|
178
|
+
>
|
|
189
179
|
{
|
|
190
180
|
Astro.slots.has("actions") && (
|
|
191
181
|
<div class="actions-mobile" slot="slot-panel">
|
|
@@ -199,11 +189,11 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
199
189
|
|
|
200
190
|
<style is:inline>
|
|
201
191
|
.header {
|
|
202
|
-
--bg-color: var(--l-bg);
|
|
203
|
-
--bg-color-opaque: var(--l-bg-opaque);
|
|
204
|
-
--text-color: var(--l-text);
|
|
205
|
-
--accent-color: var(--l-accent);
|
|
206
|
-
--backdrop-blur: var(--l-blur);
|
|
192
|
+
--bg-color: var(--l-bg, rgba(255, 255, 255, 0.9));
|
|
193
|
+
--bg-color-opaque: var(--l-bg-opaque, rgb(255, 255, 255));
|
|
194
|
+
--text-color: var(--l-text, #1a1a1a);
|
|
195
|
+
--accent-color: var(--l-accent, #3e1c71);
|
|
196
|
+
--backdrop-blur: var(--l-blur, blur(20px));
|
|
207
197
|
|
|
208
198
|
display: flex;
|
|
209
199
|
align-items: center;
|
|
@@ -227,28 +217,28 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
227
217
|
|
|
228
218
|
/* Automatic dark mode detection via root class */
|
|
229
219
|
:root.dark .header:not(.header--force-light) {
|
|
230
|
-
--bg-color: var(--d-bg);
|
|
231
|
-
--bg-color-opaque: var(--d-bg-opaque);
|
|
232
|
-
--text-color: var(--d-text);
|
|
233
|
-
--accent-color: var(--d-accent);
|
|
234
|
-
--backdrop-blur: var(--d-blur);
|
|
220
|
+
--bg-color: var(--d-bg, #0d0d0dcc);
|
|
221
|
+
--bg-color-opaque: var(--d-bg-opaque, #0d0d0d);
|
|
222
|
+
--text-color: var(--d-text, #ffffff);
|
|
223
|
+
--accent-color: var(--d-accent, #00ffff);
|
|
224
|
+
--backdrop-blur: var(--d-blur, blur(20px));
|
|
235
225
|
}
|
|
236
226
|
|
|
237
227
|
/* Forced themes */
|
|
238
228
|
.header--force-dark {
|
|
239
|
-
--bg-color: var(--d-bg) !important;
|
|
240
|
-
--bg-color-opaque: var(--d-bg-opaque) !important;
|
|
241
|
-
--text-color: var(--d-text) !important;
|
|
242
|
-
--accent-color: var(--d-accent) !important;
|
|
243
|
-
--backdrop-blur: var(--d-blur) !important;
|
|
229
|
+
--bg-color: var(--d-bg, #0d0d0dcc) !important;
|
|
230
|
+
--bg-color-opaque: var(--d-bg-opaque, #0d0d0d) !important;
|
|
231
|
+
--text-color: var(--d-text, #ffffff) !important;
|
|
232
|
+
--accent-color: var(--d-accent, #00ffff) !important;
|
|
233
|
+
--backdrop-blur: var(--d-blur, blur(20px)) !important;
|
|
244
234
|
}
|
|
245
235
|
|
|
246
236
|
.header--force-light {
|
|
247
|
-
--bg-color: var(--l-bg) !important;
|
|
248
|
-
--bg-color-opaque: var(--l-bg-opaque) !important;
|
|
249
|
-
--text-color: var(--l-text) !important;
|
|
250
|
-
--accent-color: var(--l-accent) !important;
|
|
251
|
-
--backdrop-blur: var(--l-blur) !important;
|
|
237
|
+
--bg-color: var(--l-bg, rgba(255, 255, 255, 0.9)) !important;
|
|
238
|
+
--bg-color-opaque: var(--l-bg-opaque, rgb(255, 255, 255)) !important;
|
|
239
|
+
--text-color: var(--l-text, #1a1a1a) !important;
|
|
240
|
+
--accent-color: var(--l-accent, #3e1c71) !important;
|
|
241
|
+
--backdrop-blur: var(--l-blur, blur(20px)) !important;
|
|
252
242
|
}
|
|
253
243
|
|
|
254
244
|
.header__container {
|
|
@@ -293,20 +283,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
293
283
|
}
|
|
294
284
|
}
|
|
295
285
|
|
|
296
|
-
.logo__container {
|
|
297
|
-
display: flex;
|
|
298
|
-
align-items: center;
|
|
299
|
-
text-decoration: none;
|
|
300
|
-
color: inherit;
|
|
301
|
-
& span {
|
|
302
|
-
font-weight: 600;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
286
|
|
|
306
|
-
.header__logo {
|
|
307
|
-
margin-right: 1em;
|
|
308
|
-
object-fit: contain;
|
|
309
|
-
}
|
|
310
287
|
|
|
311
288
|
.actions-desktop {
|
|
312
289
|
@media (width < 768px) {
|
package/src/MobileNav.astro
CHANGED
|
@@ -2,25 +2,34 @@
|
|
|
2
2
|
import type { MenuItem, SecondaryMenuItem, TertiaryMenuItem } from "./index.js";
|
|
3
3
|
|
|
4
4
|
export interface Props {
|
|
5
|
+
/** Header type for styling: "floating" or "fullscreen" */
|
|
5
6
|
type?: "floating" | "fullscreen";
|
|
7
|
+
/** Array of menu items to display in the mobile menu */
|
|
6
8
|
menuItems?: MenuItem[];
|
|
9
|
+
/** Whether to show the home link (defaults to true) */
|
|
7
10
|
showHomeLink?: boolean;
|
|
11
|
+
/** Text for the home link (defaults to "Home") */
|
|
8
12
|
homeText?: string;
|
|
13
|
+
/** Extra CSS class(es) for the root mobile `<nav>` panel. */
|
|
14
|
+
mobileNav__class?: string;
|
|
9
15
|
}
|
|
10
16
|
|
|
11
|
-
const { type } = Astro.props;
|
|
12
|
-
|
|
13
17
|
const {
|
|
18
|
+
type,
|
|
19
|
+
mobileNav__class,
|
|
14
20
|
menuItems = [],
|
|
15
21
|
showHomeLink = true,
|
|
16
|
-
homeText = "Home"
|
|
22
|
+
homeText = "Home"
|
|
17
23
|
} = Astro.props;
|
|
18
24
|
|
|
19
25
|
const pagePathname = Astro.url.pathname;
|
|
20
26
|
---
|
|
21
27
|
|
|
22
28
|
<nav
|
|
23
|
-
class={
|
|
29
|
+
class:list={[
|
|
30
|
+
`mobile-header__menu mobile-header__menu--${type}`,
|
|
31
|
+
mobileNav__class,
|
|
32
|
+
]}
|
|
24
33
|
id="mobile-header-menu"
|
|
25
34
|
>
|
|
26
35
|
<ol class="mobile-menu">
|
|
@@ -38,64 +47,101 @@ const pagePathname = Astro.url.pathname;
|
|
|
38
47
|
)
|
|
39
48
|
}
|
|
40
49
|
{
|
|
41
|
-
menuItems?.map((item: MenuItem) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
menuItems?.map((item: MenuItem) => {
|
|
51
|
+
const isActive = pagePathname === item.link;
|
|
52
|
+
return (
|
|
53
|
+
<li>
|
|
54
|
+
{item.submenu ? (
|
|
55
|
+
<details
|
|
56
|
+
class="mobile-details"
|
|
57
|
+
open={item.submenu.some((sub) => pagePathname === sub.link)}
|
|
58
|
+
>
|
|
59
|
+
<summary class:list={["menu__summary", { active: isActive }]}>
|
|
60
|
+
{item.text}
|
|
61
|
+
<iconify-icon
|
|
62
|
+
class="iconify-arrow"
|
|
63
|
+
icon="codicon:triangle-down"
|
|
64
|
+
width="15"
|
|
65
|
+
height="15"
|
|
66
|
+
style="color: var(--accent-color);"
|
|
67
|
+
/>
|
|
68
|
+
</summary>
|
|
69
|
+
<ol class="mobile-submenu">
|
|
70
|
+
{item?.submenu?.map((sub: SecondaryMenuItem) => {
|
|
71
|
+
const isSubActive = pagePathname === sub.link;
|
|
72
|
+
return (
|
|
73
|
+
<li>
|
|
74
|
+
{sub.submenu && sub.submenu.length > 0 ? (
|
|
75
|
+
<details
|
|
76
|
+
class="mobile-details"
|
|
77
|
+
open={sub.submenu.some(
|
|
78
|
+
(subsub) => pagePathname === subsub.link,
|
|
79
|
+
)}
|
|
80
|
+
>
|
|
81
|
+
<summary
|
|
82
|
+
class:list={[
|
|
83
|
+
"menu__summary",
|
|
84
|
+
{ active: isSubActive },
|
|
85
|
+
]}
|
|
86
|
+
>
|
|
87
|
+
{sub.text}
|
|
88
|
+
<iconify-icon
|
|
89
|
+
class="iconify-arrow"
|
|
90
|
+
icon="codicon:triangle-down"
|
|
91
|
+
width="15"
|
|
92
|
+
height="15"
|
|
93
|
+
style="color: var(--accent-color);"
|
|
94
|
+
/>
|
|
95
|
+
</summary>
|
|
96
|
+
<ol class="mobile-subsubmenu">
|
|
97
|
+
{sub.submenu.map((subsub: TertiaryMenuItem) => {
|
|
98
|
+
const isSubSubActive =
|
|
99
|
+
pagePathname === subsub.link;
|
|
100
|
+
return (
|
|
101
|
+
<li class="mobile-submenu__item secondary">
|
|
102
|
+
<a
|
|
103
|
+
class:list={[
|
|
104
|
+
"menu__link",
|
|
105
|
+
"mobile-menu__link",
|
|
106
|
+
{ active: isSubSubActive },
|
|
107
|
+
]}
|
|
108
|
+
href={subsub.link}
|
|
109
|
+
>
|
|
110
|
+
{subsub.text}
|
|
111
|
+
</a>
|
|
112
|
+
</li>
|
|
113
|
+
);
|
|
114
|
+
})}
|
|
115
|
+
</ol>
|
|
116
|
+
</details>
|
|
117
|
+
) : (
|
|
118
|
+
<a
|
|
119
|
+
class:list={[
|
|
120
|
+
"menu__link",
|
|
121
|
+
"mobile-menu__link",
|
|
122
|
+
{ active: isSubActive },
|
|
123
|
+
]}
|
|
124
|
+
href={sub.link}
|
|
125
|
+
>
|
|
126
|
+
{sub.text}
|
|
127
|
+
</a>
|
|
128
|
+
)}
|
|
129
|
+
</li>
|
|
130
|
+
);
|
|
131
|
+
})}
|
|
132
|
+
</ol>
|
|
133
|
+
</details>
|
|
134
|
+
) : (
|
|
135
|
+
<a
|
|
136
|
+
class:list={["mobile-menu__link", { active: isActive }]}
|
|
137
|
+
href={item.link}
|
|
138
|
+
>
|
|
46
139
|
{item.text}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
style="color: var(--accent-color);"
|
|
53
|
-
/>
|
|
54
|
-
</summary>
|
|
55
|
-
<ol class="mobile-submenu">
|
|
56
|
-
{item?.submenu?.map((sub: SecondaryMenuItem) => (
|
|
57
|
-
<li>
|
|
58
|
-
{sub.submenu && sub.submenu.length > 0 ? (
|
|
59
|
-
<details class="mobile-details">
|
|
60
|
-
<summary class="menu__summary">
|
|
61
|
-
{sub.text}
|
|
62
|
-
<iconify-icon
|
|
63
|
-
class="iconify-arrow"
|
|
64
|
-
icon="codicon:triangle-down"
|
|
65
|
-
width="15"
|
|
66
|
-
height="15"
|
|
67
|
-
style="color: var(--accent-color);"
|
|
68
|
-
/>
|
|
69
|
-
</summary>
|
|
70
|
-
<ol class="mobile-subsubmenu">
|
|
71
|
-
{sub.submenu.map((subsub: TertiaryMenuItem) => (
|
|
72
|
-
<li class="mobile-submenu__item secondary">
|
|
73
|
-
<a
|
|
74
|
-
class="menu__link mobile-menu__link"
|
|
75
|
-
href={subsub.link}
|
|
76
|
-
>
|
|
77
|
-
{subsub.text}
|
|
78
|
-
</a>
|
|
79
|
-
</li>
|
|
80
|
-
))}
|
|
81
|
-
</ol>
|
|
82
|
-
</details>
|
|
83
|
-
) : (
|
|
84
|
-
<a class="menu__link mobile-menu__link" href={sub.link}>
|
|
85
|
-
{sub.text}
|
|
86
|
-
</a>
|
|
87
|
-
)}
|
|
88
|
-
</li>
|
|
89
|
-
))}
|
|
90
|
-
</ol>
|
|
91
|
-
</details>
|
|
92
|
-
) : (
|
|
93
|
-
<a class="mobile-menu__link" href={item.link}>
|
|
94
|
-
{item.text}
|
|
95
|
-
</a>
|
|
96
|
-
)}
|
|
97
|
-
</li>
|
|
98
|
-
))
|
|
140
|
+
</a>
|
|
141
|
+
)}
|
|
142
|
+
</li>
|
|
143
|
+
);
|
|
144
|
+
})
|
|
99
145
|
}
|
|
100
146
|
</ol>
|
|
101
147
|
<slot name="slot-panel" />
|
|
@@ -115,14 +161,24 @@ const pagePathname = Astro.url.pathname;
|
|
|
115
161
|
left: 0;
|
|
116
162
|
background: var(--bg-color-opaque, #151515);
|
|
117
163
|
z-index: 20;
|
|
118
|
-
transition:
|
|
164
|
+
transition:
|
|
165
|
+
transform 0.3s ease-in-out,
|
|
166
|
+
background-color 0.3s ease;
|
|
119
167
|
transform: translateX(100%);
|
|
120
168
|
padding: 10rem 2rem 2rem;
|
|
121
169
|
color: var(--text-color, #fff);
|
|
122
170
|
}
|
|
123
171
|
|
|
124
172
|
a {
|
|
125
|
-
color:
|
|
173
|
+
color: color-mix(
|
|
174
|
+
in srgb,
|
|
175
|
+
var(--accent-color, #00ffff) 30%,
|
|
176
|
+
var(--text-color, #ffffff) 70%
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.active {
|
|
181
|
+
color: var(--accent-color, #00ffff) !important;
|
|
126
182
|
}
|
|
127
183
|
}
|
|
128
184
|
|
|
@@ -139,7 +195,7 @@ const pagePathname = Astro.url.pathname;
|
|
|
139
195
|
|
|
140
196
|
.mobile-menu li a {
|
|
141
197
|
border: solid 1px
|
|
142
|
-
color-mix(in srgb, var(--accent-color, #00ffff)
|
|
198
|
+
color-mix(in srgb, var(--accent-color, #00ffff) 15%, transparent);
|
|
143
199
|
border-radius: 4px;
|
|
144
200
|
width: 100%;
|
|
145
201
|
height: 4rem;
|
package/src/NavMenu.astro
CHANGED
|
@@ -2,12 +2,39 @@
|
|
|
2
2
|
import type { MenuItem, SecondaryMenuItem, TertiaryMenuItem } from "./index.js";
|
|
3
3
|
|
|
4
4
|
export interface Props {
|
|
5
|
+
/**
|
|
6
|
+
* Header type for styling.
|
|
7
|
+
* @example "floating"
|
|
8
|
+
*/
|
|
9
|
+
type?: "floating" | "fullscreen";
|
|
10
|
+
/** Array of menu items to display. */
|
|
5
11
|
menuItems?: MenuItem[];
|
|
12
|
+
/**
|
|
13
|
+
* Whether to show the home link in the menu.
|
|
14
|
+
* @default true
|
|
15
|
+
*/
|
|
6
16
|
showHomeLink?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Text for the home link.
|
|
19
|
+
* @default "Home"
|
|
20
|
+
*/
|
|
7
21
|
homeText?: string;
|
|
22
|
+
/** Extra CSS class(es) for the `<nav>` element. */
|
|
23
|
+
header__menu__class?: string;
|
|
24
|
+
/** Extra CSS class(es) for each top-level `<li>` item. */
|
|
25
|
+
header__item__class?: string;
|
|
26
|
+
/** Extra CSS class(es) for each top-level menu `<a>` link. */
|
|
27
|
+
menu__link__class?: string;
|
|
8
28
|
}
|
|
9
29
|
|
|
10
|
-
const {
|
|
30
|
+
const {
|
|
31
|
+
menuItems = [],
|
|
32
|
+
showHomeLink = true,
|
|
33
|
+
homeText = "Home",
|
|
34
|
+
header__menu__class,
|
|
35
|
+
header__item__class,
|
|
36
|
+
menu__link__class,
|
|
37
|
+
} = Astro.props;
|
|
11
38
|
|
|
12
39
|
const pagePathname = Astro.url.pathname;
|
|
13
40
|
---
|
|
@@ -114,12 +141,16 @@ const pagePathname = Astro.url.pathname;
|
|
|
114
141
|
});
|
|
115
142
|
</script>
|
|
116
143
|
|
|
117
|
-
<nav class="header__menu" id="header-menu">
|
|
144
|
+
<nav class:list={["header__menu", header__menu__class]} id="header-menu">
|
|
118
145
|
<ul class="menu">
|
|
119
146
|
{
|
|
120
147
|
showHomeLink && pagePathname !== "/" && (
|
|
121
|
-
<li class="header__item">
|
|
122
|
-
<a
|
|
148
|
+
<li class:list={["header__item", header__item__class]}>
|
|
149
|
+
<a
|
|
150
|
+
class:list={["menu__link", menu__link__class]}
|
|
151
|
+
href="/"
|
|
152
|
+
data-astro-prefetch="hover"
|
|
153
|
+
>
|
|
123
154
|
{homeText}
|
|
124
155
|
</a>
|
|
125
156
|
</li>
|
|
@@ -127,8 +158,8 @@ const pagePathname = Astro.url.pathname;
|
|
|
127
158
|
}
|
|
128
159
|
{
|
|
129
160
|
menuItems.map((item: MenuItem) => (
|
|
130
|
-
<li class="menu__item">
|
|
131
|
-
<a class="menu__link" href={item.link}>
|
|
161
|
+
<li class:list={["menu__item", header__item__class]}>
|
|
162
|
+
<a class:list={["menu__link", menu__link__class]} href={item.link}>
|
|
132
163
|
{item.text}
|
|
133
164
|
{item.submenu && (
|
|
134
165
|
<iconify-icon
|
package/src/defaults.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { DualThemeConfig } from "./index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Built-in default theme tokens used by the Header component.
|
|
5
|
+
* These are merged with any user-supplied `theme` overrides.
|
|
6
|
+
*
|
|
7
|
+
* You can import this object if you want to build on top of the defaults
|
|
8
|
+
* rather than replacing them wholesale:
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { defaultThemes } from '@sofidevo/astro-dynamic-header/defaults';
|
|
13
|
+
* const theme = {
|
|
14
|
+
* light: { ...defaultThemes.light, accentColor: "#e11d48" },
|
|
15
|
+
* dark: { ...defaultThemes.dark, accentColor: "#f43f5e" },
|
|
16
|
+
* };
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const defaultThemes: Required<DualThemeConfig> = {
|
|
20
|
+
light: {
|
|
21
|
+
backgroundColor: "rgba(255, 255, 255, 0.9)",
|
|
22
|
+
backgroundColorOpaque: "rgb(255, 255, 255)",
|
|
23
|
+
backdropBlur: "blur(20px)",
|
|
24
|
+
zIndex: 10,
|
|
25
|
+
textColor: "#1a1a1a",
|
|
26
|
+
accentColor: "#3e1c71",
|
|
27
|
+
},
|
|
28
|
+
dark: {
|
|
29
|
+
backgroundColor: "#0d0d0dcc",
|
|
30
|
+
backgroundColorOpaque: "#0d0d0d",
|
|
31
|
+
backdropBlur: "blur(20px)",
|
|
32
|
+
zIndex: 10,
|
|
33
|
+
textColor: "#ffffff",
|
|
34
|
+
accentColor: "#00ffff",
|
|
35
|
+
},
|
|
36
|
+
};
|
package/src/index.ts
CHANGED
|
@@ -1,72 +1,194 @@
|
|
|
1
|
+
export { defaultThemes } from "./defaults.js";
|
|
1
2
|
|
|
2
|
-
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* Represents a menu item in the navigation.
|
|
5
|
+
*/
|
|
4
6
|
export interface MenuItemType {
|
|
7
|
+
/** The URL path for the link */
|
|
5
8
|
link: string;
|
|
9
|
+
/** The text label to display */
|
|
6
10
|
text: string;
|
|
11
|
+
/** Optional nested submenu items */
|
|
7
12
|
submenu?: MenuItemType[];
|
|
8
13
|
}
|
|
9
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Represents a third-level menu item.
|
|
17
|
+
*/
|
|
10
18
|
export interface TertiaryMenuItem {
|
|
19
|
+
/** The URL path for the link */
|
|
11
20
|
link: string;
|
|
21
|
+
/** The text label to display */
|
|
12
22
|
text: string;
|
|
13
23
|
}
|
|
14
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Represents a second-level menu item with optional nested tertiary items.
|
|
27
|
+
*/
|
|
15
28
|
export interface SecondaryMenuItem {
|
|
29
|
+
/** The URL path for the link */
|
|
16
30
|
link: string;
|
|
31
|
+
/** The text label to display */
|
|
17
32
|
text: string;
|
|
33
|
+
/** Optional nested tertiary menu items */
|
|
18
34
|
submenu?: TertiaryMenuItem[];
|
|
19
35
|
}
|
|
20
36
|
|
|
37
|
+
/**
|
|
38
|
+
* Represents a top-level menu item with optional nested secondary items.
|
|
39
|
+
*/
|
|
21
40
|
export interface MenuItem {
|
|
41
|
+
/** The URL path for the link */
|
|
22
42
|
link: string;
|
|
43
|
+
/** The text label to display */
|
|
23
44
|
text: string;
|
|
45
|
+
/** Optional nested secondary menu items */
|
|
24
46
|
submenu?: SecondaryMenuItem[];
|
|
25
47
|
}
|
|
26
48
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
width?: string;
|
|
31
|
-
text?: string;
|
|
32
|
-
textSize?: string;
|
|
33
|
-
textColor?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for the main navigation.
|
|
51
|
+
*/
|
|
36
52
|
export interface NavConfig {
|
|
53
|
+
/**
|
|
54
|
+
* The URL for the home link.
|
|
55
|
+
* @default "/"
|
|
56
|
+
*/
|
|
37
57
|
homeUrl?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Array of top-level menu items.
|
|
60
|
+
* @example [{ link: "/about", text: "About Us" }]
|
|
61
|
+
*/
|
|
38
62
|
menuItems?: MenuItem[];
|
|
63
|
+
/**
|
|
64
|
+
* Fine-grained class override for the desktop `<nav>` element.
|
|
65
|
+
* Use this when you want the class to live alongside the rest of the
|
|
66
|
+
* navigation configuration rather than in the top-level `classNames` prop.
|
|
67
|
+
* @example "flex gap-4"
|
|
68
|
+
*/
|
|
69
|
+
header__menu__class?: string;
|
|
70
|
+
/**
|
|
71
|
+
* Fine-grained class override applied to every top-level `<li>` item
|
|
72
|
+
* in the desktop navigation.
|
|
73
|
+
* @example "px-2 py-1"
|
|
74
|
+
*/
|
|
75
|
+
header__item__class?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Fine-grained class override applied to every top-level `<a>` link
|
|
78
|
+
* in the desktop navigation.
|
|
79
|
+
* @example "hover:underline font-medium"
|
|
80
|
+
*/
|
|
81
|
+
menu__link__class?: string;
|
|
39
82
|
}
|
|
40
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Individual theme settings for a specific state (light/dark).
|
|
86
|
+
*/
|
|
41
87
|
export interface ThemeConfig {
|
|
88
|
+
/**
|
|
89
|
+
* Main background color. Supports hex, rgb, rgba, etc.
|
|
90
|
+
* @example "rgba(255, 255, 255, 0.9)"
|
|
91
|
+
*/
|
|
42
92
|
backgroundColor?: string;
|
|
93
|
+
/**
|
|
94
|
+
* Solid background color for submenus and mobile panels to ensure readability.
|
|
95
|
+
* @example "#ffffff"
|
|
96
|
+
*/
|
|
43
97
|
backgroundColorOpaque?: string;
|
|
98
|
+
/**
|
|
99
|
+
* CSS backdrop-filter blur value.
|
|
100
|
+
* @default "blur(20px)"
|
|
101
|
+
*/
|
|
44
102
|
backdropBlur?: string;
|
|
103
|
+
/**
|
|
104
|
+
* CSS z-index for the header container.
|
|
105
|
+
* @default 10
|
|
106
|
+
*/
|
|
45
107
|
zIndex?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Primary text color for navigation and logo.
|
|
110
|
+
*/
|
|
46
111
|
textColor?: string;
|
|
112
|
+
/**
|
|
113
|
+
* Color for highlights, active states, underscores, and small borders.
|
|
114
|
+
*/
|
|
47
115
|
accentColor?: string;
|
|
48
116
|
}
|
|
49
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Combined theme configuration for both light and dark modes.
|
|
120
|
+
*/
|
|
50
121
|
export interface DualThemeConfig {
|
|
122
|
+
/** Settings applied when light mode is active. */
|
|
51
123
|
light?: ThemeConfig;
|
|
124
|
+
/** Settings applied when dark mode is active. */
|
|
52
125
|
dark?: ThemeConfig;
|
|
53
126
|
}
|
|
54
127
|
|
|
55
|
-
|
|
128
|
+
/**
|
|
129
|
+
* Custom CSS class names for high-level layout & appearance customization.
|
|
130
|
+
*
|
|
131
|
+
* These target the structural wrapper elements of the Header. For fine-grained
|
|
132
|
+
* control over individual nav links or the logo internals, use the nested
|
|
133
|
+
* `xxx__class` props inside the `navigation` or `logo` config objects instead.
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* ```astro
|
|
137
|
+
* <Header classNames={{ header: "shadow-xl", container: "top-4 px-6" }} />
|
|
138
|
+
* ```
|
|
139
|
+
*/
|
|
140
|
+
export interface HeaderClassNames {
|
|
141
|
+
/** Outermost fixed `<div>` that positions the header on the page. */
|
|
56
142
|
container?: string;
|
|
143
|
+
/** Inner `<header>` element — best place for shadows, borders, transitions. */
|
|
57
144
|
header?: string;
|
|
145
|
+
/** Logo anchor `<a>` — add hover states or focus rings here. */
|
|
58
146
|
logo?: string;
|
|
147
|
+
/** Logo text `<span>` — override typography here. */
|
|
59
148
|
logoText?: string;
|
|
149
|
+
/** Desktop nav wrapper `<div>` — adjust spacing between logo and menu. */
|
|
60
150
|
nav?: string;
|
|
151
|
+
/** Mobile nav panel `<nav>` — add slide-in overrides or z-index tweaks. */
|
|
152
|
+
mobileNav?: string;
|
|
61
153
|
}
|
|
62
154
|
|
|
155
|
+
/**
|
|
156
|
+
* @deprecated Use {@link HeaderClassNames} instead.
|
|
157
|
+
* Kept as an alias for backwards compatibility.
|
|
158
|
+
*/
|
|
159
|
+
export type CustomClassNames = HeaderClassNames;
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Main properties for the Header component.
|
|
163
|
+
*/
|
|
63
164
|
export interface HeaderProps {
|
|
165
|
+
/**
|
|
166
|
+
* Layout style.
|
|
167
|
+
* - "floating": Centered with max-width and rounded corners.
|
|
168
|
+
* - "fullscreen": Full width with no border radius.
|
|
169
|
+
* @default "floating"
|
|
170
|
+
*/
|
|
64
171
|
headerType?: "floating" | "fullscreen";
|
|
172
|
+
/**
|
|
173
|
+
* Theme behavior.
|
|
174
|
+
* - "light": Force light mode.
|
|
175
|
+
* - "dark": Force dark mode.
|
|
176
|
+
* - "auto": Detects .dark class on the root element.
|
|
177
|
+
* @default "auto"
|
|
178
|
+
*/
|
|
65
179
|
preset?: "light" | "dark" | "auto";
|
|
66
|
-
|
|
180
|
+
|
|
181
|
+
/** Navigation links and structure. */
|
|
67
182
|
navigation?: NavConfig;
|
|
183
|
+
/** Custom theme overrides. See {@link DualThemeConfig} */
|
|
68
184
|
theme?: DualThemeConfig;
|
|
69
|
-
|
|
185
|
+
/**
|
|
186
|
+
* High-level CSS class overrides for structural wrapper elements.
|
|
187
|
+
* For fine-grained nav/logo element classes, use the nested `xxx__class`
|
|
188
|
+
* props inside `navigation` or `logo` instead.
|
|
189
|
+
* @example { header: "shadow-lg", container: "top-4" }
|
|
190
|
+
*/
|
|
191
|
+
classNames?: HeaderClassNames;
|
|
70
192
|
}
|
|
71
193
|
|
|
72
194
|
export interface NavMenuProps {
|