@sofidevo/astro-dynamic-header 3.0.1 → 4.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
@@ -1,52 +1,47 @@
1
1
  # @sofidevo/astro-dynamic-header
2
2
 
3
- A dynamic, responsive header component for Astro projects that can switch between floating and fullscreen styles with multi-level dropdown navigation support.
3
+ A dynamic, responsive header component for Astro projects. Supports floating and fullscreen layouts, multi-level dropdown navigation, native CSS variable customization, dark mode, and TypeScript — all with zero external icon dependencies.
4
4
 
5
- > [!WARNING]
6
- > **Breaking Changes — v2.2+**: The `logo` prop has been removed in favor of a much more flexible `logo` slot. The `color` prop has also been removed from the `HamburgerButton` component to ensure flawless dark mode synchronization. The `theme` property behavior was also refactored to prioritize native CSS variables, reducing DOM bloat and enabling purely CSS-based customization. See the [Changelog](#changelog) section below.
7
-
8
- > [!WARNING]
9
- > **Breaking Changes — v2.0+**: The `logo` and `navigation` props were restructured from strings/arrays to configuration objects. See the [Component Props](#component-props) and [Comprehensive Example](#comprehensive-example) to migrate.
10
-
11
- > [!NOTE]
12
- > **What's new — v2.1**: `CustomClassNames` has been renamed `HeaderClassNames` (the old name still works as an alias). New nested class props added to `LogoConfig` and `NavConfig`. New `mobileNav` slot in `HeaderClassNames`. `defaultThemes` is now exported for use outside the component. See the [Changelog](#changelog) section below.
5
+ As of v4.0.0 the component styles are shipped inside CSS cascade layers, so any utility class you pass to the component wins over the built-in styles **without `!important`**.
13
6
 
14
7
  ## Features
15
8
 
16
- - **Dynamic Styles**: Switch between floating and fullscreen header layouts
17
- - **Fully Responsive**: Mobile-first design with hamburger menu
18
- - **Multi-level Dropdowns**: Support for nested navigation menus
19
- - **Slot Support**: Customizable slots for desktop header and mobile panel content
20
- - **TypeScript Support**: Full type safety and IntelliSense
21
- - **Two-layer customization**: High-level `classNames` prop + fine-grained nested class props inside `navigation` and `logo`
22
- - **Exportable defaults**: Import and extend `defaultThemes` from your own code
23
- - **Astro Optimized**: Built specifically for Astro framework
9
+ - **Floating & Fullscreen layouts** — switch layouts with a single prop.
10
+ - **Fully Responsive** — mobile-first accordion dropdowns with optimized hit targets.
11
+ - **3-level Dropdowns** — top, secondary, and tertiary nested navigation items.
12
+ - **Dark Mode Ready** — auto-detects `.dark` on `<html>`, or forces a state with `preset`.
13
+ - **Inline SVG Icons** — no external CDNs, no extra network requests, no flash of missing icons.
14
+ - **Slot Support** — inject your custom logo and header actions directly into slots.
15
+ - **Pure CSS Customization** — customize background, blur, and colors using native CSS variables.
16
+ - **Cascade-layer friendly** — component styles live in `@layer components`, so your utility classes override them without `!important` and without `twMerge()`.
17
+ - **Full TypeScript** — all props and config interfaces are fully typed.
24
18
 
25
- ### Live demo
19
+ ### Live Demo
26
20
 
27
21
  [https://base-astro-psi.vercel.app/fullscreen-demo](https://base-astro-psi.vercel.app/fullscreen-demo)
28
22
 
23
+ ---
24
+
29
25
  ## Installation
30
26
 
31
27
  ```bash
32
- npm i @sofidevo/astro-dynamic-header
33
- ```
28
+ # npm
29
+ npm i @sofidevo/astro-dynamic-header
34
30
 
35
- ## Required Dependencies
31
+ # pnpm
32
+ pnpm add @sofidevo/astro-dynamic-header
36
33
 
37
- You need to add the Iconify CDN to the head of your project for the hamburger menu icons to work properly:
38
-
39
- ```html
40
- <script src="https://code.iconify.design/iconify-icon/3.0.0/iconify-icon.min.js"></script>
34
+ # yarn
35
+ yarn add @sofidevo/astro-dynamic-header
41
36
  ```
42
37
 
43
- Add this to your main layout or in the `<head>` section of your Astro pages.
44
-
45
- ## Quick Start
38
+ - Peer dependency: `astro ^7.0.0`.
39
+ - No external CDN scripts or stylesheet additions are required.
40
+ - No CSS framework is required. Tailwind, UnoCSS, or plain CSS all work.
46
41
 
47
- ### Basic Usage (Automatic Theme)
42
+ ---
48
43
 
49
- By default, the header uses `preset="auto"`, which automatically detects the theme based on a `.dark` class on your root element (`html` or `body`).
44
+ ## Quick Start
50
45
 
51
46
  ```astro
52
47
  ---
@@ -54,452 +49,663 @@ import Header from '@sofidevo/astro-dynamic-header/Header';
54
49
 
55
50
  const menuItems = [
56
51
  { link: '/about', text: 'About' },
52
+ { link: '/contact', text: 'Contact' },
57
53
  ];
54
+
55
+ const navigation = { menuItems };
58
56
  ---
59
57
 
60
- <!-- Detects .dark class on root automatically -->
61
- <Header navigation={{ menuItems }} />
58
+ <Header navigation={navigation}>
59
+ <a slot="logo" href="/">MyLogo</a>
60
+ </Header>
62
61
  ```
63
62
 
64
- ### Advanced Usage (Dual-Theme Customization)
63
+ ---
64
+
65
+ ## Breaking Changes in v4.0.0
66
+
67
+ > [!WARNING]
68
+ > v4.0.0 changes how the component's CSS participates in the cascade. Read this section before upgrading from `3.x`.
65
69
 
66
- You can provide custom colors for both light and dark modes simultaneously.
70
+ ### 1. `classNames.logo` and `classNames.logoText` were removed
71
+
72
+ `HeaderClassNames` no longer accepts `logo` or `logoText`. Those keys never matched any rendered markup (the logo has always been a slot), but passing them now fails type-checking.
73
+
74
+ Before (v3) — never actually styled anything:
67
75
 
68
76
  ```astro
77
+ <Header classNames={{ logo: "hover:opacity-80", logoText: "font-bold" }} />
78
+ ```
69
79
 
70
- ---
71
- import Header from '@sofidevo/astro-dynamic-header/Header';
72
- const navigation = {
73
- menuItems: [
74
- { link: '/about', text: 'About' },
75
- ]
76
- };
77
- const theme = {
78
- light: {
79
- accentColor: "#3e1c71",
80
- backgroundColor: "rgba(255, 255, 255, 0.8)"
81
- },
82
- dark: {
83
- accentColor: "#00ffff",
84
- backgroundColor: "rgba(20, 20, 20, 0.9)"
85
- }
86
- };
87
- ---
80
+ After (v4) — style the markup you put in the slot:
88
81
 
89
- <Header
90
- preset="auto"
91
- theme={theme}
92
- navigation={navigation}
93
- />
82
+ ```astro
83
+ <Header>
84
+ <a slot="logo" href="/" class="logo-link hover:opacity-80">
85
+ <span class="font-bold">MyLogo</span>
86
+ </a>
87
+ </Header>
88
+ ```
89
+
90
+ ### 2. Component styles are now scoped and layered
91
+
92
+ | | v3.x | v4.0.0 |
93
+ | --- | --- | --- |
94
+ | Style block | `<style is:inline>` (global) | `<style>` (scoped to the component) |
95
+ | Layer | none (unlayered) | `@layer components` |
96
+ | Order statement | none | `@layer theme, base, components, utilities;` |
97
+ | Override your header with utilities | required `!important` | works out of the box |
98
+ | Your unlayered CSS vs. component | component usually won (specificity + document order) | **your unlayered CSS wins** |
99
+
100
+ Two practical consequences:
101
+
102
+ - **Utilities now win.** `classNames`, Tailwind classes, and any rule you put in `@layer utilities` beat the built-in styles even at equal or lower specificity. You can delete the `!important`s you may have added.
103
+ - **Unlayered CSS now wins over the component.** Global resets, hand-written element selectors (`header { ... }`, `a { ... }`), and Tailwind v3 Preflight are emitted outside any layer, and unlayered rules beat *every* layered rule. If your reset should sit *under* the header, wrap it in `@layer base` (see the [Styling Guide](#styling-guide) and the [FAQ](#my-overrides-do-not-apply)).
104
+
105
+ ### 3. Other behavior changes
106
+
107
+ - **`theme.dark.zIndex` is now honored.** The container z-index resolves as `theme.light.zIndex ?? theme.dark.zIndex ?? 10`; previously only `light.zIndex` was read.
108
+ - **Forced theme rules no longer use `!important`.** `.header.header--force-light` / `.header.header--force-dark` rely on specificity instead, so they can be overridden by your own layered CSS if you ever need to.
109
+ - **The mobile panel's `.active` rule no longer uses `!important`**, so active-link styles are overridable like everything else.
110
+ - **`MobileNav` defaults to `type="floating"`** when rendered on its own (previously it produced an undefined modifier class).
111
+ - **Theme variable fallbacks now match `defaultThemes`** exactly (for example `rgba(255, 255, 255, 0.9)` for `--l-bg`).
112
+
113
+ ### Migration checklist
114
+
115
+ ```md
116
+ - [ ] Update the package: npm i -U @sofidevo/astro-dynamic-header
117
+ - [ ] Remove `logo` / `logoText` from `classNames` (style the `logo` slot instead).
118
+ - [ ] Remove the `!important` declarations you added to override the header (they are no longer needed).
119
+ - [ ] Move your global resets / element selectors into `@layer base`.
120
+ - [ ] Using Tailwind v3? Read "Preflight changed my nav link colors" in the FAQ.
121
+ - [ ] Setting only `dark.zIndex`? It now takes effect — double-check stacking.
94
122
  ```
95
123
 
124
+ ---
125
+
96
126
  ## Component Props
97
127
 
98
- ### Header Component
128
+ ### `<Header>`
99
129
 
100
- | Prop | Type | Default | Description |
101
- | ------------ | ----------------------------- | ------------ | ---------------------------------------------- |
102
- | `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Header layout style |
103
- | `preset` | `"light" \| "dark" \| "auto"` | `"auto"` | Theme behavior. `auto` follows root class. |
104
- | `navigation` | `NavConfig` | `{}` | Navigation configuration object |
105
- | `theme` | `DualThemeConfig` | `{}` | Custom theme overrides for light/dark |
106
- | `classNames` | `HeaderClassNames` | `{}` | High-level class overrides for layout elements |
130
+ | Prop | Type | Default | Description |
131
+ | ------------ | ----------------------------- | ------------ | ------------------------------------------------- |
132
+ | `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Layout style |
133
+ | `preset` | `"light" \| "dark" \| "auto"` | `"auto"` | Theme mode. `"auto"` follows `.dark` on `<html>`. |
134
+ | `navigation` | `NavConfig` | `{}` | Menu items, home link, and custom CSS classes |
135
+ | `theme` | `DualThemeConfig` | `{}` | Optional theme overrides (prefer CSS variables) |
136
+ | `classNames` | `HeaderClassNames` | `{}` | Inject CSS classes into structural elements |
107
137
 
108
- ### Config Objects
138
+ ---
109
139
 
110
- #### DualThemeConfig
140
+ ## Configuration Objects
111
141
 
112
- | Propery | Type | Description |
113
- | ------- | ------------- | ---------------------------- |
114
- | `light` | `ThemeConfig` | Styles applied in light mode |
115
- | `dark` | `ThemeConfig` | Styles applied in dark mode |
142
+ ### `NavConfig`
116
143
 
117
- #### ThemeConfig
144
+ | Property | Type | Default | Description |
145
+ | --------------------- | ------------ | ------- | ------------------------------------------------------- |
146
+ | `menuItems` | `MenuItem[]` | `[]` | Top-level navigation items |
147
+ | `homeUrl` | `string` | `"/"` | URL for the home link |
148
+ | `header__menu__class` | `string` | — | Extra CSS class(es) for the desktop `<nav>` element |
149
+ | `header__item__class` | `string` | — | Extra CSS class(es) for each top-level `<li>` menu item |
150
+ | `menu__link__class` | `string` | — | Extra CSS class(es) for each top-level `<a>` link |
118
151
 
119
- | Propery | Type | Default |
120
- | ----------------------- | -------- | ---------------- |
121
- | `backgroundColor` | `string` | _Preset default_ |
122
- | `backgroundColorOpaque` | `string` | _Preset default_ |
123
- | `backdropBlur` | `string` | `"blur(20px)"` |
124
- | `zIndex` | `number` | `10` |
125
- | `textColor` | `string` | _Preset default_ |
126
- | `accentColor` | `string` | _Preset default_ |
152
+ ### `MenuItem`
127
153
 
128
- > [!IMPORTANT]
129
- > **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**.
130
- >
131
- > If you use a transparent `backgroundColor` (e.g., `rgba`), remember to also provide its solid counterpart in `backgroundColorOpaque`.
154
+ | Property | Type | Required | Description |
155
+ | --------- | --------------------- | -------- | --------------------------------- |
156
+ | `link` | `string` | Yes | URL the item points to |
157
+ | `text` | `string` | Yes | Display label |
158
+ | `submenu` | `SecondaryMenuItem[]` | No | Optional nested items (2nd level) |
159
+
160
+ ### `SecondaryMenuItem`
161
+
162
+ | Property | Type | Required | Description |
163
+ | --------- | -------------------- | -------- | --------------------------------- |
164
+ | `link` | `string` | Yes | URL the item points to |
165
+ | `text` | `string` | Yes | Display label |
166
+ | `submenu` | `TertiaryMenuItem[]` | No | Optional nested items (3rd level) |
167
+
168
+ ### `TertiaryMenuItem`
169
+
170
+ | Property | Type | Required | Description |
171
+ | -------- | -------- | -------- | ---------------------- |
172
+ | `link` | `string` | Yes | URL the item points to |
173
+ | `text` | `string` | Yes | Display label |
174
+
175
+ ---
176
+
177
+ ## Slots
178
+
179
+ | Slot name | Visible on | Description |
180
+ | --------- | ---------------- | ---------------------------------------------------------- |
181
+ | `logo` | Header | Render your logo exactly as you need (native HTML/widgets) |
182
+ | `actions` | Desktop + mobile | Add buttons, links, or utility widgets |
132
183
 
133
184
  ```astro
134
- <Header theme={{
135
- light: {
136
- backgroundColor: "rgba(255, 255, 255, 0.7)", // Transparent header body
137
- backgroundColorOpaque: "#ffffff", // Solid submenus
138
- }
139
- }} />
185
+ <Header navigation={{ menuItems }}>
186
+ <a slot="logo" href="/" class="logo-link">
187
+ <img src="/logo.svg" alt="Branding" width="40" />
188
+ <span>MyBrand</span>
189
+ </a>
190
+ <div slot="actions">
191
+ <a href="/login" class="btn">Log in</a>
192
+ </div>
193
+ </Header>
140
194
  ```
141
195
 
142
- #### CustomClassNames → HeaderClassNames
196
+ Slotted markup is rendered by *your* page, so your page's CSS applies to it normally. Style the logo with your own classes (this replaces the removed `classNames.logo` / `classNames.logoText`).
143
197
 
144
- > [!NOTE]
145
- > `CustomClassNames` was renamed to `HeaderClassNames` in v2.1. The old name still works as a type alias — no migration required.
198
+ ---
146
199
 
147
- The `classNames` prop targets the **structural wrapper elements** of the Header. For fine-grained control of individual nav links or the logo internals, use the nested `xxx__class` props inside the `navigation` or `logo` config objects instead.
200
+ ## Custom Class Names
148
201
 
149
- | Property | Target Element | Purpose & Common Use Cases |
150
- | ----------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
151
- | `container` | Outer `div` wrapping the header | **Positioning & Layout**: `top-0`, `z-50`, `fixed`, adjusting `max-width` and `mx-auto` logic. |
152
- | `header` | Inner `<header>` element | **Appearance**: Shadows (`shadow-md`), borders (`border-b`), or custom transition durations. |
153
- | `logo` | `<a>` tag surrounding the logo | **Interactions**: Hover states, custom focus rings, or adjusting the flex alignment of the logo group. |
154
- | `logoText` | `<span>` tag containing the logo text | **Typography**: Font weights, text shadows, or specific tracking/leading classes. |
155
- | `nav` | `<div>` wrapping the desktop navigation items | **Desktop Layout**: Spacing between the logo and the menu, or responsive visibility classes (`hidden md:flex`). |
156
- | `mobileNav` | Root `<nav>` of the mobile slide-in panel | **Mobile Panel**: Extra backdrop blur, custom z-index, slide-in overrides. |
202
+ The `classNames` prop injects CSS classes into specific structural elements.
157
203
 
158
- ##### Usage Examples
204
+ ### `HeaderClassNames`
159
205
 
160
- **Premium Shadow & Border:**
206
+ | Property | Target element | Common use cases |
207
+ | ----------- | -------------------------------------- | ------------------------------------------------- |
208
+ | `container` | Outer `<div>` wrapping the header | Positioning, padding, outer gutters |
209
+ | `header` | Inner `<header>` element | Shadows, borders, transitions, radius, backdrop |
210
+ | `nav` | `<div>` wrapping the desktop nav items | Spacing, alignment, responsive visibility |
211
+ | `mobileNav` | Root `<nav>` of the mobile panel | Backdrop blur, slide-in overrides, panel width |
161
212
 
162
213
  ```astro
214
+ <!-- Tailwind example -->
163
215
  <Header
164
216
  classNames={{
165
- header: "shadow-xl border-b border-black/5 dark:border-white/10 transition-all duration-500",
217
+ header: "shadow-xl border-b border-black/5 dark:border-white/10",
166
218
  container: "top-4 px-6",
167
219
  mobileNav: "backdrop-blur-md",
168
220
  }}
169
221
  />
170
222
  ```
171
223
 
172
- **Custom Typography & Nav Spacing:**
224
+ ---
225
+
226
+ ## Styling Guide
227
+
228
+ Everything below assumes you want to change how the header looks or behaves. Pick the lightest tool that solves your case:
229
+
230
+ | Goal | Use |
231
+ | --- | --- |
232
+ | One-off tweaks (shadow, border, padding, radius) | `classNames` with utilities |
233
+ | A different overall design (square, full-width, compact) | Your own CSS in `@layer utilities` |
234
+ | Restyle nav links, dropdowns, or the mobile panel | Selectors targeting the internal class hooks |
235
+ | Brand colors, blur, background | CSS variables (`--l-*` / `--d-*`) |
236
+ | Per-instance tokens or z-index | `theme` prop |
237
+ | Logo / buttons markup | Slots |
238
+
239
+ ### Style precedence
240
+
241
+ The component ships this statement ahead of its own rules:
242
+
243
+ ```css
244
+ @layer theme, base, components, utilities;
245
+ ```
246
+
247
+ Priority, lowest to highest:
248
+
249
+ ```text
250
+ base < components (this header) < utilities (your classNames/overrides) < unlayered CSS
251
+ ```
252
+
253
+ Rules of thumb:
254
+
255
+ - Put your resets and element defaults in `@layer base`.
256
+ - Put overrides for this header in `@layer utilities` (or pass them as `classNames`).
257
+ - Keep in mind that CSS **outside** any layer beats everything layered, including this component. That is the spec, not a bug.
258
+ - If you write plain CSS with no framework, declare the order statement at the top of your global stylesheet so the order is fixed before any layer is used. Tailwind v4 does this for you (`@layer theme, base, components, utilities;`).
259
+
260
+ ### Example: redesign with plain CSS
261
+
262
+ Square, full-width, no glass effect — the classic "make it look like a different component" case:
173
263
 
174
264
  ```astro
175
265
  <Header
176
266
  classNames={{
177
- logoText: "tracking-tighter font-black italic uppercase",
178
- nav: "ml-auto gap-8",
267
+ container: "header-wide",
268
+ header: "header-square",
179
269
  }}
180
270
  />
181
271
  ```
182
272
 
183
- > [!TIP]
184
- > Since these classes are injected using Astro's `class:list`, you can also pass objects or arrays if you need conditional logic for your custom classes.
273
+ ```css
274
+ @layer utilities {
275
+ .header-wide {
276
+ padding: 0;
277
+ }
185
278
 
279
+ .header-square {
280
+ max-width: 100%;
281
+ border-radius: 0;
282
+ box-shadow: none;
283
+ backdrop-filter: none;
284
+ background-color: #ffffff;
285
+ border-bottom: 1px solid #e5e7eb;
286
+ padding: 0.75rem 2rem;
287
+ }
288
+ }
289
+ ```
186
290
 
291
+ No `!important`, and the same classes work whether you are on Tailwind or not.
187
292
 
188
- #### NavConfig
293
+ ### Example: compact sticky bar
189
294
 
190
- | Property | Type | Description |
191
- | --------------------- | ------------ | ------------------------------------------------------- |
192
- | `homeUrl` | `string` | URL for the home link (defaults to `/`) |
193
- | `menuItems` | `MenuItem[]` | Array of navigation menu items |
194
- | `header__menu__class` | `string` | Extra CSS class(es) for the desktop `<nav>` element |
195
- | `header__item__class` | `string` | Extra CSS class(es) for each top-level `<li>` menu item |
196
- | `menu__link__class` | `string` | Extra CSS class(es) for each top-level `<a>` link |
295
+ The container is `position: fixed` by default. To make the header participate in normal flow (or stick to the top while scrolling):
197
296
 
198
- #### MenuItem
297
+ ```astro
298
+ <Header classNames={{ container: "nav-sticky", header: "nav-compact" }} />
299
+ ```
199
300
 
200
- | Property | Type | Description |
201
- | --------- | --------------------- | ----------------------------------- |
202
- | `link` | `string` | URL the menu item points to |
203
- | `text` | `string` | Label text for the menu item |
204
- | `submenu` | `SecondaryMenuItem[]` | Optional array of nested menu items |
301
+ ```css
302
+ @layer utilities {
303
+ .nav-sticky {
304
+ position: sticky;
305
+ top: 0;
306
+ }
205
307
 
206
- ## Slots Support
308
+ .nav-compact {
309
+ padding-top: 0.25rem;
310
+ padding-bottom: 0.25rem;
311
+ border-radius: 0;
312
+ }
313
+ }
314
+ ```
315
+
316
+ ### Example: override internal elements
317
+
318
+ The header renders regular DOM (no shadow root), so any selector reaches the internals. Wrap the header in a class to scope your rules to one page:
319
+
320
+ ```astro
321
+ <div class="docs-header">
322
+ <Header navigation={{ menuItems }} />
323
+ </div>
324
+ ```
325
+
326
+ ```css
327
+ @layer utilities {
328
+ /* Typography of the desktop links */
329
+ .docs-header .menu__link {
330
+ text-transform: uppercase;
331
+ letter-spacing: 0.02em;
332
+ font-size: 0.875rem;
333
+ }
334
+
335
+ /* Dropdown surfaces */
336
+ .docs-header .submenu,
337
+ .docs-header .subsubmenu {
338
+ border-radius: 12px;
339
+ padding: 0.75rem;
340
+ box-shadow: 0 12px 32px rgb(0 0 0 / 0.18);
341
+ }
342
+
343
+ /* Hover underline color */
344
+ .docs-header .menu__link::after {
345
+ background: #7c3aed;
346
+ height: 3px;
347
+ top: 30px;
348
+ }
207
349
 
208
- The Header component provides a flexible slot system that allows you to add additional content:
350
+ /* Active link in the mobile panel */
351
+ .docs-header .mobile-menu__link.active {
352
+ color: #7c3aed;
353
+ }
354
+ }
355
+ ```
209
356
 
210
- ### Available Slots
357
+ Because these live in `utilities`, they beat the component's `components` layer regardless of specificity.
211
358
 
212
- | Slot Name | Location | Visibility | Description |
213
- | --------- | --------------------- | --------------------- | ---------------------------------------- |
214
- | `logo` | Header Desktop/Mobile | Always | Add your custom logo HTML/Components |
215
- | `actions` | Header & Mobile panel | Responsive visibility | Add action buttons (login, signup, etc.) |
359
+ ### Example: custom responsive breakpoint
216
360
 
217
- ### Example with Slots
361
+ The desktop nav hides below `768px`. To keep it visible down to `640px`:
362
+
363
+ ```css
364
+ @layer utilities {
365
+ @media (width >= 640px) and (width < 768px) {
366
+ .docs-header .header__menu {
367
+ display: flex;
368
+ }
369
+
370
+ .docs-header .hamburger {
371
+ display: none;
372
+ }
373
+ }
374
+ }
375
+ ```
376
+
377
+ Media queries live inside the rule, so you re-declare the property at the breakpoints you care about; the layer decides which declaration wins.
378
+
379
+ ### Example: dark-mode-only tweaks
380
+
381
+ ```css
382
+ @layer utilities {
383
+ :root.dark .docs-header .header {
384
+ --d-bg: rgb(17 24 39 / 0.85);
385
+ box-shadow: 0 8px 30px rgb(0 0 0 / 0.35);
386
+ }
387
+ }
388
+ ```
389
+
390
+ ### Example: colors with CSS variables
391
+
392
+ See [Customization & Theme Config](#customization--theme-config) for the full variable reference. Global (whole site):
393
+
394
+ ```css
395
+ :root {
396
+ --l-accent: #7c3aed;
397
+ --l-bg: rgb(255 255 255 / 0.85);
398
+ --d-accent: #a78bfa;
399
+ --d-bg: rgb(10 10 10 / 0.85);
400
+ }
401
+ ```
402
+
403
+ Scoped to one section (marketing page gets a purple tint, the docs stay neutral):
404
+
405
+ ```css
406
+ .marketing-hero {
407
+ --l-bg: rgb(124 58 237 / 0.14);
408
+ --l-blur: blur(28px);
409
+ }
410
+ ```
411
+
412
+ ```astro
413
+ <section class="marketing-hero">
414
+ <Header navigation={{ menuItems }} />
415
+ </section>
416
+ ```
417
+
418
+ Variables are inherited, so defining them on any ancestor of the header works.
419
+
420
+ ### Example: z-index and per-instance tokens with the `theme` prop
421
+
422
+ The container renders an **inline** `style="z-index: N"`, and inline styles beat any non-`!important` declaration — so `classNames.container="z-50"` (or any z-index utility) will not win. Use the prop:
218
423
 
219
424
  ```astro
220
425
  ---
221
- import Header from '@sofidevo/astro-dynamic-header/Header';
426
+ import { defaultThemes } from '@sofidevo/astro-dynamic-header';
222
427
 
223
- const navigation = {
224
- menuItems: [{ link: '/about', text: 'About' }]
428
+ const theme = {
429
+ light: { ...defaultThemes.light, zIndex: 60 },
430
+ dark: { ...defaultThemes.dark, zIndex: 60 },
225
431
  };
226
432
  ---
227
433
 
228
- <Header navigation={navigation}>
229
- <div slot="actions">
230
- <button class="login-btn">Login</button>
231
- </div>
232
- </Header>
434
+ <Header theme={theme} classNames={{ header: "shadow-2xl" }} />
233
435
  ```
234
436
 
235
- ## Comprehensive Example
437
+ > [!NOTE]
438
+ > `zIndex` resolves as `theme.light.zIndex ?? theme.dark.zIndex ?? 10`, so setting it in either block is enough. The value is a single inline number; it does not change between light and dark mode.
236
439
 
237
- Below is a complete implementation example showcasing custom logo configuration, navigation with a home URL, and theme overrides.
440
+ ### Example: mobile panel
238
441
 
239
442
  ```astro
240
- ---
241
- import Header from '@sofidevo/astro-dynamic-header/Header';
443
+ <Header classNames={{ mobileNav: "mobile-wide" }} />
444
+ ```
242
445
 
243
- const menuItems = [
244
- {
245
- link: "#",
246
- text: "Services",
247
- submenu: [
248
- { link: "/design", text: "Design" },
249
- { link: "/consulting", text: "Consulting" },
250
- {
251
- link: "#",
252
- text: "Web Development",
253
- submenu: [
254
- { link: "/web/frontend", text: "Frontend" },
255
- { link: "/web/backend", text: "Backend" },
256
- { link: "/web/fullstack", text: "Full Stack" },
257
- ],
258
- },
259
- ],
260
- },
261
- { link: "/about", text: "About" },
262
- { link: "/contact", text: "Contact" },
263
- ];
446
+ ```css
447
+ @layer utilities {
448
+ /* Full-bleed panel instead of the default over-wide slide-in */
449
+ .mobile-wide {
450
+ width: 100vw;
451
+ }
264
452
 
265
- ];
266
- ---
453
+ .mobile-wide.is-active {
454
+ transform: translateX(0);
455
+ }
267
456
 
268
- <style is:inline>
269
- :root {
270
- /* Pure CSS Theming Configuration */
271
- --l-accent: #ff0000;
272
- --l-bg: rgba(255, 255, 255, 0.8);
273
-
274
- --d-accent: #00ffff;
275
- --d-bg: rgba(20, 20, 20, 0.9);
457
+ /* Softer surface + accent links */
458
+ .mobile-wide .mobile-menu li a {
459
+ border-color: rgb(124 58 237 / 0.35);
460
+ font-size: 1.1rem;
276
461
  }
277
- </style>
462
+ }
463
+ ```
278
464
 
279
- <Header
280
- headerType="floating"
281
- preset="dark"
282
- navigation={{
283
- homeUrl: "/",
284
- menuItems: menuItems,
285
- header__menu__class: "flex gap-6",
286
- menu__link__class: "font-medium",
287
- }}
288
- classNames={{
289
- header: "shadow-xl",
290
- mobileNav: "backdrop-blur-md",
291
- }}
292
- >
293
- <a slot="logo" href="/" style="display: flex; align-items: center; gap: 10px; color: inherit; text-decoration: none;">
294
- <img src="https://sofidev.blog/img/branding/logo.webp" alt="My Site Logo" width="44" />
295
- <span style="font-weight: bold; font-size: 1.2rem;">SofiDev</span>
296
- </a>
297
- <button slot="actions">Login</button>
298
- </Header>
465
+ If you prefer the fullscreen slide behavior everywhere, render with `headerType="fullscreen"` — the panel then uses `translate(0)` when open.
466
+
467
+ ### Example: hamburger and icons
468
+
469
+ The hamburger lines follow the header text color (`--l-text` / `--d-text`), so recoloring the text recolors the lines:
470
+
471
+ ```css
472
+ :root {
473
+ --l-text: #111827;
474
+ --d-text: #f9fafb;
475
+ }
299
476
  ```
300
477
 
301
- ## Header Types
478
+ Shape and size of the button itself:
302
479
 
303
- ### Floating Header
480
+ ```astro
481
+ <Header classNames={{ header: "header-round-btn" }} />
482
+ ```
304
483
 
305
- - Centered with max-width constraint
306
- - Rounded corners
307
- - Padding around container
308
- - Perfect for modern, card-like designs
484
+ ```css
485
+ @layer utilities {
486
+ #hamburger-btn {
487
+ border-radius: 999px;
488
+ padding: 0.6rem 0.9rem;
489
+ }
490
+ }
491
+ ```
492
+
493
+ The sun and chevron icons use `currentColor`, so they follow the surrounding text color automatically. The moon glyph uses `var(--svg-color--fff, #fff)` instead, so it stays white unless you override that variable.
494
+
495
+ ### Internal class hooks
309
496
 
310
- ### Fullscreen Header
497
+ | Element | Classes / id |
498
+ | --- | --- |
499
+ | Outer wrapper | `.header__container`, `.header__container--floating`, `.header__container--fullscreen` |
500
+ | Header bar | `.header`, `.header--floating`, `.header--fullscreen`, `.header--force-light`, `.header--force-dark` |
501
+ | Desktop nav wrapper | `.nav-menu-wrapper` |
502
+ | Desktop nav | `#header-menu`, `.header__menu`, `.menu`, `.menu__item`, `.header__item`, `.menu__link` |
503
+ | Dropdowns | `.submenu`, `.subsubmenu`, `.submenu__item--secondary`, `.submenu__item--tertiary` |
504
+ | Hamburger | `#hamburger-btn`, `.hamburger`, `.hamburger-box`, `.hamburger-inner` |
505
+ | Mobile panel | `#mobile-header-menu`, `.mobile-header__menu`, `.mobile-header__menu--floating`, `.mobile-header__menu--fullscreen` |
506
+ | Mobile list | `.mobile-menu`, `.mobile-menu__link`, `.mobile-details`, `.menu__summary`, `.mobile-submenu`, `.mobile-subsubmenu` |
507
+ | Slot wrappers | `.actions-desktop`, `.actions-mobile` |
311
508
 
312
- - Full viewport width
313
- - No border radius
314
- - Edge-to-edge design
315
- - Ideal for traditional website layouts
509
+ ### Styling caveats
510
+
511
+ - **z-index is inline.** Use `theme.zIndex`, not a utility class, on `container`.
512
+ - **`!important` in a layer still works** (important declarations reverse layer order), but you should not need it.
513
+ - **Unlayered CSS beats everything layered.** If a global rule seems "too strong", that is why — move it into `@layer base`.
514
+ - **Astro scopes component styles with `data-astro-cid-*` attributes.** Your selectors do not need them; plain class selectors work.
515
+
516
+ ---
316
517
 
317
- ## Styling and Customization
518
+ ## Customization & Theme Config
318
519
 
319
- The component uses CSS custom properties that you can override:
520
+ You can fully customize the color scheme using **CSS Custom Properties** (recommended) or the `theme` prop.
521
+
522
+ ### CSS variable reference
523
+
524
+ Input variables (set them wherever the header lives — `:root`, a wrapper, or the `theme` prop):
525
+
526
+ | Variable | Used for | Light default | Dark default |
527
+ | --- | --- | --- | --- |
528
+ | `--l-bg` / `--d-bg` | Header background (translucent) | `rgba(255, 255, 255, 0.9)` | `#0d0d0dcc` |
529
+ | `--l-bg-opaque` / `--d-bg-opaque` | Solid background of dropdowns and the mobile panel | `rgb(255, 255, 255)` | `#0d0d0d` |
530
+ | `--l-text` / `--d-text` | Text, hamburger lines, icons | `#1a1a1a` | `#ffffff` |
531
+ | `--l-accent` / `--d-accent` | Hover underline, active links, dashed borders | `#3e1c71` | `#00ffff` |
532
+ | `--l-blur` / `--d-blur` | `backdrop-filter` value | `blur(20px)` | `blur(20px)` |
533
+
534
+ Derived variables (resolved by the component per theme state; override them only if you need to target internals directly):
535
+
536
+ | Variable | Resolves to |
537
+ | --- | --- |
538
+ | `--bg-color` | `--l-bg` or `--d-bg` |
539
+ | `--bg-color-opaque` | `--l-bg-opaque` or `--d-bg-opaque` |
540
+ | `--text-color` | `--l-text` or `--d-text` |
541
+ | `--accent-color` | `--l-accent` or `--d-accent` |
542
+ | `--backdrop-blur` | `--l-blur` or `--d-blur` |
543
+
544
+ ### Option 1: Native CSS Variables (Recommended)
320
545
 
321
546
  ```css
322
547
  :root {
323
- --light-spot-color: #00ffff;
324
- --color-tertiary: #ffffff;
325
- --color-hamburger-lines: #ffffff;
548
+ /* Light theme overrides */
549
+ --l-accent: #7c3aed;
550
+ --l-bg: rgba(255, 255, 255, 0.85);
551
+ --l-bg-opaque: #ffffff;
552
+ --l-text: #1a1a1a;
553
+ --l-blur: blur(20px);
554
+
555
+ /* Dark theme overrides */
556
+ --d-accent: #a78bfa;
557
+ --d-bg: rgba(10, 10, 10, 0.85);
558
+ --d-bg-opaque: #0a0a0a;
559
+ --d-text: #f5f5f5;
560
+ --d-blur: blur(20px);
326
561
  }
327
562
  ```
328
563
 
329
- ## TypeScript Support
564
+ The hamburger lines and the sun/chevron icons follow `--l-text` / `--d-text`, so text color drives them too. The moon glyph keeps its own white fill (`--svg-color--fff`, default `#fff`).
330
565
 
331
- The package provides full TypeScript support. You can import types to ensure your configuration is correct:
566
+ ### Option 2: JS Theme Prop
332
567
 
333
568
  ```astro
334
569
  ---
335
- import Header from '@sofidevo/astro-dynamic-header/Header';
336
570
  import { defaultThemes } from '@sofidevo/astro-dynamic-header';
571
+
572
+ const theme = {
573
+ light: {
574
+ ...defaultThemes.light,
575
+ accentColor: "#7c3aed",
576
+ backgroundColor: "rgba(255, 255, 255, 0.85)",
577
+ },
578
+ dark: {
579
+ ...defaultThemes.dark,
580
+ accentColor: "#a78bfa",
581
+ }
582
+ };
583
+ ---
584
+
585
+ <Header theme={theme} />
586
+ ```
587
+
588
+ The prop writes the variables as inline styles on the header element, so it wins over `:root` values for that instance.
589
+
590
+ > [!IMPORTANT]
591
+ > When using transparent backgrounds, always supply a solid fallback in `backgroundColorOpaque`. Submenus and mobile panels utilize this solid color to prevent visual glitches with nested blur effects.
592
+
593
+ ---
594
+
595
+ ## TypeScript
596
+
597
+ ```astro
598
+ ---
599
+ import Header from '@sofidevo/astro-dynamic-header/Header';
337
600
  import type {
601
+ MenuItem,
338
602
  NavConfig,
339
- DualThemeConfig,
340
603
  HeaderClassNames,
341
- MenuItem,
342
- SecondaryMenuItem
604
+ HeaderProps,
343
605
  } from '@sofidevo/astro-dynamic-header';
344
606
 
345
- const navigation: NavConfig = {
346
- menuItems: [
347
- {
348
- link: '/products',
349
- text: 'Products',
350
- submenu: [
351
- { link: '/software', text: 'Software' },
352
- { link: '/hardware', text: 'Hardware' }
353
- ]
354
- }
355
- ],
356
- header__menu__class: "flex gap-6",
357
- };
607
+ const menuItems: MenuItem[] = [
608
+ { link: '/about', text: 'About' }
609
+ ];
358
610
 
359
- // Prefer CSS custom properties globally rather than passing the `theme` object.
360
- // But you can still use the theme prop if needed:
361
- const theme: DualThemeConfig = {
362
- light: { ...defaultThemes.light, accentColor: "#3e1c71" },
363
- dark: { ...defaultThemes.dark, accentColor: "#00ffff", backgroundColor: "rgba(10, 10, 10, 0.9)" },
611
+ const navigation: NavConfig = {
612
+ menuItems,
613
+ homeUrl: "/",
614
+ header__menu__class: "flex gap-4"
364
615
  };
365
616
 
366
617
  const classNames: HeaderClassNames = {
367
- header: "shadow-xl",
368
- mobileNav: "backdrop-blur-md",
618
+ header: "shadow-lg"
369
619
  };
370
620
  ---
371
621
 
372
622
  <Header
373
623
  navigation={navigation}
374
624
  classNames={classNames}
375
- preset="auto"
376
625
  />
377
626
  ```
378
627
 
379
- ### Available Types
380
-
381
- | Type | Description |
382
- | ------------------- | ----------------------------------------------- |
383
- | `MenuItem` | Top-level menu item with optional properties |
384
- | `SecondaryMenuItem` | Second-level menu item |
385
- | `TertiaryMenuItem` | Third-level menu item |
386
- | `NavConfig` | Navigation config (items + nested class props) |
387
- | `ThemeConfig` | Individual theme settings (colors, blur, etc.) |
388
- | `DualThemeConfig` | Combined settings for light and dark modes |
389
- | `HeaderClassNames` | Class overrides for structural wrapper elements |
390
- | `CustomClassNames` | **Deprecated alias** for `HeaderClassNames` |
391
- | `HeaderProps` | Main props for the Header component |
392
-
393
- ## Browser Support
394
-
395
- - All modern browsers (Chrome, Firefox, Safari, Edge)
396
- - Mobile responsive design with optimized touch targets
397
- - Supports CSS `backdrop-filter` for glassmorphism
398
- - Automatic theme switching based on OS or site preference via `.dark` class
399
-
400
- ## Troubleshooting
401
-
402
- ### Import Issues
403
-
404
- If you encounter import errors, try these solutions:
405
-
406
- 1. **Use direct subpath import:**
407
-
408
- ```astro
409
- import Header from '@sofidevo/astro-dynamic-header/Header';
410
- ```
411
-
412
- 2. **Check relative imports in TS:**
413
- 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.
414
-
415
- 3. **Verify TypeScript configuration:**
628
+ ---
416
629
 
417
- ```json
418
- // tsconfig.json
419
- {
420
- "compilerOptions": {
421
- "moduleResolution": "bundler",
422
- "allowImportingTsExtensions": true
423
- }
424
- }
425
- ```
630
+ ## Development
426
631
 
427
- ### Compatibility
632
+ ```bash
633
+ pnpm install
634
+ pnpm dev # demo at localhost:4321
635
+ pnpm check # astro check (0 errors expected)
636
+ pnpm test # vitest: unit + rendering + built-CSS tests
637
+ pnpm build # builds the demo site
638
+ ```
428
639
 
429
- - Astro 4.x and 5.x
430
- - SSG Projects (Static Site Generation)
431
- - SSR Projects (Server-Side Rendering)
432
- - Hybrid Projects (output: 'hybrid')
640
+ The test suite runs in two Vitest projects: `dom` (jsdom, exercises the extracted behaviour scripts) and `astro` (Node, renders `Header.astro` through Astro's Container API and asserts the built CSS contract — layer order, rules inside `@layer components`, no `!important`). See `tests/README.md`.
433
641
 
434
- ## Live Examples
642
+ ---
435
643
 
436
- Visit our demo website to see the component in action with interactive examples and complete documentation.
644
+ ## Troubleshooting & FAQ
437
645
 
438
- ## License
646
+ ### Import issues
439
647
 
440
- MIT License - see the [LICENSE](./LICENSE) file for details.
648
+ Import using the direct subpath:
441
649
 
442
- ## Support
650
+ ```astro
651
+ import Header from '@sofidevo/astro-dynamic-header/Header';
652
+ ```
443
653
 
444
- If you find this package helpful, please consider giving it a star on GitHub!
654
+ Other components are exported similarly:
445
655
 
446
- ---
656
+ ```astro
657
+ import NavMenu from '@sofidevo/astro-dynamic-header/NavMenu';
658
+ import MobileNav from '@sofidevo/astro-dynamic-header/MobileNav';
659
+ import ChevronIcon from '@sofidevo/astro-dynamic-header/ChevronIcon';
660
+ ```
447
661
 
448
- ## Changelog
662
+ ### My overrides do not apply
449
663
 
450
- ### v2.2 — Performance & DX Optimization
664
+ Check these in order:
451
665
 
452
- #### Breaking changes
666
+ 1. **Which layer is your rule in?** Overrides belong in `@layer utilities`, or in a class passed through `classNames`. Rules in `@layer base` (and your resets) lose to the component by design.
667
+ 2. **Are you fighting the inline z-index?** `container` always renders `style="z-index: N"`. Use `theme.zIndex` instead of a z-index utility.
668
+ 3. **Are you selecting the right hook?** See the [internal class hooks](#internal-class-hooks) table; plain class selectors are enough (you do not need `data-astro-cid-*`).
669
+ 4. **Is an inline style winning?** Inline styles beat every non-`!important` declaration, layered or not. The header renders inline styles for `z-index` and for any `theme` values you pass.
670
+ 5. **Tailwind v4?** Its layer order matches this component exactly, so utilities work automatically. Make sure `@import "tailwindcss"` comes first in your entry CSS.
453
671
 
454
- - **Removed `logo` Object Configuration prop**: The `logo` property and `LogoConfig` interface have been removed. You should now use `<slot name="logo" />` to render your logo exactly as you need, passing native HTML or Astro components.
455
- - **Removed `color` prop from `HamburgerButton`**: The `HamburgerButton` now inherits `--text-color` natively via CSS, which fixes a bug where the button would not properly update its color when switching to dark mode. Any direct uses of `<HamburgerButton color="..." />` will fail to compile in TypeScript and should be updated to rely on global CSS variables or context inheritance.
672
+ ### Preflight changed my nav link colors (Tailwind v3)
456
673
 
457
- #### New features and enhancements
674
+ Tailwind v3 emits Preflight and utilities **outside** native cascade layers (v3 hijacks `@layer` as its own directive), and CSS outside a layer beats CSS inside a layer. Preflight's `a { color: inherit; ... }` therefore overrides the component's layered link colors.
458
675
 
459
- - **Native CSS Variable Theming (Better DX)**: Redesigned how default styles are injected into the DOM. `Header.astro` no longer injects all default properties into `style={...}` attributes on load, solving the "DOM bloat" issue. You can now deeply customize the component purely by setting CSS variables like `--l-bg`, `--d-bg`, `--l-accent`, and `--d-accent` inside your `:root` style tag, removing the necessity to parse massive `theme` JS objects.
460
- - **Dead Code Elimination**: Cleaned up repetitive prop destructuring inside `MobileNav.astro`.
676
+ Options:
461
677
 
462
- ### v2.1 — Style & Customization Refactor
678
+ 1. **Upgrade to Tailwind v4** (recommended) — native layers with the exact order `theme, base, components, utilities`, so everything lines up.
679
+ 2. **Disable Preflight** in v3: `module.exports = { corePlugins: { preflight: false } }`.
680
+ 3. **Compensate** with your own unlayered rule (unlayered wins):
463
681
 
464
- #### New features
682
+ ```css
683
+ .header__menu a,
684
+ .mobile-menu a {
685
+ color: var(--accent-color, #3e1c71);
686
+ }
687
+ ```
465
688
 
466
- - **`HeaderClassNames`** replaces `CustomClassNames` (alias kept — no migration required).
467
- New `mobileNav` slot targets the mobile slide-in `<nav>` panel.
689
+ Note that v3 *utilities* still work as expected, because unlayered utilities also beat the component.
468
690
 
469
- - **Nested class props** on `LogoConfig`:
470
- | Prop | Targets |
471
- |------|---------|
472
- | `logo__container__class` | Logo `<a>` wrapper |
473
- | `logo__text__class` | Logo text `<span>` |
691
+ ### Accordion icons not showing up
474
692
 
475
- - **Nested class props** on `NavConfig`:
476
- | Prop | Targets |
477
- |------|---------|
478
- | `header__menu__class` | Desktop `<nav>` element |
479
- | `header__item__class` | Each top-level `<li>` |
480
- | `menu__link__class` | Each top-level `<a>` |
693
+ Icons are rendered as inline SVG components. If you are upgrading from `v1.x` or `v2.0` and have the old ChevronIcon CDN `<script>` tag in your layout `<head>`, you can safely remove it.
481
694
 
482
- - **`defaultThemes` exported** — import and spread the built-in tokens to extend them:
483
- ```ts
484
- import { defaultThemes } from "@sofidevo/astro-dynamic-header";
485
- const theme = { dark: { ...defaultThemes.dark, accentColor: "#f43f5e" } };
486
- ```
695
+ ### The header sits behind my other content
487
696
 
488
- #### Internal improvements
697
+ Set a z-index through the `theme` prop (`theme={{ light: { zIndex: 60 } }}`), not through a class on `container`. See [z-index and per-instance tokens](#example-z-index-and-per-instance-tokens-with-the-theme-prop).
489
698
 
490
- - Default theme tokens extracted to `src/defaults.ts` — easier to read and maintain.
491
- - `HamburgerButton` color is now explicitly wired from the resolved theme (no visual change).
699
+ ---
492
700
 
493
- #### Deprecations
701
+ ## Compatibility
494
702
 
495
- - `CustomClassNames` — use `HeaderClassNames` instead. The alias will remain until the next major version.
703
+ - **Astro 7.x** (peer dependency), SSG, SSR, and CSS builds (no client framework required).
704
+ - **CSS cascade layers** require a modern browser: Chrome/Edge 99+, Firefox 97+, Safari 15.4+.
705
+ - Works with Tailwind v4 out of the box; Tailwind v3 works with the Preflight caveat above.
496
706
 
497
- ### v2.0 — Object-based Configuration API
707
+ ---
498
708
 
499
- > [!WARNING]
500
- > Breaking change: `logo` changed from `string` to `LogoConfig`, `navigation` changed from `MenuItem[]` to `NavConfig`.
709
+ ## License
501
710
 
502
- - `logo` prop restructured to `LogoConfig` object (`src`, `alt`, `width`, `text`, `textSize`, `textColor`).
503
- - `navigation` prop restructured to `NavConfig` object (`homeUrl`, `menuItems`).
504
- - `classNames` prop introduced for CSS class injection (`CustomClassNames`).
505
- - Dual-theme support via `DualThemeConfig` (`light` + `dark`).
711
+ MIT License.