feat: add design system, theme toggle, SEO head and base layout

- Huemint Magazine tokens for light/dark themes with AA-safe accent variants
- FOUC-free theme init + toggle (localStorage, follows OS until chosen)
- Seo component (canonical, OG, Twitter, WebSite/Person JSON-LD)
- Header with popover mobile menu, 4-column footer with social links
- Container, GridLines, SectionHeading, Button, Badge, Card, ExternalLink
- site.json / social.json validated with zod
- dev-only /styleguide preview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-16 00:47:50 +03:00
parent c5e0b009ca
commit d8a79ccbe3
19 changed files with 915 additions and 36 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* Typed, validated access to src/data/*.json.
* A schema mismatch throws at import time, so `astro build` fails loudly.
*/
import { z } from 'astro/zod';
import siteJson from '../data/site.json';
import socialJson from '../data/social.json';
const link = z.object({ label: z.string().min(1), href: z.string().min(1) });
const siteSchema = z.object({
name: z.string(),
title: z.string(),
description: z.string().max(160),
url: z.url(),
lang: z.string(),
locale: z.string(),
titleSeparator: z.string(),
author: z.object({ name: z.string(), callsign: z.string(), locator: z.string() }),
postsPerPage: z.number().int().positive(),
nav: z.array(link).min(1),
footer: z.object({
about: z.string(),
quickLinksTitle: z.string(),
legalTitle: z.string(),
socialTitle: z.string(),
rss: link,
legal: z.array(link),
license: z.string(),
signoff: z.string(),
}),
ui: z.object({
skipToContent: z.string(),
mainNav: z.string(),
footerNav: z.string(),
menuOpen: z.string(),
menuClose: z.string(),
themeToggle: z.string(),
newTab: z.string(),
home: z.string(),
}),
});
const socialSchema = z.array(
z.object({
id: z.string(),
label: z.string(),
// Empty string = account not set → hidden in the UI.
href: z.union([z.url(), z.literal('')]),
icon: z.string().startsWith('tabler:'),
handle: z.string().optional(),
}),
);
function parse<T>(name: string, schema: z.ZodType<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error(`src/data/${name} is invalid:\n${z.prettifyError(result.error)}`);
}
return result.data;
}
export const site = parse('site.json', siteSchema, siteJson);
export const social = parse('social.json', socialSchema, socialJson).filter((s) => s.href);
export type Site = z.infer<typeof siteSchema>;
export type SocialLink = (typeof social)[number];