DRY pass #2, batch 5, F14: the web's repeated class lists become component classes (#5372)

style.css gains the classes the views spelled out in full: .section-label
(17 sites), .hint (20), .form-error (13), .alert-error (5), .row-card (5),
.list-empty (5), .field (5), .page-shell (3), and the small row action
.btn-sm (4) / .btn-sm-danger (3). Only exact runs moved, so nothing renders
differently; spacing a site adds beyond a run stays a utility beside it.

Kept: the Reminders and Timeline small buttons. They carry no text colour
and inherit it, so putting them on .btn-sm would recolour them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:41:34 -04:00
co-authored by Claude Opus 5.5
parent 38b94bd57e
commit b481e6857c
19 changed files with 128 additions and 80 deletions
+5 -5
View File
@@ -62,7 +62,7 @@ onMounted(() => {
<template> <template>
<section class="flex flex-col gap-5"> <section class="flex flex-col gap-5">
<h2 class="text-xs font-semibold uppercase tracking-wide text-neutral-400">People</h2> <h2 class="section-label">People</h2>
<OneTimeLink <OneTimeLink
v-if="fresh" v-if="fresh"
@@ -71,14 +71,14 @@ onMounted(() => {
@done="fresh = null" @done="fresh = null"
/> />
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="form-error">{{ error }}</p>
<div v-if="loading" class="py-6 text-center text-sm text-neutral-400">Loading…</div> <div v-if="loading" class="list-empty">Loading…</div>
<ul v-else class="flex flex-col gap-2"> <ul v-else class="flex flex-col gap-2">
<li <li
v-for="a in accounts" v-for="a in accounts"
:key="a.id" :key="a.id"
class="flex items-center justify-between gap-4 rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800" class="row-card"
> >
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-medium text-neutral-800 dark:text-neutral-100">{{ a.display_name }}</p> <p class="truncate text-sm font-medium text-neutral-800 dark:text-neutral-100">{{ a.display_name }}</p>
@@ -86,7 +86,7 @@ onMounted(() => {
</div> </div>
<button <button
type="button" type="button"
class="shrink-0 rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-neutral-700 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800" class="shrink-0 btn-sm"
:disabled="busy === a.id" :disabled="busy === a.id"
@click="resetLink(a)" @click="resetLink(a)"
> >
+4 -4
View File
@@ -93,11 +93,11 @@ onMounted(() => {
<template> <template>
<section class="flex flex-col gap-5"> <section class="flex flex-col gap-5">
<h2 class="text-xs font-semibold uppercase tracking-wide text-neutral-400">Activity</h2> <h2 class="section-label">Activity</h2>
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="form-error">{{ error }}</p>
<div v-if="loading" class="py-6 text-center text-sm text-neutral-400">Loading…</div> <div v-if="loading" class="list-empty">Loading…</div>
<p v-else-if="!events.length" class="text-sm text-neutral-400">Nothing yet.</p> <p v-else-if="!events.length" class="text-sm text-neutral-400">Nothing yet.</p>
<ul v-else class="flex flex-col gap-2"> <ul v-else class="flex flex-col gap-2">
<li <li
@@ -118,7 +118,7 @@ onMounted(() => {
<button <button
v-if="more" v-if="more"
type="button" type="button"
class="self-start rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-neutral-700 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800" class="self-start btn-sm"
:disabled="loadingMore" :disabled="loadingMore"
@click="loadMore" @click="loadMore"
> >
+1 -1
View File
@@ -396,7 +396,7 @@ async function signOut() {
</RouterLink> </RouterLink>
<div class="mt-3 flex items-center justify-between px-3 pb-1"> <div class="mt-3 flex items-center justify-between px-3 pb-1">
<span class="text-xs font-semibold uppercase tracking-wide text-neutral-400">Tags</span> <span class="section-label">Tags</span>
<button <button
type="button" type="button"
class="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200" class="text-neutral-400 hover:text-neutral-600 dark:hover:text-neutral-200"
+1 -1
View File
@@ -132,6 +132,6 @@ onMounted(async () => {
</button> </button>
</li> </li>
</ul> </ul>
<p v-else class="px-4 py-6 text-center text-sm text-neutral-400">No matches</p> <p v-else class="px-4 list-empty">No matches</p>
</BaseModal> </BaseModal>
</template> </template>
+1 -1
View File
@@ -42,7 +42,7 @@ async function toggle() {
<section v-if="status?.is_appimage" class="mt-10 border-t border-neutral-200 pt-8 dark:border-neutral-800"> <section v-if="status?.is_appimage" class="mt-10 border-t border-neutral-200 pt-8 dark:border-neutral-800">
<h2 class="text-sm font-semibold">Applications menu</h2> <h2 class="text-sm font-semibold">Applications menu</h2>
<div class="mt-3 flex items-center justify-between gap-4"> <div class="mt-3 flex items-center justify-between gap-4">
<p class="text-sm text-neutral-500 dark:text-neutral-400"> <p class="hint">
{{ status.is_integrated ? "Inkwell is in your applications menu." : "Not in your applications menu." }} {{ status.is_integrated ? "Inkwell is in your applications menu." : "Not in your applications menu." }}
</p> </p>
<BaseButton <BaseButton
+6 -6
View File
@@ -140,16 +140,16 @@ onMounted(() => {
<template> <template>
<section class="flex flex-col gap-5"> <section class="flex flex-col gap-5">
<h2 class="text-xs font-semibold uppercase tracking-wide text-neutral-400">Groups</h2> <h2 class="section-label">Groups</h2>
<form class="flex flex-wrap items-end gap-3" @submit.prevent="create"> <form class="flex flex-wrap items-end gap-3" @submit.prevent="create">
<BaseInput id="group-name" v-model="name" label="New group" placeholder="Family" class="min-w-48 flex-1" /> <BaseInput id="group-name" v-model="name" label="New group" placeholder="Family" class="min-w-48 flex-1" />
<BaseButton type="submit" :loading="creating" :disabled="!name.trim()">Create group</BaseButton> <BaseButton type="submit" :loading="creating" :disabled="!name.trim()">Create group</BaseButton>
</form> </form>
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="form-error">{{ error }}</p>
<div v-if="loading" class="py-6 text-center text-sm text-neutral-400">Loading…</div> <div v-if="loading" class="list-empty">Loading…</div>
<p v-else-if="empty" class="text-sm text-neutral-400">No groups yet.</p> <p v-else-if="empty" class="text-sm text-neutral-400">No groups yet.</p>
<ul v-else class="flex flex-col gap-2"> <ul v-else class="flex flex-col gap-2">
<li <li
@@ -167,7 +167,7 @@ onMounted(() => {
<div class="flex shrink-0 items-center gap-2"> <div class="flex shrink-0 items-center gap-2">
<button <button
type="button" type="button"
class="rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-neutral-700 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800" class="btn-sm"
:disabled="busy === g.id" :disabled="busy === g.id"
@click="rename(g)" @click="rename(g)"
> >
@@ -175,7 +175,7 @@ onMounted(() => {
</button> </button>
<button <button
type="button" type="button"
class="rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-red-600 hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50 dark:border-neutral-700 dark:text-red-400 dark:hover:bg-red-950/40" class="btn-sm-danger"
:disabled="busy === g.id" :disabled="busy === g.id"
@click="remove(g)" @click="remove(g)"
> >
@@ -216,7 +216,7 @@ onMounted(() => {
</select> </select>
<button <button
type="submit" type="submit"
class="rounded-md border border-neutral-300 px-2.5 py-1.5 text-xs text-neutral-700 hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand disabled:opacity-50 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800" class="btn-sm py-1.5"
:disabled="busy === g.id || !picks[g.id]" :disabled="busy === g.id || !picks[g.id]"
> >
Add Add
+6 -6
View File
@@ -109,7 +109,7 @@ onMounted(() => {
<template> <template>
<section class="flex flex-col gap-5"> <section class="flex flex-col gap-5">
<h2 class="text-xs font-semibold uppercase tracking-wide text-neutral-400">Invites</h2> <h2 class="section-label">Invites</h2>
<OneTimeLink <OneTimeLink
v-if="freshLink" v-if="freshLink"
@@ -132,7 +132,7 @@ onMounted(() => {
<select <select
id="invite-days" id="invite-days"
v-model.number="days" v-model.number="days"
class="rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100" class="field"
> >
<option v-for="l in LIFETIMES" :key="l.days" :value="l.days">{{ l.label }}</option> <option v-for="l in LIFETIMES" :key="l.days" :value="l.days">{{ l.label }}</option>
</select> </select>
@@ -140,15 +140,15 @@ onMounted(() => {
<BaseButton type="submit" :loading="creating">Create invite</BaseButton> <BaseButton type="submit" :loading="creating">Create invite</BaseButton>
</form> </form>
<p v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="form-error">{{ error }}</p>
<div v-if="loading" class="py-6 text-center text-sm text-neutral-400">Loading…</div> <div v-if="loading" class="list-empty">Loading…</div>
<p v-else-if="!invites.length" class="text-sm text-neutral-400">No invites yet.</p> <p v-else-if="!invites.length" class="text-sm text-neutral-400">No invites yet.</p>
<ul v-else class="flex flex-col gap-2"> <ul v-else class="flex flex-col gap-2">
<li <li
v-for="i in invites" v-for="i in invites"
:key="i.id" :key="i.id"
class="flex items-center justify-between gap-4 rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800" class="row-card"
> >
<div class="min-w-0"> <div class="min-w-0">
<p class="truncate text-sm font-medium text-neutral-800 dark:text-neutral-100"> <p class="truncate text-sm font-medium text-neutral-800 dark:text-neutral-100">
@@ -159,7 +159,7 @@ onMounted(() => {
<button <button
v-if="i.status === 'pending'" v-if="i.status === 'pending'"
type="button" type="button"
class="shrink-0 rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-red-600 hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:text-red-400 dark:hover:bg-red-950/40" class="shrink-0 btn-sm-danger"
@click="revoke(i)" @click="revoke(i)"
> >
Revoke Revoke
+1 -1
View File
@@ -761,7 +761,7 @@ function revPreview(rev: NoteRevision): string {
v-if="!isCreate && showHistory" v-if="!isCreate && showHistory"
class="flex flex-col gap-1 border-t border-neutral-100 pt-2 dark:border-neutral-800" class="flex flex-col gap-1 border-t border-neutral-100 pt-2 dark:border-neutral-800"
> >
<p class="text-xs font-semibold uppercase tracking-wide text-neutral-400">History</p> <p class="section-label">History</p>
<p v-if="!revisions.length" class="text-xs text-neutral-400"> <p v-if="!revisions.length" class="text-xs text-neutral-400">
No earlier versions yet — your edits will show up here. No earlier versions yet — your edits will show up here.
</p> </p>
+4 -4
View File
@@ -115,7 +115,7 @@ onMounted(load);
<p v-if="loading" class="py-4 text-center text-sm text-neutral-400">Loading…</p> <p v-if="loading" class="py-4 text-center text-sm text-neutral-400">Loading…</p>
<div v-else-if="loadError" class="py-4 text-center"> <div v-else-if="loadError" class="py-4 text-center">
<p class="text-sm text-red-600 dark:text-red-400">{{ loadError }}</p> <p class="form-error">{{ loadError }}</p>
<button <button
type="button" type="button"
class="mt-2 rounded-md border border-neutral-300 px-3 py-1 text-sm hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800" class="mt-2 rounded-md border border-neutral-300 px-3 py-1 text-sm hover:bg-neutral-100 dark:border-neutral-700 dark:hover:bg-neutral-800"
@@ -126,7 +126,7 @@ onMounted(load);
</div> </div>
<template v-else> <template v-else>
<p v-if="!anyone" class="text-sm text-neutral-500 dark:text-neutral-400"> <p v-if="!anyone" class="hint">
No one else has an account here yet. An admin can invite people from Settings. No one else has an account here yet. An admin can invite people from Settings.
</p> </p>
@@ -156,10 +156,10 @@ onMounted(load);
</button> </button>
</form> </form>
<p v-if="error" role="alert" class="text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" role="alert" class="form-error">{{ error }}</p>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<p class="text-xs font-semibold uppercase tracking-wide text-neutral-400">Shared with</p> <p class="section-label">Shared with</p>
<p v-if="!shares.length" class="text-sm text-neutral-400">Only you.</p> <p v-if="!shares.length" class="text-sm text-neutral-400">Only you.</p>
<ul v-else class="flex flex-col gap-1"> <ul v-else class="flex flex-col gap-1">
<li v-for="s in shares" :key="s.id" class="flex items-center gap-2 rounded-lg px-1 py-1"> <li v-for="s in shares" :key="s.id" class="flex items-center gap-2 rounded-lg px-1 py-1">
+48
View File
@@ -290,4 +290,52 @@ body {
.nav-link-active { .nav-link-active {
@apply bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100; @apply bg-neutral-200 text-neutral-900 dark:bg-neutral-800 dark:text-neutral-100;
} }
/* The small bordered action on a row in Account, Admin and Sync: Rename, Revoke,
* Sign out. Two colours, neutral and destructive; spacing a site needs beyond
* these (shrink-0, self-start, py-1.5) stays a utility beside it. */
.btn-sm {
@apply rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-neutral-700
hover:bg-neutral-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
disabled:opacity-50 dark:border-neutral-700 dark:text-neutral-200 dark:hover:bg-neutral-800;
}
.btn-sm-danger {
@apply rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-red-600
hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
disabled:opacity-50 dark:border-neutral-700 dark:text-red-400 dark:hover:bg-red-950/40;
}
/* A plain text or number input on a settings page; its width is the site's. */
.field {
@apply rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900
shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand
dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100;
}
/* A settings or account page: one centred column. */
.page-shell {
@apply mx-auto min-h-full max-w-2xl px-4 py-8;
}
/* The small caps heading over a group of rows or controls. */
.section-label {
@apply text-xs font-semibold uppercase tracking-wide text-neutral-400;
}
/* One bordered row in a list of things you can act on: a device, an account. */
.row-card {
@apply flex items-center justify-between gap-4 rounded-xl border border-neutral-200 px-4
py-3 dark:border-neutral-800;
}
/* The line a list shows when it has nothing in it. */
.list-empty {
@apply py-6 text-center text-sm text-neutral-400;
}
/* Secondary text under a heading or beside a control. */
.hint {
@apply text-sm text-neutral-500 dark:text-neutral-400;
}
/* A field's or a form's own error, in a line of text. */
.form-error {
@apply text-sm text-red-600 dark:text-red-400;
}
/* A form's error as a banner over its submit button. */
.alert-error {
@apply rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300;
}
} }
+13 -13
View File
@@ -142,11 +142,11 @@ onMounted(() => {
</script> </script>
<template> <template>
<div class="mx-auto min-h-full max-w-2xl px-4 py-8"> <div class="page-shell">
<PageHeader title="Account" /> <PageHeader title="Account" />
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">Linked devices</h2> <h2 class="mb-2 section-label">Linked devices</h2>
<p class="mb-6 max-w-xl text-sm text-neutral-500 dark:text-neutral-400"> <p class="mb-6 max-w-xl hint">
Link the Inkwell desktop or mobile app to sync your notes. Create a device token here, then Link the Inkwell desktop or mobile app to sync your notes. Create a device token here, then
paste it into the app when it asks to connect. You can revoke a device at any time. paste it into the app when it asks to connect. You can revoke a device at any time.
</p> </p>
@@ -199,7 +199,7 @@ onMounted(() => {
<BaseButton type="submit" :loading="creating">Create token</BaseButton> <BaseButton type="submit" :loading="creating">Create token</BaseButton>
</form> </form>
<p v-if="error" class="mb-4 text-sm text-red-600 dark:text-red-400">{{ error }}</p> <p v-if="error" class="mb-4 form-error">{{ error }}</p>
<!-- Device list --> <!-- Device list -->
<div v-if="devices.loading" class="py-10 text-center text-sm text-neutral-400">Loading…</div> <div v-if="devices.loading" class="py-10 text-center text-sm text-neutral-400">Loading…</div>
@@ -207,13 +207,13 @@ onMounted(() => {
v-else-if="!devices.items.length" v-else-if="!devices.items.length"
class="rounded-xl border border-dashed border-neutral-300 py-10 text-center dark:border-neutral-700" class="rounded-xl border border-dashed border-neutral-300 py-10 text-center dark:border-neutral-700"
> >
<p class="text-sm text-neutral-500 dark:text-neutral-400">No devices linked yet.</p> <p class="hint">No devices linked yet.</p>
</div> </div>
<ul v-else class="flex flex-col gap-2"> <ul v-else class="flex flex-col gap-2">
<li <li
v-for="d in devices.items" v-for="d in devices.items"
:key="d.id" :key="d.id"
class="flex items-center justify-between gap-4 rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800" class="row-card"
> >
<div class="flex min-w-0 items-center gap-3"> <div class="flex min-w-0 items-center gap-3">
<span class="text-neutral-400"><Icon name="device" /></span> <span class="text-neutral-400"><Icon name="device" /></span>
@@ -226,7 +226,7 @@ onMounted(() => {
</div> </div>
<button <button
type="button" type="button"
class="shrink-0 rounded-md border border-neutral-300 px-2.5 py-1 text-xs text-red-600 hover:bg-red-50 focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:text-red-400 dark:hover:bg-red-950/40" class="shrink-0 btn-sm-danger"
@click="revoke(d.id, d.name)" @click="revoke(d.id, d.name)"
> >
Revoke Revoke
@@ -235,9 +235,9 @@ onMounted(() => {
</ul> </ul>
<template v-if="storage"> <template v-if="storage">
<h2 class="mb-2 mt-10 text-xs font-semibold uppercase tracking-wide text-neutral-400">Storage</h2> <h2 class="mb-2 mt-10 section-label">Storage</h2>
<div <div
class="flex items-center justify-between gap-4 rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800" class="row-card"
> >
<div class="min-w-0"> <div class="min-w-0">
<p class="text-sm font-medium text-neutral-800 dark:text-neutral-100">Attachments</p> <p class="text-sm font-medium text-neutral-800 dark:text-neutral-100">Attachments</p>
@@ -247,8 +247,8 @@ onMounted(() => {
</div> </div>
</template> </template>
<h2 class="mb-2 mt-10 text-xs font-semibold uppercase tracking-wide text-neutral-400">Password</h2> <h2 class="mb-2 mt-10 section-label">Password</h2>
<p class="mb-4 text-sm text-neutral-500 dark:text-neutral-400">Changing it signs you out everywhere else.</p> <p class="mb-4 hint">Changing it signs you out everywhere else.</p>
<form class="flex max-w-sm flex-col gap-3" @submit.prevent="changePassword"> <form class="flex max-w-sm flex-col gap-3" @submit.prevent="changePassword">
<BaseInput <BaseInput
id="current-password" id="current-password"
@@ -273,9 +273,9 @@ onMounted(() => {
</div> </div>
</form> </form>
<h2 class="mb-2 mt-10 text-xs font-semibold uppercase tracking-wide text-neutral-400">Sessions</h2> <h2 class="mb-2 mt-10 section-label">Sessions</h2>
<div <div
class="flex items-center justify-between gap-4 rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800" class="row-card"
> >
<div class="min-w-0"> <div class="min-w-0">
<p class="text-sm font-medium text-neutral-800 dark:text-neutral-100">Sign out everywhere else</p> <p class="text-sm font-medium text-neutral-800 dark:text-neutral-100">Sign out everywhere else</p>
+2 -2
View File
@@ -350,11 +350,11 @@ const gridBinds = computed(() => ({
<template v-else> <template v-else>
<template v-if="isMainBoard"> <template v-if="isMainBoard">
<section v-if="pinnedNotes.length"> <section v-if="pinnedNotes.length">
<h2 class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400">Pinned</h2> <h2 class="mb-2 section-label">Pinned</h2>
<NoteGrid :notes="pinnedNotes" v-bind="gridBinds" /> <NoteGrid :notes="pinnedNotes" v-bind="gridBinds" />
</section> </section>
<section v-if="otherNotes.length" :class="pinnedNotes.length ? 'mt-8' : ''"> <section v-if="otherNotes.length" :class="pinnedNotes.length ? 'mt-8' : ''">
<h2 v-if="pinnedNotes.length" class="mb-2 text-xs font-semibold uppercase tracking-wide text-neutral-400"> <h2 v-if="pinnedNotes.length" class="mb-2 section-label">
Others Others
</h2> </h2>
<NoteGrid :notes="otherNotes" v-bind="gridBinds" /> <NoteGrid :notes="otherNotes" v-bind="gridBinds" />
+3 -3
View File
@@ -44,7 +44,7 @@ async function submit() {
height="48" height="48"
/> />
<h1 class="text-2xl font-bold tracking-tight">Forgot your password?</h1> <h1 class="text-2xl font-bold tracking-tight">Forgot your password?</h1>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400">We'll email you a link to choose a new one.</p> <p class="mt-1 hint">We'll email you a link to choose a new one.</p>
</div> </div>
<p <p
@@ -67,14 +67,14 @@ async function submit() {
<p <p
v-if="error" v-if="error"
role="alert" role="alert"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300" class="alert-error"
> >
{{ error }} {{ error }}
</p> </p>
<BaseButton type="submit" :loading="loading">Send reset link</BaseButton> <BaseButton type="submit" :loading="loading">Send reset link</BaseButton>
</form> </form>
<p class="mt-6 text-center text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-6 text-center hint">
<RouterLink to="/login" class="font-semibold text-brand-700 hover:underline dark:text-brand" <RouterLink to="/login" class="font-semibold text-brand-700 hover:underline dark:text-brand"
>Back to sign in</RouterLink >Back to sign in</RouterLink
> >
+3 -3
View File
@@ -52,7 +52,7 @@ async function submit() {
height="48" height="48"
/> />
<h1 class="text-2xl font-bold tracking-tight">Welcome back</h1> <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> <p class="mt-1 hint">Sign in to your thoughts.</p>
</div> </div>
<form class="flex flex-col gap-4" novalidate @submit.prevent="submit"> <form class="flex flex-col gap-4" novalidate @submit.prevent="submit">
@@ -77,7 +77,7 @@ async function submit() {
<p <p
v-if="error" v-if="error"
role="alert" role="alert"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300" class="alert-error"
> >
{{ error }} {{ error }}
</p> </p>
@@ -90,7 +90,7 @@ async function submit() {
> >
</form> </form>
<p v-if="config.allowRegistration" class="mt-6 text-center text-sm text-neutral-500 dark:text-neutral-400"> <p v-if="config.allowRegistration" class="mt-6 text-center hint">
No account? No account?
<RouterLink to="/register" class="font-semibold text-brand-700 hover:underline dark:text-brand" <RouterLink to="/register" class="font-semibold text-brand-700 hover:underline dark:text-brand"
>Create one</RouterLink >Create one</RouterLink
+3 -3
View File
@@ -52,7 +52,7 @@ async function submit() {
height="48" height="48"
/> />
<h1 class="text-2xl font-bold tracking-tight">{{ invite ? "You're invited" : "Create your space" }}</h1> <h1 class="text-2xl font-bold tracking-tight">{{ invite ? "You're invited" : "Create your space" }}</h1>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-1 hint">
{{ invite ? `Create your account on ${config.siteName}.` : "Start capturing in seconds." }} {{ invite ? `Create your account on ${config.siteName}.` : "Start capturing in seconds." }}
</p> </p>
</div> </div>
@@ -86,14 +86,14 @@ async function submit() {
<p <p
v-if="error" v-if="error"
role="alert" role="alert"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300" class="alert-error"
> >
{{ error }} {{ error }}
</p> </p>
<BaseButton type="submit" :loading="loading">Create account</BaseButton> <BaseButton type="submit" :loading="loading">Create account</BaseButton>
</form> </form>
<p class="mt-6 text-center text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-6 text-center hint">
Already have an account? Already have an account?
<RouterLink to="/login" class="font-semibold text-brand-700 hover:underline dark:text-brand" <RouterLink to="/login" class="font-semibold text-brand-700 hover:underline dark:text-brand"
>Sign in</RouterLink >Sign in</RouterLink
+4 -4
View File
@@ -58,7 +58,7 @@ async function submit() {
height="48" height="48"
/> />
<h1 class="text-2xl font-bold tracking-tight">Choose a new password</h1> <h1 class="text-2xl font-bold tracking-tight">Choose a new password</h1>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-1 hint">
You'll be signed out everywhere else, and your apps will ask you to sign in again. You'll be signed out everywhere else, and your apps will ask you to sign in again.
</p> </p>
</div> </div>
@@ -66,7 +66,7 @@ async function submit() {
<p <p
v-if="!token" v-if="!token"
role="alert" role="alert"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300" class="alert-error"
> >
This link is incomplete. Ask your admin for a new one. This link is incomplete. Ask your admin for a new one.
</p> </p>
@@ -91,14 +91,14 @@ async function submit() {
<p <p
v-if="error" v-if="error"
role="alert" role="alert"
class="rounded-lg bg-red-50 px-3 py-2 text-sm text-red-700 dark:bg-red-950/50 dark:text-red-300" class="alert-error"
> >
{{ error }} {{ error }}
</p> </p>
<BaseButton type="submit" :loading="loading">Set password</BaseButton> <BaseButton type="submit" :loading="loading">Set password</BaseButton>
</form> </form>
<p class="mt-6 text-center text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-6 text-center hint">
<RouterLink to="/login" class="font-semibold text-brand-700 hover:underline dark:text-brand" <RouterLink to="/login" class="font-semibold text-brand-700 hover:underline dark:text-brand"
>Back to sign in</RouterLink >Back to sign in</RouterLink
> >
+7 -7
View File
@@ -94,13 +94,13 @@ onMounted(load);
</script> </script>
<template> <template>
<div class="mx-auto min-h-full max-w-2xl px-4 py-8"> <div class="page-shell">
<PageHeader title="Settings" /> <PageHeader title="Settings" />
<AsyncState :loading="loading" :error="loadError" error-title="Couldn't load settings" @retry="load"> <AsyncState :loading="loading" :error="loadError" error-title="Couldn't load settings" @retry="load">
<form class="flex flex-col gap-8" @submit.prevent="save"> <form class="flex flex-col gap-8" @submit.prevent="save">
<section v-for="group in groups" :key="group.name" class="flex flex-col gap-5"> <section v-for="group in groups" :key="group.name" class="flex flex-col gap-5">
<h2 class="text-xs font-semibold uppercase tracking-wide text-neutral-400">{{ group.name }}</h2> <h2 class="section-label">{{ group.name }}</h2>
<div v-for="it in group.entries" :key="it.key" class="flex flex-col gap-1"> <div v-for="it in group.entries" :key="it.key" class="flex flex-col gap-1">
<div class="flex items-center justify-between gap-6"> <div class="flex items-center justify-between gap-6">
@@ -126,14 +126,14 @@ onMounted(load);
type="number" type="number"
:min="it.minimum ?? undefined" :min="it.minimum ?? undefined"
:max="it.maximum ?? undefined" :max="it.maximum ?? undefined"
class="w-28 rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100" class="w-28 field"
:value="Number(it.value)" :value="Number(it.value)"
@input="it.value = Number(($event.target as HTMLInputElement).value)" @input="it.value = Number(($event.target as HTMLInputElement).value)"
/> />
<select <select
v-else-if="it.choices" v-else-if="it.choices"
:id="it.key" :id="it.key"
class="w-56 rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100" class="w-56 field"
:value="String(it.value)" :value="String(it.value)"
@change="it.value = ($event.target as HTMLSelectElement).value" @change="it.value = ($event.target as HTMLSelectElement).value"
> >
@@ -145,7 +145,7 @@ onMounted(load);
type="password" type="password"
autocomplete="new-password" autocomplete="new-password"
:placeholder="it.is_set ? 'Saved — type to replace' : ''" :placeholder="it.is_set ? 'Saved — type to replace' : ''"
class="w-56 rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100" class="w-56 field"
:value="String(it.value)" :value="String(it.value)"
@input="it.value = ($event.target as HTMLInputElement).value" @input="it.value = ($event.target as HTMLInputElement).value"
/> />
@@ -153,7 +153,7 @@ onMounted(load);
v-else v-else
:id="it.key" :id="it.key"
type="text" type="text"
class="w-56 rounded-lg border border-neutral-300 bg-white px-3 py-2 text-sm text-neutral-900 shadow-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-brand dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100" class="w-56 field"
:value="String(it.value)" :value="String(it.value)"
@input="it.value = ($event.target as HTMLInputElement).value" @input="it.value = ($event.target as HTMLInputElement).value"
/> />
@@ -178,7 +178,7 @@ onMounted(load);
<BaseButton type="submit" :loading="saving" :disabled="!dirty">Save changes</BaseButton> <BaseButton type="submit" :loading="saving" :disabled="!dirty">Save changes</BaseButton>
<span v-if="saved && !dirty" class="text-sm text-green-600 dark:text-green-400">Saved.</span> <span v-if="saved && !dirty" class="text-sm text-green-600 dark:text-green-400">Saved.</span>
<span v-else-if="!dirty && !error" class="text-sm text-neutral-400">No unsaved changes</span> <span v-else-if="!dirty && !error" class="text-sm text-neutral-400">No unsaved changes</span>
<span v-if="error" class="text-sm text-red-600 dark:text-red-400">{{ error }}</span> <span v-if="error" class="form-error">{{ error }}</span>
</div> </div>
</form> </form>
</AsyncState> </AsyncState>
+14 -14
View File
@@ -356,7 +356,7 @@ onBeforeUnmount(() => stopSyncListener?.());
</script> </script>
<template> <template>
<div class="mx-auto min-h-full max-w-2xl px-4 py-8"> <div class="page-shell">
<PageHeader title="Sync" /> <PageHeader title="Sync" />
<div v-if="loading" class="py-10 text-center text-sm text-neutral-400">Loading…</div> <div v-if="loading" class="py-10 text-center text-sm text-neutral-400">Loading…</div>
@@ -392,7 +392,7 @@ onBeforeUnmount(() => stopSyncListener?.());
<p v-if="degraded.length" class="mt-3 text-xs text-amber-600 dark:text-amber-400"> <p v-if="degraded.length" class="mt-3 text-xs text-amber-600 dark:text-amber-400">
This server doesn't support: {{ degraded.join(", ") }}. Everything else syncs normally. This server doesn't support: {{ degraded.join(", ") }}. Everything else syncs normally.
</p> </p>
<p v-if="syncError" class="mt-3 text-sm text-red-600 dark:text-red-400">{{ syncError }}</p> <p v-if="syncError" class="mt-3 form-error">{{ syncError }}</p>
<p v-else-if="autoError" class="mt-3 text-xs text-amber-600 dark:text-amber-400"> <p v-else-if="autoError" class="mt-3 text-xs text-amber-600 dark:text-amber-400">
The last automatic sync ({{ new Date(lastCycle!.at_ms).toLocaleTimeString() }}) didn't The last automatic sync ({{ new Date(lastCycle!.at_ms).toLocaleTimeString() }}) didn't
go through: {{ autoError }} It will keep trying on its own. go through: {{ autoError }} It will keep trying on its own.
@@ -428,7 +428,7 @@ onBeforeUnmount(() => stopSyncListener?.());
<p class="text-sm font-medium text-neutral-800 dark:text-neutral-100"> <p class="text-sm font-medium text-neutral-800 dark:text-neutral-100">
Working offline on this device Working offline on this device
</p> </p>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-1 hint">
Everything works without a server — your notes are stored on this machine. Connect an Everything works without a server — your notes are stored on this machine. Connect an
Inkwell server if you want them to reach your other devices. Inkwell server if you want them to reach your other devices.
</p> </p>
@@ -452,7 +452,7 @@ onBeforeUnmount(() => stopSyncListener?.());
Uses https unless you type http:// yourself. Uses https unless you type http:// yourself.
</p> </p>
<p v-if="probeError" class="text-sm text-red-600 dark:text-red-400">{{ probeError }}</p> <p v-if="probeError" class="form-error">{{ probeError }}</p>
<!-- What answered, BEFORE any credentials are handed over --> <!-- What answered, BEFORE any credentials are handed over -->
<div <div
@@ -531,7 +531,7 @@ onBeforeUnmount(() => stopSyncListener?.());
/> />
</fieldset> </fieldset>
<p v-if="linkError" class="text-sm text-red-600 dark:text-red-400">{{ linkError }}</p> <p v-if="linkError" class="form-error">{{ linkError }}</p>
<div> <div>
<BaseButton type="submit" :loading="linking" :disabled="!canLink"> <BaseButton type="submit" :loading="linking" :disabled="!canLink">
@@ -546,7 +546,7 @@ onBeforeUnmount(() => stopSyncListener?.());
updates: a hotkey that writes to the local store needs no server. --> updates: a hotkey that writes to the local store needs no server. -->
<section class="mt-10 border-t border-neutral-200 pt-8 dark:border-neutral-800"> <section class="mt-10 border-t border-neutral-200 pt-8 dark:border-neutral-800">
<h2 class="text-sm font-semibold">Quick capture</h2> <h2 class="text-sm font-semibold">Quick capture</h2>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-1 hint">
A system-wide shortcut that opens a small window to write a note in, without A system-wide shortcut that opens a small window to write a note in, without
bringing this one forward. bringing this one forward.
</p> </p>
@@ -570,7 +570,7 @@ onBeforeUnmount(() => stopSyncListener?.());
</BaseButton> </BaseButton>
</div> </div>
<p v-if="shortcutError" class="mt-2 text-sm text-red-600 dark:text-red-400"> <p v-if="shortcutError" class="mt-2 form-error">
{{ shortcutError }} {{ shortcutError }}
</p> </p>
@@ -585,10 +585,10 @@ onBeforeUnmount(() => stopSyncListener?.());
{{ shortcut.shortcut }} is saved but isn't active — something else on this {{ shortcut.shortcut }} is saved but isn't active — something else on this
system is holding it. Try a different combination. system is holding it. Try a different combination.
</p> </p>
<p v-else-if="shortcut.registered" class="mt-2 text-sm text-neutral-500 dark:text-neutral-400"> <p v-else-if="shortcut.registered" class="mt-2 hint">
Press {{ shortcut.shortcut }} anywhere to capture a note. Press {{ shortcut.shortcut }} anywhere to capture a note.
</p> </p>
<p v-else class="mt-2 text-sm text-neutral-500 dark:text-neutral-400"> <p v-else class="mt-2 hint">
Off. There's no default on purpose — any combination picked for you is one Off. There's no default on purpose — any combination picked for you is one
taken away from something else on your machine. taken away from something else on your machine.
<button <button
@@ -605,12 +605,12 @@ onBeforeUnmount(() => stopSyncListener?.());
has never touched a server still updates itself. --> has never touched a server still updates itself. -->
<section class="mt-10 border-t border-neutral-200 pt-8 dark:border-neutral-800"> <section class="mt-10 border-t border-neutral-200 pt-8 dark:border-neutral-800">
<h2 class="text-sm font-semibold">App updates</h2> <h2 class="text-sm font-semibold">App updates</h2>
<p class="mt-1 text-sm text-neutral-500 dark:text-neutral-400"> <p class="mt-1 hint">
This is version {{ update?.current_version ?? "—" }}. This is version {{ update?.current_version ?? "—" }}.
</p> </p>
<fieldset v-if="offeredServer" class="mt-4"> <fieldset v-if="offeredServer" class="mt-4">
<legend class="text-xs font-semibold uppercase tracking-wide text-neutral-400"> <legend class="section-label">
Updates from Updates from
</legend> </legend>
<div class="mt-2 flex flex-col gap-2"> <div class="mt-2 flex flex-col gap-2">
@@ -650,7 +650,7 @@ onBeforeUnmount(() => stopSyncListener?.());
<!-- A server holds one build, so a channel only means something on the forge. --> <!-- A server holds one build, so a channel only means something on the forge. -->
<fieldset v-if="source === null" class="mt-4"> <fieldset v-if="source === null" class="mt-4">
<legend class="text-xs font-semibold uppercase tracking-wide text-neutral-400"> <legend class="section-label">
Channel Channel
</legend> </legend>
<div class="mt-2 flex flex-col gap-2"> <div class="mt-2 flex flex-col gap-2">
@@ -704,7 +704,7 @@ onBeforeUnmount(() => stopSyncListener?.());
</div> </div>
<p <p
v-else-if="checkedOnce && !updateError" v-else-if="checkedOnce && !updateError"
class="mt-4 text-sm text-neutral-500 dark:text-neutral-400" class="mt-4 hint"
> >
<template v-if="source">You're on the newest build {{ source }} has.</template> <template v-if="source">You're on the newest build {{ source }} has.</template>
<template v-else> <template v-else>
@@ -712,7 +712,7 @@ onBeforeUnmount(() => stopSyncListener?.());
</template> </template>
</p> </p>
<p v-if="updateError" class="mt-4 text-sm text-red-600 dark:text-red-400">{{ updateError }}</p> <p v-if="updateError" class="mt-4 form-error">{{ updateError }}</p>
<div class="mt-4 flex flex-wrap gap-2"> <div class="mt-4 flex flex-wrap gap-2">
<BaseButton variant="ghost" :loading="checking" @click="checkUpdates"> <BaseButton variant="ghost" :loading="checking" @click="checkUpdates">
+2 -2
View File
@@ -137,7 +137,7 @@ onMounted(load);
</button> </button>
</div> </div>
</div> </div>
<p class="mb-5 text-sm text-neutral-500 dark:text-neutral-400"> <p class="mb-5 hint">
Your notes by when you captured them{{ hasRange ? " — filtered to the chosen dates" : "" }}. Your notes by when you captured them{{ hasRange ? " — filtered to the chosen dates" : "" }}.
</p> </p>
@@ -150,7 +150,7 @@ onMounted(load);
<EmptyState v-if="items.length === 0" :title="empty.title" :subtitle="empty.subtitle" /> <EmptyState v-if="items.length === 0" :title="empty.title" :subtitle="empty.subtitle" />
<div v-else class="flex flex-col gap-8"> <div v-else class="flex flex-col gap-8">
<section v-for="group in groups" :key="group.key"> <section v-for="group in groups" :key="group.key">
<h2 class="mb-3 text-xs font-semibold uppercase tracking-wide text-neutral-400">{{ group.label }}</h2> <h2 class="mb-3 section-label">{{ group.label }}</h2>
<NoteGrid :notes="group.notes" @open="openEditor" /> <NoteGrid :notes="group.notes" @open="openEditor" />
</section> </section>
</div> </div>