AGPL-3.0-only, a source link in the app, and everything on dev since #5 #6

Merged
bvandeusen merged 214 commits from dev into main 2026-10-21 21:40:00 -04:00
7 changed files with 89 additions and 44 deletions
Showing only changes of commit c5f93cf9f1 - Show all commits
+3
View File
@@ -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>
+2 -28
View File
@@ -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();
+37
View File
@@ -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";
}
});
}
+25
View File
@@ -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}`;
});
}
+6 -6
View File
@@ -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 },
},
],
});
+7 -5
View File
@@ -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>
+9 -5
View File
@@ -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>