feat(ui): migrate entire UI to "Cozy Home" design
Warm, friendly redesign replacing the generic cold-shadcn look. Built as a swappable token bundle so other presets can be added later; dark mode and the user-tunable accent hue are retained. Foundation - app.css: warm cream (light) + "dusk" (dark) token system; terracotta accent (default hue 16); pastel --room-* palette; vivid --status-* (dots/bars) plus AA-legible --status-*-ink (text); soft warm shadows; --radius 1rem; font tokens - Fonts: Fraunces (display) + Figtree (body), self-hosted in static/fonts (no Google CDN) so offline/LAN installs work; system-ui fallbacks kept - h1/h2/h3 render in Fraunces via base layer Chrome and surfaces - Sidebar, Header, home, AppCard/BoardCard, BoardHeader, sections, favorites - 29 widgets + integration renderers: cozy card shells, room-palette charts - Default background is a static warm "cozy" glow (mesh demoted, rAF gated on prefers-reduced-motion) System-wide - Status colors tokenized (no raw bg/text-*-500 or status hex); success/warning to status tokens, categorical to room palette, errors to destructive - Inputs rounded-xl; buttons rounded-xl; cards/dialogs rounded-[1.4rem]; soft-shadow vocabulary only; focus-visible:ring-primary/30 - Forms, admin tables (now cozy cards), dialogs, popovers, auth screens a11y: reduced-motion guards; darker status "ink" text for AA on cream. Known tradeoff: terracotta primary + white button text ~2.96:1 (signature color, user-tunable). Verified: svelte-check 0/0, build ok, 274 tests pass, eslint 0 errors. Design refs + system sheet in design-mockups/.
This commit is contained in:
@@ -0,0 +1,789 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Web App Launcher — Command Deck</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Saira:wght@400;500;600;700&family=Saira+Condensed:wght@500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #070a0d;
|
||||||
|
--panel: #0d1217;
|
||||||
|
--panel-2: #10171e;
|
||||||
|
--line: #1d2730;
|
||||||
|
--line-bright: #2b3946;
|
||||||
|
--ink: #e7eef3;
|
||||||
|
--ink-dim: #7c8b97;
|
||||||
|
--ink-faint: #4a5763;
|
||||||
|
--accent: #36e0a4; /* tactical green */
|
||||||
|
--accent-2: #ffb020; /* amber */
|
||||||
|
--danger: #ff4d5e;
|
||||||
|
--warn: #ffb020;
|
||||||
|
--grid: rgba(54, 224, 164, 0.04);
|
||||||
|
--radius: 4px;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'Saira', system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
/* subtle scanline grid backdrop */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background-image:
|
||||||
|
linear-gradient(var(--grid) 1px, transparent 1px),
|
||||||
|
linear-gradient(90deg, var(--grid) 1px, transparent 1px);
|
||||||
|
background-size: 40px 40px;
|
||||||
|
mask-image: radial-gradient(ellipse 80% 60% at 70% 0%, #000 30%, transparent 90%);
|
||||||
|
}
|
||||||
|
.app {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 74px 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.mono {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
}
|
||||||
|
.cond {
|
||||||
|
font-family: 'Saira Condensed', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Rail ===== */
|
||||||
|
.rail {
|
||||||
|
border-right: 1px solid var(--line);
|
||||||
|
background: linear-gradient(180deg, var(--panel), #080c10);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 16px 0;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 18px;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px #0a1f18,
|
||||||
|
0 0 18px -4px var(--accent);
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.logo svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.rail-btn {
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
position: relative;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.15s;
|
||||||
|
}
|
||||||
|
.rail-btn:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--panel-2);
|
||||||
|
}
|
||||||
|
.rail-btn.active {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.rail-btn.active::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -16px;
|
||||||
|
top: 10px;
|
||||||
|
bottom: 10px;
|
||||||
|
width: 2px;
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 8px var(--accent);
|
||||||
|
}
|
||||||
|
.rail-btn svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.rail-spacer {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
.rail-avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: linear-gradient(135deg, #1a2a22, #0f1a14);
|
||||||
|
border: 1px solid var(--line-bright);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Main ===== */
|
||||||
|
.main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
height: 60px;
|
||||||
|
padding: 0 26px;
|
||||||
|
border-bottom: 1px solid var(--line);
|
||||||
|
background: rgba(8, 12, 16, 0.6);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
}
|
||||||
|
.crumbs {
|
||||||
|
font-family: 'Saira Condensed';
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.14em;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.crumbs b {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 340px;
|
||||||
|
max-width: 38vw;
|
||||||
|
background: var(--panel);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 9px 12px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-size: 13px;
|
||||||
|
cursor: text;
|
||||||
|
transition: 0.15s;
|
||||||
|
}
|
||||||
|
.search:hover {
|
||||||
|
border-color: var(--line-bright);
|
||||||
|
}
|
||||||
|
.search svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
.search .kbd {
|
||||||
|
margin-left: auto;
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
.ico-btn {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.15s;
|
||||||
|
background: var(--panel);
|
||||||
|
}
|
||||||
|
.ico-btn:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
border-color: var(--line-bright);
|
||||||
|
}
|
||||||
|
.ico-btn svg {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 26px;
|
||||||
|
max-width: 1320px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* status bar */
|
||||||
|
.statline {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
background: var(--line);
|
||||||
|
}
|
||||||
|
.stat {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--panel);
|
||||||
|
padding: 16px 18px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.stat .lbl {
|
||||||
|
font-family: 'Saira Condensed';
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.stat .val {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
margin-top: 6px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.stat .val.ok {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.stat .val.warn {
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
.stat .val.bad {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.stat .sub {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.stat::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(90deg, var(--accent), transparent);
|
||||||
|
}
|
||||||
|
.stat.s2::after {
|
||||||
|
background: linear-gradient(90deg, var(--accent-2), transparent);
|
||||||
|
}
|
||||||
|
.stat.s3::after {
|
||||||
|
background: linear-gradient(90deg, #3aa0ff, transparent);
|
||||||
|
}
|
||||||
|
.stat.s4::after {
|
||||||
|
background: linear-gradient(90deg, var(--danger), transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
margin: 30px 0 16px;
|
||||||
|
}
|
||||||
|
.sec-head h2 {
|
||||||
|
font-family: 'Saira Condensed';
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.sec-head .rule {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, var(--line-bright), transparent);
|
||||||
|
}
|
||||||
|
.sec-head .count {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* app grid */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(248px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.node {
|
||||||
|
background: linear-gradient(180deg, var(--panel), var(--panel-2));
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition:
|
||||||
|
transform 0.15s,
|
||||||
|
border-color 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.node::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: radial-gradient(circle at 100% 0%, rgba(54, 224, 164, 0.08), transparent 60%);
|
||||||
|
opacity: 0;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
.node:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--line-bright);
|
||||||
|
box-shadow: 0 10px 30px -12px #000;
|
||||||
|
}
|
||||||
|
.node:hover::before {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.node-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.node-ico {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: #0a0f13;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.node-name {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.node-cat {
|
||||||
|
font-family: 'Saira Condensed';
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.led {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: auto;
|
||||||
|
flex-shrink: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.led.ok {
|
||||||
|
background: var(--accent);
|
||||||
|
box-shadow: 0 0 10px var(--accent);
|
||||||
|
}
|
||||||
|
.led.ok::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 1px solid var(--accent);
|
||||||
|
opacity: 0.4;
|
||||||
|
animation: ping 2s ease-out infinite;
|
||||||
|
}
|
||||||
|
.led.warn {
|
||||||
|
background: var(--warn);
|
||||||
|
box-shadow: 0 0 10px var(--warn);
|
||||||
|
}
|
||||||
|
.led.bad {
|
||||||
|
background: var(--danger);
|
||||||
|
box-shadow: 0 0 10px var(--danger);
|
||||||
|
}
|
||||||
|
@keyframes ping {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.8);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(2.2);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.node-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 16px;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.node-foot .up {
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
}
|
||||||
|
.node-foot .up b {
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.node-foot .up.bad b {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
.spark {
|
||||||
|
height: 22px;
|
||||||
|
width: 96px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-note {
|
||||||
|
margin-top: 40px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'JetBrains Mono';
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* entrance */
|
||||||
|
@keyframes rise {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.node,
|
||||||
|
.stat {
|
||||||
|
animation: rise 0.5s both;
|
||||||
|
}
|
||||||
|
.stat:nth-child(2) {
|
||||||
|
animation-delay: 0.05s;
|
||||||
|
}
|
||||||
|
.stat:nth-child(3) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
.stat:nth-child(4) {
|
||||||
|
animation-delay: 0.15s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(2) {
|
||||||
|
animation-delay: 0.16s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(3) {
|
||||||
|
animation-delay: 0.22s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(4) {
|
||||||
|
animation-delay: 0.28s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(5) {
|
||||||
|
animation-delay: 0.34s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(6) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(7) {
|
||||||
|
animation-delay: 0.46s;
|
||||||
|
}
|
||||||
|
.grid .node:nth-child(8) {
|
||||||
|
animation-delay: 0.52s;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="app">
|
||||||
|
<!-- Rail -->
|
||||||
|
<nav class="rail">
|
||||||
|
<div class="logo" title="Launcher">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<a class="rail-btn active" title="Overview"
|
||||||
|
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="2" />
|
||||||
|
<line x1="3" y1="9" x2="21" y2="9" />
|
||||||
|
<line x1="9" y1="21" x2="9" y2="9" /></svg
|
||||||
|
></a>
|
||||||
|
<a class="rail-btn" title="Apps"
|
||||||
|
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<line x1="3" y1="12" x2="21" y2="12" />
|
||||||
|
<path d="M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" /></svg
|
||||||
|
></a>
|
||||||
|
<a class="rail-btn" title="Status"
|
||||||
|
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" /></svg
|
||||||
|
></a>
|
||||||
|
<a class="rail-btn" title="Admin"
|
||||||
|
><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
<path
|
||||||
|
d="M19 12a7 7 0 0 0-.1-1l2-1.6-2-3.4-2.4 1a7 7 0 0 0-1.7-1l-.4-2.6h-4l-.4 2.6a7 7 0 0 0-1.7 1l-2.4-1-2 3.4 2 1.6a7 7 0 0 0 0 2l-2 1.6 2 3.4 2.4-1a7 7 0 0 0 1.7 1l.4 2.6h4l.4-2.6a7 7 0 0 0 1.7-1l2.4 1 2-3.4-2-1.6a7 7 0 0 0 .1-1z"
|
||||||
|
/></svg
|
||||||
|
></a>
|
||||||
|
<div class="rail-spacer"></div>
|
||||||
|
<div class="rail-avatar">AD</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<div class="main">
|
||||||
|
<header class="topbar">
|
||||||
|
<div class="crumbs">SYSTEMS / <b>OVERVIEW</b></div>
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="7" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
</svg>
|
||||||
|
Search apps, boards, commands…
|
||||||
|
<span class="kbd">⌘K</span>
|
||||||
|
</div>
|
||||||
|
<div class="ico-btn" title="Notifications">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||||
|
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="ico-btn" title="Theme">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path
|
||||||
|
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<!-- Status line -->
|
||||||
|
<div class="statline">
|
||||||
|
<div class="stat">
|
||||||
|
<div class="lbl">Services Online</div>
|
||||||
|
<div class="val ok">08 / 10</div>
|
||||||
|
<div class="sub">2 require attention</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat s2">
|
||||||
|
<div class="lbl">Avg Response</div>
|
||||||
|
<div class="val warn">
|
||||||
|
142<span style="font-size: 14px; color: var(--ink-faint)"> ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="sub">p95 over 24h</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat s3">
|
||||||
|
<div class="lbl">Fleet Uptime</div>
|
||||||
|
<div class="val" style="color: #3aa0ff">99.4%</div>
|
||||||
|
<div class="sub">rolling 30 days</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat s4">
|
||||||
|
<div class="lbl">UPS Load</div>
|
||||||
|
<div class="val bad">61%</div>
|
||||||
|
<div class="sub">est. 38 min on battery</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Favorites / pinned -->
|
||||||
|
<div class="sec-head">
|
||||||
|
<h2>Pinned Services</h2>
|
||||||
|
<span class="rule"></span><span class="count">8 ACTIVE</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<!-- 1 Jellyfin -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">🎬</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Jellyfin</div>
|
||||||
|
<div class="node-cat">Media</div>
|
||||||
|
</div>
|
||||||
|
<div class="led ok"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#36e0a4"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,16 12,14 24,17 36,9 48,12 60,7 72,10 84,5 96,8"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b>99.9%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 2 Immich -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">📷</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Immich</div>
|
||||||
|
<div class="node-cat">Photos</div>
|
||||||
|
</div>
|
||||||
|
<div class="led ok"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#36e0a4"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,12 12,13 24,11 36,12 48,10 60,11 72,9 84,11 96,10"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b>100%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 3 Gitea -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">🌿</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Gitea</div>
|
||||||
|
<div class="node-cat">Git</div>
|
||||||
|
</div>
|
||||||
|
<div class="led ok"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#36e0a4"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,14 12,10 24,12 36,8 48,11 60,9 72,13 84,8 96,9"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b>99.8%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 4 Portainer -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">🐳</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Portainer</div>
|
||||||
|
<div class="node-cat">Containers</div>
|
||||||
|
</div>
|
||||||
|
<div class="led warn"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#ffb020"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,10 12,12 24,9 36,15 48,11 60,18 72,12 84,16 96,13"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b style="color: var(--warn)">98.1%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 5 Pi-hole -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">🛡️</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Pi-hole</div>
|
||||||
|
<div class="node-cat">DNS</div>
|
||||||
|
</div>
|
||||||
|
<div class="led ok"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#36e0a4"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,13 12,11 24,12 36,10 48,11 60,9 72,10 84,8 96,9"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b>100%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 6 Planka -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">📋</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Planka</div>
|
||||||
|
<div class="node-cat">Kanban</div>
|
||||||
|
</div>
|
||||||
|
<div class="led ok"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#36e0a4"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,15 12,12 24,14 36,11 48,12 60,10 72,12 84,9 96,11"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b>99.5%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 7 Deluge -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">⬇️</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Deluge</div>
|
||||||
|
<div class="node-cat">Downloads</div>
|
||||||
|
</div>
|
||||||
|
<div class="led bad"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#ff4d5e"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,9 12,11 24,14 36,12 48,18 60,16 72,20 84,19 96,21"
|
||||||
|
/></svg
|
||||||
|
><span class="up bad"><b>OFFLINE</b></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- 8 Pi-hole / NPM -->
|
||||||
|
<div class="node">
|
||||||
|
<div class="node-top">
|
||||||
|
<div class="node-ico">🔀</div>
|
||||||
|
<div>
|
||||||
|
<div class="node-name">Nginx Proxy Mgr</div>
|
||||||
|
<div class="node-cat">Network</div>
|
||||||
|
</div>
|
||||||
|
<div class="led ok"></div>
|
||||||
|
</div>
|
||||||
|
<div class="node-foot">
|
||||||
|
<svg class="spark" viewBox="0 0 96 22" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#36e0a4"
|
||||||
|
stroke-width="1.5"
|
||||||
|
points="0,12 12,11 24,12 36,10 48,11 60,11 72,9 84,10 96,9"
|
||||||
|
/></svg
|
||||||
|
><span class="up"><b>99.9%</b> 24h</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer-note">
|
||||||
|
// COMMAND DECK — Saira + JetBrains Mono · tactical dark · LED telemetry · monospace
|
||||||
|
data
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,915 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Web App Launcher — Aurora Glass</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Manrope:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #0a0a14;
|
||||||
|
--ink: #f3f2fb;
|
||||||
|
--ink-dim: #a7a6c4;
|
||||||
|
--ink-faint: #6f6e90;
|
||||||
|
--accent-h: 265; /* user-tunable hue → this is the killer feature */
|
||||||
|
--accent: hsl(var(--accent-h) 90% 66%);
|
||||||
|
--accent-2: hsl(calc(var(--accent-h) + 60) 85% 64%);
|
||||||
|
--accent-soft: hsl(var(--accent-h) 90% 66% / 0.14);
|
||||||
|
--glass: rgba(255, 255, 255, 0.05);
|
||||||
|
--glass-2: rgba(255, 255, 255, 0.07);
|
||||||
|
--glass-line: rgba(255, 255, 255, 0.1);
|
||||||
|
--ok: #34e0a1;
|
||||||
|
--warn: #ffc24b;
|
||||||
|
--bad: #ff5d73;
|
||||||
|
--radius: 18px;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'Manrope', system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow-x: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
/* aurora mesh */
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: -20%;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
filter: blur(60px);
|
||||||
|
opacity: 0.9;
|
||||||
|
background:
|
||||||
|
radial-gradient(40% 40% at 18% 22%, hsl(var(--accent-h) 90% 60% / 0.55), transparent 70%),
|
||||||
|
radial-gradient(
|
||||||
|
38% 38% at 82% 18%,
|
||||||
|
hsl(calc(var(--accent-h) + 70) 90% 60% / 0.42),
|
||||||
|
transparent 70%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
45% 45% at 70% 85%,
|
||||||
|
hsl(calc(var(--accent-h) - 40) 90% 58% / 0.4),
|
||||||
|
transparent 72%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
40% 40% at 25% 90%,
|
||||||
|
hsl(calc(var(--accent-h) + 120) 80% 55% / 0.3),
|
||||||
|
transparent 72%
|
||||||
|
);
|
||||||
|
animation: drift 22s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
@keyframes drift {
|
||||||
|
0% {
|
||||||
|
transform: translate(0, 0) scale(1);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translate(-3%, 2%) scale(1.08);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
body::after {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background: radial-gradient(
|
||||||
|
120% 120% at 50% -10%,
|
||||||
|
transparent 40%,
|
||||||
|
rgba(10, 10, 20, 0.6) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.shell {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 248px 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sidebar (glass) */
|
||||||
|
.side {
|
||||||
|
margin: 16px 0 16px 16px;
|
||||||
|
border-radius: 24px;
|
||||||
|
padding: 20px;
|
||||||
|
background: var(--glass);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
backdrop-filter: blur(26px) saturate(160%);
|
||||||
|
-webkit-backdrop-filter: blur(26px) saturate(160%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
margin-bottom: 26px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
}
|
||||||
|
.brand .mark {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 8px 24px -6px var(--accent);
|
||||||
|
}
|
||||||
|
.brand .mark svg {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
.brand .name {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 17px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.brand .name span {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Manrope';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
.nav-grp {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.13em;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
margin: 14px 8px 8px;
|
||||||
|
}
|
||||||
|
.nav-i {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 11px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 14.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nav-i svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
.nav-i:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--glass-2);
|
||||||
|
}
|
||||||
|
.nav-i.on {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--accent-soft);
|
||||||
|
box-shadow: inset 0 0 0 1px hsl(var(--accent-h) 90% 66% / 0.35);
|
||||||
|
}
|
||||||
|
.nav-i.on::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
left: -20px;
|
||||||
|
top: 9px;
|
||||||
|
bottom: 9px;
|
||||||
|
width: 3px;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
.nav-i .dot {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.side-foot {
|
||||||
|
margin-top: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--glass);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
}
|
||||||
|
.av {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.side-foot .who {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
.side-foot .who span {
|
||||||
|
display: block;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* main */
|
||||||
|
.main {
|
||||||
|
padding: 30px 34px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.head {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
.hello {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 30px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1.05;
|
||||||
|
}
|
||||||
|
.hello em {
|
||||||
|
font-style: normal;
|
||||||
|
background: linear-gradient(120deg, var(--accent), var(--accent-2));
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
background-clip: text;
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
.sub {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
.searchwrap {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
width: 300px;
|
||||||
|
background: var(--glass);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: 13.5px;
|
||||||
|
cursor: text;
|
||||||
|
}
|
||||||
|
.search svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.search .k {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--glass-2);
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 2px 7px;
|
||||||
|
}
|
||||||
|
.gbtn {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: var(--glass);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
backdrop-filter: blur(20px);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s;
|
||||||
|
}
|
||||||
|
.gbtn:hover {
|
||||||
|
color: var(--ink);
|
||||||
|
background: var(--glass-2);
|
||||||
|
}
|
||||||
|
.gbtn svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* metric row */
|
||||||
|
.metrics {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 28px;
|
||||||
|
}
|
||||||
|
.metric {
|
||||||
|
background: var(--glass);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
backdrop-filter: blur(22px) saturate(150%);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 18px 20px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.metric .ic {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 11px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
background: var(--accent-soft);
|
||||||
|
color: var(--accent);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
.metric .ic svg {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
.metric .v {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-size: 27px;
|
||||||
|
font-weight: 600;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.metric .l {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-size: 13px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.metric .trend {
|
||||||
|
position: absolute;
|
||||||
|
top: 18px;
|
||||||
|
right: 18px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--ok);
|
||||||
|
background: rgba(52, 224, 161, 0.12);
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
.metric .trend.dn {
|
||||||
|
color: var(--bad);
|
||||||
|
background: rgba(255, 93, 115, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sectitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin: 8px 0 16px;
|
||||||
|
}
|
||||||
|
.sectitle h2 {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.sectitle a {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(225px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--glass);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
backdrop-filter: blur(22px) saturate(150%);
|
||||||
|
-webkit-backdrop-filter: blur(22px) saturate(150%);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
transition:
|
||||||
|
transform 0.22s cubic-bezier(0.2, 0.7, 0.2, 1),
|
||||||
|
box-shadow 0.22s,
|
||||||
|
border-color 0.22s;
|
||||||
|
}
|
||||||
|
.card::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.06), transparent 40%);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
border-color: hsl(var(--accent-h) 90% 66% / 0.5);
|
||||||
|
box-shadow: 0 24px 50px -20px hsl(var(--accent-h) 90% 50% / 0.55);
|
||||||
|
}
|
||||||
|
.card .row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
}
|
||||||
|
.ico {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border-radius: 13px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.nm {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 15.5px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.ct {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.pill {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
}
|
||||||
|
.pill.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
background: rgba(52, 224, 161, 0.13);
|
||||||
|
}
|
||||||
|
.pill.warn {
|
||||||
|
color: var(--warn);
|
||||||
|
background: rgba(255, 194, 75, 0.13);
|
||||||
|
}
|
||||||
|
.pill.bad {
|
||||||
|
color: var(--bad);
|
||||||
|
background: rgba(255, 93, 115, 0.13);
|
||||||
|
}
|
||||||
|
.pill .b {
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: currentColor;
|
||||||
|
box-shadow: 0 0 8px currentColor;
|
||||||
|
}
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
.meta .up {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
}
|
||||||
|
.meta .up b {
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.spark {
|
||||||
|
height: 24px;
|
||||||
|
width: 84px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rise {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(14px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.metric,
|
||||||
|
.card {
|
||||||
|
animation: rise 0.55s both;
|
||||||
|
}
|
||||||
|
.metric:nth-child(2) {
|
||||||
|
animation-delay: 0.06s;
|
||||||
|
}
|
||||||
|
.metric:nth-child(3) {
|
||||||
|
animation-delay: 0.12s;
|
||||||
|
}
|
||||||
|
.metric:nth-child(4) {
|
||||||
|
animation-delay: 0.18s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(2) {
|
||||||
|
animation-delay: 0.16s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(3) {
|
||||||
|
animation-delay: 0.22s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(4) {
|
||||||
|
animation-delay: 0.28s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(5) {
|
||||||
|
animation-delay: 0.34s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(6) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(7) {
|
||||||
|
animation-delay: 0.46s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(8) {
|
||||||
|
animation-delay: 0.52s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.swatches {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 30px;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.sw {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: 1px solid var(--glass-line);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="side">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="mark">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="1" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="1" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="1" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="1" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="name">Launcher<span>home cloud</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="nav-grp">Workspace</div>
|
||||||
|
<div class="nav-i on">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||||
|
<path d="M3 9h18M9 21V9" />
|
||||||
|
</svg>
|
||||||
|
Overview
|
||||||
|
</div>
|
||||||
|
<div class="nav-i">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" />
|
||||||
|
</svg>
|
||||||
|
All Apps <span class="dot">10</span>
|
||||||
|
</div>
|
||||||
|
<div class="nav-i">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||||
|
</svg>
|
||||||
|
Status
|
||||||
|
</div>
|
||||||
|
<div class="nav-grp">Boards</div>
|
||||||
|
<div class="nav-i">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||||
|
</svg>
|
||||||
|
Media Center
|
||||||
|
</div>
|
||||||
|
<div class="nav-i">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="3" />
|
||||||
|
</svg>
|
||||||
|
Infrastructure
|
||||||
|
</div>
|
||||||
|
<div class="nav-i">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M12 5v14M5 12h14" />
|
||||||
|
</svg>
|
||||||
|
New board…
|
||||||
|
</div>
|
||||||
|
<div class="side-foot">
|
||||||
|
<div class="av">AD</div>
|
||||||
|
<div class="who">Alexei<span>Administrator</span></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<main class="main">
|
||||||
|
<div class="head">
|
||||||
|
<div>
|
||||||
|
<div class="hello">Good evening, <em>Alexei</em></div>
|
||||||
|
<div class="sub">All systems nominal — 8 of 10 services responding</div>
|
||||||
|
</div>
|
||||||
|
<div class="searchwrap">
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="7" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
</svg>
|
||||||
|
Search… <span class="k">⌘K</span>
|
||||||
|
</div>
|
||||||
|
<div class="gbtn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||||
|
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="gbtn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path
|
||||||
|
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- metrics -->
|
||||||
|
<div class="metrics">
|
||||||
|
<div class="metric">
|
||||||
|
<div class="ic">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="v">8<span style="color: var(--ink-faint); font-size: 18px">/10</span></div>
|
||||||
|
<div class="l">Services online</div>
|
||||||
|
<span class="trend">+2</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="ic">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M12 7v5l3 2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="v">142<span style="color: var(--ink-faint); font-size: 16px">ms</span></div>
|
||||||
|
<div class="l">Avg response</div>
|
||||||
|
<span class="trend dn">+18ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="ic">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M3 12h4l3 8 4-16 3 8h4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="v">99.4%</div>
|
||||||
|
<div class="l">Uptime · 30d</div>
|
||||||
|
<span class="trend">+0.2</span>
|
||||||
|
</div>
|
||||||
|
<div class="metric">
|
||||||
|
<div class="ic">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M13 2 3 14h7l-1 8 10-12h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="v">61%</div>
|
||||||
|
<div class="l">UPS load · 38m</div>
|
||||||
|
<span class="trend dn">batt</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sectitle">
|
||||||
|
<h2>Favorites</h2>
|
||||||
|
<a href="#">View all apps →</a>
|
||||||
|
</div>
|
||||||
|
<div class="apps">
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">🎬</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Jellyfin</div>
|
||||||
|
<div class="ct">Media</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill ok"><span class="b"></span>Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>99.9%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--accent)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,18 11,15 22,17 33,9 44,12 55,7 66,11 76,5 84,8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">📷</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Immich</div>
|
||||||
|
<div class="ct">Photos</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill ok"><span class="b"></span>Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>100%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--accent)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,13 11,14 22,12 33,13 44,11 55,12 66,10 76,12 84,11"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">🌿</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Gitea</div>
|
||||||
|
<div class="ct">Git server</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill ok"><span class="b"></span>Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>99.8%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--accent)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,15 11,10 22,13 33,8 44,12 55,9 66,14 76,8 84,10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">🐳</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Portainer</div>
|
||||||
|
<div class="ct">Containers</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill warn"><span class="b"></span>Slow</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>98.1%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--warn)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,11 11,13 22,9 33,16 44,11 55,19 66,12 76,17 84,13"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">🛡️</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Pi-hole</div>
|
||||||
|
<div class="ct">DNS · Ads</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill ok"><span class="b"></span>Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>100%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--accent)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,14 11,12 22,13 33,11 44,12 55,10 66,11 76,9 84,10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">📋</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Planka</div>
|
||||||
|
<div class="ct">Kanban</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill ok"><span class="b"></span>Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>99.5%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--accent)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,16 11,13 22,15 33,12 44,13 55,11 66,13 76,10 84,12"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">⬇️</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Deluge</div>
|
||||||
|
<div class="ct">Downloads</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill bad"><span class="b"></span>Down</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up" style="color: var(--bad)"
|
||||||
|
><b style="color: var(--bad)">offline</b> · 4m</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--bad)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,10 11,12 22,15 33,13 44,19 55,17 66,21 76,20 84,22"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<div class="ico">🔀</div>
|
||||||
|
<div>
|
||||||
|
<div class="nm">Proxy Mgr</div>
|
||||||
|
<div class="ct">Network</div>
|
||||||
|
</div>
|
||||||
|
<div class="pill ok"><span class="b"></span>Up</div>
|
||||||
|
</div>
|
||||||
|
<div class="meta">
|
||||||
|
<span class="up"><b>99.9%</b> uptime</span
|
||||||
|
><svg class="spark" viewBox="0 0 84 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--accent)"
|
||||||
|
stroke-width="2"
|
||||||
|
points="0,13 11,12 22,13 33,11 44,12 55,12 66,10 76,11 84,10"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="swatches">
|
||||||
|
Accent (user-tunable):
|
||||||
|
<span
|
||||||
|
class="sw"
|
||||||
|
style="background: hsl(265 90% 66%)"
|
||||||
|
onclick="document.documentElement.style.setProperty('--accent-h', '265')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="sw"
|
||||||
|
style="background: hsl(210 90% 60%)"
|
||||||
|
onclick="document.documentElement.style.setProperty('--accent-h', '210')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="sw"
|
||||||
|
style="background: hsl(150 80% 55%)"
|
||||||
|
onclick="document.documentElement.style.setProperty('--accent-h', '150')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="sw"
|
||||||
|
style="background: hsl(20 90% 62%)"
|
||||||
|
onclick="document.documentElement.style.setProperty('--accent-h', '20')"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
class="sw"
|
||||||
|
style="background: hsl(330 85% 65%)"
|
||||||
|
onclick="document.documentElement.style.setProperty('--accent-h', '330')"
|
||||||
|
></span>
|
||||||
|
— try clicking; the whole UI + aurora retints live
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,643 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Web App Launcher — Editorial</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Instrument+Serif:ital@0;1&family=Hanken+Grotesk:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--paper: #f4f1ea; /* warm paper */
|
||||||
|
--paper-2: #ece7db;
|
||||||
|
--card: #fbfaf6;
|
||||||
|
--ink: #191712;
|
||||||
|
--ink-2: #5a554a;
|
||||||
|
--ink-faint: #9b9484;
|
||||||
|
--line: #1a1712;
|
||||||
|
--line-soft: #d8d2c4;
|
||||||
|
--accent: #ff5436; /* vermilion */
|
||||||
|
--accent-ink: #cf3a1f;
|
||||||
|
--blue: #1f4ae0;
|
||||||
|
--ok: #1f8a4c;
|
||||||
|
--warn: #b8730a;
|
||||||
|
--bad: #cf2020;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--paper);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'Hanken Grotesk', system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
background-image: radial-gradient(rgba(0, 0, 0, 0.022) 1px, transparent 1px);
|
||||||
|
background-size: 5px 5px;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1180px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 26px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* top bar */
|
||||||
|
.masthead {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 22px 0 18px;
|
||||||
|
border-bottom: 2.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.logo .glyph {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
.logo .glyph svg {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
.logo .tt {
|
||||||
|
font-family: 'Bricolage Grotesque';
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 23px;
|
||||||
|
letter-spacing: -0.03em;
|
||||||
|
line-height: 0.9;
|
||||||
|
}
|
||||||
|
.logo .tt small {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 0;
|
||||||
|
color: var(--ink-2);
|
||||||
|
}
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
margin-left: 18px;
|
||||||
|
}
|
||||||
|
.nav a {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 8px 14px;
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: 0.15s;
|
||||||
|
}
|
||||||
|
.nav a:hover {
|
||||||
|
background: var(--paper-2);
|
||||||
|
}
|
||||||
|
.nav a.on {
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
}
|
||||||
|
.tools {
|
||||||
|
margin-left: auto;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 2px;
|
||||||
|
padding: 9px 13px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
cursor: text;
|
||||||
|
background: var(--card);
|
||||||
|
}
|
||||||
|
.search svg {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
}
|
||||||
|
.search .k {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-family: 'Hanken Grotesk';
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 10px;
|
||||||
|
border: 1.5px solid var(--line-soft);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 1px 6px;
|
||||||
|
}
|
||||||
|
.ib {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 2px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
background: var(--card);
|
||||||
|
transition: 0.15s;
|
||||||
|
}
|
||||||
|
.ib:hover {
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
}
|
||||||
|
.ib svg {
|
||||||
|
width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* hero */
|
||||||
|
.hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1.45fr 1fr;
|
||||||
|
gap: 0;
|
||||||
|
border-bottom: 2.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.hero-l {
|
||||||
|
padding: 46px 40px 46px 0;
|
||||||
|
border-right: 2.5px solid var(--line);
|
||||||
|
}
|
||||||
|
.kicker {
|
||||||
|
font-family: 'Hanken Grotesk';
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--accent-ink);
|
||||||
|
}
|
||||||
|
.hero h1 {
|
||||||
|
font-family: 'Bricolage Grotesque';
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 62px;
|
||||||
|
line-height: 0.95;
|
||||||
|
letter-spacing: -0.035em;
|
||||||
|
margin: 14px 0 0;
|
||||||
|
}
|
||||||
|
.hero h1 em {
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: 400;
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.hero p {
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
max-width: 30ch;
|
||||||
|
margin-top: 18px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.hero-cta {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 24px;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
cursor: pointer;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.btn.solid {
|
||||||
|
background: var(--ink);
|
||||||
|
color: var(--paper);
|
||||||
|
}
|
||||||
|
.btn.solid:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
.btn.ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.btn.ghost:hover {
|
||||||
|
background: var(--paper-2);
|
||||||
|
}
|
||||||
|
.hero-r {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 1fr 1fr 1fr;
|
||||||
|
}
|
||||||
|
.figure {
|
||||||
|
padding: 20px 0 20px 36px;
|
||||||
|
border-bottom: 1.5px solid var(--line-soft);
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.figure:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
.figure .num {
|
||||||
|
font-family: 'Bricolage Grotesque';
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 46px;
|
||||||
|
letter-spacing: -0.04em;
|
||||||
|
line-height: 0.85;
|
||||||
|
min-width: 120px;
|
||||||
|
}
|
||||||
|
.figure .num.acc {
|
||||||
|
color: var(--accent);
|
||||||
|
}
|
||||||
|
.figure .num.bl {
|
||||||
|
color: var(--blue);
|
||||||
|
}
|
||||||
|
.figure .desc {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.figure .desc b {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Bricolage Grotesque';
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--ink);
|
||||||
|
font-size: 15px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* section label */
|
||||||
|
.slab {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin: 36px 0 20px;
|
||||||
|
}
|
||||||
|
.slab h2 {
|
||||||
|
font-family: 'Bricolage Grotesque';
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 22px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
.slab .ln {
|
||||||
|
flex: 1;
|
||||||
|
height: 2px;
|
||||||
|
background: var(--line);
|
||||||
|
}
|
||||||
|
.slab .meta {
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 16px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* apps — asymmetric editorial grid */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(12, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.tile {
|
||||||
|
grid-column: span 3;
|
||||||
|
background: var(--card);
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 18px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.15s,
|
||||||
|
box-shadow 0.15s;
|
||||||
|
position: relative;
|
||||||
|
box-shadow: 4px 4px 0 var(--line);
|
||||||
|
}
|
||||||
|
.tile:hover {
|
||||||
|
transform: translate(-2px, -2px);
|
||||||
|
box-shadow: 7px 7px 0 var(--accent);
|
||||||
|
}
|
||||||
|
.tile.wide {
|
||||||
|
grid-column: span 6;
|
||||||
|
}
|
||||||
|
.tile.tall {
|
||||||
|
grid-column: span 3;
|
||||||
|
}
|
||||||
|
.t-top {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.t-ico {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 3px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 22px;
|
||||||
|
background: var(--paper);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.t-name {
|
||||||
|
font-family: 'Bricolage Grotesque';
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 18px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.t-cat {
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-top: 3px;
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
margin-left: auto;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 2px;
|
||||||
|
border: 1.5px solid currentColor;
|
||||||
|
}
|
||||||
|
.tag.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
}
|
||||||
|
.tag.warn {
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
.tag.bad {
|
||||||
|
color: var(--bad);
|
||||||
|
background: var(--bad);
|
||||||
|
color: #fff;
|
||||||
|
border-color: var(--bad);
|
||||||
|
}
|
||||||
|
.t-foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 18px;
|
||||||
|
padding-top: 13px;
|
||||||
|
border-top: 1.5px solid var(--line-soft);
|
||||||
|
}
|
||||||
|
.t-foot .up {
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.t-foot .up small {
|
||||||
|
font-weight: 500;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.spark {
|
||||||
|
height: 24px;
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
.tile.wide .blurb {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: 1.5;
|
||||||
|
margin-top: 14px;
|
||||||
|
max-width: 42ch;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pop {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(12px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.tile,
|
||||||
|
.figure {
|
||||||
|
animation: pop 0.5s both;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(1) {
|
||||||
|
animation-delay: 0.05s;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(2) {
|
||||||
|
animation-delay: 0.11s;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(3) {
|
||||||
|
animation-delay: 0.17s;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(4) {
|
||||||
|
animation-delay: 0.23s;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(5) {
|
||||||
|
animation-delay: 0.29s;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(6) {
|
||||||
|
animation-delay: 0.35s;
|
||||||
|
}
|
||||||
|
.grid .tile:nth-child(7) {
|
||||||
|
animation-delay: 0.41s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.colophon {
|
||||||
|
margin: 46px 0 30px;
|
||||||
|
padding-top: 18px;
|
||||||
|
border-top: 2.5px solid var(--line);
|
||||||
|
font-family: 'Instrument Serif';
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 15px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<!-- Masthead -->
|
||||||
|
<div class="masthead">
|
||||||
|
<div class="logo">
|
||||||
|
<div class="glyph">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="tt">LAUNCHER<small>the home cloud edition</small></div>
|
||||||
|
</div>
|
||||||
|
<nav class="nav"><a class="on">Overview</a><a>Apps</a><a>Boards</a><a>Status</a></nav>
|
||||||
|
<div class="tools">
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="7" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
</svg>
|
||||||
|
Search<span class="k">⌘K</span>
|
||||||
|
</div>
|
||||||
|
<div class="ib">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path
|
||||||
|
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Hero -->
|
||||||
|
<section class="hero">
|
||||||
|
<div class="hero-l">
|
||||||
|
<div class="kicker">Tuesday · 27 May · 19:42</div>
|
||||||
|
<h1>Your stack,<br />all in <em>one place.</em></h1>
|
||||||
|
<p>
|
||||||
|
Ten services under one roof. Eight humming, two asking for attention. Everything
|
||||||
|
launches from here.
|
||||||
|
</p>
|
||||||
|
<div class="hero-cta">
|
||||||
|
<a class="btn solid">Open a board →</a><a class="btn ghost">Add an app</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="hero-r">
|
||||||
|
<div class="figure">
|
||||||
|
<div class="num acc">8/10</div>
|
||||||
|
<div class="desc"><b>Services online</b>Deluge offline · Portainer slow to respond</div>
|
||||||
|
</div>
|
||||||
|
<div class="figure">
|
||||||
|
<div class="num bl">99.4%</div>
|
||||||
|
<div class="desc"><b>Fleet uptime</b>Rolling 30-day average across all monitors</div>
|
||||||
|
</div>
|
||||||
|
<div class="figure">
|
||||||
|
<div class="num">142<span style="font-size: 20px">ms</span></div>
|
||||||
|
<div class="desc"><b>Median response</b>p95 latency over the last 24 hours</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Apps -->
|
||||||
|
<div class="slab">
|
||||||
|
<h2>Favorites</h2>
|
||||||
|
<div class="ln"></div>
|
||||||
|
<div class="meta">eight pinned</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="tile wide">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">🎬</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Jellyfin</div>
|
||||||
|
<div class="t-cat">Media server · the crown jewel</div>
|
||||||
|
</div>
|
||||||
|
<span class="tag ok">Online</span>
|
||||||
|
</div>
|
||||||
|
<p class="blurb">
|
||||||
|
Streaming to 3 devices right now. Library scan completed 2 hours ago — 4,212 movies, 318
|
||||||
|
shows indexed and healthy.
|
||||||
|
</p>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up">99.9% <small>uptime · 24h</small></div>
|
||||||
|
<svg class="spark" viewBox="0 0 90 24" preserveAspectRatio="none">
|
||||||
|
<polyline
|
||||||
|
fill="none"
|
||||||
|
stroke="#ff5436"
|
||||||
|
stroke-width="2.2"
|
||||||
|
points="0,18 12,15 24,17 36,9 48,12 60,7 72,11 82,5 90,8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">📷</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Immich</div>
|
||||||
|
<div class="t-cat">Photos</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up">100% <small>24h</small></div>
|
||||||
|
<span class="tag ok">Up</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">🌿</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Gitea</div>
|
||||||
|
<div class="t-cat">Git</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up">99.8% <small>24h</small></div>
|
||||||
|
<span class="tag ok">Up</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">🐳</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Portainer</div>
|
||||||
|
<div class="t-cat">Containers</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up">98.1% <small>24h</small></div>
|
||||||
|
<span class="tag warn">Slow</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">🛡️</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Pi-hole</div>
|
||||||
|
<div class="t-cat">DNS</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up">100% <small>24h</small></div>
|
||||||
|
<span class="tag ok">Up</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">📋</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Planka</div>
|
||||||
|
<div class="t-cat">Kanban</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up">99.5% <small>24h</small></div>
|
||||||
|
<span class="tag ok">Up</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="tile">
|
||||||
|
<div class="t-top">
|
||||||
|
<div class="t-ico">⬇️</div>
|
||||||
|
<div>
|
||||||
|
<div class="t-name">Deluge</div>
|
||||||
|
<div class="t-cat">Downloads</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="t-foot">
|
||||||
|
<div class="up" style="color: var(--bad)">—</div>
|
||||||
|
<span class="tag bad">Down</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="colophon">
|
||||||
|
<span>Editorial — Bricolage Grotesque + Instrument Serif</span
|
||||||
|
><span>warm paper · ink rules · hard shadows · asymmetric grid</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,723 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Web App Launcher — Cozy Home</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Figtree:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #fdf8f2; /* warm cream */
|
||||||
|
--bg-2: #f6efe4;
|
||||||
|
--card: #fffdfa;
|
||||||
|
--ink: #3a322b;
|
||||||
|
--ink-2: #857a6d;
|
||||||
|
--ink-faint: #b3a899;
|
||||||
|
--line: #ece2d3;
|
||||||
|
--peach: #ff9a76;
|
||||||
|
--terra: #e8754f;
|
||||||
|
--sage: #7fb069;
|
||||||
|
--sky: #6ca9d6;
|
||||||
|
--butter: #f3c969;
|
||||||
|
--lav: #b09fd6;
|
||||||
|
--ok: #5fa86c;
|
||||||
|
--warn: #d99a2b;
|
||||||
|
--bad: #e0685f;
|
||||||
|
--radius: 22px;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'Figtree', system-ui, sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
body::before {
|
||||||
|
content: '';
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
background:
|
||||||
|
radial-gradient(50% 40% at 12% 0%, rgba(255, 154, 118, 0.16), transparent 70%),
|
||||||
|
radial-gradient(45% 40% at 95% 8%, rgba(108, 169, 214, 0.14), transparent 70%),
|
||||||
|
radial-gradient(50% 45% at 85% 100%, rgba(127, 176, 105, 0.12), transparent 70%);
|
||||||
|
}
|
||||||
|
.shell {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 236px 1fr;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sidebar */
|
||||||
|
.side {
|
||||||
|
padding: 24px 18px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 11px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
.brand .m {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: linear-gradient(135deg, var(--peach), var(--terra));
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: #fff;
|
||||||
|
box-shadow: 0 10px 22px -8px var(--terra);
|
||||||
|
}
|
||||||
|
.brand .m svg {
|
||||||
|
width: 21px;
|
||||||
|
height: 21px;
|
||||||
|
}
|
||||||
|
.brand .t {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 19px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.brand .t span {
|
||||||
|
display: block;
|
||||||
|
font-family: 'Figtree';
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
}
|
||||||
|
.nlabel {
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
font-size: 10.5px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
margin: 16px 10px 8px;
|
||||||
|
}
|
||||||
|
.ni {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 11px 13px;
|
||||||
|
border-radius: 14px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14.5px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.16s;
|
||||||
|
}
|
||||||
|
.ni svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
.ni:hover {
|
||||||
|
background: var(--bg-2);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
.ni.on {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--terra);
|
||||||
|
box-shadow:
|
||||||
|
0 6px 16px -8px rgba(0, 0, 0, 0.18),
|
||||||
|
inset 0 0 0 1px var(--line);
|
||||||
|
}
|
||||||
|
.ni .c {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
color: var(--ink-2);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
.side-card {
|
||||||
|
margin-top: auto;
|
||||||
|
background: linear-gradient(135deg, rgba(127, 176, 105, 0.16), rgba(108, 169, 214, 0.14));
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 16px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.side-card p {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
.side-card .who {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
.side-card .av {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 11px;
|
||||||
|
background: linear-gradient(135deg, var(--lav), var(--sky));
|
||||||
|
color: #fff;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* main */
|
||||||
|
.main {
|
||||||
|
padding: 30px 36px 40px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.top {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.top .search {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-size: 14px;
|
||||||
|
width: 320px;
|
||||||
|
cursor: text;
|
||||||
|
box-shadow: 0 4px 14px -10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.top .search svg {
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
|
.top .search .k {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
background: var(--bg-2);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.rbtn {
|
||||||
|
margin-left: auto;
|
||||||
|
width: 44px;
|
||||||
|
height: 44px;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
color: var(--ink-2);
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 4px 14px -10px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
.rbtn + .rbtn {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
.rbtn:hover {
|
||||||
|
color: var(--terra);
|
||||||
|
}
|
||||||
|
.rbtn svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.greet {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 34px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
margin: 18px 0 4px;
|
||||||
|
}
|
||||||
|
.greet .wave {
|
||||||
|
display: inline-block;
|
||||||
|
animation: wave 2.4s ease-in-out infinite;
|
||||||
|
transform-origin: 70% 70%;
|
||||||
|
}
|
||||||
|
@keyframes wave {
|
||||||
|
0%,
|
||||||
|
60%,
|
||||||
|
100% {
|
||||||
|
transform: rotate(0);
|
||||||
|
}
|
||||||
|
10% {
|
||||||
|
transform: rotate(16deg);
|
||||||
|
}
|
||||||
|
20% {
|
||||||
|
transform: rotate(-8deg);
|
||||||
|
}
|
||||||
|
30% {
|
||||||
|
transform: rotate(14deg);
|
||||||
|
}
|
||||||
|
40% {
|
||||||
|
transform: rotate(-4deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.gsub {
|
||||||
|
color: var(--ink-2);
|
||||||
|
font-size: 15px;
|
||||||
|
margin-bottom: 26px;
|
||||||
|
}
|
||||||
|
.gsub b {
|
||||||
|
color: var(--sage);
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* summary chips */
|
||||||
|
.chips {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.chip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
box-shadow: 0 8px 20px -16px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
.chip .ic {
|
||||||
|
width: 38px;
|
||||||
|
height: 38px;
|
||||||
|
border-radius: 12px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
.chip .ic svg {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.chip .v {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
.chip .l {
|
||||||
|
font-size: 12.5px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sec {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 6px 0 18px;
|
||||||
|
}
|
||||||
|
.sec h2 {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 22px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.sec .more {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 13.5px;
|
||||||
|
color: var(--terra);
|
||||||
|
font-weight: 600;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.apps {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1),
|
||||||
|
box-shadow 0.2s;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 10px 26px -20px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.card:hover {
|
||||||
|
transform: translateY(-6px) rotate(-0.4deg);
|
||||||
|
box-shadow: 0 22px 40px -22px rgba(0, 0, 0, 0.4);
|
||||||
|
}
|
||||||
|
.blob {
|
||||||
|
position: absolute;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(30px);
|
||||||
|
opacity: 0.5;
|
||||||
|
top: -50px;
|
||||||
|
right: -40px;
|
||||||
|
}
|
||||||
|
.ico {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 26px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nm {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 17px;
|
||||||
|
margin-top: 16px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.ct {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--ink-2);
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
.foot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: 18px;
|
||||||
|
}
|
||||||
|
.dot {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.dot .b {
|
||||||
|
width: 9px;
|
||||||
|
height: 9px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.dot.ok {
|
||||||
|
color: var(--ok);
|
||||||
|
}
|
||||||
|
.dot.ok .b {
|
||||||
|
background: var(--ok);
|
||||||
|
box-shadow: 0 0 0 4px rgba(95, 168, 108, 0.18);
|
||||||
|
}
|
||||||
|
.dot.warn {
|
||||||
|
color: var(--warn);
|
||||||
|
}
|
||||||
|
.dot.warn .b {
|
||||||
|
background: var(--warn);
|
||||||
|
box-shadow: 0 0 0 4px rgba(217, 154, 43, 0.18);
|
||||||
|
}
|
||||||
|
.dot.bad {
|
||||||
|
color: var(--bad);
|
||||||
|
}
|
||||||
|
.dot.bad .b {
|
||||||
|
background: var(--bad);
|
||||||
|
box-shadow: 0 0 0 4px rgba(224, 104, 95, 0.18);
|
||||||
|
}
|
||||||
|
.up {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes rise {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(16px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.card,
|
||||||
|
.chip {
|
||||||
|
animation: rise 0.55s both;
|
||||||
|
}
|
||||||
|
.chip:nth-child(2) {
|
||||||
|
animation-delay: 0.06s;
|
||||||
|
}
|
||||||
|
.chip:nth-child(3) {
|
||||||
|
animation-delay: 0.12s;
|
||||||
|
}
|
||||||
|
.chip:nth-child(4) {
|
||||||
|
animation-delay: 0.18s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(1) {
|
||||||
|
animation-delay: 0.1s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(2) {
|
||||||
|
animation-delay: 0.16s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(3) {
|
||||||
|
animation-delay: 0.22s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(4) {
|
||||||
|
animation-delay: 0.28s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(5) {
|
||||||
|
animation-delay: 0.34s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(6) {
|
||||||
|
animation-delay: 0.4s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(7) {
|
||||||
|
animation-delay: 0.46s;
|
||||||
|
}
|
||||||
|
.apps .card:nth-child(8) {
|
||||||
|
animation-delay: 0.52s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.note {
|
||||||
|
text-align: center;
|
||||||
|
color: var(--ink-faint);
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 14px;
|
||||||
|
margin-top: 36px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="shell">
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<aside class="side">
|
||||||
|
<div class="brand">
|
||||||
|
<div class="m">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<rect x="3" y="3" width="7" height="7" rx="2" />
|
||||||
|
<rect x="14" y="3" width="7" height="7" rx="2" />
|
||||||
|
<rect x="14" y="14" width="7" height="7" rx="2" />
|
||||||
|
<rect x="3" y="14" width="7" height="7" rx="2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="t">Launcher<span>our home cloud</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="nlabel">Menu</div>
|
||||||
|
<div class="ni on">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<path d="M3 11l9-8 9 8M5 10v10h14V10" />
|
||||||
|
</svg>
|
||||||
|
Home
|
||||||
|
</div>
|
||||||
|
<div class="ni">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18" />
|
||||||
|
</svg>
|
||||||
|
All apps <span class="c">10</span>
|
||||||
|
</div>
|
||||||
|
<div class="ni">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
Status
|
||||||
|
</div>
|
||||||
|
<div class="nlabel">Rooms</div>
|
||||||
|
<div class="ni">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||||
|
</svg>
|
||||||
|
Movie night
|
||||||
|
</div>
|
||||||
|
<div class="ni">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<rect x="3" y="3" width="18" height="18" rx="4" />
|
||||||
|
</svg>
|
||||||
|
The basement rack
|
||||||
|
</div>
|
||||||
|
<div class="side-card">
|
||||||
|
<p>“Everything’s running smoothly today. ☕ Two apps want a peek when you get a sec.”</p>
|
||||||
|
<div class="who">
|
||||||
|
<div class="av">AD</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 700; font-size: 13px">Alexei</div>
|
||||||
|
<div style="font-size: 11px; color: var(--ink-faint)">Admin</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<!-- Main -->
|
||||||
|
<main class="main">
|
||||||
|
<div class="top">
|
||||||
|
<div class="search">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<circle cx="11" cy="11" r="7" />
|
||||||
|
<line x1="21" y1="21" x2="16.65" y2="16.65" />
|
||||||
|
</svg>
|
||||||
|
Search your apps… <span class="k">⌘K</span>
|
||||||
|
</div>
|
||||||
|
<div class="rbtn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<path d="M18 8a6 6 0 1 0-12 0c0 7-3 9-3 9h18s-3-2-3-9" />
|
||||||
|
<path d="M13.7 21a2 2 0 0 1-3.4 0" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="rbtn">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.9">
|
||||||
|
<circle cx="12" cy="12" r="4" />
|
||||||
|
<path
|
||||||
|
d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 class="greet">Hi Alexei <span class="wave">👋</span></h1>
|
||||||
|
<p class="gsub">It’s a calm evening — <b>8 of your 10 apps</b> are happy and online.</p>
|
||||||
|
|
||||||
|
<div class="chips">
|
||||||
|
<div class="chip">
|
||||||
|
<div class="ic" style="background: var(--sage)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||||
|
<path d="M20 6 9 17l-5-5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="v">8/10</div>
|
||||||
|
<div class="l">Apps online</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chip">
|
||||||
|
<div class="ic" style="background: var(--sky)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M12 7v5l3 2" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="v">142ms</div>
|
||||||
|
<div class="l">Avg speed</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chip">
|
||||||
|
<div class="ic" style="background: var(--butter)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||||
|
<path d="M3 12h4l3 8 4-16 3 8h4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="v">99.4%</div>
|
||||||
|
<div class="l">Uptime · 30d</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="chip">
|
||||||
|
<div class="ic" style="background: var(--lav)">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2">
|
||||||
|
<path d="M13 2 3 14h7l-1 8 10-12h-7z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="v">38 min</div>
|
||||||
|
<div class="l">Battery left</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sec">
|
||||||
|
<h2>Your favorites</h2>
|
||||||
|
<a class="more" href="#">See all →</a>
|
||||||
|
</div>
|
||||||
|
<div class="apps">
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--terra)"></div>
|
||||||
|
<div class="ico" style="background: rgba(232, 117, 79, 0.16)">🎬</div>
|
||||||
|
<div class="nm">Jellyfin</div>
|
||||||
|
<div class="ct">Movies & shows</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.9%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--sky)"></div>
|
||||||
|
<div class="ico" style="background: rgba(108, 169, 214, 0.18)">📷</div>
|
||||||
|
<div class="nm">Immich</div>
|
||||||
|
<div class="ct">Photos</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot ok"><span class="b"></span>Online</span><span class="up">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--sage)"></div>
|
||||||
|
<div class="ico" style="background: rgba(127, 176, 105, 0.18)">🌿</div>
|
||||||
|
<div class="nm">Gitea</div>
|
||||||
|
<div class="ct">Code</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.8%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--butter)"></div>
|
||||||
|
<div class="ico" style="background: rgba(243, 201, 105, 0.22)">🐳</div>
|
||||||
|
<div class="nm">Portainer</div>
|
||||||
|
<div class="ct">Containers</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot warn"><span class="b"></span>A bit slow</span
|
||||||
|
><span class="up">98.1%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--lav)"></div>
|
||||||
|
<div class="ico" style="background: rgba(176, 159, 214, 0.2)">🛡️</div>
|
||||||
|
<div class="nm">Pi-hole</div>
|
||||||
|
<div class="ct">Ad blocker</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot ok"><span class="b"></span>Online</span><span class="up">100%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--peach)"></div>
|
||||||
|
<div class="ico" style="background: rgba(255, 154, 118, 0.2)">📋</div>
|
||||||
|
<div class="nm">Planka</div>
|
||||||
|
<div class="ct">To-dos</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.5%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--bad)"></div>
|
||||||
|
<div class="ico" style="background: rgba(224, 104, 95, 0.16)">⬇️</div>
|
||||||
|
<div class="nm">Deluge</div>
|
||||||
|
<div class="ct">Downloads</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot bad"><span class="b"></span>Asleep</span><span class="up">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--sky)"></div>
|
||||||
|
<div class="ico" style="background: rgba(108, 169, 214, 0.18)">🔀</div>
|
||||||
|
<div class="nm">Proxy</div>
|
||||||
|
<div class="ct">Networking</div>
|
||||||
|
<div class="foot">
|
||||||
|
<span class="dot ok"><span class="b"></span>Online</span><span class="up">99.9%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="note">
|
||||||
|
Cozy Home — Fraunces + Figtree · warm cream · soft pastel rooms · gentle motion
|
||||||
|
</p>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,639 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Cozy Home — Design System Reference</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600;9..144,700&family=Figtree:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
/* === The exact tokens now living in src/app.css (light/cream) === */
|
||||||
|
:root {
|
||||||
|
--background: hsl(35 56% 97%);
|
||||||
|
--foreground: hsl(33 18% 18%);
|
||||||
|
--muted: hsl(36 42% 93%);
|
||||||
|
--muted-foreground: hsl(34 12% 47%);
|
||||||
|
--card: hsl(40 60% 99%);
|
||||||
|
--border: hsl(36 35% 88%);
|
||||||
|
--primary: hsl(16 72% 56%);
|
||||||
|
--primary-foreground: hsl(40 60% 99%);
|
||||||
|
--accent: hsl(34 44% 90%);
|
||||||
|
--status-online: #5fa86c;
|
||||||
|
--status-offline: #e0685f;
|
||||||
|
--status-degraded: #d99a2b;
|
||||||
|
--status-unknown: #b3a899;
|
||||||
|
--room-sage: #7fb069;
|
||||||
|
--room-sky: #6ca9d6;
|
||||||
|
--room-butter: #f3c969;
|
||||||
|
--room-lav: #b09fd6;
|
||||||
|
--room-peach: #ff9a76;
|
||||||
|
--room-terra: #e8754f;
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-soft: 0 10px 26px -20px rgba(80, 50, 20, 0.45);
|
||||||
|
--shadow-lift: 0 22px 40px -22px rgba(80, 50, 20, 0.4);
|
||||||
|
--font-sans: 'Figtree', system-ui, sans-serif;
|
||||||
|
--font-display: 'Fraunces', serif;
|
||||||
|
}
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
padding: 40px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
font-family: var(--font-display);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1080px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.lede {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
margin: 6px 0 8px;
|
||||||
|
max-width: 64ch;
|
||||||
|
}
|
||||||
|
.path {
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--room-terra);
|
||||||
|
background: color-mix(in srgb, var(--room-terra) 12%, transparent);
|
||||||
|
padding: 3px 9px;
|
||||||
|
border-radius: 8px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
section {
|
||||||
|
margin-top: 42px;
|
||||||
|
}
|
||||||
|
.h {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
margin-bottom: 16px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tokens */
|
||||||
|
.swatch {
|
||||||
|
border-radius: var(--radius);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--card);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
.swatch .c {
|
||||||
|
height: 56px;
|
||||||
|
}
|
||||||
|
.swatch .n {
|
||||||
|
padding: 9px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.swatch .n b {
|
||||||
|
display: block;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.swatch .n span {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-family: ui-monospace, monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* buttons */
|
||||||
|
.btn {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 11px 18px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: 0.18s;
|
||||||
|
}
|
||||||
|
.btn-primary {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--primary-foreground);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lift);
|
||||||
|
}
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
border-color: var(--border);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
.btn-secondary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: color-mix(in srgb, var(--primary) 40%, transparent);
|
||||||
|
}
|
||||||
|
.btn-ghost {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--foreground);
|
||||||
|
}
|
||||||
|
.btn-ghost:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
.btn-icon {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 14px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* inputs */
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 7px;
|
||||||
|
max-width: 340px;
|
||||||
|
}
|
||||||
|
.field label {
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.input {
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 14px;
|
||||||
|
padding: 11px 14px;
|
||||||
|
border-radius: 14px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
transition: 0.16s;
|
||||||
|
}
|
||||||
|
.input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary);
|
||||||
|
box-shadow: 0 0 0 4px color-mix(in srgb, var(--primary) 18%, transparent);
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* badges / status pills */
|
||||||
|
.pill {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 7px;
|
||||||
|
font-size: 12.5px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 5px 11px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
.pill .b {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
.pill.ok {
|
||||||
|
color: var(--status-online);
|
||||||
|
background: color-mix(in srgb, var(--status-online) 14%, transparent);
|
||||||
|
}
|
||||||
|
.pill.warn {
|
||||||
|
color: var(--status-degraded);
|
||||||
|
background: color-mix(in srgb, var(--status-degraded) 14%, transparent);
|
||||||
|
}
|
||||||
|
.pill.bad {
|
||||||
|
color: var(--status-offline);
|
||||||
|
background: color-mix(in srgb, var(--status-offline) 14%, transparent);
|
||||||
|
}
|
||||||
|
.pill .b {
|
||||||
|
background: currentColor;
|
||||||
|
}
|
||||||
|
.room-pill {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tabs */
|
||||||
|
.tabs {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 4px;
|
||||||
|
background: var(--muted);
|
||||||
|
padding: 5px;
|
||||||
|
border-radius: 16px;
|
||||||
|
}
|
||||||
|
.tab {
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 13.5px;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
.tab.on {
|
||||||
|
background: var(--card);
|
||||||
|
color: var(--foreground);
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* card */
|
||||||
|
.card {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
max-width: 300px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card .blob {
|
||||||
|
position: absolute;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 50%;
|
||||||
|
filter: blur(30px);
|
||||||
|
opacity: 0.45;
|
||||||
|
top: -50px;
|
||||||
|
right: -40px;
|
||||||
|
}
|
||||||
|
.card .ic {
|
||||||
|
width: 52px;
|
||||||
|
height: 52px;
|
||||||
|
border-radius: 18px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 25px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.card h3 {
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.card .ct {
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
}
|
||||||
|
.card .f {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* dialog */
|
||||||
|
.dialog {
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
padding: 24px;
|
||||||
|
box-shadow: var(--shadow-lift);
|
||||||
|
max-width: 380px;
|
||||||
|
}
|
||||||
|
.dialog h3 {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.dialog p {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 8px 0 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* table */
|
||||||
|
table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
background: var(--card);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 1.2rem;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: var(--shadow-soft);
|
||||||
|
}
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
padding: 14px 18px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
td {
|
||||||
|
padding: 14px 18px;
|
||||||
|
font-size: 14px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
tr:last-child td {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
tr:hover td {
|
||||||
|
background: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* empty state */
|
||||||
|
.empty {
|
||||||
|
text-align: center;
|
||||||
|
border: 2px dashed var(--border);
|
||||||
|
border-radius: 1.4rem;
|
||||||
|
padding: 48px 24px;
|
||||||
|
background: color-mix(in srgb, var(--card) 50%, transparent);
|
||||||
|
}
|
||||||
|
.empty .e-ic {
|
||||||
|
width: 64px;
|
||||||
|
height: 64px;
|
||||||
|
border-radius: 22px;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
margin: 0 auto 16px;
|
||||||
|
background: color-mix(in srgb, var(--room-peach) 20%, transparent);
|
||||||
|
color: var(--room-terra);
|
||||||
|
}
|
||||||
|
.empty h3 {
|
||||||
|
font-size: 19px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.empty p {
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 6px auto 18px;
|
||||||
|
max-width: 36ch;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1 class="title">Cozy Home — Design System</h1>
|
||||||
|
<p class="lede">
|
||||||
|
The component pattern sheet for the migration. Every phase styles its components to match
|
||||||
|
these primitives. Tokens here mirror what now lives in
|
||||||
|
<span class="path">src/app.css</span> — change them there and the whole app follows.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Color tokens</div>
|
||||||
|
<div class="grid">
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--background)"></div>
|
||||||
|
<div class="n"><b>background</b><span>cream #fdf8f2</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--card)"></div>
|
||||||
|
<div class="n"><b>card</b><span>#fffdfa</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--primary)"></div>
|
||||||
|
<div class="n"><b>primary</b><span>terracotta · tunable</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--foreground)"></div>
|
||||||
|
<div class="n"><b>foreground</b><span>warm ink</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--muted)"></div>
|
||||||
|
<div class="n"><b>muted</b><span>#f3ecde</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--border)"></div>
|
||||||
|
<div class="n"><b>border</b><span>#ece2d3</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 14px" class="grid">
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--room-terra)"></div>
|
||||||
|
<div class="n"><b>room · terra</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--room-peach)"></div>
|
||||||
|
<div class="n"><b>room · peach</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--room-butter)"></div>
|
||||||
|
<div class="n"><b>room · butter</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--room-sage)"></div>
|
||||||
|
<div class="n"><b>room · sage</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--room-sky)"></div>
|
||||||
|
<div class="n"><b>room · sky</b></div>
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<div class="c" style="background: var(--room-lav)"></div>
|
||||||
|
<div class="n"><b>room · lav</b></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Typography — Fraunces (display) · Figtree (body)</div>
|
||||||
|
<h1 style="font-size: 46px; font-weight: 600">Good evening, Alexei</h1>
|
||||||
|
<h2 style="font-size: 28px; font-weight: 600; margin-top: 10px">Your favorites</h2>
|
||||||
|
<h3 style="font-size: 19px; font-weight: 600; margin-top: 10px">Jellyfin</h3>
|
||||||
|
<p style="margin-top: 8px; max-width: 60ch">
|
||||||
|
Body copy is Figtree — friendly, legible, rounded. It carries descriptions, hints, and
|
||||||
|
plain-language status like “a bit slow” or “asleep”.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Buttons</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="btn btn-primary">Open a board</button>
|
||||||
|
<button class="btn btn-secondary">Add an app</button>
|
||||||
|
<button class="btn btn-ghost">Cancel</button>
|
||||||
|
<button class="btn btn-secondary btn-icon" title="icon">🔔</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Form fields</div>
|
||||||
|
<div class="row" style="align-items: flex-start">
|
||||||
|
<div class="field">
|
||||||
|
<label>App name</label><input class="input" value="Jellyfin" /><span class="hint"
|
||||||
|
>Shown on the card and in search.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div class="field"><label>URL</label><input class="input" placeholder="https://…" /></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Status pills & room tags</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="pill ok"><span class="b"></span>Online</span>
|
||||||
|
<span class="pill warn"><span class="b"></span>A bit slow</span>
|
||||||
|
<span class="pill bad"><span class="b"></span>Asleep</span>
|
||||||
|
<span
|
||||||
|
class="room-pill"
|
||||||
|
style="
|
||||||
|
color: var(--room-terra);
|
||||||
|
background: color-mix(in srgb, var(--room-terra) 16%, transparent);
|
||||||
|
"
|
||||||
|
>Media</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="room-pill"
|
||||||
|
style="
|
||||||
|
color: var(--room-sky);
|
||||||
|
background: color-mix(in srgb, var(--room-sky) 16%, transparent);
|
||||||
|
"
|
||||||
|
>Network</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="room-pill"
|
||||||
|
style="
|
||||||
|
color: var(--room-sage);
|
||||||
|
background: color-mix(in srgb, var(--room-sage) 16%, transparent);
|
||||||
|
"
|
||||||
|
>Git</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Tabs</div>
|
||||||
|
<div class="tabs">
|
||||||
|
<div class="tab on">Overview</div>
|
||||||
|
<div class="tab">Activity</div>
|
||||||
|
<div class="tab">Settings</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">App card</div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="blob" style="background: var(--room-terra)"></div>
|
||||||
|
<div
|
||||||
|
class="ic"
|
||||||
|
style="
|
||||||
|
background: color-mix(in srgb, var(--room-terra) 18%, transparent);
|
||||||
|
color: var(--room-terra);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
🎬
|
||||||
|
</div>
|
||||||
|
<h3>Jellyfin</h3>
|
||||||
|
<div class="ct">Movies & shows</div>
|
||||||
|
<div class="f">
|
||||||
|
<span class="pill ok"><span class="b"></span>Online</span
|
||||||
|
><span class="hint">99.9%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Dialog</div>
|
||||||
|
<div class="dialog">
|
||||||
|
<h3>Remove Deluge?</h3>
|
||||||
|
<p>This deletes the app and its uptime history. This can’t be undone.</p>
|
||||||
|
<div class="row" style="justify-content: flex-end">
|
||||||
|
<button class="btn btn-ghost">Keep it</button
|
||||||
|
><button class="btn btn-primary" style="background: var(--status-offline)">
|
||||||
|
Remove
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Table (admin)</div>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>User</th>
|
||||||
|
<th>Role</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th>Last seen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td>Alexei</td>
|
||||||
|
<td>Admin</td>
|
||||||
|
<td>
|
||||||
|
<span class="pill ok"><span class="b"></span>Active</span>
|
||||||
|
</td>
|
||||||
|
<td>just now</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Guest</td>
|
||||||
|
<td>Viewer</td>
|
||||||
|
<td>
|
||||||
|
<span class="pill warn"><span class="b"></span>Idle</span>
|
||||||
|
</td>
|
||||||
|
<td>2h ago</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="h">Empty state</div>
|
||||||
|
<div class="empty">
|
||||||
|
<div class="e-ic">
|
||||||
|
<svg
|
||||||
|
width="30"
|
||||||
|
height="30"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.6"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="9" />
|
||||||
|
<path d="M3 12h18M12 3a14 14 0 0 1 0 18 14 14 0 0 1 0-18z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3>No apps yet</h3>
|
||||||
|
<p>Add your first service and it’ll show up here with live status.</p>
|
||||||
|
<button class="btn btn-primary">+ Add an app</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<p
|
||||||
|
style="
|
||||||
|
margin: 48px 0 20px;
|
||||||
|
text-align: center;
|
||||||
|
font-family: var(--font-display);
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--muted-foreground);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Cozy Home design system · mirrors src/app.css · use as the pattern for every migrated
|
||||||
|
component
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Launcher — Redesign Mockups</title>
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,500;9..144,600&family=Figtree:wght@400;500;600;700&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Figtree', system-ui, sans-serif;
|
||||||
|
background: #0e0e12;
|
||||||
|
color: #eee;
|
||||||
|
min-height: 100vh;
|
||||||
|
padding: 48px 24px;
|
||||||
|
}
|
||||||
|
.wrap {
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 34px;
|
||||||
|
letter-spacing: -0.02em;
|
||||||
|
}
|
||||||
|
p.sub {
|
||||||
|
color: #9a99a6;
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
max-width: 62ch;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 18px;
|
||||||
|
margin-top: 34px;
|
||||||
|
}
|
||||||
|
a.card {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border: 1px solid #24242e;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 24px;
|
||||||
|
background: #15151c;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
a.card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
border-color: #3a3a4a;
|
||||||
|
background: #191920;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.12em;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border-radius: 20px;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
.nm {
|
||||||
|
font-family: 'Fraunces';
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 21px;
|
||||||
|
margin: 14px 0 6px;
|
||||||
|
}
|
||||||
|
.ds {
|
||||||
|
color: #9a99a6;
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.swatch {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 14px;
|
||||||
|
}
|
||||||
|
.swatch span {
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h1>Web App Launcher — Redesign Directions</h1>
|
||||||
|
<p class="sub">
|
||||||
|
Four aesthetic directions for the same launcher, built as theme presets of one modernized
|
||||||
|
design system. Open each, resize, hover the cards, and try the live accent swatches in
|
||||||
|
Aurora Glass. Pick the one that fits — or mix and match.
|
||||||
|
</p>
|
||||||
|
<div class="grid">
|
||||||
|
<a class="card" href="01-command-deck.html">
|
||||||
|
<span class="badge" style="background: #0d1f18; color: #36e0a4"
|
||||||
|
>01 · Dark · Power-user</span
|
||||||
|
>
|
||||||
|
<div class="nm">Command Deck</div>
|
||||||
|
<div class="ds">
|
||||||
|
Mission-control / terminal. Dense, glanceable telemetry, LED status, monospace data.
|
||||||
|
Saira + JetBrains Mono.
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<span style="background: #070a0d"></span><span style="background: #36e0a4"></span
|
||||||
|
><span style="background: #ffb020"></span><span style="background: #ff4d5e"></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="02-aurora-glass.html">
|
||||||
|
<span class="badge" style="background: #1d1340; color: #b69cff">02 · Dark · Premium</span>
|
||||||
|
<div class="nm">Aurora Glass</div>
|
||||||
|
<div class="ds">
|
||||||
|
Frosted glass over a living gradient mesh. Soft glows, generous space, fully retintable
|
||||||
|
accent. Outfit + Manrope.
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<span style="background: #0a0a14"></span
|
||||||
|
><span style="background: hsl(265 90% 66%)"></span
|
||||||
|
><span style="background: hsl(325 85% 65%)"></span
|
||||||
|
><span style="background: #34e0a1"></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="03-editorial.html">
|
||||||
|
<span class="badge" style="background: #2a1109; color: #ff5436">03 · Light · Bold</span>
|
||||||
|
<div class="nm">Editorial</div>
|
||||||
|
<div class="ds">
|
||||||
|
Magazine masthead, big display type, ink rules, hard shadows, asymmetric grid. Bricolage
|
||||||
|
Grotesque + Instrument Serif.
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<span style="background: #f4f1ea"></span><span style="background: #191712"></span
|
||||||
|
><span style="background: #ff5436"></span><span style="background: #1f4ae0"></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
<a class="card" href="04-cozy-home.html">
|
||||||
|
<span class="badge" style="background: #2a1c12; color: #ff9a76"
|
||||||
|
>04 · Light · Friendly</span
|
||||||
|
>
|
||||||
|
<div class="nm">Cozy Home</div>
|
||||||
|
<div class="ds">
|
||||||
|
Warm cream, soft rounded cards, pastel “rooms”, gentle motion. Friendly for the whole
|
||||||
|
household. Fraunces + Figtree.
|
||||||
|
</div>
|
||||||
|
<div class="swatch">
|
||||||
|
<span style="background: #fdf8f2"></span><span style="background: #e8754f"></span
|
||||||
|
><span style="background: #7fb069"></span><span style="background: #6ca9d6"></span>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
+198
-83
@@ -4,83 +4,140 @@
|
|||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@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
@@ -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 () {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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'}"
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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'}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|
||||||
|
|||||||
@@ -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 +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)');
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user