mirror of
https://github.com/OpenSquawk/OpenSquawk
synced 2026-08-12 12:25:39 +08:00
atc header
This commit is contained in:
@@ -79,7 +79,7 @@ const scheduleHotjarInitialization = () => {
|
||||
|
||||
const resolveTrackedProduct = (path: string): 'classroom' | 'liveatc' | null => {
|
||||
if (path.startsWith('/classroom')) return 'classroom';
|
||||
if (path.startsWith('/pm') || path.startsWith('/copilot') || path.startsWith('/bridge')) return 'liveatc';
|
||||
if (path.startsWith('/live-atc') || path.startsWith('/pm') || path.startsWith('/copilot') || path.startsWith('/bridge')) return 'liveatc';
|
||||
return null;
|
||||
};
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ watch(
|
||||
}
|
||||
);
|
||||
|
||||
const restrictedRoute = computed(() => route.path.startsWith('/classroom') || route.path.startsWith('/pm') || route.path.startsWith('/login'));
|
||||
const restrictedRoute = computed(() => route.path.startsWith('/classroom') || route.path.startsWith('/live-atc') || route.path.startsWith('/pm') || route.path.startsWith('/login'));
|
||||
|
||||
const showManageButton = computed(() => hasConsent.value && !isDialogVisible.value && !restrictedRoute.value);
|
||||
|
||||
|
||||
@@ -8,11 +8,12 @@ export type PmEffectiveTheme = 'light' | 'dark'
|
||||
const PM_THEME_COOKIE = 'os_pm_theme'
|
||||
|
||||
/**
|
||||
* /pm-scoped light/dark toggle. Defaults to dark (matching the rest of the app)
|
||||
* /live-atc-scoped light/dark toggle (formerly /pm — cookie name kept for
|
||||
* backwards compatibility). Defaults to dark (matching the rest of the app)
|
||||
* until the user explicitly picks Light or System, at which point that choice is
|
||||
* remembered and wins over the default. Only /pm has a light theme today — the
|
||||
* Vuetify theme is reset back to the app-wide dark default on unmount so other
|
||||
* pages are unaffected.
|
||||
* remembered and wins over the default. Only /live-atc has a light theme today —
|
||||
* the Vuetify theme is reset back to the app-wide dark default on unmount so
|
||||
* other pages are unaffected.
|
||||
*/
|
||||
export function usePmTheme() {
|
||||
const vuetifyTheme = useTheme()
|
||||
@@ -41,6 +42,16 @@ export function usePmTheme() {
|
||||
storedPreference.value = next
|
||||
}
|
||||
|
||||
// Mirrors effectiveTheme onto <html data-theme="..."> so the --bg/--text/--border
|
||||
// CSS variables (defined in global.css) reach content that escapes .pm-page in the
|
||||
// DOM — Vuetify teleports menus, dialogs, and tooltips to <body>, where they'd
|
||||
// otherwise only see the app-wide dark defaults from :root.
|
||||
function applyDocumentTheme() {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-theme', effectiveTheme.value)
|
||||
}
|
||||
}
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
const media = window.matchMedia('(prefers-color-scheme: light)')
|
||||
systemPrefersLight.value = media.matches
|
||||
@@ -53,10 +64,14 @@ export function usePmTheme() {
|
||||
onBeforeUnmount(() => {
|
||||
media.removeEventListener('change', handleChange)
|
||||
vuetifyTheme.global.name.value = 'opensquawkDark'
|
||||
document.documentElement.removeAttribute('data-theme')
|
||||
})
|
||||
}
|
||||
|
||||
watch(effectiveTheme, applyVuetifyTheme, { immediate: true })
|
||||
watch(effectiveTheme, () => {
|
||||
applyVuetifyTheme()
|
||||
applyDocumentTheme()
|
||||
}, { immediate: true })
|
||||
|
||||
return { preference, effectiveTheme, setPreference }
|
||||
}
|
||||
|
||||
@@ -73,25 +73,25 @@
|
||||
<div class="mt-6 space-y-4">
|
||||
<div>
|
||||
<p class="text-xs uppercase tracking-[0.3em] text-cyan-200/70">Delivery</p>
|
||||
<div class="mt-3 flex flex-wrap gap-2">
|
||||
<div class="delivery-seg mt-3">
|
||||
<button
|
||||
type="button"
|
||||
class="btn soft voice-toggle"
|
||||
:class="{ active: voiceMode === 'text' }"
|
||||
class="delivery-seg-btn"
|
||||
:class="{ 'is-active': voiceMode === 'text' }"
|
||||
@click="setVoiceMode('text')"
|
||||
:aria-pressed="voiceMode === 'text'"
|
||||
>
|
||||
<v-icon icon="mdi-text" size="18" class="text-cyan-200" />
|
||||
<v-icon icon="mdi-text" size="18" />
|
||||
Text only
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn soft voice-toggle"
|
||||
:class="{ active: voiceMode === 'radio' }"
|
||||
class="delivery-seg-btn"
|
||||
:class="{ 'is-active': voiceMode === 'radio' }"
|
||||
@click="setVoiceMode('radio')"
|
||||
:aria-pressed="voiceMode === 'radio'"
|
||||
>
|
||||
<v-icon icon="mdi-radio-handheld" size="18" class="text-cyan-200" />
|
||||
<v-icon icon="mdi-radio-handheld" size="18" />
|
||||
Radio voice
|
||||
</button>
|
||||
</div>
|
||||
@@ -916,23 +916,39 @@ onBeforeUnmount(() => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.voice-toggle {
|
||||
transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
|
||||
/* Single pill instead of two competing buttons — this is a delivery
|
||||
preference, not a second call to action next to "Start guided tour". */
|
||||
.delivery-seg {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 4px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
background: color-mix(in srgb, var(--text) 4%, transparent);
|
||||
}
|
||||
|
||||
.voice-toggle .v-icon {
|
||||
color: color-mix(in srgb, var(--accent) 70%, white 30%);
|
||||
.delivery-seg-btn {
|
||||
flex: 1 1 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
padding: 9px 12px;
|
||||
border-radius: 10px;
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
color: var(--t3);
|
||||
transition: color 130ms ease, background 130ms ease;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
}
|
||||
|
||||
.voice-toggle.active {
|
||||
background: color-mix(in srgb, var(--accent) 14%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 40%, transparent);
|
||||
color: var(--text);
|
||||
box-shadow: 0 12px 28px color-mix(in srgb, var(--accent) 26%, transparent);
|
||||
.delivery-seg-btn:hover {
|
||||
color: color-mix(in srgb, var(--text) 85%, transparent);
|
||||
}
|
||||
|
||||
.voice-toggle.active .v-icon {
|
||||
color: color-mix(in srgb, var(--accent) 85%, white 15%);
|
||||
.delivery-seg-btn.is-active {
|
||||
color: #050910;
|
||||
background: #22d3ee;
|
||||
}
|
||||
|
||||
.voice-replay {
|
||||
|
||||
@@ -1931,7 +1931,7 @@ const experiences: ExperienceOption[] = [
|
||||
icon: 'mdi-radio-handheld',
|
||||
to: '/bridge',
|
||||
target: '_blank',
|
||||
matches: path => path.startsWith('/pm')
|
||||
matches: path => path.startsWith('/live-atc') || path.startsWith('/pm')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
6573
app/pages/live-atc.vue
Normal file
6573
app/pages/live-atc.vue
Normal file
File diff suppressed because it is too large
Load Diff
6551
app/pages/pm.vue
6551
app/pages/pm.vue
File diff suppressed because it is too large
Load Diff
@@ -58,7 +58,7 @@
|
||||
|
||||
<NuxtLink
|
||||
v-if="liveAtcUnlocked"
|
||||
to="/pm"
|
||||
to="/live-atc"
|
||||
class="relative flex h-full flex-col gap-3 overflow-hidden rounded-2xl border border-amber-400/30 bg-amber-400/[0.06] p-5 transition hover:border-amber-400/50 hover:bg-amber-400/10"
|
||||
>
|
||||
<svg
|
||||
|
||||
Reference in New Issue
Block a user