astro-better-nav-bar 0.1.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/NavBar.astro ADDED
@@ -0,0 +1,163 @@
1
+ ---
2
+ import ThemeSelector from './ThemeSelector.astro';
3
+
4
+ interface NavItem { name: string; href: string; }
5
+ interface NavDropdown { label: string; items: NavItem[]; }
6
+
7
+ export interface Props {
8
+ logoHref?: string;
9
+ logoAriaLabel?: string;
10
+ sectionTitle?: string;
11
+ sectionTitleHref?: string;
12
+ dropdowns?: NavDropdown[];
13
+ showThemeSelector?: boolean;
14
+ showSearch?: boolean;
15
+ loginHref?: string;
16
+ loginLabel?: string;
17
+ ctaHref?: string;
18
+ ctaLabel?: string;
19
+ breadcrumbs?: { title: string; href: string }[];
20
+ }
21
+
22
+ const {
23
+ logoHref = '/',
24
+ logoAriaLabel = 'Home',
25
+ sectionTitle,
26
+ sectionTitleHref = '/',
27
+ dropdowns = [],
28
+ showThemeSelector = true,
29
+ showSearch = false,
30
+ loginHref,
31
+ loginLabel = 'Log In',
32
+ ctaHref,
33
+ ctaLabel = 'Get a demo',
34
+ breadcrumbs = [],
35
+ } = Astro.props as Props;
36
+
37
+ const activeDropdowns = dropdowns.filter(d => d.items.length > 0);
38
+ ---
39
+
40
+ <div id="main-header" class="nav-header">
41
+
42
+ <!-- Desktop bar (lg and above) -->
43
+ <div class="nav-desktop">
44
+
45
+ <div class="nav-brand">
46
+ <a href={logoHref} class="nav-logo-link" aria-label={logoAriaLabel}>
47
+ <slot name="logo" />
48
+ </a>
49
+ {sectionTitle && (
50
+ <>
51
+ <span class="nav-brand-sep" aria-hidden="true">/</span>
52
+ <a href={sectionTitleHref} class="nav-brand-title">{sectionTitle}</a>
53
+ </>
54
+ )}
55
+ </div>
56
+
57
+ {activeDropdowns.length > 0 && (
58
+ <nav id="nav-header-nav" class="nav-dropdowns" aria-label="Site navigation">
59
+ {activeDropdowns.map(menu => (
60
+ <details class="nav-dropdown-root">
61
+ <summary class="nav-dropdown-summary">{menu.label}</summary>
62
+ <div class="nav-dropdown-panel">
63
+ <ul class="nav-dropdown-list">
64
+ {menu.items.map(item => (
65
+ <li class="nav-dropdown-item">
66
+ <a href={item.href} class="nav-dropdown-link">{item.name}</a>
67
+ </li>
68
+ ))}
69
+ </ul>
70
+ </div>
71
+ </details>
72
+ ))}
73
+ </nav>
74
+ )}
75
+
76
+ <div class="nav-actions">
77
+ <slot name="extra-actions" />
78
+ {showSearch && (
79
+ <button class="nav-icon-btn nav-search-btn" aria-label="Search" data-widget="search-button">
80
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20" aria-hidden="true">
81
+ <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 100 13.5 6.75 6.75 0 000-13.5zM2.25 10.5a8.25 8.25 0 1114.59 5.28l4.69 4.69a.75.75 0 11-1.06 1.06l-4.69-4.69A8.25 8.25 0 012.25 10.5z" clip-rule="evenodd" />
82
+ </svg>
83
+ </button>
84
+ )}
85
+ {showThemeSelector && <ThemeSelector />}
86
+ {loginHref && (
87
+ <a href={loginHref} class="nav-login-link">{loginLabel}</a>
88
+ )}
89
+ {ctaHref && (
90
+ <a href={ctaHref} class="nav-cta-btn">{ctaLabel}</a>
91
+ )}
92
+ </div>
93
+
94
+ </div>
95
+
96
+ <!-- Mobile bar (below lg) -->
97
+ <div class="nav-mobile">
98
+
99
+ <slot name="sidebar-toggle" />
100
+
101
+ {activeDropdowns.length > 0 && (
102
+ <div class="nav-mobile-dropdowns">
103
+ {activeDropdowns.map(menu => (
104
+ <details class="nav-mobile-dropdown-root nav-dropdown-root">
105
+ <summary class="nav-mobile-dropdown-summary">{menu.label}</summary>
106
+ <div class="nav-dropdown-panel nav-dropdown-panel--left">
107
+ <ul class="nav-dropdown-list">
108
+ {menu.items.map(item => (
109
+ <li class="nav-dropdown-item">
110
+ <a href={item.href} class="nav-dropdown-link">{item.name}</a>
111
+ </li>
112
+ ))}
113
+ </ul>
114
+ </div>
115
+ </details>
116
+ ))}
117
+ </div>
118
+ )}
119
+
120
+ {breadcrumbs.length > 0 && (
121
+ <div class="nav-breadcrumbs">
122
+ <nav aria-label="Breadcrumb">
123
+ <ul class="nav-breadcrumb-list">
124
+ {breadcrumbs.map((crumb, idx) => (
125
+ <>
126
+ <li class="nav-breadcrumb-item">
127
+ <a href={crumb.href}>{crumb.title}</a>
128
+ </li>
129
+ {idx < breadcrumbs.length - 1 && (
130
+ <span class="nav-breadcrumb-sep" aria-hidden="true">/</span>
131
+ )}
132
+ </>
133
+ ))}
134
+ </ul>
135
+ </nav>
136
+ </div>
137
+ )}
138
+
139
+ <div class="nav-mobile-actions">
140
+ {showSearch && (
141
+ <button class="nav-icon-btn nav-search-btn" aria-label="Search" data-widget="search-button">
142
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20" aria-hidden="true">
143
+ <path fill-rule="evenodd" d="M10.5 3.75a6.75 6.75 0 100 13.5 6.75 6.75 0 000-13.5zM2.25 10.5a8.25 8.25 0 1114.59 5.28l4.69 4.69a.75.75 0 11-1.06 1.06l-4.69-4.69A8.25 8.25 0 012.25 10.5z" clip-rule="evenodd" />
144
+ </svg>
145
+ </button>
146
+ )}
147
+ {showThemeSelector && <ThemeSelector />}
148
+ </div>
149
+
150
+ </div>
151
+
152
+ </div>
153
+
154
+ <script>
155
+ document.addEventListener('click', (event) => {
156
+ const header = document.getElementById('main-header');
157
+ if (!header) return;
158
+ const clicked = (event.target as Element).closest('details');
159
+ header.querySelectorAll('details[open]').forEach(d => {
160
+ if (d !== clicked) d.removeAttribute('open');
161
+ });
162
+ });
163
+ </script>
package/README.md ADDED
@@ -0,0 +1,180 @@
1
+ # astro-better-nav-bar
2
+
3
+ Configurable sticky navigation bar for Astro. Includes a logo slot, section title, dropdown menus, theme selector, search button, login link, and CTA button. No Tailwind required -- all styles are plain CSS with configurable custom properties.
4
+
5
+ ## Installation
6
+
7
+ ```
8
+ npm install astro-better-nav-bar
9
+ ```
10
+
11
+ Import the stylesheet once in your root layout:
12
+
13
+ ```astro
14
+ ---
15
+ import 'astro-better-nav-bar/style.css';
16
+ ---
17
+ ```
18
+
19
+ ## Basic usage
20
+
21
+ ```astro
22
+ ---
23
+ import { NavBar } from 'astro-better-nav-bar';
24
+ import MyLogo from './MyLogo.astro';
25
+ ---
26
+
27
+ <NavBar
28
+ sectionTitle="Docs"
29
+ sectionTitleHref="/docs"
30
+ dropdowns={[
31
+ { label: 'Products', items: [
32
+ { name: 'Overview', href: '/products' },
33
+ { name: 'Pricing', href: '/pricing' },
34
+ ]},
35
+ { label: 'Resources', items: [
36
+ { name: 'Blog', href: '/blog' },
37
+ { name: 'Community', href: '/community' },
38
+ ]},
39
+ ]}
40
+ loginHref="https://account.example.com/"
41
+ ctaHref="/contact"
42
+ ctaLabel="Get a demo"
43
+ showSearch
44
+ >
45
+ <MyLogo slot="logo" />
46
+ </NavBar>
47
+ ```
48
+
49
+ ## Props
50
+
51
+ | Prop | Type | Default | Description |
52
+ |------|------|---------|-------------|
53
+ | `logoHref` | `string` | `'/'` | URL the logo links to |
54
+ | `logoAriaLabel` | `string` | `'Home'` | Accessible label for the logo link |
55
+ | `sectionTitle` | `string` | — | Text shown next to the logo (e.g. "Docs", "Blog") |
56
+ | `sectionTitleHref` | `string` | `'/'` | URL the section title links to |
57
+ | `dropdowns` | `NavDropdown[]` | `[]` | Dropdown menus; see shape below |
58
+ | `showThemeSelector` | `boolean` | `true` | Show the built-in Light/Dark/System theme selector |
59
+ | `showSearch` | `boolean` | `false` | Show a search icon button |
60
+ | `loginHref` | `string` | — | If provided, renders a login link |
61
+ | `loginLabel` | `string` | `'Log In'` | Text for the login link |
62
+ | `ctaHref` | `string` | — | If provided, renders a CTA button |
63
+ | `ctaLabel` | `string` | `'Get a demo'` | Text for the CTA button |
64
+ | `breadcrumbs` | `{ title: string; href: string }[]` | `[]` | Mobile breadcrumb trail shown in the mobile bar |
65
+
66
+ ### NavDropdown shape
67
+
68
+ ```ts
69
+ interface NavDropdown {
70
+ label: string;
71
+ items: { name: string; href: string }[];
72
+ }
73
+ ```
74
+
75
+ Dropdowns with zero items are silently omitted.
76
+
77
+ ## Slots
78
+
79
+ | Slot | Description |
80
+ |------|-------------|
81
+ | `logo` | Logo SVG or image. Rendered inside a link to `logoHref`. |
82
+ | `sidebar-toggle` | Optional button to open a docs sidebar. Rendered at the left of the mobile bar. |
83
+ | `extra-actions` | Additional items injected before the search/theme/login/CTA cluster in the desktop action area. |
84
+
85
+ ## Section title per page
86
+
87
+ Pass `sectionTitle` and `sectionTitleHref` as props from each layout or page to change the label that appears next to the logo:
88
+
89
+ ```astro
90
+ <!-- docs layout -->
91
+ <NavBar sectionTitle="Docs" sectionTitleHref="/docs" ...>
92
+
93
+ <!-- blog layout -->
94
+ <NavBar sectionTitle="Blog" sectionTitleHref="/blog" ...>
95
+ ```
96
+
97
+ ## Search button
98
+
99
+ When `showSearch` is `true`, a search icon button appears in both the desktop and mobile action areas. The button carries `data-widget="search-button"` and dispatches no events itself -- wire up your own search handler:
100
+
101
+ ```js
102
+ document.querySelectorAll('[data-widget="search-button"]').forEach(btn => {
103
+ btn.addEventListener('click', () => openSearch());
104
+ });
105
+ ```
106
+
107
+ ## Theme selector
108
+
109
+ The built-in theme selector stores the chosen theme in `localStorage` under the key `theme` and toggles the `.dark` class on `<html>`. On desktop it renders as a dropdown with Light, Dark, and System options. On mobile it renders as a single toggle button (moon/sun icon).
110
+
111
+ `ThemeSelector` is also exported as a standalone component if you need it outside the nav:
112
+
113
+ ```astro
114
+ ---
115
+ import { ThemeSelector } from 'astro-better-nav-bar';
116
+ ---
117
+ <ThemeSelector />
118
+ ```
119
+
120
+ ## Sidebar toggle
121
+
122
+ For docs-style layouts with a collapsible sidebar, pass your toggle button via the `sidebar-toggle` slot:
123
+
124
+ ```astro
125
+ <NavBar ...>
126
+ <button
127
+ slot="sidebar-toggle"
128
+ data-widget="visibility-button"
129
+ data-element="side-nav"
130
+ aria-label="Show Menu"
131
+ >
132
+ <!-- hamburger icon -->
133
+ </button>
134
+ </NavBar>
135
+ ```
136
+
137
+ ## CSS custom properties
138
+
139
+ All colors and the max-width are overridable. Set these on `:root` or any ancestor of `.nav-header`:
140
+
141
+ | Property | Default | Description |
142
+ |----------|---------|-------------|
143
+ | `--nav-bg` | `#0f172a` | Header background |
144
+ | `--nav-border` | `#475569` | Header bottom border |
145
+ | `--nav-text` | `#ffffff` | Primary nav text |
146
+ | `--nav-text-muted` | `#94a3b8` | Muted text (breadcrumbs, separator) |
147
+ | `--nav-hover` | `#6366f1` | Hover color for links and icons |
148
+ | `--nav-mobile-item-bg` | `#1e293b` | Mobile dropdown pill background |
149
+ | `--nav-mobile-item-border` | `#475569` | Mobile dropdown pill border |
150
+ | `--nav-dropdown-bg` | `#ffffff` | Dropdown panel background |
151
+ | `--nav-dropdown-bg-dark` | `#0f172a` | Dropdown panel background in dark mode |
152
+ | `--nav-dropdown-border` | `#64748b` | Dropdown panel border |
153
+ | `--nav-dropdown-text` | `#374151` | Dropdown item text |
154
+ | `--nav-dropdown-hover-bg` | `#f1f5f9` | Dropdown item hover background |
155
+ | `--nav-dropdown-hover-text` | `#4338ca` | Dropdown item hover text |
156
+ | `--nav-cta-bg` | `#f97316` | CTA button background |
157
+ | `--nav-cta-hover-bg` | `#ea580c` | CTA button hover background |
158
+ | `--nav-cta-text` | `#ffffff` | CTA button text |
159
+ | `--nav-max-width` | `90rem` | Maximum width of the desktop bar content |
160
+
161
+ Example:
162
+
163
+ ```css
164
+ :root {
165
+ --nav-hover: #7c3aed;
166
+ --nav-cta-bg: #16a34a;
167
+ --nav-cta-hover-bg: #15803d;
168
+ --nav-max-width: 80rem;
169
+ }
170
+ ```
171
+
172
+ ## Dark mode
173
+
174
+ The stylesheet uses a `.dark` ancestor class (compatible with Tailwind's `darkMode: 'class'`). The dropdown panels switch background automatically when `.dark` is on `<html>` or any ancestor.
175
+
176
+ No `@media (prefers-color-scheme: dark)` rules are included. The built-in `ThemeSelector` handles setting the `.dark` class based on `localStorage` and OS preference.
177
+
178
+ ## Mobile breakpoint
179
+
180
+ The desktop bar is shown at `min-width: 1024px`. Below that, the mobile bar is shown instead. This matches Tailwind's `lg` breakpoint. The breakpoint is not currently configurable via a CSS variable.
@@ -0,0 +1,104 @@
1
+ ---
2
+ // standalone theme selector
3
+ // mobile: single icon button that flips light/dark
4
+ // desktop: dropdown with Light / Dark / System options
5
+ ---
6
+ <nav-theme-selector class="nav-theme-selector">
7
+
8
+ <!-- mobile toggle: shows moon when light, sun when dark -->
9
+ <button type="button" aria-label="Toggle Theme" class="nav-theme-mobile-btn nav-icon-btn">
10
+ <svg class="nav-theme-sun-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20" aria-hidden="true">
11
+ <path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.166 17.834a.75.75 0 00-1.06 1.06l1.59 1.591a.75.75 0 101.061-1.06l-1.59-1.591zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.166 6.166a.75.75 0 001.06-1.06L5.636 3.515a.75.75 0 10-1.061 1.06l1.59 1.591z" />
12
+ </svg>
13
+ <svg class="nav-theme-moon-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20" aria-hidden="true">
14
+ <path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z" clip-rule="evenodd" />
15
+ </svg>
16
+ </button>
17
+
18
+ <!-- desktop dropdown: moon icon summary, three options -->
19
+ <details class="nav-theme-details nav-dropdown-root">
20
+ <summary class="nav-icon-btn nav-theme-details-summary" aria-label="Theme Selector">
21
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="20" height="20" aria-hidden="true">
22
+ <path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z" clip-rule="evenodd" />
23
+ </svg>
24
+ </summary>
25
+ <div class="nav-dropdown-panel nav-theme-panel">
26
+ <ul class="nav-dropdown-list">
27
+ <li class="nav-dropdown-item">
28
+ <button type="button" class="nav-dropdown-link nav-theme-option" data-theme="light">
29
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18" aria-hidden="true">
30
+ <path d="M12 2.25a.75.75 0 01.75.75v2.25a.75.75 0 01-1.5 0V3a.75.75 0 01.75-.75zM7.5 12a4.5 4.5 0 119 0 4.5 4.5 0 01-9 0zM18.894 6.166a.75.75 0 00-1.06-1.06l-1.591 1.59a.75.75 0 101.06 1.061l1.591-1.59zM21.75 12a.75.75 0 01-.75.75h-2.25a.75.75 0 010-1.5H21a.75.75 0 01.75.75zM17.834 18.894a.75.75 0 001.06-1.06l-1.59-1.591a.75.75 0 10-1.061 1.06l1.59 1.591zM12 18a.75.75 0 01.75.75V21a.75.75 0 01-1.5 0v-2.25A.75.75 0 0112 18zM7.166 17.834a.75.75 0 00-1.06 1.06l1.59 1.591a.75.75 0 101.061-1.06l-1.59-1.591zM6 12a.75.75 0 01-.75.75H3a.75.75 0 010-1.5h2.25A.75.75 0 016 12zM6.166 6.166a.75.75 0 001.06-1.06L5.636 3.515a.75.75 0 10-1.061 1.06l1.59 1.591z" />
31
+ </svg>
32
+ Light
33
+ </button>
34
+ </li>
35
+ <li class="nav-dropdown-item">
36
+ <button type="button" class="nav-dropdown-link nav-theme-option" data-theme="dark">
37
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18" aria-hidden="true">
38
+ <path fill-rule="evenodd" d="M9.528 1.718a.75.75 0 01.162.819A8.97 8.97 0 009 6a9 9 0 009 9 8.97 8.97 0 003.463-.69.75.75 0 01.981.98 10.503 10.503 0 01-9.694 6.46c-5.799 0-10.5-4.701-10.5-10.5 0-4.368 2.667-8.112 6.46-9.694a.75.75 0 01.818.162z" clip-rule="evenodd" />
39
+ </svg>
40
+ Dark
41
+ </button>
42
+ </li>
43
+ <li class="nav-dropdown-item">
44
+ <button type="button" class="nav-dropdown-link nav-theme-option" data-theme="system">
45
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="18" height="18" aria-hidden="true">
46
+ <path fill-rule="evenodd" d="M2.25 5.25a3 3 0 013-3h13.5a3 3 0 013 3V15a3 3 0 01-3 3h-3v.257c0 .597.237 1.17.659 1.591l.621.622a.75.75 0 01-.53 1.28h-9a.75.75 0 01-.53-1.28l.621-.622a2.25 2.25 0 00.659-1.59V18h-3a3 3 0 01-3-3V5.25zm1.5 0v7.5a1.5 1.5 0 001.5 1.5h13.5a1.5 1.5 0 001.5-1.5v-7.5a1.5 1.5 0 00-1.5-1.5H5.25a1.5 1.5 0 00-1.5 1.5z" clip-rule="evenodd" />
47
+ </svg>
48
+ System
49
+ </button>
50
+ </li>
51
+ </ul>
52
+ </div>
53
+ </details>
54
+
55
+ </nav-theme-selector>
56
+
57
+ <script>
58
+ class NavThemeSelector extends HTMLElement {
59
+ connectedCallback() {
60
+ const mobileBtn = this.querySelector('.nav-theme-mobile-btn');
61
+ const optionBtns = this.querySelectorAll('.nav-theme-option');
62
+ const details = this.querySelector('.nav-theme-details') as HTMLDetailsElement | null;
63
+
64
+ const getTheme = () => localStorage.getItem('theme') || 'system';
65
+ this.applyTheme(getTheme());
66
+
67
+ mobileBtn?.addEventListener('click', () => {
68
+ const isDark = document.documentElement.classList.contains('dark');
69
+ this.applyTheme(isDark ? 'light' : 'dark');
70
+ });
71
+
72
+ optionBtns.forEach(btn => {
73
+ btn.addEventListener('click', (e) => {
74
+ this.applyTheme((e.currentTarget as HTMLButtonElement).dataset.theme!);
75
+ details?.removeAttribute('open');
76
+ });
77
+ });
78
+
79
+ document.addEventListener('click', (e) => {
80
+ if (!this.contains(e.target as Node)) details?.removeAttribute('open');
81
+ });
82
+
83
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
84
+ if (getTheme() === 'system') this.applyTheme('system');
85
+ });
86
+
87
+ window.addEventListener('storage', (e) => {
88
+ if (e.key === 'theme' && e.newValue) this.applyTheme(e.newValue);
89
+ });
90
+ }
91
+
92
+ applyTheme(theme: string) {
93
+ localStorage.setItem('theme', theme);
94
+ const isDark = theme === 'system'
95
+ ? window.matchMedia('(prefers-color-scheme: dark)').matches
96
+ : theme === 'dark';
97
+ document.documentElement.classList.toggle('dark', isDark);
98
+ }
99
+ }
100
+
101
+ if (!customElements.get('nav-theme-selector')) {
102
+ customElements.define('nav-theme-selector', NavThemeSelector);
103
+ }
104
+ </script>
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { default as NavBar } from './NavBar.astro';
2
+ export { default as ThemeSelector } from './ThemeSelector.astro';
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "astro-better-nav-bar",
3
+ "version": "0.1.0",
4
+ "description": "Configurable sticky navigation bar component for Astro with dropdowns, theme selector, search, and CTA slots",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": "./index.js",
8
+ "./style.css": "./style.css",
9
+ "./NavBar.astro": "./NavBar.astro",
10
+ "./ThemeSelector.astro": "./ThemeSelector.astro"
11
+ },
12
+ "files": [
13
+ "*.js",
14
+ "*.astro",
15
+ "*.css"
16
+ ],
17
+ "keywords": ["astro", "navbar", "navigation", "header", "theme", "dropdown"],
18
+ "license": "MIT"
19
+ }
package/style.css ADDED
@@ -0,0 +1,478 @@
1
+ /*
2
+ astro-better-nav-bar/style.css
3
+
4
+ Import in your project root layout.
5
+ No Tailwind required -- all styles are plain CSS.
6
+
7
+ CSS custom properties (set on :root or .nav-header's ancestors):
8
+ --nav-bg header background default: #0f172a (slate-900)
9
+ --nav-border header bottom border default: #475569 (slate-600)
10
+ --nav-text primary nav text color default: #ffffff
11
+ --nav-text-muted muted text (breadcrumbs, sep) default: #94a3b8 (slate-400)
12
+ --nav-hover interactive hover color default: #6366f1 (indigo-500)
13
+ --nav-mobile-item-bg mobile dropdown pill bg default: #1e293b (slate-800)
14
+ --nav-mobile-item-border mobile dropdown pill border default: #475569 (slate-600)
15
+ --nav-dropdown-bg dropdown panel background default: #ffffff
16
+ --nav-dropdown-bg-dark dropdown panel bg dark mode default: #0f172a
17
+ --nav-dropdown-border dropdown border default: #64748b (slate-500)
18
+ --nav-dropdown-text dropdown item text default: #374151 (gray-700)
19
+ --nav-dropdown-hover-bg hover bg in dropdown default: #f1f5f9 (slate-100)
20
+ --nav-dropdown-hover-text hover text in dropdown default: #4338ca (indigo-600)
21
+ --nav-cta-bg CTA button background default: #f97316 (orange-500)
22
+ --nav-cta-hover-bg CTA button hover background default: #ea580c (orange-600)
23
+ --nav-cta-text CTA button text default: #ffffff
24
+ --nav-max-width desktop bar max width default: 90rem
25
+ */
26
+
27
+ /* ---------------------------------------------------------------
28
+ Header shell
29
+ --------------------------------------------------------------- */
30
+
31
+ .nav-header {
32
+ background-color: var(--nav-bg, #0f172a);
33
+ border-bottom: 1px solid var(--nav-border, #475569);
34
+ position: sticky;
35
+ top: 0;
36
+ z-index: 50;
37
+ text-align: left;
38
+ }
39
+
40
+ /* ---------------------------------------------------------------
41
+ Desktop bar
42
+ --------------------------------------------------------------- */
43
+
44
+ .nav-desktop {
45
+ display: none;
46
+ }
47
+
48
+ @media (min-width: 1024px) {
49
+ .nav-desktop {
50
+ display: flex;
51
+ justify-content: space-between;
52
+ align-items: center;
53
+ max-width: var(--nav-max-width, 90rem);
54
+ margin-left: auto;
55
+ margin-right: auto;
56
+ padding: 0.5rem 1rem;
57
+ }
58
+ }
59
+
60
+ /* brand */
61
+ .nav-brand {
62
+ display: flex;
63
+ align-items: center;
64
+ gap: 0;
65
+ flex-shrink: 0;
66
+ }
67
+
68
+ .nav-logo-link {
69
+ display: flex;
70
+ align-items: center;
71
+ margin-right: 0.25rem;
72
+ opacity: 1;
73
+ transition: opacity 0.15s ease-in;
74
+ text-decoration: none;
75
+ }
76
+
77
+ .nav-logo-link:hover {
78
+ opacity: 0.7;
79
+ }
80
+
81
+ .nav-brand-sep {
82
+ font-weight: 300;
83
+ font-size: 1.5rem;
84
+ color: var(--nav-text-muted, #94a3b8);
85
+ margin: 0 0.25rem;
86
+ line-height: 1;
87
+ }
88
+
89
+ .nav-brand-title {
90
+ font-weight: 700;
91
+ font-size: 1.5rem;
92
+ color: var(--nav-text, #ffffff);
93
+ letter-spacing: -0.025em;
94
+ text-decoration: none;
95
+ transition: color 0.15s ease-in;
96
+ }
97
+
98
+ .nav-brand-title:hover {
99
+ color: var(--nav-text-muted, #94a3b8);
100
+ }
101
+
102
+ /* dropdowns nav */
103
+ .nav-dropdowns {
104
+ display: flex;
105
+ align-items: center;
106
+ margin-left: auto;
107
+ padding: 0;
108
+ z-index: 50;
109
+ }
110
+
111
+ /* ---------------------------------------------------------------
112
+ Shared dropdown primitives (desktop + mobile)
113
+ --------------------------------------------------------------- */
114
+
115
+ .nav-dropdown-root {
116
+ position: relative;
117
+ }
118
+
119
+ /* chevron arrow via CSS */
120
+ .nav-dropdown-summary,
121
+ .nav-mobile-dropdown-summary {
122
+ list-style: none;
123
+ cursor: pointer;
124
+ user-select: none;
125
+ }
126
+
127
+ .nav-dropdown-summary::-webkit-details-marker,
128
+ .nav-mobile-dropdown-summary::-webkit-details-marker {
129
+ display: none;
130
+ }
131
+
132
+ .nav-dropdown-summary::after,
133
+ .nav-mobile-dropdown-summary::after {
134
+ content: '';
135
+ display: inline-block;
136
+ width: 0.375rem;
137
+ height: 0.375rem;
138
+ border-right: 2px solid currentColor;
139
+ border-bottom: 2px solid currentColor;
140
+ margin-left: 0.375rem;
141
+ margin-bottom: 0.125rem;
142
+ transform: rotate(45deg);
143
+ transition: transform 0.2s ease;
144
+ vertical-align: middle;
145
+ }
146
+
147
+ details[open] > .nav-dropdown-summary::after,
148
+ details[open] > .nav-mobile-dropdown-summary::after {
149
+ transform: rotate(-135deg);
150
+ margin-bottom: -0.125rem;
151
+ }
152
+
153
+ /* desktop dropdown summary */
154
+ .nav-dropdown-summary {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: 0.25rem;
158
+ font-size: 0.875rem;
159
+ font-weight: 500;
160
+ color: var(--nav-text, #ffffff);
161
+ padding: 0.25rem 0.75rem;
162
+ transition: color 0.15s;
163
+ }
164
+
165
+ .nav-dropdown-summary:hover,
166
+ details[open] > .nav-dropdown-summary {
167
+ color: var(--nav-hover, #6366f1);
168
+ }
169
+
170
+ /* dropdown panel */
171
+ .nav-dropdown-panel {
172
+ position: absolute;
173
+ right: 0;
174
+ top: 100%;
175
+ margin-top: 0.75rem;
176
+ width: 12rem;
177
+ background-color: var(--nav-dropdown-bg, #ffffff);
178
+ border: 1px solid var(--nav-dropdown-border, #64748b);
179
+ border-radius: 0.375rem;
180
+ box-shadow: 0 10px 25px rgba(0,0,0,0.15);
181
+ overflow: hidden;
182
+ z-index: 50;
183
+ }
184
+
185
+ .dark .nav-dropdown-panel {
186
+ background-color: var(--nav-dropdown-bg-dark, #0f172a);
187
+ border-color: #1e293b;
188
+ }
189
+
190
+ /* left-aligned variant for mobile dropdowns */
191
+ .nav-dropdown-panel--left {
192
+ right: auto;
193
+ left: 0;
194
+ z-index: 60;
195
+ }
196
+
197
+ .nav-dropdown-list {
198
+ list-style: none;
199
+ margin: 0;
200
+ padding: 0;
201
+ display: flex;
202
+ flex-direction: column;
203
+ }
204
+
205
+ .nav-dropdown-item {
206
+ margin: 0;
207
+ padding: 0;
208
+ }
209
+
210
+ /* shared styles for <a> links and <button> options in dropdowns */
211
+ .nav-dropdown-link {
212
+ display: flex;
213
+ align-items: center;
214
+ gap: 0.5rem;
215
+ width: 100%;
216
+ padding: 0.5rem 1rem;
217
+ font-size: 0.875rem;
218
+ font-weight: 500;
219
+ color: var(--nav-dropdown-text, #374151);
220
+ background: transparent;
221
+ border: none;
222
+ cursor: pointer;
223
+ text-decoration: none;
224
+ transition: background-color 0.15s, color 0.15s;
225
+ text-align: left;
226
+ }
227
+
228
+ .dark .nav-dropdown-link {
229
+ color: #cbd5e1;
230
+ }
231
+
232
+ .nav-dropdown-link:hover {
233
+ background-color: var(--nav-dropdown-hover-bg, #f1f5f9);
234
+ color: var(--nav-dropdown-hover-text, #4338ca);
235
+ text-decoration: none;
236
+ }
237
+
238
+ .dark .nav-dropdown-link:hover {
239
+ background-color: #1e293b;
240
+ color: #818cf8;
241
+ }
242
+
243
+ /* ---------------------------------------------------------------
244
+ Desktop action buttons
245
+ --------------------------------------------------------------- */
246
+
247
+ .nav-actions {
248
+ display: flex;
249
+ align-items: center;
250
+ gap: 0.5rem;
251
+ margin-left: 0.5rem;
252
+ }
253
+
254
+ .nav-icon-btn {
255
+ display: flex;
256
+ align-items: center;
257
+ justify-content: center;
258
+ height: 1.75rem;
259
+ width: 1.75rem;
260
+ border-radius: 0.25rem;
261
+ border: none;
262
+ background: transparent;
263
+ color: var(--nav-text, #ffffff);
264
+ cursor: pointer;
265
+ transition: background-color 0.15s, color 0.15s;
266
+ padding: 0;
267
+ }
268
+
269
+ .nav-icon-btn:hover {
270
+ background-color: rgba(255,255,255,0.1);
271
+ color: var(--nav-hover, #6366f1);
272
+ }
273
+
274
+ .nav-login-link {
275
+ font-size: 0.875rem;
276
+ font-weight: 500;
277
+ color: var(--nav-text, #ffffff);
278
+ text-decoration: none;
279
+ padding: 0 0.75rem;
280
+ transition: color 0.15s;
281
+ }
282
+
283
+ .nav-login-link:hover {
284
+ color: var(--nav-hover, #6366f1);
285
+ text-decoration: none;
286
+ }
287
+
288
+ .nav-cta-btn {
289
+ display: inline-block;
290
+ background-color: var(--nav-cta-bg, #f97316);
291
+ color: var(--nav-cta-text, #ffffff);
292
+ font-size: 0.75rem;
293
+ font-weight: 800;
294
+ letter-spacing: -0.025em;
295
+ text-transform: uppercase;
296
+ padding: 0.5rem 0.75rem;
297
+ border-radius: 0.25rem;
298
+ text-decoration: none;
299
+ margin-left: 0.75rem;
300
+ transition: background-color 0.15s;
301
+ }
302
+
303
+ .nav-cta-btn:hover {
304
+ background-color: var(--nav-cta-hover-bg, #ea580c);
305
+ text-decoration: none;
306
+ color: var(--nav-cta-text, #ffffff);
307
+ }
308
+
309
+ /* ---------------------------------------------------------------
310
+ Theme selector
311
+ --------------------------------------------------------------- */
312
+
313
+ .nav-theme-selector {
314
+ position: relative;
315
+ display: block;
316
+ z-index: 10;
317
+ }
318
+
319
+ /* mobile: single toggle, show moon (light mode) or sun (dark mode) */
320
+ .nav-theme-mobile-btn {
321
+ display: flex;
322
+ }
323
+
324
+ .nav-theme-mobile-btn .nav-theme-sun-icon {
325
+ display: none;
326
+ }
327
+
328
+ .nav-theme-mobile-btn .nav-theme-moon-icon {
329
+ display: block;
330
+ }
331
+
332
+ .dark .nav-theme-mobile-btn .nav-theme-sun-icon {
333
+ display: block;
334
+ }
335
+
336
+ .dark .nav-theme-mobile-btn .nav-theme-moon-icon {
337
+ display: none;
338
+ }
339
+
340
+ /* desktop: full dropdown, always show moon icon in summary */
341
+ .nav-theme-details {
342
+ display: none;
343
+ }
344
+
345
+ @media (min-width: 1024px) {
346
+ .nav-theme-mobile-btn {
347
+ display: none;
348
+ }
349
+
350
+ .nav-theme-details {
351
+ display: block;
352
+ }
353
+ }
354
+
355
+ .nav-theme-details-summary {
356
+ list-style: none;
357
+ outline: none;
358
+ }
359
+
360
+ .nav-theme-details-summary::-webkit-details-marker {
361
+ display: none;
362
+ }
363
+
364
+ /* no chevron on the theme summary */
365
+ .nav-theme-details-summary::after {
366
+ display: none;
367
+ }
368
+
369
+ .nav-theme-panel {
370
+ right: 0;
371
+ width: 9rem;
372
+ margin-top: 0.75rem;
373
+ }
374
+
375
+ /* ---------------------------------------------------------------
376
+ Mobile bar
377
+ --------------------------------------------------------------- */
378
+
379
+ .nav-mobile {
380
+ display: flex;
381
+ align-items: center;
382
+ padding: 0.5rem 1rem;
383
+ width: 100%;
384
+ position: sticky;
385
+ top: 0;
386
+ z-index: 50;
387
+ gap: 0.5rem;
388
+ }
389
+
390
+ @media (min-width: 1024px) {
391
+ .nav-mobile {
392
+ display: none;
393
+ }
394
+ }
395
+
396
+ /* mobile dropdown pills */
397
+ .nav-mobile-dropdowns {
398
+ display: flex;
399
+ align-items: center;
400
+ gap: 0.5rem;
401
+ flex-shrink: 0;
402
+ }
403
+
404
+ .nav-mobile-dropdown-summary {
405
+ display: flex;
406
+ align-items: center;
407
+ padding: 0.375rem 0.5rem;
408
+ font-size: 0.6875rem;
409
+ text-transform: uppercase;
410
+ letter-spacing: 0.05em;
411
+ font-weight: 600;
412
+ color: #cbd5e1;
413
+ background-color: var(--nav-mobile-item-bg, #1e293b);
414
+ border: 1px solid var(--nav-mobile-item-border, #475569);
415
+ border-radius: 0.25rem;
416
+ cursor: pointer;
417
+ white-space: nowrap;
418
+ transition: color 0.15s, background-color 0.15s;
419
+ }
420
+
421
+ .nav-mobile-dropdown-summary:hover {
422
+ color: var(--nav-text, #ffffff);
423
+ background-color: #334155;
424
+ }
425
+
426
+ /* breadcrumbs */
427
+ .nav-breadcrumbs {
428
+ flex: 1;
429
+ min-width: 0;
430
+ overflow-x: auto;
431
+ scrollbar-width: none;
432
+ -ms-overflow-style: none;
433
+ }
434
+
435
+ .nav-breadcrumbs::-webkit-scrollbar {
436
+ display: none;
437
+ }
438
+
439
+ .nav-breadcrumb-list {
440
+ display: flex;
441
+ align-items: center;
442
+ list-style: none;
443
+ margin: 0;
444
+ padding: 0;
445
+ white-space: nowrap;
446
+ }
447
+
448
+ .nav-breadcrumb-item {
449
+ display: inline-block;
450
+ }
451
+
452
+ .nav-breadcrumb-item a {
453
+ font-size: 0.875rem;
454
+ font-weight: 500;
455
+ color: var(--nav-text-muted, #94a3b8);
456
+ text-decoration: none;
457
+ text-transform: capitalize;
458
+ transition: color 0.15s;
459
+ }
460
+
461
+ .nav-breadcrumb-item a:hover {
462
+ color: var(--nav-hover, #6366f1);
463
+ }
464
+
465
+ .nav-breadcrumb-sep {
466
+ margin: 0 0.5rem;
467
+ color: var(--nav-text-muted, #94a3b8);
468
+ font-size: 0.875rem;
469
+ }
470
+
471
+ /* mobile action buttons (pushed right) */
472
+ .nav-mobile-actions {
473
+ margin-left: auto;
474
+ display: flex;
475
+ align-items: center;
476
+ gap: 0.5rem;
477
+ flex-shrink: 0;
478
+ }