@sofidevo/astro-dynamic-header 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 SofiDev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,186 @@
1
+ # @sofidev/astro-dynamic-header
2
+
3
+ A dynamic, responsive header component for Astro projects that can switch between floating and fullscreen styles with multi-level dropdown navigation support.
4
+
5
+ ## Features
6
+
7
+ - 🎨 **Dynamic Styles**: Switch between floating and fullscreen header layouts
8
+ - 📱 **Fully Responsive**: Mobile-first design with hamburger menu
9
+ - 🎯 **Multi-level Dropdowns**: Support for nested navigation menus
10
+ - 🚀 **TypeScript Support**: Full type safety and IntelliSense
11
+ - 🎨 **Customizable**: Extensive customization options for colors, sizes, and behavior
12
+ - ⚡ **Astro Optimized**: Built specifically for Astro framework
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install @sofidev/astro-dynamic-header
18
+ ```
19
+
20
+ ## Quick Start
21
+
22
+ ### Basic Usage
23
+
24
+ ```astro
25
+ ---
26
+ import Header from '@sofidev/astro-dynamic-header/Header.astro';
27
+
28
+ const menuItems = [
29
+ { link: '/about', text: 'About' },
30
+ { link: '/contact', text: 'Contact' },
31
+ ];
32
+ ---
33
+
34
+ <Header
35
+ headerType="floating"
36
+ logoSrc="/logo.png"
37
+ menuItems={menuItems}
38
+ />
39
+ ```
40
+
41
+ ### Advanced Usage
42
+
43
+ ```astro
44
+ ---
45
+ import Header from '@sofidev/astro-dynamic-header/Header.astro';
46
+
47
+ const menuItems = [
48
+ {
49
+ link: '#',
50
+ text: 'Services',
51
+ submenu: [
52
+ {
53
+ link: '#',
54
+ text: 'Web Development',
55
+ submenu: [
56
+ { link: '/web/frontend', text: 'Frontend' },
57
+ { link: '/web/backend', text: 'Backend' },
58
+ { link: '/web/fullstack', text: 'Full Stack' },
59
+ ],
60
+ },
61
+ { link: '/design', text: 'Design' },
62
+ { link: '/consulting', text: 'Consulting' },
63
+ ],
64
+ },
65
+ { link: '/about', text: 'About' },
66
+ { link: '/contact', text: 'Contact' },
67
+ ];
68
+ ---
69
+
70
+ <Header
71
+ headerType="fullscreen"
72
+ logoSrc="/logo.png"
73
+ logoAlt="My Company"
74
+ logoWidth="150px"
75
+ homeUrl="/"
76
+ menuItems={menuItems}
77
+ backgroundColor="#000000dd"
78
+ backdropBlur="blur(15px)"
79
+ zIndex={100}
80
+ />
81
+ ```
82
+
83
+ ## Component Props
84
+
85
+ ### Header Component
86
+
87
+ | Prop | Type | Default | Description |
88
+ |------|------|---------|-------------|
89
+ | `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Header layout style |
90
+ | `logoSrc` | `string` | `"/logo.png"` | Logo image source |
91
+ | `logoAlt` | `string` | `"Logo"` | Logo alt text |
92
+ | `logoWidth` | `string` | `"120px"` | Logo width |
93
+ | `homeUrl` | `string` | `"/"` | Home page URL |
94
+ | `menuItems` | `MenuItemType[]` | `[]` | Navigation menu items |
95
+ | `backgroundColor` | `string` | `"#0d0d0dcc"` | Header background color |
96
+ | `backdropBlur` | `string` | `"blur(20px)"` | Backdrop filter blur |
97
+ | `zIndex` | `number` | `10` | CSS z-index value |
98
+
99
+ ### Menu Item Structure
100
+
101
+ ```typescript
102
+ interface MenuItemType {
103
+ link: string;
104
+ text: string;
105
+ submenu?: MenuItemType[];
106
+ }
107
+ ```
108
+
109
+ ## Header Types
110
+
111
+ ### Floating Header
112
+ - Centered with max-width constraint
113
+ - Rounded corners
114
+ - Padding around container
115
+ - Perfect for modern, card-like designs
116
+
117
+ ### Fullscreen Header
118
+ - Full viewport width
119
+ - No border radius
120
+ - Edge-to-edge design
121
+ - Ideal for traditional website layouts
122
+
123
+ ## Individual Components
124
+
125
+ You can also import and use components individually:
126
+
127
+ ```astro
128
+ ---
129
+ import NavMenu from '@sofidev/astro-dynamic-header/NavMenu.astro';
130
+ import MobileNav from '@sofidev/astro-dynamic-header/MobileNav.astro';
131
+ import HamburgerButton from '@sofidev/astro-dynamic-header/HamburgerButton.astro';
132
+ ---
133
+
134
+ <NavMenu menuItems={menuItems} showHomeLink={true} homeText="Home" />
135
+ <MobileNav menuItems={menuItems} accentColor="#00ffff" />
136
+ <HamburgerButton color="#ffffff" />
137
+ ```
138
+
139
+ ## Styling and Customization
140
+
141
+ The component uses CSS custom properties that you can override:
142
+
143
+ ```css
144
+ :root {
145
+ --light-spot-color: #00ffff;
146
+ --color-tertiary: #ffffff;
147
+ --color-hamburger-lines: #ffffff;
148
+ }
149
+ ```
150
+
151
+ ## TypeScript Support
152
+
153
+ Full TypeScript support with exported interfaces:
154
+
155
+ ```typescript
156
+ import type {
157
+ MenuItemType,
158
+ HeaderProps,
159
+ NavMenuProps,
160
+ MobileNavProps,
161
+ HamburgerButtonProps
162
+ } from '@sofidev/astro-dynamic-header';
163
+ ```
164
+
165
+ ## Browser Support
166
+
167
+ - All modern browsers
168
+ - Mobile responsive design
169
+ - Supports CSS `backdrop-filter`
170
+ - Graceful degradation for older browsers
171
+
172
+ ## Live Examples
173
+
174
+ Visit our demo website to see the component in action with interactive examples and complete documentation.
175
+
176
+ ## Contributing
177
+
178
+ Contributions are welcome! Please feel free to submit a Pull Request.
179
+
180
+ ## License
181
+
182
+ MIT License - see the [LICENSE](./LICENSE) file for details.
183
+
184
+ ## Support
185
+
186
+ If you find this package helpful, please consider giving it a ⭐ on GitHub!
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@sofidevo/astro-dynamic-header",
3
+ "version": "1.0.0",
4
+ "description": "A dynamic Astro header component that switches between floating and fullscreen styles",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": "./dist/index.js",
11
+ "types": "./dist/index.d.ts"
12
+ },
13
+ "./Header": "./src/Header.astro",
14
+ "./NavMenu": "./src/NavMenu.astro",
15
+ "./MobileNav": "./src/MobileNav.astro",
16
+ "./HamburgerButton": "./src/HamburgerButton.astro",
17
+ "./Header.astro": "./src/Header.astro",
18
+ "./NavMenu.astro": "./src/NavMenu.astro",
19
+ "./MobileNav.astro": "./src/MobileNav.astro",
20
+ "./HamburgerButton.astro": "./src/HamburgerButton.astro"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "src",
25
+ "README.md"
26
+ ],
27
+ "scripts": {
28
+ "build": "astro-package build",
29
+ "dev": "astro-package dev",
30
+ "check": "astro check",
31
+ "check:watch": "astro check --watch"
32
+ },
33
+ "keywords": [
34
+ "astro",
35
+ "component",
36
+ "header",
37
+ "navigation",
38
+ "responsive",
39
+ "typescript",
40
+ "dynamic",
41
+ "floating",
42
+ "sofidev",
43
+ "fullscreen"
44
+ ],
45
+ "author": "SofiDev contact@sofidev.top",
46
+ "license": "MIT",
47
+ "peerDependencies": {
48
+ "astro": "^4.0.0 || ^5.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "@astrojs/check": "^0.9.0",
52
+ "astro": "^5.12.8",
53
+ "typescript": "^5.0.0"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/SofiDevO/astro-dynamic-header"
58
+ },
59
+ "bugs": {
60
+ "url": "https://github.com/SofiDevO/astro-dynamic-header/issues"
61
+ },
62
+ "homepage": "https://github.com/SofiDevO/astro-dynamic-header#readme"
63
+ }
@@ -0,0 +1,117 @@
1
+ ---
2
+ export interface Props {
3
+ color?: string;
4
+ }
5
+
6
+ const { color = "var(--color-hamburger-lines, #fff)" } = Astro.props;
7
+ ---
8
+
9
+ <button
10
+ class="hamburger hamburger--collapse"
11
+ type="button"
12
+ id="hamburger-btn"
13
+ style={`--hamburger-color: ${color};`}
14
+ >
15
+ <span class="hamburger-box">
16
+ <span class="hamburger-inner"></span>
17
+ </span>
18
+ </button>
19
+
20
+ <style>
21
+ .hamburger {
22
+ display: none;
23
+ font: inherit;
24
+ overflow: visible;
25
+ margin: 0;
26
+ padding: 15px;
27
+ cursor: pointer;
28
+ transition-timing-function: linear;
29
+ transition-duration: 0.15s;
30
+ transition-property: opacity, filter;
31
+ text-transform: none;
32
+ color: inherit;
33
+ border: 0;
34
+ background-color: transparent;
35
+
36
+ @media screen and (max-width: 768px) {
37
+ z-index: 1000;
38
+ display: inline-block;
39
+ }
40
+ }
41
+
42
+ .hamburger-box {
43
+ position: relative;
44
+ display: inline-block;
45
+ width: 40px;
46
+ height: 24px;
47
+ }
48
+
49
+ .hamburger-inner {
50
+ top: 50%;
51
+ display: block;
52
+ margin-top: -2px;
53
+ }
54
+
55
+ .hamburger-inner:after,
56
+ .hamburger-inner:before {
57
+ display: block;
58
+ content: "";
59
+ }
60
+
61
+ .hamburger-inner,
62
+ .hamburger-inner::before,
63
+ .hamburger-inner::after {
64
+ background-color: var(--hamburger-color, var(--color-hamburger-lines, #fff));
65
+ position: absolute;
66
+ width: 40px;
67
+ height: 4px;
68
+ transition-timing-function: ease;
69
+ transition-duration: 0.15s;
70
+ transition-property: transform;
71
+ border-radius: 4px;
72
+ }
73
+
74
+ .hamburger-inner:before {
75
+ top: -10px;
76
+ }
77
+
78
+ .hamburger-inner:after {
79
+ bottom: -10px;
80
+ }
81
+
82
+ .hamburger--collapse .hamburger-inner {
83
+ top: auto;
84
+ bottom: 0;
85
+ transition-delay: 0.13s;
86
+ transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
87
+ transition-duration: 0.13s;
88
+ }
89
+
90
+ .hamburger--collapse .hamburger-inner:before {
91
+ transition: top 0.12s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s, transform 0.13s cubic-bezier(0.55, 0.055, 0.675, 0.19);
92
+ }
93
+
94
+ .hamburger--collapse .hamburger-inner:after {
95
+ top: -20px;
96
+ transition: top 0.2s cubic-bezier(0.33333, 0.66667, 0.66667, 1) 0.2s, opacity 0.1s linear;
97
+ }
98
+
99
+ /* Animation */
100
+ .hamburger--collapse.is-active .hamburger-inner {
101
+ transition-delay: 0.22s;
102
+ transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
103
+ transform: translate3d(0, -10px, 0) rotate(-45deg);
104
+ }
105
+
106
+ .hamburger--collapse.is-active .hamburger-inner:before {
107
+ top: 0;
108
+ transition: top 0.1s cubic-bezier(0.33333, 0, 0.66667, 0.33333) 0.16s, transform 0.13s cubic-bezier(0.215, 0.61, 0.355, 1) 0.25s;
109
+ transform: rotate(-90deg);
110
+ }
111
+
112
+ .hamburger--collapse.is-active .hamburger-inner:after {
113
+ top: 0;
114
+ transition: top 0.2s cubic-bezier(0.33333, 0, 0.66667, 0.33333), opacity 0.1s linear 0.22s;
115
+ opacity: 0;
116
+ }
117
+ </style>
@@ -0,0 +1,150 @@
1
+ ---
2
+ import HamburgerButton from "./HamburgerButton.astro";
3
+ import NavMenu from "./NavMenu.astro";
4
+ import MobileNav from "./MobileNav.astro";
5
+
6
+ export interface Props {
7
+ headerType?: "floating" | "fullscreen";
8
+ logoSrc?: string;
9
+ logoAlt?: string;
10
+ logoWidth?: string;
11
+ homeUrl?: string;
12
+ menuItems?: Array<{
13
+ link: string;
14
+ text: string;
15
+ submenu?: Array<{
16
+ link: string;
17
+ text: string;
18
+ submenu?: Array<{
19
+ link: string;
20
+ text: string;
21
+ }>;
22
+ }>;
23
+ }>;
24
+ backgroundColor?: string;
25
+ backdropBlur?: string;
26
+ zIndex?: number;
27
+ }
28
+
29
+ const {
30
+ headerType = "floating",
31
+ logoSrc = "/logo.png",
32
+ logoAlt = "Logo",
33
+ logoWidth = "120px",
34
+ homeUrl = "/",
35
+ menuItems = [],
36
+ backgroundColor = "#0d0d0dcc",
37
+ backdropBlur = "blur(20px)",
38
+ zIndex = 10
39
+ } = Astro.props;
40
+ ---
41
+
42
+ <script>
43
+ class HamburgerController {
44
+ private hamburgerBtn: HTMLElement | null;
45
+ private mobileMenu: HTMLElement | null;
46
+
47
+ constructor() {
48
+ this.hamburgerBtn = document.getElementById("hamburger-btn");
49
+ this.mobileMenu = document.getElementById("mobile-header-menu");
50
+ this.init();
51
+ }
52
+
53
+ private init(): void {
54
+ if (this.hamburgerBtn && this.mobileMenu) {
55
+ this.hamburgerBtn.addEventListener("click", () => this.toggleMenu());
56
+ }
57
+ }
58
+
59
+ private toggleMenu(): void {
60
+ if (this.hamburgerBtn && this.mobileMenu) {
61
+ this.hamburgerBtn.classList.toggle("is-active");
62
+ this.mobileMenu.classList.toggle("is-active");
63
+ }
64
+ }
65
+ }
66
+
67
+ // Auto-initialize when DOM is ready
68
+ document.addEventListener('DOMContentLoaded', () => {
69
+ new HamburgerController();
70
+ });
71
+ </script>
72
+
73
+ <div class={`header__container header__container--${headerType}`} style={`z-index: ${zIndex};`}>
74
+ <header class={`header header--${headerType}`} style={`background-color: ${backgroundColor}; backdrop-filter: ${backdropBlur}; -webkit-backdrop-filter: ${backdropBlur};`}>
75
+ <a href={homeUrl}>
76
+ <img class="header__logo" src={logoSrc} alt={logoAlt} style={`width: ${logoWidth};`} />
77
+ </a>
78
+ <NavMenu menuItems={menuItems} />
79
+ <HamburgerButton />
80
+ <MobileNav menuItems={menuItems} />
81
+ </header>
82
+ </div>
83
+
84
+ <style>
85
+ /* Base styles for header container */
86
+ .header__container {
87
+ width: 100%;
88
+ display: flex;
89
+ justify-content: center;
90
+ align-items: center;
91
+ margin: 0 auto;
92
+ position: fixed;
93
+
94
+ @media (width < 768px) {
95
+ justify-content: flex-end;
96
+ align-items: flex-end;
97
+ }
98
+ }
99
+
100
+ /* Floating header container styles */
101
+ .header__container--floating {
102
+ padding: 16px;
103
+ }
104
+
105
+ /* Fullscreen header container styles */
106
+ .header__container--fullscreen {
107
+ padding: 0;
108
+ }
109
+
110
+ /* Base styles for header */
111
+ .header {
112
+ display: flex;
113
+ align-items: center;
114
+ width: 100%;
115
+ justify-content: space-between;
116
+
117
+ @media (width < 768px) {
118
+ align-self: flex-end;
119
+ padding: 0.4rem;
120
+ margin: 0 auto;
121
+ }
122
+ }
123
+
124
+ /* Floating header styles */
125
+ .header--floating {
126
+ max-width: 1100px;
127
+ border-radius: 18px;
128
+ padding: 0.5rem 1rem;
129
+
130
+ @media (width < 768px) {
131
+ max-width: 95%;
132
+ }
133
+ }
134
+
135
+ /* Fullscreen header styles */
136
+ .header--fullscreen {
137
+ border-radius: 0;
138
+ padding: 1rem 2rem;
139
+
140
+ @media (width < 768px) {
141
+ max-width: 100%;
142
+ padding: 0.8rem 1rem;
143
+ }
144
+ }
145
+
146
+ .header__logo {
147
+ margin-right: 1rem;
148
+ object-fit: contain;
149
+ }
150
+ </style>
@@ -0,0 +1,201 @@
1
+ ---
2
+ 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
+ }>;
15
+ showHomeLink?: boolean;
16
+ homeText?: string;
17
+ accentColor?: string;
18
+ }
19
+
20
+ const {
21
+ menuItems = [],
22
+ showHomeLink = true,
23
+ homeText = "Home",
24
+ accentColor = "var(--light-spot-color, #00ffff)"
25
+ } = Astro.props;
26
+
27
+ const pagePathname = Astro.url.pathname;
28
+ ---
29
+
30
+ <nav class="mobile-header__menu" id="mobile-header-menu" style={`--mobile-accent-color: ${accentColor};`}>
31
+ <ol class="mobile-menu">
32
+ {showHomeLink && pagePathname !== "/" && (
33
+ <li class="header__item mobile-header__item">
34
+ <a class="menu__link mobile-menu__link" href="/" data-astro-prefetch="hover">
35
+ {homeText}
36
+ </a>
37
+ </li>
38
+ )}
39
+ {menuItems.map((item) => (
40
+ <li>
41
+ {item.submenu ? (
42
+ <details class="mobile-details">
43
+ <summary class="menu__summary">
44
+ {item.text}
45
+ <iconify-icon
46
+ class="iconify-arrow"
47
+ icon="codicon:triangle-down"
48
+ width="15"
49
+ height="15"
50
+ style={`color: ${accentColor};`}
51
+ />
52
+ </summary>
53
+ <ol class="mobile-submenu">
54
+ {item.submenu.map((sub) => (
55
+ <li>
56
+ {sub.submenu && sub.submenu.length > 0 ? (
57
+ <details class="mobile-details">
58
+ <summary class="menu__summary">
59
+ {sub.text}
60
+ <iconify-icon
61
+ class="iconify-arrow"
62
+ icon="codicon:triangle-down"
63
+ width="15"
64
+ height="15"
65
+ style={`color: ${accentColor};`}
66
+ />
67
+ </summary>
68
+ <ol class="mobile-subsubmenu">
69
+ {sub.submenu.map((subsub) => (
70
+ <li class="mobile-submenu__item secondary">
71
+ <a class="menu__link mobile-menu__link" href={subsub.link}>{subsub.text}</a>
72
+ </li>
73
+ ))}
74
+ </ol>
75
+ </details>
76
+ ) : (
77
+ <a class="menu__link mobile-menu__link" href={sub.link}>{sub.text}</a>
78
+ )}
79
+ </li>
80
+ ))}
81
+ </ol>
82
+ </details>
83
+ ) : (
84
+ <a class="mobile-menu__link" href={item.link}>
85
+ {item.text}
86
+ </a>
87
+ )}
88
+ </li>
89
+ ))}
90
+ </ol>
91
+ </nav>
92
+
93
+ <style>
94
+ .mobile-header__menu {
95
+ display: none;
96
+
97
+ @media (width < 768px) {
98
+ display: flex;
99
+ flex-direction: column;
100
+ width: 104vw;
101
+ height: 100dvh;
102
+ position: fixed;
103
+ top: -16px;
104
+ left: 0;
105
+ background: #151515;
106
+ z-index: 20;
107
+ transition: transform 0.3s ease-in-out;
108
+ transform: translateX(100%);
109
+ padding: 10rem 2rem 2rem;
110
+ color: var(--mobile-accent-color);
111
+ }
112
+
113
+ a {
114
+ color: var(--mobile-accent-color);
115
+ }
116
+ }
117
+
118
+ .mobile-menu {
119
+ display: flex;
120
+ flex-direction: column;
121
+ gap: 1rem;
122
+ max-height: 76dvh;
123
+ overflow-y: auto;
124
+ list-style: none;
125
+ padding: 0;
126
+ margin: 0;
127
+ }
128
+
129
+ .mobile-menu li a {
130
+ border: solid 1px color-mix(in srgb, var(--mobile-accent-color) 20%, transparent);
131
+ border-radius: 4px;
132
+ width: 100%;
133
+ height: 4rem;
134
+ font-size: 1.5rem;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: space-between;
138
+ padding: 0 1rem;
139
+ text-decoration: none;
140
+ }
141
+
142
+ .mobile-details {
143
+ width: 100%;
144
+ interpolate-size: allow-keywords;
145
+ }
146
+
147
+ .mobile-details summary {
148
+ background: linear-gradient(to right, color-mix(in srgb, var(--mobile-accent-color) 20%, transparent), transparent);
149
+ border-radius: 4px;
150
+ width: 100%;
151
+ height: 4rem;
152
+ font-size: 1.5rem;
153
+ display: flex;
154
+ align-items: center;
155
+ justify-content: space-between;
156
+ padding: 0 1rem;
157
+ cursor: pointer;
158
+ list-style: none;
159
+ }
160
+
161
+ .mobile-details::details-content {
162
+ transition:
163
+ height 0.5s ease,
164
+ opacity 0.5s ease,
165
+ content-visibility 0.5s allow-discrete;
166
+ opacity: 0;
167
+ height: 0;
168
+ overflow: clip;
169
+ }
170
+
171
+ .mobile-details:open iconify-icon {
172
+ transform: rotate(180deg);
173
+ }
174
+
175
+ .mobile-details:open::details-content {
176
+ height: auto;
177
+ opacity: 1;
178
+ }
179
+
180
+ .mobile-submenu,
181
+ .mobile-subsubmenu {
182
+ border-left: dashed 1px var(--mobile-accent-color);
183
+ padding-left: 2rem;
184
+ margin-left: 1rem;
185
+ margin-top: 1rem;
186
+ list-style: none;
187
+ }
188
+
189
+ .mobile-header__menu.is-active {
190
+ transform: translateX(-7%);
191
+ }
192
+
193
+ .menu__summary {
194
+ width: 100%;
195
+ font-size: 1.7rem;
196
+ height: 5rem;
197
+ color: #fff;
198
+ display: flex;
199
+ align-items: center;
200
+ }
201
+ </style>
@@ -0,0 +1,279 @@
1
+ ---
2
+ 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
+ }>;
15
+ showHomeLink?: boolean;
16
+ homeText?: string;
17
+ }
18
+
19
+ const {
20
+ menuItems = [],
21
+ showHomeLink = true,
22
+ homeText = "Home"
23
+ } = Astro.props;
24
+
25
+ const pagePathname = Astro.url.pathname;
26
+ ---
27
+
28
+ <script>
29
+ function adjustSubsubmenuPositionOnResize() {
30
+ const subsubmenus = document.querySelectorAll('.subsubmenu') as NodeListOf<HTMLElement>;
31
+
32
+ subsubmenus.forEach((subsubmenu) => {
33
+ const rect = subsubmenu.getBoundingClientRect();
34
+ const viewportWidth = window.innerWidth;
35
+
36
+ if (rect.right > viewportWidth) {
37
+ subsubmenu.style.right = '100%';
38
+ subsubmenu.style.left = 'auto';
39
+ } else {
40
+ subsubmenu.style.left = '100%';
41
+ subsubmenu.style.right = 'auto';
42
+ }
43
+ });
44
+ }
45
+
46
+ window.addEventListener('resize', adjustSubsubmenuPositionOnResize);
47
+ document.addEventListener('DOMContentLoaded', adjustSubsubmenuPositionOnResize);
48
+ </script>
49
+
50
+ <nav class="header__menu" id="header-menu">
51
+ <ul class="menu">
52
+ {showHomeLink && pagePathname !== "/" && (
53
+ <li class="header__item">
54
+ <a class="menu__link" href="/" data-astro-prefetch="hover">
55
+ {homeText}
56
+ </a>
57
+ </li>
58
+ )}
59
+ {menuItems.map((item) => (
60
+ <li class="menu__item">
61
+ <a class="menu__link" href={item.link}>
62
+ {item.text}
63
+ {item.submenu && (
64
+ <iconify-icon
65
+ class="iconify-arrow"
66
+ icon="codicon:triangle-down"
67
+ width="15"
68
+ height="15"
69
+ />
70
+ )}
71
+ </a>
72
+ {item.submenu && (
73
+ <ul class="submenu">
74
+ {item.submenu.map((sub) => (
75
+ <li class="submenu__item submenu__item--secondary">
76
+ <a href={sub.link}>
77
+ {sub.text}
78
+ {sub.submenu && sub.submenu.length > 0 && (
79
+ <iconify-icon
80
+ class="iconify-arrow"
81
+ icon="codicon:triangle-down"
82
+ width="15"
83
+ height="15"
84
+ />
85
+ )}
86
+ </a>
87
+ {sub.submenu && (
88
+ <ul class="subsubmenu">
89
+ {sub.submenu.map((subsub) => (
90
+ <li class="submenu__item--tertiary">
91
+ <a href={subsub.link}>{subsub.text}</a>
92
+ </li>
93
+ ))}
94
+ </ul>
95
+ )}
96
+ </li>
97
+ ))}
98
+ </ul>
99
+ )}
100
+ </li>
101
+ ))}
102
+ </ul>
103
+ </nav>
104
+
105
+ <style>
106
+ .header__menu {
107
+ @media (width < 768px) {
108
+ display: none;
109
+ }
110
+ }
111
+
112
+ .menu {
113
+ padding: 1rem;
114
+ list-style: none;
115
+ display: flex;
116
+ align-items: center;
117
+ flex-wrap: wrap;
118
+
119
+ @media (width < 768px) {
120
+ flex-direction: column;
121
+ }
122
+ }
123
+
124
+ .menu__link {
125
+ text-decoration: none;
126
+ padding: 0.5rem 1rem;
127
+ display: flex;
128
+ align-items: center;
129
+ gap: 0.8rem;
130
+ color: color-mix(in srgb, var(--light-spot-color, #00ffff) 30%, white 70%);
131
+ position: relative;
132
+ }
133
+
134
+ @media screen and (min-width: 768px) {
135
+ .menu__link::after {
136
+ position: absolute;
137
+ top: 25px;
138
+ left: 0;
139
+ content: "";
140
+ width: 100%;
141
+ transform: scale(0);
142
+ transform-origin: 1;
143
+ height: 2px;
144
+ transition: transform 0.3s ease;
145
+ background: var(--light-spot-color, #00ffff);
146
+ }
147
+
148
+ .menu__link:hover::after {
149
+ transform: scale(1);
150
+ }
151
+ }
152
+
153
+ .iconify-arrow {
154
+ transition: transform 0.3s linear;
155
+ }
156
+
157
+ .menu__link:hover > .iconify-arrow {
158
+ transform: rotate(180deg);
159
+ }
160
+
161
+ .submenu,
162
+ .subsubmenu {
163
+ color: color-mix(in srgb, var(--light-spot-color, #00ffff) 20%, white 80%);
164
+ background-color: #171717;
165
+ border: 0.5px solid #3e1c71;
166
+ position: absolute;
167
+ flex-direction: column;
168
+ gap: 0.4rem;
169
+ padding: 2rem 1rem;
170
+ border-radius: 0.6rem;
171
+ transition: transform 0.3s ease-in-out;
172
+ pointer-events: none;
173
+ opacity: 0;
174
+ text-wrap: nowrap;
175
+ }
176
+
177
+ .menu__item {
178
+ padding: 6px;
179
+ position: relative;
180
+ }
181
+
182
+ .submenu a,
183
+ .subsubmenu a {
184
+ display: flex;
185
+ justify-content: space-between;
186
+ align-items: center;
187
+ width: 100%;
188
+ height: 4rem;
189
+ border-radius: .4rem;
190
+ color: #fff;
191
+ color: var(--color-tertiary, #fff);
192
+ }
193
+
194
+ .submenu__item--secondary,
195
+ .submenu__item--tertiary {
196
+ position: relative;
197
+ }
198
+
199
+ .submenu__item--secondary a,
200
+ .submenu__item--tertiary a {
201
+ padding: 2rem;
202
+ }
203
+
204
+ .submenu__item--secondary a:hover,
205
+ .submenu__item--tertiary a:hover {
206
+ background: rgb(71, 70, 70);
207
+ color: white;
208
+ }
209
+
210
+ .submenu {
211
+ padding: 1rem;
212
+ right: -23px;
213
+ }
214
+
215
+ .submenu::after {
216
+ content: "";
217
+ position: absolute;
218
+ top: -4rem;
219
+ width: 100%;
220
+ z-index: -3;
221
+ pointer-events: none;
222
+ height: 10rem;
223
+ }
224
+
225
+ .subsubmenu {
226
+ top: 0;
227
+ left: auto;
228
+ right: auto;
229
+ min-width: 193px;
230
+ }
231
+
232
+ .subsubmenu::before {
233
+ content: "";
234
+ position: absolute;
235
+ top: -4rem;
236
+ z-index: -1;
237
+ pointer-events: none;
238
+ height: 10rem;
239
+ width: 20rem;
240
+ height: 14rem;
241
+ }
242
+
243
+ .subsubmenu::after {
244
+ content: "";
245
+ position: absolute;
246
+ top: -14px;
247
+ right: 3rem;
248
+ width: 100%;
249
+ z-index: -3;
250
+ height: 21rem;
251
+ }
252
+
253
+ .submenu li {
254
+ width: 100%;
255
+ }
256
+
257
+ .menu__item:hover > .submenu {
258
+ opacity: 1;
259
+ pointer-events: all;
260
+ transform: translateY(2.5rem);
261
+ }
262
+
263
+ .menu__item:hover > .submenu::after {
264
+ pointer-events: all;
265
+ }
266
+
267
+ .submenu li:hover > .subsubmenu {
268
+ opacity: 1;
269
+ pointer-events: all;
270
+ }
271
+
272
+ .submenu li:hover > .subsubmenu::before {
273
+ pointer-events: all;
274
+ }
275
+
276
+ .submenu li:hover > .subsubmenu::after {
277
+ pointer-events: all;
278
+ }
279
+ </style>
@@ -0,0 +1,44 @@
1
+ export class HamburgerButton {
2
+ private hamburgerBtn: HTMLElement | null;
3
+ private mobileMenu: HTMLElement | null;
4
+
5
+ constructor() {
6
+ this.hamburgerBtn = document.getElementById("hamburger-btn");
7
+ this.mobileMenu = document.getElementById("mobile-header-menu");
8
+ this.init();
9
+ }
10
+
11
+ private init(): void {
12
+ if (this.hamburgerBtn && this.mobileMenu) {
13
+ this.hamburgerBtn.addEventListener("click", () => this.toggleMenu());
14
+ }
15
+ }
16
+
17
+ private toggleMenu(): void {
18
+ if (this.hamburgerBtn && this.mobileMenu) {
19
+ this.hamburgerBtn.classList.toggle("is-active");
20
+ this.mobileMenu.classList.toggle("is-active");
21
+ }
22
+ }
23
+
24
+ public openMenu(): void {
25
+ if (this.hamburgerBtn && this.mobileMenu) {
26
+ this.hamburgerBtn.classList.add("is-active");
27
+ this.mobileMenu.classList.add("is-active");
28
+ }
29
+ }
30
+
31
+ public closeMenu(): void {
32
+ if (this.hamburgerBtn && this.mobileMenu) {
33
+ this.hamburgerBtn.classList.remove("is-active");
34
+ this.mobileMenu.classList.remove("is-active");
35
+ }
36
+ }
37
+ }
38
+
39
+ // Auto-initialize when DOM is ready
40
+ if (typeof document !== 'undefined') {
41
+ document.addEventListener('DOMContentLoaded', () => {
42
+ new HamburgerButton();
43
+ });
44
+ }
package/src/index.ts ADDED
@@ -0,0 +1,38 @@
1
+ // TypeScript utilities
2
+ export { HamburgerButton as HamburgerButtonController } from './hamburger';
3
+
4
+ // Types
5
+ export interface MenuItemType {
6
+ link: string;
7
+ text: string;
8
+ submenu?: MenuItemType[];
9
+ }
10
+
11
+ export interface HeaderProps {
12
+ headerType?: "floating" | "fullscreen";
13
+ logoSrc?: string;
14
+ logoAlt?: string;
15
+ logoWidth?: string;
16
+ homeUrl?: string;
17
+ menuItems?: MenuItemType[];
18
+ backgroundColor?: string;
19
+ backdropBlur?: string;
20
+ zIndex?: number;
21
+ }
22
+
23
+ export interface NavMenuProps {
24
+ menuItems?: MenuItemType[];
25
+ showHomeLink?: boolean;
26
+ homeText?: string;
27
+ }
28
+
29
+ export interface MobileNavProps {
30
+ menuItems?: MenuItemType[];
31
+ showHomeLink?: boolean;
32
+ homeText?: string;
33
+ accentColor?: string;
34
+ }
35
+
36
+ export interface HamburgerButtonProps {
37
+ color?: string;
38
+ }