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 *)); @custom-variant dark (&:is(.dark *));
:root { /* =====================================================================
/* HSL-based primary color (overridden by theme store via JS) */ COZY HOME design system
--primary-h: 220; ---------------------------------------------------------------------
--primary-s: 70%; Tokens are intentionally organised as a single swappable "bundle":
--primary-l: 50%; 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%); :root {
--foreground: hsl(240 10% 3.9%); /* Accent — terracotta by default, still user-tunable from settings */
--muted: hsl(240 4.8% 95.9%); --primary-h: 16;
--muted-foreground: hsl(240 3.8% 46.1%); --primary-s: 72%;
--popover: hsl(0 0% 100%); --primary-l: 56%;
--popover-foreground: hsl(240 10% 3.9%);
--card: hsl(0 0% 100%); /* Neutrals — warm cream "paper" ramp */
--card-foreground: hsl(240 10% 3.9%); --background: hsl(35 56% 97%); /* #fdf8f2 warm cream */
--border: hsl(240 5.9% 90%); --foreground: hsl(33 18% 18%); /* #3a322b warm ink */
--input: hsl(240 5.9% 90%); --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: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
--primary-foreground: hsl(0 0% 98%); --primary-foreground: hsl(40 60% 99%);
--secondary: hsl(240 4.8% 95.9%); --secondary: hsl(36 42% 93%);
--secondary-foreground: hsl(240 5.9% 10%); --secondary-foreground: hsl(33 18% 22%);
--accent: hsl(240 4.8% 95.9%); --accent: hsl(34 44% 90%); /* hover wash */
--accent-foreground: hsl(240 5.9% 10%); --accent-foreground: hsl(33 18% 20%);
--destructive: hsl(0 72.2% 50.6%); --destructive: hsl(6 68% 56%);
--destructive-foreground: hsl(0 0% 98%); --destructive-foreground: hsl(40 60% 99%);
--ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
--status-online: #22c55e;
--status-offline: #ef4444; /* Status — vivid values for dots / bars / rings / sparklines */
--status-degraded: #eab308; --status-online: #5fa86c;
--status-unknown: #6b7280; --status-offline: #e0685f;
--radius: 0.5rem; --status-degraded: #d99a2b;
--sidebar: hsl(0 0% 98%); --status-unknown: #b3a899;
--sidebar-foreground: hsl(240 5.3% 26.1%); /* 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: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
--sidebar-primary-foreground: hsl(0 0% 98%); --sidebar-primary-foreground: hsl(40 60% 99%);
--sidebar-accent: hsl(240 4.8% 95.9%); --sidebar-accent: hsl(34 44% 90%);
--sidebar-accent-foreground: hsl(240 5.9% 10%); --sidebar-accent-foreground: hsl(33 18% 20%);
--sidebar-border: hsl(220 13% 91%); --sidebar-border: hsl(36 35% 87%);
--sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%); --sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%);
} }
.dark { .dark {
--primary-l: 60%; /* "Dusk" — warm charcoal, not cold black */
--primary-l: 62%;
--background: hsl(240 10% 3.9%); --background: hsl(30 14% 9%); /* #1a1714 */
--foreground: hsl(0 0% 98%); --foreground: hsl(35 30% 90%); /* #f0e9df */
--muted: hsl(240 3.7% 15.9%); --muted: hsl(30 14% 16%); /* #2b2520 */
--muted-foreground: hsl(240 5% 64.9%); --muted-foreground: hsl(35 14% 64%); /* #b3a899 */
--popover: hsl(240 10% 3.9%); --popover: hsl(30 16% 12%);
--popover-foreground: hsl(0 0% 98%); --popover-foreground: hsl(35 30% 90%);
--card: hsl(240 6% 7%); --card: hsl(30 16% 13%); /* #262019 */
--card-foreground: hsl(0 0% 98%); --card-foreground: hsl(35 30% 90%);
--border: hsl(240 3.7% 15.9%); --border: hsl(31 16% 19%); /* #352d24 */
--input: hsl(240 3.7% 15.9%); --input: hsl(31 16% 19%);
--primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --primary: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
--primary-foreground: hsl(240 5.9% 10%); --primary-foreground: hsl(30 18% 10%);
--secondary: hsl(240 3.7% 15.9%); --secondary: hsl(30 14% 16%);
--secondary-foreground: hsl(0 0% 98%); --secondary-foreground: hsl(35 30% 90%);
--accent: hsl(240 3.7% 15.9%); --accent: hsl(30 14% 18%);
--accent-foreground: hsl(0 0% 98%); --accent-foreground: hsl(35 30% 90%);
--destructive: hsl(0 62.8% 30.6%); --destructive: hsl(6 58% 46%);
--destructive-foreground: hsl(0 0% 98%); --destructive-foreground: hsl(40 60% 99%);
--ring: hsl(var(--primary-h) var(--primary-s) var(--primary-l)); --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: hsl(var(--primary-h) var(--primary-s) var(--primary-l));
--sidebar-primary-foreground: hsl(0 0% 100%); --sidebar-primary-foreground: hsl(30 18% 10%);
--sidebar-accent: hsl(240 3.7% 15.9%); --sidebar-accent: hsl(30 14% 18%);
--sidebar-accent-foreground: hsl(240 4.8% 95.9%); --sidebar-accent-foreground: hsl(35 30% 90%);
--sidebar-border: hsl(240 3.7% 15.9%); --sidebar-border: hsl(31 16% 19%);
--sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%); --sidebar-ring: hsl(var(--primary-h) calc(var(--primary-s) * 1.2) 60%);
} }
@theme inline { @theme inline {
--radius-sm: calc(var(--radius) - 4px); --radius-sm: calc(var(--radius) - 8px);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 4px);
--radius-lg: var(--radius); --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-background: var(--background);
--color-foreground: var(--foreground); --color-foreground: var(--foreground);
@@ -101,6 +158,23 @@
--color-destructive: var(--destructive); --color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground); --color-destructive-foreground: var(--destructive-foreground);
--color-ring: var(--ring); --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: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground); --color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary); --color-sidebar-primary: var(--sidebar-primary);
@@ -117,10 +191,21 @@
} }
body { body {
@apply bg-background text-foreground; @apply bg-background text-foreground;
font-family: var(--font-sans);
font-feature-settings: 'ss01', 'cv01';
transition: transition:
background-color 0.3s ease, background-color 0.3s ease,
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 ===== */ /* ===== Status Indicator Pulse ===== */
@@ -138,27 +223,27 @@
.status-online { .status-online {
animation: status-pulse 2s ease-in-out infinite; animation: status-pulse 2s ease-in-out infinite;
color: hsl(142 71% 45%); color: var(--status-online);
} }
/* ===== Card Style Variants ===== */ /* ===== Card Style Variants ===== */
.card-solid { .card-solid {
background: var(--card); background: var(--card);
border: 1px solid var(--border); border: 1px solid var(--border);
box-shadow: var(--shadow-soft);
} }
.card-glass { .card-glass {
backdrop-filter: blur(12px); backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
background: color-mix(in srgb, var(--card) 60%, transparent); background: color-mix(in srgb, var(--card) 70%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 30%, transparent); border: 1px solid color-mix(in srgb, var(--border) 50%, transparent);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.05); box-shadow: var(--shadow-soft);
} }
.dark .card-glass { .dark .card-glass {
background: color-mix(in srgb, var(--card) 50%, transparent); background: color-mix(in srgb, var(--card) 55%, transparent);
border: 1px solid color-mix(in srgb, var(--border) 25%, transparent); border: 1px solid color-mix(in srgb, var(--border) 35%, transparent);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.15);
} }
.card-outline { .card-outline {
@@ -170,24 +255,17 @@
border-color: var(--border); border-color: var(--border);
} }
/* ===== Card Hover Effects ===== */ /* ===== Card Hover Effects — gentle cozy lift + micro-tilt ===== */
.card-hover { .card-hover {
transition: transition:
transform 0.2s ease, transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
box-shadow 0.2s ease; box-shadow 0.2s ease,
border-color 0.2s ease;
} }
.card-hover:hover { .card-hover:hover {
transform: scale(1.02); transform: translateY(-5px) rotate(-0.35deg);
box-shadow: box-shadow: var(--shadow-lift);
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);
} }
/* ===== Skeleton Loading ===== */ /* ===== Skeleton Loading ===== */
@@ -201,14 +279,14 @@
} }
.skeleton { .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%; background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite; animation: shimmer 1.5s ease-in-out infinite;
border-radius: var(--radius); border-radius: var(--radius);
} }
.dark .skeleton { .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%; background-size: 200% 100%;
} }
@@ -236,7 +314,7 @@
[data-keyboard-selected='true'] { [data-keyboard-selected='true'] {
outline: 2px solid hsl(var(--primary-h) var(--primary-s) var(--primary-l)); outline: 2px solid hsl(var(--primary-h) var(--primary-s) var(--primary-l));
outline-offset: 2px; outline-offset: 2px;
border-radius: var(--radius, 0.5rem); border-radius: var(--radius, 1rem);
} }
/* ===== Aurora Keyframes ===== */ /* ===== Aurora Keyframes ===== */
@@ -251,3 +329,40 @@
background-position: 0% 50%; 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" /> <link rel="icon" href="%sveltekit.assets%/icon.svg" type="image/svg+xml" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="%sveltekit.assets%/manifest.json" /> <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-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Launcher" /> <meta name="apple-mobile-web-app-title" content="Launcher" />
<link rel="apple-touch-icon" href="%sveltekit.assets%/icon.svg" /> <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> <script>
// Inline script to prevent FOUC — set theme class before first paint // Inline script to prevent FOUC — set theme class before first paint
(function () { (function () {
+12 -12
View File
@@ -96,11 +96,11 @@
} }
function actionBadgeClass(action: string): string { function actionBadgeClass(action: string): string {
if (action.includes('deleted')) return 'bg-red-500/10 text-red-500'; if (action.includes('deleted')) return 'bg-destructive/10 text-destructive';
if (action.includes('created')) return 'bg-green-500/10 text-green-500'; if (action.includes('created')) return 'bg-status-online/10 text-status-online-ink';
if (action.includes('updated')) return 'bg-blue-500/10 text-blue-500'; if (action.includes('updated')) return 'bg-room-sky/15 text-room-sky';
if (action === 'import') return 'bg-purple-500/10 text-purple-500'; if (action === 'import') return 'bg-room-lav/15 text-room-lav';
if (action === 'export') return 'bg-yellow-500/10 text-yellow-500'; if (action === 'export') return 'bg-status-degraded/10 text-status-degraded-ink';
return 'bg-muted text-muted-foreground'; return 'bg-muted text-muted-foreground';
} }
@@ -138,7 +138,7 @@
<select <select
id="filter-action" id="filter-action"
bind:value={filterAction} 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)} {#each actionOptions as opt (opt.value)}
<option value={opt.value}>{opt.label}</option> <option value={opt.value}>{opt.label}</option>
@@ -151,7 +151,7 @@
<select <select
id="filter-entity" id="filter-entity"
bind:value={filterEntityType} 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)} {#each entityTypeOptions as opt (opt.value)}
<option value={opt.value}>{opt.label}</option> <option value={opt.value}>{opt.label}</option>
@@ -165,7 +165,7 @@
id="filter-from" id="filter-from"
type="date" type="date"
bind:value={filterDateFrom} 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> </div>
@@ -175,14 +175,14 @@
id="filter-to" id="filter-to"
type="date" type="date"
bind:value={filterDateTo} 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> </div>
<button <button
type="button" type="button"
onclick={applyFilters} 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 Apply
</button> </button>
@@ -190,7 +190,7 @@
<button <button
type="button" type="button"
onclick={exportCsv} 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 Export CSV
</button> </button>
@@ -202,7 +202,7 @@
<p class="text-muted-foreground">No audit log entries found</p> <p class="text-muted-foreground">No audit log entries found</p>
</div> </div>
{:else} {: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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
+13 -13
View File
@@ -211,7 +211,7 @@
type="button" type="button"
onclick={handleCreate} onclick={handleCreate}
disabled={creating} 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')} {creating ? $t('admin.backup_creating') : $t('admin.backup_create')}
</button> </button>
@@ -253,7 +253,7 @@
type="button" type="button"
onclick={() => (confirmRestore = backup.filename)} onclick={() => (confirmRestore = backup.filename)}
disabled={restoringFilename === 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 {restoringFilename === backup.filename
? '...' ? '...'
@@ -282,7 +282,7 @@
<!-- Restore Confirmation Dialog --> <!-- Restore Confirmation Dialog -->
{#if confirmRestore} {#if confirmRestore}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <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"> <h3 class="mb-2 text-lg font-semibold text-card-foreground">
{$t('admin.backup_restore_confirm_title')} {$t('admin.backup_restore_confirm_title')}
</h3> </h3>
@@ -301,7 +301,7 @@
<button <button
type="button" type="button"
onclick={() => confirmRestore && handleRestore(confirmRestore)} 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')} {$t('admin.backup_restore')}
</button> </button>
@@ -313,7 +313,7 @@
<!-- Delete Confirmation Dialog --> <!-- Delete Confirmation Dialog -->
{#if confirmDelete} {#if confirmDelete}
<div class="fixed inset-0 z-50 flex items-center justify-center bg-black/50"> <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"> <h3 class="mb-2 text-lg font-semibold text-card-foreground">
{$t('admin.backup_delete_confirm_title')} {$t('admin.backup_delete_confirm_title')}
</h3> </h3>
@@ -354,7 +354,7 @@
<input <input
type="checkbox" type="checkbox"
bind:checked={schedule.backupEnabled} 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> <span class="text-sm text-foreground">{$t('admin.backup_schedule_enabled')}</span>
</label> </label>
@@ -368,7 +368,7 @@
<select <select
id="cron-preset" id="cron-preset"
bind:value={cronPreset} 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="daily">{$t('admin.backup_schedule_preset_daily')}</option>
<option value="twice_daily">{$t('admin.backup_schedule_preset_twice_daily')}</option> <option value="twice_daily">{$t('admin.backup_schedule_preset_twice_daily')}</option>
@@ -383,7 +383,7 @@
type="text" type="text"
bind:value={customCron} bind:value={customCron}
placeholder="0 3 * * *" 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> </div>
{/if} {/if}
@@ -399,7 +399,7 @@
bind:value={schedule.backupMaxCount} bind:value={schedule.backupMaxCount}
min="1" min="1"
max="100" 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> </div>
{/if} {/if}
@@ -408,7 +408,7 @@
type="button" type="button"
onclick={handleSaveSchedule} onclick={handleSaveSchedule}
disabled={savingSchedule} 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')} {savingSchedule ? $t('admin.backup_schedule_saving') : $t('admin.backup_schedule_save')}
</button> </button>
@@ -418,9 +418,9 @@
<!-- Status message --> <!-- Status message -->
{#if statusMessage} {#if statusMessage}
<div <div
class="mt-4 rounded-md p-3 text-sm {statusType === 'success' class="mt-4 rounded-xl border p-3 text-sm {statusType === 'success'
? 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400' ? 'border-status-online/30 bg-status-online/10 text-status-online-ink'
: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400'}" : 'border-destructive/30 bg-destructive/10 text-destructive'}"
> >
{statusMessage} {statusMessage}
</div> </div>
@@ -147,7 +147,7 @@
type="button" type="button"
onclick={handleScan} onclick={handleScan}
disabled={scanning || (!dockerSocketPath && !traefikApiUrl)} 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')} {scanning ? $t('admin.discovery_scanning') : $t('admin.discovery_scan')}
</button> </button>
@@ -155,7 +155,7 @@
<!-- Scan Errors --> <!-- Scan Errors -->
{#if scanErrors.length > 0} {#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)} {#each scanErrors as scanError, idx (idx)}
<p>{scanError}</p> <p>{scanError}</p>
{/each} {/each}
@@ -204,8 +204,8 @@
<td class="px-2 py-2"> <td class="px-2 py-2">
<span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium <span class="inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium
{service.source === 'docker' {service.source === 'docker'
? 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400' ? 'bg-room-sky/15 text-room-sky'
: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400' : 'bg-room-lav/15 text-room-lav'
}" }"
> >
{service.source === 'docker' ? $t('admin.discovery_source_docker') : $t('admin.discovery_source_traefik')} {service.source === 'docker' ? $t('admin.discovery_source_docker') : $t('admin.discovery_source_traefik')}
@@ -215,7 +215,7 @@
{#if service.alreadyRegistered} {#if service.alreadyRegistered}
<span class="text-xs text-muted-foreground">{$t('admin.discovery_already_registered')}</span> <span class="text-xs text-muted-foreground">{$t('admin.discovery_already_registered')}</span>
{:else} {: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} {/if}
</td> </td>
</tr> </tr>
@@ -231,7 +231,7 @@
type="button" type="button"
onclick={handleApprove} onclick={handleApprove}
disabled={approving || selected.size === 0} 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}) {approving ? $t('admin.discovery_approving') : $t('admin.discovery_approve')} ({selected.size})
</button> </button>
@@ -241,7 +241,7 @@
<!-- Status Message --> <!-- Status Message -->
{#if statusMessage} {#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} {statusMessage}
</div> </div>
{/if} {/if}
+1 -1
View File
@@ -27,7 +27,7 @@
} }
</script> </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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
@@ -198,7 +198,7 @@
<!-- Existing permissions list --> <!-- Existing permissions list -->
{#if permissions.length > 0} {#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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
+13 -13
View File
@@ -55,7 +55,7 @@
id="authMode" id="authMode"
name="authMode" name="authMode"
bind:value={$form.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="local">{$t('admin.auth_local')}</option>
<option value="oauth">{$t('admin.auth_oauth')}</option> <option value="oauth">{$t('admin.auth_oauth')}</option>
@@ -92,7 +92,7 @@
name="oauthClientId" name="oauthClientId"
type="text" type="text"
bind:value={$form.oauthClientId} 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')} placeholder={$t('admin.oauth_client_id_placeholder')}
/> />
</div> </div>
@@ -103,7 +103,7 @@
name="oauthClientSecret" name="oauthClientSecret"
type="password" type="password"
bind:value={$form.oauthClientSecret} 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')} placeholder={$t('admin.oauth_client_secret_placeholder')}
/> />
</div> </div>
@@ -114,7 +114,7 @@
name="oauthDiscoveryUrl" name="oauthDiscoveryUrl"
type="url" type="url"
bind:value={$form.oauthDiscoveryUrl} 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')} placeholder={$t('admin.oauth_discovery_url_placeholder')}
/> />
{#if $errors.oauthDiscoveryUrl}<span class="text-xs text-destructive">{$errors.oauthDiscoveryUrl}</span>{/if} {#if $errors.oauthDiscoveryUrl}<span class="text-xs text-destructive">{$errors.oauthDiscoveryUrl}</span>{/if}
@@ -124,12 +124,12 @@
type="button" type="button"
onclick={testOAuthConnection} onclick={testOAuthConnection}
disabled={oauthTesting} 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')} {oauthTesting ? $t('admin.oauth_testing') : $t('admin.oauth_test')}
</button> </button>
{#if oauthTestResult} {#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} {oauthTestResult}
</span> </span>
{/if} {/if}
@@ -147,7 +147,7 @@
id="defaultTheme" id="defaultTheme"
name="defaultTheme" name="defaultTheme"
bind:value={$form.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="dark">{$t('theme.dark')}</option>
<option value="light">{$t('theme.light')}</option> <option value="light">{$t('theme.light')}</option>
@@ -161,8 +161,8 @@
name="defaultPrimaryColor" name="defaultPrimaryColor"
type="text" type="text"
bind:value={$form.defaultPrimaryColor} bind:value={$form.defaultPrimaryColor}
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="#6366f1" placeholder="#e8754f"
pattern="^#[0-9a-fA-F]{6}$" pattern="^#[0-9a-fA-F]{6}$"
/> />
{#if $form.defaultPrimaryColor} {#if $form.defaultPrimaryColor}
@@ -188,7 +188,7 @@
name="healthcheckDefaults" name="healthcheckDefaults"
bind:value={$form.healthcheckDefaults} bind:value={$form.healthcheckDefaults}
rows="4" 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"}'} placeholder={'{"interval": 300, "timeout": 5000, "method": "GET"}'}
></textarea> ></textarea>
{#if $errors.healthcheckDefaults}<span class="text-xs text-destructive">{$errors.healthcheckDefaults}</span>{/if} {#if $errors.healthcheckDefaults}<span class="text-xs text-destructive">{$errors.healthcheckDefaults}</span>{/if}
@@ -206,7 +206,7 @@
id="dockerSocketPath" id="dockerSocketPath"
type="text" type="text"
bind:value={dockerSocketPath} 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" placeholder="/var/run/docker.sock"
/> />
<p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_docker_socket_hint')}</p> <p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_docker_socket_hint')}</p>
@@ -217,7 +217,7 @@
id="traefikApiUrl" id="traefikApiUrl"
type="url" type="url"
bind:value={traefikApiUrl} 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" placeholder="http://traefik:8080"
/> />
<p class="mt-1 text-xs text-muted-foreground">{$t('admin.discovery_traefik_url_hint')}</p> <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"> <div class="flex items-center gap-3">
<button <button
type="submit" 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} disabled={$delayed}
> >
{$delayed ? $t('admin.saving_settings') : $t('admin.save_settings')} {$delayed ? $t('admin.saving_settings') : $t('admin.save_settings')}
+7 -7
View File
@@ -14,13 +14,13 @@
// Create form // Create form
let newName = $state(''); let newName = $state('');
let newColor = $state('#6366f1'); let newColor = $state('#e8754f');
let showCreateForm = $state(false); let showCreateForm = $state(false);
// Edit form // Edit form
let editingTag = $state<Tag | null>(null); let editingTag = $state<Tag | null>(null);
let editName = $state(''); let editName = $state('');
let editColor = $state('#6366f1'); let editColor = $state('#e8754f');
// Delete confirmation // Delete confirmation
let confirmDeleteId = $state<string | null>(null); let confirmDeleteId = $state<string | null>(null);
@@ -56,7 +56,7 @@
}); });
if (res.ok) { if (res.ok) {
newName = ''; newName = '';
newColor = '#6366f1'; newColor = '#e8754f';
showCreateForm = false; showCreateForm = false;
await loadTags(); await loadTags();
} else { } else {
@@ -71,7 +71,7 @@
function startEdit(tag: Tag) { function startEdit(tag: Tag) {
editingTag = tag; editingTag = tag;
editName = tag.name; editName = tag.name;
editColor = tag.color ?? '#6366f1'; editColor = tag.color ?? '#e8754f';
} }
async function saveEdit() { async function saveEdit() {
@@ -118,7 +118,7 @@
<button <button
type="button" type="button"
onclick={() => (showCreateForm = !showCreateForm)} 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'} {showCreateForm ? 'Cancel' : 'New Tag'}
</button> </button>
@@ -141,7 +141,7 @@
type="text" type="text"
bind:value={newName} bind:value={newName}
placeholder="Tag name" 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 required
/> />
</div> </div>
@@ -159,7 +159,7 @@
</div> </div>
<button <button
type="submit" 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 Create Tag
</button> </button>
+1 -1
View File
@@ -37,7 +37,7 @@
let selectedGroupId = $state(''); let selectedGroupId = $state('');
</script> </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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
@@ -126,4 +126,13 @@
.status-ring-unknown { .status-ring-unknown {
animation: ring-rotate-dash 8s linear infinite; 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> </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(() => { const iconDisplay = $derived.by(() => {
if (!app.icon) return null; if (!app.icon) return null;
@@ -82,32 +90,39 @@
tabindex="0" tabindex="0"
onclick={() => window.open(app.url, '_blank', 'noopener,noreferrer')} onclick={() => window.open(app.url, '_blank', 'noopener,noreferrer')}
onkeydown={(e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') 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} 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="mb-3 flex items-start justify-between">
<div <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'} {#if iconDisplay?.kind === 'emoji'}
<span class="text-xl">{iconDisplay.value}</span> <span class="text-2xl">{iconDisplay.value}</span>
{:else if iconDisplay?.kind === 'image'} {:else if iconDisplay?.kind === 'image'}
<img <img
src={iconDisplay.src} src={iconDisplay.src}
alt="{app.name} icon" 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'} {:else if iconDisplay?.kind === 'text'}
<span class="text-xs font-medium">{iconDisplay.value}</span> <span class="text-sm font-bold">{iconDisplay.value}</span>
{:else} {: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} {/if}
</div> </div>
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<a <a
href="/apps/{app.id}/edit" href="/apps/{app.id}/edit"
onclick={(e: MouseEvent) => e.stopPropagation()} 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')} title={$t('app.edit')}
> >
<svg <svg
@@ -128,12 +143,12 @@
</div> </div>
</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} {app.name}
</h3> </h3>
{#if app.description} {#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} {/if}
<!-- Sparkline --> <!-- Sparkline -->
@@ -143,14 +158,15 @@
<div class="mt-2 flex items-center gap-1.5"> <div class="mt-2 flex items-center gap-1.5">
<SparklineChart data={historyData} /> <SparklineChart data={historyData} />
{#if uptimePercent !== null} {#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} {/if}
</div> </div>
{/if} {/if}
{#if app.category} {#if app.category}
<span <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} {app.category}
</span> </span>
+11 -11
View File
@@ -121,7 +121,7 @@
name="name" name="name"
type="text" type="text"
bind:value={$form.name} 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')} placeholder={$t('app.name_placeholder')}
/> />
{#if $errors.name} {#if $errors.name}
@@ -138,7 +138,7 @@
name="url" name="url"
type="url" type="url"
bind:value={$form.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')} placeholder={$t('app.url_placeholder')}
/> />
{#if $errors.url} {#if $errors.url}
@@ -170,7 +170,7 @@
name="description" name="description"
type="text" type="text"
bind:value={$form.description} 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')} placeholder={$t('app.description_placeholder')}
/> />
</div> </div>
@@ -186,7 +186,7 @@
bind:value={$form.category} bind:value={$form.category}
suggestions={categorySuggestions} suggestions={categorySuggestions}
placeholder={$t('app.category_placeholder')} 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> </div>
@@ -200,7 +200,7 @@
bind:value={$form.tags} bind:value={$form.tags}
suggestions={tagSuggestions} suggestions={tagSuggestions}
placeholder={$t('app.tags_placeholder')} 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>
</div> </div>
@@ -269,7 +269,7 @@
name="healthcheckExpectedStatus" name="healthcheckExpectedStatus"
type="number" type="number"
bind:value={$form.healthcheckExpectedStatus} 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" min="100"
max="599" max="599"
/> />
@@ -287,7 +287,7 @@
name="healthcheckTimeout" name="healthcheckTimeout"
type="number" type="number"
bind:value={$form.healthcheckTimeout} 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" min="1000"
max="30000" max="30000"
step="1000" step="1000"
@@ -307,7 +307,7 @@
name="healthcheckInterval" name="healthcheckInterval"
type="number" type="number"
bind:value={$form.healthcheckInterval} 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" min="30"
max="86400" max="86400"
/> />
@@ -349,7 +349,7 @@
id="integrationType" id="integrationType"
name="integrationType" name="integrationType"
bind:value={$form.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> <option value="">None</option>
{#each availableIntegrations as integration (integration.id)} {#each availableIntegrations as integration (integration.id)}
@@ -395,7 +395,7 @@
{testingConnection ? 'Testing...' : 'Test Connection'} {testingConnection ? 'Testing...' : 'Test Connection'}
</button> </button>
{#if testResult} {#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} {testResult.message}
</span> </span>
{/if} {/if}
@@ -412,7 +412,7 @@
<button <button
type="submit" type="submit"
disabled={$submitting} 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} {#if $submitting}
{$t('app.saving')} {$t('app.saving')}
+13 -7
View File
@@ -10,18 +10,24 @@
const config = $derived.by(() => { const config = $derived.by(() => {
switch (status) { switch (status) {
case 'online': 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': 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': 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: 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> </script>
<span class="inline-flex items-center gap-1.5 text-xs"> <span
<span class="inline-block h-2 w-2 rounded-full {config.color} {config.cssClass}"></span> class="inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-semibold"
<span class="text-muted-foreground">{$t(config.textKey)}</span> 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> </span>
+1 -1
View File
@@ -70,7 +70,7 @@
: iconType === 'url' : iconType === 'url'
? $t('app.icon_url_placeholder') ? $t('app.icon_url_placeholder')
: $t('app.icon_emoji_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} {/if}
</div> </div>
+5 -5
View File
@@ -164,13 +164,13 @@
type="text" type="text"
bind:value={newLabel} bind:value={newLabel}
placeholder="Link label" 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 <input
type="url" type="url"
bind:value={newUrl} bind:value={newUrl}
placeholder="https://..." 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>
<div class="mt-2 flex items-center gap-2"> <div class="mt-2 flex items-center gap-2">
@@ -178,13 +178,13 @@
type="text" type="text"
bind:value={newIcon} bind:value={newIcon}
placeholder="Icon (optional)" 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 <button
type="button" type="button"
onclick={addLink} onclick={addLink}
disabled={!newLabel.trim() || !newUrl.trim()} 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 Add
</button> </button>
@@ -196,7 +196,7 @@
type="button" type="button"
onclick={saveLinks} onclick={saveLinks}
disabled={saving} 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'} {saving ? 'Saving...' : 'Save Links'}
</button> </button>
+2 -2
View File
@@ -21,8 +21,8 @@
const statusColor = $derived(() => { const statusColor = $derived(() => {
if (!result) return ''; if (!result) return '';
if (result.error) return 'text-destructive'; if (result.error) return 'text-destructive';
if (result.status >= 200 && result.status < 300) return 'text-green-500'; if (result.status >= 200 && result.status < 300) return 'text-status-online-ink';
if (result.status >= 300 && result.status < 400) return 'text-yellow-500'; if (result.status >= 300 && result.status < 400) return 'text-status-degraded-ink';
return 'text-destructive'; return 'text-destructive';
}); });
@@ -10,7 +10,7 @@
let { fields, values, onchange, idPrefix = 'int' }: Props = $props(); 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> </script>
<div class="space-y-3"> <div class="space-y-3">
+4 -4
View File
@@ -13,10 +13,10 @@
let { data, width = 80, height = 20 }: Props = $props(); let { data, width = 80, height = 20 }: Props = $props();
const STATUS_COLORS: Record<string, string> = { const STATUS_COLORS: Record<string, string> = {
online: '#22c55e', online: 'var(--status-online)',
offline: '#ef4444', offline: 'var(--status-offline)',
degraded: '#eab308', degraded: 'var(--status-degraded)',
unknown: '#6b7280' unknown: 'var(--status-unknown)'
}; };
const barWidth = $derived(data.length > 0 ? Math.max(1, (width - 2) / data.length) : 1); const barWidth = $derived(data.length > 0 ? Math.max(1, (width - 2) / data.length) : 1);
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import { theme } from '$lib/stores/theme.svelte.js'; import { theme } from '$lib/stores/theme.svelte.js';
import CozyAmbient from './CozyAmbient.svelte';
import MeshGradient from './MeshGradient.svelte'; import MeshGradient from './MeshGradient.svelte';
import ParticleField from './ParticleField.svelte'; import ParticleField from './ParticleField.svelte';
import AuroraEffect from './AuroraEffect.svelte'; import AuroraEffect from './AuroraEffect.svelte';
@@ -16,7 +17,9 @@
{#if theme.backgroundType !== 'none'} {#if theme.backgroundType !== 'none'}
<div class="pointer-events-none fixed inset-0 z-0 overflow-hidden" aria-hidden="true"> <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 /> <MeshGradient />
{:else if theme.backgroundType === 'particles'} {:else if theme.backgroundType === 'particles'}
<ParticleField /> <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(() => { $effect(() => {
blobs = initBlobs(); 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); animFrame = requestAnimationFrame(animate);
return () => { return () => {
@@ -34,21 +34,21 @@
} }
</script> </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 <input
bind:this={inputEl} bind:this={inputEl}
type="text" type="text"
bind:value={title} bind:value={title}
onkeydown={handleKeydown} onkeydown={handleKeydown}
placeholder={$t('board.section_title') ?? 'Section title...'} 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" /> <IconPickerButton value={icon} onchange={(v) => { icon = v; }} size="sm" />
<button <button
type="button" type="button"
onclick={handleSubmit} onclick={handleSubmit}
disabled={!title.trim()} 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'} {$t('common.add') ?? 'Add'}
</button> </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" 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} {#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)} {#each filteredTargetOptions as option (option.id)}
<button <button
type="button" type="button"
@@ -190,7 +190,7 @@
{#if loading} {#if loading}
<p class="text-sm text-muted-foreground">{$t('board.access_loading')}</p> <p class="text-sm text-muted-foreground">{$t('board.access_loading')}</p>
{:else if permissions.length > 0} {: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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
+31 -10
View File
@@ -20,32 +20,53 @@
let { board }: Props = $props(); let { board }: Props = $props();
const sectionCount = $derived(board._count?.sections ?? 0); 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> </script>
<a <a
href="/boards/{board.id}" 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} {#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} {:else}
<span class="flex h-8 w-8 items-center justify-center rounded-md bg-muted text-sm text-muted-foreground"> <span
B 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> </span>
{/if} {/if}
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2"> <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} {board.name}
</h3> </h3>
{#if board.isDefault} {#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')} {$t('board.default')}
</span> </span>
{/if} {/if}
{#if board.isGuestAccessible} {#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"> <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" /> <circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" /> <line x1="2" y1="12" x2="22" y2="12" />
@@ -54,7 +75,7 @@
{$t('board.guest')} {$t('board.guest')}
</span> </span>
{:else} {: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"> <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" /> <rect x="3" y="11" width="18" height="11" rx="2" ry="2" />
<path d="M7 11V7a5 5 0 0 1 10 0v4" /> <path d="M7 11V7a5 5 0 0 1 10 0v4" />
@@ -62,7 +83,7 @@
</span> </span>
{/if} {/if}
{#if board.hasSharedPermissions} {#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"> <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" /> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" /> <circle cx="9" cy="7" r="4" />
+14 -9
View File
@@ -29,13 +29,18 @@
} }
</script> </script>
<div class="mb-6 flex items-start justify-between"> <div class="mb-6 flex items-start justify-between gap-4">
<div class="flex items-center gap-3"> <div class="flex items-center gap-3.5">
{#if icon} {#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} {/if}
<div> <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} {#if description}
<p class="mt-1 text-sm text-muted-foreground">{description}</p> <p class="mt-1 text-sm text-muted-foreground">{description}</p>
{/if} {/if}
@@ -45,7 +50,7 @@
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<a <a
href="/boards" 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')} {$t('board.all_boards')}
</a> </a>
@@ -53,7 +58,7 @@
<button <button
type="button" type="button"
onclick={onShare} 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"> <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" /> <circle cx="18" cy="5" r="3" />
@@ -69,9 +74,9 @@
<button <button
type="button" type="button"
onclick={handleEditToggle} onclick={handleEditToggle}
class="flex items-center gap-1.5 rounded-lg px-3 py-2 text-sm font-medium transition-colors {editMode.active 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 text-primary-foreground ring-2 ring-primary/30' ? 'bg-primary ring-2 ring-primary/30'
: 'bg-primary text-primary-foreground hover:bg-primary/90'}" : 'bg-primary hover:shadow-[var(--shadow-lift)]'}"
> >
{#if editMode.active} {#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"> <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 --> <!-- Side panel -->
<div <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 }} transition:fly={{ x: 400, duration: 250 }}
> >
<!-- Header --> <!-- Header -->
@@ -107,7 +107,7 @@
<div> <div>
<label for="bp-name" class="mb-1 block text-sm font-medium text-foreground">{$t('common.name') ?? 'Name'}</label> <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} <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> </div>
<!-- Icon --> <!-- Icon -->
@@ -121,7 +121,7 @@
<div> <div>
<label for="bp-desc" class="mb-1 block text-sm font-medium text-foreground">{$t('common.description') ?? 'Description'}</label> <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} <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> </div>
<!-- Theme Hue --> <!-- Theme Hue -->
@@ -144,7 +144,7 @@
<div> <div>
<label for="bp-bg" class="mb-1 block text-sm font-medium text-foreground">{$t('board.background') ?? 'Background'}</label> <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} <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="none">None</option>
<option value="mesh">Mesh Gradient</option> <option value="mesh">Mesh Gradient</option>
<option value="particles">Particles</option> <option value="particles">Particles</option>
@@ -159,7 +159,7 @@
<div> <div>
<label for="bp-wp-url" class="mb-1 block text-sm font-medium text-foreground">Wallpaper URL</label> <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://..." <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>
<div> <div>
<label for="bp-wp-blur" class="mb-1 block text-sm font-medium text-foreground">Blur ({wallpaperBlur}px)</label> <label for="bp-wp-blur" class="mb-1 block text-sm font-medium text-foreground">Blur ({wallpaperBlur}px)</label>
@@ -176,7 +176,7 @@
<div> <div>
<label for="bp-cardsize" class="mb-1 block text-sm font-medium text-foreground">{$t('board.card_size') ?? 'Card Size'}</label> <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} <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="compact">Compact</option>
<option value="medium">Medium</option> <option value="medium">Medium</option>
<option value="large">Large</option> <option value="large">Large</option>
@@ -187,7 +187,7 @@
<div> <div>
<label for="bp-css" class="mb-1 block text-sm font-medium text-foreground">{$t('board.custom_css') ?? 'Custom CSS'}</label> <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 { ... }'} <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> </div>
</div> </div>
@@ -204,7 +204,7 @@
<button <button
type="button" type="button"
onclick={handleSave} 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'} {$t('common.apply') ?? 'Apply'}
</button> </button>
@@ -153,7 +153,7 @@
onclick={handleBackdropClick} onclick={handleBackdropClick}
role="presentation" 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 --> <!-- Header -->
<div class="mb-4 flex items-center justify-between"> <div class="mb-4 flex items-center justify-between">
<h2 class="text-lg font-semibold text-card-foreground"> <h2 class="text-lg font-semibold text-card-foreground">
@@ -177,7 +177,7 @@
<button <button
type="button" type="button"
onclick={handleCopyLink} 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"> <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" /> <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 <select
bind:value={selectedTargetType} bind:value={selectedTargetType}
onchange={() => { selectedTargetId = ''; searchQuery = ''; }} 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.USER}>{$t('admin.perm_user')}</option>
<option value={TargetType.GROUP}>{$t('admin.perm_group')}</option> <option value={TargetType.GROUP}>{$t('admin.perm_group')}</option>
@@ -220,10 +220,10 @@
type="text" type="text"
bind:value={searchQuery} bind:value={searchQuery}
placeholder={$t('board.access_search_placeholder')} 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} {#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)} {#each filteredTargetOptions as option (option.id)}
<button <button
type="button" type="button"
@@ -238,7 +238,7 @@
</div> </div>
<select <select
bind:value={selectedLevel} 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.VIEW}>{$t('admin.perm_view')}</option>
<option value={PermissionLevel.EDIT}>{$t('admin.perm_edit')}</option> <option value={PermissionLevel.EDIT}>{$t('admin.perm_edit')}</option>
@@ -248,7 +248,7 @@
type="button" type="button"
onclick={handleGrant} onclick={handleGrant}
disabled={!selectedTargetId} 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')} {$t('common.add')}
</button> </button>
+1 -1
View File
@@ -40,7 +40,7 @@
transition:fly={{ y: 60, duration: 250 }} transition:fly={{ y: 60, duration: 250 }}
> >
<!-- Toolbar pill --> <!-- 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 --> <!-- Save -->
<button <button
type="button" type="button"
@@ -136,7 +136,7 @@
onclick={() => selectTemplate(template.id)} 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'}" 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} {#if template.icon}
<DynamicIcon name={template.icon} size={20} /> <DynamicIcon name={template.icon} size={20} />
{:else} {:else}
@@ -57,7 +57,7 @@
</script> </script>
{#if favorites.hasFavorites} {#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 <div
class="flex flex-wrap items-center gap-2" class="flex flex-wrap items-center gap-2"
use:dndzone={{ use:dndzone={{
@@ -75,7 +75,7 @@
href={item.app.url} href={item.app.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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} title={item.app.name}
oncontextmenu={(e) => handleRemove(e, item.appId)} oncontextmenu={(e) => handleRemove(e, item.appId)}
> >
+14 -12
View File
@@ -21,6 +21,7 @@
} }
const bgOptions: { value: BackgroundType; labelKey: string }[] = [ const bgOptions: { value: BackgroundType; labelKey: string }[] = [
{ value: 'cozy', labelKey: 'bg.cozy' },
{ value: 'mesh', labelKey: 'bg.mesh' }, { value: 'mesh', labelKey: 'bg.mesh' },
{ value: 'particles', labelKey: 'bg.particles' }, { value: 'particles', labelKey: 'bg.particles' },
{ value: 'aurora', labelKey: 'bg.aurora' }, { value: 'aurora', labelKey: 'bg.aurora' },
@@ -29,14 +30,14 @@
</script> </script>
<header <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 --> <!-- Mobile hamburger -->
{#if ui.isMobile} {#if ui.isMobile}
<button <button
type="button" type="button"
onclick={() => ui.toggleSidebar()} 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')} aria-label={$t('sidebar.toggle')}
> >
<svg <svg
@@ -64,7 +65,7 @@
<!-- Background selector --> <!-- Background selector -->
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger <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')} title={$t('bg.title')}
aria-label={$t('bg.aria_label')} aria-label={$t('bg.aria_label')}
> >
@@ -84,13 +85,13 @@
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content <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} sideOffset={4}
align="end" align="end"
> >
{#each bgOptions as opt (opt.value)} {#each bgOptions as opt (opt.value)}
<DropdownMenu.Item <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' ? 'bg-accent text-accent-foreground'
: 'text-popover-foreground hover:bg-accent/50'}" : 'text-popover-foreground hover:bg-accent/50'}"
onSelect={() => theme.setBackground(opt.value)} onSelect={() => theme.setBackground(opt.value)}
@@ -131,10 +132,11 @@
{#if user} {#if user}
<DropdownMenu.Root> <DropdownMenu.Root>
<DropdownMenu.Trigger <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 <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()} {user.displayName.charAt(0).toUpperCase()}
</span> </span>
@@ -144,7 +146,7 @@
</DropdownMenu.Trigger> </DropdownMenu.Trigger>
<DropdownMenu.Portal> <DropdownMenu.Portal>
<DropdownMenu.Content <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} sideOffset={4}
align="end" align="end"
> >
@@ -154,7 +156,7 @@
</div> </div>
<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')} onSelect={() => goto('/settings')}
> >
<svg <svg
@@ -174,7 +176,7 @@
</DropdownMenu.Item> </DropdownMenu.Item>
<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')} onSelect={() => goto('/settings/api-tokens')}
> >
<svg <svg
@@ -197,7 +199,7 @@
<form method="POST" action="/auth/logout" id="logout-form" class="contents"> <form method="POST" action="/auth/logout" id="logout-form" class="contents">
<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={submitLogout} onSelect={submitLogout}
> >
<svg <svg
@@ -223,7 +225,7 @@
{:else} {:else}
<a <a
href="/login" 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')} {$t('auth.login')}
</a> </a>
@@ -67,10 +67,10 @@
{#if visible} {#if visible}
<div <div
transition:fade={{ duration: 200 }} 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" 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" /> <Download class="h-5 w-5 text-primary" />
</div> </div>
@@ -86,7 +86,7 @@
<button <button
type="button" type="button"
onclick={install} 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')} {$t('install.button')}
</button> </button>
@@ -12,7 +12,7 @@
<button <button
type="button" type="button"
onclick={toggleLocale} 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' : 'Переключить на русский'} title={$locale === 'ru' ? 'Switch to English' : 'Переключить на русский'}
> >
{$locale === 'ru' ? 'RU' : 'EN'} {$locale === 'ru' ? 'RU' : 'EN'}
+74 -77
View File
@@ -24,19 +24,32 @@
function isActive(path: string): boolean { function isActive(path: string): boolean {
return $page.url.pathname.startsWith(path); 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> </script>
<aside <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-64={!collapsed}
class:w-16={collapsed} class:w-[4.75rem]={collapsed}
> >
<!-- Brand --> <!-- Brand -->
<div class="flex h-14 items-center border-b border-sidebar-border px-4"> <div class="mb-5 flex h-12 items-center px-1.5 {collapsed ? 'justify-center' : 'gap-3'}">
{#if !collapsed} <a href="/" class="flex items-center gap-3 text-sidebar-foreground" title={$t('app_name')}>
<a href="/" class="flex items-center gap-2 text-sidebar-foreground"> <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 <svg
class="h-6 w-6 text-sidebar-primary" class="h-5 w-5"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
@@ -45,62 +58,49 @@
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<rect x="3" y="3" width="7" height="7" /> <rect x="3" y="3" width="7" height="7" rx="2" />
<rect x="14" y="3" width="7" height="7" /> <rect x="14" y="3" width="7" height="7" rx="2" />
<rect x="14" y="14" width="7" height="7" /> <rect x="14" y="14" width="7" height="7" rx="2" />
<rect x="3" y="14" width="7" height="7" /> <rect x="3" y="14" width="7" height="7" rx="2" />
</svg> </svg>
<span class="text-sm font-semibold">{$t('app_name')}</span> </span>
</a> {#if !collapsed}
{:else} <span class="leading-tight">
<a href="/" class="mx-auto text-sidebar-primary" title={$t('app_name')}> <span class="block font-display text-base font-semibold">{$t('app_name')}</span>
<svg <span class="block text-[11px] font-medium text-sidebar-foreground/50">home cloud</span>
class="h-6 w-6" </span>
xmlns="http://www.w3.org/2000/svg" {/if}
viewBox="0 0 24 24" </a>
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}
</div> </div>
<!-- Navigation --> <!-- Navigation -->
<nav class="flex-1 overflow-y-auto px-2 py-3"> <nav class="flex flex-1 flex-col overflow-y-auto">
<!-- Main Links --> <!-- Main Links -->
<div class="mb-3"> <div class="mb-2">
{#if !collapsed} {#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')} {$t('nav.navigation')}
</p> </p>
{/if} {/if}
<a <a
href="/boards" href="/boards"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/boards') class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/boards')
? 'bg-sidebar-accent text-sidebar-accent-foreground' ? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}" : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
title={collapsed ? $t('nav.boards') : undefined} title={collapsed ? $t('nav.boards') : undefined}
> >
<svg <svg
class="h-4 w-4 shrink-0" class="h-5 w-5 shrink-0"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="1.9"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="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="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="21" x2="9" y2="9" /> <line x1="9" y1="21" x2="9" y2="9" />
</svg> </svg>
@@ -109,44 +109,42 @@
<a <a
href="/apps" href="/apps"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/apps') class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/apps')
? 'bg-sidebar-accent text-sidebar-accent-foreground' ? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}" : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
title={collapsed ? $t('nav.apps') : undefined} title={collapsed ? $t('nav.apps') : undefined}
> >
<svg <svg
class="h-4 w-4 shrink-0" class="h-5 w-5 shrink-0"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="1.9"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
<circle cx="12" cy="12" r="10" /> <circle cx="12" cy="12" r="9" />
<line x1="2" y1="12" x2="22" y2="12" /> <line x1="3" y1="12" x2="21" y2="12" />
<path <path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z" />
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"
/>
</svg> </svg>
{#if !collapsed}<span>{$t('nav.apps')}</span>{/if} {#if !collapsed}<span>{$t('nav.apps')}</span>{/if}
</a> </a>
<a <a
href="/status" href="/status"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/status') class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/status')
? 'bg-sidebar-accent text-sidebar-accent-foreground' ? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}" : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
title={collapsed ? 'Status Page' : undefined} title={collapsed ? 'Status Page' : undefined}
> >
<svg <svg
class="h-4 w-4 shrink-0" class="h-5 w-5 shrink-0"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="1.9"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
@@ -156,18 +154,18 @@
</a> </a>
</div> </div>
<!-- Board List --> <!-- Board List ("Rooms") -->
{#if boards.length > 0} {#if boards.length > 0}
<div class="mb-3"> <div class="mb-2 mt-1">
{#if !collapsed} {#if !collapsed}
<button <button
type="button" type="button"
onclick={() => (boardsExpanded = !boardsExpanded)} 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> <span>{$t('nav.boards')}</span>
<svg <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} class:rotate-180={boardsExpanded}
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
@@ -182,13 +180,13 @@
{/if} {/if}
{#if boardsExpanded || collapsed} {#if boardsExpanded || collapsed}
<div class="max-h-48 overflow-y-auto"> <div class="max-h-56 overflow-y-auto">
{#each boards as board (board.id)} {#each boards as board, i (board.id)}
<a <a
href="/boards/{board.id}" 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}`) class="flex items-center gap-3 rounded-2xl px-3 py-2 text-sm font-medium transition-all {isActive(`/boards/${board.id}`)
? 'bg-sidebar-accent text-sidebar-accent-foreground' ? 'bg-card text-sidebar-foreground shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}" : 'text-sidebar-foreground/75 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
title={collapsed ? board.name : undefined} title={collapsed ? board.name : undefined}
onclick={() => ui.closeMobileSidebar()} onclick={() => ui.closeMobileSidebar()}
> >
@@ -196,7 +194,8 @@
<span class="shrink-0"><DynamicIcon name={board.icon} size={18} /></span> <span class="shrink-0"><DynamicIcon name={board.icon} size={18} /></span>
{:else} {:else}
<span <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()} {board.name.charAt(0).toUpperCase()}
</span> </span>
@@ -213,29 +212,27 @@
<!-- Admin --> <!-- Admin -->
{#if isAdmin} {#if isAdmin}
<div class="mt-auto border-t border-sidebar-border pt-3"> <div class="mt-auto pt-2">
{#if !collapsed} {#if !collapsed}
<p <p class="mb-1.5 px-3 text-[10.5px] font-bold uppercase tracking-[0.1em] text-sidebar-foreground/45">
class="mb-1 px-2 text-xs font-medium uppercase tracking-wider text-sidebar-foreground/50"
>
{$t('nav.admin')} {$t('nav.admin')}
</p> </p>
{/if} {/if}
<a <a
href="/admin/users" href="/admin/users"
class="flex items-center gap-2 rounded-md px-2 py-2 text-sm transition-colors {isActive('/admin') class="flex items-center gap-3 rounded-2xl px-3 py-2.5 text-sm font-semibold transition-all {isActive('/admin')
? 'bg-sidebar-accent text-sidebar-accent-foreground' ? 'bg-card text-primary shadow-[var(--shadow-soft)] ring-1 ring-sidebar-border'
: 'text-sidebar-foreground hover:bg-sidebar-accent/50'}" : 'text-sidebar-foreground/80 hover:bg-sidebar-accent hover:text-sidebar-foreground'}"
title={collapsed ? $t('nav.admin_panel') : undefined} title={collapsed ? $t('nav.admin_panel') : undefined}
> >
<svg <svg
class="h-4 w-4 shrink-0" class="h-5 w-5 shrink-0"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"
stroke-width="2" stroke-width="1.9"
stroke-linecap="round" stroke-linecap="round"
stroke-linejoin="round" stroke-linejoin="round"
> >
@@ -252,11 +249,11 @@
<!-- Keyboard Shortcuts Hint + Collapse Toggle --> <!-- Keyboard Shortcuts Hint + Collapse Toggle -->
{#if !ui.isMobile} {#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 <button
type="button" type="button"
onclick={() => keyboard.toggleOverlay()} 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 (?)" title="Keyboard Shortcuts (?)"
> >
<svg <svg
@@ -277,7 +274,7 @@
<button <button
type="button" type="button"
onclick={() => ui.toggleSidebar()} 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')} title={collapsed ? $t('sidebar.expand') : $t('sidebar.collapse')}
> >
<svg <svg
+1 -1
View File
@@ -23,7 +23,7 @@
<button <button
type="button" type="button"
onclick={() => theme.cycleMode()} 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) } })} title={$t('theme.title', { values: { mode: $t(currentIcon.labelKey) } })}
aria-label={$t('theme.toggle', { values: { mode: $t(currentIcon.labelKey) } })} aria-label={$t('theme.toggle', { values: { mode: $t(currentIcon.labelKey) } })}
> >
@@ -47,11 +47,11 @@
function eventColor(event: string): string { function eventColor(event: string): string {
switch (event) { switch (event) {
case 'app_online': case 'app_online':
return 'text-green-500'; return 'text-status-online-ink';
case 'app_offline': case 'app_offline':
return 'text-red-500'; return 'text-status-offline-ink';
case 'app_degraded': case 'app_degraded':
return 'text-yellow-500'; return 'text-status-degraded-ink';
default: default:
return 'text-muted-foreground'; return 'text-muted-foreground';
} }
@@ -64,7 +64,7 @@
<button <button
type="button" type="button"
onclick={() => (showDropdown = !showDropdown)} 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" title="Notifications"
aria-label="Notifications" aria-label="Notifications"
> >
@@ -95,7 +95,7 @@
{#if showDropdown} {#if showDropdown}
<div <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 --> <!-- Header -->
<div class="flex items-center justify-between border-b border-border px-4 py-3"> <div class="flex items-center justify-between border-b border-border px-4 py-3">
@@ -126,7 +126,7 @@
<select <select
id="channel-type" id="channel-type"
bind:value={channelType} 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="discord">Discord</option>
<option value="slack">Slack</option> <option value="slack">Slack</option>
@@ -146,7 +146,7 @@
type="url" type="url"
bind:value={discordWebhookUrl} bind:value={discordWebhookUrl}
placeholder="https://discord.com/api/webhooks/..." 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 required
/> />
</div> </div>
@@ -160,7 +160,7 @@
type="url" type="url"
bind:value={slackWebhookUrl} bind:value={slackWebhookUrl}
placeholder="https://hooks.slack.com/services/..." 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 required
/> />
</div> </div>
@@ -174,7 +174,7 @@
type="text" type="text"
bind:value={telegramBotToken} bind:value={telegramBotToken}
placeholder="123456:ABC-DEF..." 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 required
/> />
</div> </div>
@@ -187,7 +187,7 @@
type="text" type="text"
bind:value={telegramChatId} bind:value={telegramChatId}
placeholder="-1001234567890" 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 required
/> />
</div> </div>
@@ -201,7 +201,7 @@
type="url" type="url"
bind:value={httpUrl} bind:value={httpUrl}
placeholder="https://example.com/webhook" 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 required
/> />
</div> </div>
@@ -212,7 +212,7 @@
<select <select
id="http-method" id="http-method"
bind:value={httpMethod} 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="POST">POST</option>
<option value="PUT">PUT</option> <option value="PUT">PUT</option>
@@ -230,7 +230,7 @@
bind:value={httpSecret} bind:value={httpSecret}
placeholder="Shared secret for HMAC-SHA256 signature" placeholder="Shared secret for HMAC-SHA256 signature"
autocomplete="off" 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 <button
type="button" type="button"
@@ -262,7 +262,7 @@
bind:value={httpSignatureHeader} bind:value={httpSignatureHeader}
placeholder="X-Signature-256" placeholder="X-Signature-256"
autocomplete="off" 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"> <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>). 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 --> <!-- Test Result -->
{#if testResult} {#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} {testResult}
</p> </p>
{/if} {/if}
@@ -292,7 +292,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
type="submit" 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 {channel ? 'Update' : 'Create'} Channel
</button> </button>
@@ -301,7 +301,7 @@
type="button" type="button"
onclick={sendTest} onclick={sendTest}
disabled={testing} 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'} {testing ? 'Sending...' : 'Send Test'}
</button> </button>
@@ -73,9 +73,9 @@
function eventBadgeClass(event: string): string { function eventBadgeClass(event: string): string {
switch (event) { switch (event) {
case 'app_online': return 'bg-green-500/10 text-green-500'; case 'app_online': return 'bg-status-online/15 text-status-online-ink';
case 'app_offline': return 'bg-red-500/10 text-red-500'; case 'app_offline': return 'bg-status-offline/15 text-status-offline-ink';
case 'app_degraded': return 'bg-yellow-500/10 text-yellow-500'; case 'app_degraded': return 'bg-status-degraded/15 text-status-degraded-ink';
default: return 'bg-muted text-muted-foreground'; default: return 'bg-muted text-muted-foreground';
} }
} }
@@ -87,7 +87,7 @@
<select <select
bind:value={filterEvent} bind:value={filterEvent}
onchange={applyFilters} 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="">All Events</option>
<option value="app_online">Online</option> <option value="app_online">Online</option>
@@ -104,7 +104,7 @@
<p class="text-muted-foreground">No notifications found</p> <p class="text-muted-foreground">No notifications found</p>
</div> </div>
{:else} {: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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
@@ -20,7 +20,7 @@
// Theme form // Theme form
let defaultTheme = $state<'dark' | 'light'>('dark'); let defaultTheme = $state<'dark' | 'light'>('dark');
let defaultPrimaryColor = $state('#6366f1'); let defaultPrimaryColor = $state('#e8754f');
// Board form // Board form
let boardName = $state('My Dashboard'); let boardName = $state('My Dashboard');
@@ -169,6 +169,7 @@
} }
const primaryColorOptions = [ const primaryColorOptions = [
{ label: 'Terracotta', value: '#e8754f' },
{ label: 'Indigo', value: '#6366f1' }, { label: 'Indigo', value: '#6366f1' },
{ label: 'Blue', value: '#3b82f6' }, { label: 'Blue', value: '#3b82f6' },
{ label: 'Emerald', value: '#10b981' }, { 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="fixed inset-0 z-[100] flex items-center justify-center bg-black/80 backdrop-blur-sm">
<div <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 --> <!-- Progress bar -->
<div class="border-b border-border px-6 py-4"> <div class="border-b border-border px-6 py-4">
@@ -227,7 +228,7 @@
{:else if currentStep === 'admin'} {:else if currentStep === 'admin'}
<h2 class="mb-4 text-xl font-bold text-foreground">Create Admin Account</h2> <h2 class="mb-4 text-xl font-bold text-foreground">Create Admin Account</h2>
{#if adminCreated} {#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. Admin account created successfully. You can proceed to the next step.
</div> </div>
{:else} {:else}
@@ -238,7 +239,7 @@
id="ob-display-name" id="ob-display-name"
type="text" type="text"
bind:value={adminDisplayName} 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" placeholder="Admin"
/> />
</div> </div>
@@ -248,7 +249,7 @@
id="ob-email" id="ob-email"
type="email" type="email"
bind:value={adminEmail} 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" placeholder="admin@example.com"
/> />
</div> </div>
@@ -258,7 +259,7 @@
id="ob-password" id="ob-password"
type="password" type="password"
bind:value={adminPassword} 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" placeholder="Min. 6 characters"
/> />
</div> </div>
@@ -298,19 +299,19 @@
<input <input
type="text" type="text"
bind:value={oauthClientId} 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" placeholder="Client ID"
/> />
<input <input
type="password" type="password"
bind:value={oauthClientSecret} 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" placeholder="Client Secret"
/> />
<input <input
type="url" type="url"
bind:value={oauthDiscoveryUrl} 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)" placeholder="Discovery URL (https://.../.well-known/openid-configuration)"
/> />
</div> </div>
@@ -369,7 +370,7 @@
id="ob-board-name" id="ob-board-name"
type="text" type="text"
bind:value={boardName} 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" placeholder="My Dashboard"
/> />
</div> </div>
@@ -417,7 +418,7 @@
type="button" type="button"
onclick={handleNext} onclick={handleNext}
disabled={loading} 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} {#if loading}
Processing... Processing...
@@ -76,7 +76,7 @@
> >
<!-- Dialog --> <!-- Dialog -->
<div <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)" style="max-height: 60vh; animation: searchSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
role="dialog" role="dialog"
aria-label={$t('search.placeholder')} aria-label={$t('search.placeholder')}
@@ -10,7 +10,7 @@
<button <button
type="button" type="button"
onclick={() => search.toggle()} 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 <svg
class="h-4 w-4 shrink-0" class="h-4 w-4 shrink-0"
@@ -27,7 +27,7 @@
</svg> </svg>
<span class="flex-1 text-left">{$t('search.trigger')}</span> <span class="flex-1 text-left">{$t('search.trigger')}</span>
<kbd <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 {isMac ? '\u2318' : 'Ctrl'}K
</kbd> </kbd>
@@ -105,7 +105,7 @@
} }
</script> </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="mb-3 flex items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<!-- Section drag handle --> <!-- Section drag handle -->
@@ -142,7 +142,7 @@
<!-- Card size selector --> <!-- Card size selector -->
<select <select
onchange={handleCardSizeChange} 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'} title={$t('board.card_size') ?? 'Card size'}
> >
<option value="" selected={!section.cardSize}>{$t('section.inherit_size') ?? 'Inherit'}</option> <option value="" selected={!section.cardSize}>{$t('section.inherit_size') ?? 'Inherit'}</option>
@@ -153,7 +153,7 @@
<button <button
type="button" type="button"
onclick={() => onToggleAddWidget(section.id)} 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')} {$t('widget.add')}
</button> </button>
+1 -1
View File
@@ -58,7 +58,7 @@
let expanded = $state(section.isExpandedByDefault); let expanded = $state(section.isExpandedByDefault);
</script> </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 <SectionHeader
sectionId={section.id} sectionId={section.id}
title={section.title} title={section.title}
@@ -117,7 +117,7 @@
bind:value={editTitle} bind:value={editTitle}
onkeydown={handleTitleKeydown} onkeydown={handleTitleKeydown}
onblur={handleEditBlur} 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 <IconPickerButton
value={editIcon} value={editIcon}
@@ -135,7 +135,7 @@
{#if icon} {#if icon}
<DynamicIcon name={icon} size={18} /> <DynamicIcon name={icon} size={18} />
{/if} {/if}
<span class="font-medium text-foreground">{title}</span> <span class="font-display text-lg font-semibold text-foreground">{title}</span>
</button> </button>
{/if} {/if}
@@ -21,7 +21,7 @@
name="name" name="name"
type="text" type="text"
placeholder="e.g., CI/CD Pipeline" 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 required
/> />
<p class="mt-1 text-xs text-muted-foreground">A descriptive name to identify this token</p> <p class="mt-1 text-xs text-muted-foreground">A descriptive name to identify this token</p>
@@ -34,7 +34,7 @@
<select <select
id="token-scope" id="token-scope"
name="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="read">Read — View apps, boards, and status</option>
<option value="write">Write — Modify apps, boards, and settings</option> <option value="write">Write — Modify apps, boards, and settings</option>
@@ -50,7 +50,7 @@
id="token-expires" id="token-expires"
name="expiresAt" name="expiresAt"
type="datetime-local" 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> <p class="mt-1 text-xs text-muted-foreground">Leave empty for a non-expiring token</p>
</div> </div>
@@ -58,7 +58,7 @@
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<button <button
type="submit" 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 Generate Token
</button> </button>
@@ -39,9 +39,9 @@
function scopeBadgeClass(scope: string): string { function scopeBadgeClass(scope: string): string {
switch (scope) { switch (scope) {
case 'admin': return 'bg-red-500/10 text-red-500'; case 'admin': return 'bg-destructive/10 text-destructive';
case 'write': return 'bg-yellow-500/10 text-yellow-500'; case 'write': return 'bg-status-degraded/10 text-status-degraded-ink';
default: return 'bg-green-500/10 text-green-500'; default: return 'bg-status-online/10 text-status-online-ink';
} }
} }
</script> </script>
@@ -54,7 +54,7 @@
</p> </p>
</div> </div>
{:else} {: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"> <table class="w-full text-left text-sm">
<thead class="border-b border-border bg-muted/50"> <thead class="border-b border-border bg-muted/50">
<tr> <tr>
@@ -20,7 +20,7 @@
}); });
</script> </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"> <h2 class="mb-2 text-lg font-semibold text-card-foreground">
{$t('settings.bookmarklet_title')} {$t('settings.bookmarklet_title')}
</h2> </h2>
@@ -88,7 +88,7 @@
value={localValue} value={localValue}
oninput={handleInput} oninput={handleInput}
rows="8" 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}`} placeholder={`/* Custom CSS */\n.custom-css-scope .my-widget {\n background: #333;\n}`}
spellcheck="false" spellcheck="false"
></textarea> ></textarea>
@@ -128,7 +128,7 @@
type="button" type="button"
onclick={() => setMode(opt.value)} onclick={() => setMode(opt.value)}
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.mode === 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'}" : 'text-muted-foreground hover:text-foreground'}"
> >
{$t(opt.labelKey)} {$t(opt.labelKey)}
@@ -167,7 +167,7 @@
max="360" max="360"
step="1" step="1"
bind:value={theme.primaryHue} 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};" style="color: {previewColor};"
/> />
</div> </div>
@@ -188,7 +188,7 @@
max="100" max="100"
step="1" step="1"
bind:value={theme.primarySaturation} 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};" style="color: {previewColor};"
/> />
</div> </div>
@@ -204,7 +204,7 @@
type="button" type="button"
onclick={() => setBackground(opt.value)} onclick={() => setBackground(opt.value)}
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.backgroundType === 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'}" : 'text-muted-foreground hover:text-foreground'}"
> >
{$t(opt.labelKey)} {$t(opt.labelKey)}
@@ -222,7 +222,7 @@
type="button" type="button"
onclick={() => setCardStyle(opt.value)} onclick={() => setCardStyle(opt.value)}
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {theme.cardStyle === 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'}" : 'text-muted-foreground hover:text-foreground'}"
> >
{$t(opt.labelKey) ?? opt.value} {$t(opt.labelKey) ?? opt.value}
@@ -240,7 +240,7 @@
type="button" type="button"
onclick={() => setLocale(opt.value)} onclick={() => setLocale(opt.value)}
class="flex-1 rounded-md px-3 py-2 text-sm font-medium transition-colors {$i18nLocale === 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'}" : 'text-muted-foreground hover:text-foreground'}"
> >
{opt.label} {opt.label}
@@ -255,12 +255,12 @@
type="button" type="button"
onclick={savePreferences} onclick={savePreferences}
disabled={saving} 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')} {saving ? $t('settings.saving') : $t('settings.save')}
</button> </button>
{#if saved} {#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}
{#if errorMessage} {#if errorMessage}
<span class="text-sm text-destructive">{errorMessage}</span> <span class="text-sm text-destructive">{errorMessage}</span>
@@ -91,7 +91,7 @@
/> />
{#if open && filtered.length > 0} {#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)} {#each filtered as item, i (item)}
<button <button
type="button" type="button"
+5 -5
View File
@@ -40,7 +40,7 @@
> >
<!-- Dialog --> <!-- Dialog -->
<div <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()} onclick={(e) => e.stopPropagation()}
onkeydown={(e) => e.stopPropagation()} onkeydown={(e) => e.stopPropagation()}
transition:scale={{ start: 0.95, duration: 150 }} transition:scale={{ start: 0.95, duration: 150 }}
@@ -49,21 +49,21 @@
aria-labelledby="confirm-dialog-title" aria-labelledby="confirm-dialog-title"
aria-describedby="confirm-dialog-message" aria-describedby="confirm-dialog-message"
> >
<h2 id="confirm-dialog-title" class="mb-2 text-base font-semibold text-foreground">{title}</h2> <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 text-muted-foreground">{message}</p> <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"> <div class="flex items-center justify-end gap-2">
<button <button
type="button" type="button"
onclick={onCancel} 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')} {cancelLabel ?? ($t('common.cancel') ?? 'Cancel')}
</button> </button>
<button <button
type="button" type="button"
onclick={onConfirm} 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 {destructive
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-primary text-primary-foreground hover:bg-primary/90'}" : 'bg-primary text-primary-foreground hover:bg-primary/90'}"
+2 -2
View File
@@ -130,7 +130,7 @@
<button <button
type="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} onclick={openPicker}
> >
{#if selectedItem} {#if selectedItem}
@@ -157,7 +157,7 @@
style="animation: epFadeIn 0.15s ease-out" style="animation: epFadeIn 0.15s ease-out"
> >
<div <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)" style="max-height: 60vh; animation: epSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
role="dialog" role="dialog"
aria-label={searchPlaceholder || 'Select entity'} 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" class="relative z-10 flex min-h-screen items-center justify-center bg-background/80 p-4 text-foreground"
> >
<div <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"> <div class="mb-2 text-xs uppercase tracking-widest text-muted-foreground">
{status} {status}
+2 -2
View File
@@ -112,7 +112,7 @@
<button <button
type="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} bind:this={triggerEl}
onclick={toggle} onclick={toggle}
> >
@@ -129,7 +129,7 @@
{#if open} {#if open}
<div <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} bind:this={popupEl}
style="animation: iconGridSlideIn 0.15s ease-out" style="animation: iconGridSlideIn 0.15s ease-out"
> >
@@ -82,7 +82,7 @@
<button <button
type="button" type="button"
onclick={toggleOpen} 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'}" {size === 'sm' ? 'px-2 py-1' : 'px-3 py-2'}"
title={$t('app.icon') ?? 'Select icon'} title={$t('app.icon') ?? 'Select icon'}
> >
@@ -105,7 +105,7 @@
class="fixed inset-0 z-50" class="fixed inset-0 z-50"
onclick={(e) => { if (e.target === e.currentTarget) open = false; }} 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 --> <!-- Search -->
<div class="relative mb-2"> <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"> <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" type="text"
bind:value={query} bind:value={query}
placeholder={$t('common.search') ?? 'Search icons...'} 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> </div>
@@ -160,7 +160,7 @@
value={value} value={value}
oninput={(e) => { const v = (e.target as HTMLInputElement).value; value = v; onchange(v); }} oninput={(e) => { const v = (e.target as HTMLInputElement).value; value = v; onchange(v); }}
placeholder={$t('app.icon_manual') ?? 'Or type icon name...'} 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>
</div> </div>
@@ -48,7 +48,7 @@
onkeydown={(e) => e.key === 'Escape' && keyboard.closeOverlay()} onkeydown={(e) => e.key === 'Escape' && keyboard.closeOverlay()}
> >
<div <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" role="dialog"
aria-label="Keyboard Shortcuts" aria-label="Keyboard Shortcuts"
> >
@@ -127,7 +127,7 @@
<button <button
type="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} onclick={openPicker}
> >
{#if selectedCount > 0} {#if selectedCount > 0}
@@ -148,7 +148,7 @@
style="animation: mepFadeIn 0.15s ease-out" style="animation: mepFadeIn 0.15s ease-out"
> >
<div <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)" style="max-height: 60vh; animation: mepSlideDown 0.2s cubic-bezier(0.16, 1, 0.3, 1)"
role="dialog" role="dialog"
aria-label={searchPlaceholder || 'Select items'} aria-label={searchPlaceholder || 'Select items'}
+2 -2
View File
@@ -100,7 +100,7 @@
{#if tags.length > 0} {#if tags.length > 0}
<div class="mb-1.5 flex flex-wrap gap-1"> <div class="mb-1.5 flex flex-wrap gap-1">
{#each tags as tag (tag)} {#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} {tag}
<button <button
type="button" type="button"
@@ -128,7 +128,7 @@
/> />
{#if open && filtered.length > 0} {#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)} {#each filtered as item, i (item)}
<button <button
type="button" type="button"
+8 -8
View File
@@ -139,14 +139,14 @@
href={app.url} href={app.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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-widget
data-app-url={app.url} data-app-url={app.url}
oncontextmenu={handleContextMenu} oncontextmenu={handleContextMenu}
onclick={recordClick} onclick={recordClick}
> >
<div class="relative flex-shrink-0"> <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} {#if app.iconType === 'emoji' && app.icon}
<span class="text-base">{app.icon}</span> <span class="text-base">{app.icon}</span>
{:else if iconSrc} {:else if iconSrc}
@@ -198,7 +198,7 @@
<!-- Large: icon + name + description + sparkline + tags + links --> <!-- Large: icon + name + description + sparkline + tags + links -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <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-widget
data-app-url={app.url} data-app-url={app.url}
oncontextmenu={handleContextMenu} oncontextmenu={handleContextMenu}
@@ -211,7 +211,7 @@
onclick={recordClick} onclick={recordClick}
> >
<div class="relative"> <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} {#if app.iconType === 'emoji' && app.icon}
<span class="text-3xl">{app.icon}</span> <span class="text-3xl">{app.icon}</span>
{:else if iconSrc} {:else if iconSrc}
@@ -294,7 +294,7 @@
<!-- Medium (default): icon + name + status + sparkline on hover + links --> <!-- Medium (default): icon + name + status + sparkline on hover + links -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <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-widget
data-app-url={app.url} data-app-url={app.url}
oncontextmenu={handleContextMenu} oncontextmenu={handleContextMenu}
@@ -307,7 +307,7 @@
onclick={recordClick} onclick={recordClick}
> >
<div class="relative"> <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} {#if app.iconType === 'emoji' && app.icon}
<span class="text-2xl">{app.icon}</span> <span class="text-2xl">{app.icon}</span>
{:else if iconSrc} {:else if iconSrc}
@@ -378,12 +378,12 @@
<!-- Context Menu --> <!-- Context Menu -->
{#if showContextMenu} {#if showContextMenu}
<div <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" style="left: {contextMenuPos.x}px; top: {contextMenuPos.y}px"
> >
<button <button
type="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} onclick={toggleFavorite}
> >
{#if favorites.isFavorite(app.id)} {#if favorites.isFavorite(app.id)}
@@ -17,10 +17,10 @@
href={config.url} href={config.url}
target="_blank" target="_blank"
rel="noopener noreferrer" 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 --> <!-- 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} {#if config.icon}
<span class="text-2xl">{config.icon}</span> <span class="text-2xl">{config.icon}</span>
{:else} {:else}
@@ -44,7 +44,7 @@
<!-- Badge --> <!-- Badge -->
<span class="inline-flex items-center gap-1.5 text-xs"> <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 class="text-muted-foreground">Bookmark</span>
</span> </span>
</a> </a>
@@ -110,7 +110,7 @@
}); });
</script> </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"> <div class="mb-3 flex items-center gap-2">
<Calendar class="h-4 w-4 text-muted-foreground" /> <Calendar class="h-4 w-4 text-muted-foreground" />
<span class="text-sm font-medium text-foreground">Calendar</span> <span class="text-sm font-medium text-foreground">Calendar</span>
@@ -154,7 +154,7 @@
} }
</script> </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 --> <!-- Stream view -->
<div <div
class="relative w-full bg-black" class="relative w-full bg-black"
@@ -111,7 +111,7 @@
} }
</script> </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'} {#if clockStyle === 'analog'}
<!-- Analog clock face --> <!-- Analog clock face -->
<svg viewBox="0 0 100 100" class="h-32 w-32"> <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> <p class="mt-2 text-xs text-muted-foreground">{dateStr}</p>
{:else} {:else}
<!-- Digital clock --> <!-- 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> <p class="mt-1 text-sm text-muted-foreground">{dateStr}</p>
{#if config.timezone} {#if config.timezone}
<p class="mt-0.5 text-xs text-muted-foreground/70">{config.timezone.replace(/_/g, ' ')}</p> <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> </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;"> <div class="relative" style="height: {iframeHeight}px;">
{#if !safeUrl} {#if !safeUrl}
<div class="flex h-full items-center justify-center text-sm text-muted-foreground"> <div class="flex h-full items-center justify-center text-sm text-muted-foreground">
@@ -15,7 +15,7 @@
const links = $derived(config.links ?? []); const links = $derived(config.links ?? []);
</script> </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 --> <!-- Header -->
{#if isCollapsible} {#if isCollapsible}
<button <button
@@ -53,7 +53,7 @@
} }
</script> </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 --> <!-- Toolbar -->
<div class="flex items-center justify-end border-b border-border px-3 py-1.5"> <div class="flex items-center justify-end border-b border-border px-3 py-1.5">
<button <button
@@ -64,13 +64,13 @@
}); });
const trendColor = $derived.by(() => { const trendColor = $derived.by(() => {
if (trend === 'up') return 'text-green-500'; if (trend === 'up') return 'var(--status-online-ink)';
if (trend === 'down') return 'text-red-500'; if (trend === 'down') return 'var(--status-offline-ink)';
return 'text-muted-foreground'; return 'var(--muted-foreground)';
}); });
</script> </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} {#if loading}
<div class="flex flex-col items-center gap-2"> <div class="flex flex-col items-center gap-2">
<div class="h-10 w-24 animate-pulse rounded bg-muted"></div> <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> <span class="text-xs text-muted-foreground">Failed to load metric</span>
{:else if currentValue !== null} {:else if currentValue !== null}
<!-- Trend arrow --> <!-- Trend arrow -->
<div class="mb-1 {trendColor}"> <div class="mb-1" style="color: {trendColor};">
{#if trend === 'up'} {#if trend === 'up'}
<TrendingUp class="h-5 w-5" /> <TrendingUp class="h-5 w-5" />
{:else if trend === 'down'} {:else if trend === 'down'}
@@ -92,7 +92,7 @@
<!-- Big number --> <!-- Big number -->
<div class="flex items-baseline gap-1"> <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)} {formatNumber(currentValue)}
</span> </span>
{#if config.unit} {#if config.unit}
+1 -1
View File
@@ -36,7 +36,7 @@
}); });
</script> </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"> <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 --> <!-- eslint-disable-next-line svelte/no-at-html-tags -- sanitized with DOMPurify -->
{@html renderedContent} {@html renderedContent}
@@ -80,7 +80,7 @@
} }
</script> </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"> <div class="mb-3 flex items-center gap-2">
<Rss class="h-4 w-4 text-muted-foreground" /> <Rss class="h-4 w-4 text-muted-foreground" />
<span class="text-sm font-medium text-foreground">RSS Feed</span> <span class="text-sm font-medium text-foreground">RSS Feed</span>
+13 -13
View File
@@ -46,7 +46,7 @@
let expanded = $state(false); let expanded = $state(false);
</script> </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 --> <!-- Header -->
<button <button
type="button" type="button"
@@ -63,28 +63,28 @@
<div class="mt-3 flex gap-1"> <div class="mt-3 flex gap-1">
{#if statusCounts.online > 0} {#if statusCounts.online > 0}
<div <div
class="h-2 rounded-full bg-green-500" class="h-2 rounded-full bg-status-online"
style="flex: {statusCounts.online}" style="flex: {statusCounts.online}"
title="{statusCounts.online} online" title="{statusCounts.online} online"
></div> ></div>
{/if} {/if}
{#if statusCounts.degraded > 0} {#if statusCounts.degraded > 0}
<div <div
class="h-2 rounded-full bg-yellow-500" class="h-2 rounded-full bg-status-degraded"
style="flex: {statusCounts.degraded}" style="flex: {statusCounts.degraded}"
title="{statusCounts.degraded} degraded" title="{statusCounts.degraded} degraded"
></div> ></div>
{/if} {/if}
{#if statusCounts.offline > 0} {#if statusCounts.offline > 0}
<div <div
class="h-2 rounded-full bg-red-500" class="h-2 rounded-full bg-status-offline"
style="flex: {statusCounts.offline}" style="flex: {statusCounts.offline}"
title="{statusCounts.offline} offline" title="{statusCounts.offline} offline"
></div> ></div>
{/if} {/if}
{#if statusCounts.unknown > 0} {#if statusCounts.unknown > 0}
<div <div
class="h-2 rounded-full bg-gray-500" class="h-2 rounded-full bg-status-unknown"
style="flex: {statusCounts.unknown}" style="flex: {statusCounts.unknown}"
title="{statusCounts.unknown} unknown" title="{statusCounts.unknown} unknown"
></div> ></div>
@@ -95,25 +95,25 @@
<div class="mt-2 flex flex-wrap gap-3 text-xs"> <div class="mt-2 flex flex-wrap gap-3 text-xs">
{#if statusCounts.online > 0} {#if statusCounts.online > 0}
<span class="flex items-center gap-1"> <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 {statusCounts.online} online
</span> </span>
{/if} {/if}
{#if statusCounts.degraded > 0} {#if statusCounts.degraded > 0}
<span class="flex items-center gap-1"> <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 {statusCounts.degraded} degraded
</span> </span>
{/if} {/if}
{#if statusCounts.offline > 0} {#if statusCounts.offline > 0}
<span class="flex items-center gap-1"> <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 {statusCounts.offline} offline
</span> </span>
{/if} {/if}
{#if statusCounts.unknown > 0} {#if statusCounts.unknown > 0}
<span class="flex items-center gap-1"> <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 {statusCounts.unknown} unknown
</span> </span>
{/if} {/if}
@@ -126,12 +126,12 @@
{@const status = app.statuses[0]?.status ?? 'unknown'} {@const status = app.statuses[0]?.status ?? 'unknown'}
{@const statusColor = {@const statusColor =
status === 'online' status === 'online'
? 'bg-green-500' ? 'bg-status-online'
: status === 'offline' : status === 'offline'
? 'bg-red-500' ? 'bg-status-offline'
: status === 'degraded' : status === 'degraded'
? 'bg-yellow-500' ? 'bg-status-degraded'
: 'bg-gray-500'} : 'bg-status-unknown'}
<div class="flex items-center justify-between text-xs"> <div class="flex items-center justify-between text-xs">
<span class="text-foreground">{app.name}</span> <span class="text-foreground">{app.name}</span>
<span class="flex items-center gap-1"> <span class="flex items-center gap-1">
@@ -21,15 +21,15 @@
const refreshMs = $derived((config.refreshInterval ?? 30) * 1000); const refreshMs = $derived((config.refreshInterval ?? 30) * 1000);
function thresholdColor(value: number): string { function thresholdColor(value: number): string {
if (value >= 85) return 'text-red-500'; if (value >= 85) return 'text-status-offline-ink';
if (value >= 60) return 'text-yellow-500'; if (value >= 60) return 'text-status-degraded-ink';
return 'text-green-500'; return 'text-status-online-ink';
} }
function thresholdStroke(value: number): string { function thresholdStroke(value: number): string {
if (value >= 85) return 'stroke-red-500'; if (value >= 85) return 'stroke-status-offline';
if (value >= 60) return 'stroke-yellow-500'; if (value >= 60) return 'stroke-status-degraded';
return 'stroke-green-500'; return 'stroke-status-online';
} }
function thresholdTrack(_value: number): string { function thresholdTrack(_value: number): string {
@@ -80,7 +80,7 @@
const CIRCUMFERENCE = 2 * Math.PI * RADIUS; const CIRCUMFERENCE = 2 * Math.PI * RADIUS;
</script> </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> <span class="mb-3 text-sm font-medium text-foreground">System Stats</span>
{#if loading} {#if loading}
@@ -72,7 +72,7 @@
let rssShowSummary = $state((initialConfig.showSummary as boolean) ?? true); let rssShowSummary = $state((initialConfig.showSummary as boolean) ?? true);
// Calendar // 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); let calendarDaysAhead = $state((initialConfig.daysAhead as number) ?? 7);
// Markdown // Markdown
@@ -155,7 +155,7 @@
} }
function addCalendarUrl() { function addCalendarUrl() {
calendarUrlsRaw = [...calendarUrlsRaw, { url: '', color: '#6366f1', label: '' }]; calendarUrlsRaw = [...calendarUrlsRaw, { url: '', color: '#e8754f', label: '' }];
} }
function removeCalendarUrl(index: number) { function removeCalendarUrl(index: number) {
@@ -163,7 +163,7 @@
} }
// Helper for input styling // 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'; const labelClass = 'mb-1 block text-sm font-medium text-foreground';
let firstInput: HTMLElement | undefined = $state(); let firstInput: HTMLElement | undefined = $state();
@@ -171,7 +171,7 @@
</script> </script>
<div <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 }} transition:fade={{ duration: 100 }}
onkeydown={handleKeydown} onkeydown={handleKeydown}
role="dialog" role="dialog"
@@ -201,11 +201,11 @@
bind:value={appSearchQuery} bind:value={appSearchQuery}
bind:this={firstInput} bind:this={firstInput}
placeholder={$t('common.search') ?? 'Search apps...'} 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> </div>
<!-- App grid --> <!-- 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} {#if filteredApps.length === 0}
<p class="py-4 text-center text-xs text-muted-foreground">{$t('common.no_results') ?? 'No apps found'}</p> <p class="py-4 text-center text-xs text-muted-foreground">{$t('common.no_results') ?? 'No apps found'}</p>
{:else} {:else}
@@ -548,7 +548,7 @@
{$t('common.cancel') ?? 'Cancel'} {$t('common.cancel') ?? 'Cancel'}
</button> </button>
<button type="button" onclick={handleSave} <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')} {mode === 'create' ? ($t('common.add') ?? 'Add') : ($t('common.save') ?? 'Save')}
</button> </button>
</div> </div>
@@ -60,7 +60,7 @@
// Calendar fields // Calendar fields
let calendarUrls = $state<Array<{ url: string; color: string; label: string }>>([ let calendarUrls = $state<Array<{ url: string; color: string; label: string }>>([
{ url: '', color: '#6366f1', label: '' } { url: '', color: '#e8754f', label: '' }
]); ]);
let calendarDaysAhead = $state(7); let calendarDaysAhead = $state(7);
@@ -198,7 +198,7 @@
rssFeedUrl = ''; rssFeedUrl = '';
rssMaxItems = 10; rssMaxItems = 10;
rssShowSummary = true; rssShowSummary = true;
calendarUrls = [{ url: '', color: '#6366f1', label: '' }]; calendarUrls = [{ url: '', color: '#e8754f', label: '' }];
calendarDaysAhead = 7; calendarDaysAhead = 7;
markdownContent = ''; markdownContent = '';
metricLabel = ''; metricLabel = '';
@@ -350,7 +350,7 @@
} }
function addCalendarUrl() { function addCalendarUrl() {
calendarUrls = [...calendarUrls, { url: '', color: '#6366f1', label: '' }]; calendarUrls = [...calendarUrls, { url: '', color: '#e8754f', label: '' }];
} }
function removeCalendarUrl(index: number) { function removeCalendarUrl(index: number) {
@@ -367,7 +367,7 @@
// Input CSS class for reuse // Input CSS class for reuse
const inputClass = 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> </script>
<div class="mb-3 rounded-lg border border-border bg-muted/50 p-3"> <div class="mb-3 rounded-lg border border-border bg-muted/50 p-3">
@@ -505,7 +505,7 @@
</div> </div>
<div> <div>
<span class="mb-1 block text-sm font-medium text-foreground">Select Apps</span> <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)} {#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"> <label class="flex items-center gap-2 rounded px-2 py-1 text-sm text-foreground hover:bg-accent">
<input <input
@@ -613,7 +613,7 @@
<span class="mb-1 block text-sm font-medium text-foreground">Metrics</span> <span class="mb-1 block text-sm font-medium text-foreground">Metrics</span>
<div class="flex flex-wrap gap-2"> <div class="flex flex-wrap gap-2">
{#each ['cpu', 'ram', 'disk', 'swap', 'network'] as metric (metric)} {#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 <input
type="checkbox" type="checkbox"
checked={sysStatsMetrics.includes(metric)} checked={sysStatsMetrics.includes(metric)}
@@ -704,7 +704,7 @@
<input <input
type="color" type="color"
bind:value={calendarUrls[i].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" title="Calendar color"
/> />
</div> </div>
@@ -1038,7 +1038,7 @@
<button <button
type="button" type="button"
onclick={handleSubmitWidget} 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')} {$t('common.add')}
</button> </button>
@@ -46,7 +46,7 @@
<div class="absolute inset-0 z-10 rounded-xl bg-black/5 transition-opacity"> <div class="absolute inset-0 z-10 rounded-xl bg-black/5 transition-opacity">
<!-- Top-left: drag handle --> <!-- Top-left: drag handle -->
<div class="absolute left-1.5 top-1.5"> <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"> <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="5" r="1" /><circle cx="15" cy="5" r="1" />
<circle cx="9" cy="12" r="1" /><circle cx="15" cy="12" r="1" /> <circle cx="9" cy="12" r="1" /><circle cx="15" cy="12" r="1" />
@@ -62,7 +62,7 @@
<button <button
type="button" type="button"
onclick={() => { showSizePicker = !showSizePicker; previewSpan = null; }} 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'}" {showSizePicker ? 'bg-primary text-primary-foreground' : 'hover:bg-accent hover:text-foreground'}"
title={$t('widget.resize') ?? 'Resize'} title={$t('widget.resize') ?? 'Resize'}
> >
@@ -78,7 +78,7 @@
<button <button
type="button" type="button"
onclick={() => onEdit(widgetId)} 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'} 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"> <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 <button
type="button" type="button"
onclick={() => { showDeleteConfirm = true; }} 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'} 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"> <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 --> <!-- Size picker popover -->
{#if showSizePicker && onResize} {#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"> <div class="mb-1.5 text-[10px] font-medium uppercase tracking-wide text-muted-foreground">
{$t('widget.width') ?? 'Width'} {$t('widget.width') ?? 'Width'}
</div> </div>
@@ -58,7 +58,7 @@
</script> </script>
{#snippet skeleton()} {#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> <span class="text-xs text-muted-foreground"></span>
</div> </div>
{/snippet} {/snippet}
@@ -163,7 +163,7 @@
}} /> }} />
{/await} {/await}
{:else} {: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> <span class="text-xs text-muted-foreground">{$t('widget.type', { values: { type: widget.type } })}</span>
</div> </div>
{/if} {/if}
@@ -82,7 +82,7 @@
<!-- svelte-ignore a11y_click_events_have_key_events --> <!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<div <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()} onclick={(e) => e.stopPropagation()}
transition:scale={{ start: 0.95, duration: 150 }} transition:scale={{ start: 0.95, duration: 150 }}
> >
@@ -110,7 +110,7 @@
type="text" type="text"
bind:value={filterQuery} bind:value={filterQuery}
placeholder={$t('widget.search_type') ?? 'Search widget types...'} 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>
</div> </div>
@@ -127,7 +127,7 @@
onclick={() => onSelect(wt.value)} onclick={() => onSelect(wt.value)}
class="flex items-start gap-3 rounded-xl px-3 py-3 text-left transition-colors hover:bg-accent" 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"> <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)} {#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')} {#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(() => { const severityStyles = $derived.by(() => {
switch (data.severity) { switch (data.severity) {
case 'critical': return 'border-red-500/50 bg-red-500/10 text-red-400'; case 'critical':
case 'warning': return 'border-yellow-500/50 bg-yellow-500/10 text-yellow-400'; return 'border-status-offline/40 bg-status-offline/10 text-status-offline-ink';
default: return 'border-blue-500/50 bg-blue-500/10 text-blue-400'; 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> </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"> <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} {severityIcon}
</span> </span>
@@ -15,7 +15,15 @@
data.labels.length > 0 ? Math.max(100 / data.labels.length - 2, 4) : 10 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> </script>
<div class="p-3"> <div class="p-3">
@@ -12,9 +12,9 @@
const color = $derived.by(() => { const color = $derived.by(() => {
const warn = data.thresholds?.warning ?? 60; const warn = data.thresholds?.warning ?? 60;
const crit = data.thresholds?.critical ?? 85; const crit = data.thresholds?.critical ?? 85;
if (percentage >= crit) return '#ef4444'; // red if (percentage >= crit) return 'var(--status-offline)';
if (percentage >= warn) return '#eab308'; // yellow if (percentage >= warn) return 'var(--status-degraded)';
return '#22c55e'; // green return 'var(--status-online)';
}); });
// SVG circle math // SVG circle math
@@ -12,13 +12,13 @@
); );
const trendColor = $derived( 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> </script>
<div class="flex flex-col items-center justify-center gap-1 p-4 text-center"> <div class="flex flex-col items-center justify-center gap-1 p-4 text-center">
<div class="flex items-baseline gap-1"> <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} {#if data.unit}
<span class="text-sm text-muted-foreground">{data.unit}</span> <span class="text-sm text-muted-foreground">{data.unit}</span>
{/if} {/if}
+5
View File
@@ -355,6 +355,7 @@
"theme.toggle": "Toggle theme (current: {mode})", "theme.toggle": "Toggle theme (current: {mode})",
"theme.title": "Theme: {mode}", "theme.title": "Theme: {mode}",
"bg.cozy": "Cozy Glow",
"bg.mesh": "Mesh Gradient", "bg.mesh": "Mesh Gradient",
"bg.particles": "Particles", "bg.particles": "Particles",
"bg.aurora": "Aurora", "bg.aurora": "Aurora",
@@ -370,6 +371,10 @@
"home.welcome": "Welcome, {name}. No default board is configured yet.", "home.welcome": "Welcome, {name}. No default board is configured yet.",
"home.view_boards": "View Boards", "home.view_boards": "View Boards",
"home.browse_apps": "Browse Apps", "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", "language.label": "Language",
+5
View File
@@ -339,6 +339,7 @@
"theme.system": "Системная", "theme.system": "Системная",
"theme.toggle": "Переключить тему (текущая: {mode})", "theme.toggle": "Переключить тему (текущая: {mode})",
"theme.title": "Тема: {mode}", "theme.title": "Тема: {mode}",
"bg.cozy": "Уютное свечение",
"bg.mesh": "Меш-градиент", "bg.mesh": "Меш-градиент",
"bg.particles": "Частицы", "bg.particles": "Частицы",
"bg.aurora": "Сияние", "bg.aurora": "Сияние",
@@ -352,6 +353,10 @@
"home.welcome": "Добро пожаловать, {name}. Доска по умолчанию ещё не настроена.", "home.welcome": "Добро пожаловать, {name}. Доска по умолчанию ещё не настроена.",
"home.view_boards": "Посмотреть доски", "home.view_boards": "Посмотреть доски",
"home.browse_apps": "Обзор приложений", "home.browse_apps": "Обзор приложений",
"home.greet_morning": "Доброе утро",
"home.greet_afternoon": "Добрый день",
"home.greet_evening": "Добрый вечер",
"home.greet_night": "Всё ещё не спите",
"language.label": "Язык", "language.label": "Язык",
"settings.title": "Настройки", "settings.title": "Настройки",
"settings.theme": "Режим темы", "settings.theme": "Режим темы",
+7 -7
View File
@@ -7,7 +7,7 @@ const BG_TYPE_KEY = 'wal-bg-type';
const CARD_STYLE_KEY = 'wal-card-style'; const CARD_STYLE_KEY = 'wal-card-style';
export type ThemeMode = 'dark' | 'light' | 'system'; 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'; export type CardStyle = 'solid' | 'glass' | 'outline';
function getStoredValue<T>(key: string, fallback: T): T { function getStoredValue<T>(key: string, fallback: T): T {
@@ -35,9 +35,9 @@ function getStoredNumber(key: string, fallback: number): number {
class ThemeStore { class ThemeStore {
mode = $state<ThemeMode>('system'); mode = $state<ThemeMode>('system');
primaryHue = $state(220); primaryHue = $state(16); // Cozy Home default: terracotta
primarySaturation = $state(70); primarySaturation = $state(72);
backgroundType = $state<BackgroundType>('mesh'); backgroundType = $state<BackgroundType>('cozy');
cardStyle = $state<CardStyle>('solid'); cardStyle = $state<CardStyle>('solid');
#systemPreference: 'dark' | 'light' = 'dark'; #systemPreference: 'dark' | 'light' = 'dark';
@@ -52,9 +52,9 @@ class ThemeStore {
constructor() { constructor() {
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
this.mode = getStoredValue<ThemeMode>(THEME_STORAGE_KEY, 'system'); this.mode = getStoredValue<ThemeMode>(THEME_STORAGE_KEY, 'system');
this.primaryHue = getStoredNumber(PRIMARY_HUE_KEY, 220); this.primaryHue = getStoredNumber(PRIMARY_HUE_KEY, 16);
this.primarySaturation = getStoredNumber(PRIMARY_SAT_KEY, 70); this.primarySaturation = getStoredNumber(PRIMARY_SAT_KEY, 72);
this.backgroundType = getStoredValue<BackgroundType>(BG_TYPE_KEY, 'mesh'); this.backgroundType = getStoredValue<BackgroundType>(BG_TYPE_KEY, 'cozy');
this.cardStyle = getStoredValue<CardStyle>(CARD_STYLE_KEY, 'solid'); this.cardStyle = getStoredValue<CardStyle>(CARD_STYLE_KEY, 'solid');
const mql = window.matchMedia('(prefers-color-scheme: dark)'); const mql = window.matchMedia('(prefers-color-scheme: dark)');
+1 -1
View File
@@ -180,7 +180,7 @@ export const DEFAULTS = {
REFRESH_TOKEN_EXPIRY_DAYS: 7, REFRESH_TOKEN_EXPIRY_DAYS: 7,
REMEMBER_ME_REFRESH_TOKEN_EXPIRY_DAYS: 30, REMEMBER_ME_REFRESH_TOKEN_EXPIRY_DAYS: 30,
DEFAULT_THEME: 'dark', DEFAULT_THEME: 'dark',
DEFAULT_PRIMARY_COLOR: '#6366f1', DEFAULT_PRIMARY_COLOR: '#e8754f',
SYSTEM_SETTINGS_ID: 'singleton', SYSTEM_SETTINGS_ID: 'singleton',
SALT_ROUNDS: 12, SALT_ROUNDS: 12,
INVITE_DEFAULT_EXPIRY_DAYS: 7, INVITE_DEFAULT_EXPIRY_DAYS: 7,
+1 -1
View File
@@ -48,7 +48,7 @@
{#snippet actions()} {#snippet actions()}
<a <a
href="/" 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')} {$t('error.back_to_dashboard')}
</a> </a>
+41 -7
View File
@@ -3,33 +3,67 @@
import type { PageData } from './$types.js'; import type { PageData } from './$types.js';
let { data }: { data: PageData } = $props(); 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> </script>
<svelte:head> <svelte:head>
<title>{$t('app_title')}</title> <title>{$t('app_title')}</title>
</svelte:head> </svelte:head>
<div class="flex min-h-[60vh] items-center justify-center p-6"> <div class="relative flex min-h-[70vh] items-center justify-center overflow-hidden p-6">
<div class="text-center"> <!-- warm ambient blobs -->
<h1 class="text-4xl font-bold text-foreground">{$t('app_title')}</h1> <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} {#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 } })} {$t('home.welcome', { values: { name: data.user.displayName } })}
</p> </p>
<div class="mt-6 flex items-center justify-center gap-3"> <div class="mt-8 flex items-center justify-center gap-3">
<a <a
href="/boards" 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')} {$t('home.view_boards')}
</a> </a>
<a <a
href="/apps" 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')} {$t('home.browse_apps')}
</a> </a>
</div> </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} {/if}
</div> </div>
</div> </div>
+1 -1
View File
@@ -26,7 +26,7 @@
{#snippet actions()} {#snippet actions()}
<a <a
href="/" 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')} {$t('error.back_to_dashboard')}
</a> </a>
+1 -1
View File
@@ -25,7 +25,7 @@
<div class="mx-auto max-w-6xl"> <div class="mx-auto max-w-6xl">
<!-- Admin header. On md+: single-row, nav scrolls horizontally if needed. <!-- Admin header. On md+: single-row, nav scrolls horizontally if needed.
Below md: stacks so username doesn't squeeze the nav onto two lines. --> 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"> <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> <span class="text-sm font-semibold text-foreground">{$t('admin.panel')}</span>
<div class="-mx-1 flex gap-1 overflow-x-auto px-1"> <div class="-mx-1 flex gap-1 overflow-x-auto px-1">
+4 -4
View File
@@ -28,7 +28,7 @@
<button <button
type="button" type="button"
onclick={() => (showCreateForm = !showCreateForm)} 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')} {showCreateForm ? $t('common.cancel') : $t('admin.create_group')}
</button> </button>
@@ -46,7 +46,7 @@
name="name" name="name"
type="text" type="text"
bind:value={$form.name} 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 required
/> />
{#if $errors.name}<span class="text-xs text-destructive">{$errors.name}</span>{/if} {#if $errors.name}<span class="text-xs text-destructive">{$errors.name}</span>{/if}
@@ -58,7 +58,7 @@
name="description" name="description"
type="text" type="text"
bind:value={$form.description} 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>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@@ -77,7 +77,7 @@
{/if} {/if}
<button <button
type="submit" 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')} {$t('admin.create_group')}
</button> </button>
+9 -9
View File
@@ -98,7 +98,7 @@
<button <button
type="button" type="button"
onclick={() => (showForm = true)} 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 Generate invite
</button> </button>
@@ -112,21 +112,21 @@
{/if} {/if}
{#if createdUrl} {#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> <h3 class="mb-1 text-sm font-semibold text-foreground">Invite created</h3>
<p class="mb-3 text-xs text-muted-foreground"> <p class="mb-3 text-xs text-muted-foreground">
Share this link with the recipient. It will only be shown once. Share this link with the recipient. It will only be shown once.
</p> </p>
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<code <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} {createdUrl}
</code> </code>
<button <button
type="button" type="button"
onclick={() => createdUrl && copyUrl(createdUrl)} 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 Copy
</button> </button>
@@ -155,7 +155,7 @@
type="email" type="email"
bind:value={email} bind:value={email}
placeholder="jane@example.com" 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>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
@@ -164,7 +164,7 @@
<select <select
id="inv-role" id="inv-role"
bind:value={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="user">User</option>
<option value="admin">Admin</option> <option value="admin">Admin</option>
@@ -180,7 +180,7 @@
min="1" min="1"
max="90" max="90"
bind:value={expiresInDays} 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>
</div> </div>
@@ -195,7 +195,7 @@
<button <button
type="submit" type="submit"
disabled={creating} 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'} {creating ? 'Creating…' : 'Create invite'}
</button> </button>
@@ -226,7 +226,7 @@
<td class="px-4 py-3"> <td class="px-4 py-3">
<span <span
class="rounded-full px-2 py-0.5 text-xs {status === 'Active' 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' : status === 'Used'
? 'bg-muted text-muted-foreground' ? 'bg-muted text-muted-foreground'
: 'bg-destructive/15 text-destructive'}" : 'bg-destructive/15 text-destructive'}"
@@ -102,13 +102,13 @@
required required
bind:value={email} bind:value={email}
placeholder="user@example.com" 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> </label>
<button <button
type="submit" type="submit"
disabled={issuing} 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'} {issuing ? 'Issuing…' : 'Issue link'}
</button> </button>
@@ -123,13 +123,13 @@
type="text" type="text"
readonly readonly
value={issuedLink.url} 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()} onclick={(e) => (e.currentTarget as HTMLInputElement).select()}
/> />
<button <button
type="button" type="button"
onclick={() => issuedLink && copyLink(issuedLink.url)} 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'} {copyToast ? 'Copied!' : 'Copy'}
</button> </button>
+6 -6
View File
@@ -28,7 +28,7 @@
<button <button
type="button" type="button"
onclick={() => (showCreateForm = !showCreateForm)} 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')} {showCreateForm ? $t('common.cancel') : $t('admin.create_user')}
</button> </button>
@@ -46,7 +46,7 @@
name="email" name="email"
type="email" type="email"
bind:value={$form.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 required
/> />
{#if $errors.email}<span class="text-xs text-destructive">{$errors.email}</span>{/if} {#if $errors.email}<span class="text-xs text-destructive">{$errors.email}</span>{/if}
@@ -58,7 +58,7 @@
name="displayName" name="displayName"
type="text" type="text"
bind:value={$form.displayName} 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 required
/> />
{#if $errors.displayName}<span class="text-xs text-destructive">{$errors.displayName}</span>{/if} {#if $errors.displayName}<span class="text-xs text-destructive">{$errors.displayName}</span>{/if}
@@ -70,7 +70,7 @@
name="password" name="password"
type="password" type="password"
bind:value={$form.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} {#if $errors.password}<span class="text-xs text-destructive">{$errors.password}</span>{/if}
</div> </div>
@@ -80,7 +80,7 @@
id="role" id="role"
name="role" name="role"
bind:value={$form.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="user">{$t('admin.role_user')}</option>
<option value="admin">{$t('admin.role_admin')}</option> <option value="admin">{$t('admin.role_admin')}</option>
@@ -92,7 +92,7 @@
{/if} {/if}
<button <button
type="submit" 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')} {$t('admin.create_user')}
</button> </button>
+1 -1
View File
@@ -167,7 +167,7 @@ export const POST: RequestHandler = async (event) => {
create: { create: {
id: DEFAULTS.SYSTEM_SETTINGS_ID, id: DEFAULTS.SYSTEM_SETTINGS_ID,
defaultTheme: themeData.data.defaultTheme ?? 'dark', 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