@sofidevo/astro-dynamic-header 1.1.34 → 2.0.0

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 CHANGED
@@ -12,9 +12,9 @@ A dynamic, responsive header component for Astro projects that can switch betwee
12
12
  - **Customizable**: Extensive customization options for colors, sizes, and behavior
13
13
  - **Astro Optimized**: Built specifically for Astro framework
14
14
 
15
-
16
15
  ### Live demo
17
- [https://base-astro-psi.vercel.app/fullscreen-demo](https://base-astro-psi.vercel.app/fullscreen-demo)
16
+
17
+ [https://base-astro-psi.vercel.app/fullscreen-demo](https://base-astro-psi.vercel.app/fullscreen-demo)
18
18
 
19
19
  ## Installation
20
20
 
@@ -34,86 +34,49 @@ Add this to your main layout or in the `<head>` section of your Astro pages.
34
34
 
35
35
  ## Quick Start
36
36
 
37
- ### Basic Usage
37
+ ### Basic Usage (Automatic Theme)
38
+
39
+ By default, the header uses `preset="auto"`, which automatically detects the theme based on a `.dark` class on your root element (`html` or `body`).
38
40
 
39
41
  ```astro
40
42
  ---
41
- // Option 1: Import from direct subpath (recommended)
42
43
  import Header from '@sofidevo/astro-dynamic-header/Header';
43
44
 
44
- // Option 2: Import from main entry point with types
45
- import { HeaderProps, type MenuItemType } from '@sofidevo/astro-dynamic-header';
46
-
47
- const menuItems = [
48
- { link: '/about', text: 'About' },
49
- { link: '/contact', text: 'Contact' },
50
- ];
45
+ const navigation = {
46
+ menuItems: [
47
+ { link: '/about', text: 'About' },
48
+ ]
49
+ };
51
50
  ---
52
51
 
53
- <Header
54
- headerType="floating"
55
- logoSrc="/logo.png"
56
- menuItems={menuItems}
57
- />
52
+ <!-- Detects .dark class on root automatically -->
53
+ <Header navigation={navigation} />
58
54
  ```
59
55
 
60
- ### TypeScript Configuration
56
+ ### Advanced Usage (Dual-Theme Customization)
61
57
 
62
- To ensure imports work correctly in your Astro project, make sure your `tsconfig.json` has the appropriate configuration:
63
-
64
- ```json
65
- {
66
- "compilerOptions": {
67
- "moduleResolution": "bundler",
68
- "allowImportingTsExtensions": true,
69
- "strict": true,
70
- "noEmit": true,
71
- "jsx": "preserve"
72
- },
73
- "extends": "astro/tsconfigs/strict"
74
- }
75
- ```
76
-
77
- ### Advanced Usage
58
+ You can provide custom colors for both light and dark modes simultaneously.
78
59
 
79
60
  ```astro
80
61
  ---
81
62
  import Header from '@sofidevo/astro-dynamic-header/Header';
82
- import type { MenuItemType } from '@sofidevo/astro-dynamic-header';
83
-
84
- const menuItems = [
85
- {
86
- link: '#',
87
- text: 'Services',
88
- submenu: [
89
- {
90
- link: '#',
91
- text: 'Web Development',
92
- submenu: [
93
- { link: '/web/frontend', text: 'Frontend' },
94
- { link: '/web/backend', text: 'Backend' },
95
- { link: '/web/fullstack', text: 'Full Stack' },
96
- ],
97
- },
98
- { link: '/design', text: 'Design' },
99
- { link: '/consulting', text: 'Consulting' },
100
- ],
63
+
64
+ const theme = {
65
+ light: {
66
+ accentColor: "#3e1c71",
67
+ backgroundColor: "rgba(255, 255, 255, 0.8)"
101
68
  },
102
- { link: '/about', text: 'About' },
103
- { link: '/contact', text: 'Contact' },
104
- ];
69
+ dark: {
70
+ accentColor: "#00ffff",
71
+ backgroundColor: "rgba(20, 20, 20, 0.9)"
72
+ }
73
+ };
105
74
  ---
106
75
 
107
76
  <Header
108
- headerType="fullscreen"
109
- logoSrc="/logo.png"
110
- logoAlt="My Company"
111
- logoWidth="150px"
112
- homeUrl="/"
113
- menuItems={menuItems}
114
- backgroundColor="#000000dd"
115
- backdropBlur="blur(15px)"
116
- zIndex={100}
77
+ preset="auto"
78
+ theme={theme}
79
+ navigation={navigation}
117
80
  />
118
81
  ```
119
82
 
@@ -121,105 +84,83 @@ const menuItems = [
121
84
 
122
85
  ### Header Component
123
86
 
124
- | Prop | Type | Default | Description |
125
- |------|------|---------|-------------|
126
- | `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Header layout style |
127
- | `logoSrc` | `string` | `"/logo.png"` | Logo image source |
128
- | `logoAlt` | `string` | `"Logo"` | Logo alt text |
129
- | `logoWidth` | `string` | `"55px"` | Logo width |
130
- | `logoText` | `string` | `"Your logo text"` | Text displayed next to the logo |
131
- | `logoTextSize` | `string` | `"1em"` | Font size for logo text |
132
- | `logoTextColor` | `string` | `"#ffffff"` | Color for logo text |
133
- | `homeUrl` | `string` | `"/"` | Home page URL |
134
- | `menuItems` | `MenuItemType[]` | `[]` | Navigation menu items |
135
- | `backgroundColor` | `string` | `"#0d0d0dcc"` | Header background color |
136
- | `backdropBlur` | `string` | `"blur(20px)"` | Backdrop filter blur |
137
- | `zIndex` | `number` | `10` | CSS z-index value |
138
-
139
- ### Menu Item Structure
140
-
141
- ```typescript
142
- interface MenuItemType {
143
- link: string;
144
- text: string;
145
- submenu?: MenuItemType[];
146
- }
87
+ | Prop | Type | Default | Description |
88
+ | ------------ | ----------------------------- | ------------ | ------------------------------------------ |
89
+ | `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Header layout style |
90
+ | `preset` | `"light" \| "dark" \| "auto"` | `"auto"` | Theme behavior. `auto` follows root class. |
91
+ | `logo` | `LogoConfig` | `{}` | Logo configuration object |
92
+ | `navigation` | `NavConfig` | `{}` | Navigation configuration object |
93
+ | `theme` | `DualThemeConfig` | `{}` | Custom theme overrides for light/dark |
94
+ | `classNames` | `CustomClassNames` | `{}` | Custom class names for CSS Modules |
95
+
96
+ ### Config Objects
97
+
98
+ #### DualThemeConfig
99
+
100
+ | Propery | Type | Description |
101
+ | ------- | ------------- | ---------------------------- |
102
+ | `light` | `ThemeConfig` | Styles applied in light mode |
103
+ | `dark` | `ThemeConfig` | Styles applied in dark mode |
104
+
105
+ #### ThemeConfig
106
+
107
+ | Propery | Type | Default |
108
+ | ---------------------- | -------- | ---------------- |
109
+ | `backgroundColor` | `string` | _Preset default_ |
110
+ | `backgroundColorOpaque`| `string` | _Preset default_ |
111
+ | `backdropBlur` | `string` | `"blur(20px)"` |
112
+ | `zIndex` | `number` | `10` |
113
+ | `textColor` | `string` | _Preset default_ |
114
+ | `accentColor` | `string` | _Preset default_ |
115
+
116
+ > [!IMPORTANT]
117
+ > **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**.
118
+ >
119
+ > If you use a transparent `backgroundColor` (e.g., `rgba`), remember to also provide its solid counterpart in `backgroundColorOpaque`.
120
+
121
+ ```astro
122
+ <Header theme={{
123
+ light: {
124
+ backgroundColor: "rgba(255, 255, 255, 0.7)", // Transparent header body
125
+ backgroundColorOpaque: "#ffffff", // Solid submenus
126
+ }
127
+ }} />
147
128
  ```
148
129
 
130
+ #### CustomClassNames
131
+
132
+ | Propery | Type | Description |
133
+ | ----------- | -------- | -------------------------------- |
134
+ | `container` | `string` | Main container class |
135
+ | `header` | `string` | Header element class |
136
+ | `logo` | `string` | Logo link container class |
137
+ | `logoText` | `string` | Logo text class |
138
+ | `nav` | `string` | Desktop navigation wrapper class |
139
+
149
140
  ## Slots Support
150
141
 
151
142
  The Header component provides a flexible slot system that allows you to add additional content:
152
143
 
153
144
  ### Available Slots
154
145
 
155
- | Slot Name | Location | Visibility | Description |
156
- |-----------|----------|------------|-----------|
157
- | `actions` | Header & Mobile panel | Responsive visibility | Add action buttons (login, signup, etc.) that appear in both desktop and mobile |
158
-
159
- ### Actions Slot Behavior
146
+ | Slot Name | Location | Visibility | Description |
147
+ | --------- | --------------------- | --------------------- | ---------------------------------------- |
148
+ | `actions` | Header & Mobile panel | Responsive visibility | Add action buttons (login, signup, etc.) |
160
149
 
161
- The `actions` slot is intelligent and automatically manages visibility:
162
- - **Desktop (≥768px)**: Shows actions in the main header after navigation
163
- - **Mobile (<768px)**: Shows same actions inside the mobile menu panel
164
- - **Automatic hiding**: Desktop version is hidden on mobile, mobile version is hidden on desktop
165
-
166
- ### Slot Examples
167
-
168
- #### Using the Actions Slot
150
+ ### Example with Slots
169
151
 
170
152
  ```astro
171
153
  ---
172
154
  import Header from '@sofidevo/astro-dynamic-header/Header';
173
155
 
174
- const menuItems = [
175
- { link: '/about', text: 'About' },
176
- { link: '/contact', text: 'Contact' },
177
- ];
156
+ const navigation = {
157
+ menuItems: [{ link: '/about', text: 'About' }]
158
+ };
178
159
  ---
179
160
 
180
- <Header
181
- headerType="floating"
182
- logoSrc="/logo.png"
183
- logoText="My Company"
184
- menuItems={menuItems}
185
- >
161
+ <Header navigation={navigation}>
186
162
  <div slot="actions">
187
163
  <button class="login-btn">Login</button>
188
- <button class="signup-btn">Sign Up</button>
189
- </div>
190
- </Header>
191
- ```
192
-
193
- #### Complete Example with All Props
194
-
195
- ```astro
196
- ---
197
- import Header from '@sofidevo/astro-dynamic-header/Header';
198
-
199
- const menuItems = [
200
- { link: '/about', text: 'About' },
201
- { link: '/services', text: 'Services' },
202
- { link: '/contact', text: 'Contact' },
203
- ];
204
- ---
205
-
206
- <Header
207
- headerType="fullscreen"
208
- logoSrc="/logo.png"
209
- logoAlt="My Company"
210
- logoWidth="60px"
211
- logoText="My Company"
212
- logoTextSize="1.2em"
213
- logoTextColor="#ffffff"
214
- homeUrl="/"
215
- menuItems={menuItems}
216
- backgroundColor="#000000dd"
217
- backdropBlur="blur(15px)"
218
- zIndex={100}
219
- >
220
- <div slot="actions" class="header-actions">
221
- <button class="btn btn-outline">Login</button>
222
- <button class="btn btn-primary">Get Started</button>
223
164
  </div>
224
165
  </Header>
225
166
  ```
@@ -265,21 +206,23 @@ const menuItems = [
265
206
  background: #00cccc;
266
207
  }
267
208
  ```
209
+
268
210
  ## Header Types
269
211
 
270
212
  ### Floating Header
213
+
271
214
  - Centered with max-width constraint
272
215
  - Rounded corners
273
216
  - Padding around container
274
217
  - Perfect for modern, card-like designs
275
218
 
276
219
  ### Fullscreen Header
220
+
277
221
  - Full viewport width
278
222
  - No border radius
279
223
  - Edge-to-edge design
280
224
  - Ideal for traditional website layouts
281
225
 
282
-
283
226
  ## Styling and Customization
284
227
 
285
228
  The component uses CSS custom properties that you can override:
@@ -294,24 +237,71 @@ The component uses CSS custom properties that you can override:
294
237
 
295
238
  ## TypeScript Support
296
239
 
297
- Full TypeScript support with exported interfaces:
240
+ The package provides full TypeScript support. You can import types to ensure your configuration is correct:
298
241
 
299
- ```typescript
300
- import type {
301
- MenuItemType,
302
- HeaderProps,
303
- NavMenuProps,
304
- MobileNavProps,
305
- HamburgerButtonProps
242
+ ```astro
243
+ ---
244
+ import Header from '@sofidevo/astro-dynamic-header/Header';
245
+ import type {
246
+ NavConfig,
247
+ DualThemeConfig,
248
+ MenuItem,
249
+ SecondaryMenuItem
306
250
  } from '@sofidevo/astro-dynamic-header';
251
+
252
+ const navigation: NavConfig = {
253
+ menuItems: [
254
+ {
255
+ link: '/products',
256
+ text: 'Products',
257
+ submenu: [
258
+ { link: '/software', text: 'Software' },
259
+ { link: '/hardware', text: 'Hardware' }
260
+ ]
261
+ }
262
+ ]
263
+ };
264
+
265
+ const theme: DualThemeConfig = {
266
+ light: {
267
+ accentColor: "#3e1c71",
268
+ backgroundColor: "rgba(255, 255, 255, 0.8)",
269
+ backgroundColorOpaque: "#ffffff"
270
+ },
271
+ dark: {
272
+ accentColor: "#00ffff",
273
+ backgroundColor: "rgba(10, 10, 10, 0.9)",
274
+ backgroundColorOpaque: "#0a0a0a"
275
+ }
276
+ };
277
+ ---
278
+
279
+ <Header
280
+ navigation={navigation}
281
+ theme={theme}
282
+ preset="auto"
283
+ />
307
284
  ```
308
285
 
286
+ ### Available Types
287
+
288
+ | Type | Description |
289
+ |------|-------------|
290
+ | `MenuItem` | Top-level menu item with optional properties |
291
+ | `SecondaryMenuItem` | Second-level menu item |
292
+ | `TertiaryMenuItem` | Third-level menu item |
293
+ | `NavConfig` | Main navigation configuration object |
294
+ | `ThemeConfig` | Individual theme settings (colors, blur, etc.) |
295
+ | `DualThemeConfig` | Combined settings for light and dark modes |
296
+ | `LogoConfig` | Logo image and text configuration |
297
+ | `HeaderProps` | Main props for the Header component |
298
+
309
299
  ## Browser Support
310
300
 
311
- - All modern browsers
312
- - Mobile responsive design
313
- - Supports CSS `backdrop-filter`
314
- - Graceful degradation for older browsers
301
+ - All modern browsers (Chrome, Firefox, Safari, Edge)
302
+ - Mobile responsive design with optimized touch targets
303
+ - Supports CSS `backdrop-filter` for glassmorphism
304
+ - Automatic theme switching based on OS or site preference via `.dark` class
315
305
 
316
306
  ## Troubleshooting
317
307
 
@@ -320,29 +310,26 @@ import type {
320
310
  If you encounter import errors, try these solutions:
321
311
 
322
312
  1. **Use direct subpath import:**
313
+
323
314
  ```astro
324
315
  import Header from '@sofidevo/astro-dynamic-header/Header';
325
316
  ```
326
317
 
327
- 2. **Verify TypeScript configuration:**
318
+ 2. **Check relative imports in TS:**
319
+ In some environments (like `node16`), you might need to use the `.js` extension even for TypeScript files when importing from the package internals, though the main entry point handles this for you.
320
+
321
+ 3. **Verify TypeScript configuration:**
322
+
328
323
  ```json
329
324
  // tsconfig.json
330
325
  {
331
326
  "compilerOptions": {
332
- "moduleResolution": "bundler", // or "nodenext"
327
+ "moduleResolution": "bundler",
333
328
  "allowImportingTsExtensions": true
334
329
  }
335
330
  }
336
331
  ```
337
332
 
338
- 3. **Import types separately:**
339
- ```astro
340
- ---
341
- import Header from '@sofidevo/astro-dynamic-header/Header';
342
- import type { MenuItemType } from '@sofidevo/astro-dynamic-header';
343
- ---
344
- ```
345
-
346
333
  ### Compatibility
347
334
 
348
335
  - Astro 4.x and 5.x
@@ -376,18 +363,21 @@ npm run test:coverage
376
363
  The test suite covers:
377
364
 
378
365
  #### Component Logic Tests
366
+
379
367
  - **Header Component** (4 tests): Hamburger controller functionality, menu toggle behavior
380
368
  - **HamburgerButton Component** (10 tests): Button states, responsive behavior, accessibility
381
369
  - **MobileNav Component** (7 tests): Dropdown structure, nested submenus, conditional rendering
382
370
  - **NavMenu Component** (6 tests): Dynamic positioning, submenu interactions, viewport adjustments
383
371
 
384
372
  #### Integration Tests (7 tests)
373
+
385
374
  - Component interaction flows
386
375
  - Responsive behavior between mobile/desktop
387
376
  - Keyboard navigation and accessibility
388
377
  - Menu state management during navigation
389
378
 
390
379
  ### Test Technologies
380
+
391
381
  - **Vitest**: Fast testing framework
392
382
  - **jsdom**: DOM simulation for component testing
393
383
  - **TypeScript**: Type-safe test writing
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sofidevo/astro-dynamic-header",
3
- "version": "1.1.34",
3
+ "version": "2.0.0",
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",
@@ -50,20 +50,20 @@
50
50
  "author": "SofiDev contact@sofidev.top",
51
51
  "license": "MIT",
52
52
  "peerDependencies": {
53
- "astro": "^6.0.5"
53
+ "astro": "^6.0.6"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@astrojs/check": "^0.9.8",
57
57
  "@types/jsdom": "^21.1.7",
58
58
  "@vitest/coverage-v8": "^3.2.4",
59
- "astro": "^6.0.5",
59
+ "astro": "^6.0.6",
60
60
  "jsdom": "^26.1.0",
61
61
  "typescript": "^5.0.0",
62
62
  "vitest": "^3.2.4"
63
63
  },
64
64
  "repository": {
65
65
  "type": "git",
66
- "url": "https://github.com/SofiDevO/astro-dynamic-header"
66
+ "url": "git+https://github.com/SofiDevO/astro-dynamic-header.git"
67
67
  },
68
68
  "bugs": {
69
69
  "url": "https://github.com/SofiDevO/astro-dynamic-header/issues"
package/src/Header.astro CHANGED
@@ -3,49 +3,69 @@ import HamburgerButton from "./HamburgerButton.astro";
3
3
  import NavMenu from "./NavMenu.astro";
4
4
  import MobileNav from "./MobileNav.astro";
5
5
 
6
+ import type {
7
+ LogoConfig,
8
+ NavConfig,
9
+ DualThemeConfig,
10
+ CustomClassNames,
11
+ } from "./index.js";
12
+
6
13
  export interface Props {
7
14
  headerType?: "floating" | "fullscreen";
8
- // properties for logo
9
- logoSrc?: string;
10
- logoAlt?: string;
11
- logoWidth?: string;
12
- logoText?: string;
13
- logoTextSize?: string;
14
- logoTextColor?: string;
15
- // properties for navigation
16
- homeUrl?: string;
17
- menuItems?: Array<{
18
- link: string;
19
- text: string;
20
- submenu?: Array<{
21
- link: string;
22
- text: string;
23
- submenu?: Array<{
24
- link: string;
25
- text: string;
26
- }>;
27
- }>;
28
- }>;
29
- // styles
30
- backgroundColor?: string;
31
- backdropBlur?: string;
32
- zIndex?: number;
15
+ preset?: "light" | "dark" | "auto";
16
+ logo?: LogoConfig;
17
+ navigation?: NavConfig;
18
+ theme?: DualThemeConfig;
19
+ classNames?: CustomClassNames;
33
20
  }
34
21
 
35
22
  const {
36
23
  headerType = "floating",
37
- logoSrc = "/logo.png",
38
- logoAlt = "Logo",
39
- logoWidth = "55px",
40
- logoText = "",
41
- logoTextSize = "1em",
42
- logoTextColor = "#ffffff",
43
- homeUrl = "/",
44
- menuItems = [],
45
- backgroundColor = "#0d0d0dcc",
46
- backdropBlur = "blur(20px)",
47
- zIndex = 10
24
+ preset = "auto",
25
+ logo = {},
26
+ navigation = {},
27
+ theme = {},
28
+ classNames = {},
48
29
  } = Astro.props;
30
+
31
+ // Default theme configuration
32
+ const defaultThemes = {
33
+ light: {
34
+ backgroundColor: "rgba(255, 255, 255, 0.9)",
35
+ backgroundColorOpaque: "rgb(255, 255, 255)",
36
+ backdropBlur: "blur(20px)",
37
+ zIndex: 10,
38
+ textColor: "#1a1a1a",
39
+ accentColor: "#3e1c71",
40
+ },
41
+ dark: {
42
+ backgroundColor: "#0d0d0dcc",
43
+ backgroundColorOpaque: "#0d0d0d",
44
+ backdropBlur: "blur(20px)",
45
+ zIndex: 10,
46
+ textColor: "#ffffff",
47
+ accentColor: "#00ffff",
48
+ },
49
+ };
50
+
51
+ // Merge with user overrides
52
+ const lightTheme = { ...defaultThemes.light, ...theme.light };
53
+ const darkTheme = { ...defaultThemes.dark, ...theme.dark };
54
+
55
+ // Navigation configuration
56
+ const { homeUrl = "/", menuItems = [] } = navigation;
57
+
58
+ // Logo configuration
59
+ const {
60
+ src: logoSrc = "/logo.png",
61
+ alt: logoAlt = "Logo",
62
+ width: logoWidth = "55px",
63
+ text: logoText = "",
64
+ textSize: logoTextSize = "1em",
65
+ textColor: logoTextColor = "inherit", // Will inherit from theme text-color
66
+ } = logo;
67
+
68
+ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
49
69
  ---
50
70
 
51
71
  <script>
@@ -64,7 +84,7 @@ const {
64
84
  private init(): void {
65
85
  if (this.hamburgerBtn && this.mobileMenu) {
66
86
  this.hamburgerBtn.addEventListener("click", () => this.toggleMenu());
67
- this.links.forEach(link => {
87
+ this.links.forEach((link) => {
68
88
  link.addEventListener("click", () => this.closeMenu());
69
89
  });
70
90
  }
@@ -85,38 +105,134 @@ const {
85
105
  }
86
106
  }
87
107
 
88
- // Auto-initialize when DOM is ready
89
- document.addEventListener('DOMContentLoaded', () => {
108
+ document.addEventListener("DOMContentLoaded", () => {
90
109
  new HamburgerController();
91
110
  });
92
111
  </script>
93
112
 
94
- <div class={`header__container header__container--${headerType}`} style={`z-index: ${zIndex};`}>
95
- <header class={`header header--${headerType}`} style={`background-color: ${backgroundColor}; backdrop-filter: ${backdropBlur}; -webkit-backdrop-filter: ${backdropBlur};`}>
96
- <a class="logo__container" href={homeUrl}>
97
- <img class="header__logo" src={logoSrc} alt={logoAlt} style={`width: ${logoWidth};`} />
98
- {logoText && <span class="header__logo-text" style={`color: ${logoTextColor}; font-size: ${logoTextSize};`}>{logoText}</span>}
113
+ <div
114
+ class:list={[
115
+ "header__container",
116
+ `header__container--${headerType}`,
117
+ classNames.container,
118
+ ]}
119
+ style={{ zIndex: lightTheme.zIndex }}
120
+ >
121
+ <header
122
+ class:list={["header", `header--${headerType}`, forcedClass, classNames.header]}
123
+ style={{
124
+ "--l-bg": lightTheme.backgroundColor,
125
+ "--l-bg-opaque": lightTheme.backgroundColorOpaque,
126
+ "--l-text": lightTheme.textColor,
127
+ "--l-accent": lightTheme.accentColor,
128
+ "--l-blur": lightTheme.backdropBlur,
129
+ "--d-bg": darkTheme.backgroundColor,
130
+ "--d-bg-opaque": darkTheme.backgroundColorOpaque,
131
+ "--d-text": darkTheme.textColor,
132
+ "--d-accent": darkTheme.accentColor,
133
+ "--d-blur": darkTheme.backdropBlur,
134
+ }}
135
+ >
136
+ <a class:list={["logo__container", classNames.logo]} href={homeUrl}>
137
+ <img
138
+ class="header__logo"
139
+ src={logoSrc}
140
+ alt={logoAlt}
141
+ style={{ width: logoWidth }}
142
+ />
143
+ {
144
+ logoText && (
145
+ <span
146
+ class:list={["header__logo-text", classNames.logoText]}
147
+ style={{ color: logoTextColor, fontSize: logoTextSize }}
148
+ >
149
+ {logoText}
150
+ </span>
151
+ )
152
+ }
99
153
  </a>
100
- <NavMenu menuItems={menuItems} />
101
- {Astro.slots.has('actions') && (
102
- <div class="actions-desktop">
103
- <slot name="actions" />
104
- </div>
105
- )}
106
- <HamburgerButton />
107
- <MobileNav menuItems={menuItems} type={headerType}>
108
- {Astro.slots.has('actions') && (
109
- <div class="actions-mobile" slot="slot-panel">
154
+
155
+ <div class:list={["nav-menu-wrapper", classNames.nav]}>
156
+ <NavMenu menuItems={menuItems} />
157
+ </div>
158
+
159
+ {
160
+ Astro.slots.has("actions") && (
161
+ <div class="actions-desktop">
110
162
  <slot name="actions" />
111
163
  </div>
112
- )}
164
+ )
165
+ }
166
+
167
+ <HamburgerButton />
168
+
169
+ <MobileNav
170
+ menuItems={menuItems}
171
+ type={headerType}
172
+ >
173
+ {
174
+ Astro.slots.has("actions") && (
175
+ <div class="actions-mobile" slot="slot-panel">
176
+ <slot name="actions" />
177
+ </div>
178
+ )
179
+ }
113
180
  </MobileNav>
114
-
115
181
  </header>
116
182
  </div>
117
183
 
118
- <style is:inline >
119
- /* Base styles for header container */
184
+ <style is:inline>
185
+ .header {
186
+ --bg-color: var(--l-bg);
187
+ --bg-color-opaque: var(--l-bg-opaque);
188
+ --text-color: var(--l-text);
189
+ --accent-color: var(--l-accent);
190
+ --backdrop-blur: var(--l-blur);
191
+
192
+ display: flex;
193
+ align-items: center;
194
+ width: 100%;
195
+ justify-content: space-between;
196
+ padding: 1em 2em;
197
+ background-color: var(--bg-color);
198
+ backdrop-filter: var(--backdrop-blur);
199
+ -webkit-backdrop-filter: var(--backdrop-blur);
200
+ color: var(--text-color);
201
+ transition: background-color 0.3s ease, color 0.3s ease;
202
+
203
+ @media (width < 768px) {
204
+ align-self: flex-end;
205
+ padding: 0.4em;
206
+ margin: 0 auto;
207
+ }
208
+ }
209
+
210
+ /* Automatic dark mode detection via root class */
211
+ :root.dark .header:not(.header--force-light) {
212
+ --bg-color: var(--d-bg);
213
+ --bg-color-opaque: var(--d-bg-opaque);
214
+ --text-color: var(--d-text);
215
+ --accent-color: var(--d-accent);
216
+ --backdrop-blur: var(--d-blur);
217
+ }
218
+
219
+ /* Forced themes */
220
+ .header--force-dark {
221
+ --bg-color: var(--d-bg) !important;
222
+ --bg-color-opaque: var(--d-bg-opaque) !important;
223
+ --text-color: var(--d-text) !important;
224
+ --accent-color: var(--d-accent) !important;
225
+ --backdrop-blur: var(--d-blur) !important;
226
+ }
227
+
228
+ .header--force-light {
229
+ --bg-color: var(--l-bg) !important;
230
+ --bg-color-opaque: var(--l-bg-opaque) !important;
231
+ --text-color: var(--l-text) !important;
232
+ --accent-color: var(--l-accent) !important;
233
+ --backdrop-blur: var(--l-blur) !important;
234
+ }
235
+
120
236
  .header__container {
121
237
  width: 100%;
122
238
  display: flex;
@@ -131,43 +247,24 @@ const {
131
247
  }
132
248
  }
133
249
 
134
- /* Floating header container styles */
135
250
  .header__container--floating {
136
- padding: .5em;
251
+ padding: 0.5em;
137
252
  }
138
253
 
139
- /* Fullscreen header container styles */
140
254
  .header__container--fullscreen {
141
255
  padding: 0;
142
256
  }
143
257
 
144
- /* Base styles for header */
145
- .header {
146
- display: flex;
147
- align-items: center;
148
- width: 100%;
149
- justify-content: space-between;
150
- padding: 1em 2em;
151
-
152
- @media (width < 768px) {
153
- align-self: flex-end;
154
- padding: 0.4em;
155
- margin: 0 auto;
156
- }
157
- }
158
-
159
- /* Floating header styles */
160
258
  .header--floating {
161
259
  max-width: 1100px;
162
260
  border-radius: 20px;
163
- padding: .7em 1em;
261
+ padding: 0.7em 1em;
164
262
 
165
263
  @media (width < 768px) {
166
264
  max-width: 95%;
167
265
  }
168
266
  }
169
267
 
170
- /* Fullscreen header styles */
171
268
  .header--fullscreen {
172
269
  border-radius: 0;
173
270
  padding: 1em 2em;
@@ -182,16 +279,17 @@ const {
182
279
  display: flex;
183
280
  align-items: center;
184
281
  text-decoration: none;
185
- & span{
282
+ color: inherit;
283
+ & span {
186
284
  font-weight: 600;
187
285
  }
188
286
  }
287
+
189
288
  .header__logo {
190
289
  margin-right: 1em;
191
290
  object-fit: contain;
192
291
  }
193
292
 
194
- /* Actions visibility control */
195
293
  .actions-desktop {
196
294
  @media (width < 768px) {
197
295
  display: none;
@@ -205,7 +303,6 @@ const {
205
303
  }
206
304
  }
207
305
 
208
- /* Adjust transform for fullscreen header */
209
306
  .header--fullscreen .mobile-header__menu.is-active {
210
307
  transform: translate(0%);
211
308
  }
@@ -1,21 +1,11 @@
1
1
  ---
2
+ import type { MenuItem, SecondaryMenuItem, TertiaryMenuItem } from "./index.js";
3
+
2
4
  export interface Props {
3
5
  type?: "floating" | "fullscreen";
4
- menuItems?: Array<{
5
- link: string;
6
- text: string;
7
- submenu?: Array<{
8
- link: string;
9
- text: string;
10
- submenu?: Array<{
11
- link: string;
12
- text: string;
13
- }>;
14
- }>;
15
- }>;
6
+ menuItems?: MenuItem[];
16
7
  showHomeLink?: boolean;
17
8
  homeText?: string;
18
- accentColor?: string;
19
9
  }
20
10
 
21
11
  const { type } = Astro.props;
@@ -24,7 +14,6 @@ const {
24
14
  menuItems = [],
25
15
  showHomeLink = true,
26
16
  homeText = "Home",
27
- accentColor = "var(--light-spot-color, #00ffff)",
28
17
  } = Astro.props;
29
18
 
30
19
  const pagePathname = Astro.url.pathname;
@@ -33,7 +22,6 @@ const pagePathname = Astro.url.pathname;
33
22
  <nav
34
23
  class={`mobile-header__menu mobile-header__menu--${type}`}
35
24
  id="mobile-header-menu"
36
- style={`--mobile-accent-color: ${accentColor};`}
37
25
  >
38
26
  <ol class="mobile-menu">
39
27
  {
@@ -50,7 +38,7 @@ const pagePathname = Astro.url.pathname;
50
38
  )
51
39
  }
52
40
  {
53
- menuItems?.map((item: any) => (
41
+ menuItems?.map((item: MenuItem) => (
54
42
  <li>
55
43
  {item.submenu ? (
56
44
  <details class="mobile-details">
@@ -61,11 +49,11 @@ const pagePathname = Astro.url.pathname;
61
49
  icon="codicon:triangle-down"
62
50
  width="15"
63
51
  height="15"
64
- style={`color: ${accentColor};`}
52
+ style="color: var(--accent-color);"
65
53
  />
66
54
  </summary>
67
55
  <ol class="mobile-submenu">
68
- {item?.submenu?.map((sub:any) => (
56
+ {item?.submenu?.map((sub: SecondaryMenuItem) => (
69
57
  <li>
70
58
  {sub.submenu && sub.submenu.length > 0 ? (
71
59
  <details class="mobile-details">
@@ -76,11 +64,11 @@ const pagePathname = Astro.url.pathname;
76
64
  icon="codicon:triangle-down"
77
65
  width="15"
78
66
  height="15"
79
- style={`color: ${accentColor};`}
67
+ style="color: var(--accent-color);"
80
68
  />
81
69
  </summary>
82
70
  <ol class="mobile-subsubmenu">
83
- {sub.submenu.map((subsub:any) => (
71
+ {sub.submenu.map((subsub: TertiaryMenuItem) => (
84
72
  <li class="mobile-submenu__item secondary">
85
73
  <a
86
74
  class="menu__link mobile-menu__link"
@@ -125,16 +113,16 @@ const pagePathname = Astro.url.pathname;
125
113
  position: fixed;
126
114
  top: -11px;
127
115
  left: 0;
128
- background: #151515;
116
+ background: var(--bg-color-opaque, #151515);
129
117
  z-index: 20;
130
- transition: transform 0.3s ease-in-out;
118
+ transition: transform 0.3s ease-in-out, background-color 0.3s ease;
131
119
  transform: translateX(100%);
132
120
  padding: 10rem 2rem 2rem;
133
- color: var(--mobile-accent-color);
121
+ color: var(--text-color, #fff);
134
122
  }
135
123
 
136
124
  a {
137
- color: var(--mobile-accent-color);
125
+ color: var(--text-color, #fff);
138
126
  }
139
127
  }
140
128
 
@@ -151,7 +139,7 @@ const pagePathname = Astro.url.pathname;
151
139
 
152
140
  .mobile-menu li a {
153
141
  border: solid 1px
154
- color-mix(in srgb, var(--mobile-accent-color) 20%, transparent);
142
+ color-mix(in srgb, var(--accent-color, #00ffff) 20%, transparent);
155
143
  border-radius: 4px;
156
144
  width: 100%;
157
145
  height: 4rem;
@@ -171,7 +159,7 @@ const pagePathname = Astro.url.pathname;
171
159
  .mobile-details summary {
172
160
  background: linear-gradient(
173
161
  to right,
174
- color-mix(in srgb, var(--mobile-accent-color) 20%, transparent),
162
+ color-mix(in srgb, var(--accent-color, #00ffff) 20%, transparent),
175
163
  transparent
176
164
  );
177
165
  border-radius: 4px;
@@ -207,7 +195,7 @@ const pagePathname = Astro.url.pathname;
207
195
 
208
196
  .mobile-submenu,
209
197
  .mobile-subsubmenu {
210
- border-left: dashed 1px var(--mobile-accent-color);
198
+ border-left: dashed 1px var(--accent-color, #00ffff);
211
199
  padding-left: 2rem;
212
200
  margin-left: 1rem;
213
201
  margin-top: 1rem;
@@ -230,7 +218,7 @@ const pagePathname = Astro.url.pathname;
230
218
  width: 100%;
231
219
  font-size: 1.7rem;
232
220
  height: 5rem;
233
- color: #fff;
221
+ color: inherit;
234
222
  display: flex;
235
223
  align-items: center;
236
224
  }
package/src/NavMenu.astro CHANGED
@@ -1,17 +1,8 @@
1
1
  ---
2
+ import type { MenuItem, SecondaryMenuItem, TertiaryMenuItem } from "./index.js";
3
+
2
4
  export interface Props {
3
- menuItems?: Array<{
4
- link: string;
5
- text: string;
6
- submenu?: Array<{
7
- link: string;
8
- text: string;
9
- submenu?: Array<{
10
- link: string;
11
- text: string;
12
- }>;
13
- }>;
14
- }>;
5
+ menuItems?: MenuItem[];
15
6
  showHomeLink?: boolean;
16
7
  homeText?: string;
17
8
  }
@@ -24,7 +15,7 @@ const pagePathname = Astro.url.pathname;
24
15
  <script>
25
16
  function adjustSubsubmenuPositionOnResize() {
26
17
  const subsubmenus = document.querySelectorAll(
27
- ".subsubmenu"
18
+ ".subsubmenu",
28
19
  ) as NodeListOf<HTMLElement>;
29
20
 
30
21
  subsubmenus.forEach((subsubmenu) => {
@@ -91,7 +82,7 @@ const pagePathname = Astro.url.pathname;
91
82
  const currentTransform = (subsubmenu as HTMLElement).style.transform;
92
83
  if (currentTransform && currentTransform.includes("translateX")) {
93
84
  const existingTranslate = currentTransform.match(
94
- /translateX\(([^)]+)\)/
85
+ /translateX\(([^)]+)\)/,
95
86
  );
96
87
  if (existingTranslate) {
97
88
  const existingValue = existingTranslate[1];
@@ -135,7 +126,7 @@ const pagePathname = Astro.url.pathname;
135
126
  )
136
127
  }
137
128
  {
138
- menuItems.map((item) => (
129
+ menuItems.map((item: MenuItem) => (
139
130
  <li class="menu__item">
140
131
  <a class="menu__link" href={item.link}>
141
132
  {item.text}
@@ -150,7 +141,7 @@ const pagePathname = Astro.url.pathname;
150
141
  </a>
151
142
  {item.submenu && (
152
143
  <ul class="submenu">
153
- {item.submenu.map((sub) => (
144
+ {item.submenu.map((sub: SecondaryMenuItem) => (
154
145
  <li class="submenu__item submenu__item--secondary">
155
146
  <a href={sub.link}>
156
147
  {sub.text}
@@ -165,7 +156,7 @@ const pagePathname = Astro.url.pathname;
165
156
  </a>
166
157
  {sub.submenu && (
167
158
  <ul class="subsubmenu">
168
- {sub.submenu.map((subsub) => (
159
+ {sub.submenu.map((subsub: TertiaryMenuItem) => (
169
160
  <li class="submenu__item--tertiary">
170
161
  <a href={subsub.link}>{subsub.text}</a>
171
162
  </li>
@@ -202,11 +193,15 @@ const pagePathname = Astro.url.pathname;
202
193
 
203
194
  .menu__link {
204
195
  text-decoration: none;
205
- padding: 0.5em ;
196
+ padding: 0.5em;
206
197
  display: flex;
207
198
  align-items: center;
208
199
  gap: 0.8em;
209
- color: color-mix(in srgb, var(--light-spot-color, #00ffff) 30%, white 70%);
200
+ color: color-mix(
201
+ in srgb,
202
+ var(--accent-color, #00ffff) 30%,
203
+ var(--text-color, #ffffff) 70%
204
+ );
210
205
  position: relative;
211
206
  }
212
207
 
@@ -221,7 +216,7 @@ const pagePathname = Astro.url.pathname;
221
216
  transform-origin: 1;
222
217
  height: 2px;
223
218
  transition: transform 0.3s ease;
224
- background: var(--light-spot-color, #00ffff);
219
+ background: var(--accent-color, #00ffff);
225
220
  }
226
221
 
227
222
  .menu__link:hover::after {
@@ -239,13 +234,14 @@ const pagePathname = Astro.url.pathname;
239
234
 
240
235
  .submenu,
241
236
  .subsubmenu {
242
- color: color-mix(in srgb, var(--light-spot-color, #00ffff) 20%, white 80%);
243
- background-color: #171717;
244
- border: 0.5px solid #3e1c71;
237
+ color: var(--text-color, #ffffff);
238
+ background-color: var(--bg-color-opaque, #171717);
239
+ border: 0.5px solid
240
+ color-mix(in srgb, var(--accent-color, #3e1c71) 30%, transparent);
245
241
  position: absolute;
246
242
  flex-direction: column;
247
243
  gap: 0.4em;
248
- padding: 1em;
244
+ padding: 1em;
249
245
  border-radius: 10px;
250
246
  transition: transform 0.3s ease-in-out;
251
247
  pointer-events: none;
@@ -267,8 +263,7 @@ const pagePathname = Astro.url.pathname;
267
263
  width: 100%;
268
264
  height: 64px;
269
265
  border-radius: 6px;
270
- color: #fff;
271
- color: var(--color-tertiary, #fff);
266
+ color: var(--text-color, #fff);
272
267
  }
273
268
 
274
269
  .submenu__item--secondary,
@@ -283,8 +278,12 @@ const pagePathname = Astro.url.pathname;
283
278
 
284
279
  .submenu__item--secondary a:hover,
285
280
  .submenu__item--tertiary a:hover {
286
- background: rgb(71, 70, 70);
287
- color: white;
281
+ background: color-mix(
282
+ in srgb,
283
+ var(--accent-color, #00ffff) 15%,
284
+ transparent
285
+ );
286
+ color: var(--text-color, white);
288
287
  }
289
288
 
290
289
  .submenu {
package/src/index.ts CHANGED
@@ -7,16 +7,66 @@ export interface MenuItemType {
7
7
  submenu?: MenuItemType[];
8
8
  }
9
9
 
10
- export interface HeaderProps {
11
- headerType?: "floating" | "fullscreen";
12
- logoSrc?: string;
13
- logoAlt?: string;
14
- logoWidth?: string;
10
+ export interface TertiaryMenuItem {
11
+ link: string;
12
+ text: string;
13
+ }
14
+
15
+ export interface SecondaryMenuItem {
16
+ link: string;
17
+ text: string;
18
+ submenu?: TertiaryMenuItem[];
19
+ }
20
+
21
+ export interface MenuItem {
22
+ link: string;
23
+ text: string;
24
+ submenu?: SecondaryMenuItem[];
25
+ }
26
+
27
+ export interface LogoConfig {
28
+ src?: string;
29
+ alt?: string;
30
+ width?: string;
31
+ text?: string;
32
+ textSize?: string;
33
+ textColor?: string;
34
+ }
35
+
36
+ export interface NavConfig {
15
37
  homeUrl?: string;
16
- menuItems?: MenuItemType[];
38
+ menuItems?: MenuItem[];
39
+ }
40
+
41
+ export interface ThemeConfig {
17
42
  backgroundColor?: string;
43
+ backgroundColorOpaque?: string;
18
44
  backdropBlur?: string;
19
45
  zIndex?: number;
46
+ textColor?: string;
47
+ accentColor?: string;
48
+ }
49
+
50
+ export interface DualThemeConfig {
51
+ light?: ThemeConfig;
52
+ dark?: ThemeConfig;
53
+ }
54
+
55
+ export interface CustomClassNames {
56
+ container?: string;
57
+ header?: string;
58
+ logo?: string;
59
+ logoText?: string;
60
+ nav?: string;
61
+ }
62
+
63
+ export interface HeaderProps {
64
+ headerType?: "floating" | "fullscreen";
65
+ preset?: "light" | "dark" | "auto";
66
+ logo?: LogoConfig;
67
+ navigation?: NavConfig;
68
+ theme?: DualThemeConfig;
69
+ classNames?: CustomClassNames;
20
70
  }
21
71
 
22
72
  export interface NavMenuProps {