@astronox/core 0.5.12 → 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 +135 -45
- 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 +44 -4
- 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 +14 -2
- 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,25 +4,55 @@ 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;
|
|
21
18
|
const seasonalKey = `${storageKey}:seasonal`;
|
|
22
19
|
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
let choices = ['light', 'dark'];
|
|
21
|
+
let activeEventId = null;
|
|
22
|
+
let activeTheme = null;
|
|
23
|
+
let boundaryTimeout;
|
|
24
|
+
|
|
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;
|
|
29
|
+
|
|
30
|
+
const spansNewYear =
|
|
31
|
+
period.start.month * 100 + period.start.day >
|
|
32
|
+
period.end.month * 100 + period.end.day;
|
|
33
|
+
const year = now.getFullYear();
|
|
34
|
+
for (const startYear of [year - 1, year, year + 1]) {
|
|
35
|
+
const start = new Date(
|
|
36
|
+
startYear,
|
|
37
|
+
period.start.month - 1,
|
|
38
|
+
period.start.day,
|
|
39
|
+
);
|
|
40
|
+
const end = new Date(
|
|
41
|
+
startYear + Number(spansNewYear),
|
|
42
|
+
period.end.month - 1,
|
|
43
|
+
period.end.day,
|
|
44
|
+
);
|
|
45
|
+
if (now < start) return { id: null, boundary: start.getTime() };
|
|
46
|
+
if (now < end) {
|
|
47
|
+
return {
|
|
48
|
+
id: `${season.id}-${startYear}`,
|
|
49
|
+
boundary: end.getTime(),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { id: null, boundary: null };
|
|
54
|
+
};
|
|
55
|
+
|
|
26
56
|
const read = (key) => {
|
|
27
57
|
try {
|
|
28
58
|
return window.localStorage.getItem(key);
|
|
@@ -42,11 +72,16 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
42
72
|
return value === 'light' || value === 'dark' ? value : null;
|
|
43
73
|
};
|
|
44
74
|
const readSeasonal = () => {
|
|
45
|
-
if (!
|
|
75
|
+
if (!activeEventId) return null;
|
|
46
76
|
try {
|
|
47
|
-
|
|
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
|
+
);
|
|
48
83
|
if (
|
|
49
|
-
value?.id ===
|
|
84
|
+
value?.id === activeEventId &&
|
|
50
85
|
(value.choice === 'seasonal' || value.choice === 'classic')
|
|
51
86
|
) {
|
|
52
87
|
return value.choice;
|
|
@@ -57,44 +92,102 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
57
92
|
return null;
|
|
58
93
|
};
|
|
59
94
|
const saveSeasonal = (choice) => {
|
|
60
|
-
if (!
|
|
61
|
-
write(
|
|
95
|
+
if (!activeEventId) return;
|
|
96
|
+
write(
|
|
97
|
+
`${seasonalKey}:${activeEventId}`,
|
|
98
|
+
JSON.stringify({ id: activeEventId, choice }),
|
|
99
|
+
);
|
|
62
100
|
};
|
|
63
101
|
|
|
64
102
|
let classic = readClassic();
|
|
65
|
-
let seasonal =
|
|
103
|
+
let seasonal = null;
|
|
66
104
|
let current;
|
|
67
|
-
if (seasonalTheme && seasonal === null) {
|
|
68
|
-
seasonal = 'seasonal';
|
|
69
|
-
saveSeasonal(seasonal);
|
|
70
|
-
}
|
|
71
105
|
|
|
72
106
|
const handleVisibility = () => {
|
|
73
107
|
root.toggleAttribute('data-theme-animation-paused', document.hidden);
|
|
108
|
+
if (!document.hidden) reconcile();
|
|
74
109
|
};
|
|
75
110
|
const next = () => choices[(choices.indexOf(current) + 1) % choices.length];
|
|
76
111
|
const resolve = () =>
|
|
77
|
-
|
|
78
|
-
?
|
|
112
|
+
activeEventId && seasonal === 'seasonal'
|
|
113
|
+
? activeTheme.appearance
|
|
79
114
|
: (classic ?? (media?.matches ? 'dark' : 'light'));
|
|
80
115
|
const apply = () => {
|
|
81
116
|
const appearance = resolve();
|
|
82
|
-
|
|
117
|
+
const appearanceChanged = current !== appearance;
|
|
83
118
|
const theme = appearance === 'light' ? 'light' : 'dark';
|
|
84
|
-
|
|
119
|
+
const decorations = activeEventId
|
|
120
|
+
? (activeTheme.decorations ?? 'none')
|
|
121
|
+
: 'none';
|
|
122
|
+
if (appearanceChanged) root.dataset.themeSwitching = 'true';
|
|
85
123
|
root.dataset.theme = theme;
|
|
86
124
|
root.dataset.appearance = appearance;
|
|
87
125
|
current = appearance;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
126
|
+
const nextTheme = next();
|
|
127
|
+
const controlsChanged =
|
|
128
|
+
root.dataset.nextTheme !== nextTheme ||
|
|
129
|
+
root.dataset.themeDecorations !== decorations;
|
|
130
|
+
root.dataset.nextTheme = nextTheme;
|
|
131
|
+
root.dataset.themeDecorations = decorations;
|
|
132
|
+
root.toggleAttribute(
|
|
133
|
+
'data-theme-animation-paused',
|
|
134
|
+
document.hidden && decorations === 'animated',
|
|
95
135
|
);
|
|
136
|
+
if (appearanceChanged) {
|
|
137
|
+
void root.offsetWidth;
|
|
138
|
+
delete root.dataset.themeSwitching;
|
|
139
|
+
}
|
|
140
|
+
if (appearanceChanged || controlsChanged) {
|
|
141
|
+
document.dispatchEvent(
|
|
142
|
+
new CustomEvent('astronox:theme-change', {
|
|
143
|
+
detail: { theme, appearance },
|
|
144
|
+
}),
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const reconcile = () => {
|
|
149
|
+
const now = new Date();
|
|
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;
|
|
161
|
+
seasonal = readSeasonal();
|
|
162
|
+
}
|
|
163
|
+
if (activeEventId && seasonal === null) {
|
|
164
|
+
seasonal = 'seasonal';
|
|
165
|
+
saveSeasonal(seasonal);
|
|
166
|
+
}
|
|
167
|
+
choices = activeEventId
|
|
168
|
+
? ['light', 'dark', activeTheme.appearance]
|
|
169
|
+
: ['light', 'dark'];
|
|
170
|
+
clearTimeout(boundaryTimeout);
|
|
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
|
+
);
|
|
187
|
+
apply();
|
|
96
188
|
};
|
|
97
189
|
const cycle = () => {
|
|
190
|
+
reconcile();
|
|
98
191
|
const appearance = next();
|
|
99
192
|
if (appearance === 'light' || appearance === 'dark') {
|
|
100
193
|
classic = appearance;
|
|
@@ -106,42 +199,39 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
106
199
|
saveSeasonal(seasonal);
|
|
107
200
|
apply();
|
|
108
201
|
};
|
|
109
|
-
const handleSystem = () => apply();
|
|
110
202
|
const handleStorage = (event) => {
|
|
111
203
|
if (
|
|
112
204
|
event.key !== null &&
|
|
113
205
|
event.key !== storageKey &&
|
|
114
|
-
event.key !== seasonalKey
|
|
206
|
+
event.key !== seasonalKey &&
|
|
207
|
+
!event.key.startsWith(`${seasonalKey}:`)
|
|
115
208
|
) {
|
|
116
209
|
return;
|
|
117
210
|
}
|
|
118
211
|
classic = readClassic();
|
|
119
212
|
seasonal = readSeasonal();
|
|
120
|
-
|
|
121
|
-
seasonal = 'seasonal';
|
|
122
|
-
saveSeasonal(seasonal);
|
|
123
|
-
}
|
|
124
|
-
apply();
|
|
213
|
+
reconcile();
|
|
125
214
|
};
|
|
126
215
|
|
|
127
216
|
root.dataset.themeStorageKey = storageKey;
|
|
128
|
-
root.dataset.themeDecorations = seasonalTheme?.decorations ?? 'none';
|
|
129
217
|
window.__astronoxTheme = {
|
|
130
218
|
next,
|
|
131
219
|
cycle,
|
|
132
220
|
destroy: () => {
|
|
133
|
-
|
|
221
|
+
clearTimeout(boundaryTimeout);
|
|
222
|
+
media?.removeEventListener('change', reconcile);
|
|
134
223
|
document.removeEventListener('visibilitychange', handleVisibility);
|
|
135
224
|
delete root.dataset.themeAnimationPaused;
|
|
136
225
|
window.removeEventListener('storage', handleStorage);
|
|
226
|
+
window.removeEventListener('focus', reconcile);
|
|
227
|
+
window.removeEventListener('pageshow', reconcile);
|
|
137
228
|
},
|
|
138
229
|
};
|
|
139
|
-
media?.addEventListener('change',
|
|
140
|
-
|
|
141
|
-
document.addEventListener('visibilitychange', handleVisibility);
|
|
142
|
-
handleVisibility();
|
|
143
|
-
}
|
|
230
|
+
media?.addEventListener('change', reconcile);
|
|
231
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
144
232
|
window.addEventListener('storage', handleStorage);
|
|
145
|
-
|
|
233
|
+
window.addEventListener('focus', reconcile);
|
|
234
|
+
window.addEventListener('pageshow', reconcile);
|
|
235
|
+
reconcile();
|
|
146
236
|
})();
|
|
147
237
|
</script>
|
|
@@ -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>
|