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:
2026-05-27 23:04:09 +03:00
parent f1cfb61d13
commit 5dcadd1c20
121 changed files with 4922 additions and 590 deletions
+789
View File
@@ -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>
+915
View File
@@ -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>
+643
View File
@@ -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>
+723
View File
@@ -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>“Everythings 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">Its 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 &amp; 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>
+639
View File
@@ -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 &amp; 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 &amp; 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 cant 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 itll 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>
+160
View File
@@ -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
View File
@@ -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
View File
@@ -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 () {
+12 -12
View File
@@ -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>
+13 -13
View File
@@ -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}
+1 -1
View File
@@ -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>
+13 -13
View File
@@ -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')}
+7 -7
View File
@@ -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>
+1 -1
View File
@@ -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>
+27 -11
View File
@@ -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>
+11 -11
View File
@@ -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')}
+13 -7
View File
@@ -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>
+1 -1
View File
@@ -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>
+5 -5
View File
@@ -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>
+2 -2
View File
@@ -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">
+4 -4
View File
@@ -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>
+31 -10
View File
@@ -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" />
+14 -9
View File
@@ -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>
+1 -1
View File
@@ -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)}
>
+14 -12
View File
@@ -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'}
+74 -77
View File
@@ -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
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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"
+5 -5
View File
@@ -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'}"
+2 -2
View File
@@ -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'}
+1 -1
View File
@@ -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}
+2 -2
View File
@@ -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'}
+2 -2
View File
@@ -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"
+8 -8
View File
@@ -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>
+1 -1
View File
@@ -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}
+1 -1
View File
@@ -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>
+13 -13
View File
@@ -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}
+5
View File
@@ -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",
+5
View File
@@ -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
View File
@@ -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)');
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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
View File
@@ -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>
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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">
+4 -4
View File
@@ -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>
+9 -9
View File
@@ -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>
+6 -6
View File
@@ -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>
+1 -1
View File
@@ -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