diff --git a/frontend/src/components/BaseModal.vue b/frontend/src/components/BaseModal.vue
index e5fe31d..ad7aff6 100644
--- a/frontend/src/components/BaseModal.vue
+++ b/frontend/src/components/BaseModal.vue
@@ -2,8 +2,8 @@
// The backdrop + centered dialog panel every modal shares: a fixed dimmed overlay and
// a bordered, rounded panel with role="dialog". Emits `close` on Escape and on a
// backdrop pointer-down (mousedown outside the panel). The caller sizes/shadows/pads
-// the panel via `panelClass` and fills it (its own header + content) through the
-// default slot.
+// the panel via `panelClass` and fills it through the default slot; `title` adds the
+// heading row with a close button that a titled dialog wants.
//
// Owns the dialog's focus contract on behalf of all four callers: focus moves in on
// open, Tab is trapped inside, and focus returns to the opener on close. Doing it
@@ -13,6 +13,7 @@
// NoteEditor deliberately does NOT use this — its backdrop mousedown is drag-guarded and
// its Escape/⌘-Enter handling is bespoke (unsaved-edit safety), so it keeps its own shell.
import { onBeforeUnmount, onMounted, ref } from "vue";
+import Icon from "./Icon.vue";
withDefaults(
defineProps<{
@@ -22,6 +23,8 @@ withDefaults(
align?: "start" | "center";
/** aria-label for the panel when it has no visible heading (e.g. the command palette). */
ariaLabel?: string;
+ /** A heading row with a close button, for a dialog that has a title. */
+ title?: string;
}>(),
{ panelClass: "w-full max-w-sm shadow-xl", align: "start" },
);
@@ -109,6 +112,13 @@ onBeforeUnmount(() => restoreTo?.focus?.());
@keydown.esc="emit('close')"
@keydown.tab="onTab"
>
+
+
{{ title }}
+
+
diff --git a/frontend/src/components/ClientDownloads.vue b/frontend/src/components/ClientDownloads.vue
index 8a70e33..257c3a2 100644
--- a/frontend/src/components/ClientDownloads.vue
+++ b/frontend/src/components/ClientDownloads.vue
@@ -7,6 +7,7 @@
// because a wrong guess must cost a person nothing.
import { computed } from "vue";
import { useConfigStore, type ClientRelease } from "../stores/config";
+import { roughSize } from "../size";
const config = useConfigStore();
@@ -108,13 +109,6 @@ const missingPlatform = computed(() =>
const hasLinux = computed(() => Object.keys(config.clients).some((id) => id.startsWith("linux-")));
const installCommand = `curl -fsSL ${window.location.origin}/install.sh | sh`;
-// One decimal below 10 MB, none above: these sit in one list where a 2.7 MB
-// package and a 95 MB AppImage are compared, and "3 MB" next to "95 MB" loses the
-// only distinction that matters at the small end.
-function readableSize(bytes: number): string {
- const mb = bytes / 1024 / 1024;
- return `${mb < 10 ? mb.toFixed(1) : mb.toFixed(0)} MB`;
-}
@@ -144,7 +138,7 @@ function readableSize(bytes: number): string {
second source to contradict it, a wrong version here is a wrong
answer nothing can catch. Not knowing which build it is, is also
not a reason to withhold the download. -->
- Version {{ client.version || "unknown" }} · {{ readableSize(client.size) }}, and the only one that updates itself in place
diff --git a/frontend/src/components/LabelsModal.vue b/frontend/src/components/LabelsModal.vue
index c42696c..544055e 100644
--- a/frontend/src/components/LabelsModal.vue
+++ b/frontend/src/components/LabelsModal.vue
@@ -58,11 +58,7 @@ async function doMerge(sourceId: string, targetId: string) {
-
-