@astronox/core 0.5.13 → 0.5.14
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 +4 -1
- package/src/components/actions/ThemeToggle/themeToggleController.ts +1 -0
- package/src/components/content/CardLink/CardLink.astro +2 -0
- package/src/components/content/CardLink/CardLink.css +50 -5
- package/src/components/feedback/SeasonalDecorations/SeasonalDecorations.astro +46 -1
- package/src/components/feedback/SeasonalDecorations/SeasonalDecorations.css +51 -1
- package/src/components/feedback/SeasonalDecorations/snowfall.ts +38 -0
- package/src/components/feedback/ThemeBootstrap/ThemeBootstrap.astro +51 -26
- package/src/components/icons/CurrencyIcon.css +2 -2
- package/src/components/icons/FirTreeIcon.astro +32 -0
- package/src/components/icons/GiftIcon.astro +34 -0
- package/src/components/index.ts +2 -0
- package/src/components/navigation/Header.css +22 -3
- package/src/components/navigation/NavigationBrand.astro +38 -19
- package/src/components/navigation/NavigationBrand.css +12 -2
- package/src/config/siteConfig.ts +24 -8
- package/src/display/contracts.ts +2 -1
- package/src/layouts/BaseLayout.astro +6 -2
- package/src/patterns/Modal/modalController.ts +16 -0
- package/src/patterns/RewardCalendar/RewardCalendar.astro +174 -0
- package/src/patterns/RewardCalendar/RewardCalendar.css +406 -0
- package/src/patterns/RewardCalendar/RewardPreview.astro +18 -0
- package/src/patterns/RewardCalendar/contracts.ts +55 -0
- package/src/patterns/RewardCalendar/rewardCalendarController.ts +268 -0
- package/src/patterns/index.ts +3 -0
- package/src/styles/base.css +6 -3
- package/src/styles/style.css +2 -0
- package/src/styles/tokens.css +6 -0
- package/src/theme/christmas-garland.svg +28 -0
- package/src/theme/christmas.css +25 -0
- package/src/theme/contracts.ts +7 -3
- package/src/theme/halloween.css +1 -0
package/package.json
CHANGED
|
@@ -3,12 +3,14 @@ import type { HTMLAttributes } from 'astro/types';
|
|
|
3
3
|
import DayIcon from '../../icons/DayIcon.astro';
|
|
4
4
|
import NightIcon from '../../icons/NightIcon.astro';
|
|
5
5
|
import PumpkinIcon from '../../icons/PumpkinIcon.astro';
|
|
6
|
+
import FirTreeIcon from '../../icons/FirTreeIcon.astro';
|
|
6
7
|
|
|
7
8
|
type Props = HTMLAttributes<'button'> & {
|
|
8
9
|
appearance?: 'circle' | 'utility';
|
|
9
10
|
darkLabel: string;
|
|
10
11
|
lightLabel: string;
|
|
11
12
|
halloweenLabel?: string;
|
|
13
|
+
christmasLabel?: string;
|
|
12
14
|
};
|
|
13
15
|
|
|
14
16
|
const {
|
|
@@ -17,6 +19,7 @@ const {
|
|
|
17
19
|
darkLabel,
|
|
18
20
|
lightLabel,
|
|
19
21
|
halloweenLabel,
|
|
22
|
+
christmasLabel,
|
|
20
23
|
type = 'button',
|
|
21
24
|
...attrs
|
|
22
25
|
}: Props = Astro.props;
|
|
@@ -33,6 +36,7 @@ const {
|
|
|
33
36
|
data-theme-toggle-dark-label={darkLabel}
|
|
34
37
|
data-theme-toggle-light-label={lightLabel}
|
|
35
38
|
data-theme-toggle-halloween-label={halloweenLabel}
|
|
39
|
+
data-theme-toggle-christmas-label={christmasLabel}
|
|
36
40
|
data-navigation-control={appearance === 'circle' ? '' : undefined}
|
|
37
41
|
data-navigation-control-appearance={appearance === 'circle'
|
|
38
42
|
? 'bar'
|
|
@@ -59,6 +63,13 @@ const {
|
|
|
59
63
|
>
|
|
60
64
|
<PumpkinIcon />
|
|
61
65
|
</span>
|
|
66
|
+
<span
|
|
67
|
+
class="theme-toggle__icon"
|
|
68
|
+
data-theme-toggle-target="christmas"
|
|
69
|
+
aria-hidden="true"
|
|
70
|
+
>
|
|
71
|
+
<FirTreeIcon />
|
|
72
|
+
</span>
|
|
62
73
|
</button>
|
|
63
74
|
|
|
64
75
|
<script>
|
|
@@ -58,7 +58,10 @@
|
|
|
58
58
|
:root[data-next-theme='light'] .theme-toggle [data-theme-toggle-target='light'],
|
|
59
59
|
:root[data-next-theme='halloween']
|
|
60
60
|
.theme-toggle
|
|
61
|
-
[data-theme-toggle-target='halloween']
|
|
61
|
+
[data-theme-toggle-target='halloween'],
|
|
62
|
+
:root[data-next-theme='christmas']
|
|
63
|
+
.theme-toggle
|
|
64
|
+
[data-theme-toggle-target='christmas'] {
|
|
62
65
|
display: grid;
|
|
63
66
|
}
|
|
64
67
|
|
|
@@ -13,6 +13,7 @@ export function initThemeToggle(toggle: HTMLElement): () => void {
|
|
|
13
13
|
light: toggle.dataset.themeToggleLightLabel,
|
|
14
14
|
dark: toggle.dataset.themeToggleDarkLabel,
|
|
15
15
|
halloween: toggle.dataset.themeToggleHalloweenLabel,
|
|
16
|
+
christmas: toggle.dataset.themeToggleChristmasLabel,
|
|
16
17
|
};
|
|
17
18
|
const label = labels[runtime.next()];
|
|
18
19
|
if (!label) return;
|
|
@@ -31,6 +31,7 @@ const {
|
|
|
31
31
|
media,
|
|
32
32
|
mediaPosition = 'start',
|
|
33
33
|
mediaSize = 'default',
|
|
34
|
+
mediaTreatment = 'panel',
|
|
34
35
|
meta,
|
|
35
36
|
mode = 'link',
|
|
36
37
|
textLayout = 'inline',
|
|
@@ -79,6 +80,7 @@ const resolvedTone = tone ?? dataTone;
|
|
|
79
80
|
data-has-visual={hasVisual ? 'true' : 'false'}
|
|
80
81
|
data-media-position={hasVisual ? mediaPosition : undefined}
|
|
81
82
|
data-media-size={hasVisual ? mediaSize : undefined}
|
|
83
|
+
data-media-treatment={hasVisual ? mediaTreatment : undefined}
|
|
82
84
|
data-mode={mode}
|
|
83
85
|
data-text-treatment={textTreatment}
|
|
84
86
|
data-tone={resolvedTone}
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
--card-link-border-color: var(--surface-border-color);
|
|
11
11
|
--card-link-hover-background: var(--nox-surface-strong);
|
|
12
12
|
--card-link-active-background: var(--nox-border-muted);
|
|
13
|
+
--card-link-padding-block: var(--nox-space-2);
|
|
14
|
+
--card-link-padding-inline: var(--nox-space-4);
|
|
13
15
|
|
|
14
16
|
container-type: inline-size;
|
|
15
17
|
display: block;
|
|
@@ -94,7 +96,7 @@
|
|
|
94
96
|
grid-template-rows: minmax(0, 1fr);
|
|
95
97
|
align-items: center;
|
|
96
98
|
box-sizing: border-box;
|
|
97
|
-
padding: var(--
|
|
99
|
+
padding: var(--card-link-padding-block) var(--card-link-padding-inline);
|
|
98
100
|
overflow-wrap: anywhere;
|
|
99
101
|
}
|
|
100
102
|
|
|
@@ -154,6 +156,11 @@
|
|
|
154
156
|
);
|
|
155
157
|
}
|
|
156
158
|
|
|
159
|
+
.card-link[data-media-size='large'] .card-link__layout {
|
|
160
|
+
--card-link-size: clamp(5rem, 8vw, 6rem);
|
|
161
|
+
--card-link-media-size: clamp(3rem, 5vw, 4rem);
|
|
162
|
+
}
|
|
163
|
+
|
|
157
164
|
.card-link:is([data-media-size='compact'], [data-media-size='small'])
|
|
158
165
|
.card-link__layout {
|
|
159
166
|
--card-link-artwork-max-size: var(--card-link-resolved-media-size);
|
|
@@ -197,14 +204,51 @@
|
|
|
197
204
|
--card-link-size: calc(var(--nox-navigation-block-size) + var(--nox-space-5));
|
|
198
205
|
}
|
|
199
206
|
|
|
200
|
-
.card-link[data-density='relaxed']
|
|
201
|
-
padding: var(--nox-space-5)
|
|
207
|
+
.card-link[data-density='relaxed'] {
|
|
208
|
+
--card-link-padding-block: var(--nox-space-5);
|
|
209
|
+
--card-link-padding-inline: var(--nox-space-6);
|
|
202
210
|
}
|
|
203
211
|
|
|
204
212
|
.card-link[data-density='relaxed'] .card-link__text {
|
|
205
213
|
gap: var(--nox-space-3);
|
|
206
214
|
}
|
|
207
215
|
|
|
216
|
+
.card-link[data-media-treatment='inline'] .card-link__layout {
|
|
217
|
+
align-items: center;
|
|
218
|
+
column-gap: var(--nox-space-4);
|
|
219
|
+
padding: var(--card-link-padding-block) var(--card-link-padding-inline);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.card-link[data-media-treatment='inline'][data-media-position='start']
|
|
223
|
+
.card-link__layout {
|
|
224
|
+
grid-template-columns: var(--card-link-media-size) minmax(0, 1fr);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.card-link[data-media-treatment='inline'][data-media-position='end']
|
|
228
|
+
.card-link__layout {
|
|
229
|
+
grid-template-columns: minmax(0, 1fr) var(--card-link-media-size);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
.card-link[data-media-treatment='inline'] .card-link__media {
|
|
233
|
+
min-height: 0;
|
|
234
|
+
border-inline: 0;
|
|
235
|
+
background: transparent;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.card-link[data-media-treatment='inline']
|
|
239
|
+
.card-link__media
|
|
240
|
+
[data-widget='media'] {
|
|
241
|
+
inline-size: var(--card-link-media-size);
|
|
242
|
+
block-size: var(--card-link-media-size);
|
|
243
|
+
--media-object-max-inline-size: 100%;
|
|
244
|
+
--media-object-max-block-size: 100%;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.card-link[data-media-treatment='inline'] .card-link__content {
|
|
248
|
+
min-block-size: 0;
|
|
249
|
+
padding: 0;
|
|
250
|
+
}
|
|
251
|
+
|
|
208
252
|
.card-link__compact-text {
|
|
209
253
|
display: none;
|
|
210
254
|
min-width: 0;
|
|
@@ -304,8 +348,9 @@
|
|
|
304
348
|
);
|
|
305
349
|
}
|
|
306
350
|
|
|
307
|
-
.card-link[data-density='relaxed']
|
|
308
|
-
padding: var(--nox-space-4)
|
|
351
|
+
.card-link[data-density='relaxed'] {
|
|
352
|
+
--card-link-padding-block: var(--nox-space-4);
|
|
353
|
+
--card-link-padding-inline: var(--nox-space-5);
|
|
309
354
|
}
|
|
310
355
|
|
|
311
356
|
.card-link[data-compact-content] .card-link__layout {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
---
|
|
2
|
+
import { createSnowfallStyles } from './snowfall';
|
|
3
|
+
|
|
4
|
+
const snowflakes = createSnowfallStyles(() => 0.5);
|
|
2
5
|
const drips = [
|
|
3
6
|
{
|
|
4
7
|
x: 'calc(var(--nox-page-gutter) / 2)',
|
|
@@ -20,7 +23,12 @@ const drips = [
|
|
|
20
23
|
];
|
|
21
24
|
---
|
|
22
25
|
|
|
23
|
-
<div
|
|
26
|
+
<div
|
|
27
|
+
class="seasonal-decorations"
|
|
28
|
+
data-seasonal-decoration="halloween"
|
|
29
|
+
aria-hidden="true"
|
|
30
|
+
inert
|
|
31
|
+
>
|
|
24
32
|
{
|
|
25
33
|
drips.map(({ x, length, delay, edge }) => (
|
|
26
34
|
<div
|
|
@@ -58,3 +66,40 @@ const drips = [
|
|
|
58
66
|
))
|
|
59
67
|
}
|
|
60
68
|
</div>
|
|
69
|
+
|
|
70
|
+
<div
|
|
71
|
+
class="seasonal-decorations"
|
|
72
|
+
data-seasonal-decoration="christmas"
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
inert
|
|
75
|
+
>
|
|
76
|
+
{
|
|
77
|
+
snowflakes.map((style) => (
|
|
78
|
+
<span class="seasonal-decorations__snow" style={style} />
|
|
79
|
+
))
|
|
80
|
+
}
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
<script>
|
|
84
|
+
import { createSnowfallStyles } from './snowfall';
|
|
85
|
+
|
|
86
|
+
const initialized = new WeakSet<HTMLElement>();
|
|
87
|
+
|
|
88
|
+
const initializeSnowfall = (): void => {
|
|
89
|
+
for (const decoration of document.querySelectorAll<HTMLElement>(
|
|
90
|
+
'[data-seasonal-decoration="christmas"]',
|
|
91
|
+
)) {
|
|
92
|
+
if (initialized.has(decoration)) continue;
|
|
93
|
+
initialized.add(decoration);
|
|
94
|
+
const styles = createSnowfallStyles();
|
|
95
|
+
decoration
|
|
96
|
+
.querySelectorAll<HTMLElement>('.seasonal-decorations__snow')
|
|
97
|
+
.forEach((flake, index) => {
|
|
98
|
+
flake.style.cssText = styles[index];
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
initializeSnowfall();
|
|
104
|
+
document.addEventListener('astro:page-load', initializeSnowfall);
|
|
105
|
+
</script>
|
|
@@ -18,7 +18,12 @@
|
|
|
18
18
|
[data-theme-decorations='static'],
|
|
19
19
|
[data-theme-decorations='animated']
|
|
20
20
|
)
|
|
21
|
-
.seasonal-decorations
|
|
21
|
+
.seasonal-decorations[data-seasonal-decoration='halloween'],
|
|
22
|
+
:root[data-appearance='christmas']:is(
|
|
23
|
+
[data-theme-decorations='static'],
|
|
24
|
+
[data-theme-decorations='animated']
|
|
25
|
+
)
|
|
26
|
+
.seasonal-decorations[data-seasonal-decoration='christmas'] {
|
|
22
27
|
display: block;
|
|
23
28
|
}
|
|
24
29
|
|
|
@@ -132,3 +137,48 @@
|
|
|
132
137
|
transform: translateY(min(65vh, 30rem)) scale(0.75, 1.4);
|
|
133
138
|
}
|
|
134
139
|
}
|
|
140
|
+
|
|
141
|
+
.seasonal-decorations__snow {
|
|
142
|
+
position: absolute;
|
|
143
|
+
inset-inline-start: var(--snow-x);
|
|
144
|
+
inset-block-start: var(--snow-y);
|
|
145
|
+
inline-size: var(--snow-size);
|
|
146
|
+
block-size: var(--snow-size);
|
|
147
|
+
border-radius: 50%;
|
|
148
|
+
background: var(--nox-seasonal-snow);
|
|
149
|
+
opacity: 0.65;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
@media (width < 48rem) {
|
|
153
|
+
.seasonal-decorations__snow:nth-child(even) {
|
|
154
|
+
display: none;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
159
|
+
:root[data-appearance='christmas'][data-theme-decorations='animated']
|
|
160
|
+
.seasonal-decorations__snow {
|
|
161
|
+
inset-block-start: -1rem;
|
|
162
|
+
animation: seasonal-snow-fall var(--snow-duration) linear var(--snow-delay)
|
|
163
|
+
infinite;
|
|
164
|
+
animation-play-state: var(--seasonal-animation-state, running);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
@keyframes seasonal-snow-fall {
|
|
169
|
+
0% {
|
|
170
|
+
opacity: 0;
|
|
171
|
+
transform: translate(0, 0);
|
|
172
|
+
}
|
|
173
|
+
3%,
|
|
174
|
+
97% {
|
|
175
|
+
opacity: 0.8;
|
|
176
|
+
}
|
|
177
|
+
100% {
|
|
178
|
+
opacity: 0;
|
|
179
|
+
transform: translate(
|
|
180
|
+
var(--snow-drift),
|
|
181
|
+
calc(100vh - var(--seasonal-decoration-top) + 2rem)
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const columns = 8;
|
|
2
|
+
const flakesPerColumn = 8;
|
|
3
|
+
|
|
4
|
+
export function createSnowfallStyles(
|
|
5
|
+
random: () => number = Math.random,
|
|
6
|
+
): string[] {
|
|
7
|
+
const lanes = Array.from({ length: columns }, () => ({
|
|
8
|
+
duration: 8 + random() * 6,
|
|
9
|
+
offset: random(),
|
|
10
|
+
horizontalOffset: Math.floor(random() * (flakesPerColumn / 2)) * 2,
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
return Array.from({ length: columns * flakesPerColumn }, (_, index) => {
|
|
14
|
+
const pair = Math.floor(index / 2);
|
|
15
|
+
const column = pair % columns;
|
|
16
|
+
// Interleaving two layers keeps all columns covered when mobile hides one.
|
|
17
|
+
const row = Math.floor(pair / columns) * 2 + (index % 2);
|
|
18
|
+
const lane = lanes[column];
|
|
19
|
+
const phase =
|
|
20
|
+
(lane.offset + (row + 0.2 + random() * 0.6) / flakesPerColumn) % 1;
|
|
21
|
+
const slot = (row * 3 + lane.horizontalOffset) % flakesPerColumn;
|
|
22
|
+
const localX = (slot + 0.2 + random() * 0.6) / flakesPerColumn;
|
|
23
|
+
// Mirror alternating lanes so mobile also keeps flakes near both edges.
|
|
24
|
+
const x = ((column + (column % 2 ? 1 - localX : localX)) / columns) * 100;
|
|
25
|
+
const driftLimit = Math.min(10 / columns, x / 2, (100 - x) / 2);
|
|
26
|
+
const drift = (random() * 2 - 1) * driftLimit;
|
|
27
|
+
|
|
28
|
+
// Shared speed within each lane preserves the spacing across every loop.
|
|
29
|
+
return [
|
|
30
|
+
`--snow-x: ${x.toFixed(3)}%`,
|
|
31
|
+
`--snow-y: ${(phase * 100).toFixed(3)}%`,
|
|
32
|
+
`--snow-size: ${2 + Math.floor(random() * 4)}px`,
|
|
33
|
+
`--snow-duration: ${lane.duration.toFixed(3)}s`,
|
|
34
|
+
`--snow-delay: ${(-phase * lane.duration).toFixed(3)}s`,
|
|
35
|
+
`--snow-drift: ${drift.toFixed(3)}vw`,
|
|
36
|
+
].join('; ');
|
|
37
|
+
});
|
|
38
|
+
}
|
|
@@ -4,17 +4,14 @@ import type { SeasonalThemeConfig } from '../../../theme/contracts';
|
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
6
|
storageKey?: string;
|
|
7
|
-
|
|
7
|
+
seasonalThemes?: readonly SeasonalThemeConfig[];
|
|
8
8
|
};
|
|
9
9
|
|
|
10
|
-
const { storageKey = defaultThemeStorageKey,
|
|
10
|
+
const { storageKey = defaultThemeStorageKey, seasonalThemes = [] }: Props =
|
|
11
11
|
Astro.props;
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
<script
|
|
15
|
-
is:inline
|
|
16
|
-
define:vars={{ storageKey, seasonalTheme: seasonalTheme ?? null }}
|
|
17
|
-
>
|
|
14
|
+
<script is:inline define:vars={{ storageKey, seasonalThemes }}>
|
|
18
15
|
(() => {
|
|
19
16
|
window.__astronoxTheme?.destroy();
|
|
20
17
|
const root = document.documentElement;
|
|
@@ -22,12 +19,13 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
22
19
|
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
23
20
|
let choices = ['light', 'dark'];
|
|
24
21
|
let activeEventId = null;
|
|
22
|
+
let activeTheme = null;
|
|
25
23
|
let boundaryTimeout;
|
|
26
24
|
|
|
27
|
-
const resolveSeason = (now) => {
|
|
28
|
-
if (
|
|
29
|
-
|
|
30
|
-
|
|
25
|
+
const resolveSeason = (season, now) => {
|
|
26
|
+
if (season.mode === 'disabled') return { id: null, boundary: null };
|
|
27
|
+
if (season.mode === 'always') return { id: season.id, boundary: null };
|
|
28
|
+
const { period } = season;
|
|
31
29
|
|
|
32
30
|
const spansNewYear =
|
|
33
31
|
period.start.month * 100 + period.start.day >
|
|
@@ -47,7 +45,7 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
47
45
|
if (now < start) return { id: null, boundary: start.getTime() };
|
|
48
46
|
if (now < end) {
|
|
49
47
|
return {
|
|
50
|
-
id: `${
|
|
48
|
+
id: `${season.id}-${startYear}`,
|
|
51
49
|
boundary: end.getTime(),
|
|
52
50
|
};
|
|
53
51
|
}
|
|
@@ -76,7 +74,12 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
76
74
|
const readSeasonal = () => {
|
|
77
75
|
if (!activeEventId) return null;
|
|
78
76
|
try {
|
|
79
|
-
|
|
77
|
+
// Migrate the original single-season preference without resetting Halloween.
|
|
78
|
+
const value = JSON.parse(
|
|
79
|
+
read(`${seasonalKey}:${activeEventId}`) ??
|
|
80
|
+
read(seasonalKey) ??
|
|
81
|
+
'null',
|
|
82
|
+
);
|
|
80
83
|
if (
|
|
81
84
|
value?.id === activeEventId &&
|
|
82
85
|
(value.choice === 'seasonal' || value.choice === 'classic')
|
|
@@ -90,7 +93,10 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
90
93
|
};
|
|
91
94
|
const saveSeasonal = (choice) => {
|
|
92
95
|
if (!activeEventId) return;
|
|
93
|
-
write(
|
|
96
|
+
write(
|
|
97
|
+
`${seasonalKey}:${activeEventId}`,
|
|
98
|
+
JSON.stringify({ id: activeEventId, choice }),
|
|
99
|
+
);
|
|
94
100
|
};
|
|
95
101
|
|
|
96
102
|
let classic = readClassic();
|
|
@@ -104,14 +110,14 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
104
110
|
const next = () => choices[(choices.indexOf(current) + 1) % choices.length];
|
|
105
111
|
const resolve = () =>
|
|
106
112
|
activeEventId && seasonal === 'seasonal'
|
|
107
|
-
?
|
|
113
|
+
? activeTheme.appearance
|
|
108
114
|
: (classic ?? (media?.matches ? 'dark' : 'light'));
|
|
109
115
|
const apply = () => {
|
|
110
116
|
const appearance = resolve();
|
|
111
117
|
const appearanceChanged = current !== appearance;
|
|
112
118
|
const theme = appearance === 'light' ? 'light' : 'dark';
|
|
113
119
|
const decorations = activeEventId
|
|
114
|
-
? (
|
|
120
|
+
? (activeTheme.decorations ?? 'none')
|
|
115
121
|
: 'none';
|
|
116
122
|
if (appearanceChanged) root.dataset.themeSwitching = 'true';
|
|
117
123
|
root.dataset.theme = theme;
|
|
@@ -141,9 +147,17 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
141
147
|
};
|
|
142
148
|
const reconcile = () => {
|
|
143
149
|
const now = new Date();
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
150
|
+
const seasons = seasonalThemes.map((theme) => ({
|
|
151
|
+
...resolveSeason(theme, now),
|
|
152
|
+
theme,
|
|
153
|
+
}));
|
|
154
|
+
const season = seasons
|
|
155
|
+
.filter((candidate) => candidate.id !== null)
|
|
156
|
+
.sort((left, right) => right.theme.priority - left.theme.priority)[0];
|
|
157
|
+
const eventId = season?.id ?? null;
|
|
158
|
+
activeTheme = season?.theme ?? null;
|
|
159
|
+
if (activeEventId !== eventId) {
|
|
160
|
+
activeEventId = eventId;
|
|
147
161
|
seasonal = readSeasonal();
|
|
148
162
|
}
|
|
149
163
|
if (activeEventId && seasonal === null) {
|
|
@@ -151,15 +165,25 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
151
165
|
saveSeasonal(seasonal);
|
|
152
166
|
}
|
|
153
167
|
choices = activeEventId
|
|
154
|
-
? ['light', 'dark',
|
|
168
|
+
? ['light', 'dark', activeTheme.appearance]
|
|
155
169
|
: ['light', 'dark'];
|
|
156
170
|
clearTimeout(boundaryTimeout);
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
)
|
|
162
|
-
|
|
171
|
+
// A daily reconciliation also catches a timezone change on an open page.
|
|
172
|
+
const midnight = new Date(
|
|
173
|
+
now.getFullYear(),
|
|
174
|
+
now.getMonth(),
|
|
175
|
+
now.getDate() + 1,
|
|
176
|
+
);
|
|
177
|
+
const boundary = Math.min(
|
|
178
|
+
midnight.getTime(),
|
|
179
|
+
...seasons.flatMap((candidate) =>
|
|
180
|
+
candidate.boundary === null ? [] : [candidate.boundary],
|
|
181
|
+
),
|
|
182
|
+
);
|
|
183
|
+
boundaryTimeout = setTimeout(
|
|
184
|
+
reconcile,
|
|
185
|
+
Math.max(1, boundary - now.getTime()),
|
|
186
|
+
);
|
|
163
187
|
apply();
|
|
164
188
|
};
|
|
165
189
|
const cycle = () => {
|
|
@@ -179,7 +203,8 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
179
203
|
if (
|
|
180
204
|
event.key !== null &&
|
|
181
205
|
event.key !== storageKey &&
|
|
182
|
-
event.key !== seasonalKey
|
|
206
|
+
event.key !== seasonalKey &&
|
|
207
|
+
!event.key.startsWith(`${seasonalKey}:`)
|
|
183
208
|
) {
|
|
184
209
|
return;
|
|
185
210
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
.dollar-icon {
|
|
2
2
|
display: block;
|
|
3
|
-
color: var(--nox-source-tone-success);
|
|
3
|
+
color: var(--nox-currency-icon-color, var(--nox-source-tone-success));
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
.ribbon-icon {
|
|
7
7
|
display: block;
|
|
8
|
-
color: var(--nox-source-tone-warning);
|
|
8
|
+
color: var(--nox-currency-icon-color, var(--nox-source-tone-warning));
|
|
9
9
|
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
const {
|
|
6
|
+
width = 32,
|
|
7
|
+
height = 32,
|
|
8
|
+
'aria-hidden': ariaHidden = 'true',
|
|
9
|
+
focusable = 'false',
|
|
10
|
+
...attrs
|
|
11
|
+
}: Props = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<svg
|
|
15
|
+
{...attrs}
|
|
16
|
+
width={width}
|
|
17
|
+
height={height}
|
|
18
|
+
viewBox="0 0 32 32"
|
|
19
|
+
fill="none"
|
|
20
|
+
aria-hidden={ariaHidden}
|
|
21
|
+
focusable={focusable}
|
|
22
|
+
>
|
|
23
|
+
<g
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
stroke-width="1.5"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
>
|
|
29
|
+
<path d="M16 6L10 13H13L8 19H12L6.5 24H25.5L20 19H24L19 13H22Z"></path>
|
|
30
|
+
<path d="M16 24V27"></path>
|
|
31
|
+
</g>
|
|
32
|
+
</svg>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
---
|
|
2
|
+
import type { HTMLAttributes } from 'astro/types';
|
|
3
|
+
|
|
4
|
+
type Props = HTMLAttributes<'svg'>;
|
|
5
|
+
const {
|
|
6
|
+
width = 32,
|
|
7
|
+
height = 32,
|
|
8
|
+
'aria-hidden': ariaHidden = 'true',
|
|
9
|
+
focusable = 'false',
|
|
10
|
+
...attrs
|
|
11
|
+
}: Props = Astro.props;
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<svg
|
|
15
|
+
{...attrs}
|
|
16
|
+
width={width}
|
|
17
|
+
height={height}
|
|
18
|
+
viewBox="0 0 32 32"
|
|
19
|
+
fill="none"
|
|
20
|
+
aria-hidden={ariaHidden}
|
|
21
|
+
focusable={focusable}
|
|
22
|
+
>
|
|
23
|
+
<g
|
|
24
|
+
stroke="currentColor"
|
|
25
|
+
stroke-width="1.5"
|
|
26
|
+
stroke-linecap="round"
|
|
27
|
+
stroke-linejoin="round"
|
|
28
|
+
>
|
|
29
|
+
<path d="M5 12H27V17H5ZM7 17V27H25V17M16 12V27"></path>
|
|
30
|
+
<path
|
|
31
|
+
d="M16 12C10 12 7 10 9 7C11 4 16 8 16 12ZM16 12C22 12 25 10 23 7C21 4 16 8 16 12Z"
|
|
32
|
+
></path>
|
|
33
|
+
</g>
|
|
34
|
+
</svg>
|
package/src/components/index.ts
CHANGED
|
@@ -95,6 +95,8 @@ 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
97
|
export { default as PumpkinIcon } from './icons/PumpkinIcon.astro';
|
|
98
|
+
export { default as FirTreeIcon } from './icons/FirTreeIcon.astro';
|
|
99
|
+
export { default as GiftIcon } from './icons/GiftIcon.astro';
|
|
98
100
|
export { default as PlayIcon } from './icons/PlayIcon.astro';
|
|
99
101
|
export { default as PlusIcon } from './icons/PlusIcon.astro';
|
|
100
102
|
export { default as PoisonIcon } from './icons/PoisonIcon.astro';
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
position: sticky;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
:root[data-appearance='halloween']:is(
|
|
22
|
+
:root:is([data-appearance='halloween'], [data-appearance='christmas']):is(
|
|
23
23
|
[data-theme-decorations='static'],
|
|
24
24
|
[data-theme-decorations='animated']
|
|
25
25
|
)
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
--dropdown-attached-border-top-width: var(--site-header-menu-border-width);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
:root[data-appearance='halloween']:is(
|
|
33
|
+
:root:is([data-appearance='halloween'], [data-appearance='christmas']):is(
|
|
34
34
|
[data-theme-decorations='static'],
|
|
35
35
|
[data-theme-decorations='animated']
|
|
36
36
|
)
|
|
@@ -39,7 +39,26 @@
|
|
|
39
39
|
inset-inline: 0;
|
|
40
40
|
inset-block-start: 100%;
|
|
41
41
|
block-size: var(--nox-seasonal-border-size);
|
|
42
|
-
background: var(--nox-seasonal-
|
|
42
|
+
background: var(--nox-seasonal-header-border);
|
|
43
|
+
content: '';
|
|
44
|
+
pointer-events: none;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
:root[data-appearance='christmas']:is(
|
|
48
|
+
[data-theme-decorations='static'],
|
|
49
|
+
[data-theme-decorations='animated']
|
|
50
|
+
)
|
|
51
|
+
.site-header::before {
|
|
52
|
+
position: absolute;
|
|
53
|
+
/* Decorations stay behind every navigation menu in the header context. */
|
|
54
|
+
z-index: -1;
|
|
55
|
+
inset-inline: 0;
|
|
56
|
+
inset-block-start: calc(100% + 1px);
|
|
57
|
+
block-size: var(--nox-seasonal-header-ornament-size);
|
|
58
|
+
background-image: var(--nox-seasonal-header-ornament);
|
|
59
|
+
/* Repeat whole motifs so neither edge cuts through a hanging bulb. */
|
|
60
|
+
background-repeat: round no-repeat;
|
|
61
|
+
background-size: 144px 100%;
|
|
43
62
|
content: '';
|
|
44
63
|
pointer-events: none;
|
|
45
64
|
}
|