@astronox/core 0.5.12 → 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
CHANGED
|
@@ -20,9 +20,41 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
20
20
|
const root = document.documentElement;
|
|
21
21
|
const seasonalKey = `${storageKey}:seasonal`;
|
|
22
22
|
const media = window.matchMedia?.('(prefers-color-scheme: dark)');
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
let choices = ['light', 'dark'];
|
|
24
|
+
let activeEventId = null;
|
|
25
|
+
let boundaryTimeout;
|
|
26
|
+
|
|
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 };
|
|
31
|
+
|
|
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
|
+
|
|
26
58
|
const read = (key) => {
|
|
27
59
|
try {
|
|
28
60
|
return window.localStorage.getItem(key);
|
|
@@ -42,11 +74,11 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
42
74
|
return value === 'light' || value === 'dark' ? value : null;
|
|
43
75
|
};
|
|
44
76
|
const readSeasonal = () => {
|
|
45
|
-
if (!
|
|
77
|
+
if (!activeEventId) return null;
|
|
46
78
|
try {
|
|
47
79
|
const value = JSON.parse(read(seasonalKey) ?? 'null');
|
|
48
80
|
if (
|
|
49
|
-
value?.id ===
|
|
81
|
+
value?.id === activeEventId &&
|
|
50
82
|
(value.choice === 'seasonal' || value.choice === 'classic')
|
|
51
83
|
) {
|
|
52
84
|
return value.choice;
|
|
@@ -57,44 +89,81 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
57
89
|
return null;
|
|
58
90
|
};
|
|
59
91
|
const saveSeasonal = (choice) => {
|
|
60
|
-
if (!
|
|
61
|
-
write(seasonalKey, JSON.stringify({ id:
|
|
92
|
+
if (!activeEventId) return;
|
|
93
|
+
write(seasonalKey, JSON.stringify({ id: activeEventId, choice }));
|
|
62
94
|
};
|
|
63
95
|
|
|
64
96
|
let classic = readClassic();
|
|
65
|
-
let seasonal =
|
|
97
|
+
let seasonal = null;
|
|
66
98
|
let current;
|
|
67
|
-
if (seasonalTheme && seasonal === null) {
|
|
68
|
-
seasonal = 'seasonal';
|
|
69
|
-
saveSeasonal(seasonal);
|
|
70
|
-
}
|
|
71
99
|
|
|
72
100
|
const handleVisibility = () => {
|
|
73
101
|
root.toggleAttribute('data-theme-animation-paused', document.hidden);
|
|
102
|
+
if (!document.hidden) reconcile();
|
|
74
103
|
};
|
|
75
104
|
const next = () => choices[(choices.indexOf(current) + 1) % choices.length];
|
|
76
105
|
const resolve = () =>
|
|
77
|
-
|
|
106
|
+
activeEventId && seasonal === 'seasonal'
|
|
78
107
|
? seasonalTheme.appearance
|
|
79
108
|
: (classic ?? (media?.matches ? 'dark' : 'light'));
|
|
80
109
|
const apply = () => {
|
|
81
110
|
const appearance = resolve();
|
|
82
|
-
|
|
111
|
+
const appearanceChanged = current !== appearance;
|
|
83
112
|
const theme = appearance === 'light' ? 'light' : 'dark';
|
|
84
|
-
|
|
113
|
+
const decorations = activeEventId
|
|
114
|
+
? (seasonalTheme.decorations ?? 'none')
|
|
115
|
+
: 'none';
|
|
116
|
+
if (appearanceChanged) root.dataset.themeSwitching = 'true';
|
|
85
117
|
root.dataset.theme = theme;
|
|
86
118
|
root.dataset.appearance = appearance;
|
|
87
119
|
current = appearance;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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',
|
|
95
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();
|
|
96
164
|
};
|
|
97
165
|
const cycle = () => {
|
|
166
|
+
reconcile();
|
|
98
167
|
const appearance = next();
|
|
99
168
|
if (appearance === 'light' || appearance === 'dark') {
|
|
100
169
|
classic = appearance;
|
|
@@ -106,7 +175,6 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
106
175
|
saveSeasonal(seasonal);
|
|
107
176
|
apply();
|
|
108
177
|
};
|
|
109
|
-
const handleSystem = () => apply();
|
|
110
178
|
const handleStorage = (event) => {
|
|
111
179
|
if (
|
|
112
180
|
event.key !== null &&
|
|
@@ -117,31 +185,28 @@ const { storageKey = defaultThemeStorageKey, seasonalTheme }: Props =
|
|
|
117
185
|
}
|
|
118
186
|
classic = readClassic();
|
|
119
187
|
seasonal = readSeasonal();
|
|
120
|
-
|
|
121
|
-
seasonal = 'seasonal';
|
|
122
|
-
saveSeasonal(seasonal);
|
|
123
|
-
}
|
|
124
|
-
apply();
|
|
188
|
+
reconcile();
|
|
125
189
|
};
|
|
126
190
|
|
|
127
191
|
root.dataset.themeStorageKey = storageKey;
|
|
128
|
-
root.dataset.themeDecorations = seasonalTheme?.decorations ?? 'none';
|
|
129
192
|
window.__astronoxTheme = {
|
|
130
193
|
next,
|
|
131
194
|
cycle,
|
|
132
195
|
destroy: () => {
|
|
133
|
-
|
|
196
|
+
clearTimeout(boundaryTimeout);
|
|
197
|
+
media?.removeEventListener('change', reconcile);
|
|
134
198
|
document.removeEventListener('visibilitychange', handleVisibility);
|
|
135
199
|
delete root.dataset.themeAnimationPaused;
|
|
136
200
|
window.removeEventListener('storage', handleStorage);
|
|
201
|
+
window.removeEventListener('focus', reconcile);
|
|
202
|
+
window.removeEventListener('pageshow', reconcile);
|
|
137
203
|
},
|
|
138
204
|
};
|
|
139
|
-
media?.addEventListener('change',
|
|
140
|
-
|
|
141
|
-
document.addEventListener('visibilitychange', handleVisibility);
|
|
142
|
-
handleVisibility();
|
|
143
|
-
}
|
|
205
|
+
media?.addEventListener('change', reconcile);
|
|
206
|
+
document.addEventListener('visibilitychange', handleVisibility);
|
|
144
207
|
window.addEventListener('storage', handleStorage);
|
|
145
|
-
|
|
208
|
+
window.addEventListener('focus', reconcile);
|
|
209
|
+
window.addEventListener('pageshow', reconcile);
|
|
210
|
+
reconcile();
|
|
146
211
|
})();
|
|
147
212
|
</script>
|
package/src/config/siteConfig.ts
CHANGED
|
@@ -81,6 +81,30 @@ export function defineSiteConfig<const TConfig extends SiteConfig>(
|
|
|
81
81
|
throw new Error('A seasonal theme requires a non-empty event id.');
|
|
82
82
|
}
|
|
83
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
|
+
|
|
84
108
|
const localeIds = new Set<string>();
|
|
85
109
|
const urlCodes = new Set<string>();
|
|
86
110
|
|
package/src/theme/contracts.ts
CHANGED
|
@@ -7,8 +7,16 @@ export type SeasonalAppearance = 'halloween';
|
|
|
7
7
|
|
|
8
8
|
export type ThemeAppearance = ColorTheme | SeasonalAppearance;
|
|
9
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
|
+
|
|
10
17
|
export type SeasonalThemeConfig = {
|
|
11
18
|
id: string;
|
|
12
19
|
appearance: SeasonalAppearance;
|
|
13
20
|
decorations?: 'static' | 'animated';
|
|
21
|
+
annualPeriod?: AnnualThemePeriod;
|
|
14
22
|
};
|