refactor(ui): group extra remote addresses behind disclosure

Replace the standalone show/hide button with a full-width collapsible disclosure container in both remote access surfaces. This keeps the hidden address list visually grouped, makes the header easier to scan and click, and centers the label so it reads as structure rather than an unrelated action button.

Keep the first recommended remote address visible by default while preserving access to the full list behind the compact disclosure header.
This commit is contained in:
VooDisss
2026-04-01 17:04:20 +03:00
parent b0b0a55e14
commit 61e06ef883
3 changed files with 187 additions and 118 deletions

View File

@@ -2,7 +2,7 @@ import { Dialog } from "@kobalte/core/dialog"
import { Switch } from "@kobalte/core/switch" import { Switch } from "@kobalte/core/switch"
import { For, Show, createEffect, createMemo, createSignal } from "solid-js" import { For, Show, createEffect, createMemo, createSignal } from "solid-js"
import { toDataURL } from "qrcode" import { toDataURL } from "qrcode"
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid" import { ChevronDown, ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types" import type { NetworkAddress, ServerMeta } from "../../../server/src/api-types"
import { serverApi } from "../lib/api-client" import { serverApi } from "../lib/api-client"
import { restartCli } from "../lib/native/cli" import { restartCli } from "../lib/native/cli"
@@ -431,69 +431,79 @@ export function RemoteAccessOverlay(props: RemoteAccessOverlayProps) {
</Show> </Show>
<Show when={displayAddresses().hidden.length > 0}> <Show when={displayAddresses().hidden.length > 0}>
<div class="remote-actions" style={{ "justify-content": "flex-start" }}> <div class="remote-address-disclosure" data-expanded={showAllAddresses()}>
<button class="remote-pill" type="button" onClick={() => setShowAllAddresses(!showAllAddresses())}> <button
{showAllAddresses() class="remote-address-disclosure-trigger"
? t("remoteAccess.addresses.actions.hideOther") type="button"
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })} onClick={() => setShowAllAddresses(!showAllAddresses())}
aria-expanded={showAllAddresses()}
>
<span class="remote-address-disclosure-label">
{showAllAddresses()
? t("remoteAccess.addresses.actions.hideOther")
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })}
</span>
<ChevronDown class={`remote-address-disclosure-chevron ${showAllAddresses() ? "is-expanded" : ""}`} />
</button> </button>
</div>
</Show>
<Show when={showAllAddresses()}> <Show when={showAllAddresses()}>
<For each={displayAddresses().hidden}> <div class="remote-address-disclosure-content">
{(address) => { <For each={displayAddresses().hidden}>
const url = address.remoteUrl {(address) => {
const expandedState = () => expandedUrl() === url const url = address.remoteUrl
const qr = () => qrCodes()[url] const expandedState = () => expandedUrl() === url
const scopeLabel = () => const qr = () => qrCodes()[url]
address.scope === "external" const scopeLabel = () =>
? t("remoteAccess.address.scope.network") address.scope === "external"
: address.scope === "loopback" ? t("remoteAccess.address.scope.network")
? t("remoteAccess.address.scope.loopback") : address.scope === "loopback"
: t("remoteAccess.address.scope.internal") ? t("remoteAccess.address.scope.loopback")
return ( : t("remoteAccess.address.scope.internal")
<div class="remote-address"> return (
<div class="remote-address-main"> <div class="remote-address">
<div> <div class="remote-address-main">
<p class="remote-address-url">{url}</p> <div>
<p class="remote-address-meta"> <p class="remote-address-url">{url}</p>
{address.family.toUpperCase()} {scopeLabel()} {address.ip} <p class="remote-address-meta">
</p> {address.family.toUpperCase()} {scopeLabel()} {address.ip}
</div> </p>
<div class="remote-actions"> </div>
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}> <div class="remote-actions">
<ExternalLink class="remote-icon" /> <button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
{t("remoteAccess.address.open")} <ExternalLink class="remote-icon" />
</button> {t("remoteAccess.address.open")}
<button </button>
class="remote-pill" <button
type="button" class="remote-pill"
onClick={() => void toggleExpanded(url)} type="button"
aria-expanded={expandedState()} onClick={() => void toggleExpanded(url)}
> aria-expanded={expandedState()}
<Link2 class="remote-icon" /> >
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")} <Link2 class="remote-icon" />
</button> {expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
</div> </button>
</div> </div>
<Show when={expandedState()}> </div>
<div class="remote-qr"> <Show when={expandedState()}>
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}> <div class="remote-qr">
{(dataUrl) => ( <Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
<img {(dataUrl) => (
src={dataUrl()} <img
alt={t("remoteAccess.address.qrAlt", { url })} src={dataUrl()}
class="remote-qr-img" alt={t("remoteAccess.address.qrAlt", { url })}
/> class="remote-qr-img"
)} />
</Show> )}
</div> </Show>
</Show> </div>
</Show>
</div>
)
}}
</For>
</div> </div>
) </Show>
}} </div>
</For>
</Show> </Show>
</div> </div>
</Show> </Show>

View File

@@ -1,7 +1,7 @@
import { Switch } from "@kobalte/core/switch" import { Switch } from "@kobalte/core/switch"
import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js" import { For, Show, createMemo, createSignal, type Component, onMount } from "solid-js"
import { toDataURL } from "qrcode" import { toDataURL } from "qrcode"
import { ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid" import { ChevronDown, ExternalLink, Link2, Loader2, RefreshCw, Shield, Wifi } from "lucide-solid"
import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types" import type { NetworkAddress, ServerMeta } from "../../../../server/src/api-types"
import { serverApi } from "../../lib/api-client" import { serverApi } from "../../lib/api-client"
import { restartCli } from "../../lib/native/cli" import { restartCli } from "../../lib/native/cli"
@@ -401,66 +401,76 @@ export const RemoteAccessSettingsSection: Component = () => {
</Show> </Show>
<Show when={displayAddresses().hidden.length > 0}> <Show when={displayAddresses().hidden.length > 0}>
<div class="remote-actions" style={{ "justify-content": "flex-start" }}> <div class="remote-address-disclosure" data-expanded={showAllAddresses()}>
<button class="remote-pill" type="button" onClick={() => setShowAllAddresses(!showAllAddresses())}> <button
{showAllAddresses() class="remote-address-disclosure-trigger"
? t("remoteAccess.addresses.actions.hideOther") type="button"
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })} onClick={() => setShowAllAddresses(!showAllAddresses())}
aria-expanded={showAllAddresses()}
>
<span class="remote-address-disclosure-label">
{showAllAddresses()
? t("remoteAccess.addresses.actions.hideOther")
: t("remoteAccess.addresses.actions.showOther", { count: String(displayAddresses().hidden.length) })}
</span>
<ChevronDown class={`remote-address-disclosure-chevron ${showAllAddresses() ? "is-expanded" : ""}`} />
</button> </button>
</div>
</Show>
<Show when={showAllAddresses()}> <Show when={showAllAddresses()}>
<For each={displayAddresses().hidden}> <div class="remote-address-disclosure-content">
{(address) => { <For each={displayAddresses().hidden}>
const url = address.remoteUrl {(address) => {
const expandedState = () => expandedUrl() === url const url = address.remoteUrl
const qr = () => qrCodes()[url] const expandedState = () => expandedUrl() === url
const scopeLabel = () => const qr = () => qrCodes()[url]
address.scope === "external" const scopeLabel = () =>
? t("remoteAccess.address.scope.network") address.scope === "external"
: address.scope === "loopback" ? t("remoteAccess.address.scope.network")
? t("remoteAccess.address.scope.loopback") : address.scope === "loopback"
: t("remoteAccess.address.scope.internal") ? t("remoteAccess.address.scope.loopback")
: t("remoteAccess.address.scope.internal")
return ( return (
<div class="remote-address"> <div class="remote-address">
<div class="remote-address-main"> <div class="remote-address-main">
<div> <div>
<p class="remote-address-url">{url}</p> <p class="remote-address-url">{url}</p>
<p class="remote-address-meta"> <p class="remote-address-meta">
{address.family.toUpperCase()} - {scopeLabel()} - {address.ip} {address.family.toUpperCase()} - {scopeLabel()} - {address.ip}
</p> </p>
</div> </div>
<div class="remote-actions"> <div class="remote-actions">
<button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}> <button class="remote-pill" type="button" onClick={() => handleOpenUrl(url)}>
<ExternalLink class="remote-icon" /> <ExternalLink class="remote-icon" />
{t("remoteAccess.address.open")} {t("remoteAccess.address.open")}
</button> </button>
<button <button
class="remote-pill" class="remote-pill"
type="button" type="button"
onClick={() => void toggleExpanded(url)} onClick={() => void toggleExpanded(url)}
aria-expanded={expandedState()} aria-expanded={expandedState()}
> >
<Link2 class="remote-icon" /> <Link2 class="remote-icon" />
{expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")} {expandedState() ? t("remoteAccess.address.hideQr") : t("remoteAccess.address.showQr")}
</button> </button>
</div> </div>
</div> </div>
<Show when={expandedState()}> <Show when={expandedState()}>
<div class="remote-qr"> <div class="remote-qr">
<Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}> <Show when={qr()} fallback={<Loader2 class="remote-icon remote-spin" aria-hidden="true" />}>
{(dataUrl) => ( {(dataUrl) => (
<img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" /> <img src={dataUrl()} alt={t("remoteAccess.address.qrAlt", { url })} class="remote-qr-img" />
)} )}
</Show> </Show>
</div> </div>
</Show> </Show>
</div>
)
}}
</For>
</div> </div>
) </Show>
}} </div>
</For>
</Show> </Show>
</div> </div>
</Show> </Show>

View File

@@ -256,6 +256,55 @@
cursor: pointer; cursor: pointer;
} }
.remote-address-disclosure {
border: 1px solid var(--border-base);
border-radius: 12px;
background: var(--surface-primary);
overflow: hidden;
}
.remote-address-disclosure-trigger {
width: 100%;
min-height: 40px;
display: grid;
grid-template-columns: 1fr auto 1fr;
align-items: center;
padding: 8px 12px;
border: 0;
background: transparent;
color: var(--text-primary);
cursor: pointer;
}
.remote-address-disclosure-label {
grid-column: 2;
justify-self: center;
text-align: center;
font-size: 13px;
font-weight: 600;
}
.remote-address-disclosure-chevron {
grid-column: 3;
justify-self: end;
width: 16px;
height: 16px;
color: var(--text-secondary);
transition: transform 0.2s ease;
}
.remote-address-disclosure-chevron.is-expanded {
transform: rotate(180deg);
}
.remote-address-disclosure-content {
display: flex;
flex-direction: column;
gap: 10px;
padding: 0 10px 10px;
border-top: 1px solid var(--border-base);
}
.remote-qr { .remote-qr {
margin-top: 12px; margin-top: 12px;
display: flex; display: flex;