mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
fix(ui): use the full page width and fit phone screens
The page no longer caps its width. On very wide screens the attention list shows two columns of projects. On phones the header, the health strip and the filter chips wrap or scroll inside their own box, and each item shows its age under the text. The side panel scrolls on its own.
This commit is contained in:
+3
-3
@@ -64,7 +64,7 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<main class="mx-auto max-w-7xl px-5 py-6 sm:px-8">
|
<main class="w-full px-4 py-4 sm:px-6 sm:py-6 lg:px-10">
|
||||||
<AppHeader
|
<AppHeader
|
||||||
:last-refresh="state?.lastRefresh ?? ''"
|
:last-refresh="state?.lastRefresh ?? ''"
|
||||||
:now-ms="nowMs"
|
:now-ms="nowMs"
|
||||||
@@ -92,7 +92,7 @@ onBeforeUnmount(() => {
|
|||||||
Fetching data from GitHub
|
Fetching data from GitHub
|
||||||
</p>
|
</p>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<div class="mt-12">
|
<div class="mt-8 sm:mt-12">
|
||||||
<FleetOverview
|
<FleetOverview
|
||||||
:crit="state.counts.crit"
|
:crit="state.counts.crit"
|
||||||
:warn="state.counts.warn"
|
:warn="state.counts.warn"
|
||||||
@@ -106,7 +106,7 @@ onBeforeUnmount(() => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="mt-14 grid grid-cols-[minmax(0,1fr)] gap-x-10 gap-y-12 lg:grid-cols-[minmax(0,1fr)_23rem]"
|
class="mt-10 grid grid-cols-[minmax(0,1fr)] gap-x-10 gap-y-10 sm:mt-14 lg:grid-cols-[minmax(0,1fr)_24rem] 2xl:grid-cols-[minmax(0,1fr)_28rem]"
|
||||||
>
|
>
|
||||||
<AttentionList
|
<AttentionList
|
||||||
ref="list"
|
ref="list"
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const nextIn = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<header class="flex items-center justify-between gap-4">
|
<header class="flex flex-wrap items-center justify-between gap-x-4 gap-y-2">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<span
|
<span
|
||||||
class="border-border bg-surface text-link flex size-9 items-center justify-center rounded-md border"
|
class="border-border bg-surface text-link flex size-9 items-center justify-center rounded-md border"
|
||||||
@@ -37,8 +37,12 @@ const nextIn = computed(() => {
|
|||||||
</span>
|
</span>
|
||||||
<span class="text-lg font-semibold tracking-tight">ghtracker</span>
|
<span class="text-lg font-semibold tracking-tight">ghtracker</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-muted-foreground flex items-center gap-5 text-sm">
|
<div class="text-muted-foreground flex flex-wrap items-center gap-x-5 gap-y-1 text-sm">
|
||||||
<span v-if="updated" class="hidden sm:inline" data-test="updated">
|
<span
|
||||||
|
v-if="updated"
|
||||||
|
class="order-last w-full text-xs sm:order-none sm:w-auto sm:text-sm"
|
||||||
|
data-test="updated"
|
||||||
|
>
|
||||||
Scanned {{ updated }} ago
|
Scanned {{ updated }} ago
|
||||||
<span class="text-muted-foreground/70"> · next in {{ nextIn }}</span>
|
<span class="text-muted-foreground/70"> · next in {{ nextIn }}</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -52,7 +56,8 @@ const nextIn = computed(() => {
|
|||||||
@click="$emit('rescan')"
|
@click="$emit('rescan')"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon :icon="faArrowsRotate" :spin="scanning" aria-hidden="true" />
|
<FontAwesomeIcon :icon="faArrowsRotate" :spin="scanning" aria-hidden="true" />
|
||||||
{{ scanning ? 'Rescanning…' : 'Rescan repositories' }}
|
<span class="hidden sm:inline">{{ scanning ? 'Rescanning…' : 'Rescan repositories' }}</span>
|
||||||
|
<span class="sm:hidden">{{ scanning ? 'Scanning…' : 'Rescan' }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ defineExpose({ focusSearch: () => bar.value?.focus() })
|
|||||||
<FontAwesomeIcon :icon="faCircleCheck" class="text-ok text-xl" aria-hidden="true" />
|
<FontAwesomeIcon :icon="faCircleCheck" class="text-ok text-xl" aria-hidden="true" />
|
||||||
Nothing matches the current filters.
|
Nothing matches the current filters.
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="flex flex-col gap-4">
|
<div v-else class="grid grid-cols-[minmax(0,1fr)] items-start gap-4 2xl:grid-cols-2">
|
||||||
<RepoGroup
|
<RepoGroup
|
||||||
v-for="block in blocks"
|
v-for="block in blocks"
|
||||||
:key="block.repo.fullName"
|
:key="block.repo.fullName"
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ const ratio = computed(() => secondsSince(props.row.since, props.nowMs) / props.
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<li class="grid grid-cols-[2rem_minmax(0,1fr)_auto] items-start gap-x-3 py-3" data-test="row">
|
<li
|
||||||
|
class="grid grid-cols-[2rem_minmax(0,1fr)] items-start gap-x-3 gap-y-1 py-3 sm:grid-cols-[2rem_minmax(0,1fr)_auto]"
|
||||||
|
data-test="row"
|
||||||
|
>
|
||||||
<span
|
<span
|
||||||
class="flex size-8 items-center justify-center rounded-md"
|
class="flex size-8 items-center justify-center rounded-md"
|
||||||
:class="row.severity === 'crit' ? 'bg-crit/12 text-crit' : 'bg-warn/12 text-warn'"
|
:class="row.severity === 'crit' ? 'bg-crit/12 text-crit' : 'bg-warn/12 text-warn'"
|
||||||
@@ -31,7 +34,9 @@ const ratio = computed(() => secondsSince(props.row.since, props.nowMs) / props.
|
|||||||
{{ shorten(row.detail, 120) }}
|
{{ shorten(row.detail, 120) }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col items-end gap-1.5 pt-0.5">
|
<div
|
||||||
|
class="col-start-2 flex items-center gap-3 sm:col-start-auto sm:flex-col sm:items-end sm:gap-1.5 sm:pt-0.5"
|
||||||
|
>
|
||||||
<span class="text-muted-foreground font-mono text-xs whitespace-nowrap">
|
<span class="text-muted-foreground font-mono text-xs whitespace-nowrap">
|
||||||
{{ isZeroTime(row.since) ? '' : ageSince(row.since, nowMs) }}
|
{{ isZeroTime(row.since) ? '' : ageSince(row.since, nowMs) }}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function onOwners(value: unknown): void {
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-3" data-test="filter-bar">
|
<div class="flex flex-col gap-3" data-test="filter-bar">
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
<div class="relative min-w-56 flex-1">
|
<div class="relative min-w-0 basis-full sm:min-w-56 sm:flex-1 sm:basis-0">
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
:icon="faMagnifyingGlass"
|
:icon="faMagnifyingGlass"
|
||||||
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
class="text-muted-foreground pointer-events-none absolute top-1/2 left-3 -translate-y-1/2"
|
||||||
@@ -75,6 +75,7 @@ function onOwners(value: unknown): void {
|
|||||||
</div>
|
</div>
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="single"
|
type="single"
|
||||||
|
class="max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="filters.severity"
|
:model-value="filters.severity"
|
||||||
@@ -96,6 +97,7 @@ function onOwners(value: unknown): void {
|
|||||||
<div class="flex flex-wrap items-center gap-x-5 gap-y-2">
|
<div class="flex flex-wrap items-center gap-x-5 gap-y-2">
|
||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
type="multiple"
|
type="multiple"
|
||||||
|
class="max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="filters.kinds"
|
:model-value="filters.kinds"
|
||||||
@@ -117,6 +119,7 @@ function onOwners(value: unknown): void {
|
|||||||
<ToggleGroup
|
<ToggleGroup
|
||||||
v-if="owners.length > 1"
|
v-if="owners.length > 1"
|
||||||
type="multiple"
|
type="multiple"
|
||||||
|
class="max-w-full overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
:model-value="filters.owners"
|
:model-value="filters.owners"
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const rows = computed<{ label: string; value: number; icon: IconDefinition; tone
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section
|
<section
|
||||||
class="grid grid-cols-[minmax(0,1fr)] items-end gap-x-14 gap-y-8 lg:grid-cols-[minmax(0,1fr)_20rem]"
|
class="grid grid-cols-[minmax(0,1fr)] items-end gap-x-14 gap-y-8 lg:grid-cols-[minmax(0,1fr)_22rem]"
|
||||||
aria-label="Fleet status"
|
aria-label="Fleet status"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -68,22 +68,22 @@ const rows = computed<{ label: string; value: number; icon: IconDefinition; tone
|
|||||||
<template v-else>All {{ repos.length }} projects are healthy</template>
|
<template v-else>All {{ repos.length }} projects are healthy</template>
|
||||||
</p>
|
</p>
|
||||||
<h1
|
<h1
|
||||||
class="flex flex-wrap items-baseline gap-x-10 gap-y-1 font-semibold tracking-tight"
|
class="flex flex-wrap items-baseline gap-x-6 gap-y-1 sm:gap-x-10 font-semibold tracking-tight"
|
||||||
data-test="headline"
|
data-test="headline"
|
||||||
>
|
>
|
||||||
<template v-if="crit > 0 || warn > 0">
|
<template v-if="crit > 0 || warn > 0">
|
||||||
<span v-if="crit > 0" class="text-crit">
|
<span v-if="crit > 0" class="text-crit">
|
||||||
<span class="text-7xl leading-none tabular-nums">{{ crit }}</span>
|
<span class="text-5xl leading-none tabular-nums sm:text-7xl">{{ crit }}</span>
|
||||||
<span class="ml-2 text-xl font-medium">critical</span>
|
<span class="ml-2 text-xl font-medium">critical</span>
|
||||||
</span>
|
</span>
|
||||||
<span v-if="warn > 0" class="text-warn">
|
<span v-if="warn > 0" class="text-warn">
|
||||||
<span class="text-7xl leading-none tabular-nums">{{ warn }}</span>
|
<span class="text-5xl leading-none tabular-nums sm:text-7xl">{{ warn }}</span>
|
||||||
<span class="ml-2 text-xl font-medium">{{ warn === 1 ? 'warning' : 'warnings' }}</span>
|
<span class="ml-2 text-xl font-medium">{{ warn === 1 ? 'warning' : 'warnings' }}</span>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="text-ok text-7xl leading-none">All clear</span>
|
<span v-else class="text-ok text-5xl leading-none sm:text-7xl">All clear</span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="mt-8">
|
<div class="mt-6 sm:mt-8">
|
||||||
<FleetStrip
|
<FleetStrip
|
||||||
:repos="repos"
|
:repos="repos"
|
||||||
:counts="counts"
|
:counts="counts"
|
||||||
@@ -101,7 +101,7 @@ const rows = computed<{ label: string; value: number; icon: IconDefinition; tone
|
|||||||
<span class="flex items-center gap-1.5"
|
<span class="flex items-center gap-1.5"
|
||||||
><span class="bg-ok/40 size-2 rounded-[2px]"></span>healthy</span
|
><span class="bg-ok/40 size-2 rounded-[2px]"></span>healthy</span
|
||||||
>
|
>
|
||||||
<span class="ml-auto">Select a segment to focus a project</span>
|
<span class="sm:ml-auto">Tap a segment to focus a project</span>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,14 +24,19 @@ function summary(c: RepoCounts | undefined): string {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<TooltipProvider :delay-duration="80">
|
<TooltipProvider :delay-duration="80">
|
||||||
<div class="flex gap-1" role="group" aria-label="Project health" data-test="strip">
|
<div
|
||||||
|
class="grid grid-cols-[repeat(auto-fill,minmax(1.5rem,1fr))] gap-1"
|
||||||
|
role="group"
|
||||||
|
aria-label="Project health"
|
||||||
|
data-test="strip"
|
||||||
|
>
|
||||||
<Tooltip v-for="repo in repos" :key="repo.fullName">
|
<Tooltip v-for="repo in repos" :key="repo.fullName">
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
:aria-label="`${repo.fullName}: ${summary(counts.get(repo.fullName))}`"
|
:aria-label="`${repo.fullName}: ${summary(counts.get(repo.fullName))}`"
|
||||||
:aria-pressed="focused === repo.fullName"
|
:aria-pressed="focused === repo.fullName"
|
||||||
class="h-14 min-w-0 flex-1 rounded-[3px] transition-colors focus-visible:outline-offset-1"
|
class="h-9 min-w-0 rounded-[3px] sm:h-11 transition-colors focus-visible:outline-offset-1"
|
||||||
:class="[
|
:class="[
|
||||||
SEGMENT[repo.health],
|
SEGMENT[repo.health],
|
||||||
focused === repo.fullName
|
focused === repo.fullName
|
||||||
|
|||||||
@@ -26,7 +26,10 @@ const LABEL = { crit: 'Critical', warn: 'Warning', ok: 'Healthy' } as const
|
|||||||
>
|
>
|
||||||
No projects match the search.
|
No projects match the search.
|
||||||
</p>
|
</p>
|
||||||
<ul v-else class="border-border bg-surface divide-border divide-y rounded-lg border">
|
<ul
|
||||||
|
v-else
|
||||||
|
class="border-border bg-surface divide-border max-h-[70vh] divide-y overflow-y-auto rounded-lg border lg:max-h-[calc(100vh-7rem)]"
|
||||||
|
>
|
||||||
<li
|
<li
|
||||||
v-for="repo in repos"
|
v-for="repo in repos"
|
||||||
:key="repo.fullName"
|
:key="repo.fullName"
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ describe('AppHeader', () => {
|
|||||||
|
|
||||||
it('emits rescan and shows progress while a scan runs', async () => {
|
it('emits rescan and shows progress while a scan runs', async () => {
|
||||||
const w = mount(AppHeader, { props: { ...base, manualRefresh: true } })
|
const w = mount(AppHeader, { props: { ...base, manualRefresh: true } })
|
||||||
expect(w.get('[data-test=rescan]').text()).toBe('Rescan repositories')
|
expect(w.get('[data-test=rescan]').text()).toContain('Rescan repositories')
|
||||||
await w.get('[data-test=rescan]').trigger('click')
|
await w.get('[data-test=rescan]').trigger('click')
|
||||||
expect(w.emitted('rescan')).toHaveLength(1)
|
expect(w.emitted('rescan')).toHaveLength(1)
|
||||||
const busy = mount(AppHeader, { props: { ...base, manualRefresh: true, scanning: true } })
|
const busy = mount(AppHeader, { props: { ...base, manualRefresh: true, scanning: true } })
|
||||||
expect(busy.get('[data-test=rescan]').attributes('disabled')).toBeDefined()
|
expect(busy.get('[data-test=rescan]').attributes('disabled')).toBeDefined()
|
||||||
expect(busy.get('[data-test=rescan]').text()).toBe('Rescanning…')
|
expect(busy.get('[data-test=rescan]').text()).toContain('Rescanning…')
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user