feat(ui): extend design tokens, add dark mode, add UI primitives
- Add warning/success token roles across light and dark themes - Add Light/Dark/System theme mode (localStorage + Appwrite prefs) - Derive dark token stacks from existing theme seeds - Remove forced light app-shell background; dynamic color-scheme - Add Button/Card/Field/Badge/EmptyState primitives under src/components/ui
This commit is contained in:
+76
-5
@@ -61,11 +61,16 @@ import {
|
||||
import { BUILT_IN_FLAVOURS, DEFAULT_FLAVOUR, accentForCustomFlavour, flavourMeta, mergedFlavours } from "./data/flavours";
|
||||
import {
|
||||
APP_THEMES,
|
||||
THEME_MODE_STORAGE_KEY,
|
||||
THEME_STORAGE_KEY,
|
||||
getThemeById,
|
||||
getThemeTokens,
|
||||
normaliseThemeId,
|
||||
normaliseThemeMode,
|
||||
readStoredThemeId,
|
||||
readStoredThemeMode,
|
||||
type AppTheme,
|
||||
type ThemeMode,
|
||||
} from "./data/themes";
|
||||
import { themeTokensToStyle } from "./lib/themeTokens";
|
||||
import { account, appwriteConfig, Channel, client, pingAppwrite } from "./lib/appwrite";
|
||||
@@ -178,10 +183,21 @@ const NAV_ITEMS: Array<{ id: AppView; label: string; icon: LucideIcon }> = [
|
||||
{ id: "settings", label: "Settings", icon: Settings2 },
|
||||
];
|
||||
|
||||
const THEME_MODES: Array<{ id: ThemeMode; label: string }> = [
|
||||
{ id: "light", label: "Light" },
|
||||
{ id: "dark", label: "Dark" },
|
||||
{ id: "system", label: "System" },
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [themeId, setThemeId] = useState(() => readStoredThemeId());
|
||||
const [themeMode, setThemeMode] = useState<ThemeMode>(() => readStoredThemeMode());
|
||||
const [systemDark, setSystemDark] = useState(
|
||||
() => typeof window !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
const activeTheme = useMemo(() => getThemeById(themeId), [themeId]);
|
||||
const shellStyle = useMemo(() => themeTokensToStyle(activeTheme.tokens), [activeTheme]);
|
||||
const resolvedMode: "light" | "dark" = themeMode === "system" ? (systemDark ? "dark" : "light") : themeMode;
|
||||
const shellStyle = useMemo(() => themeTokensToStyle(getThemeTokens(themeId, resolvedMode)), [themeId, resolvedMode]);
|
||||
const [user, setUser] = useState<AuthUser | null>(null);
|
||||
const [authLoading, setAuthLoading] = useState(true);
|
||||
const [authError, setAuthError] = useState("");
|
||||
@@ -214,6 +230,19 @@ function App() {
|
||||
localStorage.setItem(THEME_STORAGE_KEY, normaliseThemeId(themeId));
|
||||
}, [themeId]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(THEME_MODE_STORAGE_KEY, themeMode);
|
||||
}, [themeMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window === "undefined") return undefined;
|
||||
const media = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handleChange = (event: MediaQueryListEvent) => setSystemDark(event.matches);
|
||||
setSystemDark(media.matches);
|
||||
media.addEventListener("change", handleChange);
|
||||
return () => media.removeEventListener("change", handleChange);
|
||||
}, []);
|
||||
|
||||
const refreshEntries = useCallback(async (userId: string, showLoader = true) => {
|
||||
if (showLoader) setDataLoading(true);
|
||||
setSyncError("");
|
||||
@@ -256,6 +285,9 @@ function App() {
|
||||
if (typeof currentUser.prefs.themeId === "string" && currentUser.prefs.themeId) {
|
||||
setThemeId(normaliseThemeId(currentUser.prefs.themeId));
|
||||
}
|
||||
if (typeof currentUser.prefs.themeMode === "string" && currentUser.prefs.themeMode) {
|
||||
setThemeMode(normaliseThemeMode(currentUser.prefs.themeMode));
|
||||
}
|
||||
setNotice(`Signed in as ${currentUser.email || currentUser.name || "Appwrite user"}.`);
|
||||
if (!currentUser.prefs.onboarded) {
|
||||
setSetupOpen(true);
|
||||
@@ -325,6 +357,9 @@ function App() {
|
||||
if (typeof currentUser.prefs.themeId === "string" && currentUser.prefs.themeId) {
|
||||
setThemeId(normaliseThemeId(currentUser.prefs.themeId));
|
||||
}
|
||||
if (typeof currentUser.prefs.themeMode === "string" && currentUser.prefs.themeMode) {
|
||||
setThemeMode(normaliseThemeMode(currentUser.prefs.themeMode));
|
||||
}
|
||||
setNotice(`Signed in as ${currentUser.email}.`);
|
||||
if (!currentUser.prefs.onboarded) {
|
||||
setSetupOpen(true);
|
||||
@@ -422,6 +457,7 @@ function App() {
|
||||
const nextPrefs = {
|
||||
...limitsPrefs,
|
||||
themeId: onboardingThemeId,
|
||||
themeMode,
|
||||
onboarded: true,
|
||||
};
|
||||
await account.updatePrefs(nextPrefs);
|
||||
@@ -627,7 +663,7 @@ function App() {
|
||||
}
|
||||
|
||||
if (authLoading) {
|
||||
return <LoadingScreen setupStatus={setupStatus} shellStyle={shellStyle} themeId={themeId} />;
|
||||
return <LoadingScreen setupStatus={setupStatus} shellStyle={shellStyle} themeId={themeId} resolvedMode={resolvedMode} />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
@@ -638,6 +674,7 @@ function App() {
|
||||
setupStatus={setupStatus}
|
||||
shellStyle={shellStyle}
|
||||
themeId={themeId}
|
||||
resolvedMode={resolvedMode}
|
||||
onLogin={login}
|
||||
onSignup={signup}
|
||||
/>
|
||||
@@ -648,6 +685,7 @@ function App() {
|
||||
<div
|
||||
className="app-shell min-h-screen overflow-x-hidden"
|
||||
data-theme={themeId}
|
||||
data-mode={resolvedMode}
|
||||
style={shellStyle}
|
||||
>
|
||||
{setupOpen && user && (
|
||||
@@ -770,6 +808,7 @@ function App() {
|
||||
notice={notice}
|
||||
setupStatus={setupStatus}
|
||||
themeId={themeId}
|
||||
themeMode={themeMode}
|
||||
user={user}
|
||||
userLimits={userLimits}
|
||||
limitCheck={limitCheck}
|
||||
@@ -781,6 +820,7 @@ function App() {
|
||||
onLogout={() => void logout()}
|
||||
onReset={() => setIsResetOpen(true)}
|
||||
onThemeChange={setThemeId}
|
||||
onThemeModeChange={setThemeMode}
|
||||
onSaveLimits={(next) => void saveUserLimits(next)}
|
||||
onRerunOnboarding={() => setSetupOpen(true)}
|
||||
/>
|
||||
@@ -864,13 +904,15 @@ function LoadingScreen({
|
||||
setupStatus,
|
||||
shellStyle,
|
||||
themeId,
|
||||
resolvedMode,
|
||||
}: {
|
||||
setupStatus: SetupStatus;
|
||||
shellStyle: CSSProperties;
|
||||
themeId: string;
|
||||
resolvedMode: "light" | "dark";
|
||||
}) {
|
||||
return (
|
||||
<div className="app-shell min-h-screen" data-theme={themeId} style={shellStyle}>
|
||||
<div className="app-shell min-h-screen" data-theme={themeId} data-mode={resolvedMode} style={shellStyle}>
|
||||
<ShellBackdrop />
|
||||
<div className="flex min-h-screen items-center justify-center p-6">
|
||||
<div className="glass-panel w-full max-w-md p-6 text-center">
|
||||
@@ -891,6 +933,7 @@ function AuthView({
|
||||
setupStatus,
|
||||
shellStyle,
|
||||
themeId,
|
||||
resolvedMode,
|
||||
onLogin,
|
||||
onSignup,
|
||||
}: {
|
||||
@@ -899,6 +942,7 @@ function AuthView({
|
||||
setupStatus: SetupStatus;
|
||||
shellStyle: CSSProperties;
|
||||
themeId: string;
|
||||
resolvedMode: "light" | "dark";
|
||||
onLogin: (email: string, password: string) => Promise<void>;
|
||||
onSignup: (name: string, email: string, password: string) => Promise<void>;
|
||||
}) {
|
||||
@@ -917,7 +961,7 @@ function AuthView({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="app-shell min-h-screen" data-theme={themeId} style={shellStyle}>
|
||||
<div className="app-shell min-h-screen" data-theme={themeId} data-mode={resolvedMode} style={shellStyle}>
|
||||
<ShellBackdrop />
|
||||
<main className="flex min-h-screen items-center justify-center p-6">
|
||||
<div className="auth-panel-shell">
|
||||
@@ -980,15 +1024,38 @@ function AuthView({
|
||||
|
||||
function ThemePicker({
|
||||
themeId,
|
||||
themeMode,
|
||||
onChange,
|
||||
onThemeModeChange,
|
||||
}: {
|
||||
themeId: string;
|
||||
themeMode: ThemeMode;
|
||||
onChange: (id: string) => void;
|
||||
onThemeModeChange: (mode: ThemeMode) => void;
|
||||
}) {
|
||||
const activeTheme = getThemeById(themeId);
|
||||
|
||||
return (
|
||||
<div className="settings-section">
|
||||
<div
|
||||
className="segmented-control max-w-xs"
|
||||
role="group"
|
||||
aria-label="Colour mode"
|
||||
style={{ gridTemplateColumns: "repeat(3, minmax(0, 1fr))" }}
|
||||
>
|
||||
{THEME_MODES.map((mode) => (
|
||||
<button
|
||||
key={mode.id}
|
||||
type="button"
|
||||
aria-pressed={themeMode === mode.id}
|
||||
className={themeMode === mode.id ? "segmented-control-active" : ""}
|
||||
onClick={() => onThemeModeChange(mode.id)}
|
||||
>
|
||||
{mode.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="theme-preview-strip">
|
||||
<div className="theme-preview-chip primary-button px-4 py-2 text-sm">Button</div>
|
||||
<div className="theme-preview-chip glass-panel px-4 py-2 text-sm">Panel</div>
|
||||
@@ -1884,6 +1951,7 @@ function SettingsView({
|
||||
notice,
|
||||
setupStatus,
|
||||
themeId,
|
||||
themeMode,
|
||||
user,
|
||||
userLimits,
|
||||
limitCheck,
|
||||
@@ -1895,6 +1963,7 @@ function SettingsView({
|
||||
onLogout,
|
||||
onReset,
|
||||
onThemeChange,
|
||||
onThemeModeChange,
|
||||
onSaveLimits,
|
||||
onRerunOnboarding,
|
||||
}: {
|
||||
@@ -1905,6 +1974,7 @@ function SettingsView({
|
||||
notice: string;
|
||||
setupStatus: SetupStatus;
|
||||
themeId: string;
|
||||
themeMode: ThemeMode;
|
||||
user: AuthUser | null;
|
||||
userLimits: UserLimits;
|
||||
limitCheck: LimitCheckResult;
|
||||
@@ -1916,6 +1986,7 @@ function SettingsView({
|
||||
onLogout: () => void;
|
||||
onReset: () => void;
|
||||
onThemeChange: (id: string) => void;
|
||||
onThemeModeChange: (mode: ThemeMode) => void;
|
||||
onSaveLimits: (limits: UserLimits) => void;
|
||||
onRerunOnboarding: () => void;
|
||||
}) {
|
||||
@@ -1942,7 +2013,7 @@ function SettingsView({
|
||||
</AppCard>
|
||||
|
||||
<AppCard title="Appearance" subtitle={`${activeTheme.label} theme active`}>
|
||||
<ThemePicker themeId={themeId} onChange={onThemeChange} />
|
||||
<ThemePicker themeId={themeId} themeMode={themeMode} onChange={onThemeChange} onThemeModeChange={onThemeModeChange} />
|
||||
</AppCard>
|
||||
|
||||
<AppCard title="Data & sync" subtitle={`${entries.length} entries synced for this user`}>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import type { HTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export type BadgeTone = "neutral" | "accent" | "success" | "warning" | "danger";
|
||||
|
||||
export type BadgeProps = {
|
||||
tone?: BadgeTone;
|
||||
children: ReactNode;
|
||||
} & HTMLAttributes<HTMLSpanElement>;
|
||||
|
||||
export function Badge({ tone = "neutral", className, children, ...rest }: BadgeProps) {
|
||||
const classes = ["badge", tone !== "neutral" ? `badge-${tone}` : "", className].filter(Boolean).join(" ");
|
||||
return (
|
||||
<span className={classes} {...rest}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { Loader2 } from "lucide-react";
|
||||
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
||||
|
||||
export type ButtonVariant = "primary" | "secondary" | "danger";
|
||||
|
||||
export type ButtonProps = {
|
||||
variant?: ButtonVariant;
|
||||
loading?: boolean;
|
||||
icon?: ReactNode;
|
||||
} & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
|
||||
const BUTTON_VARIANT_CLASSES: Record<ButtonVariant, string> = {
|
||||
primary: "primary-button",
|
||||
secondary: "secondary-button",
|
||||
danger: "danger-button",
|
||||
};
|
||||
|
||||
export function Button({
|
||||
variant = "primary",
|
||||
loading = false,
|
||||
icon,
|
||||
className,
|
||||
children,
|
||||
disabled,
|
||||
type = "button",
|
||||
...rest
|
||||
}: ButtonProps) {
|
||||
const classes = [BUTTON_VARIANT_CLASSES[variant], className].filter(Boolean).join(" ");
|
||||
return (
|
||||
<button className={classes} type={type} disabled={disabled || loading} aria-busy={loading || undefined} {...rest}>
|
||||
{loading ? <Loader2 className="animate-spin" size={17} aria-hidden="true" /> : icon}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type CardProps = {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
actions?: ReactNode;
|
||||
className?: string;
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export function Card({ title, subtitle, actions, className, children }: CardProps) {
|
||||
const classes = ["app-card p-4 sm:p-5", className].filter(Boolean).join(" ");
|
||||
return (
|
||||
<section className={classes}>
|
||||
{(title || subtitle || actions) && (
|
||||
<div className="mb-4 flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
{title && <h2 className="app-card-title text-xl">{title}</h2>}
|
||||
{subtitle && <p className="app-card-subtitle mt-1">{subtitle}</p>}
|
||||
</div>
|
||||
{actions && <div className="flex shrink-0 items-center gap-2">{actions}</div>}
|
||||
</div>
|
||||
)}
|
||||
{children}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Zap } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type EmptyStateProps = {
|
||||
icon?: ReactNode;
|
||||
title: string;
|
||||
body: string;
|
||||
action?: ReactNode;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function EmptyState({ icon, title, body, action, className }: EmptyStateProps) {
|
||||
const classes = ["empty-state", className].filter(Boolean).join(" ");
|
||||
return (
|
||||
<div className={classes}>
|
||||
<div className="empty-state-icon">{icon ?? <Zap size={22} aria-hidden="true" />}</div>
|
||||
<h3 className="empty-state-title">{title}</h3>
|
||||
<p className="empty-state-copy mt-2 max-w-sm">{body}</p>
|
||||
{action && <div className="mt-4">{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export type FieldControlProps = {
|
||||
id: string;
|
||||
"aria-invalid": true | undefined;
|
||||
"aria-describedby": string | undefined;
|
||||
};
|
||||
|
||||
export type FieldProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
error?: string;
|
||||
help?: string;
|
||||
className?: string;
|
||||
children: (controlProps: FieldControlProps) => ReactNode;
|
||||
};
|
||||
|
||||
export function Field({ id, label, error, help, className, children }: FieldProps) {
|
||||
const message = error ?? help;
|
||||
const messageId = message ? `${id}-message` : undefined;
|
||||
return (
|
||||
<div className={className}>
|
||||
<label className="field-label" htmlFor={id}>
|
||||
{label}
|
||||
</label>
|
||||
<div className="mt-2">
|
||||
{children({
|
||||
id,
|
||||
"aria-invalid": error ? true : undefined,
|
||||
"aria-describedby": messageId,
|
||||
})}
|
||||
</div>
|
||||
{message && (
|
||||
<p id={messageId} className={error ? "field-error-text" : "field-help-text"}>
|
||||
{message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
export { Badge } from "./Badge";
|
||||
export type { BadgeProps, BadgeTone } from "./Badge";
|
||||
export { Button } from "./Button";
|
||||
export type { ButtonProps, ButtonVariant } from "./Button";
|
||||
export { Card } from "./Card";
|
||||
export type { CardProps } from "./Card";
|
||||
export { EmptyState } from "./EmptyState";
|
||||
export type { EmptyStateProps } from "./EmptyState";
|
||||
export { Field } from "./Field";
|
||||
export type { FieldControlProps, FieldProps } from "./Field";
|
||||
+29
-1
@@ -4,12 +4,16 @@ export type AppTheme = {
|
||||
id: string;
|
||||
label: string;
|
||||
swatch: string;
|
||||
seed: ThemeSeed;
|
||||
tokens: ThemeTokens;
|
||||
};
|
||||
|
||||
export type ThemeMode = "light" | "dark" | "system";
|
||||
|
||||
export const THEME_STORAGE_KEY = "red-bull-intake-tracker.theme.v2";
|
||||
export const OLD_THEME_STORAGE_KEY = "red-bull-intake-tracker.theme.v1";
|
||||
export const LEGACY_ACCENT_STORAGE_KEY = "red-bull-intake-tracker.accent.v1";
|
||||
export const THEME_MODE_STORAGE_KEY = "red-bull-intake-tracker.theme-mode.v1";
|
||||
export const DEFAULT_THEME_ID = "mist";
|
||||
|
||||
const OLD_THEME_MAP: Record<string, string> = {
|
||||
@@ -50,7 +54,7 @@ const OLD_THEME_MAP: Record<string, string> = {
|
||||
};
|
||||
|
||||
function theme(id: string, label: string, swatch: string, seed: ThemeSeed): AppTheme {
|
||||
return { id, label, swatch, tokens: buildThemeTokens(seed) };
|
||||
return { id, label, swatch, seed, tokens: buildThemeTokens(seed) };
|
||||
}
|
||||
|
||||
export const APP_THEMES: AppTheme[] = [
|
||||
@@ -97,6 +101,30 @@ export function getThemeById(id: string): AppTheme {
|
||||
return APP_THEMES.find((entry) => entry.id === id) ?? APP_THEMES[0];
|
||||
}
|
||||
|
||||
export function getThemeTokens(id: string, mode: "light" | "dark"): ThemeTokens {
|
||||
const entry = getThemeById(id);
|
||||
if (mode === "light") return entry.tokens;
|
||||
|
||||
// Dark mode drops the light-only explicit overrides; chart colours that
|
||||
// still read well on dark surfaces are carried over.
|
||||
const overrides = entry.seed.tokens;
|
||||
const chartOverrides: Partial<ThemeTokens> = {};
|
||||
if (overrides?.chartPrimary) chartOverrides.chartPrimary = overrides.chartPrimary;
|
||||
if (overrides?.chartSecondary) chartOverrides.chartSecondary = overrides.chartSecondary;
|
||||
if (overrides?.chartTertiary) chartOverrides.chartTertiary = overrides.chartTertiary;
|
||||
return buildThemeTokens({ ...entry.seed, dark: true, tokens: chartOverrides });
|
||||
}
|
||||
|
||||
export function normaliseThemeMode(value: string | null | undefined): ThemeMode {
|
||||
if (value === "light" || value === "dark" || value === "system") return value;
|
||||
return "system";
|
||||
}
|
||||
|
||||
export function readStoredThemeMode(): ThemeMode {
|
||||
if (typeof window === "undefined") return "system";
|
||||
return normaliseThemeMode(localStorage.getItem(THEME_MODE_STORAGE_KEY));
|
||||
}
|
||||
|
||||
export function normaliseThemeId(id: string | null | undefined): string {
|
||||
if (!id) return DEFAULT_THEME_ID;
|
||||
if (APP_THEMES.some((entry) => entry.id === id)) return id;
|
||||
|
||||
+64
-3
@@ -3,7 +3,9 @@
|
||||
@tailwind utilities;
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--radius-sm: 10px;
|
||||
--radius-md: 14px;
|
||||
--radius-lg: 20px;
|
||||
font-family: "Google Sans", "SF Pro Display", Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
background: #eef3fb;
|
||||
}
|
||||
@@ -76,10 +78,15 @@ textarea:focus-visible {
|
||||
|
||||
.app-shell {
|
||||
min-height: 100vh;
|
||||
color: var(--text, #202124);
|
||||
background:
|
||||
radial-gradient(circle at 78% 12%, color-mix(in srgb, var(--primary-container, #dbe9ff) 42%, transparent), transparent 32rem),
|
||||
#eef3fb !important;
|
||||
color: #202124 !important;
|
||||
var(--bg, #eef3fb);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
.app-shell[data-mode="dark"] {
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
.app-layout {
|
||||
@@ -996,6 +1003,21 @@ textarea:focus-visible {
|
||||
color: #80868b;
|
||||
}
|
||||
|
||||
.field-help-text,
|
||||
.field-error-text {
|
||||
margin-top: 6px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.field-help-text {
|
||||
color: var(--muted, #5f6670);
|
||||
}
|
||||
|
||||
.field-error-text {
|
||||
color: var(--error, #ba1a1a);
|
||||
}
|
||||
|
||||
.entry-row {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
@@ -1085,6 +1107,45 @@ textarea:focus-visible {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
min-height: 26px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
color: var(--muted, #5f6670);
|
||||
background: var(--surface-container-high, #f1f4f9);
|
||||
border: 1px solid var(--outline-variant, #dce5f1);
|
||||
border-radius: 999px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.badge-accent {
|
||||
color: var(--on-primary-container, #10243f);
|
||||
background: var(--primary-container, #dbe9ff);
|
||||
border-color: color-mix(in srgb, var(--primary, #2563c7) 24%, transparent);
|
||||
}
|
||||
|
||||
.badge-success {
|
||||
color: var(--on-success-container, #03210f);
|
||||
background: var(--success-container, #ceead6);
|
||||
border-color: color-mix(in srgb, var(--success, #0d652d) 24%, transparent);
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
color: var(--on-warning-container, #291800);
|
||||
background: var(--warning-container, #ffddb0);
|
||||
border-color: color-mix(in srgb, var(--warning, #8d5700) 24%, transparent);
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
color: var(--on-error-container, #410002);
|
||||
background: var(--error-container, #ffdad6);
|
||||
border-color: color-mix(in srgb, var(--error, #ba1a1a) 24%, transparent);
|
||||
}
|
||||
|
||||
.segmented-control {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
|
||||
@@ -17,6 +17,14 @@ export type ThemeTokens = {
|
||||
onError: string;
|
||||
errorContainer: string;
|
||||
onErrorContainer: string;
|
||||
warning: string;
|
||||
onWarning: string;
|
||||
warningContainer: string;
|
||||
onWarningContainer: string;
|
||||
success: string;
|
||||
onSuccess: string;
|
||||
successContainer: string;
|
||||
onSuccessContainer: string;
|
||||
bg: string;
|
||||
surface: string;
|
||||
surfaceContainerLowest: string;
|
||||
@@ -160,6 +168,10 @@ export function buildThemeTokens(seed: ThemeSeed): ThemeTokens {
|
||||
const tertiaryContainer = containerColor(tertiary);
|
||||
const error = "#ba1a1a";
|
||||
const errorContainer = "#ffdad6";
|
||||
const warning = dark ? "#fbbc04" : "#8d5700";
|
||||
const warningContainer = dark ? "#594400" : "#ffddb0";
|
||||
const success = dark ? "#81c995" : "#0d652d";
|
||||
const successContainer = dark ? "#0f5223" : "#ceead6";
|
||||
|
||||
const tokens: ThemeTokens = {
|
||||
primary,
|
||||
@@ -178,6 +190,14 @@ export function buildThemeTokens(seed: ThemeSeed): ThemeTokens {
|
||||
onError: "#ffffff",
|
||||
errorContainer,
|
||||
onErrorContainer: "#410002",
|
||||
warning,
|
||||
onWarning: dark ? "#3a2a00" : "#ffffff",
|
||||
warningContainer,
|
||||
onWarningContainer: dark ? "#ffe1a6" : "#291800",
|
||||
success,
|
||||
onSuccess: dark ? "#07391c" : "#ffffff",
|
||||
successContainer,
|
||||
onSuccessContainer: dark ? "#ceead6" : "#03210f",
|
||||
bg: surfaces.bg,
|
||||
surface: surfaces.surface,
|
||||
surfaceContainerLowest: surfaces.surfaceContainerLowest,
|
||||
@@ -220,6 +240,14 @@ export function themeTokensToStyle(tokens: ThemeTokens): CSSProperties {
|
||||
"--on-error": tokens.onError,
|
||||
"--error-container": tokens.errorContainer,
|
||||
"--on-error-container": tokens.onErrorContainer,
|
||||
"--warning": tokens.warning,
|
||||
"--on-warning": tokens.onWarning,
|
||||
"--warning-container": tokens.warningContainer,
|
||||
"--on-warning-container": tokens.onWarningContainer,
|
||||
"--success": tokens.success,
|
||||
"--on-success": tokens.onSuccess,
|
||||
"--success-container": tokens.successContainer,
|
||||
"--on-success-container": tokens.onSuccessContainer,
|
||||
"--bg": tokens.bg,
|
||||
"--surface": tokens.surface,
|
||||
"--surface-container-lowest": tokens.surfaceContainerLowest,
|
||||
|
||||
Reference in New Issue
Block a user