@astronox/core 0.5.11 → 0.5.13
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/package.json +1 -1
- package/src/components/actions/ThemeToggle/ThemeToggle.astro +11 -0
- package/src/components/actions/ThemeToggle/ThemeToggle.css +7 -6
- package/src/components/actions/ThemeToggle/themeToggleController.ts +17 -98
- package/src/components/feedback/SeasonalDecorations/SeasonalDecorations.astro +60 -0
- package/src/components/feedback/SeasonalDecorations/SeasonalDecorations.css +134 -0
- package/src/components/feedback/ThemeBootstrap/ThemeBootstrap.astro +195 -14
- package/src/components/icons/DayIcon.astro +9 -5
- package/src/components/icons/NightIcon.astro +5 -3
- package/src/components/icons/PumpkinIcon.astro +37 -0
- package/src/components/index.ts +1 -0
- package/src/components/navigation/Footer/Footer.astro +3 -0
- package/src/components/navigation/Footer/Footer.css +12 -1
- package/src/components/navigation/Header.css +30 -1
- package/src/components/navigation/MobileNavigation.css +12 -5
- package/src/components/navigation/NavigationBrand.astro +18 -0
- package/src/components/navigation/NavigationBrand.css +31 -0
- package/src/components/navigation/NavigationControl.css +1 -1
- package/src/components/navigation/SubNavigation/SubNavigation.css +1 -1
- package/src/components/seo/SeoHead.astro +18 -8
- package/src/config/siteConfig.ts +32 -2
- package/src/layouts/BaseLayout.astro +6 -1
- package/src/patterns/Dropdown/Dropdown.css +11 -2
- package/src/patterns/StageFlow/StageFlowStage.css +11 -5
- package/src/styles/base.css +13 -0
- package/src/styles/interaction.css +4 -1
- package/src/styles/style.css +3 -0
- package/src/theme/contracts.ts +18 -0
- package/src/theme/halloween.css +20 -0
- package/src/theme/runtime.ts +13 -0
package/package.json
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import type { HTMLAttributes } from 'astro/types';
|
|
3
3
|
import DayIcon from '../../icons/DayIcon.astro';
|
|
4
4
|
import NightIcon from '../../icons/NightIcon.astro';
|
|
5
|
+
import PumpkinIcon from '../../icons/PumpkinIcon.astro';
|
|
5
6
|
|
|
6
7
|
type Props = HTMLAttributes<'button'> & {
|
|
7
8
|
appearance?: 'circle' | 'utility';
|
|
8
9
|
darkLabel: string;
|
|
9
10
|
lightLabel: string;
|
|
11
|
+
halloweenLabel?: string;
|
|
10
12
|
};
|
|
11
13
|
|
|
12
14
|
const {
|
|
@@ -14,6 +16,7 @@ const {
|
|
|
14
16
|
class: className,
|
|
15
17
|
darkLabel,
|
|
16
18
|
lightLabel,
|
|
19
|
+
halloweenLabel,
|
|
17
20
|
type = 'button',
|
|
18
21
|
...attrs
|
|
19
22
|
}: Props = Astro.props;
|
|
@@ -29,6 +32,7 @@ const {
|
|
|
29
32
|
data-theme-toggle-appearance={appearance}
|
|
30
33
|
data-theme-toggle-dark-label={darkLabel}
|
|
31
34
|
data-theme-toggle-light-label={lightLabel}
|
|
35
|
+
data-theme-toggle-halloween-label={halloweenLabel}
|
|
32
36
|
data-navigation-control={appearance === 'circle' ? '' : undefined}
|
|
33
37
|
data-navigation-control-appearance={appearance === 'circle'
|
|
34
38
|
? 'bar'
|
|
@@ -48,6 +52,13 @@ const {
|
|
|
48
52
|
>
|
|
49
53
|
<DayIcon />
|
|
50
54
|
</span>
|
|
55
|
+
<span
|
|
56
|
+
class="theme-toggle__icon"
|
|
57
|
+
data-theme-toggle-target="halloween"
|
|
58
|
+
aria-hidden="true"
|
|
59
|
+
>
|
|
60
|
+
<PumpkinIcon />
|
|
61
|
+
</span>
|
|
51
62
|
</button>
|
|
52
63
|
|
|
53
64
|
<script>
|
|
@@ -49,15 +49,16 @@
|
|
|
49
49
|
block-size: 100%;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
.theme-toggle [data-theme-toggle-target
|
|
52
|
+
.theme-toggle [data-theme-toggle-target] {
|
|
53
53
|
display: none;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
:root[data-theme
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
:root:not([data-next-theme]) .theme-toggle [data-theme-toggle-target='dark'],
|
|
57
|
+
:root[data-next-theme='dark'] .theme-toggle [data-theme-toggle-target='dark'],
|
|
58
|
+
:root[data-next-theme='light'] .theme-toggle [data-theme-toggle-target='light'],
|
|
59
|
+
:root[data-next-theme='halloween']
|
|
60
|
+
.theme-toggle
|
|
61
|
+
[data-theme-toggle-target='halloween'] {
|
|
61
62
|
display: grid;
|
|
62
63
|
}
|
|
63
64
|
|
|
@@ -1,114 +1,33 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import { defaultThemeStorageKey } from '../../../theme/storage';
|
|
1
|
+
import type { ThemeRuntime } from '../../../theme/runtime';
|
|
3
2
|
|
|
4
3
|
const initializedToggles = new WeakSet<HTMLElement>();
|
|
5
|
-
const systemThemeQuery = '(prefers-color-scheme: dark)';
|
|
6
|
-
let globalListenersInitialized = false;
|
|
7
|
-
|
|
8
|
-
function isColorTheme(value: string | null): value is ColorTheme {
|
|
9
|
-
return value === 'light' || value === 'dark';
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function getStorageKey(): string {
|
|
13
|
-
return (
|
|
14
|
-
document.documentElement.dataset.themeStorageKey ?? defaultThemeStorageKey
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function readStoredTheme(): ColorTheme | null {
|
|
19
|
-
try {
|
|
20
|
-
const value = window.localStorage.getItem(getStorageKey());
|
|
21
|
-
return isColorTheme(value) ? value : null;
|
|
22
|
-
} catch {
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function getSystemTheme(): ColorTheme {
|
|
28
|
-
return window.matchMedia(systemThemeQuery).matches ? 'dark' : 'light';
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function getCurrentTheme(): ColorTheme {
|
|
32
|
-
const value = document.documentElement.dataset.theme ?? null;
|
|
33
|
-
return isColorTheme(value) ? value : (readStoredTheme() ?? getSystemTheme());
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function updateToggle(toggle: HTMLElement, theme: ColorTheme): void {
|
|
37
|
-
const label =
|
|
38
|
-
theme === 'light'
|
|
39
|
-
? toggle.dataset.themeToggleDarkLabel
|
|
40
|
-
: toggle.dataset.themeToggleLightLabel;
|
|
41
|
-
|
|
42
|
-
if (label) {
|
|
43
|
-
toggle.setAttribute('aria-label', label);
|
|
44
|
-
toggle.setAttribute('title', label);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function updateToggles(theme: ColorTheme): void {
|
|
49
|
-
for (const toggle of document.querySelectorAll<HTMLElement>(
|
|
50
|
-
'[data-theme-toggle]',
|
|
51
|
-
)) {
|
|
52
|
-
updateToggle(toggle, theme);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function applyTheme(theme: ColorTheme, persist: boolean): void {
|
|
57
|
-
const root = document.documentElement;
|
|
58
|
-
root.dataset.themeSwitching = 'true';
|
|
59
|
-
root.dataset.theme = theme;
|
|
60
|
-
void root.offsetWidth;
|
|
61
|
-
delete root.dataset.themeSwitching;
|
|
62
|
-
|
|
63
|
-
if (persist) {
|
|
64
|
-
try {
|
|
65
|
-
window.localStorage.setItem(getStorageKey(), theme);
|
|
66
|
-
} catch {
|
|
67
|
-
// The selected theme remains active for the current document.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
updateToggles(theme);
|
|
72
|
-
document.dispatchEvent(
|
|
73
|
-
new CustomEvent('astronox:theme-change', { detail: { theme } }),
|
|
74
|
-
);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function initializeGlobalListeners(): void {
|
|
78
|
-
if (globalListenersInitialized) return;
|
|
79
|
-
globalListenersInitialized = true;
|
|
80
|
-
|
|
81
|
-
const media = window.matchMedia(systemThemeQuery);
|
|
82
|
-
media.addEventListener('change', (event) => {
|
|
83
|
-
if (readStoredTheme() === null) {
|
|
84
|
-
applyTheme(event.matches ? 'dark' : 'light', false);
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
window.addEventListener('storage', (event) => {
|
|
89
|
-
if (event.key !== getStorageKey()) return;
|
|
90
|
-
|
|
91
|
-
const theme = isColorTheme(event.newValue)
|
|
92
|
-
? event.newValue
|
|
93
|
-
: getSystemTheme();
|
|
94
|
-
applyTheme(theme, false);
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
4
|
|
|
98
5
|
export function initThemeToggle(toggle: HTMLElement): () => void {
|
|
99
6
|
if (initializedToggles.has(toggle)) return () => {};
|
|
7
|
+
const runtime: ThemeRuntime | undefined = window.__astronoxTheme;
|
|
8
|
+
if (!runtime) return () => {};
|
|
100
9
|
initializedToggles.add(toggle);
|
|
101
|
-
initializeGlobalListeners();
|
|
102
|
-
updateToggle(toggle, getCurrentTheme());
|
|
103
10
|
|
|
104
|
-
const
|
|
105
|
-
|
|
11
|
+
const update = (): void => {
|
|
12
|
+
const labels = {
|
|
13
|
+
light: toggle.dataset.themeToggleLightLabel,
|
|
14
|
+
dark: toggle.dataset.themeToggleDarkLabel,
|
|
15
|
+
halloween: toggle.dataset.themeToggleHalloweenLabel,
|
|
16
|
+
};
|
|
17
|
+
const label = labels[runtime.next()];
|
|
18
|
+
if (!label) return;
|
|
19
|
+
toggle.setAttribute('aria-label', label);
|
|
20
|
+
toggle.setAttribute('title', label);
|
|
106
21
|
};
|
|
22
|
+
const handleClick = (): void => runtime.cycle();
|
|
107
23
|
|
|
24
|
+
update();
|
|
108
25
|
toggle.addEventListener('click', handleClick);
|
|
26
|
+
document.addEventListener('astronox:theme-change', update);
|
|
109
27
|
|
|
110
28
|
return () => {
|
|
111
29
|
toggle.removeEventListener('click', handleClick);
|
|
30
|
+
document.removeEventListener('astronox:theme-change', update);
|
|
112
31
|
initializedToggles.delete(toggle);
|
|
113
32
|
};
|
|
114
33
|
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
const drips = [
|
|
3
|
+
{
|
|
4
|
+
x: 'calc(var(--nox-page-gutter) / 2)',
|
|
5
|
+
length: 34,
|
|
6
|
+
delay: 0,
|
|
7
|
+
edge: true,
|
|
8
|
+
},
|
|
9
|
+
{ x: '14%', length: 22, delay: 2000, edge: false },
|
|
10
|
+
{ x: '29%', length: 39, delay: 6000, edge: false },
|
|
11
|
+
{ x: '47%', length: 26, delay: 10000, edge: false },
|
|
12
|
+
{ x: '66%', length: 32, delay: 4000, edge: false },
|
|
13
|
+
{ x: '83%', length: 20, delay: 8000, edge: false },
|
|
14
|
+
{
|
|
15
|
+
x: 'calc(100% - var(--nox-page-gutter) / 2)',
|
|
16
|
+
length: 42,
|
|
17
|
+
delay: 4000,
|
|
18
|
+
edge: true,
|
|
19
|
+
},
|
|
20
|
+
];
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
<div class="seasonal-decorations" aria-hidden="true" inert>
|
|
24
|
+
{
|
|
25
|
+
drips.map(({ x, length, delay, edge }) => (
|
|
26
|
+
<div
|
|
27
|
+
class="seasonal-decorations__drip"
|
|
28
|
+
data-drip-edge={edge ? '' : undefined}
|
|
29
|
+
style={`--drip-x: ${x}; --drip-length: ${length}px; --drip-delay: ${delay}ms;`}
|
|
30
|
+
>
|
|
31
|
+
<svg
|
|
32
|
+
viewBox="0 0 24 48"
|
|
33
|
+
preserveAspectRatio="none"
|
|
34
|
+
fill="currentColor"
|
|
35
|
+
focusable="false"
|
|
36
|
+
>
|
|
37
|
+
<path d="M0 0H24C18 2 17 5 17 13V33C17 43 15 48 12 48S7 43 7 33V13C7 5 6 2 0 0Z" />
|
|
38
|
+
</svg>
|
|
39
|
+
<span class="seasonal-decorations__drop" />
|
|
40
|
+
</div>
|
|
41
|
+
))
|
|
42
|
+
}
|
|
43
|
+
{
|
|
44
|
+
['start', 'end'].map((side) => (
|
|
45
|
+
<svg
|
|
46
|
+
class="seasonal-decorations__web"
|
|
47
|
+
data-web-side={side}
|
|
48
|
+
viewBox="0 0 120 120"
|
|
49
|
+
fill="none"
|
|
50
|
+
focusable="false"
|
|
51
|
+
>
|
|
52
|
+
<path
|
|
53
|
+
d="M0 0V116M0 0L43 110M0 0L85 85M0 0L110 43M0 0H116M0 24Q7 21 9 23Q15 16 17 17Q21 8 23 9Q22 4 24 0M0 48Q13 43 18 46Q28 33 34 34Q41 20 46 18Q42 7 48 0M0 77Q22 68 29 72Q47 51 54 54Q65 35 72 29Q68 14 77 0M0 108Q30 93 40 101Q64 71 76 76Q90 49 101 40Q92 20 108 0"
|
|
54
|
+
stroke="currentColor"
|
|
55
|
+
stroke-width="1"
|
|
56
|
+
/>
|
|
57
|
+
</svg>
|
|
58
|
+
))
|
|
59
|
+
}
|
|
60
|
+
</div>
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
.seasonal-decorations {
|
|
2
|
+
--seasonal-decoration-top: calc(
|
|
3
|
+
var(--nox-site-header-compact-height) + var(--nox-seasonal-border-size)
|
|
4
|
+
);
|
|
5
|
+
|
|
6
|
+
position: fixed;
|
|
7
|
+
z-index: -1;
|
|
8
|
+
inset: var(--seasonal-decoration-top) 0 0;
|
|
9
|
+
display: none;
|
|
10
|
+
overflow: hidden;
|
|
11
|
+
scrollbar-gutter: inherit;
|
|
12
|
+
pointer-events: none;
|
|
13
|
+
user-select: none;
|
|
14
|
+
color: var(--nox-seasonal-blood);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
:root[data-appearance='halloween']:is(
|
|
18
|
+
[data-theme-decorations='static'],
|
|
19
|
+
[data-theme-decorations='animated']
|
|
20
|
+
)
|
|
21
|
+
.seasonal-decorations {
|
|
22
|
+
display: block;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.seasonal-decorations__drip {
|
|
26
|
+
--drip-period: 12s;
|
|
27
|
+
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset-block-start: 0;
|
|
30
|
+
inset-inline-start: var(--drip-x);
|
|
31
|
+
inline-size: 18px;
|
|
32
|
+
block-size: var(--drip-length);
|
|
33
|
+
transform: translateX(-50%);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.seasonal-decorations__drip[data-drip-edge] {
|
|
37
|
+
--drip-period: 8s;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.seasonal-decorations__drip > svg {
|
|
41
|
+
display: block;
|
|
42
|
+
inline-size: 100%;
|
|
43
|
+
block-size: 100%;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.seasonal-decorations__drop {
|
|
47
|
+
position: absolute;
|
|
48
|
+
inset-block-start: calc(100% - 8px);
|
|
49
|
+
inset-inline-start: 5px;
|
|
50
|
+
inline-size: 8px;
|
|
51
|
+
block-size: 13px;
|
|
52
|
+
border-radius: 45% 45% 55% 55%;
|
|
53
|
+
background: var(--nox-seasonal-blood);
|
|
54
|
+
opacity: 0;
|
|
55
|
+
transform-origin: top;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
.seasonal-decorations__web {
|
|
59
|
+
position: absolute;
|
|
60
|
+
inset-block-start: 0;
|
|
61
|
+
inline-size: clamp(3rem, 7vw, 7rem);
|
|
62
|
+
block-size: auto;
|
|
63
|
+
color: var(--nox-seasonal-web);
|
|
64
|
+
opacity: 0.35;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
.seasonal-decorations__web[data-web-side='start'] {
|
|
68
|
+
inset-inline-start: 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.seasonal-decorations__web[data-web-side='end'] {
|
|
72
|
+
inset-inline-end: 0;
|
|
73
|
+
transform: scaleX(-1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
@media (width >= 72rem) {
|
|
77
|
+
.seasonal-decorations {
|
|
78
|
+
--seasonal-decoration-top: calc(
|
|
79
|
+
var(--nox-site-header-height) - var(--nox-border-width) +
|
|
80
|
+
var(--nox-seasonal-border-size)
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@media (width < 48rem) {
|
|
86
|
+
.seasonal-decorations__drip {
|
|
87
|
+
block-size: calc(var(--drip-length) * 0.65);
|
|
88
|
+
inline-size: 14px;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.seasonal-decorations__drip:not([data-drip-edge]) {
|
|
92
|
+
display: none;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.seasonal-decorations__drop {
|
|
96
|
+
inset-inline-start: 3px;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
101
|
+
:root[data-appearance='halloween'][data-theme-decorations='animated']
|
|
102
|
+
.seasonal-decorations__drop {
|
|
103
|
+
animation: seasonal-blood-fall var(--drip-period) linear var(--drip-delay)
|
|
104
|
+
infinite both;
|
|
105
|
+
animation-play-state: var(--seasonal-animation-state, running);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
:root[data-theme-animation-paused] .seasonal-decorations {
|
|
110
|
+
--seasonal-animation-state: paused;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
@keyframes seasonal-blood-fall {
|
|
114
|
+
0% {
|
|
115
|
+
opacity: 0;
|
|
116
|
+
transform: translateY(0) scale(0.5, 0.2);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
4% {
|
|
120
|
+
opacity: 1;
|
|
121
|
+
transform: translateY(0) scale(1, 1.15);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
8% {
|
|
125
|
+
opacity: 1;
|
|
126
|
+
transform: translateY(20px) scale(0.85, 1.3);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
25%,
|
|
130
|
+
100% {
|
|
131
|
+
opacity: 0;
|
|
132
|
+
transform: translateY(min(65vh, 30rem)) scale(0.75, 1.4);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
@@ -1,31 +1,212 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { defaultThemeStorageKey } from '../../../theme/storage';
|
|
3
|
+
import type { SeasonalThemeConfig } from '../../../theme/contracts';
|
|
3
4
|
|
|
4
5
|
type Props = {
|
|
5
6
|
storageKey?: string;
|
|
7
|
+
seasonalTheme?: SeasonalThemeConfig;
|
|
6
8
|
};
|
|
7
9
|
|
|
8
|
-
const { storageKey = defaultThemeStorageKey }: Props =
|
|
10
|
+
const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
11
|
+
Astro.props;
|
|
9
12
|
---
|
|
10
13
|
|
|
11
|
-
<script
|
|
14
|
+
<script
|
|
15
|
+
is:inline
|
|
16
|
+
define:vars={{ storageKey, seasonalTheme: seasonalTheme ?? null }}
|
|
17
|
+
>
|
|
12
18
|
(() => {
|
|
19
|
+
window.__astronoxTheme?.destroy();
|
|
13
20
|
const root = document.documentElement;
|
|
14
|
-
|
|
21
|
+
const seasonalKey = `${storageKey}:seasonal`;
|
|
22
|
+
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
23
|
+
let choices = ['light', 'dark'];
|
|
24
|
+
let activeEventId = null;
|
|
25
|
+
let boundaryTimeout;
|
|
15
26
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// The system preference remains available without local storage.
|
|
21
|
-
}
|
|
27
|
+
const resolveSeason = (now) => {
|
|
28
|
+
if (!seasonalTheme) return { id: null, boundary: null };
|
|
29
|
+
const period = seasonalTheme.annualPeriod;
|
|
30
|
+
if (!period) return { id: seasonalTheme.id, boundary: null };
|
|
22
31
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
32
|
+
const spansNewYear =
|
|
33
|
+
period.start.month * 100 + period.start.day >
|
|
34
|
+
period.end.month * 100 + period.end.day;
|
|
35
|
+
const year = now.getFullYear();
|
|
36
|
+
for (const startYear of [year - 1, year, year + 1]) {
|
|
37
|
+
const start = new Date(
|
|
38
|
+
startYear,
|
|
39
|
+
period.start.month - 1,
|
|
40
|
+
period.start.day,
|
|
41
|
+
);
|
|
42
|
+
const end = new Date(
|
|
43
|
+
startYear + Number(spansNewYear),
|
|
44
|
+
period.end.month - 1,
|
|
45
|
+
period.end.day,
|
|
46
|
+
);
|
|
47
|
+
if (now < start) return { id: null, boundary: start.getTime() };
|
|
48
|
+
if (now < end) {
|
|
49
|
+
return {
|
|
50
|
+
id: `${seasonalTheme.id}-${startYear}`,
|
|
51
|
+
boundary: end.getTime(),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return { id: null, boundary: null };
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const read = (key) => {
|
|
59
|
+
try {
|
|
60
|
+
return window.localStorage.getItem(key);
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const write = (key, value) => {
|
|
66
|
+
try {
|
|
67
|
+
window.localStorage.setItem(key, value);
|
|
68
|
+
} catch {
|
|
69
|
+
// Keep the document's choice when browser storage is unavailable.
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const readClassic = () => {
|
|
73
|
+
const value = read(storageKey);
|
|
74
|
+
return value === 'light' || value === 'dark' ? value : null;
|
|
75
|
+
};
|
|
76
|
+
const readSeasonal = () => {
|
|
77
|
+
if (!activeEventId) return null;
|
|
78
|
+
try {
|
|
79
|
+
const value = JSON.parse(read(seasonalKey) ?? 'null');
|
|
80
|
+
if (
|
|
81
|
+
value?.id === activeEventId &&
|
|
82
|
+
(value.choice === 'seasonal' || value.choice === 'classic')
|
|
83
|
+
) {
|
|
84
|
+
return value.choice;
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
// An invalid or older event preference does not disable a new event.
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
};
|
|
91
|
+
const saveSeasonal = (choice) => {
|
|
92
|
+
if (!activeEventId) return;
|
|
93
|
+
write(seasonalKey, JSON.stringify({ id: activeEventId, choice }));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
let classic = readClassic();
|
|
97
|
+
let seasonal = null;
|
|
98
|
+
let current;
|
|
99
|
+
|
|
100
|
+
const handleVisibility = () => {
|
|
101
|
+
root.toggleAttribute('data-theme-animation-paused', document.hidden);
|
|
102
|
+
if (!document.hidden) reconcile();
|
|
103
|
+
};
|
|
104
|
+
const next = () => choices[(choices.indexOf(current) + 1) % choices.length];
|
|
105
|
+
const resolve = () =>
|
|
106
|
+
activeEventId && seasonal === 'seasonal'
|
|
107
|
+
? seasonalTheme.appearance
|
|
108
|
+
: (classic ?? (media?.matches ? 'dark' : 'light'));
|
|
109
|
+
const apply = () => {
|
|
110
|
+
const appearance = resolve();
|
|
111
|
+
const appearanceChanged = current !== appearance;
|
|
112
|
+
const theme = appearance === 'light' ? 'light' : 'dark';
|
|
113
|
+
const decorations = activeEventId
|
|
114
|
+
? (seasonalTheme.decorations ?? 'none')
|
|
115
|
+
: 'none';
|
|
116
|
+
if (appearanceChanged) root.dataset.themeSwitching = 'true';
|
|
117
|
+
root.dataset.theme = theme;
|
|
118
|
+
root.dataset.appearance = appearance;
|
|
119
|
+
current = appearance;
|
|
120
|
+
const nextTheme = next();
|
|
121
|
+
const controlsChanged =
|
|
122
|
+
root.dataset.nextTheme !== nextTheme ||
|
|
123
|
+
root.dataset.themeDecorations !== decorations;
|
|
124
|
+
root.dataset.nextTheme = nextTheme;
|
|
125
|
+
root.dataset.themeDecorations = decorations;
|
|
126
|
+
root.toggleAttribute(
|
|
127
|
+
'data-theme-animation-paused',
|
|
128
|
+
document.hidden && decorations === 'animated',
|
|
129
|
+
);
|
|
130
|
+
if (appearanceChanged) {
|
|
131
|
+
void root.offsetWidth;
|
|
132
|
+
delete root.dataset.themeSwitching;
|
|
133
|
+
}
|
|
134
|
+
if (appearanceChanged || controlsChanged) {
|
|
135
|
+
document.dispatchEvent(
|
|
136
|
+
new CustomEvent('astronox:theme-change', {
|
|
137
|
+
detail: { theme, appearance },
|
|
138
|
+
}),
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
const reconcile = () => {
|
|
143
|
+
const now = new Date();
|
|
144
|
+
const season = resolveSeason(now);
|
|
145
|
+
if (activeEventId !== season.id) {
|
|
146
|
+
activeEventId = season.id;
|
|
147
|
+
seasonal = readSeasonal();
|
|
148
|
+
}
|
|
149
|
+
if (activeEventId && seasonal === null) {
|
|
150
|
+
seasonal = 'seasonal';
|
|
151
|
+
saveSeasonal(seasonal);
|
|
152
|
+
}
|
|
153
|
+
choices = activeEventId
|
|
154
|
+
? ['light', 'dark', seasonalTheme.appearance]
|
|
155
|
+
: ['light', 'dark'];
|
|
156
|
+
clearTimeout(boundaryTimeout);
|
|
157
|
+
if (season.boundary !== null) {
|
|
158
|
+
boundaryTimeout = setTimeout(
|
|
159
|
+
reconcile,
|
|
160
|
+
Math.min(season.boundary - now.getTime(), 2_147_483_647),
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
apply();
|
|
164
|
+
};
|
|
165
|
+
const cycle = () => {
|
|
166
|
+
reconcile();
|
|
167
|
+
const appearance = next();
|
|
168
|
+
if (appearance === 'light' || appearance === 'dark') {
|
|
169
|
+
classic = appearance;
|
|
170
|
+
write(storageKey, classic);
|
|
171
|
+
seasonal = 'classic';
|
|
172
|
+
} else {
|
|
173
|
+
seasonal = 'seasonal';
|
|
174
|
+
}
|
|
175
|
+
saveSeasonal(seasonal);
|
|
176
|
+
apply();
|
|
177
|
+
};
|
|
178
|
+
const handleStorage = (event) => {
|
|
179
|
+
if (
|
|
180
|
+
event.key !== null &&
|
|
181
|
+
event.key !== storageKey &&
|
|
182
|
+
event.key !== seasonalKey
|
|
183
|
+
) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
classic = readClassic();
|
|
187
|
+
seasonal = readSeasonal();
|
|
188
|
+
reconcile();
|
|
189
|
+
};
|
|
27
190
|
|
|
28
|
-
root.dataset.theme = storedTheme ?? systemTheme;
|
|
29
191
|
root.dataset.themeStorageKey = storageKey;
|
|
192
|
+
window.__astronoxTheme = {
|
|
193
|
+
next,
|
|
194
|
+
cycle,
|
|
195
|
+
destroy: () => {
|
|
196
|
+
clearTimeout(boundaryTimeout);
|
|
197
|
+
media?.removeEventListener('change', reconcile);
|
|
198
|
+
document.removeEventListener('visibilitychange', handleVisibility);
|
|
199
|
+
delete root.dataset.themeAnimationPaused;
|
|
200
|
+
window.removeEventListener('storage', handleStorage);
|
|
201
|
+
window.removeEventListener('focus', reconcile);
|
|
202
|
+
window.removeEventListener('pageshow', reconcile);
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
media?.addEventListener('change', reconcile);
|
|
206
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
207
|
+
window.addEventListener('storage', handleStorage);
|
|
208
|
+
window.addEventListener('focus', reconcile);
|
|
209
|
+
window.addEventListener('pageshow', reconcile);
|
|
210
|
+
reconcile();
|
|
30
211
|
})();
|
|
31
212
|
</script>
|
|
@@ -23,11 +23,15 @@ const {
|
|
|
23
23
|
aria-hidden={ariaHidden}
|
|
24
24
|
focusable={focusable}
|
|
25
25
|
>
|
|
26
|
-
<
|
|
27
|
-
></circle>
|
|
28
|
-
<path
|
|
29
|
-
d="M16 8v2M16 22v2M8 16h2M22 16h2M10.35 10.35l1.4 1.4M20.25 20.25l1.4 1.4M21.65 10.35l-1.4 1.4M11.75 20.25l-1.4 1.4"
|
|
26
|
+
<g
|
|
30
27
|
stroke="currentColor"
|
|
31
28
|
stroke-width="1.5"
|
|
32
|
-
stroke-linecap="round"
|
|
29
|
+
stroke-linecap="round"
|
|
30
|
+
stroke-linejoin="round"
|
|
31
|
+
>
|
|
32
|
+
<circle cx="16" cy="16" r="4.5"></circle>
|
|
33
|
+
<path
|
|
34
|
+
d="M16 7v2M16 23v2M7 16h2M23 16h2M9.64 9.64l1.41 1.41M20.95 20.95l1.41 1.41M22.36 9.64l-1.41 1.41M11.05 20.95l-1.41 1.41"
|
|
35
|
+
></path>
|
|
36
|
+
</g>
|
|
33
37
|
</svg>
|
|
@@ -23,10 +23,12 @@ const {
|
|
|
23
23
|
aria-hidden={ariaHidden}
|
|
24
24
|
focusable={focusable}
|
|
25
25
|
>
|
|
26
|
-
<
|
|
27
|
-
d="M22.5 19.25A7.5 7.5 0 0 1 12.75 9.5a7.5 7.5 0 1 0 9.75 9.75Z"
|
|
26
|
+
<g
|
|
28
27
|
stroke="currentColor"
|
|
29
28
|
stroke-width="1.5"
|
|
30
29
|
stroke-linecap="round"
|
|
31
|
-
stroke-linejoin="round"
|
|
30
|
+
stroke-linejoin="round"
|
|
31
|
+
>
|
|
32
|
+
<path d="M25 16.8A9 9 0 1 1 15.2 7A7 7 0 0 0 25 16.8Z"></path>
|
|
33
|
+
</g>
|
|
32
34
|
</svg>
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
|
|
6
|
+
const {
|
|
7
|
+
width = 32,
|
|
8
|
+
height = 32,
|
|
9
|
+
'aria-hidden': ariaHidden = 'true',
|
|
10
|
+
focusable = 'false',
|
|
11
|
+
...attrs
|
|
12
|
+
}: Props = Astro.props;
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
<svg
|
|
16
|
+
{...attrs}
|
|
17
|
+
width={width}
|
|
18
|
+
height={height}
|
|
19
|
+
viewBox="0 0 32 32"
|
|
20
|
+
fill="none"
|
|
21
|
+
aria-hidden={ariaHidden}
|
|
22
|
+
focusable={focusable}
|
|
23
|
+
>
|
|
24
|
+
<g
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
stroke-width="1.5"
|
|
27
|
+
stroke-linecap="round"
|
|
28
|
+
stroke-linejoin="round"
|
|
29
|
+
>
|
|
30
|
+
<path d="M16 11Q15 8 18 7"></path>
|
|
31
|
+
<path
|
|
32
|
+
d="M16 11C13 8.5 7 9.5 7 16C7 21 9.8 24.5 13 23.5Q16 25 19 23.5C22.2 24.5 25 21 25 16C25 9.5 19 8.5 16 11Z"
|
|
33
|
+
></path>
|
|
34
|
+
<path d="M11 16.5l1.5-2 1.5 2ZM18 16.5l1.5-2 1.5 2Z"></path>
|
|
35
|
+
<path d="M12 19.5l2 1.5 2-1 2 1 2-1.5"></path>
|
|
36
|
+
</g>
|
|
37
|
+
</svg>
|
package/src/components/index.ts
CHANGED
|
@@ -94,6 +94,7 @@ export { default as InfoIcon } from './icons/InfoIcon.astro';
|
|
|
94
94
|
export { default as MinusIcon } from './icons/MinusIcon.astro';
|
|
95
95
|
export { default as MountainIcon } from './icons/MountainIcon.astro';
|
|
96
96
|
export { default as NightIcon } from './icons/NightIcon.astro';
|
|
97
|
+
export { default as PumpkinIcon } from './icons/PumpkinIcon.astro';
|
|
97
98
|
export { default as PlayIcon } from './icons/PlayIcon.astro';
|
|
98
99
|
export { default as PlusIcon } from './icons/PlusIcon.astro';
|
|
99
100
|
export { default as PoisonIcon } from './icons/PoisonIcon.astro';
|
|
@@ -7,6 +7,7 @@ import ArrowRightIcon from '../../icons/ArrowRightIcon.astro';
|
|
|
7
7
|
type Props = HTMLAttributes<'footer'> & {
|
|
8
8
|
columnSeparator?: 'divider' | 'none';
|
|
9
9
|
columns?: 1 | 2;
|
|
10
|
+
endSpacing?: 'default' | 'extended';
|
|
10
11
|
groups: readonly FooterGroup[];
|
|
11
12
|
label: string;
|
|
12
13
|
tone?: Tone;
|
|
@@ -17,6 +18,7 @@ const {
|
|
|
17
18
|
columnSeparator = 'none',
|
|
18
19
|
columns = 1,
|
|
19
20
|
'data-tone': dataTone,
|
|
21
|
+
endSpacing = 'default',
|
|
20
22
|
groups,
|
|
21
23
|
label,
|
|
22
24
|
tone,
|
|
@@ -31,6 +33,7 @@ const resolveRel = (target: FooterItem['target'], rel?: FooterItem['rel']) =>
|
|
|
31
33
|
<footer
|
|
32
34
|
{...attrs}
|
|
33
35
|
class:list={['site-footer', className]}
|
|
36
|
+
data-footer-end-spacing={endSpacing}
|
|
34
37
|
data-tone={resolvedTone}
|
|
35
38
|
data-widget="site-footer"
|
|
36
39
|
>
|
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
color: var(--nox-text-muted);
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
.site-footer[data-footer-end-spacing='extended'] {
|
|
6
|
+
--site-footer-end-reserve: calc(
|
|
7
|
+
clamp(10rem, 20vh, 16rem) + env(safe-area-inset-bottom, 0px)
|
|
8
|
+
);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
:root[data-appearance='halloween'] .site-footer {
|
|
12
|
+
background: var(--nox-page-background);
|
|
13
|
+
}
|
|
14
|
+
|
|
5
15
|
.site-footer__content {
|
|
6
16
|
--site-footer-item-gap: var(--nox-space-2);
|
|
7
17
|
--site-footer-marker-gap: 0.0625rem;
|
|
@@ -12,7 +22,8 @@
|
|
|
12
22
|
justify-items: center;
|
|
13
23
|
gap: var(--nox-space-1);
|
|
14
24
|
padding-block-end: calc(
|
|
15
|
-
var(--site-content-gap) + var(--site-floating-control-reserve, 0px)
|
|
25
|
+
var(--site-content-gap) + var(--site-floating-control-reserve, 0px) +
|
|
26
|
+
var(--site-footer-end-reserve, 0px)
|
|
16
27
|
);
|
|
17
28
|
font-size: var(--nox-font-size-sm);
|
|
18
29
|
line-height: 1.25;
|
|
@@ -4,10 +4,14 @@
|
|
|
4
4
|
var(--nox-site-header-height) - var(--site-header-border-width)
|
|
5
5
|
);
|
|
6
6
|
|
|
7
|
+
position: relative;
|
|
7
8
|
z-index: 50;
|
|
8
9
|
top: 0;
|
|
9
10
|
border-bottom: var(--site-header-border-width) solid var(--nox-border);
|
|
10
|
-
background:
|
|
11
|
+
background: var(
|
|
12
|
+
--nox-seasonal-header-background,
|
|
13
|
+
color-mix(in srgb, var(--nox-surface) 94%, transparent)
|
|
14
|
+
);
|
|
11
15
|
backdrop-filter: blur(14px);
|
|
12
16
|
}
|
|
13
17
|
|
|
@@ -15,6 +19,31 @@
|
|
|
15
19
|
position: sticky;
|
|
16
20
|
}
|
|
17
21
|
|
|
22
|
+
:root[data-appearance='halloween']:is(
|
|
23
|
+
[data-theme-decorations='static'],
|
|
24
|
+
[data-theme-decorations='animated']
|
|
25
|
+
)
|
|
26
|
+
.site-header {
|
|
27
|
+
--site-header-menu-block-offset: var(--nox-seasonal-border-size);
|
|
28
|
+
--site-header-menu-border-width: 0px;
|
|
29
|
+
--dropdown-attached-block-offset: var(--site-header-menu-block-offset);
|
|
30
|
+
--dropdown-attached-border-top-width: var(--site-header-menu-border-width);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
:root[data-appearance='halloween']:is(
|
|
34
|
+
[data-theme-decorations='static'],
|
|
35
|
+
[data-theme-decorations='animated']
|
|
36
|
+
)
|
|
37
|
+
.site-header::after {
|
|
38
|
+
position: absolute;
|
|
39
|
+
inset-inline: 0;
|
|
40
|
+
inset-block-start: 100%;
|
|
41
|
+
block-size: var(--nox-seasonal-border-size);
|
|
42
|
+
background: var(--nox-seasonal-blood);
|
|
43
|
+
content: '';
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
18
47
|
.site-nav {
|
|
19
48
|
position: relative;
|
|
20
49
|
}
|
|
@@ -111,15 +111,22 @@
|
|
|
111
111
|
z-index: 2;
|
|
112
112
|
display: grid;
|
|
113
113
|
inset-inline: 0;
|
|
114
|
-
top: 100
|
|
114
|
+
top: calc(100% + var(--site-header-menu-block-offset, 0px));
|
|
115
115
|
width: 100%;
|
|
116
|
-
height: calc(
|
|
117
|
-
|
|
116
|
+
height: calc(
|
|
117
|
+
100vh - var(--nox-site-header-compact-height) -
|
|
118
|
+
var(--site-header-menu-block-offset, 0px)
|
|
119
|
+
);
|
|
120
|
+
height: calc(
|
|
121
|
+
100dvh - var(--nox-site-header-compact-height) -
|
|
122
|
+
var(--site-header-menu-block-offset, 0px)
|
|
123
|
+
);
|
|
118
124
|
grid-template-rows: minmax(0, 1fr) auto;
|
|
119
125
|
overflow-x: hidden;
|
|
120
126
|
overflow-y: hidden;
|
|
121
127
|
padding: var(--nox-space-3);
|
|
122
|
-
border-top:
|
|
128
|
+
border-top: var(--site-header-menu-border-width, 3px) solid
|
|
129
|
+
var(--nox-theme-accent-color, var(--nox-link-active));
|
|
123
130
|
border-bottom: var(--nox-border-width) solid var(--nox-border);
|
|
124
131
|
border-end-start-radius: var(--nox-radius-md);
|
|
125
132
|
border-end-end-radius: var(--nox-radius-md);
|
|
@@ -160,7 +167,7 @@
|
|
|
160
167
|
left: 0;
|
|
161
168
|
height: 2px;
|
|
162
169
|
border-radius: var(--nox-radius-pill);
|
|
163
|
-
background: var(--nox-source-tone-success);
|
|
170
|
+
background: var(--nox-theme-accent-color, var(--nox-source-tone-success));
|
|
164
171
|
content: '';
|
|
165
172
|
}
|
|
166
173
|
|
|
@@ -36,9 +36,11 @@ const logoAspectRatio = logo ? `${logo.width} / ${logo.height}` : undefined;
|
|
|
36
36
|
<div
|
|
37
37
|
class="site-nav__brand-logo-frame"
|
|
38
38
|
style={`--site-nav-brand-logo-aspect-ratio: ${logoAspectRatio};`}
|
|
39
|
+
data-brand-variants={Object.keys(logo.variants ?? {}).join(' ')}
|
|
39
40
|
>
|
|
40
41
|
<Media
|
|
41
42
|
class="site-nav__brand-logo"
|
|
43
|
+
data-brand-appearance="default"
|
|
42
44
|
media={{
|
|
43
45
|
src: logo.src,
|
|
44
46
|
alt: logo.alt,
|
|
@@ -50,6 +52,22 @@ const logoAspectRatio = logo ? `${logo.width} / ${logo.height}` : undefined;
|
|
|
50
52
|
}}
|
|
51
53
|
fit="contain"
|
|
52
54
|
/>
|
|
55
|
+
{Object.entries(logo.variants ?? {}).map(([appearance, src]) => (
|
|
56
|
+
<Media
|
|
57
|
+
class="site-nav__brand-logo"
|
|
58
|
+
data-brand-appearance={appearance}
|
|
59
|
+
media={{
|
|
60
|
+
src,
|
|
61
|
+
alt: logo.alt,
|
|
62
|
+
width: logo.width,
|
|
63
|
+
height: logo.height,
|
|
64
|
+
loading: 'eager',
|
|
65
|
+
decoding: 'sync',
|
|
66
|
+
fetchPriority: 'high',
|
|
67
|
+
}}
|
|
68
|
+
fit="contain"
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
53
71
|
</div>
|
|
54
72
|
) : (
|
|
55
73
|
<>
|
|
@@ -21,6 +21,37 @@
|
|
|
21
21
|
aspect-ratio: var(--site-nav-brand-logo-aspect-ratio);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
.site-nav__brand-logo[data-brand-appearance] {
|
|
25
|
+
grid-area: 1 / 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.site-nav__brand-logo[data-brand-appearance]:not(
|
|
29
|
+
[data-brand-appearance='default']
|
|
30
|
+
) {
|
|
31
|
+
display: none;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
:root[data-appearance='light']
|
|
35
|
+
.site-nav__brand-logo[data-brand-appearance='light'],
|
|
36
|
+
:root[data-appearance='dark']
|
|
37
|
+
.site-nav__brand-logo[data-brand-appearance='dark'],
|
|
38
|
+
:root[data-appearance='halloween']
|
|
39
|
+
.site-nav__brand-logo[data-brand-appearance='halloween'] {
|
|
40
|
+
display: grid;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
:root[data-appearance='light']
|
|
44
|
+
.site-nav__brand-logo-frame[data-brand-variants~='light']
|
|
45
|
+
[data-brand-appearance='default'],
|
|
46
|
+
:root[data-appearance='dark']
|
|
47
|
+
.site-nav__brand-logo-frame[data-brand-variants~='dark']
|
|
48
|
+
[data-brand-appearance='default'],
|
|
49
|
+
:root[data-appearance='halloween']
|
|
50
|
+
.site-nav__brand-logo-frame[data-brand-variants~='halloween']
|
|
51
|
+
[data-brand-appearance='default'] {
|
|
52
|
+
display: none;
|
|
53
|
+
}
|
|
54
|
+
|
|
24
55
|
.site-nav__brand-logo-frame [data-widget='media'] {
|
|
25
56
|
inline-size: 100%;
|
|
26
57
|
block-size: 100%;
|
|
@@ -110,7 +110,7 @@
|
|
|
110
110
|
left: var(--nox-space-4);
|
|
111
111
|
height: 3px;
|
|
112
112
|
border-radius: var(--nox-radius-pill);
|
|
113
|
-
background: var(--nox-source-tone-success);
|
|
113
|
+
background: var(--nox-theme-accent-color, var(--nox-source-tone-success));
|
|
114
114
|
content: '';
|
|
115
115
|
opacity: 0;
|
|
116
116
|
transform: translateY(var(--nox-space-1));
|
|
@@ -101,20 +101,30 @@ const jsonLdNodes = [
|
|
|
101
101
|
}
|
|
102
102
|
<script is:inline>
|
|
103
103
|
(() => {
|
|
104
|
-
const applyThemeColor = (theme) => {
|
|
105
|
-
|
|
104
|
+
const applyThemeColor = (theme, appearance) => {
|
|
105
|
+
const candidates = [
|
|
106
|
+
...document.querySelectorAll(
|
|
107
|
+
'meta[name="theme-color"][data-theme-color]',
|
|
108
|
+
),
|
|
109
|
+
];
|
|
110
|
+
const target = candidates.some(
|
|
111
|
+
(meta) => meta.dataset.themeColor === appearance,
|
|
112
|
+
)
|
|
113
|
+
? appearance
|
|
114
|
+
: theme;
|
|
106
115
|
|
|
107
|
-
for (const meta of
|
|
108
|
-
'meta[name="theme-color"][data-theme-color]',
|
|
109
|
-
)) {
|
|
116
|
+
for (const meta of candidates) {
|
|
110
117
|
if (!(meta instanceof HTMLMetaElement)) continue;
|
|
111
|
-
meta.media = meta.dataset.themeColor ===
|
|
118
|
+
meta.media = meta.dataset.themeColor === target ? 'all' : 'not all';
|
|
112
119
|
}
|
|
113
120
|
};
|
|
114
121
|
|
|
115
|
-
applyThemeColor(
|
|
122
|
+
applyThemeColor(
|
|
123
|
+
document.documentElement.dataset.theme,
|
|
124
|
+
document.documentElement.dataset.appearance,
|
|
125
|
+
);
|
|
116
126
|
document.addEventListener('astronox:theme-change', (event) => {
|
|
117
|
-
applyThemeColor(event.detail?.theme);
|
|
127
|
+
applyThemeColor(event.detail?.theme, event.detail?.appearance);
|
|
118
128
|
});
|
|
119
129
|
})();
|
|
120
130
|
</script>
|
package/src/config/siteConfig.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { PageMeta } from './pageMeta';
|
|
2
|
-
import type {
|
|
2
|
+
import type { SeasonalThemeConfig, ThemeAppearance } from '../theme/contracts';
|
|
3
3
|
|
|
4
4
|
export type SiteTextDirection = 'ltr' | 'rtl';
|
|
5
5
|
|
|
@@ -22,6 +22,7 @@ export type SiteIdentity = {
|
|
|
22
22
|
alt: string;
|
|
23
23
|
width: number;
|
|
24
24
|
height: number;
|
|
25
|
+
variants?: Partial<Record<ThemeAppearance, string>>;
|
|
25
26
|
};
|
|
26
27
|
};
|
|
27
28
|
|
|
@@ -36,7 +37,7 @@ export type SiteIconConfig = {
|
|
|
36
37
|
export type SiteThemeColor = {
|
|
37
38
|
color: string;
|
|
38
39
|
media?: string;
|
|
39
|
-
theme?:
|
|
40
|
+
theme?: ThemeAppearance;
|
|
40
41
|
};
|
|
41
42
|
|
|
42
43
|
export type SiteSeoConfig = {
|
|
@@ -56,6 +57,7 @@ export type SiteSeoConfig = {
|
|
|
56
57
|
|
|
57
58
|
export type SiteConfig = {
|
|
58
59
|
identity: SiteIdentity;
|
|
60
|
+
seasonalTheme?: SeasonalThemeConfig;
|
|
59
61
|
defaultLocale: string;
|
|
60
62
|
locales: readonly [SiteLocaleConfig, ...SiteLocaleConfig[]];
|
|
61
63
|
routing: {
|
|
@@ -75,6 +77,34 @@ export function defineSiteConfig<const TConfig extends SiteConfig>(
|
|
|
75
77
|
throw new Error(`Invalid site identity: ${config.identity.id}`);
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
if (config.seasonalTheme && !config.seasonalTheme.id.trim()) {
|
|
81
|
+
throw new Error('A seasonal theme requires a non-empty event id.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const period = config.seasonalTheme?.annualPeriod;
|
|
85
|
+
if (period) {
|
|
86
|
+
for (const { month, day } of [period.start, period.end]) {
|
|
87
|
+
const date = new Date(Date.UTC(2001, month - 1, day));
|
|
88
|
+
if (
|
|
89
|
+
!Number.isInteger(month) ||
|
|
90
|
+
!Number.isInteger(day) ||
|
|
91
|
+
date.getUTCFullYear() !== 2001 ||
|
|
92
|
+
date.getUTCMonth() !== month - 1 ||
|
|
93
|
+
date.getUTCDate() !== day
|
|
94
|
+
) {
|
|
95
|
+
throw new Error(
|
|
96
|
+
'Annual theme periods require month/day dates valid every year.',
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (
|
|
101
|
+
period.start.month === period.end.month &&
|
|
102
|
+
period.start.day === period.end.day
|
|
103
|
+
) {
|
|
104
|
+
throw new Error('An annual theme period requires distinct boundaries.');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
78
108
|
const localeIds = new Set<string>();
|
|
79
109
|
const urlCodes = new Set<string>();
|
|
80
110
|
|
|
@@ -11,6 +11,7 @@ import PageSurface from './PageSurface.astro';
|
|
|
11
11
|
import Breadcrumbs from '../components/navigation/Breadcrumbs/Breadcrumbs.astro';
|
|
12
12
|
import ScrollToTop from '../components/actions/ScrollToTop/ScrollToTop.astro';
|
|
13
13
|
import ThemeBootstrap from '../components/feedback/ThemeBootstrap/ThemeBootstrap.astro';
|
|
14
|
+
import SeasonalDecorations from '../components/feedback/SeasonalDecorations/SeasonalDecorations.astro';
|
|
14
15
|
import { defaultThemeStorageKey } from '../theme/storage';
|
|
15
16
|
|
|
16
17
|
type Props = HTMLAttributes<'html'> & {
|
|
@@ -51,7 +52,10 @@ const resolvedTone = tone ?? dataTone;
|
|
|
51
52
|
<head>
|
|
52
53
|
<meta charset="utf-8" />
|
|
53
54
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
54
|
-
<ThemeBootstrap
|
|
55
|
+
<ThemeBootstrap
|
|
56
|
+
storageKey={themeStorageKey}
|
|
57
|
+
seasonalTheme={site.seasonalTheme}
|
|
58
|
+
/>
|
|
55
59
|
<SeoHead
|
|
56
60
|
breadcrumbs={breadcrumbs}
|
|
57
61
|
meta={meta}
|
|
@@ -65,6 +69,7 @@ const resolvedTone = tone ?? dataTone;
|
|
|
65
69
|
<div class="site-shell" data-site-scroll-root>
|
|
66
70
|
<slot name="skip-link" />
|
|
67
71
|
<slot name="header" />
|
|
72
|
+
{site.seasonalTheme?.decorations ? <SeasonalDecorations /> : null}
|
|
68
73
|
<div class="site-viewport" data-site-viewport>
|
|
69
74
|
<main id="content" class="site-main" tabindex="-1">
|
|
70
75
|
<div class="site-main__content">
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
padding: var(--nox-space-2) 0;
|
|
58
58
|
gap: 0;
|
|
59
59
|
border: var(--nox-border-width) solid var(--nox-border);
|
|
60
|
-
border-top: 3px solid var(--nox-link-active);
|
|
60
|
+
border-top: 3px solid var(--nox-theme-accent-color, var(--nox-link-active));
|
|
61
61
|
border-radius: var(--nox-radius-md);
|
|
62
62
|
background: var(--nox-surface);
|
|
63
63
|
box-shadow: var(--nox-shadow-surface);
|
|
@@ -76,12 +76,21 @@
|
|
|
76
76
|
.dropdown[data-dropdown-attached][data-dropdown-placement='bottom']
|
|
77
77
|
.dropdown__content {
|
|
78
78
|
top: calc(100% + var(--dropdown-attached-block-offset, 0px));
|
|
79
|
-
border-top:
|
|
79
|
+
border-top-width: var(--dropdown-attached-border-top-width, 3px);
|
|
80
80
|
border-start-start-radius: 0;
|
|
81
81
|
border-start-end-radius: 0;
|
|
82
82
|
box-shadow: none;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
.dropdown[data-dropdown-attached][data-dropdown-placement='bottom'][data-dropdown-open]::before {
|
|
86
|
+
position: absolute;
|
|
87
|
+
z-index: 90;
|
|
88
|
+
inset-inline: 0;
|
|
89
|
+
inset-block-start: 100%;
|
|
90
|
+
block-size: var(--dropdown-attached-block-offset, 0px);
|
|
91
|
+
content: '';
|
|
92
|
+
}
|
|
93
|
+
|
|
85
94
|
.dropdown[data-dropdown-placement='top'] .dropdown__content {
|
|
86
95
|
bottom: calc(100% + var(--nox-space-2));
|
|
87
96
|
}
|
|
@@ -4,6 +4,11 @@
|
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
.stage-flow__stage {
|
|
7
|
+
--stage-flow-accent-color: var(
|
|
8
|
+
--nox-theme-accent-color,
|
|
9
|
+
var(--nox-tone-color)
|
|
10
|
+
);
|
|
11
|
+
|
|
7
12
|
display: grid;
|
|
8
13
|
inline-size: max-content;
|
|
9
14
|
min-inline-size: var(--stage-flow-stage-min-inline-size);
|
|
@@ -33,7 +38,7 @@
|
|
|
33
38
|
|
|
34
39
|
.stage-flow__stage[aria-current='page'] [data-widget='media-frame'] {
|
|
35
40
|
border-width: 4px;
|
|
36
|
-
border-color: var(--
|
|
41
|
+
border-color: var(--stage-flow-accent-color);
|
|
37
42
|
background:
|
|
38
43
|
radial-gradient(
|
|
39
44
|
circle at 50% 42%,
|
|
@@ -42,7 +47,7 @@
|
|
|
42
47
|
),
|
|
43
48
|
var(--nox-tone-background);
|
|
44
49
|
box-shadow:
|
|
45
|
-
0 0 0 1px var(--
|
|
50
|
+
0 0 0 1px var(--stage-flow-accent-color),
|
|
46
51
|
0 0.35rem 0.75rem
|
|
47
52
|
color-mix(in srgb, var(--nox-tone-shadow) 16%, transparent);
|
|
48
53
|
}
|
|
@@ -56,9 +61,9 @@
|
|
|
56
61
|
|
|
57
62
|
.stage-flow__stage[aria-current='page']:focus-visible
|
|
58
63
|
[data-widget='media-frame'] {
|
|
59
|
-
border-color: var(--
|
|
64
|
+
border-color: var(--stage-flow-accent-color);
|
|
60
65
|
box-shadow:
|
|
61
|
-
0 0 0 1px var(--
|
|
66
|
+
0 0 0 1px var(--stage-flow-accent-color),
|
|
62
67
|
0 0 0 6px color-mix(in srgb, var(--nox-tone-focus-ring) 18%, transparent),
|
|
63
68
|
0 0.5rem 1rem color-mix(in srgb, var(--nox-tone-shadow) 18%, transparent);
|
|
64
69
|
}
|
|
@@ -83,7 +88,8 @@
|
|
|
83
88
|
[data-widget='media-frame'] {
|
|
84
89
|
box-shadow:
|
|
85
90
|
0 0 0 1px var(--nox-border),
|
|
86
|
-
0 0 0 5px
|
|
91
|
+
0 0 0 5px
|
|
92
|
+
color-mix(in srgb, var(--stage-flow-accent-color) 16%, transparent),
|
|
87
93
|
0 0.5rem 1rem color-mix(in srgb, var(--nox-tone-shadow) 18%, transparent);
|
|
88
94
|
}
|
|
89
95
|
}
|
package/src/styles/base.css
CHANGED
|
@@ -167,6 +167,19 @@ p {
|
|
|
167
167
|
gap: 0;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
+
:root[data-appearance='halloween'] .site-shell {
|
|
171
|
+
isolation: isolate;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
:root[data-appearance='halloween'] .site-viewport {
|
|
175
|
+
background: transparent;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
:root[data-appearance='halloween'] .site-main__content {
|
|
179
|
+
background: var(--nox-page-background);
|
|
180
|
+
border-radius: var(--nox-radius-lg);
|
|
181
|
+
}
|
|
182
|
+
|
|
170
183
|
@media (width < 48rem) {
|
|
171
184
|
.site-viewport {
|
|
172
185
|
--site-content-gap: var(--nox-space-4);
|
package/src/styles/style.css
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
@layer settings, base, composition, primitives, components, patterns, surfaces, states, responsive;
|
|
2
2
|
|
|
3
3
|
@import './tokens.css' layer(settings);
|
|
4
|
+
@import '../theme/halloween.css' layer(settings);
|
|
4
5
|
@import './base.css' layer(base);
|
|
5
6
|
@import './interaction.css' layer(states);
|
|
6
7
|
|
|
@@ -46,6 +47,8 @@
|
|
|
46
47
|
@import '../components/forms/Textarea/Textarea.css' layer(components);
|
|
47
48
|
@import '../components/feedback/Tooltip/Tooltip.css' layer(components);
|
|
48
49
|
@import '../components/feedback/Spinner/Spinner.css' layer(components);
|
|
50
|
+
@import '../components/feedback/SeasonalDecorations/SeasonalDecorations.css'
|
|
51
|
+
layer(components);
|
|
49
52
|
@import '../components/icons/CurrencyIcon.css' layer(components);
|
|
50
53
|
@import '../components/icons/CircleSymbolIcon.css' layer(components);
|
|
51
54
|
@import '../components/media/CanvasFrame/CanvasFrame.css' layer(components);
|
package/src/theme/contracts.ts
CHANGED
|
@@ -2,3 +2,21 @@ export type Tone =
|
|
|
2
2
|
'neutral' | 'primary' | 'info' | 'success' | 'warning' | 'danger';
|
|
3
3
|
|
|
4
4
|
export type ColorTheme = 'light' | 'dark';
|
|
5
|
+
|
|
6
|
+
export type SeasonalAppearance = 'halloween';
|
|
7
|
+
|
|
8
|
+
export type ThemeAppearance = ColorTheme | SeasonalAppearance;
|
|
9
|
+
|
|
10
|
+
export type AnnualThemePeriod = {
|
|
11
|
+
/** Inclusive local date, with months numbered from 1 to 12. */
|
|
12
|
+
start: { month: number; day: number };
|
|
13
|
+
/** Exclusive local date, possibly in the following year. */
|
|
14
|
+
end: { month: number; day: number };
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type SeasonalThemeConfig = {
|
|
18
|
+
id: string;
|
|
19
|
+
appearance: SeasonalAppearance;
|
|
20
|
+
decorations?: 'static' | 'animated';
|
|
21
|
+
annualPeriod?: AnnualThemePeriod;
|
|
22
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
:root[data-appearance='halloween'] {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
|
|
4
|
+
--nox-source-tone-neutral: #b6aaa0;
|
|
5
|
+
--nox-source-tone-primary: #ff9a3c;
|
|
6
|
+
--nox-content-link-color: #ffb66c;
|
|
7
|
+
--nox-theme-page-base: #12100f;
|
|
8
|
+
--nox-theme-surface-base: #201b18;
|
|
9
|
+
--nox-theme-text-base: #f3eee8;
|
|
10
|
+
--nox-theme-text-disabled-base: #776b63;
|
|
11
|
+
--nox-theme-border-base: #342820;
|
|
12
|
+
--nox-theme-inverse-base: #12100f;
|
|
13
|
+
--nox-theme-shadow-base: #090706;
|
|
14
|
+
|
|
15
|
+
--nox-seasonal-header-background: #211512;
|
|
16
|
+
--nox-seasonal-blood: #a52330;
|
|
17
|
+
--nox-seasonal-border-size: 3px;
|
|
18
|
+
--nox-seasonal-web: #d1bfa9;
|
|
19
|
+
--nox-theme-accent-color: var(--nox-seasonal-blood);
|
|
20
|
+
}
|