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
b5c7fe67bf
commit
7bf9df4ba3
5 changed files with 96 additions and 42 deletions
|
|
@ -1,34 +1,55 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="min-h-screen bg-surface font-mono text-text-primary">
|
<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="flex items-center gap-2">
|
<div class="px-4 sm:px-6 py-3 flex items-center gap-3">
|
||||||
<span class="text-accent font-semibold tracking-wide">TURNSTONE</span>
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-text-dim text-xs">diagnostic intelligence</span>
|
<span class="text-accent font-semibold tracking-wide">TURNSTONE</span>
|
||||||
|
<span class="text-text-dim text-xs hidden sm:inline">diagnostic intelligence</span>
|
||||||
|
</div>
|
||||||
|
<!-- Desktop nav links -->
|
||||||
|
<div class="hidden md:flex gap-1 ml-4">
|
||||||
|
<RouterLink
|
||||||
|
v-for="link in navLinks"
|
||||||
|
:key="link.to"
|
||||||
|
:to="link.to"
|
||||||
|
class="px-3 py-1 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>
|
||||||
|
<div class="ml-auto flex items-center gap-3">
|
||||||
|
<RouterLink
|
||||||
|
to="/settings"
|
||||||
|
class="text-text-dim hover:text-text-primary transition-colors leading-none"
|
||||||
|
title="Settings"
|
||||||
|
aria-label="Settings"
|
||||||
|
>⚙</RouterLink>
|
||||||
|
<button
|
||||||
|
@click="toggleTheme"
|
||||||
|
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
||||||
|
class="text-text-dim hover:text-text-primary transition-colors text-base leading-none px-1"
|
||||||
|
: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>
|
</div>
|
||||||
<div class="flex gap-1 ml-4">
|
<!-- 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
|
<RouterLink
|
||||||
v-for="link in navLinks"
|
v-for="link in navLinks"
|
||||||
:key="link.to"
|
:key="link.to"
|
||||||
:to="link.to"
|
:to="link.to"
|
||||||
class="px-3 py-1 rounded text-sm text-text-muted hover:text-text-primary hover:bg-surface-raised transition-colors"
|
@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"
|
active-class="text-accent bg-accent-muted font-semibold"
|
||||||
>{{ link.label }}</RouterLink>
|
>{{ link.label }}</RouterLink>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-auto flex items-center gap-3">
|
|
||||||
<RouterLink
|
|
||||||
to="/settings"
|
|
||||||
class="text-text-dim hover:text-text-primary transition-colors leading-none"
|
|
||||||
title="Settings"
|
|
||||||
aria-label="Settings"
|
|
||||||
>⚙</RouterLink>
|
|
||||||
<button
|
|
||||||
@click="toggleTheme"
|
|
||||||
:title="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
||||||
class="text-text-dim hover:text-text-primary transition-colors text-base leading-none px-1"
|
|
||||||
:aria-label="isDark ? 'Switch to light mode' : 'Switch to dark mode'"
|
|
||||||
>{{ isDark ? '☀' : '☾' }}</button>
|
|
||||||
<StatusDot />
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Persistent quick-capture bar (topbar mode) -->
|
<!-- Persistent quick-capture bar (topbar mode) -->
|
||||||
|
|
@ -62,6 +83,7 @@ const navLinks = [
|
||||||
]
|
]
|
||||||
|
|
||||||
const isDark = ref(document.documentElement.classList.contains('dark'))
|
const isDark = ref(document.documentElement.classList.contains('dark'))
|
||||||
|
const menuOpen = ref(false)
|
||||||
const entryPointStyle = ref<'topbar' | 'fab'>('topbar')
|
const entryPointStyle = ref<'topbar' | 'fab'>('topbar')
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 max-w-5xl mx-auto">
|
<div class="p-4 sm:p-6 max-w-5xl mx-auto">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
|
|
@ -23,17 +23,17 @@
|
||||||
>
|
>
|
||||||
<!-- Bundle header row -->
|
<!-- Bundle header row -->
|
||||||
<div
|
<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)"
|
@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">
|
<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' }}
|
{{ b.issue_type || 'untyped' }}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-text-primary text-sm flex-1 min-w-0 truncate">{{ b.label }}</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="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">{{ 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>
|
<span class="text-text-dim text-xs shrink-0">{{ selected?.id === b.id ? '▲' : '▼' }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="p-6 max-w-5xl mx-auto">
|
<div class="p-4 sm:p-6 max-w-5xl mx-auto">
|
||||||
|
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
No incidents tagged yet.
|
No incidents tagged yet.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-else class="rounded border border-surface-border overflow-hidden">
|
<div v-else class="rounded border border-surface-border overflow-hidden overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm min-w-[600px]">
|
||||||
<thead class="bg-surface-raised border-b border-surface-border">
|
<thead class="bg-surface-raised border-b border-surface-border">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Description</th>
|
<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>
|
<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>
|
<span v-if="selected.issue_type" class="font-mono text-xs text-accent">{{ selected.issue_type }}</span>
|
||||||
</div>
|
</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
|
<button
|
||||||
@click="sendBundle(selected.id)"
|
@click="sendBundle(selected.id)"
|
||||||
:disabled="sending"
|
:disabled="sending"
|
||||||
|
|
@ -80,7 +80,25 @@
|
||||||
{{ sending ? 'Sending…' : 'Send Bundle' }}
|
{{ sending ? 'Sending…' : 'Send Bundle' }}
|
||||||
</button>
|
</button>
|
||||||
<span v-if="sendStatus" :class="sendStatus.ok ? 'text-green-500' : 'text-sev-error'" class="text-xs">{{ sendStatus.msg }}</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,20 @@
|
||||||
<template>
|
<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 -->
|
<!-- 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>
|
<div>
|
||||||
<h3 class="text-text-dim text-xs uppercase tracking-widest mb-2">Source</h3>
|
<h3 class="text-text-dim text-xs uppercase tracking-widest mb-2">Source</h3>
|
||||||
<div class="flex flex-col gap-1">
|
<div class="flex flex-col gap-1">
|
||||||
|
|
@ -59,18 +72,18 @@
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Main: search + results -->
|
<!-- 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 -->
|
<!-- 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
|
<input
|
||||||
v-model="store.query"
|
v-model="store.query"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Search logs… (e.g. 'EAE timeout' or 'auth failed')"
|
placeholder="Search logs…"
|
||||||
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"
|
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()"
|
@keydown.enter="store.runSearch()"
|
||||||
/>
|
/>
|
||||||
<button
|
<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()"
|
:disabled="store.loading || !store.query.trim()"
|
||||||
@click="store.runSearch()"
|
@click="store.runSearch()"
|
||||||
>
|
>
|
||||||
|
|
@ -86,7 +99,7 @@
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Empty / zero states -->
|
<!-- 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">
|
<div v-if="!store.query" class="text-center">
|
||||||
<p class="text-lg mb-1">Search your log corpus</p>
|
<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>
|
<p class="text-sm">Type a query and press Enter or click Search.</p>
|
||||||
|
|
@ -110,10 +123,11 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { useSearchStore } from '@/stores/search'
|
import { useSearchStore } from '@/stores/search'
|
||||||
import LogEntryRow from '@/components/LogEntryRow.vue'
|
import LogEntryRow from '@/components/LogEntryRow.vue'
|
||||||
|
|
||||||
const store = useSearchStore()
|
const store = useSearchStore()
|
||||||
|
const sidebarOpen = ref(false)
|
||||||
onMounted(() => store.loadSources())
|
onMounted(() => store.loadSources())
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<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">
|
<div class="mb-6">
|
||||||
<h1 class="text-text-primary text-xl font-semibold mb-1">Log Sources</h1>
|
<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>
|
<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>
|
<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>
|
||||||
|
|
||||||
<div v-else class="rounded border border-surface-border overflow-hidden">
|
<div v-else class="rounded border border-surface-border overflow-hidden overflow-x-auto">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm min-w-[480px]">
|
||||||
<thead class="bg-surface-raised border-b border-surface-border">
|
<thead class="bg-surface-raised border-b border-surface-border">
|
||||||
<tr>
|
<tr>
|
||||||
<th class="text-left px-4 py-2.5 text-text-dim font-medium text-xs uppercase tracking-wider">Source</th>
|
<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