Browse Source

Clean up

master
Nuša Pukšič 7 months ago committed by buttercat1791
parent
commit
da82c85810
  1. 52
      scripts/a-ui.manifest.json
  2. 84
      scripts/scaffold.ts
  3. 32
      src/lib/theme/build-tokens.ts
  4. 93
      src/lib/theme/generated/themes.css
  5. 6
      src/lib/theme/theme-store.ts
  6. 15
      src/lib/theme/themes/dark.yaml
  7. 15
      src/lib/theme/themes/light.yaml
  8. 15
      src/lib/theme/themes/sepia.yaml

52
scripts/a-ui.manifest.json

@ -1,52 +0,0 @@ @@ -1,52 +0,0 @@
{
"files": [
{
"path": "src/lib/theme/build-tokens.ts",
"content": "#!/usr/bin/env -S deno run --allow-read --allow-write\nimport { join } from \"https://deno.land/std@0.224.0/path/mod.ts\";\nimport { parse as parseYaml } from \"https://deno.land/std@0.224.0/yaml/mod.ts\";\nconst themesDir = \"src/lib/theme/themes\";\nconst outCss = \"src/lib/theme/generated/themes.css\";\nfunction toRgb(hex: string) {\n const h = hex.replace(\"#\", \"\").trim();\n const n = (s: string) => parseInt(s, 16);\n if (h.length === 3) return `${n(h[0]+h[0])} ${n(h[1]+h[1])} ${n(h[2]+h[2])}`;\n return `${n(h.slice(0,2))} ${n(h.slice(2,4))} ${n(h.slice(4,6))}`;\n}\nconst entries: string[] = [];\nfor await (const ent of Deno.readDir(themesDir)) if (ent.isFile && ent.name.endsWith(\".yaml\")) entries.push(ent.name);\nentries.sort();\nlet css = \"\";\nfor (const file of entries) {\n const t = parseYaml(await Deno.readTextFile(join(themesDir, file))) as any;\n const sel = t.name === \"light\" ? ':root,[data-theme=\"light\"]' : `[data-theme=\"${t.name}\"]`;\n css += `${sel}{\\n`;\n for (const [k, v] of Object.entries(t.colors ?? {})) css += `--color-${k}: ${toRgb(String(v))};\\n`;\n for (const [k, v] of Object.entries(t.radii ?? {})) css += `--radius-${k}: ${v};\\n`;\n for (const [k, v] of Object.entries(t.spacing ?? {})) css += `--space-${k}: ${v};\\n`;\n const ty = t.typography ?? {};\n if (ty[\"font-reading\"]) css += `--font-reading: ${ty[\"font-reading\"]};\\n`;\n if (ty[\"font-ui\"]) css += `--font-ui: ${ty[\"font-ui\"]};\\n`;\n if (ty[\"leading-reading\"]) css += `--leading-reading: ${ty[\"leading-reading\"]};\\n`;\n if (ty[\"measure-ch\"]) css += `--measure-ch: ${ty[\"measure-ch\"]};\\n`;\n css += `}\\n\\n`;\n}\nawait Deno.mkdir(join(\"src/lib/theme/generated\"), { recursive: true });\nawait Deno.writeTextFile(outCss, css);\nconsole.log(\"Wrote\", outCss);\n"
},
{
"path": "deno.json",
"content": "{\n \"tasks\": {\n \"tokens\": \"deno run --allow-read --allow-write src/lib/theme/build-tokens.ts\",\n \"scaffold\": \"deno run --allow-read --allow-write scripts/scaffold.ts scripts/a-ui.v5.manifest.json\"\n }\n}\n"
},
{
"path": "src/lib/a/primitives/AButton.svelte",
"content": "<script lang=\"ts\">\n import { cva, twMerge } from '$lib/styles/cva';\n let {\n variant = 'solid',\n size = 'md',\n as = 'button',\n disabled = false,\n class: className = '',\n // common attrs (no $$restProps in runes)\n href = undefined as string | undefined,\n target = undefined as string | undefined,\n rel = undefined as string | undefined,\n type = 'button',\n onclick = undefined as undefined | ((e:MouseEvent)=>void)\n } = $props();\n\n const styles = cva(\n 'inline-flex items-center justify-center font-medium rounded-md focus:outline-none focus:ring-2 focus:ring-primary/40 transition',\n { variants: { variant:\n { solid: 'bg-primary text-[rgb(var(--color-primary-contrast,255 255 255))] hover:bg-primary/90',\n outline: 'border border-primary text-primary hover:bg-primary/10',\n ghost: 'text-primary hover:bg-primary/10' },\n size: { sm: 'h-8 px-3 text-sm', md: 'h-10 px-4', lg: 'h-12 px-5 text-lg' } },\n defaultVariants: { variant: 'solid', size: 'md' } }\n );\n</script>\n\n<svelte:element\n this={as}\n class={twMerge(styles({ variant, size }), className)}\n disabled={as === 'button' ? disabled : undefined}\n href={as === 'a' ? href : undefined}\n target={as === 'a' ? target : undefined}\n rel={as === 'a' ? rel : undefined}\n type={as === 'button' ? type : undefined}\n onclick={onclick}\n>\n <slot />\n</svelte:element>\n"
},
{
"path": "src/lib/a/primitives/AInput.svelte",
"content": "<script lang=\"ts\"> let { value = '', class: className = '', placeholder = '' } = $props(); </script>\n<input\n class={`w-full h-10 px-3 rounded-md border border-muted/30 bg-surface text-text placeholder:opacity-60 focus:outline-none focus:ring-2 focus:ring-primary/40 ${className}`}\n bind:value\n placeholder={placeholder}\n/>\n"
},
{
"path": "src/lib/a/primitives/ACard.svelte",
"content": "<script lang=\"ts\"> let { class: className = '' } = $props(); </script>\n<div class={`rounded-lg border border-muted/20 bg-surface shadow-sm ${className}`}>\n <slot name=\"header\" />\n <div class=\"p-4\"><slot /></div>\n <slot name=\"footer\" />\n</div>\n"
},
{
"path": "src/lib/a/primitives/ASwitch.svelte",
"content": "<script lang=\"ts\">\n let { checked = false, class: className = '', onchange = undefined as undefined | ((v:boolean)=>void) } = $props();\n function toggle(){ checked = !checked; onchange?.(checked); }\n</script>\n<button type=\"button\" role=\"switch\" aria-checked={checked}\n onclick={toggle}\n class={`inline-flex items-center h-6 w-11 rounded-full border border-muted/30 transition px-0.5 ${checked ? 'bg-primary' : 'bg-surface'} ${className}`}\n>\n <span class={`inline-block h-5 w-5 rounded-full bg-white shadow transform transition ${checked ? 'translate-x-5' : 'translate-x-0'}`} />\n</button>\n"
},
{
"path": "src/lib/a/primitives/ADetails.svelte",
"content": "<script lang=\"ts\">\n import { showTech } from '$lib/tech/tech-store';\n let { summary = '', tech = false, defaultOpen = false, forceHide = false, class: className = '' } = $props();\n let open = defaultOpen;\n $: if (tech && !$showTech) open = false;\n function onToggle(e: Event){ const el = e.currentTarget as HTMLDetailsElement; open = el.open; }\n</script>\n<details open={open} ontoggle={onToggle} class={`group rounded-lg border border-muted/20 bg-surface ${className}`} data-kind={tech ? 'tech':'general'}>\n <summary class=\"flex items-center gap-2 cursor-pointer list-none px-3 py-2 rounded-lg select-none hover:bg-primary/10\">\n <svg class={`h-4 w-4 transition-transform ${open ? 'rotate-90':''}`} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M9 18l6-6-6-6\"/></svg>\n <span class=\"font-medium\">{summary}</span>\n {#if tech}<span class=\"ml-2 text-[10px] uppercase tracking-wide rounded px-1.5 py-0.5 border border-primary/30 text-primary bg-primary/5\">Technical</span>{/if}\n <span class=\"ml-auto text-xs opacity-60 group-open:opacity-50\">{open ? 'Hide':'Show'}</span>\n </summary>\n {#if !(tech && !$showTech && forceHide)}<div class=\"px-3 pb-3 pt-1 text-[0.95rem] leading-6\"><slot /></div>{/if}\n</details>\n"
},
{
"path": "src/lib/a/reader/AReaderTOC.svelte",
"content": "<script lang=\"ts\">\n import { onMount } from 'svelte';\n import TocNode from './TocNode.svelte';\n import type { TocItem } from './toc-utils';\n let { items = [] as TocItem[], activeId = null as string | null, collapsible = true, expandDepth = 1, class: className = '', onnavigate = undefined as undefined | ((href:string)=>void) } = $props();\n let expanded = new Set<string>();\n let parentOf = new Map<string, string | null>();\n function mapParents(list: TocItem[], parent: string | null = null){ for (const it of list){ parentOf.set(it.id, parent); if (it.children?.length) mapParents(it.children, it.id); } }\n $: mapParents(items);\n function initExpansion(list: TocItem[], depth = 0){ for (const it of list){ if (depth < expandDepth) expanded.add(it.id); if (it.children?.length) initExpansion(it.children, depth + 1); } expanded = new Set(expanded); }\n function expandAncestors(id: string | null){ if (!id) return; let cur: string | null | undefined = id; while (cur){ expanded.add(cur); cur = parentOf.get(cur) ?? null; } expanded = new Set(expanded); }\n onMount(()=>{ initExpansion(items,0); expandAncestors(activeId); if (activeId){ const el = document.querySelector(`[data-toc-id=\"${activeId}\"]`); if (el instanceof HTMLElement) el.scrollIntoView({ block:'nearest' }); } });\n $: if (activeId) expandAncestors(activeId);\n function toggle(id: string){ if (!collapsible) return; if (expanded.has(id)) expanded.delete(id); else expanded.add(id); expanded = new Set(expanded); }\n const pad = (depth:number)=>`padding-left: calc(var(--space-4) * ${Math.max(depth,0)})`;\n const onNavigate = (href:string)=> onnavigate?.(href);\n</script>\n<nav aria-label=\"Table of contents\" class={`text-sm ${className}`}>\n <ul class=\"space-y-1 max-h-[calc(100vh-6rem)] overflow-auto pr-1\">\n {#each items as item (item.id)}\n <TocNode {item} depth={0} {activeId} {collapsible} {expanded} {toggle} {pad} {onNavigate} />\n {/each}\n </ul>\n</nav>\n"
},
{
"path": "src/lib/a/reader/TocNode.svelte",
"content": "<script lang=\"ts\">\n import type { TocItem } from './toc-utils';\n import TocNode from './TocNode.svelte';\n let { item, depth = 0, activeId = null as string | null, collapsible = true, expanded, toggle, pad, onNavigate }: { item: TocItem; depth?: number; activeId?: string|null; collapsible?: boolean; expanded: Set<string>; toggle: (id:string)=>void; pad: (depth:number)=>string; onNavigate: (href:string)=>void; } = $props();\n const hasChildren = !!(item.children && item.children.length > 0);\n $: isOpen = expanded.has(item.id);\n $: isActive = activeId === item.id;\n</script>\n<li>\n <div class=\"flex items-center gap-1 rounded-md hover:bg-primary/10\" style={pad(depth)}>\n {#if collapsible && hasChildren}\n <button class=\"shrink-0 h-6 w-6 grid place-items-center rounded-md hover:bg-primary/10\" aria-label={isOpen?`Collapse ${item.title}`:`Expand ${item.title}`} aria-expanded={isOpen} onclick={() => toggle(item.id)}>\n <svg width=\"16\" height=\"16\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" class={isOpen ? 'rotate-90 transition-transform' : 'transition-transform'}><path d=\"M9 18l6-6-6-6\" /></svg>\n </button>\n {:else}\n <span class=\"shrink-0 h-6 w-6\" />\n {/if}\n <a href={item.href ?? `#${item.id}`} data-toc-id={item.id}\n class=\"flex-1 min-w-0 rounded-md px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-primary/40\"\n class:hover=\"bg-primary/10\" class:text-primary={isActive} class:bg-primary/10={isActive}\n onclick={() => onNavigate(item.href ?? `#${item.id}`)}>\n <span class=\"truncate\">{item.title}</span>\n </a>\n </div>\n {#if hasChildren}\n <ul id={`sub-${item.id}`} aria-hidden={!isOpen} class={isOpen ? 'mt-1 space-y-1' : 'hidden'}>\n {#each item.children as child (child.id)}\n <TocNode item={child} depth={depth + 1} {activeId} {collapsible} {expanded} {toggle} {pad} {onNavigate} />\n {/each}\n </ul>\n {/if}\n</li>\n"
},
{
"path": "src/lib/a/nav/ANavbar.svelte",
"content": "<script lang=\"ts\">\n import type { NavItem, UserInfo } from './nav-types';\n import UserDropdown from './UserDropdown.svelte';\n import MobileNav from './MobileNav.svelte';\n import { onMount } from 'svelte';\n let { items = [] as NavItem[], currentPath = '', user = null as UserInfo | null, userMenu = [] as NavItem[], logo = null, brand = 'Alexandria', onselect = undefined as undefined | ((i:NavItem)=>void) } = $props();\n let mobileOpen = false; let openId: string | null = null;\n function onKey(e: KeyboardEvent){ if (e.key === 'Escape'){ openId = null; mobileOpen = false; } }\n onMount(()=>{ window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); });\n const topItems = items.map((it,i)=>({ ...it, id: it.id || `${(it.title||'item').toLowerCase().replace(/[^\\w]+/g,'-')}-${i}` }));\n function isActive(href?: string){ if (!href) return false; try { return new URL(href, 'http://x').pathname === currentPath; } catch { return href === currentPath; } }\n</script>\n<header class=\"sticky top-0 z-40 border-b border-muted/20 bg-surface/95 backdrop-blur supports-[backdrop-filter]:bg-surface/80\">\n <div class=\"mx-auto max-w-7xl px-3 sm:px-4 lg:px-6\">\n <div class=\"h-14 flex items-center gap-3\">\n <button class=\"lg:hidden inline-flex h-9 w-9 items-center justify-center rounded-md border border-muted/30\" aria-label=\"Open menu\" onclick={() => (mobileOpen = true)}>\n <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"3\" y1=\"6\" x2=\"21\" y2=\"6\"/><line x1=\"3\" y1=\"12\" x2=\"21\" y2=\"12\"/><line x1=\"3\" y1=\"18\" x2=\"21\" y2=\"18\"/></svg>\n </button>\n <a href=\"/\" class=\"flex items-center gap-2\">{#if logo}<svelte:component this={logo} />{/if}<span class=\"font-semibold\">{brand}</span></a>\n <nav class=\"ml-4 hidden lg:flex items-stretch gap-1\">\n {#each topItems as item}\n <div class=\"relative\" onmouseleave={() => (openId = null)}>\n <button class=\"px-2.5 h-10 rounded-md inline-flex items-center gap-1.5 hover:bg-primary/10 focus:outline-none focus:ring-2 focus:ring-primary/40\"\n aria-haspopup={item.children?.length ? 'menu' : undefined}\n aria-expanded={openId === item.id}\n onmouseenter={() => (openId = item.children?.length ? item.id! : null)}\n onfocus={() => (openId = item.children?.length ? item.id! : null)}\n onclick={() => (openId = openId === item.id ? null : (item.children?.length ? item.id! : null))}\n >\n {#if item.icon}<svelte:component this={item.icon} class=\"h-4 w-4\" />{/if}\n <a href={item.href || '#'} class=\"px-0.5 rounded-md\" class:text-primary={isActive(item.href)}>{item.title}</a>\n {#if item.children?.length}<svg class=\"h-4 w-4 opacity-70\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>{/if}\n {#if item.badge}<span class=\"text-[10px] ml-1 rounded px-1.5 py-0.5 border border-primary/30 text-primary bg-primary/5\">{item.badge}</span>{/if}\n </button>\n {#if item.children?.length && openId === item.id}\n <div class=\"absolute left-0 mt-1 w-[min(90vw,48rem)] rounded-lg border border-muted/20 bg-surface shadow-lg p-3\">\n <div class=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3\">\n {#each item.children as section}\n <div>\n <a href={section.href || '#'} class=\"font-medium hover:underline flex items-center gap-2\">{#if section.icon}<svelte:component this={section.icon} class=\"h-4 w-4\" />{/if}{section.title}</a>\n {#if section.children?.length}\n <ul class=\"mt-2 space-y-1\">\n {#each section.children as leaf}\n <li><a href={leaf.href || '#'} target={leaf.external ? '_blank' : undefined} rel={leaf.external ? 'noreferrer' : undefined} class=\"block rounded-md px-2 py-1 hover:bg-primary/10\">{leaf.title}</a></li>\n {/each}\n </ul>\n {/if}\n </div>\n {/each}\n </div>\n </div>\n {/if}\n </div>\n {/each}\n </nav>\n <div class=\"ml-auto flex items-center gap-2\">\n <slot name=\"search\" />\n <UserDropdown {user} items={userMenu} onselect={onselect} />\n </div>\n </div>\n </div>\n <MobileNav bind:open={mobileOpen} {items} {user} userItems={userMenu} />\n</header>\n"
},
{
"path": "src/lib/a/nav/UserDropdown.svelte",
"content": "<script lang=\"ts\">\n import type { NavItem, UserInfo } from './nav-types';\n import { onMount } from 'svelte';\n let { user = null as UserInfo | null, items = [] as NavItem[], onselect = undefined as undefined | ((i:NavItem)=>void) } = $props();\n let open = false; let btn: HTMLButtonElement;\n function onDoc(e: MouseEvent){ if (!open) return; if (!(e.target instanceof Node)) return; if (!btn?.parentElement?.contains(e.target)) open = false; }\n onMount(()=>{ document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); });\n</script>\n<div class=\"relative\">\n <button bind:this={btn} class=\"inline-flex items-center gap-2 h-9 px-2 rounded-md border border-muted/30 hover:bg-primary/10\" aria-haspopup=\"menu\" aria-expanded={open} onclick={() => (open = !open)}>\n <img src={user?.avatarUrl || 'https://via.placeholder.com/24'} alt=\"\" class=\"h-6 w-6 rounded-full object-cover\" />\n <span class=\"hidden sm:block text-sm\">{user?.name || 'Account'}</span>\n <svg class=\"h-4 w-4 opacity-70\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </button>\n {#if open}\n <div class=\"absolute right-0 mt-1 min-w-[14rem] rounded-lg border border-muted/20 bg-surface shadow-lg py-1 z-50\">\n {#if user}\n <div class=\"px-3 py-2 text-sm\">\n <div class=\"font-medium\">{user.name}</div>\n {#if user.email}<div class=\"opacity-70\">{user.email}</div>{/if}\n </div>\n <div class=\"my-1 h-px bg-muted/20\" />\n {/if}\n <ul>\n {#each items as it}\n {#if it.divider}\n <li class=\"my-1 h-px bg-muted/20\"></li>\n {:else}\n <li>\n <a href={it.href || '#'} target={it.external ? '_blank':undefined} rel={it.external ? 'noreferrer':undefined}\n class=\"flex items-center gap-2 px-3 py-2 text-sm hover:bg-primary/10\"\n onclick={(e)=>{ if (!it.href || it.href==='#'){ e.preventDefault(); open=false; onselect?.(it); } else { open=false; } }}>\n {it.title}\n {#if it.badge}<span class=\"ml-auto text-[10px] rounded px-1.5 py-0.5 border border-primary/30 text-primary bg-primary/5\">{it.badge}</span>{/if}\n </a>\n </li>\n {/if}\n {/each}\n </ul>\n </div>\n {/if}\n</div>\n"
},
{
"path": "src/lib/a/nav/MobileNav.svelte",
"content": "<script lang=\"ts\">\n import type { NavItem, UserInfo } from './nav-types';\n let { open = false, items = [] as NavItem[], user = null as UserInfo | null, userItems = [] as NavItem[] } = $props();\n function close(){ open = false; }\n</script>\n{#if open}\n <div class=\"fixed inset-0 z-50\">\n <div class=\"absolute inset-0 bg-black/40\" onclick={close}></div>\n <aside class=\"absolute left-0 top-0 h-full w-[88vw] max-w-sm bg-surface border-r border-muted/20 shadow-xl p-3 overflow-y-auto\">\n <div class=\"flex items-center gap-3\">\n <img src={user?.avatarUrl || 'https://via.placeholder.com/32'} alt=\"\" class=\"h-8 w-8 rounded-full\" />\n <div>\n <div class=\"text-sm font-medium\">{user?.name || 'Welcome'}</div>\n {#if user?.email}<div class=\"text-xs opacity-70\">{user.email}</div>{/if}\n </div>\n <button class=\"ml-auto h-8 w-8 grid place-items-center rounded-md border border-muted/30\" onclick={close} aria-label=\"Close\">\n <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><line x1=\"18\" y1=\"6\" x2=\"6\" y2=\"18\"/><line x1=\"6\" y1=\"6\" x2=\"18\" y2=\"18\"/></svg>\n </button>\n </div>\n <nav class=\"mt-4\">\n <ul class=\"space-y-1\">\n {#each items as item}\n <li>\n {#if item.children?.length}\n <details>\n <summary class=\"flex items-center gap-2 cursor-pointer list-none rounded-md px-2 py-2 hover:bg-primary/10\">\n <span class=\"flex-1\">{item.title}</span>\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </summary>\n <ul class=\"mt-1 ml-3 space-y-1\">\n {#each item.children as child}\n <li>\n {#if child.children?.length}\n <details>\n <summary class=\"flex items-center gap-2 cursor-pointer list-none rounded-md px-2 py-2 hover:bg-primary/10\">\n <span class=\"flex-1\">{child.title}</span>\n <svg class=\"h-4 w-4\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><polyline points=\"6 9 12 15 18 9\"/></svg>\n </summary>\n <ul class=\"mt-1 ml-3 space-y-1\">\n {#each child.children as leaf}\n <li><a href={leaf.href || '#'} target={leaf.external ? '_blank' : undefined} rel={leaf.external ? 'noreferrer' : undefined} class=\"block rounded-md px-2 py-2 hover:bg-primary/10\">{leaf.title}</a></li>\n {/each}\n </ul>\n </details>\n {:else}\n <a href={child.href || '#'} class=\"block rounded-md px-2 py-2 hover:bg-primary/10\">{child.title}</a>\n {/if}\n </li>\n {/each}\n </ul>\n </details>\n {:else}\n <a href={item.href || '#'} class=\"block rounded-md px-2 py-2 hover:bg-primary/10\">{item.title}</a>\n {/if}\n </li>\n {/each}\n </ul>\n </nav>\n {#if userItems.length}\n <div class=\"my-4 h-px bg-muted/20\"></div>\n <ul class=\"space-y-1\">\n {#each userItems as it}\n {#if it.divider}\n <li class=\"my-2 h-px bg-muted/20\"></li>\n {:else}\n <li><a href={it.href || '#'} class=\"block rounded-md px-2 py-2 hover:bg-primary/10\">{it.title}</a></li>\n {/if}\n {/each}\n </ul>\n {/if}\n </aside>\n </div>\n{/if}\n"
}
]
}

84
scripts/scaffold.ts

@ -1,84 +0,0 @@ @@ -1,84 +0,0 @@
#!/usr/bin/env -S deno run --allow-read --allow-write
import { dirname, resolve } from "jsr:@std/path";
/** Read and parse JSON */
async function readJson(p: string) {
try {
const txt = await Deno.readTextFile(p);
return JSON.parse(txt);
} catch (e) {
console.error(`Failed to read JSON: ${p}\n${e.message}`);
Deno.exit(1);
}
}
/** Ensure parent directory exists */
async function ensureDir(filePath: string) {
await Deno.mkdir(dirname(filePath), { recursive: true });
}
/** File exists? */
async function exists(p: string) {
try {
await Deno.lstat(p);
return true;
} catch (e) {
if (e instanceof Deno.errors.NotFound) return false;
throw e;
}
}
/** Write file if changed; return action marker */
async function writeFile(p: string, content: string) {
await ensureDir(p);
const had = await exists(p);
if (had) {
const current = await Deno.readTextFile(p);
if (current === content) return "skip" as const;
}
await Deno.writeTextFile(p, content);
return had ? ("update" as const) : ("create" as const);
}
/** Shallow-merge package.json fields */
function mergePackage(pkg: any, merge: any) {
const next = { ...pkg };
for (const key of ["scripts", "dependencies", "devDependencies"]) {
if (merge?.[key]) next[key] = { ...(pkg[key] || {}), ...merge[key] };
}
return next;
}
async function main() {
const manifestPath = Deno.args[0];
if (!manifestPath) {
console.error("Usage: deno run --allow-read --allow-write scripts/scaffold.ts <manifest.json>");
Deno.exit(1);
}
const manifest = await readJson(manifestPath);
// Optional: merge package.json if present
const pkgPath = resolve(Deno.cwd(), "package.json");
if (manifest.package?.merge && (await exists(pkgPath))) {
const pkg = await readJson(pkgPath);
const merged = mergePackage(pkg, manifest.package.merge);
await Deno.writeTextFile(pkgPath, JSON.stringify(merged, null, 2) + "\n");
console.log("✓ package.json merged");
} else if (manifest.package?.merge) {
console.log("• package.json not found — skipping merge");
}
// Write files from manifest
const files = manifest.files ?? [];
for (const f of files) {
const out = resolve(Deno.cwd(), String(f.path));
const action = await writeFile(out, String(f.content ?? ""));
const marker = action === "skip" ? "•" : action === "update" ? "↻" : "✓";
console.log(`${marker} ${f.path}`);
}
console.log("\nDone.");
}
if (import.meta.main) main();

32
src/lib/theme/build-tokens.ts

@ -1,32 +0,0 @@ @@ -1,32 +0,0 @@
#!/usr/bin/env -S deno run --allow-read --allow-write
import { join } from "https://deno.land/std@0.224.0/path/mod.ts";
import { parse as parseYaml } from "https://deno.land/std@0.224.0/yaml/mod.ts";
const themesDir = "src/lib/theme/themes";
const outCss = "src/lib/theme/generated/themes.css";
function toRgb(hex: string) {
const h = hex.replace("#", "").trim();
const n = (s: string) => parseInt(s, 16);
if (h.length === 3) return `${n(h[0]+h[0])} ${n(h[1]+h[1])} ${n(h[2]+h[2])}`;
return `${n(h.slice(0,2))} ${n(h.slice(2,4))} ${n(h.slice(4,6))}`;
}
const entries: string[] = [];
for await (const ent of Deno.readDir(themesDir)) if (ent.isFile && ent.name.endsWith(".yaml")) entries.push(ent.name);
entries.sort();
let css = "";
for (const file of entries) {
const t = parseYaml(await Deno.readTextFile(join(themesDir, file))) as any;
const sel = t.name === "light" ? ':root,[data-theme="light"]' : `[data-theme="${t.name}"]`;
css += `${sel}{\n`;
for (const [k, v] of Object.entries(t.colors ?? {})) css += `--color-${k}: ${toRgb(String(v))};\n`;
for (const [k, v] of Object.entries(t.radii ?? {})) css += `--radius-${k}: ${v};\n`;
for (const [k, v] of Object.entries(t.spacing ?? {})) css += `--space-${k}: ${v};\n`;
const ty = t.typography ?? {};
if (ty["font-reading"]) css += `--font-reading: ${ty["font-reading"]};\n`;
if (ty["font-ui"]) css += `--font-ui: ${ty["font-ui"]};\n`;
if (ty["leading-reading"]) css += `--leading-reading: ${ty["leading-reading"]};\n`;
if (ty["measure-ch"]) css += `--measure-ch: ${ty["measure-ch"]};\n`;
css += `}\n\n`;
}
await Deno.mkdir(join("src/lib/theme/generated"), { recursive: true });
await Deno.writeTextFile(outCss, css);
console.log("Wrote", outCss);

93
src/lib/theme/generated/themes.css

@ -1,93 +0,0 @@ @@ -1,93 +0,0 @@
[data-theme="dark"]{
--color-bg: 11 16 32;
--color-surface: 15 20 38;
--color-text: 229 231 235;
--color-muted: 148 163 184;
--color-primary: 96 165 250;
--color-primary-50: #fff5f2;
--color-primary-100: #fff1ee;
--color-primary-200: #ffe4de;
--color-primary-300: #ffd5cc;
--color-primary-400: #ffbcad;
--color-primary-500: #fe795d;
--color-primary-600: #ef562f;
--color-primary-700: #eb4f27;
--color-primary-800: #cc4522;
--color-primary-900: #a5371b;
--color-secondary-50: #f0f9ff;
--color-secondary-100: #e0f2fe;
--color-secondary-200: #bae6fd;
--color-secondary-300: #7dd3fc;
--color-secondary-400: #38bdf8;
--color-secondary-500: #0ea5e9;
--color-secondary-600: #0284c7;
--color-secondary-700: #0369a1;
--color-secondary-800: #075985;
--color-secondary-900: #0c4a6e;
--color-primary-contrast: 11 16 32;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 14px;
--radius-full: 9999px;
--space-0: 0;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--font-reading: Literata, ui-serif, Georgia, serif;
--font-ui: Inter, ui-sans-serif, system-ui, sans-serif;
--leading-reading: 1.8;
--measure-ch: 68;
}
:root,[data-theme="light"]{
--color-bg: 255 255 255;
--color-surface: 250 250 250;
--color-text: 17 24 39;
--color-muted: 107 114 128;
--color-primary: 29 78 216;
--color-primary-contrast: 255 255 255;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 14px;
--radius-full: 9999px;
--space-0: 0;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--font-reading: Literata, ui-serif, Georgia, serif;
--font-ui: Inter, ui-sans-serif, system-ui, sans-serif;
--leading-reading: 1.7;
--measure-ch: 70;
}
[data-theme="sepia"]{
--color-bg: 245 236 217;
--color-surface: 249 243 231;
--color-text: 58 47 30;
--color-muted: 109 92 70;
--color-primary: 138 90 68;
--color-primary-contrast: 255 255 255;
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 14px;
--radius-full: 9999px;
--space-0: 0;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--font-reading: Literata, ui-serif, Georgia, serif;
--font-ui: Inter, ui-sans-serif, system-ui, sans-serif;
--leading-reading: 1.7;
--measure-ch: 70;
}

6
src/lib/theme/theme-store.ts

@ -1,6 +0,0 @@ @@ -1,6 +0,0 @@
import { writable } from 'svelte/store';
const KEY='theme';
const initial = (typeof localStorage!=='undefined' && localStorage.getItem(KEY)) || 'light';
export const theme = writable(initial);
theme.subscribe(v=>{ if(typeof document!=='undefined'){ document.documentElement.dataset.theme=String(v); localStorage.setItem(KEY,String(v)); } });
export const setTheme = (t:string)=>theme.set(t);

15
src/lib/theme/themes/dark.yaml

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
name: dark
colors:
bg: '#0B1020'
surface: '#0F1426'
text: '#E5E7EB'
muted: '#94A3B8'
primary: '#60A5FA'
primary-contrast: '#0B1020'
radii: { sm: 4px, md: 8px, lg: 14px, full: 9999px }
spacing: { 0: 0, 1: 0.25rem, 2: 0.5rem, 3: 0.75rem, 4: 1rem, 6: 1.5rem, 8: 2rem }
typography:
font-reading: 'Literata, ui-serif, Georgia, serif'
font-ui: 'Inter, ui-sans-serif, system-ui, sans-serif'
leading-reading: 1.8
measure-ch: 68

15
src/lib/theme/themes/light.yaml

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
name: light
colors:
bg: '#FFFFFF'
surface: '#FAFAFA'
text: '#111827'
muted: '#6B7280'
primary: '#1D4ED8'
primary-contrast: '#FFFFFF'
radii: { sm: 4px, md: 8px, lg: 14px, full: 9999px }
spacing: { 0: 0, 1: 0.25rem, 2: 0.5rem, 3: 0.75rem, 4: 1rem, 6: 1.5rem, 8: 2rem }
typography:
font-reading: 'Literata, ui-serif, Georgia, serif'
font-ui: 'Inter, ui-sans-serif, system-ui, sans-serif'
leading-reading: 1.7
measure-ch: 70

15
src/lib/theme/themes/sepia.yaml

@ -1,15 +0,0 @@ @@ -1,15 +0,0 @@
name: sepia
colors:
bg: '#F5ECD9'
surface: '#F9F3E7'
text: '#3A2F1E'
muted: '#6D5C46'
primary: '#8A5A44'
primary-contrast: '#FFFFFF'
radii: { sm: 4px, md: 8px, lg: 14px, full: 9999px }
spacing: { 0: 0, 1: 0.25rem, 2: 0.5rem, 3: 0.75rem, 4: 1rem, 6: 1.5rem, 8: 2rem }
typography:
font-reading: 'Literata, ui-serif, Georgia, serif'
font-ui: 'Inter, ui-sans-serif, system-ui, sans-serif'
leading-reading: 1.7
measure-ch: 70
Loading…
Cancel
Save