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:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user