feat: improve mobile layout responsiveness

This commit is contained in:
rcourtman 2025-10-22 12:39:53 +00:00
parent 9d1b9a78c4
commit 4ba5dcc785
7 changed files with 178 additions and 54 deletions

View file

@ -21,6 +21,7 @@ import Settings from './components/Settings/Settings';
import { Alerts } from './pages/Alerts'; import { Alerts } from './pages/Alerts';
import { DockerHosts } from './components/Docker/DockerHosts'; import { DockerHosts } from './components/Docker/DockerHosts';
import { ToastContainer } from './components/Toast/Toast'; import { ToastContainer } from './components/Toast/Toast';
import NotificationContainer from './components/NotificationContainer';
import { ErrorBoundary } from './components/ErrorBoundary'; import { ErrorBoundary } from './components/ErrorBoundary';
import { SecurityWarning } from './components/SecurityWarning'; import { SecurityWarning } from './components/SecurityWarning';
import { Login } from './components/Login'; import { Login } from './components/Login';
@ -584,6 +585,7 @@ function App() {
</AppLayout> </AppLayout>
</div> </div>
<ToastContainer /> <ToastContainer />
<NotificationContainer />
<TokenRevealDialog /> <TokenRevealDialog />
<TooltipRoot /> <TooltipRoot />
</DarkModeContext.Provider> </DarkModeContext.Provider>
@ -784,12 +786,34 @@ function AppLayout(props: {
navigate(tab.route); navigate(tab.route);
}; };
type PlatformTab = ReturnType<typeof platformTabs>[number];
type UtilityTab = ReturnType<typeof utilityTabs>[number];
const mobileTabs = createMemo<
Array<(PlatformTab & { type: 'platform' }) | (UtilityTab & { type: 'utility' })>
>(() => {
const platformEntries = platformTabs().map((tab) => ({ ...tab, type: 'platform' as const }));
const utilityEntries = utilityTabs().map((tab) => ({ ...tab, type: 'utility' as const }));
return [...platformEntries, ...utilityEntries];
});
const handleMobileTabChange = (tabId: PlatformTab['id'] | UtilityTab['id']) => {
const platform = platformTabs().find((tab) => tab.id === tabId);
if (platform) {
handlePlatformClick(platform);
return;
}
const utility = utilityTabs().find((tab) => tab.id === tabId);
if (utility) {
handleUtilityClick(utility);
}
};
return ( return (
<div class="pulse-shell"> <div class="pulse-shell">
{/* Header */} {/* Header */}
<div class="header flex flex-row justify-between items-center mb-2"> <div class="header mb-3 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div class="hidden md:block md:flex-1"></div> <div class="flex items-center justify-between gap-2 sm:gap-3">
<div class="flex items-center gap-1 flex-none">
<svg <svg
width="20" width="20"
height="20" height="20"
@ -824,8 +848,8 @@ function AppLayout(props: {
</span> </span>
</Show> </Show>
</div> </div>
<div class="header-controls flex justify-end items-center gap-4 md:flex-1"> <div class="header-controls flex w-full flex-wrap items-center gap-3 justify-start sm:w-auto sm:flex-1 sm:justify-end">
<div class="flex items-center gap-2"> <div class="flex w-full flex-wrap items-center gap-2 justify-start sm:w-auto sm:justify-end">
<div <div
class={`group status text-xs rounded-full flex items-center justify-center transition-all duration-500 ease-in-out px-1.5 ${ class={`group status text-xs rounded-full flex items-center justify-center transition-all duration-500 ease-in-out px-1.5 ${
props.connected() props.connected()
@ -904,9 +928,41 @@ function AppLayout(props: {
</div> </div>
</div> </div>
{/* Mobile Tab Switcher */}
<div class="sm:hidden mb-3">
<label for="mobile-tab-switcher" class="sr-only">
Navigate between tabs
</label>
<div class="relative">
<select
id="mobile-tab-switcher"
value={getActiveTab()}
onChange={(event) => handleMobileTabChange(event.currentTarget.value as PlatformTab['id'] | UtilityTab['id'])}
class="w-full appearance-none rounded-lg border border-gray-300 bg-white py-2 pl-3 pr-9 text-sm font-medium text-gray-700 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-200 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-200 dark:focus:border-blue-400 dark:focus:ring-blue-500/40"
>
<For each={mobileTabs()}>
{(tab) => (
<option value={tab.id}>
{tab.type === 'platform' && !tab.enabled ? `${tab.label} (Configure)` : tab.label}
</option>
)}
</For>
</select>
<div class="pointer-events-none absolute inset-y-0 right-3 flex items-center text-gray-400 dark:text-gray-500">
<svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 0 1 1.06.02L10 10.94l3.71-3.71a.75.75 0 1 1 1.06 1.06l-4.24 4.24a.75.75 0 0 1-1.06 0L5.21 8.29a.75.75 0 0 1 .02-1.08z"
clip-rule="evenodd"
/>
</svg>
</div>
</div>
</div>
{/* Tabs */} {/* Tabs */}
<div <div
class="tabs flex items-end gap-2 mb-2 border-b border-gray-300 dark:border-gray-700 overflow-x-auto overflow-y-hidden whitespace-nowrap scrollbar-hide" class="tabs mb-2 flex items-end gap-2 overflow-x-auto overflow-y-hidden whitespace-nowrap border-b border-gray-300 dark:border-gray-700 scrollbar-hide"
role="tablist" role="tablist"
aria-label="Primary navigation" aria-label="Primary navigation"
> >
@ -1008,7 +1064,10 @@ function AppLayout(props: {
})()} })()}
</span> </span>
<Show when={tab.badge === 'update'}> <Show when={tab.badge === 'update'}>
<span class="ml-1 w-2 h-2 bg-red-500 rounded-full animate-pulse"></span> <span class="ml-1 flex items-center">
<span class="sr-only">Update available</span>
<span aria-hidden="true" class="block h-2 w-2 rounded-full bg-red-500 animate-pulse"></span>
</span>
</Show> </Show>
</div> </div>
); );

View file

@ -1353,7 +1353,7 @@ const UnifiedBackups: Component = () => {
{/* Available Backups chart - keep visible while backups exist or a date filter is active */} {/* Available Backups chart - keep visible while backups exist or a date filter is active */}
<Show when={hasAnyBackups() || selectedDateRange() !== null}> <Show when={hasAnyBackups() || selectedDateRange() !== null}>
<Card padding="md"> <Card padding="md">
<div class="mb-3 flex items-start justify-between gap-3"> <div class="mb-3 flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
<SectionHeader <SectionHeader
title={ title={
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
@ -1416,7 +1416,7 @@ const UnifiedBackups: Component = () => {
</div> </div>
)} )}
</Show> </Show>
<div class="flex flex-wrap items-center justify-end gap-2"> <div class="flex flex-wrap items-center justify-start gap-2 sm:justify-end">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<button <button
type="button" type="button"
@ -1463,7 +1463,7 @@ const UnifiedBackups: Component = () => {
1y 1y
</button> </button>
</div> </div>
<div class="h-4 w-px bg-gray-300 dark:bg-gray-600"></div> <div class="hidden h-4 w-px bg-gray-300 dark:bg-gray-600 sm:block"></div>
<span class="text-gray-500 dark:text-gray-400">Last {chartTimeRange()} days</span> <span class="text-gray-500 dark:text-gray-400">Last {chartTimeRange()} days</span>
<Show when={selectedDateRange()}> <Show when={selectedDateRange()}>
<button <button
@ -1480,7 +1480,7 @@ const UnifiedBackups: Component = () => {
</div> </div>
</div> </div>
</div> </div>
<div class="h-32 relative bg-gray-100 dark:bg-gray-800 rounded overflow-hidden"> <div class="relative min-h-[14rem] sm:min-h-[12rem] bg-gray-100 dark:bg-gray-800 rounded overflow-hidden">
<Show <Show
when={chartData().data.length > 0} when={chartData().data.length > 0}
fallback={ fallback={
@ -1511,7 +1511,9 @@ const UnifiedBackups: Component = () => {
> >
<svg <svg
class="available-backups-svg w-full h-full" class="available-backups-svg w-full h-full"
style="cursor: pointer" role="img"
aria-label="Available backups chart"
style="cursor: pointer; touch-action: pan-y;"
ref={(el) => { ref={(el) => {
// Use createEffect to reactively update the chart // Use createEffect to reactively update the chart
createEffect(() => { createEffect(() => {

View file

@ -4,6 +4,7 @@ import { Card } from '@/components/shared/Card';
import { MetricBar } from '@/components/Dashboard/MetricBar'; import { MetricBar } from '@/components/Dashboard/MetricBar';
import { renderDockerStatusBadge } from './DockerStatusBadge'; import { renderDockerStatusBadge } from './DockerStatusBadge';
import { formatUptime } from '@/utils/format'; import { formatUptime } from '@/utils/format';
import { ScrollableTable } from '@/components/shared/ScrollableTable';
export interface DockerHostSummary { export interface DockerHostSummary {
host: DockerHost; host: DockerHost;
@ -114,15 +115,15 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
return ( return (
<Card padding="none" class="mb-4 overflow-hidden"> <Card padding="none" class="mb-4 overflow-hidden">
<div class="overflow-x-auto"> <ScrollableTable minWidth="720px">
<table class="w-full min-w-[720px] border-collapse"> <table class="w-full border-collapse">
<thead> <thead>
<tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600"> <tr class="bg-gray-50 dark:bg-gray-700/50 text-gray-600 dark:text-gray-300 border-b border-gray-200 dark:border-gray-600">
<th <th
class="pl-3 pr-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-1/4 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500" class="pl-3 pr-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider w-1/4 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500"
onClick={() => handleSort('name')} onClick={() => handleSort('name')}
onKeyDown={(e) => e.key === 'Enter' && handleSort('name')} onKeyDown={(e) => e.key === 'Enter' && handleSort('name')}
tabindex="0" tabIndex={0}
role="button" role="button"
aria-label={`Sort by host ${sortKey() === 'name' ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : ''}`} aria-label={`Sort by host ${sortKey() === 'name' ? (sortDirection() === 'asc' ? 'ascending' : 'descending') : ''}`}
> >
@ -150,19 +151,19 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
Containers {renderSortIndicator('running')} Containers {renderSortIndicator('running')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
onClick={() => handleSort('uptime')} onClick={() => handleSort('uptime')}
> >
Uptime {renderSortIndicator('uptime')} Uptime {renderSortIndicator('uptime')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-32 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-32 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
onClick={() => handleSort('lastSeen')} onClick={() => handleSort('lastSeen')}
> >
Last Update {renderSortIndicator('lastSeen')} Last Update {renderSortIndicator('lastSeen')}
</th> </th>
<th <th
class="px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600" class="hidden px-2 py-1.5 text-left text-[11px] sm:text-xs font-medium uppercase tracking-wider min-w-24 cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-600 sm:table-cell"
onClick={() => handleSort('agent')} onClick={() => handleSort('agent')}
> >
Agent {renderSortIndicator('agent')} Agent {renderSortIndicator('agent')}
@ -174,6 +175,8 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
{(summary) => { {(summary) => {
const selected = props.selectedHostId() === summary.host.id; const selected = props.selectedHostId() === summary.host.id;
const online = isHostOnline(summary.host); const online = isHostOnline(summary.host);
const uptimeLabel = summary.uptimeSeconds ? formatUptime(summary.uptimeSeconds) : '—';
const agentLabel = summary.host.agentVersion ? `v${summary.host.agentVersion}` : '—';
const rowStyle = () => { const rowStyle = () => {
const styles: Record<string, string> = {}; const styles: Record<string, string> = {};
@ -206,14 +209,16 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
return className; return className;
}; };
const agentOutdated = isAgentOutdated(summary.host.agentVersion);
return ( return (
<tr <tr
class={rowClass()} class={rowClass()}
style={rowStyle()} style={rowStyle()}
onClick={() => props.onSelect(summary.host.id)} onClick={() => props.onSelect(summary.host.id)}
> >
<td class="pr-2 py-0.5 pl-3 whitespace-nowrap"> <td class="pr-2 py-1 pl-3 align-top">
<div class="flex items-center gap-1"> <div class="flex flex-wrap items-center gap-1">
<span class="font-medium text-[11px] text-gray-900 dark:text-gray-100"> <span class="font-medium text-[11px] text-gray-900 dark:text-gray-100">
{summary.host.displayName} {summary.host.displayName}
</span> </span>
@ -228,11 +233,51 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
</span> </span>
</Show> </Show>
</div> </div>
<div class="mt-2 grid grid-cols-1 gap-1 text-[10px] text-gray-500 dark:text-gray-400 sm:hidden">
<div class="flex items-center gap-1">
<span class="font-semibold text-gray-600 dark:text-gray-300">Uptime:</span>
<span>{uptimeLabel}</span>
</div>
<div class="flex items-start gap-1">
<span class="font-semibold text-gray-600 dark:text-gray-300">Last:</span>
<span class="flex-1">
<span>{summary.lastSeenRelative}</span>
<Show when={summary.lastSeenAbsolute}>
<span class="block text-[9px] text-gray-400 dark:text-gray-500">
{summary.lastSeenAbsolute}
</span>
</Show>
</span>
</div>
<div class="flex flex-wrap items-center gap-1">
<span class="font-semibold text-gray-600 dark:text-gray-300">Agent:</span>
<span
class={
agentOutdated
? 'rounded px-1 py-0.5 bg-yellow-100 dark:bg-yellow-900/20 text-yellow-700 dark:text-yellow-400 font-medium'
: 'rounded px-1 py-0.5 bg-green-100 dark:bg-green-900/20 text-green-700 dark:text-green-400 font-medium'
}
>
{agentLabel}
</span>
<Show when={agentOutdated}>
<span class="text-[9px] font-medium text-yellow-600 dark:text-yellow-500">
Update recommended
</span>
</Show>
</div>
<Show when={summary.host.intervalSeconds}>
<div class="flex items-center gap-1">
<span class="font-semibold text-gray-600 dark:text-gray-300">Interval:</span>
<span>{summary.host.intervalSeconds}s</span>
</div>
</Show>
</div>
</td> </td>
<td class="px-2 py-0.5"> <td class="px-2 py-1 align-top">
{renderDockerStatusBadge(summary.host.status)} {renderDockerStatusBadge(summary.host.status)}
</td> </td>
<td class="px-2 py-0.5"> <td class="px-2 py-1 align-top">
<Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500"></span>}> <Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500"></span>}>
<MetricBar <MetricBar
value={summary.cpuPercent} value={summary.cpuPercent}
@ -241,7 +286,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
/> />
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5"> <td class="px-2 py-1 align-top">
<Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500"></span>}> <Show when={online} fallback={<span class="text-xs text-gray-400 dark:text-gray-500"></span>}>
<MetricBar <MetricBar
value={summary.memoryPercent} value={summary.memoryPercent}
@ -251,8 +296,8 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
/> />
</Show> </Show>
</td> </td>
<td class="px-2 py-0.5"> <td class="px-2 py-1 align-top">
<div class="flex justify-center"> <div class="flex justify-start sm:justify-center">
<MetricBar <MetricBar
value={summary.runningPercent} value={summary.runningPercent}
label={`${summary.runningCount}/${summary.totalCount}`} label={`${summary.runningCount}/${summary.totalCount}`}
@ -260,38 +305,40 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
/> />
</div> </div>
</td> </td>
<td class="px-2 py-0.5 whitespace-nowrap"> <td class="hidden px-2 py-1 align-top whitespace-nowrap sm:table-cell">
<span class="text-xs text-gray-600 dark:text-gray-400"> <span class="text-xs text-gray-600 dark:text-gray-400">
{summary.uptimeSeconds ? formatUptime(summary.uptimeSeconds) : '—'} {uptimeLabel}
</span> </span>
</td> </td>
<td class="px-2 py-0.5"> <td class="hidden px-2 py-1 align-top sm:table-cell">
<div class="text-sm text-gray-900 dark:text-gray-100"> <div class="text-sm text-gray-900 dark:text-gray-100">
{summary.lastSeenRelative} {summary.lastSeenRelative}
</div> </div>
<div class="text-xs text-gray-500 dark:text-gray-400"> <Show when={summary.lastSeenAbsolute}>
{summary.lastSeenAbsolute} <div class="text-xs text-gray-500 dark:text-gray-400">
</div> {summary.lastSeenAbsolute}
</div>
</Show>
</td> </td>
<td class="px-2 py-0.5"> <td class="hidden px-2 py-1 align-top sm:table-cell">
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<Show when={summary.host.agentVersion}> <Show when={summary.host.agentVersion}>
<div class="flex flex-col gap-0.5"> <div class="flex flex-col gap-0.5">
<span <span
class={ class={
isAgentOutdated(summary.host.agentVersion) agentOutdated
? 'text-[10px] px-1.5 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 font-medium inline-block w-fit' ? 'text-[10px] px-1.5 py-0.5 rounded bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 font-medium inline-block w-fit'
: 'text-[10px] px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-medium inline-block w-fit' : 'text-[10px] px-1.5 py-0.5 rounded bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 font-medium inline-block w-fit'
} }
title={ title={
isAgentOutdated(summary.host.agentVersion) agentOutdated
? 'Outdated - Update recommended' ? 'Outdated - Update recommended'
: 'Up to date - Auto-update enabled' : 'Up to date - Auto-update enabled'
} }
> >
v{summary.host.agentVersion} v{summary.host.agentVersion}
</span> </span>
<Show when={isAgentOutdated(summary.host.agentVersion)}> <Show when={agentOutdated}>
<span class="text-[9px] text-yellow-600 dark:text-yellow-500 font-medium"> <span class="text-[9px] text-yellow-600 dark:text-yellow-500 font-medium">
Update available Update available
</span> </span>
@ -311,7 +358,7 @@ export const DockerHostSummaryTable: Component<DockerHostSummaryTableProps> = (p
</For> </For>
</tbody> </tbody>
</table> </table>
</div> </ScrollableTable>
</Card> </Card>
); );
}; };

View file

@ -1,4 +1,4 @@
import { Component, JSX, createSignal, createEffect, onMount } from 'solid-js'; import { Component, JSX, createSignal, createEffect, onMount, onCleanup } from 'solid-js';
import { Show } from 'solid-js'; import { Show } from 'solid-js';
interface ScrollableTableProps { interface ScrollableTableProps {
@ -23,6 +23,9 @@ export const ScrollableTable: Component<ScrollableTableProps> = (props) => {
onMount(() => { onMount(() => {
checkScroll(); checkScroll();
window.addEventListener('resize', checkScroll); window.addEventListener('resize', checkScroll);
onCleanup(() => {
window.removeEventListener('resize', checkScroll);
});
}); });
createEffect(() => { createEffect(() => {
@ -43,7 +46,7 @@ export const ScrollableTable: Component<ScrollableTableProps> = (props) => {
<div <div
ref={scrollContainer} ref={scrollContainer}
class="overflow-x-auto" class="overflow-x-auto"
style="scrollbar-width: none; -ms-overflow-style: none;" style="scrollbar-width: none; -ms-overflow-style: none; -webkit-overflow-scrolling: touch; overscroll-behavior-x: contain;"
> >
<style>{` <style>{`
.overflow-x-auto::-webkit-scrollbar { display: none; } .overflow-x-auto::-webkit-scrollbar { display: none; }

View file

@ -31,11 +31,11 @@ export function SectionHeader(props: SectionHeaderProps) {
const sizeClass = () => { const sizeClass = () => {
switch (local.size) { switch (local.size) {
case 'sm': case 'sm':
return 'text-base'; return 'text-base sm:text-lg';
case 'lg': case 'lg':
return 'text-2xl'; return 'text-xl sm:text-2xl lg:text-3xl';
default: default:
return 'text-lg'; return 'text-lg sm:text-xl';
} }
}; };
@ -47,13 +47,13 @@ export function SectionHeader(props: SectionHeaderProps) {
</span> </span>
</Show> </Show>
<h2 <h2
class={`${sizeClass()} font-semibold text-gray-900 dark:text-gray-100 ${local.titleClass ?? ''}`.trim()} class={`${sizeClass()} font-semibold leading-snug text-gray-900 dark:text-gray-100 ${local.titleClass ?? ''}`.trim()}
> >
{local.title} {local.title}
</h2> </h2>
<Show when={local.description}> <Show when={local.description}>
<p <p
class={`text-sm text-gray-600 dark:text-gray-400 ${local.descriptionClass ?? ''}`.trim()} class={`text-sm sm:text-base text-gray-600 dark:text-gray-400 ${local.descriptionClass ?? ''}`.trim()}
> >
{local.description} {local.description}
</p> </p>

View file

@ -119,15 +119,27 @@
@layer components { @layer components {
.pulse-shell { .pulse-shell {
width: min(95.5vw, 95rem); width: 100%;
max-width: 100%; max-width: 95rem;
margin-inline: auto; margin-inline: auto;
padding-inline: clamp(1.25rem, 3vw, 3.25rem); padding-inline: clamp(1rem, 3vw, 3.25rem);
transition: padding-inline 0.3s ease; transition: padding-inline 0.3s ease;
} }
@media (max-width: 640px) {
.pulse-shell {
padding-inline: clamp(0.75rem, 6vw, 1.25rem);
}
}
.pulse-panel { .pulse-panel {
padding: clamp(0.75rem, 1.6vw, 1.5rem); padding: clamp(0.75rem, 1.8vw, 1.5rem);
}
@media (max-width: 640px) {
.pulse-panel {
padding: clamp(0.75rem, 5vw, 1.25rem);
}
} }
/* Card styles */ /* Card styles */

View file

@ -10,6 +10,7 @@ import { SectionHeader } from '@/components/shared/SectionHeader';
import { SettingsPanel } from '@/components/shared/SettingsPanel'; import { SettingsPanel } from '@/components/shared/SettingsPanel';
import { Toggle } from '@/components/shared/Toggle'; import { Toggle } from '@/components/shared/Toggle';
import { formField, formControl, formHelpText, labelClass, controlClass } from '@/components/shared/Form'; import { formField, formControl, formHelpText, labelClass, controlClass } from '@/components/shared/Form';
import { ScrollableTable } from '@/components/shared/ScrollableTable';
import { useWebSocket } from '@/App'; import { useWebSocket } from '@/App';
import { showSuccess, showError } from '@/utils/toast'; import { showSuccess, showError } from '@/utils/toast';
import { showTooltip, hideTooltip } from '@/components/shared/Tooltip'; import { showTooltip, hideTooltip } from '@/components/shared/Tooltip';
@ -1795,7 +1796,7 @@ function OverviewTab(props: {
return ( return (
<div class="space-y-6"> <div class="space-y-6">
{/* Stats Cards */} {/* Stats Cards */}
<div class="grid grid-cols-2 gap-3 sm:gap-4 lg:grid-cols-4"> <div class="grid grid-cols-1 gap-3 sm:grid-cols-2 sm:gap-4 lg:grid-cols-4">
<Card padding="sm" class="sm:p-4"> <Card padding="sm" class="sm:p-4">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div> <div>
@ -3073,7 +3074,7 @@ function ScheduleTab(props: ScheduleTabProps) {
<span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} mb-2 block`}> <span class={`${labelClass('text-xs uppercase tracking-[0.08em]')} mb-2 block`}>
Grouping strategy Grouping strategy
</span> </span>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<label <label
class={`relative flex items-center gap-2 rounded-lg border-2 p-3 transition-all ${ class={`relative flex items-center gap-2 rounded-lg border-2 p-3 transition-all ${
grouping().byNode grouping().byNode
@ -4221,7 +4222,7 @@ function HistoryTab() {
<select <select
value={timeFilter()} value={timeFilter()}
onChange={(e) => setTimeFilter(e.currentTarget.value as '24h' | '7d' | '30d' | 'all')} onChange={(e) => setTimeFilter(e.currentTarget.value as '24h' | '7d' | '30d' | 'all')}
class="px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" class="w-full sm:w-auto px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600"
> >
<option value="24h">Last 24h</option> <option value="24h">Last 24h</option>
<option value="7d">Last 7d</option> <option value="7d">Last 7d</option>
@ -4232,14 +4233,14 @@ function HistoryTab() {
<select <select
value={severityFilter()} value={severityFilter()}
onChange={(e) => setSeverityFilter(e.currentTarget.value as 'warning' | 'critical' | 'all')} onChange={(e) => setSeverityFilter(e.currentTarget.value as 'warning' | 'critical' | 'all')}
class="px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600" class="w-full sm:w-auto px-3 py-2 text-sm border rounded-lg dark:bg-gray-700 dark:border-gray-600"
> >
<option value="all">All Levels</option> <option value="all">All Levels</option>
<option value="critical">Critical Only</option> <option value="critical">Critical Only</option>
<option value="warning">Warning Only</option> <option value="warning">Warning Only</option>
</select> </select>
<div class="flex-1 max-w-xs"> <div class="w-full sm:flex-1 sm:max-w-xs">
<input <input
ref={searchInputRef} ref={searchInputRef}
type="text" type="text"
@ -4273,7 +4274,7 @@ function HistoryTab() {
> >
{/* Table */} {/* Table */}
<div class="mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden"> <div class="mb-2 border border-gray-200 dark:border-gray-700 rounded overflow-hidden">
<div class="overflow-x-auto"> <ScrollableTable minWidth="900px">
<table class="w-full min-w-[900px] text-xs sm:text-sm"> <table class="w-full min-w-[900px] text-xs sm:text-sm">
<thead> <thead>
<tr class="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600"> <tr class="bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 border-b border-gray-300 dark:border-gray-600">
@ -4418,7 +4419,7 @@ function HistoryTab() {
</For> </For>
</tbody> </tbody>
</table> </table>
</div> </ScrollableTable>
</div> </div>
</Show> </Show>
} }