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:
pyr0ball 2026-05-16 02:11:58 -07:00
parent b5c7fe67bf
commit 7bf9df4ba3
5 changed files with 96 additions and 42 deletions

View file

@ -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() {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>