@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
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { copyText } from '../../client/clipboard';
|
|
2
|
+
import { initModal, openModal } from '../Modal/modalController';
|
|
3
|
+
import {
|
|
4
|
+
setFieldError,
|
|
5
|
+
clearFieldError,
|
|
6
|
+
} from '../../components/forms/FieldError/fieldErrorState';
|
|
7
|
+
import type {
|
|
8
|
+
RewardCalendarAdapter,
|
|
9
|
+
RewardCalendarEntry,
|
|
10
|
+
RewardCalendarLabels,
|
|
11
|
+
} from './contracts';
|
|
12
|
+
|
|
13
|
+
const connections = new WeakMap<HTMLElement, () => void>();
|
|
14
|
+
type ClientEntry = Pick<RewardCalendarEntry, 'id' | 'label' | 'title'>;
|
|
15
|
+
|
|
16
|
+
export function connectRewardCalendar(
|
|
17
|
+
root: HTMLElement,
|
|
18
|
+
adapter: RewardCalendarAdapter,
|
|
19
|
+
): () => void {
|
|
20
|
+
connections.get(root)?.();
|
|
21
|
+
const element = <T extends HTMLElement>(selector: string): T => {
|
|
22
|
+
const found = root.querySelector<T>(selector);
|
|
23
|
+
if (!found) throw new Error(`Missing reward calendar element: ${selector}`);
|
|
24
|
+
return found;
|
|
25
|
+
};
|
|
26
|
+
const config = element<HTMLScriptElement>('[data-reward-calendar-config]');
|
|
27
|
+
const { entries, labels, openingDuration } = JSON.parse(
|
|
28
|
+
config.textContent ?? '{}',
|
|
29
|
+
) as {
|
|
30
|
+
entries: ClientEntry[];
|
|
31
|
+
labels: RewardCalendarLabels;
|
|
32
|
+
openingDuration: number;
|
|
33
|
+
};
|
|
34
|
+
const dialog = element<HTMLDialogElement>('[data-calendar-dialog]');
|
|
35
|
+
const form = element<HTMLFormElement>('[data-calendar-form]');
|
|
36
|
+
const input = element<HTMLInputElement>('[data-calendar-identity]');
|
|
37
|
+
const error = element('[data-calendar-error]');
|
|
38
|
+
const generate = element<HTMLButtonElement>('[data-calendar-generate]');
|
|
39
|
+
const status = element('[data-calendar-status]');
|
|
40
|
+
const previews = [
|
|
41
|
+
...root.querySelectorAll<HTMLElement>('[data-calendar-reward]'),
|
|
42
|
+
];
|
|
43
|
+
const result = element('[data-calendar-result]');
|
|
44
|
+
const code = element('[data-calendar-code]');
|
|
45
|
+
const copy = element<HTMLButtonElement>('[data-calendar-copy]');
|
|
46
|
+
const feedback = element('[data-calendar-feedback]');
|
|
47
|
+
const cleanupModal = initModal(dialog);
|
|
48
|
+
const buttons = [
|
|
49
|
+
...root.querySelectorAll<HTMLButtonElement>('[data-calendar-entry]'),
|
|
50
|
+
];
|
|
51
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
52
|
+
let activeId: string | null = null;
|
|
53
|
+
let operation = 0;
|
|
54
|
+
let busy = false;
|
|
55
|
+
let opening: HTMLButtonElement | null = null;
|
|
56
|
+
let revealTimer: ReturnType<typeof setTimeout> | undefined;
|
|
57
|
+
let identity = adapter.getSnapshot().identity;
|
|
58
|
+
input.value = identity;
|
|
59
|
+
|
|
60
|
+
const clearResult = (): void => {
|
|
61
|
+
result.hidden = true;
|
|
62
|
+
code.textContent = '';
|
|
63
|
+
feedback.textContent = '';
|
|
64
|
+
};
|
|
65
|
+
const cancelOpening = (): void => {
|
|
66
|
+
clearTimeout(revealTimer);
|
|
67
|
+
opening?.removeAttribute('data-opening');
|
|
68
|
+
opening = null;
|
|
69
|
+
root.removeAttribute('aria-busy');
|
|
70
|
+
};
|
|
71
|
+
const update = (): void => {
|
|
72
|
+
const snapshot = adapter.getSnapshot();
|
|
73
|
+
status.textContent = snapshot.message;
|
|
74
|
+
status.hidden = !snapshot.message;
|
|
75
|
+
if (identity !== snapshot.identity) {
|
|
76
|
+
identity = snapshot.identity;
|
|
77
|
+
if (input.value !== identity) input.value = identity;
|
|
78
|
+
operation += 1;
|
|
79
|
+
busy = false;
|
|
80
|
+
clearResult();
|
|
81
|
+
cancelOpening();
|
|
82
|
+
}
|
|
83
|
+
if (
|
|
84
|
+
opening &&
|
|
85
|
+
(!snapshot.entries[opening.dataset.calendarEntry ?? ''] ||
|
|
86
|
+
snapshot.entries[opening.dataset.calendarEntry ?? ''] === 'locked')
|
|
87
|
+
) {
|
|
88
|
+
cancelOpening();
|
|
89
|
+
}
|
|
90
|
+
for (const button of buttons) {
|
|
91
|
+
const id = button.dataset.calendarEntry ?? '';
|
|
92
|
+
const entry = byId.get(id);
|
|
93
|
+
if (!entry) continue;
|
|
94
|
+
const state = snapshot.entries[id] ?? 'locked';
|
|
95
|
+
button.dataset.state = state;
|
|
96
|
+
button.disabled = state === 'locked';
|
|
97
|
+
button.setAttribute(
|
|
98
|
+
'aria-disabled',
|
|
99
|
+
String(state === 'locked' || opening !== null),
|
|
100
|
+
);
|
|
101
|
+
const rewardName =
|
|
102
|
+
state === 'revealed' || state === 'generated'
|
|
103
|
+
? ` — ${entry.title}`
|
|
104
|
+
: '';
|
|
105
|
+
button.setAttribute(
|
|
106
|
+
'aria-label',
|
|
107
|
+
`${entry.label} — ${labels.states[state]}${rewardName}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
const locked =
|
|
111
|
+
activeId !== null &&
|
|
112
|
+
(!snapshot.entries[activeId] || snapshot.entries[activeId] === 'locked');
|
|
113
|
+
if (locked && dialog.open) {
|
|
114
|
+
operation += 1;
|
|
115
|
+
busy = false;
|
|
116
|
+
clearResult();
|
|
117
|
+
feedback.textContent = labels.unavailable;
|
|
118
|
+
}
|
|
119
|
+
generate.disabled = busy || opening !== null || locked;
|
|
120
|
+
generate.textContent = busy ? labels.generating : labels.generate;
|
|
121
|
+
form.setAttribute('aria-busy', String(busy));
|
|
122
|
+
};
|
|
123
|
+
const showReward = (button: HTMLButtonElement, entry: ClientEntry): void => {
|
|
124
|
+
cancelOpening();
|
|
125
|
+
// Availability is checked again after the wrapping animation, including midnight.
|
|
126
|
+
if (!adapter.reveal(entry.id)) {
|
|
127
|
+
update();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
operation += 1;
|
|
131
|
+
busy = false;
|
|
132
|
+
activeId = entry.id;
|
|
133
|
+
for (const preview of previews) {
|
|
134
|
+
preview.hidden = preview.dataset.calendarReward !== entry.id;
|
|
135
|
+
}
|
|
136
|
+
dialog.setAttribute('aria-label', `${entry.label} — ${entry.title}`);
|
|
137
|
+
clearFieldError(input, error);
|
|
138
|
+
clearResult();
|
|
139
|
+
update();
|
|
140
|
+
openModal(dialog, button);
|
|
141
|
+
};
|
|
142
|
+
const handleClick = (event: MouseEvent): void => {
|
|
143
|
+
const button =
|
|
144
|
+
event.target instanceof Element
|
|
145
|
+
? event.target.closest<HTMLButtonElement>('[data-calendar-entry]')
|
|
146
|
+
: null;
|
|
147
|
+
const entry = byId.get(button?.dataset.calendarEntry ?? '');
|
|
148
|
+
if (!button || !entry || !root.contains(button) || opening) return;
|
|
149
|
+
const state = adapter.getSnapshot().entries[entry.id];
|
|
150
|
+
if (!state || state === 'locked') return;
|
|
151
|
+
if (
|
|
152
|
+
state !== 'available' ||
|
|
153
|
+
window.matchMedia('(prefers-reduced-motion: reduce)').matches
|
|
154
|
+
) {
|
|
155
|
+
showReward(button, entry);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
opening = button;
|
|
159
|
+
button.setAttribute('data-opening', '');
|
|
160
|
+
root.setAttribute('aria-busy', 'true');
|
|
161
|
+
revealTimer = setTimeout(() => showReward(button, entry), openingDuration);
|
|
162
|
+
update();
|
|
163
|
+
};
|
|
164
|
+
const handleInput = (): void => {
|
|
165
|
+
operation += 1;
|
|
166
|
+
busy = false;
|
|
167
|
+
clearFieldError(input, error);
|
|
168
|
+
clearResult();
|
|
169
|
+
adapter.setIdentity(input.value);
|
|
170
|
+
// The gift already opened in this dialog stays revealed for the new identity.
|
|
171
|
+
if (activeId && adapter.getSnapshot().entries[activeId] === 'available') {
|
|
172
|
+
adapter.reveal(activeId);
|
|
173
|
+
}
|
|
174
|
+
update();
|
|
175
|
+
};
|
|
176
|
+
const handleSubmit = async (event: SubmitEvent): Promise<void> => {
|
|
177
|
+
event.preventDefault();
|
|
178
|
+
if (!activeId || busy || opening) return;
|
|
179
|
+
adapter.setIdentity(input.value);
|
|
180
|
+
const id = activeId;
|
|
181
|
+
const currentOperation = ++operation;
|
|
182
|
+
clearFieldError(input, error);
|
|
183
|
+
clearResult();
|
|
184
|
+
busy = true;
|
|
185
|
+
update();
|
|
186
|
+
try {
|
|
187
|
+
const response = await adapter.generate(id);
|
|
188
|
+
if (operation !== currentOperation || !dialog.open) return;
|
|
189
|
+
if (!response.ok) {
|
|
190
|
+
if (response.reason === 'invalidIdentity') {
|
|
191
|
+
setFieldError(input, error, labels.invalidIdentity);
|
|
192
|
+
input.focus();
|
|
193
|
+
} else {
|
|
194
|
+
feedback.textContent = labels[response.reason];
|
|
195
|
+
}
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
code.textContent = response.code;
|
|
199
|
+
result.hidden = false;
|
|
200
|
+
} catch {
|
|
201
|
+
if (operation === currentOperation) {
|
|
202
|
+
feedback.textContent = labels.failed;
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
if (operation === currentOperation) {
|
|
206
|
+
busy = false;
|
|
207
|
+
update();
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const handleCopy = async (): Promise<void> => {
|
|
212
|
+
if (!code.textContent) return;
|
|
213
|
+
const currentOperation = operation;
|
|
214
|
+
try {
|
|
215
|
+
await copyText(code.textContent);
|
|
216
|
+
if (operation === currentOperation) {
|
|
217
|
+
feedback.textContent = labels.copied;
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
if (operation === currentOperation) {
|
|
221
|
+
feedback.textContent = labels.copyFailed;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
const handleClose = (): void => {
|
|
226
|
+
activeId = null;
|
|
227
|
+
operation += 1;
|
|
228
|
+
busy = false;
|
|
229
|
+
cancelOpening();
|
|
230
|
+
clearResult();
|
|
231
|
+
update();
|
|
232
|
+
};
|
|
233
|
+
const handleCancelOpening = (event: Event): void => {
|
|
234
|
+
if (!opening) return;
|
|
235
|
+
if (event instanceof KeyboardEvent && event.key !== 'Escape') return;
|
|
236
|
+
if (event.type === 'visibilitychange' && !document.hidden) return;
|
|
237
|
+
event.preventDefault();
|
|
238
|
+
cancelOpening();
|
|
239
|
+
update();
|
|
240
|
+
};
|
|
241
|
+
root.addEventListener('click', handleClick);
|
|
242
|
+
input.addEventListener('input', handleInput);
|
|
243
|
+
form.addEventListener('submit', handleSubmit);
|
|
244
|
+
copy.addEventListener('click', handleCopy);
|
|
245
|
+
dialog.addEventListener('close', handleClose);
|
|
246
|
+
document.addEventListener('keydown', handleCancelOpening);
|
|
247
|
+
document.addEventListener('visibilitychange', handleCancelOpening);
|
|
248
|
+
window.addEventListener('pagehide', handleCancelOpening);
|
|
249
|
+
const unsubscribe = adapter.subscribe(update);
|
|
250
|
+
update();
|
|
251
|
+
const cleanup = (): void => {
|
|
252
|
+
operation += 1;
|
|
253
|
+
cancelOpening();
|
|
254
|
+
unsubscribe();
|
|
255
|
+
root.removeEventListener('click', handleClick);
|
|
256
|
+
input.removeEventListener('input', handleInput);
|
|
257
|
+
form.removeEventListener('submit', handleSubmit);
|
|
258
|
+
copy.removeEventListener('click', handleCopy);
|
|
259
|
+
dialog.removeEventListener('close', handleClose);
|
|
260
|
+
document.removeEventListener('keydown', handleCancelOpening);
|
|
261
|
+
document.removeEventListener('visibilitychange', handleCancelOpening);
|
|
262
|
+
window.removeEventListener('pagehide', handleCancelOpening);
|
|
263
|
+
cleanupModal();
|
|
264
|
+
connections.delete(root);
|
|
265
|
+
};
|
|
266
|
+
connections.set(root, cleanup);
|
|
267
|
+
return cleanup;
|
|
268
|
+
}
|
package/src/patterns/index.ts
CHANGED
|
@@ -10,6 +10,9 @@ export { default as Modal } from './Modal/Modal.astro';
|
|
|
10
10
|
export { default as ModalClose } from './Modal/ModalClose.astro';
|
|
11
11
|
export { default as ModalOpen } from './Modal/ModalOpen.astro';
|
|
12
12
|
export { default as PuzzleGrid } from './PuzzleGrid/PuzzleGrid.astro';
|
|
13
|
+
export { default as RewardCalendar } from './RewardCalendar/RewardCalendar.astro';
|
|
14
|
+
export * from './RewardCalendar/contracts';
|
|
15
|
+
export * from './RewardCalendar/rewardCalendarController';
|
|
13
16
|
export { default as SlidingPuzzle } from './SlidingPuzzle/SlidingPuzzle.astro';
|
|
14
17
|
export { default as LightMazePuzzle } from './LightMazePuzzle/LightMazePuzzle.astro';
|
|
15
18
|
export * from './LightMazePuzzle/contracts';
|
package/src/styles/base.css
CHANGED
|
@@ -167,15 +167,18 @@ p {
|
|
|
167
167
|
gap: 0;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
:root[data-appearance='halloween']
|
|
170
|
+
:root:is([data-appearance='halloween'], [data-appearance='christmas'])
|
|
171
|
+
.site-shell {
|
|
171
172
|
isolation: isolate;
|
|
172
173
|
}
|
|
173
174
|
|
|
174
|
-
:root[data-appearance='halloween']
|
|
175
|
+
:root:is([data-appearance='halloween'], [data-appearance='christmas'])
|
|
176
|
+
.site-viewport {
|
|
175
177
|
background: transparent;
|
|
176
178
|
}
|
|
177
179
|
|
|
178
|
-
:root[data-appearance='halloween']
|
|
180
|
+
:root:is([data-appearance='halloween'], [data-appearance='christmas'])
|
|
181
|
+
.site-main__content {
|
|
179
182
|
background: var(--nox-page-background);
|
|
180
183
|
border-radius: var(--nox-radius-lg);
|
|
181
184
|
}
|
package/src/styles/style.css
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
@import './tokens.css' layer(settings);
|
|
4
4
|
@import '../theme/halloween.css' layer(settings);
|
|
5
|
+
@import '../theme/christmas.css' layer(settings);
|
|
5
6
|
@import './base.css' layer(base);
|
|
6
7
|
@import './interaction.css' layer(states);
|
|
7
8
|
|
|
@@ -79,6 +80,7 @@
|
|
|
79
80
|
@import '../patterns/MagneticFieldPuzzle/MagneticFieldPuzzle.css'
|
|
80
81
|
layer(patterns);
|
|
81
82
|
@import '../patterns/Modal/Modal.css' layer(patterns);
|
|
83
|
+
@import '../patterns/RewardCalendar/RewardCalendar.css' layer(patterns);
|
|
82
84
|
@import '../patterns/SpriteProjectilePreview/SpriteProjectilePreview.css'
|
|
83
85
|
layer(patterns);
|
|
84
86
|
@import '../patterns/StageFlow/StageFlow.css' layer(patterns);
|
package/src/styles/tokens.css
CHANGED
|
@@ -27,6 +27,12 @@
|
|
|
27
27
|
--nox-source-magnetic-north: #2f6bff;
|
|
28
28
|
--nox-source-magnetic-south: #dc2626;
|
|
29
29
|
|
|
30
|
+
/* Gift wrapping stays stable across UI color themes. */
|
|
31
|
+
--nox-reward-green: #287247;
|
|
32
|
+
--nox-reward-red: #bd3946;
|
|
33
|
+
--nox-reward-gold: #e5bf72;
|
|
34
|
+
--nox-reward-ribbon: #f6fbff;
|
|
35
|
+
|
|
30
36
|
/* Internal recipe anchors. */
|
|
31
37
|
--nox-mix-light: #ffffff;
|
|
32
38
|
--nox-mix-dark: #020617;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="144" height="18" viewBox="0 3 144 18" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<radialGradient id="green">
|
|
4
|
+
<stop stop-color="#85e9a7" stop-opacity=".55"/>
|
|
5
|
+
<stop offset=".45" stop-color="#63d886" stop-opacity=".2"/>
|
|
6
|
+
<stop offset="1" stop-color="#63d886" stop-opacity="0"/>
|
|
7
|
+
</radialGradient>
|
|
8
|
+
<radialGradient id="red">
|
|
9
|
+
<stop stop-color="#ffaaa6" stop-opacity=".55"/>
|
|
10
|
+
<stop offset=".45" stop-color="#ee6574" stop-opacity=".2"/>
|
|
11
|
+
<stop offset="1" stop-color="#ee6574" stop-opacity="0"/>
|
|
12
|
+
</radialGradient>
|
|
13
|
+
<radialGradient id="gold">
|
|
14
|
+
<stop stop-color="#fff0b5" stop-opacity=".55"/>
|
|
15
|
+
<stop offset=".45" stop-color="#e5bf72" stop-opacity=".2"/>
|
|
16
|
+
<stop offset="1" stop-color="#e5bf72" stop-opacity="0"/>
|
|
17
|
+
</radialGradient>
|
|
18
|
+
</defs>
|
|
19
|
+
<circle cx="24" cy="11" r="10" fill="url(#green)"/>
|
|
20
|
+
<circle cx="72" cy="11" r="10" fill="url(#red)"/>
|
|
21
|
+
<circle cx="120" cy="11" r="10" fill="url(#gold)"/>
|
|
22
|
+
<path d="M24 3V6M72 3V6M120 3V6" stroke="#45664f" stroke-width="1.5"/>
|
|
23
|
+
<ellipse cx="24" cy="11" rx="3.2" ry="4.3" fill="#85e9a7" stroke="#378551" stroke-width=".6"/>
|
|
24
|
+
<ellipse cx="72" cy="11" rx="3.2" ry="4.3" fill="#ff9297" stroke="#b74755" stroke-width=".6"/>
|
|
25
|
+
<ellipse cx="120" cy="11" rx="3.2" ry="4.3" fill="#f4d78c" stroke="#ba893e" stroke-width=".6"/>
|
|
26
|
+
<path d="M22 5H26V7H22ZM70 5H74V7H70ZM118 5H122V7H118Z" fill="#45664f"/>
|
|
27
|
+
<path d="M23 9V10.5M71 9V10.5M119 9V10.5" stroke="#fff9e8" stroke-width="1.2" stroke-linecap="round" opacity=".9"/>
|
|
28
|
+
</svg>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
:root[data-appearance='christmas'] {
|
|
2
|
+
color-scheme: dark;
|
|
3
|
+
|
|
4
|
+
--nox-source-tone-neutral: #afbfb8;
|
|
5
|
+
--nox-source-tone-primary: var(--nox-seasonal-red);
|
|
6
|
+
--nox-source-tone-warning: var(--nox-seasonal-gold);
|
|
7
|
+
--nox-content-link-color: #ef9297;
|
|
8
|
+
--nox-theme-page-base: #0e1916;
|
|
9
|
+
--nox-theme-surface-base: #172822;
|
|
10
|
+
--nox-theme-text-base: #f4f0e7;
|
|
11
|
+
--nox-theme-text-disabled-base: #75867c;
|
|
12
|
+
--nox-theme-border-base: #30483d;
|
|
13
|
+
--nox-theme-inverse-base: #0e1916;
|
|
14
|
+
--nox-theme-shadow-base: #060e0b;
|
|
15
|
+
|
|
16
|
+
--nox-seasonal-header-background: #14231d;
|
|
17
|
+
--nox-seasonal-border-size: 3px;
|
|
18
|
+
--nox-seasonal-header-ornament-size: 18px;
|
|
19
|
+
--nox-seasonal-red: #c3464d;
|
|
20
|
+
--nox-seasonal-gold: #e5bf72;
|
|
21
|
+
--nox-seasonal-snow: #f6fbff;
|
|
22
|
+
--nox-theme-accent-color: var(--nox-seasonal-red);
|
|
23
|
+
--nox-seasonal-header-border: #3a7752;
|
|
24
|
+
--nox-seasonal-header-ornament: url('./christmas-garland.svg');
|
|
25
|
+
}
|
package/src/theme/contracts.ts
CHANGED
|
@@ -3,12 +3,24 @@ export type Tone =
|
|
|
3
3
|
|
|
4
4
|
export type ColorTheme = 'light' | 'dark';
|
|
5
5
|
|
|
6
|
-
export type SeasonalAppearance = 'halloween';
|
|
6
|
+
export type SeasonalAppearance = 'halloween' | 'christmas';
|
|
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;
|
|
20
|
+
/** Higher priorities win when enabled seasons overlap. */
|
|
21
|
+
priority: number;
|
|
13
22
|
decorations?: 'static' | 'animated';
|
|
14
|
-
}
|
|
23
|
+
} & (
|
|
24
|
+
| { mode: 'disabled' | 'always' }
|
|
25
|
+
| { mode: 'annual'; period: AnnualThemePeriod }
|
|
26
|
+
);
|
package/src/theme/halloween.css
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
--nox-seasonal-header-background: #211512;
|
|
16
16
|
--nox-seasonal-blood: #a52330;
|
|
17
|
+
--nox-seasonal-header-border: var(--nox-seasonal-blood);
|
|
17
18
|
--nox-seasonal-border-size: 3px;
|
|
18
19
|
--nox-seasonal-web: #d1bfa9;
|
|
19
20
|
--nox-theme-accent-color: var(--nox-seasonal-blood);
|