feat(webui): adopt lavender mascot branding

This commit is contained in:
DrSmoothl
2026-11-03 10:41:11 +08:00
parent a8f460499b
commit 1cd838e7a1
14 changed files with 64 additions and 37 deletions
+2
View File
@@ -121,6 +121,8 @@ After changing an action name, parameter, result description/schema, example or
SDK breaking changes also update [CHANGELOG.md](packages/sdk/CHANGELOG.md) and [VERSIONING.md](packages/sdk/VERSIONING.md). WebUI changes follow its [interaction conventions](packages/webui/README.md). Keep user-facing documentation aligned with behavior. SDK breaking changes also update [CHANGELOG.md](packages/sdk/CHANGELOG.md) and [VERSIONING.md](packages/sdk/VERSIONING.md). WebUI changes follow its [interaction conventions](packages/webui/README.md). Keep user-facing documentation aligned with behavior.
Standalone WebUI branding uses the [SnowLuma mascot](assets/readme/mascot.png) and a lavender default accent. Keep server defaults, frontend presets and pre-mount CSS aligned as described in [WebUI default appearance](packages/webui/README.md#default-appearance), while preserving saved user choices and host appearance overlays.
The root [README.md](README.md) presents the project's purpose, overall capabilities, quick start and documentation entry points. Update it only when those overview details change; adding or fixing an individual feature does not justify adding a dedicated section there. Put feature-specific API usage, configuration, limitations and troubleshooting in the relevant package README or topic document, preferring existing documentation. The root [README.md](README.md) presents the project's purpose, overall capabilities, quick start and documentation entry points. Update it only when those overview details change; adding or fixing an individual feature does not justify adding a dedicated section there. Put feature-specific API usage, configuration, limitations and troubleshooting in the relevant package README or topic document, preferring existing documentation.
Edit sources rather than `dist` outputs. Native artifacts in `packages/runtime/native` are supplied build inputs; do not substitute arbitrary local binaries to make a build pass. Packaging changes must preserve the checks in [Core's build configuration](packages/core/vite.config.ts) and [the release layout validator](tools/check-release-layout.mjs). Edit sources rather than `dist` outputs. Native artifacts in `packages/runtime/native` are supplied build inputs; do not substitute arbitrary local binaries to make a build pass. Packaging changes must preserve the checks in [Core's build configuration](packages/core/vite.config.ts) and [the release layout validator](tools/check-release-layout.mjs).
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+7 -7
View File
@@ -44,7 +44,7 @@ export type SidebarStyle = 'follow' | 'panel' | 'accent';
export type BackgroundType = 'none' | 'solid' | 'gradient' | 'image'; export type BackgroundType = 'none' | 'solid' | 'gradient' | 'image';
export type Density = 'cozy' | 'compact'; export type Density = 'cozy' | 'compact';
export type TimeFormat = '12h' | '24h'; export type TimeFormat = '12h' | '24h';
/** Full color scheme. 'default' keeps the sky theme (driven by `mode`); the /** Full color scheme. 'default' keeps the lavender theme (driven by `mode`); the
* Catppuccin flavors are complete palettes that also fix light/dark. */ * Catppuccin flavors are complete palettes that also fix light/dark. */
export type Palette = export type Palette =
| 'default' | 'default'
@@ -85,7 +85,7 @@ export interface UiAppearance {
accentCustom: string; accentCustom: string;
accentScope: AccentScope; accentScope: AccentScope;
darkIntensity: DarkIntensity; darkIntensity: DarkIntensity;
/** Full color scheme; 'default' = the built-in sky theme. */ /** Full color scheme; 'default' = the built-in lavender theme. */
palette: Palette; palette: Palette;
sidebarStyle: SidebarStyle; sidebarStyle: SidebarStyle;
background: UiBackground; background: UiBackground;
@@ -203,12 +203,12 @@ export interface UiConfig {
} }
// ─── Defaults ──────────────────────────────────────────────────────────── // ─── Defaults ────────────────────────────────────────────────────────────
// Mirror the pre-existing ThemeContext defaults so an upgrade is a no-op // Keep these defaults aligned with ThemeContext. Saved appearance choices
// visually until the operator changes something. // are normalized independently and remain the operator's preference.
const DEFAULT_BACKGROUND: UiBackground = { const DEFAULT_BACKGROUND: UiBackground = {
type: 'none', type: 'none',
color: '#0ea5e9', color: '#b99ae6',
gradient: 'none', gradient: 'none',
imageOpacity: 0.15, imageOpacity: 0.15,
imageBlur: 0, imageBlur: 0,
@@ -220,8 +220,8 @@ const DEFAULT_BACKGROUND: UiBackground = {
const DEFAULT_APPEARANCE: UiAppearance = { const DEFAULT_APPEARANCE: UiAppearance = {
mode: 'system', mode: 'system',
accentMode: 'preset', accentMode: 'preset',
accentPreset: 'sky', accentPreset: 'lavender',
accentCustom: '#38bdf8', accentCustom: '#8561bd',
accentScope: 'global', accentScope: 'global',
darkIntensity: 'soft', darkIntensity: 'soft',
palette: 'default', palette: 'default',
+17
View File
@@ -3,6 +3,23 @@
SnowLuma's browser console is a Vite + React application for managing runtime SnowLuma's browser console is a Vite + React application for managing runtime
status, OneBot configuration, logs, storage, and server settings. status, OneBot configuration, logs, storage, and server settings.
## Default appearance
New installations and appearance resets use the mascot's lavender accent.
The light variant uses a deeper purple for readable button labels; dark mode
uses a softer lavender. Existing saved accents and custom colors are preserved.
The original sky-blue accent remains available in Settings.
Keep the server defaults in `packages/core/src/webui/ui-config.ts`, the frontend
defaults and preset in `src/contexts/ThemeContext.tsx`, and the pre-mount tokens
in `src/index.css` aligned when changing the default accent.
The browser favicon, home-screen icon and in-app logo share the mascot avatar
in [`assets/brand/mascot-avatar.png`](../../assets/brand/mascot-avatar.png).
Its exported sizes live in `public/`: `logo.png` (512 px), `favicon.png` (64 px),
and `apple-touch-icon.png` (180 px). Export from the master without redrawing
individual sizes.
## Host appearance integration ## Host appearance integration
`SNOWLUMA_WEBUI_TARGET=maibot` opts a deployment into a reversible millennium `SNOWLUMA_WEBUI_TARGET=maibot` opts a deployment into a reversible millennium
+3 -2
View File
@@ -2,9 +2,10 @@
<html lang="zh-CN" class="light" data-theme="light"> <html lang="zh-CN" class="light" data-theme="light">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/logo.png" /> <link rel="icon" type="image/png" sizes="64x64" href="/favicon.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0ea5e9" /> <meta name="theme-color" content="#8561bd" />
<!-- WebUI is an admin surface; tell every compliant crawler to skip it. --> <!-- WebUI is an admin surface; tell every compliant crawler to skip it. -->
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" /> <meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
<meta name="googlebot" content="noindex, nofollow, noarchive, nosnippet" /> <meta name="googlebot" content="noindex, nofollow, noarchive, nosnippet" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 269 KiB

@@ -147,7 +147,7 @@ export function Sidebar({ collapsed = false, onItemClick }: SidebarProps) {
<div className="flex h-16 shrink-0 items-center px-2"> <div className="flex h-16 shrink-0 items-center px-2">
<div className="grid w-12 shrink-0 place-items-center"> <div className="grid w-12 shrink-0 place-items-center">
<div className="relative flex size-9 items-center justify-center overflow-hidden rounded-xl bg-primary/10 ring-1 ring-primary/20"> <div className="relative flex size-9 items-center justify-center overflow-hidden rounded-xl bg-primary/10 ring-1 ring-primary/20">
<img src="/logo.png" alt="SnowLuma" className="size-7 object-contain" /> <img src="/logo.png" alt="SnowLuma" className="size-full object-cover" />
</div> </div>
</div> </div>
<div <div
@@ -319,7 +319,7 @@ function LoginBrand({ subtitle }: { subtitle: string }) {
return ( return (
<div className="flex flex-col items-center gap-3 text-center"> <div className="flex flex-col items-center gap-3 text-center">
<div className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 shadow-sm ring-1 ring-primary/20"> <div className="flex size-14 items-center justify-center rounded-2xl bg-primary/10 shadow-sm ring-1 ring-primary/20">
<img src="/logo.png" alt="SnowLuma" className="size-10 object-contain" /> <img src="/logo.png" alt="SnowLuma" className="size-10 rounded-xl object-cover" />
</div> </div>
<div> <div>
<div className="flex items-center justify-center gap-2"> <div className="flex items-center justify-center gap-2">
@@ -2078,7 +2078,7 @@ function AboutPanel() {
<img <img
src="/logo.png" src="/logo.png"
alt="SnowLuma" alt="SnowLuma"
className="size-20 object-contain drop-shadow-sm" className="size-20 rounded-2xl object-cover drop-shadow-sm"
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
+14 -6
View File
@@ -46,7 +46,8 @@ export type AppearancePatch = Partial<Omit<UiAppearance, 'background'>> & {
// ─── Frontend catalogues (the server stores only ids; the visuals live here) ─── // ─── Frontend catalogues (the server stores only ids; the visuals live here) ───
export type AccentColor = 'sky' | 'blue' | 'violet' | 'rose' | 'emerald' | 'amber' | 'orange'; export type AccentColor =
'lavender' | 'sky' | 'blue' | 'violet' | 'rose' | 'emerald' | 'amber' | 'orange';
export interface AccentSpec { export interface AccentSpec {
id: AccentColor; id: AccentColor;
@@ -57,6 +58,13 @@ export interface AccentSpec {
} }
export const ACCENTS: AccentSpec[] = [ export const ACCENTS: AccentSpec[] = [
{
id: 'lavender',
label: '薰衣草',
swatch: '#8561bd',
light: { primary: 'oklch(57% 0.14 300)', ring: 'oklch(57% 0.14 300)' },
dark: { primary: 'oklch(78% 0.115 300)', ring: 'oklch(78% 0.115 300)' },
},
{ {
id: 'sky', id: 'sky',
label: '天蓝', label: '天蓝',
@@ -198,15 +206,15 @@ export const UI_SCALE = { min: 0.9, max: 1.2, step: 0.05 } as const;
export const DEFAULT_APPEARANCE: UiAppearance = { export const DEFAULT_APPEARANCE: UiAppearance = {
mode: 'system', mode: 'system',
accentMode: 'preset', accentMode: 'preset',
accentPreset: 'sky', accentPreset: 'lavender',
accentCustom: '#38bdf8', accentCustom: '#8561bd',
accentScope: 'global', accentScope: 'global',
darkIntensity: 'soft', darkIntensity: 'soft',
palette: 'default', palette: 'default',
sidebarStyle: 'follow', sidebarStyle: 'follow',
background: { background: {
type: 'none', type: 'none',
color: '#0ea5e9', color: '#b99ae6',
gradient: 'none', gradient: 'none',
imageOpacity: 0.15, imageOpacity: 0.15,
imageBlur: 0, imageBlur: 0,
@@ -327,7 +335,7 @@ function accentVarsCss(a: UiAppearance): string {
const surf = a.palette === 'default'; const surf = a.palette === 'default';
if (a.accentMode === 'custom') { if (a.accentMode === 'custom') {
const hex = safeHex(a.accentCustom, '#38bdf8'); const hex = safeHex(a.accentCustom, DEFAULT_APPEARANCE.accentCustom);
const lightFg = readableForeground(hex); const lightFg = readableForeground(hex);
// Dark mode: lighten the accent for contrast on dark surfaces, and pick // Dark mode: lighten the accent for contrast on dark surfaces, and pick
// the foreground from the *lightened* colour (a light accent needs dark text). // the foreground from the *lightened* colour (a light accent needs dark text).
@@ -590,7 +598,7 @@ export const PALETTE_OPTIONS: PaletteSpec[] = [
{ {
id: 'default', id: 'default',
label: '默认', label: '默认',
preview: { bg: '#f1f5f9', surface: '#ffffff', dots: ['#0ea5e9', '#10b981', '#f43f5e'] }, preview: { bg: '#f1f5f9', surface: '#ffffff', dots: ['#8561bd', '#10b981', '#f43f5e'] },
}, },
{ {
id: 'catppuccin-latte', id: 'catppuccin-latte',
+18 -18
View File
@@ -4,7 +4,7 @@
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
/* ────────────────────────────────────────────── /* ──────────────────────────────────────────────
Theme tokens (sky-blue accent) Theme tokens (mascot lavender accent)
────────────────────────────────────────────── */ ────────────────────────────────────────────── */
:root { :root {
--radius: 0.75rem; --radius: 0.75rem;
@@ -18,8 +18,8 @@
--popover: oklch(100% 0 0); --popover: oklch(100% 0 0);
--popover-foreground: oklch(20% 0.03 250); --popover-foreground: oklch(20% 0.03 250);
/* sky-500 */ /* A deeper lavender keeps light-mode button labels readable. */
--primary: oklch(68.5% 0.155 230); --primary: oklch(57% 0.14 300);
--primary-foreground: oklch(99% 0.005 240); --primary-foreground: oklch(99% 0.005 240);
--secondary: oklch(96% 0.012 240); --secondary: oklch(96% 0.012 240);
@@ -28,8 +28,8 @@
--muted: oklch(96% 0.012 240); --muted: oklch(96% 0.012 240);
--muted-foreground: oklch(50% 0.02 250); --muted-foreground: oklch(50% 0.02 250);
--accent: oklch(94% 0.03 230); --accent: oklch(94% 0.03 300);
--accent-foreground: oklch(35% 0.07 230); --accent-foreground: oklch(35% 0.07 300);
--destructive: oklch(60% 0.22 27); --destructive: oklch(60% 0.22 27);
--destructive-foreground: oklch(99% 0.005 240); --destructive-foreground: oklch(99% 0.005 240);
@@ -49,16 +49,16 @@
--border: oklch(92% 0.01 240); --border: oklch(92% 0.01 240);
--input: oklch(94% 0.012 240); --input: oklch(94% 0.012 240);
--ring: oklch(68.5% 0.155 230); --ring: oklch(57% 0.14 300);
--sidebar: oklch(94% 0.008 240); --sidebar: oklch(94% 0.008 240);
--sidebar-foreground: oklch(25% 0.03 250); --sidebar-foreground: oklch(25% 0.03 250);
--sidebar-primary: oklch(68.5% 0.155 230); --sidebar-primary: oklch(57% 0.14 300);
--sidebar-primary-foreground: oklch(99% 0.005 240); --sidebar-primary-foreground: oklch(99% 0.005 240);
--sidebar-accent: oklch(90% 0.025 230); --sidebar-accent: oklch(90% 0.025 300);
--sidebar-accent-foreground: oklch(35% 0.08 230); --sidebar-accent-foreground: oklch(35% 0.08 300);
--sidebar-border: oklch(92% 0.01 240); --sidebar-border: oklch(92% 0.01 240);
--sidebar-ring: oklch(68.5% 0.155 230); --sidebar-ring: oklch(57% 0.14 300);
--media-outline: oklch(0% 0 0 / 0.1); --media-outline: oklch(0% 0 0 / 0.1);
/* Cards need depth without turning every surface into a hard outlined box. /* Cards need depth without turning every surface into a hard outlined box.
@@ -79,7 +79,7 @@
--popover: oklch(20% 0.025 250); --popover: oklch(20% 0.025 250);
--popover-foreground: oklch(96% 0.01 240); --popover-foreground: oklch(96% 0.01 240);
--primary: oklch(75% 0.14 230); --primary: oklch(78% 0.115 300);
--primary-foreground: oklch(15% 0.02 250); --primary-foreground: oklch(15% 0.02 250);
--secondary: oklch(25% 0.025 250); --secondary: oklch(25% 0.025 250);
@@ -88,8 +88,8 @@
--muted: oklch(24% 0.02 250); --muted: oklch(24% 0.02 250);
--muted-foreground: oklch(68% 0.02 240); --muted-foreground: oklch(68% 0.02 240);
--accent: oklch(28% 0.05 230); --accent: oklch(28% 0.05 300);
--accent-foreground: oklch(85% 0.1 230); --accent-foreground: oklch(85% 0.1 300);
--destructive: oklch(62% 0.2 27); --destructive: oklch(62% 0.2 27);
--destructive-foreground: oklch(98% 0.005 240); --destructive-foreground: oklch(98% 0.005 240);
@@ -106,16 +106,16 @@
--border: oklch(28% 0.02 250); --border: oklch(28% 0.02 250);
--input: oklch(26% 0.02 250); --input: oklch(26% 0.02 250);
--ring: oklch(75% 0.14 230); --ring: oklch(78% 0.115 300);
--sidebar: oklch(12% 0.018 250); --sidebar: oklch(12% 0.018 250);
--sidebar-foreground: oklch(92% 0.01 240); --sidebar-foreground: oklch(92% 0.01 240);
--sidebar-primary: oklch(75% 0.14 230); --sidebar-primary: oklch(78% 0.115 300);
--sidebar-primary-foreground: oklch(15% 0.02 250); --sidebar-primary-foreground: oklch(15% 0.02 250);
--sidebar-accent: oklch(28% 0.05 230); --sidebar-accent: oklch(28% 0.05 300);
--sidebar-accent-foreground: oklch(85% 0.1 230); --sidebar-accent-foreground: oklch(85% 0.1 300);
--sidebar-border: oklch(26% 0.02 250); --sidebar-border: oklch(26% 0.02 250);
--sidebar-ring: oklch(75% 0.14 230); --sidebar-ring: oklch(78% 0.115 300);
--media-outline: oklch(100% 0 0 / 0.12); --media-outline: oklch(100% 0 0 / 0.12);
--shadow-card: --shadow-card: