@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.
Files changed (34) hide show
  1. package/package.json +1 -1
  2. package/src/components/actions/ThemeToggle/ThemeToggle.astro +11 -0
  3. package/src/components/actions/ThemeToggle/ThemeToggle.css +4 -1
  4. package/src/components/actions/ThemeToggle/themeToggleController.ts +1 -0
  5. package/src/components/content/CardLink/CardLink.astro +2 -0
  6. package/src/components/content/CardLink/CardLink.css +50 -5
  7. package/src/components/feedback/SeasonalDecorations/SeasonalDecorations.astro +46 -1
  8. package/src/components/feedback/SeasonalDecorations/SeasonalDecorations.css +51 -1
  9. package/src/components/feedback/SeasonalDecorations/snowfall.ts +38 -0
  10. package/src/components/feedback/ThemeBootstrap/ThemeBootstrap.astro +135 -45
  11. package/src/components/icons/CurrencyIcon.css +2 -2
  12. package/src/components/icons/FirTreeIcon.astro +32 -0
  13. package/src/components/icons/GiftIcon.astro +34 -0
  14. package/src/components/index.ts +2 -0
  15. package/src/components/navigation/Header.css +22 -3
  16. package/src/components/navigation/NavigationBrand.astro +38 -19
  17. package/src/components/navigation/NavigationBrand.css +12 -2
  18. package/src/config/siteConfig.ts +44 -4
  19. package/src/display/contracts.ts +2 -1
  20. package/src/layouts/BaseLayout.astro +6 -2
  21. package/src/patterns/Modal/modalController.ts +16 -0
  22. package/src/patterns/RewardCalendar/RewardCalendar.astro +174 -0
  23. package/src/patterns/RewardCalendar/RewardCalendar.css +406 -0
  24. package/src/patterns/RewardCalendar/RewardPreview.astro +18 -0
  25. package/src/patterns/RewardCalendar/contracts.ts +55 -0
  26. package/src/patterns/RewardCalendar/rewardCalendarController.ts +268 -0
  27. package/src/patterns/index.ts +3 -0
  28. package/src/styles/base.css +6 -3
  29. package/src/styles/style.css +2 -0
  30. package/src/styles/tokens.css +6 -0
  31. package/src/theme/christmas-garland.svg +28 -0
  32. package/src/theme/christmas.css +25 -0
  33. package/src/theme/contracts.ts +14 -2
  34. package/src/theme/halloween.css +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@astronox/core",
3
- "version": "0.5.12",
3
+ "version": "0.5.14",
4
4
  "description": "Reusable Astro components, patterns, styles, and infrastructure for AstroNox sites.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -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(--nox-space-2) var(--nox-space-4);
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'] .card-link__content {
201
- padding: var(--nox-space-5) var(--nox-space-6);
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'] .card-link__content {
308
- padding: var(--nox-space-4) var(--nox-space-5);
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 class="seasonal-decorations" aria-hidden="true" inert>
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
- seasonalTheme?: SeasonalThemeConfig;
7
+ seasonalThemes?: readonly SeasonalThemeConfig[];
8
8
  };
9
9
 
10
- const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
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
- const choices = seasonalTheme
24
- ? ['light', 'dark', seasonalTheme.appearance]
25
- : ['light', 'dark'];
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 (!seasonalTheme) return null;
75
+ if (!activeEventId) return null;
46
76
  try {
47
- const value = JSON.parse(read(seasonalKey) ?? 'null');
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 === seasonalTheme.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 (!seasonalTheme) return;
61
- write(seasonalKey, JSON.stringify({ id: seasonalTheme.id, choice }));
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 = readSeasonal();
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
- seasonalTheme && seasonal === 'seasonal'
78
- ? seasonalTheme.appearance
112
+ activeEventId && seasonal === 'seasonal'
113
+ ? activeTheme.appearance
79
114
  : (classic ?? (media?.matches ? 'dark' : 'light'));
80
115
  const apply = () => {
81
116
  const appearance = resolve();
82
- if (current === appearance) return;
117
+ const appearanceChanged = current !== appearance;
83
118
  const theme = appearance === 'light' ? 'light' : 'dark';
84
- root.dataset.themeSwitching = 'true';
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
- root.dataset.nextTheme = next();
89
- void root.offsetWidth;
90
- delete root.dataset.themeSwitching;
91
- document.dispatchEvent(
92
- new CustomEvent('astronox:theme-change', {
93
- detail: { theme, appearance },
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
- if (seasonalTheme && seasonal === null) {
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
- media?.removeEventListener('change', handleSystem);
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', handleSystem);
140
- if (seasonalTheme?.decorations === 'animated') {
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
- apply();
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>