@sofidevo/astro-dynamic-header 4.0.0 → 5.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 +113 -52
- package/package.json +12 -13
- package/src/Header.astro +89 -31
- package/src/index.ts +108 -64
- package/src/scripts/layer-diagnostics.ts +156 -0
- package/src/defaults.ts +0 -36
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** —
|
|
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
|
-
- **
|
|
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
|
|
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 |
|
|
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
|
|
479
|
+
### Example: z-index and per-instance tokens
|
|
421
480
|
|
|
422
|
-
The container
|
|
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
|
-
|
|
484
|
+
<Header classNames={{ container: "z-50" }} />
|
|
485
|
+
```
|
|
427
486
|
|
|
428
|
-
|
|
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
|
-
|
|
489
|
+
```css
|
|
490
|
+
:root {
|
|
491
|
+
--header-z-index: 60;
|
|
492
|
+
}
|
|
435
493
|
```
|
|
436
494
|
|
|
437
|
-
|
|
438
|
-
|
|
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
|
-
- **
|
|
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**
|
|
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
|
|
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
|
-
###
|
|
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
|
-
|
|
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 `
|
|
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. **
|
|
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. **
|
|
670
|
-
|
|
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
|
-
|
|
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": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "A dynamic Astro header component that switches between floating and fullscreen styles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -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",
|
|
@@ -28,6 +27,15 @@
|
|
|
28
27
|
"src/*.astro",
|
|
29
28
|
"README.md"
|
|
30
29
|
],
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "astro build",
|
|
32
|
+
"dev": "astro dev",
|
|
33
|
+
"check": "astro check",
|
|
34
|
+
"check:watch": "astro check --watch",
|
|
35
|
+
"test": "vitest run",
|
|
36
|
+
"test:watch": "vitest",
|
|
37
|
+
"test:coverage": "vitest run --coverage"
|
|
38
|
+
},
|
|
31
39
|
"keywords": [
|
|
32
40
|
"astro",
|
|
33
41
|
"component",
|
|
@@ -63,14 +71,5 @@
|
|
|
63
71
|
"bugs": {
|
|
64
72
|
"url": "https://github.com/SofiDevO/astro-dynamic-header/issues"
|
|
65
73
|
},
|
|
66
|
-
"homepage": "https://
|
|
67
|
-
|
|
68
|
-
"build": "astro build",
|
|
69
|
-
"dev": "astro dev",
|
|
70
|
-
"check": "astro check",
|
|
71
|
-
"check:watch": "astro check --watch",
|
|
72
|
-
"test": "vitest run",
|
|
73
|
-
"test:watch": "vitest",
|
|
74
|
-
"test:coverage": "vitest run --coverage"
|
|
75
|
-
}
|
|
76
|
-
}
|
|
74
|
+
"homepage": "https://header.sofidev.top"
|
|
75
|
+
}
|
package/src/Header.astro
CHANGED
|
@@ -3,17 +3,80 @@ 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
|
-
|
|
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;
|
|
18
81
|
}
|
|
19
82
|
|
|
@@ -22,11 +85,17 @@ const {
|
|
|
22
85
|
preset = "auto",
|
|
23
86
|
|
|
24
87
|
navigation = {},
|
|
25
|
-
theme = {},
|
|
26
88
|
classNames = {},
|
|
27
89
|
} = Astro.props;
|
|
28
90
|
|
|
29
91
|
if (import.meta.env.DEV) {
|
|
92
|
+
const removedThemeProp = (Astro.props as Record<string, unknown>).theme;
|
|
93
|
+
|
|
94
|
+
if (removedThemeProp !== undefined) {
|
|
95
|
+
console.warn(
|
|
96
|
+
"[@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'.",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
30
99
|
|
|
31
100
|
if (Array.isArray(navigation)) {
|
|
32
101
|
console.warn(
|
|
@@ -35,26 +104,6 @@ if (import.meta.env.DEV) {
|
|
|
35
104
|
}
|
|
36
105
|
}
|
|
37
106
|
|
|
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
107
|
// Navigation configuration
|
|
59
108
|
const {
|
|
60
109
|
homeUrl = "/",
|
|
@@ -69,10 +118,19 @@ const {
|
|
|
69
118
|
const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
70
119
|
---
|
|
71
120
|
|
|
121
|
+
<style is:inline>
|
|
122
|
+
@layer theme, base, components, utilities;
|
|
123
|
+
</style>
|
|
124
|
+
|
|
72
125
|
<script>
|
|
73
126
|
import { initHamburger } from "./scripts/hamburger.js";
|
|
127
|
+
import { initLayerDiagnostics } from "./scripts/layer-diagnostics.js";
|
|
74
128
|
|
|
75
129
|
initHamburger();
|
|
130
|
+
|
|
131
|
+
if (import.meta.env.DEV) {
|
|
132
|
+
initLayerDiagnostics();
|
|
133
|
+
}
|
|
76
134
|
</script>
|
|
77
135
|
|
|
78
136
|
<div
|
|
@@ -81,7 +139,6 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
81
139
|
`header__container--${headerType}`,
|
|
82
140
|
classNames.container,
|
|
83
141
|
]}
|
|
84
|
-
style={{ zIndex }}
|
|
85
142
|
>
|
|
86
143
|
<header
|
|
87
144
|
class:list={[
|
|
@@ -90,7 +147,6 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
90
147
|
forcedClass,
|
|
91
148
|
classNames.header,
|
|
92
149
|
]}
|
|
93
|
-
style={Object.keys(inlineStyles).length > 0 ? inlineStyles : undefined}
|
|
94
150
|
>
|
|
95
151
|
<slot name="logo" />
|
|
96
152
|
|
|
@@ -135,8 +191,9 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
135
191
|
/*
|
|
136
192
|
* Cascade layers keep this component overridable:
|
|
137
193
|
* base (resets/preflight) < components (ours) < utilities (your classNames)
|
|
138
|
-
* The order statement below
|
|
139
|
-
* the
|
|
194
|
+
* The order statement below fixes that order before any layer of ours is
|
|
195
|
+
* used; the dev-only diagnostic warns when a consumer's CSS created the
|
|
196
|
+
* layers in a different order first. No `!important` is needed to override us.
|
|
140
197
|
*/
|
|
141
198
|
@layer theme, base, components, utilities;
|
|
142
199
|
|
|
@@ -201,6 +258,7 @@ const forcedClass = preset !== "auto" ? `header--force-${preset}` : "";
|
|
|
201
258
|
align-items: center;
|
|
202
259
|
margin: 0 auto;
|
|
203
260
|
position: fixed;
|
|
261
|
+
z-index: var(--header-z-index, 10);
|
|
204
262
|
|
|
205
263
|
@media (width < 768px) {
|
|
206
264
|
justify-content: flex-end;
|
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
|
-
/**
|
|
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
|
|
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
|
-
};
|