mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 16:02:55 +00:00
fix(mobile): stabilize pairing option layout
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -206,41 +206,36 @@ export function HeroFlow({
|
||||
compact
|
||||
/>
|
||||
|
||||
{connectionMode === 'local-only' ? (
|
||||
<div className="mp-network-row">
|
||||
<span className="mp-network-label">
|
||||
{translate('auto.components.mobile.MobileHero.dfd2aa9d5d', 'Network')}
|
||||
</span>
|
||||
<NetworkInterfacePicker
|
||||
networkInterfaces={networkInterfaces}
|
||||
selectedAddress={selectedAddress}
|
||||
onSelectedAddressChange={onSelectedAddressChange}
|
||||
// Why: when discovery is empty, a custom address is the
|
||||
// only route to a Tailscale hostname or static IP.
|
||||
disabled={false}
|
||||
className="mp-network-select"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn(
|
||||
'mp-network-refresh',
|
||||
refreshingNetworkInterfaces && 'is-spinning'
|
||||
)}
|
||||
onClick={onRefreshNetworkInterfaces}
|
||||
disabled={refreshingNetworkInterfaces}
|
||||
aria-label={translate(
|
||||
'auto.components.mobile.MobileHero.85067b9e06',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
title={translate(
|
||||
'auto.components.mobile.MobileHero.85067b9e06',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mp-network-row">
|
||||
<span className="mp-network-label">
|
||||
{translate('auto.components.mobile.MobileHero.dfd2aa9d5d', 'Network')}
|
||||
</span>
|
||||
<NetworkInterfacePicker
|
||||
networkInterfaces={networkInterfaces}
|
||||
selectedAddress={selectedAddress}
|
||||
onSelectedAddressChange={onSelectedAddressChange}
|
||||
// Why: direct-first and local-only pairing both advertise a
|
||||
// local route; keeping it visible also prevents mode shifts.
|
||||
disabled={false}
|
||||
className="mp-network-select"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className={cn('mp-network-refresh', refreshingNetworkInterfaces && 'is-spinning')}
|
||||
onClick={onRefreshNetworkInterfaces}
|
||||
disabled={refreshingNetworkInterfaces}
|
||||
aria-label={translate(
|
||||
'auto.components.mobile.MobileHero.85067b9e06',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
title={translate(
|
||||
'auto.components.mobile.MobileHero.85067b9e06',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mp-inline-actions">
|
||||
<span className="mp-action-divider">
|
||||
|
||||
@@ -36,6 +36,135 @@ function relayStatusLabel(status: MobileRelayStatus): string {
|
||||
)
|
||||
}
|
||||
|
||||
type CompactConnectionOptionsProps = {
|
||||
value: MobilePairingConnectionMode
|
||||
onChange: (value: MobilePairingConnectionMode) => void
|
||||
signedIn: boolean
|
||||
configured: boolean
|
||||
connecting: boolean
|
||||
connect: () => Promise<unknown>
|
||||
relayStatus: MobileRelayStatus
|
||||
}
|
||||
|
||||
function CompactConnectionOptions({
|
||||
value,
|
||||
onChange,
|
||||
signedIn,
|
||||
configured,
|
||||
connecting,
|
||||
connect,
|
||||
relayStatus
|
||||
}: CompactConnectionOptionsProps): React.JSX.Element {
|
||||
return (
|
||||
<section className="w-full space-y-2">
|
||||
<h3 className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.title',
|
||||
'How should the new phone connect?'
|
||||
)}
|
||||
</h3>
|
||||
<div className="grid grid-cols-2 gap-2" role="radiogroup">
|
||||
<label
|
||||
className={cn(
|
||||
'flex min-h-16 items-center gap-2 rounded-lg border border-border/60 px-3 py-2',
|
||||
signedIn ? 'cursor-pointer' : 'cursor-not-allowed opacity-60',
|
||||
value === 'automatic' && signedIn && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="mobile-pairing-connection-mode"
|
||||
value="automatic"
|
||||
checked={value === 'automatic'}
|
||||
disabled={!signedIn}
|
||||
onChange={() => onChange('automatic')}
|
||||
className="size-4 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.anywhere',
|
||||
'Connect from anywhere'
|
||||
)}
|
||||
</span>
|
||||
<span className="block text-[11px] text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.recommended',
|
||||
'Recommended'
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
{signedIn ? (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{relayStatusLabel(relayStatus)}
|
||||
</Badge>
|
||||
) : !configured ? (
|
||||
<Badge variant="outline" className="shrink-0">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.unavailable',
|
||||
'Unavailable'
|
||||
)}
|
||||
</Badge>
|
||||
) : null}
|
||||
</label>
|
||||
|
||||
<label
|
||||
className={cn(
|
||||
'flex min-h-16 cursor-pointer items-center gap-2 rounded-lg border border-border/60 px-3 py-2',
|
||||
value === 'local-only' && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="mobile-pairing-connection-mode"
|
||||
value="local-only"
|
||||
checked={value === 'local-only'}
|
||||
onChange={() => onChange('local-only')}
|
||||
className="size-4 shrink-0 accent-primary"
|
||||
/>
|
||||
<span className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.localOnly',
|
||||
'Local network only'
|
||||
)}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex min-h-9 items-center justify-between gap-3">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{!signedIn
|
||||
? translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.signInDescription',
|
||||
'Sign in on this desktop to use Orca Relay.'
|
||||
)
|
||||
: value === 'automatic'
|
||||
? translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.automaticDescription',
|
||||
'Orca uses a direct connection when available and Relay otherwise.'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.localDescription',
|
||||
'Uses LAN or Tailscale without connecting this phone through Orca Relay.'
|
||||
)}
|
||||
</p>
|
||||
{!signedIn && configured ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="xs"
|
||||
className="shrink-0"
|
||||
disabled={connecting}
|
||||
onClick={() => void connect()}
|
||||
>
|
||||
{connecting ? <Loader2 className="animate-spin" /> : null}
|
||||
{translate('auto.components.settings.MobilePairingConnectionOptions.signIn', 'Sign in')}
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
export function MobilePairingConnectionOptions({
|
||||
value,
|
||||
onChange,
|
||||
@@ -75,8 +204,22 @@ export function MobilePairingConnectionOptions({
|
||||
}
|
||||
}, [])
|
||||
|
||||
if (compact) {
|
||||
return (
|
||||
<CompactConnectionOptions
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
signedIn={signedIn}
|
||||
configured={configured}
|
||||
connecting={connecting}
|
||||
connect={connect}
|
||||
relayStatus={relayStatus}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<section className={cn('space-y-2', compact && 'max-w-md')}>
|
||||
<section className="space-y-2">
|
||||
<h3 className="text-sm font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingConnectionOptions.title',
|
||||
@@ -87,7 +230,7 @@ export function MobilePairingConnectionOptions({
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-3 rounded-lg border border-border/60 px-3',
|
||||
compact ? 'py-2' : 'py-3',
|
||||
'py-3',
|
||||
value === 'automatic' && signedIn && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
@@ -160,7 +303,7 @@ export function MobilePairingConnectionOptions({
|
||||
<label
|
||||
className={cn(
|
||||
'flex cursor-pointer items-start gap-3 rounded-lg border border-border/60 px-3',
|
||||
compact ? 'py-2' : 'py-3',
|
||||
'py-3',
|
||||
value === 'local-only' && 'bg-accent'
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -43,20 +43,18 @@ function renderSection(
|
||||
}
|
||||
|
||||
describe('MobilePairingSetupSection', () => {
|
||||
it('opens local settings by default for local-only pairing', () => {
|
||||
it('keeps local settings visible for local-only pairing', () => {
|
||||
renderSection()
|
||||
expect(screen.getByRole('combobox')).toHaveTextContent('100.64.1.20 (tailscale0)')
|
||||
expect(screen.getByText(/connects only through the local network address/i)).toBeVisible()
|
||||
})
|
||||
|
||||
it('keeps local settings advanced for automatic Relay fallback', async () => {
|
||||
const { user } = renderSection({ connectionMode: 'automatic' })
|
||||
expect(screen.queryByRole('combobox')).not.toBeInTheDocument()
|
||||
it('keeps local settings visible for automatic direct-first pairing', () => {
|
||||
renderSection({ connectionMode: 'automatic' })
|
||||
expect(screen.getByRole('combobox')).toBeVisible()
|
||||
expect(
|
||||
screen.getByText(/includes direct access and encrypted Orca Relay fallback/i)
|
||||
).toBeVisible()
|
||||
await user.click(screen.getByRole('button', { name: 'Local connection settings' }))
|
||||
expect(screen.getByRole('combobox')).toBeVisible()
|
||||
})
|
||||
|
||||
it('commits an OS interface picked from the list', async () => {
|
||||
|
||||
@@ -69,62 +69,57 @@ export function MobilePairingSetupSection({
|
||||
)}
|
||||
</Button>
|
||||
|
||||
<Accordion
|
||||
key={connectionMode}
|
||||
type="single"
|
||||
collapsible
|
||||
defaultValue={connectionMode === 'local-only' ? 'local-address' : undefined}
|
||||
className="mt-4 border-t border-border/60 pt-2"
|
||||
>
|
||||
<AccordionItem value="local-address">
|
||||
<AccordionTrigger className="py-2 text-xs">
|
||||
<div className="mt-4 space-y-3 border-t border-border/60 pt-4">
|
||||
<div className="space-y-1">
|
||||
<h4 className="text-xs font-medium">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.localSettings',
|
||||
'Local connection settings'
|
||||
)}
|
||||
</AccordionTrigger>
|
||||
<AccordionContent className="space-y-3 text-xs text-muted-foreground">
|
||||
<p>
|
||||
</h4>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.localAddressDescription',
|
||||
'Choose the LAN or private-network address that Orca Mobile can use to reach this computer directly.'
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<NetworkInterfacePicker
|
||||
networkInterfaces={networkInterfaces}
|
||||
selectedAddress={selectedAddress}
|
||||
onSelectedAddressChange={onSelectedAddressChange}
|
||||
className="min-w-[220px] justify-between font-normal"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onRefreshNetworkInterfaces}
|
||||
disabled={refreshingNetworkInterfaces}
|
||||
aria-label={translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.refresh',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<RefreshCw className={refreshingNetworkInterfaces ? 'animate-spin' : ''} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.localAddressDescription',
|
||||
'Choose the LAN or private-network address that Orca Mobile can use to reach this computer directly.'
|
||||
'auto.components.settings.MobilePairingSetupSection.refresh',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
</p>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<NetworkInterfacePicker
|
||||
networkInterfaces={networkInterfaces}
|
||||
selectedAddress={selectedAddress}
|
||||
onSelectedAddressChange={onSelectedAddressChange}
|
||||
className="min-w-[220px] justify-between font-normal"
|
||||
/>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
onClick={onRefreshNetworkInterfaces}
|
||||
disabled={refreshingNetworkInterfaces}
|
||||
aria-label={translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.refresh',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<RefreshCw className={refreshingNetworkInterfaces ? 'animate-spin' : ''} />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="bottom" sideOffset={6}>
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.refresh',
|
||||
'Refresh network interfaces'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</AccordionContent>
|
||||
</AccordionItem>
|
||||
<AccordionItem value="tailnet-guide">
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Accordion type="single" collapsible className="mt-2">
|
||||
<AccordionItem value="tailnet-guide" className="border-b-0">
|
||||
<AccordionTrigger className="py-2 text-xs">
|
||||
{translate(
|
||||
'auto.components.settings.MobilePairingSetupSection.tailnet',
|
||||
|
||||
Reference in New Issue
Block a user