@sofidevo/astro-dynamic-header 2.0.0 → 2.0.2
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 +158 -116
- package/package.json +1 -1
- package/src/HamburgerButton.astro +24 -9
- package/src/Header.astro +48 -10
- package/src/MobileNav.astro +80 -59
- package/src/NavMenu.astro +15 -0
- package/src/index.ts +122 -1
package/README.md
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
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
|
+
> [!WARNING]
|
|
6
|
+
> **Breaking Changes**: Version 2.0+ introduces a restructured configuration object. If you are upgrading from an older version, please review the [Component Props](#component-props) and the [Comprehensive Example](#comprehensive-example) to migrate your configuration.
|
|
7
|
+
|
|
5
8
|
## Features
|
|
6
9
|
|
|
7
10
|
- **Dynamic Styles**: Switch between floating and fullscreen header layouts
|
|
@@ -42,15 +45,15 @@ By default, the header uses `preset="auto"`, which automatically detects the the
|
|
|
42
45
|
---
|
|
43
46
|
import Header from '@sofidevo/astro-dynamic-header/Header';
|
|
44
47
|
|
|
45
|
-
|
|
46
|
-
menuItems: [
|
|
48
|
+
|
|
49
|
+
const = menuItems: [
|
|
47
50
|
{ link: '/about', text: 'About' },
|
|
48
51
|
]
|
|
49
|
-
|
|
52
|
+
|
|
50
53
|
---
|
|
51
54
|
|
|
52
55
|
<!-- Detects .dark class on root automatically -->
|
|
53
|
-
<Header navigation={
|
|
56
|
+
<Header navigation={{ menuItems }} />
|
|
54
57
|
```
|
|
55
58
|
|
|
56
59
|
### Advanced Usage (Dual-Theme Customization)
|
|
@@ -58,9 +61,14 @@ const navigation = {
|
|
|
58
61
|
You can provide custom colors for both light and dark modes simultaneously.
|
|
59
62
|
|
|
60
63
|
```astro
|
|
64
|
+
|
|
61
65
|
---
|
|
62
66
|
import Header from '@sofidevo/astro-dynamic-header/Header';
|
|
63
|
-
|
|
67
|
+
const navigation = {
|
|
68
|
+
menuItems: [
|
|
69
|
+
{ link: '/about', text: 'About' },
|
|
70
|
+
]
|
|
71
|
+
};
|
|
64
72
|
const theme = {
|
|
65
73
|
light: {
|
|
66
74
|
accentColor: "#3e1c71",
|
|
@@ -104,14 +112,14 @@ const theme = {
|
|
|
104
112
|
|
|
105
113
|
#### ThemeConfig
|
|
106
114
|
|
|
107
|
-
| Propery
|
|
108
|
-
|
|
|
109
|
-
| `backgroundColor`
|
|
110
|
-
| `backgroundColorOpaque
|
|
111
|
-
| `backdropBlur`
|
|
112
|
-
| `zIndex`
|
|
113
|
-
| `textColor`
|
|
114
|
-
| `accentColor`
|
|
115
|
+
| Propery | Type | Default |
|
|
116
|
+
| ----------------------- | -------- | ---------------- |
|
|
117
|
+
| `backgroundColor` | `string` | _Preset default_ |
|
|
118
|
+
| `backgroundColorOpaque` | `string` | _Preset default_ |
|
|
119
|
+
| `backdropBlur` | `string` | `"blur(20px)"` |
|
|
120
|
+
| `zIndex` | `number` | `10` |
|
|
121
|
+
| `textColor` | `string` | _Preset default_ |
|
|
122
|
+
| `accentColor` | `string` | _Preset default_ |
|
|
115
123
|
|
|
116
124
|
> [!IMPORTANT]
|
|
117
125
|
> **Transparency vs Solid Submenus**: To ensure the best UI and avoid rendering bugs with `backdrop-filter` on nested elements, submenus and the mobile navigation panel are **solid/opaque**.
|
|
@@ -129,13 +137,70 @@ const theme = {
|
|
|
129
137
|
|
|
130
138
|
#### CustomClassNames
|
|
131
139
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
|
135
|
-
|
|
|
136
|
-
| `
|
|
137
|
-
| `
|
|
138
|
-
| `
|
|
140
|
+
The `classNames` prop allows you to inject custom CSS classes (such as Tailwind CSS utility classes) into specific high-level elements of the Header component. This provides a bridge between the component's internal styles and your project's global styling system.
|
|
141
|
+
|
|
142
|
+
| Property | Target Element | Purpose & Common Use Cases |
|
|
143
|
+
| ----------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
|
|
144
|
+
| `container` | Outer `div` wrapping the header | **Positioning & Layout**: Use for `top-0`, `z-50`, `fixed`, or adjusting the `max-width` and `mx-auto` logic. |
|
|
145
|
+
| `header` | Inner `<header>` element | **Appearance**: The best place for shadows (`shadow-md`), borders (`border-b`), or custom transition durations. |
|
|
146
|
+
| `logo` | `<a>` tag surrounding the logo | **Interactions**: Add hover states, custom focus rings, or adjust the flex alignment of the logo group. |
|
|
147
|
+
| `logoText` | `<span>` tag containing the logo text | **Typography**: Override font weights, apply text shadows, or use specific tracking/leading classes. |
|
|
148
|
+
| `nav` | `div` wrapping the desktop navigation items | **Desktop Layout**: Adjust spacing between the logo and the menu, or add responsive visibility classes (`hidden md:flex`). |
|
|
149
|
+
|
|
150
|
+
##### Advanced Usage Examples
|
|
151
|
+
|
|
152
|
+
**Implementing a Premium Shadow & Border (Tailwind):**
|
|
153
|
+
Ideal for creating a modern "glass" effect with a subtle border and shadow that adapts to dark mode.
|
|
154
|
+
|
|
155
|
+
```astro
|
|
156
|
+
<Header
|
|
157
|
+
classNames={{
|
|
158
|
+
header: "shadow-xl border-b border-black/5 dark:border-white/10 transition-all duration-500",
|
|
159
|
+
container: "top-4 px-6"
|
|
160
|
+
}}
|
|
161
|
+
/>
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Custom Typography for Logo & Nav Spacing:**
|
|
165
|
+
Perfect for matching the header with your brand's specific typography and layout requirements.
|
|
166
|
+
|
|
167
|
+
```astro
|
|
168
|
+
<Header
|
|
169
|
+
classNames={{
|
|
170
|
+
logoText: "tracking-tighter font-black italic uppercase",
|
|
171
|
+
nav: "ml-auto gap-8" /* Moves menu to the right and increases gap */
|
|
172
|
+
}}
|
|
173
|
+
/>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
> [!TIP]
|
|
177
|
+
> 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.
|
|
178
|
+
|
|
179
|
+
#### LogoConfig
|
|
180
|
+
|
|
181
|
+
| Property | Type | Description |
|
|
182
|
+
| ----------- | -------- | ------------------------------------------------ |
|
|
183
|
+
| `src` | `string` | URL of the logo image |
|
|
184
|
+
| `alt` | `string` | Alternative text for the logo image |
|
|
185
|
+
| `width` | `string` | Width of the logo (e.g., "50px", "5rem") |
|
|
186
|
+
| `text` | `string` | Text to display next to or instead of logo image |
|
|
187
|
+
| `textSize` | `string` | Font size for the logo text |
|
|
188
|
+
| `textColor` | `string` | Color for the logo text |
|
|
189
|
+
|
|
190
|
+
#### NavConfig
|
|
191
|
+
|
|
192
|
+
| Property | Type | Description |
|
|
193
|
+
| ----------- | ------------ | --------------------------------------- |
|
|
194
|
+
| `homeUrl` | `string` | URL for the home link (defaults to `/`) |
|
|
195
|
+
| `menuItems` | `MenuItem[]` | Array of navigation menu items |
|
|
196
|
+
|
|
197
|
+
#### MenuItem
|
|
198
|
+
|
|
199
|
+
| Property | Type | Description |
|
|
200
|
+
| --------- | --------------------- | ----------------------------------- |
|
|
201
|
+
| `link` | `string` | URL the menu item points to |
|
|
202
|
+
| `text` | `string` | Label text for the menu item |
|
|
203
|
+
| `submenu` | `SecondaryMenuItem[]` | Optional array of nested menu items |
|
|
139
204
|
|
|
140
205
|
## Slots Support
|
|
141
206
|
|
|
@@ -165,46 +230,64 @@ const navigation = {
|
|
|
165
230
|
</Header>
|
|
166
231
|
```
|
|
167
232
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
```css
|
|
171
|
-
.header-actions {
|
|
172
|
-
display: flex;
|
|
173
|
-
gap: 0.5em;
|
|
174
|
-
align-items: center;
|
|
175
|
-
}
|
|
233
|
+
## Comprehensive Example
|
|
176
234
|
|
|
177
|
-
.
|
|
178
|
-
padding: 0.5em 1em;
|
|
179
|
-
border: none;
|
|
180
|
-
border-radius: 6px;
|
|
181
|
-
cursor: pointer;
|
|
182
|
-
font-weight: 500;
|
|
183
|
-
text-decoration: none;
|
|
184
|
-
display: inline-flex;
|
|
185
|
-
align-items: center;
|
|
186
|
-
transition: all 0.2s ease;
|
|
187
|
-
}
|
|
235
|
+
Below is a complete implementation example showcasing custom logo configuration, navigation with a home URL, and theme overrides.
|
|
188
236
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
border: 1px solid #ffffff;
|
|
193
|
-
}
|
|
237
|
+
```astro
|
|
238
|
+
---
|
|
239
|
+
import Header from '@sofidevo/astro-dynamic-header/Header';
|
|
194
240
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
241
|
+
const menuItems = [
|
|
242
|
+
{
|
|
243
|
+
link: "#",
|
|
244
|
+
text: "Services",
|
|
245
|
+
submenu: [
|
|
246
|
+
{ link: "/design", text: "Design" },
|
|
247
|
+
{ link: "/consulting", text: "Consulting" },
|
|
248
|
+
{
|
|
249
|
+
link: "#",
|
|
250
|
+
text: "Web Development",
|
|
251
|
+
submenu: [
|
|
252
|
+
{ link: "/web/frontend", text: "Frontend" },
|
|
253
|
+
{ link: "/web/backend", text: "Backend" },
|
|
254
|
+
{ link: "/web/fullstack", text: "Full Stack" },
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
],
|
|
258
|
+
},
|
|
259
|
+
{ link: "/about", text: "About" },
|
|
260
|
+
{ link: "/contact", text: "Contact" },
|
|
261
|
+
];
|
|
199
262
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
263
|
+
const theme = {
|
|
264
|
+
light: {
|
|
265
|
+
accentColor: "#ff0000",
|
|
266
|
+
backgroundColor: "rgba(255, 255, 255, 0.8)",
|
|
267
|
+
},
|
|
268
|
+
dark: {
|
|
269
|
+
accentColor: "#00ffff",
|
|
270
|
+
backgroundColor: "rgba(20, 20, 20, 0.9)",
|
|
271
|
+
},
|
|
272
|
+
};
|
|
273
|
+
---
|
|
204
274
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
275
|
+
<Header
|
|
276
|
+
headerType="floating"
|
|
277
|
+
preset="dark"
|
|
278
|
+
logo={{
|
|
279
|
+
src: "https://itssofi.dev/img/icons/sofi-icon.webp",
|
|
280
|
+
alt: "My Site Logo",
|
|
281
|
+
width: "44px",
|
|
282
|
+
}}
|
|
283
|
+
navigation={{
|
|
284
|
+
homeUrl: "/",
|
|
285
|
+
menuItems: menuItems,
|
|
286
|
+
}}
|
|
287
|
+
theme={theme}
|
|
288
|
+
>
|
|
289
|
+
<button slot="actions">Login</button>
|
|
290
|
+
</Header>
|
|
208
291
|
```
|
|
209
292
|
|
|
210
293
|
## Header Types
|
|
@@ -242,17 +325,17 @@ The package provides full TypeScript support. You can import types to ensure you
|
|
|
242
325
|
```astro
|
|
243
326
|
---
|
|
244
327
|
import Header from '@sofidevo/astro-dynamic-header/Header';
|
|
245
|
-
import type {
|
|
246
|
-
NavConfig,
|
|
247
|
-
DualThemeConfig,
|
|
328
|
+
import type {
|
|
329
|
+
NavConfig,
|
|
330
|
+
DualThemeConfig,
|
|
248
331
|
MenuItem,
|
|
249
332
|
SecondaryMenuItem
|
|
250
333
|
} from '@sofidevo/astro-dynamic-header';
|
|
251
334
|
|
|
252
335
|
const navigation: NavConfig = {
|
|
253
336
|
menuItems: [
|
|
254
|
-
{
|
|
255
|
-
link: '/products',
|
|
337
|
+
{
|
|
338
|
+
link: '/products',
|
|
256
339
|
text: 'Products',
|
|
257
340
|
submenu: [
|
|
258
341
|
{ link: '/software', text: 'Software' },
|
|
@@ -276,25 +359,25 @@ const theme: DualThemeConfig = {
|
|
|
276
359
|
};
|
|
277
360
|
---
|
|
278
361
|
|
|
279
|
-
<Header
|
|
280
|
-
navigation={navigation}
|
|
281
|
-
theme={theme}
|
|
282
|
-
preset="auto"
|
|
362
|
+
<Header
|
|
363
|
+
navigation={navigation}
|
|
364
|
+
theme={theme}
|
|
365
|
+
preset="auto"
|
|
283
366
|
/>
|
|
284
367
|
```
|
|
285
368
|
|
|
286
369
|
### Available Types
|
|
287
370
|
|
|
288
|
-
| Type
|
|
289
|
-
|
|
290
|
-
| `MenuItem`
|
|
291
|
-
| `SecondaryMenuItem` | Second-level menu item
|
|
292
|
-
| `TertiaryMenuItem`
|
|
293
|
-
| `NavConfig`
|
|
294
|
-
| `ThemeConfig`
|
|
295
|
-
| `DualThemeConfig`
|
|
296
|
-
| `LogoConfig`
|
|
297
|
-
| `HeaderProps`
|
|
371
|
+
| Type | Description |
|
|
372
|
+
| ------------------- | ---------------------------------------------- |
|
|
373
|
+
| `MenuItem` | Top-level menu item with optional properties |
|
|
374
|
+
| `SecondaryMenuItem` | Second-level menu item |
|
|
375
|
+
| `TertiaryMenuItem` | Third-level menu item |
|
|
376
|
+
| `NavConfig` | Main navigation configuration object |
|
|
377
|
+
| `ThemeConfig` | Individual theme settings (colors, blur, etc.) |
|
|
378
|
+
| `DualThemeConfig` | Combined settings for light and dark modes |
|
|
379
|
+
| `LogoConfig` | Logo image and text configuration |
|
|
380
|
+
| `HeaderProps` | Main props for the Header component |
|
|
298
381
|
|
|
299
382
|
## Browser Support
|
|
300
383
|
|
|
@@ -324,7 +407,7 @@ If you encounter import errors, try these solutions:
|
|
|
324
407
|
// tsconfig.json
|
|
325
408
|
{
|
|
326
409
|
"compilerOptions": {
|
|
327
|
-
"moduleResolution": "bundler",
|
|
410
|
+
"moduleResolution": "bundler",
|
|
328
411
|
"allowImportingTsExtensions": true
|
|
329
412
|
}
|
|
330
413
|
}
|
|
@@ -341,47 +424,6 @@ If you encounter import errors, try these solutions:
|
|
|
341
424
|
|
|
342
425
|
Visit our demo website to see the component in action with interactive examples and complete documentation.
|
|
343
426
|
|
|
344
|
-
## Testing
|
|
345
|
-
|
|
346
|
-
This project includes a comprehensive test suite with 34 tests covering all critical functionality.
|
|
347
|
-
|
|
348
|
-
### Running Tests
|
|
349
|
-
|
|
350
|
-
```bash
|
|
351
|
-
# Run all tests
|
|
352
|
-
npm test
|
|
353
|
-
|
|
354
|
-
# Run tests in watch mode
|
|
355
|
-
npm run test:watch
|
|
356
|
-
|
|
357
|
-
# Run tests with coverage report
|
|
358
|
-
npm run test:coverage
|
|
359
|
-
```
|
|
360
|
-
|
|
361
|
-
### Test Coverage
|
|
362
|
-
|
|
363
|
-
The test suite covers:
|
|
364
|
-
|
|
365
|
-
#### Component Logic Tests
|
|
366
|
-
|
|
367
|
-
- **Header Component** (4 tests): Hamburger controller functionality, menu toggle behavior
|
|
368
|
-
- **HamburgerButton Component** (10 tests): Button states, responsive behavior, accessibility
|
|
369
|
-
- **MobileNav Component** (7 tests): Dropdown structure, nested submenus, conditional rendering
|
|
370
|
-
- **NavMenu Component** (6 tests): Dynamic positioning, submenu interactions, viewport adjustments
|
|
371
|
-
|
|
372
|
-
#### Integration Tests (7 tests)
|
|
373
|
-
|
|
374
|
-
- Component interaction flows
|
|
375
|
-
- Responsive behavior between mobile/desktop
|
|
376
|
-
- Keyboard navigation and accessibility
|
|
377
|
-
- Menu state management during navigation
|
|
378
|
-
|
|
379
|
-
### Test Technologies
|
|
380
|
-
|
|
381
|
-
- **Vitest**: Fast testing framework
|
|
382
|
-
- **jsdom**: DOM simulation for component testing
|
|
383
|
-
- **TypeScript**: Type-safe test writing
|
|
384
|
-
|
|
385
427
|
## License
|
|
386
428
|
|
|
387
429
|
MIT License - see the [LICENSE](./LICENSE) file for details.
|
package/package.json
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
export interface Props {
|
|
3
|
+
/** Override the default color of the hamburger lines */
|
|
3
4
|
color?: string;
|
|
4
5
|
}
|
|
5
6
|
|
|
6
|
-
const { color = "var(--color-hamburger-lines, #fff)" } =
|
|
7
|
+
const { color = "var(--text-color, var(--color-hamburger-lines, #fff))" } =
|
|
8
|
+
Astro.props;
|
|
7
9
|
---
|
|
8
10
|
|
|
9
11
|
<button
|
|
@@ -23,14 +25,16 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
23
25
|
font: inherit;
|
|
24
26
|
overflow: visible;
|
|
25
27
|
margin: 0;
|
|
26
|
-
padding:
|
|
28
|
+
padding: 10px;
|
|
27
29
|
cursor: pointer;
|
|
28
30
|
transition-timing-function: linear;
|
|
29
31
|
transition-duration: 0.15s;
|
|
30
|
-
transition-property: opacity, filter;
|
|
32
|
+
transition-property: opacity, filter, border-color;
|
|
31
33
|
text-transform: none;
|
|
32
34
|
color: inherit;
|
|
33
|
-
border:
|
|
35
|
+
border: 1px solid
|
|
36
|
+
color-mix(in srgb, var(--text-color, #fff) 25%, transparent);
|
|
37
|
+
border-radius: 8px;
|
|
34
38
|
background-color: transparent;
|
|
35
39
|
|
|
36
40
|
@media screen and (max-width: 768px) {
|
|
@@ -61,7 +65,10 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
61
65
|
.hamburger-inner,
|
|
62
66
|
.hamburger-inner::before,
|
|
63
67
|
.hamburger-inner::after {
|
|
64
|
-
background-color: var(
|
|
68
|
+
background-color: var(
|
|
69
|
+
--hamburger-color,
|
|
70
|
+
var(--color-hamburger-lines, #fff)
|
|
71
|
+
);
|
|
65
72
|
position: absolute;
|
|
66
73
|
width: 40px;
|
|
67
74
|
height: 4px;
|
|
@@ -88,12 +95,16 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
88
95
|
}
|
|
89
96
|
|
|
90
97
|
.hamburger--collapse .hamburger-inner:before {
|
|
91
|
-
transition:
|
|
98
|
+
transition:
|
|
99
|
+
top 0.12s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s,
|
|
100
|
+
transform 0.13s cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
|
92
101
|
}
|
|
93
102
|
|
|
94
103
|
.hamburger--collapse .hamburger-inner:after {
|
|
95
104
|
top: -20px;
|
|
96
|
-
transition:
|
|
105
|
+
transition:
|
|
106
|
+
top 0.2s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s,
|
|
107
|
+
opacity 0.1s linear;
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
/* Animation */
|
|
@@ -105,13 +116,17 @@ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
|
|
|
105
116
|
|
|
106
117
|
.hamburger--collapse.is-active .hamburger-inner:before {
|
|
107
118
|
top: 0;
|
|
108
|
-
transition:
|
|
119
|
+
transition:
|
|
120
|
+
top 0.1s cubic-bezier(0.33333, 0, 0.66667, 0.33333) 0.16s,
|
|
121
|
+
transform 0.13s cubic-bezier(0.215, 0.61, 0.355, 1) 0.25s;
|
|
109
122
|
transform: rotate(-90deg);
|
|
110
123
|
}
|
|
111
124
|
|
|
112
125
|
.hamburger--collapse.is-active .hamburger-inner:after {
|
|
113
126
|
top: 0;
|
|
114
|
-
transition:
|
|
127
|
+
transition:
|
|
128
|
+
top 0.2s cubic-bezier(0.33333, 0, 0.66667, 0.33333),
|
|
129
|
+
opacity 0.1s linear 0.22s;
|
|
115
130
|
opacity: 0;
|
|
116
131
|
}
|
|
117
132
|
</style>
|
package/src/Header.astro
CHANGED
|
@@ -11,11 +11,31 @@ import type {
|
|
|
11
11
|
} from "./index.js";
|
|
12
12
|
|
|
13
13
|
export interface Props {
|
|
14
|
+
/**
|
|
15
|
+
* Layout style.
|
|
16
|
+
* - "floating": Centered with max-width and rounded corners.
|
|
17
|
+
* - "fullscreen": Full width with no border radius.
|
|
18
|
+
* @default "floating"
|
|
19
|
+
*/
|
|
14
20
|
headerType?: "floating" | "fullscreen";
|
|
21
|
+
/**
|
|
22
|
+
* Theme behavior.
|
|
23
|
+
* - "light": Force light mode.
|
|
24
|
+
* - "dark": Force dark mode.
|
|
25
|
+
* - "auto": Detects .dark class on the root element.
|
|
26
|
+
* @default "auto"
|
|
27
|
+
*/
|
|
15
28
|
preset?: "light" | "dark" | "auto";
|
|
29
|
+
/** Logo configuration object (src, alt, text, etc.) */
|
|
16
30
|
logo?: LogoConfig;
|
|
31
|
+
/** Navigation links and structure. */
|
|
17
32
|
navigation?: NavConfig;
|
|
33
|
+
/** Custom theme overrides for light and dark modes. */
|
|
18
34
|
theme?: DualThemeConfig;
|
|
35
|
+
/**
|
|
36
|
+
* Custom CSS classes for specific internal elements.
|
|
37
|
+
* @example { header: "shadow-lg", nav: "ml-auto" }
|
|
38
|
+
*/
|
|
19
39
|
classNames?: CustomClassNames;
|
|
20
40
|
}
|
|
21
41
|
|
|
@@ -28,6 +48,20 @@ const {
|
|
|
28
48
|
classNames = {},
|
|
29
49
|
} = Astro.props;
|
|
30
50
|
|
|
51
|
+
if (import.meta.env.DEV) {
|
|
52
|
+
// Check if logo is being passed as a string (legacy)
|
|
53
|
+
if (typeof logo === "string") {
|
|
54
|
+
console.warn(
|
|
55
|
+
"[@sofidevo/astro-dynamic-header] BREAKING CHANGE: The 'logo' prop now expects an object. Please use logo={{ src: '...' }} instead.",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
if (Array.isArray(navigation)) {
|
|
59
|
+
console.warn(
|
|
60
|
+
"[@sofidevo/astro-dynamic-header] BREAKING CHANGE: The 'navigation' prop now expects an object with 'menuItems'. Please use navigation={{ menuItems: [...] }} instead.",
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
31
65
|
// Default theme configuration
|
|
32
66
|
const defaultThemes = {
|
|
33
67
|
light: {
|
|
@@ -119,7 +153,12 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
119
153
|
style={{ zIndex: lightTheme.zIndex }}
|
|
120
154
|
>
|
|
121
155
|
<header
|
|
122
|
-
class:list={[
|
|
156
|
+
class:list={[
|
|
157
|
+
"header",
|
|
158
|
+
`header--${headerType}`,
|
|
159
|
+
forcedClass,
|
|
160
|
+
classNames.header,
|
|
161
|
+
]}
|
|
123
162
|
style={{
|
|
124
163
|
"--l-bg": lightTheme.backgroundColor,
|
|
125
164
|
"--l-bg-opaque": lightTheme.backgroundColorOpaque,
|
|
@@ -151,7 +190,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
151
190
|
)
|
|
152
191
|
}
|
|
153
192
|
</a>
|
|
154
|
-
|
|
193
|
+
|
|
155
194
|
<div class:list={["nav-menu-wrapper", classNames.nav]}>
|
|
156
195
|
<NavMenu menuItems={menuItems} />
|
|
157
196
|
</div>
|
|
@@ -163,13 +202,10 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
163
202
|
</div>
|
|
164
203
|
)
|
|
165
204
|
}
|
|
166
|
-
|
|
205
|
+
|
|
167
206
|
<HamburgerButton />
|
|
168
|
-
|
|
169
|
-
<MobileNav
|
|
170
|
-
menuItems={menuItems}
|
|
171
|
-
type={headerType}
|
|
172
|
-
>
|
|
207
|
+
|
|
208
|
+
<MobileNav menuItems={menuItems} type={headerType}>
|
|
173
209
|
{
|
|
174
210
|
Astro.slots.has("actions") && (
|
|
175
211
|
<div class="actions-mobile" slot="slot-panel">
|
|
@@ -198,7 +234,9 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
198
234
|
backdrop-filter: var(--backdrop-blur);
|
|
199
235
|
-webkit-backdrop-filter: var(--backdrop-blur);
|
|
200
236
|
color: var(--text-color);
|
|
201
|
-
transition:
|
|
237
|
+
transition:
|
|
238
|
+
background-color 0.3s ease,
|
|
239
|
+
color 0.3s ease;
|
|
202
240
|
|
|
203
241
|
@media (width < 768px) {
|
|
204
242
|
align-self: flex-end;
|
|
@@ -284,7 +322,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
284
322
|
font-weight: 600;
|
|
285
323
|
}
|
|
286
324
|
}
|
|
287
|
-
|
|
325
|
+
|
|
288
326
|
.header__logo {
|
|
289
327
|
margin-right: 1em;
|
|
290
328
|
object-fit: contain;
|
package/src/MobileNav.astro
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
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;
|
|
9
13
|
}
|
|
10
14
|
|
|
@@ -38,64 +42,73 @@ const pagePathname = Astro.url.pathname;
|
|
|
38
42
|
)
|
|
39
43
|
}
|
|
40
44
|
{
|
|
41
|
-
menuItems?.map((item: MenuItem) =>
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
45
|
+
menuItems?.map((item: MenuItem) => {
|
|
46
|
+
const isActive = pagePathname === item.link;
|
|
47
|
+
return (
|
|
48
|
+
<li>
|
|
49
|
+
{item.submenu ? (
|
|
50
|
+
<details class="mobile-details" open={item.submenu.some(sub => pagePathname === sub.link)}>
|
|
51
|
+
<summary class:list={["menu__summary", { active: isActive }]}>
|
|
52
|
+
{item.text}
|
|
53
|
+
<iconify-icon
|
|
54
|
+
class="iconify-arrow"
|
|
55
|
+
icon="codicon:triangle-down"
|
|
56
|
+
width="15"
|
|
57
|
+
height="15"
|
|
58
|
+
style="color: var(--accent-color);"
|
|
59
|
+
/>
|
|
60
|
+
</summary>
|
|
61
|
+
<ol class="mobile-submenu">
|
|
62
|
+
{item?.submenu?.map((sub: SecondaryMenuItem) => {
|
|
63
|
+
const isSubActive = pagePathname === sub.link;
|
|
64
|
+
return (
|
|
65
|
+
<li>
|
|
66
|
+
{sub.submenu && sub.submenu.length > 0 ? (
|
|
67
|
+
<details class="mobile-details" open={sub.submenu.some(subsub => pagePathname === subsub.link)}>
|
|
68
|
+
<summary class:list={["menu__summary", { active: isSubActive }]}>
|
|
69
|
+
{sub.text}
|
|
70
|
+
<iconify-icon
|
|
71
|
+
class="iconify-arrow"
|
|
72
|
+
icon="codicon:triangle-down"
|
|
73
|
+
width="15"
|
|
74
|
+
height="15"
|
|
75
|
+
style="color: var(--accent-color);"
|
|
76
|
+
/>
|
|
77
|
+
</summary>
|
|
78
|
+
<ol class="mobile-subsubmenu">
|
|
79
|
+
{sub.submenu.map((subsub: TertiaryMenuItem) => {
|
|
80
|
+
const isSubSubActive = pagePathname === subsub.link;
|
|
81
|
+
return (
|
|
82
|
+
<li class="mobile-submenu__item secondary">
|
|
83
|
+
<a
|
|
84
|
+
class:list={["menu__link", "mobile-menu__link", { active: isSubSubActive }]}
|
|
85
|
+
href={subsub.link}
|
|
86
|
+
>
|
|
87
|
+
{subsub.text}
|
|
88
|
+
</a>
|
|
89
|
+
</li>
|
|
90
|
+
);
|
|
91
|
+
})}
|
|
92
|
+
</ol>
|
|
93
|
+
</details>
|
|
94
|
+
) : (
|
|
95
|
+
<a class:list={["menu__link", "mobile-menu__link", { active: isSubActive }]} href={sub.link}>
|
|
96
|
+
{sub.text}
|
|
97
|
+
</a>
|
|
98
|
+
)}
|
|
99
|
+
</li>
|
|
100
|
+
);
|
|
101
|
+
})}
|
|
102
|
+
</ol>
|
|
103
|
+
</details>
|
|
104
|
+
) : (
|
|
105
|
+
<a class:list={["mobile-menu__link", { active: isActive }]} href={item.link}>
|
|
46
106
|
{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
|
-
))
|
|
107
|
+
</a>
|
|
108
|
+
)}
|
|
109
|
+
</li>
|
|
110
|
+
);
|
|
111
|
+
})
|
|
99
112
|
}
|
|
100
113
|
</ol>
|
|
101
114
|
<slot name="slot-panel" />
|
|
@@ -122,7 +135,15 @@ const pagePathname = Astro.url.pathname;
|
|
|
122
135
|
}
|
|
123
136
|
|
|
124
137
|
a {
|
|
125
|
-
color:
|
|
138
|
+
color: color-mix(
|
|
139
|
+
in srgb,
|
|
140
|
+
var(--accent-color, #00ffff) 30%,
|
|
141
|
+
var(--text-color, #ffffff) 70%
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.active {
|
|
146
|
+
color: var(--accent-color, #00ffff) !important;
|
|
126
147
|
}
|
|
127
148
|
}
|
|
128
149
|
|
|
@@ -139,7 +160,7 @@ const pagePathname = Astro.url.pathname;
|
|
|
139
160
|
|
|
140
161
|
.mobile-menu li a {
|
|
141
162
|
border: solid 1px
|
|
142
|
-
color-mix(in srgb, var(--accent-color, #00ffff)
|
|
163
|
+
color-mix(in srgb, var(--accent-color, #00ffff) 15%, transparent);
|
|
143
164
|
border-radius: 4px;
|
|
144
165
|
width: 100%;
|
|
145
166
|
height: 4rem;
|
package/src/NavMenu.astro
CHANGED
|
@@ -2,8 +2,23 @@
|
|
|
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
|
+
*/
|
|
21
|
+
|
|
7
22
|
homeText?: string;
|
|
8
23
|
}
|
|
9
24
|
|
package/src/index.ts
CHANGED
|
@@ -1,71 +1,192 @@
|
|
|
1
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
|
|
|
49
|
+
/**
|
|
50
|
+
* Configuration for the site logo.
|
|
51
|
+
*/
|
|
27
52
|
export interface LogoConfig {
|
|
53
|
+
/**
|
|
54
|
+
* The URL of the logo image.
|
|
55
|
+
* @example "/assets/logo.png"
|
|
56
|
+
* @default "/logo.png"
|
|
57
|
+
*/
|
|
28
58
|
src?: string;
|
|
59
|
+
/** Alternative text for the logo image */
|
|
29
60
|
alt?: string;
|
|
61
|
+
/**
|
|
62
|
+
* CSS width for the logo.
|
|
63
|
+
* @example "150px" or "4rem"
|
|
64
|
+
* @default "55px"
|
|
65
|
+
*/
|
|
30
66
|
width?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Optional text to display next to the logo image.
|
|
69
|
+
* @example "My Awesome Site"
|
|
70
|
+
*/
|
|
31
71
|
text?: string;
|
|
72
|
+
/**
|
|
73
|
+
* CSS font-size for the logo text.
|
|
74
|
+
* @default "1em"
|
|
75
|
+
*/
|
|
32
76
|
textSize?: string;
|
|
77
|
+
/**
|
|
78
|
+
* CSS color for the logo text.
|
|
79
|
+
* If not provided, it will inherit from the theme's text color.
|
|
80
|
+
* @default "inherit"
|
|
81
|
+
*/
|
|
33
82
|
textColor?: string;
|
|
34
83
|
}
|
|
35
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Configuration for the main navigation.
|
|
87
|
+
*/
|
|
36
88
|
export interface NavConfig {
|
|
89
|
+
/**
|
|
90
|
+
* The URL for the home link.
|
|
91
|
+
* @default "/"
|
|
92
|
+
*/
|
|
37
93
|
homeUrl?: string;
|
|
94
|
+
/**
|
|
95
|
+
* Array of top-level menu items.
|
|
96
|
+
* @example [{ link: "/about", text: "About Us" }]
|
|
97
|
+
*/
|
|
38
98
|
menuItems?: MenuItem[];
|
|
39
99
|
}
|
|
40
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Individual theme settings for a specific state (light/dark).
|
|
103
|
+
*/
|
|
41
104
|
export interface ThemeConfig {
|
|
105
|
+
/**
|
|
106
|
+
* Main background color. Supports hex, rgb, rgba, etc.
|
|
107
|
+
* @example "rgba(255, 255, 255, 0.9)"
|
|
108
|
+
*/
|
|
42
109
|
backgroundColor?: string;
|
|
110
|
+
/**
|
|
111
|
+
* Solid background color for submenus and mobile panels to ensure readability.
|
|
112
|
+
* @example "#ffffff"
|
|
113
|
+
*/
|
|
43
114
|
backgroundColorOpaque?: string;
|
|
115
|
+
/**
|
|
116
|
+
* CSS backdrop-filter blur value.
|
|
117
|
+
* @default "blur(20px)"
|
|
118
|
+
*/
|
|
44
119
|
backdropBlur?: string;
|
|
120
|
+
/**
|
|
121
|
+
* CSS z-index for the header container.
|
|
122
|
+
* @default 10
|
|
123
|
+
*/
|
|
45
124
|
zIndex?: number;
|
|
125
|
+
/**
|
|
126
|
+
* Primary text color for navigation and logo.
|
|
127
|
+
*/
|
|
46
128
|
textColor?: string;
|
|
129
|
+
/**
|
|
130
|
+
* Color for highlights, active states, underscores, and small borders.
|
|
131
|
+
*/
|
|
47
132
|
accentColor?: string;
|
|
48
133
|
}
|
|
49
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Combined theme configuration for both light and dark modes.
|
|
137
|
+
*/
|
|
50
138
|
export interface DualThemeConfig {
|
|
139
|
+
/** Settings applied when light mode is active. */
|
|
51
140
|
light?: ThemeConfig;
|
|
141
|
+
/** Settings applied when dark mode is active. */
|
|
52
142
|
dark?: ThemeConfig;
|
|
53
143
|
}
|
|
54
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Custom CSS class names for deep customization.
|
|
147
|
+
*/
|
|
55
148
|
export interface CustomClassNames {
|
|
149
|
+
/** Class for the outermost fixed container */
|
|
56
150
|
container?: string;
|
|
151
|
+
/** Class for the main header element */
|
|
57
152
|
header?: string;
|
|
153
|
+
/** Class for the logo anchor tag */
|
|
58
154
|
logo?: string;
|
|
155
|
+
/** Class for the logo span text */
|
|
59
156
|
logoText?: string;
|
|
157
|
+
/** Class for the desktop navigation wrapper */
|
|
60
158
|
nav?: string;
|
|
61
159
|
}
|
|
62
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";
|
|
180
|
+
/** Logo configuration object. */
|
|
66
181
|
logo?: LogoConfig;
|
|
182
|
+
/** Navigation links and structure. */
|
|
67
183
|
navigation?: NavConfig;
|
|
184
|
+
/** Custom theme overrides. See @interface DualThemeConfig */
|
|
68
185
|
theme?: DualThemeConfig;
|
|
186
|
+
/**
|
|
187
|
+
* Custom CSS classes for injecting utility classes (e.g., Tailwind).
|
|
188
|
+
* @example { header: "shadow-lg", logoText: "font-bold" }
|
|
189
|
+
*/
|
|
69
190
|
classNames?: CustomClassNames;
|
|
70
191
|
}
|
|
71
192
|
|