Implemented a three-layer theming architecture to standardize admin component styling and prepare for future dark mode support. **Architecture:** - Layer 1: Base colors ($gray-80, $red-60) in variables.scss - Layer 2: Semantic SCSS variables ($input-bg, $error-bg) in variables.scss - Layer 3: CSS custom properties (--input-bg, --error-bg) in themes.scss **New semantic variables (~30 added):** - Inputs & forms (bg, hover, focus, text, border states) - State messages (error, success, warning with bg/text/border) - Empty states (text, heading colors) - Cards, dropdowns, popovers, modals (bg, border, shadow) **New reusable components:** - EmptyState.svelte - Supports icon and action snippets - ErrorMessage.svelte - Supports dismissible errors **Pages refactored:** - /admin/projects - Uses EmptyState and ErrorMessage (~30 lines removed) - /admin/posts - Uses EmptyState and ErrorMessage with icon (~30 lines removed) **Benefits:** - 60+ lines of duplicate styles removed (just 2 pages) - Future dark mode = remap CSS variables in themes.scss only - Guaranteed visual consistency for errors and empty states - $unit-based spacing system enforced **Remaining work (Phase 2):** - Replace hardcoded colors in ~40 files - Fix hardcoded spacing in ~20 files - Expand EmptyState/ErrorMessage to remaining pages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
Admin Interface Modernization Plan
Progress Overview
Current Status: Phase 4 In Progress 🚧 (Task 7 Phase 1 Complete)
- ✅ Phase 0: Runed integration (Task 0)
- ✅ Phase 1: Auth & data foundation (Tasks 1, 2)
- ✅ Phase 2: Form modernization (Tasks 3, 6)
- ✅ Phase 3: List utilities & primitives (Tasks 4, 5)
- 🚧 Phase 4: Styling harmonization (Task 7) - IN PROGRESS
Recent Completions:
- Task 7 Phase 1 - Styling & Theming Foundation (Oct 8, 2025)
- Created 3-layer theming architecture (SCSS → CSS variables)
- Added ~30 semantic SCSS variables for components
- Built EmptyState and ErrorMessage reusable components
- Refactored projects and posts pages (~60 lines removed)
- Task 5 - Dropdown & Click-Outside Primitives (Oct 8, 2025)
- Documented existing implementation (~85% already done)
- Cleaned up GenericMetadataPopover to use clickOutside action
- Task 4 - Shared List Filtering Utilities (Oct 8, 2025)
- Removed ~100 lines of duplicated filter/sort code
- Integrated into projects and posts lists
Goals
- Deliver an admin surface that uses idiomatic Svelte 5 + Runes with first-class TypeScript.
- Replace client-side authentication fallbacks with server-validated sessions and consistent typing.
- Reduce duplication across resource screens (projects, posts, media) by extracting reusable list, form, and dropdown primitives.
- Improve reliability by centralizing data loading, mutation, and invalidation logic.
Guiding Principles
- Prefer
+layout.server.ts/+page.server.tswith typedloadresults overonMountfetches; usesatisfiesclauses for strong typing. - Use Svelte runes (
$derived,$state,$effect) inside components, but push cross-route state into stores orloaddata. - Model mutations as form
actions(with optionalenhance) to avoid bespokefetchcalls and to keep optimistic UI localized. - Encode shared behaviors (filters, dropdowns, autosave) as reusable helpers or actions so we can verify and test them once.
- Annotate shared helpers with explicit generics, exported types, and narrow
ReturnTypehelpers for downstream safety. - Leverage the Runed utility library where it meaningfully reduces rune boilerplate while keeping bundle size in check.
Task 0 – Adopt Runed Utility Layer
Objective: Introduce Runed as a shared dependency for rune-focused utilities, formalize usage boundaries, and pilot it in list/data flows.
Steps
- Add the dependency:
pnpm add runed(or equivalent) and ensure type declarations are available to the TypeScript compiler. - Create
src/lib/runed/README.mddocumenting approved utilities (e.g.,asyncState,memo,taskQueue,clickOutside) and guidelines for contributions. - Establish a thin wrapper export in
src/lib/runed/index.tsso future refactors can swap implementations without touching call sites. - Update Task 2 prototype (projects list) to replace manual async state handling with
resourceand memoized filters via$derivedhelpers. - Evaluate bundle impact via
pnpm run buildand record findings in the doc, adjusting the allowed utility list if necessary.
Current Adoption: Projects index page now uses resource for data fetching and onClickOutside for dropdowns as the pilot integration.
Implementation Notes
- Prefer wrapping Runed utilities so downstream components import from a single local module (
import { asyncState } from '$lib/runed'). - Pair Runed helpers with
satisfiesclauses to keep returned state strongly typed. - Audit for tree-shaking compliance; Runed utilities are individually exported to support dead code elimination.
Dependencies
- None; execute before Task 1 to unlock downstream usage.
Task 1 – Server-Side Authentication & Session Flow
Objective: Move credential validation out of the browser and expose typed session data to all admin routes.
Steps
- Create
src/routes/admin/+layout.server.tsthat:- Reads an HttpOnly cookie (e.g.,
admin_session). - Validates credentials via shared server utility (reusable by API routes).
- Returns
{ user }(ornull) while throwingredirect(303, '/admin/login')for unauthenticated requests.
- Reads an HttpOnly cookie (e.g.,
- Add
src/routes/admin/login/+page.server.tswith:- A
loadthat returns any flash errors. - A default
actionsexport that validates the submitted password, sets the cookie viacookies.set, andredirects into/admin.
- A
- Update
src/routes/admin/+layout.svelteto:- Remove
onMount,$pagederived auth checks, andgotousage. - Read the session via
const { user } = await parent()and gate rendering accordingly. - Handle the login route by checking
datafrom parent rather than client state.
- Remove
- Replace all
localStorage.getItem('admin_auth')references (e.g.,Admin API, media page) with reliance on server session (see Task 2).
Implementation Notes
- Use
LayoutServerLoadtyping:export const load = (async (event) => { ... }) satisfies LayoutServerLoad;. - Define a
SessionUsertype insrc/lib/types/session.tsto share across routes and endpoint handlers. - For Basic auth compatibility during transition, consider reading the existing header and issuing the new cookie so legacy API calls keep working.
Dependencies
- Requires shared credential validation utility (see Task 2 Step 1).
- Requires infra support for HttpOnly cookie (name, maxAge, secure flag).
Task 2 – Unified Data Fetching & Mutation Pipeline
Objective: Standardize how admin pages load data and mutate resources with TypeScript-checked flows.
Steps
- Extract a server helper
src/lib/server/admin/authenticated-fetch.tsthat wrapsevent.fetch, injects auth headers if needed, and narrows error handling. - Convert project, post, media list routes to use server loads:
- Add
+page.server.tsreturning{ items, filters }withdepends('admin:projects')-style cache keys. - Update
+page.sveltefiles to readexport let dataand derive view state fromdata.items. - Use
$derivedto compute filtered lists inside the component rather than re-fetching.
- Add
- Replace manual
fetchcalls for mutations with typed form actions:- Define actions in
+page.server.ts(export const actions = { toggleStatus: async (event) => { ... } }). - In Svelte, use
<form use:enhance>orformwrappers to submit withfetch, readingevent.detail.result.
- Define actions in
- After successful mutations, call
invalidate('admin:projects')(client side) or returninvalidateinstructions within actions to refresh data.
Implementation Notes
- Leverage
type ProjectListData = Awaited<ReturnType<typeof load>>for consumer typing. - Use discriminated union responses from actions (
{ type: 'success'; payload: ... } | { type: 'error'; message: string }). - For media pagination, accept
url.searchParamsin the server load and returnpaginationmetadata for the UI.
Dependencies
- Requires Task 1 cookie/session handling.
- Coordinate with API endpoint typing to avoid duplicating DTO definitions (reuse from
src/lib/schemas/...).
Task 3 – Project Form Modularization & Store Extraction ✅
Status: ✅ COMPLETED (Oct 7, 2025) - Commit 34a3e37
Objective: Split ProjectForm.svelte into composable, typed stores and view modules.
Implementation Summary
Created reusable form patterns following Svelte 5 best practices:
New Files:
src/lib/stores/project-form.svelte.ts(114 lines) - Store factory with$state,$derived, validationsrc/lib/admin/useDraftRecovery.svelte.ts(62 lines) - Generic draft restoration with auto-detectionsrc/lib/admin/useFormGuards.svelte.ts(56 lines) - Navigation guards, beforeunload, Cmd+S shortcutssrc/lib/components/admin/DraftPrompt.svelte(92 lines) - Reusable draft prompt UI component
Refactored:
src/lib/components/admin/ProjectForm.svelte- Reduced from 720 → 417 lines (42% reduction)
Key Achievements
- All form state centralized in composable store
- Draft recovery, navigation guards fully extracted and reusable
- Type-safe with full generic support (
useDraftRecovery<TPayload>) - Patterns ready for PostForm, MediaForm, etc.
- Build passes, manual QA complete
Implementation Notes
- State returned directly from factories (no
readonlywrappers needed in Svelte 5) - Used
$state,$derived,$effectrunes throughout - Store factory uses
z.infer<typeof projectSchema>for type alignment - Exported
type ProjectFormStore = ReturnType<typeof createProjectFormStore>for downstream usage
Dependencies
- ✅ Task 2 (data fetching) - complete
- ✅ Task 6 (autosave store) - complete
Task 4 – Shared List Filtering Utilities ✅
Status: ✅ COMPLETED (Oct 8, 2025)
Objective: Remove duplicated filter/sort code across projects, posts, and media.
Implementation Summary
Created src/lib/admin/listFilters.svelte.ts with:
- Generic
createListFilters<T>(items, config)factory - Rune-backed reactivity using
$stateand$derived - Type-safe filter and sort configuration
ListFiltersResult<T>interface withvalues,items,count,set(),setSort(),reset()commonSortscollection with 8 reusable sort functions
Integrated into:
- ✅ Projects list (
/admin/projects) - ✅ Posts list (
/admin/posts) - ⏸️ Media list uses server-side pagination (intentionally separate)
Removed ~100 lines of duplicated filtering logic
Testing Approach
Rune-based utilities cannot be unit tested outside Svelte's compiler context. Instead, extensively integration-tested through actual usage in projects and posts pages. Manual QA complete for all filtering and sorting scenarios.
Documented in: docs/task-4-list-filters-completion.md
Implementation Notes
- Uses
export interface ListFiltersResult<T>for return type - Filters use exact equality comparison with special 'all' bypass
- Sorts use standard JavaScript comparator functions
- Media page intentionally uses manual filtering due to server-side pagination needs
Dependencies
- ✅ Task 2 (server loads provide initial data) - complete
Task 5 – Dropdown, Modal, and Click-Outside Primitives ✅
Status: ✅ COMPLETED (Oct 8, 2025) - Option A (Minimal Cleanup)
Objective: Centralize interaction patterns to reduce ad-hoc document listeners.
Implementation Summary
Task 5 was ~85% complete when reviewed. Core infrastructure already existed and worked well.
What Already Existed:
- ✅
src/lib/actions/clickOutside.ts- Full TypeScript implementation - ✅
BaseDropdown.svelte- Svelte 5 snippets + clickOutside integration - ✅ Dropdown primitives:
DropdownMenuContainer,DropdownItem,DropdownMenu - ✅ Used in ~10 components across admin interface
- ✅ Specialized dropdowns:
StatusDropdown,PostDropdown,PublishDropdown
Changes Made:
- Refactored
GenericMetadataPopover.svelteto use clickOutside action - Removed manual event listener code
- Documented remaining manual listeners as justified exceptions
Justified Exceptions (15 manual listeners remaining):
DropdownMenu.svelte- Complex submenu hierarchy (uses Floating UI)ProjectListItem.svelte+PostListItem.svelte- Global dropdown coordinationBaseModal.svelte+ forms - Keyboard shortcuts (Escape, Cmd+S)- Various - Scroll/resize positioning (layout, not interaction)
Documented in: docs/task-5-dropdown-primitives-completion.md
Implementation Notes
- Did not use Runed library (custom
clickOutsideis production-ready) - BaseDropdown uses Svelte 5 snippets for flexible composition
- Dropdown coordination uses custom event pattern (valid approach)
- Future: Could extract keyboard handling to actions (
useEscapeKey,useKeyboardShortcut)
Dependencies
- ✅ No external dependencies required
Task 6 – Autosave Store & Draft Persistence ✅
Status: ✅ COMPLETED (Earlier in Phase 2)
Objective: Turn autosave logic into a typed store for reuse across forms.
Implementation Summary
Created src/lib/admin/autoSave.svelte.ts with:
- Generic
createAutoSaveStore<TPayload, TResponse>(options)factory - Reactive status using
$state<AutoSaveStatus> - Methods:
schedule(),flush(),destroy(),prime() - Debounced saves with abort controller support
- Online/offline detection with automatic retry
- Draft persistence fallback when offline
Documented in: docs/autosave-completion-guide.md
Key Features
- Fully typed with TypeScript generics
- Integrates with
draftStore.tsfor localStorage fallback - Used successfully in refactored ProjectForm
- Reusable across all admin forms
Implementation Notes
- Returns reactive
$statefor status tracking - Accepts
onSavedcallback withprime()helper for baseline updates - Handles concurrent saves with abort controller
- Automatically transitions from 'saved' → 'idle' after delay
Dependencies
- ✅ Task 2 (mutation endpoints) - complete
Task 7 – Styling & Theming Harmonization 🚧
Status: 🚧 PHASE 1 COMPLETE (Oct 8, 2025)
Objective: Reduce SCSS duplication, standardize component styling, and prepare for future dark mode theming.
Phase 1: Foundation (Complete ✅)
Completed:
- ✅ Created 3-layer theming architecture:
- Base colors (
$gray-80,$red-60) invariables.scss - Semantic SCSS variables (
$input-bg,$error-bg) invariables.scss - CSS custom properties (
--input-bg,--error-bg) inthemes.scss
- Base colors (
- ✅ Added ~30 semantic SCSS variables for:
- Inputs & forms (bg, hover, focus, text, border)
- State messages (error, success, warning)
- Empty states
- Cards & containers
- Dropdowns & popovers
- Modals
- ✅ Created reusable components:
EmptyState.svelte- Replaces 10+ duplicate implementationsErrorMessage.svelte- Replaces 4+ duplicate implementations
- ✅ Refactored pages using new components:
/admin/projects- Removed ~30 lines of duplicate styles/admin/posts- Removed ~30 lines of duplicate styles
Results:
- 60+ lines of duplicated styles removed (2 pages)
- Theme-ready architecture for future dark mode
- Guaranteed visual consistency for errors and empty states
Phase 2: Rollout (Future)
Remaining work:
- ⏳ Replace hardcoded colors with semantic variables (~40 files)
rgba(239, 68, 68, 0.1)→$error-bg#dc2626→$error-text
- ⏳ Fix hardcoded spacing with $unit system (~20 files)
padding: 24px→$unit-3xmargin: 12px→calc($unit * 1.5)
- ⏳ Expand EmptyState usage to media, albums pages (~8 more usages)
- ⏳ Expand ErrorMessage usage across forms/modals (~4 more usages)
Implementation Notes
- Three-layer architecture enables dark mode without touching component code
- Components use SCSS variables; themes.scss maps to CSS custom properties
- Future dark mode = remap
[data-theme='dark']block in themes.scss - Documented in:
docs/task-7-styling-harmonization-completion.md
Dependencies
- ✅ No dependencies - can be done incrementally
Rollout Strategy
✅ Phase 0: Runed Integration (Complete)
- ✅ Task 0: Runed utility layer integrated and documented
- Projects index page using
resourcefor data fetching onClickOutsideimplemented for dropdowns
✅ Phase 1: Auth & Data Foundation (Complete)
- ✅ Task 1: Server-side authentication with session flow
- ✅ Task 2: Unified data fetching & mutation pipeline
- HttpOnly cookie authentication working
- Server loads with typed
satisfiesclauses
✅ Phase 2: Form Modernization (Complete)
- ✅ Task 6: Autosave store with draft persistence
- ✅ Task 3: Project form modularization with composable stores
- Reduced ProjectForm from 720 → 417 lines (42%)
- Reusable patterns ready for other forms
✅ Phase 3: List Utilities & Primitives (Complete)
- ✅ Task 4: Shared list filtering utilities (Oct 8, 2025)
- ✅ Task 5: Dropdown, modal, and click-outside primitives (Oct 8, 2025)
- Removed ~100 lines of duplicated filtering logic
- Standardized dropdown patterns across admin interface
🚧 Phase 4: Styling Harmonization (In Progress)
- 🚧 Task 7: Styling & theming cleanup (Phase 1 Complete)
- ✅ Semantic SCSS variable system
- ✅ CSS custom properties for theming
- ✅ EmptyState and ErrorMessage components
- ✅ Projects and posts pages refactored
- ⏳ Remaining: Hardcoded color/spacing fixes across 40+ files
Each task section above can serve as a standalone issue. Ensure QA includes regression passes for projects, posts, and media operations after every phase.