mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-07-31 22:05:34 +08:00
feat(live-atc): dock the expected-comm hint onto a round, glowing PTT pad
The PTT pad is the control the pilot uses most, but it read as one card among several. It is now a circular transmit key that lights up cyan at rest and green while transmitting, with the active frequency called out underneath. Expected Communication shrinks from a full card to a two-line strip (ATC line, pilot line) sharing a frame with the pad, so it reads as guidance for the next transmission rather than as general reading material. The strip still renders in text mode when the learning aid has something to say, and disappears with the learningMode toggle as before. The pilot line's icon was `mdi-account-pilot`, which is not an MDI icon and rendered as an empty glyph; it is now `mdi-headset`. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
139
app/components/live-atc/cockpit/ExpectedCommStrip.vue
Normal file
139
app/components/live-atc/cockpit/ExpectedCommStrip.vue
Normal file
@@ -0,0 +1,139 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
/** What ATC just said, or is about to say — rendered as the upper line. */
|
||||
controllerSay: string
|
||||
/** The phrase the pilot is expected to transmit next — the lower line. */
|
||||
expectedPhrase: string
|
||||
}>()
|
||||
|
||||
const showPronunciation = defineModel<boolean>('showPronunciation', { required: true })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="ecs" aria-label="Expected communication">
|
||||
<header class="ecs__head">
|
||||
<span class="ecs__title">Expected</span>
|
||||
<v-tooltip
|
||||
:text="showPronunciation ? 'Switch to plain text' : 'Switch to radio pronunciation'"
|
||||
location="top"
|
||||
>
|
||||
<template #activator="{ props: tip }">
|
||||
<button
|
||||
v-bind="tip"
|
||||
type="button"
|
||||
class="ecs__toggle"
|
||||
:class="{ 'is-on': showPronunciation }"
|
||||
:aria-pressed="showPronunciation ? 'true' : 'false'"
|
||||
aria-label="Toggle radio pronunciation"
|
||||
@click="showPronunciation = !showPronunciation"
|
||||
>
|
||||
<v-icon size="15">{{ showPronunciation ? 'mdi-text' : 'mdi-radio' }}</v-icon>
|
||||
</button>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</header>
|
||||
|
||||
<p v-if="controllerSay" class="ecs__row ecs__row--atc">
|
||||
<v-icon size="14" class="ecs__icon">mdi-radio-tower</v-icon>
|
||||
<span class="ecs__role">ATC</span>
|
||||
<span class="ecs__text">{{ controllerSay }}</span>
|
||||
</p>
|
||||
|
||||
<p v-if="expectedPhrase" class="ecs__row ecs__row--pilot">
|
||||
<v-icon size="14" class="ecs__icon">mdi-headset</v-icon>
|
||||
<span class="ecs__role">You</span>
|
||||
<span class="ecs__text">{{ expectedPhrase }}</span>
|
||||
</p>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ecs {
|
||||
padding: 10px 12px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--text) 3%, transparent);
|
||||
}
|
||||
|
||||
.ecs__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.ecs__title {
|
||||
font-size: 9.5px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--t4);
|
||||
}
|
||||
|
||||
.ecs__toggle {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--t3);
|
||||
background: transparent;
|
||||
transition: color 120ms ease, border-color 120ms ease, background 120ms ease;
|
||||
}
|
||||
|
||||
.ecs__toggle:hover {
|
||||
color: #67e8f9;
|
||||
border-color: rgba(34, 211, 238, 0.4);
|
||||
}
|
||||
|
||||
.ecs__toggle.is-on {
|
||||
color: #67e8f9;
|
||||
border-color: rgba(34, 211, 238, 0.45);
|
||||
background: rgba(34, 211, 238, 0.1);
|
||||
}
|
||||
|
||||
/* One line per speaker: role tag, then the phrase. Wraps rather than truncates —
|
||||
a clipped clearance is worse than a two-line strip. */
|
||||
.ecs__row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 7px;
|
||||
padding: 5px 0;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.ecs__row + .ecs__row {
|
||||
border-top: 1px solid color-mix(in srgb, var(--text) 6%, transparent);
|
||||
}
|
||||
|
||||
.ecs__icon {
|
||||
align-self: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ecs__role {
|
||||
flex-shrink: 0;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.ecs__text {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
color: var(--text);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ecs__row--atc .ecs__icon,
|
||||
.ecs__row--atc .ecs__role {
|
||||
color: #6ee7a8;
|
||||
}
|
||||
|
||||
.ecs__row--pilot .ecs__icon,
|
||||
.ecs__row--pilot .ecs__role {
|
||||
color: #7dd3fc;
|
||||
}
|
||||
</style>
|
||||
243
app/components/live-atc/cockpit/PttPad.vue
Normal file
243
app/components/live-atc/cockpit/PttPad.vue
Normal file
@@ -0,0 +1,243 @@
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
isRecording: boolean
|
||||
micPermission: boolean
|
||||
/** True when the desktop Bridge companion has a PTT hotkey bound. */
|
||||
bridgePttConnected: boolean
|
||||
activeFrequency: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'start'): void
|
||||
(e: 'stop'): void
|
||||
(e: 'request-mic'): void
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ptt">
|
||||
<v-alert
|
||||
v-if="!micPermission"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="bg-cyan-500/10 text-cyan-100 mb-4"
|
||||
density="compact"
|
||||
>
|
||||
Microphone permission required for push-to-talk.
|
||||
<template #append>
|
||||
<v-btn color="cyan" size="small" variant="flat" @click="emit('request-mic')">Allow</v-btn>
|
||||
</template>
|
||||
</v-alert>
|
||||
|
||||
<ClientOnly>
|
||||
<button
|
||||
type="button"
|
||||
class="ptt-btn"
|
||||
:class="isRecording ? 'is-live' : 'is-idle'"
|
||||
:aria-pressed="isRecording ? 'true' : 'false'"
|
||||
aria-label="Hold to transmit"
|
||||
@touchstart.prevent="emit('start')"
|
||||
@touchend.prevent="emit('stop')"
|
||||
@touchcancel.prevent="emit('stop')"
|
||||
@mousedown.prevent="emit('start')"
|
||||
@mouseup.prevent="emit('stop')"
|
||||
@mouseleave="emit('stop')"
|
||||
>
|
||||
<v-icon :icon="isRecording ? 'mdi-access-point' : 'mdi-radio-handheld'" size="52" class="ptt-btn__icon" />
|
||||
<span class="ptt-btn__label">{{ isRecording ? 'Transmitting' : 'Hold to transmit' }}</span>
|
||||
</button>
|
||||
</ClientOnly>
|
||||
|
||||
<p v-if="bridgePttConnected" class="ptt-hotkey" :class="{ 'is-live': isRecording }">
|
||||
<span class="ptt-hotkey__dot" aria-hidden="true" />
|
||||
{{ isRecording ? 'Hotkey transmitting' : 'Hotkey armed' }}
|
||||
</p>
|
||||
|
||||
<div class="ptt-freq">
|
||||
<span class="ptt-freq__value">{{ activeFrequency || '---' }}</span>
|
||||
<span class="ptt-freq__label">Active frequency</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ptt {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 20px 14px 22px;
|
||||
}
|
||||
|
||||
.ptt :deep(.v-alert) {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* The single most-used control on the screen, so it reads as a physical
|
||||
transmit key: round, raised, and lit from within. */
|
||||
.ptt-btn {
|
||||
position: relative;
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
width: clamp(148px, 44vw, 188px);
|
||||
aspect-ratio: 1;
|
||||
border-radius: 999px;
|
||||
/* 2px at 0.28 opacity, not a hairline: the border has to survive bright
|
||||
ambient light on a tablet in a real cockpit. */
|
||||
border: 2px solid rgba(255, 255, 255, 0.28);
|
||||
background:
|
||||
radial-gradient(circle at 50% 34%,
|
||||
rgba(34, 211, 238, 0.16),
|
||||
rgba(4, 8, 16, 0.9) 68%);
|
||||
color: #67e8f9;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
transition: transform 120ms ease, border-color 160ms ease, box-shadow 160ms ease, background 160ms ease;
|
||||
}
|
||||
|
||||
.ptt-btn.is-idle {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(34, 211, 238, 0.2),
|
||||
0 0 34px -8px rgba(34, 211, 238, 0.5),
|
||||
0 18px 38px -18px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.ptt-btn.is-idle:hover {
|
||||
border-color: rgba(34, 211, 238, 0.6);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(34, 211, 238, 0.32),
|
||||
0 0 46px -6px rgba(34, 211, 238, 0.62),
|
||||
0 18px 38px -18px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
.ptt-btn.is-live {
|
||||
border-color: rgba(74, 222, 128, 0.75);
|
||||
background:
|
||||
radial-gradient(circle at 50% 34%,
|
||||
rgba(34, 197, 94, 0.32),
|
||||
rgba(4, 12, 8, 0.9) 68%);
|
||||
color: #86efac;
|
||||
transform: scale(0.97);
|
||||
box-shadow:
|
||||
0 0 0 6px rgba(34, 197, 94, 0.22),
|
||||
0 0 60px -4px rgba(34, 197, 94, 0.75);
|
||||
animation: ptt-live-glow 1.4s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes ptt-live-glow {
|
||||
0%, 100% { box-shadow: 0 0 0 6px rgba(34, 197, 94, 0.22), 0 0 60px -4px rgba(34, 197, 94, 0.75); }
|
||||
50% { box-shadow: 0 0 0 10px rgba(34, 197, 94, 0.1), 0 0 74px 0 rgba(34, 197, 94, 0.55); }
|
||||
}
|
||||
|
||||
.ptt-btn__icon {
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.ptt-btn__label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3em;
|
||||
text-transform: uppercase;
|
||||
padding-left: 0.3em; /* balances the trailing letter-spacing */
|
||||
color: currentColor;
|
||||
}
|
||||
|
||||
.ptt-btn.is-idle .ptt-btn__label {
|
||||
color: var(--t4);
|
||||
}
|
||||
|
||||
.ptt-hotkey {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 14px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.25em;
|
||||
text-transform: uppercase;
|
||||
color: rgba(103, 232, 249, 0.7);
|
||||
}
|
||||
|
||||
.ptt-hotkey.is-live {
|
||||
color: #86efac;
|
||||
}
|
||||
|
||||
.ptt-hotkey__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.ptt-hotkey.is-live .ptt-hotkey__dot {
|
||||
animation: ptt-dot-pulse 1s infinite;
|
||||
}
|
||||
|
||||
@keyframes ptt-dot-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.35; }
|
||||
}
|
||||
|
||||
.ptt-freq {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 1px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.ptt-freq__value {
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
font-size: clamp(26px, 8vw, 34px);
|
||||
font-weight: 700;
|
||||
line-height: 1.05;
|
||||
font-variant-numeric: tabular-nums;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.ptt-freq__label {
|
||||
font-size: 11px;
|
||||
color: var(--t4);
|
||||
}
|
||||
|
||||
.pm-page[data-theme="light"] .ptt-btn {
|
||||
border-color: rgba(15, 20, 32, 0.24);
|
||||
background: radial-gradient(circle at 50% 34%, rgba(8, 145, 178, 0.14), rgba(255, 255, 255, 0.9) 70%);
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
.pm-page[data-theme="light"] .ptt-btn.is-idle {
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(8, 145, 178, 0.22),
|
||||
0 12px 26px -16px rgba(15, 20, 32, 0.5);
|
||||
}
|
||||
|
||||
.pm-page[data-theme="light"] .ptt-btn.is-live {
|
||||
border-color: rgba(22, 163, 74, 0.6);
|
||||
background: radial-gradient(circle at 50% 34%, rgba(34, 197, 94, 0.24), rgba(255, 255, 255, 0.92) 70%);
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
/* Cyan-on-white washes out; darken the hotkey line for the light skin. */
|
||||
.pm-page[data-theme="light"] .ptt-hotkey {
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
.pm-page[data-theme="light"] .ptt-hotkey.is-live {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.ptt-btn,
|
||||
.ptt-btn.is-live,
|
||||
.ptt-hotkey.is-live .ptt-hotkey__dot {
|
||||
animation: none;
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -254,49 +254,6 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Expected Communication (learning aid) -->
|
||||
<div
|
||||
v-show="activeTab === 'funk'"
|
||||
v-if="learningMode && (displayControllerSay || displayExpectedPhrase)"
|
||||
class="pm-block"
|
||||
>
|
||||
<v-card class="bg-white/5 border border-white/10">
|
||||
<v-card-text class="space-y-3">
|
||||
<div class="flex items-center justify-between">
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-white/40">Expected communication</p>
|
||||
<v-tooltip :text="showRadioPronunciation ? 'Switch to plain text' : 'Switch to radio pronunciation'" location="top">
|
||||
<template #activator="{ props: tip }">
|
||||
<v-btn
|
||||
v-bind="tip"
|
||||
:icon="showRadioPronunciation ? 'mdi-text' : 'mdi-radio'"
|
||||
size="x-small"
|
||||
variant="text"
|
||||
:color="showRadioPronunciation ? 'cyan' : 'white'"
|
||||
@click="showRadioPronunciation = !showRadioPronunciation"
|
||||
/>
|
||||
</template>
|
||||
</v-tooltip>
|
||||
</div>
|
||||
|
||||
<div v-if="displayControllerSay" class="space-y-2 rounded-2xl bg-green-500/10 border border-green-500/20 p-3 text-sm">
|
||||
<div class="flex items-center gap-2 text-green-300">
|
||||
<v-icon size="16">mdi-radio-tower</v-icon>
|
||||
<span class="text-xs uppercase font-semibold">ATC</span>
|
||||
</div>
|
||||
<p class="font-mono text-white">{{ displayControllerSay }}</p>
|
||||
</div>
|
||||
|
||||
<div v-if="displayExpectedPhrase" class="space-y-2 rounded-2xl bg-blue-500/10 border border-blue-500/20 p-3 text-sm">
|
||||
<div class="flex items-center gap-2 text-blue-300">
|
||||
<v-icon size="16">mdi-account-pilot</v-icon>
|
||||
<span class="text-xs uppercase font-semibold">Pilot (You)</span>
|
||||
</div>
|
||||
<p class="font-mono text-white">{{ displayExpectedPhrase }}</p>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
|
||||
<!-- Input mode switch: voice / text -->
|
||||
<div v-show="activeTab === 'funk'" class="pm-block">
|
||||
<div class="pm-seg">
|
||||
@@ -321,62 +278,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Push to Talk (voice mode) -->
|
||||
<div v-show="activeTab === 'funk' && inputMode === 'voice'" class="pm-block">
|
||||
<v-sheet class="rounded-3xl border border-white/10 bg-gradient-to-br from-white/5 via-white/10 to-transparent p-4 shadow-lg">
|
||||
<v-alert
|
||||
v-if="!micPermission"
|
||||
type="info"
|
||||
variant="tonal"
|
||||
class="bg-cyan-500/10 text-cyan-100 mb-4"
|
||||
density="compact"
|
||||
>
|
||||
Microphone permission required for push-to-talk.
|
||||
<template #append>
|
||||
<v-btn color="cyan" size="small" variant="flat" @click="requestMicAccess">Allow</v-btn>
|
||||
</template>
|
||||
</v-alert>
|
||||
|
||||
<ClientOnly>
|
||||
<div
|
||||
class="ptt-pad flex h-52 lg:h-60 items-center justify-center rounded-2xl border-2 text-center transition cursor-pointer"
|
||||
:class="isRecording ? 'ptt-pad--active border-green-400/50 ring-4 ring-green-400/40 bg-green-500/10' : 'ptt-pad--idle bg-black/40'"
|
||||
@touchstart.prevent="startRecording(false)"
|
||||
@touchend.prevent="stopRecording"
|
||||
@touchcancel.prevent="stopRecording"
|
||||
@mousedown.prevent="startRecording(false)"
|
||||
@mouseup.prevent="stopRecording"
|
||||
@mouseleave="stopRecording"
|
||||
>
|
||||
<div class="space-y-1">
|
||||
<v-icon
|
||||
:icon="isRecording ? 'mdi-access-point' : 'mdi-radio-handheld'"
|
||||
size="44"
|
||||
:class="isRecording ? 'text-green-400 animate-pulse' : 'text-cyan-300'"
|
||||
/>
|
||||
<p
|
||||
class="text-[11px] uppercase tracking-[0.35em] mt-1"
|
||||
:class="isRecording ? 'text-green-300' : 'text-white/40'"
|
||||
>
|
||||
{{ isRecording ? 'Transmitting' : 'Hold to transmit' }}
|
||||
</p>
|
||||
<p
|
||||
v-if="bridgePttConnected"
|
||||
class="text-[10px] uppercase tracking-[0.25em] flex items-center justify-center gap-1.5"
|
||||
:class="isRecording ? 'text-green-300' : 'text-cyan-300/70'"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-1.5 w-1.5 rounded-full"
|
||||
:class="isRecording ? 'bg-green-400 animate-pulse' : 'bg-cyan-300/60'"
|
||||
/>
|
||||
{{ isRecording ? 'Hotkey transmitting' : 'Hotkey armed' }}
|
||||
</p>
|
||||
<p class="pt-2 text-4xl font-bold font-mono tracking-tight">{{ frequencies.active || '---' }}</p>
|
||||
<p class="text-xs text-white/45">Active frequency</p>
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</v-sheet>
|
||||
<!-- Transmit block: the expected-comm hint is docked to the PTT pad so it
|
||||
reads as guidance for the next transmission, not as a separate card. -->
|
||||
<div
|
||||
v-show="activeTab === 'funk'"
|
||||
v-if="inputMode === 'voice' || showExpectedComm"
|
||||
class="pm-block transmit-stack"
|
||||
>
|
||||
<ExpectedCommStrip
|
||||
v-if="learningMode && (displayControllerSay || displayExpectedPhrase)"
|
||||
:controller-say="displayControllerSay"
|
||||
:expected-phrase="displayExpectedPhrase"
|
||||
v-model:show-pronunciation="showRadioPronunciation"
|
||||
/>
|
||||
<PttPad
|
||||
v-show="inputMode === 'voice'"
|
||||
:is-recording="isRecording"
|
||||
:mic-permission="micPermission"
|
||||
:bridge-ptt-connected="bridgePttConnected"
|
||||
:active-frequency="frequencies.active"
|
||||
@start="startRecording(false)"
|
||||
@stop="stopRecording"
|
||||
@request-mic="requestMicAccess"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Manual text input (text mode) -->
|
||||
@@ -1205,6 +1129,8 @@ import { usePmTheme } from '~/composables/usePmTheme'
|
||||
import { normalizeManualFreq } from '../../shared/utils/frequency'
|
||||
import { useFrequencyPresets } from '~/composables/useFrequencyPresets'
|
||||
import RadioPanel from '~/components/live-atc/cockpit/RadioPanel.vue'
|
||||
import ExpectedCommStrip from '~/components/live-atc/cockpit/ExpectedCommStrip.vue'
|
||||
import PttPad from '~/components/live-atc/cockpit/PttPad.vue'
|
||||
import { useAtisPlayback } from '~/composables/useAtisPlayback'
|
||||
import { usePttRecording } from '~/composables/usePttRecording'
|
||||
import { useBugReport } from '~/composables/useBugReport'
|
||||
@@ -1388,6 +1314,9 @@ const activeTab = ref<'funk' | 'log'>('funk')
|
||||
const experienceMenu = ref(false)
|
||||
const inputMode = ref<'voice' | 'text'>('voice')
|
||||
const learningMode = ref(true)
|
||||
const showExpectedComm = computed(
|
||||
() => learningMode.value && Boolean(displayControllerSay.value || displayExpectedPhrase.value),
|
||||
)
|
||||
|
||||
// Overlays / sheets opened from HUD controls
|
||||
const showFlightSheet = ref(false)
|
||||
@@ -1693,37 +1622,19 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.ptt-pad {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
touch-action: manipulation;
|
||||
}
|
||||
|
||||
/* Idle state needs a boundary that survives bright ambient light/glare — a
|
||||
hairline low-opacity border reads as "no border at all" outdoors. Width +
|
||||
opacity + a soft shadow together are more glare-resistant than opacity alone. */
|
||||
.ptt-pad--idle {
|
||||
border-color: rgba(255, 255, 255, 0.28);
|
||||
box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.16), 0 14px 30px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.pm-page[data-theme="light"] .ptt-pad--idle {
|
||||
border-color: rgba(15, 20, 32, 0.24);
|
||||
background-color: rgba(15, 20, 32, 0.045) !important;
|
||||
box-shadow: 0 0 0 1px rgba(8, 145, 178, 0.22), 0 10px 24px rgba(15, 20, 32, 0.1);
|
||||
}
|
||||
|
||||
.pm-page[data-theme="light"] .ptt-pad--active {
|
||||
box-shadow: 0 0 0 4px rgba(34, 197, 94, 0.28);
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 1s infinite;
|
||||
/* Expected-comm strip + PTT pad share one frame, so the hint reads as part of
|
||||
the transmit control rather than as another card above it. */
|
||||
.transmit-stack {
|
||||
overflow: hidden;
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--border);
|
||||
background:
|
||||
linear-gradient(165deg,
|
||||
color-mix(in srgb, var(--bg2) 90%, transparent),
|
||||
color-mix(in srgb, var(--bg) 96%, transparent));
|
||||
box-shadow:
|
||||
inset 0 1px 0 color-mix(in srgb, var(--text) 6%, transparent),
|
||||
0 20px 44px -28px rgba(0, 0, 0, 0.9);
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user