feat(ui): migrate entire UI to "Cozy Home" design
Warm, friendly redesign replacing the generic cold-shadcn look. Built as a swappable token bundle so other presets can be added later; dark mode and the user-tunable accent hue are retained. Foundation - app.css: warm cream (light) + "dusk" (dark) token system; terracotta accent (default hue 16); pastel --room-* palette; vivid --status-* (dots/bars) plus AA-legible --status-*-ink (text); soft warm shadows; --radius 1rem; font tokens - Fonts: Fraunces (display) + Figtree (body), self-hosted in static/fonts (no Google CDN) so offline/LAN installs work; system-ui fallbacks kept - h1/h2/h3 render in Fraunces via base layer Chrome and surfaces - Sidebar, Header, home, AppCard/BoardCard, BoardHeader, sections, favorites - 29 widgets + integration renderers: cozy card shells, room-palette charts - Default background is a static warm "cozy" glow (mesh demoted, rAF gated on prefers-reduced-motion) System-wide - Status colors tokenized (no raw bg/text-*-500 or status hex); success/warning to status tokens, categorical to room palette, errors to destructive - Inputs rounded-xl; buttons rounded-xl; cards/dialogs rounded-[1.4rem]; soft-shadow vocabulary only; focus-visible:ring-primary/30 - Forms, admin tables (now cozy cards), dialogs, popovers, auth screens a11y: reduced-motion guards; darker status "ink" text for AA on cream. Known tradeoff: terracotta primary + white button text ~2.96:1 (signature color, user-tunable). Verified: svelte-check 0/0, build ok, 274 tests pass, eslint 0 errors. Design refs + system sheet in design-mockups/.
This commit is contained in:
@@ -0,0 +1,789 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="dark">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Web App Launcher — Command Deck</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Saira:wght@400;500;600;700&family=Saira+Condensed:wght@500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #070a0d;
|
||||
--panel: #0d1217;
|
||||
--panel-2: #10171e;
|
||||
--line: #1d2730;
|
||||
--line-bright: #2b3946;
|
||||
--ink: #e7eef3;
|
||||
--ink-dim: #7c8b97;
|
||||
--ink-faint: #4a5763;
|
||||
--accent: #36e0a4; /* tactical green */
|
||||
--accent-2: #ffb020; /* amber */
|
||||
--danger: #ff4d5e;
|
||||
--warn: #ffb020;
|
||||
--grid: rgba(54, 224, 164, 0.04);
|
||||
--radius: 4px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: 'Saira', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
/* subtle scanline grid backdrop */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background-image:
|
||||
linear-gradient(var(--grid) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
||||
background-size: 40px 40px;
|
||||
mask-image: radial-gradient(ellipse 80% 60% at 70% 0%, #000 30%, transparent 90%);
|
||||
}
|
||||
.app {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 74px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
.mono {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
}
|
||||
.cond {
|
||||
font-family: 'Saira Condensed', sans-serif;
|
||||
}
|
||||
|
||||
/* ===== Rail ===== */
|
||||
.rail {
|
||||
border-right: 1px solid var(--line);
|
||||
background: linear-gradient(180deg, var(--panel), #080c10);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
.logo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--accent);
|
||||
margin-bottom: 18px;
|
||||
box-shadow:
|
||||
0 0 0 1px #0a1f18,
|
||||
0 0 18px -4px var(--accent);
|
||||
position: relative;
|
||||
}
|
||||
.logo svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.rail-btn {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink-dim);
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
transition: 0.15s;
|
||||
}
|
||||
.rail-btn:hover {
|
||||
color: var(--ink);
|
||||
background: var(--panel-2);
|
||||
}
|
||||
.rail-btn.active {
|
||||
color: var(--accent);
|
||||
}
|
||||
.rail-btn.active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -16px;
|
||||
top: 10px;
|
||||
bottom: 10px;
|
||||
width: 2px;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 8px var(--accent);
|
||||
}
|
||||
.rail-btn svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.rail-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
.rail-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(135deg, #1a2a22, #0f1a14);
|
||||
border: 1px solid var(--line-bright);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ===== Main ===== */
|
||||
.main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
height: 60px;
|
||||
padding: 0 26px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(8, 12, 16, 0.6);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.crumbs {
|
||||
font-family: 'Saira Condensed';
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
font-size: 12px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.crumbs b {
|
||||
color: var(--ink-dim);
|
||||
font-weight: 600;
|
||||
}
|
||||
.search {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 340px;
|
||||
max-width: 38vw;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 9px 12px;
|
||||
color: var(--ink-dim);
|
||||
font-size: 13px;
|
||||
cursor: text;
|
||||
transition: 0.15s;
|
||||
}
|
||||
.search:hover {
|
||||
border-color: var(--line-bright);
|
||||
}
|
||||
.search svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
.search .kbd {
|
||||
margin-left: auto;
|
||||
font-family: 'JetBrains Mono';
|
||||
font-size: 10px;
|
||||
color: var(--ink-faint);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 3px;
|
||||
padding: 2px 6px;
|
||||
}
|
||||
.ico-btn {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink-dim);
|
||||
cursor: pointer;
|
||||
transition: 0.15s;
|
||||
background: var(--panel);
|
||||
}
|
||||
.ico-btn:hover {
|
||||
color: var(--ink);
|
||||
border-color: var(--line-bright);
|
||||
}
|
||||
.ico-btn svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
.content {
|
||||
padding: 26px;
|
||||
max-width: 1320px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* status bar */
|
||||
.statline {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 1px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
margin-bottom: 24px;
|
||||
background: var(--line);
|
||||
}
|
||||
.stat {
|
||||
flex: 1;
|
||||
background: var(--panel);
|
||||
padding: 16px 18px;
|
||||
position: relative;
|
||||
}
|
||||
.stat .lbl {
|
||||
font-family: 'Saira Condensed';
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.stat .val {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-size: 26px;
|
||||
font-weight: 700;
|
||||
margin-top: 6px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.stat .val.ok {
|
||||
color: var(--accent);
|
||||
}
|
||||
.stat .val.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
.stat .val.bad {
|
||||
color: var(--danger);
|
||||
}
|
||||
.stat .sub {
|
||||
font-size: 12px;
|
||||
color: var(--ink-dim);
|
||||
margin-top: 2px;
|
||||
}
|
||||
.stat::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
width: 100%;
|
||||
background: linear-gradient(90deg, var(--accent), transparent);
|
||||
}
|
||||
.stat.s2::after {
|
||||
background: linear-gradient(90deg, var(--accent-2), transparent);
|
||||
}
|
||||
.stat.s3::after {
|
||||
background: linear-gradient(90deg, #3aa0ff, transparent);
|
||||
}
|
||||
.stat.s4::after {
|
||||
background: linear-gradient(90deg, var(--danger), transparent);
|
||||
}
|
||||
|
||||
.sec-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin: 30px 0 16px;
|
||||
}
|
||||
.sec-head h2 {
|
||||
font-family: 'Saira Condensed';
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-size: 15px;
|
||||
color: var(--ink);
|
||||
}
|
||||
.sec-head .rule {
|
||||
flex: 1;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, var(--line-bright), transparent);
|
||||
}
|
||||
.sec-head .count {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
/* app grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.node {
|
||||
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 16px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
transition:
|
||||
transform 0.15s,
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
overflow: hidden;
|
||||
}
|
||||
.node::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at 100% 0%, rgba(54, 224, 164, 0.08), transparent 60%);
|
||||
opacity: 0;
|
||||
transition: 0.2s;
|
||||
}
|
||||
.node:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--line-bright);
|
||||
box-shadow: 0 10px 30px -12px #000;
|
||||
}
|
||||
.node:hover::before {
|
||||
opacity: 1;
|
||||
}
|
||||
.node-top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.node-ico {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: var(--radius);
|
||||
background: #0a0f13;
|
||||
border: 1px solid var(--line);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.node-name {
|
||||
font-weight: 600;
|
||||
font-size: 15px;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.node-cat {
|
||||
font-family: 'Saira Condensed';
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 10px;
|
||||
color: var(--ink-faint);
|
||||
margin-top: 3px;
|
||||
}
|
||||
.led {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
margin-left: auto;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
}
|
||||
.led.ok {
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 10px var(--accent);
|
||||
}
|
||||
.led.ok::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -4px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--accent);
|
||||
opacity: 0.4;
|
||||
animation: ping 2s ease-out infinite;
|
||||
}
|
||||
.led.warn {
|
||||
background: var(--warn);
|
||||
box-shadow: 0 0 10px var(--warn);
|
||||
}
|
||||
.led.bad {
|
||||
background: var(--danger);
|
||||
box-shadow: 0 0 10px var(--danger);
|
||||
}
|
||||
@keyframes ping {
|
||||
0% {
|
||||
transform: scale(0.8);
|
||||
opacity: 0.5;
|
||||
}
|
||||
100% {
|
||||
transform: scale(2.2);
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
.node-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
}
|
||||
.node-foot .up {
|
||||
font-family: 'JetBrains Mono';
|
||||
font-size: 11px;
|
||||
color: var(--ink-dim);
|
||||
}
|
||||
.node-foot .up b {
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
}
|
||||
.node-foot .up.bad b {
|
||||
color: var(--danger);
|
||||
}
|
||||
.spark {
|
||||
height: 22px;
|
||||
width: 96px;
|
||||
}
|
||||
|
||||
.footer-note {
|
||||
margin-top: 40px;
|
||||
text-align: center;
|
||||
font-family: 'JetBrains Mono';
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
|
||||
/* entrance */
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.node,
|
||||
.stat {
|
||||
animation: rise 0.5s both;
|
||||
}
|
||||
.stat:nth-child(2) {
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
.stat:nth-child(3) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.stat:nth-child(4) {
|
||||
animation-delay: 0.15s;
|
||||
}
|
||||
.grid .node:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.grid .node:nth-child(2) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
.grid .node:nth-child(3) {
|
||||
animation-delay: 0.22s;
|
||||
}
|
||||
.grid .node:nth-child(4) {
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
.grid .node:nth-child(5) {
|
||||
animation-delay: 0.34s;
|
||||
}
|
||||
.grid .node:nth-child(6) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.grid .node:nth-child(7) {
|
||||
animation-delay: 0.46s;
|
||||
}
|
||||
.grid .node:nth-child(8) {
|
||||
animation-delay: 0.52s;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app">
|
||||
<!-- Rail -->
|
||||
<nav class="rail">
|
||||
<div class="logo" title="Launcher">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</div>
|
||||
<a class="rail-btn active" title="Overview"
|
||||
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
<line x1="9" y1="21" x2="9" y2="9" /></svg
|
||||
></a>
|
||||
<a class="rail-btn" title="Apps"
|
||||
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" /></svg
|
||||
></a>
|
||||
<a class="rail-btn" title="Status"
|
||||
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" /></svg
|
||||
></a>
|
||||
<a class="rail-btn" title="Admin"
|
||||
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M19 12a7 7 0 0 0-.1-1l2-1.6-2-3.4-2.4 1a7 7 0 0 0-1.7-1l-.4-2.6h-4l-.4 2.6a7 7 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 1.7 1l.4 2.6h4l.4-2.6a7 7 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6a7 7 0 0 0 .1-1z"
|
||||
/></svg
|
||||
></a>
|
||||
<div class="rail-spacer"></div>
|
||||
<div class="rail-avatar">AD</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main -->
|
||||
<div class="main">
|
||||
<header class="topbar">
|
||||
<div class="crumbs">SYSTEMS / <b>OVERVIEW</b></div>
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
Search apps, boards, commands…
|
||||
<span class="kbd">⌘K</span>
|
||||
</div>
|
||||
<div class="ico-btn" title="Notifications">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="ico-btn" title="Theme">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path
|
||||
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="content">
|
||||
<!-- Status line -->
|
||||
<div class="statline">
|
||||
<div class="stat">
|
||||
<div class="lbl">Services Online</div>
|
||||
<div class="val ok">08 / 10</div>
|
||||
<div class="sub">2 require attention</div>
|
||||
</div>
|
||||
<div class="stat s2">
|
||||
<div class="lbl">Avg Response</div>
|
||||
<div class="val warn">
|
||||
142<span style="font-size: 14px; color: var(--ink-faint)"> ms</span>
|
||||
</div>
|
||||
<div class="sub">p95 over 24h</div>
|
||||
</div>
|
||||
<div class="stat s3">
|
||||
<div class="lbl">Fleet Uptime</div>
|
||||
<div class="val" style="color: #3aa0ff">99.4%</div>
|
||||
<div class="sub">rolling 30 days</div>
|
||||
</div>
|
||||
<div class="stat s4">
|
||||
<div class="lbl">UPS Load</div>
|
||||
<div class="val bad">61%</div>
|
||||
<div class="sub">est. 38 min on battery</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Favorites / pinned -->
|
||||
<div class="sec-head">
|
||||
<h2>Pinned Services</h2>
|
||||
<span class="rule"></span><span class="count">8 ACTIVE</span>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<!-- 1 Jellyfin -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">🎬</div>
|
||||
<div>
|
||||
<div class="node-name">Jellyfin</div>
|
||||
<div class="node-cat">Media</div>
|
||||
</div>
|
||||
<div class="led ok"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#36e0a4"
|
||||
stroke-width="1.5"
|
||||
points="0,16 12,14 24,17 36,9 48,12 60,7 72,10 84,5 96,8"
|
||||
/></svg
|
||||
><span class="up"><b>99.9%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 2 Immich -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">📷</div>
|
||||
<div>
|
||||
<div class="node-name">Immich</div>
|
||||
<div class="node-cat">Photos</div>
|
||||
</div>
|
||||
<div class="led ok"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#36e0a4"
|
||||
stroke-width="1.5"
|
||||
points="0,12 12,13 24,11 36,12 48,10 60,11 72,9 84,11 96,10"
|
||||
/></svg
|
||||
><span class="up"><b>100%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 3 Gitea -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">🌿</div>
|
||||
<div>
|
||||
<div class="node-name">Gitea</div>
|
||||
<div class="node-cat">Git</div>
|
||||
</div>
|
||||
<div class="led ok"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#36e0a4"
|
||||
stroke-width="1.5"
|
||||
points="0,14 12,10 24,12 36,8 48,11 60,9 72,13 84,8 96,9"
|
||||
/></svg
|
||||
><span class="up"><b>99.8%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 4 Portainer -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">🐳</div>
|
||||
<div>
|
||||
<div class="node-name">Portainer</div>
|
||||
<div class="node-cat">Containers</div>
|
||||
</div>
|
||||
<div class="led warn"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#ffb020"
|
||||
stroke-width="1.5"
|
||||
points="0,10 12,12 24,9 36,15 48,11 60,18 72,12 84,16 96,13"
|
||||
/></svg
|
||||
><span class="up"><b style="color: var(--warn)">98.1%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 5 Pi-hole -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">🛡️</div>
|
||||
<div>
|
||||
<div class="node-name">Pi-hole</div>
|
||||
<div class="node-cat">DNS</div>
|
||||
</div>
|
||||
<div class="led ok"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#36e0a4"
|
||||
stroke-width="1.5"
|
||||
points="0,13 12,11 24,12 36,10 48,11 60,9 72,10 84,8 96,9"
|
||||
/></svg
|
||||
><span class="up"><b>100%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 6 Planka -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">📋</div>
|
||||
<div>
|
||||
<div class="node-name">Planka</div>
|
||||
<div class="node-cat">Kanban</div>
|
||||
</div>
|
||||
<div class="led ok"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#36e0a4"
|
||||
stroke-width="1.5"
|
||||
points="0,15 12,12 24,14 36,11 48,12 60,10 72,12 84,9 96,11"
|
||||
/></svg
|
||||
><span class="up"><b>99.5%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 7 Deluge -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">⬇️</div>
|
||||
<div>
|
||||
<div class="node-name">Deluge</div>
|
||||
<div class="node-cat">Downloads</div>
|
||||
</div>
|
||||
<div class="led bad"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#ff4d5e"
|
||||
stroke-width="1.5"
|
||||
points="0,9 12,11 24,14 36,12 48,18 60,16 72,20 84,19 96,21"
|
||||
/></svg
|
||||
><span class="up bad"><b>OFFLINE</b></span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 8 Pi-hole / NPM -->
|
||||
<div class="node">
|
||||
<div class="node-top">
|
||||
<div class="node-ico">🔀</div>
|
||||
<div>
|
||||
<div class="node-name">Nginx Proxy Mgr</div>
|
||||
<div class="node-cat">Network</div>
|
||||
</div>
|
||||
<div class="led ok"></div>
|
||||
</div>
|
||||
<div class="node-foot">
|
||||
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#36e0a4"
|
||||
stroke-width="1.5"
|
||||
points="0,12 12,11 24,12 36,10 48,11 60,11 72,9 84,10 96,9"
|
||||
/></svg
|
||||
><span class="up"><b>99.9%</b> 24h</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="footer-note">
|
||||
// COMMAND DECK — Saira + JetBrains Mono · tactical dark · LED telemetry · monospace
|
||||
data
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,915 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Web App Launcher — Aurora Glass</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0a14;
|
||||
--ink: #f3f2fb;
|
||||
--ink-dim: #a7a6c4;
|
||||
--ink-faint: #6f6e90;
|
||||
--accent-h: 265; /* user-tunable hue → this is the killer feature */
|
||||
--accent: hsl(var(--accent-h) 90% 66%);
|
||||
--accent-2: hsl(calc(var(--accent-h) + 60) 85% 64%);
|
||||
--accent-soft: hsl(var(--accent-h) 90% 66% / 0.14);
|
||||
--glass: rgba(255, 255, 255, 0.05);
|
||||
--glass-2: rgba(255, 255, 255, 0.07);
|
||||
--glass-line: rgba(255, 255, 255, 0.1);
|
||||
--ok: #34e0a1;
|
||||
--warn: #ffc24b;
|
||||
--bad: #ff5d73;
|
||||
--radius: 18px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: 'Manrope', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
position: relative;
|
||||
}
|
||||
/* aurora mesh */
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: -20%;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
filter: blur(60px);
|
||||
opacity: 0.9;
|
||||
background:
|
||||
radial-gradient(40% 40% at 18% 22%, hsl(var(--accent-h) 90% 60% / 0.55), transparent 70%),
|
||||
radial-gradient(
|
||||
38% 38% at 82% 18%,
|
||||
hsl(calc(var(--accent-h) + 70) 90% 60% / 0.42),
|
||||
transparent 70%
|
||||
),
|
||||
radial-gradient(
|
||||
45% 45% at 70% 85%,
|
||||
hsl(calc(var(--accent-h) - 40) 90% 58% / 0.4),
|
||||
transparent 72%
|
||||
),
|
||||
radial-gradient(
|
||||
40% 40% at 25% 90%,
|
||||
hsl(calc(var(--accent-h) + 120) 80% 55% / 0.3),
|
||||
transparent 72%
|
||||
);
|
||||
animation: drift 22s ease-in-out infinite alternate;
|
||||
}
|
||||
@keyframes drift {
|
||||
0% {
|
||||
transform: translate(0, 0) scale(1);
|
||||
}
|
||||
100% {
|
||||
transform: translate(-3%, 2%) scale(1.08);
|
||||
}
|
||||
}
|
||||
body::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background: radial-gradient(
|
||||
120% 120% at 50% -10%,
|
||||
transparent 40%,
|
||||
rgba(10, 10, 20, 0.6) 100%
|
||||
);
|
||||
}
|
||||
|
||||
.shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 248px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* sidebar (glass) */
|
||||
.side {
|
||||
margin: 16px 0 16px 16px;
|
||||
border-radius: 24px;
|
||||
padding: 20px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-line);
|
||||
backdrop-filter: blur(26px) saturate(160%);
|
||||
-webkit-backdrop-filter: blur(26px) saturate(160%);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
margin-bottom: 26px;
|
||||
padding: 4px 6px;
|
||||
}
|
||||
.brand .mark {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 8px 24px -6px var(--accent);
|
||||
}
|
||||
.brand .mark svg {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
.brand .name {
|
||||
font-family: 'Outfit';
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.brand .name span {
|
||||
display: block;
|
||||
font-family: 'Manrope';
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.nav-grp {
|
||||
font-family: 'Outfit';
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.13em;
|
||||
font-size: 10px;
|
||||
color: var(--ink-faint);
|
||||
margin: 14px 8px 8px;
|
||||
}
|
||||
.nav-i {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 11px 12px;
|
||||
border-radius: 12px;
|
||||
color: var(--ink-dim);
|
||||
font-weight: 500;
|
||||
font-size: 14.5px;
|
||||
cursor: pointer;
|
||||
transition: 0.18s;
|
||||
position: relative;
|
||||
}
|
||||
.nav-i svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
.nav-i:hover {
|
||||
color: var(--ink);
|
||||
background: var(--glass-2);
|
||||
}
|
||||
.nav-i.on {
|
||||
color: var(--ink);
|
||||
background: var(--accent-soft);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--accent-h) 90% 66% / 0.35);
|
||||
}
|
||||
.nav-i.on::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -20px;
|
||||
top: 9px;
|
||||
bottom: 9px;
|
||||
width: 3px;
|
||||
border-radius: 3px;
|
||||
background: var(--accent);
|
||||
}
|
||||
.nav-i .dot {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
font-weight: 600;
|
||||
}
|
||||
.side-foot {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 10px;
|
||||
border-radius: 14px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-line);
|
||||
}
|
||||
.av {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
}
|
||||
.side-foot .who {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 1.1;
|
||||
}
|
||||
.side-foot .who span {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* main */
|
||||
.main {
|
||||
padding: 30px 34px;
|
||||
min-width: 0;
|
||||
}
|
||||
.head {
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 20px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
.hello {
|
||||
font-family: 'Outfit';
|
||||
font-weight: 600;
|
||||
font-size: 30px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1.05;
|
||||
}
|
||||
.hello em {
|
||||
font-style: normal;
|
||||
background: linear-gradient(120deg, var(--accent), var(--accent-2));
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
color: transparent;
|
||||
}
|
||||
.sub {
|
||||
color: var(--ink-dim);
|
||||
font-size: 14px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.searchwrap {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
width: 300px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-line);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: 14px;
|
||||
padding: 11px 14px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 13.5px;
|
||||
cursor: text;
|
||||
}
|
||||
.search svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.search .k {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
background: var(--glass-2);
|
||||
border-radius: 6px;
|
||||
padding: 2px 7px;
|
||||
}
|
||||
.gbtn {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-line);
|
||||
backdrop-filter: blur(20px);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink-dim);
|
||||
cursor: pointer;
|
||||
transition: 0.18s;
|
||||
}
|
||||
.gbtn:hover {
|
||||
color: var(--ink);
|
||||
background: var(--glass-2);
|
||||
}
|
||||
.gbtn svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
/* metric row */
|
||||
.metrics {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.metric {
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-line);
|
||||
backdrop-filter: blur(22px) saturate(150%);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px 20px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.metric .ic {
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 11px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.metric .ic svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
.metric .v {
|
||||
font-family: 'Outfit';
|
||||
font-size: 27px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.metric .l {
|
||||
color: var(--ink-dim);
|
||||
font-size: 13px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
.metric .trend {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
right: 18px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--ok);
|
||||
background: rgba(52, 224, 161, 0.12);
|
||||
padding: 3px 9px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.metric .trend.dn {
|
||||
color: var(--bad);
|
||||
background: rgba(255, 93, 115, 0.12);
|
||||
}
|
||||
|
||||
.sectitle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
.sectitle h2 {
|
||||
font-family: 'Outfit';
|
||||
font-weight: 600;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.sectitle a {
|
||||
font-size: 13px;
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.apps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
.card {
|
||||
background: var(--glass);
|
||||
border: 1px solid var(--glass-line);
|
||||
backdrop-filter: blur(22px) saturate(150%);
|
||||
-webkit-backdrop-filter: blur(22px) saturate(150%);
|
||||
border-radius: var(--radius);
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
transition:
|
||||
transform 0.22s cubic-bezier(0.2, 0.7, 0.2, 1),
|
||||
box-shadow 0.22s,
|
||||
border-color 0.22s;
|
||||
}
|
||||
.card::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border-radius: var(--radius);
|
||||
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), transparent 40%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-5px);
|
||||
border-color: hsl(var(--accent-h) 90% 66% / 0.5);
|
||||
box-shadow: 0 24px 50px -20px hsl(var(--accent-h) 90% 50% / 0.55);
|
||||
}
|
||||
.card .row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 13px;
|
||||
}
|
||||
.ico {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border-radius: 13px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border: 1px solid var(--glass-line);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nm {
|
||||
font-family: 'Outfit';
|
||||
font-weight: 600;
|
||||
font-size: 15.5px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.ct {
|
||||
font-size: 12px;
|
||||
color: var(--ink-faint);
|
||||
margin-top: 1px;
|
||||
}
|
||||
.pill {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 5px 10px;
|
||||
border-radius: 20px;
|
||||
}
|
||||
.pill.ok {
|
||||
color: var(--ok);
|
||||
background: rgba(52, 224, 161, 0.13);
|
||||
}
|
||||
.pill.warn {
|
||||
color: var(--warn);
|
||||
background: rgba(255, 194, 75, 0.13);
|
||||
}
|
||||
.pill.bad {
|
||||
color: var(--bad);
|
||||
background: rgba(255, 93, 115, 0.13);
|
||||
}
|
||||
.pill .b {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
box-shadow: 0 0 8px currentColor;
|
||||
}
|
||||
.meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 16px;
|
||||
}
|
||||
.meta .up {
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-dim);
|
||||
}
|
||||
.meta .up b {
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
.spark {
|
||||
height: 24px;
|
||||
width: 84px;
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(14px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.metric,
|
||||
.card {
|
||||
animation: rise 0.55s both;
|
||||
}
|
||||
.metric:nth-child(2) {
|
||||
animation-delay: 0.06s;
|
||||
}
|
||||
.metric:nth-child(3) {
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
.metric:nth-child(4) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
.apps .card:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.apps .card:nth-child(2) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
.apps .card:nth-child(3) {
|
||||
animation-delay: 0.22s;
|
||||
}
|
||||
.apps .card:nth-child(4) {
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
.apps .card:nth-child(5) {
|
||||
animation-delay: 0.34s;
|
||||
}
|
||||
.apps .card:nth-child(6) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.apps .card:nth-child(7) {
|
||||
animation-delay: 0.46s;
|
||||
}
|
||||
.apps .card:nth-child(8) {
|
||||
animation-delay: 0.52s;
|
||||
}
|
||||
|
||||
.swatches {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
margin-top: 30px;
|
||||
justify-content: center;
|
||||
color: var(--ink-faint);
|
||||
font-size: 12px;
|
||||
}
|
||||
.sw {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
border: 1px solid var(--glass-line);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<!-- Sidebar -->
|
||||
<aside class="side">
|
||||
<div class="brand">
|
||||
<div class="mark">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="1" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="name">Launcher<span>home cloud</span></div>
|
||||
</div>
|
||||
<div class="nav-grp">Workspace</div>
|
||||
<div class="nav-i on">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||
<path d="M3 9h18M9 21V9" />
|
||||
</svg>
|
||||
Overview
|
||||
</div>
|
||||
<div class="nav-i">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" />
|
||||
</svg>
|
||||
All Apps <span class="dot">10</span>
|
||||
</div>
|
||||
<div class="nav-i">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
Status
|
||||
</div>
|
||||
<div class="nav-grp">Boards</div>
|
||||
<div class="nav-i">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||
</svg>
|
||||
Media Center
|
||||
</div>
|
||||
<div class="nav-i">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||
</svg>
|
||||
Infrastructure
|
||||
</div>
|
||||
<div class="nav-i">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M12 5v14M5 12h14" />
|
||||
</svg>
|
||||
New board…
|
||||
</div>
|
||||
<div class="side-foot">
|
||||
<div class="av">AD</div>
|
||||
<div class="who">Alexei<span>Administrator</span></div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="main">
|
||||
<div class="head">
|
||||
<div>
|
||||
<div class="hello">Good evening, <em>Alexei</em></div>
|
||||
<div class="sub">All systems nominal — 8 of 10 services responding</div>
|
||||
</div>
|
||||
<div class="searchwrap">
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
Search… <span class="k">⌘K</span>
|
||||
</div>
|
||||
<div class="gbtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="gbtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path
|
||||
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- metrics -->
|
||||
<div class="metrics">
|
||||
<div class="metric">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="v">8<span style="color: var(--ink-faint); font-size: 18px">/10</span></div>
|
||||
<div class="l">Services online</div>
|
||||
<span class="trend">+2</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M12 7v5l3 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="v">142<span style="color: var(--ink-faint); font-size: 16px">ms</span></div>
|
||||
<div class="l">Avg response</div>
|
||||
<span class="trend dn">+18ms</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M3 12h4l3 8 4-16 3 8h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="v">99.4%</div>
|
||||
<div class="l">Uptime · 30d</div>
|
||||
<span class="trend">+0.2</span>
|
||||
</div>
|
||||
<div class="metric">
|
||||
<div class="ic">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M13 2 3 14h7l-1 8 10-12h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="v">61%</div>
|
||||
<div class="l">UPS load · 38m</div>
|
||||
<span class="trend dn">batt</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sectitle">
|
||||
<h2>Favorites</h2>
|
||||
<a href="#">View all apps →</a>
|
||||
</div>
|
||||
<div class="apps">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">🎬</div>
|
||||
<div>
|
||||
<div class="nm">Jellyfin</div>
|
||||
<div class="ct">Media</div>
|
||||
</div>
|
||||
<div class="pill ok"><span class="b"></span>Up</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>99.9%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
points="0,18 11,15 22,17 33,9 44,12 55,7 66,11 76,5 84,8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">📷</div>
|
||||
<div>
|
||||
<div class="nm">Immich</div>
|
||||
<div class="ct">Photos</div>
|
||||
</div>
|
||||
<div class="pill ok"><span class="b"></span>Up</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>100%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
points="0,13 11,14 22,12 33,13 44,11 55,12 66,10 76,12 84,11"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">🌿</div>
|
||||
<div>
|
||||
<div class="nm">Gitea</div>
|
||||
<div class="ct">Git server</div>
|
||||
</div>
|
||||
<div class="pill ok"><span class="b"></span>Up</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>99.8%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
points="0,15 11,10 22,13 33,8 44,12 55,9 66,14 76,8 84,10"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">🐳</div>
|
||||
<div>
|
||||
<div class="nm">Portainer</div>
|
||||
<div class="ct">Containers</div>
|
||||
</div>
|
||||
<div class="pill warn"><span class="b"></span>Slow</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>98.1%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--warn)"
|
||||
stroke-width="2"
|
||||
points="0,11 11,13 22,9 33,16 44,11 55,19 66,12 76,17 84,13"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">🛡️</div>
|
||||
<div>
|
||||
<div class="nm">Pi-hole</div>
|
||||
<div class="ct">DNS · Ads</div>
|
||||
</div>
|
||||
<div class="pill ok"><span class="b"></span>Up</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>100%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
points="0,14 11,12 22,13 33,11 44,12 55,10 66,11 76,9 84,10"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">📋</div>
|
||||
<div>
|
||||
<div class="nm">Planka</div>
|
||||
<div class="ct">Kanban</div>
|
||||
</div>
|
||||
<div class="pill ok"><span class="b"></span>Up</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>99.5%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
points="0,16 11,13 22,15 33,12 44,13 55,11 66,13 76,10 84,12"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">⬇️</div>
|
||||
<div>
|
||||
<div class="nm">Deluge</div>
|
||||
<div class="ct">Downloads</div>
|
||||
</div>
|
||||
<div class="pill bad"><span class="b"></span>Down</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up" style="color: var(--bad)"
|
||||
><b style="color: var(--bad)">offline</b> · 4m</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--bad)"
|
||||
stroke-width="2"
|
||||
points="0,10 11,12 22,15 33,13 44,19 55,17 66,21 76,20 84,22"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="ico">🔀</div>
|
||||
<div>
|
||||
<div class="nm">Proxy Mgr</div>
|
||||
<div class="ct">Network</div>
|
||||
</div>
|
||||
<div class="pill ok"><span class="b"></span>Up</div>
|
||||
</div>
|
||||
<div class="meta">
|
||||
<span class="up"><b>99.9%</b> uptime</span
|
||||
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="var(--accent)"
|
||||
stroke-width="2"
|
||||
points="0,13 11,12 22,13 33,11 44,12 55,12 66,10 76,11 84,10"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="swatches">
|
||||
Accent (user-tunable):
|
||||
<span
|
||||
class="sw"
|
||||
style="background: hsl(265 90% 66%)"
|
||||
onclick="document.documentElement.style.setProperty('--accent-h', '265')"
|
||||
></span>
|
||||
<span
|
||||
class="sw"
|
||||
style="background: hsl(210 90% 60%)"
|
||||
onclick="document.documentElement.style.setProperty('--accent-h', '210')"
|
||||
></span>
|
||||
<span
|
||||
class="sw"
|
||||
style="background: hsl(150 80% 55%)"
|
||||
onclick="document.documentElement.style.setProperty('--accent-h', '150')"
|
||||
></span>
|
||||
<span
|
||||
class="sw"
|
||||
style="background: hsl(20 90% 62%)"
|
||||
onclick="document.documentElement.style.setProperty('--accent-h', '20')"
|
||||
></span>
|
||||
<span
|
||||
class="sw"
|
||||
style="background: hsl(330 85% 65%)"
|
||||
onclick="document.documentElement.style.setProperty('--accent-h', '330')"
|
||||
></span>
|
||||
— try clicking; the whole UI + aurora retints live
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,643 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Web App Launcher — Editorial</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Instrument+Serif:ital@0;1&family=Hanken+Grotesk:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--paper: #f4f1ea; /* warm paper */
|
||||
--paper-2: #ece7db;
|
||||
--card: #fbfaf6;
|
||||
--ink: #191712;
|
||||
--ink-2: #5a554a;
|
||||
--ink-faint: #9b9484;
|
||||
--line: #1a1712;
|
||||
--line-soft: #d8d2c4;
|
||||
--accent: #ff5436; /* vermilion */
|
||||
--accent-ink: #cf3a1f;
|
||||
--blue: #1f4ae0;
|
||||
--ok: #1f8a4c;
|
||||
--warn: #b8730a;
|
||||
--bad: #cf2020;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background: var(--paper);
|
||||
color: var(--ink);
|
||||
font-family: 'Hanken Grotesk', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
background-image: radial-gradient(rgba(0, 0, 0, 0.022) 1px, transparent 1px);
|
||||
background-size: 5px 5px;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1180px;
|
||||
margin: 0 auto;
|
||||
padding: 0 26px;
|
||||
}
|
||||
|
||||
/* top bar */
|
||||
.masthead {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
padding: 22px 0 18px;
|
||||
border-bottom: 2.5px solid var(--line);
|
||||
}
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.logo .glyph {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.logo .glyph svg {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
}
|
||||
.logo .tt {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-weight: 800;
|
||||
font-size: 23px;
|
||||
letter-spacing: -0.03em;
|
||||
line-height: 0.9;
|
||||
}
|
||||
.logo .tt small {
|
||||
display: block;
|
||||
font-family: 'Instrument Serif';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
.nav {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
margin-left: 18px;
|
||||
}
|
||||
.nav a {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
color: var(--ink);
|
||||
text-decoration: none;
|
||||
padding: 8px 14px;
|
||||
border-radius: 2px;
|
||||
transition: 0.15s;
|
||||
}
|
||||
.nav a:hover {
|
||||
background: var(--paper-2);
|
||||
}
|
||||
.nav a.on {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.tools {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
.search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 2px;
|
||||
padding: 9px 13px;
|
||||
font-size: 13px;
|
||||
color: var(--ink-2);
|
||||
cursor: text;
|
||||
background: var(--card);
|
||||
}
|
||||
.search svg {
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
.search .k {
|
||||
margin-left: 8px;
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
border: 1.5px solid var(--line-soft);
|
||||
border-radius: 3px;
|
||||
padding: 1px 6px;
|
||||
}
|
||||
.ib {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 2px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
cursor: pointer;
|
||||
background: var(--card);
|
||||
transition: 0.15s;
|
||||
}
|
||||
.ib:hover {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.ib svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
}
|
||||
|
||||
/* hero */
|
||||
.hero {
|
||||
display: grid;
|
||||
grid-template-columns: 1.45fr 1fr;
|
||||
gap: 0;
|
||||
border-bottom: 2.5px solid var(--line);
|
||||
}
|
||||
.hero-l {
|
||||
padding: 46px 40px 46px 0;
|
||||
border-right: 2.5px solid var(--line);
|
||||
}
|
||||
.kicker {
|
||||
font-family: 'Hanken Grotesk';
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.22em;
|
||||
font-size: 12px;
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
.hero h1 {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-weight: 800;
|
||||
font-size: 62px;
|
||||
line-height: 0.95;
|
||||
letter-spacing: -0.035em;
|
||||
margin: 14px 0 0;
|
||||
}
|
||||
.hero h1 em {
|
||||
font-family: 'Instrument Serif';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
color: var(--accent);
|
||||
}
|
||||
.hero p {
|
||||
font-size: 16px;
|
||||
color: var(--ink-2);
|
||||
max-width: 30ch;
|
||||
margin-top: 18px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.hero-cta {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
.btn {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
padding: 12px 20px;
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--line);
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
}
|
||||
.btn.solid {
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
}
|
||||
.btn.solid:hover {
|
||||
background: var(--accent);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.btn.ghost {
|
||||
background: transparent;
|
||||
color: var(--ink);
|
||||
}
|
||||
.btn.ghost:hover {
|
||||
background: var(--paper-2);
|
||||
}
|
||||
.hero-r {
|
||||
display: grid;
|
||||
grid-template-rows: 1fr 1fr 1fr;
|
||||
}
|
||||
.figure {
|
||||
padding: 20px 0 20px 36px;
|
||||
border-bottom: 1.5px solid var(--line-soft);
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 14px;
|
||||
}
|
||||
.figure:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.figure .num {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-weight: 800;
|
||||
font-size: 46px;
|
||||
letter-spacing: -0.04em;
|
||||
line-height: 0.85;
|
||||
min-width: 120px;
|
||||
}
|
||||
.figure .num.acc {
|
||||
color: var(--accent);
|
||||
}
|
||||
.figure .num.bl {
|
||||
color: var(--blue);
|
||||
}
|
||||
.figure .desc {
|
||||
font-size: 13px;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.35;
|
||||
}
|
||||
.figure .desc b {
|
||||
display: block;
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-weight: 700;
|
||||
color: var(--ink);
|
||||
font-size: 15px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* section label */
|
||||
.slab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin: 36px 0 20px;
|
||||
}
|
||||
.slab h2 {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
.slab .ln {
|
||||
flex: 1;
|
||||
height: 2px;
|
||||
background: var(--line);
|
||||
}
|
||||
.slab .meta {
|
||||
font-family: 'Instrument Serif';
|
||||
font-style: italic;
|
||||
font-size: 16px;
|
||||
color: var(--ink-2);
|
||||
}
|
||||
|
||||
/* apps — asymmetric editorial grid */
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(12, 1fr);
|
||||
gap: 14px;
|
||||
}
|
||||
.tile {
|
||||
grid-column: span 3;
|
||||
background: var(--card);
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 3px;
|
||||
padding: 18px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.15s,
|
||||
box-shadow 0.15s;
|
||||
position: relative;
|
||||
box-shadow: 4px 4px 0 var(--line);
|
||||
}
|
||||
.tile:hover {
|
||||
transform: translate(-2px, -2px);
|
||||
box-shadow: 7px 7px 0 var(--accent);
|
||||
}
|
||||
.tile.wide {
|
||||
grid-column: span 6;
|
||||
}
|
||||
.tile.tall {
|
||||
grid-column: span 3;
|
||||
}
|
||||
.t-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
.t-ico {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
border: 2px solid var(--line);
|
||||
border-radius: 3px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 22px;
|
||||
background: var(--paper);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.t-name {
|
||||
font-family: 'Bricolage Grotesque';
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
.t-cat {
|
||||
font-family: 'Instrument Serif';
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
color: var(--ink-2);
|
||||
margin-top: 3px;
|
||||
}
|
||||
.tag {
|
||||
margin-left: auto;
|
||||
font-weight: 700;
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
padding: 4px 8px;
|
||||
border-radius: 2px;
|
||||
border: 1.5px solid currentColor;
|
||||
}
|
||||
.tag.ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
.tag.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
.tag.bad {
|
||||
color: var(--bad);
|
||||
background: var(--bad);
|
||||
color: #fff;
|
||||
border-color: var(--bad);
|
||||
}
|
||||
.t-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
padding-top: 13px;
|
||||
border-top: 1.5px solid var(--line-soft);
|
||||
}
|
||||
.t-foot .up {
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
.t-foot .up small {
|
||||
font-weight: 500;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.spark {
|
||||
height: 24px;
|
||||
width: 90px;
|
||||
}
|
||||
.tile.wide .blurb {
|
||||
font-size: 14px;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.5;
|
||||
margin-top: 14px;
|
||||
max-width: 42ch;
|
||||
}
|
||||
|
||||
@keyframes pop {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(12px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.tile,
|
||||
.figure {
|
||||
animation: pop 0.5s both;
|
||||
}
|
||||
.grid .tile:nth-child(1) {
|
||||
animation-delay: 0.05s;
|
||||
}
|
||||
.grid .tile:nth-child(2) {
|
||||
animation-delay: 0.11s;
|
||||
}
|
||||
.grid .tile:nth-child(3) {
|
||||
animation-delay: 0.17s;
|
||||
}
|
||||
.grid .tile:nth-child(4) {
|
||||
animation-delay: 0.23s;
|
||||
}
|
||||
.grid .tile:nth-child(5) {
|
||||
animation-delay: 0.29s;
|
||||
}
|
||||
.grid .tile:nth-child(6) {
|
||||
animation-delay: 0.35s;
|
||||
}
|
||||
.grid .tile:nth-child(7) {
|
||||
animation-delay: 0.41s;
|
||||
}
|
||||
|
||||
.colophon {
|
||||
margin: 46px 0 30px;
|
||||
padding-top: 18px;
|
||||
border-top: 2.5px solid var(--line);
|
||||
font-family: 'Instrument Serif';
|
||||
font-style: italic;
|
||||
font-size: 15px;
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<!-- Masthead -->
|
||||
<div class="masthead">
|
||||
<div class="logo">
|
||||
<div class="glyph">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="tt">LAUNCHER<small>the home cloud edition</small></div>
|
||||
</div>
|
||||
<nav class="nav"><a class="on">Overview</a><a>Apps</a><a>Boards</a><a>Status</a></nav>
|
||||
<div class="tools">
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
Search<span class="k">⌘K</span>
|
||||
</div>
|
||||
<div class="ib">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path
|
||||
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Hero -->
|
||||
<section class="hero">
|
||||
<div class="hero-l">
|
||||
<div class="kicker">Tuesday · 27 May · 19:42</div>
|
||||
<h1>Your stack,<br />all in <em>one place.</em></h1>
|
||||
<p>
|
||||
Ten services under one roof. Eight humming, two asking for attention. Everything
|
||||
launches from here.
|
||||
</p>
|
||||
<div class="hero-cta">
|
||||
<a class="btn solid">Open a board →</a><a class="btn ghost">Add an app</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="hero-r">
|
||||
<div class="figure">
|
||||
<div class="num acc">8/10</div>
|
||||
<div class="desc"><b>Services online</b>Deluge offline · Portainer slow to respond</div>
|
||||
</div>
|
||||
<div class="figure">
|
||||
<div class="num bl">99.4%</div>
|
||||
<div class="desc"><b>Fleet uptime</b>Rolling 30-day average across all monitors</div>
|
||||
</div>
|
||||
<div class="figure">
|
||||
<div class="num">142<span style="font-size: 20px">ms</span></div>
|
||||
<div class="desc"><b>Median response</b>p95 latency over the last 24 hours</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Apps -->
|
||||
<div class="slab">
|
||||
<h2>Favorites</h2>
|
||||
<div class="ln"></div>
|
||||
<div class="meta">eight pinned</div>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="tile wide">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">🎬</div>
|
||||
<div>
|
||||
<div class="t-name">Jellyfin</div>
|
||||
<div class="t-cat">Media server · the crown jewel</div>
|
||||
</div>
|
||||
<span class="tag ok">Online</span>
|
||||
</div>
|
||||
<p class="blurb">
|
||||
Streaming to 3 devices right now. Library scan completed 2 hours ago — 4,212 movies, 318
|
||||
shows indexed and healthy.
|
||||
</p>
|
||||
<div class="t-foot">
|
||||
<div class="up">99.9% <small>uptime · 24h</small></div>
|
||||
<svg class="spark" viewBox="0 0 90 24" preserveAspectRatio="none">
|
||||
<polyline
|
||||
fill="none"
|
||||
stroke="#ff5436"
|
||||
stroke-width="2.2"
|
||||
points="0,18 12,15 24,17 36,9 48,12 60,7 72,11 82,5 90,8"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">📷</div>
|
||||
<div>
|
||||
<div class="t-name">Immich</div>
|
||||
<div class="t-cat">Photos</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-foot">
|
||||
<div class="up">100% <small>24h</small></div>
|
||||
<span class="tag ok">Up</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">🌿</div>
|
||||
<div>
|
||||
<div class="t-name">Gitea</div>
|
||||
<div class="t-cat">Git</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-foot">
|
||||
<div class="up">99.8% <small>24h</small></div>
|
||||
<span class="tag ok">Up</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">🐳</div>
|
||||
<div>
|
||||
<div class="t-name">Portainer</div>
|
||||
<div class="t-cat">Containers</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-foot">
|
||||
<div class="up">98.1% <small>24h</small></div>
|
||||
<span class="tag warn">Slow</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">🛡️</div>
|
||||
<div>
|
||||
<div class="t-name">Pi-hole</div>
|
||||
<div class="t-cat">DNS</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-foot">
|
||||
<div class="up">100% <small>24h</small></div>
|
||||
<span class="tag ok">Up</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">📋</div>
|
||||
<div>
|
||||
<div class="t-name">Planka</div>
|
||||
<div class="t-cat">Kanban</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-foot">
|
||||
<div class="up">99.5% <small>24h</small></div>
|
||||
<span class="tag ok">Up</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tile">
|
||||
<div class="t-top">
|
||||
<div class="t-ico">⬇️</div>
|
||||
<div>
|
||||
<div class="t-name">Deluge</div>
|
||||
<div class="t-cat">Downloads</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="t-foot">
|
||||
<div class="up" style="color: var(--bad)">—</div>
|
||||
<span class="tag bad">Down</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="colophon">
|
||||
<span>Editorial — Bricolage Grotesque + Instrument Serif</span
|
||||
><span>warm paper · ink rules · hard shadows · asymmetric grid</span>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,723 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Web App Launcher — Cozy Home</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Figtree:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #fdf8f2; /* warm cream */
|
||||
--bg-2: #f6efe4;
|
||||
--card: #fffdfa;
|
||||
--ink: #3a322b;
|
||||
--ink-2: #857a6d;
|
||||
--ink-faint: #b3a899;
|
||||
--line: #ece2d3;
|
||||
--peach: #ff9a76;
|
||||
--terra: #e8754f;
|
||||
--sage: #7fb069;
|
||||
--sky: #6ca9d6;
|
||||
--butter: #f3c969;
|
||||
--lav: #b09fd6;
|
||||
--ok: #5fa86c;
|
||||
--warn: #d99a2b;
|
||||
--bad: #e0685f;
|
||||
--radius: 22px;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--ink);
|
||||
font-family: 'Figtree', system-ui, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
body::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 0;
|
||||
pointer-events: none;
|
||||
background:
|
||||
radial-gradient(50% 40% at 12% 0%, rgba(255, 154, 118, 0.16), transparent 70%),
|
||||
radial-gradient(45% 40% at 95% 8%, rgba(108, 169, 214, 0.14), transparent 70%),
|
||||
radial-gradient(50% 45% at 85% 100%, rgba(127, 176, 105, 0.12), transparent 70%);
|
||||
}
|
||||
.shell {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
grid-template-columns: 236px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* sidebar */
|
||||
.side {
|
||||
padding: 24px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 11px;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.brand .m {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 14px;
|
||||
background: linear-gradient(135deg, var(--peach), var(--terra));
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: #fff;
|
||||
box-shadow: 0 10px 22px -8px var(--terra);
|
||||
}
|
||||
.brand .m svg {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
.brand .t {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 19px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.brand .t span {
|
||||
display: block;
|
||||
font-family: 'Figtree';
|
||||
font-weight: 500;
|
||||
font-size: 11px;
|
||||
color: var(--ink-faint);
|
||||
}
|
||||
.nlabel {
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 10.5px;
|
||||
color: var(--ink-faint);
|
||||
margin: 16px 10px 8px;
|
||||
}
|
||||
.ni {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 11px 13px;
|
||||
border-radius: 14px;
|
||||
color: var(--ink-2);
|
||||
font-weight: 600;
|
||||
font-size: 14.5px;
|
||||
cursor: pointer;
|
||||
transition: 0.16s;
|
||||
}
|
||||
.ni svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
.ni:hover {
|
||||
background: var(--bg-2);
|
||||
color: var(--ink);
|
||||
}
|
||||
.ni.on {
|
||||
background: var(--card);
|
||||
color: var(--terra);
|
||||
box-shadow:
|
||||
0 6px 16px -8px rgba(0, 0, 0, 0.18),
|
||||
inset 0 0 0 1px var(--line);
|
||||
}
|
||||
.ni .c {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
background: var(--bg-2);
|
||||
color: var(--ink-2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
.side-card {
|
||||
margin-top: auto;
|
||||
background: linear-gradient(135deg, rgba(127, 176, 105, 0.16), rgba(108, 169, 214, 0.14));
|
||||
border-radius: 18px;
|
||||
padding: 16px;
|
||||
border: 1px solid var(--line);
|
||||
}
|
||||
.side-card p {
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-2);
|
||||
line-height: 1.4;
|
||||
}
|
||||
.side-card .who {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
.side-card .av {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 11px;
|
||||
background: linear-gradient(135deg, var(--lav), var(--sky));
|
||||
color: #fff;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* main */
|
||||
.main {
|
||||
padding: 30px 36px 40px;
|
||||
min-width: 0;
|
||||
}
|
||||
.top {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.top .search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 16px;
|
||||
padding: 12px 16px;
|
||||
color: var(--ink-faint);
|
||||
font-size: 14px;
|
||||
width: 320px;
|
||||
cursor: text;
|
||||
box-shadow: 0 4px 14px -10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.top .search svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.top .search .k {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
background: var(--bg-2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.rbtn {
|
||||
margin-left: auto;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 15px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
color: var(--ink-2);
|
||||
cursor: pointer;
|
||||
box-shadow: 0 4px 14px -10px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.rbtn + .rbtn {
|
||||
margin-left: 0;
|
||||
}
|
||||
.rbtn:hover {
|
||||
color: var(--terra);
|
||||
}
|
||||
.rbtn svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
}
|
||||
|
||||
.greet {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 34px;
|
||||
letter-spacing: -0.02em;
|
||||
margin: 18px 0 4px;
|
||||
}
|
||||
.greet .wave {
|
||||
display: inline-block;
|
||||
animation: wave 2.4s ease-in-out infinite;
|
||||
transform-origin: 70% 70%;
|
||||
}
|
||||
@keyframes wave {
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(16deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
}
|
||||
.gsub {
|
||||
color: var(--ink-2);
|
||||
font-size: 15px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
.gsub b {
|
||||
color: var(--sage);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* summary chips */
|
||||
.chips {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 30px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.chip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 18px;
|
||||
padding: 14px 18px;
|
||||
box-shadow: 0 8px 20px -16px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
.chip .ic {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 12px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
}
|
||||
.chip .ic svg {
|
||||
width: 19px;
|
||||
height: 19px;
|
||||
color: #fff;
|
||||
}
|
||||
.chip .v {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 21px;
|
||||
line-height: 1;
|
||||
}
|
||||
.chip .l {
|
||||
font-size: 12.5px;
|
||||
color: var(--ink-2);
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.sec {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 12px;
|
||||
margin: 6px 0 18px;
|
||||
}
|
||||
.sec h2 {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 22px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.sec .more {
|
||||
margin-left: auto;
|
||||
font-size: 13.5px;
|
||||
color: var(--terra);
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.apps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
cursor: pointer;
|
||||
transition:
|
||||
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
box-shadow 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 10px 26px -20px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.card:hover {
|
||||
transform: translateY(-6px) rotate(-0.4deg);
|
||||
box-shadow: 0 22px 40px -22px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
.blob {
|
||||
position: absolute;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
filter: blur(30px);
|
||||
opacity: 0.5;
|
||||
top: -50px;
|
||||
right: -40px;
|
||||
}
|
||||
.ico {
|
||||
width: 54px;
|
||||
height: 54px;
|
||||
border-radius: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 26px;
|
||||
position: relative;
|
||||
}
|
||||
.nm {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 17px;
|
||||
margin-top: 16px;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.ct {
|
||||
font-size: 13px;
|
||||
color: var(--ink-2);
|
||||
margin-top: 1px;
|
||||
}
|
||||
.foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 18px;
|
||||
}
|
||||
.dot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dot .b {
|
||||
width: 9px;
|
||||
height: 9px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.dot.ok {
|
||||
color: var(--ok);
|
||||
}
|
||||
.dot.ok .b {
|
||||
background: var(--ok);
|
||||
box-shadow: 0 0 0 4px rgba(95, 168, 108, 0.18);
|
||||
}
|
||||
.dot.warn {
|
||||
color: var(--warn);
|
||||
}
|
||||
.dot.warn .b {
|
||||
background: var(--warn);
|
||||
box-shadow: 0 0 0 4px rgba(217, 154, 43, 0.18);
|
||||
}
|
||||
.dot.bad {
|
||||
color: var(--bad);
|
||||
}
|
||||
.dot.bad .b {
|
||||
background: var(--bad);
|
||||
box-shadow: 0 0 0 4px rgba(224, 104, 95, 0.18);
|
||||
}
|
||||
.up {
|
||||
font-size: 12px;
|
||||
color: var(--ink-faint);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@keyframes rise {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(16px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
.card,
|
||||
.chip {
|
||||
animation: rise 0.55s both;
|
||||
}
|
||||
.chip:nth-child(2) {
|
||||
animation-delay: 0.06s;
|
||||
}
|
||||
.chip:nth-child(3) {
|
||||
animation-delay: 0.12s;
|
||||
}
|
||||
.chip:nth-child(4) {
|
||||
animation-delay: 0.18s;
|
||||
}
|
||||
.apps .card:nth-child(1) {
|
||||
animation-delay: 0.1s;
|
||||
}
|
||||
.apps .card:nth-child(2) {
|
||||
animation-delay: 0.16s;
|
||||
}
|
||||
.apps .card:nth-child(3) {
|
||||
animation-delay: 0.22s;
|
||||
}
|
||||
.apps .card:nth-child(4) {
|
||||
animation-delay: 0.28s;
|
||||
}
|
||||
.apps .card:nth-child(5) {
|
||||
animation-delay: 0.34s;
|
||||
}
|
||||
.apps .card:nth-child(6) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
.apps .card:nth-child(7) {
|
||||
animation-delay: 0.46s;
|
||||
}
|
||||
.apps .card:nth-child(8) {
|
||||
animation-delay: 0.52s;
|
||||
}
|
||||
|
||||
.note {
|
||||
text-align: center;
|
||||
color: var(--ink-faint);
|
||||
font-family: 'Fraunces';
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
margin-top: 36px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<!-- Sidebar -->
|
||||
<aside class="side">
|
||||
<div class="brand">
|
||||
<div class="m">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<rect x="3" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="2" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="t">Launcher<span>our home cloud</span></div>
|
||||
</div>
|
||||
<div class="nlabel">Menu</div>
|
||||
<div class="ni on">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<path d="M3 11l9-8 9 8M5 10v10h14V10" />
|
||||
</svg>
|
||||
Home
|
||||
</div>
|
||||
<div class="ni">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" />
|
||||
</svg>
|
||||
All apps <span class="c">10</span>
|
||||
</div>
|
||||
<div class="ni">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
Status
|
||||
</div>
|
||||
<div class="nlabel">Rooms</div>
|
||||
<div class="ni">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||
</svg>
|
||||
Movie night
|
||||
</div>
|
||||
<div class="ni">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||
</svg>
|
||||
The basement rack
|
||||
</div>
|
||||
<div class="side-card">
|
||||
<p>“Everything’s running smoothly today. ☕ Two apps want a peek when you get a sec.”</p>
|
||||
<div class="who">
|
||||
<div class="av">AD</div>
|
||||
<div>
|
||||
<div style="font-weight: 700; font-size: 13px">Alexei</div>
|
||||
<div style="font-size: 11px; color: var(--ink-faint)">Admin</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main -->
|
||||
<main class="main">
|
||||
<div class="top">
|
||||
<div class="search">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<circle cx="11" cy="11" r="7" />
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||
</svg>
|
||||
Search your apps… <span class="k">⌘K</span>
|
||||
</div>
|
||||
<div class="rbtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="rbtn">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path
|
||||
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h1 class="greet">Hi Alexei <span class="wave">👋</span></h1>
|
||||
<p class="gsub">It’s a calm evening — <b>8 of your 10 apps</b> are happy and online.</p>
|
||||
|
||||
<div class="chips">
|
||||
<div class="chip">
|
||||
<div class="ic" style="background: var(--sage)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="v">8/10</div>
|
||||
<div class="l">Apps online</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chip">
|
||||
<div class="ic" style="background: var(--sky)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M12 7v5l3 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="v">142ms</div>
|
||||
<div class="l">Avg speed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chip">
|
||||
<div class="ic" style="background: var(--butter)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M3 12h4l3 8 4-16 3 8h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="v">99.4%</div>
|
||||
<div class="l">Uptime · 30d</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chip">
|
||||
<div class="ic" style="background: var(--lav)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||
<path d="M13 2 3 14h7l-1 8 10-12h-7z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<div class="v">38 min</div>
|
||||
<div class="l">Battery left</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sec">
|
||||
<h2>Your favorites</h2>
|
||||
<a class="more" href="#">See all →</a>
|
||||
</div>
|
||||
<div class="apps">
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--terra)"></div>
|
||||
<div class="ico" style="background: rgba(232, 117, 79, 0.16)">🎬</div>
|
||||
<div class="nm">Jellyfin</div>
|
||||
<div class="ct">Movies & shows</div>
|
||||
<div class="foot">
|
||||
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--sky)"></div>
|
||||
<div class="ico" style="background: rgba(108, 169, 214, 0.18)">📷</div>
|
||||
<div class="nm">Immich</div>
|
||||
<div class="ct">Photos</div>
|
||||
<div class="foot">
|
||||
<span class="dot ok"><span class="b"></span>Online</span><span class="up">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--sage)"></div>
|
||||
<div class="ico" style="background: rgba(127, 176, 105, 0.18)">🌿</div>
|
||||
<div class="nm">Gitea</div>
|
||||
<div class="ct">Code</div>
|
||||
<div class="foot">
|
||||
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.8%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--butter)"></div>
|
||||
<div class="ico" style="background: rgba(243, 201, 105, 0.22)">🐳</div>
|
||||
<div class="nm">Portainer</div>
|
||||
<div class="ct">Containers</div>
|
||||
<div class="foot">
|
||||
<span class="dot warn"><span class="b"></span>A bit slow</span
|
||||
><span class="up">98.1%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--lav)"></div>
|
||||
<div class="ico" style="background: rgba(176, 159, 214, 0.2)">🛡️</div>
|
||||
<div class="nm">Pi-hole</div>
|
||||
<div class="ct">Ad blocker</div>
|
||||
<div class="foot">
|
||||
<span class="dot ok"><span class="b"></span>Online</span><span class="up">100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--peach)"></div>
|
||||
<div class="ico" style="background: rgba(255, 154, 118, 0.2)">📋</div>
|
||||
<div class="nm">Planka</div>
|
||||
<div class="ct">To-dos</div>
|
||||
<div class="foot">
|
||||
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.5%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--bad)"></div>
|
||||
<div class="ico" style="background: rgba(224, 104, 95, 0.16)">⬇️</div>
|
||||
<div class="nm">Deluge</div>
|
||||
<div class="ct">Downloads</div>
|
||||
<div class="foot">
|
||||
<span class="dot bad"><span class="b"></span>Asleep</span><span class="up">—</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--sky)"></div>
|
||||
<div class="ico" style="background: rgba(108, 169, 214, 0.18)">🔀</div>
|
||||
<div class="nm">Proxy</div>
|
||||
<div class="ct">Networking</div>
|
||||
<div class="foot">
|
||||
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="note">
|
||||
Cozy Home — Fraunces + Figtree · warm cream · soft pastel rooms · gentle motion
|
||||
</p>
|
||||
</main>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,639 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Cozy Home — Design System Reference</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Figtree:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
/* === The exact tokens now living in src/app.css (light/cream) === */
|
||||
:root {
|
||||
--background: hsl(35 56% 97%);
|
||||
--foreground: hsl(33 18% 18%);
|
||||
--muted: hsl(36 42% 93%);
|
||||
--muted-foreground: hsl(34 12% 47%);
|
||||
--card: hsl(40 60% 99%);
|
||||
--border: hsl(36 35% 88%);
|
||||
--primary: hsl(16 72% 56%);
|
||||
--primary-foreground: hsl(40 60% 99%);
|
||||
--accent: hsl(34 44% 90%);
|
||||
--status-online: #5fa86c;
|
||||
--status-offline: #e0685f;
|
||||
--status-degraded: #d99a2b;
|
||||
--status-unknown: #b3a899;
|
||||
--room-sage: #7fb069;
|
||||
--room-sky: #6ca9d6;
|
||||
--room-butter: #f3c969;
|
||||
--room-lav: #b09fd6;
|
||||
--room-peach: #ff9a76;
|
||||
--room-terra: #e8754f;
|
||||
--radius: 1rem;
|
||||
--shadow-soft: 0 10px 26px -20px rgba(80, 50, 20, 0.45);
|
||||
--shadow-lift: 0 22px 40px -22px rgba(80, 50, 20, 0.4);
|
||||
--font-sans: 'Figtree', system-ui, sans-serif;
|
||||
--font-display: 'Fraunces', serif;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-sans);
|
||||
padding: 40px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
font-family: var(--font-display);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1080px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.title {
|
||||
font-size: 34px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.lede {
|
||||
color: var(--muted-foreground);
|
||||
margin: 6px 0 8px;
|
||||
max-width: 64ch;
|
||||
}
|
||||
.path {
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 12px;
|
||||
color: var(--room-terra);
|
||||
background: color-mix(in srgb, var(--room-terra) 12%, transparent);
|
||||
padding: 3px 9px;
|
||||
border-radius: 8px;
|
||||
display: inline-block;
|
||||
}
|
||||
section {
|
||||
margin-top: 42px;
|
||||
}
|
||||
.h {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted-foreground);
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 14px;
|
||||
align-items: center;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
/* tokens */
|
||||
.swatch {
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
background: var(--card);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
.swatch .c {
|
||||
height: 56px;
|
||||
}
|
||||
.swatch .n {
|
||||
padding: 9px 12px;
|
||||
font-size: 12px;
|
||||
}
|
||||
.swatch .n b {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
}
|
||||
.swatch .n span {
|
||||
color: var(--muted-foreground);
|
||||
font-family: ui-monospace, monospace;
|
||||
font-size: 10.5px;
|
||||
}
|
||||
|
||||
/* buttons */
|
||||
.btn {
|
||||
font-family: var(--font-sans);
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
padding: 11px 18px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: 0.18s;
|
||||
}
|
||||
.btn-primary {
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
.btn-primary:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: var(--shadow-lift);
|
||||
}
|
||||
.btn-secondary {
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
border-color: var(--border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
.btn-secondary:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: color-mix(in srgb, var(--primary) 40%, transparent);
|
||||
}
|
||||
.btn-ghost {
|
||||
background: transparent;
|
||||
color: var(--foreground);
|
||||
}
|
||||
.btn-ghost:hover {
|
||||
background: var(--accent);
|
||||
}
|
||||
.btn-icon {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 14px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* inputs */
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 7px;
|
||||
max-width: 340px;
|
||||
}
|
||||
.field label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.input {
|
||||
font-family: var(--font-sans);
|
||||
font-size: 14px;
|
||||
padding: 11px 14px;
|
||||
border-radius: 14px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
box-shadow: var(--shadow-soft);
|
||||
transition: 0.16s;
|
||||
}
|
||||
.input:focus {
|
||||
outline: none;
|
||||
border-color: var(--primary);
|
||||
box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent);
|
||||
}
|
||||
.hint {
|
||||
font-size: 12px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
|
||||
/* badges / status pills */
|
||||
.pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
padding: 5px 11px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
.pill .b {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
.pill.ok {
|
||||
color: var(--status-online);
|
||||
background: color-mix(in srgb, var(--status-online) 14%, transparent);
|
||||
}
|
||||
.pill.warn {
|
||||
color: var(--status-degraded);
|
||||
background: color-mix(in srgb, var(--status-degraded) 14%, transparent);
|
||||
}
|
||||
.pill.bad {
|
||||
color: var(--status-offline);
|
||||
background: color-mix(in srgb, var(--status-offline) 14%, transparent);
|
||||
}
|
||||
.pill .b {
|
||||
background: currentColor;
|
||||
}
|
||||
.room-pill {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
/* tabs */
|
||||
.tabs {
|
||||
display: inline-flex;
|
||||
gap: 4px;
|
||||
background: var(--muted);
|
||||
padding: 5px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.tab {
|
||||
font-weight: 600;
|
||||
font-size: 13.5px;
|
||||
padding: 8px 16px;
|
||||
border-radius: 11px;
|
||||
cursor: pointer;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
.tab.on {
|
||||
background: var(--card);
|
||||
color: var(--foreground);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
/* card */
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1.4rem;
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow-soft);
|
||||
max-width: 300px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.card .blob {
|
||||
position: absolute;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
border-radius: 50%;
|
||||
filter: blur(30px);
|
||||
opacity: 0.45;
|
||||
top: -50px;
|
||||
right: -40px;
|
||||
}
|
||||
.card .ic {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 18px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 25px;
|
||||
position: relative;
|
||||
}
|
||||
.card h3 {
|
||||
font-size: 17px;
|
||||
font-weight: 600;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.card .ct {
|
||||
font-size: 13px;
|
||||
color: var(--muted-foreground);
|
||||
}
|
||||
.card .f {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* dialog */
|
||||
.dialog {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1.4rem;
|
||||
padding: 24px;
|
||||
box-shadow: var(--shadow-lift);
|
||||
max-width: 380px;
|
||||
}
|
||||
.dialog h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dialog p {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 14px;
|
||||
margin: 8px 0 20px;
|
||||
}
|
||||
|
||||
/* table */
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 1.2rem;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
th {
|
||||
text-align: left;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--muted-foreground);
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
td {
|
||||
padding: 14px 18px;
|
||||
font-size: 14px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
tr:hover td {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
/* empty state */
|
||||
.empty {
|
||||
text-align: center;
|
||||
border: 2px dashed var(--border);
|
||||
border-radius: 1.4rem;
|
||||
padding: 48px 24px;
|
||||
background: color-mix(in srgb, var(--card) 50%, transparent);
|
||||
}
|
||||
.empty .e-ic {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 22px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
margin: 0 auto 16px;
|
||||
background: color-mix(in srgb, var(--room-peach) 20%, transparent);
|
||||
color: var(--room-terra);
|
||||
}
|
||||
.empty h3 {
|
||||
font-size: 19px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.empty p {
|
||||
color: var(--muted-foreground);
|
||||
font-size: 14px;
|
||||
margin: 6px auto 18px;
|
||||
max-width: 36ch;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1 class="title">Cozy Home — Design System</h1>
|
||||
<p class="lede">
|
||||
The component pattern sheet for the migration. Every phase styles its components to match
|
||||
these primitives. Tokens here mirror what now lives in
|
||||
<span class="path">src/app.css</span> — change them there and the whole app follows.
|
||||
</p>
|
||||
|
||||
<section>
|
||||
<div class="h">Color tokens</div>
|
||||
<div class="grid">
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--background)"></div>
|
||||
<div class="n"><b>background</b><span>cream #fdf8f2</span></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--card)"></div>
|
||||
<div class="n"><b>card</b><span>#fffdfa</span></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--primary)"></div>
|
||||
<div class="n"><b>primary</b><span>terracotta · tunable</span></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--foreground)"></div>
|
||||
<div class="n"><b>foreground</b><span>warm ink</span></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--muted)"></div>
|
||||
<div class="n"><b>muted</b><span>#f3ecde</span></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--border)"></div>
|
||||
<div class="n"><b>border</b><span>#ece2d3</span></div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 14px" class="grid">
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--room-terra)"></div>
|
||||
<div class="n"><b>room · terra</b></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--room-peach)"></div>
|
||||
<div class="n"><b>room · peach</b></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--room-butter)"></div>
|
||||
<div class="n"><b>room · butter</b></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--room-sage)"></div>
|
||||
<div class="n"><b>room · sage</b></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--room-sky)"></div>
|
||||
<div class="n"><b>room · sky</b></div>
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<div class="c" style="background: var(--room-lav)"></div>
|
||||
<div class="n"><b>room · lav</b></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Typography — Fraunces (display) · Figtree (body)</div>
|
||||
<h1 style="font-size: 46px; font-weight: 600">Good evening, Alexei</h1>
|
||||
<h2 style="font-size: 28px; font-weight: 600; margin-top: 10px">Your favorites</h2>
|
||||
<h3 style="font-size: 19px; font-weight: 600; margin-top: 10px">Jellyfin</h3>
|
||||
<p style="margin-top: 8px; max-width: 60ch">
|
||||
Body copy is Figtree — friendly, legible, rounded. It carries descriptions, hints, and
|
||||
plain-language status like “a bit slow” or “asleep”.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Buttons</div>
|
||||
<div class="row">
|
||||
<button class="btn btn-primary">Open a board</button>
|
||||
<button class="btn btn-secondary">Add an app</button>
|
||||
<button class="btn btn-ghost">Cancel</button>
|
||||
<button class="btn btn-secondary btn-icon" title="icon">🔔</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Form fields</div>
|
||||
<div class="row" style="align-items: flex-start">
|
||||
<div class="field">
|
||||
<label>App name</label><input class="input" value="Jellyfin" /><span class="hint"
|
||||
>Shown on the card and in search.</span
|
||||
>
|
||||
</div>
|
||||
<div class="field"><label>URL</label><input class="input" placeholder="https://…" /></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Status pills & room tags</div>
|
||||
<div class="row">
|
||||
<span class="pill ok"><span class="b"></span>Online</span>
|
||||
<span class="pill warn"><span class="b"></span>A bit slow</span>
|
||||
<span class="pill bad"><span class="b"></span>Asleep</span>
|
||||
<span
|
||||
class="room-pill"
|
||||
style="
|
||||
color: var(--room-terra);
|
||||
background: color-mix(in srgb, var(--room-terra) 16%, transparent);
|
||||
"
|
||||
>Media</span
|
||||
>
|
||||
<span
|
||||
class="room-pill"
|
||||
style="
|
||||
color: var(--room-sky);
|
||||
background: color-mix(in srgb, var(--room-sky) 16%, transparent);
|
||||
"
|
||||
>Network</span
|
||||
>
|
||||
<span
|
||||
class="room-pill"
|
||||
style="
|
||||
color: var(--room-sage);
|
||||
background: color-mix(in srgb, var(--room-sage) 16%, transparent);
|
||||
"
|
||||
>Git</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Tabs</div>
|
||||
<div class="tabs">
|
||||
<div class="tab on">Overview</div>
|
||||
<div class="tab">Activity</div>
|
||||
<div class="tab">Settings</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">App card</div>
|
||||
<div class="card">
|
||||
<div class="blob" style="background: var(--room-terra)"></div>
|
||||
<div
|
||||
class="ic"
|
||||
style="
|
||||
background: color-mix(in srgb, var(--room-terra) 18%, transparent);
|
||||
color: var(--room-terra);
|
||||
"
|
||||
>
|
||||
🎬
|
||||
</div>
|
||||
<h3>Jellyfin</h3>
|
||||
<div class="ct">Movies & shows</div>
|
||||
<div class="f">
|
||||
<span class="pill ok"><span class="b"></span>Online</span
|
||||
><span class="hint">99.9%</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Dialog</div>
|
||||
<div class="dialog">
|
||||
<h3>Remove Deluge?</h3>
|
||||
<p>This deletes the app and its uptime history. This can’t be undone.</p>
|
||||
<div class="row" style="justify-content: flex-end">
|
||||
<button class="btn btn-ghost">Keep it</button
|
||||
><button class="btn btn-primary" style="background: var(--status-offline)">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Table (admin)</div>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
<th>Role</th>
|
||||
<th>Status</th>
|
||||
<th>Last seen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Alexei</td>
|
||||
<td>Admin</td>
|
||||
<td>
|
||||
<span class="pill ok"><span class="b"></span>Active</span>
|
||||
</td>
|
||||
<td>just now</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Guest</td>
|
||||
<td>Viewer</td>
|
||||
<td>
|
||||
<span class="pill warn"><span class="b"></span>Idle</span>
|
||||
</td>
|
||||
<td>2h ago</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<div class="h">Empty state</div>
|
||||
<div class="empty">
|
||||
<div class="e-ic">
|
||||
<svg
|
||||
width="30"
|
||||
height="30"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.6"
|
||||
>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z" />
|
||||
</svg>
|
||||
</div>
|
||||
<h3>No apps yet</h3>
|
||||
<p>Add your first service and it’ll show up here with live status.</p>
|
||||
<button class="btn btn-primary">+ Add an app</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<p
|
||||
style="
|
||||
margin: 48px 0 20px;
|
||||
text-align: center;
|
||||
font-family: var(--font-display);
|
||||
font-style: italic;
|
||||
color: var(--muted-foreground);
|
||||
"
|
||||
>
|
||||
Cozy Home design system · mirrors src/app.css · use as the pattern for every migrated
|
||||
component
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,160 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Launcher — Redesign Mockups</title>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Figtree:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
body {
|
||||
font-family: 'Figtree', system-ui, sans-serif;
|
||||
background: #0e0e12;
|
||||
color: #eee;
|
||||
min-height: 100vh;
|
||||
padding: 48px 24px;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 1000px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
h1 {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 34px;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
p.sub {
|
||||
color: #9a99a6;
|
||||
margin-top: 8px;
|
||||
font-size: 15px;
|
||||
line-height: 1.5;
|
||||
max-width: 62ch;
|
||||
}
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 18px;
|
||||
margin-top: 34px;
|
||||
}
|
||||
a.card {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border: 1px solid #24242e;
|
||||
border-radius: 16px;
|
||||
padding: 24px;
|
||||
background: #15151c;
|
||||
transition: 0.2s;
|
||||
}
|
||||
a.card:hover {
|
||||
transform: translateY(-4px);
|
||||
border-color: #3a3a4a;
|
||||
background: #191920;
|
||||
}
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
display: inline-block;
|
||||
}
|
||||
.nm {
|
||||
font-family: 'Fraunces';
|
||||
font-weight: 600;
|
||||
font-size: 21px;
|
||||
margin: 14px 0 6px;
|
||||
}
|
||||
.ds {
|
||||
color: #9a99a6;
|
||||
font-size: 13.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.swatch {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
.swatch span {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="wrap">
|
||||
<h1>Web App Launcher — Redesign Directions</h1>
|
||||
<p class="sub">
|
||||
Four aesthetic directions for the same launcher, built as theme presets of one modernized
|
||||
design system. Open each, resize, hover the cards, and try the live accent swatches in
|
||||
Aurora Glass. Pick the one that fits — or mix and match.
|
||||
</p>
|
||||
<div class="grid">
|
||||
<a class="card" href="01-command-deck.html">
|
||||
<span class="badge" style="background: #0d1f18; color: #36e0a4"
|
||||
>01 · Dark · Power-user</span
|
||||
>
|
||||
<div class="nm">Command Deck</div>
|
||||
<div class="ds">
|
||||
Mission-control / terminal. Dense, glanceable telemetry, LED status, monospace data.
|
||||
Saira + JetBrains Mono.
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<span style="background: #070a0d"></span><span style="background: #36e0a4"></span
|
||||
><span style="background: #ffb020"></span><span style="background: #ff4d5e"></span>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card" href="02-aurora-glass.html">
|
||||
<span class="badge" style="background: #1d1340; color: #b69cff">02 · Dark · Premium</span>
|
||||
<div class="nm">Aurora Glass</div>
|
||||
<div class="ds">
|
||||
Frosted glass over a living gradient mesh. Soft glows, generous space, fully retintable
|
||||
accent. Outfit + Manrope.
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<span style="background: #0a0a14"></span
|
||||
><span style="background: hsl(265 90% 66%)"></span
|
||||
><span style="background: hsl(325 85% 65%)"></span
|
||||
><span style="background: #34e0a1"></span>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card" href="03-editorial.html">
|
||||
<span class="badge" style="background: #2a1109; color: #ff5436">03 · Light · Bold</span>
|
||||
<div class="nm">Editorial</div>
|
||||
<div class="ds">
|
||||
Magazine masthead, big display type, ink rules, hard shadows, asymmetric grid. Bricolage
|
||||
Grotesque + Instrument Serif.
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<span style="background: #f4f1ea"></span><span style="background: #191712"></span
|
||||
><span style="background: #ff5436"></span><span style="background: #1f4ae0"></span>
|
||||
</div>
|
||||
</a>
|
||||
<a class="card" href="04-cozy-home.html">
|
||||
<span class="badge" style="background: #2a1c12; color: #ff9a76"
|
||||
>04 · Light · Friendly</span
|
||||
>
|
||||
<div class="nm">Cozy Home</div>
|
||||
<div class="ds">
|
||||
Warm cream, soft rounded cards, pastel “rooms”, gentle motion. Friendly for the whole
|
||||
household. Fraunces + Figtree.
|
||||
</div>
|
||||
<div class="swatch">
|
||||
<span style="background: #fdf8f2"></span><span style="background: #e8754f"></span
|
||||
><span style="background: #7fb069"></span><span style="background: #6ca9d6"></span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
+198
-83
@@ -4,83 +4,140 @@
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
/* HSL-based primary color (overridden by theme store via JS) */
|
||||
--primary-h: 220;
|
||||
--primary-s: 70%;
|
||||
--primary-l: 50%;
|
||||
/* =====================================================================
|
||||
COZY HOME design system
|
||||
---------------------------------------------------------------------
|
||||
Tokens are intentionally organised as a single swappable "bundle":
|
||||
the neutral ramp + accent + shape + type live here in :root / .dark.
|
||||
Swapping these blocks for another set (e.g. Command Deck / Aurora /
|
||||
Editorial) is all a future theme-preset system needs to do — no
|
||||
component edits required, because the whole app reads these vars.
|
||||
Accent stays user-tunable via --primary-h / --primary-s.
|
||||
===================================================================== */
|
||||
|
||||
--background: hsl(0 0% 100%);
|
||||
--foreground: hsl(240 10% 3.9%);
|
||||
--muted: hsl(240 4.8% 95.9%);
|
||||
--muted-foreground: hsl(240 3.8% 46.1%);
|
||||
--popover: hsl(0 0% 100%);
|
||||
--popover-foreground: hsl(240 10% 3.9%);
|
||||
--card: hsl(0 0% 100%);
|
||||
--card-foreground: hsl(240 10% 3.9%);
|
||||
--border: hsl(240 5.9% 90%);
|
||||
--input: hsl(240 5.9% 90%);
|
||||
:root {
|
||||
/* Accent — terracotta by default, still user-tunable from settings */
|
||||
--primary-h: 16;
|
||||
--primary-s: 72%;
|
||||
--primary-l: 56%;
|
||||
|
||||
/* Neutrals — warm cream "paper" ramp */
|
||||
--background: hsl(35 56% 97%); /* #fdf8f2 warm cream */
|
||||
--foreground: hsl(33 18% 18%); /* #3a322b warm ink */
|
||||
--muted: hsl(36 42% 93%); /* #f3ecde */
|
||||
--muted-foreground: hsl(34 12% 47%); /* #857a6d */
|
||||
--popover: hsl(40 60% 99%);
|
||||
--popover-foreground: hsl(33 18% 18%);
|
||||
--card: hsl(40 60% 99%); /* #fffdfa */
|
||||
--card-foreground: hsl(33 18% 18%);
|
||||
--border: hsl(36 35% 88%); /* #ece2d3 */
|
||||
--input: hsl(36 35% 88%);
|
||||
--primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
--primary-foreground: hsl(0 0% 98%);
|
||||
--secondary: hsl(240 4.8% 95.9%);
|
||||
--secondary-foreground: hsl(240 5.9% 10%);
|
||||
--accent: hsl(240 4.8% 95.9%);
|
||||
--accent-foreground: hsl(240 5.9% 10%);
|
||||
--destructive: hsl(0 72.2% 50.6%);
|
||||
--destructive-foreground: hsl(0 0% 98%);
|
||||
--primary-foreground: hsl(40 60% 99%);
|
||||
--secondary: hsl(36 42% 93%);
|
||||
--secondary-foreground: hsl(33 18% 22%);
|
||||
--accent: hsl(34 44% 90%); /* hover wash */
|
||||
--accent-foreground: hsl(33 18% 20%);
|
||||
--destructive: hsl(6 68% 56%);
|
||||
--destructive-foreground: hsl(40 60% 99%);
|
||||
--ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
--status-online: #22c55e;
|
||||
--status-offline: #ef4444;
|
||||
--status-degraded: #eab308;
|
||||
--status-unknown: #6b7280;
|
||||
--radius: 0.5rem;
|
||||
--sidebar: hsl(0 0% 98%);
|
||||
--sidebar-foreground: hsl(240 5.3% 26.1%);
|
||||
|
||||
/* Status — vivid values for dots / bars / rings / sparklines */
|
||||
--status-online: #5fa86c;
|
||||
--status-offline: #e0685f;
|
||||
--status-degraded: #d99a2b;
|
||||
--status-unknown: #b3a899;
|
||||
/* Status "ink" — darker, AA-legible as small text on cream + tinted washes */
|
||||
--status-online-ink: #2c723f;
|
||||
--status-offline-ink: #bd382e;
|
||||
--status-degraded-ink: #785406;
|
||||
--status-unknown-ink: #6b5f50;
|
||||
|
||||
/* Pastel "rooms" — category / board accents */
|
||||
--room-sage: #7fb069;
|
||||
--room-sky: #6ca9d6;
|
||||
--room-butter: #f3c969;
|
||||
--room-lav: #b09fd6;
|
||||
--room-peach: #ff9a76;
|
||||
--room-terra: #e8754f;
|
||||
|
||||
/* Shape — cozy rounding */
|
||||
--radius: 1rem;
|
||||
|
||||
/* Soft warm shadows */
|
||||
--shadow-soft: 0 10px 26px -20px rgba(80, 50, 20, 0.45);
|
||||
--shadow-lift: 0 22px 40px -22px rgba(80, 50, 20, 0.4);
|
||||
|
||||
/* Typography */
|
||||
--font-sans: 'Figtree', system-ui, -apple-system, sans-serif;
|
||||
--font-display: 'Fraunces', 'Figtree', Georgia, serif;
|
||||
|
||||
/* Sidebar */
|
||||
--sidebar: hsl(36 48% 95%);
|
||||
--sidebar-foreground: hsl(34 14% 32%);
|
||||
--sidebar-primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
--sidebar-primary-foreground: hsl(0 0% 98%);
|
||||
--sidebar-accent: hsl(240 4.8% 95.9%);
|
||||
--sidebar-accent-foreground: hsl(240 5.9% 10%);
|
||||
--sidebar-border: hsl(220 13% 91%);
|
||||
--sidebar-primary-foreground: hsl(40 60% 99%);
|
||||
--sidebar-accent: hsl(34 44% 90%);
|
||||
--sidebar-accent-foreground: hsl(33 18% 20%);
|
||||
--sidebar-border: hsl(36 35% 87%);
|
||||
--sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--primary-l: 60%;
|
||||
/* "Dusk" — warm charcoal, not cold black */
|
||||
--primary-l: 62%;
|
||||
|
||||
--background: hsl(240 10% 3.9%);
|
||||
--foreground: hsl(0 0% 98%);
|
||||
--muted: hsl(240 3.7% 15.9%);
|
||||
--muted-foreground: hsl(240 5% 64.9%);
|
||||
--popover: hsl(240 10% 3.9%);
|
||||
--popover-foreground: hsl(0 0% 98%);
|
||||
--card: hsl(240 6% 7%);
|
||||
--card-foreground: hsl(0 0% 98%);
|
||||
--border: hsl(240 3.7% 15.9%);
|
||||
--input: hsl(240 3.7% 15.9%);
|
||||
--background: hsl(30 14% 9%); /* #1a1714 */
|
||||
--foreground: hsl(35 30% 90%); /* #f0e9df */
|
||||
--muted: hsl(30 14% 16%); /* #2b2520 */
|
||||
--muted-foreground: hsl(35 14% 64%); /* #b3a899 */
|
||||
--popover: hsl(30 16% 12%);
|
||||
--popover-foreground: hsl(35 30% 90%);
|
||||
--card: hsl(30 16% 13%); /* #262019 */
|
||||
--card-foreground: hsl(35 30% 90%);
|
||||
--border: hsl(31 16% 19%); /* #352d24 */
|
||||
--input: hsl(31 16% 19%);
|
||||
--primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
--primary-foreground: hsl(240 5.9% 10%);
|
||||
--secondary: hsl(240 3.7% 15.9%);
|
||||
--secondary-foreground: hsl(0 0% 98%);
|
||||
--accent: hsl(240 3.7% 15.9%);
|
||||
--accent-foreground: hsl(0 0% 98%);
|
||||
--destructive: hsl(0 62.8% 30.6%);
|
||||
--destructive-foreground: hsl(0 0% 98%);
|
||||
--primary-foreground: hsl(30 18% 10%);
|
||||
--secondary: hsl(30 14% 16%);
|
||||
--secondary-foreground: hsl(35 30% 90%);
|
||||
--accent: hsl(30 14% 18%);
|
||||
--accent-foreground: hsl(35 30% 90%);
|
||||
--destructive: hsl(6 58% 46%);
|
||||
--destructive-foreground: hsl(40 60% 99%);
|
||||
--ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
--sidebar: hsl(240 5.9% 6%);
|
||||
--sidebar-foreground: hsl(240 4.8% 95.9%);
|
||||
|
||||
--status-online: #6dba79;
|
||||
--status-offline: #ea7a72;
|
||||
--status-degraded: #e3ab4a;
|
||||
--status-unknown: #9a8f80;
|
||||
/* On dusk charcoal the vivid values already clear AA — ink == vivid */
|
||||
--status-online-ink: #6dba79;
|
||||
--status-offline-ink: #ea7a72;
|
||||
--status-degraded-ink: #e3ab4a;
|
||||
--status-unknown-ink: #9a8f80;
|
||||
|
||||
--shadow-soft: 0 12px 30px -20px rgba(0, 0, 0, 0.65);
|
||||
--shadow-lift: 0 26px 46px -22px rgba(0, 0, 0, 0.6);
|
||||
|
||||
--sidebar: hsl(30 16% 11%);
|
||||
--sidebar-foreground: hsl(35 22% 82%);
|
||||
--sidebar-primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
--sidebar-primary-foreground: hsl(0 0% 100%);
|
||||
--sidebar-accent: hsl(240 3.7% 15.9%);
|
||||
--sidebar-accent-foreground: hsl(240 4.8% 95.9%);
|
||||
--sidebar-border: hsl(240 3.7% 15.9%);
|
||||
--sidebar-primary-foreground: hsl(30 18% 10%);
|
||||
--sidebar-accent: hsl(30 14% 18%);
|
||||
--sidebar-accent-foreground: hsl(35 30% 90%);
|
||||
--sidebar-border: hsl(31 16% 19%);
|
||||
--sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%);
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 8px);
|
||||
--radius-md: calc(var(--radius) - 4px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--radius-xl: calc(var(--radius) + 6px);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-display: var(--font-display);
|
||||
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
@@ -101,6 +158,23 @@
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-ring: var(--ring);
|
||||
|
||||
--color-status-online: var(--status-online);
|
||||
--color-status-offline: var(--status-offline);
|
||||
--color-status-degraded: var(--status-degraded);
|
||||
--color-status-unknown: var(--status-unknown);
|
||||
--color-status-online-ink: var(--status-online-ink);
|
||||
--color-status-offline-ink: var(--status-offline-ink);
|
||||
--color-status-degraded-ink: var(--status-degraded-ink);
|
||||
--color-status-unknown-ink: var(--status-unknown-ink);
|
||||
|
||||
--color-room-sage: var(--room-sage);
|
||||
--color-room-sky: var(--room-sky);
|
||||
--color-room-butter: var(--room-butter);
|
||||
--color-room-lav: var(--room-lav);
|
||||
--color-room-peach: var(--room-peach);
|
||||
--color-room-terra: var(--room-terra);
|
||||
|
||||
--color-sidebar: var(--sidebar);
|
||||
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||
--color-sidebar-primary: var(--sidebar-primary);
|
||||
@@ -117,10 +191,21 @@
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: var(--font-sans);
|
||||
font-feature-settings: 'ss01', 'cv01';
|
||||
transition:
|
||||
background-color 0.3s ease,
|
||||
color 0.3s ease;
|
||||
}
|
||||
/* Display face for headings — gives the cozy/editorial warmth */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
.font-display {
|
||||
font-family: var(--font-display);
|
||||
font-optical-sizing: auto;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Status Indicator Pulse ===== */
|
||||
@@ -138,27 +223,27 @@
|
||||
|
||||
.status-online {
|
||||
animation: status-pulse 2s ease-in-out infinite;
|
||||
color: hsl(142 71% 45%);
|
||||
color: var(--status-online);
|
||||
}
|
||||
|
||||
/* ===== Card Style Variants ===== */
|
||||
.card-solid {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.card-glass {
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background: color-mix(in srgb, var(--card) 60%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 30%, transparent);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.05);
|
||||
background: color-mix(in srgb, var(--card) 70%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
|
||||
box-shadow: var(--shadow-soft);
|
||||
}
|
||||
|
||||
.dark .card-glass {
|
||||
background: color-mix(in srgb, var(--card) 50%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 25%, transparent);
|
||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.15);
|
||||
background: color-mix(in srgb, var(--card) 55%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
|
||||
}
|
||||
|
||||
.card-outline {
|
||||
@@ -170,24 +255,17 @@
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
/* ===== Card Hover Effects ===== */
|
||||
/* ===== Card Hover Effects — gentle cozy lift + micro-tilt ===== */
|
||||
.card-hover {
|
||||
transition:
|
||||
transform 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
}
|
||||
|
||||
.card-hover:hover {
|
||||
transform: scale(1.02);
|
||||
box-shadow:
|
||||
0 10px 25px -5px rgba(0, 0, 0, 0.15),
|
||||
0 4px 10px -5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.dark .card-hover:hover {
|
||||
box-shadow:
|
||||
0 10px 25px -5px rgba(0, 0, 0, 0.4),
|
||||
0 4px 10px -5px rgba(0, 0, 0, 0.3);
|
||||
transform: translateY(-5px) rotate(-0.35deg);
|
||||
box-shadow: var(--shadow-lift);
|
||||
}
|
||||
|
||||
/* ===== Skeleton Loading ===== */
|
||||
@@ -201,14 +279,14 @@
|
||||
}
|
||||
|
||||
.skeleton {
|
||||
background: linear-gradient(90deg, var(--muted) 25%, hsl(240 4.8% 85%) 50%, var(--muted) 75%);
|
||||
background: linear-gradient(90deg, var(--muted) 25%, hsl(36 30% 86%) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
animation: shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: var(--radius);
|
||||
}
|
||||
|
||||
.dark .skeleton {
|
||||
background: linear-gradient(90deg, var(--muted) 25%, hsl(240 3.7% 22%) 50%, var(--muted) 75%);
|
||||
background: linear-gradient(90deg, var(--muted) 25%, hsl(30 12% 22%) 50%, var(--muted) 75%);
|
||||
background-size: 200% 100%;
|
||||
}
|
||||
|
||||
@@ -236,7 +314,7 @@
|
||||
[data-keyboard-selected='true'] {
|
||||
outline: 2px solid hsl(var(--primary-h) var(--primary-s) var(--primary-l));
|
||||
outline-offset: 2px;
|
||||
border-radius: var(--radius, 0.5rem);
|
||||
border-radius: var(--radius, 1rem);
|
||||
}
|
||||
|
||||
/* ===== Aurora Keyframes ===== */
|
||||
@@ -251,3 +329,40 @@
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== Cozy greeting wave ===== */
|
||||
@keyframes cozy-wave {
|
||||
0%,
|
||||
60%,
|
||||
100% {
|
||||
transform: rotate(0);
|
||||
}
|
||||
10% {
|
||||
transform: rotate(16deg);
|
||||
}
|
||||
20% {
|
||||
transform: rotate(-8deg);
|
||||
}
|
||||
30% {
|
||||
transform: rotate(14deg);
|
||||
}
|
||||
40% {
|
||||
transform: rotate(-4deg);
|
||||
}
|
||||
}
|
||||
|
||||
.cozy-wave {
|
||||
display: inline-block;
|
||||
transform-origin: 70% 70%;
|
||||
animation: cozy-wave 2.6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.cozy-wave,
|
||||
.status-online {
|
||||
animation: none;
|
||||
}
|
||||
.card-hover:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-1
@@ -5,11 +5,14 @@
|
||||
<link rel="icon" href="%sveltekit.assets%/icon.svg" type="image/svg+xml" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="manifest" href="%sveltekit.assets%/manifest.json" />
|
||||
<meta name="theme-color" content="#6366f1" />
|
||||
<meta name="theme-color" content="#e8754f" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
||||
<meta name="apple-mobile-web-app-title" content="Launcher" />
|
||||
<link rel="apple-touch-icon" href="%sveltekit.assets%/icon.svg" />
|
||||
<!-- Cozy Home typography: Fraunces (display) + Figtree (body).
|
||||
Self-hosted from /static/fonts so offline/LAN installs work with no external calls. -->
|
||||
<link rel="stylesheet" href="%sveltekit.assets%/fonts/fonts.css" />
|
||||
<script>
|
||||
// Inline script to prevent FOUC — set theme class before first paint
|
||||
(function () {
|
||||
|
||||
@@ -96,11 +96,11 @@
|
||||
}
|
||||
|
||||
function actionBadgeClass(action: string): string {
|
||||
if (action.includes('deleted')) return 'bg-red-500/10 text-red-500';
|
||||
if (action.includes('created')) return 'bg-green-500/10 text-green-500';
|
||||
if (action.includes('updated')) return 'bg-blue-500/10 text-blue-500';
|
||||
if (action === 'import') return 'bg-purple-500/10 text-purple-500';
|
||||
if (action === 'export') return 'bg-yellow-500/10 text-yellow-500';
|
||||
if (action.includes('deleted')) return 'bg-destructive/10 text-destructive';
|
||||
if (action.includes('created')) return 'bg-status-online/10 text-status-online-ink';
|
||||
if (action.includes('updated')) return 'bg-room-sky/15 text-room-sky';
|
||||
if (action === 'import') return 'bg-room-lav/15 text-room-lav';
|
||||
if (action === 'export') return 'bg-status-degraded/10 text-status-degraded-ink';
|
||||
return 'bg-muted text-muted-foreground';
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<select
|
||||
id="filter-action"
|
||||
bind:value={filterAction}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
{#each actionOptions as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
@@ -151,7 +151,7 @@
|
||||
<select
|
||||
id="filter-entity"
|
||||
bind:value={filterEntityType}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
{#each entityTypeOptions as opt (opt.value)}
|
||||
<option value={opt.value}>{opt.label}</option>
|
||||
@@ -165,7 +165,7 @@
|
||||
id="filter-from"
|
||||
type="date"
|
||||
bind:value={filterDateFrom}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -175,14 +175,14 @@
|
||||
id="filter-to"
|
||||
type="date"
|
||||
bind:value={filterDateTo}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onclick={applyFilters}
|
||||
class="rounded-md bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
@@ -190,7 +190,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={exportCsv}
|
||||
class="ml-auto rounded-md border border-input px-4 py-1.5 text-sm font-medium text-foreground hover:bg-accent"
|
||||
class="ml-auto rounded-xl border border-input px-4 py-1.5 text-sm font-medium text-foreground hover:bg-accent"
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
@@ -202,7 +202,7 @@
|
||||
<p class="text-muted-foreground">No audit log entries found</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -211,7 +211,7 @@
|
||||
type="button"
|
||||
onclick={handleCreate}
|
||||
disabled={creating}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{creating ? $t('admin.backup_creating') : $t('admin.backup_create')}
|
||||
</button>
|
||||
@@ -253,7 +253,7 @@
|
||||
type="button"
|
||||
onclick={() => (confirmRestore = backup.filename)}
|
||||
disabled={restoringFilename === backup.filename}
|
||||
class="rounded px-2 py-1 text-xs font-medium text-amber-600 hover:bg-amber-600/10 disabled:opacity-50 dark:text-amber-400"
|
||||
class="rounded-lg px-2 py-1 text-xs font-medium text-status-degraded-ink hover:bg-status-degraded/10 disabled:opacity-50"
|
||||
>
|
||||
{restoringFilename === backup.filename
|
||||
? '...'
|
||||
@@ -282,7 +282,7 @@
|
||||
<!-- Restore Confirmation Dialog -->
|
||||
{#if confirmRestore}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="mx-4 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div class="mx-4 w-full max-w-md rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]">
|
||||
<h3 class="mb-2 text-lg font-semibold text-card-foreground">
|
||||
{$t('admin.backup_restore_confirm_title')}
|
||||
</h3>
|
||||
@@ -301,7 +301,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => confirmRestore && handleRestore(confirmRestore)}
|
||||
class="rounded-md bg-amber-600 px-4 py-2 text-sm font-medium text-white hover:bg-amber-700"
|
||||
class="rounded-xl px-4 py-2 text-sm font-semibold text-white shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5" style="background: var(--status-degraded);"
|
||||
>
|
||||
{$t('admin.backup_restore')}
|
||||
</button>
|
||||
@@ -313,7 +313,7 @@
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
{#if confirmDelete}
|
||||
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="mx-4 w-full max-w-md rounded-lg border border-border bg-card p-6 shadow-lg">
|
||||
<div class="mx-4 w-full max-w-md rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]">
|
||||
<h3 class="mb-2 text-lg font-semibold text-card-foreground">
|
||||
{$t('admin.backup_delete_confirm_title')}
|
||||
</h3>
|
||||
@@ -354,7 +354,7 @@
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={schedule.backupEnabled}
|
||||
class="h-4 w-4 rounded border-border text-primary focus:ring-ring"
|
||||
class="h-4 w-4 rounded border-border text-primary focus-visible:ring-primary/30"
|
||||
/>
|
||||
<span class="text-sm text-foreground">{$t('admin.backup_schedule_enabled')}</span>
|
||||
</label>
|
||||
@@ -368,7 +368,7 @@
|
||||
<select
|
||||
id="cron-preset"
|
||||
bind:value={cronPreset}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-auto"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-auto"
|
||||
>
|
||||
<option value="daily">{$t('admin.backup_schedule_preset_daily')}</option>
|
||||
<option value="twice_daily">{$t('admin.backup_schedule_preset_twice_daily')}</option>
|
||||
@@ -383,7 +383,7 @@
|
||||
type="text"
|
||||
bind:value={customCron}
|
||||
placeholder="0 3 * * *"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-64"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground sm:w-64"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -399,7 +399,7 @@
|
||||
bind:value={schedule.backupMaxCount}
|
||||
min="1"
|
||||
max="100"
|
||||
class="w-24 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-24 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
@@ -408,7 +408,7 @@
|
||||
type="button"
|
||||
onclick={handleSaveSchedule}
|
||||
disabled={savingSchedule}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{savingSchedule ? $t('admin.backup_schedule_saving') : $t('admin.backup_schedule_save')}
|
||||
</button>
|
||||
@@ -418,9 +418,9 @@
|
||||
<!-- Status message -->
|
||||
{#if statusMessage}
|
||||
<div
|
||||
class="mt-4 rounded-md p-3 text-sm {statusType === 'success'
|
||||
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400'
|
||||
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'}"
|
||||
class="mt-4 rounded-xl border p-3 text-sm {statusType === 'success'
|
||||
? 'border-status-online/30 bg-status-online/10 text-status-online-ink'
|
||||
: 'border-destructive/30 bg-destructive/10 text-destructive'}"
|
||||
>
|
||||
{statusMessage}
|
||||
</div>
|
||||
|
||||
@@ -147,7 +147,7 @@
|
||||
type="button"
|
||||
onclick={handleScan}
|
||||
disabled={scanning || (!dockerSocketPath && !traefikApiUrl)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{scanning ? $t('admin.discovery_scanning') : $t('admin.discovery_scan')}
|
||||
</button>
|
||||
@@ -155,7 +155,7 @@
|
||||
|
||||
<!-- Scan Errors -->
|
||||
{#if scanErrors.length > 0}
|
||||
<div class="mb-4 rounded-md bg-yellow-100 p-3 text-sm text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
<div class="mb-4 rounded-xl border border-status-degraded/30 bg-status-degraded/10 p-3 text-sm text-status-degraded-ink">
|
||||
{#each scanErrors as scanError, idx (idx)}
|
||||
<p>{scanError}</p>
|
||||
{/each}
|
||||
@@ -204,8 +204,8 @@
|
||||
<td class="px-2 py-2">
|
||||
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
|
||||
{service.source === 'docker'
|
||||
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400'
|
||||
: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400'
|
||||
? 'bg-room-sky/15 text-room-sky'
|
||||
: 'bg-room-lav/15 text-room-lav'
|
||||
}"
|
||||
>
|
||||
{service.source === 'docker' ? $t('admin.discovery_source_docker') : $t('admin.discovery_source_traefik')}
|
||||
@@ -215,7 +215,7 @@
|
||||
{#if service.alreadyRegistered}
|
||||
<span class="text-xs text-muted-foreground">{$t('admin.discovery_already_registered')}</span>
|
||||
{:else}
|
||||
<span class="text-xs font-medium text-green-600 dark:text-green-400">{$t('admin.discovery_new')}</span>
|
||||
<span class="text-xs font-medium text-status-online-ink dark:text-status-online-ink">{$t('admin.discovery_new')}</span>
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -231,7 +231,7 @@
|
||||
type="button"
|
||||
onclick={handleApprove}
|
||||
disabled={approving || selected.size === 0}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{approving ? $t('admin.discovery_approving') : $t('admin.discovery_approve')} ({selected.size})
|
||||
</button>
|
||||
@@ -241,7 +241,7 @@
|
||||
|
||||
<!-- Status Message -->
|
||||
{#if statusMessage}
|
||||
<div class="mt-4 rounded-md p-3 text-sm {statusType === 'success' ? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' : 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'}">
|
||||
<div class="mt-4 rounded-xl border p-3 text-sm {statusType === 'success' ? 'border-status-online/30 bg-status-online/10 text-status-online-ink' : 'border-destructive/30 bg-destructive/10 text-destructive'}">
|
||||
{statusMessage}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
|
||||
<!-- Existing permissions list -->
|
||||
{#if permissions.length > 0}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
id="authMode"
|
||||
name="authMode"
|
||||
bind:value={$form.authMode}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="local">{$t('admin.auth_local')}</option>
|
||||
<option value="oauth">{$t('admin.auth_oauth')}</option>
|
||||
@@ -92,7 +92,7 @@
|
||||
name="oauthClientId"
|
||||
type="text"
|
||||
bind:value={$form.oauthClientId}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder={$t('admin.oauth_client_id_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
@@ -103,7 +103,7 @@
|
||||
name="oauthClientSecret"
|
||||
type="password"
|
||||
bind:value={$form.oauthClientSecret}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder={$t('admin.oauth_client_secret_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
@@ -114,7 +114,7 @@
|
||||
name="oauthDiscoveryUrl"
|
||||
type="url"
|
||||
bind:value={$form.oauthDiscoveryUrl}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder={$t('admin.oauth_discovery_url_placeholder')}
|
||||
/>
|
||||
{#if $errors.oauthDiscoveryUrl}<span class="text-xs text-destructive">{$errors.oauthDiscoveryUrl}</span>{/if}
|
||||
@@ -124,12 +124,12 @@
|
||||
type="button"
|
||||
onclick={testOAuthConnection}
|
||||
disabled={oauthTesting}
|
||||
class="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{oauthTesting ? $t('admin.oauth_testing') : $t('admin.oauth_test')}
|
||||
</button>
|
||||
{#if oauthTestResult}
|
||||
<span class="ml-3 text-sm {oauthTestSuccess ? 'text-green-600 dark:text-green-400' : 'text-destructive'}">
|
||||
<span class="ml-3 text-sm {oauthTestSuccess ? 'text-status-online-ink dark:text-status-online-ink' : 'text-destructive'}">
|
||||
{oauthTestResult}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -147,7 +147,7 @@
|
||||
id="defaultTheme"
|
||||
name="defaultTheme"
|
||||
bind:value={$form.defaultTheme}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="dark">{$t('theme.dark')}</option>
|
||||
<option value="light">{$t('theme.light')}</option>
|
||||
@@ -161,8 +161,8 @@
|
||||
name="defaultPrimaryColor"
|
||||
type="text"
|
||||
bind:value={$form.defaultPrimaryColor}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="#6366f1"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="#e8754f"
|
||||
pattern="^#[0-9a-fA-F]{6}$"
|
||||
/>
|
||||
{#if $form.defaultPrimaryColor}
|
||||
@@ -188,7 +188,7 @@
|
||||
name="healthcheckDefaults"
|
||||
bind:value={$form.healthcheckDefaults}
|
||||
rows="4"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 font-mono text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 font-mono text-sm text-foreground"
|
||||
placeholder={'{"interval": 300, "timeout": 5000, "method": "GET"}'}
|
||||
></textarea>
|
||||
{#if $errors.healthcheckDefaults}<span class="text-xs text-destructive">{$errors.healthcheckDefaults}</span>{/if}
|
||||
@@ -206,7 +206,7 @@
|
||||
id="dockerSocketPath"
|
||||
type="text"
|
||||
bind:value={dockerSocketPath}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="/var/run/docker.sock"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_docker_socket_hint')}</p>
|
||||
@@ -217,7 +217,7 @@
|
||||
id="traefikApiUrl"
|
||||
type="url"
|
||||
bind:value={traefikApiUrl}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
placeholder="http://traefik:8080"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_traefik_url_hint')}</p>
|
||||
@@ -244,7 +244,7 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
disabled={$delayed}
|
||||
>
|
||||
{$delayed ? $t('admin.saving_settings') : $t('admin.save_settings')}
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
|
||||
// Create form
|
||||
let newName = $state('');
|
||||
let newColor = $state('#6366f1');
|
||||
let newColor = $state('#e8754f');
|
||||
let showCreateForm = $state(false);
|
||||
|
||||
// Edit form
|
||||
let editingTag = $state<Tag | null>(null);
|
||||
let editName = $state('');
|
||||
let editColor = $state('#6366f1');
|
||||
let editColor = $state('#e8754f');
|
||||
|
||||
// Delete confirmation
|
||||
let confirmDeleteId = $state<string | null>(null);
|
||||
@@ -56,7 +56,7 @@
|
||||
});
|
||||
if (res.ok) {
|
||||
newName = '';
|
||||
newColor = '#6366f1';
|
||||
newColor = '#e8754f';
|
||||
showCreateForm = false;
|
||||
await loadTags();
|
||||
} else {
|
||||
@@ -71,7 +71,7 @@
|
||||
function startEdit(tag: Tag) {
|
||||
editingTag = tag;
|
||||
editName = tag.name;
|
||||
editColor = tag.color ?? '#6366f1';
|
||||
editColor = tag.color ?? '#e8754f';
|
||||
}
|
||||
|
||||
async function saveEdit() {
|
||||
@@ -118,7 +118,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{showCreateForm ? 'Cancel' : 'New Tag'}
|
||||
</button>
|
||||
@@ -141,7 +141,7 @@
|
||||
type="text"
|
||||
bind:value={newName}
|
||||
placeholder="Tag name"
|
||||
class="rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -159,7 +159,7 @@
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Create Tag
|
||||
</button>
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
let selectedGroupId = $state('');
|
||||
</script>
|
||||
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -126,4 +126,13 @@
|
||||
.status-ring-unknown {
|
||||
animation: ring-rotate-dash 8s linear infinite;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.status-ring-online,
|
||||
.status-ring-offline,
|
||||
.status-ring-degraded,
|
||||
.status-ring-unknown {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -58,6 +58,14 @@
|
||||
});
|
||||
}
|
||||
|
||||
// Cozy "room" pastel tint — stable per app, derived from its name
|
||||
const roomTints = ['var(--room-terra)', 'var(--room-sky)', 'var(--room-sage)', 'var(--room-butter)', 'var(--room-lav)', 'var(--room-peach)'];
|
||||
const tint = $derived.by(() => {
|
||||
let h = 0;
|
||||
for (const ch of app.name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
||||
return roomTints[h % roomTints.length];
|
||||
});
|
||||
|
||||
const iconDisplay = $derived.by(() => {
|
||||
if (!app.icon) return null;
|
||||
|
||||
@@ -82,32 +90,39 @@
|
||||
tabindex="0"
|
||||
onclick={() => window.open(app.url, '_blank', 'noopener,noreferrer')}
|
||||
onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') window.open(app.url, '_blank', 'noopener,noreferrer'); }}
|
||||
class="card-hover group flex cursor-pointer flex-col rounded-xl border border-border bg-card p-4 transition-colors hover:border-primary/50"
|
||||
class="card-hover group relative flex cursor-pointer flex-col overflow-hidden rounded-[1.4rem] border border-border bg-card p-5 shadow-[var(--shadow-soft)]"
|
||||
title={app.description ?? app.name}
|
||||
>
|
||||
<!-- soft blob accent -->
|
||||
<span
|
||||
class="pointer-events-none absolute -right-10 -top-12 h-28 w-28 rounded-full opacity-40 blur-2xl transition-opacity duration-300 group-hover:opacity-70"
|
||||
style="background: {tint};"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div class="mb-3 flex items-start justify-between">
|
||||
<div
|
||||
class="flex h-10 w-10 items-center justify-center rounded-lg bg-muted text-lg text-muted-foreground"
|
||||
class="flex h-12 w-12 items-center justify-center rounded-2xl text-lg"
|
||||
style="background: color-mix(in srgb, {tint} 18%, transparent); color: {tint};"
|
||||
>
|
||||
{#if iconDisplay?.kind === 'emoji'}
|
||||
<span class="text-xl">{iconDisplay.value}</span>
|
||||
<span class="text-2xl">{iconDisplay.value}</span>
|
||||
{:else if iconDisplay?.kind === 'image'}
|
||||
<img
|
||||
src={iconDisplay.src}
|
||||
alt="{app.name} icon"
|
||||
class="h-6 w-6 rounded object-contain"
|
||||
class="h-7 w-7 rounded-lg object-contain"
|
||||
/>
|
||||
{:else if iconDisplay?.kind === 'text'}
|
||||
<span class="text-xs font-medium">{iconDisplay.value}</span>
|
||||
<span class="text-sm font-bold">{iconDisplay.value}</span>
|
||||
{:else}
|
||||
<span class="text-xs font-bold">{app.name.charAt(0).toUpperCase()}</span>
|
||||
<span class="text-sm font-bold">{app.name.charAt(0).toUpperCase()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5">
|
||||
<a
|
||||
href="/apps/{app.id}/edit"
|
||||
onclick={(e: MouseEvent) => e.stopPropagation()}
|
||||
class="rounded-md p-1 text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
||||
class="rounded-xl p-1.5 text-muted-foreground opacity-0 transition-all hover:bg-accent hover:text-foreground group-hover:opacity-100"
|
||||
title={$t('app.edit')}
|
||||
>
|
||||
<svg
|
||||
@@ -128,12 +143,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="truncate text-sm font-semibold text-card-foreground transition-colors group-hover:text-primary">
|
||||
<h3 class="truncate font-display text-base font-semibold text-card-foreground transition-colors group-hover:text-primary">
|
||||
{app.name}
|
||||
</h3>
|
||||
|
||||
{#if app.description}
|
||||
<p class="mt-1 line-clamp-2 text-xs text-muted-foreground">{app.description}</p>
|
||||
<p class="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">{app.description}</p>
|
||||
{/if}
|
||||
|
||||
<!-- Sparkline -->
|
||||
@@ -143,14 +158,15 @@
|
||||
<div class="mt-2 flex items-center gap-1.5">
|
||||
<SparklineChart data={historyData} />
|
||||
{#if uptimePercent !== null}
|
||||
<span class="text-[10px] text-muted-foreground">{uptimePercent}% {$t('app.uptime')}</span>
|
||||
<span class="text-[11px] font-medium text-muted-foreground">{uptimePercent}% {$t('app.uptime')}</span>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if app.category}
|
||||
<span
|
||||
class="mt-2 inline-block self-start rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground"
|
||||
class="mt-3 inline-block self-start rounded-full px-2.5 py-0.5 text-[11px] font-semibold"
|
||||
style="background: color-mix(in srgb, {tint} 18%, transparent); color: color-mix(in srgb, {tint} 68%, var(--foreground));"
|
||||
>
|
||||
{app.category}
|
||||
</span>
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={$form.name}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={$t('app.name_placeholder')}
|
||||
/>
|
||||
{#if $errors.name}
|
||||
@@ -138,7 +138,7 @@
|
||||
name="url"
|
||||
type="url"
|
||||
bind:value={$form.url}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={$t('app.url_placeholder')}
|
||||
/>
|
||||
{#if $errors.url}
|
||||
@@ -170,7 +170,7 @@
|
||||
name="description"
|
||||
type="text"
|
||||
bind:value={$form.description}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={$t('app.description_placeholder')}
|
||||
/>
|
||||
</div>
|
||||
@@ -186,7 +186,7 @@
|
||||
bind:value={$form.category}
|
||||
suggestions={categorySuggestions}
|
||||
placeholder={$t('app.category_placeholder')}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -200,7 +200,7 @@
|
||||
bind:value={$form.tags}
|
||||
suggestions={tagSuggestions}
|
||||
placeholder={$t('app.tags_placeholder')}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -269,7 +269,7 @@
|
||||
name="healthcheckExpectedStatus"
|
||||
type="number"
|
||||
bind:value={$form.healthcheckExpectedStatus}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
min="100"
|
||||
max="599"
|
||||
/>
|
||||
@@ -287,7 +287,7 @@
|
||||
name="healthcheckTimeout"
|
||||
type="number"
|
||||
bind:value={$form.healthcheckTimeout}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
min="1000"
|
||||
max="30000"
|
||||
step="1000"
|
||||
@@ -307,7 +307,7 @@
|
||||
name="healthcheckInterval"
|
||||
type="number"
|
||||
bind:value={$form.healthcheckInterval}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
min="30"
|
||||
max="86400"
|
||||
/>
|
||||
@@ -349,7 +349,7 @@
|
||||
id="integrationType"
|
||||
name="integrationType"
|
||||
bind:value={$form.integrationType}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{#each availableIntegrations as integration (integration.id)}
|
||||
@@ -395,7 +395,7 @@
|
||||
{testingConnection ? 'Testing...' : 'Test Connection'}
|
||||
</button>
|
||||
{#if testResult}
|
||||
<span class="text-sm {testResult.success ? 'text-green-500' : 'text-destructive'}">
|
||||
<span class="text-sm {testResult.success ? 'text-status-online-ink' : 'text-destructive'}">
|
||||
{testResult.message}
|
||||
</span>
|
||||
{/if}
|
||||
@@ -412,7 +412,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
disabled={$submitting}
|
||||
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{#if $submitting}
|
||||
{$t('app.saving')}
|
||||
|
||||
@@ -10,18 +10,24 @@
|
||||
const config = $derived.by(() => {
|
||||
switch (status) {
|
||||
case 'online':
|
||||
return { color: 'bg-green-500', cssClass: 'status-online', textKey: 'status.online' };
|
||||
return { color: 'var(--status-online)', ink: 'var(--status-online-ink)', cssClass: 'status-online', textKey: 'status.online' };
|
||||
case 'offline':
|
||||
return { color: 'bg-red-500', cssClass: '', textKey: 'status.offline' };
|
||||
return { color: 'var(--status-offline)', ink: 'var(--status-offline-ink)', cssClass: '', textKey: 'status.offline' };
|
||||
case 'degraded':
|
||||
return { color: 'bg-yellow-500', cssClass: '', textKey: 'status.degraded' };
|
||||
return { color: 'var(--status-degraded)', ink: 'var(--status-degraded-ink)', cssClass: '', textKey: 'status.degraded' };
|
||||
default:
|
||||
return { color: 'bg-gray-500', cssClass: '', textKey: 'status.unknown' };
|
||||
return { color: 'var(--status-unknown)', ink: 'var(--status-unknown-ink)', cssClass: '', textKey: 'status.unknown' };
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<span class="inline-flex items-center gap-1.5 text-xs">
|
||||
<span class="inline-block h-2 w-2 rounded-full {config.color} {config.cssClass}"></span>
|
||||
<span class="text-muted-foreground">{$t(config.textKey)}</span>
|
||||
<span
|
||||
class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold"
|
||||
style="color: {config.ink}; background: color-mix(in srgb, {config.color} 14%, transparent);"
|
||||
>
|
||||
<span
|
||||
class="inline-block h-2 w-2 rounded-full {config.cssClass}"
|
||||
style="background: {config.color};"
|
||||
></span>
|
||||
<span>{$t(config.textKey)}</span>
|
||||
</span>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
: iconType === 'url'
|
||||
? $t('app.icon_url_placeholder')
|
||||
: $t('app.icon_emoji_placeholder')}
|
||||
class="flex-1 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -164,13 +164,13 @@
|
||||
type="text"
|
||||
bind:value={newLabel}
|
||||
placeholder="Link label"
|
||||
class="rounded-md border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={newUrl}
|
||||
placeholder="https://..."
|
||||
class="rounded-md border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
@@ -178,13 +178,13 @@
|
||||
type="text"
|
||||
bind:value={newIcon}
|
||||
placeholder="Icon (optional)"
|
||||
class="flex-1 rounded-md border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={addLink}
|
||||
disabled={!newLabel.trim() || !newUrl.trim()}
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
Add
|
||||
</button>
|
||||
@@ -196,7 +196,7 @@
|
||||
type="button"
|
||||
onclick={saveLinks}
|
||||
disabled={saving}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{saving ? 'Saving...' : 'Save Links'}
|
||||
</button>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
const statusColor = $derived(() => {
|
||||
if (!result) return '';
|
||||
if (result.error) return 'text-destructive';
|
||||
if (result.status >= 200 && result.status < 300) return 'text-green-500';
|
||||
if (result.status >= 300 && result.status < 400) return 'text-yellow-500';
|
||||
if (result.status >= 200 && result.status < 300) return 'text-status-online-ink';
|
||||
if (result.status >= 300 && result.status < 400) return 'text-status-degraded-ink';
|
||||
return 'text-destructive';
|
||||
});
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
let { fields, values, onchange, idPrefix = 'int' }: Props = $props();
|
||||
|
||||
const inputClass = 'w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30';
|
||||
const inputClass = 'w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30';
|
||||
</script>
|
||||
|
||||
<div class="space-y-3">
|
||||
|
||||
@@ -13,10 +13,10 @@
|
||||
let { data, width = 80, height = 20 }: Props = $props();
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
online: '#22c55e',
|
||||
offline: '#ef4444',
|
||||
degraded: '#eab308',
|
||||
unknown: '#6b7280'
|
||||
online: 'var(--status-online)',
|
||||
offline: 'var(--status-offline)',
|
||||
degraded: 'var(--status-degraded)',
|
||||
unknown: 'var(--status-unknown)'
|
||||
};
|
||||
|
||||
const barWidth = $derived(data.length > 0 ? Math.max(1, (width - 2) / data.length) : 1);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
import { theme } from '$lib/stores/theme.svelte.js';
|
||||
import CozyAmbient from './CozyAmbient.svelte';
|
||||
import MeshGradient from './MeshGradient.svelte';
|
||||
import ParticleField from './ParticleField.svelte';
|
||||
import AuroraEffect from './AuroraEffect.svelte';
|
||||
@@ -16,7 +17,9 @@
|
||||
|
||||
{#if theme.backgroundType !== 'none'}
|
||||
<div class="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true">
|
||||
{#if theme.backgroundType === 'mesh'}
|
||||
{#if theme.backgroundType === 'cozy'}
|
||||
<CozyAmbient />
|
||||
{:else if theme.backgroundType === 'mesh'}
|
||||
<MeshGradient />
|
||||
{:else if theme.backgroundType === 'particles'}
|
||||
<ParticleField />
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<!--
|
||||
Cozy Home ambient backdrop — static, soft warm-corner radial gradients.
|
||||
Calm "lit room" atmosphere (no animation), retints with the accent hue.
|
||||
-->
|
||||
<div class="cozy-ambient absolute inset-0"></div>
|
||||
|
||||
<style>
|
||||
.cozy-ambient {
|
||||
background:
|
||||
radial-gradient(50% 42% at 12% 0%, color-mix(in srgb, var(--room-peach) 26%, transparent), transparent 70%),
|
||||
radial-gradient(45% 40% at 95% 6%, color-mix(in srgb, var(--room-sky) 22%, transparent), transparent 70%),
|
||||
radial-gradient(52% 46% at 85% 100%, color-mix(in srgb, var(--room-sage) 20%, transparent), transparent 72%),
|
||||
radial-gradient(46% 42% at 8% 96%, color-mix(in srgb, var(--room-lav) 16%, transparent), transparent 72%);
|
||||
}
|
||||
|
||||
:global(.dark) .cozy-ambient {
|
||||
background:
|
||||
radial-gradient(52% 44% at 12% 0%, color-mix(in srgb, var(--room-terra) 20%, transparent), transparent 70%),
|
||||
radial-gradient(46% 42% at 95% 6%, color-mix(in srgb, var(--room-sky) 16%, transparent), transparent 70%),
|
||||
radial-gradient(54% 48% at 85% 100%, color-mix(in srgb, var(--room-sage) 14%, transparent), transparent 72%);
|
||||
opacity: 0.7;
|
||||
}
|
||||
</style>
|
||||
@@ -42,6 +42,13 @@
|
||||
|
||||
$effect(() => {
|
||||
blobs = initBlobs();
|
||||
|
||||
// Respect reduced-motion: render a static mesh, skip the rAF loop.
|
||||
const prefersReducedMotion =
|
||||
typeof window !== 'undefined' &&
|
||||
window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
if (prefersReducedMotion) return;
|
||||
|
||||
animFrame = requestAnimationFrame(animate);
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -34,21 +34,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex items-center gap-2 rounded-xl border border-border bg-card p-3 shadow-sm">
|
||||
<div class="flex items-center gap-2 rounded-xl border border-border bg-card p-3 shadow-[var(--shadow-soft)]">
|
||||
<input
|
||||
bind:this={inputEl}
|
||||
type="text"
|
||||
bind:value={title}
|
||||
onkeydown={handleKeydown}
|
||||
placeholder={$t('board.section_title') ?? 'Section title...'}
|
||||
class="flex-1 rounded-lg border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
<IconPickerButton value={icon} onchange={(v) => { icon = v; }} size="sm" />
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSubmit}
|
||||
disabled={!title.trim()}
|
||||
class="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{$t('common.add') ?? 'Add'}
|
||||
</button>
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
class="w-full rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground"
|
||||
/>
|
||||
{#if searchQuery.length > 0 && filteredTargetOptions.length > 0}
|
||||
<div class="absolute z-10 mt-1 max-h-40 w-full overflow-y-auto rounded border border-border bg-card shadow-lg">
|
||||
<div class="absolute z-10 mt-1 max-h-40 w-full overflow-y-auto rounded border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filteredTargetOptions as option (option.id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -190,7 +190,7 @@
|
||||
{#if loading}
|
||||
<p class="text-sm text-muted-foreground">{$t('board.access_loading')}</p>
|
||||
{:else if permissions.length > 0}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -20,32 +20,53 @@
|
||||
let { board }: Props = $props();
|
||||
|
||||
const sectionCount = $derived(board._count?.sections ?? 0);
|
||||
|
||||
// Stable per-board pastel "room" tint derived from the name
|
||||
const roomTints = ['var(--room-terra)', 'var(--room-sky)', 'var(--room-sage)', 'var(--room-butter)', 'var(--room-lav)', 'var(--room-peach)'];
|
||||
const tint = $derived.by(() => {
|
||||
let h = 0;
|
||||
for (const ch of board.name) h = (h * 31 + ch.charCodeAt(0)) >>> 0;
|
||||
return roomTints[h % roomTints.length];
|
||||
});
|
||||
</script>
|
||||
|
||||
<a
|
||||
href="/boards/{board.id}"
|
||||
class="card-hover group block rounded-xl border border-border bg-card p-5 transition-colors hover:border-primary/50"
|
||||
class="card-hover group relative block overflow-hidden rounded-[1.4rem] border border-border bg-card p-5 shadow-[var(--shadow-soft)]"
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<span
|
||||
class="pointer-events-none absolute -right-10 -top-12 h-28 w-28 rounded-full opacity-40 blur-2xl transition-opacity duration-300 group-hover:opacity-70"
|
||||
style="background: {tint};"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<div class="flex items-start gap-3.5">
|
||||
{#if board.icon}
|
||||
<DynamicIcon name={board.icon} size={22} />
|
||||
<span
|
||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl"
|
||||
style="background: color-mix(in srgb, {tint} 18%, transparent); color: {tint};"
|
||||
>
|
||||
<DynamicIcon name={board.icon} size={22} />
|
||||
</span>
|
||||
{:else}
|
||||
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted text-sm text-muted-foreground">
|
||||
B
|
||||
<span
|
||||
class="flex h-11 w-11 shrink-0 items-center justify-center rounded-2xl text-sm font-bold text-white"
|
||||
style="background: {tint};"
|
||||
>
|
||||
{board.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
{/if}
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2">
|
||||
<h3 class="truncate font-semibold text-foreground transition-colors group-hover:text-primary">
|
||||
<h3 class="truncate font-display text-base font-semibold text-foreground transition-colors group-hover:text-primary">
|
||||
{board.name}
|
||||
</h3>
|
||||
{#if board.isDefault}
|
||||
<span class="shrink-0 rounded bg-primary/15 px-1.5 py-0.5 text-xs text-primary">
|
||||
<span class="shrink-0 rounded-full bg-primary/15 px-2 py-0.5 text-xs font-semibold text-primary">
|
||||
{$t('board.default')}
|
||||
</span>
|
||||
{/if}
|
||||
{#if board.isGuestAccessible}
|
||||
<span class="shrink-0 flex items-center gap-1 rounded bg-accent px-1.5 py-0.5 text-xs text-accent-foreground" title={$t('board.guest_accessible')}>
|
||||
<span class="shrink-0 flex items-center gap-1 rounded-full bg-accent px-2 py-0.5 text-xs font-medium text-accent-foreground" title={$t('board.guest_accessible')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
@@ -54,7 +75,7 @@
|
||||
{$t('board.guest')}
|
||||
</span>
|
||||
{:else}
|
||||
<span class="shrink-0 flex items-center gap-1 rounded bg-muted px-1.5 py-0.5 text-xs text-muted-foreground" title={$t('board.access_private')}>
|
||||
<span class="shrink-0 flex items-center gap-1 rounded-full bg-muted px-2 py-0.5 text-xs font-medium text-muted-foreground" title={$t('board.access_private')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
@@ -62,7 +83,7 @@
|
||||
</span>
|
||||
{/if}
|
||||
{#if board.hasSharedPermissions}
|
||||
<span class="shrink-0 flex items-center gap-1 rounded bg-blue-500/15 px-1.5 py-0.5 text-xs text-blue-500" title={$t('board.access_shared')}>
|
||||
<span class="shrink-0 flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium" style="background: color-mix(in srgb, var(--room-sky) 18%, transparent); color: color-mix(in srgb, var(--room-sky) 70%, var(--foreground));" title={$t('board.access_shared')}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||
<circle cx="9" cy="7" r="4" />
|
||||
|
||||
@@ -29,13 +29,18 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="mb-6 flex items-start justify-between">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="mb-6 flex items-start justify-between gap-4">
|
||||
<div class="flex items-center gap-3.5">
|
||||
{#if icon}
|
||||
<DynamicIcon name={icon} size={28} />
|
||||
<span
|
||||
class="flex h-12 w-12 shrink-0 items-center justify-center rounded-2xl text-primary shadow-[var(--shadow-soft)]"
|
||||
style="background: color-mix(in srgb, var(--primary) 14%, transparent);"
|
||||
>
|
||||
<DynamicIcon name={icon} size={26} />
|
||||
</span>
|
||||
{/if}
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-foreground">{name}</h1>
|
||||
<h1 class="font-display text-3xl font-semibold text-foreground">{name}</h1>
|
||||
{#if description}
|
||||
<p class="mt-1 text-sm text-muted-foreground">{description}</p>
|
||||
{/if}
|
||||
@@ -45,7 +50,7 @@
|
||||
<div class="flex items-center gap-2">
|
||||
<a
|
||||
href="/boards"
|
||||
class="rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="rounded-xl border border-border bg-card px-3.5 py-2 text-sm font-medium text-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:border-primary/40"
|
||||
>
|
||||
{$t('board.all_boards')}
|
||||
</a>
|
||||
@@ -53,7 +58,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={onShare}
|
||||
class="flex items-center gap-1.5 rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="flex items-center gap-1.5 rounded-xl border border-border bg-card px-3.5 py-2 text-sm font-medium text-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:border-primary/40"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="5" r="3" />
|
||||
@@ -69,9 +74,9 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleEditToggle}
|
||||
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors {editMode.active
|
||||
? 'bg-primary text-primary-foreground ring-2 ring-primary/30'
|
||||
: 'bg-primary text-primary-foreground hover:bg-primary/90'}"
|
||||
class="flex items-center gap-1.5 rounded-xl px-3.5 py-2 text-sm font-semibold text-primary-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 {editMode.active
|
||||
? 'bg-primary ring-2 ring-primary/30'
|
||||
: 'bg-primary hover:shadow-[var(--shadow-lift)]'}"
|
||||
>
|
||||
{#if editMode.active}
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
|
||||
<!-- Side panel -->
|
||||
<div
|
||||
class="fixed right-0 top-0 z-50 flex h-full w-full max-w-md flex-col border-l border-border bg-card shadow-2xl"
|
||||
class="fixed right-0 top-0 z-50 flex h-full w-full max-w-md flex-col border-l border-border bg-card shadow-[var(--shadow-lift)]"
|
||||
transition:fly={{ x: 400, duration: 250 }}
|
||||
>
|
||||
<!-- Header -->
|
||||
@@ -107,7 +107,7 @@
|
||||
<div>
|
||||
<label for="bp-name" class="mb-1 block text-sm font-medium text-foreground">{$t('common.name') ?? 'Name'}</label>
|
||||
<input id="bp-name" type="text" bind:value={name}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30" />
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30" />
|
||||
</div>
|
||||
|
||||
<!-- Icon -->
|
||||
@@ -121,7 +121,7 @@
|
||||
<div>
|
||||
<label for="bp-desc" class="mb-1 block text-sm font-medium text-foreground">{$t('common.description') ?? 'Description'}</label>
|
||||
<textarea id="bp-desc" rows="2" bind:value={description}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"></textarea>
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- Theme Hue -->
|
||||
@@ -144,7 +144,7 @@
|
||||
<div>
|
||||
<label for="bp-bg" class="mb-1 block text-sm font-medium text-foreground">{$t('board.background') ?? 'Background'}</label>
|
||||
<select id="bp-bg" bind:value={backgroundType}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30">
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30">
|
||||
<option value="none">None</option>
|
||||
<option value="mesh">Mesh Gradient</option>
|
||||
<option value="particles">Particles</option>
|
||||
@@ -159,7 +159,7 @@
|
||||
<div>
|
||||
<label for="bp-wp-url" class="mb-1 block text-sm font-medium text-foreground">Wallpaper URL</label>
|
||||
<input id="bp-wp-url" type="text" bind:value={wallpaperUrl} placeholder="https://..."
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30" />
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="bp-wp-blur" class="mb-1 block text-sm font-medium text-foreground">Blur ({wallpaperBlur}px)</label>
|
||||
@@ -176,7 +176,7 @@
|
||||
<div>
|
||||
<label for="bp-cardsize" class="mb-1 block text-sm font-medium text-foreground">{$t('board.card_size') ?? 'Card Size'}</label>
|
||||
<select id="bp-cardsize" bind:value={cardSize}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30">
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30">
|
||||
<option value="compact">Compact</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="large">Large</option>
|
||||
@@ -187,7 +187,7 @@
|
||||
<div>
|
||||
<label for="bp-css" class="mb-1 block text-sm font-medium text-foreground">{$t('board.custom_css') ?? 'Custom CSS'}</label>
|
||||
<textarea id="bp-css" rows="4" bind:value={customCss} placeholder={'.board { ... }'}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 font-mono text-xs text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"></textarea>
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 font-mono text-xs text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSave}
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('common.apply') ?? 'Apply'}
|
||||
</button>
|
||||
|
||||
@@ -153,7 +153,7 @@
|
||||
onclick={handleBackdropClick}
|
||||
role="presentation"
|
||||
>
|
||||
<div class="mx-4 w-full max-w-lg rounded-xl border border-border bg-card p-6 shadow-xl" role="dialog" aria-modal="true">
|
||||
<div class="mx-4 w-full max-w-lg rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]" role="dialog" aria-modal="true">
|
||||
<!-- Header -->
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-card-foreground">
|
||||
@@ -177,7 +177,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleCopyLink}
|
||||
class="flex items-center gap-2 rounded-lg border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="flex items-center gap-2 rounded-xl border border-border px-3 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
@@ -210,7 +210,7 @@
|
||||
<select
|
||||
bind:value={selectedTargetType}
|
||||
onchange={() => { selectedTargetId = ''; searchQuery = ''; }}
|
||||
class="rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
<option value={TargetType.USER}>{$t('admin.perm_user')}</option>
|
||||
<option value={TargetType.GROUP}>{$t('admin.perm_group')}</option>
|
||||
@@ -220,10 +220,10 @@
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder={$t('board.access_search_placeholder')}
|
||||
class="w-full rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground placeholder:text-muted-foreground"
|
||||
/>
|
||||
{#if searchQuery.length > 0 && filteredTargetOptions.length > 0 && !selectedTargetId}
|
||||
<div class="absolute z-10 mt-1 max-h-32 w-full overflow-y-auto rounded border border-border bg-card shadow-lg">
|
||||
<div class="absolute z-10 mt-1 max-h-32 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filteredTargetOptions as option (option.id)}
|
||||
<button
|
||||
type="button"
|
||||
@@ -238,7 +238,7 @@
|
||||
</div>
|
||||
<select
|
||||
bind:value={selectedLevel}
|
||||
class="rounded border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
<option value={PermissionLevel.VIEW}>{$t('admin.perm_view')}</option>
|
||||
<option value={PermissionLevel.EDIT}>{$t('admin.perm_edit')}</option>
|
||||
@@ -248,7 +248,7 @@
|
||||
type="button"
|
||||
onclick={handleGrant}
|
||||
disabled={!selectedTargetId}
|
||||
class="shrink-0 rounded bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="shrink-0 rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{$t('common.add')}
|
||||
</button>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
transition:fly={{ y: 60, duration: 250 }}
|
||||
>
|
||||
<!-- Toolbar pill -->
|
||||
<div class="flex items-center gap-1 rounded-2xl border border-border bg-card/95 px-2 py-1.5 shadow-xl backdrop-blur-sm">
|
||||
<div class="flex items-center gap-1 rounded-2xl border border-border bg-card/95 px-2 py-1.5 shadow-[var(--shadow-lift)] backdrop-blur-sm">
|
||||
<!-- Save -->
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
onclick={() => selectTemplate(template.id)}
|
||||
class="flex flex-col items-center gap-2 rounded-lg border-2 p-4 text-center transition-colors {selected === template.id ? 'border-primary bg-primary/5' : 'border-border hover:border-primary/50'}"
|
||||
>
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div class="flex h-10 w-10 items-center justify-center rounded-xl bg-primary/10">
|
||||
{#if template.icon}
|
||||
<DynamicIcon name={template.icon} size={20} />
|
||||
{:else}
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
</script>
|
||||
|
||||
{#if favorites.hasFavorites}
|
||||
<div class="mb-4 rounded-lg border border-border bg-card/50 px-3 py-2 backdrop-blur-sm">
|
||||
<div class="mb-4 rounded-2xl border border-border bg-card/60 px-3 py-2.5 shadow-[var(--shadow-soft)] backdrop-blur-sm">
|
||||
<div
|
||||
class="flex flex-wrap items-center gap-2"
|
||||
use:dndzone={{
|
||||
@@ -75,7 +75,7 @@
|
||||
href={item.app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="group relative flex items-center gap-1.5 rounded-md bg-muted/50 px-2.5 py-1.5 text-xs font-medium text-foreground transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||
class="group relative flex items-center gap-1.5 rounded-xl bg-muted/60 px-3 py-1.5 text-xs font-semibold text-foreground transition-all hover:-translate-y-0.5 hover:bg-accent hover:text-accent-foreground hover:shadow-[var(--shadow-soft)]"
|
||||
title={item.app.name}
|
||||
oncontextmenu={(e) => handleRemove(e, item.appId)}
|
||||
>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
}
|
||||
|
||||
const bgOptions: { value: BackgroundType; labelKey: string }[] = [
|
||||
{ value: 'cozy', labelKey: 'bg.cozy' },
|
||||
{ value: 'mesh', labelKey: 'bg.mesh' },
|
||||
{ value: 'particles', labelKey: 'bg.particles' },
|
||||
{ value: 'aurora', labelKey: 'bg.aurora' },
|
||||
@@ -29,14 +30,14 @@
|
||||
</script>
|
||||
|
||||
<header
|
||||
class="sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-border bg-background/80 px-4 backdrop-blur-sm"
|
||||
class="sticky top-0 z-20 flex h-16 items-center gap-3 bg-background/70 px-5 backdrop-blur-md"
|
||||
>
|
||||
<!-- Mobile hamburger -->
|
||||
{#if ui.isMobile}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ui.toggleSidebar()}
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
aria-label={$t('sidebar.toggle')}
|
||||
>
|
||||
<svg
|
||||
@@ -64,7 +65,7 @@
|
||||
<!-- Background selector -->
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
title={$t('bg.title')}
|
||||
aria-label={$t('bg.aria_label')}
|
||||
>
|
||||
@@ -84,13 +85,13 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
class="z-50 w-44 rounded-md border border-border bg-popover p-1 shadow-lg"
|
||||
class="z-50 w-44 rounded-2xl border border-border bg-popover p-1.5 shadow-[var(--shadow-lift)]"
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
>
|
||||
{#each bgOptions as opt (opt.value)}
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-2 py-1.5 text-sm transition-colors {theme.backgroundType === opt.value
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-2.5 py-2 text-sm transition-colors {theme.backgroundType === opt.value
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-popover-foreground hover:bg-accent/50'}"
|
||||
onSelect={() => theme.setBackground(opt.value)}
|
||||
@@ -131,10 +132,11 @@
|
||||
{#if user}
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="flex items-center gap-2.5 rounded-2xl px-2 py-1.5 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
<span
|
||||
class="flex h-7 w-7 items-center justify-center rounded-full bg-primary text-xs font-medium text-primary-foreground"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-xl text-xs font-bold text-white shadow-[var(--shadow-soft)]"
|
||||
style="background: linear-gradient(135deg, var(--room-lav), var(--room-sky));"
|
||||
>
|
||||
{user.displayName.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -144,7 +146,7 @@
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Portal>
|
||||
<DropdownMenu.Content
|
||||
class="z-50 w-48 rounded-md border border-border bg-popover p-1 shadow-lg"
|
||||
class="z-50 w-48 rounded-2xl border border-border bg-popover p-1.5 shadow-[var(--shadow-lift)]"
|
||||
sideOffset={4}
|
||||
align="end"
|
||||
>
|
||||
@@ -154,7 +156,7 @@
|
||||
</div>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onSelect={() => goto('/settings')}
|
||||
>
|
||||
<svg
|
||||
@@ -174,7 +176,7 @@
|
||||
</DropdownMenu.Item>
|
||||
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onSelect={() => goto('/settings/api-tokens')}
|
||||
>
|
||||
<svg
|
||||
@@ -197,7 +199,7 @@
|
||||
|
||||
<form method="POST" action="/auth/logout" id="logout-form" class="contents">
|
||||
<DropdownMenu.Item
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full cursor-pointer items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onSelect={submitLogout}
|
||||
>
|
||||
<svg
|
||||
@@ -223,7 +225,7 @@
|
||||
{:else}
|
||||
<a
|
||||
href="/login"
|
||||
class="rounded-md bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-semibold text-primary-foreground shadow-[var(--shadow-soft)] transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('auth.login')}
|
||||
</a>
|
||||
|
||||
@@ -67,10 +67,10 @@
|
||||
{#if visible}
|
||||
<div
|
||||
transition:fade={{ duration: 200 }}
|
||||
class="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-lg items-center gap-3 rounded-xl border border-border bg-card p-4 shadow-lg"
|
||||
class="fixed bottom-4 left-4 right-4 z-50 mx-auto flex max-w-lg items-center gap-3 rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]"
|
||||
role="alert"
|
||||
>
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10">
|
||||
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
<Download class="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={install}
|
||||
class="shrink-0 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="shrink-0 rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('install.button')}
|
||||
</button>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleLocale}
|
||||
class="inline-flex items-center justify-center rounded-md px-2 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="inline-flex h-10 w-10 items-center justify-center rounded-xl text-xs font-bold text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
title={$locale === 'ru' ? 'Switch to English' : 'Переключить на русский'}
|
||||
>
|
||||
{$locale === 'ru' ? 'RU' : 'EN'}
|
||||
|
||||
@@ -24,19 +24,32 @@
|
||||
function isActive(path: string): boolean {
|
||||
return $page.url.pathname.startsWith(path);
|
||||
}
|
||||
|
||||
// Cozy "room" accent palette — board chips rotate through these
|
||||
const roomColors = [
|
||||
'var(--room-terra)',
|
||||
'var(--room-sky)',
|
||||
'var(--room-sage)',
|
||||
'var(--room-butter)',
|
||||
'var(--room-lav)',
|
||||
'var(--room-peach)'
|
||||
];
|
||||
</script>
|
||||
|
||||
<aside
|
||||
class="flex h-full flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200"
|
||||
class="flex h-full flex-col bg-sidebar p-3 transition-all duration-200"
|
||||
class:w-64={!collapsed}
|
||||
class:w-16={collapsed}
|
||||
class:w-[4.75rem]={collapsed}
|
||||
>
|
||||
<!-- Brand -->
|
||||
<div class="flex h-14 items-center border-b border-sidebar-border px-4">
|
||||
{#if !collapsed}
|
||||
<a href="/" class="flex items-center gap-2 text-sidebar-foreground">
|
||||
<div class="mb-5 flex h-12 items-center px-1.5 {collapsed ? 'justify-center' : 'gap-3'}">
|
||||
<a href="/" class="flex items-center gap-3 text-sidebar-foreground" title={$t('app_name')}>
|
||||
<span
|
||||
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl text-primary-foreground shadow-[var(--shadow-soft)]"
|
||||
style="background: linear-gradient(135deg, var(--room-peach), var(--room-terra));"
|
||||
>
|
||||
<svg
|
||||
class="h-6 w-6 text-sidebar-primary"
|
||||
class="h-5 w-5"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -45,62 +58,49 @@
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
<rect x="3" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="2" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="2" />
|
||||
</svg>
|
||||
<span class="text-sm font-semibold">{$t('app_name')}</span>
|
||||
</a>
|
||||
{:else}
|
||||
<a href="/" class="mx-auto text-sidebar-primary" title={$t('app_name')}>
|
||||
<svg
|
||||
class="h-6 w-6"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" />
|
||||
<rect x="14" y="3" width="7" height="7" />
|
||||
<rect x="14" y="14" width="7" height="7" />
|
||||
<rect x="3" y="14" width="7" height="7" />
|
||||
</svg>
|
||||
</a>
|
||||
{/if}
|
||||
</span>
|
||||
{#if !collapsed}
|
||||
<span class="leading-tight">
|
||||
<span class="block font-display text-base font-semibold">{$t('app_name')}</span>
|
||||
<span class="block text-[11px] font-medium text-sidebar-foreground/50">home cloud</span>
|
||||
</span>
|
||||
{/if}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 overflow-y-auto px-2 py-3">
|
||||
<nav class="flex flex-1 flex-col overflow-y-auto">
|
||||
<!-- Main Links -->
|
||||
<div class="mb-3">
|
||||
<div class="mb-2">
|
||||
{#if !collapsed}
|
||||
<p class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50">
|
||||
<p class="mb-1.5 px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45">
|
||||
{$t('nav.navigation')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/boards"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/boards')
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/boards')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? $t('nav.boards') : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2" />
|
||||
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||
<line x1="3" y1="9" x2="21" y2="9" />
|
||||
<line x1="9" y1="21" x2="9" y2="9" />
|
||||
</svg>
|
||||
@@ -109,44 +109,42 @@
|
||||
|
||||
<a
|
||||
href="/apps"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/apps')
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/apps')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? $t('nav.apps') : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="2" y1="12" x2="22" y2="12" />
|
||||
<path
|
||||
d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"
|
||||
/>
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<line x1="3" y1="12" x2="21" y2="12" />
|
||||
<path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z" />
|
||||
</svg>
|
||||
{#if !collapsed}<span>{$t('nav.apps')}</span>{/if}
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/status"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/status')
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/status')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? 'Status Page' : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
@@ -156,18 +154,18 @@
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Board List -->
|
||||
<!-- Board List ("Rooms") -->
|
||||
{#if boards.length > 0}
|
||||
<div class="mb-3">
|
||||
<div class="mb-2 mt-1">
|
||||
{#if !collapsed}
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (boardsExpanded = !boardsExpanded)}
|
||||
class="mb-1 flex w-full items-center justify-between px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50 transition-colors hover:text-sidebar-foreground/80"
|
||||
class="mb-1.5 flex w-full items-center justify-between px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45 transition-colors hover:text-sidebar-foreground/70"
|
||||
>
|
||||
<span>{$t('nav.boards')}</span>
|
||||
<svg
|
||||
class="h-3 w-3 transition-transform duration-200"
|
||||
class="h-3.5 w-3.5 transition-transform duration-200"
|
||||
class:rotate-180={boardsExpanded}
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
@@ -182,13 +180,13 @@
|
||||
{/if}
|
||||
|
||||
{#if boardsExpanded || collapsed}
|
||||
<div class="max-h-48 overflow-y-auto">
|
||||
{#each boards as board (board.id)}
|
||||
<div class="max-h-56 overflow-y-auto">
|
||||
{#each boards as board, i (board.id)}
|
||||
<a
|
||||
href="/boards/{board.id}"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors {isActive(`/boards/${board.id}`)
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all {isActive(`/boards/${board.id}`)
|
||||
? 'bg-card text-sidebar-foreground shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? board.name : undefined}
|
||||
onclick={() => ui.closeMobileSidebar()}
|
||||
>
|
||||
@@ -196,7 +194,8 @@
|
||||
<span class="shrink-0"><DynamicIcon name={board.icon} size={18} /></span>
|
||||
{:else}
|
||||
<span
|
||||
class="flex h-5 w-5 shrink-0 items-center justify-center rounded bg-sidebar-accent text-[10px] font-medium text-sidebar-foreground"
|
||||
class="flex h-6 w-6 shrink-0 items-center justify-center rounded-lg text-[11px] font-bold text-white"
|
||||
style="background: {roomColors[i % roomColors.length]};"
|
||||
>
|
||||
{board.name.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
@@ -213,29 +212,27 @@
|
||||
|
||||
<!-- Admin -->
|
||||
{#if isAdmin}
|
||||
<div class="mt-auto border-t border-sidebar-border pt-3">
|
||||
<div class="mt-auto pt-2">
|
||||
{#if !collapsed}
|
||||
<p
|
||||
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
|
||||
>
|
||||
<p class="mb-1.5 px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45">
|
||||
{$t('nav.admin')}
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<a
|
||||
href="/admin/users"
|
||||
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/admin')
|
||||
? 'bg-sidebar-accent text-sidebar-accent-foreground'
|
||||
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}"
|
||||
class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/admin')
|
||||
? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
|
||||
: 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
|
||||
title={collapsed ? $t('nav.admin_panel') : undefined}
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
class="h-5 w-5 shrink-0"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-width="1.9"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
@@ -252,11 +249,11 @@
|
||||
|
||||
<!-- Keyboard Shortcuts Hint + Collapse Toggle -->
|
||||
{#if !ui.isMobile}
|
||||
<div class="border-t border-sidebar-border p-2 flex items-center {collapsed ? 'flex-col gap-1' : 'gap-1'}">
|
||||
<div class="mt-2 flex items-center {collapsed ? 'flex-col gap-1.5' : 'gap-1.5'}">
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => keyboard.toggleOverlay()}
|
||||
class="flex items-center justify-center rounded-md p-2 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
class="flex items-center justify-center rounded-xl p-2.5 text-sidebar-foreground/55 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
title="Keyboard Shortcuts (?)"
|
||||
>
|
||||
<svg
|
||||
@@ -277,7 +274,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => ui.toggleSidebar()}
|
||||
class="flex w-full items-center justify-center rounded-md p-2 text-sidebar-foreground transition-colors hover:bg-sidebar-accent"
|
||||
class="flex w-full items-center justify-center rounded-xl p-2.5 text-sidebar-foreground/70 transition-colors hover:bg-sidebar-accent hover:text-sidebar-foreground"
|
||||
title={collapsed ? $t('sidebar.expand') : $t('sidebar.collapse')}
|
||||
>
|
||||
<svg
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => theme.cycleMode()}
|
||||
class="inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
title={$t('theme.title', { values: { mode: $t(currentIcon.labelKey) } })}
|
||||
aria-label={$t('theme.toggle', { values: { mode: $t(currentIcon.labelKey) } })}
|
||||
>
|
||||
|
||||
@@ -47,11 +47,11 @@
|
||||
function eventColor(event: string): string {
|
||||
switch (event) {
|
||||
case 'app_online':
|
||||
return 'text-green-500';
|
||||
return 'text-status-online-ink';
|
||||
case 'app_offline':
|
||||
return 'text-red-500';
|
||||
return 'text-status-offline-ink';
|
||||
case 'app_degraded':
|
||||
return 'text-yellow-500';
|
||||
return 'text-status-degraded-ink';
|
||||
default:
|
||||
return 'text-muted-foreground';
|
||||
}
|
||||
@@ -64,7 +64,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showDropdown = !showDropdown)}
|
||||
class="relative inline-flex items-center justify-center rounded-md p-2 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
class="relative inline-flex items-center justify-center rounded-xl p-2.5 text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
title="Notifications"
|
||||
aria-label="Notifications"
|
||||
>
|
||||
@@ -95,7 +95,7 @@
|
||||
|
||||
{#if showDropdown}
|
||||
<div
|
||||
class="absolute right-0 top-full z-50 mt-1 w-80 rounded-lg border border-border bg-popover shadow-lg"
|
||||
class="absolute right-0 top-full z-50 mt-1 w-80 rounded-xl border border-border bg-popover shadow-[var(--shadow-soft)]"
|
||||
>
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between border-b border-border px-4 py-3">
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
<select
|
||||
id="channel-type"
|
||||
bind:value={channelType}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="discord">Discord</option>
|
||||
<option value="slack">Slack</option>
|
||||
@@ -146,7 +146,7 @@
|
||||
type="url"
|
||||
bind:value={discordWebhookUrl}
|
||||
placeholder="https://discord.com/api/webhooks/..."
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -160,7 +160,7 @@
|
||||
type="url"
|
||||
bind:value={slackWebhookUrl}
|
||||
placeholder="https://hooks.slack.com/services/..."
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -174,7 +174,7 @@
|
||||
type="text"
|
||||
bind:value={telegramBotToken}
|
||||
placeholder="123456:ABC-DEF..."
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -187,7 +187,7 @@
|
||||
type="text"
|
||||
bind:value={telegramChatId}
|
||||
placeholder="-1001234567890"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -201,7 +201,7 @@
|
||||
type="url"
|
||||
bind:value={httpUrl}
|
||||
placeholder="https://example.com/webhook"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@
|
||||
<select
|
||||
id="http-method"
|
||||
bind:value={httpMethod}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="PUT">PUT</option>
|
||||
@@ -230,7 +230,7 @@
|
||||
bind:value={httpSecret}
|
||||
placeholder="Shared secret for HMAC-SHA256 signature"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 pr-10 font-mono text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 pr-10 font-mono text-sm text-foreground"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -262,7 +262,7 @@
|
||||
bind:value={httpSignatureHeader}
|
||||
placeholder="X-Signature-256"
|
||||
autocomplete="off"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">
|
||||
Defaults to <code class="rounded bg-muted/40 px-1">X-Signature-256</code>. Override for receivers expecting a different name (e.g. <code class="rounded bg-muted/40 px-1">X-Hub-Signature-256</code>).
|
||||
@@ -283,7 +283,7 @@
|
||||
|
||||
<!-- Test Result -->
|
||||
{#if testResult}
|
||||
<p class="text-sm {testResult.startsWith('Failed') ? 'text-destructive' : 'text-emerald-500'}">
|
||||
<p class="text-sm {testResult.startsWith('Failed') ? 'text-destructive' : 'text-status-online-ink'}">
|
||||
{testResult}
|
||||
</p>
|
||||
{/if}
|
||||
@@ -292,7 +292,7 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{channel ? 'Update' : 'Create'} Channel
|
||||
</button>
|
||||
@@ -301,7 +301,7 @@
|
||||
type="button"
|
||||
onclick={sendTest}
|
||||
disabled={testing}
|
||||
class="rounded-md border border-input px-4 py-2 text-sm font-medium text-foreground hover:bg-accent disabled:opacity-50"
|
||||
class="rounded-xl border border-input px-4 py-2 text-sm font-medium text-foreground hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
{testing ? 'Sending...' : 'Send Test'}
|
||||
</button>
|
||||
|
||||
@@ -73,9 +73,9 @@
|
||||
|
||||
function eventBadgeClass(event: string): string {
|
||||
switch (event) {
|
||||
case 'app_online': return 'bg-green-500/10 text-green-500';
|
||||
case 'app_offline': return 'bg-red-500/10 text-red-500';
|
||||
case 'app_degraded': return 'bg-yellow-500/10 text-yellow-500';
|
||||
case 'app_online': return 'bg-status-online/15 text-status-online-ink';
|
||||
case 'app_offline': return 'bg-status-offline/15 text-status-offline-ink';
|
||||
case 'app_degraded': return 'bg-status-degraded/15 text-status-degraded-ink';
|
||||
default: return 'bg-muted text-muted-foreground';
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,7 @@
|
||||
<select
|
||||
bind:value={filterEvent}
|
||||
onchange={applyFilters}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground"
|
||||
>
|
||||
<option value="">All Events</option>
|
||||
<option value="app_online">Online</option>
|
||||
@@ -104,7 +104,7 @@
|
||||
<p class="text-muted-foreground">No notifications found</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
|
||||
// Theme form
|
||||
let defaultTheme = $state<'dark' | 'light'>('dark');
|
||||
let defaultPrimaryColor = $state('#6366f1');
|
||||
let defaultPrimaryColor = $state('#e8754f');
|
||||
|
||||
// Board form
|
||||
let boardName = $state('My Dashboard');
|
||||
@@ -169,6 +169,7 @@
|
||||
}
|
||||
|
||||
const primaryColorOptions = [
|
||||
{ label: 'Terracotta', value: '#e8754f' },
|
||||
{ label: 'Indigo', value: '#6366f1' },
|
||||
{ label: 'Blue', value: '#3b82f6' },
|
||||
{ label: 'Emerald', value: '#10b981' },
|
||||
@@ -182,7 +183,7 @@
|
||||
|
||||
<div class="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
|
||||
<div
|
||||
class="mx-4 w-full max-w-lg rounded-2xl border border-border bg-card shadow-2xl"
|
||||
class="mx-4 w-full max-w-lg rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-lift)]"
|
||||
>
|
||||
<!-- Progress bar -->
|
||||
<div class="border-b border-border px-6 py-4">
|
||||
@@ -227,7 +228,7 @@
|
||||
{:else if currentStep === 'admin'}
|
||||
<h2 class="mb-4 text-xl font-bold text-foreground">Create Admin Account</h2>
|
||||
{#if adminCreated}
|
||||
<div class="rounded-lg bg-green-500/10 p-4 text-sm text-green-600 dark:text-green-400">
|
||||
<div class="rounded-lg bg-status-online/10 p-4 text-sm text-status-online-ink dark:text-status-online-ink">
|
||||
Admin account created successfully. You can proceed to the next step.
|
||||
</div>
|
||||
{:else}
|
||||
@@ -238,7 +239,7 @@
|
||||
id="ob-display-name"
|
||||
type="text"
|
||||
bind:value={adminDisplayName}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="Admin"
|
||||
/>
|
||||
</div>
|
||||
@@ -248,7 +249,7 @@
|
||||
id="ob-email"
|
||||
type="email"
|
||||
bind:value={adminEmail}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="admin@example.com"
|
||||
/>
|
||||
</div>
|
||||
@@ -258,7 +259,7 @@
|
||||
id="ob-password"
|
||||
type="password"
|
||||
bind:value={adminPassword}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="Min. 6 characters"
|
||||
/>
|
||||
</div>
|
||||
@@ -298,19 +299,19 @@
|
||||
<input
|
||||
type="text"
|
||||
bind:value={oauthClientId}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
placeholder="Client ID"
|
||||
/>
|
||||
<input
|
||||
type="password"
|
||||
bind:value={oauthClientSecret}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
placeholder="Client Secret"
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
bind:value={oauthDiscoveryUrl}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-1.5 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none"
|
||||
placeholder="Discovery URL (https://.../.well-known/openid-configuration)"
|
||||
/>
|
||||
</div>
|
||||
@@ -369,7 +370,7 @@
|
||||
id="ob-board-name"
|
||||
type="text"
|
||||
bind:value={boardName}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder="My Dashboard"
|
||||
/>
|
||||
</div>
|
||||
@@ -417,7 +418,7 @@
|
||||
type="button"
|
||||
onclick={handleNext}
|
||||
disabled={loading}
|
||||
class="rounded-lg bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{#if loading}
|
||||
Processing...
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="flex w-full max-w-lg flex-col rounded-lg border border-border bg-popover shadow-2xl"
|
||||
class="flex w-full max-w-lg flex-col rounded-[1.4rem] border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
style="max-height: 60vh; animation: searchSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
|
||||
role="dialog"
|
||||
aria-label={$t('search.placeholder')}
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => search.toggle()}
|
||||
class="flex w-full max-w-sm items-center gap-2 rounded-md border border-input bg-background/50 px-3 py-1.5 text-sm text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
|
||||
class="flex w-full max-w-sm items-center gap-2.5 rounded-2xl border border-border bg-card px-4 py-2.5 text-sm text-muted-foreground shadow-[var(--shadow-soft)] transition-all hover:border-primary/40 hover:text-foreground"
|
||||
>
|
||||
<svg
|
||||
class="h-4 w-4 shrink-0"
|
||||
@@ -27,7 +27,7 @@
|
||||
</svg>
|
||||
<span class="flex-1 text-left">{$t('search.trigger')}</span>
|
||||
<kbd
|
||||
class="hidden rounded border border-border bg-muted px-1.5 py-0.5 text-[10px] font-medium text-muted-foreground sm:inline"
|
||||
class="hidden rounded-lg bg-muted px-2 py-0.5 text-[10px] font-bold text-muted-foreground sm:inline"
|
||||
>
|
||||
{isMac ? '\u2318' : 'Ctrl'}K
|
||||
</kbd>
|
||||
|
||||
@@ -105,7 +105,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card p-4 shadow-sm">
|
||||
<div class="rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]">
|
||||
<div class="mb-3 flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<!-- Section drag handle -->
|
||||
@@ -142,7 +142,7 @@
|
||||
<!-- Card size selector -->
|
||||
<select
|
||||
onchange={handleCardSizeChange}
|
||||
class="rounded-md border border-input bg-background px-2 py-1 text-xs text-foreground transition-colors focus:border-primary focus:outline-none"
|
||||
class="rounded-xl border border-input bg-background px-2 py-1 text-xs text-foreground transition-colors focus:border-primary focus:outline-none"
|
||||
title={$t('board.card_size') ?? 'Card size'}
|
||||
>
|
||||
<option value="" selected={!section.cardSize}>{$t('section.inherit_size') ?? 'Inherit'}</option>
|
||||
@@ -153,7 +153,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onToggleAddWidget(section.id)}
|
||||
class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('widget.add')}
|
||||
</button>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
let expanded = $state(section.isExpandedByDefault);
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card/30 shadow-sm {editMode.active ? 'ring-1 ring-primary/10' : ''}">
|
||||
<div class="rounded-[1.4rem] border border-border bg-card/40 shadow-[var(--shadow-soft)] backdrop-blur-sm {editMode.active ? 'ring-1 ring-primary/15' : ''}">
|
||||
<SectionHeader
|
||||
sectionId={section.id}
|
||||
title={section.title}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
bind:value={editTitle}
|
||||
onkeydown={handleTitleKeydown}
|
||||
onblur={handleEditBlur}
|
||||
class="flex-1 rounded border border-input bg-background px-2 py-0.5 text-sm font-medium text-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="flex-1 rounded border border-input bg-background px-2 py-0.5 text-sm font-medium text-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
<IconPickerButton
|
||||
value={editIcon}
|
||||
@@ -135,7 +135,7 @@
|
||||
{#if icon}
|
||||
<DynamicIcon name={icon} size={18} />
|
||||
{/if}
|
||||
<span class="font-medium text-foreground">{title}</span>
|
||||
<span class="font-display text-lg font-semibold text-foreground">{title}</span>
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
name="name"
|
||||
type="text"
|
||||
placeholder="e.g., CI/CD Pipeline"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">A descriptive name to identify this token</p>
|
||||
@@ -34,7 +34,7 @@
|
||||
<select
|
||||
id="token-scope"
|
||||
name="scope"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="read">Read — View apps, boards, and status</option>
|
||||
<option value="write">Write — Modify apps, boards, and settings</option>
|
||||
@@ -50,7 +50,7 @@
|
||||
id="token-expires"
|
||||
name="expiresAt"
|
||||
type="datetime-local"
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
<p class="mt-1 text-xs text-muted-foreground">Leave empty for a non-expiring token</p>
|
||||
</div>
|
||||
@@ -58,7 +58,7 @@
|
||||
<div class="flex items-center gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Generate Token
|
||||
</button>
|
||||
|
||||
@@ -39,9 +39,9 @@
|
||||
|
||||
function scopeBadgeClass(scope: string): string {
|
||||
switch (scope) {
|
||||
case 'admin': return 'bg-red-500/10 text-red-500';
|
||||
case 'write': return 'bg-yellow-500/10 text-yellow-500';
|
||||
default: return 'bg-green-500/10 text-green-500';
|
||||
case 'admin': return 'bg-destructive/10 text-destructive';
|
||||
case 'write': return 'bg-status-degraded/10 text-status-degraded-ink';
|
||||
default: return 'bg-status-online/10 text-status-online-ink';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -54,7 +54,7 @@
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto rounded-lg border border-border">
|
||||
<div class="overflow-x-auto rounded-2xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<table class="w-full text-left text-sm">
|
||||
<thead class="border-b border-border bg-muted/50">
|
||||
<tr>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="rounded-xl border border-border bg-card p-6 shadow-sm">
|
||||
<div class="rounded-xl border border-border bg-card p-6 shadow-[var(--shadow-soft)]">
|
||||
<h2 class="mb-2 text-lg font-semibold text-card-foreground">
|
||||
{$t('settings.bookmarklet_title')}
|
||||
</h2>
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
value={localValue}
|
||||
oninput={handleInput}
|
||||
rows="8"
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 font-mono text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
placeholder={`/* Custom CSS */\n.custom-css-scope .my-widget {\n background: #333;\n}`}
|
||||
spellcheck="false"
|
||||
></textarea>
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
type="button"
|
||||
onclick={() => setMode(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.mode === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{$t(opt.labelKey)}
|
||||
@@ -167,7 +167,7 @@
|
||||
max="360"
|
||||
step="1"
|
||||
bind:value={theme.primaryHue}
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-md [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-md"
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-[var(--shadow-soft)] [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-[var(--shadow-soft)]"
|
||||
style="color: {previewColor};"
|
||||
/>
|
||||
</div>
|
||||
@@ -188,7 +188,7 @@
|
||||
max="100"
|
||||
step="1"
|
||||
bind:value={theme.primarySaturation}
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-md [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-md"
|
||||
class="absolute inset-0 h-3 w-full cursor-pointer appearance-none bg-transparent [&::-moz-range-thumb]:h-5 [&::-moz-range-thumb]:w-5 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border-2 [&::-moz-range-thumb]:border-white [&::-moz-range-thumb]:bg-current [&::-moz-range-thumb]:shadow-[var(--shadow-soft)] [&::-webkit-slider-thumb]:h-5 [&::-webkit-slider-thumb]:w-5 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:border-white [&::-webkit-slider-thumb]:bg-current [&::-webkit-slider-thumb]:shadow-[var(--shadow-soft)]"
|
||||
style="color: {previewColor};"
|
||||
/>
|
||||
</div>
|
||||
@@ -204,7 +204,7 @@
|
||||
type="button"
|
||||
onclick={() => setBackground(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.backgroundType === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{$t(opt.labelKey)}
|
||||
@@ -222,7 +222,7 @@
|
||||
type="button"
|
||||
onclick={() => setCardStyle(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.cardStyle === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{$t(opt.labelKey) ?? opt.value}
|
||||
@@ -240,7 +240,7 @@
|
||||
type="button"
|
||||
onclick={() => setLocale(opt.value)}
|
||||
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {$i18nLocale === opt.value
|
||||
? 'bg-background text-foreground shadow-sm'
|
||||
? 'bg-background text-foreground shadow-[var(--shadow-soft)]'
|
||||
: 'text-muted-foreground hover:text-foreground'}"
|
||||
>
|
||||
{opt.label}
|
||||
@@ -255,12 +255,12 @@
|
||||
type="button"
|
||||
onclick={savePreferences}
|
||||
disabled={saving}
|
||||
class="rounded-md bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-6 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{saving ? $t('settings.saving') : $t('settings.save')}
|
||||
</button>
|
||||
{#if saved}
|
||||
<span class="text-sm text-green-500">{$t('settings.saved')}</span>
|
||||
<span class="text-sm text-status-online-ink">{$t('settings.saved')}</span>
|
||||
{/if}
|
||||
{#if errorMessage}
|
||||
<span class="text-sm text-destructive">{errorMessage}</span>
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
/>
|
||||
|
||||
{#if open && filtered.length > 0}
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-card shadow-lg">
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filtered as item, i (item)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
>
|
||||
<!-- Dialog -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-sm rounded-2xl border border-border bg-card p-6 shadow-2xl"
|
||||
class="mx-4 w-full max-w-sm rounded-[1.4rem] border border-border bg-card p-6 shadow-[var(--shadow-lift)]"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
onkeydown={(e) => e.stopPropagation()}
|
||||
transition:scale={{ start: 0.95, duration: 150 }}
|
||||
@@ -49,21 +49,21 @@
|
||||
aria-labelledby="confirm-dialog-title"
|
||||
aria-describedby="confirm-dialog-message"
|
||||
>
|
||||
<h2 id="confirm-dialog-title" class="mb-2 text-base font-semibold text-foreground">{title}</h2>
|
||||
<p id="confirm-dialog-message" class="mb-5 text-sm text-muted-foreground">{message}</p>
|
||||
<h2 id="confirm-dialog-title" class="mb-2 font-display text-lg font-semibold text-foreground">{title}</h2>
|
||||
<p id="confirm-dialog-message" class="mb-5 text-sm leading-relaxed text-muted-foreground">{message}</p>
|
||||
|
||||
<div class="flex items-center justify-end gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onclick={onCancel}
|
||||
class="rounded-lg border border-border px-4 py-2 text-sm text-foreground transition-colors hover:bg-accent"
|
||||
class="rounded-xl border border-border px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-accent"
|
||||
>
|
||||
{cancelLabel ?? ($t('common.cancel') ?? 'Cancel')}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onclick={onConfirm}
|
||||
class="rounded-lg px-4 py-2 text-sm font-medium transition-colors
|
||||
class="rounded-xl px-4 py-2 text-sm font-semibold shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5
|
||||
{destructive
|
||||
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
|
||||
: 'bg-primary text-primary-foreground hover:bg-primary/90'}"
|
||||
|
||||
@@ -130,7 +130,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
onclick={openPicker}
|
||||
>
|
||||
{#if selectedItem}
|
||||
@@ -157,7 +157,7 @@
|
||||
style="animation: epFadeIn 0.15s ease-out"
|
||||
>
|
||||
<div
|
||||
class="flex w-full max-w-md flex-col rounded-lg border border-border bg-popover shadow-2xl"
|
||||
class="flex w-full max-w-md flex-col rounded-[1.4rem] border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
style="max-height: 60vh; animation: epSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
|
||||
role="dialog"
|
||||
aria-label={searchPlaceholder || 'Select entity'}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
class="relative z-10 flex min-h-screen items-center justify-center bg-background/80 p-4 text-foreground"
|
||||
>
|
||||
<div
|
||||
class="w-full max-w-lg rounded-xl border border-border bg-card/90 p-8 text-center shadow-xl backdrop-blur-sm"
|
||||
class="w-full max-w-lg rounded-xl border border-border bg-card/90 p-8 text-center shadow-[var(--shadow-lift)] backdrop-blur-sm"
|
||||
>
|
||||
<div class="mb-2 text-xs uppercase tracking-widest text-muted-foreground">
|
||||
{status}
|
||||
|
||||
@@ -112,7 +112,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="icon-grid-trigger flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="icon-grid-trigger flex w-full items-center gap-2 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
bind:this={triggerEl}
|
||||
onclick={toggle}
|
||||
>
|
||||
@@ -129,7 +129,7 @@
|
||||
|
||||
{#if open}
|
||||
<div
|
||||
class="icon-grid-popup fixed z-50 overflow-y-auto rounded-lg border border-border bg-popover shadow-xl"
|
||||
class="icon-grid-popup fixed z-50 overflow-y-auto rounded-xl border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
bind:this={popupEl}
|
||||
style="animation: iconGridSlideIn 0.15s ease-out"
|
||||
>
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={toggleOpen}
|
||||
class="flex items-center gap-1.5 rounded-lg border border-input bg-background transition-colors hover:bg-accent
|
||||
class="flex items-center gap-1.5 rounded-xl border border-input bg-background transition-colors hover:bg-accent
|
||||
{size === 'sm' ? 'px-2 py-1' : 'px-3 py-2'}"
|
||||
title={$t('app.icon') ?? 'Select icon'}
|
||||
>
|
||||
@@ -105,7 +105,7 @@
|
||||
class="fixed inset-0 z-50"
|
||||
onclick={(e) => { if (e.target === e.currentTarget) open = false; }}
|
||||
>
|
||||
<div class="fixed left-1/2 top-1/2 z-50 w-80 -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card p-3 shadow-2xl">
|
||||
<div class="fixed left-1/2 top-1/2 z-50 w-80 -translate-x-1/2 -translate-y-1/2 rounded-xl border border-border bg-card p-3 shadow-[var(--shadow-lift)]">
|
||||
<!-- Search -->
|
||||
<div class="relative mb-2">
|
||||
<svg class="absolute left-2.5 top-1/2 -translate-y-1/2 text-muted-foreground" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -116,7 +116,7 @@
|
||||
type="text"
|
||||
bind:value={query}
|
||||
placeholder={$t('common.search') ?? 'Search icons...'}
|
||||
class="w-full rounded-lg border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -160,7 +160,7 @@
|
||||
value={value}
|
||||
oninput={(e) => { const v = (e.target as HTMLInputElement).value; value = v; onchange(v); }}
|
||||
placeholder={$t('app.icon_manual') ?? 'Or type icon name...'}
|
||||
class="w-full rounded-lg border border-input bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background px-2 py-1 text-xs text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
onkeydown={(e) => e.key === 'Escape' && keyboard.closeOverlay()}
|
||||
>
|
||||
<div
|
||||
class="mx-4 w-full max-w-xl rounded-2xl border border-border bg-card shadow-2xl"
|
||||
class="mx-4 w-full max-w-xl rounded-2xl border border-border bg-card shadow-[var(--shadow-lift)]"
|
||||
role="dialog"
|
||||
aria-label="Keyboard Shortcuts"
|
||||
>
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="flex w-full items-center gap-2 rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground transition-colors hover:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
onclick={openPicker}
|
||||
>
|
||||
{#if selectedCount > 0}
|
||||
@@ -148,7 +148,7 @@
|
||||
style="animation: mepFadeIn 0.15s ease-out"
|
||||
>
|
||||
<div
|
||||
class="flex w-full max-w-md flex-col rounded-lg border border-border bg-popover shadow-2xl"
|
||||
class="flex w-full max-w-md flex-col rounded-[1.4rem] border border-border bg-popover shadow-[var(--shadow-lift)]"
|
||||
style="max-height: 60vh; animation: mepSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
|
||||
role="dialog"
|
||||
aria-label={searchPlaceholder || 'Select items'}
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
{#if tags.length > 0}
|
||||
<div class="mb-1.5 flex flex-wrap gap-1">
|
||||
{#each tags as tag (tag)}
|
||||
<span class="flex items-center gap-1 rounded-md bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||
<span class="flex items-center gap-1 rounded-xl bg-primary/10 px-2 py-0.5 text-xs text-primary">
|
||||
{tag}
|
||||
<button
|
||||
type="button"
|
||||
@@ -128,7 +128,7 @@
|
||||
/>
|
||||
|
||||
{#if open && filtered.length > 0}
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-lg border border-border bg-card shadow-lg">
|
||||
<div class="absolute left-0 top-full z-50 mt-1 max-h-48 w-full overflow-y-auto rounded-xl border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
{#each filtered as item, i (item)}
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -139,14 +139,14 @@
|
||||
href={app.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="card-hover group flex items-center gap-2 rounded-lg {cardStyleClass} px-3 py-2 text-left transition-colors hover:border-primary/50"
|
||||
class="card-hover group flex items-center gap-2 rounded-2xl {cardStyleClass} px-3 py-2 text-left transition-colors hover:border-primary/50"
|
||||
data-app-widget
|
||||
data-app-url={app.url}
|
||||
oncontextmenu={handleContextMenu}
|
||||
onclick={recordClick}
|
||||
>
|
||||
<div class="relative flex-shrink-0">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-md bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if app.iconType === 'emoji' && app.icon}
|
||||
<span class="text-base">{app.icon}</span>
|
||||
{:else if iconSrc}
|
||||
@@ -198,7 +198,7 @@
|
||||
<!-- Large: icon + name + description + sparkline + tags + links -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="card-hover group rounded-xl {cardStyleClass} p-5 transition-colors hover:border-primary/50"
|
||||
class="card-hover group rounded-[1.4rem] {cardStyleClass} p-5 transition-colors hover:border-primary/50"
|
||||
data-app-widget
|
||||
data-app-url={app.url}
|
||||
oncontextmenu={handleContextMenu}
|
||||
@@ -211,7 +211,7 @@
|
||||
onclick={recordClick}
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-xl bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if app.iconType === 'emoji' && app.icon}
|
||||
<span class="text-3xl">{app.icon}</span>
|
||||
{:else if iconSrc}
|
||||
@@ -294,7 +294,7 @@
|
||||
<!-- Medium (default): icon + name + status + sparkline on hover + links -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="card-hover group rounded-xl {cardStyleClass} p-4 transition-colors hover:border-primary/50"
|
||||
class="card-hover group rounded-[1.4rem] {cardStyleClass} p-4 transition-colors hover:border-primary/50"
|
||||
data-app-widget
|
||||
data-app-url={app.url}
|
||||
oncontextmenu={handleContextMenu}
|
||||
@@ -307,7 +307,7 @@
|
||||
onclick={recordClick}
|
||||
>
|
||||
<div class="relative">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if app.iconType === 'emoji' && app.icon}
|
||||
<span class="text-2xl">{app.icon}</span>
|
||||
{:else if iconSrc}
|
||||
@@ -378,12 +378,12 @@
|
||||
<!-- Context Menu -->
|
||||
{#if showContextMenu}
|
||||
<div
|
||||
class="fixed z-50 rounded-lg border border-border bg-popover p-1 shadow-lg"
|
||||
class="fixed z-50 rounded-2xl border border-border bg-popover p-1 shadow-[var(--shadow-soft)]"
|
||||
style="left: {contextMenuPos.x}px; top: {contextMenuPos.y}px"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 rounded-sm px-3 py-1.5 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
class="flex w-full items-center gap-2 rounded-xl px-3 py-2 text-sm text-popover-foreground transition-colors hover:bg-accent"
|
||||
onclick={toggleFavorite}
|
||||
>
|
||||
{#if favorites.isFavorite(app.id)}
|
||||
|
||||
@@ -17,10 +17,10 @@
|
||||
href={config.url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="card-hover group flex flex-col items-center gap-2 rounded-xl border border-border bg-card p-4 text-center transition-colors hover:border-primary/50"
|
||||
class="card-hover group flex flex-col items-center gap-2 rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4 text-center transition-colors hover:border-primary/50"
|
||||
>
|
||||
<!-- Icon -->
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-lg bg-muted transition-colors group-hover:bg-accent">
|
||||
<div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-muted transition-colors group-hover:bg-accent">
|
||||
{#if config.icon}
|
||||
<span class="text-2xl">{config.icon}</span>
|
||||
{:else}
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
<!-- Badge -->
|
||||
<span class="inline-flex items-center gap-1.5 text-xs">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-blue-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full" style="background: var(--room-sky);"></span>
|
||||
<span class="text-muted-foreground">Bookmark</span>
|
||||
</span>
|
||||
</a>
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Calendar class="h-4 w-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium text-foreground">Calendar</span>
|
||||
|
||||
@@ -154,7 +154,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card overflow-hidden">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] overflow-hidden">
|
||||
<!-- Stream view -->
|
||||
<div
|
||||
class="relative w-full bg-black"
|
||||
|
||||
@@ -111,7 +111,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
{#if clockStyle === 'analog'}
|
||||
<!-- Analog clock face -->
|
||||
<svg viewBox="0 0 100 100" class="h-32 w-32">
|
||||
@@ -154,7 +154,7 @@
|
||||
<p class="mt-2 text-xs text-muted-foreground">{dateStr}</p>
|
||||
{:else}
|
||||
<!-- Digital clock -->
|
||||
<p class="font-mono text-4xl font-bold tabular-nums text-foreground">{timeStr}</p>
|
||||
<p class="font-display text-4xl font-semibold tabular-nums text-foreground">{timeStr}</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">{dateStr}</p>
|
||||
{#if config.timezone}
|
||||
<p class="mt-0.5 text-xs text-muted-foreground/70">{config.timezone.replace(/_/g, ' ')}</p>
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col rounded-xl border border-border bg-card">
|
||||
<div class="flex flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<div class="relative" style="height: {iframeHeight}px;">
|
||||
{#if !safeUrl}
|
||||
<div class="flex h-full items-center justify-center text-sm text-muted-foreground">
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
const links = $derived(config.links ?? []);
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<!-- Header -->
|
||||
{#if isCollapsible}
|
||||
<button
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)]">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center justify-end border-b border-border px-3 py-1.5">
|
||||
<button
|
||||
|
||||
@@ -64,13 +64,13 @@
|
||||
});
|
||||
|
||||
const trendColor = $derived.by(() => {
|
||||
if (trend === 'up') return 'text-green-500';
|
||||
if (trend === 'down') return 'text-red-500';
|
||||
return 'text-muted-foreground';
|
||||
if (trend === 'up') return 'var(--status-online-ink)';
|
||||
if (trend === 'down') return 'var(--status-offline-ink)';
|
||||
return 'var(--muted-foreground)';
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
{#if loading}
|
||||
<div class="flex flex-col items-center gap-2">
|
||||
<div class="h-10 w-24 animate-pulse rounded bg-muted"></div>
|
||||
@@ -80,7 +80,7 @@
|
||||
<span class="text-xs text-muted-foreground">Failed to load metric</span>
|
||||
{:else if currentValue !== null}
|
||||
<!-- Trend arrow -->
|
||||
<div class="mb-1 {trendColor}">
|
||||
<div class="mb-1" style="color: {trendColor};">
|
||||
{#if trend === 'up'}
|
||||
<TrendingUp class="h-5 w-5" />
|
||||
{:else if trend === 'down'}
|
||||
@@ -92,7 +92,7 @@
|
||||
|
||||
<!-- Big number -->
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-4xl font-bold tabular-nums text-foreground">
|
||||
<span class="font-display text-4xl font-semibold tabular-nums text-foreground">
|
||||
{formatNumber(currentValue)}
|
||||
</span>
|
||||
{#if config.unit}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<div class="prose prose-sm prose-invert max-w-none flex-1 overflow-auto text-foreground">
|
||||
<!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized with DOMPurify -->
|
||||
{@html renderedContent}
|
||||
|
||||
@@ -80,7 +80,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<div class="mb-3 flex items-center gap-2">
|
||||
<Rss class="h-4 w-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium text-foreground">RSS Feed</span>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
let expanded = $state(false);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<!-- Header -->
|
||||
<button
|
||||
type="button"
|
||||
@@ -63,28 +63,28 @@
|
||||
<div class="mt-3 flex gap-1">
|
||||
{#if statusCounts.online > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-green-500"
|
||||
class="h-2 rounded-full bg-status-online"
|
||||
style="flex: {statusCounts.online}"
|
||||
title="{statusCounts.online} online"
|
||||
></div>
|
||||
{/if}
|
||||
{#if statusCounts.degraded > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-yellow-500"
|
||||
class="h-2 rounded-full bg-status-degraded"
|
||||
style="flex: {statusCounts.degraded}"
|
||||
title="{statusCounts.degraded} degraded"
|
||||
></div>
|
||||
{/if}
|
||||
{#if statusCounts.offline > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-red-500"
|
||||
class="h-2 rounded-full bg-status-offline"
|
||||
style="flex: {statusCounts.offline}"
|
||||
title="{statusCounts.offline} offline"
|
||||
></div>
|
||||
{/if}
|
||||
{#if statusCounts.unknown > 0}
|
||||
<div
|
||||
class="h-2 rounded-full bg-gray-500"
|
||||
class="h-2 rounded-full bg-status-unknown"
|
||||
style="flex: {statusCounts.unknown}"
|
||||
title="{statusCounts.unknown} unknown"
|
||||
></div>
|
||||
@@ -95,25 +95,25 @@
|
||||
<div class="mt-2 flex flex-wrap gap-3 text-xs">
|
||||
{#if statusCounts.online > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-green-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-online"></span>
|
||||
{statusCounts.online} online
|
||||
</span>
|
||||
{/if}
|
||||
{#if statusCounts.degraded > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-yellow-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-degraded"></span>
|
||||
{statusCounts.degraded} degraded
|
||||
</span>
|
||||
{/if}
|
||||
{#if statusCounts.offline > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-red-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-offline"></span>
|
||||
{statusCounts.offline} offline
|
||||
</span>
|
||||
{/if}
|
||||
{#if statusCounts.unknown > 0}
|
||||
<span class="flex items-center gap-1">
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-gray-500"></span>
|
||||
<span class="inline-block h-2 w-2 rounded-full bg-status-unknown"></span>
|
||||
{statusCounts.unknown} unknown
|
||||
</span>
|
||||
{/if}
|
||||
@@ -126,12 +126,12 @@
|
||||
{@const status = app.statuses[0]?.status ?? 'unknown'}
|
||||
{@const statusColor =
|
||||
status === 'online'
|
||||
? 'bg-green-500'
|
||||
? 'bg-status-online'
|
||||
: status === 'offline'
|
||||
? 'bg-red-500'
|
||||
? 'bg-status-offline'
|
||||
: status === 'degraded'
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-gray-500'}
|
||||
? 'bg-status-degraded'
|
||||
: 'bg-status-unknown'}
|
||||
<div class="flex items-center justify-between text-xs">
|
||||
<span class="text-foreground">{app.name}</span>
|
||||
<span class="flex items-center gap-1">
|
||||
|
||||
@@ -21,15 +21,15 @@
|
||||
const refreshMs = $derived((config.refreshInterval ?? 30) * 1000);
|
||||
|
||||
function thresholdColor(value: number): string {
|
||||
if (value >= 85) return 'text-red-500';
|
||||
if (value >= 60) return 'text-yellow-500';
|
||||
return 'text-green-500';
|
||||
if (value >= 85) return 'text-status-offline-ink';
|
||||
if (value >= 60) return 'text-status-degraded-ink';
|
||||
return 'text-status-online-ink';
|
||||
}
|
||||
|
||||
function thresholdStroke(value: number): string {
|
||||
if (value >= 85) return 'stroke-red-500';
|
||||
if (value >= 60) return 'stroke-yellow-500';
|
||||
return 'stroke-green-500';
|
||||
if (value >= 85) return 'stroke-status-offline';
|
||||
if (value >= 60) return 'stroke-status-degraded';
|
||||
return 'stroke-status-online';
|
||||
}
|
||||
|
||||
function thresholdTrack(_value: number): string {
|
||||
@@ -80,7 +80,7 @@
|
||||
const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
|
||||
</script>
|
||||
|
||||
<div class="flex h-full flex-col rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full flex-col rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<span class="mb-3 text-sm font-medium text-foreground">System Stats</span>
|
||||
|
||||
{#if loading}
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
let rssShowSummary = $state((initialConfig.showSummary as boolean) ?? true);
|
||||
|
||||
// Calendar
|
||||
let calendarUrlsRaw = $state((initialConfig.icalUrls as Array<{ url: string; color: string; label: string }>) ?? [{ url: '', color: '#6366f1', label: '' }]);
|
||||
let calendarUrlsRaw = $state((initialConfig.icalUrls as Array<{ url: string; color: string; label: string }>) ?? [{ url: '', color: '#e8754f', label: '' }]);
|
||||
let calendarDaysAhead = $state((initialConfig.daysAhead as number) ?? 7);
|
||||
|
||||
// Markdown
|
||||
@@ -155,7 +155,7 @@
|
||||
}
|
||||
|
||||
function addCalendarUrl() {
|
||||
calendarUrlsRaw = [...calendarUrlsRaw, { url: '', color: '#6366f1', label: '' }];
|
||||
calendarUrlsRaw = [...calendarUrlsRaw, { url: '', color: '#e8754f', label: '' }];
|
||||
}
|
||||
|
||||
function removeCalendarUrl(index: number) {
|
||||
@@ -163,7 +163,7 @@
|
||||
}
|
||||
|
||||
// Helper for input styling
|
||||
const inputClass = 'w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30';
|
||||
const inputClass = 'w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30';
|
||||
const labelClass = 'mb-1 block text-sm font-medium text-foreground';
|
||||
|
||||
let firstInput: HTMLElement | undefined = $state();
|
||||
@@ -171,7 +171,7 @@
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="rounded-xl border border-border bg-card p-4 shadow-lg"
|
||||
class="rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]"
|
||||
transition:fade={{ duration: 100 }}
|
||||
onkeydown={handleKeydown}
|
||||
role="dialog"
|
||||
@@ -201,11 +201,11 @@
|
||||
bind:value={appSearchQuery}
|
||||
bind:this={firstInput}
|
||||
placeholder={$t('common.search') ?? 'Search apps...'}
|
||||
class="w-full rounded-lg border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus:ring-1 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background py-1.5 pl-8 pr-3 text-sm text-foreground placeholder:text-muted-foreground focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
<!-- App grid -->
|
||||
<div class="max-h-48 overflow-y-auto rounded-lg border border-input bg-background p-1">
|
||||
<div class="max-h-48 overflow-y-auto rounded-xl border border-input bg-background p-1">
|
||||
{#if filteredApps.length === 0}
|
||||
<p class="py-4 text-center text-xs text-muted-foreground">{$t('common.no_results') ?? 'No apps found'}</p>
|
||||
{:else}
|
||||
@@ -548,7 +548,7 @@
|
||||
{$t('common.cancel') ?? 'Cancel'}
|
||||
</button>
|
||||
<button type="button" onclick={handleSave}
|
||||
class="rounded-lg bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
|
||||
class="rounded-xl bg-primary px-3 py-1.5 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90">
|
||||
{mode === 'create' ? ($t('common.add') ?? 'Add') : ($t('common.save') ?? 'Save')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
// Calendar fields
|
||||
let calendarUrls = $state<Array<{ url: string; color: string; label: string }>>([
|
||||
{ url: '', color: '#6366f1', label: '' }
|
||||
{ url: '', color: '#e8754f', label: '' }
|
||||
]);
|
||||
let calendarDaysAhead = $state(7);
|
||||
|
||||
@@ -198,7 +198,7 @@
|
||||
rssFeedUrl = '';
|
||||
rssMaxItems = 10;
|
||||
rssShowSummary = true;
|
||||
calendarUrls = [{ url: '', color: '#6366f1', label: '' }];
|
||||
calendarUrls = [{ url: '', color: '#e8754f', label: '' }];
|
||||
calendarDaysAhead = 7;
|
||||
markdownContent = '';
|
||||
metricLabel = '';
|
||||
@@ -350,7 +350,7 @@
|
||||
}
|
||||
|
||||
function addCalendarUrl() {
|
||||
calendarUrls = [...calendarUrls, { url: '', color: '#6366f1', label: '' }];
|
||||
calendarUrls = [...calendarUrls, { url: '', color: '#e8754f', label: '' }];
|
||||
}
|
||||
|
||||
function removeCalendarUrl(index: number) {
|
||||
@@ -367,7 +367,7 @@
|
||||
|
||||
// Input CSS class for reuse
|
||||
const inputClass =
|
||||
'w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30';
|
||||
'w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30';
|
||||
</script>
|
||||
|
||||
<div class="mb-3 rounded-lg border border-border bg-muted/50 p-3">
|
||||
@@ -505,7 +505,7 @@
|
||||
</div>
|
||||
<div>
|
||||
<span class="mb-1 block text-sm font-medium text-foreground">Select Apps</span>
|
||||
<div class="max-h-40 space-y-1 overflow-y-auto rounded-lg border border-input bg-background p-2">
|
||||
<div class="max-h-40 space-y-1 overflow-y-auto rounded-xl border border-input bg-background p-2">
|
||||
{#each apps as app (app.id)}
|
||||
<label class="flex items-center gap-2 rounded px-2 py-1 text-sm text-foreground hover:bg-accent">
|
||||
<input
|
||||
@@ -613,7 +613,7 @@
|
||||
<span class="mb-1 block text-sm font-medium text-foreground">Metrics</span>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{#each ['cpu', 'ram', 'disk', 'swap', 'network'] as metric (metric)}
|
||||
<label class="flex items-center gap-1.5 rounded-md border border-input px-2 py-1 text-sm text-foreground hover:bg-accent">
|
||||
<label class="flex items-center gap-1.5 rounded-xl border border-input px-2 py-1 text-sm text-foreground hover:bg-accent">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={sysStatsMetrics.includes(metric)}
|
||||
@@ -704,7 +704,7 @@
|
||||
<input
|
||||
type="color"
|
||||
bind:value={calendarUrls[i].color}
|
||||
class="h-9 w-9 cursor-pointer rounded-lg border border-input bg-background"
|
||||
class="h-9 w-9 cursor-pointer rounded-xl border border-input bg-background"
|
||||
title="Calendar color"
|
||||
/>
|
||||
</div>
|
||||
@@ -1038,7 +1038,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={handleSubmitWidget}
|
||||
class="rounded-md bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-2 py-1 text-xs font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{$t('common.add')}
|
||||
</button>
|
||||
|
||||
@@ -46,7 +46,7 @@
|
||||
<div class="absolute inset-0 z-10 rounded-xl bg-black/5 transition-opacity">
|
||||
<!-- Top-left: drag handle -->
|
||||
<div class="absolute left-1.5 top-1.5">
|
||||
<div class="cursor-grab rounded-md bg-card/90 p-1 text-muted-foreground shadow-sm backdrop-blur-sm" title="Drag to reorder">
|
||||
<div class="cursor-grab rounded-md bg-card/90 p-1 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm" title="Drag to reorder">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="9" cy="5" r="1" /><circle cx="15" cy="5" r="1" />
|
||||
<circle cx="9" cy="12" r="1" /><circle cx="15" cy="12" r="1" />
|
||||
@@ -62,7 +62,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { showSizePicker = !showSizePicker; previewSpan = null; }}
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm transition-colors
|
||||
{showSizePicker ? 'bg-primary text-primary-foreground' : 'hover:bg-accent hover:text-foreground'}"
|
||||
title={$t('widget.resize') ?? 'Resize'}
|
||||
>
|
||||
@@ -78,7 +78,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => onEdit(widgetId)}
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors hover:bg-primary hover:text-primary-foreground"
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm transition-colors hover:bg-primary hover:text-primary-foreground"
|
||||
title={$t('common.edit') ?? 'Edit'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -91,7 +91,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => { showDeleteConfirm = true; }}
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-sm backdrop-blur-sm transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
||||
class="rounded-md bg-card/90 p-1.5 text-muted-foreground shadow-[var(--shadow-soft)] backdrop-blur-sm transition-colors hover:bg-destructive hover:text-destructive-foreground"
|
||||
title={$t('common.delete') ?? 'Delete'}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
@@ -103,7 +103,7 @@
|
||||
|
||||
<!-- Size picker popover -->
|
||||
{#if showSizePicker && onResize}
|
||||
<div class="absolute right-1.5 top-10 z-20 rounded-lg border border-border bg-card p-2 shadow-xl backdrop-blur-sm">
|
||||
<div class="absolute right-1.5 top-10 z-20 rounded-xl border border-border bg-card p-2 shadow-[var(--shadow-lift)] backdrop-blur-sm">
|
||||
<div class="mb-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
|
||||
{$t('widget.width') ?? 'Width'}
|
||||
</div>
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
</script>
|
||||
|
||||
{#snippet skeleton()}
|
||||
<div class="flex h-full items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<span class="text-xs text-muted-foreground">…</span>
|
||||
</div>
|
||||
{/snippet}
|
||||
@@ -163,7 +163,7 @@
|
||||
}} />
|
||||
{/await}
|
||||
{:else}
|
||||
<div class="flex h-full items-center justify-center rounded-xl border border-border bg-card p-4">
|
||||
<div class="flex h-full items-center justify-center rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-soft)] p-4">
|
||||
<span class="text-xs text-muted-foreground">{$t('widget.type', { values: { type: widget.type } })}</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div
|
||||
class="mx-4 w-full max-w-lg rounded-2xl border border-border bg-card shadow-2xl lg:max-w-2xl"
|
||||
class="mx-4 w-full max-w-lg rounded-[1.4rem] border border-border bg-card shadow-[var(--shadow-lift)] lg:max-w-2xl"
|
||||
onclick={(e) => e.stopPropagation()}
|
||||
transition:scale={{ start: 0.95, duration: 150 }}
|
||||
>
|
||||
@@ -110,7 +110,7 @@
|
||||
type="text"
|
||||
bind:value={filterQuery}
|
||||
placeholder={$t('widget.search_type') ?? 'Search widget types...'}
|
||||
class="w-full rounded-lg border border-input bg-background py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus:ring-2 focus:ring-ring/30"
|
||||
class="w-full rounded-xl border border-input bg-background py-2 pl-9 pr-3 text-sm text-foreground placeholder:text-muted-foreground transition-colors focus:border-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,7 +127,7 @@
|
||||
onclick={() => onSelect(wt.value)}
|
||||
class="flex items-start gap-3 rounded-xl px-3 py-3 text-left transition-colors hover:bg-accent"
|
||||
>
|
||||
<div class="mt-0.5 shrink-0 rounded-lg bg-primary/10 p-2 text-primary">
|
||||
<div class="mt-0.5 shrink-0 rounded-xl bg-primary/10 p-2 text-primary">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
{#each iconFor(wt.value).split('|') as segment, si (si)}
|
||||
{#if segment.includes('m') || segment.includes('M') || segment.includes('a') || segment.includes('z') || segment.includes('A') || segment.includes('c') || segment.includes('l') || segment.includes('v') || segment.includes('h') || segment.includes('V') || segment.includes('H')}
|
||||
|
||||
@@ -9,9 +9,12 @@
|
||||
|
||||
const severityStyles = $derived.by(() => {
|
||||
switch (data.severity) {
|
||||
case 'critical': return 'border-red-500/50 bg-red-500/10 text-red-400';
|
||||
case 'warning': return 'border-yellow-500/50 bg-yellow-500/10 text-yellow-400';
|
||||
default: return 'border-blue-500/50 bg-blue-500/10 text-blue-400';
|
||||
case 'critical':
|
||||
return 'border-status-offline/40 bg-status-offline/10 text-status-offline-ink';
|
||||
case 'warning':
|
||||
return 'border-status-degraded/40 bg-status-degraded/10 text-status-degraded-ink';
|
||||
default:
|
||||
return 'border-room-sky/40 bg-room-sky/10 text-room-sky';
|
||||
}
|
||||
});
|
||||
|
||||
@@ -20,7 +23,7 @@
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex items-start gap-3 rounded-lg border p-3 {severityStyles}">
|
||||
<div class="flex items-start gap-3 rounded-2xl border p-3.5 {severityStyles}">
|
||||
<span class="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-current text-xs font-bold">
|
||||
{severityIcon}
|
||||
</span>
|
||||
|
||||
@@ -15,7 +15,15 @@
|
||||
data.labels.length > 0 ? Math.max(100 / data.labels.length - 2, 4) : 10
|
||||
);
|
||||
|
||||
const defaultColors = ['#6366f1', '#22c55e', '#eab308', '#ef4444', '#06b6d4'];
|
||||
// Cozy "room" palette for multi-series charts
|
||||
const defaultColors = [
|
||||
'var(--room-terra)',
|
||||
'var(--room-sky)',
|
||||
'var(--room-sage)',
|
||||
'var(--room-butter)',
|
||||
'var(--room-lav)',
|
||||
'var(--room-peach)'
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="p-3">
|
||||
|
||||
@@ -12,9 +12,9 @@
|
||||
const color = $derived.by(() => {
|
||||
const warn = data.thresholds?.warning ?? 60;
|
||||
const crit = data.thresholds?.critical ?? 85;
|
||||
if (percentage >= crit) return '#ef4444'; // red
|
||||
if (percentage >= warn) return '#eab308'; // yellow
|
||||
return '#22c55e'; // green
|
||||
if (percentage >= crit) return 'var(--status-offline)';
|
||||
if (percentage >= warn) return 'var(--status-degraded)';
|
||||
return 'var(--status-online)';
|
||||
});
|
||||
|
||||
// SVG circle math
|
||||
|
||||
@@ -12,13 +12,13 @@
|
||||
);
|
||||
|
||||
const trendColor = $derived(
|
||||
data.trend === 'up' ? 'text-green-500' : data.trend === 'down' ? 'text-red-500' : 'text-muted-foreground'
|
||||
data.trend === 'up' ? 'text-status-online-ink' : data.trend === 'down' ? 'text-status-offline-ink' : 'text-muted-foreground'
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col items-center justify-center gap-1 p-4 text-center">
|
||||
<div class="flex items-baseline gap-1">
|
||||
<span class="text-3xl font-bold text-foreground">{data.value}</span>
|
||||
<span class="font-display text-3xl font-semibold text-foreground">{data.value}</span>
|
||||
{#if data.unit}
|
||||
<span class="text-sm text-muted-foreground">{data.unit}</span>
|
||||
{/if}
|
||||
|
||||
@@ -355,6 +355,7 @@
|
||||
"theme.toggle": "Toggle theme (current: {mode})",
|
||||
"theme.title": "Theme: {mode}",
|
||||
|
||||
"bg.cozy": "Cozy Glow",
|
||||
"bg.mesh": "Mesh Gradient",
|
||||
"bg.particles": "Particles",
|
||||
"bg.aurora": "Aurora",
|
||||
@@ -370,6 +371,10 @@
|
||||
"home.welcome": "Welcome, {name}. No default board is configured yet.",
|
||||
"home.view_boards": "View Boards",
|
||||
"home.browse_apps": "Browse Apps",
|
||||
"home.greet_morning": "Good morning",
|
||||
"home.greet_afternoon": "Good afternoon",
|
||||
"home.greet_evening": "Good evening",
|
||||
"home.greet_night": "Still up",
|
||||
|
||||
"language.label": "Language",
|
||||
|
||||
|
||||
@@ -339,6 +339,7 @@
|
||||
"theme.system": "Системная",
|
||||
"theme.toggle": "Переключить тему (текущая: {mode})",
|
||||
"theme.title": "Тема: {mode}",
|
||||
"bg.cozy": "Уютное свечение",
|
||||
"bg.mesh": "Меш-градиент",
|
||||
"bg.particles": "Частицы",
|
||||
"bg.aurora": "Сияние",
|
||||
@@ -352,6 +353,10 @@
|
||||
"home.welcome": "Добро пожаловать, {name}. Доска по умолчанию ещё не настроена.",
|
||||
"home.view_boards": "Посмотреть доски",
|
||||
"home.browse_apps": "Обзор приложений",
|
||||
"home.greet_morning": "Доброе утро",
|
||||
"home.greet_afternoon": "Добрый день",
|
||||
"home.greet_evening": "Добрый вечер",
|
||||
"home.greet_night": "Всё ещё не спите",
|
||||
"language.label": "Язык",
|
||||
"settings.title": "Настройки",
|
||||
"settings.theme": "Режим темы",
|
||||
|
||||
@@ -7,7 +7,7 @@ const BG_TYPE_KEY = 'wal-bg-type';
|
||||
const CARD_STYLE_KEY = 'wal-card-style';
|
||||
|
||||
export type ThemeMode = 'dark' | 'light' | 'system';
|
||||
export type BackgroundType = 'mesh' | 'particles' | 'aurora' | 'none' | 'wallpaper';
|
||||
export type BackgroundType = 'cozy' | 'mesh' | 'particles' | 'aurora' | 'none' | 'wallpaper';
|
||||
export type CardStyle = 'solid' | 'glass' | 'outline';
|
||||
|
||||
function getStoredValue<T>(key: string, fallback: T): T {
|
||||
@@ -35,9 +35,9 @@ function getStoredNumber(key: string, fallback: number): number {
|
||||
|
||||
class ThemeStore {
|
||||
mode = $state<ThemeMode>('system');
|
||||
primaryHue = $state(220);
|
||||
primarySaturation = $state(70);
|
||||
backgroundType = $state<BackgroundType>('mesh');
|
||||
primaryHue = $state(16); // Cozy Home default: terracotta
|
||||
primarySaturation = $state(72);
|
||||
backgroundType = $state<BackgroundType>('cozy');
|
||||
cardStyle = $state<CardStyle>('solid');
|
||||
|
||||
#systemPreference: 'dark' | 'light' = 'dark';
|
||||
@@ -52,9 +52,9 @@ class ThemeStore {
|
||||
constructor() {
|
||||
if (typeof window !== 'undefined') {
|
||||
this.mode = getStoredValue<ThemeMode>(THEME_STORAGE_KEY, 'system');
|
||||
this.primaryHue = getStoredNumber(PRIMARY_HUE_KEY, 220);
|
||||
this.primarySaturation = getStoredNumber(PRIMARY_SAT_KEY, 70);
|
||||
this.backgroundType = getStoredValue<BackgroundType>(BG_TYPE_KEY, 'mesh');
|
||||
this.primaryHue = getStoredNumber(PRIMARY_HUE_KEY, 16);
|
||||
this.primarySaturation = getStoredNumber(PRIMARY_SAT_KEY, 72);
|
||||
this.backgroundType = getStoredValue<BackgroundType>(BG_TYPE_KEY, 'cozy');
|
||||
this.cardStyle = getStoredValue<CardStyle>(CARD_STYLE_KEY, 'solid');
|
||||
|
||||
const mql = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
@@ -180,7 +180,7 @@ export const DEFAULTS = {
|
||||
REFRESH_TOKEN_EXPIRY_DAYS: 7,
|
||||
REMEMBER_ME_REFRESH_TOKEN_EXPIRY_DAYS: 30,
|
||||
DEFAULT_THEME: 'dark',
|
||||
DEFAULT_PRIMARY_COLOR: '#6366f1',
|
||||
DEFAULT_PRIMARY_COLOR: '#e8754f',
|
||||
SYSTEM_SETTINGS_ID: 'singleton',
|
||||
SALT_ROUNDS: 12,
|
||||
INVITE_DEFAULT_EXPIRY_DAYS: 7,
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
{#snippet actions()}
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('error.back_to_dashboard')}
|
||||
</a>
|
||||
|
||||
+41
-7
@@ -3,33 +3,67 @@
|
||||
import type { PageData } from './$types.js';
|
||||
|
||||
let { data }: { data: PageData } = $props();
|
||||
|
||||
const hour = new Date().getHours();
|
||||
const greetKey =
|
||||
hour < 5 ? 'home.greet_night'
|
||||
: hour < 12 ? 'home.greet_morning'
|
||||
: hour < 18 ? 'home.greet_afternoon'
|
||||
: 'home.greet_evening';
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$t('app_title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex min-h-[60vh] items-center justify-center p-6">
|
||||
<div class="text-center">
|
||||
<h1 class="text-4xl font-bold text-foreground">{$t('app_title')}</h1>
|
||||
<div class="relative flex min-h-[70vh] items-center justify-center overflow-hidden p-6">
|
||||
<!-- warm ambient blobs -->
|
||||
<span class="pointer-events-none absolute -left-20 top-0 h-72 w-72 rounded-full opacity-40 blur-3xl" style="background: var(--room-peach);" aria-hidden="true"></span>
|
||||
<span class="pointer-events-none absolute -right-16 bottom-0 h-72 w-72 rounded-full opacity-30 blur-3xl" style="background: var(--room-sky);" aria-hidden="true"></span>
|
||||
|
||||
<div class="relative z-10 w-full max-w-lg text-center">
|
||||
<span
|
||||
class="mx-auto mb-6 flex h-16 w-16 items-center justify-center rounded-[1.4rem] text-primary-foreground shadow-[var(--shadow-lift)]"
|
||||
style="background: linear-gradient(135deg, var(--room-peach), var(--room-terra));"
|
||||
>
|
||||
<svg class="h-8 w-8" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7" rx="2" /><rect x="14" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="2" /><rect x="3" y="14" width="7" height="7" rx="2" />
|
||||
</svg>
|
||||
</span>
|
||||
|
||||
{#if data.user}
|
||||
<p class="mt-4 text-muted-foreground">
|
||||
<h1 class="font-display text-4xl font-semibold text-foreground">
|
||||
{$t(greetKey, { default: 'Welcome back' })}, {data.user.displayName}
|
||||
<span class="cozy-wave">👋</span>
|
||||
</h1>
|
||||
<p class="mx-auto mt-3 max-w-sm text-base leading-relaxed text-muted-foreground">
|
||||
{$t('home.welcome', { values: { name: data.user.displayName } })}
|
||||
</p>
|
||||
<div class="mt-6 flex items-center justify-center gap-3">
|
||||
<div class="mt-8 flex items-center justify-center gap-3">
|
||||
<a
|
||||
href="/boards"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-2xl bg-primary px-5 py-3 text-sm font-semibold text-primary-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:shadow-[var(--shadow-lift)]"
|
||||
>
|
||||
{$t('home.view_boards')}
|
||||
</a>
|
||||
<a
|
||||
href="/apps"
|
||||
class="rounded-md border border-border px-4 py-2 text-sm font-medium text-foreground hover:bg-accent"
|
||||
class="rounded-2xl border border-border bg-card px-5 py-3 text-sm font-semibold text-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:border-primary/40"
|
||||
>
|
||||
{$t('home.browse_apps')}
|
||||
</a>
|
||||
</div>
|
||||
{:else}
|
||||
<h1 class="font-display text-4xl font-semibold text-foreground">{$t('app_title')}</h1>
|
||||
<div class="mt-8">
|
||||
<a
|
||||
href="/login"
|
||||
class="rounded-2xl bg-primary px-6 py-3 text-sm font-semibold text-primary-foreground shadow-[var(--shadow-soft)] transition-all hover:-translate-y-0.5 hover:shadow-[var(--shadow-lift)]"
|
||||
>
|
||||
{$t('auth.login')}
|
||||
</a>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{#snippet actions()}
|
||||
<a
|
||||
href="/"
|
||||
class="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
|
||||
>
|
||||
{$t('error.back_to_dashboard')}
|
||||
</a>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div class="mx-auto max-w-6xl">
|
||||
<!-- Admin header. On md+: single-row, nav scrolls horizontally if needed.
|
||||
Below md: stacks so username doesn't squeeze the nav onto two lines. -->
|
||||
<div class="mb-6 rounded-xl border border-border bg-card p-4 shadow-sm">
|
||||
<div class="mb-6 rounded-xl border border-border bg-card p-4 shadow-[var(--shadow-soft)]">
|
||||
<div class="flex flex-col gap-3 md:flex-row md:items-center md:gap-4">
|
||||
<span class="text-sm font-semibold text-foreground">{$t('admin.panel')}</span>
|
||||
<div class="-mx-1 flex gap-1 overflow-x-auto px-1">
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
>
|
||||
{showCreateForm ? $t('common.cancel') : $t('admin.create_group')}
|
||||
</button>
|
||||
@@ -46,7 +46,7 @@
|
||||
name="name"
|
||||
type="text"
|
||||
bind:value={$form.name}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
{#if $errors.name}<span class="text-xs text-destructive">{$errors.name}</span>{/if}
|
||||
@@ -58,7 +58,7 @@
|
||||
name="description"
|
||||
type="text"
|
||||
bind:value={$form.description}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
@@ -77,7 +77,7 @@
|
||||
{/if}
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{$t('admin.create_group')}
|
||||
</button>
|
||||
|
||||
@@ -98,7 +98,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showForm = true)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Generate invite
|
||||
</button>
|
||||
@@ -112,21 +112,21 @@
|
||||
{/if}
|
||||
|
||||
{#if createdUrl}
|
||||
<div class="rounded-lg border border-yellow-500/50 bg-yellow-500/10 p-4">
|
||||
<div class="rounded-lg border border-status-degraded/50 bg-status-degraded/10 p-4">
|
||||
<h3 class="mb-1 text-sm font-semibold text-foreground">Invite created</h3>
|
||||
<p class="mb-3 text-xs text-muted-foreground">
|
||||
Share this link with the recipient. It will only be shown once.
|
||||
</p>
|
||||
<div class="flex items-center gap-2">
|
||||
<code
|
||||
class="flex-1 truncate rounded-md border border-input bg-background px-3 py-2 text-xs font-mono text-foreground"
|
||||
class="flex-1 truncate rounded-xl border border-input bg-background px-3 py-2 text-xs font-mono text-foreground"
|
||||
>
|
||||
{createdUrl}
|
||||
</code>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => createdUrl && copyUrl(createdUrl)}
|
||||
class="rounded-md bg-primary px-3 py-2 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-3 py-2 text-xs font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
@@ -155,7 +155,7 @@
|
||||
type="email"
|
||||
bind:value={email}
|
||||
placeholder="jane@example.com"
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
@@ -164,7 +164,7 @@
|
||||
<select
|
||||
id="inv-role"
|
||||
bind:value={role}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="user">User</option>
|
||||
<option value="admin">Admin</option>
|
||||
@@ -180,7 +180,7 @@
|
||||
min="1"
|
||||
max="90"
|
||||
bind:value={expiresInDays}
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -195,7 +195,7 @@
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{creating ? 'Creating…' : 'Create invite'}
|
||||
</button>
|
||||
@@ -226,7 +226,7 @@
|
||||
<td class="px-4 py-3">
|
||||
<span
|
||||
class="rounded-full px-2 py-0.5 text-xs {status === 'Active'
|
||||
? 'bg-emerald-500/15 text-emerald-500'
|
||||
? 'bg-status-online/15 text-status-online-ink'
|
||||
: status === 'Used'
|
||||
? 'bg-muted text-muted-foreground'
|
||||
: 'bg-destructive/15 text-destructive'}"
|
||||
|
||||
@@ -102,13 +102,13 @@
|
||||
required
|
||||
bind:value={email}
|
||||
placeholder="user@example.com"
|
||||
class="w-full rounded-lg border border-input bg-background px-3 py-2 text-sm"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={issuing}
|
||||
class="w-full rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 sm:w-auto"
|
||||
class="w-full rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50 sm:w-auto"
|
||||
>
|
||||
{issuing ? 'Issuing…' : 'Issue link'}
|
||||
</button>
|
||||
@@ -123,13 +123,13 @@
|
||||
type="text"
|
||||
readonly
|
||||
value={issuedLink.url}
|
||||
class="flex-1 rounded-md border border-input bg-background px-2 py-1.5 font-mono text-xs"
|
||||
class="flex-1 rounded-xl border border-input bg-background px-2 py-1.5 font-mono text-xs"
|
||||
onclick={(e) => (e.currentTarget as HTMLInputElement).select()}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => issuedLink && copyLink(issuedLink.url)}
|
||||
class="rounded-md border border-input bg-background px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
class="rounded-xl border border-input bg-background px-3 py-1.5 text-xs font-medium hover:bg-accent"
|
||||
>
|
||||
{copyToast ? 'Copied!' : 'Copy'}
|
||||
</button>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
<button
|
||||
type="button"
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus:ring-2 focus:ring-ring"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 focus:outline-none focus-visible:ring-2 focus-visible:ring-primary/30"
|
||||
>
|
||||
{showCreateForm ? $t('common.cancel') : $t('admin.create_user')}
|
||||
</button>
|
||||
@@ -46,7 +46,7 @@
|
||||
name="email"
|
||||
type="email"
|
||||
bind:value={$form.email}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
{#if $errors.email}<span class="text-xs text-destructive">{$errors.email}</span>{/if}
|
||||
@@ -58,7 +58,7 @@
|
||||
name="displayName"
|
||||
type="text"
|
||||
bind:value={$form.displayName}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
required
|
||||
/>
|
||||
{#if $errors.displayName}<span class="text-xs text-destructive">{$errors.displayName}</span>{/if}
|
||||
@@ -70,7 +70,7 @@
|
||||
name="password"
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
/>
|
||||
{#if $errors.password}<span class="text-xs text-destructive">{$errors.password}</span>{/if}
|
||||
</div>
|
||||
@@ -80,7 +80,7 @@
|
||||
id="role"
|
||||
name="role"
|
||||
bind:value={$form.role}
|
||||
class="w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
class="w-full rounded-xl border border-input bg-background px-3 py-2 text-sm text-foreground"
|
||||
>
|
||||
<option value="user">{$t('admin.role_user')}</option>
|
||||
<option value="admin">{$t('admin.role_admin')}</option>
|
||||
@@ -92,7 +92,7 @@
|
||||
{/if}
|
||||
<button
|
||||
type="submit"
|
||||
class="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
class="rounded-xl bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
{$t('admin.create_user')}
|
||||
</button>
|
||||
|
||||
@@ -167,7 +167,7 @@ export const POST: RequestHandler = async (event) => {
|
||||
create: {
|
||||
id: DEFAULTS.SYSTEM_SETTINGS_ID,
|
||||
defaultTheme: themeData.data.defaultTheme ?? 'dark',
|
||||
defaultPrimaryColor: themeData.data.defaultPrimaryColor ?? '#6366f1'
|
||||
defaultPrimaryColor: themeData.data.defaultPrimaryColor ?? '#e8754f'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user