feat: add visual customization presets to Settings > Appearance tab
Lint & Test / test (push) Failing after 30s
Lint & Test / test (push) Failing after 30s
Add style presets (font + color combinations) and background effect presets as a new Appearance tab in the settings modal. Style presets include Default, Midnight, Ember, Arctic, Terminal, and Neon — each with curated dark/light theme colors and Google Font pairings. Background effects (Dot Grid, Gradient Mesh, Scanlines, Particles) use a dedicated overlay div alongside the existing WebGL Noise Field. All choices persist to localStorage and restore on page load.
This commit is contained in:
@@ -14,4 +14,5 @@
|
||||
@import './tree-nav.css';
|
||||
@import './tutorials.css';
|
||||
@import './graph-editor.css';
|
||||
@import './appearance.css';
|
||||
@import './mobile.css';
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
/* ── Appearance tab: preset cards & background effects ── */
|
||||
|
||||
/* Use --font-body / --font-heading CSS variables for preset font switching */
|
||||
body {
|
||||
font-family: var(--font-body, 'DM Sans', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-family: var(--font-heading, 'Orbitron', sans-serif);
|
||||
}
|
||||
|
||||
/* ─── Preset grid ─── */
|
||||
|
||||
.ap-hint {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
.ap-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
/* ─── Preset card (shared) ─── */
|
||||
|
||||
.ap-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px;
|
||||
border: 2px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
background: var(--card-bg);
|
||||
cursor: pointer;
|
||||
transition: border-color var(--duration-normal) var(--ease-out),
|
||||
box-shadow var(--duration-normal) var(--ease-out),
|
||||
transform var(--duration-fast) var(--ease-out);
|
||||
}
|
||||
|
||||
.ap-card:hover {
|
||||
border-color: var(--text-muted);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.ap-card.active {
|
||||
border-color: var(--primary-color);
|
||||
box-shadow: 0 0 0 1px var(--primary-color),
|
||||
0 0 12px -2px color-mix(in srgb, var(--primary-color) 40%, transparent);
|
||||
}
|
||||
|
||||
.ap-card.active::after {
|
||||
content: '\2713';
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
right: 6px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.ap-card-label {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
text-align: center;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.ap-card.active .ap-card-label {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
/* ─── Style preset preview ─── */
|
||||
|
||||
.ap-card-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: var(--radius-sm);
|
||||
border: 1px solid;
|
||||
padding: 8px 7px 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ap-card-accent {
|
||||
width: 24px;
|
||||
height: 4px;
|
||||
border-radius: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.ap-card-lines {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.ap-card-lines span {
|
||||
display: block;
|
||||
height: 2px;
|
||||
border-radius: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* ─── Background effect preview ─── */
|
||||
|
||||
.ap-bg-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 4 / 3;
|
||||
border-radius: var(--radius-sm);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
background: var(--bg-color);
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.ap-bg-preview-inner {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
/* Mini previews for each effect type */
|
||||
[data-effect="none"] .ap-bg-preview-inner {
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
[data-effect="noise"] .ap-bg-preview-inner {
|
||||
background: radial-gradient(ellipse at 30% 50%,
|
||||
color-mix(in srgb, var(--primary-color) 20%, var(--bg-color)) 0%,
|
||||
var(--bg-color) 70%);
|
||||
animation: ap-noise-shimmer 4s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes ap-noise-shimmer {
|
||||
from { opacity: 0.7; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
[data-effect="grid"] .ap-bg-preview-inner {
|
||||
background-image:
|
||||
radial-gradient(circle, var(--text-muted) 0.5px, transparent 0.5px);
|
||||
background-size: 8px 8px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
[data-effect="mesh"] .ap-bg-preview-inner {
|
||||
background:
|
||||
radial-gradient(ellipse at 20% 30%,
|
||||
color-mix(in srgb, var(--primary-color) 18%, transparent) 0%, transparent 60%),
|
||||
radial-gradient(ellipse at 80% 70%,
|
||||
color-mix(in srgb, var(--primary-color) 12%, transparent) 0%, transparent 60%);
|
||||
}
|
||||
|
||||
[data-effect="scanlines"] .ap-bg-preview-inner {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0px,
|
||||
transparent 2px,
|
||||
color-mix(in srgb, var(--text-muted) 10%, transparent) 2px,
|
||||
color-mix(in srgb, var(--text-muted) 10%, transparent) 3px
|
||||
);
|
||||
}
|
||||
|
||||
[data-effect="particles"] .ap-bg-preview-inner {
|
||||
background:
|
||||
radial-gradient(circle 2px at 20% 40%, var(--primary-color) 0%, transparent 100%),
|
||||
radial-gradient(circle 1.5px at 60% 25%, var(--primary-color) 0%, transparent 100%),
|
||||
radial-gradient(circle 2px at 75% 65%, var(--primary-color) 0%, transparent 100%),
|
||||
radial-gradient(circle 1px at 40% 80%, var(--primary-color) 0%, transparent 100%),
|
||||
radial-gradient(circle 1.5px at 90% 35%, var(--primary-color) 0%, transparent 100%);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* ═══ Full-page background effects ═══
|
||||
Uses a dedicated <div id="bg-effect-layer"> (same pattern as the WebGL canvas).
|
||||
The active effect class (e.g. .bg-effect-grid) is set directly on the div. */
|
||||
|
||||
/* When a CSS bg effect is active, make body transparent so the layer shows through
|
||||
(mirrors [data-bg-anim="on"] body { background: transparent } in base.css) */
|
||||
[data-bg-effect] body {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-bg-effect] header {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
[data-bg-effect] header::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
backdrop-filter: blur(12px);
|
||||
-webkit-backdrop-filter: blur(12px);
|
||||
background: color-mix(in srgb, var(--bg-color) 60%, transparent);
|
||||
}
|
||||
|
||||
/* Card translucency for CSS bg effects (match existing bg-anim behaviour) */
|
||||
[data-bg-effect][data-theme="dark"] .card,
|
||||
[data-bg-effect][data-theme="dark"] .template-card,
|
||||
[data-bg-effect][data-theme="dark"] .add-device-card,
|
||||
[data-bg-effect][data-theme="dark"] .dashboard-target {
|
||||
background: rgba(45, 45, 45, 0.92);
|
||||
}
|
||||
|
||||
[data-bg-effect][data-theme="light"] .card,
|
||||
[data-bg-effect][data-theme="light"] .template-card,
|
||||
[data-bg-effect][data-theme="light"] .add-device-card,
|
||||
[data-bg-effect][data-theme="light"] .dashboard-target {
|
||||
background: rgba(255, 255, 255, 0.88);
|
||||
}
|
||||
|
||||
#bg-effect-layer {
|
||||
display: none;
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
#bg-effect-layer.bg-effect-grid,
|
||||
#bg-effect-layer.bg-effect-mesh,
|
||||
#bg-effect-layer.bg-effect-scanlines,
|
||||
#bg-effect-layer.bg-effect-particles {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── Grid: dot matrix ── */
|
||||
#bg-effect-layer.bg-effect-grid {
|
||||
background-image:
|
||||
radial-gradient(circle 1px, var(--text-secondary) 0%, transparent 100%);
|
||||
background-size: 28px 28px;
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
/* ── Gradient mesh: ambient blobs ── */
|
||||
#bg-effect-layer.bg-effect-mesh {
|
||||
background:
|
||||
radial-gradient(ellipse 600px 400px at 15% 20%,
|
||||
color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(ellipse 500px 500px at 85% 80%,
|
||||
color-mix(in srgb, var(--primary-color) 18%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(ellipse 400px 300px at 60% 40%,
|
||||
color-mix(in srgb, var(--primary-color) 14%, transparent) 0%, transparent 100%);
|
||||
animation: bg-mesh-drift 20s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bg-mesh-drift {
|
||||
0% { transform: translate(0, 0) scale(1); }
|
||||
50% { transform: translate(-20px, 15px) scale(1.05); }
|
||||
100% { transform: translate(10px, -10px) scale(0.98); }
|
||||
}
|
||||
|
||||
/* ── Scanlines: retro CRT ── */
|
||||
#bg-effect-layer.bg-effect-scanlines {
|
||||
background: repeating-linear-gradient(
|
||||
0deg,
|
||||
transparent 0px,
|
||||
transparent 3px,
|
||||
color-mix(in srgb, var(--text-muted) 8%, transparent) 3px,
|
||||
color-mix(in srgb, var(--text-muted) 8%, transparent) 4px
|
||||
);
|
||||
}
|
||||
|
||||
/* ── CSS Particles: glowing dots ── */
|
||||
#bg-effect-layer.bg-effect-particles {
|
||||
background:
|
||||
radial-gradient(circle 40px at 10% 20%, color-mix(in srgb, var(--primary-color) 30%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 25px at 30% 70%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 50px at 55% 15%, color-mix(in srgb, var(--primary-color) 20%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 30px at 70% 55%, color-mix(in srgb, var(--primary-color) 28%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 35px at 85% 35%, color-mix(in srgb, var(--primary-color) 22%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 20px at 45% 45%, color-mix(in srgb, var(--primary-color) 32%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 45px at 20% 85%, color-mix(in srgb, var(--primary-color) 18%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 28px at 92% 78%, color-mix(in srgb, var(--primary-color) 25%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 15px at 65% 90%, color-mix(in srgb, var(--primary-color) 35%, transparent) 0%, transparent 100%),
|
||||
radial-gradient(circle 35px at 5% 50%, color-mix(in srgb, var(--primary-color) 20%, transparent) 0%, transparent 100%);
|
||||
animation: bg-particles-float 30s ease-in-out infinite alternate;
|
||||
}
|
||||
|
||||
@keyframes bg-particles-float {
|
||||
0% { transform: translate(0, 0); }
|
||||
33% { transform: translate(15px, -20px); }
|
||||
66% { transform: translate(-10px, 10px); }
|
||||
100% { transform: translate(5px, -5px); }
|
||||
}
|
||||
|
||||
/* ─── Mobile: 2-column grid ─── */
|
||||
@media (max-width: 480px) {
|
||||
.ap-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user