docs(ui): Add UI layout refactor plan and OpenSpec changes

- Update decision-log with UI layout decisions (Feb 18, 2026)
- Update architecture with frontend layout patterns
- Update config.yaml with TDD, documentation, UI standards rules
- Create p00-api-documentation change (Scribe annotations)
- Create p01-ui-foundation change (types, stores, Lucide)
- Create p02-app-layout change (AppLayout, Sidebar, TopBar)
- Create p03-dashboard-enhancement change (PageHeader, StatCard)
- Create p04-content-patterns change (DataTable, FilterBar)
- Create p05-page-migrations change (page migrations)
- Fix E2E auth tests (11/11 passing)
- Add JWT expiry validation to dashboard guard
This commit is contained in:
2026-02-18 13:03:08 -05:00
parent f935754df4
commit 3e36ea8888
29 changed files with 3341 additions and 59 deletions

View File

@@ -23,7 +23,7 @@
type LoginFormData = z.infer<typeof loginSchema>;
// Form data - use $state for reactivity
// Form data
let formData: LoginFormData = $state({
email: '',
password: '',
@@ -43,7 +43,9 @@
if (err instanceof z.ZodError) {
err.errors.forEach((error) => {
const field = error.path[0] as keyof LoginFormData;
errors[field] = error.message;
if (!errors[field]) {
errors[field] = error.message;
}
});
}
return false;
@@ -59,7 +61,7 @@
}
</script>
<form class="space-y-4" onsubmit={handleSubmit}>
<form class="space-y-4" onsubmit={handleSubmit} novalidate>
{#if errorMessage}
<div class="alert alert-error" role="alert">
<svg

View File

@@ -22,6 +22,33 @@ export function getRefreshToken(): string | null {
return localStorage.getItem(REFRESH_TOKEN_KEY);
}
export function isValidJwtFormat(token: string | null): boolean {
if (!token) return false;
const parts = token.split('.');
if (parts.length !== 3) return false;
return parts.every(part => part.length > 0);
}
export function isJwtExpired(token: string | null): boolean {
if (!isValidJwtFormat(token)) return true;
try {
const payload = token!.split('.')[1];
const normalized = payload.replace(/-/g, '+').replace(/_/g, '/');
const padded = normalized + '='.repeat((4 - (normalized.length % 4)) % 4);
const decoded = atob(padded);
const data = JSON.parse(decoded) as { exp?: number };
if (typeof data.exp !== 'number') {
return false;
}
return data.exp <= Math.floor(Date.now() / 1000);
} catch {
return true;
}
}
export function setTokens(accessToken: string, refreshToken: string): void {
if (typeof localStorage === 'undefined') return;
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);