From 2a6c55dacb456a8f40c8e7ba781b00adbf55a392 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Fri, 21 Aug 2026 12:41:18 -0400 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20auth-shared.css,=20apiErr?= =?UTF-8?q?orMessage,=20one=20date=20helper=20per=20shape,=20modal=20canon?= =?UTF-8?q?=20in=20components.css=20=E2=80=94=20the=20frontend=20pass=20of?= =?UTF-8?q?=20the=20shape=20audit=20(#2831=20#2832,=20milestone=20296)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - assets/auth-shared.css: the five auth views carried byte-identical scoped copies of the page/card/brand/footer/field/input/error rules (~60 lines each); they now load one stylesheet the way the editors load editor-shared.css. .closed-msg/.error-block/.success-msg (identical bodies) are one .auth-note; the form rules are scoped under .auth-card so nothing leaks into the rest of the app. - api/client.apiErrorMessage(e, fallback): the one place the {"error"} envelope is unpacked; replaces ten six-line `"body" in e` catch blocks. - utils/dateFormat: fmtDate / fmtStamp / fmtLogStamp replace eight local formatDate/formatTime copies (three byte-identical pairs); the file’s old Calendar/Home helpers had no callers and are gone. useRelativeTime gains relativeTimeOrDate for the two workspace panels’ identical variant. - components.css now owns the .modal-* shape (overlay/card/title/message/ actions/btn/primary/danger). It was copied into four views and lived in editor-shared.css, which ConfirmDialog — styleless, teleported to — silently depended on: opened from SnippetDetailView before any editor view had loaded, it rendered unstyled. Views keep only their own overrides. Co-Authored-By: Claude Fable 5 --- frontend/src/api/client.ts | 14 +++ frontend/src/assets/auth-shared.css | 115 ++++++++++++++++++ frontend/src/assets/components.css | 76 ++++++++++++ frontend/src/assets/editor-shared.css | 47 ------- frontend/src/components/HistoryPanel.vue | 11 +- frontend/src/components/SystemsSection.vue | 25 ---- frontend/src/components/TaskLogSection.vue | 10 +- .../src/components/WorkspaceNoteEditor.vue | 17 +-- .../src/components/WorkspaceTaskPanel.vue | 19 +-- frontend/src/composables/useRelativeTime.ts | 12 ++ frontend/src/utils/dateFormat.ts | 81 ++++-------- frontend/src/views/ForgotPasswordView.vue | 92 +------------- frontend/src/views/LoginView.vue | 81 +----------- frontend/src/views/LogsView.vue | 14 +-- frontend/src/views/ProjectListView.vue | 36 ------ frontend/src/views/ProjectView.vue | 25 ---- frontend/src/views/RegisterInviteView.vue | 114 +---------------- frontend/src/views/RegisterView.vue | 109 +---------------- frontend/src/views/ResetPasswordView.vue | 113 +---------------- frontend/src/views/SettingsView.vue | 46 ++----- frontend/src/views/SnippetListView.vue | 36 ------ frontend/src/views/UserManagementView.vue | 31 ++--- 22 files changed, 290 insertions(+), 834 deletions(-) create mode 100644 frontend/src/assets/auth-shared.css diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 93ffbea..286d247 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -38,6 +38,20 @@ async function handleResponse(res: Response, path: string): Promise { return res.json() as Promise; } +/** + * The server's `{"error": "..."}` message from a failed call, or `fallback` + * when the failure carried none (network error, non-JSON body). The one place + * the error envelope is unpacked on the client — views used to restate this + * as a six-line `"body" in e` branch at every catch site. + */ +export function apiErrorMessage(e: unknown, fallback: string): string { + if (e && typeof e === "object" && "body" in e) { + const body = (e as { body?: { error?: unknown } }).body; + if (body && typeof body.error === "string" && body.error) return body.error; + } + return fallback; +} + export async function apiGet(path: string): Promise { const res = await fetch(path); return handleResponse(res, path); diff --git a/frontend/src/assets/auth-shared.css b/frontend/src/assets/auth-shared.css new file mode 100644 index 0000000..fe507dd --- /dev/null +++ b/frontend/src/assets/auth-shared.css @@ -0,0 +1,115 @@ +/* ── Auth surface (Login / Register / RegisterInvite / ForgotPassword / ResetPassword) ── + The five auth views used to carry byte-identical copies of these rules in + their scoped blocks (2026-08 shape audit). Loaded per view with + diff --git a/frontend/src/components/TaskLogSection.vue b/frontend/src/components/TaskLogSection.vue index 57c917b..dc8ed2e 100644 --- a/frontend/src/components/TaskLogSection.vue +++ b/frontend/src/components/TaskLogSection.vue @@ -3,6 +3,7 @@ import { ref, onMounted } from "vue"; import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client"; import { renderMarkdown } from "@/utils/markdown"; import type { TaskLog } from "@/types/task"; +import { fmtStamp } from "@/utils/dateFormat"; const props = defineProps<{ taskId: number }>(); @@ -15,13 +16,6 @@ const editingId = ref(null); const editContent = ref(""); const editDuration = ref(""); -function formatDate(iso: string): string { - const d = new Date(iso); - const datePart = d.toLocaleDateString(undefined, { month: "short", day: "numeric", year: "numeric" }); - const timePart = d.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" }); - return `${datePart}, ${timePart}`; -} - function formatDuration(minutes: number): string { if (minutes < 60) return `${minutes} min`; const h = Math.floor(minutes / 60); @@ -128,7 +122,7 @@ onMounted(loadLogs);