DRY pass #2, batch 5, F17: the web's smaller pairs (#5372)

- desktop/bridge.ts listen<T>(): the no-op-off-desktop event listener
  that onSynced, onCaptured and onReminderDue each wrote.
- size.ts roughSize(): one decimal below 10, none above, for the client
  download sizes (MB) and the storage line (GB).
- BaseModal's title prop draws the heading row and close button that
  LabelsModal and ShareDialog each built.
- SyncView: switchChannel and switchSource share recheck(); the four
  update-card radios render from two option lists.

Kept: the board's and the shell's is-typing guards (they ignore different
elements), the drawer's nav beside the palette's commands (one is a laid-
out list with tags between its entries, the other a command list), the
status pills (each colours differently), and plurals (the copy is the
operator's call, raised in #5371).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-08 14:48:31 -04:00
co-authored by Claude Opus 5.5
parent 6e4c4930cb
commit 3dd47fa178
8 changed files with 104 additions and 101 deletions
+58 -65
View File
@@ -107,6 +107,46 @@ const offeredServer = computed(
() => source.value ?? status.value?.server_url?.replace(/\/+$/, "") ?? null,
);
// The two choices on the update card, each a radio with a name and a line on what
// choosing it means.
interface UpdateOption {
title: string;
detail: string;
checked: boolean;
pick: () => Promise<void>;
}
const sourceOptions = computed<UpdateOption[]>(() => [
{
title: "Fabled-Git",
detail: "Where Inkwell is published, on the channel below.",
checked: source.value === null,
pick: () => switchSource(null),
},
{
title: offeredServer.value ?? "",
detail:
"The build that server holds. It passes on official builds only: every update is checked against the signature built into this app first.",
checked: source.value !== null,
pick: () => switchSource(offeredServer.value),
},
]);
const channelOptions = computed<UpdateOption[]>(() => [
{
title: "Stable",
detail: "Released versions only.",
checked: channel.value === "stable",
pick: () => switchChannel("stable"),
},
{
title: "Development",
detail: "Every build that passes CI. Newer, and less tested.",
checked: channel.value === "dev",
pick: () => switchChannel("dev"),
},
]);
// --- Quick capture -----------------------------------------------------------
// A desktop-local preference, so it lives here beside the update channel rather
// than in admin Settings: that screen is the SERVER's, and this is a property of
@@ -147,15 +187,20 @@ async function checkUpdates() {
}
}
// After a channel or source switch: the last answer described somewhere else, so it
// means nothing now. Forget it and ask again.
async function recheck() {
update.value = null;
checkedOnce.value = false;
await checkUpdates();
}
async function switchChannel(next: UpdateChannel) {
if (next === channel.value) return;
updateError.value = "";
try {
channel.value = await updateBridge.setChannel(next);
// The previous answer described the OTHER channel, so it's meaningless now.
update.value = null;
checkedOnce.value = false;
await checkUpdates();
await recheck();
} catch (e) {
updateError.value = errorMessage(e, "The update channel couldn't be changed.");
}
@@ -166,10 +211,7 @@ async function switchSource(next: string | null) {
updateError.value = "";
try {
source.value = await updateBridge.setSource(next);
// Same reason as a channel switch: the last answer came from somewhere else.
update.value = null;
checkedOnce.value = false;
await checkUpdates();
await recheck();
} catch (e) {
updateError.value = errorMessage(e, "The update source couldn't be changed.");
}
@@ -613,35 +655,11 @@ onBeforeUnmount(() => stopSyncListener?.());
Updates from
</legend>
<div class="mt-2 flex flex-col gap-2">
<label class="flex items-start gap-2 text-sm">
<input
type="radio"
class="mt-1"
name="update-source"
:checked="source === null"
@change="switchSource(null)"
/>
<label v-for="o in sourceOptions" :key="o.title" class="flex items-start gap-2 text-sm">
<input type="radio" class="mt-1" name="update-source" :checked="o.checked" @change="o.pick()" />
<span>
<span class="font-medium">Fabled-Git</span>
<span class="block text-neutral-500 dark:text-neutral-400">
Where Inkwell is published, on the channel below.
</span>
</span>
</label>
<label class="flex items-start gap-2 text-sm">
<input
type="radio"
class="mt-1"
name="update-source"
:checked="source !== null"
@change="switchSource(offeredServer)"
/>
<span>
<span class="font-medium">{{ offeredServer }}</span>
<span class="block text-neutral-500 dark:text-neutral-400">
The build that server holds. It passes on official builds only: every
update is checked against the signature built into this app first.
</span>
<span class="font-medium">{{ o.title }}</span>
<span class="block text-neutral-500 dark:text-neutral-400">{{ o.detail }}</span>
</span>
</label>
</div>
@@ -653,36 +671,11 @@ onBeforeUnmount(() => stopSyncListener?.());
Channel
</legend>
<div class="mt-2 flex flex-col gap-2">
<label class="flex items-start gap-2 text-sm">
<input
type="radio"
class="mt-1"
name="update-channel"
value="stable"
:checked="channel === 'stable'"
@change="switchChannel('stable')"
/>
<label v-for="o in channelOptions" :key="o.title" class="flex items-start gap-2 text-sm">
<input type="radio" class="mt-1" name="update-channel" :checked="o.checked" @change="o.pick()" />
<span>
<span class="font-medium">Stable</span>
<span class="block text-neutral-500 dark:text-neutral-400">
Released versions only.
</span>
</span>
</label>
<label class="flex items-start gap-2 text-sm">
<input
type="radio"
class="mt-1"
name="update-channel"
value="dev"
:checked="channel === 'dev'"
@change="switchChannel('dev')"
/>
<span>
<span class="font-medium">Development</span>
<span class="block text-neutral-500 dark:text-neutral-400">
Every build that passes CI. Newer, and less tested.
</span>
<span class="font-medium">{{ o.title }}</span>
<span class="block text-neutral-500 dark:text-neutral-400">{{ o.detail }}</span>
</span>
</label>
</div>