Initial Commit

This commit is contained in:
RiseForever
2026-08-11 10:25:27 +08:00
parent 70d33c0996
commit cd5e2796c2
161 changed files with 29688 additions and 2 deletions
+165
View File
@@ -0,0 +1,165 @@
---
import { CloudSun, Monitor, Moon, Minus, Plus, Sun, Type } from '@lucide/astro';
interface Props { placement?: 'down' | 'up'; }
const { placement = 'down' } = Astro.props;
const themes = [
{ value: 'auto', label: '自动', icon: Monitor },
{ value: 'light', label: '日间', icon: Sun },
{ value: 'sunset', label: '日落', icon: CloudSun },
{ value: 'dark', label: '夜间', icon: Moon }
] as const;
---
<details class:list={[placement === 'down' ? 'relative' : 'static', { 'placement-up': placement === 'up' }]} data-reading-settings>
<summary class="grid size-[2.5rem] cursor-pointer list-none place-items-center text-current/80 hover:bg-current/10 hover:text-current [&::-webkit-details-marker]:hidden" aria-label="打开阅读设置" aria-expanded="false" title="阅读设置">
<Type aria-hidden="true" size="1em" />
</summary>
<div class:list={['reading-settings-panel absolute z-[80] border border-line bg-raised p-[0.75rem] shadow-mirages', placement === 'up' ? 'bottom-[calc(100%+0.5rem)] left-0 w-[min(17.5rem,84vw)] max-w-[calc(100vw-1.75rem)] max-h-[calc(100svh-1.75rem)] overflow-y-auto' : 'top-[calc(100%+0.5rem)] right-0 w-[min(17.875rem,calc(100vw-1.75rem))]']} role="dialog" aria-label="阅读设置">
<div class="m-0">
<div class="mb-[0.25rem] flex items-center gap-[0.5rem]">
<h3 class="m-0 shrink-0 text-xs font-normal leading-5 text-subtle">颜色主题</h3>
<span class="h-px min-w-0 flex-1 bg-line" aria-hidden="true"></span>
</div>
<div class="grid grid-cols-4 gap-1" role="radiogroup" aria-label="颜色主题">
{themes.map(({ value, label, icon: Icon }) => (
<button class="grid min-h-[3.5rem] cursor-pointer place-items-center gap-[0.125rem] border border-transparent bg-transparent px-[0.25rem] py-[0.25rem] text-xs text-foreground hover:border-line hover:bg-mirages-accent/10" type="button" role="radio" aria-checked="false" data-theme-value={value}>
<Icon aria-hidden="true" size="1.0625rem" />
<span>{label}</span>
</button>
))}
</div>
</div>
<div class="mt-[0.375rem]">
<div class="mb-[0.25rem] flex items-center gap-[0.5rem]">
<h3 class="m-0 shrink-0 text-xs font-normal leading-5 text-subtle">字号</h3>
<span class="h-px min-w-0 flex-1 bg-line" aria-hidden="true"></span>
</div>
<div class="flex items-center gap-[0.5rem]">
<button class="grid h-[2rem] min-w-0 flex-[2] cursor-pointer place-items-center border border-line bg-transparent text-foreground hover:bg-mirages-accent/10" type="button" aria-label="减小字号" data-scale-step="-5"><Minus aria-hidden="true" size="1em" /></button>
<output class="min-w-0 flex-1 text-center text-xs font-semibold" data-scale-output>100%</output>
<button class="grid h-[2rem] min-w-0 flex-[2] cursor-pointer place-items-center border border-line bg-transparent text-foreground hover:bg-mirages-accent/10" type="button" aria-label="增大字号" data-scale-step="5"><Plus aria-hidden="true" size="1em" /></button>
</div>
</div>
<div class="mt-[0.375rem]">
<div class="mb-[0.25rem] flex items-center gap-[0.5rem]">
<h3 class="m-0 shrink-0 text-xs font-normal leading-5 text-subtle">字体</h3>
<span class="h-px min-w-0 flex-1 bg-line" aria-hidden="true"></span>
</div>
<div class="grid grid-cols-2 gap-1" role="radiogroup" aria-label="字体">
<button class="cursor-pointer border border-line bg-transparent px-[0.5rem] py-[0.375rem] text-xs text-foreground hover:bg-mirages-accent/10 [font-family:var(--mirages-font-serif)]" type="button" role="radio" aria-checked="false" data-font-value="serif">宋体</button>
<button class="cursor-pointer border border-line bg-transparent px-[0.5rem] py-[0.375rem] text-xs text-foreground hover:bg-mirages-accent/10 [font-family:var(--mirages-font-sans)]" type="button" role="radio" aria-checked="false" data-font-value="sans">黑体</button>
</div>
</div>
</div>
</details>
<script>
const root = document.documentElement;
const getStoredFont = () => {
const font = localStorage.getItem('mirages-font-family');
return font === 'serif' || font === 'sans' ? font : 'sans';
};
const getStoredTheme = () => {
const theme = localStorage.getItem('mirages-theme');
return theme === 'auto' || theme === 'light' || theme === 'sunset' || theme === 'dark' ? theme : null;
};
const syncSwitchers = (theme: string) => {
const switchers = document.querySelectorAll<HTMLElement>('[data-reading-settings]');
switchers.forEach((switcher) => {
switcher.querySelectorAll<HTMLButtonElement>('[data-theme-value]').forEach((button) => {
button.setAttribute('aria-checked', String(button.dataset.themeValue === theme));
});
switcher.querySelectorAll<HTMLButtonElement>('[data-font-value]').forEach((button) => {
button.setAttribute('aria-checked', String(button.dataset.fontValue === (root.dataset.font ?? 'sans')));
});
});
};
const getScale = () => {
const storedScale = Number(localStorage.getItem('mirages-font-scale'));
return Math.min(150, Math.max(80, Math.round((storedScale >= 0.8 && storedScale <= 1.5 ? storedScale * 100 : storedScale >= 80 && storedScale <= 150 ? storedScale : 100) / 5) * 5));
};
const syncScale = (value: number, persist = true) => {
const scale = Math.min(150, Math.max(80, Math.round(value / 5) * 5));
root.style.fontSize = `${scale}%`;
document.querySelectorAll<HTMLOutputElement>('[data-scale-output]').forEach((output) => {
output.value = `${scale}%`;
output.textContent = `${scale}%`;
});
if (persist) localStorage.setItem('mirages-font-scale', String(scale / 100));
};
root.dataset.font = getStoredFont();
syncSwitchers(root.dataset.theme ?? 'auto');
syncScale(getScale(), false);
document.addEventListener('click', (event) => {
if (!(event.target instanceof Element)) return;
const target = event.target;
const switcher = target.closest<HTMLElement>('[data-reading-settings]');
if (!switcher) return;
const theme = target.closest<HTMLButtonElement>('[data-theme-value]')?.dataset.themeValue;
if (theme) {
root.dataset.theme = theme;
localStorage.setItem('mirages-theme', theme);
syncSwitchers(theme);
return;
}
const step = target.closest<HTMLButtonElement>('[data-scale-step]')?.dataset.scaleStep;
if (step) {
syncScale((Number.parseFloat(root.style.fontSize) || getScale()) + Number(step));
return;
}
const font = target.closest<HTMLButtonElement>('[data-font-value]')?.dataset.fontValue;
if (font) {
root.dataset.font = font;
localStorage.setItem('mirages-font-family', font);
syncSwitchers(root.dataset.theme ?? 'auto');
}
});
document.addEventListener('toggle', (event) => {
const switcher = event.target as HTMLElement;
if (!switcher.matches('[data-reading-settings]')) return;
const trigger = switcher.querySelector<HTMLElement>('summary');
const open = switcher.hasAttribute('open');
trigger?.setAttribute('aria-expanded', String(open));
if (open) {
document.querySelectorAll<HTMLElement>('[data-reading-settings]').forEach((other) => { if (other !== switcher) other.removeAttribute('open'); });
switcher.querySelector<HTMLButtonElement>('[data-theme-value]')?.focus();
}
}, true);
document.addEventListener('keydown', (event) => {
if (event.key !== 'Escape' || !(event.target instanceof Element)) return;
const switcher = event.target.closest<HTMLElement>('[data-reading-settings]');
if (!switcher) return;
event.preventDefault();
event.stopPropagation();
switcher.removeAttribute('open');
switcher.querySelector<HTMLElement>('summary')?.focus();
});
document.addEventListener('pointerdown', (event) => {
document.querySelectorAll<HTMLElement>('[data-reading-settings]').forEach((switcher) => {
if (!switcher.contains(event.target as Node)) switcher.removeAttribute('open');
});
});
document.addEventListener('astro:before-swap', (event) => {
const theme = getStoredTheme();
if (theme) event.newDocument.documentElement.dataset.theme = theme;
});
document.addEventListener('astro:page-load', () => {
root.dataset.theme = getStoredTheme() ?? root.dataset.theme ?? 'auto';
root.dataset.font = getStoredFont();
syncSwitchers(root.dataset.theme ?? 'auto');
syncScale(getScale(), false);
});
</script>
<style>
button[aria-checked='true'] { border-color: var(--color-accent); background: color-mix(in srgb, var(--color-accent) 10%, transparent); }
[data-reading-settings] > summary { border-radius: var(--radius-sm); }
.reading-settings-panel,
.reading-settings-panel button { font-family: var(--reading-font-family); }
.reading-settings-panel button[data-font-value='serif'] { font-family: var(--mirages-font-serif) !important; }
.reading-settings-panel button[data-font-value='sans'] { font-family: var(--mirages-font-sans) !important; }
</style>