discovery-by-llm/frontend/src/components/AppHeader.vue

192 lines
3.9 KiB
Vue

<script setup lang="ts">
import { computed } from 'vue'
import { messages } from '../lib/messages'
import type { AppMode, AuthState } from '../lib/models'
import AuthStatus from './AuthStatus.vue'
const props = defineProps<{
auth: AuthState
mode: AppMode | null
healthFailed: boolean
}>()
const emit = defineEmits<{ logout: []; reset: []; toggleDev: [opener: HTMLElement] }>()
function openDevPanel(event: MouseEvent): void {
if (event.currentTarget instanceof HTMLElement) {
emit('toggleDev', event.currentTarget)
}
}
const modeLabel = computed(() => {
if (props.mode === 'demo') return messages.appHeaderDemoMode
if (props.mode === 'live') return messages.appHeaderLiveMode
if (props.healthFailed) return messages.appHeaderModeUnavailable
return messages.appHeaderCheckingMode
})
</script>
<template>
<header class="header">
<div class="inner">
<div class="brand">
<button
class="brand-control"
type="button"
:aria-label="messages.appHeaderBrandResetLabel"
@click="$emit('reset')"
>
<span class="wordmark"
>{{ messages.appHeaderBrandPrefix
}}<span class="accent">{{ messages.appHeaderBrandAccent }}</span
>{{ messages.appHeaderBrandSuffix }}</span
>
</button>
<span class="kicker">{{ messages.appHeaderKicker }}</span>
</div>
<div class="controls">
<span class="mode" :class="{ failed: healthFailed }">
{{ modeLabel }}
</span>
<AuthStatus :auth="auth" @logout="$emit('logout')" />
<button class="button" type="button" data-dev-panel-opener @click="openDevPanel">
<span class="full-label">
{{ messages.appHeaderDevPanel }}
<span class="key">{{ messages.appHeaderDevPanelShortcut }}</span>
</span>
<span class="short-label">{{ messages.appHeaderDevPanelShort }}</span>
</button>
</div>
</div>
</header>
</template>
<style scoped>
.header {
z-index: 20;
flex: none;
padding: var(--s-5) var(--gutter);
background: var(--c-bg);
border-bottom: 1px solid var(--c-line);
}
.inner {
display: flex;
gap: var(--s-6);
align-items: center;
justify-content: space-between;
max-width: var(--measure);
margin: 0 auto;
}
.brand {
display: flex;
gap: var(--s-5);
align-items: baseline;
min-width: 0;
}
.brand-control {
padding: 0;
color: inherit;
cursor: pointer;
background: transparent;
border: 0;
border-radius: var(--r-sm);
}
.brand-control:focus-visible {
outline: 2px solid var(--c-accent);
outline-offset: 4px;
}
.wordmark {
font-family: var(--f-display);
font-size: var(--t-brand);
font-weight: 600;
letter-spacing: -0.02em;
}
.accent {
color: var(--c-accent);
}
.kicker,
.mode,
.button {
font-family: var(--f-mono);
font-size: var(--t-micro);
}
.kicker {
color: var(--c-text-faint);
font-size: var(--t-caps);
letter-spacing: var(--ls-caps);
text-transform: uppercase;
}
.controls {
display: flex;
flex: none;
gap: var(--s-4);
align-items: center;
white-space: nowrap;
}
.mode {
color: var(--c-accent-soft);
text-transform: uppercase;
}
.mode.failed {
color: var(--c-error);
}
.button {
min-height: 31px;
padding: 6px 11px;
color: var(--c-text-muted);
cursor: pointer;
background: transparent;
border: 1px solid var(--c-line);
border-radius: var(--r-md);
}
.button:hover {
color: var(--c-text);
border-color: var(--c-focus-border);
}
.key {
color: var(--c-text-ghost);
}
.short-label {
display: none;
}
@media (max-width: 760px) {
.kicker,
.mode,
.key {
display: none;
}
}
@media (max-width: 560px) {
.button {
min-width: var(--tap-min);
min-height: var(--tap-min);
padding: 8px;
font-size: 0;
}
.full-label {
display: none;
}
.short-label {
display: inline;
font-size: var(--t-micro);
}
}
</style>