fix(ui): stack page headers and toolbars on mobile

Non-wrapping justify-between headers now stack below sm; fixed-width searches go full-width. Calendar header wraps and seeds isMobile at setup to avoid the month-grid flash.
This commit is contained in:
Paulo Castellano 2026-07-17 18:38:20 -03:00
parent ccf888258d
commit 49282d2acb
8 changed files with 28 additions and 25 deletions

View file

@ -83,7 +83,7 @@ const changeRole = (member: Member, role: string) => {
<template>
<div class="flex flex-col space-y-6">
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<HeadingSmall
:title="$t('settings.workspace.members_heading')"
:description="$t('settings.workspace.members_description')"
@ -115,7 +115,7 @@ const changeRole = (member: Member, role: string) => {
<TableCell>
<DropdownMenu v-if="canManageTeam && member.id !== currentUserId">
<DropdownMenuTrigger as-child>
<Button variant="outline" size="icon" class="size-8">
<Button variant="outline" size="icon" class="size-9">
<IconDots class="size-4" />
</Button>
</DropdownMenuTrigger>

View file

@ -72,7 +72,7 @@ const platformLabel = (platform: string): string => platform.charAt(0).toUpperCa
<template>
<AutomationDetailLayout :automation="automation" current="metrics">
<div class="space-y-6 p-4">
<div class="flex items-center justify-between gap-2">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<h2 class="text-sm font-semibold text-foreground/70">{{ $t('automations.metrics.overview') }}</h2>
<DateRangePicker v-model="dateRange" dusk="metrics-range" />
</div>
@ -85,9 +85,9 @@ const platformLabel = (platform: string): string => platform.charAt(0).toUpperCa
</div>
<div class="rounded-xl border-2 border-foreground/10 bg-card p-4">
<div class="mb-3 flex items-center justify-between">
<div class="mb-3 flex flex-wrap items-center justify-between gap-2">
<h3 class="text-sm font-semibold">{{ $t('automations.metrics.runs_over_time') }}</h3>
<div class="flex items-center gap-4">
<div class="flex flex-wrap items-center gap-4">
<div v-for="item in legend" :key="item.key" class="flex items-center gap-1.5">
<span class="size-2.5 rounded-full" :style="{ backgroundColor: item.color }"></span>
<span class="text-xs text-foreground/60">{{ $t(item.label) }}</span>

View file

@ -85,13 +85,13 @@ const hasActiveSearch = computed(() => Boolean(searchQuery.value?.trim()));
<div class="flex h-full flex-1 flex-col gap-6 px-6 py-8">
<PageHeader :title="$t('labels.title')" :description="$t('labels.description')" />
<div class="flex items-center justify-between gap-3">
<div class="relative">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="relative w-full sm:w-auto">
<IconSearch class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
v-model="searchQuery"
:placeholder="trans('labels.search')"
class="w-64 pl-9"
class="w-full pl-9 sm:w-64"
/>
</div>

View file

@ -52,8 +52,9 @@ interface Props {
const props = defineProps<Props>();
// Mobile detection
const isMobile = ref(false);
// Mobile detection — seed from the viewport at setup (available on client render
// and SPA navigations) so a narrow screen never paints the month grid for a frame.
const isMobile = ref(typeof window !== 'undefined' ? window.innerWidth < 1024 : false);
const { canCreatePost } = useWorkspaceRole();
const createPostUrl = (isoDate: string | null = null) =>
@ -253,7 +254,7 @@ const formatTime = (scheduledAt: string): string => {
<AppLayout :fullWidth="true">
<div class="flex flex-col h-full">
<header class="grid shrink-0 grid-cols-[auto_1fr_auto] items-center gap-3 border-b-2 border-foreground bg-card px-4 py-3 md:px-6">
<header class="flex shrink-0 flex-wrap items-center justify-between gap-3 border-b-2 border-foreground bg-card px-4 py-3 md:grid md:grid-cols-[auto_1fr_auto] md:px-6">
<div class="flex items-center gap-2 pl-12 md:pl-0">
<Button variant="outline" size="icon" @click="navigate(-1)">
<IconChevronLeft class="size-4" />
@ -264,14 +265,16 @@ const formatTime = (scheduledAt: string): string => {
<Button variant="outline" size="icon" @click="navigate(1)">
<IconChevronRight class="size-4" />
</Button>
<DatePicker v-if="isMobile" v-model="selectedDate" @update:model-value="(v: any) => goToDate(v)" />
</div>
<div class="flex items-center justify-center">
<div class="hidden items-center justify-center md:flex">
<span class="truncate text-sm font-bold capitalize text-foreground">
{{ headerTitle }}
</span>
</div>
<div class="flex items-center gap-2">
<div v-if="isMobile" class="w-44">
<DatePicker v-model="selectedDate" :show-time="false" @update:model-value="(v: any) => goToDate(v)" />
</div>
<Tabs v-if="!isMobile" :default-value="view" @update:model-value="switchView">
<TabsList>
<TabsTrigger value="day">{{ $t('calendar.day') }}</TabsTrigger>

View file

@ -183,14 +183,14 @@ useWorkspaceEcho(
<PageHeader :title="pageTitle" />
<!-- Toolbar -->
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-3">
<div class="relative">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="flex w-full items-center gap-3 sm:w-auto">
<div class="relative w-full sm:w-auto">
<IconSearch class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-foreground/60" />
<Input
v-model="searchQuery"
:placeholder="trans('posts.search')"
class="w-64 pl-9"
class="w-full pl-9 sm:w-64"
/>
</div>

View file

@ -73,7 +73,7 @@ const tabs = computed(() => [
<SettingsTabsNav :tabs="tabs" active="api-keys" />
<div class="flex items-center justify-between gap-4">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<HeadingSmall
:title="$t('settings.api_keys.heading')"
:description="$t('settings.api_keys.description')"
@ -128,7 +128,7 @@ const tabs = computed(() => [
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" size="icon" class="size-8">
<Button variant="outline" size="icon" class="size-9">
<IconDots class="size-4" />
</Button>
</DropdownMenuTrigger>

View file

@ -91,13 +91,13 @@ const hasActiveSearch = computed(() => Boolean(searchQuery.value?.trim()));
<div class="flex h-full flex-1 flex-col gap-6 px-6 py-8">
<PageHeader :title="$t('signatures.title')" />
<div class="flex items-center justify-between gap-3">
<div class="relative">
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="relative w-full sm:w-auto">
<IconSearch class="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
v-model="searchQuery"
:placeholder="trans('signatures.search')"
class="w-64 pl-9"
class="w-full pl-9 sm:w-64"
/>
</div>
@ -130,7 +130,7 @@ const hasActiveSearch = computed(() => Boolean(searchQuery.value?.trim()));
@click="openEditDialog(signature)"
>
<TableCell>{{ signature.name }}</TableCell>
<TableCell class="max-w-md">
<TableCell class="max-w-[160px] sm:max-w-md">
<p class="truncate">{{ signature.content }}</p>
</TableCell>
<TableCell>{{ formatDate(signature.created_at) }}</TableCell>

View file

@ -41,14 +41,14 @@ defineProps<Props>();
<AppLayout :title="workspace.name">
<div class="flex flex-col gap-6 p-6">
<div class="flex items-center justify-between">
<div class="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div>
<h1 class="text-2xl font-bold tracking-tight">{{ workspace.name }}</h1>
<p class="text-muted-foreground">
Workspace dashboard
</p>
</div>
<div class="flex gap-2">
<div class="flex flex-wrap gap-2">
<Link :href="settings.url()">
<Button variant="ghost" size="icon">
<IconSettings class="h-4 w-4" />