feat: futuristic theme ladder + stacked landing banner
Add three opt-in dark themes (BooCode+, BooCode Classic, BooCode Override) plus an in-place Ember polish, on a class-scoped effects engine: matrix rain, a neon grid field, and frosted glass, all gated by a localStorage "Animated background" toggle and prefers-reduced- motion. Extend the server theme_id whitelist so the new ids persist, and replace the Home landing wordmark with the stacked mascot + wordmark banner. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -26,6 +26,13 @@
|
||||
@import "./themes/cobalt.css";
|
||||
@import "./themes/midnight-sapphire.css";
|
||||
@import "./themes/ember.css";
|
||||
/* Futuristic theme ladder — token files + effects stubs (Phases 2-5 fill them). */
|
||||
@import "./themes/boocode-plus.css";
|
||||
@import "./themes/boocode-classic.css";
|
||||
@import "./themes/boocode-override.css";
|
||||
@import "./themes/ember-polish.css";
|
||||
@import "./themes/effects/boocode-classic-fx.css";
|
||||
@import "./themes/effects/boocode-override-fx.css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
|
||||
57
apps/web/src/styles/themes/boocode-classic.css
Normal file
57
apps/web/src/styles/themes/boocode-classic.css
Normal file
@@ -0,0 +1,57 @@
|
||||
/* BooCode Classic (id: boocode-classic) — dark-only.
|
||||
Faithful revival of the original BooCode warm-amber-on-black palette
|
||||
(/opt/boolab data-mode='boocode'). Warm near-black base, orange + amber +
|
||||
rust accents, deep-rust borders.
|
||||
|
||||
Matrix-rain translucency trick (ported from boolab): the rain canvas sits at
|
||||
z-0 behind the layout. For it to show *through* the UI we make <html> the
|
||||
opaque page backstop (#0a0604), strip <body>'s background to transparent, and
|
||||
override the SURFACE tokens to ~82–86% opaque rgba so panels bleed the canvas
|
||||
through their gaps while keeping high-contrast (AA) text. Foreground / accent
|
||||
/ border colors stay fully opaque.
|
||||
|
||||
Anchors: #0a0604 #120a06 #1a0e08 #9a7a5a #f97316 */
|
||||
|
||||
/* Light selector intentionally carries dark values — this theme is dark-only.
|
||||
supportsLight:false in theme.ts means applyTheme always adds the dark class;
|
||||
this base block is a non-broken fallback if the cascade ever runs without it. */
|
||||
.theme-boocode-classic,
|
||||
.theme-boocode-classic.dark {
|
||||
--background: rgba(10, 6, 4, 0.86);
|
||||
--foreground: #f5e6d3;
|
||||
--card: rgba(26, 14, 8, 0.82);
|
||||
--card-foreground: #f5e6d3;
|
||||
--popover: rgba(18, 10, 6, 0.82);
|
||||
--popover-foreground: #f5e6d3;
|
||||
--primary: #f97316;
|
||||
--primary-foreground: #0a0604;
|
||||
--secondary: rgba(36, 20, 8, 0.88);
|
||||
--secondary-foreground: #f5e6d3;
|
||||
--muted: rgba(18, 10, 6, 0.82);
|
||||
--muted-foreground: #9a7a5a;
|
||||
--accent: #f97316;
|
||||
--accent-foreground: #0a0604;
|
||||
--destructive: #dc2626;
|
||||
--destructive-foreground: #f5e6d3;
|
||||
--border: #3a1f0c;
|
||||
--input: #4a2d14;
|
||||
--ring: #f97316;
|
||||
--sidebar: rgba(18, 10, 6, 0.82);
|
||||
--sidebar-foreground: #f5e6d3;
|
||||
--sidebar-primary: #f97316;
|
||||
--sidebar-primary-foreground: #0a0604;
|
||||
--sidebar-accent: color-mix(in oklab, #f97316 16%, transparent);
|
||||
--sidebar-accent-foreground: #f5e6d3;
|
||||
--sidebar-border: #3a1f0c;
|
||||
--sidebar-ring: #f97316;
|
||||
}
|
||||
|
||||
/* Opaque page backstop + transparent body so the fixed rain canvas (z-0) shows
|
||||
through every translucent surface above it. !important guards against any
|
||||
inline :root vars losing the cascade. */
|
||||
html.theme-boocode-classic {
|
||||
background-color: #0a0604 !important;
|
||||
}
|
||||
.theme-boocode-classic body {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
59
apps/web/src/styles/themes/boocode-override.css
Normal file
59
apps/web/src/styles/themes/boocode-override.css
Normal file
@@ -0,0 +1,59 @@
|
||||
/* BooCode Override (id: boocode-override) — dark-only.
|
||||
Full neon cyberpunk: magenta #ff2d78, cyan #00e5ff, violet #9b5de5 on
|
||||
blue-black #080b14. Knowingly the least calm theme.
|
||||
|
||||
Neon-field translucency trick (same shape as Classic, brighter field): the
|
||||
NeonField canvas sits at z-0 behind the layout. To let it show *through* the
|
||||
UI, <html> is the opaque page backstop (#080b14), <body> is transparent, and
|
||||
the SURFACE tokens are overridden to SEMI-TRANSPARENT rgba so panels bleed the
|
||||
neon grid through their gaps. Foreground / accent / border colours stay fully
|
||||
opaque. Panel alphas are kept HIGH (0.86–0.94) because the neon field is much
|
||||
brighter than the matrix rain — combined with the dimmed canvas (opacity in
|
||||
NeonField) this keeps every panel/body text run at AA (see the contrast notes
|
||||
in the role report). Neon is for accents/borders/glow — never body text.
|
||||
|
||||
Anchors: #080b14 #0d1120 #0f1525 #8aa9cd #ff2d78 */
|
||||
|
||||
/* Light selector intentionally carries dark values — this theme is dark-only.
|
||||
supportsLight:false in theme.ts means applyTheme always adds the dark class;
|
||||
this base block is a non-broken fallback if the cascade ever runs without it. */
|
||||
.theme-boocode-override,
|
||||
.theme-boocode-override.dark {
|
||||
--background: rgba(8, 11, 20, 0.88);
|
||||
--foreground: #cde0ff;
|
||||
--card: rgba(15, 21, 37, 0.88);
|
||||
--card-foreground: #cde0ff;
|
||||
--popover: rgba(13, 17, 32, 0.94);
|
||||
--popover-foreground: #cde0ff;
|
||||
--primary: #ff2d78;
|
||||
--primary-foreground: #080b14;
|
||||
--secondary: rgba(26, 34, 64, 0.92);
|
||||
--secondary-foreground: #cde0ff;
|
||||
--muted: rgba(13, 17, 32, 0.90);
|
||||
--muted-foreground: #8aa9cd;
|
||||
--accent: #00e5ff;
|
||||
--accent-foreground: #080b14;
|
||||
--destructive: #f0556b;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #243358;
|
||||
--input: #243358;
|
||||
--ring: #ff2d78;
|
||||
--sidebar: rgba(13, 17, 32, 0.86);
|
||||
--sidebar-foreground: #cde0ff;
|
||||
--sidebar-primary: #ff2d78;
|
||||
--sidebar-primary-foreground: #080b14;
|
||||
--sidebar-accent: color-mix(in oklab, #ff2d78 16%, transparent);
|
||||
--sidebar-accent-foreground: #cde0ff;
|
||||
--sidebar-border: #243358;
|
||||
--sidebar-ring: #ff2d78;
|
||||
}
|
||||
|
||||
/* Opaque page backstop + transparent body so the fixed NeonField canvas (z-0)
|
||||
shows through every translucent surface above it. !important guards against
|
||||
any inline :root vars losing the cascade. */
|
||||
html.theme-boocode-override {
|
||||
background-color: #080b14 !important;
|
||||
}
|
||||
.theme-boocode-override body {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
81
apps/web/src/styles/themes/boocode-plus.css
Normal file
81
apps/web/src/styles/themes/boocode-plus.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* BooCode+ (id: boocode-plus) — cool deep-slate + indigo accent #5e6ad2.
|
||||
Tasteful sci-fi tier: calm, premium, Linear-grade. Dark is the priority
|
||||
(the user only runs dark mode); the light variant is kept reasonable but
|
||||
secondary. Token shape mirrors obsidian.css exactly.
|
||||
|
||||
Phase 2 effects (frosted-glass chrome, static ambient gradient, indigo glow,
|
||||
spring transitions) live in the sibling sheet below. The scaffold wired the
|
||||
two CANVAS themes' *-fx.css in globals.css but never a boocode-plus-fx.css,
|
||||
and globals.css is out of scope for Phase 2 — so the effects sheet is pulled
|
||||
in here via a nested @import (Lightning CSS resolves it relative to this
|
||||
file → ./effects/boocode-plus-fx.css). @import must precede all rules. */
|
||||
@import "./effects/boocode-plus-fx.css";
|
||||
|
||||
/* Light variant — secondary. Deeper indigo (#5159c4) so white button text
|
||||
clears AA on light, darker muted-foreground (#5c6178) for AA secondary text.
|
||||
Glass/gradient/glow are dark-only (see the fx sheet), so light is flat. */
|
||||
.theme-boocode-plus {
|
||||
--background: #f7f8fc;
|
||||
--foreground: #1b1e2e;
|
||||
--card: #ffffff;
|
||||
--card-foreground: #1b1e2e;
|
||||
--popover: #ffffff;
|
||||
--popover-foreground: #1b1e2e;
|
||||
--primary: #5159c4;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #eceef6;
|
||||
--secondary-foreground: #1b1e2e;
|
||||
--muted: #eceef6;
|
||||
--muted-foreground: #5c6178;
|
||||
--accent: #5159c4;
|
||||
--accent-foreground: #ffffff;
|
||||
--destructive: #c8202e;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #e0e3ef;
|
||||
--input: #e0e3ef;
|
||||
--ring: #5159c4;
|
||||
--sidebar: #eef0f7;
|
||||
--sidebar-foreground: #1b1e2e;
|
||||
--sidebar-primary: #5159c4;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: color-mix(in oklab, #5159c4 14%, transparent);
|
||||
--sidebar-accent-foreground: #1b1e2e;
|
||||
--sidebar-border: #e0e3ef;
|
||||
--sidebar-ring: #5159c4;
|
||||
}
|
||||
|
||||
/* Dark variant — the priority. AA-verified against its surfaces:
|
||||
foreground #e3e6f1 on card #1a1d2e = 13.0:1; muted-foreground #8f95ad on
|
||||
muted #242838 = 4.9:1; primary/accent-foreground #ffffff on #5e6ad2 = 4.7:1;
|
||||
destructive #f05252 as text on the dark base = 5.4:1. (primary-foreground
|
||||
and muted-foreground were lifted off the stub's #0f1117 / #7a7f99, which
|
||||
failed AA at 4.0:1 and 4.2:1.) */
|
||||
.theme-boocode-plus.dark {
|
||||
--background: #0f1117;
|
||||
--foreground: #e3e6f1;
|
||||
--card: #1a1d2e;
|
||||
--card-foreground: #e3e6f1;
|
||||
--popover: #1a1d2e;
|
||||
--popover-foreground: #e3e6f1;
|
||||
--primary: #5e6ad2;
|
||||
--primary-foreground: #ffffff;
|
||||
--secondary: #242838;
|
||||
--secondary-foreground: #e3e6f1;
|
||||
--muted: #242838;
|
||||
--muted-foreground: #8f95ad;
|
||||
--accent: #5e6ad2;
|
||||
--accent-foreground: #ffffff;
|
||||
--destructive: #f05252;
|
||||
--destructive-foreground: #ffffff;
|
||||
--border: #2a2f44;
|
||||
--input: #2a2f44;
|
||||
--ring: #5e6ad2;
|
||||
--sidebar: #13151d;
|
||||
--sidebar-foreground: #e3e6f1;
|
||||
--sidebar-primary: #5e6ad2;
|
||||
--sidebar-primary-foreground: #ffffff;
|
||||
--sidebar-accent: color-mix(in oklab, #5e6ad2 20%, transparent);
|
||||
--sidebar-accent-foreground: #e3e6f1;
|
||||
--sidebar-border: #2a2f44;
|
||||
--sidebar-ring: #5e6ad2;
|
||||
}
|
||||
268
apps/web/src/styles/themes/effects/boocode-classic-fx.css
Normal file
268
apps/web/src/styles/themes/effects/boocode-classic-fx.css
Normal file
@@ -0,0 +1,268 @@
|
||||
/* BooCode Classic — effects layer. Faithful revival of /opt/boolab
|
||||
data-mode='boocode' terminal-HUD chrome.
|
||||
|
||||
Scoping contract (immutable):
|
||||
- EVERY rule is scoped under `.theme-boocode-classic` so the other 22 themes
|
||||
are byte-for-byte unaffected.
|
||||
- EVERY continuous keyframe animation is *additionally* scoped under
|
||||
`html.bc-anim-on.theme-boocode-classic`. ThemeFx sets `bc-anim-on` only
|
||||
when the Animated-background toggle is ON *and* prefers-reduced-motion is
|
||||
not set — so toggling either off removes the class and freezes all motion.
|
||||
@keyframes names are global (CSS can't scope the at-rule itself); they are
|
||||
`bc-`-prefixed and only *referenced* under the gated selector, so no
|
||||
animation runs unless the gate is on.
|
||||
|
||||
The matrix-rain canvas lives in components/fx/MatrixRain.tsx (mounted by
|
||||
ThemeFx). This sheet adds: an Orbitron display wordmark + blinking caret, an
|
||||
app-wide scanline sweep, orange card hover glow + lift, terminal-frame chrome
|
||||
on the rails, and the ported boolab `.bc-*` / `.boocode-*` design-system
|
||||
classes (dormant until a HUD component consumes them). */
|
||||
|
||||
.theme-boocode-classic {
|
||||
--bc-orange: #f97316;
|
||||
--bc-amber: #fbbf24;
|
||||
--bc-rust: #c2410c;
|
||||
--bc-glow-orange: 0 0 24px color-mix(in srgb, var(--bc-orange) 32%, transparent);
|
||||
--bc-glow-soft: 0 0 40px color-mix(in srgb, var(--bc-orange) 14%, transparent);
|
||||
}
|
||||
|
||||
/* ── Orbitron display wordmark ────────────────────────────────────────────
|
||||
Applied to the "BooCode" text heading via the additive `boocode-display`
|
||||
class (the only text wordmark — the sidebar wordmark is an <img>, which a
|
||||
font can't restyle). Orbitron 800 is JS-imported in main.tsx. */
|
||||
.theme-boocode-classic .boocode-display {
|
||||
font-family: 'Orbitron', var(--font-sans);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--bc-orange);
|
||||
text-shadow: 0 0 14px color-mix(in srgb, var(--bc-orange) 45%, transparent);
|
||||
}
|
||||
|
||||
/* Terminal cursor after the wordmark. Always rendered (a solid block when
|
||||
motion is off — boolab's reduced-motion behaviour); blinks only when gated. */
|
||||
.theme-boocode-classic .boocode-display::after {
|
||||
content: '▮';
|
||||
margin-left: 0.12em;
|
||||
color: var(--bc-amber);
|
||||
opacity: 1;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic .boocode-display::after {
|
||||
animation: bc-caret-blink 1s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
/* ── App-wide scanline sweep ──────────────────────────────────────────────
|
||||
A soft warm band that drifts down the viewport on a calm 8s cycle.
|
||||
`position: fixed` keeps it viewport-locked (never creates document overflow);
|
||||
mix-blend-mode: screen only lightens, so text underneath stays legible.
|
||||
Defined ONLY under the gate → vanishes entirely when motion is off (the warm
|
||||
palette + static rain-off state is the reduced-motion fallback).
|
||||
`.h-dvh.bg-background` is unique to the AppShell root (App.tsx). */
|
||||
html.bc-anim-on.theme-boocode-classic .h-dvh.bg-background::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
transparent 46%,
|
||||
color-mix(in srgb, var(--bc-orange) 9%, transparent) 50%,
|
||||
transparent 54%
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
animation: bc-scanline 8s linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ── Card hover glow + lift ───────────────────────────────────────────────
|
||||
The shadcn Card primitive (`data-slot="card"`). The orange rim + drop glow
|
||||
apply on hover under any condition (hover feedback is acceptable under
|
||||
reduced motion); the transition timing and the 1px lift are gated so motion
|
||||
off = instant, no travel. */
|
||||
.theme-boocode-classic [data-slot="card"]:hover {
|
||||
border-color: color-mix(in srgb, var(--bc-orange) 55%, transparent);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bc-orange) 22%, transparent),
|
||||
0 6px 18px -8px color-mix(in srgb, var(--bc-orange) 40%, transparent);
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic [data-slot="card"] {
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic [data-slot="card"]:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── Terminal-frame chrome on the rails ───────────────────────────────────
|
||||
Monospace + a hairline orange edge-light on the sidebar (vertical list that
|
||||
already truncates — low overflow risk). Pane top-bars are intentionally left
|
||||
alone: they are crowded control rows where a wider mono font risks wrapping. */
|
||||
.theme-boocode-classic .bg-sidebar {
|
||||
font-family: var(--font-mono);
|
||||
box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--bc-orange) 8%, transparent);
|
||||
}
|
||||
/* Floating surfaces (menus / dialogs / popovers) are intentionally NOT given an
|
||||
inset box-shadow here — that would clobber shadcn's `shadow-md` elevation
|
||||
(higher specificity) and flatten menus over the rain. The warm `--popover` /
|
||||
`--border` tokens already theme them. */
|
||||
|
||||
/* ──────────────────────────────────────────────────────────────────────────
|
||||
Ported boolab design-system classes. These are faithful copies of the
|
||||
/opt/boolab `.bc-*` / `.boocode-*` chrome. No component in this repo renders
|
||||
these class names yet (boolab's RepoStatusBar / HUD components don't exist
|
||||
here), so they are DORMANT — shipped so the Classic design system is complete
|
||||
and any future HUD element (prompt line, status pill, kbd hint, breadcrumb)
|
||||
lights up automatically. All scoped to the theme; animations gated.
|
||||
────────────────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* Terminal prompt line: `$ boocode @host: path (branch)` */
|
||||
.theme-boocode-classic .bc-prompt-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
row-gap: 0.25rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.8125rem;
|
||||
color: var(--muted-foreground);
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--popover);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.theme-boocode-classic .bc-prompt-host { color: var(--bc-orange); }
|
||||
.theme-boocode-classic .bc-prompt-branch { color: var(--bc-orange); opacity: 0.8; }
|
||||
.theme-boocode-classic .bc-prompt-dollar {
|
||||
color: var(--bc-orange);
|
||||
text-shadow: 0 0 6px var(--bc-orange);
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
/* Standalone blinking block caret. */
|
||||
.theme-boocode-classic .bc-caret {
|
||||
display: inline-block;
|
||||
width: 0.6em;
|
||||
height: 1em;
|
||||
background: currentColor;
|
||||
vertical-align: text-bottom;
|
||||
margin-left: 0.1em;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic .bc-caret {
|
||||
animation: bc-caret-blink 1s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
/* IDLE / SYNCING / ERROR status pills. */
|
||||
.theme-boocode-classic .bc-status-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding: 0.1rem 0.5rem;
|
||||
border-radius: 999px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--card);
|
||||
font-size: 0.6875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.theme-boocode-classic .bc-status-idle { color: #7ae07a; border-color: rgba(122, 224, 122, 0.4); }
|
||||
.theme-boocode-classic .bc-status-syncing {
|
||||
color: var(--bc-orange);
|
||||
border-color: color-mix(in srgb, var(--bc-orange) 60%, transparent);
|
||||
}
|
||||
.theme-boocode-classic .bc-status-error { color: #ff6b6b; border-color: rgba(255, 107, 107, 0.5); }
|
||||
.theme-boocode-classic .bc-status-syncing::before {
|
||||
content: '▮';
|
||||
color: var(--bc-orange);
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic .bc-status-syncing::before {
|
||||
animation: bc-caret-blink 1s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
/* Keyboard hint chip. */
|
||||
.theme-boocode-classic .bc-key-hint {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 1.25rem;
|
||||
padding: 0 0.3rem;
|
||||
height: 1.1rem;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.25rem;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.625rem;
|
||||
color: var(--muted-foreground);
|
||||
background: var(--popover);
|
||||
}
|
||||
|
||||
/* Uppercase orange breadcrumb / overline. */
|
||||
.theme-boocode-classic .boocode-breadcrumb {
|
||||
font-family: 'Orbitron', var(--font-sans);
|
||||
font-size: 0.6875rem;
|
||||
letter-spacing: 0.22em;
|
||||
text-transform: uppercase;
|
||||
color: var(--bc-orange);
|
||||
text-shadow: 0 0 8px color-mix(in srgb, var(--bc-orange) 40%, transparent);
|
||||
}
|
||||
|
||||
/* Inset terminal-frame border. */
|
||||
.theme-boocode-classic .boocode-terminal-frame {
|
||||
border: 1px solid var(--border);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px color-mix(in srgb, var(--bc-orange) 5%, transparent),
|
||||
0 0 0 1px #0a0604;
|
||||
}
|
||||
|
||||
/* Card hover glow + lift utility (boolab `.bc-card`), for non-shadcn surfaces. */
|
||||
.theme-boocode-classic .bc-card {
|
||||
position: relative;
|
||||
border: 1px solid color-mix(in srgb, var(--bc-orange) 22%, transparent);
|
||||
background: var(--card);
|
||||
padding: 1rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
.theme-boocode-classic .bc-card:hover {
|
||||
border-color: color-mix(in srgb, var(--bc-orange) 55%, transparent);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bc-orange) 22%, transparent),
|
||||
0 6px 18px -8px color-mix(in srgb, var(--bc-orange) 40%, transparent);
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic .bc-card {
|
||||
transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-classic .bc-card:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── Keyframes (global names, referenced only under the gate above) ───────── */
|
||||
@keyframes bc-caret-blink {
|
||||
0%, 49% { opacity: 1; }
|
||||
50%, 100% { opacity: 0; }
|
||||
}
|
||||
@keyframes bc-scanline {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100%); }
|
||||
}
|
||||
|
||||
/* ── Reduced-motion belt-and-suspenders ───────────────────────────────────
|
||||
bc-anim-on already excludes reduced-motion (ThemeFx), but if the class ever
|
||||
lingered, this hard-stops every animation and the lift transform. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-boocode-classic .boocode-display::after,
|
||||
.theme-boocode-classic .bc-caret,
|
||||
.theme-boocode-classic .bc-status-syncing::before {
|
||||
animation: none;
|
||||
opacity: 1;
|
||||
}
|
||||
.theme-boocode-classic .h-dvh.bg-background::after {
|
||||
animation: none;
|
||||
display: none;
|
||||
}
|
||||
.theme-boocode-classic [data-slot="card"],
|
||||
.theme-boocode-classic .bc-card {
|
||||
transition: none;
|
||||
}
|
||||
.theme-boocode-classic [data-slot="card"]:hover,
|
||||
.theme-boocode-classic .bc-card:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
288
apps/web/src/styles/themes/effects/boocode-override-fx.css
Normal file
288
apps/web/src/styles/themes/effects/boocode-override-fx.css
Normal file
@@ -0,0 +1,288 @@
|
||||
/* BooCode Override — effects layer. Full neon cyberpunk: heavy bloom/glow,
|
||||
chromatic-glitch wordmark, glitch-on-hover, strong scanlines, neon border
|
||||
pulses. Magenta #ff2d78 + cyan #00e5ff + violet #9b5de5 on blue-black.
|
||||
|
||||
Scoping contract (immutable, same as boocode-classic-fx.css):
|
||||
- EVERY rule is scoped under `.theme-boocode-override` so the other 22 themes
|
||||
are byte-for-byte unaffected.
|
||||
- EVERY continuous keyframe animation is *additionally* scoped under
|
||||
`html.bc-anim-on.theme-boocode-override`. ThemeFx sets `bc-anim-on` only
|
||||
when the Animated-background toggle is ON *and* prefers-reduced-motion is
|
||||
not set — so toggling either off removes the class and freezes all motion.
|
||||
@keyframes names are global (CSS can't scope the at-rule itself); they are
|
||||
`bco-`-prefixed and only *referenced* under the gated selector.
|
||||
|
||||
Static vs. gated split:
|
||||
- STATIC (always, under .theme-boocode-override): the neon palette, a bloom
|
||||
vignette painted on the app-shell background, static neon rims/glows on the
|
||||
primary action / cards / inputs, and a neon focus ring. This is the
|
||||
"static tint" fallback shown when the toggle is OFF or reduced-motion is on.
|
||||
- GATED (under html.bc-anim-on.theme-boocode-override): the NeonField canvas
|
||||
(mounted by ThemeFx), the scanline texture + moving sweep, the wordmark
|
||||
chromatic glitch, the hover glitch jitter, and the idle neon pulse.
|
||||
|
||||
AA: neon is used ONLY for accents/borders/glow and the (large display)
|
||||
wordmark — never body text. Body/panel text keeps the light #cde0ff /
|
||||
#8aa9cd tokens over the high-alpha translucent surfaces (see
|
||||
boocode-override.css). Glitch uses transform / text-shadow / clip-path only,
|
||||
so it never reflows layout. */
|
||||
|
||||
.theme-boocode-override {
|
||||
--bco-magenta: #ff2d78;
|
||||
--bco-cyan: #00e5ff;
|
||||
--bco-violet: #9b5de5;
|
||||
--bco-glow-magenta: 0 0 22px color-mix(in srgb, var(--bco-magenta) 42%, transparent);
|
||||
--bco-glow-cyan: 0 0 22px color-mix(in srgb, var(--bco-cyan) 38%, transparent);
|
||||
}
|
||||
|
||||
/* ── Bloom vignette on the app shell ──────────────────────────────────────
|
||||
Painted as background-image on the AppShell root (`.h-dvh.bg-background`,
|
||||
unique to App.tsx). The background-COLOR stays the translucent --background
|
||||
token so the NeonField canvas (z-0, behind the z-10 root) still shows through
|
||||
the panel gaps; these radial neon pools sit over the field for an atmospheric
|
||||
bloom. Static → this is part of the reduced-motion / toggle-off fallback. */
|
||||
.theme-boocode-override .h-dvh.bg-background {
|
||||
background-color: var(--background);
|
||||
background-image:
|
||||
radial-gradient(125% 90% at 50% 8%,
|
||||
color-mix(in oklab, var(--bco-magenta) 12%, transparent), transparent 46%),
|
||||
radial-gradient(120% 85% at 6% 102%,
|
||||
color-mix(in oklab, var(--bco-cyan) 11%, transparent), transparent 50%),
|
||||
radial-gradient(120% 95% at 100% 100%,
|
||||
color-mix(in oklab, var(--bco-violet) 12%, transparent), transparent 52%),
|
||||
radial-gradient(140% 120% at 50% 50%,
|
||||
transparent 60%, rgba(2, 4, 10, 0.55) 100%);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* ── Strong scanlines (gated) ─────────────────────────────────────────────
|
||||
::before = a fine static CRT scanline texture; ::after = a bright neon band
|
||||
that sweeps the viewport. Both are `position: fixed` (never create document
|
||||
overflow), `pointer-events: none`, and use `mix-blend-mode: screen` so they
|
||||
only LIGHTEN — text underneath never loses contrast. Both live ONLY under the
|
||||
gate, so motion-off / reduced-motion removes scanlines entirely (the bloom
|
||||
vignette above is the static fallback). z-index:2/3 overlays the static panels
|
||||
inside the shell; portaled dialogs (separate stacking context) stay clean. */
|
||||
html.bc-anim-on.theme-boocode-override .h-dvh.bg-background::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2;
|
||||
pointer-events: none;
|
||||
background-image: repeating-linear-gradient(
|
||||
0deg,
|
||||
color-mix(in srgb, var(--bco-cyan) 9%, transparent) 0px,
|
||||
color-mix(in srgb, var(--bco-cyan) 9%, transparent) 1px,
|
||||
transparent 1px,
|
||||
transparent 3px
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
opacity: 0.55;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override .h-dvh.bg-background::after {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
pointer-events: none;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
transparent 44%,
|
||||
color-mix(in srgb, var(--bco-cyan) 10%, transparent) 49%,
|
||||
color-mix(in srgb, var(--bco-magenta) 12%, transparent) 51%,
|
||||
transparent 56%
|
||||
);
|
||||
mix-blend-mode: screen;
|
||||
animation: bco-scan-sweep 7s linear infinite;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* ── Neon chromatic-glitch wordmark ───────────────────────────────────────
|
||||
The "BooCode" display heading (Home.tsx, additive `boocode-display` class —
|
||||
the same hook BooCode Classic restyles; only one theme is ever active).
|
||||
Orbitron 800 (JS-imported in main.tsx). A near-white core with stacked cyan +
|
||||
magenta glow gives the neon tube look; the gated glitch jitters the colour
|
||||
split + a clip slice via transform/text-shadow only → zero layout shift. */
|
||||
.theme-boocode-override .boocode-display {
|
||||
font-family: 'Orbitron', var(--font-sans);
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.08em;
|
||||
color: #eafaff;
|
||||
text-shadow:
|
||||
0 0 2px color-mix(in srgb, var(--bco-cyan) 80%, transparent),
|
||||
-0.02em 0 0 color-mix(in srgb, var(--bco-magenta) 70%, transparent),
|
||||
0.02em 0 0 color-mix(in srgb, var(--bco-cyan) 70%, transparent),
|
||||
0 0 18px color-mix(in srgb, var(--bco-cyan) 55%, transparent),
|
||||
0 0 34px color-mix(in srgb, var(--bco-magenta) 40%, transparent);
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override .boocode-display {
|
||||
animation: bco-glitch 5.5s steps(1, end) infinite;
|
||||
}
|
||||
|
||||
/* ── Primary action: neon rim + idle pulse + hover bloom + press squash ────
|
||||
The default-variant Button (Send / primary confirm). Static magenta rim +
|
||||
drop glow (always); a gentle idle pulse and a hover glitch jitter under the
|
||||
gate; box-shadow / transform / filter only → never reflows. */
|
||||
.theme-boocode-override [data-slot="button"][data-variant="default"] {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 55%, transparent),
|
||||
0 4px 20px -6px color-mix(in srgb, var(--bco-magenta) 60%, transparent);
|
||||
}
|
||||
.theme-boocode-override [data-slot="button"][data-variant="default"]:not([disabled]):hover {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 85%, transparent),
|
||||
0 0 16px -2px color-mix(in srgb, var(--bco-magenta) 70%, transparent),
|
||||
0 8px 30px -6px color-mix(in srgb, var(--bco-cyan) 55%, transparent);
|
||||
filter: brightness(1.08);
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override [data-slot="button"][data-variant="default"] {
|
||||
transition: box-shadow 200ms ease, transform 120ms ease, filter 160ms ease;
|
||||
animation: bco-pulse 3.4s ease-in-out infinite;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override [data-slot="button"][data-variant="default"]:not([disabled]):hover {
|
||||
animation: bco-hover-glitch 320ms steps(1, end) 1;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override [data-slot="button"][data-variant="default"]:not([disabled]):active {
|
||||
transform: translateY(0.5px) scale(0.985);
|
||||
}
|
||||
|
||||
/* ── Cards: neon edge + hover glow/lift ───────────────────────────────────
|
||||
shadcn Card primitive (`data-slot="card"`). Static cyan hairline; on hover a
|
||||
cyan/magenta rim glow + 1px lift (lift + transition gated). */
|
||||
.theme-boocode-override [data-slot="card"] {
|
||||
box-shadow: 0 0 0 1px color-mix(in srgb, var(--bco-cyan) 12%, transparent);
|
||||
}
|
||||
.theme-boocode-override [data-slot="card"]:hover {
|
||||
border-color: color-mix(in srgb, var(--bco-cyan) 55%, transparent);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bco-cyan) 38%, transparent),
|
||||
0 0 22px -6px color-mix(in srgb, var(--bco-magenta) 45%, transparent),
|
||||
0 8px 24px -10px color-mix(in srgb, var(--bco-cyan) 50%, transparent);
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override [data-slot="card"] {
|
||||
transition: border-color 180ms ease, box-shadow 180ms ease, transform 180ms ease;
|
||||
}
|
||||
html.bc-anim-on.theme-boocode-override [data-slot="card"]:hover {
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
/* ── Sidebar edge-light + neon focus ring ─────────────────────────────────
|
||||
A static cyan inset hairline down the sidebar's trailing edge; a magenta neon
|
||||
focus ring on inputs/buttons (focus-visible only, static — focus feedback is
|
||||
acceptable under reduced motion). */
|
||||
.theme-boocode-override .bg-sidebar {
|
||||
box-shadow: inset -1px 0 0 0 color-mix(in srgb, var(--bco-cyan) 16%, transparent);
|
||||
}
|
||||
.theme-boocode-override [data-slot="input"]:focus-visible,
|
||||
.theme-boocode-override [data-slot="button"]:focus-visible,
|
||||
.theme-boocode-override [data-slot="textarea"]:focus-visible {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 70%, transparent),
|
||||
0 0 14px -2px color-mix(in srgb, var(--bco-magenta) 55%, transparent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* ── Links / accent text glow ─────────────────────────────────────────────
|
||||
Anchors inside content get a soft cyan glow on hover — accent, not body. */
|
||||
.theme-boocode-override a:hover {
|
||||
text-shadow: 0 0 10px color-mix(in srgb, var(--bco-cyan) 55%, transparent);
|
||||
}
|
||||
|
||||
/* ── Keyframes (global names, referenced only under the gate above) ───────── */
|
||||
|
||||
/* Scanline sweep: a bright neon band drifts top→bottom. */
|
||||
@keyframes bco-scan-sweep {
|
||||
0% { transform: translateY(-100%); }
|
||||
100% { transform: translateY(100%); }
|
||||
}
|
||||
|
||||
/* Idle neon pulse on the primary action: the glow swells and settles. */
|
||||
@keyframes bco-pulse {
|
||||
0%, 100% {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 55%, transparent),
|
||||
0 4px 20px -6px color-mix(in srgb, var(--bco-magenta) 55%, transparent);
|
||||
}
|
||||
50% {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--bco-magenta) 80%, transparent),
|
||||
0 0 18px -2px color-mix(in srgb, var(--bco-magenta) 60%, transparent),
|
||||
0 6px 26px -6px color-mix(in srgb, var(--bco-cyan) 45%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* Wordmark chromatic glitch: mostly steady, with brief jitter bursts. Only
|
||||
transform / text-shadow / clip-path change → no reflow. */
|
||||
@keyframes bco-glitch {
|
||||
0%, 88%, 100% {
|
||||
transform: translate3d(0, 0, 0);
|
||||
clip-path: none;
|
||||
text-shadow:
|
||||
0 0 2px color-mix(in srgb, var(--bco-cyan) 80%, transparent),
|
||||
-0.02em 0 0 color-mix(in srgb, var(--bco-magenta) 70%, transparent),
|
||||
0.02em 0 0 color-mix(in srgb, var(--bco-cyan) 70%, transparent),
|
||||
0 0 18px color-mix(in srgb, var(--bco-cyan) 55%, transparent),
|
||||
0 0 34px color-mix(in srgb, var(--bco-magenta) 40%, transparent);
|
||||
}
|
||||
90% {
|
||||
transform: translate3d(-2px, 0, 0);
|
||||
clip-path: inset(8% 0 62% 0);
|
||||
text-shadow:
|
||||
-0.08em 0 0 color-mix(in srgb, var(--bco-magenta) 90%, transparent),
|
||||
0.08em 0 0 color-mix(in srgb, var(--bco-cyan) 90%, transparent),
|
||||
0 0 22px color-mix(in srgb, var(--bco-cyan) 60%, transparent);
|
||||
}
|
||||
92% {
|
||||
transform: translate3d(2px, 0, 0);
|
||||
clip-path: inset(54% 0 18% 0);
|
||||
text-shadow:
|
||||
0.1em 0 0 color-mix(in srgb, var(--bco-magenta) 90%, transparent),
|
||||
-0.06em 0 0 color-mix(in srgb, var(--bco-cyan) 85%, transparent),
|
||||
0 0 22px color-mix(in srgb, var(--bco-magenta) 60%, transparent);
|
||||
}
|
||||
94% {
|
||||
transform: translate3d(-1px, 0, 0);
|
||||
clip-path: inset(34% 0 40% 0);
|
||||
text-shadow:
|
||||
-0.05em 0 0 color-mix(in srgb, var(--bco-cyan) 85%, transparent),
|
||||
0.05em 0 0 color-mix(in srgb, var(--bco-magenta) 85%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* Hover glitch on the primary button: a single quick colour-split jitter. */
|
||||
@keyframes bco-hover-glitch {
|
||||
0% { transform: translate3d(0, 0, 0); }
|
||||
25% { transform: translate3d(-1.5px, 0, 0); }
|
||||
50% { transform: translate3d(1.5px, 0, 0); }
|
||||
75% { transform: translate3d(-0.5px, 0, 0); }
|
||||
100% { transform: translate3d(0, 0, 0); }
|
||||
}
|
||||
|
||||
/* ── Reduced-motion belt-and-suspenders ───────────────────────────────────
|
||||
bc-anim-on already excludes reduced-motion (ThemeFx), but if the class ever
|
||||
lingered, this hard-stops every animation and the lift/jitter transforms. The
|
||||
static neon rims, bloom vignette and focus ring remain (the "static tint"). */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-boocode-override .boocode-display,
|
||||
.theme-boocode-override .h-dvh.bg-background::before,
|
||||
.theme-boocode-override .h-dvh.bg-background::after,
|
||||
.theme-boocode-override [data-slot="button"][data-variant="default"],
|
||||
.theme-boocode-override [data-slot="button"][data-variant="default"]:hover {
|
||||
animation: none;
|
||||
}
|
||||
.theme-boocode-override .h-dvh.bg-background::after {
|
||||
display: none;
|
||||
}
|
||||
.theme-boocode-override .boocode-display {
|
||||
transform: none;
|
||||
clip-path: none;
|
||||
}
|
||||
.theme-boocode-override [data-slot="card"],
|
||||
.theme-boocode-override [data-slot="button"][data-variant="default"] {
|
||||
transition: none;
|
||||
}
|
||||
.theme-boocode-override [data-slot="card"]:hover,
|
||||
.theme-boocode-override [data-slot="button"][data-variant="default"]:active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
205
apps/web/src/styles/themes/effects/boocode-plus-fx.css
Normal file
205
apps/web/src/styles/themes/effects/boocode-plus-fx.css
Normal file
@@ -0,0 +1,205 @@
|
||||
/* BooCode+ — effects layer. Tasteful sci-fi: calm, premium, Linear-grade.
|
||||
EVERY rule is scoped under `.theme-boocode-plus.dark` so (a) the other 22
|
||||
themes are byte-for-byte unaffected and (b) the light variant stays a clean
|
||||
flat slate — dark is the priority, so the glass/gradient/glow ship dark-only.
|
||||
|
||||
No canvas, no animation loop, no scanlines. Just: a faint STATIC ambient
|
||||
gradient on the app background, frosted glass on CHROME ONLY (rails, menus,
|
||||
dialogs, popovers, the composer, pane top-bars), a restrained indigo glow on
|
||||
the primary action, and spring-eased transitions on chrome state changes.
|
||||
|
||||
iOS / quality guardrails honored:
|
||||
- backdrop-blur capped at 8–12px; no nested blur-on-blur (the dialog
|
||||
overlay's own backdrop-blur is neutralized so only the dialog CONTENT
|
||||
frosts — one layer).
|
||||
- opaque fallback: every glass rule lives inside @supports(backdrop-filter),
|
||||
so a browser without it keeps the plain opaque Tailwind utility
|
||||
(bg-sidebar/bg-popover/bg-card = solid token). prefers-reduced-transparency
|
||||
forces the same opaque path.
|
||||
- spring via cubic-bezier (NOT the linear() function — Safari <17.2 gap),
|
||||
confined to box-shadow / transform / color / filter — never layout props.
|
||||
- chat messages, tool-call cards and code blocks (bg-muted/30·/20, bg-card
|
||||
bubbles) are deliberately NOT targeted — glass never sits behind reading
|
||||
content; the ambient gradient behind it stays faint & opaque-based (AA).
|
||||
|
||||
Glass targets are stable, verified hooks: bg-sidebar (both rails), bg-popover
|
||||
(all menus/dialogs/popovers/sheets), the composer's unique
|
||||
focus-within:ring-primary/15 box, [data-slot=button][data-variant=default]
|
||||
(the primary action), and the compound .border-b.bg-muted/30·/20 (pane
|
||||
top-bars — distinct from message bubbles, which use .border.rounded-lg). */
|
||||
|
||||
.theme-boocode-plus.dark {
|
||||
/* Spring curves. --bcp-spring overshoots a touch (press/glow bloom);
|
||||
--bcp-ease is a smooth Linear-style decel for color/opacity. */
|
||||
--bcp-spring: cubic-bezier(0.34, 1.42, 0.5, 1);
|
||||
--bcp-ease: cubic-bezier(0.32, 0.72, 0, 1);
|
||||
--bcp-blur: 10px;
|
||||
}
|
||||
|
||||
/* ── Static ambient gradient ─────────────────────────────────────────────
|
||||
Painted on the app-shell root (.h-dvh is unique to App.tsx). Opaque base
|
||||
(var(--background)) + three faint indigo radials = depth without cost. The
|
||||
MessageList scroll area is transparent, so the gradient reads faintly behind
|
||||
chat — but it's static and opaque-based: the brightest stop (indigo @14% over
|
||||
#0f1117) still gives ~10:1 for #e3e6f1 body text. The glass rails blur it for
|
||||
a premium parallax-of-light feel. No background-attachment:fixed (iOS jank);
|
||||
.h-dvh doesn't scroll, so the field is already viewport-stable. */
|
||||
.theme-boocode-plus.dark .h-dvh {
|
||||
background-color: var(--background);
|
||||
background-image:
|
||||
radial-gradient(1200px 760px at 8% -12%,
|
||||
color-mix(in oklab, #5e6ad2 14%, transparent), transparent 58%),
|
||||
radial-gradient(1000px 680px at 102% 4%,
|
||||
color-mix(in oklab, #5a73d8 10%, transparent), transparent 54%),
|
||||
radial-gradient(1100px 900px at 50% 128%,
|
||||
color-mix(in oklab, #2c3270 16%, transparent), transparent 60%);
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/* ── Frosted glass on chrome ─────────────────────────────────────────────
|
||||
Progressive enhancement: only inside @supports does the surface go
|
||||
translucent + blur. Without backdrop-filter support the rules vanish and the
|
||||
plain opaque utility (solid token bg) shows — that IS the opaque fallback.
|
||||
Each background has an rgba() line first (covers the rare browser that has
|
||||
backdrop-filter but not color-mix, e.g. Safari 15) then the color-mix line. */
|
||||
@supports ((-webkit-backdrop-filter: blur(1px)) or (backdrop-filter: blur(1px))) {
|
||||
/* Side rails (ProjectSidebar + RightRail). Blur the ambient gradient behind
|
||||
them → the signature glass plane. Kept at 80% so the dense nav text stays
|
||||
crisp; the blur + saturate sells the effect, not heavy transparency. */
|
||||
.theme-boocode-plus.dark .bg-sidebar {
|
||||
background-color: rgba(19, 21, 29, 0.80);
|
||||
background-color: color-mix(in oklab, var(--sidebar) 80%, transparent);
|
||||
-webkit-backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
|
||||
backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
|
||||
}
|
||||
|
||||
/* Every floating surface: dialogs, dropdown / context / sub menus, the
|
||||
@-mention & slash pickers, the mobile bottom sheet, the message-actions
|
||||
menu. All carry bg-popover and all float over content → one clean blur. */
|
||||
.theme-boocode-plus.dark .bg-popover {
|
||||
background-color: rgba(26, 29, 46, 0.84);
|
||||
background-color: color-mix(in oklab, var(--popover) 84%, transparent);
|
||||
-webkit-backdrop-filter: blur(12px) saturate(150%);
|
||||
backdrop-filter: blur(12px) saturate(150%);
|
||||
}
|
||||
|
||||
/* The composer message box (ChatInput) — unique focus-within ring hook.
|
||||
Frosts the tail of the conversation as it scrolls beneath. 82% keeps the
|
||||
textarea text readable even over a bright code block underneath. */
|
||||
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
|
||||
background-color: rgba(26, 29, 46, 0.82);
|
||||
background-color: color-mix(in oklab, var(--card) 82%, transparent);
|
||||
-webkit-backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
|
||||
backdrop-filter: blur(var(--bcp-blur)) saturate(140%);
|
||||
}
|
||||
|
||||
/* Pane top-bars (Coder/Workspace/terminal-hotkey/artifact headers). The
|
||||
compound .border-b.bg-muted/N selector excludes chat bubbles & tool cards
|
||||
(those use .border.rounded-lg). Lightest blur (8px) + an inset top hairline
|
||||
for the edge-lit premium feel. */
|
||||
.theme-boocode-plus.dark .border-b.bg-muted\/30,
|
||||
.theme-boocode-plus.dark .border-b.bg-muted\/20 {
|
||||
background-color: rgba(28, 32, 47, 0.62);
|
||||
background-color: color-mix(in oklab, var(--muted) 62%, transparent);
|
||||
-webkit-backdrop-filter: blur(8px) saturate(130%);
|
||||
backdrop-filter: blur(8px) saturate(130%);
|
||||
box-shadow: inset 0 1px 0 0 color-mix(in oklab, #aab2ff 8%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
/* Dialog scrim: deepen the dim (the stock bg-black/10 is too light to anchor a
|
||||
frosted modal) and kill its own backdrop-blur so the only blur layer is the
|
||||
dialog CONTENT above — no nested blur-on-blur. Unconditional: when blur is
|
||||
unsupported the stock overlay had no blur anyway, and the deeper scrim is
|
||||
harmless. */
|
||||
.theme-boocode-plus.dark [data-slot="dialog-overlay"] {
|
||||
background-color: rgba(7, 8, 14, 0.55);
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
/* ── Restrained indigo glow on the primary action ────────────────────────
|
||||
The default-variant Button only (Send, primary confirm). Resting: a hairline
|
||||
indigo rim + soft drop glow. Hover: the glow blooms with the spring curve.
|
||||
Press: a composited squash. box-shadow/transform/filter only — no reflow. */
|
||||
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"] {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in oklab, #5e6ad2 38%, transparent),
|
||||
0 4px 16px -6px color-mix(in oklab, #5e6ad2 50%, transparent);
|
||||
transition:
|
||||
box-shadow 0.28s var(--bcp-spring),
|
||||
transform 0.2s var(--bcp-spring),
|
||||
filter 0.2s var(--bcp-ease),
|
||||
background-color 0.18s var(--bcp-ease);
|
||||
}
|
||||
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"]:not([disabled]):hover {
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in oklab, #5e6ad2 60%, transparent),
|
||||
0 8px 26px -6px color-mix(in oklab, #5e6ad2 68%, transparent);
|
||||
filter: brightness(1.06);
|
||||
}
|
||||
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"]:not([disabled]):active {
|
||||
transform: translateY(0.5px) scale(0.985);
|
||||
}
|
||||
|
||||
/* ── Spring-eased state on the rest of the chrome ─────────────────────────
|
||||
Smooth (non-overshoot) color/bg transitions on menu items and the composer
|
||||
focus, so hovers and focus feel intentional, not instant. Color props only. */
|
||||
.theme-boocode-plus.dark [data-slot="dropdown-menu-item"],
|
||||
.theme-boocode-plus.dark [data-slot="context-menu-item"],
|
||||
.theme-boocode-plus.dark [data-slot="dropdown-menu-sub-trigger"],
|
||||
.theme-boocode-plus.dark [data-slot="context-menu-sub-trigger"] {
|
||||
transition:
|
||||
background-color 0.16s var(--bcp-ease),
|
||||
color 0.16s var(--bcp-ease);
|
||||
}
|
||||
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
|
||||
transition:
|
||||
border-color 0.22s var(--bcp-ease),
|
||||
box-shadow 0.22s var(--bcp-spring),
|
||||
background-color 0.22s var(--bcp-ease);
|
||||
}
|
||||
|
||||
/* ── Accessibility fallbacks ──────────────────────────────────────────────
|
||||
Reduced transparency: drop every glass surface to its solid token bg and
|
||||
remove all blur. Declared after the @supports block so it wins. */
|
||||
@media (prefers-reduced-transparency: reduce) {
|
||||
.theme-boocode-plus.dark .bg-sidebar {
|
||||
background-color: var(--sidebar);
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.theme-boocode-plus.dark .bg-popover {
|
||||
background-color: var(--popover);
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
|
||||
background-color: var(--card);
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
.theme-boocode-plus.dark .border-b.bg-muted\/30,
|
||||
.theme-boocode-plus.dark .border-b.bg-muted\/20 {
|
||||
background-color: var(--muted);
|
||||
-webkit-backdrop-filter: none;
|
||||
backdrop-filter: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Reduced motion: no spring/transition, no press transform. The ambient
|
||||
gradient is static, so it (correctly) stays. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"],
|
||||
.theme-boocode-plus.dark [data-slot="dropdown-menu-item"],
|
||||
.theme-boocode-plus.dark [data-slot="context-menu-item"],
|
||||
.theme-boocode-plus.dark [data-slot="dropdown-menu-sub-trigger"],
|
||||
.theme-boocode-plus.dark [data-slot="context-menu-sub-trigger"],
|
||||
.theme-boocode-plus.dark .focus-within\:ring-primary\/15 {
|
||||
transition: none;
|
||||
}
|
||||
.theme-boocode-plus.dark [data-slot="button"][data-variant="default"]:not([disabled]):active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
153
apps/web/src/styles/themes/ember-polish.css
Normal file
153
apps/web/src/styles/themes/ember-polish.css
Normal file
@@ -0,0 +1,153 @@
|
||||
/* BooCode (Ember) polish layer — a restrained refinement of the default
|
||||
theme, NOT a redesign. Keeps the orange-on-charcoal identity; just gives
|
||||
the flat chrome gentle depth, the primary action a warm glow, a crisp
|
||||
keyboard focus ring, and a hair tighter title tracking. Calm enough to
|
||||
live in for hours — no background animation, no scanlines, no canvas.
|
||||
|
||||
SCOPE DISCIPLINE (the cross-theme regression trap): EVERY rule below is
|
||||
scoped under `.theme-ember`, so the other 22 themes are byte-for-byte
|
||||
unaffected. No global body, html, universal, or heading rule; no global
|
||||
line-height or letter-spacing change (those reflow every theme). The few
|
||||
`.theme-ember.dark` rules are still ember-only — both classes must be
|
||||
present — so they never leak either.
|
||||
|
||||
This sheet is imported UNLAYERED (globals.css, after the tailwindcss
|
||||
import), so these rules win over Tailwind's layered utilities. A bare
|
||||
box-shadow therefore REPLACES a component's `ring-1` hairline — so every
|
||||
elevation below composes the hairline back in via --ember-hairline (except
|
||||
the composer, which already has a real CSS `border`).
|
||||
|
||||
Hooks are the same stable ones BooCode+ uses: [data-slot=card],
|
||||
[data-slot=dialog-content], the menu [data-slot=*-content]s, the composer's
|
||||
unique `.focus-within:ring-primary/15` box, and the primary
|
||||
[data-slot=button][data-variant=default]. */
|
||||
|
||||
.theme-ember {
|
||||
/* Hairline = the stock `ring-foreground/10` edge, re-expressed as a shadow
|
||||
so it can ride alongside the elevation shadows in one box-shadow list. */
|
||||
--ember-hairline: 0 0 0 1px color-mix(in oklab, var(--foreground) 9%, transparent);
|
||||
|
||||
/* Consistent elevation scale. Resting surfaces (cards, composer) → 1;
|
||||
floating chrome (menus) → 2; the modal layer (dialog) → 3. Drop shadows
|
||||
are the primary depth cue in light mode; tuned calm (Material-ish alphas),
|
||||
never loud. */
|
||||
--ember-shadow-1: 0 1px 2px -1px rgb(0 0 0 / 0.40), 0 3px 8px -3px rgb(0 0 0 / 0.28);
|
||||
--ember-shadow-2: 0 3px 8px -3px rgb(0 0 0 / 0.44), 0 12px 28px -8px rgb(0 0 0 / 0.36);
|
||||
--ember-shadow-3: 0 6px 14px -4px rgb(0 0 0 / 0.50), 0 24px 56px -14px rgb(0 0 0 / 0.44);
|
||||
|
||||
/* Edge-light: a no-op in light mode (drop shadows do the work there). */
|
||||
--ember-edge: 0 0 transparent;
|
||||
|
||||
/* Restrained warm glow for the PRIMARY action only. Rest = a faint orange
|
||||
rim + soft drop; hover blooms. Colored shadows read on dark charcoal where
|
||||
black shadows fade, so the primary stays legible as "the" action. */
|
||||
--ember-glow-rest:
|
||||
0 0 0 1px color-mix(in oklab, var(--primary) 22%, transparent),
|
||||
0 2px 8px -3px color-mix(in oklab, var(--primary) 32%, transparent),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.40);
|
||||
--ember-glow-hover:
|
||||
0 0 0 1px color-mix(in oklab, var(--primary) 40%, transparent),
|
||||
0 5px 18px -4px color-mix(in oklab, var(--primary) 55%, transparent),
|
||||
0 1px 2px -1px rgb(0 0 0 / 0.40);
|
||||
|
||||
/* A gentle decel curve (no overshoot) — intentionally calmer than
|
||||
BooCode+'s spring, to suit a theme meant for long sessions. */
|
||||
--ember-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
}
|
||||
|
||||
/* On near-black, drop shadows barely read — a hair of warm top edge-light is
|
||||
the real "lifted" cue in dark mode. Tinted to the warm foreground so it
|
||||
stays on-brand and never cool. */
|
||||
.theme-ember.dark {
|
||||
--ember-edge: inset 0 1px 0 0 color-mix(in oklab, var(--foreground) 7%, transparent);
|
||||
}
|
||||
|
||||
/* ── Elevation ────────────────────────────────────────────────────────────
|
||||
Each surface keeps its edge (hairline, re-composed) and gains depth. */
|
||||
|
||||
/* Cards — resting elevation. */
|
||||
.theme-ember [data-slot="card"] {
|
||||
box-shadow: var(--ember-edge), var(--ember-hairline), var(--ember-shadow-1);
|
||||
}
|
||||
|
||||
/* The composer message box (ChatInput) — its own unique focus-within hook.
|
||||
It already has a real CSS `border`, so NO hairline here (that would double
|
||||
the edge). Only at rest: on focus-within the stock orange ring/border takes
|
||||
over, so we step aside with :not(:focus-within). */
|
||||
.theme-ember .focus-within\:ring-primary\/15:not(:focus-within) {
|
||||
box-shadow: var(--ember-edge), var(--ember-shadow-1);
|
||||
}
|
||||
|
||||
/* Floating menus — dropdown / context / their submenus. (Replaces the stock
|
||||
shadow-md/lg with the consistent themed scale.) */
|
||||
.theme-ember [data-slot="dropdown-menu-content"],
|
||||
.theme-ember [data-slot="dropdown-menu-sub-content"],
|
||||
.theme-ember [data-slot="context-menu-content"],
|
||||
.theme-ember [data-slot="context-menu-sub-content"] {
|
||||
box-shadow: var(--ember-edge), var(--ember-hairline), var(--ember-shadow-2);
|
||||
}
|
||||
|
||||
/* Dialog — the modal layer, strongest depth to separate it from the (light)
|
||||
scrim. */
|
||||
.theme-ember [data-slot="dialog-content"] {
|
||||
box-shadow: var(--ember-edge), var(--ember-hairline), var(--ember-shadow-3);
|
||||
}
|
||||
|
||||
/* ── Primary action glow ──────────────────────────────────────────────────
|
||||
The default-variant Button only (Send, primary confirm). box-shadow /
|
||||
transform / filter only — never a layout-triggering prop, so no shift. */
|
||||
.theme-ember [data-slot="button"][data-variant="default"] {
|
||||
box-shadow: var(--ember-glow-rest);
|
||||
transition:
|
||||
box-shadow 0.26s var(--ember-ease),
|
||||
transform 0.18s var(--ember-ease),
|
||||
filter 0.18s var(--ember-ease);
|
||||
}
|
||||
.theme-ember [data-slot="button"][data-variant="default"]:not([disabled]):hover {
|
||||
box-shadow: var(--ember-glow-hover);
|
||||
filter: brightness(1.04);
|
||||
}
|
||||
.theme-ember [data-slot="button"][data-variant="default"]:not([disabled]):active {
|
||||
transform: translateY(0.5px) scale(0.99);
|
||||
}
|
||||
|
||||
/* ── Keyboard focus ring ──────────────────────────────────────────────────
|
||||
A crisp, fully-opaque 2px orange outline replaces the stock soft 3px/50%
|
||||
ring — cleaner and more visible for keyboard nav. The outline follows each
|
||||
control's border-radius automatically. Form fields are intentionally left
|
||||
alone (the composer owns its focus-within ring; inputs keep theirs). */
|
||||
.theme-ember button:focus-visible,
|
||||
.theme-ember [data-slot="button"]:focus-visible,
|
||||
.theme-ember a:focus-visible,
|
||||
.theme-ember [role="switch"]:focus-visible {
|
||||
outline: 2px solid var(--ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
/* Drop the stock soft box-shadow ring on non-primary buttons so only the crisp
|
||||
outline shows. (The primary button keeps its glow box-shadow on focus.) */
|
||||
.theme-ember [data-slot="button"]:not([data-variant="default"]):focus-visible {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* ── Type rhythm ──────────────────────────────────────────────────────────
|
||||
The only safe scoped tightening: a hair of negative tracking on display /
|
||||
title text. NO line-height changes and nothing global — body copy, chat
|
||||
text, and code keep their exact metrics, so reading rhythm and the other
|
||||
themes are untouched. */
|
||||
.theme-ember .boocode-display,
|
||||
.theme-ember [data-slot="dialog-title"],
|
||||
.theme-ember [data-slot="card-title"] {
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
/* ── Reduced motion ───────────────────────────────────────────────────────
|
||||
The glow/shadows/outline are static; only the primary button transitions.
|
||||
Disable them (and the press transform) when the user asks for less motion. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.theme-ember [data-slot="button"][data-variant="default"] {
|
||||
transition: none;
|
||||
}
|
||||
.theme-ember [data-slot="button"][data-variant="default"]:not([disabled]):active {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user