From c5f93cf9f181f03bc1e22d13c23a8ba66fe960c4 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Tue, 6 Oct 2026 17:52:50 -0400 Subject: [PATCH] rename: the sign-in screen shows Inkwell's mark, and every tab says Inkwell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The login and register screens still drew a hard-coded "TS" tile. They now use /icon.svg, the same mark the shell's header shows. Browser tabs took index.html's static and never changed it, so every tab read the same, and some browsers showed the URL instead. usePageTitle, mounted once in App.vue, sets "<page> · <site name>". Routes outside the shell name themselves with meta.title. Board lenses use the lens name the header already shows, now in useLensName so the tab and the header read from one place. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- frontend/src/App.vue | 3 ++ frontend/src/components/AppShell.vue | 30 ++----------------- frontend/src/composables/useLensName.ts | 37 ++++++++++++++++++++++++ frontend/src/composables/usePageTitle.ts | 25 ++++++++++++++++ frontend/src/router/index.ts | 12 ++++---- frontend/src/views/LoginView.vue | 12 ++++---- frontend/src/views/RegisterView.vue | 14 +++++---- 7 files changed, 89 insertions(+), 44 deletions(-) create mode 100644 frontend/src/composables/useLensName.ts create mode 100644 frontend/src/composables/usePageTitle.ts diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 8d15d1f..85177f1 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,6 +1,9 @@ <script setup lang="ts"> import ToastHost from "./components/ToastHost.vue"; import DesktopIntegrationPrompt from "./components/DesktopIntegrationPrompt.vue"; +import { usePageTitle } from "./composables/usePageTitle"; + +usePageTitle(); </script> <template> diff --git a/frontend/src/components/AppShell.vue b/frontend/src/components/AppShell.vue index b4a9b6b..8320f39 100644 --- a/frontend/src/components/AppShell.vue +++ b/frontend/src/components/AppShell.vue @@ -13,6 +13,7 @@ import Icon from "./Icon.vue"; import ImportNotes from "./ImportNotes.vue"; import LabelsModal from "./LabelsModal.vue"; import { isDesktop } from "../desktop/bridge"; +import { useLensName } from "../composables/useLensName"; import { facetsToQuery } from "../notes/facets"; import { NOTE_SWATCH_CLASSES, resolveLabelColor } from "../notes/colors"; @@ -242,34 +243,7 @@ watch( }, ); -/** - * What to call the lens currently in view. - * - * Keyed off the route name rather than each view declaring its own title, so the - * label sits in one place and can't go missing (the board never had one) - * or drift in styling (timeline and reminders each had their own h1). - * - * A label lens is named by the label itself — "Groceries" is what the user came - * looking for; "Label" would tell them nothing they didn't already know. - */ -const lensName = computed<string>(() => { - switch (route.name) { - case "archive": - return "Archive"; - case "trash": - return "Trash"; - case "timeline": - return "Timeline"; - case "reminders": - return "Reminders"; - case "label": - // The store may not have loaded yet on a deep link; fall back rather than - // flashing an empty slot. - return labels.items.find((l) => l.id === String(route.params.id))?.name ?? "Label"; - default: - return "Notes"; - } -}); +const lensName = useLensName(); async function signOut() { await session.logout(); diff --git a/frontend/src/composables/useLensName.ts b/frontend/src/composables/useLensName.ts new file mode 100644 index 0000000..3b605ab --- /dev/null +++ b/frontend/src/composables/useLensName.ts @@ -0,0 +1,37 @@ +import { computed, type ComputedRef } from "vue"; +import { useRoute } from "vue-router"; +import { useLabelsStore } from "../stores/labels"; + +/** + * What to call the lens currently in view. + * + * Keyed off the route name rather than each view declaring its own title, so the + * label sits in one place and can't go missing (the board never had one) + * or drift in styling (timeline and reminders each had their own h1). The shell's + * header and the browser tab both read it, so the two can't disagree either. + * + * A label lens is named by the label itself — "Groceries" is what the user came + * looking for; "Label" would tell them nothing they didn't already know. + */ +export function useLensName(): ComputedRef<string> { + const route = useRoute(); + const labels = useLabelsStore(); + return computed<string>(() => { + switch (route.name) { + case "archive": + return "Archive"; + case "trash": + return "Trash"; + case "timeline": + return "Timeline"; + case "reminders": + return "Reminders"; + case "label": + // The store may not have loaded yet on a deep link; fall back rather than + // flashing an empty slot. + return labels.items.find((l) => l.id === String(route.params.id))?.name ?? "Label"; + default: + return "Notes"; + } + }); +} diff --git a/frontend/src/composables/usePageTitle.ts b/frontend/src/composables/usePageTitle.ts new file mode 100644 index 0000000..ff9a5db --- /dev/null +++ b/frontend/src/composables/usePageTitle.ts @@ -0,0 +1,25 @@ +import { computed, watchEffect } from "vue"; +import { useRoute } from "vue-router"; +import { useConfigStore } from "../stores/config"; +import { useLensName } from "./useLensName"; + +/** + * Keep the browser tab titled "<page> · <site name>". + * + * index.html's static <title> only names the app, and nothing updated it after + * that, so every tab read the same — or, where the browser preferred the URL, no + * name at all. One writer, mounted once in App.vue: a route outside the shell names + * itself with `meta.title`, and a board lens takes the name the shell's header + * already shows. + */ +export function usePageTitle(): void { + const route = useRoute(); + const config = useConfigStore(); + const lensName = useLensName(); + + const page = computed(() => (route.meta.title as string | undefined) ?? lensName.value); + + watchEffect(() => { + document.title = `${page.value} · ${config.siteName}`; + }); +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index cee5542..5cc1fca 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -31,13 +31,13 @@ const router = createRouter({ path: "/capture", name: "capture", component: () => import("../views/CaptureView.vue"), - meta: { requiresAuth: true, requiresDesktop: true }, + meta: { title: "Quick capture", requiresAuth: true, requiresDesktop: true }, }, { path: "/settings", name: "settings", component: () => import("../views/SettingsView.vue"), - meta: { requiresAuth: true, requiresAdmin: true }, + meta: { title: "Settings", requiresAuth: true, requiresAdmin: true }, }, { // Desktop only: connect this app to a server. Meaningless in the web build, @@ -45,7 +45,7 @@ const router = createRouter({ path: "/sync", name: "sync", component: () => import("../views/SyncView.vue"), - meta: { requiresAuth: true, requiresDesktop: true }, + meta: { title: "Sync", requiresAuth: true, requiresDesktop: true }, }, { // Per-user account: linked devices (native-client sync tokens). Any user. @@ -58,19 +58,19 @@ const router = createRouter({ path: "/account", name: "account", component: () => import("../views/AccountView.vue"), - meta: { requiresAuth: true, requiresServer: true }, + meta: { title: "Linked devices", requiresAuth: true, requiresServer: true }, }, { path: "/login", name: "login", component: () => import("../views/LoginView.vue"), - meta: { guestOnly: true }, + meta: { title: "Sign in", guestOnly: true }, }, { path: "/register", name: "register", component: () => import("../views/RegisterView.vue"), - meta: { guestOnly: true }, + meta: { title: "Create account", guestOnly: true }, }, ], }); diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue index 52b3871..4398cb7 100644 --- a/frontend/src/views/LoginView.vue +++ b/frontend/src/views/LoginView.vue @@ -44,11 +44,13 @@ async function submit() { <main class="flex min-h-full items-center justify-center px-4 py-12"> <div class="w-full max-w-sm"> <div class="mb-8 text-center"> - <div - class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-brand text-lg font-black text-neutral-900" - > - TS - </div> + <img + src="/icon.svg" + :alt="config.siteName" + class="mx-auto mb-3 h-12 w-12 rounded-xl" + width="48" + height="48" + /> <h1 class="text-2xl font-bold tracking-tight">Welcome back</h1> <p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">Sign in to your thoughts.</p> </div> diff --git a/frontend/src/views/RegisterView.vue b/frontend/src/views/RegisterView.vue index ba01056..c0624af 100644 --- a/frontend/src/views/RegisterView.vue +++ b/frontend/src/views/RegisterView.vue @@ -2,11 +2,13 @@ import { ref } from "vue"; import { useRouter } from "vue-router"; import { useSessionStore } from "../stores/session"; +import { useConfigStore } from "../stores/config"; import BaseInput from "../components/BaseInput.vue"; import BaseButton from "../components/BaseButton.vue"; import type { ApiError } from "../api/client"; const session = useSessionStore(); +const config = useConfigStore(); const router = useRouter(); const displayName = ref(""); @@ -37,11 +39,13 @@ async function submit() { <main class="flex min-h-full items-center justify-center px-4 py-12"> <div class="w-full max-w-sm"> <div class="mb-8 text-center"> - <div - class="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-xl bg-brand text-lg font-black text-neutral-900" - > - TS - </div> + <img + src="/icon.svg" + :alt="config.siteName" + class="mx-auto mb-3 h-12 w-12 rounded-xl" + width="48" + height="48" + /> <h1 class="text-2xl font-bold tracking-tight">Create your space</h1> <p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">Start capturing in seconds.</p> </div>