refactor(components): standardize z-index values and improve class naming conventions across multiple components

This commit is contained in:
Ivan
2026-04-27 11:15:25 -05:00
parent 356aea3560
commit b2c620eb20
86 changed files with 2881 additions and 1136 deletions
+49 -27
View File
@@ -28,7 +28,7 @@
<template v-else>
<div
class="sticky top-0 z-[100] flex bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-sm transition-colors overflow-x-hidden"
class="sticky top-0 z-100 flex bg-white dark:bg-zinc-950 border-gray-200 dark:border-zinc-800 border-b min-h-16 shadow-xs transition-colors overflow-x-hidden"
>
<div class="flex w-full px-2 sm:px-4 overflow-x-auto no-scrollbar">
<button
@@ -93,7 +93,7 @@
</button>
<button type="button" class="hidden sm:flex rounded-full" @click="syncPropagationNode">
<span
class="flex text-gray-800 dark:text-zinc-100 bg-white dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 hover:border-blue-400 dark:hover:border-blue-400/60 px-3 py-1.5 rounded-full shadow-sm transition"
class="flex text-gray-800 dark:text-zinc-100 bg-white dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 hover:border-blue-400 dark:hover:border-blue-400/60 px-3 py-1.5 rounded-full shadow-xs transition"
>
<MaterialDesignIcon
icon-name="refresh"
@@ -107,7 +107,7 @@
</button>
<button type="button" class="hidden sm:flex rounded-full" @click="composeNewMessage">
<span
class="flex rounded-full border border-zinc-800 bg-zinc-900 px-3 py-1.5 text-white shadow-sm transition hover:bg-zinc-800 dark:border-zinc-400 dark:bg-zinc-200 dark:text-zinc-900 dark:hover:bg-white"
class="flex rounded-full border border-zinc-800 bg-zinc-900 px-3 py-1.5 text-white shadow-xs transition hover:bg-zinc-800 dark:border-zinc-400 dark:bg-zinc-200 dark:text-zinc-900 dark:hover:bg-white"
>
<span>
<MaterialDesignIcon icon-name="email" class="w-6 h-6" />
@@ -130,13 +130,13 @@
<!-- sidebar backdrop for mobile -->
<div
v-if="isSidebarOpen"
class="fixed inset-0 z-[65] bg-black/20 backdrop-blur-sm sm:hidden"
class="fixed inset-0 z-65 bg-black/20 backdrop-blur-xs sm:hidden"
@click="isSidebarOpen = false"
></div>
<!-- sidebar -->
<div
class="fixed inset-y-0 left-0 z-[70] transform transition-all duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
class="fixed inset-y-0 left-0 z-70 transform transition-all duration-300 ease-in-out sm:relative sm:z-0 sm:flex sm:translate-x-0"
:class="[
isSidebarOpen ? 'translate-x-0' : '-translate-x-full',
isSidebarCollapsed ? 'w-16' : 'w-80 md:max-lg:w-64 lg:w-80',
@@ -371,13 +371,17 @@
/>
</RouterLink>
</div>
<div v-if="!isSidebarCollapsed" class="my-auto dark:text-white truncate">
{{ $t("app.my_identity") }}
<div
v-if="!isSidebarCollapsed"
class="my-auto min-w-0 flex-1 dark:text-white truncate"
:title="identitySidebarLabel"
>
{{ identitySidebarLabel }}
</div>
<div v-if="!isSidebarCollapsed" class="my-auto ml-auto shrink-0">
<button
type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-xs hover:bg-gray-400 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-zinc-100 dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
@click.stop="saveIdentitySettings"
>
{{ $t("common.save") }}
@@ -386,7 +390,7 @@
</div>
<div
v-if="isShowingMyIdentitySection && !isSidebarCollapsed"
class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
class="divide-y divide-gray-200 text-gray-900 border-t border-gray-200 dark:divide-zinc-800 dark:text-zinc-200 dark:border-zinc-800"
>
<div class="p-2">
<input
@@ -408,17 +412,17 @@
</div>
<div class="p-2 dark:border-zinc-900 overflow-hidden text-xs">
<div>{{ $t("app.lxmf_address") }}</div>
<div
class="text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono cursor-pointer"
:title="config.lxmf_address_hash"
@click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
>
{{ config.lxmf_address_hash }}
</div>
<div class="flex items-center justify-end pt-1">
<div class="flex min-w-0 items-center gap-1">
<div
class="min-w-0 flex-1 text-[10px] text-gray-700 dark:text-zinc-400 truncate font-mono cursor-pointer"
:title="config.lxmf_address_hash"
@click="copyValue(config.lxmf_address_hash, $t('app.lxmf_address'))"
>
{{ config.lxmf_address_hash }}
</div>
<button
type="button"
class="p-1 rounded-lg text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
class="shrink-0 rounded-lg p-1 text-gray-500 hover:text-blue-500 dark:hover:text-blue-400 transition-colors"
:title="$t('app.show_qr')"
@click.stop="openLxmfQr"
>
@@ -436,18 +440,25 @@
>
<div
class="flex text-gray-700 p-3 cursor-pointer dark:text-white"
data-testid="sidebar-announce-header"
@click="isShowingAnnounceSection = !isShowingAnnounceSection"
>
<div class="my-auto mr-2 shrink-0">
<button
type="button"
class="my-auto mr-2 flex shrink-0 items-center justify-center rounded-md border-0 bg-transparent p-0 text-inherit cursor-pointer"
:title="$t('app.announce_now')"
data-testid="sidebar-announce-radio"
@click.stop="sendAnnounce"
>
<MaterialDesignIcon icon-name="radio" class="size-6" />
</div>
</button>
<div v-if="!isSidebarCollapsed" class="my-auto truncate">
{{ $t("app.announce") }}
</div>
<div v-if="!isSidebarCollapsed" class="ml-auto shrink-0">
<button
type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-xs hover:bg-gray-400 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
@click.stop="sendAnnounce"
>
{{ $t("app.announce_now") }}
@@ -456,7 +467,7 @@
</div>
<div
v-if="isShowingAnnounceSection && !isSidebarCollapsed"
class="divide-y text-gray-900 border-t border-gray-200 dark:text-zinc-200 dark:border-zinc-800"
class="divide-y divide-gray-200 text-gray-900 border-t border-gray-200 dark:divide-zinc-800 dark:text-zinc-200 dark:border-zinc-800"
>
<div class="p-2 dark:border-zinc-800">
<select
@@ -522,7 +533,7 @@
<!-- LXMF QR modal -->
<div
v-if="showLxmfQr"
class="fixed inset-0 z-[190] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm"
class="fixed inset-0 z-190 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs"
@click.self="showLxmfQr = false"
>
<div class="w-full max-w-sm bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden">
@@ -547,7 +558,7 @@
</div>
<div
v-if="config?.lxmf_address_hash"
class="text-xs font-mono text-gray-700 dark:text-zinc-200 text-center break-words"
class="text-xs font-mono text-gray-700 dark:text-zinc-200 text-center wrap-break-word"
>
{{ getMyIdentityUri() }}
</div>
@@ -568,7 +579,7 @@
<transition name="fade-blur">
<div
v-if="isSwitchingIdentity"
class="fixed inset-0 z-[200] flex items-center justify-center bg-slate-900/45 dark:bg-black/55 backdrop-blur-sm px-4"
class="fixed inset-0 z-200 flex items-center justify-center bg-slate-900/45 dark:bg-black/55 backdrop-blur-xs px-4"
role="status"
aria-live="polite"
>
@@ -631,6 +642,7 @@ import TutorialModal from "./TutorialModal.vue";
import AppShellBanners from "./layout/AppShellBanners.vue";
import KeyboardShortcuts from "../js/KeyboardShortcuts";
import ElectronUtils from "../js/ElectronUtils";
import { postRequestPath } from "../js/reticulumPathfinding.js";
import ToneGenerator from "../js/ToneGenerator";
import logoUrl from "../assets/images/logo.png";
@@ -738,6 +750,11 @@ export default {
showWsDisconnectedBanner() {
return this.shellRunning && this.wsDisconnected && this.$route?.name !== "auth";
},
identitySidebarLabel() {
const raw = this.displayName;
const name = raw != null && String(raw).trim() !== "" ? String(raw).trim() : "";
return name || this.$t("app.my_identity");
},
shellCanvasStyle() {
const raw = Number(this.config?.ui_transparency ?? 0);
const t = Number.isFinite(raw) ? Math.max(0, Math.min(100, raw)) : 0;
@@ -796,6 +813,10 @@ export default {
if (v === "true" || v === "false") {
GlobalState.detailedOutboundSendStatus = v === "true";
}
const tg = localStorage.getItem("meshchatx_message_timestamp_grouping_enabled");
if (tg === "true" || tg === "false") {
GlobalState.messageTimestampGroupingEnabled = tg === "true";
}
} catch {
// ignore
}
@@ -1424,7 +1445,7 @@ export default {
try {
const preferredHash = this.config?.lxmf_preferred_propagation_node_destination_hash;
if (preferredHash) {
await window.api.post(`/api/v1/destination/${preferredHash}/request-path`);
await postRequestPath(window.api, preferredHash);
}
await window.api.get("/api/v1/lxmf/propagation-node/sync");
} catch (e) {
@@ -1821,8 +1842,9 @@ export default {
</script>
<style>
@reference "../style.css";
.banished-overlay {
@apply absolute inset-0 z-[100] flex items-center justify-center overflow-hidden pointer-events-none rounded-[inherit];
@apply absolute inset-0 z-100 flex items-center justify-center overflow-hidden pointer-events-none rounded-[inherit];
background: rgba(220, 38, 38, 0.12);
backdrop-filter: blur(3px) saturate(180%);
}
@@ -33,19 +33,19 @@
</div>
<!-- Controls -->
<div v-if="items.length > 1" class="absolute -bottom-2 right-0 flex items-center gap-2 z-[60]">
<div v-if="items.length > 1" class="absolute -bottom-2 right-0 flex items-center gap-2 z-60">
<div class="text-xs font-mono text-gray-500 dark:text-gray-400 mr-2">
{{ activeIndex + 1 }} / {{ items.length }}
</div>
<button
class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-sm border border-gray-200 dark:border-zinc-700"
class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-xs border border-gray-200 dark:border-zinc-700"
title="Previous"
@click.stop="prev"
>
<MaterialDesignIcon icon-name="chevron-left" class="size-5" />
</button>
<button
class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-sm border border-gray-200 dark:border-zinc-700"
class="p-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-gray-700 dark:text-gray-300 transition shadow-xs border border-gray-200 dark:border-zinc-700"
title="Next"
@click.stop="next"
>
@@ -57,7 +57,7 @@
<div v-if="items && items.length > 1" class="mt-4 flex justify-center">
<button
class="flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-xs font-bold text-gray-700 dark:text-gray-300 transition shadow-sm border border-gray-200 dark:border-zinc-700 uppercase tracking-wider"
class="flex items-center gap-1.5 px-4 py-1.5 rounded-full bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 text-xs font-bold text-gray-700 dark:text-gray-300 transition shadow-xs border border-gray-200 dark:border-zinc-700 uppercase tracking-wider"
@click="isExpanded = !isExpanded"
>
<MaterialDesignIcon :icon-name="isExpanded ? 'collapse-all' : 'expand-all'" class="size-4" />
@@ -12,7 +12,7 @@
@update:model-value="onVisibleUpdate"
>
<v-card
class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-900 border-0 overflow-hidden h-full max-h-[100dvh]"
class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-900 border-0 overflow-hidden h-full max-h-dvh"
>
<!-- Header -->
<v-toolbar flat color="transparent" class="px-3 sm:px-4 border-b dark:border-zinc-800 shrink-0">
@@ -25,7 +25,7 @@
</v-toolbar-title>
<span
v-if="version"
class="ml-3 font-black text-[10px] px-2 h-5 tracking-tighter uppercase rounded-sm bg-blue-600 text-white inline-flex items-center"
class="ml-3 font-black text-[10px] px-2 h-5 tracking-tighter uppercase rounded-xs bg-blue-600 text-white inline-flex items-center"
>
v{{ version }}
</span>
@@ -50,7 +50,7 @@
<div v-else-if="error" class="flex flex-col items-center justify-center h-full text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
<button type="button" class="primary-chip !px-6" @click="fetchChangelog">Retry</button>
<button type="button" class="primary-chip px-6!" @click="fetchChangelog">Retry</button>
</div>
<div
@@ -86,7 +86,7 @@
></v-checkbox>
</div>
<v-spacer></v-spacer>
<button type="button" class="primary-chip !px-8 !h-10 !rounded-xl" @click="close">
<button type="button" class="primary-chip px-8! h-10! rounded-xl!" @click="close">
{{ $t("common.close", "Close") }}
</button>
</v-card-actions>
@@ -106,7 +106,7 @@
</h1>
<div class="flex items-center gap-2">
<span
class="font-black text-[10px] px-2 h-5 rounded-sm bg-blue-600 text-white inline-flex items-center"
class="font-black text-[10px] px-2 h-5 rounded-xs bg-blue-600 text-white inline-flex items-center"
>
v{{ version }}
</span>
@@ -122,7 +122,7 @@
<div v-else-if="error" class="flex flex-col items-center justify-center py-20 text-center space-y-4">
<v-icon icon="mdi-alert-circle-outline" size="64" color="red"></v-icon>
<div class="text-red-500 font-bold text-lg">{{ error }}</div>
<button type="button" class="primary-chip !px-6" @click="fetchChangelog">Retry</button>
<button type="button" class="primary-chip px-6!" @click="fetchChangelog">Retry</button>
</div>
<div v-else class="changelog-content max-w-none prose dark:prose-invert pb-20">
@@ -250,6 +250,7 @@ export default {
</script>
<style>
@reference "../style.css";
.changelog-dialog .v-overlay__content {
border-radius: 0.5rem !important;
overflow: hidden;
@@ -265,54 +266,54 @@ export default {
}
.changelog-content {
@apply leading-relaxed !important;
@apply leading-relaxed;
}
.changelog-content h1 {
@apply text-3xl font-black mt-2 mb-6 text-gray-900 dark:text-white tracking-tight uppercase border-b-2 border-gray-100 dark:border-zinc-800 pb-2 !important;
@apply text-3xl font-black mt-2 mb-6 text-gray-900 dark:text-white tracking-tight uppercase border-b-2 border-gray-100 dark:border-zinc-800 pb-2;
}
.changelog-content h2 {
@apply flex items-center gap-3 text-xl font-bold mt-8 mb-4 text-gray-900 dark:text-white !important;
@apply flex items-center gap-3 text-xl font-bold mt-8 mb-4 text-gray-900 dark:text-white;
}
/* Style for [v4.0.0] style headers in markdown */
.changelog-content h2::before {
content: "VERSION";
@apply text-[10px] font-black bg-blue-500 text-white px-1.5 py-0.5 rounded-sm tracking-tighter !important;
@apply text-[10px] font-black bg-blue-500 text-white px-1.5 py-0.5 rounded-xs tracking-tighter;
}
.changelog-content h3 {
@apply text-lg font-bold mt-6 mb-3 text-blue-600 dark:text-blue-400 flex items-center gap-2 !important;
@apply text-lg font-bold mt-6 mb-3 text-blue-600 dark:text-blue-400 flex items-center gap-2;
}
.changelog-content h3::before {
content: "•";
@apply text-blue-500 font-black !important;
@apply text-blue-500 font-black;
}
.changelog-content p {
@apply my-4 text-gray-700 dark:text-zinc-300 leading-relaxed !important;
@apply my-4 text-gray-700 dark:text-zinc-300 leading-relaxed;
}
.changelog-content ul {
@apply my-6 space-y-3 list-disc pl-6 !important;
@apply my-6 space-y-3 list-disc pl-6;
}
.changelog-content li {
@apply text-gray-600 dark:text-zinc-400 transition-colors hover:text-gray-900 dark:hover:text-white !important;
@apply text-gray-600 dark:text-zinc-400 transition-colors hover:text-gray-900 dark:hover:text-white;
}
.changelog-content strong {
@apply font-bold text-gray-900 dark:text-zinc-100 !important;
@apply font-bold text-gray-900 dark:text-zinc-100;
}
.changelog-content code {
@apply bg-blue-50 dark:bg-blue-900/20 px-1.5 py-0.5 rounded-sm text-blue-700 dark:text-blue-300 font-mono text-[0.85em] border border-blue-100 dark:border-blue-800/30 !important;
@apply bg-blue-50 dark:bg-blue-900/20 px-1.5 py-0.5 rounded-xs text-blue-700 dark:text-blue-300 font-mono text-[0.85em] border border-blue-100 dark:border-blue-800/30;
}
.changelog-content hr {
@apply my-10 border-gray-100 dark:border-zinc-800 !important;
@apply my-10 border-gray-100 dark:border-zinc-800;
}
/* Highlight tags like [4.0.0] if they are inside the text */
@@ -321,10 +322,10 @@ export default {
}
.changelog-content h2 {
@apply py-2 px-4 bg-gray-50 dark:bg-zinc-800/50 rounded-md border border-gray-100 dark:border-zinc-800 !important;
@apply py-2 px-4 bg-gray-50 dark:bg-zinc-800/50 rounded-md border border-gray-100 dark:border-zinc-800;
}
.changelog-content .version-tag {
@apply bg-blue-600 text-white px-2 py-0.5 rounded-sm font-black text-sm tracking-tighter !important;
@apply bg-blue-600 text-white px-2 py-0.5 rounded-xs font-black text-sm tracking-tighter;
}
</style>
@@ -9,7 +9,7 @@
<div ref="dropdown-button" @click.stop="toggleMenu">
<slot>
<div
class="size-8 border border-gray-300 dark:border-zinc-700 rounded shadow cursor-pointer"
class="size-8 border border-gray-300 dark:border-zinc-700 rounded-sm shadow-sm cursor-pointer"
:style="{ 'background-color': colour }"
></div>
</slot>
@@ -24,7 +24,7 @@
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
<div v-if="isShowingMenu" class="absolute left-0 z-[100] mt-2">
<div v-if="isShowingMenu" class="absolute left-0 z-100 mt-2">
<v-color-picker
v-model="colourPickerValue"
:modes="['hex']"
@@ -2,10 +2,7 @@
<template>
<transition name="slide-down">
<div
v-if="isOpen"
class="fixed inset-x-0 top-0 z-[200] flex items-start justify-center p-4 pointer-events-none"
>
<div v-if="isOpen" class="fixed inset-x-0 top-0 z-200 flex items-start justify-center p-4 pointer-events-none">
<div
v-click-outside="close"
class="w-full max-w-2xl bg-white/95 dark:bg-zinc-900/95 backdrop-blur-md rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden flex flex-col max-h-[70vh] pointer-events-auto mt-2 sm:mt-8"
@@ -26,7 +23,7 @@
/>
<div class="flex items-center gap-1 ml-2">
<kbd
class="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg shadow-sm"
class="px-2 py-1 text-xs font-semibold text-gray-500 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xs"
>ESC</kbd
>
</div>
@@ -93,14 +90,14 @@
>
<div class="flex items-center gap-1.5">
<kbd
class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded shadow-sm"
class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-sm shadow-xs"
></kbd
>
<span>{{ $t("command_palette.footer_navigate") }}</span>
</div>
<div class="flex items-center gap-1.5">
<kbd
class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded shadow-sm"
class="px-1.5 py-0.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-sm shadow-xs"
>Enter</kbd
>
<span>{{ $t("command_palette.footer_select") }}</span>
@@ -2,8 +2,8 @@
<template>
<Transition name="confirm-dialog">
<div v-if="pendingConfirm" class="fixed inset-0 z-[9999] flex items-center justify-center p-4">
<div class="fixed inset-0 bg-black/50 backdrop-blur-sm shadow-2xl" @click="cancel"></div>
<div v-if="pendingConfirm" class="fixed inset-0 z-9999 flex items-center justify-center p-4">
<div class="fixed inset-0 bg-black/50 backdrop-blur-xs shadow-2xl" @click="cancel"></div>
<div
class="relative w-full sm:w-auto sm:min-w-[400px] sm:max-w-md bg-white dark:bg-zinc-900 sm:rounded-3xl rounded-3xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transform transition-all"
@@ -12,7 +12,7 @@
<div class="p-8">
<div class="flex items-start mb-6">
<div
class="flex-shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"
class="shrink-0 flex items-center justify-center w-12 h-12 rounded-2xl bg-red-100 dark:bg-red-900/30 text-red-600 dark:text-red-400 mr-4"
>
<MaterialDesignIcon icon-name="alert-circle" class="w-6 h-6" />
</div>
@@ -22,7 +22,7 @@
<div
v-if="isShowingMenu && dropdownPosition"
ref="dropdownPanel"
class="overflow-x-hidden fixed z-[200] w-56 rounded-md bg-white dark:bg-zinc-800 shadow-lg border border-gray-200 dark:border-zinc-700 focus:outline-none"
class="overflow-x-hidden fixed z-200 w-56 rounded-md bg-white dark:bg-zinc-800 shadow-lg border border-gray-200 dark:border-zinc-700 focus:outline-hidden"
:style="dropdownPanelStyle"
@click.stop="hideMenu"
>
@@ -3,7 +3,7 @@
<template>
<button
type="button"
class="text-gray-500 hover:text-gray-700 dark:text-zinc-400 dark:hover:text-zinc-100 hover:bg-gray-100 dark:hover:bg-zinc-800 p-2 rounded-full w-9 h-9 flex items-center justify-center flex-shrink-0 transition-all duration-200"
class="text-gray-500 hover:text-gray-700 dark:text-zinc-400 dark:hover:text-zinc-100 hover:bg-gray-100 dark:hover:bg-zinc-800 p-2 rounded-full w-9 h-9 flex items-center justify-center shrink-0 transition-all duration-200"
>
<slot />
</button>
@@ -16,7 +16,7 @@
v-if="isDropdownOpen"
ref="languageDropdown"
v-click-outside="closeDropdown"
class="fixed w-48 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-[9999] overflow-x-hidden"
class="fixed w-48 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-9999 overflow-x-hidden"
:style="dropdownStyle"
>
<div class="p-2">
@@ -21,7 +21,7 @@
v-if="isDropdownOpen"
ref="notificationDropdown"
v-click-outside="closeDropdown"
class="fixed w-80 sm:w-96 md:max-lg:w-80 lg:w-96 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-[9999] max-h-[min(500px,calc(100vh-2rem))] overflow-hidden flex flex-col"
class="fixed w-80 sm:w-96 md:max-lg:w-80 lg:w-96 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xl z-9999 max-h-[min(500px,calc(100vh-2rem))] overflow-hidden flex flex-col"
:style="dropdownStyle"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
@@ -88,7 +88,7 @@
@click="onNotificationClick(notification)"
>
<div class="flex gap-3">
<div class="flex-shrink-0">
<div class="shrink-0">
<div
v-if="notification.lxmf_user_icon"
class="p-2 rounded-lg"
@@ -118,7 +118,7 @@
{{ notification.custom_display_name ?? notification.display_name }}
</div>
<div
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap flex-shrink-0"
class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap shrink-0"
>
{{ formatTimeAgo(notification.updated_at) }}
</div>
@@ -10,7 +10,7 @@
? 'bg-blue-100 text-blue-800 group:text-blue-800 dark:bg-zinc-800 dark:text-blue-300'
: 'hover:bg-gray-100 dark:hover:bg-zinc-700',
]"
class="w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500 overflow-hidden"
class="w-full text-gray-800 dark:text-zinc-200 group flex gap-x-3 rounded-r-full p-2 mr-2 text-sm leading-6 font-semibold focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:focus-visible:outline-zinc-500 overflow-hidden"
@click="handleNavigate($event, navigate)"
>
<span class="my-auto shrink-0">
+2 -2
View File
@@ -2,7 +2,7 @@
<template>
<div
class="fixed bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-[100] flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
class="fixed bottom-4 left-1/2 -translate-x-1/2 sm:left-auto sm:right-4 sm:translate-x-0 z-100 flex flex-col gap-2 pointer-events-none w-[calc(100%-2rem)] max-w-sm sm:w-auto sm:max-w-md"
>
<TransitionGroup name="toast">
<div
@@ -12,7 +12,7 @@
:class="toastClass(toast.type)"
>
<!-- icon -->
<div class="mr-3 flex-shrink-0">
<div class="mr-3 shrink-0">
<MaterialDesignIcon
v-if="toast.type === 'success'"
icon-name="check-circle"
@@ -13,7 +13,7 @@
@update:model-value="onVisibleUpdate"
>
<v-card
class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950 border-0 overflow-hidden relative h-full max-h-[100dvh]"
class="flex min-h-0 flex-1 flex-col bg-white dark:bg-zinc-950 border-0 overflow-hidden relative h-full max-h-dvh"
>
<!-- Settings Controls -->
<div class="absolute top-4 left-4 z-50 flex items-center gap-1">
@@ -39,7 +39,7 @@
class="h-full transition-all duration-500 ease-out"
:class="[
currentStep >= step ? 'bg-blue-500' : 'bg-transparent',
currentStep === step ? 'flex-[2]' : 'flex-1',
currentStep === step ? 'flex-2' : 'flex-1',
]"
:style="{ borderRight: step < totalSteps ? '1px solid rgba(0,0,0,0.05)' : 'none' }"
></div>
@@ -282,103 +282,218 @@
</p>
</div>
<div
class="flex items-start gap-3 sm:gap-4 rounded-2xl border border-gray-200 dark:border-zinc-700 bg-white/80 dark:bg-zinc-900/60 p-3.5 sm:p-4"
>
<div class="shrink-0 pr-0.5 pt-0.5 sm:pt-1 sm:pr-1 flex items-start">
<Toggle
v-model="defaultBootstrapOnly"
@update:model-value="persistDefaultBootstrapOnly"
/>
</div>
<div class="min-w-0 flex-1 pl-0.5 sm:pl-0 sm:pt-0.5">
<div class="text-sm font-semibold text-gray-900 dark:text-white leading-snug">
{{ $t("tutorial.bootstrap_only_label") }}
</div>
<p class="text-xs text-gray-500 dark:text-zinc-400 mt-1.5 leading-relaxed">
{{ $t("tutorial.bootstrap_only_hint") }}
</p>
</div>
</div>
<div class="space-y-4">
<div
v-if="sortedDiscoveredInterfaces.length > 0"
class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl p-4 border border-emerald-500/20"
v-if="hasAnyBootstrapsToShow"
class="w-full max-w-6xl mx-auto flex items-center gap-2 border-0 border-b border-gray-200/90 dark:border-zinc-600/90 py-1.5"
>
<div class="flex items-center gap-2 mb-3 px-1 text-sm">
<v-icon icon="mdi-radar" color="emerald"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_discovered")
}}</span>
</div>
<div class="space-y-2 max-h-[260px] overflow-y-auto pr-2 custom-scrollbar">
<label
v-for="iface in sortedDiscoveredInterfaces"
:key="iface.discovery_hash || iface.name"
class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
:class="[
isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-100 dark:border-zinc-700 hover:border-emerald-400',
]"
>
<input
type="checkbox"
class="w-4 h-4 accent-emerald-500"
:checked="isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)"
@change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
/>
<v-icon icon="mdi-magnify" size="20" class="shrink-0 text-gray-400" />
<input
v-model="bootstrapListSearch"
type="search"
autocomplete="off"
:placeholder="$t('tutorial.bootstrap_search_placeholder')"
class="min-w-0 flex-1 border-0 bg-transparent p-0 text-sm text-gray-900 shadow-none ring-0 outline-hidden focus:ring-0 dark:text-zinc-100 placeholder:text-gray-400 dark:placeholder:text-zinc-500"
/>
<button
v-if="bootstrapListSearch"
type="button"
class="shrink-0 rounded p-1 text-gray-400 transition-colors hover:text-gray-700 dark:hover:text-zinc-200"
:title="$t('tutorial.bootstrap_search_clear')"
:aria-label="$t('tutorial.bootstrap_search_clear')"
@click="bootstrapListSearch = ''"
>
<v-icon icon="mdi-close" size="18" />
</button>
</div>
<div
v-if="sortedDiscoveredInterfaces.length > 0"
class="h-fit min-w-0 bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl border border-emerald-500/20"
>
<button
type="button"
class="flex w-full items-center justify-between gap-2 p-4 text-left sm:px-4"
:aria-expanded="bootstrapDiscoveredSectionOpen"
@click="bootstrapDiscoveredSectionOpen = !bootstrapDiscoveredSectionOpen"
>
<div class="flex min-w-0 items-center gap-2 text-sm">
<MaterialDesignIcon
:icon-name="getDiscoveryIcon(iface)"
class="w-5 h-5 text-emerald-500 shrink-0"
:icon-name="bootstrapDiscoveredSectionOpen ? 'chevron-up' : 'chevron-down'"
class="size-4 shrink-0 text-gray-500"
/>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
{{ iface.name }}
</div>
<div
class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
>
<span v-if="iface.reachable_on"
>{{ iface.reachable_on
}}<span v-if="iface.port">:{{ iface.port }}</span></span
<v-icon icon="mdi-radar" color="emerald"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_discovered")
}}</span>
</div>
</button>
<div v-show="bootstrapDiscoveredSectionOpen" class="px-4 pb-4">
<p
v-if="
bootstrapListSearch &&
sortedDiscoveredInterfaces.length > 0 &&
filteredDiscoveredForBootstrap.length === 0
"
class="text-xs text-gray-500 dark:text-zinc-400"
>
{{ $t("tutorial.bootstrap_search_no_match") }}
</p>
<div
v-else
class="space-y-2 max-h-[260px] overflow-y-auto pr-2 pt-1 custom-scrollbar"
>
<label
v-for="iface in filteredDiscoveredForBootstrap"
:key="iface.discovery_hash || iface.name"
class="flex cursor-pointer items-center gap-3 rounded-xl border bg-white p-3 transition-all dark:bg-zinc-800"
:class="[
isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-100 dark:border-zinc-700 hover:border-emerald-400',
]"
>
<input
type="checkbox"
class="h-4 w-4 accent-emerald-500"
:checked="
isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
"
@change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
/>
<MaterialDesignIcon
:icon-name="getDiscoveryIcon(iface)"
class="h-5 w-5 shrink-0 text-emerald-500"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-bold text-gray-900 dark:text-white">
{{ iface.name }}
</div>
<div
class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
>
<span v-else>{{ iface.type }}</span>
<span class="ml-2 capitalize">{{ iface.status }}</span>
<span v-if="iface.reachable_on"
>{{ iface.reachable_on
}}<span v-if="iface.port">:{{ iface.port }}</span></span
>
<span v-else>{{ iface.type }}</span>
<span class="ml-2 capitalize">{{ iface.status }}</span>
</div>
</div>
</div>
</label>
</label>
</div>
</div>
</div>
<div
class="bg-gray-50 dark:bg-zinc-900 rounded-3xl p-4 border border-gray-100 dark:border-zinc-800"
class="h-fit min-w-0 rounded-3xl border border-gray-100 bg-gray-50 p-0 dark:border-zinc-800 dark:bg-zinc-900"
>
<div class="flex items-center gap-2 mb-3 px-1 text-sm">
<v-icon icon="mdi-web" color="blue"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_community")
}}</span>
</div>
<div class="space-y-2 max-h-[260px] overflow-y-auto pr-2 custom-scrollbar">
<label
v-for="iface in communityInterfaces"
:key="iface.name"
class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
:class="[
isBootstrapSelected(`comm:${iface.name}`)
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-100 dark:border-zinc-700 hover:border-blue-400',
]"
<div class="flex items-center justify-between gap-2 p-4 pr-2 sm:px-4">
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-2 text-left text-sm"
:aria-expanded="bootstrapCommunitySectionOpen"
@click="bootstrapCommunitySectionOpen = !bootstrapCommunitySectionOpen"
>
<input
type="checkbox"
class="w-4 h-4 accent-blue-500"
:checked="isBootstrapSelected(`comm:${iface.name}`)"
@change="toggleBootstrap(`comm:${iface.name}`)"
<MaterialDesignIcon
:icon-name="bootstrapCommunitySectionOpen ? 'chevron-up' : 'chevron-down'"
class="size-4 shrink-0 text-gray-500"
/>
<v-icon icon="mdi-server-network" color="blue" size="20"></v-icon>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
{{ iface.name }}
</div>
<div
class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
>
{{ iface.target_host
}}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
</div>
</div>
<span
v-if="iface.online"
class="text-[9px] font-bold text-green-500 uppercase tracking-widest shrink-0"
>{{ $t("tutorial.online") }}</span
<v-icon icon="mdi-web" color="blue"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_community")
}}</span>
</button>
<button
type="button"
class="shrink-0 rounded-lg p-1.5 text-gray-500 transition-colors hover:bg-gray-100 hover:text-blue-600 disabled:opacity-50 dark:hover:bg-zinc-800 dark:hover:text-blue-400"
:disabled="refreshingCommunityPresets"
:title="$t('interfaces.community_presets_refresh')"
:aria-label="$t('interfaces.community_presets_refresh')"
@click.stop="refreshCommunityPresets"
>
<v-icon
icon="mdi-refresh"
size="20"
:class="{ 'animate-spin': refreshingCommunityPresets }"
/>
</button>
</div>
<div v-show="bootstrapCommunitySectionOpen" class="px-4 pb-4">
<p
v-if="
bootstrapListSearch &&
communityInterfaces.length > 0 &&
filteredCommunityForBootstrap.length === 0
"
class="text-xs text-gray-500 dark:text-zinc-400"
>
{{ $t("tutorial.bootstrap_search_no_match") }}
</p>
<div
v-else
class="space-y-2 max-h-[260px] overflow-y-auto pr-2 pt-1 custom-scrollbar"
>
<label
v-for="iface in filteredCommunityForBootstrap"
:key="iface.name"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 transition-all dark:border-zinc-700 dark:bg-zinc-800"
:class="[
isBootstrapSelected(`comm:${iface.name}`)
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'hover:border-blue-400',
]"
>
</label>
<div v-if="loadingInterfaces" class="flex justify-center py-3">
<v-progress-circular indeterminate color="blue" size="24"></v-progress-circular>
<input
type="checkbox"
class="h-4 w-4 accent-blue-500"
:checked="isBootstrapSelected(`comm:${iface.name}`)"
@change="toggleBootstrap(`comm:${iface.name}`)"
/>
<v-icon icon="mdi-server-network" color="blue" size="20"></v-icon>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-bold text-gray-900 dark:text-white">
{{ iface.name }}
</div>
<div
class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
>
{{ iface.target_host
}}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
</div>
</div>
<span
v-if="iface.online"
class="shrink-0 text-[9px] font-bold uppercase tracking-widest text-green-500"
>{{ $t("tutorial.online") }}</span
>
</label>
<div v-if="loadingInterfaces" class="flex justify-center py-3">
<v-progress-circular
indeterminate
color="blue"
size="24"
></v-progress-circular>
</div>
</div>
</div>
</div>
@@ -401,7 +516,7 @@
</button>
<button
type="button"
class="px-5 py-2 text-xs rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold shadow transition-all"
class="px-5 py-2 text-xs rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-bold shadow-sm transition-all"
:disabled="
addingBootstraps || reloadingReticulum || selectedBootstrapCount === 0
"
@@ -434,7 +549,7 @@
<div class="flex flex-col items-center gap-6 py-4">
<div
class="bg-blue-500/10 dark:bg-blue-500/20 p-6 rounded-[2rem] text-center space-y-4 border border-blue-500/20 max-w-md"
class="bg-blue-500/10 dark:bg-blue-500/20 p-6 rounded-4xl text-center space-y-4 border border-blue-500/20 max-w-md"
>
<v-icon icon="mdi-server-network" color="blue" size="48"></v-icon>
<div class="text-lg font-bold text-gray-900 dark:text-white">
@@ -461,7 +576,7 @@
</button>
<button
type="button"
class="w-full px-6 py-3 rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-sm transition-all transform hover:scale-[1.02]"
class="w-full px-6 py-3 rounded-2xl border-2 border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-bold shadow-xs transition-all transform hover:scale-[1.02]"
@click="nextStep"
>
{{ $t("tutorial.propagation_skip_auto") }}
@@ -506,14 +621,14 @@
<a
href="/meshchatx-docs/index.html"
target="_blank"
class="px-3 py-1 text-[10px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all inline-block"
class="px-3 py-1 text-[10px] rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-xs transition-all inline-block"
>
{{ $t("tutorial.meshchatx_docs") }}
</a>
<a
:href="reticulumBundledDocsUrl"
target="_blank"
class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-block"
class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-block"
>
{{ $t("tutorial.reticulum_docs") }}
</a>
@@ -534,7 +649,7 @@
</div>
<button
type="button"
class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
class="px-3 py-1 text-[10px] rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="gotoRoute('micron-editor')"
>
{{ $t("tutorial.open_micron_editor") }}
@@ -643,7 +758,7 @@
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="previousStep"
>
{{ $t("tutorial.back") }}
@@ -654,7 +769,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
class="px-6 py-2.5 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
@click="skipTutorial"
>
{{ $t("tutorial.skip") }}
@@ -663,7 +778,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
class="px-8 h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold text-sm shadow-sm transition-all"
class="px-8 h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold text-sm shadow-xs transition-all"
@click="nextStep"
>
{{ $t("tutorial.next") }}
@@ -672,7 +787,7 @@
<button
v-else
type="button"
class="px-8 h-12 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm shadow-sm transition-all"
class="px-8 h-12 rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-sm shadow-xs transition-all"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
@@ -707,7 +822,7 @@
class="h-full transition-all duration-500 ease-out"
:class="[
currentStep >= step ? 'bg-blue-500' : 'bg-transparent',
currentStep === step ? 'flex-[2]' : 'flex-1',
currentStep === step ? 'flex-2' : 'flex-1',
]"
:style="{ borderRight: step < totalSteps ? '1px solid rgba(0,0,0,0.05)' : 'none' }"
></div>
@@ -950,104 +1065,223 @@
</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 max-w-6xl mx-auto">
<div
class="flex items-start gap-3 sm:gap-5 max-w-3xl mx-auto rounded-2xl border border-gray-200 dark:border-zinc-700 bg-white/80 dark:bg-zinc-900/60 p-3.5 sm:p-5"
>
<div class="shrink-0 pr-0.5 pt-0.5 sm:pt-1.5 sm:pr-1 flex items-start">
<Toggle
v-model="defaultBootstrapOnly"
@update:model-value="persistDefaultBootstrapOnly"
/>
</div>
<div class="min-w-0 flex-1 pl-0.5 sm:pl-0 sm:pt-0.5">
<div
class="text-sm sm:text-base font-semibold text-gray-900 dark:text-white leading-snug"
>
{{ $t("tutorial.bootstrap_only_label") }}
</div>
<p
class="text-xs sm:text-sm text-gray-500 dark:text-zinc-400 mt-1.5 sm:mt-2 leading-relaxed"
>
{{ $t("tutorial.bootstrap_only_hint") }}
</p>
</div>
</div>
<div
v-if="hasAnyBootstrapsToShow"
class="flex w-full max-w-6xl mx-auto items-center gap-2 border-0 border-b border-gray-200/90 dark:border-zinc-600/90 py-1.5"
>
<v-icon icon="mdi-magnify" size="22" class="shrink-0 text-gray-400" />
<input
v-model="bootstrapListSearch"
type="search"
autocomplete="off"
:placeholder="$t('tutorial.bootstrap_search_placeholder')"
class="min-w-0 flex-1 border-0 bg-transparent p-0 text-base text-gray-900 shadow-none ring-0 outline-hidden focus:ring-0 dark:text-zinc-100 placeholder:text-gray-400 dark:placeholder:text-zinc-500"
/>
<button
v-if="bootstrapListSearch"
type="button"
class="shrink-0 rounded p-1.5 text-gray-400 transition-colors hover:text-gray-700 dark:hover:text-zinc-200"
:title="$t('tutorial.bootstrap_search_clear')"
:aria-label="$t('tutorial.bootstrap_search_clear')"
@click="bootstrapListSearch = ''"
>
<v-icon icon="mdi-close" size="20" />
</button>
</div>
<div class="grid max-w-6xl mx-auto grid-cols-1 items-start gap-6 lg:grid-cols-2">
<div
v-if="sortedDiscoveredInterfaces.length > 0"
class="bg-emerald-500/5 dark:bg-emerald-500/10 rounded-[1.5rem] p-5 border border-emerald-500/20"
class="h-fit min-w-0 bg-emerald-500/5 dark:bg-emerald-500/10 rounded-3xl border border-emerald-500/20"
>
<div class="flex items-center gap-2 mb-4">
<v-icon icon="mdi-radar" color="emerald" size="22"></v-icon>
<span class="text-base font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_discovered")
}}</span>
</div>
<div class="space-y-2 max-h-[480px] overflow-y-auto pr-2 custom-scrollbar">
<label
v-for="iface in sortedDiscoveredInterfaces"
:key="iface.discovery_hash || iface.name"
class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
:class="[
isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'border-gray-100 dark:border-zinc-700 hover:border-emerald-400',
]"
>
<input
type="checkbox"
class="w-4 h-4 accent-emerald-500"
:checked="isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)"
@change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
/>
<button
type="button"
class="flex w-full items-center justify-between gap-2 p-4 text-left sm:px-5"
:aria-expanded="bootstrapDiscoveredSectionOpen"
@click="bootstrapDiscoveredSectionOpen = !bootstrapDiscoveredSectionOpen"
>
<div class="flex min-w-0 items-center gap-2.5 text-base">
<MaterialDesignIcon
:icon-name="getDiscoveryIcon(iface)"
class="w-5 h-5 text-emerald-500 shrink-0"
:icon-name="bootstrapDiscoveredSectionOpen ? 'chevron-up' : 'chevron-down'"
class="size-4 shrink-0 text-gray-500"
/>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
{{ iface.name }}
</div>
<div
class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
>
<span v-if="iface.reachable_on"
>{{ iface.reachable_on
}}<span v-if="iface.port">:{{ iface.port }}</span></span
<v-icon icon="mdi-radar" color="emerald" size="22"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_discovered")
}}</span>
</div>
</button>
<div v-show="bootstrapDiscoveredSectionOpen" class="px-4 pb-5 sm:px-5">
<p
v-if="
bootstrapListSearch &&
sortedDiscoveredInterfaces.length > 0 &&
filteredDiscoveredForBootstrap.length === 0
"
class="text-sm text-gray-500 dark:text-zinc-400"
>
{{ $t("tutorial.bootstrap_search_no_match") }}
</p>
<div
v-else
class="max-h-[480px] space-y-2 overflow-y-auto pr-2 pt-1 custom-scrollbar"
>
<label
v-for="iface in filteredDiscoveredForBootstrap"
:key="iface.discovery_hash || iface.name"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 transition-all dark:border-zinc-700 dark:bg-zinc-800"
:class="[
isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
? 'border-emerald-500 bg-emerald-50 dark:bg-emerald-900/20'
: 'hover:border-emerald-400',
]"
>
<input
type="checkbox"
class="h-4 w-4 accent-emerald-500"
:checked="
isBootstrapSelected(`disc:${iface.discovery_hash || iface.name}`)
"
@change="toggleBootstrap(`disc:${iface.discovery_hash || iface.name}`)"
/>
<MaterialDesignIcon
:icon-name="getDiscoveryIcon(iface)"
class="h-5 w-5 shrink-0 text-emerald-500"
/>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-bold text-gray-900 dark:text-white">
{{ iface.name }}
</div>
<div
class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
>
<span v-else>{{ iface.type }}</span>
<span class="ml-2 capitalize">{{ iface.status }}</span>
<span v-if="iface.reachable_on"
>{{ iface.reachable_on
}}<span v-if="iface.port">:{{ iface.port }}</span></span
>
<span v-else>{{ iface.type }}</span>
<span class="ml-2 capitalize">{{ iface.status }}</span>
</div>
</div>
</div>
</label>
</label>
</div>
</div>
</div>
<div
class="bg-gray-50 dark:bg-zinc-900 rounded-[1.5rem] p-5 border border-gray-100 dark:border-zinc-800"
class="h-fit min-w-0 rounded-3xl border border-gray-100 bg-gray-50 p-0 dark:border-zinc-800 dark:bg-zinc-900"
:class="[sortedDiscoveredInterfaces.length === 0 ? 'lg:col-span-2' : '']"
>
<div class="flex items-center gap-2 mb-4">
<v-icon icon="mdi-web" color="blue" size="22"></v-icon>
<span class="text-base font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_community")
}}</span>
</div>
<div class="space-y-2 max-h-[480px] overflow-y-auto pr-2 custom-scrollbar">
<label
v-for="iface in communityInterfaces"
:key="iface.name"
class="flex items-center gap-3 p-3 bg-white dark:bg-zinc-800 rounded-xl border cursor-pointer transition-all"
:class="[
isBootstrapSelected(`comm:${iface.name}`)
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-100 dark:border-zinc-700 hover:border-blue-400',
]"
<div class="flex items-center justify-between gap-2 p-4 pr-2 sm:px-5">
<button
type="button"
class="flex min-w-0 flex-1 items-center gap-2.5 text-left text-base"
:aria-expanded="bootstrapCommunitySectionOpen"
@click="bootstrapCommunitySectionOpen = !bootstrapCommunitySectionOpen"
>
<input
type="checkbox"
class="w-4 h-4 accent-blue-500"
:checked="isBootstrapSelected(`comm:${iface.name}`)"
@change="toggleBootstrap(`comm:${iface.name}`)"
<MaterialDesignIcon
:icon-name="bootstrapCommunitySectionOpen ? 'chevron-up' : 'chevron-down'"
class="size-4 shrink-0 text-gray-500"
/>
<v-icon icon="mdi-server-network" color="blue" size="22"></v-icon>
<div class="flex-1 min-w-0">
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
{{ iface.name }}
</div>
<div
class="text-[10px] font-mono text-gray-500 dark:text-zinc-400 truncate"
>
{{ iface.target_host
}}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
</div>
</div>
<span
v-if="iface.online"
class="text-[9px] font-bold text-green-500 uppercase tracking-widest shrink-0"
>{{ $t("tutorial.online") }}</span
<v-icon icon="mdi-web" color="blue" size="22"></v-icon>
<span class="font-bold text-gray-900 dark:text-white">{{
$t("tutorial.bootstrap_community")
}}</span>
</button>
<button
type="button"
class="shrink-0 rounded-lg p-2 text-gray-500 transition-colors hover:bg-gray-100 hover:text-blue-600 disabled:opacity-50 dark:hover:bg-zinc-800 dark:hover:text-blue-400"
:disabled="refreshingCommunityPresets"
:title="$t('interfaces.community_presets_refresh')"
:aria-label="$t('interfaces.community_presets_refresh')"
@click.stop="refreshCommunityPresets"
>
<v-icon
icon="mdi-refresh"
size="22"
:class="{ 'animate-spin': refreshingCommunityPresets }"
/>
</button>
</div>
<div v-show="bootstrapCommunitySectionOpen" class="px-4 pb-5 sm:px-5">
<p
v-if="
bootstrapListSearch &&
communityInterfaces.length > 0 &&
filteredCommunityForBootstrap.length === 0
"
class="text-sm text-gray-500 dark:text-zinc-400"
>
{{ $t("tutorial.bootstrap_search_no_match") }}
</p>
<div
v-else
class="max-h-[480px] space-y-2 overflow-y-auto pr-2 pt-1 custom-scrollbar"
>
<label
v-for="iface in filteredCommunityForBootstrap"
:key="iface.name"
class="flex cursor-pointer items-center gap-3 rounded-xl border border-gray-100 bg-white p-3 transition-all dark:border-zinc-700 dark:bg-zinc-800"
:class="[
isBootstrapSelected(`comm:${iface.name}`)
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'hover:border-blue-400',
]"
>
</label>
<div v-if="loadingInterfaces" class="flex justify-center py-3">
<v-progress-circular indeterminate color="blue" size="24"></v-progress-circular>
<input
type="checkbox"
class="h-4 w-4 accent-blue-500"
:checked="isBootstrapSelected(`comm:${iface.name}`)"
@change="toggleBootstrap(`comm:${iface.name}`)"
/>
<v-icon icon="mdi-server-network" color="blue" size="22"></v-icon>
<div class="min-w-0 flex-1">
<div class="truncate text-sm font-bold text-gray-900 dark:text-white">
{{ iface.name }}
</div>
<div
class="truncate font-mono text-[10px] text-gray-500 dark:text-zinc-400"
>
{{ iface.target_host
}}<span v-if="iface.target_port">:{{ iface.target_port }}</span>
</div>
</div>
<span
v-if="iface.online"
class="shrink-0 text-[9px] font-bold uppercase tracking-widest text-green-500"
>{{ $t("tutorial.online") }}</span
>
</label>
<div v-if="loadingInterfaces" class="flex justify-center py-3">
<v-progress-circular
indeterminate
color="blue"
size="24"
></v-progress-circular>
</div>
</div>
</div>
</div>
@@ -1161,7 +1395,7 @@
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
<div
class="flex flex-col items-center gap-6 p-8 rounded-[2rem] bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
class="flex flex-col items-center gap-6 p-8 rounded-4xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-book-open-variant" color="blue" size="64"></v-icon>
<div>
@@ -1175,14 +1409,14 @@
<a
href="/meshchatx-docs/index.html"
target="_blank"
class="h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all inline-flex items-center justify-center px-6"
class="h-12 rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-xs transition-all inline-flex items-center justify-center px-6"
>
{{ $t("tutorial.read_meshchatx_docs") }}
</a>
<a
:href="reticulumBundledDocsUrl"
target="_blank"
class="h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-flex items-center justify-center px-6"
class="h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500 inline-flex items-center justify-center px-6"
>
{{ $t("tutorial.reticulum_manual") }}
</a>
@@ -1191,7 +1425,7 @@
</div>
<div
class="flex flex-col items-center gap-6 p-8 rounded-[2rem] bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
class="flex flex-col items-center gap-6 p-8 rounded-4xl bg-gray-50 dark:bg-zinc-900 text-center border border-gray-100 dark:border-zinc-800"
>
<v-icon icon="mdi-file-document-edit-outline" color="orange" size="64"></v-icon>
<div>
@@ -1203,7 +1437,7 @@
</p>
<button
type="button"
class="w-full h-12 rounded-xl bg-orange-600 hover:bg-orange-500 text-white font-semibold shadow-sm transition-all"
class="w-full h-12 rounded-xl bg-orange-600 hover:bg-orange-500 text-white font-semibold shadow-xs transition-all"
@click="gotoRoute('micron-editor')"
>
{{ $t("tutorial.open_micron_editor") }}
@@ -1312,7 +1546,7 @@
<button
v-if="currentStep > 1 && currentStep < totalSteps"
type="button"
class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all hover:bg-gray-50 dark:hover:bg-zinc-700 hover:border-blue-400 dark:hover:border-blue-500"
@click="previousStep"
>
{{ $t("tutorial.back") }}
@@ -1323,7 +1557,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-sm transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
class="px-8 h-12 rounded-xl border border-gray-300 dark:border-zinc-700 bg-white dark:bg-zinc-800 text-gray-700 dark:text-zinc-300 font-semibold text-sm shadow-xs transition-all opacity-50 hover:opacity-100 hover:bg-gray-50 dark:hover:bg-zinc-700"
@click="skipTutorial"
>
{{ $t("tutorial.skip_setup") }}
@@ -1332,7 +1566,7 @@
<button
v-if="currentStep < totalSteps"
type="button"
class="px-12 h-14 text-lg rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-sm transition-all"
class="px-12 h-14 text-lg rounded-xl bg-blue-600 hover:bg-blue-500 text-white font-semibold shadow-xs transition-all"
@click="nextStep"
>
{{ $t("tutorial.continue") }}
@@ -1341,7 +1575,7 @@
<button
v-else
type="button"
class="px-12 h-14 text-lg rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold shadow-sm transition-all"
class="px-12 h-14 text-lg rounded-xl bg-emerald-600 hover:bg-emerald-500 text-white font-semibold shadow-xs transition-all"
@click="finishTutorial"
>
{{ $t("tutorial.finish_setup") }}
@@ -1361,12 +1595,14 @@ import GlobalState from "../js/GlobalState";
import { bundledReticulumDocsUrl } from "../js/reticulumDocsEntryUrl.js";
import LanguageSelector from "./LanguageSelector.vue";
import MaterialDesignIcon from "./MaterialDesignIcon.vue";
import Toggle from "./forms/Toggle.vue";
export default {
name: "TutorialModal",
components: {
LanguageSelector,
MaterialDesignIcon,
Toggle,
},
data() {
return {
@@ -1391,6 +1627,11 @@ export default {
discoveryInterval: null,
markingSeen: false,
windowWidth: typeof window !== "undefined" ? window.innerWidth : 1024,
defaultBootstrapOnly: true,
refreshingCommunityPresets: false,
bootstrapListSearch: "",
bootstrapDiscoveredSectionOpen: true,
bootstrapCommunitySectionOpen: true,
};
},
computed: {
@@ -1418,6 +1659,37 @@ export default {
hasAnyBootstrapsToShow() {
return this.communityInterfaces.length > 0 || this.sortedDiscoveredInterfaces.length > 0;
},
filteredDiscoveredForBootstrap() {
const list = this.sortedDiscoveredInterfaces;
const q = (this.bootstrapListSearch || "").trim().toLowerCase();
if (!q) {
return list;
}
return list.filter((iface) => {
const parts = [
iface.name,
iface.type,
iface.reachable_on,
String(iface.port ?? ""),
iface.status,
iface.discovery_hash,
].filter(Boolean);
return parts.join(" ").toLowerCase().includes(q);
});
},
filteredCommunityForBootstrap() {
const list = this.communityInterfaces;
const q = (this.bootstrapListSearch || "").trim().toLowerCase();
if (!q) {
return list;
}
return list.filter((iface) => {
const parts = [iface.name, iface.target_host, String(iface.target_port ?? ""), iface.type].filter(
Boolean
);
return parts.join(" ").toLowerCase().includes(q);
});
},
selectedBootstrapCount() {
return this.selectedBootstrapKeys.length;
},
@@ -1439,6 +1711,7 @@ export default {
};
window.addEventListener("resize", this.onWindowResize, { passive: true });
if (this.isPage) {
this.loadDiscoveryBootstrapDefaults();
this.loadCommunityInterfaces();
this.loadDiscoveredInterfaces();
this.discoveryInterval = setInterval(() => {
@@ -1476,6 +1749,10 @@ export default {
this.connectionMode = null;
this.selectedBootstrapKeys = [];
this.addedBootstrapKeys = [];
this.bootstrapListSearch = "";
this.bootstrapDiscoveredSectionOpen = true;
this.bootstrapCommunitySectionOpen = true;
await this.loadDiscoveryBootstrapDefaults();
await this.loadCommunityInterfaces();
await this.loadDiscoveredInterfaces();
@@ -1497,6 +1774,21 @@ export default {
this.loadingInterfaces = false;
}
},
async refreshCommunityPresets() {
if (this.refreshingCommunityPresets) return;
this.refreshingCommunityPresets = true;
try {
const r = await window.api.post("/api/v1/community-interfaces/refresh", {});
const n = r.data?.count ?? 0;
ToastUtils.success(this.$t("interfaces.community_presets_refreshed", { count: n }));
await this.loadCommunityInterfaces();
} catch (e) {
ToastUtils.error(e.response?.data?.message || this.$t("interfaces.community_presets_refresh_failed"));
console.error(e);
} finally {
this.refreshingCommunityPresets = false;
}
},
async loadDiscoveredInterfaces() {
this.loadingDiscovered = true;
try {
@@ -1532,11 +1824,16 @@ export default {
const payload = {
discover_interfaces: true,
autoconnect_discovered_interfaces: 3,
default_bootstrap_only: true,
};
await window.api.patch(`/api/v1/reticulum/discovery`, payload);
this.defaultBootstrapOnly = true;
ToastUtils.success(this.$t("tutorial.discovery_enabled"));
this.connectionMode = "discovery";
this.currentStep = 3;
this.bootstrapListSearch = "";
this.bootstrapDiscoveredSectionOpen = true;
this.bootstrapCommunitySectionOpen = true;
} catch (e) {
console.error("Failed to enable discovery:", e);
ToastUtils.error(this.$t("tutorial.failed_enable_discovery"));
@@ -1589,6 +1886,7 @@ export default {
name: iface.name || `Discovered ${iface.discovery_hash || ""}`.trim(),
type: iface.type === "BackboneInterface" ? "TCPClientInterface" : iface.type,
enabled: true,
bootstrap_only: this.defaultBootstrapOnly === true,
};
if (iface.reachable_on) {
payload.target_host = iface.reachable_on;
@@ -1605,8 +1903,37 @@ export default {
target_host: iface.target_host,
target_port: iface.target_port,
enabled: true,
bootstrap_only: this.defaultBootstrapOnly === true,
};
},
parseDiscoveryBool(value, defaultValue = true) {
if (value === undefined || value === null || value === "") {
return defaultValue;
}
if (typeof value === "string") {
return ["true", "yes", "1", "y", "on"].includes(value.toLowerCase());
}
return Boolean(value);
},
async loadDiscoveryBootstrapDefaults() {
try {
const response = await window.api.get("/api/v1/reticulum/discovery");
const d = response.data?.discovery ?? {};
this.defaultBootstrapOnly = this.parseDiscoveryBool(d.default_bootstrap_only, true);
} catch (e) {
console.error(e);
this.defaultBootstrapOnly = true;
}
},
async persistDefaultBootstrapOnly(value) {
try {
await window.api.patch("/api/v1/reticulum/discovery", {
default_bootstrap_only: value === true,
});
} catch (e) {
console.error("Failed to save default_bootstrap_only:", e);
}
},
async confirmBootstraps() {
if (this.addingBootstraps) return;
if (this.selectedBootstrapKeys.length === 0) {
@@ -1761,6 +2088,11 @@ export default {
return;
}
this.currentStep++;
if (this.currentStep === 3) {
this.bootstrapListSearch = "";
this.bootstrapDiscoveredSectionOpen = true;
this.bootstrapCommunitySectionOpen = true;
}
},
previousStep() {
if (this.currentStep <= 1) return;
@@ -284,18 +284,18 @@
</div>
<div class="grid gap-8 sm:grid-cols-2 lg:grid-cols-3 text-sm min-w-0">
<div>
<div class="glass-label !text-[10px] mb-2 opacity-50">
<div class="glass-label text-[10px]! mb-2 opacity-50">
{{ $t("about.reticulum_config") }}
</div>
<div
class="monospace-field !bg-zinc-50 dark:!bg-zinc-950 break-all text-[11px] !p-3 rounded-xl border border-zinc-100 dark:border-zinc-800"
class="monospace-field bg-zinc-50! dark:bg-zinc-950! break-all text-[11px] p-3! rounded-xl border border-zinc-100 dark:border-zinc-800"
>
{{ appInfo.reticulum_config_path || $t("about.path_unknown") }}
</div>
<button
v-if="isElectron"
type="button"
class="secondary-chip mt-3 !px-3 !py-1 !text-[10px]"
class="secondary-chip mt-3 px-3! py-1! text-[10px]!"
@click="showReticulumConfigFile"
>
<v-icon icon="mdi-folder-open" start size="14"></v-icon>
@@ -303,18 +303,18 @@
</button>
</div>
<div>
<div class="glass-label !text-[10px] mb-2 opacity-50">
<div class="glass-label text-[10px]! mb-2 opacity-50">
{{ $t("about.database_path") }}
</div>
<div
class="monospace-field !bg-zinc-50 dark:!bg-zinc-950 break-all text-[11px] !p-3 rounded-xl border border-zinc-100 dark:border-zinc-800"
class="monospace-field bg-zinc-50! dark:bg-zinc-950! break-all text-[11px] p-3! rounded-xl border border-zinc-100 dark:border-zinc-800"
>
{{ appInfo.database_path || $t("about.path_unknown") }}
</div>
<button
v-if="isElectron"
type="button"
class="secondary-chip mt-3 !px-3 !py-1 !text-[10px]"
class="secondary-chip mt-3 px-3! py-1! text-[10px]!"
@click="showDatabaseFile"
>
<v-icon icon="mdi-database-search" start size="14"></v-icon>
@@ -322,7 +322,7 @@
</button>
</div>
<div
class="flex flex-col justify-center space-y-3 py-2 sm:py-3 border-t sm:border border-gray-200/60 dark:border-zinc-800/80 sm:rounded-xl sm:p-4 sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
class="flex flex-col justify-center space-y-3 py-2 sm:py-3 border-t sm:border border-gray-200/60 dark:border-zinc-800/80 sm:rounded-xl sm:p-4 sm:bg-black/2 dark:sm:bg-white/2"
>
<div
v-if="config"
@@ -411,7 +411,7 @@
<div class="flex flex-col space-y-8 min-w-0">
<div class="flex items-center gap-5">
<div
class="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 shadow-sm"
class="w-12 h-12 rounded-2xl bg-blue-500/10 flex items-center justify-center border border-blue-500/20 shadow-xs"
>
<img
src="../../public/favicons/favicon-512x512.png"
@@ -431,10 +431,10 @@
class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
>
<div
class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-blue-500 to-emerald-500"
class="absolute left-[-2px] top-0 bottom-0 w-[2px] bg-linear-to-b from-blue-500 to-emerald-500"
></div>
<div
class="w-12 h-12 rounded-2xl bg-emerald-500/10 flex items-center justify-center border border-emerald-500/20 text-emerald-600 font-black text-[10px] tracking-tighter shadow-sm"
class="w-12 h-12 rounded-2xl bg-emerald-500/10 flex items-center justify-center border border-emerald-500/20 text-emerald-600 font-black text-[10px] tracking-tighter shadow-xs"
>
LXMFy
</div>
@@ -451,10 +451,10 @@
class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
>
<div
class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-emerald-500 to-purple-500"
class="absolute left-[-2px] top-0 bottom-0 w-[2px] bg-linear-to-b from-emerald-500 to-purple-500"
></div>
<div
class="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center border border-purple-500/20 text-purple-600 font-black text-[10px] tracking-tighter shadow-sm"
class="w-12 h-12 rounded-2xl bg-purple-500/10 flex items-center justify-center border border-purple-500/20 text-purple-600 font-black text-[10px] tracking-tighter shadow-xs"
>
LXMF
</div>
@@ -471,10 +471,10 @@
class="flex items-center gap-5 pl-5 border-l-2 border-zinc-100 dark:border-zinc-800 ml-6 relative"
>
<div
class="absolute -left-[2px] top-0 bottom-0 w-[2px] bg-gradient-to-b from-purple-500 to-indigo-500"
class="absolute left-[-2px] top-0 bottom-0 w-[2px] bg-linear-to-b from-purple-500 to-indigo-500"
></div>
<div
class="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 text-indigo-600 font-black text-[10px] tracking-tighter shadow-sm"
class="w-12 h-12 rounded-2xl bg-indigo-500/10 flex items-center justify-center border border-indigo-500/20 text-indigo-600 font-black text-[10px] tracking-tighter shadow-xs"
>
RNS
</div>
@@ -492,7 +492,7 @@
? 'bg-blue-500/10 text-blue-500 border-blue-500/20'
: 'bg-emerald-500/10 text-emerald-500 border-emerald-500/20',
]"
class="text-[8px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded border max-w-full break-words"
class="text-[8px] font-black uppercase tracking-wider px-1.5 py-0.5 rounded-sm border max-w-full wrap-break-word"
>
{{
appInfo.is_connected_to_shared_instance
@@ -511,7 +511,7 @@
<div class="space-y-8 min-w-0">
<div
class="py-4 sm:p-5 border-t border-gray-200/60 dark:border-zinc-800/80 sm:border sm:rounded-2xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02] min-w-0"
class="py-4 sm:p-5 border-t border-gray-200/60 dark:border-zinc-800/80 sm:border sm:rounded-2xl sm:bg-black/2 dark:sm:bg-white/2 min-w-0"
>
<div
class="text-[10px] font-black text-gray-400 dark:text-zinc-600 uppercase tracking-[0.2em] mb-4"
@@ -601,7 +601,7 @@
<div class="flex flex-wrap gap-2 w-full md:w-auto">
<button
type="button"
class="secondary-chip !px-4 !py-1.5 !text-xs min-h-[44px] sm:min-h-0"
class="secondary-chip px-4! py-1.5! text-xs! min-h-[44px] sm:min-h-0"
:disabled="databaseActionInProgress || healthLoading"
@click="getDatabaseHealth(true)"
>
@@ -611,7 +611,7 @@
</button>
<button
type="button"
class="primary-chip !px-4 !py-1.5 !text-xs"
class="primary-chip px-4! py-1.5! text-xs!"
:disabled="databaseActionInProgress"
@click="vacuumDatabase"
>
@@ -619,7 +619,7 @@
</button>
<button
type="button"
class="danger-chip !px-4 !py-1.5 !text-xs"
class="danger-chip px-4! py-1.5! text-xs!"
:disabled="databaseActionInProgress"
@click="runRecovery"
>
@@ -699,7 +699,7 @@
</div>
<button
type="button"
class="primary-chip !px-5 !py-2.5"
class="primary-chip px-5! py-2.5!"
:disabled="backupInProgress"
@click="backupDatabase"
>
@@ -728,11 +728,11 @@
v-model="snapshotName"
type="text"
:placeholder="$t('about.snapshot_placeholder')"
class="bg-zinc-50 dark:bg-zinc-900 px-4 py-2 rounded-xl text-sm border border-zinc-100 dark:border-zinc-800 focus:outline-none focus:ring-2 focus:ring-blue-500/20 flex-1 md:min-w-[200px]"
class="bg-zinc-50 dark:bg-zinc-900 px-4 py-2 rounded-xl text-sm border border-zinc-100 dark:border-zinc-800 focus:outline-hidden focus:ring-2 focus:ring-blue-500/20 flex-1 md:min-w-[200px]"
/>
<button
type="button"
class="primary-chip !px-6"
class="primary-chip px-6!"
:disabled="snapshotInProgress"
@click="createSnapshot"
>
@@ -747,7 +747,7 @@
<div
v-for="snapshot in snapshots"
:key="snapshot.path"
class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/[0.02] dark:sm:bg-white/[0.02] transition-colors"
class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/2 dark:sm:bg-white/2 transition-colors"
>
<div class="flex flex-col min-w-0">
<span
@@ -764,7 +764,7 @@
>
<button
type="button"
class="primary-chip !px-3 !py-1 !text-[10px]"
class="primary-chip px-3! py-1! text-[10px]!"
@click="downloadSnapshot(snapshot.name)"
>
<v-icon icon="mdi-download" size="12" start></v-icon>
@@ -772,14 +772,14 @@
</button>
<button
type="button"
class="secondary-chip !px-3 !py-1 !text-[10px]"
class="secondary-chip px-3! py-1! text-[10px]!"
@click="restoreFromSnapshot(snapshot.path)"
>
{{ $t("about.snapshot_restore") }}
</button>
<button
type="button"
class="danger-chip !px-3 !py-1 !text-[10px]"
class="danger-chip px-3! py-1! text-[10px]!"
@click="deleteSnapshot(snapshot.name)"
>
<v-icon icon="mdi-delete" size="12"></v-icon>
@@ -803,14 +803,14 @@
</div>
<div class="flex gap-2">
<button
class="secondary-chip !p-1 disabled:opacity-30"
class="secondary-chip p-1! disabled:opacity-30"
:disabled="snapshotsOffset === 0"
@click="prevSnapshots"
>
<v-icon icon="mdi-chevron-left"></v-icon>
</button>
<button
class="secondary-chip !p-1 disabled:opacity-30"
class="secondary-chip p-1! disabled:opacity-30"
:disabled="snapshotsOffset + snapshotsLimit >= snapshotsTotal"
@click="nextSnapshots"
>
@@ -842,7 +842,7 @@
<div
v-for="backup in autoBackups"
:key="backup.path"
class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/[0.02] dark:sm:bg-white/[0.02] transition-colors"
class="flex items-center justify-between gap-2 py-3 sm:p-4 border-b border-gray-200/60 dark:border-zinc-800/80 last:border-0 sm:border sm:rounded-lg sm:bg-black/2 dark:sm:bg-white/2 transition-colors"
>
<div class="flex flex-col min-w-0">
<span
@@ -859,7 +859,7 @@
>
<button
type="button"
class="primary-chip !px-3 !py-1 !text-[10px]"
class="primary-chip px-3! py-1! text-[10px]!"
@click="downloadBackupFile(backup.name)"
>
<v-icon icon="mdi-download" size="12" start></v-icon>
@@ -867,14 +867,14 @@
</button>
<button
type="button"
class="secondary-chip !px-3 !py-1 !text-[10px]"
class="secondary-chip px-3! py-1! text-[10px]!"
@click="restoreFromSnapshot(backup.path)"
>
{{ $t("about.snapshot_restore") }}
</button>
<button
type="button"
class="danger-chip !px-3 !py-1 !text-[10px]"
class="danger-chip px-3! py-1! text-[10px]!"
@click="deleteBackup(backup.name)"
>
<v-icon icon="mdi-delete" size="12"></v-icon>
@@ -898,14 +898,14 @@
</div>
<div class="flex gap-2">
<button
class="secondary-chip !p-1 disabled:opacity-30"
class="secondary-chip p-1! disabled:opacity-30"
:disabled="autoBackupsOffset === 0"
@click="prevBackups"
>
<v-icon icon="mdi-chevron-left"></v-icon>
</button>
<button
class="secondary-chip !p-1 disabled:opacity-30"
class="secondary-chip p-1! disabled:opacity-30"
:disabled="autoBackupsOffset + autoBackupsLimit >= autoBackupsTotal"
@click="nextBackups"
>
@@ -1254,9 +1254,13 @@ export default {
if (response.data.database?.health) {
this.databaseHealth = response.data.database.health;
}
this.databaseActionMessage = response.data.message || "Database vacuum completed";
const msg = response.data.message || this.$t("about.vacuum_complete");
this.databaseActionMessage = msg;
ToastUtils.success(this.$t("about.vacuum_complete"));
} catch (e) {
this.databaseActionError = "Vacuum failed";
this.databaseActionError = this.$t("about.vacuum_failed");
const detail = e?.response?.data?.message;
ToastUtils.error(detail || this.$t("about.vacuum_failed"));
console.log(e);
} finally {
this.databaseActionInProgress = false;
@@ -1326,6 +1330,9 @@ export default {
if (this.databaseActionInProgress) {
return;
}
if (!(await DialogUtils.confirm(this.$t("about.recovery_confirm")))) {
return;
}
this.databaseActionInProgress = true;
this.databaseActionMessage = "";
this.databaseActionError = "";
@@ -1335,9 +1342,13 @@ export default {
this.databaseHealth = response.data.database.health;
}
this.databaseRecoveryActions = response.data.database?.actions || [];
this.databaseActionMessage = response.data.message || "Database recovery completed";
const msg = response.data.message || this.$t("about.recovery_complete");
this.databaseActionMessage = msg;
ToastUtils.success(this.$t("about.recovery_complete"));
} catch (e) {
this.databaseActionError = this.$t("about.recovery_failed");
const detail = e?.response?.data?.message;
ToastUtils.error(detail || this.$t("about.recovery_failed"));
console.log(e);
} finally {
this.databaseActionInProgress = false;
@@ -1471,6 +1482,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.about-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8 last:border-0;
}
@@ -28,7 +28,7 @@
v-model="searchQuery"
type="text"
placeholder="Search nodes or content..."
class="w-full pl-9 pr-3 py-1.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-none focus:ring-1 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all"
class="w-full pl-9 pr-3 py-1.5 bg-white dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs text-gray-900 dark:text-white placeholder:text-gray-400 dark:placeholder:text-zinc-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500/50 focus:border-blue-500/50 transition-all"
@input="$emit('update:search-query', searchQuery)"
/>
<button
@@ -34,7 +34,7 @@
{{ selectedNode.node_name }}
</h2>
<div
class="text-[10px] font-bold px-1.5 py-0.5 bg-gray-200 dark:bg-zinc-700 text-gray-600 dark:text-gray-400 rounded"
class="text-[10px] font-bold px-1.5 py-0.5 bg-gray-200 dark:bg-zinc-700 text-gray-600 dark:text-gray-400 rounded-sm"
>
{{ selectedNode.archives.length }}
</div>
@@ -46,7 +46,7 @@
<label class="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
class="rounded border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
class="rounded-sm border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
:checked="isAllSelected"
@change="toggleSelectAll"
/>
@@ -95,7 +95,7 @@
<input
v-model="selectedArchives"
type="checkbox"
class="rounded border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
class="rounded-sm border-gray-300 dark:border-zinc-700 text-blue-500 focus:ring-blue-500/20 bg-white dark:bg-zinc-800"
:value="archive.id"
/>
</div>
@@ -155,7 +155,7 @@
<div class="flex items-center gap-1">
<button
class="hidden rounded p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
class="hidden rounded-sm p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
:class="{
'text-blue-600 dark:text-blue-400': !isSidebar1Hidden,
'text-gray-400 dark:text-zinc-500': isSidebar1Hidden,
@@ -166,7 +166,7 @@
<MaterialDesignIcon icon-name="page-layout-sidebar-left" class="size-4" />
</button>
<button
class="hidden rounded p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
class="hidden rounded-sm p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
:class="{
'text-blue-600 dark:text-blue-400': !isSidebar2Hidden,
'text-gray-400 dark:text-zinc-500': isSidebar2Hidden,
@@ -178,7 +178,7 @@
</button>
<div class="mx-1 hidden h-6 w-px bg-gray-200 dark:bg-zinc-800 sm:block"></div>
<button
class="flex items-center gap-2 rounded p-2 text-gray-700 transition-colors hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
class="flex items-center gap-2 rounded-sm p-2 text-gray-700 transition-colors hover:bg-gray-100 dark:text-zinc-300 dark:hover:bg-zinc-800"
:title="$t('archives.export_mu')"
@click="exportArchiveAsMu(viewingArchive)"
>
@@ -189,7 +189,7 @@
</button>
<div class="mx-1 hidden h-6 w-px bg-gray-200 dark:bg-zinc-800 xs:block"></div>
<button
class="flex items-center gap-2 rounded p-2 text-blue-600 transition-colors hover:bg-gray-100 dark:text-blue-400 dark:hover:bg-zinc-800"
class="flex items-center gap-2 rounded-sm p-2 text-blue-600 transition-colors hover:bg-gray-100 dark:text-blue-400 dark:hover:bg-zinc-800"
@click="openInNomadnet(viewingArchive)"
>
<MaterialDesignIcon icon-name="open-in-new" class="size-4" />
@@ -197,7 +197,7 @@
</button>
<div class="mx-1 hidden h-6 w-px bg-gray-200 dark:bg-zinc-800 xs:block"></div>
<button
class="hidden rounded p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
class="hidden rounded-sm p-2 transition-colors hover:bg-gray-100 dark:hover:bg-zinc-800 sm:block"
title="Close"
@click="viewingArchive = null"
>
@@ -284,13 +284,13 @@ export default {
archiveViewerClasses() {
const a = this.viewingArchive;
if (!a?.page_path) {
return ["break-words", "whitespace-pre-wrap", "text-gray-100"];
return ["wrap-break-word", "whitespace-pre-wrap", "text-gray-100"];
}
const pl = (a.page_path || "").split("`")[0].toLowerCase();
const isRich = pl.endsWith(".mu") || pl.endsWith(".md") || pl.endsWith(".html");
const isHtml = pl.endsWith(".html");
const isMd = pl.endsWith(".md");
const classes = ["break-words"];
const classes = ["wrap-break-word"];
if (isRich) {
classes.push("nomad-page-rich");
} else {
@@ -35,7 +35,7 @@
type="password"
required
minlength="8"
class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Enter password"
autocomplete="current-password"
/>
@@ -57,7 +57,7 @@
type="password"
required
minlength="8"
class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
class="w-full px-4 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-white dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Confirm password"
autocomplete="new-password"
/>
@@ -3,7 +3,7 @@
<template>
<div class="flex flex-col flex-1 h-full overflow-hidden bg-slate-50 dark:bg-zinc-950">
<div
class="flex items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-sm"
class="flex items-center px-4 py-4 bg-white dark:bg-zinc-900 border-b border-gray-200 dark:border-zinc-800 shadow-xs"
>
<div class="flex items-center gap-3">
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg">
@@ -23,7 +23,7 @@
<input
v-model="searchQuery"
type="text"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-gray-100 placeholder-gray-500 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all"
:placeholder="$t('banishment.search_placeholder')"
@input="onSearchInput"
/>
@@ -71,7 +71,7 @@
<div class="flex items-start justify-between mb-4">
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-2">
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg flex-shrink-0">
<div class="p-2 bg-red-100 dark:bg-red-900/30 rounded-lg shrink-0">
<MaterialDesignIcon
icon-name="account-off"
class="size-5 text-red-600 dark:text-red-400"
@@ -80,26 +80,26 @@
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 mb-1">
<h4
class="text-base font-semibold text-gray-900 dark:text-white break-words"
class="text-base font-semibold text-gray-900 dark:text-white wrap-break-word"
:title="item.display_name"
>
{{ item.display_name || $t("call.unknown") }}
</h4>
<span
v-if="item.is_node"
class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded"
class="px-2 py-0.5 text-xs font-medium bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300 rounded-sm"
>
{{ $t("banishment.node") }}
</span>
<span
v-else
class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded"
class="px-2 py-0.5 text-xs font-medium bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-300 rounded-sm"
>
{{ $t("banishment.user") }}
</span>
<span
v-if="item.is_rns_blackholed"
class="px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded border border-zinc-200 dark:border-zinc-700"
class="px-2 py-0.5 text-xs font-medium bg-zinc-100 dark:bg-zinc-800 text-zinc-700 dark:text-zinc-300 rounded-sm border border-zinc-200 dark:border-zinc-700"
title="Blackholed at Reticulum transport layer"
>
RNS Blackhole
@@ -3,7 +3,7 @@
<template>
<div
v-if="activeCall || initiationStatus || isEnded || wasDeclined"
class="fixed bottom-4 right-4 z-[100] w-80 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
class="fixed bottom-4 right-4 z-100 w-80 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
:class="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded || wasDeclined }"
>
<!-- Header -->
@@ -221,7 +221,7 @@
class="p-2.5 rounded-full bg-red-600 text-white hover:bg-red-700 shadow-lg shadow-red-600/30 transition-all duration-200"
@click="hangupCall(null)"
>
<MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
<MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-135" />
</button>
<!-- Send to Voicemail (if incoming) -->
@@ -290,7 +290,7 @@
<div class="flex items-center space-x-1">
<button
type="button"
class="p-1.5 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded transition-colors"
class="p-1.5 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-sm transition-colors"
@click="toggleMicrophone"
>
<MaterialDesignIcon
@@ -301,10 +301,10 @@
</button>
<button
type="button"
class="p-1.5 hover:bg-red-100 dark:hover:bg-red-900/30 rounded transition-colors"
class="p-1.5 hover:bg-red-100 dark:hover:bg-red-900/30 rounded-sm transition-colors"
@click="hangupCall"
>
<MaterialDesignIcon icon-name="phone-hangup" class="size-4 text-red-500 rotate-[135deg]" />
<MaterialDesignIcon icon-name="phone-hangup" class="size-4 text-red-500 rotate-135" />
</button>
</div>
</div>
@@ -86,7 +86,7 @@
class="flex-1 flex flex-col items-center justify-center py-12 px-4"
>
<div
class="w-full max-w-md border-b border-gray-200 dark:border-zinc-800 !p-8 flex flex-col items-center text-center relative overflow-hidden"
class="w-full max-w-md border-b border-gray-200 dark:border-zinc-800 p-8! flex flex-col items-center text-center relative overflow-hidden"
>
<!-- Status pulse background -->
<div
@@ -279,7 +279,7 @@
<div class="flex flex-col gap-4">
<select
v-model="selectedAudioProfileId"
class="input-field !rounded-xl !py-2 shadow-sm"
class="input-field rounded-xl! py-2! shadow-xs"
@change="switchAudioProfile(selectedAudioProfileId)"
>
<option
@@ -375,7 +375,7 @@
class="w-full flex items-center justify-center gap-2 rounded-2xl bg-red-600 py-4 text-sm font-bold text-white shadow-xl shadow-red-600/20 hover:bg-red-500 transition-all duration-200"
@click="hangupCall"
>
<MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
<MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-135" />
<span>{{
activeCall && activeCall.is_incoming && activeCall.status === 4
? $t("call.decline")
@@ -549,7 +549,7 @@
<select
v-if="config"
v-model="config.telephone_audio_profile_id"
class="input-field min-w-0 !rounded-lg !border-gray-200 !py-1 !px-2 !text-xs dark:!border-zinc-800 lg:min-w-[120px]"
class="input-field min-w-0 rounded-lg! border-gray-200! py-1! px-2! text-xs! dark:border-zinc-800! lg:min-w-[120px]"
@change="
updateConfig({
telephone_audio_profile_id: config.telephone_audio_profile_id,
@@ -579,7 +579,7 @@
</div>
<select
v-model="selectedAudioInputId"
class="input-field !py-1 !px-2 !text-[10px] !rounded-lg !border-gray-200 dark:!border-zinc-800 min-w-[120px]"
class="input-field py-1! px-2! text-[10px]! rounded-lg! border-gray-200! dark:border-zinc-800! min-w-[120px]"
@change="
stopWebAudio();
startWebAudio();
@@ -602,7 +602,7 @@
</div>
<select
v-model="selectedAudioOutputId"
class="input-field !py-1 !px-2 !text-[10px] !rounded-lg !border-gray-200 dark:!border-zinc-800 min-w-[120px]"
class="input-field py-1! px-2! text-[10px]! rounded-lg! border-gray-200! dark:border-zinc-800! min-w-[120px]"
@change="
stopWebAudio();
startWebAudio();
@@ -636,7 +636,7 @@
v-if="callHistory.length > 0 && !activeCall && !isCallEnded"
class="space-y-4 max-w-3xl mx-auto w-full"
>
<div class="w-full border-b border-gray-200 dark:border-zinc-800 !p-0 overflow-hidden">
<div class="w-full border-b border-gray-200 dark:border-zinc-800 p-0! overflow-hidden">
<div
class="px-5 py-4 border-b border-gray-100 dark:border-zinc-800 flex flex-col gap-4 bg-transparent"
>
@@ -667,7 +667,7 @@
v-model="callHistorySearch"
type="text"
:placeholder="$t('call.search_history')"
class="input-field !py-2 !pl-10"
class="input-field py-2! pl-10!"
@input="onCallHistorySearchInput"
/>
<MaterialDesignIcon
@@ -699,7 +699,7 @@
icon-class="size-10"
/>
<div
class="absolute -bottom-1 -right-1 bg-white dark:bg-zinc-900 rounded-full p-0.5 shadow-sm border border-gray-100 dark:border-zinc-800 shrink-0 flex items-center justify-center size-5"
class="absolute -bottom-1 -right-1 bg-white dark:bg-zinc-900 rounded-full p-0.5 shadow-xs border border-gray-100 dark:border-zinc-800 shrink-0 flex items-center justify-center size-5"
>
<MaterialDesignIcon
:icon-name="entry.is_incoming ? 'phone-incoming' : 'phone-outgoing'"
@@ -762,7 +762,7 @@
</div>
<div
class="flex items-center gap-1.5 opacity-0 group-hover:opacity-100 transition-opacity shrink-0 ml-4"
class="flex items-center gap-1.5 opacity-100 transition-opacity shrink-0 ml-4 lg:opacity-0 lg:group-hover:opacity-100"
>
<button
v-if="!entry.is_contact"
@@ -837,7 +837,7 @@
v-model="discoverySearch"
type="text"
:placeholder="`Search phonebook (${totalDiscoveryCount})...`"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onDiscoverySearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -953,7 +953,7 @@
v-model="voicemailSearch"
type="text"
placeholder="Search voicemails..."
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onVoicemailSearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -1012,7 +1012,7 @@
</div>
<button
:disabled="!voicemailStatus.has_espeak"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden disabled:opacity-50 disabled:cursor-not-allowed"
:class="config.voicemail_enabled ? 'bg-blue-600' : 'bg-gray-200 dark:bg-zinc-700'"
@click="
config.voicemail_enabled = !config.voicemail_enabled;
@@ -1020,7 +1020,7 @@
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
:class="config.voicemail_enabled ? 'translate-x-5' : 'translate-x-0'"
></span>
</button>
@@ -1035,7 +1035,7 @@
<textarea
v-model="config.voicemail_greeting"
rows="3"
class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm sm:leading-6 dark:bg-zinc-900"
placeholder="Enter greeting text..."
></textarea>
@@ -1051,7 +1051,7 @@
type="number"
min="80"
max="450"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="updateConfig({ voicemail_tts_speed: config.voicemail_tts_speed })"
/>
</div>
@@ -1065,7 +1065,7 @@
type="number"
min="0"
max="99"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="updateConfig({ voicemail_tts_pitch: config.voicemail_tts_pitch })"
/>
</div>
@@ -1079,7 +1079,7 @@
type="number"
min="0"
max="100"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="
updateConfig({ voicemail_tts_word_gap: config.voicemail_tts_word_gap })
"
@@ -1093,7 +1093,7 @@
<input
v-model="config.voicemail_tts_voice"
type="text"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-1 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 text-xs dark:bg-zinc-900"
@change="updateConfig({ voicemail_tts_voice: config.voicemail_tts_voice })"
/>
</div>
@@ -1202,7 +1202,7 @@
type="number"
min="1"
max="120"
class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@change="
updateConfig({
voicemail_auto_answer_delay_seconds:
@@ -1221,7 +1221,7 @@
type="number"
min="5"
max="600"
class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-1.5 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@change="
updateConfig({
voicemail_max_recording_seconds: config.voicemail_max_recording_seconds,
@@ -1373,7 +1373,7 @@
v-model="contactsSearch"
type="text"
placeholder="Search contacts..."
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onContactsSearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -1382,7 +1382,7 @@
</div>
<button
type="button"
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-colors flex items-center gap-2"
class="rounded-lg bg-blue-600 px-3 py-2 text-sm font-semibold text-white shadow-xs hover:bg-blue-500 transition-colors flex items-center gap-2"
@click="openAddContactModal"
>
<MaterialDesignIcon icon-name="plus" class="size-5" />
@@ -1428,7 +1428,7 @@
<div class="flex items-center gap-2">
<span
v-if="contact.preferred_ringtone_id"
class="text-[9px] px-1.5 py-0.5 rounded bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 border border-amber-100 dark:border-amber-800/50 flex items-center gap-1"
class="text-[9px] px-1.5 py-0.5 rounded-sm bg-amber-50 dark:bg-amber-900/30 text-amber-600 dark:text-amber-400 border border-amber-100 dark:border-amber-800/50 flex items-center gap-1"
title="Custom Ringtone Set"
>
<MaterialDesignIcon icon-name="music" class="size-2.5" />
@@ -1528,7 +1528,7 @@
{{ $t("call.enable_custom_ringtone") }}
</div>
<button
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden"
:class="
config.custom_ringtone_enabled
? 'bg-blue-600'
@@ -1542,7 +1542,7 @@
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
:class="
config.custom_ringtone_enabled
? 'translate-x-5'
@@ -1588,7 +1588,7 @@
Tone Generator
</div>
<button
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none"
class="relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-hidden"
:class="
config.telephone_tone_generator_enabled
? 'bg-blue-600'
@@ -1604,7 +1604,7 @@
"
>
<span
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"
class="pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow-sm ring-0 transition duration-200 ease-in-out"
:class="
config.telephone_tone_generator_enabled
? 'translate-x-5'
@@ -1660,7 +1660,7 @@
</div>
<select
v-model="config.ringtone_preferred_id"
class="input-field !py-1.5 !px-3 !text-sm !rounded-xl !border-gray-200 dark:!border-zinc-800 min-w-[200px]"
class="input-field py-1.5! px-3! text-sm! rounded-xl! border-gray-200! dark:border-zinc-800! min-w-[200px]"
@change="
updateConfig({ ringtone_preferred_id: config.ringtone_preferred_id })
"
@@ -1716,7 +1716,7 @@
>
<input
v-model="editingRingtoneName"
class="text-sm bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded px-2 py-1 flex-1 min-w-0"
class="text-sm bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-sm px-2 py-1 flex-1 min-w-0"
@keyup.enter="saveRingtoneName"
@blur="saveRingtoneName"
/>
@@ -1730,7 +1730,7 @@
</span>
<span
v-if="ringtone.is_primary"
class="shrink-0 text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 rounded"
class="shrink-0 text-[10px] uppercase font-bold text-blue-600 dark:text-blue-400 bg-blue-100 dark:bg-blue-900/40 px-1.5 py-0.5 rounded-sm"
>
Primary
</span>
@@ -1826,7 +1826,7 @@
v-model="recordingSearch"
type="text"
placeholder="Search recordings..."
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-2 pl-10 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@input="onRecordingSearchInput"
/>
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
@@ -1962,7 +1962,7 @@
<!-- Contact Modal -->
<div
v-if="isContactModalOpen"
class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity"
class="fixed inset-0 z-150 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs transition-opacity"
@click.self="isContactModalOpen = false"
>
<div
@@ -2106,7 +2106,7 @@
</button>
<button
type="button"
class="flex-[2] px-6 py-3 rounded-2xl bg-blue-600 text-white font-bold shadow-lg shadow-blue-600/20 hover:bg-blue-500 transition-all active:scale-95"
class="flex-2 px-6 py-3 rounded-2xl bg-blue-600 text-white font-bold shadow-lg shadow-blue-600/20 hover:bg-blue-500 transition-all active:scale-95"
@click="saveContact(contactForm)"
>
{{ $t("call.save_contact") }}
@@ -116,7 +116,7 @@
id="saveAsNew"
v-model="saveAsNew"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label for="saveAsNew" class="text-sm text-gray-600 dark:text-zinc-400 cursor-pointer"
>Save as new ringtone</label
@@ -1,8 +1,8 @@
<!-- SPDX-License-Identifier: 0BSD -->
<template>
<div class="fixed inset-0 z-[100] flex items-center justify-center p-4 sm:p-6">
<div class="absolute inset-0 bg-zinc-900/80 backdrop-blur-sm" @click="$emit('close')"></div>
<div class="fixed inset-0 z-100 flex items-center justify-center p-4 sm:p-6">
<div class="absolute inset-0 bg-zinc-900/80 backdrop-blur-xs" @click="$emit('close')"></div>
<div
class="relative w-full max-w-4xl bg-white dark:bg-zinc-900 rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh]"
>
@@ -131,7 +131,7 @@
id="saveAsNew"
v-model="saveAsNew"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
/>
<label for="saveAsNew" class="text-sm text-gray-600 dark:text-zinc-400 cursor-pointer"
>Save as new ringtone</label
@@ -54,7 +54,7 @@
v-model="contactsSearch"
type="text"
:placeholder="$t('contacts.search_placeholder')"
class="input-field !pl-11"
class="input-field pl-11!"
@input="onContactsSearchInput"
/>
</div>
@@ -71,8 +71,8 @@
class="size-10 sm:size-12 rounded-full bg-gray-200 dark:bg-zinc-700 animate-pulse shrink-0"
/>
<div class="flex-1 min-w-0 space-y-2">
<div class="h-4 w-32 bg-gray-200 dark:bg-zinc-700 rounded animate-pulse" />
<div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded animate-pulse" />
<div class="h-4 w-32 bg-gray-200 dark:bg-zinc-700 rounded-sm animate-pulse" />
<div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded-sm animate-pulse" />
</div>
</div>
</template>
@@ -89,7 +89,7 @@
class="group flex cursor-default items-center gap-3 px-1 py-3 transition-colors hover:bg-gray-50/80 dark:hover:bg-zinc-900/70"
@contextmenu.prevent="openContextMenu($event, contact)"
>
<div class="flex-shrink-0">
<div class="shrink-0">
<LxmfUserIcon
:custom-image="contact.custom_image"
:icon-name="contact.remote_icon ? contact.remote_icon.icon_name : ''"
@@ -110,7 +110,9 @@
{{ contact.lxmf_address || contact.remote_identity_hash }}
</div>
</div>
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<div
class="flex items-center gap-1 opacity-100 transition-opacity lg:opacity-0 lg:group-hover:opacity-100"
>
<button
type="button"
class="p-1.5 rounded-lg text-gray-500 dark:text-zinc-400 hover:bg-blue-100 dark:hover:bg-blue-900/40 hover:text-blue-600 dark:hover:text-blue-400 transition-colors"
@@ -154,7 +156,7 @@
<button
type="button"
class="sm:hidden fixed bottom-5 right-4 z-[180] flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
class="sm:hidden fixed bottom-5 right-4 z-180 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
:title="$t('contacts.add_contact')"
@click="openAddDialog"
>
@@ -162,7 +164,7 @@
</button>
<!-- Contact context menu -->
<ContextMenuPanel :show="contextMenu.visible" :x="contextMenu.x" :y="contextMenu.y" panel-class="z-[210]">
<ContextMenuPanel :show="contextMenu.visible" :x="contextMenu.x" :y="contextMenu.y" panel-class="z-210">
<ContextMenuItem @click="openConversation(contextMenu.contact)">
<MaterialDesignIcon icon-name="message-text-outline" class="size-4" />
{{ $t("contacts.send_message") }}
@@ -194,7 +196,7 @@
<!-- Add contact dialog -->
<div
v-if="isAddDialogOpen"
class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-200 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="closeAddDialog"
>
<div class="w-full max-w-lg rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -229,7 +231,7 @@
v-model="newContactInput"
type="text"
class="input-field font-mono"
:class="cameraSupported ? '!pr-12' : ''"
:class="cameraSupported ? 'pr-12!' : ''"
:placeholder="$t('contacts.hash_or_uri_placeholder')"
@keydown.enter.prevent="submitAddContact"
/>
@@ -279,7 +281,7 @@
<!-- Scanner dialog -->
<div
v-if="isScannerDialogOpen"
class="fixed inset-0 z-[220] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
class="fixed inset-0 z-220 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs"
@click.self="closeScannerDialog"
>
<div class="w-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -311,7 +313,7 @@
<!-- Import contacts dialog -->
<div
v-if="isImportDialogOpen"
class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-200 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="closeImportDialog"
>
<div class="w-full max-w-lg rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -354,7 +356,7 @@
<!-- My identity dialog -->
<div
v-if="isMyIdentityDialogOpen"
class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-200 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="isMyIdentityDialogOpen = false"
>
<div class="w-full max-w-md rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -923,8 +925,9 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.glass-card {
@apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-sm p-4;
@apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-2xl shadow-xs p-4;
}
.input-field {
@@ -63,7 +63,7 @@
<input
v-model="search"
type="text"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
:placeholder="$t('debug.search_logs_placeholder')"
@input="debouncedSearch"
/>
@@ -71,7 +71,7 @@
<select
v-model="level"
class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-hidden focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
@change="refreshLogs"
>
<option value="">{{ $t("debug.level_all") }}</option>
@@ -106,14 +106,14 @@
<input
v-model="accessSearch"
type="text"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-none focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
class="block w-full pl-10 pr-3 py-2 border border-gray-300 dark:border-zinc-600 rounded-md leading-5 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white placeholder-gray-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
:placeholder="$t('debug.search_access_placeholder')"
@input="debouncedAccessSearch"
/>
</div>
<select
v-model="accessOutcome"
class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
class="block pl-3 pr-10 py-2 text-base border-gray-300 dark:border-zinc-600 focus:outline-hidden focus:ring-blue-500 focus:border-blue-500 sm:text-sm rounded-md bg-white dark:bg-zinc-900 text-gray-900 dark:text-white"
@change="refreshAccessAttempts"
>
<option value="">{{ $t("debug.outcome_all") }}</option>
@@ -130,7 +130,7 @@
</div>
</div>
<div class="flex-1 overflow-hidden glass-card max-w-6xl mx-auto w-full p-0 flex flex-col rounded-sm">
<div class="flex-1 overflow-hidden glass-card max-w-6xl mx-auto w-full p-0 flex flex-col rounded-xs">
<div
v-if="activeTab === 'logs'"
class="flex-1 overflow-auto p-3 sm:p-4 font-mono text-xs leading-relaxed select-text bg-white dark:bg-zinc-950"
@@ -153,7 +153,7 @@
<span class="text-blue-500 shrink-0 w-20 sm:w-24 overflow-hidden text-ellipsis italic"
>[{{ log.module }}]</span
>
<span class="text-gray-800 dark:text-gray-200 break-words flex-1">
<span class="text-gray-800 dark:text-gray-200 wrap-break-word flex-1">
{{ log.message }}
<span
v-if="log.is_anomaly"
@@ -30,7 +30,7 @@
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'meshchatx'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'meshchatx'"
@@ -41,7 +41,7 @@
class="px-3 py-1 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'reticulum'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'reticulum'"
@@ -58,7 +58,7 @@
<input
v-model="searchQuery"
type="text"
class="block w-full pl-8 pr-8 py-1.5 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-[11px] focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
class="block w-full pl-8 pr-8 py-1.5 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-[11px] focus:outline-hidden focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
placeholder="Search documentation..."
@input="debounceSearch"
/>
@@ -222,7 +222,7 @@
v-if="status.has_docs"
:href="localDocsUrl"
target="_blank"
class="hidden sm:flex items-center px-2.5 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg hover:opacity-90 transition-opacity font-bold text-[10px] shadow-sm"
class="hidden sm:flex items-center px-2.5 py-1.5 bg-zinc-900 dark:bg-white text-white dark:text-zinc-900 rounded-lg hover:opacity-90 transition-opacity font-bold text-[10px] shadow-xs"
>
<MaterialDesignIcon icon-name="open-in-new" class="w-3 h-3 mr-1.5" />
Open
@@ -242,7 +242,7 @@
class="flex-1 md:flex-none px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'meshchatx'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'meshchatx'"
@@ -253,7 +253,7 @@
class="flex-1 md:flex-none px-4 py-1.5 text-[10px] font-bold uppercase tracking-wider rounded-md transition-all"
:class="
activeTab === 'reticulum'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-sm'
? 'bg-white dark:bg-zinc-700 text-blue-600 dark:text-blue-400 shadow-xs'
: 'text-gray-500 hover:text-gray-700 dark:hover:text-zinc-300'
"
@click="activeTab = 'reticulum'"
@@ -270,7 +270,7 @@
<input
v-model="searchQuery"
type="text"
class="block w-full pl-9 pr-9 py-2 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-xs focus:outline-none focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
class="block w-full pl-9 pr-9 py-2 border border-gray-200 dark:border-zinc-700 rounded-lg bg-gray-50 dark:bg-zinc-800 text-gray-900 dark:text-zinc-100 text-xs focus:outline-hidden focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 transition-all"
placeholder="Search all documentation..."
@input="debounceSearch"
/>
@@ -330,7 +330,7 @@
</div>
<div class="flex items-center space-x-2">
<span
class="px-1.5 py-0.5 rounded bg-gray-100 dark:bg-zinc-800 text-[8px] font-bold text-gray-500 uppercase tracking-tighter"
class="px-1.5 py-0.5 rounded-sm bg-gray-100 dark:bg-zinc-800 text-[8px] font-bold text-gray-500 uppercase tracking-tighter"
>
{{ result.source }}
</span>
@@ -370,7 +370,7 @@
<div
v-if="status.last_error"
class="absolute inset-0 z-10 flex items-center justify-center p-6 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-sm"
class="absolute inset-0 z-10 flex items-center justify-center p-6 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-xs"
>
<div
class="max-w-md w-full p-6 bg-red-50 dark:bg-red-900/20 border border-red-100 dark:border-red-900/30 rounded-2xl text-red-600 dark:text-red-400 text-center shadow-xl"
@@ -436,7 +436,7 @@
class="w-full text-left px-3 py-2 rounded-xl text-xs transition-all flex items-center space-x-3"
:class="
selectedDocPath === doc.path
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-bold shadow-sm'
? 'bg-blue-50 dark:bg-blue-900/20 text-blue-600 dark:text-blue-400 font-bold shadow-xs'
: 'text-gray-600 dark:text-zinc-400 hover:bg-white dark:hover:bg-zinc-800'
"
@click="selectDoc(doc.path)"
@@ -467,7 +467,7 @@
<div v-if="selectedDocContent" class="flex-1 overflow-y-auto p-6 md:p-10 scroll-smooth">
<div class="max-w-3xl mx-auto">
<div class="max-w-none break-words" v-html="selectedDocContent.html"></div>
<div class="max-w-none wrap-break-word" v-html="selectedDocContent.html"></div>
</div>
</div>
<div
@@ -786,7 +786,7 @@ export default {
const regex = new RegExp(`(${query})`, "gi");
return escapedText.replace(
regex,
'<span class="bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-0.5 rounded">$1</span>'
'<span class="bg-blue-100 dark:bg-blue-900/40 text-blue-700 dark:text-blue-300 px-0.5 rounded-sm">$1</span>'
);
},
},
@@ -15,7 +15,7 @@
@change="!disabled && $emit('update:modelValue', $event.target.checked)"
/>
<div
class="relative h-6 w-11 shrink-0 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-zinc-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"
class="relative h-6 w-11 shrink-0 bg-gray-200 peer-focus:outline-hidden peer-focus:ring-4 peer-focus:ring-blue-300 dark:peer-focus:ring-blue-800 rounded-full peer dark:bg-zinc-700 peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all dark:border-gray-600 peer-checked:bg-blue-600 dark:peer-checked:bg-blue-600"
></div>
<span v-if="label" class="min-w-0 text-sm font-medium leading-snug text-gray-900 dark:text-gray-300">{{
label
@@ -29,7 +29,7 @@
v-model="newRule.name"
type="text"
:placeholder="$t('forwarder.name_placeholder')"
class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-none"
class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-hidden"
/>
</div>
<div class="space-y-1">
@@ -41,7 +41,7 @@
v-model="newRule.forward_to_hash"
type="text"
:placeholder="$t('forwarder.destination_placeholder')"
class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-none"
class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-hidden"
/>
</div>
<div class="space-y-1">
@@ -53,7 +53,7 @@
v-model="newRule.source_filter_hash"
type="text"
:placeholder="$t('forwarder.source_filter_placeholder')"
class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-none"
class="w-full px-4 py-2 rounded-xl border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500 transition-all outline-hidden"
/>
</div>
</div>
@@ -87,7 +87,7 @@
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-1">
<div
class="px-2 py-0.5 rounded text-[10px] font-bold uppercase tracking-wider"
class="px-2 py-0.5 rounded-sm text-[10px] font-bold uppercase tracking-wider"
:class="
rule.is_active
? 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'
@@ -155,7 +155,7 @@
<div class="mt-3">
<select
v-model="newInterfaceType"
class="input-field appearance-none pr-10 !py-1.5 !text-[11px] opacity-70 hover:opacity-100"
class="input-field appearance-none pr-10 py-1.5! text-[11px]! opacity-70 hover:opacity-100"
>
<option :value="null">More options...</option>
<option value="AX25KISSInterface">AX.25 KISS (Amateur Radio)</option>
@@ -218,7 +218,7 @@
id="tcp-kiss-framing"
v-model="newInterfaceKISSFramingEnabled"
/>
<FormLabel for="tcp-kiss-framing" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="tcp-kiss-framing" class="cursor-pointer mb-0! text-sm"
>Use KISS framing (legacy compatibility)</FormLabel
>
</div>
@@ -227,10 +227,25 @@
id="tcp-i2p-tunneled"
v-model="newInterfaceI2PTunnelingEnabled"
/>
<FormLabel for="tcp-i2p-tunneled" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="tcp-i2p-tunneled" class="cursor-pointer mb-0! text-sm"
>I2P Tunneled (target is an I2P b32)</FormLabel
>
</div>
<div class="flex items-start gap-2">
<Toggle id="tcp-bootstrap-only" v-model="newInterfaceBootstrapOnly" />
<div class="min-w-0">
<FormLabel
for="tcp-bootstrap-only"
class="cursor-pointer mb-0! text-sm block"
>{{
$t("interfaces.discovery_default_bootstrap_only")
}}</FormLabel
>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
</p>
</div>
</div>
<div class="grid grid-cols-3 gap-3">
<div>
<FormLabel class="glass-label">Connect Timeout (s)</FormLabel>
@@ -273,7 +288,7 @@
/>
<FormLabel
for="backbone-listen-mode"
class="cursor-pointer !mb-0 text-sm"
class="cursor-pointer mb-0! text-sm"
>Listener mode (host this backbone)</FormLabel
>
</div>
@@ -305,6 +320,24 @@
class="input-field font-mono text-xs"
/>
</div>
<div class="flex items-start gap-2">
<Toggle
id="backbone-bootstrap-only"
v-model="newInterfaceBootstrapOnly"
/>
<div class="min-w-0">
<FormLabel
for="backbone-bootstrap-only"
class="cursor-pointer mb-0! text-sm block"
>{{
$t("interfaces.discovery_default_bootstrap_only")
}}</FormLabel
>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
</p>
</div>
</div>
</div>
<div v-else class="space-y-4">
<div>
@@ -341,7 +374,7 @@
/>
<FormLabel
for="backbone-listen-ipv6"
class="cursor-pointer !mb-0 text-sm"
class="cursor-pointer mb-0! text-sm"
>Prefer IPv6</FormLabel
>
</div>
@@ -388,7 +421,7 @@
<Toggle id="tcp-server-ipv6" v-model="newInterfacePreferIPV6" />
<FormLabel
for="tcp-server-ipv6"
class="cursor-pointer !mb-0 text-sm"
class="cursor-pointer mb-0! text-sm"
>Prefer IPv6</FormLabel
>
</div>
@@ -397,7 +430,7 @@
id="tcp-server-i2p"
v-model="newInterfaceI2PTunnelingEnabled"
/>
<FormLabel for="tcp-server-i2p" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="tcp-server-i2p" class="cursor-pointer mb-0! text-sm"
>I2P Tunneled</FormLabel
>
</div>
@@ -436,7 +469,7 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="i2p-connectable" v-model="newInterfaceConnectable" />
<FormLabel for="i2p-connectable" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="i2p-connectable" class="cursor-pointer mb-0! text-sm"
>Allow incoming peers (connectable)</FormLabel
>
</div>
@@ -467,7 +500,7 @@
</div>
<button
type="button"
class="secondary-chip !py-1 !px-3 !text-[10px]"
class="secondary-chip py-1! px-3! text-[10px]!"
@click="addI2PPeer('')"
>
<MaterialDesignIcon icon-name="plus" class="size-3" /> Add Peer
@@ -494,7 +527,7 @@
class="flex items-center gap-2 pb-2"
>
<Toggle id="rnode-use-ip" v-model="newInterfaceRNodeUseIP" />
<FormLabel for="rnode-use-ip" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="rnode-use-ip" class="cursor-pointer mb-0! text-sm"
>Connect over network (IP)</FormLabel
>
</div>
@@ -655,7 +688,7 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="rnode-flow-control" v-model="newInterfaceFlowControl" />
<FormLabel for="rnode-flow-control" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="rnode-flow-control" class="cursor-pointer mb-0! text-sm"
>Hardware flow control</FormLabel
>
</div>
@@ -810,7 +843,7 @@
</div>
<div class="flex items-center gap-2">
<Toggle id="kiss-flow-control" v-model="newInterfaceFlowControl" />
<FormLabel for="kiss-flow-control" class="cursor-pointer !mb-0 text-sm"
<FormLabel for="kiss-flow-control" class="cursor-pointer mb-0! text-sm"
>Hardware flow control</FormLabel
>
</div>
@@ -1003,7 +1036,7 @@
<!-- RNode Advanced Tools -->
<ExpandingSection
v-if="['RNodeInterface', 'RNodeIPInterface'].includes(newInterfaceType)"
class="glass-card !p-0 overflow-hidden"
class="glass-card p-0! overflow-hidden"
>
<template #title
><span class="text-sm font-bold">Calculated Parameters</span></template
@@ -1055,7 +1088,7 @@
</ExpandingSection>
<!-- Interface Discovery Settings -->
<ExpandingSection class="glass-card !p-0 overflow-hidden">
<ExpandingSection class="glass-card p-0! overflow-hidden">
<template #title
><span class="text-sm font-bold">Interface Discovery</span></template
>
@@ -1063,7 +1096,7 @@
<div class="p-6 space-y-6">
<div class="flex items-center justify-between">
<div class="max-w-md">
<FormLabel class="glass-label !mb-0"
<FormLabel class="glass-label mb-0!"
>Publish Discovery Announce</FormLabel
>
<p class="text-xs text-gray-400">
@@ -1157,13 +1190,13 @@
</div>
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="flex items-center justify-between gap-4 max-w-md">
<FormLabel class="glass-label !mb-0"
<FormLabel class="glass-label mb-0!"
>Encrypt discovery</FormLabel
>
<Toggle v-model="discovery.discovery_encrypt" />
</div>
<div class="flex items-center justify-between gap-4 max-w-md">
<FormLabel class="glass-label !mb-0"
<FormLabel class="glass-label mb-0!"
>Publish IFAC in announce</FormLabel
>
<Toggle v-model="discovery.publish_ifac" />
@@ -1175,7 +1208,7 @@
</ExpandingSection>
<!-- Global Discovery Settings -->
<ExpandingSection class="glass-card !p-0 overflow-hidden">
<ExpandingSection class="glass-card p-0! overflow-hidden">
<template #title
><span class="text-sm font-bold">Discovery Listener (Peer)</span></template
>
@@ -1183,7 +1216,7 @@
<div class="p-6 space-y-6">
<div class="flex items-center justify-between">
<div class="max-w-md">
<FormLabel class="glass-label !mb-0"
<FormLabel class="glass-label mb-0!"
>Enable Discovery Listener</FormLabel
>
<p class="text-xs text-gray-400">
@@ -1192,6 +1225,19 @@
</div>
<Toggle v-model="reticulumDiscovery.discover_interfaces" />
</div>
<div
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between pt-2"
>
<div class="max-w-xl min-w-0">
<FormLabel class="glass-label mb-0! text-sm">{{
$t("interfaces.discovery_default_bootstrap_only")
}}</FormLabel>
<p class="text-xs text-gray-400">
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
</p>
</div>
<Toggle v-model="reticulumDiscovery.default_bootstrap_only" />
</div>
<div
v-if="reticulumDiscovery.discover_interfaces"
class="space-y-4 pt-4 border-t border-gray-100 dark:border-zinc-800 animate-in fade-in slide-in-from-top-2"
@@ -1213,7 +1259,7 @@
<div class="flex justify-end">
<button
type="button"
class="primary-chip !text-[10px]"
class="primary-chip text-[10px]!"
:disabled="savingDiscovery"
@click="saveReticulumDiscoveryConfig"
>
@@ -1227,7 +1273,7 @@
</ExpandingSection>
<!-- Shared Advanced Settings -->
<ExpandingSection class="glass-card !p-0 overflow-hidden">
<ExpandingSection class="glass-card p-0! overflow-hidden">
<template #title
><span class="text-sm font-bold"
>Advanced Parameters (IFAC, Mode)</span
@@ -1285,14 +1331,14 @@
>
<button
type="button"
class="secondary-chip !px-10 !py-3 !text-sm"
class="secondary-chip px-10! py-3! text-sm!"
@click="$router.push({ name: 'interfaces' })"
>
Cancel
</button>
<button
type="button"
class="primary-chip !px-16 !py-3 !text-sm"
class="primary-chip px-16! py-3! text-sm!"
:disabled="isSaving"
@click="saveInterface"
>
@@ -1313,7 +1359,7 @@
>
<div
v-if="!isEditingInterface && communityPresetsEnabled && communityInterfaces.length > 0"
class="glass-card !p-0 overflow-hidden"
class="glass-card p-0! overflow-hidden"
>
<div
class="bg-gray-50/50 dark:bg-zinc-800/50 p-4 border-b border-gray-200 dark:border-zinc-800 flex items-center justify-between gap-2"
@@ -1327,14 +1373,30 @@
{{ $t("interfaces.community_quick_start_hint") }}
</p>
</div>
<button
type="button"
class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 transition-colors p-1 shrink-0"
:title="$t('interfaces.community_quick_start_hide')"
@click="updateConfig({ show_suggested_community_interfaces: false })"
>
<MaterialDesignIcon icon-name="close" class="size-5" />
</button>
<div class="flex items-center gap-0.5 shrink-0">
<button
type="button"
class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 transition-colors p-1 rounded-full"
:disabled="refreshingCommunityPresets"
:title="$t('interfaces.community_presets_refresh')"
:aria-label="$t('interfaces.community_presets_refresh')"
@click="refreshCommunityPresets"
>
<MaterialDesignIcon
icon-name="refresh"
class="size-5"
:class="{ 'animate-spin-reverse': refreshingCommunityPresets }"
/>
</button>
<button
type="button"
class="text-gray-400 hover:text-gray-600 dark:hover:text-zinc-200 transition-colors p-1 shrink-0"
:title="$t('interfaces.community_quick_start_hide')"
@click="updateConfig({ show_suggested_community_interfaces: false })"
>
<MaterialDesignIcon icon-name="close" class="size-5" />
</button>
</div>
</div>
<div
@@ -1383,7 +1445,7 @@
</div>
<button
type="button"
class="primary-chip !py-1.5 !px-2 !text-[10px] shrink-0"
class="primary-chip py-1.5! px-2! text-[10px]! shrink-0"
@click="quickAddInterfaceFromConfig(communityIface)"
>
{{ $t("interfaces.community_use_preset") }}
@@ -1403,7 +1465,7 @@
</p>
<button
type="button"
class="primary-chip !py-2 !px-4 !text-xs w-full"
class="primary-chip py-2! px-4! text-xs! w-full"
@click="updateConfig({ show_suggested_community_interfaces: true })"
>
{{ $t("interfaces.community_presets_show_again") }}
@@ -1440,14 +1502,14 @@
href="https://directory.rns.recipes/"
target="_blank"
rel="noopener noreferrer"
class="secondary-chip !py-1 !px-2 !text-[9px]"
class="secondary-chip py-1! px-2! text-[9px]!"
>rns.recipes</a
>
<a
href="https://rmap.world/"
target="_blank"
rel="noopener noreferrer"
class="secondary-chip !py-1 !px-2 !text-[9px]"
class="secondary-chip py-1! px-2! text-[9px]!"
>rmap.world</a
>
</div>
@@ -1455,7 +1517,7 @@
</div>
<div
class="glass-card flex flex-col gap-2 !p-4 bg-emerald-50/20 dark:bg-emerald-900/5 border-emerald-100 dark:border-emerald-900/20"
class="glass-card flex flex-col gap-2 p-4! bg-emerald-50/20 dark:bg-emerald-900/5 border-emerald-100 dark:border-emerald-900/20"
>
<div class="flex items-center justify-between gap-2">
<h3
@@ -1471,7 +1533,7 @@
<textarea
v-model="rawConfigInput"
:placeholder="$t('interfaces.quick_import_placeholder')"
class="w-full h-20 bg-white/50 dark:bg-zinc-900/50 border border-emerald-100/50 dark:border-emerald-900/30 rounded-xl p-2 text-[10px] font-mono focus:ring-1 focus:ring-emerald-500 outline-none transition"
class="w-full h-20 bg-white/50 dark:bg-zinc-900/50 border border-emerald-100/50 dark:border-emerald-900/30 rounded-xl p-2 text-[10px] font-mono focus:ring-1 focus:ring-emerald-500 outline-hidden transition"
@input="handleRawConfigInput"
></textarea>
@@ -1551,6 +1613,7 @@ export default {
newInterfaceConnectTimeout: null,
newInterfaceMaxReconnectTries: null,
newInterfaceFixedMTU: null,
newInterfaceBootstrapOnly: true,
newInterfaceConfiguredBitrate: null,
newInterfaceConnectable: true,
newInterfaceBackboneListenMode: false,
@@ -1589,10 +1652,12 @@ export default {
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
default_bootstrap_only: true,
network_identity: "",
},
savingDiscovery: false,
refreshingCommunityPresets: false,
newInterfaceForwardIp: null,
newInterfaceForwardPort: null,
@@ -1774,6 +1839,12 @@ export default {
this.reticulumDiscovery.discover_interfaces = this.parseBool(discovery.discover_interfaces);
this.reticulumDiscovery.interface_discovery_whitelist = discovery.interface_discovery_whitelist ?? "";
this.reticulumDiscovery.interface_discovery_blacklist = discovery.interface_discovery_blacklist ?? "";
this.reticulumDiscovery.default_bootstrap_only = this.parseBool(
discovery.default_bootstrap_only ?? true
);
if (!this.isEditingInterface) {
this.newInterfaceBootstrapOnly = this.reticulumDiscovery.default_bootstrap_only;
}
} catch (e) {
console.log(e);
}
@@ -1786,6 +1857,7 @@ export default {
discover_interfaces: this.reticulumDiscovery.discover_interfaces,
interface_discovery_whitelist: this.reticulumDiscovery.interface_discovery_whitelist || null,
interface_discovery_blacklist: this.reticulumDiscovery.interface_discovery_blacklist || null,
default_bootstrap_only: this.reticulumDiscovery.default_bootstrap_only,
};
await window.api.patch(`/api/v1/reticulum/discovery`, payload);
ToastUtils.success("Discovery listener preferences saved.");
@@ -1815,6 +1887,22 @@ export default {
this.communityInterfacesFetchDone = true;
}
},
async refreshCommunityPresets() {
if (this.refreshingCommunityPresets) return;
this.refreshingCommunityPresets = true;
try {
const r = await window.api.post("/api/v1/community-interfaces/refresh", {});
const n = r.data?.count ?? 0;
ToastUtils.success(this.$t("interfaces.community_presets_refreshed", { count: n }));
await this.loadCommunityInterfaces();
} catch (e) {
const msg = e.response?.data?.message || this.$t("interfaces.community_presets_refresh_failed");
ToastUtils.error(msg);
console.log(e);
} finally {
this.refreshingCommunityPresets = false;
}
},
async loadInterfaceToEdit(interfaceName) {
try {
const response = await window.api.get(`/api/v1/reticulum/interfaces`);
@@ -1855,6 +1943,18 @@ export default {
this.newInterfaceBackboneListenDevice = iface.device ?? null;
}
if (
iface.type === "TCPClientInterface" ||
(iface.type === "BackboneInterface" && !(iface.listen_port != null && iface.listen_port !== ""))
) {
this.newInterfaceBootstrapOnly =
iface.bootstrap_only !== undefined &&
iface.bootstrap_only !== null &&
iface.bootstrap_only !== ""
? this.parseBool(iface.bootstrap_only)
: false;
}
this.newInterfaceGroupID = iface.group_id ?? null;
this.newInterfaceMulticastAddressType = iface.multicast_address_type ?? null;
this.newInterfaceDevices = iface.devices ?? null;
@@ -2059,6 +2159,9 @@ export default {
if (config.connect_timeout) this.newInterfaceConnectTimeout = Number(config.connect_timeout);
if (config.max_reconnect_tries) this.newInterfaceMaxReconnectTries = Number(config.max_reconnect_tries);
if (config.fixed_mtu) this.newInterfaceFixedMTU = Number(config.fixed_mtu);
if (config.bootstrap_only !== undefined && config.bootstrap_only !== null && config.bootstrap_only !== "") {
this.newInterfaceBootstrapOnly = this.parseBool(config.bootstrap_only);
}
if (config.device) this.newInterfaceNetworkDevice = config.device;
if (config.prefer_ipv6 !== undefined) this.newInterfacePreferIPV6 = this.parseBool(config.prefer_ipv6);
if (config.connectable !== undefined) this.newInterfaceConnectable = this.parseBool(config.connectable);
@@ -2099,6 +2202,20 @@ export default {
config.discoverable !== undefined && config.discoverable !== null && config.discoverable !== ""
? this.parseBool(config.discoverable)
: false;
const backboneConnector =
config.type === "BackboneInterface" &&
Boolean(config.remote || config.target_host) &&
!(config.listen_port != null && String(config.listen_port).trim() !== "");
let bootstrapOnlyPayload;
if (config.type === "TCPClientInterface" || backboneConnector) {
if (
config.bootstrap_only !== undefined &&
config.bootstrap_only !== null &&
config.bootstrap_only !== ""
) {
bootstrapOnlyPayload = this.parseBool(config.bootstrap_only);
}
}
const i2pPeers =
config.type === "I2PInterface"
? Array.isArray(config.i2p_peers)
@@ -2200,6 +2317,7 @@ export default {
config.flow_control !== undefined && config.flow_control !== null && config.flow_control !== ""
? this.parseBool(config.flow_control)
: null,
bootstrap_only: bootstrapOnlyPayload,
};
},
applyDiscoveredInterfacePrefill() {
@@ -2306,6 +2424,11 @@ export default {
connect_timeout: this.numOrNull(this.newInterfaceConnectTimeout),
max_reconnect_tries: this.numOrNull(this.newInterfaceMaxReconnectTries),
fixed_mtu: this.numOrNull(this.newInterfaceFixedMTU),
bootstrap_only:
this.newInterfaceType === "TCPClientInterface" ||
(this.newInterfaceType === "BackboneInterface" && !this.newInterfaceBackboneListenMode)
? this.newInterfaceBootstrapOnly === true
: undefined,
connectable:
this.newInterfaceType === "I2PInterface" ? this.newInterfaceConnectable === true : null,
port: this.newInterfaceRNodeUseIP
@@ -2437,8 +2560,9 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.glass-card {
@apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-xl p-6;
@apply bg-white/95 dark:bg-zinc-900/85 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-xl p-6;
}
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition;
@@ -1,7 +1,9 @@
<!-- SPDX-License-Identifier: 0BSD AND MIT -->
<template>
<div class="bg-white rounded shadow divide-y divide-gray-300 dark:divide-zinc-700 dark:bg-zinc-900 overflow-hidden">
<div
class="bg-white rounded-sm shadow-sm divide-y divide-gray-300 dark:divide-zinc-700 dark:bg-zinc-900 overflow-hidden"
>
<div
class="flex p-2 justify-between cursor-pointer hover:bg-gray-50 dark:hover:bg-zinc-800"
@click="isExpanded = !isExpanded"
@@ -55,7 +55,7 @@
<div
v-for="iface in importableInterfaces"
:key="iface.name"
class="bg-white cursor-pointer flex items-center p-2 border rounded shadow dark:bg-zinc-900 dark:border-zinc-700"
class="bg-white cursor-pointer flex items-center p-2 border rounded-sm shadow-sm dark:bg-zinc-900 dark:border-zinc-700"
>
<div class="mr-auto text-sm flex-1" @click="toggleSelectedInterface(iface.name)">
<div class="font-semibold text-gray-700 dark:text-zinc-100">{{ iface.name }}</div>
@@ -49,7 +49,7 @@
}}</span>
<span v-if="isDiscoverable()" class="discoverable-chip shrink-0">Discoverable</span>
</div>
<div class="text-sm text-gray-600 dark:text-gray-300 break-words min-w-0">
<div class="text-sm text-gray-600 dark:text-gray-300 wrap-break-word min-w-0">
{{ description }}
</div>
<div class="flex flex-wrap gap-2 text-xs text-gray-600 dark:text-gray-300">
@@ -303,8 +303,9 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.interface-card {
@apply relative bg-white/95 dark:bg-zinc-900/85 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg p-4 space-y-3 hover:z-10 min-w-0;
@apply relative bg-white/95 dark:bg-zinc-900/85 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg p-4 space-y-3 hover:z-10 min-w-0;
overflow: visible;
}
.interface-card__icon {
@@ -2,10 +2,10 @@
<template>
<div
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full px-3 sm:px-5 md:px-5 lg:px-8 py-4 sm:py-6">
<div class="space-y-0 w-full min-w-0 max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto flex-1">
<div class="space-y-0 w-full min-w-0 max-w-6xl xl:max-w-7xl 2xl:max-w-360 mx-auto flex-1">
<div
v-if="showRestartReminder"
class="bg-amber-600 text-white border border-amber-500/30 p-4 sm:rounded-xl flex flex-wrap gap-3 items-center mb-4 sm:mb-6"
@@ -20,7 +20,7 @@
<button
v-if="isElectron"
type="button"
class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-sm"
class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-xs"
@click="relaunch"
>
<MaterialDesignIcon icon-name="restart" class="w-4 h-4" />
@@ -71,6 +71,20 @@
<MaterialDesignIcon icon-name="restart" class="w-4 h-4" />
<span>{{ reloadingRns ? $t("app.reloading_rns") : "Restart RNS" }}</span>
</button>
<button
type="button"
class="secondary-chip text-sm min-h-[44px] sm:min-h-0 inline-flex items-center justify-center gap-1.5"
:disabled="refreshingCommunityInterfaces"
:title="$t('interfaces.community_presets_refresh')"
:aria-label="$t('interfaces.community_presets_refresh')"
@click="refreshCommunityInterfaces"
>
<MaterialDesignIcon
icon-name="refresh"
class="w-4 h-4"
:class="{ 'animate-spin-reverse': refreshingCommunityInterfaces }"
/>
</button>
</div>
</div>
@@ -84,7 +98,7 @@
v-model="searchTerm"
type="text"
:placeholder="$t('interfaces.search_placeholder')"
class="w-full pl-12 pr-4 py-3 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 shadow-sm"
class="w-full pl-12 pr-4 py-3 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl focus:outline-hidden focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 shadow-xs"
/>
<button
v-if="searchTerm"
@@ -97,7 +111,7 @@
<div>
<select
v-model="typeFilter"
class="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-none focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-white"
class="w-full px-4 py-2.5 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-700 rounded-xl text-sm focus:outline-hidden focus:ring-2 focus:ring-blue-500/50 text-gray-900 dark:text-white"
>
<option value="all">{{ $t("interfaces.all_types") }}</option>
<option v-for="type in sortedInterfaceTypes" :key="type" :value="type">
@@ -142,7 +156,7 @@
<button
type="button"
:class="filterChipClass(statusFilter === 'all')"
class="!py-1 !px-3"
class="py-1! px-3!"
@click="setStatusFilter('all')"
>
{{ $t("interfaces.all") }}
@@ -150,7 +164,7 @@
<button
type="button"
:class="filterChipClass(statusFilter === 'enabled')"
class="!py-1 !px-3"
class="py-1! px-3!"
@click="setStatusFilter('enabled')"
>
{{ $t("app.enabled") }}
@@ -158,7 +172,7 @@
<button
type="button"
:class="filterChipClass(statusFilter === 'disabled')"
class="!py-1 !px-3"
class="py-1! px-3!"
@click="setStatusFilter('disabled')"
>
{{ $t("app.disabled") }}
@@ -215,7 +229,7 @@
<button
type="button"
:class="filterChipClass(discoveredStatusFilter === 'all')"
class="!py-1 !px-3"
class="py-1! px-3!"
@click="discoveredStatusFilter = 'all'"
>
{{ $t("interfaces.all") }}
@@ -223,7 +237,7 @@
<button
type="button"
:class="filterChipClass(discoveredStatusFilter === 'connected')"
class="!py-1 !px-3"
class="py-1! px-3!"
@click="discoveredStatusFilter = 'connected'"
>
{{ $t("interfaces.connected_only") }}
@@ -465,7 +479,7 @@
<div class="relative">
<button
type="button"
class="secondary-chip !p-2 !rounded-xl"
class="secondary-chip p-2! rounded-xl!"
title="Discovery actions"
@click="toggleDiscoveryActionsMenu(iface)"
>
@@ -513,7 +527,7 @@
<button
v-if="iface.latitude != null && iface.longitude != null"
type="button"
class="secondary-chip !p-2 !rounded-xl"
class="secondary-chip p-2! rounded-xl!"
:title="$t('map.title')"
@click="goToMap(iface)"
>
@@ -633,6 +647,24 @@
0 disables auto-connect.
</div>
</div>
<div class="sm:col-span-2">
<div
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
>
<div class="min-w-0 pr-0 sm:pr-4">
<div class="text-sm font-semibold text-gray-900 dark:text-white">
{{ $t("interfaces.discovery_default_bootstrap_only") }}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{{ $t("interfaces.discovery_default_bootstrap_only_hint") }}
</div>
</div>
<Toggle
v-model="discoveryConfig.default_bootstrap_only"
class="shrink-0 sm:my-auto"
/>
</div>
</div>
<div>
<div class="text-xs font-semibold text-gray-700 dark:text-gray-200">
Network Identity Path
@@ -671,7 +703,7 @@
<RouterLink
:to="{ name: 'interfaces.add' }"
class="sm:hidden fixed bottom-5 right-4 z-[60] flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
class="sm:hidden fixed bottom-5 right-4 z-60 flex h-14 w-14 items-center justify-center rounded-full bg-blue-600 text-white shadow-lg ring-1 ring-blue-400/30 transition active:scale-95"
:title="$t('interfaces.add_interface')"
>
<MaterialDesignIcon icon-name="plus" class="w-7 h-7" />
@@ -717,6 +749,7 @@ export default {
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: 0,
default_bootstrap_only: true,
network_identity: "",
},
savingDiscovery: false,
@@ -727,6 +760,7 @@ export default {
discoveredStatusFilter: "all",
discoveryInterval: null,
activeTab: "overview",
refreshingCommunityInterfaces: false,
};
},
computed: {
@@ -1175,6 +1209,7 @@ export default {
discovery.autoconnect_discovered_interfaces !== ""
? Number(discovery.autoconnect_discovered_interfaces)
: 0;
this.discoveryConfig.default_bootstrap_only = this.parseBool(discovery.default_bootstrap_only ?? true);
this.discoveryConfig.network_identity = discovery.network_identity ?? "";
} catch (e) {
console.log(e);
@@ -1199,6 +1234,7 @@ export default {
this.discoveryConfig.autoconnect_discovered_interfaces === ""
? 0
: Number(this.discoveryConfig.autoconnect_discovered_interfaces),
default_bootstrap_only: this.discoveryConfig.default_bootstrap_only,
network_identity: this.discoveryConfig.network_identity || null,
};
@@ -1438,6 +1474,21 @@ export default {
filterChipClass(isActive) {
return isActive ? "primary-chip text-xs" : "secondary-chip text-xs";
},
async refreshCommunityInterfaces() {
if (this.refreshingCommunityInterfaces) return;
this.refreshingCommunityInterfaces = true;
try {
const r = await window.api.post("/api/v1/community-interfaces/refresh", {});
const n = r.data?.count ?? 0;
ToastUtils.success(this.$t("interfaces.community_presets_refreshed", { count: n }));
} catch (e) {
const msg = e.response?.data?.message || this.$t("interfaces.community_presets_refresh_failed");
ToastUtils.error(msg);
console.error(e);
} finally {
this.refreshingCommunityInterfaces = false;
}
},
async reloadRns() {
if (this.reloadingRns) return;
@@ -1462,6 +1513,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.interfaces-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8;
}
@@ -4,7 +4,7 @@
<div>
<div
v-if="showEmergency"
class="relative z-[100] bg-red-600 text-white px-4 py-2 text-center text-sm font-bold shadow-md animate-pulse"
class="relative z-100 bg-red-600 text-white px-4 py-2 text-center text-sm font-bold shadow-md animate-pulse"
>
<div class="flex items-center justify-center gap-2">
<MaterialDesignIcon icon-name="alert-decagram" class="size-5" />
@@ -14,7 +14,7 @@
<div
v-if="showWsDisconnected"
class="relative z-[100] bg-red-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-red-800/80"
class="relative z-100 bg-red-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-red-800/80"
role="status"
aria-live="polite"
>
@@ -22,7 +22,7 @@
</div>
<div
v-if="showWsReconnected"
class="relative z-[100] bg-emerald-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-emerald-800/80 transition-opacity duration-300"
class="relative z-100 bg-emerald-700 text-white px-4 py-2 text-center text-sm font-medium shadow-md border-b border-emerald-800/80 transition-opacity duration-300"
role="status"
aria-live="polite"
>
@@ -17,7 +17,7 @@
<div class="text-sm text-gray-600 dark:text-gray-300 leading-relaxed max-w-2xl">
{{ $t("licenses.description") }}
</div>
<p v-if="meta?.generated_at" class="text-xs text-gray-500 dark:text-zinc-500 break-words">
<p v-if="meta?.generated_at" class="text-xs text-gray-500 dark:text-zinc-500 wrap-break-word">
{{ $t("licenses.generated_at", { time: meta.generated_at }) }}
<span v-if="meta.frontend_source" class="ml-2 inline-block sm:inline">
({{ $t("licenses.frontend_source", { source: meta.frontend_source }) }})
@@ -39,7 +39,7 @@
enterkeyhint="search"
autocomplete="off"
:placeholder="$t('licenses.search_placeholder')"
class="w-full min-h-[44px] sm:min-h-0 pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-base sm:text-sm"
class="w-full min-h-[44px] sm:min-h-0 pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-hidden focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-base sm:text-sm"
/>
<button
v-if="searchQuery"
@@ -82,7 +82,7 @@
<summary
class="cursor-pointer select-none px-3 sm:px-4 py-3.5 sm:py-3 min-h-[48px] sm:min-h-0 font-semibold text-sm sm:text-base text-gray-900 dark:text-white flex items-center justify-between gap-2 list-none touch-manipulation"
>
<span class="min-w-0 break-words pr-2"
<span class="min-w-0 wrap-break-word pr-2"
>{{ $t("licenses.backend_section") }} ({{ filteredBackend.length }})</span
>
<MaterialDesignIcon
@@ -96,7 +96,7 @@
<table class="min-w-full text-left border-collapse text-xs sm:text-sm">
<thead>
<tr
class="sticky top-0 z-[1] border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-sm text-gray-600 dark:text-zinc-400"
class="sticky top-0 z-1 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-xs text-gray-600 dark:text-zinc-400"
>
<th class="py-2 px-2 sm:px-3 font-medium">
{{ $t("licenses.col_package") }}
@@ -129,13 +129,13 @@
{{ row.version }}
</td>
<td
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[10rem] sm:max-w-[14rem] truncate align-top"
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-40 sm:max-w-56 truncate align-top"
:title="row.author"
>
{{ row.author }}
</td>
<td
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[8rem] sm:max-w-xs align-top break-words"
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-32 sm:max-w-xs align-top wrap-break-word"
>
{{ row.license }}
</td>
@@ -158,7 +158,7 @@
<summary
class="cursor-pointer select-none px-3 sm:px-4 py-3.5 sm:py-3 min-h-[48px] sm:min-h-0 font-semibold text-sm sm:text-base text-gray-900 dark:text-white flex items-center justify-between gap-2 list-none touch-manipulation"
>
<span class="min-w-0 break-words pr-2"
<span class="min-w-0 wrap-break-word pr-2"
>{{ $t("licenses.frontend_section") }} ({{ filteredFrontend.length }})</span
>
<MaterialDesignIcon
@@ -172,7 +172,7 @@
<table class="min-w-full text-left border-collapse text-xs sm:text-sm">
<thead>
<tr
class="sticky top-0 z-[1] border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-sm text-gray-600 dark:text-zinc-400"
class="sticky top-0 z-1 border-b border-gray-200 dark:border-zinc-800 bg-gray-50/95 dark:bg-zinc-900/95 backdrop-blur-xs text-gray-600 dark:text-zinc-400"
>
<th class="py-2 px-2 sm:px-3 font-medium">
{{ $t("licenses.col_package") }}
@@ -205,13 +205,13 @@
{{ row.version }}
</td>
<td
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[10rem] sm:max-w-[14rem] truncate align-top"
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-40 sm:max-w-56 truncate align-top"
:title="row.author"
>
{{ row.author }}
</td>
<td
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-[8rem] sm:max-w-xs align-top break-words"
class="py-2 px-2 sm:px-3 text-gray-700 dark:text-zinc-300 max-w-32 sm:max-w-xs align-top wrap-break-word"
>
{{ row.license }}
</td>
@@ -4,7 +4,7 @@
<div class="flex flex-col h-full w-full bg-white dark:bg-zinc-950 overflow-hidden">
<!-- header -->
<div
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-0 px-3 py-2 sm:px-4 border-b border-gray-200 dark:border-zinc-800 bg-white/80 dark:bg-zinc-900/80 backdrop-blur z-10 relative"
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:gap-0 px-3 py-2 sm:px-4 border-b border-gray-200 dark:border-zinc-800 bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm z-10 relative"
>
<div class="hidden sm:flex items-center min-w-0 gap-2">
<v-icon icon="mdi-map" class="text-blue-500 dark:text-blue-400 shrink-0" size="24"></v-icon>
@@ -21,7 +21,7 @@
<button
:class="
discoveredVisible
? 'bg-white dark:bg-zinc-700 shadow-sm text-emerald-600 dark:text-emerald-400'
? 'bg-white dark:bg-zinc-700 shadow-xs text-emerald-600 dark:text-emerald-400'
: 'text-gray-500 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-zinc-700'
"
class="p-1.5 sm:p-2 rounded-lg transition-colors shrink-0"
@@ -34,7 +34,7 @@
<button
:class="
!offlineEnabled
? 'bg-white dark:bg-zinc-700 shadow-sm text-blue-600 dark:text-blue-400'
? 'bg-white dark:bg-zinc-700 shadow-xs text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-300'
"
class="px-2 py-1 text-xs sm:px-3 sm:text-sm font-medium rounded-md transition-all shrink-0"
@@ -45,7 +45,7 @@
<button
:class="
offlineEnabled
? 'bg-white dark:bg-zinc-700 shadow-sm text-blue-600 dark:text-blue-400'
? 'bg-white dark:bg-zinc-700 shadow-xs text-blue-600 dark:text-blue-400'
: 'text-gray-500 dark:text-gray-300'
"
class="px-2 py-1 text-xs sm:px-3 sm:text-sm font-medium rounded-md transition-all shrink-0"
@@ -59,7 +59,7 @@
<!-- upload: icon on mobile, full label from sm -->
<button
type="button"
class="inline-flex items-center justify-center sm:gap-1 p-2 sm:px-3 sm:py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow-sm transition-colors text-sm font-medium shrink-0"
class="inline-flex items-center justify-center sm:gap-1 p-2 sm:px-3 sm:py-1.5 bg-blue-500 hover:bg-blue-600 text-white rounded-lg shadow-xs transition-colors text-sm font-medium shrink-0"
:title="$t('map.upload_mbtiles')"
@click="$refs.fileInput.click()"
>
@@ -160,7 +160,7 @@
(hoveredFeature.get('telemetry') && hoveredFeature.get('telemetry').note)) &&
!editingFeature
"
class="absolute pointer-events-none z-50 bg-white/90 dark:bg-zinc-900/90 backdrop-blur border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xl p-2 text-sm text-gray-900 dark:text-zinc-100 max-w-xs transform -translate-x-1/2 -translate-y-full mb-4"
class="absolute pointer-events-none z-50 bg-white/90 dark:bg-zinc-900/90 backdrop-blur-sm border border-gray-200 dark:border-zinc-700 rounded-lg shadow-xl p-2 text-sm text-gray-900 dark:text-zinc-100 max-w-xs transform -translate-x-1/2 -translate-y-full mb-4"
:style="{
left: map.getPixelFromCoordinate(hoveredFeature.getGeometry().getCoordinates())[0] + 'px',
top: map.getPixelFromCoordinate(hoveredFeature.getGeometry().getCoordinates())[1] + 'px',
@@ -172,7 +172,7 @@
hoveredFeature.get("telemetry") ? hoveredFeature.get("peer")?.display_name || "Peer" : "Note"
}}</span>
</div>
<div class="whitespace-pre-wrap break-words">
<div class="whitespace-pre-wrap wrap-break-word">
{{ hoveredFeature.get("note") || hoveredFeature.get("telemetry")?.note }}
</div>
</div>
@@ -197,7 +197,7 @@
</div>
<textarea
v-model="noteText"
class="w-full h-24 p-2 text-sm bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none text-gray-900 dark:text-zinc-100"
class="w-full h-24 p-2 text-sm bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-hidden resize-none text-gray-900 dark:text-zinc-100"
placeholder="Type your note here..."
></textarea>
<div class="flex justify-between mt-3">
@@ -209,7 +209,7 @@
Delete
</button>
<button
class="px-3 py-1.5 text-xs font-semibold bg-amber-500 text-white hover:bg-amber-600 rounded-lg shadow-sm transition-colors"
class="px-3 py-1.5 text-xs font-semibold bg-amber-500 text-white hover:bg-amber-600 rounded-lg shadow-xs transition-colors"
@click="saveNote"
>
Save
@@ -218,17 +218,17 @@
</div>
</div>
<div ref="drawFeatureInfoElement" class="absolute z-[45] pointer-events-none">
<div ref="drawFeatureInfoElement" class="absolute z-45 pointer-events-none">
<div
v-show="drawFeatureInfoPayload"
class="pointer-events-auto min-w-[11rem] max-w-[min(18rem,calc(100vw-2rem))] rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900/95 backdrop-blur shadow-xl px-3 py-2.5 transform -translate-x-1/2 -translate-y-full mb-2"
class="pointer-events-auto min-w-44 max-w-[min(18rem,calc(100vw-2rem))] rounded-xl border border-gray-200 dark:border-zinc-700 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm shadow-xl px-3 py-2.5 transform -translate-x-1/2 -translate-y-full mb-2"
>
<template v-if="drawFeatureInfoPayload">
<div v-if="drawFeatureInfoPayload.iconSrc" class="flex justify-center mb-2">
<img
:src="drawFeatureInfoPayload.iconSrc"
alt=""
class="max-h-12 max-w-[4.5rem] object-contain rounded border border-gray-100 dark:border-zinc-800 bg-gray-50 dark:bg-zinc-800/50"
class="max-h-12 max-w-18 object-contain rounded-sm border border-gray-100 dark:border-zinc-800 bg-gray-50 dark:bg-zinc-800/50"
/>
</div>
<div
@@ -239,7 +239,7 @@
</div>
<div
v-if="drawFeatureInfoPayload.description && !drawFeatureInfoPayload.descriptionIsHtml"
class="text-[11px] text-gray-600 dark:text-zinc-400 whitespace-pre-wrap break-words leading-snug"
class="text-[11px] text-gray-600 dark:text-zinc-400 whitespace-pre-wrap wrap-break-word leading-snug"
>
{{ drawFeatureInfoPayload.description }}
</div>
@@ -262,7 +262,9 @@
>
{{ row.key }}
</dt>
<dd class="text-gray-800 dark:text-zinc-200 break-words m-0">{{ row.value }}</dd>
<dd class="text-gray-800 dark:text-zinc-200 wrap-break-word m-0">
{{ row.value }}
</dd>
</div>
</template>
</dl>
@@ -275,7 +277,7 @@
:show="showContextMenu"
:x="contextMenuPos.x"
:y="contextMenuPos.y"
panel-class="z-[120] overflow-hidden text-sm"
panel-class="z-120 overflow-hidden text-sm"
>
<template #header>
<div
@@ -369,7 +371,7 @@
<div
v-if="showMapPingModal"
class="fixed inset-0 z-[200] flex items-center justify-center bg-black/40 p-4"
class="fixed inset-0 z-200 flex items-center justify-center bg-black/40 p-4"
role="dialog"
aria-modal="true"
@click.self="showMapPingModal = false"
@@ -424,7 +426,7 @@
<div
v-show="!isMobileScreen"
ref="scaleLineMount"
class="ol-scale-line-host absolute z-10 bottom-4 right-4 sm:bottom-4 max-sm:bottom-[5.5rem] pointer-events-auto min-w-[120px] max-w-[min(55vw,14rem)]"
class="ol-scale-line-host absolute z-10 bottom-4 right-4 sm:bottom-4 max-sm:bottom-22 pointer-events-auto min-w-[120px] max-w-[min(55vw,14rem)]"
:class="{ 'ol-scale-line-host--dark-basemap': isDarkRasterBasemap }"
></div>
@@ -433,7 +435,7 @@
class="absolute bottom-4 left-4 z-10 flex flex-col gap-2 pointer-events-none max-w-[min(100vw-2rem,22rem)]"
>
<div
class="flex flex-col items-center justify-end text-gray-800 dark:text-zinc-100 bg-white/80 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1 shadow-sm pointer-events-auto w-fit"
class="flex flex-col items-center justify-end text-gray-800 dark:text-zinc-100 bg-white/80 dark:bg-zinc-900/80 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1 shadow-xs pointer-events-auto w-fit"
:title="$t('map.north_up')"
>
<div
@@ -449,7 +451,7 @@
</div>
<div
v-if="metadata && metadata.name && !metadata.name.startsWith('Map Export')"
class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-xs text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-sm"
class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-xs text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-xs"
>
<div class="font-semibold text-gray-900 dark:text-zinc-100 mb-1">
{{ metadata.name }}
@@ -465,7 +467,7 @@
<!-- Lat/Lon Box -->
<div
class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-[10px] font-mono text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-sm flex flex-col space-y-0.5"
class="bg-white/80 dark:bg-zinc-900/80 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 p-2 rounded-lg text-[10px] font-mono text-gray-600 dark:text-zinc-400 pointer-events-auto shadow-xs flex flex-col space-y-0.5"
>
<div class="flex justify-between space-x-4">
<span class="opacity-50 uppercase tracking-tighter">Lat</span>
@@ -482,7 +484,7 @@
<div
v-if="isSettingsOpen"
ref="settingsPanel"
class="absolute z-20 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden flex flex-col min-h-0 animate-in fade-in zoom-in-95 duration-200"
class="absolute z-20 bg-white/95 dark:bg-zinc-900/95 backdrop-blur-xs rounded-xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden flex flex-col min-h-0 animate-in fade-in zoom-in-95 duration-200"
:class="
settingsPanelPos
? 'w-96 max-w-[min(100vw-2rem,28rem)] max-h-full'
@@ -524,7 +526,7 @@
<!-- Quick Actions -->
<div class="grid grid-cols-2 gap-2">
<button
class="flex items-center justify-center space-x-1.5 px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95"
class="flex items-center justify-center space-x-1.5 px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95"
@click="setAsDefaultView"
>
<MaterialDesignIcon icon-name="pin" class="size-3" />
@@ -578,7 +580,7 @@
(style.id === 'carto-voyager' && tileServerUrl.includes('rastertiles/voyager')) ||
(style.id === 'carto-light' &&
tileServerUrl.includes('basemaps.cartocdn.com/light_all'))
? 'bg-blue-500 border-blue-600 text-white shadow-sm ring-2 ring-blue-500/20'
? 'bg-blue-500 border-blue-600 text-white shadow-xs ring-2 ring-blue-500/20'
: 'bg-white dark:bg-zinc-900 border-gray-200 dark:border-zinc-800 text-gray-500 dark:text-zinc-400 hover:bg-gray-50 dark:hover:bg-zinc-800'
"
@click="setTileServer(style.id)"
@@ -597,7 +599,7 @@
<input
v-model="tileServerUrl"
type="text"
class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-none"
class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-hidden"
:placeholder="$t('map.tile_server_url_placeholder')"
@blur="saveTileServerUrl"
/>
@@ -613,7 +615,7 @@
<input
v-model="nominatimApiUrl"
type="text"
class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-none"
class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-hidden"
:placeholder="$t('map.nominatim_api_url_placeholder')"
@blur="saveNominatimApiUrl"
/>
@@ -688,7 +690,7 @@
<input
v-model="mbtilesDir"
type="text"
class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-none"
class="w-full bg-gray-50/50 dark:bg-zinc-950/50 border border-gray-200 dark:border-zinc-800 rounded-lg px-2 py-1.5 text-[10px] dark:text-zinc-100 font-mono focus:ring-1 focus:ring-blue-500 transition-all outline-hidden"
placeholder="Default storage"
@blur="saveMBTilesDir"
/>
@@ -710,7 +712,7 @@
:class="
file.is_active
? 'bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800/50'
: 'bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 shadow-sm'
: 'bg-white dark:bg-zinc-900 border border-gray-100 dark:border-zinc-800 shadow-xs'
"
>
<div class="flex flex-col min-w-0 flex-1 mr-2">
@@ -845,7 +847,7 @@
<!-- onboarding tooltip -->
<div
v-if="showOnboardingTooltip"
class="fixed inset-0 z-[100] pointer-events-none"
class="fixed inset-0 z-100 pointer-events-none"
@click="dismissOnboardingTooltip"
>
<div class="absolute inset-0 bg-black/50 pointer-events-auto"></div>
@@ -897,7 +899,7 @@
<!-- save drawing modal -->
<div
v-if="showSaveDrawingModal"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
>
<div
class="bg-white dark:bg-zinc-900 w-full max-w-md rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200"
@@ -949,7 +951,7 @@
<!-- load drawing modal -->
<div
v-if="showLoadDrawingModal"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
>
<div
class="bg-white dark:bg-zinc-900 w-full max-w-lg rounded-2xl shadow-2xl overflow-hidden animate-in fade-in zoom-in duration-200"
@@ -1023,7 +1025,7 @@
<transition name="fade">
<div
v-if="showNoteModal"
class="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-end sm:items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="closeNoteEditor"
>
<div
@@ -1044,7 +1046,7 @@
<div class="p-4">
<textarea
v-model="noteText"
class="w-full h-40 p-4 text-base bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-none resize-none text-gray-900 dark:text-zinc-100"
class="w-full h-40 p-4 text-base bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-xl focus:ring-2 focus:ring-amber-500 focus:border-transparent outline-hidden resize-none text-gray-900 dark:text-zinc-100"
placeholder="Type your note here..."
autofocus
></textarea>
@@ -1058,7 +1060,7 @@
Delete
</button>
<button
class="flex-[2] px-4 py-3 text-sm font-bold bg-amber-500 text-white hover:bg-amber-600 rounded-xl shadow-lg shadow-amber-500/30 transition-colors"
class="flex-2 px-4 py-3 text-sm font-bold bg-amber-500 text-white hover:bg-amber-600 rounded-xl shadow-lg shadow-amber-500/30 transition-colors"
@click="saveNote"
>
Save Note
@@ -17,7 +17,7 @@
:class="msg.is_outbound ? 'ml-auto items-end' : 'mr-auto items-start'"
>
<div
class="px-2 py-1 rounded-lg text-xs break-words shadow-sm"
class="px-2 py-1 rounded-lg text-xs wrap-break-word shadow-xs"
:class="
msg.is_outbound
? 'bg-blue-600 text-white'
@@ -81,7 +81,7 @@
<input
v-model="newMessage"
type="text"
class="flex-1 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-md px-2 py-1 text-xs focus:outline-none focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-zinc-100"
class="flex-1 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-md px-2 py-1 text-xs focus:outline-hidden focus:ring-1 focus:ring-blue-500 text-gray-900 dark:text-zinc-100"
placeholder="Type a message..."
@keydown.enter="sendMessage"
/>
@@ -2,10 +2,10 @@
<template>
<div
class="absolute top-[calc(0.5rem+2.75rem+0.5rem+2.75rem)] left-1/2 -translate-x-1/2 z-[19] w-[min(100vw-2rem,24rem)] pointer-events-auto"
class="absolute top-[calc(0.5rem+2.75rem+0.5rem+2.75rem)] left-1/2 -translate-x-1/2 z-19 w-[min(100vw-2rem,24rem)] pointer-events-auto"
>
<div
class="bg-white/95 dark:bg-zinc-900/95 backdrop-blur border border-gray-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-2 text-xs text-gray-800 dark:text-zinc-200"
class="bg-white/95 dark:bg-zinc-900/95 backdrop-blur-sm border border-gray-200 dark:border-zinc-700 rounded-xl shadow-lg px-3 py-2 text-xs text-gray-800 dark:text-zinc-200"
>
<p class="font-medium text-center" :class="showFromHere ? 'mb-2' : ''">{{ instructionText }}</p>
<button
@@ -20,7 +20,7 @@
:title="tool.type === 'Export' ? 'MBTiles exporter' : $t(`map.tool_${tool.type.toLowerCase()}`)"
@click="onToolClick(tool)"
>
<v-icon :icon="'mdi-' + tool.icon" size="18" class="sm:!size-5"></v-icon>
<v-icon :icon="'mdi-' + tool.icon" size="18" class="sm:size-5!"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-0.5 sm:mx-1"></div>
<button
@@ -33,7 +33,7 @@
:title="$t('map.tool_measure')"
@click="$emit('toggle-measure')"
>
<v-icon icon="mdi-ruler" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-ruler" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
@@ -45,7 +45,7 @@
:title="$t('map.tool_bearing')"
@click="$emit('toggle-bearing')"
>
<v-icon icon="mdi-compass-outline" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-compass-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl transition-all hover:scale-110 active:scale-90"
@@ -57,14 +57,14 @@
:title="$t('map.tool_bearing_from_here')"
@click="$emit('bearing-from-here')"
>
<v-icon icon="mdi-navigation-variant" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-navigation-variant" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-red-50 dark:hover:bg-red-900/20 text-red-500 transition-all hover:scale-110 active:scale-90"
:title="$t('map.tool_clear')"
@click="$emit('clear')"
>
<v-icon icon="mdi-trash-can-outline" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-trash-can-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
v-if="selectedFeature"
@@ -72,7 +72,7 @@
title="Edit note"
@click="$emit('edit-note', selectedFeature)"
>
<v-icon icon="mdi-note-edit-outline" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-note-edit-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
v-if="selectedFeature && !selectedFeature.get('telemetry')"
@@ -80,7 +80,7 @@
title="Delete selected item"
@click="$emit('delete-feature')"
>
<v-icon icon="mdi-selection-remove" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-selection-remove" size="18" class="sm:size-5!"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-0.5 sm:mx-1"></div>
<button
@@ -88,14 +88,14 @@
:title="$t('map.save_drawing')"
@click="$emit('save')"
>
<v-icon icon="mdi-content-save-outline" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-content-save-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-gray-100 dark:hover:bg-zinc-800 text-gray-600 dark:text-gray-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.load_drawing')"
@click="$emit('load')"
>
<v-icon icon="mdi-folder-open-outline" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-folder-open-outline" size="18" class="sm:size-5!"></v-icon>
</button>
<div class="w-px h-6 bg-gray-200 dark:bg-zinc-800 my-auto mx-0.5 sm:mx-1"></div>
<button
@@ -103,21 +103,21 @@
:title="$t('map.go_to_my_location')"
@click="$emit('locate')"
>
<v-icon icon="mdi-crosshairs-gps" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-crosshairs-gps" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-emerald-50 dark:hover:bg-emerald-900/20 text-emerald-600 dark:text-emerald-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.share_view')"
@click="$emit('share-view')"
>
<v-icon icon="mdi-share-variant" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-share-variant" size="18" class="sm:size-5!"></v-icon>
</button>
<button
class="p-1.5 sm:p-2 rounded-xl hover:bg-amber-50 dark:hover:bg-amber-900/20 text-amber-600 dark:text-amber-400 transition-all hover:scale-110 active:scale-90"
:title="$t('map.ping_here_toolbar')"
@click="$emit('ping-here')"
>
<v-icon icon="mdi-send" size="18" class="sm:!size-5"></v-icon>
<v-icon icon="mdi-send" size="18" class="sm:size-5!"></v-icon>
</button>
</div>
</div>
@@ -14,7 +14,7 @@
v-for="p in presets"
:key="p.id"
type="button"
class="px-2 py-1 text-[10px] font-bold uppercase tracking-tight rounded-lg bg-white/95 dark:bg-zinc-900/95 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 shadow-sm hover:bg-gray-50 dark:hover:bg-zinc-800"
class="px-2 py-1 text-[10px] font-bold uppercase tracking-tight rounded-lg bg-white/95 dark:bg-zinc-900/95 border border-gray-200 dark:border-zinc-700 text-gray-800 dark:text-zinc-100 shadow-xs hover:bg-gray-50 dark:hover:bg-zinc-800"
@click="$emit('select-preset', p)"
>
{{ $t(`map.export_region_${p.id}`) }}
@@ -1,7 +1,7 @@
<!-- SPDX-License-Identifier: 0BSD -->
<template>
<div class="absolute inset-0 z-20 flex items-center justify-center bg-white/50 dark:bg-black/50 backdrop-blur-sm">
<div class="absolute inset-0 z-20 flex items-center justify-center bg-white/50 dark:bg-black/50 backdrop-blur-xs">
<div class="bg-white dark:bg-zinc-900 p-6 rounded-xl shadow-xl flex flex-col items-center space-y-4">
<div class="animate-spin rounded-full h-12 w-12 border-4 border-blue-500 border-t-transparent"></div>
<p class="text-gray-900 dark:text-zinc-100 font-medium">{{ message || $t("map.uploading") }}</p>
@@ -7,7 +7,7 @@
ref="inputEl"
:value="modelValue"
type="text"
class="flex-1 px-4 py-2.5 bg-transparent text-gray-900 dark:text-zinc-100 placeholder-gray-400 focus:outline-none focus:ring-0 border-0 text-sm"
class="flex-1 px-4 py-2.5 bg-transparent text-gray-900 dark:text-zinc-100 placeholder-gray-400 focus:outline-hidden focus:ring-0 border-0 text-sm"
:placeholder="$t('map.search_placeholder')"
@input="onInput"
@keydown.enter="$emit('search')"
@@ -8,7 +8,7 @@
}}</span>
</div>
<label class="flex items-center gap-2 text-[10px] text-gray-600 dark:text-zinc-400 cursor-pointer select-none">
<input v-model="mergeImport" type="checkbox" class="rounded border-gray-300 dark:border-zinc-600" />
<input v-model="mergeImport" type="checkbox" class="rounded-sm border-gray-300 dark:border-zinc-600" />
{{ $t("map.vector_exchange_merge") }}
</label>
<div class="grid grid-cols-2 gap-2 sm:grid-cols-3">
@@ -38,7 +38,7 @@
</button>
<button
type="button"
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="disabled || !hasFeatures"
@click="$emit('export-geojson')"
>
@@ -46,7 +46,7 @@
</button>
<button
type="button"
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="disabled || !hasFeatures"
@click="$emit('export-kml')"
>
@@ -54,7 +54,7 @@
</button>
<button
type="button"
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-sm active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
class="flex items-center justify-center px-2 py-2 bg-blue-500 hover:bg-blue-600 text-white rounded-lg transition-all text-[10px] font-bold uppercase tracking-tight shadow-xs active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
:disabled="disabled || !hasFeatures"
@click="$emit('export-kmz')"
>
@@ -5,7 +5,7 @@
<button
v-if="isRecordingAudioAttachment"
type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-semibold text-red-700 shadow-sm hover:border-red-400 transition dark:border-red-500/40 dark:bg-red-900/30 dark:text-red-100"
class="my-auto inline-flex items-center gap-x-1 rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-semibold text-red-700 shadow-xs hover:border-red-400 transition dark:border-red-500/40 dark:bg-red-900/30 dark:text-red-100"
@click="stopRecordingAudioAttachment"
>
<MaterialDesignIcon icon-name="microphone" class="w-4 h-4" />
@@ -35,7 +35,7 @@
<div
v-if="isShowingMenu"
v-click-outside="hideMenu"
class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none"
class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-hidden"
>
<div class="py-1">
<button
@@ -4,7 +4,7 @@
<div class="inline-flex">
<button
type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-full border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/80 px-3 py-1.5 text-xs font-semibold text-gray-800 dark:text-gray-100 shadow-sm hover:border-blue-400 dark:hover:border-blue-500 transition"
class="my-auto inline-flex items-center gap-x-1 rounded-full border border-gray-200 dark:border-zinc-700 bg-white/90 dark:bg-zinc-900/80 px-3 py-1.5 text-xs font-semibold text-gray-800 dark:text-gray-100 shadow-xs hover:border-blue-400 dark:hover:border-blue-500 transition"
@click="showMenu"
>
<MaterialDesignIcon icon-name="image-plus" class="w-4 h-4" />
@@ -23,7 +23,7 @@
<div
v-if="isShowingMenu"
v-click-outside="hideMenu"
class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none"
class="absolute bottom-0 -ml-11 sm:right-0 sm:ml-0 z-10 mb-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-hidden"
>
<div class="py-1">
<button
@@ -33,7 +33,7 @@
:id="`message-${imgItem.lxmf_message.hash}`"
:key="imgItem.lxmf_message.hash"
type="button"
class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(cv.lxmfImageUrl(imgItem.lxmf_message.hash), cv.imageGroupGalleryUrls(entry.items))
"
@@ -63,7 +63,7 @@
:id="`message-${imgItem.lxmf_message.hash}`"
:key="imgItem.lxmf_message.hash"
type="button"
class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(cv.lxmfImageUrl(imgItem.lxmf_message.hash), cv.imageGroupGalleryUrls(entry.items))
"
@@ -86,7 +86,7 @@
<button
:id="`message-${cv.imageGroupSortedChron(entry.items)[2].lxmf_message.hash}`"
type="button"
class="relative col-span-2 aspect-[2/1] max-h-52 min-h-[80px] w-full overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
class="relative col-span-2 aspect-2/1 max-h-52 min-h-[80px] w-full overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(
cv.lxmfImageUrl(cv.imageGroupSortedChron(entry.items)[2].lxmf_message.hash),
@@ -120,7 +120,7 @@
:id="`message-${cell.lxmf_message.hash}`"
:key="cell.lxmf_message.hash"
type="button"
class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-none focus-visible:ring-2 focus-visible:ring-white/80"
class="relative aspect-square min-h-[96px] max-h-[220px] min-w-0 overflow-hidden focus:outline-hidden focus-visible:ring-2 focus-visible:ring-white/80"
@click.stop="
cv.openImage(cv.lxmfImageUrl(cell.lxmf_message.hash), cv.imageGroupGalleryUrls(entry.items))
"
@@ -152,20 +152,24 @@
class="relative rounded-2xl overflow-hidden transition-all duration-200 hover:shadow-md min-w-0 px-3 py-2"
:class="[
['cancelled', 'failed'].includes(entry.items[0].lxmf_message.state)
? 'shadow-sm'
? 'shadow-xs'
: entry.items[0].lxmf_message.is_spam
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-sm'
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-xs'
: cv.isOutboundWaitingBubble(entry.items[0])
? 'shadow-sm'
? 'shadow-xs'
: entry.items[0].is_outbound
? cv.outboundBubbleSurfaceClass(entry.items[0])
: 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-sm',
: 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-xs',
]"
:style="cv.bubbleStyles(entry.items[0])"
@contextmenu.prevent.stop="cv.onMessageContextMenu($event, entry.items[0], true)"
>
<div class="flex items-center justify-end gap-1.5 select-none h-3">
<div
v-if="entry.showTimestamp !== false || entry.items[0].is_outbound"
class="flex items-center justify-end gap-1.5 select-none h-3"
>
<span
v-if="entry.showTimestamp !== false"
class="text-[9px] opacity-80 font-medium"
:class="cv.outboundBubbleFooterTimeClass(entry.items[0])"
:title="cv.getMessageInfoLines(entry.items[0].lxmf_message, entry.items[0].is_outbound).join('\n')"
@@ -193,7 +197,7 @@
<button
v-if="['failed', 'cancelled'].includes(entry.items[0].lxmf_message.state)"
type="button"
class="ml-0.5 p-0.5 rounded hover:bg-white/20 transition-colors"
class="ml-0.5 p-0.5 rounded-sm hover:bg-white/20 transition-colors"
title="Retry sending"
@click.stop="cv.retrySendingMessage(entry.items[0])"
>
@@ -272,21 +276,21 @@
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-600 transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-blue-600 transition-colors"
@click.stop="cv.replyToMessage(entry.items[0])"
>
{{ $t("messages.reply") }}
</button>
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-red-600 transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-red-600 transition-colors"
@click.stop="cv.deleteChatItem(entry.items[0])"
>
Delete
</button>
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-700 transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-gray-700 transition-colors"
@click.stop="cv.showRawMessage(entry.items[0])"
>
Raw LXM
@@ -316,7 +320,7 @@
<span
v-for="(r, ridx) in entry.items[0].lxmf_message.reactions"
:key="r.reactionHash || ridx"
class="inline-flex min-h-[1.125rem] min-w-[1.125rem] cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-sm dark:border-zinc-600/90 dark:bg-zinc-900"
class="inline-flex min-h-4.5 min-w-4.5 cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-xs dark:border-zinc-600/90 dark:bg-zinc-900"
:style="{
order: entry.items[0].is_outbound ? ridx + 2 : ridx + 1,
}"
@@ -328,7 +332,7 @@
class="inline-flex items-center justify-center rounded-full border border-dashed border-gray-300 bg-transparent text-xs leading-none text-gray-400 hover:border-gray-400 hover:text-gray-600 hover:bg-gray-50 dark:border-zinc-600 dark:text-zinc-500 dark:hover:border-zinc-500 dark:hover:text-zinc-300 dark:hover:bg-zinc-800 transition-colors opacity-0 group-hover:opacity-100"
:class="
(entry.items[0].lxmf_message.reactions?.length ?? 0) > 0
? 'min-h-[1.125rem] min-w-[1.125rem] px-1 py-0'
? 'min-h-4.5 min-w-4.5 px-1 py-0'
: 'h-4 w-4 min-h-0 p-0'
"
:style="{
@@ -341,6 +345,18 @@
</button>
</div>
</div>
<div
v-else-if="entry.type === 'dateDivider'"
class="flex justify-center w-full max-w-full my-3 shrink-0"
role="separator"
:aria-label="cv.formatDateDividerLabel(entry.dayKey)"
>
<span
class="inline-flex items-center rounded-full border border-gray-200/90 bg-gray-50/95 px-3 py-1 text-[11px] font-medium tracking-wide text-gray-600 shadow-xs dark:border-zinc-700/90 dark:bg-zinc-800/90 dark:text-zinc-300"
>
{{ cv.formatDateDividerLabel(entry.dayKey) }}
</span>
</div>
<div
v-for="chatItem in [entry.chatItem]"
v-else
@@ -387,7 +403,7 @@
/>
</template>
<div
class="pointer-events-none absolute bottom-2 left-2 rounded-lg bg-black/60 px-2.5 py-1 text-xs text-white opacity-0 backdrop-blur-sm transition-opacity group-hover:opacity-100 sm:opacity-100"
class="pointer-events-none absolute bottom-2 left-2 rounded-lg bg-black/60 px-2.5 py-1 text-xs text-white opacity-0 backdrop-blur-xs transition-opacity group-hover:opacity-100 sm:opacity-100"
>
<span>{{ (chatItem.lxmf_message.fields.image.image_type ?? "image").toUpperCase() }}</span>
<span class="mx-1">·</span>
@@ -396,11 +412,11 @@
</div>
<!-- image-only: inline timestamp overlay (no bubble) -->
<div
v-if="cv.isImageOnlyMessage(chatItem)"
v-if="cv.isImageOnlyMessage(chatItem) && (entry.showTimestamp !== false || chatItem.is_outbound)"
class="flex items-center gap-1.5 select-none mt-0.5"
:class="chatItem.is_outbound ? 'justify-end' : 'justify-start'"
>
<span class="text-[9px] opacity-50 font-medium">
<span v-if="entry.showTimestamp !== false" class="text-[9px] opacity-50 font-medium">
{{ cv.formatTimeAgo(chatItem.lxmf_message.created_at) }}
</span>
<template v-if="chatItem.is_outbound">
@@ -429,14 +445,14 @@
class="relative rounded-2xl overflow-hidden transition-all duration-200 hover:shadow-md min-w-0"
:class="[
['cancelled', 'failed'].includes(chatItem.lxmf_message.state)
? 'shadow-sm'
? 'shadow-xs'
: chatItem.lxmf_message.is_spam
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-sm'
? 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-900 dark:text-yellow-100 border border-yellow-300 dark:border-yellow-700 shadow-xs'
: cv.isOutboundWaitingBubble(chatItem)
? 'shadow-sm'
? 'shadow-xs'
: chatItem.is_outbound
? cv.outboundBubbleSurfaceClass(chatItem)
: 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-sm',
: 'bg-white dark:bg-zinc-900 text-gray-900 dark:text-zinc-100 border border-gray-200/60 dark:border-zinc-800/60 shadow-xs',
]"
:style="cv.bubbleStyles(chatItem)"
@click="cv.onChatItemClick(chatItem)"
@@ -493,28 +509,107 @@
</div>
<!-- content -->
<!-- eslint-disable vue/no-v-html -->
<div
v-if="
chatItem.lxmf_message.content &&
!cv.getParsedItems(chatItem)?.isOnlyPaperMessage &&
!cv.getParsedItems(chatItem)?.isOnlyMapLink &&
!cv.shouldHideAutoImageCaption(chatItem) &&
cv.isMessageBodyTooLargeForDisplay(chatItem)
"
class="rounded-lg border border-amber-200/90 dark:border-amber-800/50 bg-amber-50/90 dark:bg-amber-950/25 px-3 py-2.5 space-y-2 min-w-0"
>
<div class="flex items-start gap-2">
<MaterialDesignIcon
icon-name="text-box-outline"
class="size-5 shrink-0 text-amber-800 dark:text-amber-300/90 mt-0.5"
/>
<p class="text-xs text-amber-950 dark:text-amber-100/90 leading-relaxed min-w-0">
{{
$t("messages.oversized_body_notice", {
count: cv.messageBodyCharCount(chatItem),
})
}}
</p>
</div>
<button
type="button"
class="inline-flex items-center gap-2 rounded-lg bg-amber-700 hover:bg-amber-800 dark:bg-amber-700 dark:hover:bg-amber-600 px-3 py-2 text-xs font-semibold text-white transition-colors"
@click.stop="cv.copyOversizedMessageBody(chatItem)"
>
<MaterialDesignIcon icon-name="content-copy" class="size-4 shrink-0" />
{{ $t("messages.oversized_body_copy") }}
</button>
</div>
<!-- eslint-disable vue/no-v-html -->
<div
v-else-if="
chatItem.lxmf_message.content &&
!cv.getParsedItems(chatItem)?.isOnlyPaperMessage &&
!cv.getParsedItems(chatItem)?.isOnlyMapLink &&
!cv.shouldHideAutoImageCaption(chatItem)
"
class="leading-relaxed break-words [word-break:break-word] min-w-0 markdown-content"
:class="{
'markdown-content--outbound-theme': chatItem.is_outbound && cv.isThemeOutboundBubble(chatItem),
'markdown-content--outbound-solid': chatItem.is_outbound && !cv.isThemeOutboundBubble(chatItem),
'markdown-content--inbound': !chatItem.is_outbound,
'markdown-content--single-emoji': cv.messageMarkdownSingleEmoji(chatItem),
}"
:style="{
'font-family': 'inherit',
'font-size': cv.messageMarkdownFontSizePx(chatItem) + 'px',
}"
@click="cv.handleMessageClick"
v-html="cv.renderMarkdown(chatItem.lxmf_message.content)"
></div>
class="min-w-0"
>
<div
v-if="cv.bubbleViewModel(chatItem).kind === 'loading'"
class="text-sm text-indigo-600/90 dark:text-indigo-300 py-0.5"
>
{{ $t("messages.translating_message") }}
</div>
<div v-else>
<div
class="leading-relaxed wrap-break-word [word-break:break-word] min-w-0 markdown-content"
:class="{
'markdown-content--outbound-theme':
chatItem.is_outbound && cv.isThemeOutboundBubble(chatItem),
'markdown-content--outbound-solid':
chatItem.is_outbound && !cv.isThemeOutboundBubble(chatItem),
'markdown-content--inbound': !chatItem.is_outbound,
'markdown-content--single-emoji': cv.bubbleViewModel(chatItem).singleEmoji,
}"
:style="{
'font-family': 'inherit',
'font-size': cv.bubbleMessageBodyFontSizePx(cv.bubbleViewModel(chatItem)) + 'px',
}"
@click="cv.handleMessageClick"
v-html="cv.renderMarkdown(cv.bubbleViewModel(chatItem).textForRender)"
></div>
<div
v-if="cv.bubbleViewModel(chatItem).showFooter"
class="mt-1.5 pt-1.5 border-t border-black/5 dark:border-white/5 text-xs text-gray-500 dark:text-zinc-500"
>
<div v-if="cv.bubbleViewModel(chatItem).showOriginalLink" class="wrap-break-word">
<span>{{
$t("messages.translated_from_to", {
source: String(cv.bubbleViewModel(chatItem).fromCode || "").toUpperCase(),
target: String(cv.bubbleViewModel(chatItem).toCode || "").toUpperCase(),
})
}}</span>
<button
type="button"
class="ml-1.5 text-indigo-600 dark:text-indigo-400 hover:underline"
@click.stop="
cv.setBubbleMessageShowOriginal(cv.bubbleViewModel(chatItem).messageHash, true)
"
>
{{ $t("messages.show_original") }}
</button>
</div>
<div v-else-if="cv.bubbleViewModel(chatItem).showTranslationLink">
<button
type="button"
class="text-indigo-600 dark:text-indigo-400 hover:underline"
@click.stop="
cv.setBubbleMessageShowOriginal(cv.bubbleViewModel(chatItem).messageHash, false)
"
>
{{ $t("messages.show_translation") }}
</button>
</div>
</div>
</div>
</div>
<!-- eslint-enable vue/no-v-html -->
<!-- telemetry placeholder for empty content messages -->
@@ -600,7 +695,7 @@
</div>
<button
type="button"
class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
class="w-full py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg text-xs font-bold transition-colors shadow-xs"
@click="
cv.addContact(
cv.getParsedItems(chatItem).contact.name,
@@ -628,7 +723,7 @@
</p>
<button
type="button"
class="w-full py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
class="w-full py-2 bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg text-xs font-bold transition-colors shadow-xs"
@click="cv.ingestPaperMessage(cv.getParsedItems(chatItem).paperMessage)"
>
Ingest Message
@@ -654,7 +749,7 @@
</div>
<button
type="button"
class="w-full py-2 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-colors shadow-sm"
class="w-full py-2 bg-sky-600 hover:bg-sky-700 text-white rounded-lg text-xs font-bold transition-colors shadow-xs"
@click="cv.openMapShareFromParsed(cv.getParsedItems(chatItem).mapLink.parsed)"
>
{{ $t("messages.map_link_open") }}
@@ -851,8 +946,12 @@
</div>
<!-- message footer: timestamp and status icons -->
<div class="flex items-center justify-end gap-1.5 mt-1.5 select-none h-3">
<div
v-if="entry.showTimestamp !== false || chatItem.is_outbound"
class="flex items-center justify-end gap-1.5 mt-1.5 select-none h-3"
>
<span
v-if="entry.showTimestamp !== false"
class="text-[9px] opacity-80 font-medium"
:class="cv.outboundBubbleFooterTimeClass(chatItem)"
:title="cv.getMessageInfoLines(chatItem.lxmf_message, chatItem.is_outbound).join('\n')"
@@ -882,7 +981,7 @@
<button
v-if="['failed', 'cancelled'].includes(chatItem.lxmf_message.state)"
type="button"
class="ml-0.5 p-0.5 rounded hover:bg-white/20 transition-colors"
class="ml-0.5 p-0.5 rounded-sm hover:bg-white/20 transition-colors"
title="Retry sending"
@click.stop="cv.retrySendingMessage(chatItem)"
>
@@ -965,21 +1064,21 @@
<div class="flex items-center gap-2">
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-blue-600 transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-blue-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-blue-600 transition-colors"
@click.stop="cv.replyToMessage(chatItem)"
>
{{ $t("messages.reply") }}
</button>
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-red-600 transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-red-500 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-red-600 transition-colors"
@click.stop="cv.deleteChatItem(chatItem)"
>
Delete
</button>
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm hover:bg-gray-700 transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-lg bg-gray-600 px-3 py-1.5 text-xs font-semibold text-white shadow-xs hover:bg-gray-700 transition-colors"
@click.stop="cv.showRawMessage(chatItem)"
>
Raw LXM
@@ -999,7 +1098,7 @@
<span
v-for="(r, ridx) in chatItem.lxmf_message.reactions"
:key="r.reactionHash || ridx"
class="inline-flex min-h-[1.125rem] min-w-[1.125rem] cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-sm dark:border-zinc-600/90 dark:bg-zinc-900"
class="inline-flex min-h-4.5 min-w-4.5 cursor-default select-none items-center justify-center rounded-full border border-gray-200/90 bg-white px-1 py-0 text-sm leading-none shadow-xs dark:border-zinc-600/90 dark:bg-zinc-900"
:style="{
order: chatItem.is_outbound ? ridx + 2 : ridx + 1,
}"
@@ -1012,7 +1111,7 @@
class="inline-flex items-center justify-center rounded-full border border-dashed border-gray-300 bg-transparent text-xs leading-none text-gray-400 hover:border-gray-400 hover:text-gray-600 hover:bg-gray-50 dark:border-zinc-600 dark:text-zinc-500 dark:hover:border-zinc-500 dark:hover:text-zinc-300 dark:hover:bg-zinc-800 transition-colors opacity-0 group-hover:opacity-100"
:class="
(chatItem.lxmf_message.reactions?.length ?? 0) > 0
? 'min-h-[1.125rem] min-w-[1.125rem] px-1 py-0'
? 'min-h-4.5 min-w-4.5 px-1 py-0'
: 'h-4 w-4 min-h-0 p-0'
"
:style="{
@@ -4,7 +4,7 @@
<div
class="relative z-20 flex flex-wrap items-center gap-y-2 px-3 sm:px-4 py-3 border-b border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
>
<div class="flex-shrink-0 mr-3">
<div class="shrink-0 mr-3">
<LxmfUserIcon
:custom-image="selectedPeer.contact_image"
:icon-name="selectedPeer.lxmf_user_icon ? selectedPeer.lxmf_user_icon.icon_name : ''"
File diff suppressed because it is too large Load Diff
@@ -42,7 +42,7 @@ export default {
if (this.fitParent) {
return "absolute inset-0 bg-zinc-200/30 dark:bg-white/10";
}
return "min-h-[8rem] w-full rounded-2xl bg-gray-100/90 dark:bg-zinc-800/60";
return "min-h-32 w-full rounded-2xl bg-gray-100/90 dark:bg-zinc-800/60";
},
},
mounted() {
@@ -66,7 +66,7 @@
<button
v-if="!isPopoutMode && !destinationHash"
type="button"
class="sm:hidden fixed bottom-5 right-4 z-[65] flex h-14 w-14 items-center justify-center rounded-full bg-zinc-900 text-white shadow-lg ring-1 ring-white/10 transition active:scale-95 dark:bg-zinc-100 dark:text-zinc-900 dark:ring-zinc-800"
class="sm:hidden fixed bottom-5 right-4 z-65 flex h-14 w-14 items-center justify-center rounded-full bg-zinc-900 text-white shadow-lg ring-1 ring-white/10 transition active:scale-95 dark:bg-zinc-100 dark:text-zinc-900 dark:ring-zinc-800"
:title="$t('app.compose')"
@click="openMobileCompose"
>
@@ -75,7 +75,7 @@
<div
v-if="isMobileComposeModalOpen"
class="fixed inset-0 z-[95] flex items-end justify-center sm:items-center p-0 sm:p-4 bg-black/50 backdrop-blur-sm sm:bg-black/50"
class="fixed inset-0 z-95 flex items-end justify-center sm:items-center p-0 sm:p-4 bg-black/50 backdrop-blur-xs sm:bg-black/50"
@click.self="isMobileComposeModalOpen = false"
>
<div
@@ -115,14 +115,14 @@
autocorrect="off"
spellcheck="false"
:placeholder="$t('messages.mobile_compose_destination_placeholder')"
class="block w-full rounded-xl border-0 py-2.5 px-3 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-xl border-0 py-2.5 px-3 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@keydown.enter="submitMobileCompose"
/>
</div>
<div class="flex flex-col gap-2">
<button
type="button"
class="w-full flex justify-center items-center gap-2 py-2.5 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:pointer-events-none"
class="w-full flex justify-center items-center gap-2 py-2.5 px-4 border border-transparent rounded-xl shadow-xs text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all disabled:opacity-50 disabled:pointer-events-none"
:disabled="!mobileComposeAddress.trim()"
@click="submitMobileCompose"
>
@@ -144,7 +144,7 @@
<!-- Ingest Paper Message Modal -->
<div
v-if="isIngestModalOpen"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="isIngestModalOpen = false"
>
<div class="w-full max-w-md bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl overflow-hidden">
@@ -176,7 +176,7 @@
v-model="ingestUri"
type="text"
placeholder="lxmf://... or lxma://..."
class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-sm ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
class="block w-full rounded-lg border-0 py-2 text-gray-900 dark:text-white shadow-xs ring-1 ring-inset ring-gray-300 dark:ring-zinc-800 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-blue-600 sm:text-sm dark:bg-zinc-900"
@keydown.enter="ingestPaperMessage"
/>
<button
@@ -200,7 +200,7 @@
</div>
<button
type="button"
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-xl shadow-sm text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
class="w-full flex justify-center py-2.5 px-4 border border-transparent rounded-xl shadow-xs text-sm font-bold text-white bg-blue-600 hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 transition-all"
:disabled="!ingestUri"
@click="ingestPaperMessage"
>
@@ -216,7 +216,7 @@
<div
v-if="isIngestScannerModalOpen"
class="fixed inset-0 z-[120] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
class="fixed inset-0 z-120 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs"
@click.self="closeIngestScannerModal"
>
<div class="w-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -55,7 +55,7 @@
v-for="c in collapsedSidebarConversations"
:key="c.destination_hash"
type="button"
class="shrink-0 p-0.5 rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500"
class="shrink-0 p-0.5 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-indigo-500"
:class="
selectedDestinationHash === c.destination_hash
? 'ring-2 ring-indigo-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950'
@@ -156,7 +156,7 @@
<div
v-if="folderMenu.show"
v-click-outside="{ handler: () => (folderMenu.show = false), capture: true }"
class="absolute right-0 top-full mt-1 z-[60] min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
class="absolute right-0 top-full mt-1 z-60 min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
>
<button
type="button"
@@ -314,7 +314,7 @@
<input
type="checkbox"
:checked="allSelected"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
@change="toggleSelectAll"
/>
<span class="text-xs font-semibold text-blue-700 dark:text-blue-400">
@@ -347,7 +347,7 @@
<div
v-if="moveMenu.show"
v-click-outside="{ handler: () => (moveMenu.show = false), capture: true }"
class="absolute right-0 top-full mt-1 z-[60] min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
class="absolute right-0 top-full mt-1 z-60 min-w-[160px] bg-white dark:bg-zinc-800 rounded-xl shadow-xl border border-gray-200 dark:border-zinc-700 py-1 overflow-hidden animate-in fade-in zoom-in duration-100"
>
<button
type="button"
@@ -376,10 +376,10 @@
<div v-if="isLoading" class="w-full divide-y divide-gray-100 dark:divide-zinc-800">
<div v-for="i in 6" :key="i" class="p-3 animate-pulse">
<div class="flex gap-3">
<div class="rounded bg-gray-200 dark:bg-zinc-800" :style="messageIconStyle"></div>
<div class="rounded-sm bg-gray-200 dark:bg-zinc-800" :style="messageIconStyle"></div>
<div class="flex-1 space-y-2 py-1">
<div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded w-3/4"></div>
<div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded w-1/2"></div>
<div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded-sm w-3/4"></div>
<div class="h-2 bg-gray-200 dark:bg-zinc-800 rounded-sm w-1/2"></div>
</div>
</div>
</div>
@@ -425,7 +425,7 @@
<input
type="checkbox"
:checked="selectedHashes.has(conversation.destination_hash)"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500"
@click.stop
@change="toggleSelectConversation(conversation.destination_hash)"
/>
@@ -441,7 +441,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
@@ -477,9 +477,7 @@
>
{{ conversation.custom_display_name ?? conversation.display_name }}
</div>
<div
class="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap flex-shrink-0"
>
<div class="text-gray-500 dark:text-gray-400 text-xs whitespace-nowrap shrink-0">
{{ formatTimeAgo(conversation.updated_at) }}
</div>
</div>
@@ -532,7 +530,7 @@
:show="contextMenu.show"
:x="contextMenu.x"
:y="contextMenu.y"
panel-class="z-[100]"
panel-class="z-100"
>
<ContextMenuItem @click="bulkMarkAsRead">
<MaterialDesignIcon icon-name="email-open-outline" class="size-4 text-gray-400" />
@@ -695,7 +693,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
@@ -2,7 +2,7 @@
<template>
<div
class="fixed inset-0 z-[150] flex items-center justify-center p-4 bg-black/60 backdrop-blur-sm transition-opacity"
class="fixed inset-0 z-150 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs transition-opacity"
@click.self="close"
>
<div
@@ -66,7 +66,7 @@
</div>
<button
type="button"
class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-sm"
class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-xs"
title="Copy URI"
@click="copyUri"
>
@@ -120,7 +120,7 @@
</div>
<button
type="button"
class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-sm"
class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-xs"
title="Copy URI"
@click="copyUri"
>
@@ -1,12 +1,12 @@
<!-- SPDX-License-Identifier: 0BSD AND MIT -->
<template>
<div class="relative inline-flex items-stretch rounded-xl shadow-sm">
<div class="relative inline-flex items-stretch rounded-xl shadow-xs">
<template v-if="compact">
<button
:disabled="!canSendMessage"
type="button"
class="inline-flex items-center justify-center rounded-xl p-2.5 min-h-[44px] min-w-[44px] text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 touch-manipulation select-none"
class="inline-flex items-center justify-center rounded-xl p-2.5 min-h-[44px] min-w-[44px] text-white transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 touch-manipulation select-none"
:class="[
canSendMessage
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
@@ -40,7 +40,7 @@
<button
:disabled="!canSendMessage"
type="button"
class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2.5 text-sm font-semibold text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
class="inline-flex items-center gap-2 rounded-l-xl px-4 py-2.5 text-sm font-semibold text-white transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2"
:class="[
canSendMessage
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500'
@@ -75,7 +75,7 @@
<button
:disabled="!canSendMessage"
type="button"
class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 h-full text-white transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2"
class="border-l relative inline-flex items-center justify-center rounded-r-xl px-2.5 h-full text-white transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2"
:class="[
canSendMessage
? 'bg-blue-600 dark:bg-blue-600 hover:bg-blue-700 dark:hover:bg-blue-700 focus-visible:outline-blue-500 border-blue-700 dark:border-blue-800'
@@ -105,7 +105,7 @@
<div
v-if="isShowingMenu"
v-click-outside="hideMenu"
class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-none overflow-hidden min-w-[200px]"
class="absolute bottom-full right-0 mb-1 z-10 rounded-xl bg-white dark:bg-zinc-900 shadow-lg ring-1 ring-gray-200 dark:ring-zinc-800 focus:outline-hidden overflow-hidden min-w-[200px]"
>
<div class="py-1">
<button
@@ -22,6 +22,9 @@ export function estimateGroupHeight(entry) {
if (!entry || typeof entry !== "object") {
return 96;
}
if (entry.type === "dateDivider") {
return 44;
}
if (entry.type === "imageGroup") {
return 340;
}
@@ -42,6 +45,9 @@ export function findDisplayGroupIndexForMessageHash(groupsOldestFirst, hash) {
if (!g || typeof g !== "object") {
continue;
}
if (g.type === "dateDivider") {
continue;
}
if (g.type === "imageGroup" && Array.isArray(g.items)) {
if (g.items.some((it) => it?.lxmf_message?.hash === hash)) {
return i;
@@ -4,7 +4,7 @@
<div class="p-3 rounded-xl border border-gray-100 dark:border-zinc-800 bg-gray-50/50 dark:bg-zinc-900/30">
<div class="flex justify-between items-start mb-2">
<span
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded bg-gray-200 dark:bg-zinc-800 text-gray-600 dark:text-zinc-400"
class="text-[10px] font-bold uppercase tracking-wider px-2 py-0.5 rounded-sm bg-gray-200 dark:bg-zinc-800 text-gray-600 dark:text-zinc-400"
>
{{ item.is_outbound ? $t("messages.telemetry_label_sent") : $t("messages.telemetry_label_received") }}
</span>
@@ -4,7 +4,7 @@
<Teleport to="body">
<div
v-if="modelValue"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/50 backdrop-blur-xs"
@click.self="close"
>
<div
@@ -56,7 +56,7 @@
<input
:checked="showTelemetryInChat"
type="checkbox"
class="rounded border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
class="rounded-sm border-gray-300 text-blue-600 focus:ring-blue-500 shrink-0"
@change="onShowTelemetryChange"
/>
<span
@@ -66,7 +66,7 @@
</label>
<button
type="button"
class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-sm shrink-0"
class="px-4 py-2 bg-blue-600 text-white text-xs font-bold rounded-lg hover:bg-blue-700 transition-colors shadow-xs shrink-0"
@click="close"
>
{{ $t("messages.telemetry_history_done") }}
@@ -4,7 +4,7 @@
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
<!-- Compact Header -->
<div
class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-sm shrink-0 min-w-0"
class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-xs shrink-0 min-w-0"
>
<div class="flex items-center gap-2 sm:gap-3 min-w-0">
<div class="bg-teal-100 dark:bg-teal-900/30 p-1.5 rounded-xl shrink-0">
@@ -27,20 +27,27 @@
</div>
</div>
<div class="flex items-center gap-2">
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-teal-600 dark:text-teal-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
<button
type="button"
class="secondary-chip !py-1 !px-3 !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
class="secondary-chip py-1! px-3! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
@click="resetAll"
>
<MaterialDesignIcon icon-name="refresh" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">{{ $t("tools.micron_editor.reset") }}</span>
</button>
<button type="button" class="secondary-chip !py-1 !px-3" @click="downloadFile">
<button type="button" class="secondary-chip py-1! px-3!" @click="downloadFile">
<MaterialDesignIcon icon-name="download" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">{{ $t("tools.micron_editor.save") }}</span>
</button>
<div class="relative">
<button type="button" class="primary-chip !py-1 !px-3" @click="togglePublishMenu">
<button type="button" class="primary-chip py-1! px-3!" @click="togglePublishMenu">
<MaterialDesignIcon icon-name="publish" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">Publish</span>
</button>
@@ -82,7 +89,7 @@
</div>
</div>
</div>
<button v-if="isMobileView" type="button" class="primary-chip !py-1 !px-3" @click="toggleView">
<button v-if="isMobileView" type="button" class="primary-chip py-1! px-3!" @click="toggleView">
<MaterialDesignIcon :icon-name="showEditor ? 'eye' : 'pencil'" class="w-3.5 h-3.5" />
{{ showEditor ? $t("tools.micron_editor.view_preview") : $t("tools.micron_editor.edit") }}
</button>
@@ -99,7 +106,7 @@
class="group flex items-center h-8 px-3 rounded-lg text-xs font-medium transition-colors cursor-pointer whitespace-nowrap"
:class="[
activeTabIndex === index
? 'bg-white dark:bg-zinc-800 text-teal-600 dark:text-teal-400 shadow-sm'
? 'bg-white dark:bg-zinc-800 text-teal-600 dark:text-teal-400 shadow-xs'
: 'text-gray-500 hover:bg-white/50 dark:hover:bg-zinc-800/50 hover:text-gray-700 dark:hover:text-zinc-300',
]"
@click="activeTabIndex = index"
@@ -143,7 +150,7 @@
<textarea
ref="editorRef"
v-model="tabs[activeTabIndex].content"
class="flex-1 w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-sm resize-none focus:outline-none"
class="flex-1 w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-sm resize-none focus:outline-hidden"
:placeholder="$t('tools.micron_editor.placeholder')"
@input="handleInput"
></textarea>
@@ -159,7 +166,7 @@
<!-- eslint-disable vue/no-v-html -->
<div
ref="previewRef"
class="flex-1 overflow-auto text-zinc-100 p-4 font-mono text-sm whitespace-pre-wrap break-words nodeContainer"
class="flex-1 overflow-auto text-zinc-100 p-4 font-mono text-sm whitespace-pre-wrap wrap-break-word nodeContainer"
v-html="renderedContent"
></div>
<!-- eslint-enable vue/no-v-html -->
@@ -86,7 +86,7 @@
v-show="isShowingControls"
class="px-5 pb-5 space-y-4 animate-in fade-in slide-in-from-top-2 duration-300"
>
<div class="h-px bg-gradient-to-r from-transparent via-gray-200 dark:via-zinc-800 to-transparent"></div>
<div class="h-px bg-linear-to-r from-transparent via-gray-200 dark:via-zinc-800 to-transparent"></div>
<div class="flex items-center justify-between">
<label
@@ -128,7 +128,7 @@
autocomplete="off"
maxlength="4"
:aria-label="$t('visualiser.max_hops_filter')"
class="w-[3.25rem] shrink-0 rounded-lg border border-gray-200 bg-white px-1.5 py-1 text-center text-xs font-bold text-blue-600 tabular-nums shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500/40 dark:border-zinc-600 dark:bg-zinc-800 dark:text-blue-400 dark:focus:border-blue-500"
class="w-13 shrink-0 rounded-lg border border-gray-200 bg-white px-1.5 py-1 text-center text-xs font-bold text-blue-600 tabular-nums shadow-xs focus:border-blue-500 focus:outline-hidden focus:ring-1 focus:ring-blue-500/40 dark:border-zinc-600 dark:bg-zinc-800 dark:text-blue-400 dark:focus:border-blue-500"
:value="hopMaxInputShown"
:placeholder="$t('visualiser.all')"
@focus="onHopMaxInputFocus"
@@ -213,7 +213,7 @@
:value="searchQuery"
type="text"
:placeholder="`Search nodes (${nodeCount})...`"
class="block w-full sm:w-64 pl-9 pr-10 py-2.5 sm:py-3 bg-white/70 dark:bg-zinc-900/70 backdrop-blur-xl border border-gray-200/50 dark:border-zinc-800/50 rounded-2xl text-xs font-semibold focus:outline-none focus:ring-2 focus:ring-blue-500/50 sm:focus:w-80 md:max-lg:focus:w-72 lg:focus:w-80 transition-all dark:text-zinc-100 shadow-sm"
class="block w-full sm:w-64 pl-9 pr-10 py-2.5 sm:py-3 bg-white/70 dark:bg-zinc-900/70 backdrop-blur-xl border border-gray-200/50 dark:border-zinc-800/50 rounded-2xl text-xs font-semibold focus:outline-hidden focus:ring-2 focus:ring-blue-500/50 sm:focus:w-80 md:max-lg:focus:w-72 lg:focus:w-80 transition-all dark:text-zinc-100 shadow-xs"
@input="$emit('update:searchQuery', $event.target.value)"
/>
<button
@@ -39,7 +39,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
class="banished-text !opacity-100 !text-white !shadow-lg !bg-red-600 !px-4 !py-2 !rounded-xl !border-2 !tracking-widest"
class="banished-text opacity-100! text-white! shadow-lg! bg-red-600! px-4! py-2! rounded-xl! border-2! tracking-widest!"
:style="{
'background-color': GlobalState.config.banished_color,
'border-color': GlobalState.config.banished_color,
@@ -81,7 +81,7 @@
>
<span
v-if="selectedNodePath"
class="text-sm cursor-pointer whitespace-nowrap flex-shrink-0 hidden sm:inline"
class="text-sm cursor-pointer whitespace-nowrap shrink-0 hidden sm:inline"
@click="onDestinationPathClick(selectedNodePath)"
>
- {{ selectedNodePath.hops }}
@@ -286,7 +286,7 @@
<!-- page content: capture-phase clicks so <a href> is handled before browser default navigation -->
<div
:class="[
'flex-1 overflow-y-auto nodeContainer relative [contain:layout_paint]',
'flex-1 overflow-y-auto nodeContainer relative contain-[layout_paint]',
nomadRenderedShellFullBleed
? 'p-0 bg-transparent text-gray-900 dark:text-gray-100'
: 'p-3 bg-black text-white',
@@ -297,7 +297,7 @@
<div
v-if="isShowingArchivedVersion"
:class="[
'mb-4 p-2 bg-yellow-900/40 border border-yellow-700/50 rounded flex items-center justify-between text-yellow-200',
'mb-4 p-2 bg-yellow-900/40 border border-yellow-700/50 rounded-sm flex items-center justify-between text-yellow-200',
nomadRenderedShellFullBleed ? 'mx-3 mt-3' : '',
]"
>
@@ -311,7 +311,7 @@
}}</span>
</div>
<button
class="text-xs bg-yellow-700/50 hover:bg-yellow-700 px-2 py-1 rounded transition"
class="text-xs bg-yellow-700/50 hover:bg-yellow-700 px-2 py-1 rounded-sm transition"
@click="reloadNodePage"
>
{{ $t("nomadnet.load_live") }}
@@ -344,7 +344,7 @@
<div class="my-auto flex-1">{{ nomadnetPageLoadingLine }}</div>
<button
type="button"
class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded px-3 py-1 text-sm font-semibold cursor-pointer ml-3"
class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded-sm px-3 py-1 text-sm font-semibold cursor-pointer ml-3"
@click="cancelPageDownload"
>
{{ $t("common.cancel") }}
@@ -360,7 +360,7 @@
<div v-if="hasArchivesForCurrentPage" class="space-y-2">
<div class="text-sm text-gray-300">{{ $t("nomadnet.archived_version_available") }}</div>
<button
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow hover:bg-blue-500 transition"
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition"
@click="toggleArchiveDropdown"
>
<MaterialDesignIcon icon-name="archive" class="size-4" />
@@ -413,7 +413,7 @@
</div>
<button
type="button"
class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded px-3 py-1 text-sm font-semibold cursor-pointer"
class="my-auto text-white bg-red-600 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-800 rounded-sm px-3 py-1 text-sm font-semibold cursor-pointer"
@click="cancelFileDownload"
>
{{ $t("common.cancel") }}
@@ -431,7 +431,7 @@
<div class="mx-auto mt-2">
<button
type="button"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-sm hover:bg-gray-400 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
class="my-auto inline-flex items-center gap-x-1 rounded-md bg-gray-500 px-2 py-1 text-sm font-semibold text-white shadow-xs hover:bg-gray-400 focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-gray-500 dark:bg-zinc-800 dark:text-white dark:hover:bg-zinc-700 dark:focus-visible:outline-zinc-500"
@click.stop="openUrl"
>
{{ $t("nomadnet.open_nomadnet_url") }}
@@ -452,6 +452,7 @@ import NomadNetworkSidebar from "./NomadNetworkSidebar.vue";
import Utils from "../../js/Utils";
import DownloadUtils from "../../js/DownloadUtils";
import ToastUtils from "../../js/ToastUtils";
import { getDestinationPath, runDestinationPathFinder } from "../../js/reticulumPathfinding.js";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import IconButton from "../IconButton.vue";
import DropDownMenu from "../DropDownMenu.vue";
@@ -620,14 +621,14 @@ export default {
},
nomadPageContentClasses() {
if (!this.nodePagePath || this.isShowingNodePageSource) {
return ["h-full", "break-words", "whitespace-pre-wrap", "text-gray-100"];
return ["h-full", "wrap-break-word", "whitespace-pre-wrap", "text-gray-100"];
}
const [p] = this.nodePagePath.split("`");
const pl = (p || "").toLowerCase();
const isRich = pl.endsWith(".mu") || pl.endsWith(".md") || pl.endsWith(".html");
const isHtml = pl.endsWith(".html");
const isMd = pl.endsWith(".md");
const classes = ["h-full", "break-words"];
const classes = ["h-full", "wrap-break-word"];
if (this.nomadRenderedShellFullBleed && !isHtml) {
classes.push("px-3", "py-3");
}
@@ -1463,7 +1464,7 @@ export default {
if (!hash || this.pathfinderInProgress) return;
this.pathfinderInProgress = true;
try {
await window.api.post(`/api/v1/destination/${hash}/request-path`);
await runDestinationPathFinder(window.api, hash, "quick");
ToastUtils.success(this.$t("nomadnet.path_finder_request_sent"));
await this.reloadNodePage();
} catch (e) {
@@ -1478,10 +1479,10 @@ export default {
if (!hash || this.pathfinderInProgress) return;
this.pathfinderInProgress = true;
try {
const response = await window.api.get(`/api/v1/destination/${hash}/path`, {
params: { request: "1", timeout: 15 },
const { path } = await runDestinationPathFinder(window.api, hash, "force", {
forceTimeout: 15,
});
if (response?.data?.path) {
if (path) {
ToastUtils.success(this.$t("nomadnet.path_finder_found"));
await this.reloadNodePage();
} else {
@@ -1499,12 +1500,9 @@ export default {
if (!hash || this.pathfinderInProgress) return;
this.pathfinderInProgress = true;
try {
try {
await window.api.post(`/api/v1/destination/${hash}/drop-path`);
} catch (e) {
console.warn("drop-path failed (continuing)", e);
}
await window.api.post(`/api/v1/destination/${hash}/request-path`);
await runDestinationPathFinder(window.api, hash, "drop_then_request", {
onDropPathError: (e) => console.warn("drop-path failed (continuing)", e),
});
ToastUtils.success(this.$t("nomadnet.path_finder_dropped_and_requested"));
await this.reloadNodePage();
} catch (e) {
@@ -1908,14 +1906,11 @@ export default {
return `${m}m ${rs}s`;
},
async getNodePath(destinationHash) {
// clear previous known path
this.selectedNodePath = null;
try {
// get path to destination
const response = await window.api.get(`/api/v1/destination/${destinationHash}/path`);
const response = await getDestinationPath(window.api, destinationHash, {});
// update ui
this.selectedNodePath = response.data.path;
} catch (e) {
console.log(e);
@@ -51,7 +51,7 @@
v-for="fav in collapsedFavouritePreview"
:key="fav.destination_hash"
type="button"
class="shrink-0 p-1 rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
class="shrink-0 p-1 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
:class="
fav.destination_hash === selectedDestinationHash
? 'ring-2 ring-blue-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950'
@@ -71,7 +71,7 @@
v-for="node in collapsedAnnounceNodesPreview"
:key="node.destination_hash"
type="button"
class="shrink-0 p-1 rounded-xl transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
class="shrink-0 p-1 rounded-xl transition-colors focus:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-500"
:class="
node.destination_hash === selectedDestinationHash
? 'ring-2 ring-blue-500 ring-offset-1 ring-offset-white dark:ring-offset-zinc-950'
@@ -182,7 +182,7 @@
:ref="`sectionInput-${section.id}`"
v-model="editingSectionName"
type="text"
class="flex-1 bg-transparent border-b border-blue-500 text-xs font-semibold uppercase tracking-wide text-gray-900 dark:text-white focus:outline-none min-w-0"
class="flex-1 bg-transparent border-b border-blue-500 text-xs font-semibold uppercase tracking-wide text-gray-900 dark:text-white focus:outline-hidden min-w-0"
@click.stop
@keydown.enter="saveSectionName"
@keydown.esc="cancelEditingSection"
@@ -242,13 +242,13 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
</div>
<div class="favourite-card__icon flex-shrink-0">
<div class="favourite-card__icon shrink-0">
<MaterialDesignIcon icon-name="server-network" class="w-5 h-5" />
</div>
<div class="min-w-0 flex-1">
@@ -267,7 +267,7 @@
</div>
</div>
<IconButton
class="flex-shrink-0 text-gray-500 dark:text-gray-300"
class="shrink-0 text-gray-500 dark:text-gray-300"
@click.stop="openFavouriteContextMenu($event, favourite, section.id)"
>
<MaterialDesignIcon icon-name="dots-vertical" class="w-5 h-5" />
@@ -301,7 +301,7 @@
:show="favouriteContextMenu.show"
:x="favouriteContextMenu.x"
:y="favouriteContextMenu.y"
panel-class="z-[200] min-w-56"
panel-class="z-200 min-w-56"
>
<ContextMenuItem @click="renameFavouriteFromContext">
<MaterialDesignIcon icon-name="pencil" class="size-4 text-gray-400" />
@@ -352,7 +352,7 @@
:show="sectionContextMenu.show"
:x="sectionContextMenu.x"
:y="sectionContextMenu.y"
panel-class="z-[200]"
panel-class="z-200"
>
<ContextMenuItem @click="renameSectionFromContext">
<MaterialDesignIcon icon-name="pencil" class="size-4 text-gray-400" />
@@ -405,7 +405,7 @@
:style="{ background: GlobalState.config.banished_color + '33' }"
>
<span
class="banished-text !text-[10px] !opacity-100 !tracking-widest !border !px-1 !py-0.5 !text-white !shadow-lg"
class="banished-text text-[10px]! opacity-100! tracking-widest! border! px-1! py-0.5! text-white! shadow-lg!"
:style="{ 'background-color': GlobalState.config.banished_color }"
>{{ GlobalState.config.banished_text }}</span
>
@@ -415,7 +415,7 @@
class="flex items-center gap-3 flex-1 min-w-0 cursor-pointer"
@click="onNodeClick(node)"
>
<div class="announce-card__icon flex-shrink-0">
<div class="announce-card__icon shrink-0">
<MaterialDesignIcon icon-name="satellite-uplink" class="w-5 h-5" />
</div>
<div class="min-w-0 flex-1">
@@ -441,7 +441,7 @@
</div>
</div>
</div>
<div class="flex-shrink-0">
<div class="shrink-0">
<DropDownMenu>
<template #button>
<IconButton>
@@ -494,7 +494,7 @@
:show="announceContextMenu.show"
:x="announceContextMenu.x"
:y="announceContextMenu.y"
panel-class="z-[200]"
panel-class="z-200"
>
<ContextMenuItem
v-if="!isFavourite(announceContextMenu.node?.destination_hash)"
@@ -1304,6 +1304,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.sidebar-tab {
@apply flex h-full w-1/2 items-center justify-center text-sm font-semibold text-gray-500 dark:text-gray-400 border-b-2 border-transparent transition;
}
@@ -70,35 +70,35 @@
</span>
<button
v-if="!node.running"
class="primary-chip !py-1 !px-3 !text-xs"
class="primary-chip py-1! px-3! text-xs!"
@click.stop="startNode(node.node_id)"
>
Start
</button>
<button
v-else
class="secondary-chip !py-1 !px-3 !text-xs !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
class="secondary-chip py-1! px-3! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
@click.stop="stopNode(node.node_id)"
>
Stop
</button>
<button
v-if="node.running"
class="secondary-chip !py-1 !px-3 !text-xs"
class="secondary-chip py-1! px-3! text-xs!"
@click.stop="announceNode(node.node_id)"
>
Announce
</button>
<button
v-if="node.running && node.destination_hash"
class="secondary-chip !py-1 !px-3 !text-xs"
class="secondary-chip py-1! px-3! text-xs!"
@click.stop="viewNode(node)"
>
<MaterialDesignIcon icon-name="eye" class="w-3.5 h-3.5" />
View
</button>
<button
class="secondary-chip !py-1 !px-3 !text-xs !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
class="secondary-chip py-1! px-3! text-xs! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
@click.stop="deleteNode(node.node_id)"
>
<MaterialDesignIcon icon-name="delete" class="w-3.5 h-3.5" />
@@ -126,10 +126,10 @@
{{ selectedNode.name }}
</div>
<div class="flex items-center gap-2">
<button class="secondary-chip !py-1 !px-3 !text-xs" @click="showRenameDialog = true">
<button class="secondary-chip py-1! px-3! text-xs!" @click="showRenameDialog = true">
Rename
</button>
<button class="secondary-chip !py-1 !px-3 !text-xs" @click="selectedNode = null">
<button class="secondary-chip py-1! px-3! text-xs!" @click="selectedNode = null">
<MaterialDesignIcon icon-name="close" class="w-3.5 h-3.5" />
</button>
</div>
@@ -143,7 +143,7 @@
<div class="text-xs font-bold uppercase tracking-wider">Destination Hash</div>
<button
v-if="selectedNode.running"
class="primary-chip !py-0.5 !px-2 !text-xs"
class="primary-chip py-0.5! px-2! text-xs!"
@click="viewNode(selectedNode)"
>
<MaterialDesignIcon icon-name="eye" class="w-3 h-3" />
@@ -189,7 +189,7 @@
class="input-field flex-1"
@keyup.enter="addPage"
/>
<button class="primary-chip !py-1 !px-3 !text-xs" @click="addPage">
<button class="primary-chip py-1! px-3! text-xs!" @click="addPage">
<MaterialDesignIcon icon-name="plus" class="w-3.5 h-3.5" />
Add Page
</button>
@@ -212,11 +212,11 @@
<span class="text-sm font-mono text-gray-900 dark:text-white">{{ page }}</span>
</div>
<div class="flex items-center gap-2">
<button class="secondary-chip !py-0.5 !px-2 !text-xs" @click="editPage(page)">
<button class="secondary-chip py-0.5! px-2! text-xs!" @click="editPage(page)">
Edit
</button>
<button
class="secondary-chip !py-0.5 !px-2 !text-xs !text-red-500"
class="secondary-chip py-0.5! px-2! text-xs! text-red-500!"
@click="deletePage(page)"
>
<MaterialDesignIcon icon-name="delete" class="w-3 h-3" />
@@ -231,15 +231,15 @@
Editing: {{ editingPage }}
</div>
<div class="flex gap-2">
<button class="primary-chip !py-1 !px-3 !text-xs" @click="savePage">Save</button>
<button class="secondary-chip !py-1 !px-3 !text-xs" @click="editingPage = null">
<button class="primary-chip py-1! px-3! text-xs!" @click="savePage">Save</button>
<button class="secondary-chip py-1! px-3! text-xs!" @click="editingPage = null">
Cancel
</button>
</div>
</div>
<textarea
v-model="editingPageContent"
class="w-full h-64 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-3 font-mono text-sm rounded-lg border border-gray-200 dark:border-zinc-700 resize-y focus:outline-none focus:ring-2 focus:ring-blue-500/50"
class="w-full h-64 bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-3 font-mono text-sm rounded-lg border border-gray-200 dark:border-zinc-700 resize-y focus:outline-hidden focus:ring-2 focus:ring-blue-500/50"
></textarea>
</div>
</div>
@@ -248,7 +248,7 @@
<div v-if="detailTab === 'files'" class="space-y-3">
<div class="flex gap-2">
<input ref="fileInput" type="file" class="hidden" @change="uploadFile" />
<button class="primary-chip !py-1 !px-3 !text-xs" @click="$refs.fileInput.click()">
<button class="primary-chip py-1! px-3! text-xs!" @click="$refs.fileInput.click()">
<MaterialDesignIcon icon-name="upload" class="w-3.5 h-3.5" />
Upload File
</button>
@@ -274,7 +274,7 @@
}}</span>
</div>
<button
class="secondary-chip !py-0.5 !px-2 !text-xs !text-red-500"
class="secondary-chip py-0.5! px-2! text-xs! text-red-500!"
@click="deleteFile(file.name)"
>
<MaterialDesignIcon icon-name="delete" class="w-3 h-3" />
@@ -304,10 +304,10 @@
/>
</div>
<div class="flex justify-end gap-2">
<button class="secondary-chip !py-1.5 !px-4 !text-sm" @click="showCreateDialog = false">
<button class="secondary-chip py-1.5! px-4! text-sm!" @click="showCreateDialog = false">
Cancel
</button>
<button class="primary-chip !py-1.5 !px-4 !text-sm" @click="createNode">Create</button>
<button class="primary-chip py-1.5! px-4! text-sm!" @click="createNode">Create</button>
</div>
</div>
</div>
@@ -331,10 +331,10 @@
/>
</div>
<div class="flex justify-end gap-2">
<button class="secondary-chip !py-1.5 !px-4 !text-sm" @click="showRenameDialog = false">
<button class="secondary-chip py-1.5! px-4! text-sm!" @click="showRenameDialog = false">
Cancel
</button>
<button class="primary-chip !py-1.5 !px-4 !text-sm" @click="renameNode">Rename</button>
<button class="primary-chip py-1.5! px-4! text-sm!" @click="renameNode">Rename</button>
</div>
</div>
</div>
@@ -49,7 +49,7 @@
<button
v-else
type="button"
class="secondary-chip !text-red-600 dark:!text-red-300 !border-red-200 dark:!border-red-500/50"
class="secondary-chip text-red-600! dark:text-red-300! border-red-200! dark:border-red-500/50!"
@click="stop"
>
<MaterialDesignIcon icon-name="pause" class="w-4 h-4" />
@@ -5,7 +5,7 @@
<div class="overflow-y-auto">
<div class="max-w-4xl mx-auto p-4 space-y-6">
<!-- Header with Preview -->
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800">
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800">
<div class="p-6 border-b border-gray-200 dark:border-zinc-800">
<div class="flex items-center justify-between">
<div>
@@ -66,7 +66,7 @@
</div>
<!-- Color Selection -->
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800">
<div class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800">
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Colors</h3>
</div>
@@ -112,7 +112,7 @@
<!-- Icon Selection -->
<div
class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800 overflow-hidden"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Icon</h3>
@@ -183,7 +183,7 @@
<!-- Remove Icon Section -->
<div
class="bg-white dark:bg-zinc-900 rounded-xl shadow-sm border border-gray-200 dark:border-zinc-800 overflow-hidden"
class="bg-white dark:bg-zinc-900 rounded-xl shadow-xs border border-gray-200 dark:border-zinc-800 overflow-hidden"
>
<div class="p-4 border-b border-gray-200 dark:border-zinc-800">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Remove Icon</h3>
@@ -237,7 +237,7 @@
<div class="flex flex-wrap gap-2">
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors disabled:opacity-40"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors disabled:opacity-40"
:disabled="!localPropagationNode"
@click="useLocalPropagationNode"
>
@@ -257,11 +257,11 @@
v-model="searchTerm"
type="text"
:placeholder="`Search ${propagationNodes.length} Propagation Nodes...`"
class="flex-1 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-sm transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
class="flex-1 bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-xs transition-all placeholder:text-gray-400 dark:placeholder:text-zinc-500"
/>
<select
v-model="sortBy"
class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-sm transition-all min-w-[180px]"
class="bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 text-gray-900 dark:text-zinc-100 text-sm rounded-xl focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 px-4 py-2 shadow-xs transition-all min-w-[180px]"
>
<option value="name">Sort by Name</option>
<option value="name-desc">Sort by Name (Z-A)</option>
@@ -277,7 +277,7 @@
<div
v-for="propagationNode of paginatedNodes"
:key="propagationNode.destination_hash"
class="border border-gray-200 dark:border-zinc-800 rounded-2xl bg-white dark:bg-zinc-900 shadow-sm hover:shadow-md transition-shadow overflow-hidden"
class="border border-gray-200 dark:border-zinc-800 rounded-2xl bg-white dark:bg-zinc-900 shadow-xs hover:shadow-md transition-shadow overflow-hidden"
:class="{
'ring-2 ring-blue-500 dark:ring-blue-400':
config.lxmf_preferred_propagation_node_destination_hash ===
@@ -360,14 +360,14 @@
<span>TX {{ formatByteSize(propagationNode.local_node_stats.tx_bytes) }}</span>
</div>
</div>
<div class="flex-shrink-0">
<div class="shrink-0">
<button
v-if="
config.lxmf_preferred_propagation_node_destination_hash ===
propagationNode.destination_hash
"
type="button"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-red-600 hover:bg-red-700 dark:bg-red-600 dark:hover:bg-red-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500"
@click="stopUsingPropagationNode"
>
Stop Using
@@ -375,7 +375,7 @@
<button
v-else
type="button"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
@click="usePropagationNode(propagationNode.destination_hash)"
>
Set as Preferred
@@ -397,7 +397,7 @@
<button
:disabled="currentPage === 1"
type="button"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-sm transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-xs transition-colors"
@click="currentPage = Math.max(1, currentPage - 1)"
>
<svg
@@ -422,7 +422,7 @@
? 'bg-blue-600 text-white dark:bg-blue-600'
: 'bg-white dark:bg-zinc-900 text-gray-700 dark:text-zinc-300 hover:bg-gray-50 dark:hover:bg-zinc-800',
]"
class="w-10 h-10 rounded-xl border border-gray-200 dark:border-zinc-800 text-sm font-medium shadow-sm transition-colors"
class="w-10 h-10 rounded-xl border border-gray-200 dark:border-zinc-800 text-sm font-medium shadow-xs transition-colors"
@click="currentPage = page"
>
{{ page }}
@@ -431,7 +431,7 @@
<button
:disabled="currentPage === totalPages"
type="button"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-sm transition-colors"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-sm font-medium text-gray-700 dark:text-zinc-300 shadow-xs transition-colors"
@click="currentPage = Math.min(totalPages, currentPage + 1)"
>
Next
@@ -474,7 +474,7 @@
<div class="mt-4">
<button
type="button"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-sm transition-colors focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
class="inline-flex items-center gap-x-1.5 rounded-xl bg-blue-600 hover:bg-blue-700 dark:bg-blue-600 dark:hover:bg-blue-700 px-4 py-2 text-sm font-semibold text-white shadow-xs transition-colors focus-visible:outline-solid focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-500"
@click="loadPropagationNodes"
>
Reload
@@ -513,6 +513,7 @@
import Utils from "../../js/Utils";
import WebSocketConnection from "../../js/WebSocketConnection";
import ToastUtils from "../../js/ToastUtils";
import { getDestinationPath } from "../../js/reticulumPathfinding.js";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import {
incomingDeliveryBytesFromCustom,
@@ -855,8 +856,9 @@ export default {
return;
}
try {
const response = await window.api.get(`/api/v1/destination/${hash}/path`, {
params: { request: "1", timeout: 4 },
const response = await getDestinationPath(window.api, hash, {
request: "1",
timeout: 4,
});
this.nodePathsByHash = {
...this.nodePathsByHash,
@@ -136,7 +136,7 @@
</div>
<div class="flex items-end">
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="sendNoCompress" type="checkbox" class="rounded" />
<input v-model="sendNoCompress" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.disable_compression")
}}</span>
@@ -251,7 +251,7 @@
</div>
<div class="flex items-center gap-2">
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="fetchAllowOverwrite" type="checkbox" class="rounded" />
<input v-model="fetchAllowOverwrite" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.allow_overwrite")
}}</span>
@@ -341,13 +341,13 @@
</div>
<div class="flex items-end gap-4">
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="listenFetchAllowed" type="checkbox" class="rounded" />
<input v-model="listenFetchAllowed" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.allow_fetch")
}}</span>
</label>
<label class="flex items-center gap-2 cursor-pointer">
<input v-model="listenAllowOverwrite" type="checkbox" class="rounded" />
<input v-model="listenAllowOverwrite" type="checkbox" class="rounded-sm" />
<span class="text-sm text-gray-700 dark:text-gray-300">{{
$t("rncp.allow_overwrite")
}}</span>
@@ -57,6 +57,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.rnf-action-btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-xl bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 px-3 py-2.5 text-[11px] font-bold text-gray-700 dark:text-zinc-300 border border-gray-200 dark:border-zinc-700 transition-all active:scale-95;
}
@@ -21,7 +21,7 @@
{{ $t("tools.rnode_flasher.disable") }}
</button>
<button
class="rnf-action-btn col-span-2 sm:col-span-1 bg-blue-500 !text-white !border-none hover:bg-blue-600"
class="rnf-action-btn col-span-2 sm:col-span-1 bg-blue-500 text-white! border-none! hover:bg-blue-600"
@click="$emit('action', 'pair-bluetooth')"
>
<MaterialDesignIcon icon-name="key-link" class="size-4" />
@@ -46,6 +46,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.rnf-action-btn {
@apply inline-flex items-center justify-center gap-1.5 rounded-xl bg-gray-100 dark:bg-zinc-800 hover:bg-gray-200 dark:hover:bg-zinc-700 px-3 py-2.5 text-[11px] font-bold text-gray-700 dark:text-zinc-300 border border-gray-200 dark:border-zinc-700 transition-all active:scale-95;
}
@@ -164,6 +164,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.rnf-label {
@apply text-xs font-semibold text-gray-500 dark:text-zinc-500 uppercase tracking-wider;
}
@@ -55,7 +55,7 @@
type="file"
accept=".zip"
data-testid="rnode-firmware-file"
class="block w-full text-sm text-gray-900 dark:text-zinc-100 border border-gray-200 dark:border-zinc-800 rounded-xl cursor-pointer bg-white dark:bg-zinc-900 focus:outline-none file:mr-4 file:py-2.5 file:px-4 file:border-0 file:text-sm file:font-bold file:bg-zinc-200 dark:file:bg-zinc-700 file:text-zinc-700 dark:file:text-zinc-200 hover:file:bg-zinc-300 dark:hover:file:bg-zinc-600"
class="block w-full text-sm text-gray-900 dark:text-zinc-100 border border-gray-200 dark:border-zinc-800 rounded-xl cursor-pointer bg-white dark:bg-zinc-900 focus:outline-hidden file:mr-4 file:py-2.5 file:px-4 file:border-0 file:text-sm file:font-bold file:bg-zinc-200 dark:file:bg-zinc-700 file:text-zinc-700 dark:file:text-zinc-200 hover:file:bg-zinc-300 dark:hover:file:bg-zinc-600"
@change="onFileChange"
/>
</div>
@@ -121,6 +121,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.rnf-label {
@apply text-xs font-semibold text-gray-500 dark:text-zinc-500 uppercase tracking-wider;
}
@@ -11,7 +11,7 @@
icon-name="alert-circle"
class="size-4 mt-0.5 text-red-600 dark:text-red-400 shrink-0"
/>
<span class="text-xs text-red-600 dark:text-red-400 break-words">{{ errorMessage }}</span>
<span class="text-xs text-red-600 dark:text-red-400 wrap-break-word">{{ errorMessage }}</span>
</div>
<button
@@ -55,7 +55,7 @@
</div>
<div class="grid grid-cols-2 gap-2">
<button
class="rnf-action-btn bg-green-600 !text-white !border-none hover:bg-green-700"
class="rnf-action-btn bg-green-600 text-white! border-none! hover:bg-green-700"
@click="$emit('action', 'enable-tnc')"
>
{{ $t("tools.rnode_flasher.enable") }}
@@ -94,6 +94,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.rnf-input-label {
@apply text-[10px] font-bold text-zinc-400 uppercase tracking-widest;
}
@@ -34,12 +34,12 @@
Refresh
</button>
<label class="secondary-chip inline-flex cursor-pointer items-center gap-2 px-4 py-2 text-sm">
<input v-model="includeLinkStats" type="checkbox" class="rounded" />
<input v-model="includeLinkStats" type="checkbox" class="rounded-sm" />
<span>Include Link Stats</span>
</label>
<div class="flex min-w-0 flex-wrap items-center gap-2">
<label class="shrink-0 text-sm text-gray-700 dark:text-gray-300">Sort by:</label>
<select v-model="sorting" class="input-field min-w-[10rem] text-sm">
<select v-model="sorting" class="input-field min-w-40 text-sm">
<option value="">None</option>
<option value="bitrate">Bitrate</option>
<option value="rx">RX Bytes</option>
@@ -81,7 +81,7 @@
<div
v-for="source in blackholeSources"
:key="source"
class="text-sm font-mono bg-gray-50 dark:bg-gray-800 p-2 rounded truncate"
class="text-sm font-mono bg-gray-50 dark:bg-gray-800 p-2 rounded-sm truncate"
>
{{ source }}
</div>
@@ -106,7 +106,7 @@
<div class="min-w-0 flex-1 space-y-2">
<div class="flex flex-wrap items-center gap-2">
<h3
class="break-words text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
class="wrap-break-word text-base font-semibold leading-snug text-gray-900 dark:text-white sm:text-lg"
>
{{ iface.name }}
</h3>
@@ -2,7 +2,7 @@
<template>
<div
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
<div class="flex-1 overflow-y-auto w-full px-4 md:px-5 lg:px-8 py-6">
<div class="space-y-6 w-full max-w-4xl mx-auto">
@@ -19,7 +19,7 @@
<div class="flex flex-row gap-2 sm:flex-wrap sm:items-stretch sm:justify-end">
<button
type="button"
class="inline-flex items-center justify-center gap-x-2 rounded-xl bg-blue-600 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-white shadow-sm hover:bg-blue-500 transition-all active:scale-[0.98] sm:rounded-2xl"
class="inline-flex items-center justify-center gap-x-2 rounded-xl bg-blue-600 p-2.5 sm:px-4 sm:py-2.5 text-sm font-semibold text-white shadow-xs hover:bg-blue-500 transition-all active:scale-[0.98] sm:rounded-2xl"
:title="$t('identities.new_identity')"
@click="showCreateModal = true"
>
@@ -65,8 +65,8 @@
>
<div class="w-14 h-14 rounded-2xl bg-gray-200 dark:bg-zinc-700 animate-pulse shrink-0" />
<div class="flex-1 min-w-0 space-y-2">
<div class="h-5 w-32 bg-gray-200 dark:bg-zinc-700 rounded animate-pulse" />
<div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded animate-pulse" />
<div class="h-5 w-32 bg-gray-200 dark:bg-zinc-700 rounded-sm animate-pulse" />
<div class="h-3 w-48 bg-gray-100 dark:bg-zinc-800 rounded-sm animate-pulse" />
</div>
</div>
</template>
@@ -99,9 +99,9 @@
class="w-12 h-12 sm:w-14 sm:h-14 rounded-2xl flex items-center justify-center shadow-inner overflow-hidden transition-all duration-500"
:class="
identity.is_current && !identity.icon_background_colour
? 'bg-gradient-to-br from-blue-100 to-indigo-100 dark:from-blue-900/50 dark:to-indigo-900/50'
? 'bg-linear-to-br from-blue-100 to-indigo-100 dark:from-blue-900/50 dark:to-indigo-900/50'
: !identity.icon_background_colour
? 'bg-gradient-to-br from-gray-100 to-slate-100 dark:from-zinc-800 dark:to-zinc-800/50'
? 'bg-linear-to-br from-gray-100 to-slate-100 dark:from-zinc-800 dark:to-zinc-800/50'
: ''
"
:style="
@@ -129,14 +129,14 @@
</div>
<div
v-if="identity.is_current"
class="absolute -top-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-zinc-900 shadow-sm"
class="absolute -top-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-zinc-900 shadow-xs"
></div>
</div>
<!-- info -->
<div class="flex-1 min-w-0">
<div class="flex flex-wrap items-center gap-2">
<h3 class="font-bold text-gray-900 dark:text-white break-words sm:truncate">
<h3 class="font-bold text-gray-900 dark:text-white wrap-break-word sm:truncate">
{{ identity.display_name }}
</h3>
<span
@@ -242,7 +242,7 @@
<!-- create modal -->
<div
v-if="showCreateModal"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
>
<div class="glass-card w-full max-w-md shadow-2xl animate-in fade-in zoom-in duration-200">
<div class="p-6">
@@ -292,7 +292,7 @@
<!-- import modal -->
<div
v-if="showImportModal"
class="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/40 backdrop-blur-sm"
class="fixed inset-0 z-100 flex items-center justify-center p-4 bg-black/40 backdrop-blur-xs"
@click.self="showImportModal = false"
>
<div class="glass-card w-full max-w-md shadow-2xl animate-in fade-in zoom-in duration-200">
@@ -605,8 +605,9 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.glass-card {
@apply bg-white/90 dark:bg-zinc-900/80 backdrop-blur border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg;
@apply bg-white/90 dark:bg-zinc-900/80 backdrop-blur-sm border border-gray-200 dark:border-zinc-800 rounded-3xl shadow-lg;
}
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-500 dark:focus:border-blue-500 block w-full p-3 text-gray-900 dark:text-gray-100 transition;
@@ -3,10 +3,10 @@
<template>
<div
v-if="config"
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-gradient-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
class="flex flex-col flex-1 overflow-hidden min-w-0 bg-linear-to-br from-slate-50 via-slate-100 to-white dark:from-zinc-950 dark:via-zinc-900 dark:to-zinc-900"
>
<div class="flex-1 overflow-y-auto overflow-x-hidden w-full min-w-0 px-3 sm:px-5 md:px-5 lg:px-8 py-4 sm:py-6">
<div class="space-y-0 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto min-w-0">
<div class="space-y-0 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-360 mx-auto min-w-0">
<div class="settings-section settings-section--hero">
<div class="flex flex-col lg:flex-row lg:items-center gap-4">
<div class="flex-1 space-y-1">
@@ -19,7 +19,7 @@
v-model="config.display_name"
type="text"
:placeholder="$t('app.display_name_placeholder')"
class="w-full rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-base font-semibold text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500/60 focus:border-blue-500 outline-none transition"
class="w-full rounded-xl border border-gray-200 dark:border-zinc-700 bg-white dark:bg-zinc-800 px-3 py-2 text-base font-semibold text-gray-900 dark:text-white focus:ring-2 focus:ring-blue-500/60 focus:border-blue-500 outline-hidden transition"
@input="onDisplayNameChange"
/>
</div>
@@ -42,7 +42,7 @@
class="grid grid-cols-1 sm:grid-cols-3 gap-2 sm:gap-3 mt-4 text-sm text-gray-600 dark:text-gray-300"
>
<div
class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2"
>
<div class="text-xs uppercase tracking-wide">{{ $t("app.theme") }}</div>
<div class="font-semibold text-gray-900 dark:text-white capitalize">
@@ -50,7 +50,7 @@
</div>
</div>
<div
class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2"
>
<div class="text-xs uppercase tracking-wide">{{ $t("app.transport") }}</div>
<div class="font-semibold text-gray-900 dark:text-white">
@@ -58,7 +58,7 @@
</div>
</div>
<div
class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02]"
class="border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2"
>
<div class="text-xs uppercase tracking-wide">{{ $t("app.propagation") }}</div>
<div class="font-semibold text-gray-900 dark:text-white">
@@ -102,20 +102,30 @@
<div
class="sticky top-0 z-10 py-3 sm:py-4 mb-2 border-b border-gray-200/50 dark:border-zinc-800/50 bg-transparent min-w-0"
>
<div class="relative w-full max-w-6xl xl:max-w-7xl 2xl:max-w-[90rem] mx-auto min-w-0 px-0">
<div class="relative w-full max-w-6xl xl:max-w-7xl 2xl:max-w-360 mx-auto min-w-0 px-0">
<div class="absolute inset-y-0 left-0 pl-4 flex items-center pointer-events-none">
<MaterialDesignIcon icon-name="magnify" class="size-5 text-gray-400" />
</div>
<input
v-model="searchQuery"
type="text"
class="w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl py-3 pl-12 pr-4 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-none transition-all shadow-sm"
:value="searchQuery"
type="search"
inputmode="search"
enterkeyhint="search"
autocomplete="off"
autocorrect="off"
autocapitalize="none"
spellcheck="false"
class="w-full bg-white dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-2xl py-3 pl-12 pr-4 text-sm focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 outline-hidden transition-all shadow-xs"
:placeholder="$t('app.search_settings') || 'Search settings...'"
@input="onSettingsSearchInput"
@change="onSettingsSearchInput"
@compositionend="onSettingsSearchCompositionEnd"
/>
<button
v-if="searchQuery"
v-if="settingsSearchActive"
type="button"
class="absolute inset-y-0 right-0 pr-4 flex items-center text-gray-400 hover:text-gray-600 dark:hover:text-gray-200"
@click="searchQuery = ''"
@click="clearSettingsSearch"
>
<MaterialDesignIcon icon-name="close-circle" class="size-5" />
</button>
@@ -124,7 +134,7 @@
<!-- no results -->
<div
v-if="searchQuery && !hasSearchResults"
v-if="settingsSearchActive && !hasSearchResults"
class="flex flex-col items-center justify-center py-12 text-center"
>
<div
@@ -133,10 +143,11 @@
<MaterialDesignIcon icon-name="magnify-close" class="size-8 text-gray-400" />
</div>
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">No results found</h3>
<p class="text-gray-500 dark:text-gray-400">No settings match "{{ searchQuery }}"</p>
<p class="text-gray-500 dark:text-gray-400">No settings match "{{ settingsSearchDisplay }}"</p>
<button
type="button"
class="mt-4 px-4 py-2 bg-blue-500 text-white rounded-xl hover:bg-blue-600 transition font-semibold text-sm"
@click="searchQuery = ''"
@click="clearSettingsSearch"
>
Clear search
</button>
@@ -148,9 +159,7 @@
class="columns-1 md:columns-2 xl:columns-2 2xl:columns-3 gap-x-8 gap-y-0"
>
<SettingsSectionBlock
v-show="
matchesSearch('stranger', 'attachments', 'trust', 'block', 'banner', 'unknown', 'contact')
"
v-show="matchesSearch(...sectionKeywords.strangerProtection)"
eyebrow="Security"
:title="$t('app.stranger_protection')"
:description="$t('app.stranger_protection_description')"
@@ -296,7 +305,7 @@
<label
class="flex items-center gap-2 text-sm text-gray-800 dark:text-gray-200 cursor-pointer"
>
<input v-model="stickerImportReplaceDuplicates" type="checkbox" class="rounded" />
<input v-model="stickerImportReplaceDuplicates" type="checkbox" class="rounded-sm" />
{{ $t("stickers.replace_duplicates") }}
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -360,7 +369,7 @@
<label
class="flex items-center gap-2 text-sm text-gray-800 dark:text-gray-200 cursor-pointer"
>
<input v-model="gifImportReplaceDuplicates" type="checkbox" class="rounded" />
<input v-model="gifImportReplaceDuplicates" type="checkbox" class="rounded-sm" />
{{ $t("gifs.replace_duplicates") }}
</label>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3">
@@ -1120,7 +1129,7 @@
<input
id="detailed-outbound-send-status"
type="checkbox"
class="mt-1 rounded border-gray-300 dark:border-zinc-600"
class="mt-1 rounded-sm border-gray-300 dark:border-zinc-600"
:checked="GlobalState.detailedOutboundSendStatus"
@change="onDetailedOutboundSendStatusChange"
/>
@@ -1134,6 +1143,26 @@
</label>
</div>
<div
class="flex items-start gap-3 rounded-xl border border-gray-200 dark:border-zinc-700 px-3 py-2.5"
>
<input
id="message-timestamp-grouping"
type="checkbox"
class="mt-1 rounded-sm border-gray-300 dark:border-zinc-600"
:checked="GlobalState.messageTimestampGroupingEnabled"
@change="onMessageTimestampGroupingChange"
/>
<label for="message-timestamp-grouping" class="min-w-0 cursor-pointer">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.message_timestamp_grouping") }}
</div>
<div class="text-xs text-gray-500 dark:text-zinc-400 mt-0.5">
{{ $t("app.message_timestamp_grouping_description") }}
</div>
</label>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
@@ -1289,21 +1318,23 @@
</div>
</section>
<!-- Location -->
<!-- Location (map & coordinates) -->
<section
v-show="matchesSearch(...sectionKeywords.location)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
<div>
<div class="settings-section__eyebrow">Privacy</div>
<h2>Location</h2>
<p>Manage how your location is shared.</p>
<div class="settings-section__eyebrow">{{ $t("app.settings_map_eyebrow") }}</div>
<h2>{{ $t("app.location") }}</h2>
<p>{{ $t("app.location_manage_desc") }}</p>
</div>
</header>
<div class="settings-section__body space-y-4">
<div class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">Location Source</div>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.location_source") }}
</div>
<select
v-model="config.location_source"
class="input-field"
@@ -1311,21 +1342,20 @@
updateConfig({ location_source: config.location_source }, 'location_source')
"
>
<option value="browser">Automatic (Browser)</option>
<option value="manual">Manual</option>
<option value="browser">{{ $t("app.location_source_browser") }}</option>
<option value="manual">{{ $t("app.location_source_manual") }}</option>
</select>
<div
v-if="config.location_source === 'browser'"
class="text-xs text-gray-600 dark:text-gray-400"
>
Uses your browser's geolocation API. Note: In the desktop app, this can use Google
services, which is blocked by CORS so you would need to specifically allow it.
{{ $t("app.location_source_browser_desc") }}
</div>
<div
v-if="config.location_source === 'manual'"
class="text-xs text-gray-600 dark:text-gray-400"
>
Use manually entered coordinates for maximum privacy.
{{ $t("app.location_source_manual_desc") }}
</div>
</div>
@@ -1385,69 +1415,6 @@
/>
</div>
</div>
<div class="pt-4 border-t border-gray-100 dark:border-zinc-800 space-y-4">
<label class="setting-toggle">
<Toggle
id="telemetry-enabled"
v-model="config.telemetry_enabled"
@update:model-value="
updateConfig(
{ telemetry_enabled: config.telemetry_enabled },
'telemetry_enabled'
)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{ $t("app.telemetry_enabled") }}</span>
<span class="setting-toggle__description">{{
$t("app.telemetry_description")
}}</span>
</span>
</label>
</div>
<div
v-if="config.telemetry_enabled"
class="pt-4 border-t border-gray-100 dark:border-zinc-800 space-y-4"
>
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.telemetry_trusted_peers") }}
</div>
<div v-if="trustedTelemetryPeers.length === 0" class="text-xs text-gray-500 italic">
{{ $t("app.telemetry_no_trusted_peers") }}
</div>
<div v-else class="space-y-2">
<div
v-for="peer in trustedTelemetryPeers"
:key="peer.id"
class="flex items-center justify-between p-2 rounded-xl bg-gray-50 dark:bg-zinc-800 border border-gray-100 dark:border-zinc-700"
>
<div class="flex items-center gap-3">
<div
class="size-8 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-500 flex items-center justify-center"
>
<MaterialDesignIcon icon-name="account" class="size-5" />
</div>
<div class="min-w-0">
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
{{ peer.name }}
</div>
<div class="text-[10px] text-gray-500 font-mono truncate">
{{ peer.remote_identity_hash }}
</div>
</div>
</div>
<button
class="p-2 text-gray-400 hover:text-red-500 transition-colors"
:title="$t('app.telemetry_revoke_trust')"
@click="revokeTelemetryTrust(peer)"
>
<MaterialDesignIcon icon-name="shield-off-outline" class="size-5" />
</button>
</div>
</div>
</div>
</div>
</section>
@@ -1490,7 +1457,13 @@
'Network Security',
'app.blackhole_integration_enabled',
'app.blackhole_integration_description',
'app.announce_limits'
'app.announce_limits',
'app.announce_store_heading',
'app.announce_store_lxmf',
'app.announce_store_lxst',
'app.announce_store_nomad',
'app.announce_store_prop',
'app.announce_store_git'
)
"
class="settings-section break-inside-avoid"
@@ -1531,6 +1504,79 @@
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("app.announce_limits_description") }}
</div>
<div class="text-xs font-medium text-gray-800 dark:text-gray-200">
{{ $t("app.announce_store_heading") }}
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("app.announce_store_description") }}
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-2">
<label class="setting-toggle">
<Toggle
:model-value="config.announce_store_lxmf_delivery"
@update:model-value="
(v) => onAnnounceStoreToggle('announce_store_lxmf_delivery', v)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{
$t("app.announce_store_lxmf")
}}</span>
</span>
</label>
<label class="setting-toggle">
<Toggle
:model-value="config.announce_store_lxst_telephony"
@update:model-value="
(v) => onAnnounceStoreToggle('announce_store_lxst_telephony', v)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{
$t("app.announce_store_lxst")
}}</span>
</span>
</label>
<label class="setting-toggle">
<Toggle
:model-value="config.announce_store_nomadnetwork_node"
@update:model-value="
(v) => onAnnounceStoreToggle('announce_store_nomadnetwork_node', v)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{
$t("app.announce_store_nomad")
}}</span>
</span>
</label>
<label class="setting-toggle">
<Toggle
:model-value="config.announce_store_lxmf_propagation"
@update:model-value="
(v) => onAnnounceStoreToggle('announce_store_lxmf_propagation', v)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{
$t("app.announce_store_prop")
}}</span>
</span>
</label>
<label class="setting-toggle sm:col-span-2">
<Toggle
:model-value="config.announce_store_git_repositories"
@update:model-value="
(v) => onAnnounceStoreToggle('announce_store_git_repositories', v)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{
$t("app.announce_store_git")
}}</span>
</span>
</label>
</div>
<div
class="text-xs font-semibold text-gray-700 dark:text-zinc-300 uppercase tracking-wide"
>
@@ -1705,7 +1751,7 @@
<!-- Blocked -->
<section
v-show="matchesSearch('Privacy', 'Banished', 'Manage Banished users and nodes')"
v-show="matchesSearch(...sectionKeywords.blocked)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
@@ -1724,6 +1770,138 @@
</div>
</section>
<SettingsSectionBlock
v-show="matchesSearch(...sectionKeywords.privacyData)"
:eyebrow="$t('app.privacy_eyebrow')"
:title="$t('app.privacy_data_title')"
:description="$t('app.privacy_data_description')"
body-class="space-y-4"
>
<div class="space-y-3">
<div
class="text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-zinc-400"
>
{{ $t("app.privacy_subsection_device") }}
</div>
<label class="setting-toggle">
<Toggle
id="local-message-auto-delete"
v-model="config.local_message_auto_delete_enabled"
@update:model-value="onLocalMessageAutoDeleteEnabledChange"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{
$t("app.local_message_auto_delete_title")
}}</span>
<span class="setting-toggle__description">{{
$t("app.local_message_auto_delete_description")
}}</span>
</span>
</label>
<div
v-if="config.local_message_auto_delete_enabled"
class="grid grid-cols-1 sm:grid-cols-2 gap-3 pl-0 sm:pl-1"
>
<div class="space-y-1">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.local_message_auto_delete_age") }}
</div>
<div class="flex flex-wrap items-center gap-2">
<input
v-model.number="config.local_message_auto_delete_value"
type="number"
min="1"
:max="config.local_message_auto_delete_unit === 'months' ? 120 : 10000"
class="input-field w-24"
:aria-label="$t('app.local_message_auto_delete_age')"
@input="onLocalMessageAutoDeleteParamsChange"
/>
<select
v-model="config.local_message_auto_delete_unit"
class="input-field min-w-[7rem]"
:aria-label="$t('app.local_message_auto_delete_unit_aria')"
@change="onLocalMessageAutoDeleteParamsChange"
>
<option value="days">
{{ $t("app.local_message_auto_delete_unit_days") }}
</option>
<option value="months">
{{ $t("app.local_message_auto_delete_unit_months") }}
</option>
</select>
</div>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("app.local_message_auto_delete_month_note") }}
</div>
</div>
</div>
</div>
<div class="border-t border-gray-200 dark:border-zinc-800 pt-4 space-y-4">
<div
class="text-[11px] font-semibold uppercase tracking-wider text-gray-500 dark:text-zinc-400"
>
{{ $t("app.privacy_subsection_telemetry") }}
</div>
<label class="setting-toggle">
<Toggle
id="telemetry-enabled"
v-model="config.telemetry_enabled"
@update:model-value="
updateConfig(
{ telemetry_enabled: config.telemetry_enabled },
'telemetry_enabled'
)
"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{ $t("app.telemetry_enabled") }}</span>
<span class="setting-toggle__description">{{
$t("app.telemetry_description")
}}</span>
</span>
</label>
<div v-if="config.telemetry_enabled" class="space-y-4">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.telemetry_trusted_peers") }}
</div>
<div v-if="trustedTelemetryPeers.length === 0" class="text-xs text-gray-500 italic">
{{ $t("app.telemetry_no_trusted_peers") }}
</div>
<div v-else class="space-y-2">
<div
v-for="peer in trustedTelemetryPeers"
:key="peer.id"
class="flex items-center justify-between p-2 rounded-xl bg-gray-50 dark:bg-zinc-800 border border-gray-100 dark:border-zinc-700"
>
<div class="flex items-center gap-3">
<div
class="size-8 rounded-full bg-blue-50 dark:bg-blue-900/20 text-blue-500 flex items-center justify-center"
>
<MaterialDesignIcon icon-name="account" class="size-5" />
</div>
<div class="min-w-0">
<div class="text-sm font-bold text-gray-900 dark:text-white truncate">
{{ peer.name }}
</div>
<div class="text-[10px] text-gray-500 font-mono truncate">
{{ peer.remote_identity_hash }}
</div>
</div>
</div>
<button
class="p-2 text-gray-400 hover:text-red-500 transition-colors"
:title="$t('app.telemetry_revoke_trust')"
@click="revokeTelemetryTrust(peer)"
>
<MaterialDesignIcon icon-name="shield-off-outline" class="size-5" />
</button>
</div>
</div>
</div>
</div>
</SettingsSectionBlock>
<!-- Authentication -->
<section
v-show="matchesSearch(...sectionKeywords.auth)"
@@ -1759,51 +1937,6 @@
</div>
</section>
<!-- Translator -->
<section
v-show="matchesSearch(...sectionKeywords.translator)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
<div>
<div class="settings-section__eyebrow">i18n</div>
<h2>{{ $t("app.translator") }}</h2>
<p>{{ $t("translator.description") }}</p>
</div>
</header>
<div class="settings-section__body space-y-4">
<label class="setting-toggle">
<Toggle
id="translator-enabled"
v-model="config.translator_enabled"
@update:model-value="onTranslatorEnabledChange"
/>
<span class="setting-toggle__label">
<span class="setting-toggle__title">{{ $t("app.translator_enabled") }}</span>
<span class="setting-toggle__description">{{
$t("app.translator_description")
}}</span>
</span>
</label>
<div v-if="config.translator_enabled" class="space-y-2">
<div class="text-sm font-medium text-gray-900 dark:text-gray-100">
{{ $t("app.libretranslate_url") }}
</div>
<input
v-model="config.libretranslate_url"
type="text"
placeholder="http://localhost:5000"
class="input-field"
@input="onTranslatorConfigChange"
/>
<div class="text-xs text-gray-600 dark:text-gray-400">
{{ $t("app.libretranslate_url_description") }}
</div>
</div>
</div>
</section>
<!-- Sources & Infrastructure -->
<section
v-show="matchesSearch(...sectionKeywords.infrastructure)"
@@ -1925,14 +2058,14 @@
</div>
</section>
<!-- Messages -->
<!-- Messages (LXMF delivery, retries, inbound stamps) -->
<section
v-show="matchesSearch(...sectionKeywords.messages)"
class="settings-section break-inside-avoid"
>
<header class="settings-section__header">
<div>
<div class="settings-section__eyebrow">{{ $t("app.reliability") }}</div>
<div class="settings-section__eyebrow">{{ $t("app.lxmf_settings_eyebrow") }}</div>
<h2>{{ $t("app.messages") }}</h2>
<p>{{ $t("app.messages_description") }}</p>
</div>
@@ -2150,12 +2283,12 @@
type="number"
min="0.001"
step="any"
class="input-field max-w-[10rem]"
class="input-field max-w-40"
@input="onLxmfIncomingDeliveryCustomChange"
/>
<select
v-model="lxmfIncomingDeliveryCustomUnit"
class="input-field max-w-[8rem]"
class="input-field max-w-32"
@change="onLxmfIncomingDeliveryCustomChange"
>
<option value="mb">{{ $t("app.incoming_message_size_unit_mb") }}</option>
@@ -2337,6 +2470,8 @@ import {
incomingDeliveryBytesFromPresetKey,
syncIncomingDeliveryFieldsFromBytes,
} from "../../js/settings/incomingDeliveryLimit";
import { normalizeRetentionValue } from "../../js/localMessageRetention";
import { matchesSettingSearch, normalizeSearchString } from "../../js/settingsSearchUtils";
export default {
name: "SettingsPage",
@@ -2379,6 +2514,11 @@ export default {
banished_text: "BANISHED",
banished_color: "#dc2626",
blackhole_integration_enabled: true,
announce_store_lxmf_delivery: true,
announce_store_lxst_telephony: true,
announce_store_nomadnetwork_node: true,
announce_store_lxmf_propagation: true,
announce_store_git_repositories: true,
announce_max_stored_lxmf_delivery: 1000,
announce_max_stored_nomadnetwork_node: 1000,
announce_max_stored_lxmf_propagation: 1000,
@@ -2413,6 +2553,9 @@ export default {
nomad_render_html_enabled: true,
nomad_render_plaintext_enabled: true,
nomad_default_page_path: "/page/index.mu",
local_message_auto_delete_enabled: false,
local_message_auto_delete_value: 30,
local_message_auto_delete_unit: "days",
},
saveTimeouts: {},
lxmfIncomingDeliveryPreset: "10mb",
@@ -2433,6 +2576,27 @@ export default {
visualiserShowDisabledInterfaces: false,
visualiserShowDiscoveredInterfaces: false,
sectionKeywords: {
strangerProtection: [
"Security",
"app.stranger_protection",
"app.stranger_protection_description",
"app.block_stranger_attachments",
"app.block_stranger_attachments_description",
"app.block_all_from_strangers",
"app.block_all_from_strangers_description",
"app.show_unknown_contact_banner",
"app.show_unknown_contact_banner_description",
"app.warn_on_stranger_links",
"app.warn_on_stranger_links_description",
"stranger",
"attachments",
"trust",
"block",
"banner",
"unknown",
"contact",
"links",
],
visualiser: [
"Visualiser",
"Network Visualiser",
@@ -2578,6 +2742,12 @@ export default {
"app.blackhole_integration_enabled",
"app.blackhole_integration_description",
"app.announce_limits",
"app.announce_store_heading",
"app.announce_store_lxmf",
"app.announce_store_lxst",
"app.announce_store_nomad",
"app.announce_store_prop",
"app.announce_store_git",
"app.announce_limit_lxmf",
"app.announce_limit_nomadnet",
"app.announce_limit_prop",
@@ -2601,18 +2771,9 @@ export default {
],
blocked: ["Privacy", "Banished", "Manage Banished users and nodes"],
auth: ["Security", "Authentication", "password", "Protect your instance with a password"],
translator: [
"i18n",
"app.translator",
"translator.description",
"app.translator_enabled",
"app.translator_description",
"app.libretranslate_url",
"app.libretranslate_url_description",
],
infrastructure: ["Infrastructure", "Sources & Mirroring", "gitea", "documentation", "download", "urls"],
messages: [
"reliability",
"app.lxmf_settings_eyebrow",
"app.messages",
"app.messages_description",
"app.auto_resend_title",
@@ -2645,24 +2806,46 @@ export default {
"app.propagation_stamp_description",
],
location: [
"app.location",
"app.location_manage_desc",
"app.location_source",
"Map",
"Location",
"GPS",
"Privacy",
"manual",
"latitude",
"longitude",
"altitude",
"telemetry",
"trusted peers",
],
privacyData: [
"app.privacy_data_title",
"app.privacy_data_description",
"app.local_message_auto_delete_title",
"app.local_message_auto_delete_description",
"app.local_message_auto_delete_age",
"app.telemetry_enabled",
"app.telemetry_description",
"app.telemetry_trusted_peers",
"ephemeral",
"retention",
"Privacy",
],
shortcuts: ["Keyboard Shortcuts", "actions", "workflow"],
},
};
},
computed: {
settingsSearchActive() {
return normalizeSearchString(this.searchQuery).length > 0;
},
settingsSearchDisplay() {
return normalizeSearchString(this.searchQuery) || this.searchQuery;
},
hasSearchResults() {
if (!this.searchQuery) return true;
return Object.values(this.sectionKeywords).some((keywords) => this.matchesSearch(...keywords));
if (!normalizeSearchString(this.searchQuery)) return true;
return Object.values(this.sectionKeywords).some((keywords) =>
matchesSettingSearch(keywords, (k) => this.$t(k), this.searchQuery)
);
},
safeConfig() {
if (!this.config) {
@@ -2747,15 +2930,21 @@ export default {
console.error(e);
}
},
onSettingsSearchInput(e) {
const el = e?.target;
if (!el || el.tagName !== "INPUT") return;
this.searchQuery = el.value;
},
onSettingsSearchCompositionEnd(e) {
const el = e?.target;
if (!el || el.tagName !== "INPUT") return;
this.searchQuery = el.value;
},
clearSettingsSearch() {
this.searchQuery = "";
},
matchesSearch(...texts) {
if (!this.searchQuery) return true;
const query = this.searchQuery.toLowerCase();
return texts.some((text) => {
if (!text) return false;
// If it looks like a translation key, translate it
const content = text.includes(".") ? this.$t(text) : text;
return content.toLowerCase().includes(query);
});
return matchesSettingSearch(texts, (k) => this.$t(k), this.searchQuery);
},
async onWebsocketMessage(message) {
const json = JSON.parse(message.data);
@@ -2896,6 +3085,10 @@ export default {
"announce_limits"
);
},
async onAnnounceStoreToggle(key, value) {
this.config[key] = value;
await this.updateConfig({ [key]: value }, key);
},
async copyValue(value, label) {
if (!value) {
ToastUtils.warning(`Nothing to copy for ${label}`);
@@ -3030,6 +3223,15 @@ export default {
// ignore
}
},
onMessageTimestampGroupingChange(event) {
const checked = event.target.checked;
GlobalState.messageTimestampGroupingEnabled = checked;
try {
localStorage.setItem("meshchatx_message_timestamp_grouping_enabled", checked ? "true" : "false");
} catch {
// ignore
}
},
async onLanguageChange() {
await this.updateConfig(
{
@@ -3312,6 +3514,30 @@ export default {
this.config.warn_on_stranger_links = value;
await this.updateConfig({ warn_on_stranger_links: value }, "stranger_protection");
},
async onLocalMessageAutoDeleteEnabledChange(value) {
this.config.local_message_auto_delete_enabled = value;
await this.updateConfig({ local_message_auto_delete_enabled: value }, "privacy_data");
},
onLocalMessageAutoDeleteParamsChange() {
if (this.saveTimeouts.localMessageAutoDelete) {
clearTimeout(this.saveTimeouts.localMessageAutoDelete);
}
this.saveTimeouts.localMessageAutoDelete = setTimeout(async () => {
const { value: v, unit: u } = normalizeRetentionValue(
this.config.local_message_auto_delete_value,
this.config.local_message_auto_delete_unit
);
this.config.local_message_auto_delete_value = v;
this.config.local_message_auto_delete_unit = u;
await this.updateConfig(
{
local_message_auto_delete_value: v,
local_message_auto_delete_unit: u,
},
"privacy_data"
);
}, 400);
},
async onBanishedEffectEnabledChange(value) {
this.config.banished_effect_enabled = value;
await this.updateConfig(
@@ -3386,26 +3612,6 @@ export default {
this.$router.push({ name: "auth" });
}
},
async onTranslatorEnabledChange(value) {
this.config.translator_enabled = value;
await this.updateConfig(
{
translator_enabled: value,
},
"translator"
);
},
async onTranslatorConfigChange() {
if (this.saveTimeouts.translator) clearTimeout(this.saveTimeouts.translator);
this.saveTimeouts.translator = setTimeout(async () => {
await this.updateConfig(
{
libretranslate_url: this.config.libretranslate_url,
},
"translator"
);
}, 1000);
},
async onGiteaConfigChange() {
if (this.saveTimeouts.gitea) clearTimeout(this.saveTimeouts.gitea);
this.saveTimeouts.gitea = setTimeout(async () => {
@@ -3914,6 +4120,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.settings-section {
@apply w-full border-b border-gray-200/60 dark:border-zinc-800/60 py-6 sm:py-8 flex flex-col break-inside-avoid;
}
@@ -3969,13 +4176,13 @@ export default {
font-family: "Roboto Mono", monospace;
}
.address-card {
@apply relative border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/[0.02] dark:sm:bg-white/[0.02] space-y-2;
@apply relative border border-gray-200/70 dark:border-zinc-800/80 py-3 px-3 sm:rounded-xl sm:bg-black/2 dark:sm:bg-white/2 space-y-2;
}
.address-card__label {
@apply text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400;
}
.address-card__value {
@apply text-sm text-gray-900 dark:text-white break-words pr-16;
@apply text-sm text-gray-900 dark:text-white wrap-break-word pr-16;
}
.address-card__action {
@apply absolute top-3 right-3 inline-flex items-center gap-1 rounded-full border border-gray-200 dark:border-zinc-700 px-3 py-1 text-xs font-semibold text-gray-700 dark:text-gray-100 bg-white/70 dark:bg-zinc-900/60 hover:border-blue-400 dark:hover:border-blue-500 transition;
@@ -10,7 +10,7 @@
<kbd
v-for="key in keys"
:key="key"
class="px-2 py-1 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs font-bold text-gray-600 dark:text-zinc-300 shadow-sm uppercase"
class="px-2 py-1 bg-gray-100 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-xs font-bold text-gray-600 dark:text-zinc-300 shadow-xs uppercase"
>
{{ formatKey(key) }}
</kbd>
@@ -22,7 +22,7 @@
<button
type="button"
class="px-4 py-2 rounded-xl font-bold transition-all shadow-sm flex items-center gap-2"
class="px-4 py-2 rounded-xl font-bold transition-all shadow-xs flex items-center gap-2"
:class="[
isRecording
? 'bg-blue-600 text-white hover:bg-blue-700'
@@ -77,7 +77,7 @@
:src="stickerImageUrl(s.id)"
:image-type="s.image_type"
size="xs"
class="rounded border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900"
class="rounded-sm border border-gray-200 dark:border-zinc-700 bg-gray-50 dark:bg-zinc-900"
/>
</div>
<div v-else class="text-xs text-gray-500 dark:text-zinc-400 italic mt-1">
@@ -88,7 +88,7 @@
<div
v-if="createOpen"
class="fixed inset-0 z-[150] flex items-center justify-center bg-black/60 p-4"
class="fixed inset-0 z-150 flex items-center justify-center bg-black/60 p-4"
@click.self="createOpen = false"
>
<div
@@ -6,14 +6,25 @@
class="flex-1 overflow-y-auto w-full px-3 sm:px-4 md:px-5 lg:px-8 py-4 sm:py-6 pb-[max(1.5rem,env(safe-area-inset-bottom))]"
>
<div class="space-y-8 w-full max-w-4xl mx-auto">
<div class="space-y-2 border-b border-gray-200 dark:border-zinc-800 pb-6">
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
{{ $t("bots.bot_framework") }}
</div>
<div class="text-2xl font-semibold text-gray-900 dark:text-white">{{ $t("bots.title") }}</div>
<div class="text-sm text-gray-600 dark:text-gray-300">
{{ $t("bots.description") }}
<div
class="flex flex-wrap items-start justify-between gap-3 border-b border-gray-200 dark:border-zinc-800 pb-6"
>
<div class="space-y-2 min-w-0">
<div class="text-xs uppercase tracking-wide text-gray-500 dark:text-gray-400">
{{ $t("bots.bot_framework") }}
</div>
<div class="text-2xl font-semibold text-gray-900 dark:text-white">{{ $t("bots.title") }}</div>
<div class="text-sm text-gray-600 dark:text-gray-300">
{{ $t("bots.description") }}
</div>
</div>
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
<div class="space-y-6">
@@ -159,7 +170,7 @@
<input
v-model="editingNameDraft"
type="text"
class="input-field text-xs py-1 h-8 px-2 min-w-0 flex-1 max-w-[10rem] sm:max-w-[12rem]"
class="input-field text-xs py-1 h-8 px-2 min-w-0 flex-1 max-w-40 sm:max-w-48"
maxlength="256"
@keydown.enter.prevent="saveBotName(bot)"
@keydown.escape="cancelEditName"
@@ -246,7 +257,7 @@
<div
v-if="selectedTemplate"
class="fixed inset-0 z-[100] flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/50"
class="fixed inset-0 z-100 flex items-end sm:items-center justify-center p-0 sm:p-4 bg-black/50"
@click.self="selectedTemplate = null"
>
<div
@@ -539,6 +550,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.glass-label {
@apply block text-sm font-semibold text-gray-700 dark:text-gray-300 mb-1;
}
@@ -5,7 +5,9 @@
<div class="flex-1 overflow-y-auto w-full pb-[max(1rem,env(safe-area-inset-bottom))]">
<div class="p-3 sm:p-4 md:p-6 max-w-5xl mx-auto w-full space-y-4 min-w-0">
<!-- header -->
<div class="border-b border-gray-200 dark:border-zinc-800 pb-4">
<div
class="flex flex-wrap items-start justify-between gap-3 border-b border-gray-200 dark:border-zinc-800 pb-4"
>
<div class="flex items-start gap-3 min-w-0">
<div
class="p-2 bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 rounded-lg shrink-0"
@@ -21,6 +23,13 @@
</p>
</div>
</div>
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4">
@@ -199,7 +208,7 @@
</div>
<button
type="button"
class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-sm"
class="size-9 flex items-center justify-center bg-white dark:bg-zinc-900 text-gray-500 dark:text-zinc-400 rounded-lg border border-gray-200 dark:border-zinc-700 hover:bg-blue-50 hover:text-blue-600 hover:border-blue-200 transition-all shadow-xs"
title="Copy URI"
@click="copyUri"
>
@@ -258,7 +267,7 @@
<div
v-if="isIngestScannerModalOpen"
class="fixed inset-0 z-[210] flex items-center justify-center p-4 bg-black/70 backdrop-blur-sm"
class="fixed inset-0 z-210 flex items-center justify-center p-4 bg-black/70 backdrop-blur-xs"
@click.self="closeIngestScannerModal"
>
<div class="w-full max-w-xl rounded-2xl bg-white dark:bg-zinc-900 shadow-2xl overflow-hidden">
@@ -598,6 +607,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-2xl focus:ring-2 focus:ring-blue-400 focus:border-blue-400 dark:focus:ring-blue-50 dark:focus:border-blue-500 block w-full p-2.5 text-gray-900 dark:text-gray-100 transition;
}
@@ -2,7 +2,7 @@
<template>
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
<div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-sm z-10">
<div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-xs z-10">
<div class="px-4 py-3 md:px-6 md:py-4 flex flex-wrap items-center justify-between gap-3 min-w-0">
<div class="flex items-center gap-3 min-w-0">
<div class="p-2 bg-teal-100 dark:bg-teal-900/30 rounded-xl shrink-0">
@@ -25,7 +25,7 @@
class="inline-flex items-center gap-2 text-sm text-teal-600 dark:text-teal-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("rngit_explorer.back_tools") }}
{{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
</div>
@@ -123,7 +123,7 @@
<input
v-model="forPush"
type="checkbox"
class="rounded border-gray-300 shrink-0 mt-0.5"
class="rounded-sm border-gray-300 shrink-0 mt-0.5"
:disabled="busy"
/>
<span>{{ $t("rngit_explorer.for_push") }}</span>
@@ -204,7 +204,7 @@
</div>
<div class="flex flex-wrap gap-2 items-start">
<pre
class="flex-1 min-w-0 p-2 rounded bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 overflow-x-auto whitespace-pre-wrap break-all"
class="flex-1 min-w-0 p-2 rounded-sm bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 overflow-x-auto whitespace-pre-wrap break-all"
>{{ selected.clone_command }}</pre
>
<button
@@ -227,7 +227,7 @@
}}</span>
</div>
<pre
class="p-2 rounded bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
class="p-2 rounded-sm bg-gray-100 dark:bg-zinc-900 text-xs font-mono text-gray-800 dark:text-zinc-200 max-h-48 overflow-y-auto whitespace-pre-wrap break-all"
>{{ selected.refs_preview }}</pre
>
</div>
@@ -4,7 +4,7 @@
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
<!-- header -->
<div
class="flex flex-wrap items-center gap-2 px-3 sm:px-4 md:px-6 py-3 sm:py-4 bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800"
class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 md:px-6 py-3 sm:py-4 bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800"
>
<div class="flex items-center gap-2 sm:gap-3 min-w-0 flex-1">
<div class="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-lg shrink-0">
@@ -21,7 +21,14 @@
</div>
</div>
<div class="ml-auto flex items-center gap-2 shrink-0">
<div class="flex items-center gap-2 shrink-0 w-full sm:w-auto justify-end">
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-indigo-600 dark:text-indigo-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
<button
class="p-2 text-gray-500 hover:text-indigo-500 dark:text-gray-400 dark:hover:text-indigo-400 transition-colors"
title="Refresh"
@@ -143,12 +150,12 @@
{{ path.hash }}
</span>
<span
class="px-2 py-0.5 text-[10px] font-bold bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded uppercase tracking-wider"
class="px-2 py-0.5 text-[10px] font-bold bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300 rounded-sm uppercase tracking-wider"
>
{{ path.hops }} {{ path.hops === 1 ? "hop" : "hops" }}
</span>
<span
class="px-2 py-0.5 text-[10px] font-bold rounded uppercase tracking-wider"
class="px-2 py-0.5 text-[10px] font-bold rounded-sm uppercase tracking-wider"
:class="getStateColor(path.state)"
>
{{ getStateText(path.state) }}
@@ -236,7 +243,7 @@
</span>
<span
v-if="rate.blocked_until > Date.now() / 1000"
class="px-2 py-0.5 text-[10px] font-bold bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded"
class="px-2 py-0.5 text-[10px] font-bold bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300 rounded-sm"
>
RATE LIMITED
</span>
@@ -538,6 +545,7 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.input-field {
@apply bg-gray-50/90 dark:bg-zinc-800/80 border border-gray-200 dark:border-zinc-700 text-sm rounded-xl focus:ring-2 focus:ring-indigo-400 focus:border-indigo-400 dark:focus:ring-indigo-500 dark:focus:border-indigo-500 block w-full p-3 text-gray-900 dark:text-gray-100 transition;
}
@@ -3,9 +3,9 @@
<template>
<div class="flex flex-col flex-1 h-full min-w-0 overflow-hidden bg-slate-50 dark:bg-zinc-950">
<!-- header -->
<div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-sm z-10">
<div class="bg-slate-50 dark:bg-zinc-950 border-b border-gray-200 dark:border-zinc-800 shadow-xs z-10">
<div class="px-4 py-3 md:px-6 md:py-4 flex flex-wrap items-center justify-between gap-3 md:gap-4 min-w-0">
<div class="flex items-center gap-3">
<div class="flex items-center gap-3 min-w-0">
<div class="p-2 bg-indigo-100 dark:bg-indigo-900/30 rounded-xl shrink-0">
<MaterialDesignIcon
icon-name="map-marker-path"
@@ -22,7 +22,14 @@
</div>
</div>
<div class="flex items-center gap-2">
<div class="flex items-center gap-2 shrink-0">
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-indigo-600 dark:text-indigo-300 hover:underline"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
<button
v-if="traceResult"
class="p-2 text-gray-400 hover:text-indigo-500 dark:hover:text-indigo-400 transition-colors shrink-0"
@@ -49,7 +56,7 @@
v-model="destinationHash"
type="text"
placeholder="input destination hash"
class="w-full pl-4 pr-12 py-3 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-sm md:text-base font-mono focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition-all dark:text-white"
class="w-full pl-4 pr-12 py-3 bg-gray-50 dark:bg-zinc-800 border border-gray-200 dark:border-zinc-700 rounded-lg text-sm md:text-base font-mono focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-hidden transition-all dark:text-white"
@keyup.enter="runTrace"
/>
<div
@@ -60,7 +67,7 @@
</div>
<button
type="button"
class="w-full sm:w-auto sm:min-w-[3rem] h-12 sm:h-14 px-4 sm:px-0 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg flex items-center justify-center gap-2 transition active:scale-95 disabled:opacity-50 shrink-0"
class="w-full sm:w-auto sm:min-w-12 h-12 sm:h-14 px-4 sm:px-0 bg-indigo-600 hover:bg-indigo-500 text-white rounded-lg flex items-center justify-center gap-2 transition active:scale-95 disabled:opacity-50 shrink-0"
:disabled="!isValidHash || isLoading"
title="Trace Path"
@click="runTrace"
@@ -211,7 +218,7 @@
v-if="
traceResult.path[idx + 1].type !== 'unknown' && node.type !== 'unknown'
"
class="absolute right-0 -top-1 w-2 h-2 rounded-full bg-indigo-500 shadow-sm shadow-indigo-500/50"
class="absolute right-0 -top-1 w-2 h-2 rounded-full bg-indigo-500 shadow-xs shadow-indigo-500/50"
></div>
</div>
</template>
@@ -259,7 +266,7 @@
</div>
<div
v-if="node.interface"
class="inline-flex items-center gap-1 mt-1.5 px-2 py-0.5 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 rounded text-[9px] font-bold uppercase tracking-wider"
class="inline-flex items-center gap-1 mt-1.5 px-2 py-0.5 bg-indigo-50 dark:bg-indigo-900/20 text-indigo-600 dark:text-indigo-400 rounded-sm text-[9px] font-bold uppercase tracking-wider"
>
<MaterialDesignIcon icon-name="router-wireless" class="size-3" />
{{ node.interface }}
@@ -21,6 +21,13 @@
</div>
<div class="ml-auto flex flex-wrap items-center justify-end gap-1 sm:gap-2 shrink-0">
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-violet-600 dark:text-violet-300 hover:underline"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
<button
class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors flex items-center gap-2 text-sm font-medium"
@click="showAdvanced = !showAdvanced"
@@ -39,12 +46,6 @@
<MaterialDesignIcon icon-name="open-in-new" class="size-5" />
<span class="hidden sm:inline">{{ $t("tools.rnode_flasher.original") }}</span>
</a>
<button
class="p-2 text-gray-500 hover:bg-gray-100 dark:hover:bg-zinc-800 rounded-lg transition-colors"
@click="$router.push({ name: 'tools' })"
>
<MaterialDesignIcon icon-name="close" class="size-5" />
</button>
</div>
</div>
@@ -150,15 +151,15 @@
<div class="flex items-center gap-4">
<a
target="_blank"
:href="`${giteaBaseUrl}/Reticulum/rnode-flasher`"
href="https://github.com/liamcottle/rnode-flasher"
class="text-blue-500 hover:underline text-sm font-bold"
>RNode Flasher GH</a
>RNode Flasher</a
>
<a
target="_blank"
:href="`${giteaBaseUrl}/Reticulum/RNode_Firmware`"
href="https://github.com/markqvist/RNode_Firmware"
class="text-blue-500 hover:underline text-sm font-bold"
>RNode Firmware GH</a
>RNode Firmware</a
>
</div>
</div>
@@ -198,7 +199,6 @@ import WifiTransport from "../../js/rnode/transports/WifiTransport.js";
import { diagnose } from "../../js/rnode/Diagnostics.js";
import ToastUtils from "../../js/ToastUtils.js";
import GlobalState from "../../js/GlobalState";
export default {
name: "RNodeFlasherPage",
@@ -249,9 +249,6 @@ export default {
recommendedFirmwareFilename() {
return this.selectedModel?.firmware_filename ?? this.selectedProduct?.firmware_filename;
},
giteaBaseUrl() {
return GlobalState.config?.gitea_base_url || "https://git.quad4.io";
},
canFlash() {
if (this.connectionMethod === TRANSPORT_WIFI) {
return Boolean(this.wifiHost && this.firmwareFile);
@@ -299,7 +296,7 @@ export default {
},
async fetchLatestRelease() {
try {
const response = await fetch("/api/v1/tools/rnode/latest_release?repo=Reticulum/RNode_Firmware");
const response = await fetch("/api/v1/tools/rnode/latest_release");
if (response.ok) {
this.latestRelease = await response.json();
}
@@ -314,8 +311,7 @@ export default {
if (asset?.browser_download_url) {
return asset.browser_download_url;
}
const base = (this.giteaBaseUrl || "https://git.quad4.io").replace(/\/$/, "");
return `${base}/Reticulum/RNode_Firmware/releases/latest/download/${filename}`;
return `https://github.com/markqvist/RNode_Firmware/releases/latest/download/${filename}`;
},
async downloadRecommendedFirmware() {
const assetUrl = this._resolveRecommendedAssetUrl();
@@ -40,7 +40,7 @@
class="inline-flex items-center gap-2 text-sm text-sky-600 dark:text-sky-300 hover:underline"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.repository_server.back_tools") }}
{{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
</div>
@@ -67,7 +67,7 @@
{{ $t("tools.repository_server.http_heading") }}
</h2>
<div class="flex flex-wrap gap-3 items-end">
<label class="flex flex-col gap-1 text-xs min-w-[10rem]">
<label class="flex flex-col gap-1 text-xs min-w-40">
<span class="text-gray-500 dark:text-zinc-500">{{
$t("tools.repository_server.host_label")
}}</span>
@@ -3,7 +3,7 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden min-w-0 bg-slate-50 dark:bg-zinc-950">
<div
class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-sm shrink-0 min-w-0"
class="flex flex-wrap items-center justify-between gap-2 px-3 sm:px-4 py-2 border-b border-gray-200 dark:border-zinc-800 bg-slate-50/95 dark:bg-zinc-950/95 backdrop-blur-xs shrink-0 min-w-0"
>
<div class="flex items-center gap-2 sm:gap-3 min-w-0">
<div class="bg-blue-100 dark:bg-blue-900/30 p-1.5 rounded-xl shrink-0">
@@ -25,13 +25,20 @@
</div>
</div>
<div class="flex items-center gap-2 flex-wrap">
<button type="button" class="secondary-chip !py-1 !px-3" :disabled="loading" @click="loadConfig">
<RouterLink
to="/tools"
class="inline-flex items-center gap-2 text-sm text-blue-600 dark:text-blue-300 hover:underline"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.back_to_tools") }}
</RouterLink>
<button type="button" class="secondary-chip py-1! px-3!" :disabled="loading" @click="loadConfig">
<MaterialDesignIcon icon-name="refresh" class="w-3.5 h-3.5" />
<span class="hidden sm:inline">{{ $t("tools.reticulum_config_editor.reload") }}</span>
</button>
<button
type="button"
class="secondary-chip !py-1 !px-3 !text-red-500 hover:!bg-red-50 dark:hover:!bg-red-900/20"
class="secondary-chip py-1! px-3! text-red-500! hover:bg-red-50! dark:hover:bg-red-900/20!"
:disabled="loading || resetting"
@click="restoreDefaults"
>
@@ -40,7 +47,7 @@
</button>
<button
type="button"
class="secondary-chip !py-1 !px-3"
class="secondary-chip py-1! px-3!"
:disabled="!isDirty || saving"
@click="discardChanges"
>
@@ -49,7 +56,7 @@
</button>
<button
type="button"
class="primary-chip !py-1 !px-3"
class="primary-chip py-1! px-3!"
:disabled="!isDirty || saving"
@click="saveConfig"
>
@@ -82,7 +89,7 @@
</div>
<button
type="button"
class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-sm disabled:opacity-50"
class="ml-auto inline-flex items-center gap-2 rounded-full bg-white px-4 py-1.5 text-sm font-bold text-amber-600 hover:bg-white/90 transition shadow-xs disabled:opacity-50"
:disabled="reloadingRns"
:class="reloadingRns ? '' : 'animate-pulse motion-reduce:animate-none'"
@click="reloadRns"
@@ -114,7 +121,7 @@
autocomplete="off"
autocorrect="off"
:placeholder="loading ? $t('tools.reticulum_config_editor.loading') : ''"
class="w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-xs sm:text-sm resize-none focus:outline-none min-h-[420px] sm:min-h-[60vh]"
class="w-full bg-white dark:bg-zinc-900 text-gray-900 dark:text-white p-4 font-mono text-xs sm:text-sm resize-none focus:outline-hidden min-h-[420px] sm:min-h-[60vh]"
@keydown.tab.prevent="insertTab"
></textarea>
</div>
@@ -30,7 +30,7 @@
class="inline-flex items-center gap-2 text-sm text-violet-600 dark:text-violet-300 hover:underline shrink-0"
>
<MaterialDesignIcon icon-name="arrow-left" class="size-4" />
{{ $t("tools.sieve_filters.back_tools") }}
{{ $t("tools.back_to_tools") }}
</RouterLink>
</div>
@@ -76,7 +76,7 @@
<input
v-model="rule.enabled"
type="checkbox"
class="rounded border-gray-300"
class="rounded-sm border-gray-300"
/>
{{ $t("tools.sieve_filters.enabled") }}
</label>
@@ -152,7 +152,7 @@
<input
v-model="rule.match_peer_fields"
type="checkbox"
class="rounded border-gray-300"
class="rounded-sm border-gray-300"
@change="onMatchTargetsChange(rule)"
/>
{{ $t("tools.sieve_filters.match_peer_fields") }}
@@ -161,7 +161,7 @@
<input
v-model="rule.match_message"
type="checkbox"
class="rounded border-gray-300"
class="rounded-sm border-gray-300"
@change="onMatchTargetsChange(rule)"
/>
{{ $t("tools.sieve_filters.match_message") }}
@@ -26,7 +26,7 @@
v-model="searchQuery"
type="text"
:placeholder="$t('common.search')"
class="w-full pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-sm"
class="w-full pl-10 pr-10 py-3 bg-gray-50 dark:bg-zinc-900 border border-gray-200 dark:border-zinc-800 rounded-lg focus:outline-hidden focus:ring-2 focus:ring-blue-500/40 focus:border-blue-500 text-gray-900 dark:text-white placeholder-gray-400 dark:placeholder-gray-500 text-sm"
/>
<button
v-if="searchQuery"
@@ -41,7 +41,7 @@
</div>
</div>
<div class="p-4 md:p-6 xl:p-8 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-[96rem] mx-auto">
<div class="p-4 md:p-6 xl:p-8 w-full max-w-6xl xl:max-w-7xl 2xl:max-w-384 mx-auto">
<div
class="rounded-lg overflow-hidden border border-gray-200 dark:border-zinc-800 bg-white dark:bg-zinc-950"
>
@@ -75,7 +75,7 @@
<div class="tool-card__title">{{ tool.title }}</div>
<span
v-if="tool.comingSoon"
class="px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gray-100 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 rounded border border-gray-200 dark:border-zinc-700"
class="px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider bg-gray-100 dark:bg-zinc-800 text-gray-500 dark:text-gray-400 rounded-sm border border-gray-200 dark:border-zinc-700"
>
Soon
</span>
@@ -340,8 +340,9 @@ export default {
</script>
<style scoped>
@reference "../../style.css";
.tool-row {
@apply flex items-start sm:items-center gap-3 sm:gap-4 px-4 py-3.5 min-h-[4.25rem] transition-colors;
@apply flex items-start sm:items-center gap-3 sm:gap-4 px-4 py-3.5 min-h-17 transition-colors;
@apply hover:bg-gray-50 dark:hover:bg-zinc-900/80 active:bg-gray-100 dark:active:bg-zinc-800/80;
}
.tool-card__icon {
@@ -17,33 +17,56 @@
</div>
</div>
<div
v-if="config && !config.translator_enabled"
class="p-4 rounded-xl bg-amber-50 dark:bg-amber-900/10 border border-amber-200/50 dark:border-amber-800/30"
>
<div class="flex items-start gap-3">
<div class="p-2 bg-amber-100 dark:bg-amber-900/30 rounded-lg">
<MaterialDesignIcon
icon-name="alert-outline"
class="size-5 text-amber-600 dark:text-amber-400"
/>
</div>
<div class="flex-1 text-sm text-amber-800 dark:text-amber-200">
<p class="font-bold mb-1">Translator is Disabled</p>
<p class="opacity-90">
The translation service is currently disabled in your settings. You can enable it
under
<RouterLink :to="{ name: 'settings' }" class="font-bold underline"
>Settings</RouterLink
>.
</p>
</div>
</div>
<div v-if="config" class="space-y-3">
<div class="text-sm font-semibold text-gray-800 dark:text-gray-200">Translation backends</div>
<label
v-if="hasArgos"
class="flex items-start gap-3 cursor-pointer p-2 rounded-lg hover:bg-slate-100/80 dark:hover:bg-zinc-900/40"
>
<Toggle
:model-value="config.translator_argos_enabled"
@update:model-value="onArgosEnabledChange"
/>
<span>
<span class="block text-sm font-medium text-gray-900 dark:text-white"
>Argos Translate (local)</span
>
<span class="text-xs text-gray-500 dark:text-gray-400"
>Local packages when Argos is installed. Load languages to refresh this list.</span
>
</span>
</label>
<label
v-if="libreClientAvailable"
class="flex items-start gap-3 cursor-pointer p-2 rounded-lg hover:bg-slate-100/80 dark:hover:bg-zinc-900/40"
>
<Toggle
:model-value="config.translator_libretranslate_enabled"
@update:model-value="onLibreEnabledChange"
/>
<span>
<span class="block text-sm font-medium text-gray-900 dark:text-white"
>LibreTranslate (HTTP)</span
>
<span class="text-xs text-gray-500 dark:text-gray-400"
>Set the base URL below, then enable. Use Refresh languages after the server is
up.</span
>
</span>
</label>
<p
v-if="libreClientAvailable && !libretranslateReachable"
class="text-xs text-amber-800/90 dark:text-amber-200/80 px-2 -mt-1"
>
No response from the LibreTranslate URL yet. Check the address, start the service, and tap
Refresh languages.
</p>
</div>
<div class="border-b border-gray-200 dark:border-zinc-700">
<div class="flex -mb-px">
<div v-if="hasArgos || libreClientAvailable" class="flex -mb-px">
<button
v-if="hasArgos"
type="button"
class="px-4 py-2 text-sm font-semibold border-b-2 transition-colors"
:class="
@@ -56,6 +79,7 @@
Argos Translate
</button>
<button
v-if="libreClientAvailable"
type="button"
class="px-4 py-2 text-sm font-semibold border-b-2 transition-colors"
:class="
@@ -139,7 +163,7 @@
</button>
</div>
<div
class="bg-amber-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all"
class="bg-amber-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all"
>
pip install argostranslate
</div>
@@ -158,7 +182,7 @@
</button>
</div>
<div
class="bg-amber-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all"
class="bg-amber-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all"
>
pipx install argostranslate
</div>
@@ -220,7 +244,7 @@
Install All
</button>
<div
class="bg-blue-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all flex-1"
class="bg-blue-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all flex-1"
>
argospm install translate
</div>
@@ -240,7 +264,7 @@
</button>
</div>
<div
class="bg-blue-100/50 dark:bg-black/30 p-2 rounded font-mono text-xs break-all"
class="bg-blue-100/50 dark:bg-black/30 p-2 rounded-sm font-mono text-xs break-all"
>
argospm install translate-en_de
</div>
@@ -330,7 +354,7 @@
<span
v-for="lang in filteredLanguages"
:key="lang.code"
class="px-2 py-1 rounded text-xs bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300"
class="px-2 py-1 rounded-sm text-xs bg-gray-100 dark:bg-zinc-800 text-gray-700 dark:text-gray-300"
>
{{ lang.name }} ({{ lang.code }})
<span class="text-gray-500 dark:text-gray-500">- {{ lang.source }}</span>
@@ -366,11 +390,13 @@
import DialogUtils from "../../js/DialogUtils";
import ToastUtils from "../../js/ToastUtils";
import MaterialDesignIcon from "../MaterialDesignIcon.vue";
import Toggle from "../forms/Toggle.vue";
export default {
name: "TranslatorPage",
components: {
MaterialDesignIcon,
Toggle,
},
data() {
return {
@@ -381,7 +407,10 @@ export default {
inputText: "",
translationMode: "argos",
libretranslateUrl: "http://localhost:5000",
hasArgos: true,
hasArgos: false,
libreClientAvailable: false,
libretranslateReachable: false,
saveLibreUrlTimer: null,
isTranslating: false,
isInstallingLanguages: false,
translationResult: null,
@@ -390,8 +419,12 @@ export default {
},
computed: {
canTranslate() {
const a = this.config?.translator_argos_enabled;
const l = this.config?.translator_libretranslate_enabled;
const argosOk = this.translationMode === "argos" && a;
const libreOk = this.translationMode === "libretranslate" && l;
return (
this.config?.translator_enabled &&
(argosOk || libreOk) &&
this.inputText.trim().length > 0 &&
this.targetLang &&
this.targetLang !== this.sourceLang
@@ -421,6 +454,12 @@ export default {
this.loadLanguages();
},
libretranslateUrl() {
if (this.saveLibreUrlTimer) {
clearTimeout(this.saveLibreUrlTimer);
}
this.saveLibreUrlTimer = setTimeout(() => {
this.persistLibreUrl();
}, 800);
if (this.translationMode === "libretranslate") {
this.loadLanguages();
}
@@ -434,15 +473,67 @@ export default {
try {
const response = await window.api.get("/api/v1/config");
this.config = response.data.config;
if (this.config.translator_enabled) {
this.loadLanguages();
if (this.config?.libretranslate_url) {
this.libretranslateUrl = this.config.libretranslate_url;
}
this.loadLanguages();
} catch (e) {
console.log(e);
}
},
syncTranslationModeFromBackends() {
const canArgos = this.hasArgos;
const canLibre = this.libreClientAvailable;
if (this.translationMode === "argos" && !canArgos && canLibre) {
this.translationMode = "libretranslate";
this.sourceLang = "auto";
} else if (this.translationMode === "libretranslate" && !canLibre && canArgos) {
this.translationMode = "argos";
if (this.sourceLang === "auto") {
this.sourceLang = "";
}
} else if (canArgos && !canLibre) {
this.translationMode = "argos";
} else if (!canArgos && canLibre) {
this.translationMode = "libretranslate";
if (!this.sourceLang) {
this.sourceLang = "auto";
}
}
},
async onArgosEnabledChange(value) {
if (this.config) {
this.config.translator_argos_enabled = value;
}
try {
await window.api.patch("/api/v1/config", { translator_argos_enabled: value });
} catch (e) {
console.error(e);
}
},
async onLibreEnabledChange(value) {
if (this.config) {
this.config.translator_libretranslate_enabled = value;
}
try {
await window.api.patch("/api/v1/config", { translator_libretranslate_enabled: value });
} catch (e) {
console.error(e);
}
},
async persistLibreUrl() {
if (!this.config || this.libretranslateUrl === (this.config.libretranslate_url || "")) {
return;
}
try {
await window.api.patch("/api/v1/config", { libretranslate_url: this.libretranslateUrl });
this.config.libretranslate_url = this.libretranslateUrl;
} catch (e) {
console.error(e);
}
},
async loadLanguages() {
if (this.config && !this.config.translator_enabled) {
if (!this.config) {
return;
}
try {
@@ -453,6 +544,9 @@ export default {
const response = await window.api.get("/api/v1/translator/languages", { params });
this.languages = response.data.languages || [];
this.hasArgos = response.data.has_argos;
this.libreClientAvailable = Boolean(response.data.libre_client_available);
this.libretranslateReachable = Boolean(response.data.libretranslate_reachable);
this.syncTranslationModeFromBackends();
} catch (e) {
console.error(e);
DialogUtils.alert(this.$t("translator.failed_load_languages"));