CSS Audit Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Remove all HTML IDs, inline styles, and hardcoded JS styles — replacing them with data attributes (JS targeting), CSS classes (styling), and Tailwind utilities.
Architecture: Component-namespaced data attributes (data-layout, data-nav, data-cp, data-chat, data-login, data-home) replace all IDs. Classes replace ID selectors in <style> blocks. Tailwind replaces inline style= attributes. Class toggling replaces element.style.* in JS.
Tech Stack: Astro 5, Tailwind CSS v4 (@tailwindcss/vite), TypeScript
Design doc: docs/plans/2026-03-01-css-audit-design.md
Task 1: Add --color-error to theme and convert inline styles to Tailwind
Small standalone changes with no cross-dependencies.
Files:
- Modify:
app/src/styles/theme.css - Modify:
app/src/components/SpotifyPlayer/SpotifyPlayer.astro - Modify:
app/src/components/SectionHeader/SectionHeader.astro - Modify:
app/src/components/Card/LinkCard.astro - Modify:
app/src/pages/index.astro:64 - Modify:
app/src/components/CommandPalette/CommandPalette.astro:157
Step 1: Add --color-error to theme.css
In app/src/styles/theme.css, add inside @theme {} block after --color-admin:
--color-error: #ff4444;Step 2: Replace inline styles with Tailwind classes
In app/src/components/SpotifyPlayer/SpotifyPlayer.astro:
Line 5 — remove style="transform: scale(0.88); transform-origin: bottom left; margin-right: -12%;", add Tailwind classes:
<div class="mt-auto pt-3 scale-[0.88] origin-bottom-left mr-[-12%]">Line 16 — remove style="border-radius:12px;", add class:
loading="lazy" class="rounded-xl"></iframe>In app/src/components/SectionHeader/SectionHeader.astro:
Line 21 — remove style="font-size:0.55em", add class:
<i class="fa-solid fa-chevron-right text-[0.55em]" aria-hidden="true" />In app/src/components/Card/LinkCard.astro:
Line 59 — remove style="font-size:10px", add class:
<i class="fa-solid fa-arrow-up-right-from-square text-muted opacity-60 text-[10px]" aria-hidden="true"In app/src/pages/index.astro:
Line 64 — remove style="min-width:max-content", add class:
<div class="home-timeline__track flex gap-6 pb-4 relative min-w-max">In app/src/components/CommandPalette/CommandPalette.astro:
Line 157 — remove style="font-size:10px", add class:
style="font-size:10px" → class removed, Tailwind addedChange:
<i class="fa-solid fa-arrow-up-right-from-square text-muted opacity-60" style="font-size:10px" aria-hidden="true"></i>To:
<i class="fa-solid fa-arrow-up-right-from-square text-muted opacity-60 text-[10px]" aria-hidden="true"></i>Step 3: Build to verify
Run: just app::build
Expected: Build succeeds with no errors.
Step 4: Commit
git add app/src/styles/theme.css app/src/components/SpotifyPlayer/SpotifyPlayer.astro app/src/components/SectionHeader/SectionHeader.astro app/src/components/Card/LinkCard.astro app/src/pages/index.astro app/src/components/CommandPalette/CommandPalette.astrogit commit -m "refactor: replace inline styles with Tailwind utilities and add --color-error to theme"Task 2: Migrate ProjectTree component (HTML + CSS + TS + cross-refs)
Migrate all ProjectTree IDs to data attributes. Update ALL files that reference ProjectTree elements.
Files:
- Modify:
app/src/components/ProjectTree/ProjectTree.astro - Modify:
app/src/components/ProjectTree/ProjectTree.ts - Modify:
app/src/layouts/BaseLayout/BaseLayout.ts(lines 97, 102 —site-navrefs) - Modify:
app/src/components/CommandPalette/CommandPalette.ts(lines 65, 315 —site-nav,js-nav-search-btnrefs) - Modify:
app/src/components/Chat/chat-client.ts(line 396 —js-admin-linkref)
Step 1: Update ProjectTree.astro HTML
Line 16 — change id="site-nav" to data-nav="root" class="nav-root ...":
<nav data-nav="root" class="nav-root font-body relative xl:flex xl:flex-col min-h-full overflow-y-auto overflow-x-hidden hidden xl:shrink-0 xl:grow-0 xl:basis-1/5 xl:max-w-64 xl:py-3 xl:pl-3 xl:pr-0" aria-label="Main navigation" data-active-collection={activeCollection || ""}>Line 28 — change id="js-nav-search-btn" to data-nav="search-btn":
id="js-nav-search-btn" → data-nav="search-btn"Line 69 — change id="js-admin-link" to data-nav="admin-link":
id="js-admin-link" → data-nav="admin-link"Step 2: Update ProjectTree.astro <style> block
Line 80 — change :global(#nav-panel) #site-nav to :global(.layout-nav-panel) .nav-root:
<style> :global(.layout-nav-panel) .nav-root { @apply flex flex-col p-3; }</style>Step 3: Update ProjectTree.ts
Line 2 — change getElementById("site-nav") to querySelector:
const siteNav = document.querySelector<HTMLElement>('[data-nav="root"]');Line 9 — the .nav-search-shortcut querySelector is dead code (no element has this class). Remove or keep as-is. Since the element with ⌘K text is a <kbd> with only Tailwind classes (line 32 of ProjectTree.astro), this code never matches. Remove the dead code:
Replace lines 5-12:
// Platform detection for keyboard shortcut hint const platform = (navigator as Navigator & { userAgentData?: { platform: string } }).userAgentData?.platform ?? navigator.platform; const isMac = platform.toUpperCase().includes("MAC"); const shortcutHint = siteNav.querySelector(".nav-search-shortcut"); if (shortcutHint) { shortcutHint.textContent = isMac ? "⌘K" : "Ctrl+K"; }With (using a data attribute for the kbd element):
// Platform detection for keyboard shortcut hint const platform = (navigator as Navigator & { userAgentData?: { platform: string } }).userAgentData?.platform ?? navigator.platform; const isMac = platform.toUpperCase().includes("MAC"); const shortcutHint = siteNav.querySelector<HTMLElement>('[data-nav="shortcut-hint"]'); if (shortcutHint) { shortcutHint.textContent = isMac ? "⌘K" : "Ctrl+K"; }And add data-nav="shortcut-hint" to the kbd element in ProjectTree.astro line 32:
<kbd data-nav="shortcut-hint" class="text-xs py-0.5 px-2 border border-border rounded bg-surface-active text-muted font-body">⌘K</kbd>Step 4: Update cross-component references
In app/src/layouts/BaseLayout/BaseLayout.ts:
Line 97 — change getElementById("site-nav"):
const nav = document.querySelector<HTMLElement>('[data-nav="root"]');Line 102 — same:
const nav = document.querySelector<HTMLElement>('[data-nav="root"]');In app/src/components/CommandPalette/CommandPalette.ts:
Line 65 — change getElementById("site-nav"):
const nav = document.querySelector<HTMLElement>('[data-nav="root"]');Line 315 — change querySelectorAll("#js-nav-search-btn"):
document.querySelectorAll('[data-nav="search-btn"]').forEach((btn) => {In app/src/components/Chat/chat-client.ts:
Line 396 — change querySelectorAll("#js-admin-link"):
const adminLinks = document.querySelectorAll<HTMLAnchorElement>('[data-nav="admin-link"]');Step 5: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 6: Commit
git add app/src/components/ProjectTree/ app/src/layouts/BaseLayout/BaseLayout.ts app/src/components/CommandPalette/CommandPalette.ts app/src/components/Chat/chat-client.tsgit commit -m "refactor: migrate ProjectTree IDs to data-nav attributes"Task 3: Migrate Chat component (HTML + CSS + TS + cross-refs)
Migrate all Chat IDs to data attributes. Update CSS to use classes.
Files:
- Modify:
app/src/components/Chat/Chat.astro - Modify:
app/src/components/Chat/chat-client.ts - Modify:
app/src/layouts/BaseLayout/BaseLayout.ts(lines 5, 75 —js-chat-panel,chat-overlay-closerefs)
Step 1: Update Chat.astro HTML
Line 6 — id="js-chat-panel" → data-chat="panel":
<aside data-chat="panel" class="hidden lg:flex lg:flex-col lg:border-l lg:border-border font-body overflow-y-auto overflow-x-hidden lg:shrink-0 lg:grow-0 lg:w-80 lg:min-w-80" transition:persist="chat">Line 13 — id="js-chat-owner-wrap" → data-chat="owner-wrap":
<span class="flex items-center gap-1" data-chat="owner-wrap" title="Site owner: offline">Line 15 — id="js-chat-status-dot" → data-chat="owner-status-dot":
data-chat="owner-status-dot"Note: keep the existing classes. Full element:
<span data-chat="owner-status-dot" class="inline-block self-center shrink-0 w-1.5 h-1.5 rounded-full bg-muted/50 data-online:bg-neon/70"></span>Line 17 — id="js-chat-owner-status" → data-chat="owner-status":
<span data-chat="owner-status" class="text-admin">offline</span>Line 20 — id="js-chat-user-count" → data-chat="user-count":
<span data-chat="user-count" title="0 online"Line 24 — id="chat-overlay-close" → data-chat="overlay-close":
<button data-chat="overlay-close" class="hidden" type="button" aria-label="Close chat">Line 30 — id="js-chat-messages" → data-chat="messages":
<div data-chat="messages" class="flex-1 text-sm text-text overflow-y-auto min-h-0"></div>Lines 31-45 — id="js-chat-username-row" → use <label> wrapping for accessibility (removes need for for/id):
<label data-chat="username-row" class="flex items-center gap-1.5"> <span class="whitespace-nowrap uppercase tracking-widest font-body text-2xs text-muted"> chatting as </span> <input data-chat="username-input" class="chat-username-input flex-1 py-0.5 px-1 text-xs font-body bg-transparent text-text min-w-0 focus:outline-none" type="text" autocomplete="off" minlength="2" maxlength="20" pattern="[a-z0-9_.-]+" title="2-20 characters: lowercase letters, numbers, hyphens, underscores, dots. No spaces." /> </label>Line 48 — id="js-chat-input" → data-chat="input":
data-chat="input"Line 55 — id="js-chat-send" → data-chat="send":
data-chat="send"Line 63 — id="chat-msg-tpl" → data-chat="msg-tpl":
<template data-chat="msg-tpl">Line 98 — id="chat-notice-tpl" → data-chat="notice-tpl":
<template data-chat="notice-tpl">Step 2: Update Chat.astro <style> block
Replace the entire <style> block (lines 113-133):
<style> .chat-username-input { border: none; border-bottom: 1px dashed var(--color-border); border-radius: 0; box-shadow: none; } .chat-username-input:focus { border-bottom-color: var(--color-teal); } .chat-username-input:read-only { border-bottom: none; cursor: default; } .chat-delete-confirm { color: #f87171; } .chat-flag-confirm { color: #facc15; }</style>Note: data-confirm state was using nonexistent CSS vars (--color-red-400, --color-yellow-400) with hardcoded fallbacks. The new class-based approach uses the fallback colors directly.
Step 3: Update chat-client.ts
Replace all getElementById calls (lines 84-92):
const usernameInput = document.querySelector<HTMLInputElement>('[data-chat="username-input"]')!;const usernameRow = document.querySelector<HTMLLabelElement>('[data-chat="username-row"]')!;const messagesEl = document.querySelector<HTMLDivElement>('[data-chat="messages"]')!;const inputEl = document.querySelector<HTMLInputElement>('[data-chat="input"]')!;const sendBtn = document.querySelector<HTMLButtonElement>('[data-chat="send"]')!;const statusDotEl = document.querySelector<HTMLSpanElement>('[data-chat="owner-status-dot"]')!;const ownerStatusEl = document.querySelector<HTMLSpanElement>('[data-chat="owner-status"]')!;const ownerWrapEl = document.querySelector<HTMLSpanElement>('[data-chat="owner-wrap"]')!;const userCountEl = document.querySelector<HTMLSpanElement>('[data-chat="user-count"]')!;Replace template element queries (lines 106-107):
const msgTpl = document.querySelector<HTMLTemplateElement>('[data-chat="msg-tpl"]')!;const noticeTpl = document.querySelector<HTMLTemplateElement>('[data-chat="notice-tpl"]')!;For the data-confirm → class-based approach, search the codebase for where data-confirm is set. If it’s set in JS (e.g., el.dataset.confirm = ""), change it to el.classList.add("chat-delete-confirm") / el.classList.add("chat-flag-confirm"). If not found, the CSS rules are dead code — keep the new class-based versions for future use.
Step 4: Update BaseLayout.ts cross-refs
Line 5 — change getElementById("js-chat-panel"):
const chatPanel = document.querySelector<HTMLElement>('[data-chat="panel"]');Line 75 — change getElementById("chat-overlay-close"):
const chatOverlayClose = document.querySelector<HTMLElement>('[data-chat="overlay-close"]');Step 5: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 6: Commit
git add app/src/components/Chat/ app/src/layouts/BaseLayout/BaseLayout.tsgit commit -m "refactor: migrate Chat IDs to data-chat attributes and class selectors"Task 4: Migrate BaseLayout component (HTML + CSS + TS + cross-refs)
Migrate all BaseLayout-owned IDs to data attributes.
Files:
- Modify:
app/src/layouts/BaseLayout/BaseLayout.astro - Modify:
app/src/layouts/BaseLayout/BaseLayout.ts - Modify:
app/src/components/CommandPalette/CommandPalette.ts(line 324 —toolbar-search-btnref)
Step 1: Update BaseLayout.astro HTML
Line 68 — id="toolbar-search-btn" → data-layout="search-btn":
<button data-layout="search-btn" class="bg-transparent border-none p-2 text-muted hover:text-neon" type="button" aria-label="Search" >Line 76 — id="js-toolbar-menu-btn" → data-layout="menu-btn":
<button data-layout="menu-btn" class="bg-transparent border-none p-2 text-muted hover:text-neon" type="button" aria-label="Open navigation" >Line 86 — id="nav-backdrop" → data-layout="nav-backdrop" class="layout-nav-backdrop ...":
<div data-layout="nav-backdrop" class="layout-nav-backdrop hidden fixed top-12 left-0 right-0 lg:right-80 h-[calc(100svh-48px)] z-40 bg-midnight/70 backdrop-blur-xs xl:hidden" >Line 90 — id="chat-overlay-backdrop" → data-layout="chat-overlay-backdrop" class="layout-chat-overlay-backdrop ...":
<div data-layout="chat-overlay-backdrop" class="layout-chat-overlay-backdrop hidden fixed inset-0 z-40 bg-midnight/70 backdrop-blur-xs lg:hidden"></div>Line 92 — id="nav-panel" → data-layout="nav-panel" class="layout-nav-panel ...":
<div data-layout="nav-panel" class="layout-nav-panel fixed top-12 left-0 right-0 lg:right-80 max-h-[calc(100svh-3rem)] overflow-y-auto bg-midnight/95 backdrop-blur-md border-b border-border z-50 -translate-y-[calc(100%+4px)] transition-[translate] duration-300 ease-in-out xl:hidden" >Line 107 — id="js-mobile-chat-fab" → data-layout="chat-fab":
<button data-layout="chat-fab" class="fixed bottom-4 right-4 z-30 w-12 h-12 rounded-full bg-teal text-midnight flex items-center justify-center shadow-lg lg:hidden hover:bg-neon" type="button" aria-label="Open chat" >Line 115 — id="chat-overlay" → data-layout="chat-overlay" class="layout-chat-overlay ...":
<div data-layout="chat-overlay" class="layout-chat-overlay fixed bottom-0 left-0 w-full h-[85vh] bg-midnight border-t border-border z-50 translate-y-full transition-[translate] duration-300 ease-in-out lg:hidden flex flex-col px-4 pt-2 pb-4" >Line 118 — id="js-chat-overlay-content" → data-layout="chat-overlay-content":
<div data-layout="chat-overlay-content" class="flex flex-col flex-1 min-h-0 font-body gap-2"></div>Step 2: Update BaseLayout.astro <style is:global> block
Replace the entire <style is:global> block (lines 125-150):
<style is:global> .layout-nav-backdrop.visible, .layout-chat-overlay-backdrop.visible { @apply block; }
.layout-nav-panel.open { translate: 0; }
.layout-chat-overlay.open { translate: 0; }
.layout-chat-overlay .layout-chat-overlay-close { @apply block bg-transparent border-none p-1 text-muted; }
.layout-chat-overlay .layout-chat-overlay-close:hover { @apply text-neon; }</style>Note: The close button inside Chat.astro also needs the .layout-chat-overlay-close class. Add it to the button in Chat.astro:
<button data-chat="overlay-close" class="layout-chat-overlay-close hidden" type="button" aria-label="Close chat">Step 3: Update BaseLayout.ts
Replace all getElementById calls and style.overflow assignments. Full rewrite of lines 1-9:
function initResponsiveUI() { const menuBtn = document.querySelector<HTMLElement>('[data-layout="menu-btn"]'); const navPanel = document.querySelector<HTMLElement>('[data-layout="nav-panel"]'); const navBackdrop = document.querySelector<HTMLElement>('[data-layout="nav-backdrop"]'); const chatPanel = document.querySelector<HTMLElement>('[data-chat="panel"]'); const chatFab = document.querySelector<HTMLElement>('[data-layout="chat-fab"]'); const chatOverlay = document.querySelector<HTMLElement>('[data-layout="chat-overlay"]'); const chatOverlayBackdrop = document.querySelector<HTMLElement>('[data-layout="chat-overlay-backdrop"]'); const chatOverlayContent = document.querySelector<HTMLElement>('[data-layout="chat-overlay-content"]');Replace all document.body.style.overflow occurrences:
Line 14 — document.body.style.overflow = ""; → document.body.classList.remove("overflow-hidden");
Line 35 — document.body.style.overflow = ""; → document.body.classList.remove("overflow-hidden");
Line 50 — document.body.style.overflow = "hidden"; → document.body.classList.add("overflow-hidden");
Line 68 — document.body.style.overflow = "hidden"; → document.body.classList.add("overflow-hidden");
Line 99 — document.body.style.overflow = ""; → document.body.classList.remove("overflow-hidden");
Line 75 — already updated in Task 3 (chat-overlay-close → data-chat=“overlay-close”).
Lines 97, 102 — already updated in Task 2 (site-nav → data-nav=“root”).
Step 4: Update CommandPalette.ts cross-ref
Line 324 — change getElementById("toolbar-search-btn"):
const tabletSearchBtn = document.querySelector<HTMLElement>('[data-layout="search-btn"]');Step 5: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 6: Commit
git add app/src/layouts/BaseLayout/ app/src/components/CommandPalette/CommandPalette.ts app/src/components/Chat/Chat.astrogit commit -m "refactor: migrate BaseLayout IDs to data-layout attributes and class selectors"Task 5: Migrate CommandPalette component (HTML + CSS + TS)
All cross-component refs already updated in previous tasks. This task handles CommandPalette’s own elements.
Files:
- Modify:
app/src/components/CommandPalette/CommandPalette.astro - Modify:
app/src/components/CommandPalette/CommandPalette.ts
Step 1: Update CommandPalette.astro HTML
Line 19 — id="command-palette" → data-cp="overlay" class="cp-overlay ...":
<div data-cp="overlay" class="cp-overlay hidden fixed inset-0 z-1000 overflow-hidden" aria-hidden="true">Line 20 — id="js-cp-backdrop" → data-cp="backdrop":
<div data-cp="backdrop" class="absolute inset-0 bg-midnight/70 backdrop-blur-sm"></div>Line 22 — id="js-cp-dialog" → data-cp="dialog" class="cp-dialog ...":
data-cp="dialog" class="cp-dialog relative w-full bg-surface overflow-hidden font-body flex flex-col shadow-dialog"Line 29 — id="js-cp-collection-select" → data-cp="collection-select":
data-cp="collection-select"Line 37 — id="js-cp-input" → data-cp="input":
data-cp="input"Line 44 — id="cp-results" → data-cp="results" class="cp-results ...":
<div class="cp-results overflow-y-auto flex-1 p-2" data-cp="results"></div>Line 104 — id="cp-row-tpl" → data-cp="row-tpl":
<template data-cp="row-tpl">Line 129 — id="cp-row-external-tpl" → data-cp="row-external-tpl":
<template data-cp="row-external-tpl">Line 164 — id="cp-empty-tpl" → data-cp="empty-tpl":
<template data-cp="empty-tpl">Step 2: Update CommandPalette.astro <style> blocks
Replace scoped <style> block (lines 61-81):
<style> .cp-overlay.cp-overlay--open { @apply flex items-center justify-center; } .cp-dialog { max-width: calc(100% - 2rem); @apply rounded-lg; max-height: calc(100svh - 6rem); } @media (min-width: 768px) { .cp-dialog { max-width: 32rem; max-height: 60%; } } .cp-input::placeholder { color: color-mix(in srgb, var(--color-muted) 60%, transparent); }</style>Replace <style is:global> block (lines 82-102):
<style is:global> .cp-results .cp-row--selected { @apply bg-surface-active text-white; } .cp-results .cp-row--selected .cp-row__title { @apply text-white; } .cp-results .cp-row__img--empty span { @apply gradient-text; } .cp-results .cp-row__initial { @apply gradient-text; }</style>Step 3: Update CommandPalette.ts
Replace all remaining getElementById calls.
Lines 45-53:
const overlay = document.querySelector<HTMLElement>('[data-cp="overlay"]')!; const input = document.querySelector<HTMLInputElement>('[data-cp="input"]')!; const resultsContainer = document.querySelector<HTMLElement>('[data-cp="results"]')!; const collectionSelect = document.querySelector<HTMLSelectElement>('[data-cp="collection-select"]')!;
if (!overlay || !input || !resultsContainer || !collectionSelect) return;
const backdrop = document.querySelector<HTMLElement>('[data-cp="backdrop"]'); const dialog = document.querySelector<HTMLElement>('[data-cp="dialog"]');Lines 171-173:
const cpRowTpl = document.querySelector<HTMLTemplateElement>('[data-cp="row-tpl"]')!; const cpRowExternalTpl = document.querySelector<HTMLTemplateElement>('[data-cp="row-external-tpl"]')!; const cpEmptyTpl = document.querySelector<HTMLTemplateElement>('[data-cp="empty-tpl"]')!;Lines 315, 324 — already updated in Tasks 2 and 4.
Step 4: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 5: Commit
git add app/src/components/CommandPalette/git commit -m "refactor: migrate CommandPalette IDs to data-cp attributes and class selectors"Task 6: Migrate Homepage (index.astro) — JS + CSS cleanup
Files:
- Modify:
app/src/pages/index.astro
Step 1: Update HTML
Line 28 — id="js-greeting" → data-home="greeting":
<span data-home="greeting">Hey</span>, I'm ThalidaStep 2: Update inline <script>
Line 116 — change getElementById("js-greeting"):
const el = document.querySelector<HTMLElement>('[data-home="greeting"]');Step 3: Update <style> block CSS cleanup
In the <style> block, convert raw CSS to @apply where possible:
Line 177 — add @apply for positioning:
.home-timeline__track::before { content: ""; @apply absolute border-t border-border top-[35px] left-0 right-0; }Lines 190-192 — use theme token:
.home-timeline__name { @apply text-2xs; }Step 4: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 5: Commit
git add app/src/pages/index.astrogit commit -m "refactor: migrate homepage IDs to data-home and clean up CSS"Task 7: Clean up post page CSS ([…id].astro)
Files:
- Modify:
app/src/pages/[collection]/post/[...id].astro
Step 1: Convert raw CSS to @apply in <style> block
Lines 187-194 — convert .heading-anchor raw CSS:
.post-content :global(.heading-anchor) { @apply opacity-0 ml-[0.35rem] text-muted no-underline font-normal transition-opacity duration-150; }Lines 216-221 — convert img/video raw CSS:
.post-content :global(img), .post-content :global(video) { @apply max-w-full h-auto rounded-lg; }Step 2: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 3: Commit
git add app/src/pages/[collection]/post/git commit -m "refactor: convert post page raw CSS to @apply"Task 8: Refactor login page onto BaseLayout + Tailwind
Files:
- Modify:
app/src/pages/login.astro
Step 1: Rewrite login.astro
Replace the entire file. The page now uses BaseLayout and Tailwind:
---const wsUrl = import.meta.env.PUBLIC_CHAT_WS_URL || "ws://localhost:8787/ws";const LS_ADMIN_TOKEN_KEY = "admin_token";const apiBase = wsUrl.replace(/^ws(s?):/, "http$1:").replace(/\/ws$/, "");---
<BaseLayout title="login · thalida"> <div class="flex items-center justify-center min-h-[calc(100vh-6rem)]"> <div class="max-w-[360px] w-full p-8"> <h1 class="m-0 mb-6 font-display font-bold text-neon text-2xl">Login</h1> <form data-login="form" class="flex flex-col gap-3"> <input data-login="secret" type="password" placeholder="Enter password..." autocomplete="current-password" required class="py-2.5 px-3 border border-border rounded-md bg-surface text-text font-body text-sm focus:outline-none focus:border-teal" /> <button type="submit" class="py-2.5 px-5 border-none rounded-md bg-teal text-midnight font-heading font-bold text-sm cursor-pointer hover:bg-neon transition-colors" > Log in </button> <p data-login="error" class="login-error m-0 text-error text-sm" hidden></p> </form> <p class="mt-6"> <a href="/" class="text-teal text-sm no-underline hover:text-neon">← back to chat</a> </p> </div> </div></BaseLayout>
<script is:inline define:vars={{ apiBase, LS_ADMIN_TOKEN_KEY }}> const form = document.querySelector('[data-login="form"]'); const input = document.querySelector('[data-login="secret"]'); const errorEl = document.querySelector('[data-login="error"]');
form.addEventListener("submit", async (e) => { e.preventDefault(); const secret = input.value.trim(); if (!secret) return;
errorEl.hidden = true; form.querySelector("button").disabled = true;
try { const resp = await fetch(`${apiBase}/auth`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ token: secret }), }); if (!resp.ok) { errorEl.textContent = "Invalid password."; errorEl.hidden = false; return; } } catch { errorEl.textContent = "Could not reach the API. Is the server running?"; errorEl.hidden = false; return; } finally { form.querySelector("button").disabled = false; }
localStorage.setItem(LS_ADMIN_TOKEN_KEY, secret); window.location.href = "/"; });</script>Note: Add import for BaseLayout at the top of the frontmatter:
import BaseLayout from "@layouts/BaseLayout/BaseLayout.astro";Step 2: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 3: Commit
git add app/src/pages/login.astrogit commit -m "refactor: migrate login page to BaseLayout with Tailwind"Task 9: Refactor logout page onto BaseLayout + Tailwind
Files:
- Modify:
app/src/pages/logout.astro
Step 1: Rewrite logout.astro
---import BaseLayout from "@layouts/BaseLayout/BaseLayout.astro";const LS_ADMIN_TOKEN_KEY = "admin_token";---
<BaseLayout title="logging out… · thalida"> <div class="flex items-center justify-center min-h-[calc(100vh-6rem)]"> <p class="font-body text-text text-sm">Logging out…</p> </div>
<script is:inline define:vars={{ LS_ADMIN_TOKEN_KEY }}> localStorage.removeItem(LS_ADMIN_TOKEN_KEY); window.location.href = "/"; </script>
<noscript> <p class="font-body text-text text-sm">JavaScript is required to log out. <a href="/" class="text-teal">Go back</a>.</p> </noscript></BaseLayout>Step 2: Build to verify
Run: just app::build
Expected: Build succeeds.
Step 3: Commit
git add app/src/pages/logout.astrogit commit -m "refactor: migrate logout page to BaseLayout with Tailwind"Task 10: Final build and typecheck verification
Step 1: Run typecheck
Run: just app::typecheck
Expected: No type errors.
Step 2: Run full build
Run: just app::build
Expected: Build succeeds with no errors or warnings.
Step 3: Verify no remaining IDs
Search for any remaining id= attributes in app source (excluding node_modules, dist):
grep -rn 'id=' app/src/ --include='*.astro' --include='*.ts' | grep -v 'data-' | grep -v 'clientId' | grep -v 'node_modules' | grep -v 'itemId' | grep -v 'msg.id' | grep -v 'data.id' | grep -v 'result.id' | grep -v '\.id ' | grep -v 'className'Expected: No HTML id= attributes remain on elements (some .id property accesses in TS are fine — those are data properties, not HTML IDs).
Step 4: Verify no remaining inline styles
grep -rn 'style=' app/src/ --include='*.astro' | grep -v 'node_modules' | grep -v '<style' | grep -v 'is:global' | grep -v 'CardCover'Expected: Only CardCover.astro (runtime-computed, approved exception) should have style=.
Step 5: Verify no remaining getElementById
grep -rn 'getElementById' app/src/ --include='*.ts' | grep -v 'node_modules'Expected: No results.
Step 6: Verify no remaining document.body.style
grep -rn 'document.body.style' app/src/ --include='*.ts' | grep -v 'node_modules'Expected: No results.