Add dark and light theme with OS preference support
Follow OS color scheme by default, with a three-way toggle (System / Light / Dark) in the kebab menu. Light theme uses warm, neutral tones with soft card-to-background contrast. Semantic colors (damage, healing, conditions) keep their hue across themes. Closes #10 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ import { useBulkImport } from "./hooks/use-bulk-import";
|
|||||||
import { useEncounter } from "./hooks/use-encounter";
|
import { useEncounter } from "./hooks/use-encounter";
|
||||||
import { usePlayerCharacters } from "./hooks/use-player-characters";
|
import { usePlayerCharacters } from "./hooks/use-player-characters";
|
||||||
import { useSidePanelState } from "./hooks/use-side-panel-state";
|
import { useSidePanelState } from "./hooks/use-side-panel-state";
|
||||||
|
import { useTheme } from "./hooks/use-theme";
|
||||||
import { cn } from "./lib/utils";
|
import { cn } from "./lib/utils";
|
||||||
|
|
||||||
function rollDice(): number {
|
function rollDice(): number {
|
||||||
@@ -115,6 +116,7 @@ export function App() {
|
|||||||
|
|
||||||
const bulkImport = useBulkImport();
|
const bulkImport = useBulkImport();
|
||||||
const sidePanel = useSidePanelState();
|
const sidePanel = useSidePanelState();
|
||||||
|
const { preference: themePreference, cycleTheme } = useTheme();
|
||||||
|
|
||||||
const [rollSkippedCount, setRollSkippedCount] = useState(0);
|
const [rollSkippedCount, setRollSkippedCount] = useState(0);
|
||||||
const [rollSingleSkipped, setRollSingleSkipped] = useState(false);
|
const [rollSingleSkipped, setRollSingleSkipped] = useState(false);
|
||||||
@@ -263,6 +265,8 @@ export function App() {
|
|||||||
showRollAllInitiative={hasCreatureCombatants}
|
showRollAllInitiative={hasCreatureCombatants}
|
||||||
rollAllInitiativeDisabled={!canRollAllInitiative}
|
rollAllInitiativeDisabled={!canRollAllInitiative}
|
||||||
onOpenSourceManager={sidePanel.showSourceManager}
|
onOpenSourceManager={sidePanel.showSourceManager}
|
||||||
|
themePreference={themePreference}
|
||||||
|
onCycleTheme={cycleTheme}
|
||||||
autoFocus
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -325,6 +329,8 @@ export function App() {
|
|||||||
showRollAllInitiative={hasCreatureCombatants}
|
showRollAllInitiative={hasCreatureCombatants}
|
||||||
rollAllInitiativeDisabled={!canRollAllInitiative}
|
rollAllInitiativeDisabled={!canRollAllInitiative}
|
||||||
onOpenSourceManager={sidePanel.showSourceManager}
|
onOpenSourceManager={sidePanel.showSourceManager}
|
||||||
|
themePreference={themePreference}
|
||||||
|
onCycleTheme={cycleTheme}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ describe("CombatantRow", () => {
|
|||||||
it("active combatant gets active border styling", () => {
|
it("active combatant gets active border styling", () => {
|
||||||
const { container } = renderRow({ isActive: true });
|
const { container } = renderRow({ isActive: true });
|
||||||
const row = container.firstElementChild;
|
const row = container.firstElementChild;
|
||||||
expect(row?.className).toContain("border-accent/40");
|
expect(row?.className).toContain("border-active-row-border");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("unconscious combatant (currentHp === 0) gets dimmed styling", () => {
|
it("unconscious combatant (currentHp === 0) gets dimmed styling", () => {
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import {
|
|||||||
Import,
|
Import,
|
||||||
Library,
|
Library,
|
||||||
Minus,
|
Minus,
|
||||||
|
Monitor,
|
||||||
|
Moon,
|
||||||
Plus,
|
Plus,
|
||||||
|
Sun,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import React, { type RefObject, useDeferredValue, useState } from "react";
|
import React, { type RefObject, useDeferredValue, useState } from "react";
|
||||||
@@ -43,6 +46,8 @@ interface ActionBarProps {
|
|||||||
rollAllInitiativeDisabled?: boolean;
|
rollAllInitiativeDisabled?: boolean;
|
||||||
onOpenSourceManager?: () => void;
|
onOpenSourceManager?: () => void;
|
||||||
autoFocus?: boolean;
|
autoFocus?: boolean;
|
||||||
|
themePreference?: "system" | "light" | "dark";
|
||||||
|
onCycleTheme?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function creatureKey(r: SearchResult): string {
|
function creatureKey(r: SearchResult): string {
|
||||||
@@ -171,7 +176,7 @@ function AddModeSuggestions({
|
|||||||
>
|
>
|
||||||
<Minus className="h-3 w-3" />
|
<Minus className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
<span className="min-w-5 rounded-full bg-accent px-1.5 py-0.5 text-center font-medium text-foreground">
|
<span className="min-w-5 rounded-full bg-accent px-1.5 py-0.5 text-center font-medium text-primary-foreground">
|
||||||
{queued.count}
|
{queued.count}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
@@ -215,12 +220,26 @@ function AddModeSuggestions({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const THEME_ICONS = {
|
||||||
|
system: Monitor,
|
||||||
|
light: Sun,
|
||||||
|
dark: Moon,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const THEME_LABELS = {
|
||||||
|
system: "Theme: System",
|
||||||
|
light: "Theme: Light",
|
||||||
|
dark: "Theme: Dark",
|
||||||
|
} as const;
|
||||||
|
|
||||||
function buildOverflowItems(opts: {
|
function buildOverflowItems(opts: {
|
||||||
onManagePlayers?: () => void;
|
onManagePlayers?: () => void;
|
||||||
onOpenSourceManager?: () => void;
|
onOpenSourceManager?: () => void;
|
||||||
bestiaryLoaded: boolean;
|
bestiaryLoaded: boolean;
|
||||||
onBulkImport?: () => void;
|
onBulkImport?: () => void;
|
||||||
bulkImportDisabled?: boolean;
|
bulkImportDisabled?: boolean;
|
||||||
|
themePreference?: "system" | "light" | "dark";
|
||||||
|
onCycleTheme?: () => void;
|
||||||
}): OverflowMenuItem[] {
|
}): OverflowMenuItem[] {
|
||||||
const items: OverflowMenuItem[] = [];
|
const items: OverflowMenuItem[] = [];
|
||||||
if (opts.onManagePlayers) {
|
if (opts.onManagePlayers) {
|
||||||
@@ -245,6 +264,16 @@ function buildOverflowItems(opts: {
|
|||||||
disabled: opts.bulkImportDisabled,
|
disabled: opts.bulkImportDisabled,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if (opts.onCycleTheme) {
|
||||||
|
const pref = opts.themePreference ?? "system";
|
||||||
|
const ThemeIcon = THEME_ICONS[pref];
|
||||||
|
items.push({
|
||||||
|
icon: <ThemeIcon className="h-4 w-4" />,
|
||||||
|
label: THEME_LABELS[pref],
|
||||||
|
onClick: opts.onCycleTheme,
|
||||||
|
keepOpen: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,6 +294,8 @@ export function ActionBar({
|
|||||||
rollAllInitiativeDisabled,
|
rollAllInitiativeDisabled,
|
||||||
onOpenSourceManager,
|
onOpenSourceManager,
|
||||||
autoFocus,
|
autoFocus,
|
||||||
|
themePreference,
|
||||||
|
onCycleTheme,
|
||||||
}: Readonly<ActionBarProps>) {
|
}: Readonly<ActionBarProps>) {
|
||||||
const [nameInput, setNameInput] = useState("");
|
const [nameInput, setNameInput] = useState("");
|
||||||
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
|
const [suggestions, setSuggestions] = useState<SearchResult[]>([]);
|
||||||
@@ -454,6 +485,8 @@ export function ActionBar({
|
|||||||
bestiaryLoaded,
|
bestiaryLoaded,
|
||||||
onBulkImport,
|
onBulkImport,
|
||||||
bulkImportDisabled,
|
bulkImportDisabled,
|
||||||
|
themePreference,
|
||||||
|
onCycleTheme,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -367,9 +367,9 @@ function rowBorderClass(
|
|||||||
isConcentrating: boolean | undefined,
|
isConcentrating: boolean | undefined,
|
||||||
): string {
|
): string {
|
||||||
if (isActive && isConcentrating)
|
if (isActive && isConcentrating)
|
||||||
return "border border-l-2 border-accent/40 border-l-purple-400 bg-accent/10 card-glow";
|
return "border border-l-2 border-active-row-border border-l-purple-400 bg-active-row-bg card-glow";
|
||||||
if (isActive)
|
if (isActive)
|
||||||
return "border border-l-2 border-accent/40 bg-accent/10 card-glow";
|
return "border border-l-2 border-active-row-border bg-active-row-bg card-glow";
|
||||||
if (isConcentrating)
|
if (isConcentrating)
|
||||||
return "border border-l-2 border-transparent border-l-purple-400";
|
return "border border-l-2 border-transparent border-l-purple-400";
|
||||||
return "border border-l-2 border-transparent";
|
return "border border-l-2 border-transparent";
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ export function HpAdjustPopover({ onAdjust, onClose }: HpAdjustPopoverProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-red-400 transition-colors hover:bg-red-950 hover:text-red-300 disabled:pointer-events-none disabled:opacity-50"
|
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-red-400 transition-colors hover:bg-hp-damage-hover-bg hover:text-red-300 disabled:pointer-events-none disabled:opacity-50"
|
||||||
onClick={() => applyDelta(-1)}
|
onClick={() => applyDelta(-1)}
|
||||||
title="Apply damage"
|
title="Apply damage"
|
||||||
aria-label="Apply damage"
|
aria-label="Apply damage"
|
||||||
@@ -123,7 +123,7 @@ export function HpAdjustPopover({ onAdjust, onClose }: HpAdjustPopoverProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={!isValid}
|
disabled={!isValid}
|
||||||
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-emerald-400 transition-colors hover:bg-emerald-950 hover:text-emerald-300 disabled:pointer-events-none disabled:opacity-50"
|
className="inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-emerald-400 transition-colors hover:bg-hp-heal-hover-bg hover:text-emerald-300 disabled:pointer-events-none disabled:opacity-50"
|
||||||
onClick={() => applyDelta(1)}
|
onClick={() => applyDelta(1)}
|
||||||
title="Apply healing"
|
title="Apply healing"
|
||||||
aria-label="Apply healing"
|
aria-label="Apply healing"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ function PropertyLine({
|
|||||||
|
|
||||||
function SectionDivider() {
|
function SectionDivider() {
|
||||||
return (
|
return (
|
||||||
<div className="my-2 h-px bg-gradient-to-r from-amber-800/60 via-amber-700/40 to-transparent" />
|
<div className="my-2 h-px bg-gradient-to-r from-stat-divider-from via-stat-divider-via to-transparent" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
<div className="space-y-1 text-foreground">
|
<div className="space-y-1 text-foreground">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="font-bold text-amber-400 text-xl">{creature.name}</h2>
|
<h2 className="font-bold text-stat-heading text-xl">{creature.name}</h2>
|
||||||
<p className="text-muted-foreground text-sm italic">
|
<p className="text-muted-foreground text-sm italic">
|
||||||
{creature.size} {creature.type}, {creature.alignment}
|
{creature.size} {creature.type}, {creature.alignment}
|
||||||
</p>
|
</p>
|
||||||
@@ -194,7 +194,7 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
{creature.actions && creature.actions.length > 0 && (
|
{creature.actions && creature.actions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SectionDivider />
|
<SectionDivider />
|
||||||
<h3 className="font-bold text-amber-400 text-base">Actions</h3>
|
<h3 className="font-bold text-base text-stat-heading">Actions</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{creature.actions.map((a) => (
|
{creature.actions.map((a) => (
|
||||||
<div key={a.name} className="text-sm">
|
<div key={a.name} className="text-sm">
|
||||||
@@ -209,7 +209,9 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
{creature.bonusActions && creature.bonusActions.length > 0 && (
|
{creature.bonusActions && creature.bonusActions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SectionDivider />
|
<SectionDivider />
|
||||||
<h3 className="font-bold text-amber-400 text-base">Bonus Actions</h3>
|
<h3 className="font-bold text-base text-stat-heading">
|
||||||
|
Bonus Actions
|
||||||
|
</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{creature.bonusActions.map((a) => (
|
{creature.bonusActions.map((a) => (
|
||||||
<div key={a.name} className="text-sm">
|
<div key={a.name} className="text-sm">
|
||||||
@@ -224,7 +226,7 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
{creature.reactions && creature.reactions.length > 0 && (
|
{creature.reactions && creature.reactions.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<SectionDivider />
|
<SectionDivider />
|
||||||
<h3 className="font-bold text-amber-400 text-base">Reactions</h3>
|
<h3 className="font-bold text-base text-stat-heading">Reactions</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{creature.reactions.map((a) => (
|
{creature.reactions.map((a) => (
|
||||||
<div key={a.name} className="text-sm">
|
<div key={a.name} className="text-sm">
|
||||||
@@ -239,7 +241,7 @@ export function StatBlock({ creature }: Readonly<StatBlockProps>) {
|
|||||||
{!!creature.legendaryActions && (
|
{!!creature.legendaryActions && (
|
||||||
<>
|
<>
|
||||||
<SectionDivider />
|
<SectionDivider />
|
||||||
<h3 className="font-bold text-amber-400 text-base">
|
<h3 className="font-bold text-base text-stat-heading">
|
||||||
Legendary Actions
|
Legendary Actions
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-muted-foreground text-sm italic">
|
<p className="text-muted-foreground text-sm italic">
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export interface OverflowMenuItem {
|
|||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly onClick: () => void;
|
readonly onClick: () => void;
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
|
readonly keepOpen?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OverflowMenuProps {
|
interface OverflowMenuProps {
|
||||||
@@ -58,7 +59,7 @@ export function OverflowMenu({ items }: OverflowMenuProps) {
|
|||||||
disabled={item.disabled}
|
disabled={item.disabled}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
item.onClick();
|
item.onClick();
|
||||||
setOpen(false);
|
if (!item.keepOpen) setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{item.icon}
|
{item.icon}
|
||||||
|
|||||||
98
apps/web/src/hooks/use-theme.ts
Normal file
98
apps/web/src/hooks/use-theme.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
||||||
|
|
||||||
|
type ThemePreference = "system" | "light" | "dark";
|
||||||
|
type ResolvedTheme = "light" | "dark";
|
||||||
|
|
||||||
|
const STORAGE_KEY = "initiative:theme";
|
||||||
|
|
||||||
|
const listeners = new Set<() => void>();
|
||||||
|
let currentPreference: ThemePreference = loadPreference();
|
||||||
|
|
||||||
|
function loadPreference(): ThemePreference {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (raw === "light" || raw === "dark" || raw === "system") return raw;
|
||||||
|
} catch {
|
||||||
|
// storage unavailable
|
||||||
|
}
|
||||||
|
return "system";
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreference(pref: ThemePreference): void {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, pref);
|
||||||
|
} catch {
|
||||||
|
// quota exceeded or storage unavailable
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSystemTheme(): ResolvedTheme {
|
||||||
|
if (typeof globalThis.matchMedia !== "function") return "dark";
|
||||||
|
return globalThis.matchMedia("(prefers-color-scheme: light)").matches
|
||||||
|
? "light"
|
||||||
|
: "dark";
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolve(pref: ThemePreference): ResolvedTheme {
|
||||||
|
return pref === "system" ? getSystemTheme() : pref;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyTheme(resolved: ResolvedTheme): void {
|
||||||
|
document.documentElement.dataset.theme = resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function notifyAll(): void {
|
||||||
|
for (const listener of listeners) {
|
||||||
|
listener();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply on load
|
||||||
|
applyTheme(resolve(currentPreference));
|
||||||
|
|
||||||
|
// Listen for OS preference changes
|
||||||
|
if (typeof globalThis.matchMedia === "function") {
|
||||||
|
globalThis
|
||||||
|
.matchMedia("(prefers-color-scheme: light)")
|
||||||
|
.addEventListener("change", () => {
|
||||||
|
if (currentPreference === "system") {
|
||||||
|
applyTheme(resolve("system"));
|
||||||
|
notifyAll();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function subscribe(callback: () => void): () => void {
|
||||||
|
listeners.add(callback);
|
||||||
|
return () => listeners.delete(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnapshot(): ThemePreference {
|
||||||
|
return currentPreference;
|
||||||
|
}
|
||||||
|
|
||||||
|
const CYCLE: ThemePreference[] = ["system", "light", "dark"];
|
||||||
|
|
||||||
|
export function useTheme() {
|
||||||
|
const preference = useSyncExternalStore(subscribe, getSnapshot);
|
||||||
|
const resolved = resolve(preference);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
applyTheme(resolved);
|
||||||
|
}, [resolved]);
|
||||||
|
|
||||||
|
const setPreference = useCallback((pref: ThemePreference) => {
|
||||||
|
currentPreference = pref;
|
||||||
|
savePreference(pref);
|
||||||
|
applyTheme(resolve(pref));
|
||||||
|
notifyAll();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const cycleTheme = useCallback(() => {
|
||||||
|
const idx = CYCLE.indexOf(currentPreference);
|
||||||
|
const next = CYCLE[(idx + 1) % CYCLE.length];
|
||||||
|
setPreference(next);
|
||||||
|
}, [setPreference]);
|
||||||
|
|
||||||
|
return { preference, resolved, setPreference, cycleTheme } as const;
|
||||||
|
}
|
||||||
@@ -19,12 +19,47 @@
|
|||||||
--color-hover-neutral-bg: oklch(0.623 0.214 259 / 0.15);
|
--color-hover-neutral-bg: oklch(0.623 0.214 259 / 0.15);
|
||||||
--color-hover-action-bg: var(--color-muted);
|
--color-hover-action-bg: var(--color-muted);
|
||||||
--color-hover-destructive-bg: transparent;
|
--color-hover-destructive-bg: transparent;
|
||||||
|
--color-stat-heading: #fbbf24;
|
||||||
|
--color-stat-divider-from: oklch(0.5 0.1 65 / 0.6);
|
||||||
|
--color-stat-divider-via: oklch(0.5 0.1 65 / 0.4);
|
||||||
|
--color-hp-damage-hover-bg: oklch(0.25 0.05 25);
|
||||||
|
--color-hp-heal-hover-bg: oklch(0.25 0.05 155);
|
||||||
|
--color-active-row-bg: oklch(0.623 0.214 259 / 0.1);
|
||||||
|
--color-active-row-border: oklch(0.623 0.214 259 / 0.4);
|
||||||
--radius-sm: 0.25rem;
|
--radius-sm: 0.25rem;
|
||||||
--radius-md: 0.5rem;
|
--radius-md: 0.5rem;
|
||||||
--radius-lg: 0.75rem;
|
--radius-lg: 0.75rem;
|
||||||
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] {
|
||||||
|
--color-background: #eeecea;
|
||||||
|
--color-foreground: #374151;
|
||||||
|
--color-muted: #e0ddd9;
|
||||||
|
--color-muted-foreground: #6b7280;
|
||||||
|
--color-card: #f7f6f4;
|
||||||
|
--color-card-foreground: #374151;
|
||||||
|
--color-border: #ddd9d5;
|
||||||
|
--color-input: #cdc8c3;
|
||||||
|
--color-primary: #2563eb;
|
||||||
|
--color-primary-foreground: #ffffff;
|
||||||
|
--color-accent: #2563eb;
|
||||||
|
--color-destructive: #dc2626;
|
||||||
|
--color-hover-neutral: var(--color-primary);
|
||||||
|
--color-hover-action: var(--color-primary);
|
||||||
|
--color-hover-destructive: var(--color-destructive);
|
||||||
|
--color-hover-neutral-bg: oklch(0.623 0.214 259 / 0.08);
|
||||||
|
--color-hover-action-bg: var(--color-muted);
|
||||||
|
--color-hover-destructive-bg: transparent;
|
||||||
|
--color-stat-heading: #92400e;
|
||||||
|
--color-stat-divider-from: oklch(0.55 0.1 65 / 0.5);
|
||||||
|
--color-stat-divider-via: oklch(0.55 0.1 65 / 0.25);
|
||||||
|
--color-hp-damage-hover-bg: #fef2f2;
|
||||||
|
--color-hp-heal-hover-bg: #ecfdf5;
|
||||||
|
--color-active-row-bg: oklch(0.623 0.214 259 / 0.08);
|
||||||
|
--color-active-row-border: oklch(0.623 0.214 259 / 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes concentration-shake {
|
@keyframes concentration-shake {
|
||||||
0% {
|
0% {
|
||||||
translate: 0;
|
translate: 0;
|
||||||
@@ -178,6 +213,11 @@
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 15px -2px oklch(0.623 0.214 259 / 0.2),
|
0 0 15px -2px oklch(0.623 0.214 259 / 0.2),
|
||||||
inset 0 1px 0 0 oklch(0.7 0.15 259 / 0.1);
|
inset 0 1px 0 0 oklch(0.7 0.15 259 / 0.1);
|
||||||
|
|
||||||
|
[data-theme="light"] & {
|
||||||
|
background-image: none;
|
||||||
|
box-shadow: 0 1px 3px 0 oklch(0 0 0 / 0.08);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@utility panel-glow {
|
@utility panel-glow {
|
||||||
@@ -189,6 +229,11 @@
|
|||||||
box-shadow:
|
box-shadow:
|
||||||
0 0 20px -2px oklch(0.623 0.214 259 / 0.15),
|
0 0 20px -2px oklch(0.623 0.214 259 / 0.15),
|
||||||
inset 0 1px 0 0 oklch(0.7 0.15 259 / 0.1);
|
inset 0 1px 0 0 oklch(0.7 0.15 259 / 0.1);
|
||||||
|
|
||||||
|
[data-theme="light"] & {
|
||||||
|
background-image: none;
|
||||||
|
box-shadow: -1px 0 6px 0 oklch(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -207,3 +252,7 @@ body {
|
|||||||
color: var(--color-foreground);
|
color: var(--color-foreground);
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme="light"] body {
|
||||||
|
background-image: none;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user