mirror of
https://github.com/Dvorinka/Dash.git
synced 2026-06-03 15:02:56 +00:00
refactor(frontend): simplify theme system and unify UI components
Remove the "casaos" theme in favor of a unified design system. This involves cleaning up conditional styling across components, simplifying the theme toggle, and updating the global CSS variables to a more consistent dark/light mode implementation. - Remove `casaos` theme from `themes.ts` and `ThemeToggle` - Refactor `globals.css` to use a single dark mode definition - Simplify component styling by removing `isCasaOS` conditional logic - Update UI components (`Card`, `Badge`, `WidgetCard`, etc.) to use standard design tokens - Update E2E smoke tests to reflect theme changes
This commit is contained in:
@@ -7,7 +7,6 @@ import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSepara
|
||||
import { MoreVertical, RefreshCw, Pencil, Trash2, GripVertical, Clock, Shield, ImageIcon, StickyNote, Camera, Activity } from "lucide-react";
|
||||
import { useWidgetData, useRefreshWidget } from "@/lib/api/hooks";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTheme } from "@/components/providers";
|
||||
|
||||
const widgetTypeIcons: Record<string, React.ReactNode> = {
|
||||
clock: <Clock className="h-3.5 w-3.5" />,
|
||||
@@ -17,13 +16,6 @@ const widgetTypeIcons: Record<string, React.ReactNode> = {
|
||||
immich: <Camera className="h-3.5 w-3.5" />,
|
||||
};
|
||||
|
||||
const widgetTypeColors: Record<string, string> = {
|
||||
clock: "from-blue-500/20 to-cyan-500/20",
|
||||
pihole: "from-emerald-500/20 to-teal-500/20",
|
||||
image: "from-purple-500/20 to-pink-500/20",
|
||||
memos: "from-amber-500/20 to-orange-500/20",
|
||||
immich: "from-rose-500/20 to-red-500/20",
|
||||
};
|
||||
|
||||
export function WidgetCard({
|
||||
widget,
|
||||
@@ -38,34 +30,25 @@ export function WidgetCard({
|
||||
}) {
|
||||
const { data, isLoading, error } = useWidgetData(widget.id);
|
||||
const refreshMut = useRefreshWidget();
|
||||
const { theme } = useTheme();
|
||||
const isCasaOS = theme === "casaos";
|
||||
|
||||
const handleRefresh = () => refreshMut.mutate(widget.id);
|
||||
|
||||
const statusLabel = data?.status === "stale" ? "stale" : data?.status === "error" ? "error" : "";
|
||||
const typeIcon = widgetTypeIcons[widget.type] || <Activity className="h-3.5 w-3.5" />;
|
||||
const typeGradient = widgetTypeColors[widget.type] || "from-muted to-muted";
|
||||
|
||||
return (
|
||||
<Card className={cn(
|
||||
"group relative border-0 overflow-hidden",
|
||||
isCasaOS
|
||||
? "rounded-[20px] bg-card border border-border shadow-[0_4px_16px_rgba(0,0,0,0.15)] hover:shadow-[0_8px_32px_rgba(0,0,0,0.25)] hover:-translate-y-[2px] transition-all duration-300"
|
||||
: "rounded-2xl shadow-[0px_0px_0px_1px_var(--color-border)] hover:shadow-border-hover transition-all duration-200"
|
||||
)}>
|
||||
<Card className="group relative border-0 overflow-hidden rounded-2xl shadow-[0px_0px_0px_1px_var(--color-border)] hover:shadow-border-hover transition-all duration-200">
|
||||
<div className={cn(
|
||||
"absolute top-0 left-0 right-0 h-1 opacity-60",
|
||||
isCasaOS ? `bg-gradient-to-r ${typeGradient}` : "bg-gradient-to-r from-ring/40 to-transparent"
|
||||
"absolute top-0 left-0 right-0 h-1 opacity-60 bg-ring"
|
||||
)} />
|
||||
<CardHeader className={cn("flex flex-row items-center justify-between pt-4 pb-2", isCasaOS ? "px-5" : "px-4")}>
|
||||
<CardHeader className="flex flex-row items-center justify-between pt-4 pb-2 px-4">
|
||||
<div className="flex items-center gap-2.5 min-w-0">
|
||||
{dragHandleProps && (
|
||||
<div {...dragHandleProps} className="cursor-grab opacity-0 group-hover:opacity-60 transition-opacity rounded-md p-0.5 hover:bg-accent">
|
||||
<GripVertical className={cn("text-muted-foreground", isCasaOS ? "h-5 w-5" : "h-4 w-4")} />
|
||||
<GripVertical className="h-4 w-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<div className={cn("flex h-6 w-6 items-center justify-center rounded-md shrink-0", isCasaOS ? "bg-white/10" : "bg-accent")}>
|
||||
<div className="flex h-6 w-6 items-center justify-center rounded-md shrink-0 bg-accent">
|
||||
{typeIcon}
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
@@ -75,7 +58,7 @@ export function WidgetCard({
|
||||
{statusLabel && (
|
||||
<span className={cn(
|
||||
"text-[9px] px-1.5 py-0.5 rounded-full font-medium uppercase shrink-0",
|
||||
statusLabel === "stale" ? "bg-amber-500/15 text-amber-400" : "bg-destructive/15 text-destructive"
|
||||
statusLabel === "stale" ? "bg-amber-950 text-amber-400" : "bg-red-950 text-destructive"
|
||||
)}>
|
||||
{statusLabel}
|
||||
</span>
|
||||
@@ -83,12 +66,12 @@ export function WidgetCard({
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 shrink-0">
|
||||
<Button variant="ghost" size="icon" className={cn("relative z-10 pointer-events-auto rounded-lg h-7 w-7", isCasaOS ? "text-white/50 hover:text-white hover:bg-white/10" : "hover:bg-accent")} onClick={handleRefresh} disabled={refreshMut.isPending}>
|
||||
<RefreshCw className={cn(refreshMut.isPending && "animate-spin", isCasaOS ? "h-4 w-4" : "h-3.5 w-3.5")} />
|
||||
<Button variant="ghost" size="icon" className="relative z-10 pointer-events-auto rounded-lg h-7 w-7 hover:bg-accent" onClick={handleRefresh} disabled={refreshMut.isPending}>
|
||||
<RefreshCw className={cn(refreshMut.isPending && "animate-spin", "h-3.5 w-3.5")} />
|
||||
</Button>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className={cn("rounded-lg h-7 w-7", isCasaOS ? "text-white/50 hover:text-white hover:bg-white/10" : "hover:bg-accent")}>
|
||||
<Button variant="ghost" size="icon" className="rounded-lg h-7 w-7 hover:bg-accent">
|
||||
<MoreVertical className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -104,7 +87,7 @@ export function WidgetCard({
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className={cn(isCasaOS ? "px-5 pb-5 pt-1" : "px-4 pb-4 pt-1")}>
|
||||
<CardContent className="px-4 pb-4 pt-1">
|
||||
{isLoading ? (
|
||||
<span className="font-mono text-xs text-muted-foreground">[LOADING...]</span>
|
||||
) : error || data?.status === "error" ? (
|
||||
@@ -145,7 +128,7 @@ function ClockContent({ config }: { config: Record<string, unknown>; data?: Widg
|
||||
<div className="font-mono text-3xl tabular-nums tracking-tight text-foreground">{localTime}</div>
|
||||
<div className="text-xs text-muted-foreground font-medium">{localDate}</div>
|
||||
{timezones.length > 0 && (
|
||||
<div className="mt-2 flex flex-col gap-1.5 border-t border-border/30 pt-2">
|
||||
<div className="mt-2 flex flex-col gap-1.5 border-t border-border pt-2">
|
||||
{timezones.map((tz) => {
|
||||
try {
|
||||
const t = new Date().toLocaleTimeString([], { timeZone: tz, hour: "2-digit", minute: "2-digit" });
|
||||
@@ -192,21 +175,21 @@ function PiHoleContent({ data }: { data?: WidgetData }) {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg bg-emerald-500/10 p-2.5">
|
||||
<div className="rounded-lg bg-secondary p-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-emerald-400 font-medium mb-0.5">Status</div>
|
||||
<div className={cn("text-sm font-semibold", d.status === "enabled" ? "text-emerald-400" : "text-destructive")}>
|
||||
{String(d.status || "unknown")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-blue-500/10 p-2.5">
|
||||
<div className="rounded-lg bg-secondary p-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-blue-400 font-medium mb-0.5">Blocked</div>
|
||||
<div className="font-mono text-sm font-semibold text-foreground">{String(d.ads_blocked_today || "0")}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-purple-500/10 p-2.5">
|
||||
<div className="rounded-lg bg-secondary p-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-purple-400 font-medium mb-0.5">Queries</div>
|
||||
<div className="font-mono text-sm font-semibold text-foreground">{String(d.dns_queries_today || "0")}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-amber-500/10 p-2.5">
|
||||
<div className="rounded-lg bg-secondary p-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-amber-400 font-medium mb-0.5">% Blocked</div>
|
||||
<div className="font-mono text-sm font-semibold text-foreground">{String(d.ads_percentage_today || "0")}%</div>
|
||||
</div>
|
||||
@@ -222,8 +205,8 @@ function MemosContent({ data }: { data?: WidgetData }) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2 max-h-40 overflow-y-auto pr-1">
|
||||
{memos.slice(0, 5).map((m, i) => (
|
||||
<div key={i} className="rounded-lg bg-amber-500/10 p-2.5 border border-amber-500/10">
|
||||
<div className="text-[11px] leading-relaxed line-clamp-2 text-foreground/90">
|
||||
<div key={i} className="rounded-lg bg-secondary p-2.5 border border-border">
|
||||
<div className="text-[11px] leading-relaxed line-clamp-2 text-foreground">
|
||||
{String(m.content || m.snippet || "")}
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,11 +221,11 @@ function ImmichContent({ data }: { data?: WidgetData }) {
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="rounded-lg bg-blue-500/10 p-2.5">
|
||||
<div className="rounded-lg bg-secondary p-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-blue-400 font-medium mb-0.5">Photos</div>
|
||||
<div className="font-mono text-sm font-semibold text-foreground">{String(d.photos || "0")}</div>
|
||||
</div>
|
||||
<div className="rounded-lg bg-rose-500/10 p-2.5">
|
||||
<div className="rounded-lg bg-secondary p-2.5">
|
||||
<div className="text-[10px] uppercase tracking-wider text-rose-400 font-medium mb-0.5">Videos</div>
|
||||
<div className="font-mono text-sm font-semibold text-foreground">{String(d.videos || "0")}</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user