mirror of
https://github.com/block/buzz.git
synced 2026-11-14 13:21:02 +00:00
feat(desktop): add Buzz light + dark themes with branded sidebar gradient
Introduce a first-party "Buzz" theme pair (Buzz / Buzz Dark) that reuses the GitHub Light / GitHub Dark base palettes for every base color and adds a branded gradient painted across the sidebar/nav canvas in place of the flat grey. - theme-loader: register `buzz` + `buzz-dark`, pair them in THEME_PAIRS, and slot them first so they lead the System / Light / Dark picker categories. Both borrow the github-light / github-dark Shiki bundles for syntax colors. - ThemeProvider: toggle a `data-buzz-sidebar` attribute on <html> when a Buzz theme is active (both the cached-FOUC path and the async apply path). - theme.css: gradient fill for every `bg-sidebar` surface (header, sidebar column, footer) anchored with `background-attachment: fixed` so they read as one continuous ramp; white active pill (translucent tint on dark), and a shared `--buzz-hover-surface` translucent-white token used by nav hover, the search box, and the section "+" / "…" action buttons — replacing the GitHub grey. All rules are scoped to `[data-buzz-sidebar]` so other themes are untouched. - Section action buttons pick up a `sidebar-section-action` marker class so the Buzz hover override can target them without disturbing the shared Tailwind class in other themes. - ThemePreviewFrame: paint the gradient into the picker swatches (single and System-preview tiles) via BUZZ_GRADIENT_STOPS so the tiles are honest. Desktop-only (TS/React + CSS). `just ci` clean. Signed-off-by: Fizz <8a675edd33677aa0389f6650d467b2041fb0df4ca820eacb009babb95e3715d4@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
5dce80aff5
commit
9564cc8a38
@@ -41,6 +41,7 @@ import {
|
||||
getThemePair,
|
||||
} from "@/shared/theme/theme-loader";
|
||||
import {
|
||||
BUZZ_GRADIENT_STOPS,
|
||||
SystemPreferencePreviewFrame,
|
||||
ThemePreviewFrame,
|
||||
type ThemePreviewVars,
|
||||
@@ -292,6 +293,7 @@ function PairedThemeTile({
|
||||
darkVars: ThemePreviewVars | null;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const darkName = getThemePair(lightName);
|
||||
return (
|
||||
<button
|
||||
aria-pressed={isActive}
|
||||
@@ -307,7 +309,9 @@ function PairedThemeTile({
|
||||
? "ring-2 ring-primary ring-offset-2 ring-offset-background"
|
||||
: "group-hover:ring-2 group-hover:ring-border",
|
||||
)}
|
||||
darkGradient={darkName ? BUZZ_GRADIENT_STOPS[darkName] : undefined}
|
||||
darkVars={darkVars}
|
||||
lightGradient={BUZZ_GRADIENT_STOPS[lightName]}
|
||||
lightVars={lightVars}
|
||||
/>
|
||||
<span
|
||||
@@ -348,6 +352,7 @@ function SingleThemeTile({
|
||||
? "ring-2 ring-primary ring-offset-2 ring-offset-background"
|
||||
: "group-hover:ring-2 group-hover:ring-border",
|
||||
)}
|
||||
sidebarGradient={BUZZ_GRADIENT_STOPS[name]}
|
||||
vars={vars}
|
||||
/>
|
||||
<span
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const SECTION_ICON_BUTTON_CLASS =
|
||||
"flex size-6 items-center justify-center rounded-[4px] p-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-border/35 hover:text-sidebar-foreground focus-visible:bg-sidebar-border/35 focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
|
||||
"sidebar-section-action flex size-6 items-center justify-center rounded-[4px] p-1 text-sidebar-foreground/50 transition-colors hover:bg-sidebar-border/35 hover:text-sidebar-foreground focus-visible:bg-sidebar-border/35 focus-visible:text-sidebar-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-sidebar-ring [&>svg]:size-4 [&>svg]:shrink-0";
|
||||
|
||||
export const SECTION_ACTION_VISIBILITY_CLASS =
|
||||
"opacity-0 transition-opacity group-hover/sidebar-section:opacity-100 group-focus-within/sidebar-section:opacity-100";
|
||||
|
||||
@@ -163,3 +163,185 @@
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Buzz theme — sidebar gradient.
|
||||
*
|
||||
* Buzz reuses the GitHub Light palette for every base color; its one
|
||||
* distinguishing feature is this gradient painted across the sidebar/nav
|
||||
* canvas, replacing GitHub Light's flat grey. ThemeProvider toggles the
|
||||
* `data-buzz-sidebar` attribute on <html> when the Buzz theme is active.
|
||||
*
|
||||
* These rules are intentionally UNLAYERED: the `bg-sidebar` background-color
|
||||
* lives in Tailwind's `utilities` layer, and unlayered declarations win over
|
||||
* any layered ones, so this reliably overrides it without an `!important`.
|
||||
*
|
||||
* The gradient is applied to every `bg-sidebar` canvas surface (the top
|
||||
* chrome bar, the sidebar column, and the inset margin behind the white
|
||||
* content card). `background-attachment: fixed` anchors the gradient to the
|
||||
* viewport so these separate surfaces read as one continuous gradient rather
|
||||
* than each restarting the ramp. `background-color` is cleared so the grey
|
||||
* `--sidebar-background` never shows through.
|
||||
*
|
||||
* Item hover/active states use `bg-sidebar-accent` / `bg-sidebar-active`
|
||||
* (distinct tokens) and are intentionally left untouched.
|
||||
*/
|
||||
:root[data-buzz-sidebar] {
|
||||
--buzz-gradient-top: #e6e6b6;
|
||||
--buzz-gradient-bottom: #c4d0da;
|
||||
}
|
||||
|
||||
/*
|
||||
* Dark variant (Buzz Dark). Reuses the GitHub Dark base palette; only the
|
||||
* three Buzz tokens change, so every downstream rule (gradient fill, hover,
|
||||
* search box, active pill) adapts automatically. The `.dark` class is set on
|
||||
* the document root whenever a dark theme is active.
|
||||
*/
|
||||
:root[data-buzz-sidebar].dark {
|
||||
--buzz-gradient-top: #2b2b18;
|
||||
--buzz-gradient-bottom: #1b2530;
|
||||
}
|
||||
|
||||
:root[data-buzz-sidebar] .bg-sidebar {
|
||||
background-color: transparent;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--buzz-gradient-top),
|
||||
var(--buzz-gradient-bottom)
|
||||
);
|
||||
background-attachment: fixed;
|
||||
background-size: 100vw 100vh;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
/*
|
||||
* The pinned header (search + nav) and the footer (profile card) are painted
|
||||
* with an opaque `--sidebar-background` fill plus a soft edge-fade pseudo-
|
||||
* element (see `components.css`). Under GitHub Light that fill is the flat
|
||||
* grey we are replacing, so it reads as an opaque panel floating over the
|
||||
* gradient. Repaint both regions with the SAME viewport-fixed gradient so
|
||||
* they line up seamlessly with the surrounding sidebar canvas, and drop the
|
||||
* edge-fade (there is no longer a distinct surface to fade into).
|
||||
*/
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-testid="sidebar-pinned-header"],
|
||||
:root[data-buzz-sidebar] [data-testid="app-sidebar"] [data-sidebar="footer"] {
|
||||
background-color: transparent;
|
||||
background-image: linear-gradient(
|
||||
to bottom,
|
||||
var(--buzz-gradient-top),
|
||||
var(--buzz-gradient-bottom)
|
||||
);
|
||||
background-attachment: fixed;
|
||||
background-size: 100vw 100vh;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-testid="sidebar-pinned-header"]::before,
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-sidebar="footer"]::before {
|
||||
background: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Active nav item. The accent system (`applyAccentColor` in ThemeProvider)
|
||||
* sets `--sidebar-active` to the theme foreground as an INLINE style on
|
||||
* :root, which renders the selected page as a solid black pill under GitHub
|
||||
* Light. For the Buzz theme we want a white pill instead. Override the two
|
||||
* active tokens here — `!important` is required because an important
|
||||
* stylesheet declaration is what beats the non-important inline style set by
|
||||
* the accent system. Text flips to the theme foreground so it stays legible
|
||||
* on the white pill; the derived `bg-sidebar-active-foreground/20` badge on
|
||||
* active items follows automatically.
|
||||
*/
|
||||
:root[data-buzz-sidebar] {
|
||||
--sidebar-active: 0 0% 100% !important;
|
||||
--sidebar-active-foreground: var(--foreground) !important;
|
||||
|
||||
/* Shared translucent-white fill for hover states and the search box. */
|
||||
--buzz-hover-surface: rgb(255 255 255 / 0.6);
|
||||
}
|
||||
|
||||
/*
|
||||
* Dark variant: a solid white pill and 60%-white fills are too harsh on the
|
||||
* dark gradient. Use a light translucent tint for the active pill (keeping
|
||||
* the light theme foreground for legible text) and a lower-opacity white for
|
||||
* hover / search surfaces.
|
||||
*/
|
||||
:root[data-buzz-sidebar].dark {
|
||||
--sidebar-active: 0 0% 100% !important;
|
||||
--sidebar-active-foreground: 0 0% 100% !important;
|
||||
|
||||
--buzz-hover-surface: rgb(255 255 255 / 0.12);
|
||||
}
|
||||
|
||||
/*
|
||||
* On dark the active pill is a translucent white tint rather than a solid
|
||||
* fill, so it sits on the gradient without blowing out. The `!important`
|
||||
* background overrides the `bg-sidebar-active` utility for this state only.
|
||||
*/
|
||||
:root[data-buzz-sidebar].dark [data-testid="app-sidebar"] [data-active="true"] {
|
||||
background-color: rgb(255 255 255 / 0.16) !important;
|
||||
}
|
||||
|
||||
/*
|
||||
* Active nav pill: drop the drop shadow so the white tab sits flat on the
|
||||
* gradient (Buzz-only; other themes keep `shadow-xs`). Higher specificity
|
||||
* than the `data-[active=true]:shadow-xs` utility and unlayered, so it wins.
|
||||
*/
|
||||
:root[data-buzz-sidebar] [data-testid="app-sidebar"] [data-active="true"] {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/*
|
||||
* Hover on non-active nav items uses a translucent white fill (see
|
||||
* `--buzz-hover-surface`) instead of the grey `--sidebar-accent`. Active
|
||||
* items are excluded — they stay solid on hover (their
|
||||
* `data-[active=true]:hover:bg-sidebar-active` rule). Covers both top-level
|
||||
* menu buttons and channel sub-buttons.
|
||||
*/
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-sidebar="menu-button"]:not([data-active="true"]):hover,
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-sidebar="menu-sub-button"]:not([data-active="true"]):hover {
|
||||
background-color: var(--buzz-hover-surface);
|
||||
}
|
||||
|
||||
/*
|
||||
* Search box: match the same translucent white fill (see
|
||||
* `--buzz-hover-surface`) instead of the grey `bg-sidebar-border/35` fill, at
|
||||
* rest and on hover/focus.
|
||||
*/
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-testid="open-search"],
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-testid="open-search"]:hover,
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
[data-testid="open-search"]:focus-visible {
|
||||
background-color: var(--buzz-hover-surface);
|
||||
}
|
||||
|
||||
/*
|
||||
* Section action buttons (the "+" and "…" at the right edge of each sidebar
|
||||
* section header): match the same translucent white fill (see
|
||||
* `--buzz-hover-surface`) on hover/focus instead of the grey
|
||||
* `bg-sidebar-border/35` fill. Unlayered + `[data-buzz-sidebar]` prefix beats
|
||||
* the Tailwind utility so it only applies under the Buzz theme.
|
||||
*/
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
.sidebar-section-action:hover,
|
||||
:root[data-buzz-sidebar]
|
||||
[data-testid="app-sidebar"]
|
||||
.sidebar-section-action:focus-visible {
|
||||
background-color: var(--buzz-hover-surface);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,19 @@ import { cn } from "@/shared/lib/cn";
|
||||
|
||||
export type ThemePreviewVars = Record<string, string>;
|
||||
|
||||
/**
|
||||
* Buzz sidebar-gradient stop colors, keyed by theme name. Single source of
|
||||
* truth for the picker swatch — must stay in sync with the `--buzz-gradient-*`
|
||||
* values in `shared/styles/globals/theme.css`.
|
||||
*/
|
||||
export const BUZZ_GRADIENT_STOPS: Record<
|
||||
string,
|
||||
{ top: string; bottom: string }
|
||||
> = {
|
||||
buzz: { top: "#e6e6b6", bottom: "#c4d0da" },
|
||||
"buzz-dark": { top: "#2b2b18", bottom: "#1b2530" },
|
||||
};
|
||||
|
||||
export const LIGHT_PREVIEW_VARS: ThemePreviewVars = {
|
||||
"--background": "0 0% 100%",
|
||||
"--border": "0 0% 89.8%",
|
||||
@@ -33,8 +46,15 @@ function hslAlpha(vars: ThemePreviewVars | null, key: string, alpha: number) {
|
||||
return `hsl(${vars?.[key] ?? LIGHT_PREVIEW_VARS[key]} / ${alpha})`;
|
||||
}
|
||||
|
||||
function ThemePreviewSvg({ vars }: { vars: ThemePreviewVars | null }) {
|
||||
function ThemePreviewSvg({
|
||||
vars,
|
||||
sidebarGradient,
|
||||
}: {
|
||||
vars: ThemePreviewVars | null;
|
||||
sidebarGradient?: { top: string; bottom: string };
|
||||
}) {
|
||||
const clipId = React.useId().replace(/:/g, "");
|
||||
const gradientId = `${clipId}-buzz`;
|
||||
const background = hsl(vars, "--background");
|
||||
const border = hsl(vars, "--border");
|
||||
const foreground = hsl(vars, "--foreground");
|
||||
@@ -56,7 +76,11 @@ function ThemePreviewSvg({ vars }: { vars: ThemePreviewVars | null }) {
|
||||
<g clipPath={`url(#${clipId})`}>
|
||||
<rect fill={background} height="180" rx="3.6" width="288" />
|
||||
<line stroke={border} x1="57" x2="117" y1="10.5" y2="10.5" />
|
||||
<rect fill={sidebar} height="180" width="57.375" />
|
||||
<rect
|
||||
fill={sidebarGradient ? `url(#${gradientId})` : sidebar}
|
||||
height="180"
|
||||
width="57.375"
|
||||
/>
|
||||
<rect
|
||||
fill={sidebarForeground}
|
||||
height="3.6"
|
||||
@@ -191,6 +215,19 @@ function ThemePreviewSvg({ vars }: { vars: ThemePreviewVars | null }) {
|
||||
<clipPath id={clipId}>
|
||||
<rect fill={background} height="180" rx="3.6" width="288" />
|
||||
</clipPath>
|
||||
{sidebarGradient ? (
|
||||
<linearGradient
|
||||
id={gradientId}
|
||||
x1="0"
|
||||
x2="0"
|
||||
y1="0"
|
||||
y2="180"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop offset="0" stopColor={sidebarGradient.top} />
|
||||
<stop offset="1" stopColor={sidebarGradient.bottom} />
|
||||
</linearGradient>
|
||||
) : null}
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -203,14 +240,20 @@ function ThemePreviewSvg({ vars }: { vars: ThemePreviewVars | null }) {
|
||||
function SystemPreferencePreviewSvg({
|
||||
darkVars,
|
||||
lightVars,
|
||||
lightGradient,
|
||||
darkGradient,
|
||||
}: {
|
||||
darkVars: ThemePreviewVars | null;
|
||||
lightVars: ThemePreviewVars | null;
|
||||
lightGradient?: { top: string; bottom: string };
|
||||
darkGradient?: { top: string; bottom: string };
|
||||
}) {
|
||||
const clipBase = React.useId().replace(/:/g, "");
|
||||
const clipDark = `${clipBase}-dark`;
|
||||
const clipLight = `${clipBase}-light`;
|
||||
const clipOuter = `${clipBase}-outer`;
|
||||
const lightGradientId = `${clipBase}-buzz-light`;
|
||||
const darkGradientId = `${clipBase}-buzz-dark`;
|
||||
|
||||
// Dark half colors
|
||||
const darkBg = hsl(darkVars, "--background");
|
||||
@@ -240,7 +283,11 @@ function SystemPreferencePreviewSvg({
|
||||
{/* Light half (top) */}
|
||||
<g clipPath={`url(#${clipLight})`}>
|
||||
<rect fill={lightBg} height="180" rx="3.6" width="288" />
|
||||
<rect fill={lightSidebar} height="180" width="57.375" />
|
||||
<rect
|
||||
fill={lightGradient ? `url(#${lightGradientId})` : lightSidebar}
|
||||
height="180"
|
||||
width="57.375"
|
||||
/>
|
||||
<rect
|
||||
fill={lightSidebarFg}
|
||||
height="3.6"
|
||||
@@ -391,7 +438,12 @@ function SystemPreferencePreviewSvg({
|
||||
<g clipPath={`url(#${clipDark})`}>
|
||||
<g clipPath={`url(#${clipOuter})`}>
|
||||
<rect fill={darkBg} height="180" rx="3.6" width="288" y="22" />
|
||||
<rect fill={darkSidebar} height="180" width="57.375" y="22" />
|
||||
<rect
|
||||
fill={darkGradient ? `url(#${darkGradientId})` : darkSidebar}
|
||||
height="180"
|
||||
width="57.375"
|
||||
y="22"
|
||||
/>
|
||||
<rect
|
||||
fill={darkSidebarFg}
|
||||
height="3.6"
|
||||
@@ -477,6 +529,32 @@ function SystemPreferencePreviewSvg({
|
||||
<clipPath id={clipOuter}>
|
||||
<rect fill="white" height="180" rx="3.6" width="288" y="22" />
|
||||
</clipPath>
|
||||
{lightGradient ? (
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id={lightGradientId}
|
||||
x1="0"
|
||||
x2="0"
|
||||
y1="0"
|
||||
y2="80"
|
||||
>
|
||||
<stop offset="0" stopColor={lightGradient.top} />
|
||||
<stop offset="1" stopColor={lightGradient.bottom} />
|
||||
</linearGradient>
|
||||
) : null}
|
||||
{darkGradient ? (
|
||||
<linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
id={darkGradientId}
|
||||
x1="0"
|
||||
x2="0"
|
||||
y1="22"
|
||||
y2="80"
|
||||
>
|
||||
<stop offset="0" stopColor={darkGradient.top} />
|
||||
<stop offset="1" stopColor={darkGradient.bottom} />
|
||||
</linearGradient>
|
||||
) : null}
|
||||
</defs>
|
||||
</svg>
|
||||
);
|
||||
@@ -485,9 +563,11 @@ function SystemPreferencePreviewSvg({
|
||||
export function ThemePreviewFrame({
|
||||
className,
|
||||
vars,
|
||||
sidebarGradient,
|
||||
}: {
|
||||
className?: string;
|
||||
vars: ThemePreviewVars | null;
|
||||
sidebarGradient?: { top: string; bottom: string };
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -501,7 +581,7 @@ export function ThemePreviewFrame({
|
||||
}}
|
||||
>
|
||||
<div className="absolute -bottom-1 -right-1 h-[90%] w-[90%]">
|
||||
<ThemePreviewSvg vars={vars} />
|
||||
<ThemePreviewSvg vars={vars} sidebarGradient={sidebarGradient} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -515,10 +595,14 @@ export function SystemPreferencePreviewFrame({
|
||||
className,
|
||||
darkVars,
|
||||
lightVars,
|
||||
lightGradient,
|
||||
darkGradient,
|
||||
}: {
|
||||
className?: string;
|
||||
darkVars: ThemePreviewVars | null;
|
||||
lightVars: ThemePreviewVars | null;
|
||||
lightGradient?: { top: string; bottom: string };
|
||||
darkGradient?: { top: string; bottom: string };
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
@@ -531,7 +615,12 @@ export function SystemPreferencePreviewFrame({
|
||||
}}
|
||||
>
|
||||
<div className="absolute -bottom-1 -right-1 h-[90%] w-[90%]">
|
||||
<SystemPreferencePreviewSvg darkVars={darkVars} lightVars={lightVars} />
|
||||
<SystemPreferencePreviewSvg
|
||||
darkGradient={darkGradient}
|
||||
darkVars={darkVars}
|
||||
lightGradient={lightGradient}
|
||||
lightVars={lightVars}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -206,6 +206,16 @@ function applyAccentColor(value: string) {
|
||||
root.style.setProperty("--sidebar-active-foreground", fgHsl);
|
||||
}
|
||||
|
||||
/** Toggle the Buzz sidebar-gradient marker on the document root. */
|
||||
function applyBuzzSidebar(themeName: string) {
|
||||
const root = document.documentElement;
|
||||
if (themeName === "buzz" || themeName === "buzz-dark") {
|
||||
root.setAttribute("data-buzz-sidebar", "");
|
||||
} else {
|
||||
root.removeAttribute("data-buzz-sidebar");
|
||||
}
|
||||
}
|
||||
|
||||
/** Apply cached CSS vars synchronously to prevent FOUC. */
|
||||
function applyCachedVars(): string | null {
|
||||
try {
|
||||
@@ -218,6 +228,7 @@ function applyCachedVars(): string | null {
|
||||
}
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(isDark ? "dark" : "light");
|
||||
applyBuzzSidebar(themeName);
|
||||
|
||||
const accent =
|
||||
window.localStorage.getItem(ACCENT_STORAGE_KEY) ?? DEFAULT_ACCENT;
|
||||
@@ -246,6 +257,7 @@ async function applyTheme(name: SyntaxThemeName): Promise<{ isDark: boolean }> {
|
||||
|
||||
root.classList.remove("light", "dark");
|
||||
root.classList.add(isDark ? "dark" : "light");
|
||||
applyBuzzSidebar(name);
|
||||
|
||||
// Cache for FOUC prevention
|
||||
try {
|
||||
|
||||
@@ -7,8 +7,39 @@
|
||||
|
||||
import type { ThemeRegistrationRaw } from "shiki";
|
||||
|
||||
// Available syntax themes (all Shiki bundled themes, alphabetically sorted)
|
||||
/**
|
||||
* Buzz theme name. Buzz is a first-party light theme that reuses GitHub
|
||||
* Light for every base color (backgrounds, text, borders, code) — the
|
||||
* message area and containers are indistinguishable from GitHub Light. Its
|
||||
* one distinguishing feature is a branded gradient painted across the
|
||||
* sidebar/nav canvas, replacing GitHub Light's flat grey. The gradient is
|
||||
* applied by {@link ThemeProvider} toggling a `data-buzz-sidebar` attribute
|
||||
* on the document root; the CSS lives in `shared/styles/globals/theme.css`.
|
||||
*/
|
||||
export const BUZZ_THEME_NAME = "buzz";
|
||||
|
||||
/**
|
||||
* Buzz Dark theme name. The dark-mode counterpart to {@link BUZZ_THEME_NAME}:
|
||||
* reuses the GitHub Dark palette for every base color, with the same branded
|
||||
* sidebar gradient (dark-tuned colors, see `shared/styles/globals/theme.css`).
|
||||
* {@link ThemeProvider} toggles the shared `data-buzz-sidebar` attribute for
|
||||
* this theme too; the `.dark` root class selects the dark gradient values.
|
||||
*
|
||||
* Buzz and Buzz Dark are paired in {@link THEME_PAIRS}, so the picker shows a
|
||||
* combined "Buzz" tile under System mode (follow-OS) plus a single "Buzz" tile
|
||||
* under Light and a "Buzz Dark" tile under Dark.
|
||||
*/
|
||||
export const BUZZ_DARK_THEME_NAME = "buzz-dark";
|
||||
|
||||
/** The Shiki bundle Buzz borrows its base palette from. */
|
||||
export const BUZZ_BASE_THEME: SyntaxThemeName = "github-light";
|
||||
|
||||
// Available themes. "buzz" is a Buzz-branded theme that reuses the
|
||||
// github-light palette plus a sidebar gradient; the rest are the Shiki
|
||||
// bundled syntax themes, alphabetically sorted.
|
||||
export const SYNTAX_THEMES = [
|
||||
"buzz",
|
||||
"buzz-dark",
|
||||
"andromeeda",
|
||||
"aurora-x",
|
||||
"ayu-dark",
|
||||
@@ -86,6 +117,7 @@ export const ONBOARDING_DEFAULT_THEME_NAME = (ONBOARDING_THEME_PREFERENCES.find(
|
||||
// Known light themes — used by the theme picker to show sun/moon icons
|
||||
// for themes that haven't been loaded yet.
|
||||
export const LIGHT_THEMES: ReadonlySet<SyntaxThemeName> = new Set([
|
||||
"buzz",
|
||||
"catppuccin-latte",
|
||||
"everforest-light",
|
||||
"github-light",
|
||||
@@ -111,6 +143,10 @@ const themeImports: Record<
|
||||
SyntaxThemeName,
|
||||
() => Promise<{ default: ThemeRegistrationRaw }>
|
||||
> = {
|
||||
// Buzz reuses the github-light palette; its gradient is applied separately.
|
||||
buzz: () => import("shiki/themes/github-light.mjs"),
|
||||
// Buzz Dark reuses the github-dark palette; dark gradient applied separately.
|
||||
"buzz-dark": () => import("shiki/themes/github-dark.mjs"),
|
||||
andromeeda: () => import("shiki/themes/andromeeda.mjs"),
|
||||
"aurora-x": () => import("shiki/themes/aurora-x.mjs"),
|
||||
"ayu-dark": () => import("shiki/themes/ayu-dark.mjs"),
|
||||
@@ -189,6 +225,8 @@ export function isLightTheme(name: string): boolean {
|
||||
export const THEME_PAIRS: ReadonlyMap<SyntaxThemeName, SyntaxThemeName> =
|
||||
new Map([
|
||||
// Light → Dark
|
||||
// Buzz is the first-party pair; keep it first so it leads every category.
|
||||
["buzz", "buzz-dark"],
|
||||
["catppuccin-latte", "catppuccin-mocha"],
|
||||
["everforest-light", "everforest-dark"],
|
||||
["github-light", "github-dark"],
|
||||
@@ -207,6 +245,7 @@ export const THEME_PAIRS: ReadonlyMap<SyntaxThemeName, SyntaxThemeName> =
|
||||
["solarized-light", "solarized-dark"],
|
||||
["vitesse-light", "vitesse-dark"],
|
||||
// Dark → Light (reverse mappings)
|
||||
["buzz-dark", "buzz"],
|
||||
["catppuccin-mocha", "catppuccin-latte"],
|
||||
["everforest-dark", "everforest-light"],
|
||||
["github-dark", "github-light"],
|
||||
|
||||
Reference in New Issue
Block a user