Files
rust-ipam/style/rust-ipam.css
mathieu 042793f385 feat(hosts): add hosts list page with filters, pagination and delete
Implements task #7. The Hosts page provides:
- Name/network/port/application filters (sentinel values instead of
  Option<T> to avoid server function serialization issues)
- Configurable page size (15 default, 25/50/100/All)
- Prev/next navigation with total host count
- Add host form with network selector
- Delete action per row

Sub-components (AddHostForm, FilterBar, PaginationBar, HostTable) each
call .into_any() to erase their concrete view types. This breaks the
deeply-nested generic type that caused "queries overflow the depth
limit!" without requiring an unbounded recursion_limit increase.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 23:23:24 +02:00

909 lines
20 KiB
CSS

/* ============================================================
DESIGN TOKENS
All visual properties live as CSS custom properties.
To add a new theme, define a [data-theme="my-theme"] block
with the same variable set below.
============================================================ */
:root {
/* --- Shared tokens (identical across all themes) --- */
--font-sans: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
--font-mono: "SF Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
--size-xs: 4px;
--size-sm: 8px;
--size-md: 16px;
--size-lg: 24px;
--size-xl: 40px;
--size-2xl: 64px;
--radius-sm: 6px;
--radius-md: 10px;
--radius-lg: 14px;
--radius-pill: 999px;
--font-xs: 12px;
--font-sm: 13px;
--font-base: 15px;
--font-lg: 17px;
--font-xl: 22px;
--font-2xl: 28px;
--font-3xl: 34px;
--transition-fast: 100ms ease;
--transition-base: 200ms ease;
--nav-height: 52px;
--content-width: 1000px;
/* --- Light theme (default) --- */
--bg: #f5f5f7;
--bg-surface: #ffffff;
--bg-surface2: #f9f9fb;
--bg-hover: rgba(0, 0, 0, 0.04);
--bg-overlay: rgba(255, 255, 255, 0.8);
--border: rgba(0, 0, 0, 0.12);
--border-focus: #0071e3;
--text: #1d1d1f;
--text-secondary: #6e6e73;
--text-tertiary: #aeaeb2;
--text-on-accent: #ffffff;
--accent: #0071e3;
--accent-hover: #0077ed;
--accent-light: rgba(0, 113, 227, 0.10);
--danger: #ff3b30;
--danger-hover: #ff2d20;
--danger-light: rgba(255, 59, 48, 0.10);
--success: #34c759;
--shadow-sm: 0 1px 2px rgba(0,0,0,.06), 0 1px 4px rgba(0,0,0,.04);
--shadow-md: 0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04);
--shadow-lg: 0 8px 24px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06);
}
/* System dark mode — only applies when no explicit data-theme is set */
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) {
--bg: #000000;
--bg-surface: #1c1c1e;
--bg-surface2: #2c2c2e;
--bg-hover: rgba(255, 255, 255, 0.06);
--bg-overlay: rgba(28, 28, 30, 0.85);
--border: rgba(255, 255, 255, 0.12);
--border-focus: #0a84ff;
--text: #f5f5f7;
--text-secondary: #aeaeb2;
--text-tertiary: #636366;
--text-on-accent: #ffffff;
--accent: #0a84ff;
--accent-hover: #409cff;
--accent-light: rgba(10, 132, 255, 0.15);
--danger: #ff453a;
--danger-hover: #ff6961;
--danger-light: rgba(255, 69, 58, 0.15);
--success: #32d74b;
--shadow-sm: 0 1px 2px rgba(0,0,0,.3), 0 1px 4px rgba(0,0,0,.2);
--shadow-md: 0 4px 12px rgba(0,0,0,.4), 0 2px 4px rgba(0,0,0,.2);
--shadow-lg: 0 8px 24px rgba(0,0,0,.5), 0 4px 8px rgba(0,0,0,.3);
}
}
/* Explicit light theme */
[data-theme="light"] {
--bg: #f5f5f7;
--bg-surface: #ffffff;
--bg-surface2: #f9f9fb;
--bg-hover: rgba(0, 0, 0, 0.04);
--bg-overlay: rgba(255, 255, 255, 0.8);
--border: rgba(0, 0, 0, 0.12);
--border-focus: #0071e3;
--text: #1d1d1f;
--text-secondary: #6e6e73;
--text-tertiary: #aeaeb2;
--text-on-accent: #ffffff;
--accent: #0071e3;
--accent-hover: #0077ed;
--accent-light: rgba(0, 113, 227, 0.10);
--danger: #ff3b30;
--danger-hover: #ff2d20;
--danger-light: rgba(255, 59, 48, 0.10);
--success: #34c759;
--shadow-sm: 0 1px 2px rgba(0,0,0,.06), 0 1px 4px rgba(0,0,0,.04);
--shadow-md: 0 4px 12px rgba(0,0,0,.08), 0 2px 4px rgba(0,0,0,.04);
--shadow-lg: 0 8px 24px rgba(0,0,0,.12), 0 4px 8px rgba(0,0,0,.06);
}
/* Explicit dark theme */
[data-theme="dark"] {
--bg: #000000;
--bg-surface: #1c1c1e;
--bg-surface2: #2c2c2e;
--bg-hover: rgba(255, 255, 255, 0.06);
--bg-overlay: rgba(28, 28, 30, 0.85);
--border: rgba(255, 255, 255, 0.12);
--border-focus: #0a84ff;
--text: #f5f5f7;
--text-secondary: #aeaeb2;
--text-tertiary: #636366;
--text-on-accent: #ffffff;
--accent: #0a84ff;
--accent-hover: #409cff;
--accent-light: rgba(10, 132, 255, 0.15);
--danger: #ff453a;
--danger-hover: #ff6961;
--danger-light: rgba(255, 69, 58, 0.15);
--success: #32d74b;
--shadow-sm: 0 1px 2px rgba(0,0,0,.3), 0 1px 4px rgba(0,0,0,.2);
--shadow-md: 0 4px 12px rgba(0,0,0,.4), 0 2px 4px rgba(0,0,0,.2);
--shadow-lg: 0 8px 24px rgba(0,0,0,.5), 0 4px 8px rgba(0,0,0,.3);
}
/* ============================================================
RESET & BASE
============================================================ */
*, *::before, *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html {
font-size: var(--font-base);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-rendering: optimizeLegibility;
}
body {
font-family: var(--font-sans);
font-size: var(--font-base);
line-height: 1.6;
color: var(--text);
background: var(--bg);
min-height: 100vh;
transition: background var(--transition-base), color var(--transition-base);
}
/* ============================================================
NAVIGATION
============================================================ */
nav {
position: sticky;
top: 0;
z-index: 100;
height: var(--nav-height);
display: flex;
align-items: center;
gap: var(--size-sm);
padding: 0 var(--size-lg);
background: var(--bg-overlay);
border-bottom: 1px solid var(--border);
backdrop-filter: blur(20px) saturate(180%);
-webkit-backdrop-filter: blur(20px) saturate(180%);
}
nav a {
font-size: var(--font-sm);
font-weight: 500;
color: var(--text-secondary);
text-decoration: none;
padding: var(--size-xs) var(--size-sm);
border-radius: var(--radius-sm);
transition: color var(--transition-fast), background var(--transition-fast);
}
nav a:hover {
color: var(--text);
background: var(--bg-hover);
}
nav a.active {
color: var(--accent);
}
/* Spacer pushes theme toggle to the right */
nav .nav-spacer {
flex: 1;
}
/* ============================================================
LAYOUT
============================================================ */
main {
max-width: var(--content-width);
margin: 0 auto;
padding: var(--size-xl) var(--size-lg);
}
/* ============================================================
TYPOGRAPHY
============================================================ */
h1 {
font-size: var(--font-3xl);
font-weight: 700;
letter-spacing: -0.5px;
color: var(--text);
margin-bottom: var(--size-sm);
}
h2 {
font-size: var(--font-xl);
font-weight: 600;
letter-spacing: -0.3px;
color: var(--text);
margin-bottom: var(--size-md);
}
h3 {
font-size: var(--font-lg);
font-weight: 600;
color: var(--text);
margin-bottom: var(--size-sm);
}
p {
color: var(--text-secondary);
line-height: 1.7;
}
code {
font-family: var(--font-mono);
font-size: 0.9em;
background: var(--bg-surface2);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 1px 6px;
}
/* ============================================================
BUTTONS
============================================================ */
button,
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--size-xs);
font-family: var(--font-sans);
font-size: var(--font-sm);
font-weight: 500;
line-height: 1;
padding: 8px 16px;
border-radius: var(--radius-sm);
border: 1px solid transparent;
cursor: pointer;
transition:
background var(--transition-fast),
color var(--transition-fast),
box-shadow var(--transition-fast),
transform var(--transition-fast);
text-decoration: none;
white-space: nowrap;
user-select: none;
}
button:active,
.btn:active {
transform: scale(0.98);
}
/* Primary — filled accent */
button[type="submit"],
.btn-primary {
background: var(--accent);
color: var(--text-on-accent);
border-color: var(--accent);
}
button[type="submit"]:hover,
.btn-primary:hover {
background: var(--accent-hover);
border-color: var(--accent-hover);
box-shadow: var(--shadow-sm);
}
/* Secondary — outlined */
.btn-secondary {
background: transparent;
color: var(--text);
border-color: var(--border);
}
.btn-secondary:hover {
background: var(--bg-hover);
}
/* Danger — for delete actions */
.btn-danger {
background: transparent;
color: var(--danger);
border-color: transparent;
font-size: var(--font-xs);
padding: 4px 10px;
}
.btn-danger:hover {
background: var(--danger-light);
border-color: var(--danger);
}
/* Theme toggle */
.theme-toggle {
background: var(--bg-hover);
color: var(--text-secondary);
border: 1px solid var(--border);
font-size: var(--font-xs);
font-weight: 600;
padding: 4px 10px;
border-radius: var(--radius-pill);
letter-spacing: 0.3px;
}
.theme-toggle:hover {
color: var(--text);
background: var(--bg-surface2);
border-color: var(--border-focus);
}
/* ============================================================
FORMS & INPUTS
============================================================ */
.form-group {
display: flex;
flex-direction: column;
gap: var(--size-xs);
margin-bottom: var(--size-md);
}
label {
display: flex;
flex-direction: column;
gap: var(--size-xs);
font-size: var(--font-sm);
font-weight: 500;
color: var(--text-secondary);
}
input[type="text"],
input[type="email"],
input[type="number"],
input[type="password"],
input[type="search"],
select,
textarea {
font-family: var(--font-sans);
font-size: var(--font-base);
color: var(--text);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-sm);
padding: 9px 12px;
width: 100%;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast);
outline: none;
-webkit-appearance: none;
}
input::placeholder,
textarea::placeholder {
color: var(--text-tertiary);
}
input:focus,
select:focus,
textarea:focus {
border-color: var(--border-focus);
box-shadow: 0 0 0 3px var(--accent-light);
}
/* Inline form layout (label + input + button on one line) */
.form-inline {
display: flex;
align-items: flex-end;
gap: var(--size-sm);
flex-wrap: wrap;
}
.form-inline label {
flex: 1;
min-width: 200px;
}
/* ============================================================
CARDS & SECTIONS
============================================================ */
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--size-lg);
transition: box-shadow var(--transition-base);
}
.card:hover {
box-shadow: var(--shadow-md);
}
/* Section spacing between cards */
section + section {
margin-top: var(--size-lg);
}
/* ============================================================
TABLES
============================================================ */
.table-container {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
font-size: var(--font-sm);
}
thead {
background: var(--bg-surface2);
border-bottom: 1px solid var(--border);
}
thead th {
padding: 10px var(--size-md);
text-align: left;
font-size: var(--font-xs);
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: var(--text-tertiary);
}
tbody tr {
border-bottom: 1px solid var(--border);
transition: background var(--transition-fast);
}
tbody tr:last-child {
border-bottom: none;
}
tbody tr:hover {
background: var(--bg-hover);
}
tbody td {
padding: 12px var(--size-md);
color: var(--text);
vertical-align: middle;
}
/* Count columns (Hosts, Applications...) — right-aligned, muted color */
th.col-count,
td.col-count {
text-align: right;
color: var(--text-secondary);
font-variant-numeric: tabular-nums;
width: 120px;
}
th.col-count {
color: var(--text-tertiary);
}
/* Actions column — right-aligned to match the button position */
th.col-actions,
td.col-actions {
text-align: right;
width: 100px;
}
/* ============================================================
STATUS MESSAGES
============================================================ */
.error {
display: flex;
align-items: center;
gap: var(--size-sm);
font-size: var(--font-sm);
color: var(--danger);
background: var(--danger-light);
border: 1px solid var(--danger);
border-radius: var(--radius-sm);
padding: var(--size-sm) var(--size-md);
margin-bottom: var(--size-md);
}
.empty {
font-size: var(--font-sm);
color: var(--text-tertiary);
text-align: center;
padding: var(--size-xl);
}
/* ============================================================
HOME PAGE — Dashboard
============================================================ */
.home-page {
max-width: 860px;
margin: 0 auto;
padding-top: var(--size-2xl);
}
.home-header {
text-align: center;
margin-bottom: var(--size-2xl);
}
.home-header h1 {
font-size: var(--font-3xl);
margin-bottom: var(--size-xs);
}
.home-header p {
font-size: var(--font-lg);
color: var(--text-secondary);
margin-bottom: 0;
}
/* Summary grid — one card per entity */
.summary-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: var(--size-md);
}
/* Clickable summary card */
.summary-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: var(--size-sm);
padding: var(--size-xl) var(--size-lg);
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
text-decoration: none;
cursor: pointer;
transition:
box-shadow var(--transition-base),
border-color var(--transition-base),
transform var(--transition-base),
background var(--transition-base);
}
.summary-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--accent);
background: var(--accent-light);
transform: translateY(-2px);
}
.summary-card:active {
transform: translateY(0);
box-shadow: var(--shadow-sm);
}
.summary-card__count {
font-size: var(--font-3xl);
font-weight: 700;
letter-spacing: -1px;
color: var(--text);
line-height: 1;
/* Animate count changes gracefully */
transition: color var(--transition-base);
}
.summary-card:hover .summary-card__count {
color: var(--accent);
}
.summary-card__label {
font-size: var(--font-sm);
font-weight: 500;
color: var(--text-secondary);
letter-spacing: 0.3px;
text-transform: uppercase;
}
/* ============================================================
NETWORKS PAGE
============================================================ */
.networks-page h1 {
margin-bottom: var(--size-lg);
}
.networks-page .add-form {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--size-lg);
margin-bottom: var(--size-lg);
}
.networks-page .add-form h2 {
margin-bottom: var(--size-md);
}
.networks-page .add-form form {
display: flex;
align-items: flex-end;
gap: var(--size-sm);
flex-wrap: wrap;
}
.networks-page .add-form label {
flex: 1;
min-width: 220px;
}
.networks-page .add-form input {
margin-top: var(--size-xs);
}
.networks-page .list h2 {
margin-bottom: var(--size-md);
}
.networks-page .list .table-container {
margin-top: 0;
}
/* Delete button inside table */
.networks-page td button {
background: transparent;
color: var(--danger);
border: 1px solid transparent;
font-size: var(--font-xs);
padding: 3px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast);
}
.networks-page td button:hover {
background: var(--danger-light);
border-color: var(--danger);
}
/* ============================================================
404 PAGE
============================================================ */
.not-found {
text-align: center;
padding-top: var(--size-2xl);
}
.not-found h1 {
font-size: var(--font-2xl);
margin-bottom: var(--size-md);
}
.not-found a {
color: var(--accent);
text-decoration: none;
font-weight: 500;
}
.not-found a:hover {
text-decoration: underline;
}
/* ============================================================
TABLE UTILITIES — shared across pages
============================================================ */
/* Clickable link inside a table cell */
.table-link {
color: var(--accent);
text-decoration: none;
font-weight: 500;
}
.table-link:hover {
text-decoration: underline;
}
/* Monospace cell (IPs, CIDRs…) */
.cell-mono {
font-family: var(--font-mono);
font-size: var(--font-sm);
color: var(--text-secondary);
}
/* ============================================================
FILTER BAR
============================================================ */
.filter-bar {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--size-md) var(--size-lg);
margin-bottom: var(--size-md);
}
.filter-bar__fields {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--size-md);
align-items: end;
}
.filter-field {
display: flex;
flex-direction: column;
gap: var(--size-xs);
font-size: var(--font-sm);
font-weight: 500;
color: var(--text-secondary);
}
/* ============================================================
PAGINATION BAR
============================================================ */
.pagination-bar {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: wrap;
gap: var(--size-sm);
padding: var(--size-sm) 0;
margin-bottom: var(--size-sm);
}
.pagination-bar__info {
font-size: var(--font-sm);
color: var(--text-secondary);
}
.pagination-bar__controls {
display: flex;
align-items: center;
gap: var(--size-md);
}
.pagination-per-page {
display: flex;
align-items: center;
gap: var(--size-xs);
font-size: var(--font-sm);
color: var(--text-secondary);
font-weight: 500;
}
.pagination-per-page select {
width: auto;
padding: 4px 8px;
font-size: var(--font-sm);
}
.pagination-nav {
display: flex;
align-items: center;
gap: var(--size-xs);
}
.pagination-nav button {
background: var(--bg-surface);
color: var(--text);
border: 1px solid var(--border);
padding: 4px 10px;
font-size: var(--font-base);
border-radius: var(--radius-sm);
min-width: 32px;
}
.pagination-nav button:hover:not(:disabled) {
background: var(--bg-hover);
border-color: var(--accent);
color: var(--accent);
}
.pagination-nav button:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.pagination-nav__label {
font-size: var(--font-sm);
color: var(--text-secondary);
white-space: nowrap;
padding: 0 var(--size-xs);
}
/* ============================================================
HOSTS PAGE
============================================================ */
.hosts-page h1 {
margin-bottom: var(--size-lg);
}
.hosts-page .add-form {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm);
padding: var(--size-lg);
margin-bottom: var(--size-md);
}
.hosts-page .add-form h2 {
margin-bottom: var(--size-md);
}
.add-form__fields {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: var(--size-md);
align-items: end;
}
.add-form__fields button[type="submit"] {
align-self: end;
}
/* Delete button inside hosts table */
.hosts-page td button {
background: transparent;
color: var(--danger);
border: 1px solid transparent;
font-size: var(--font-xs);
padding: 3px 10px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background var(--transition-fast), border-color var(--transition-fast);
}
.hosts-page td button:hover {
background: var(--danger-light);
border-color: var(--danger);
}