@sofidevo/astro-dynamic-header 4.0.1 → 5.0.1

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
@@ -2,7 +2,7 @@
2
2
 
3
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
- 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`**.
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`**. As of v5.0.0 theming is CSS-variables-only (`--l-*`, `--d-*`, `--header-z-index`): the `theme` prop was removed and the component renders no inline styles.
6
6
 
7
7
  ## Features
8
8
 
@@ -12,9 +12,10 @@ As of v4.0.0 the component styles are shipped inside CSS cascade layers, so any
12
12
  - **Dark Mode Ready** — auto-detects `.dark` on `<html>`, or forces a state with `preset`.
13
13
  - **Inline SVG Icons** — no external CDNs, no extra network requests, no flash of missing icons.
14
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.
15
+ - **Pure CSS Customization** — background, blur, colors, and z-index via native CSS variables.
16
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.
17
+ - **Dev diagnostics** — in dev, warns in the console (with the fix) when your overrides cannot win or an old v3.x install is detected.
18
+ - **Full TypeScript** — all props and config interfaces are fully typed and documented (TSDoc).
18
19
 
19
20
  ### Live Demo
20
21
 
@@ -62,6 +63,64 @@ const navigation = { menuItems };
62
63
 
63
64
  ---
64
65
 
66
+ ## Breaking Changes in v5.0.0
67
+
68
+ > [!WARNING]
69
+ > v5.0.0 removes the JavaScript theming API. The component now has a single theming mechanism: CSS variables.
70
+
71
+ ### 1. The `theme` prop and `defaultThemes` were removed
72
+
73
+ Colors, blur, and z-index are plain CSS variables — one mechanism instead of two, and no inline styles at all, so layered CSS and utilities can always win.
74
+
75
+ Before (v4):
76
+
77
+ ```astro
78
+ ---
79
+ import { defaultThemes } from '@sofidevo/astro-dynamic-header';
80
+
81
+ const theme = { light: { ...defaultThemes.light, accentColor: "#7c3aed", zIndex: 60 } };
82
+ ---
83
+ <Header theme={theme} />
84
+ ```
85
+
86
+ After (v5):
87
+
88
+ ```css
89
+ :root {
90
+ --l-accent: #7c3aed;
91
+ --header-z-index: 60;
92
+ }
93
+ ```
94
+
95
+ In dev, the component logs a migration warning when it detects the removed prop. The `DualThemeConfig` and `ThemeConfig` types and the `./defaults` export are gone; passing `theme` now fails type-checking.
96
+
97
+ ### 2. The container no longer renders an inline z-index
98
+
99
+ | | v4.x | v5.0.0 |
100
+ | --- | --- | --- |
101
+ | Theming API | `theme` prop + CSS variables | CSS variables only |
102
+ | Container z-index | inline `style="z-index: 10"` (no CSS could beat it) | `z-index: var(--header-z-index, 10)` in `@layer components` (utilities win) |
103
+ | `defaultThemes` | exported from `./defaults` | removed |
104
+
105
+ z-index utilities passed as `classNames.container` (for example `"z-50"`) now work as expected.
106
+
107
+ ### 3. Other changes in v5.0.0
108
+
109
+ - **Dev diagnostics for override problems.** In dev the component checks the final cascade layer order after page load and logs a `console.warn` with the exact fix when your overrides cannot win (wrong layer order) or when an old v3.x install is detected. The check is stripped from production builds.
110
+ - **The layer order statement also ships inline.** Next to the bundled statement, the component renders `<style is:inline>` with `@layer theme, base, components, utilities;`, so styles injected at runtime are ordered correctly too.
111
+ - **TSDoc everywhere.** Every prop and exported interface is documented in English with usage examples, including hover states via `classNames` and `navigation.menu__link__class`.
112
+
113
+ ### Migration checklist (to v5)
114
+
115
+ ```md
116
+ - [ ] Update the package: npm i -U @sofidevo/astro-dynamic-header
117
+ - [ ] Replace every `theme={{ light: {...}, dark: {...} }}` with the matching CSS variables (see "CSS variable reference").
118
+ - [ ] Replace `theme.zIndex` with `--header-z-index` (or pass a z-index utility via `classNames.container`).
119
+ - [ ] Remove `defaultThemes` imports; the defaults live in the CSS variable table below.
120
+ ```
121
+
122
+ ---
123
+
65
124
  ## Breaking Changes in v4.0.0
66
125
 
67
126
  > [!WARNING]
@@ -104,11 +163,10 @@ Two practical consequences:
104
163
 
105
164
  ### 3. Other behavior changes
106
165
 
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
166
  - **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
167
  - **The mobile panel's `.active` rule no longer uses `!important`**, so active-link styles are overridable like everything else.
110
168
  - **`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`).
169
+ - **Theme variable fallbacks now match the documented defaults** exactly (for example `rgba(255, 255, 255, 0.9)` for `--l-bg`, see [CSS variable reference](#css-variable-reference)).
112
170
 
113
171
  ### Migration checklist
114
172
 
@@ -118,7 +176,6 @@ Two practical consequences:
118
176
  - [ ] Remove the `!important` declarations you added to override the header (they are no longer needed).
119
177
  - [ ] Move your global resets / element selectors into `@layer base`.
120
178
  - [ ] 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.
122
179
  ```
123
180
 
124
181
  ---
@@ -132,9 +189,11 @@ Two practical consequences:
132
189
  | `headerType` | `"floating" \| "fullscreen"` | `"floating"` | Layout style |
133
190
  | `preset` | `"light" \| "dark" \| "auto"` | `"auto"` | Theme mode. `"auto"` follows `.dark` on `<html>`. |
134
191
  | `navigation` | `NavConfig` | `{}` | Menu items, home link, and custom CSS classes |
135
- | `theme` | `DualThemeConfig` | `{}` | Optional theme overrides (prefer CSS variables) |
136
192
  | `classNames` | `HeaderClassNames` | `{}` | Inject CSS classes into structural elements |
137
193
 
194
+ > [!NOTE]
195
+ > There is no `theme` prop — it was removed in v5. Colors, blur, and z-index are CSS variables (see [CSS variable reference](#css-variable-reference)).
196
+
138
197
  ---
139
198
 
140
199
  ## Configuration Objects
@@ -233,7 +292,7 @@ Everything below assumes you want to change how the header looks or behaves. Pic
233
292
  | A different overall design (square, full-width, compact) | Your own CSS in `@layer utilities` |
234
293
  | Restyle nav links, dropdowns, or the mobile panel | Selectors targeting the internal class hooks |
235
294
  | Brand colors, blur, background | CSS variables (`--l-*` / `--d-*`) |
236
- | Per-instance tokens or z-index | `theme` prop |
295
+ | Per-instance tokens or z-index | Scoped CSS variables (`--header-z-index`, `--l-*` on a wrapper) |
237
296
  | Logo / buttons markup | Slots |
238
297
 
239
298
  ### Style precedence
@@ -417,25 +476,36 @@ Scoped to one section (marketing page gets a purple tint, the docs stay neutral)
417
476
 
418
477
  Variables are inherited, so defining them on any ancestor of the header works.
419
478
 
420
- ### Example: z-index and per-instance tokens with the `theme` prop
479
+ ### Example: z-index and per-instance tokens
421
480
 
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:
481
+ The container resolves its stacking level as `z-index: var(--header-z-index, 10)` inside `@layer components`, and there is no inline style — so z-index utilities passed as `classNames.container` win:
423
482
 
424
483
  ```astro
425
- ---
426
- import { defaultThemes } from '@sofidevo/astro-dynamic-header';
484
+ <Header classNames={{ container: "z-50" }} />
485
+ ```
427
486
 
428
- const theme = {
429
- light: { ...defaultThemes.light, zIndex: 60 },
430
- dark: { ...defaultThemes.dark, zIndex: 60 },
431
- };
432
- ---
487
+ Or set it once, globally or on a wrapper (variables are inherited):
433
488
 
434
- <Header theme={theme} classNames={{ header: "shadow-2xl" }} />
489
+ ```css
490
+ :root {
491
+ --header-z-index: 60;
492
+ }
435
493
  ```
436
494
 
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.
495
+ Per-instance tokens work the same way:
496
+
497
+ ```astro
498
+ <section class="marketing-hero">
499
+ <Header navigation={{ menuItems }} />
500
+ </section>
501
+ ```
502
+
503
+ ```css
504
+ .marketing-hero {
505
+ --l-bg: rgb(124 58 237 / 0.14);
506
+ --header-z-index: 60;
507
+ }
508
+ ```
439
509
 
440
510
  ### Example: mobile panel
441
511
 
@@ -508,7 +578,8 @@ The sun and chevron icons use `currentColor`, so they follow the surrounding tex
508
578
 
509
579
  ### Styling caveats
510
580
 
511
- - **z-index is inline.** Use `theme.zIndex`, not a utility class, on `container`.
581
+ - **Layer order is fixed by first appearance.** If your plain-CSS overrides lose even from `@layer utilities`, put `@layer theme, base, components, utilities;` at the very top of your global stylesheet — the dev console warning points it out.
582
+ - **There are no inline styles.** The container z-index is `var(--header-z-index, 10)` in `@layer components`, so utilities beat it.
512
583
  - **`!important` in a layer still works** (important declarations reverse layer order), but you should not need it.
513
584
  - **Unlayered CSS beats everything layered.** If a global rule seems "too strong", that is why — move it into `@layer base`.
514
585
  - **Astro scopes component styles with `data-astro-cid-*` attributes.** Your selectors do not need them; plain class selectors work.
@@ -517,11 +588,11 @@ The sun and chevron icons use `currentColor`, so they follow the surrounding tex
517
588
 
518
589
  ## Customization & Theme Config
519
590
 
520
- You can fully customize the color scheme using **CSS Custom Properties** (recommended) or the `theme` prop.
591
+ You can fully customize the color scheme using **CSS Custom Properties** — the only theming mechanism since v5.
521
592
 
522
593
  ### CSS variable reference
523
594
 
524
- Input variables (set them wherever the header lives — `:root`, a wrapper, or the `theme` prop):
595
+ Input variables (set them wherever the header lives — `:root`, a wrapper, or a per-instance scope):
525
596
 
526
597
  | Variable | Used for | Light default | Dark default |
527
598
  | --- | --- | --- | --- |
@@ -530,6 +601,7 @@ Input variables (set them wherever the header lives — `:root`, a wrapper, or t
530
601
  | `--l-text` / `--d-text` | Text, hamburger lines, icons | `#1a1a1a` | `#ffffff` |
531
602
  | `--l-accent` / `--d-accent` | Hover underline, active links, dashed borders | `#3e1c71` | `#00ffff` |
532
603
  | `--l-blur` / `--d-blur` | `backdrop-filter` value | `blur(20px)` | `blur(20px)` |
604
+ | `--header-z-index` | Stacking level of the fixed container | `10` | `10` |
533
605
 
534
606
  Derived variables (resolved by the component per theme state; override them only if you need to target internals directly):
535
607
 
@@ -541,7 +613,7 @@ Derived variables (resolved by the component per theme state; override them only
541
613
  | `--accent-color` | `--l-accent` or `--d-accent` |
542
614
  | `--backdrop-blur` | `--l-blur` or `--d-blur` |
543
615
 
544
- ### Option 1: Native CSS Variables (Recommended)
616
+ ### Setting the variables
545
617
 
546
618
  ```css
547
619
  :root {
@@ -558,37 +630,18 @@ Derived variables (resolved by the component per theme state; override them only
558
630
  --d-bg-opaque: #0a0a0a;
559
631
  --d-text: #f5f5f5;
560
632
  --d-blur: blur(20px);
633
+
634
+ /* Stacking */
635
+ --header-z-index: 60;
561
636
  }
562
637
  ```
563
638
 
564
639
  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`).
565
640
 
566
- ### Option 2: JS Theme Prop
567
-
568
- ```astro
569
- ---
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.
641
+ Because there are no inline styles, your own layered CSS always beats these defaults — override any variable on a wrapper to scope it to one instance (see [z-index and per-instance tokens](#example-z-index-and-per-instance-tokens)).
589
642
 
590
643
  > [!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.
644
+ > When using transparent backgrounds, always supply a solid fallback in `--l-bg-opaque` / `--d-bg-opaque`. Submenus and mobile panels utilize this solid color to prevent visual glitches with nested blur effects.
592
645
 
593
646
  ---
594
647
 
@@ -663,11 +716,13 @@ import ChevronIcon from '@sofidevo/astro-dynamic-header/ChevronIcon';
663
716
 
664
717
  Check these in order:
665
718
 
719
+ 0. **Which version is installed?** Run `npm ls @sofidevo/astro-dynamic-header` (or the pnpm/yarn equivalent). v3.x ships unlayered styles that beat every layered override, so no amount of `@layer utilities` or Tailwind classes can win. Update to the latest version — in dev, the component logs a console warning when it detects this.
666
720
  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.
721
+ 2. **Did your CSS create `utilities` before the layer order statement ran?** Layers are ordered by their *first appearance* in the document; a later statement cannot reorder them. If your global CSS uses `@layer` but no statement comes first, add `@layer theme, base, components, utilities;` as its very first line (Tailwind v4 already emits this). In dev, the component logs a console warning with the exact fix when it detects this ordering.
668
722
  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.
723
+ 4. **Tailwind v4?** Its layer order matches this component exactly, so utilities work automatically. Make sure `@import "tailwindcss"` comes first in your entry CSS.
724
+
725
+ Since v5 the component renders no inline styles at all — CSS variables (`--l-*`, `--d-*`, `--header-z-index`) and `@layer utilities` always win.
671
726
 
672
727
  ### Preflight changed my nav link colors (Tailwind v3)
673
728
 
@@ -694,7 +749,13 @@ Icons are rendered as inline SVG components. If you are upgrading from `v1.x` or
694
749
 
695
750
  ### The header sits behind my other content
696
751
 
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).
752
+ Raise it with `--header-z-index` (globally, on a wrapper, or via `classNames.container="z-50"` — utilities win since there is no inline z-index):
753
+
754
+ ```css
755
+ :root {
756
+ --header-z-index: 60;
757
+ }
758
+ ```
698
759
 
699
760
  ---
700
761
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sofidevo/astro-dynamic-header",
3
- "version": "4.0.1",
3
+ "version": "5.0.1",
4
4
  "description": "A dynamic Astro header component that switches between floating and fullscreen styles",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -15,7 +15,6 @@
15
15
  "./MobileNav": "./src/MobileNav.astro",
16
16
  "./HamburgerButton": "./src/HamburgerButton.astro",
17
17
  "./ChevronIcon": "./src/ChevronIcon.astro",
18
- "./defaults": "./src/defaults.ts",
19
18
  "./Header.astro": "./src/Header.astro",
20
19
  "./NavMenu.astro": "./src/NavMenu.astro",
21
20
  "./MobileNav.astro": "./src/MobileNav.astro",
package/src/Header.astro CHANGED
@@ -3,30 +3,100 @@ import HamburgerButton from "./HamburgerButton.astro";
3
3
  import NavMenu from "./NavMenu.astro";
4
4
  import MobileNav from "./MobileNav.astro";
5
5
 
6
- import type {
7
- NavConfig,
8
- DualThemeConfig,
9
- HeaderClassNames,
10
- } from "./index.js";
6
+ import type { NavConfig, HeaderClassNames } from "./index.js";
11
7
 
12
8
  export interface Props {
9
+ /**
10
+ * Layout style of the header.
11
+ * - `"floating"` — centered, max width, rounded corners (default).
12
+ * - `"fullscreen"` — full width, no border radius.
13
+ *
14
+ * @default "floating"
15
+ * @example
16
+ * ```astro
17
+ * <Header headerType="fullscreen" />
18
+ * ```
19
+ */
13
20
  headerType?: "floating" | "fullscreen";
21
+
22
+ /**
23
+ * Theme behavior of the header.
24
+ * - `"auto"` (default) — follows the `.dark` class on the root element.
25
+ * - `"light"` — always render the light theme (`header--force-light`).
26
+ * - `"dark"` — always render the dark theme (`header--force-dark`).
27
+ *
28
+ * Colors are controlled with CSS variables (see the README "CSS variable
29
+ * reference"), never with props.
30
+ *
31
+ * @default "auto"
32
+ * @example
33
+ * ```astro
34
+ * <Header preset="dark" />
35
+ * ```
36
+ */
14
37
  preset?: "light" | "dark" | "auto";
38
+
39
+ /**
40
+ * Navigation links, home link, and fine-grained classes for the desktop
41
+ * menu items.
42
+ *
43
+ * @example
44
+ * ```astro
45
+ * <Header
46
+ * navigation={{
47
+ * menuItems: [
48
+ * { link: "/about", text: "About" },
49
+ * { link: "/work", text: "Work", submenu: [{ link: "/ux", text: "UX" }] },
50
+ * ],
51
+ * menu__link__class: "hover:text-purple-400",
52
+ * }}
53
+ * />
54
+ * ```
55
+ */
15
56
  navigation?: NavConfig;
16
- theme?: DualThemeConfig;
57
+
58
+ /**
59
+ * CSS classes for the structural wrapper elements. The component styles
60
+ * live in `@layer components`, so any class passed here — Tailwind
61
+ * utilities included — wins over the built-in styles without `!important`.
62
+ *
63
+ * @example
64
+ * ```astro
65
+ * <Header classNames={{ header: "bg-red-500 shadow-xl", container: "top-4" }} />
66
+ * ```
67
+ *
68
+ * With plain CSS, put your own class in `@layer utilities` so it beats the
69
+ * component:
70
+ *
71
+ * ```astro
72
+ * <Header classNames={{ header: "custom-header-bg" }} />
73
+ * <style is:inline>
74
+ * @layer utilities {
75
+ * .custom-header-bg { background-color: red; }
76
+ * }
77
+ * </style>
78
+ * ```
79
+ */
17
80
  classNames?: HeaderClassNames;
81
+ headerBg?: string;
82
+
18
83
  }
19
84
 
20
85
  const {
21
86
  headerType = "floating",
22
87
  preset = "auto",
23
-
24
88
  navigation = {},
25
- theme = {},
26
89
  classNames = {},
27
90
  } = Astro.props;
28
91
 
29
92
  if (import.meta.env.DEV) {
93
+ const removedThemeProp = (Astro.props as Record<string, unknown>).theme;
94
+
95
+ if (removedThemeProp !== undefined) {
96
+ console.warn(
97
+ "[@sofidevo/astro-dynamic-header] BREAKING CHANGE: The 'theme' prop was removed in v5. Set the same tokens as CSS variables instead: --l-bg / --d-bg, --l-bg-opaque / --d-bg-opaque, --l-text / --d-text, --l-accent / --d-accent, --l-blur / --d-blur, and --header-z-index for the container z-index. See the README 'CSS variable reference'.",
98
+ );
99
+ }
30
100
 
31
101
  if (Array.isArray(navigation)) {
32
102
  console.warn(
@@ -35,26 +105,6 @@ if (import.meta.env.DEV) {
35
105
  }
36
106
  }
37
107
 
38
-
39
- const inlineStyles: Record<string, string> = {};
40
-
41
- if (theme.light) {
42
- if (theme.light.backgroundColor) inlineStyles["--l-bg"] = theme.light.backgroundColor;
43
- if (theme.light.backgroundColorOpaque) inlineStyles["--l-bg-opaque"] = theme.light.backgroundColorOpaque;
44
- if (theme.light.textColor) inlineStyles["--l-text"] = theme.light.textColor;
45
- if (theme.light.accentColor) inlineStyles["--l-accent"] = theme.light.accentColor;
46
- if (theme.light.backdropBlur) inlineStyles["--l-blur"] = theme.light.backdropBlur;
47
- }
48
- if (theme.dark) {
49
- if (theme.dark.backgroundColor) inlineStyles["--d-bg"] = theme.dark.backgroundColor;
50
- if (theme.dark.backgroundColorOpaque) inlineStyles["--d-bg-opaque"] = theme.dark.backgroundColorOpaque;
51
- if (theme.dark.textColor) inlineStyles["--d-text"] = theme.dark.textColor;
52
- if (theme.dark.accentColor) inlineStyles["--d-accent"] = theme.dark.accentColor;
53
- if (theme.dark.backdropBlur) inlineStyles["--d-blur"] = theme.dark.backdropBlur;
54
- }
55
-
56
- const zIndex = theme.light?.zIndex ?? theme.dark?.zIndex ?? 10;
57
-
58
108
  // Navigation configuration
59
109
  const {
60
110
  homeUrl = "/",
@@ -69,10 +119,19 @@ const {
69
119
  const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
70
120
  ---
71
121
 
122
+ <style is:inline>
123
+ @layer theme, base, components, utilities;
124
+ </style>
125
+
72
126
  <script>
73
127
  import { initHamburger } from "./scripts/hamburger.js";
128
+ import { initLayerDiagnostics } from "./scripts/layer-diagnostics.js";
74
129
 
75
130
  initHamburger();
131
+
132
+ if (import.meta.env.DEV) {
133
+ initLayerDiagnostics();
134
+ }
76
135
  </script>
77
136
 
78
137
  <div
@@ -81,20 +140,18 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
81
140
  `header__container--${headerType}`,
82
141
  classNames.container,
83
142
  ]}
84
- style={{ zIndex }}
85
143
  >
86
144
  <header
87
145
  class:list={[
146
+ classNames.header,
88
147
  "header",
89
148
  `header--${headerType}`,
90
149
  forcedClass,
91
- classNames.header,
92
150
  ]}
93
- style={Object.keys(inlineStyles).length > 0 ? inlineStyles : undefined}
94
151
  >
95
152
  <slot name="logo" />
96
153
 
97
- <div class:list={["nav-menu-wrapper", classNames.nav]}>
154
+ <div class:list={[classNames.nav, "nav-menu-wrapper"]}>
98
155
  <NavMenu
99
156
  menuItems={menuItems}
100
157
  homeUrl={homeUrl}
@@ -135,25 +192,24 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
135
192
  /*
136
193
  * Cascade layers keep this component overridable:
137
194
  * base (resets/preflight) < components (ours) < utilities (your classNames)
138
- * The order statement below makes that order deterministic no matter how
139
- * the consumer's CSS is bundled. No `!important` is needed to override us.
195
+ * The order statement below fixes that order before any layer of ours is
196
+ * used; the dev-only diagnostic warns when a consumer's CSS created the
197
+ * layers in a different order first. No `!important` is needed to override us.
140
198
  */
141
199
  @layer theme, base, components, utilities;
142
200
 
143
201
  @layer components {
144
202
  .header {
145
- --bg-color: var(--l-bg, rgba(255, 255, 255, 0.9));
146
- --bg-color-opaque: var(--l-bg-opaque, rgb(255, 255, 255));
203
+ --bg-color: var(--l-bg);
204
+ --bg-color-opaque: var(--l-bg-opaque, rgba(255, 255, 255, 0.748));
147
205
  --text-color: var(--l-text, #1a1a1a);
148
206
  --accent-color: var(--l-accent, #3e1c71);
149
207
  --backdrop-blur: var(--l-blur, blur(20px));
150
-
151
208
  display: flex;
152
209
  align-items: center;
153
210
  width: 100%;
154
211
  justify-content: space-between;
155
212
  padding: 1em 2em;
156
- background-color: var(--bg-color);
157
213
  backdrop-filter: var(--backdrop-blur);
158
214
  -webkit-backdrop-filter: var(--backdrop-blur);
159
215
  color: var(--text-color);
@@ -170,7 +226,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
170
226
 
171
227
  /* Automatic dark mode detection via root class */
172
228
  :root.dark .header:not(.header--force-light):not(.header--force-dark) {
173
- --bg-color: var(--d-bg, #0d0d0dcc);
229
+ --bg-color: var(--d-bg, rgba(0, 0, 0, 0.823));
174
230
  --bg-color-opaque: var(--d-bg-opaque, #0d0d0d);
175
231
  --text-color: var(--d-text, #ffffff);
176
232
  --accent-color: var(--d-accent, #00ffff);
@@ -179,15 +235,15 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
179
235
 
180
236
  /* Forced themes: higher specificity than .header, no !important needed */
181
237
  .header.header--force-dark {
182
- --bg-color: var(--d-bg, #0d0d0dcc);
183
- --bg-color-opaque: var(--d-bg-opaque, #0d0d0d);
238
+ --bg-color: var(--d-bg, rgba(0, 0, 0, 0.823));
239
+ --bg-color-opaque: var(--d-bg-opaque, #000000d9);
184
240
  --text-color: var(--d-text, #ffffff);
185
241
  --accent-color: var(--d-accent, #00ffff);
186
242
  --backdrop-blur: var(--d-blur, blur(30px));
187
243
  }
188
244
 
189
245
  .header.header--force-light {
190
- --bg-color: var(--l-bg, rgba(255, 255, 255, 0.9));
246
+ --bg-color: var(--l-bg, rgba(255, 255, 255, 0.748));
191
247
  --bg-color-opaque: var(--l-bg-opaque, rgb(255, 255, 255));
192
248
  --text-color: var(--l-text, #1a1a1a);
193
249
  --accent-color: var(--l-accent, #3e1c71);
@@ -201,6 +257,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
201
257
  align-items: center;
202
258
  margin: 0 auto;
203
259
  position: fixed;
260
+ z-index: var(--header-z-index, 10);
204
261
 
205
262
  @media (width < 768px) {
206
263
  justify-content: flex-end;
@@ -228,7 +285,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
228
285
 
229
286
  .header--fullscreen {
230
287
  border-radius: 0;
231
- padding: 1em 2em;
288
+ padding: 1em 0.6rem;
232
289
 
233
290
  @media (width < 768px) {
234
291
  max-width: 100%;
package/src/index.ts CHANGED
@@ -1,14 +1,12 @@
1
- export { defaultThemes } from "./defaults.js";
2
-
3
1
  /**
4
- * Represents a menu item in the navigation.
2
+ * Represents a menu item in the navigation (top level).
5
3
  */
6
4
  export interface MenuItemType {
7
- /** The URL path for the link */
5
+ /** The URL path for the link. */
8
6
  link: string;
9
- /** The text label to display */
7
+ /** The text label to display. */
10
8
  text: string;
11
- /** Optional nested submenu items */
9
+ /** Optional nested submenu items. */
12
10
  submenu?: MenuItemType[];
13
11
  }
14
12
 
@@ -16,9 +14,9 @@ export interface MenuItemType {
16
14
  * Represents a third-level menu item.
17
15
  */
18
16
  export interface TertiaryMenuItem {
19
- /** The URL path for the link */
17
+ /** The URL path for the link. */
20
18
  link: string;
21
- /** The text label to display */
19
+ /** The text label to display. */
22
20
  text: string;
23
21
  }
24
22
 
@@ -26,28 +24,50 @@ export interface TertiaryMenuItem {
26
24
  * Represents a second-level menu item with optional nested tertiary items.
27
25
  */
28
26
  export interface SecondaryMenuItem {
29
- /** The URL path for the link */
27
+ /** The URL path for the link. */
30
28
  link: string;
31
- /** The text label to display */
29
+ /** The text label to display. */
32
30
  text: string;
33
- /** Optional nested tertiary menu items */
31
+ /** Optional nested tertiary menu items. */
34
32
  submenu?: TertiaryMenuItem[];
35
33
  }
36
34
 
37
35
  /**
38
36
  * Represents a top-level menu item with optional nested secondary items.
37
+ *
38
+ * @example
39
+ * ```ts
40
+ * const item: MenuItem = {
41
+ * link: "/services",
42
+ * text: "Services",
43
+ * submenu: [
44
+ * { link: "/design", text: "Design", submenu: [{ link: "/ux", text: "UX" }] },
45
+ * ],
46
+ * };
47
+ * ```
39
48
  */
40
49
  export interface MenuItem {
41
- /** The URL path for the link */
50
+ /** The URL path for the link. */
42
51
  link: string;
43
- /** The text label to display */
52
+ /** The text label to display. */
44
53
  text: string;
45
- /** Optional nested secondary menu items */
54
+ /** Optional nested secondary menu items. */
46
55
  submenu?: SecondaryMenuItem[];
47
56
  }
48
57
 
49
58
  /**
50
59
  * Configuration for the main navigation.
60
+ *
61
+ * @example
62
+ * ```astro
63
+ * <Header
64
+ * navigation={{
65
+ * homeUrl: "/",
66
+ * menuItems: [{ link: "/about", text: "About" }],
67
+ * menu__link__class: "hover:underline hover:text-purple-400",
68
+ * }}
69
+ * />
70
+ * ```
51
71
  */
52
72
  export interface NavConfig {
53
73
  /**
@@ -75,56 +95,20 @@ export interface NavConfig {
75
95
  header__item__class?: string;
76
96
  /**
77
97
  * Fine-grained class override applied to every top-level `<a>` link
78
- * in the desktop navigation.
98
+ * in the desktop navigation. This is the place for hover styles: the
99
+ * classes live in `@layer utilities`, so they beat the component's
100
+ * default link styles without `!important`.
79
101
  * @example "hover:underline font-medium"
102
+ * @example Plain CSS equivalent:
103
+ * ```css
104
+ * @layer utilities {
105
+ * #header-menu a:hover { color: var(--d-accent, #00ffff); }
106
+ * }
107
+ * ```
80
108
  */
81
109
  menu__link__class?: string;
82
110
  }
83
111
 
84
- /**
85
- * Individual theme settings for a specific state (light/dark).
86
- */
87
- export interface ThemeConfig {
88
- /**
89
- * Main background color. Supports hex, rgb, rgba, etc.
90
- * @example "rgba(255, 255, 255, 0.9)"
91
- */
92
- backgroundColor?: string;
93
- /**
94
- * Solid background color for submenus and mobile panels to ensure readability.
95
- * @example "#ffffff"
96
- */
97
- backgroundColorOpaque?: string;
98
- /**
99
- * CSS backdrop-filter blur value.
100
- * @default "blur(20px)"
101
- */
102
- backdropBlur?: string;
103
- /**
104
- * CSS z-index for the header container.
105
- * @default 10
106
- */
107
- zIndex?: number;
108
- /**
109
- * Primary text color for navigation and logo.
110
- */
111
- textColor?: string;
112
- /**
113
- * Color for highlights, active states, underscores, and small borders.
114
- */
115
- accentColor?: string;
116
- }
117
-
118
- /**
119
- * Combined theme configuration for both light and dark modes.
120
- */
121
- export interface DualThemeConfig {
122
- /** Settings applied when light mode is active. */
123
- light?: ThemeConfig;
124
- /** Settings applied when dark mode is active. */
125
- dark?: ThemeConfig;
126
- }
127
-
128
112
  /**
129
113
  * Custom CSS class names for high-level layout & appearance customization.
130
114
  *
@@ -140,6 +124,23 @@ export interface DualThemeConfig {
140
124
  * ```astro
141
125
  * <Header classNames={{ header: "shadow-xl", container: "top-4 px-6" }} />
142
126
  * ```
127
+ *
128
+ * @example Tailwind hover states work out of the box:
129
+ * ```astro
130
+ * <Header classNames={{ header: "hover:shadow-2xl transition-shadow" }} />
131
+ * ```
132
+ *
133
+ * @example With plain CSS, define your class in `@layer utilities` (hover
134
+ * included) so it beats the component:
135
+ * ```astro
136
+ * <Header classNames={{ header: "custom-header-bg" }} />
137
+ * <style is:inline>
138
+ * @layer utilities {
139
+ * .custom-header-bg { background-color: red; }
140
+ * .custom-header-bg:hover { background-color: darkred; }
141
+ * }
142
+ * </style>
143
+ * ```
143
144
  */
144
145
  export interface HeaderClassNames {
145
146
  /** Outermost fixed `<div>` that positions the header on the page. */
@@ -160,6 +161,9 @@ export type CustomClassNames = HeaderClassNames;
160
161
 
161
162
  /**
162
163
  * Main properties for the Header component.
164
+ *
165
+ * Colors, blur, and z-index are plain CSS variables — see the README
166
+ * "CSS variable reference". There is no `theme` prop (removed in v5).
163
167
  */
164
168
  export interface HeaderProps {
165
169
  /**
@@ -167,6 +171,10 @@ export interface HeaderProps {
167
171
  * - "floating": Centered with max-width and rounded corners.
168
172
  * - "fullscreen": Full width with no border radius.
169
173
  * @default "floating"
174
+ * @example
175
+ * ```astro
176
+ * <Header headerType="fullscreen" />
177
+ * ```
170
178
  */
171
179
  headerType?: "floating" | "fullscreen";
172
180
  /**
@@ -175,42 +183,78 @@ export interface HeaderProps {
175
183
  * - "dark": Force dark mode.
176
184
  * - "auto": Detects .dark class on the root element.
177
185
  * @default "auto"
186
+ * @example
187
+ * ```astro
188
+ * <Header preset="dark" />
189
+ * ```
178
190
  */
179
191
  preset?: "light" | "dark" | "auto";
180
192
 
181
- /** Navigation links and structure. */
193
+ /**
194
+ * Navigation links and structure.
195
+ * @example
196
+ * ```astro
197
+ * <Header navigation={{ menuItems: [{ link: "/about", text: "About" }] }} />
198
+ * ```
199
+ */
182
200
  navigation?: NavConfig;
183
- /** Custom theme overrides. See {@link DualThemeConfig} */
184
- theme?: DualThemeConfig;
185
201
  /**
186
202
  * High-level CSS class overrides for structural wrapper elements.
187
203
  * For fine-grained nav/logo element classes, use the nested `xxx__class`
188
204
  * props inside `navigation` or `logo` instead.
189
- * @example { header: "shadow-lg", container: "top-4" }
205
+ * @example
206
+ * ```astro
207
+ * <Header classNames={{ header: "shadow-lg bg-red-500", container: "top-4" }} />
208
+ * ```
190
209
  */
191
210
  classNames?: HeaderClassNames;
192
211
  }
193
212
 
213
+ /**
214
+ * Properties for the standalone {@link '/NavMenu'} desktop navigation
215
+ * component (the Header renders it internally).
216
+ */
194
217
  export interface NavMenuProps {
218
+ /** Layout variant, matches `HeaderProps.headerType`. */
195
219
  type?: "floating" | "fullscreen";
220
+ /** Top-level menu items with nested submenus. */
196
221
  menuItems?: MenuItem[];
222
+ /** Whether to render the home link (hidden automatically on `/`). */
197
223
  showHomeLink?: boolean;
224
+ /** Label for the home link. */
198
225
  homeText?: string;
226
+ /** Class override for the desktop `<nav>` element. */
199
227
  header__menu__class?: string;
228
+ /** Class override for every top-level `<li>`. */
200
229
  header__item__class?: string;
230
+ /** Class override for every top-level `<a>` — hover styles live here. */
201
231
  menu__link__class?: string;
202
232
  }
203
233
 
234
+ /**
235
+ * Properties for the standalone {@link '/MobileNav'} slide-in panel
236
+ * (the Header renders it internally).
237
+ */
204
238
  export interface MobileNavProps {
239
+ /** Layout variant, matches `HeaderProps.headerType`. */
205
240
  type?: "floating" | "fullscreen";
241
+ /** Top-level menu items with nested submenus. */
206
242
  menuItems?: MenuItem[];
243
+ /** Whether to render the home link (hidden automatically on `/`). */
207
244
  showHomeLink?: boolean;
245
+ /** Label for the home link. */
208
246
  homeText?: string;
247
+ /** Class override for the mobile panel `<nav>`. */
209
248
  mobileNav__class?: string;
249
+ /** Accent color used by the panel's active states and icons. */
210
250
  accentColor?: string;
211
251
  }
212
252
 
253
+ /**
254
+ * Properties for the standalone {@link '/HamburgerButton'} (the Header
255
+ * renders it internally on mobile viewports).
256
+ */
213
257
  export interface HamburgerButtonProps {
258
+ /** Text/stroke color; defaults to the current text color. */
214
259
  color?: string;
215
260
  }
216
-
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Dev-only cascade-layer order diagnostic.
3
+ *
4
+ * The component ships its styles inside `@layer components` and declares the
5
+ * order statement `@layer theme, base, components, utilities;` so that any
6
+ * consumer override in `@layer utilities` (or a Tailwind utility class passed
7
+ * through `classNames`) beats the built-in styles without `!important`.
8
+ *
9
+ * Two situations silently break that guarantee:
10
+ *
11
+ * 1. An old v3.x version is installed — v3 shipped unlayered styles, and
12
+ * unlayered CSS beats every layered rule.
13
+ * 2. The consumer's CSS creates the `utilities` layer *before* any layer
14
+ * order statement runs. Per the CSS Cascade spec, a later statement cannot
15
+ * reorder already-created layers, so the component's `components` layer
16
+ * ends up above `utilities` and consumer overrides lose.
17
+ *
18
+ * This module runs only in dev (the call site is guarded with
19
+ * `import.meta.env.DEV`) and warns with the exact fix instead of leaving the
20
+ * developer guessing why an override "does nothing".
21
+ */
22
+
23
+ /** The canonical order statement this component guarantees. */
24
+ export const LAYER_ORDER_STATEMENT = "@layer theme, base, components, utilities;";
25
+
26
+ export type LayerProblem =
27
+ | { kind: "missing-components-layer" }
28
+ | { kind: "wrong-order"; componentsIndex: number; utilitiesIndex: number };
29
+
30
+ /**
31
+ * Walks every readable stylesheet in the document and returns the layer names
32
+ * in the order they were first created (creation order = cascade order from
33
+ * weakest to strongest, per CSS Cascade 5 §6.4.3).
34
+ *
35
+ * Uses `cssText` parsing instead of `CSSLayerStatementRule`/`CSSLayerBlockRule`
36
+ * constructors so it also works in environments with partial CSSOM support.
37
+ */
38
+ export function collectLayerOrder(doc: Document = document): string[] {
39
+ const order: string[] = [];
40
+ const seen = new Set<string>();
41
+
42
+ const push = (rawName: string): void => {
43
+ const name = rawName.trim();
44
+ if (name.length > 0 && !seen.has(name)) {
45
+ seen.add(name);
46
+ order.push(name);
47
+ }
48
+ };
49
+
50
+ const walk = (rules: CSSRuleList): void => {
51
+ for (const rawRule of Array.from(rules)) {
52
+ const rule = rawRule as CSSRule & { cssRules?: CSSRuleList };
53
+ const cssText = rule.cssText ?? "";
54
+
55
+ if (cssText.startsWith("@layer")) {
56
+ const braceIndex = cssText.indexOf("{");
57
+ if (braceIndex === -1) {
58
+ // Order statement: "@layer theme, base, components, utilities;"
59
+ const list = cssText
60
+ .slice(cssText.indexOf("@layer") + "@layer".length, cssText.indexOf(";"))
61
+ .split(",");
62
+ list.forEach(push);
63
+ } else {
64
+ // Layer block: "@layer components { … }"
65
+ push(cssText.slice("@layer".length, braceIndex));
66
+ }
67
+ } else if (rule.cssRules) {
68
+ // @media / @supports / … — descend so nested layer usage still counts.
69
+ walk(rule.cssRules);
70
+ }
71
+ }
72
+ };
73
+
74
+ for (const sheet of Array.from(doc.styleSheets)) {
75
+ try {
76
+ const rules = sheet.cssRules;
77
+ if (rules) walk(rules);
78
+ } catch {
79
+ // Cross-origin stylesheets are unreadable — they cannot be ours anyway.
80
+ }
81
+ }
82
+
83
+ return order;
84
+ }
85
+
86
+ /**
87
+ * Compares the final layer order against the order the component guarantees
88
+ * and returns the first problem found, or `null` when everything is healthy.
89
+ */
90
+ export function findLayerProblem(
91
+ doc: Document = document,
92
+ ): LayerProblem | null {
93
+ const order = collectLayerOrder(doc);
94
+ const componentsIndex = order.indexOf("components");
95
+ const utilitiesIndex = order.indexOf("utilities");
96
+
97
+ if (componentsIndex === -1) {
98
+ // Our styles always create `components`. If the header is rendered but the
99
+ // layer never appears, the shipped CSS is the old unlayered v3 styles.
100
+ const headerRendered = doc.querySelector(".header__container") !== null;
101
+ return headerRendered ? { kind: "missing-components-layer" } : null;
102
+ }
103
+
104
+ if (utilitiesIndex !== -1 && componentsIndex > utilitiesIndex) {
105
+ return { kind: "wrong-order", componentsIndex, utilitiesIndex };
106
+ }
107
+
108
+ return null;
109
+ }
110
+
111
+ /** Human-readable console message with the exact fix for each problem. */
112
+ export function formatProblemMessage(problem: LayerProblem): string {
113
+ const banner = "[@sofidevo/astro-dynamic-header]";
114
+
115
+ if (problem.kind === "missing-components-layer") {
116
+ return (
117
+ `${banner} The component's styles were not found in \`@layer components\`. ` +
118
+ `This usually means an old v3.x version is installed, whose unlayered ` +
119
+ `styles beat every layered override. Run \`npm ls ` +
120
+ `@sofidevo/astro-dynamic-header\` (or the pnpm/yarn equivalent) and ` +
121
+ `update to the latest version.`
122
+ );
123
+ }
124
+
125
+ return (
126
+ `${banner} Wrong cascade layer order: \`utilities\` was created first ` +
127
+ `(position ${problem.utilitiesIndex}) and \`components\` after it ` +
128
+ `(position ${problem.componentsIndex}), so your overrides lose to the ` +
129
+ `component. Your CSS creates the \`utilities\` layer before any layer ` +
130
+ `order statement runs, and per the CSS Cascade spec a later statement ` +
131
+ `cannot reorder existing layers. Fix: add ` +
132
+ `\`@layer theme, base, components, utilities;\` as the very first line of ` +
133
+ `your global CSS, before any \`@layer\` usage (Tailwind v4 already ` +
134
+ `emits this).`
135
+ );
136
+ }
137
+
138
+ /**
139
+ * Schedules the check after `load` (plus a short delay so styles injected by
140
+ * dev tooling are included) and prints a `console.warn` when the order is
141
+ * broken. Call site guards this with `import.meta.env.DEV`.
142
+ */
143
+ export function initLayerDiagnostics(): void {
144
+ const run = (): void => {
145
+ window.setTimeout(() => {
146
+ const problem = findLayerProblem(document);
147
+ if (problem) console.warn(formatProblemMessage(problem));
148
+ }, 250);
149
+ };
150
+
151
+ if (document.readyState === "complete") {
152
+ run();
153
+ } else {
154
+ window.addEventListener("load", run, { once: true });
155
+ }
156
+ }
package/src/defaults.ts DELETED
@@ -1,36 +0,0 @@
1
- import type { DualThemeConfig } from "./index.js";
2
-
3
- /**
4
- * Built-in default theme tokens used by the Header component.
5
- * These are merged with any user-supplied `theme` overrides.
6
- *
7
- * You can import this object if you want to build on top of the defaults
8
- * rather than replacing them wholesale:
9
- *
10
- * @example
11
- * ```ts
12
- * import { defaultThemes } from '@sofidevo/astro-dynamic-header/defaults';
13
- * const theme = {
14
- * light: { ...defaultThemes.light, accentColor: "#e11d48" },
15
- * dark: { ...defaultThemes.dark, accentColor: "#f43f5e" },
16
- * };
17
- * ```
18
- */
19
- export const defaultThemes: Required<DualThemeConfig> = {
20
- light: {
21
- backgroundColor: "rgba(255, 255, 255, 0.9)",
22
- backgroundColorOpaque: "rgb(255, 255, 255)",
23
- backdropBlur: "blur(20px)",
24
- zIndex: 10,
25
- textColor: "#1a1a1a",
26
- accentColor: "#3e1c71",
27
- },
28
- dark: {
29
- backgroundColor: "#0d0d0dcc",
30
- backgroundColorOpaque: "#0d0d0d",
31
- backdropBlur: "blur(20px)",
32
- zIndex: 10,
33
- textColor: "#ffffff",
34
- accentColor: "#00ffff",
35
- },
36
- };