feat(ui): mobile responsive layout
- App: hamburger menu on mobile, nav links hidden below md breakpoint - LogSearch: collapsible sidebar on mobile, stacks above results vertically - Incidents/Sources: overflow-x-auto on table containers, min-w to preserve column layout on desktop; drawer action buttons flex-wrap on small screens - Bundles: flex-wrap on header row, hide source_host + timestamp below sm - General: p-4 sm:p-6 padding on all standard views
This commit is contained in:
parent
9052939ae1
commit
1538a3cf69
5 changed files with 96 additions and 42 deletions
|
|
@ -1,11 +1,13 @@
|
|||
<template>
|
||||
<div class="min-h-screen bg-surface font-mono text-text-primary">
|
||||
<nav aria-label="Main navigation" class="border-b border-surface-border px-6 py-3 flex items-center gap-6">
|
||||
<nav aria-label="Main navigation" class="border-b border-surface-border">
|
||||
<div class="px-4 sm:px-6 py-3 flex items-center gap-3">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-accent font-semibold tracking-wide">TURNSTONE</span>
|
||||
<span class="text-text-dim text-xs">diagnostic intelligence</span>
|
||||
<span class="text-text-dim text-xs hidden sm:inline">diagnostic intelligence</span>
|
||||
</div>
|
||||
<div class="flex gap-1 ml-4">
|
||||
<!-- Desktop nav links -->
|
||||
<div class="hidden md:flex gap-1 ml-4">
|
||||
<RouterLink
|
||||
v-for="link in navLinks"
|
||||
:key="link.to"
|
||||
|
|
@ -28,6 +30,25 @@
|
|||
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||
>{{ isDark ? '☀' : '☾' }}</button>
|
||||
<StatusDot />
|
||||
<!-- Hamburger (mobile only) -->
|
||||
<button
|
||||
@click="menuOpen = !menuOpen"
|
||||
:aria-expanded="menuOpen"
|
||||
aria-label="Toggle navigation menu"
|
||||
class="md:hidden text-text-dim hover:text-text-primary transition-colors text-lg leading-none px-1"
|
||||
>{{ menuOpen ? '✕' : '☰' }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Mobile nav dropdown -->
|
||||
<div v-if="menuOpen" class="md:hidden border-t border-surface-border px-4 py-2 flex flex-col gap-0.5">
|
||||
<RouterLink
|
||||
v-for="link in navLinks"
|
||||
:key="link.to"
|
||||
:to="link.to"
|
||||
@click="menuOpen = false"
|
||||
class="px-3 py-2 rounded text-sm text-text-muted hover:text-text-primary hover:bg-surface-raised transition-colors"
|
||||
active-class="text-accent bg-accent-muted font-semibold"
|
||||
>{{ link.label }}</RouterLink>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
|
@ -62,6 +83,7 @@ const navLinks = [
|
|||
]
|
||||
|
||||
const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||
const menuOpen = ref(false)
|
||||
const entryPointStyle = ref<'topbar' | 'fab'>('topbar')
|
||||
|
||||
function toggleTheme() {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="p-6 max-w-5xl mx-auto">
|
||||
<div class="p-4 sm:p-6 max-w-5xl mx-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
|
|
@ -23,17 +23,17 @@
|
|||
>
|
||||
<!-- Bundle header row -->
|
||||
<div
|
||||
class="flex items-center gap-3 px-4 py-3 cursor-pointer hover:bg-surface transition-colors"
|
||||
class="flex flex-wrap items-center gap-2 sm:gap-3 px-3 sm:px-4 py-3 cursor-pointer hover:bg-surface transition-colors"
|
||||
@click="toggleBundle(b)"
|
||||
>
|
||||
<span class="font-mono text-xs text-accent bg-surface px-1.5 py-0.5 rounded border border-surface-border shrink-0">
|
||||
{{ b.issue_type || 'untyped' }}
|
||||
</span>
|
||||
<span class="text-text-primary text-sm flex-1 min-w-0 truncate">{{ b.label }}</span>
|
||||
<span class="text-text-dim text-xs shrink-0">{{ b.source_host }}</span>
|
||||
<span class="text-text-dim text-xs shrink-0 hidden sm:inline">{{ b.source_host }}</span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium border shrink-0" :style="severityStyle(b.severity)">{{ b.severity }}</span>
|
||||
<span class="text-text-dim text-xs shrink-0">{{ b.entry_count }} entries</span>
|
||||
<span class="text-text-dim text-xs shrink-0">{{ formatTs(b.bundled_at) }}</span>
|
||||
<span class="text-text-dim text-xs shrink-0 hidden sm:inline">{{ formatTs(b.bundled_at) }}</span>
|
||||
<span class="text-text-dim text-xs shrink-0">{{ selected?.id === b.id ? '▲' : '▼' }}</span>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="p-6 max-w-5xl mx-auto">
|
||||
<div class="p-4 sm:p-6 max-w-5xl mx-auto">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="mb-6">
|
||||
|
|
@ -20,8 +20,8 @@
|
|||
No incidents tagged yet.
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded border border-surface-border overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<div v-else class="rounded border border-surface-border overflow-hidden overflow-x-auto">
|
||||
<table class="w-full text-sm min-w-[600px]">
|
||||
<thead class="bg-surface-raised border-b border-surface-border">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Description</th>
|
||||
|
|
@ -71,7 +71,7 @@
|
|||
<h2 class="text-text-primary text-sm font-semibold">{{ selected.label }}</h2>
|
||||
<span v-if="selected.issue_type" class="font-mono text-xs text-accent">{{ selected.issue_type }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3 mt-1 sm:mt-0">
|
||||
<button
|
||||
@click="sendBundle(selected.id)"
|
||||
:disabled="sending"
|
||||
|
|
@ -80,7 +80,25 @@
|
|||
{{ sending ? 'Sending…' : 'Send Bundle' }}
|
||||
</button>
|
||||
<span v-if="sendStatus" :class="sendStatus.ok ? 'text-green-500' : 'text-sev-error'" class="text-xs">{{ sendStatus.msg }}</span>
|
||||
<button @click="selected = null" class="text-text-dim hover:text-text-primary text-xs">✕ close</button>
|
||||
<button @click="selected = null" class="text-text-dim hover:text-text-primary text-xs ml-auto sm:ml-0">✕ close</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Incident metadata: severity + notes -->
|
||||
<div class="flex flex-wrap gap-x-6 gap-y-2 text-sm mb-4 pb-4 border-b border-surface-border">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs text-text-dim">Severity</span>
|
||||
<span class="px-2 py-0.5 rounded text-xs font-medium border" :style="severityStyle(selected.severity)">
|
||||
{{ selected.severity || 'medium' }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="selected.notes" class="flex items-start gap-2 min-w-0">
|
||||
<span class="text-xs text-text-dim shrink-0 mt-0.5">Notes</span>
|
||||
<span class="text-sm text-text-primary break-words">{{ selected.notes }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 text-xs text-text-dim">
|
||||
<span>Window</span>
|
||||
<span>{{ windowLabel(selected) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,20 @@
|
|||
<template>
|
||||
<div class="flex h-[calc(100vh-49px)]">
|
||||
<div class="flex flex-col md:flex-row md:h-[calc(100vh-49px)]">
|
||||
|
||||
<!-- Mobile filter toggle -->
|
||||
<div class="md:hidden border-b border-surface-border px-4 py-2">
|
||||
<button
|
||||
@click="sidebarOpen = !sidebarOpen"
|
||||
class="text-xs text-text-dim hover:text-text-primary transition-colors"
|
||||
>{{ sidebarOpen ? 'Hide filters ▲' : 'Filters ▼' }}</button>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar: filters -->
|
||||
<aside class="w-56 shrink-0 border-r border-surface-border bg-surface-raised p-4 flex flex-col gap-5 overflow-y-auto">
|
||||
<aside :class="[
|
||||
'bg-surface-raised p-4 flex flex-col gap-5 overflow-y-auto',
|
||||
'md:w-56 md:shrink-0 md:border-r md:border-surface-border',
|
||||
sidebarOpen ? 'flex border-b border-surface-border' : 'hidden md:flex',
|
||||
]">
|
||||
<div>
|
||||
<h3 class="text-text-dim text-xs uppercase tracking-widest mb-2">Source</h3>
|
||||
<div class="flex flex-col gap-1">
|
||||
|
|
@ -59,18 +72,18 @@
|
|||
</aside>
|
||||
|
||||
<!-- Main: search + results -->
|
||||
<main class="flex-1 flex flex-col min-w-0">
|
||||
<main class="flex-1 flex flex-col min-w-0 min-h-0">
|
||||
<!-- Search bar -->
|
||||
<div class="border-b border-surface-border p-4 flex gap-3">
|
||||
<div class="border-b border-surface-border p-3 sm:p-4 flex gap-2 sm:gap-3">
|
||||
<input
|
||||
v-model="store.query"
|
||||
type="text"
|
||||
placeholder="Search logs… (e.g. 'EAE timeout' or 'auth failed')"
|
||||
class="flex-1 bg-surface-raised border border-surface-border rounded px-4 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
|
||||
placeholder="Search logs…"
|
||||
class="flex-1 bg-surface-raised border border-surface-border rounded px-3 sm:px-4 py-2 text-sm text-text-primary placeholder-text-dim focus:outline-none focus:border-accent transition-colors"
|
||||
@keydown.enter="store.runSearch()"
|
||||
/>
|
||||
<button
|
||||
class="px-5 py-2 rounded bg-accent text-white text-sm font-semibold hover:bg-blue-400 transition-colors disabled:opacity-50"
|
||||
class="px-4 sm:px-5 py-2 rounded bg-accent text-white text-sm font-semibold hover:bg-blue-400 transition-colors disabled:opacity-50"
|
||||
:disabled="store.loading || !store.query.trim()"
|
||||
@click="store.runSearch()"
|
||||
>
|
||||
|
|
@ -86,7 +99,7 @@
|
|||
</div>
|
||||
|
||||
<!-- Empty / zero states -->
|
||||
<div v-if="!store.loading && !store.hasResults && !store.error" class="flex-1 flex flex-col items-center justify-center text-text-dim gap-3">
|
||||
<div v-if="!store.loading && !store.hasResults && !store.error" class="flex-1 flex flex-col items-center justify-center text-text-dim gap-3 px-4">
|
||||
<div v-if="!store.query" class="text-center">
|
||||
<p class="text-lg mb-1">Search your log corpus</p>
|
||||
<p class="text-sm">Type a query and press Enter or click Search.</p>
|
||||
|
|
@ -110,10 +123,11 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useSearchStore } from '@/stores/search'
|
||||
import LogEntryRow from '@/components/LogEntryRow.vue'
|
||||
|
||||
const store = useSearchStore()
|
||||
const sidebarOpen = ref(false)
|
||||
onMounted(() => store.loadSources())
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="p-6 max-w-5xl mx-auto">
|
||||
<div class="p-4 sm:p-6 max-w-5xl mx-auto">
|
||||
<div class="mb-6">
|
||||
<h1 class="text-text-primary text-xl font-semibold mb-1">Log Sources</h1>
|
||||
<p class="text-text-dim text-sm">All hosts and services in the ingested corpus.</p>
|
||||
|
|
@ -12,8 +12,8 @@
|
|||
<p class="text-sm">Run the ingest pipeline: <code class="bg-surface-raised px-1 rounded">python scripts/ingest_corpus.py</code></p>
|
||||
</div>
|
||||
|
||||
<div v-else class="rounded border border-surface-border overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<div v-else class="rounded border border-surface-border overflow-hidden overflow-x-auto">
|
||||
<table class="w-full text-sm min-w-[480px]">
|
||||
<thead class="bg-surface-raised border-b border-surface-border">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Source</th>
|
||||
|
|
|
|||
Loading…
Reference in a new issue