diff --git a/frontend/app/globals.css b/frontend/app/globals.css index 69637a3..67e8193 100644 --- a/frontend/app/globals.css +++ b/frontend/app/globals.css @@ -1,6 +1,6 @@ @import "tailwindcss"; -@custom-variant dark (&:where([data-theme="dark"], [data-theme="casaos"])); +@custom-variant dark (&:where([data-theme="dark"])); /* ── Light (Vercel-inspired) ── */ :root, @@ -21,10 +21,11 @@ --color-accent-foreground: #171717; --color-destructive: #ef4444; --color-destructive-foreground: #ffffff; - --color-border: rgba(0, 0, 0, 0.08); + --color-border: #e5e5e5; --color-ring: #0072f5; --color-signal: #ff5b4f; - --color-input: rgba(0, 0, 0, 0.08); + --color-input: #e5e5e5; + --color-overlay: #f5f5f5; --radius: 0.5rem; --font-geist-sans: "Geist", "Arial", "Apple Color Emoji", "Segoe UI Emoji", sans-serif; --font-geist-mono: "Geist Mono", "ui-monospace", "SFMono-Regular", "Roboto Mono", monospace; @@ -32,57 +33,29 @@ /* ── Dark (Rich warm dark — not pure black) ── */ [data-theme="dark"] { - --color-background: #1b1b1b; + --color-background: #0d0d0d; --color-foreground: #ececec; - --color-card: #222222; + --color-card: #141414; --color-card-foreground: #ececec; - --color-popover: #262626; + --color-popover: #1a1a1a; --color-popover-foreground: #ececec; --color-primary: #ececec; - --color-primary-foreground: #1b1b1b; - --color-secondary: #2a2a2a; + --color-primary-foreground: #0d0d0d; + --color-secondary: #1a1a1a; --color-secondary-foreground: #ececec; - --color-muted: #2a2a2a; + --color-muted: #1a1a1a; --color-muted-foreground: #888888; - --color-accent: #2a2a2a; + --color-accent: #1a1a1a; --color-accent-foreground: #ececec; --color-destructive: #f43f5e; --color-destructive-foreground: #ececec; - --color-border: #333333; + --color-border: #262626; --color-ring: #3b82f6; --color-signal: #f43f5e; - --color-input: #333333; + --color-input: #262626; + --color-overlay: #050505; } -/* ── CasaOS (Colorful dark) ── */ -[data-theme="casaos"] { - --color-background: #1b1b2e; - --color-foreground: #f1f5f9; - --color-card: #22223a; - --color-card-foreground: #f1f5f9; - --color-popover: #26264a; - --color-popover-foreground: #f1f5f9; - --color-primary: #60a5fa; - --color-primary-foreground: #1b1b2e; - --color-secondary: #2a2a4a; - --color-secondary-foreground: #f1f5f9; - --color-muted: #2a2a4a; - --color-muted-foreground: #94a3b8; - --color-accent: #2a2a4a; - --color-accent-foreground: #60a5fa; - --color-destructive: #f43f5e; - --color-destructive-foreground: #f1f5f9; - --color-border: #333355; - --color-ring: #60a5fa; - --color-signal: #f43f5e; - --color-input: #333355; -} - -/* ── CasaOS background gradient ── */ -[data-theme="casaos"] body { - background: #1b1b2e; - background-attachment: fixed; -} /* ── Base ── */ * { @@ -152,10 +125,6 @@ body { transform: translateY(-2px); } -/* ── CasaOS card hover ── */ -[data-theme="casaos"] .service-card:hover { - transform: translateY(-4px); -} /* ── Drag overlay ── */ .drag-overlay { @@ -212,14 +181,14 @@ body { /* ── Colorful badge variants ── */ .badge-local { - background: rgba(16, 185, 129, 0.15); + background: #0f291e; color: #34d399; } .badge-external { - background: rgba(96, 165, 250, 0.15); + background: #162038; color: #60a5fa; } .badge-custom { - background: rgba(139, 92, 246, 0.15); + background: #231a38; color: #a78bfa; } diff --git a/frontend/components/dashboard/dashboard-page.tsx b/frontend/components/dashboard/dashboard-page.tsx index 6bdbfdb..f9d83e2 100644 --- a/frontend/components/dashboard/dashboard-page.tsx +++ b/frontend/components/dashboard/dashboard-page.tsx @@ -11,6 +11,7 @@ import { GroupForm } from "@/components/groups/group-form"; import { WidgetCard } from "@/components/widgets/widget-card"; import { WidgetForm } from "@/components/widgets/widget-form"; import { Button } from "@/components/ui/button"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { Plus, Loader2, AlertCircle, LayoutGrid, List, Pencil, Trash2, GripVertical } from "lucide-react"; import { DndContext, @@ -27,74 +28,13 @@ import { } from "@dnd-kit/core"; import { SortableContext, - verticalListSortingStrategy, rectSortingStrategy, useSortable, } from "@dnd-kit/sortable"; import { CSS } from "@dnd-kit/utilities"; import { cn } from "@/lib/utils"; -/* ---------- Sortable wrappers ---------- */ - -function SortableGroup({ - group, - onEditService, - onDeleteService, - onEditGroup, -}: { - group: Group; - onEditService: (s: Service) => void; - onDeleteService: (id: string) => void; - onEditGroup: (g: Group) => void; -}) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: group.id, - data: { type: "group" }, - }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.4 : 1, - }; - - return ( -
- -
- ); -} - -function SortableService({ - service, - onEdit, - onDelete, -}: { - service: Service; - onEdit: (s: Service) => void; - onDelete: (id: string) => void; -}) { - const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({ - id: service.id, - data: { type: "service", groupId: service.groupId }, - }); - const style = { - transform: CSS.Transform.toString(transform), - transition, - opacity: isDragging ? 0.4 : 1, - }; - - return ( -
- -
- ); -} +/* ---------- Sortable wrapper for widgets only ---------- */ function SortableWidget({ widget, @@ -173,7 +113,7 @@ function ServiceListItem({ - @@ -181,42 +121,11 @@ function ServiceListItem({ ); } -/* ---------- Drag Overlay ---------- */ +/* ---------- Drag Overlay (widgets only) ---------- */ function DashboardDragOverlay({ activeId, dashboard }: { activeId: string; dashboard: Dashboard }) { - const allServices = [ - ...dashboard.ungroupedServices, - ...dashboard.groups.flatMap((g) => g.services), - ]; - const service = allServices.find((s) => s.id === activeId); - const group = dashboard.groups.find((g) => g.id === activeId); const widget = dashboard.widgets.find((w) => w.id === activeId); - if (service) { - return ( -
-
- {service.name.slice(0, 2).toUpperCase()} -
- {service.name} -
- ); - } - - if (group) { - return ( -
-
- -
-
- {group.name} - {group.services.length} apps -
-
- ); - } - if (widget) { return (
@@ -231,7 +140,7 @@ function DashboardDragOverlay({ activeId, dashboard }: { activeId: string; dashb ); } - return
Moving…
; + return null; } /* ---------- Main Dashboard ---------- */ @@ -273,95 +182,13 @@ export default function DashboardPage() { const activeIdStr = String(active.id); const overIdStr = String(over.id); - const allServiceIds = [ - ...dashboard.ungroupedServices.map((s) => s.id), - ...dashboard.groups.flatMap((g) => g.services.map((s) => s.id)), - ]; const groupIds = dashboard.groups.map((g) => g.id); const widgetIds = dashboard.widgets.map((w) => w.id); - const isActiveService = allServiceIds.includes(activeIdStr); - const isOverService = allServiceIds.includes(overIdStr); - const isActiveGroup = groupIds.includes(activeIdStr); - const isOverGroup = groupIds.includes(overIdStr); const isActiveWidget = widgetIds.includes(activeIdStr); const isOverWidget = widgetIds.includes(overIdStr); - // Service → Service (reorder / cross-group) - if (isActiveService && isOverService) { - const findServiceLocation = (sid: string): { groupId: string | null; index: number } => { - const ungroupedIdx = dashboard.ungroupedServices.findIndex((s) => s.id === sid); - if (ungroupedIdx !== -1) return { groupId: null, index: ungroupedIdx }; - for (const g of dashboard.groups) { - const idx = g.services.findIndex((s) => s.id === sid); - if (idx !== -1) return { groupId: g.id, index: idx }; - } - return { groupId: null, index: -1 }; - }; - - const activeLoc = findServiceLocation(activeIdStr); - const overLoc = findServiceLocation(overIdStr); - - const groupServices: Record = {}; - for (const g of dashboard.groups) { - const ids = [...g.services.map((s) => s.id)]; - if (activeLoc.groupId === g.id) ids.splice(activeLoc.index, 1); - if (overLoc.groupId === g.id) { - const insertIdx = activeLoc.groupId === g.id && activeLoc.index < overLoc.index ? overLoc.index : overLoc.index; - ids.splice(insertIdx, 0, activeIdStr); - } - groupServices[g.id] = ids; - } - - const ungroupedIds = [...dashboard.ungroupedServices.map((s) => s.id)]; - if (activeLoc.groupId === null) ungroupedIds.splice(activeLoc.index, 1); - if (overLoc.groupId === null) { - const insertIdx = activeLoc.groupId === null && activeLoc.index < overLoc.index ? overLoc.index : overLoc.index; - ungroupedIds.splice(insertIdx, 0, activeIdStr); - } - if (activeLoc.groupId !== null && overLoc.groupId === null) { - ungroupedIds.splice(overLoc.index, 0, activeIdStr); - } - - updateLayout.mutate({ groupIds, widgetIds, ungroupedServiceIds: ungroupedIds, groupServices }); - return; - } - - // Service → Group header (move into group) - if (isActiveService && isOverGroup) { - const groupServices: Record = {}; - const ungroupedIds = [...dashboard.ungroupedServices.map((s) => s.id)]; - - for (const g of dashboard.groups) { - const ids = g.services.map((s) => s.id); - const idx = ids.indexOf(activeIdStr); - if (idx !== -1) ids.splice(idx, 1); - if (g.id === overIdStr) ids.push(activeIdStr); - groupServices[g.id] = ids; - } - const uIdx = ungroupedIds.indexOf(activeIdStr); - if (uIdx !== -1) ungroupedIds.splice(uIdx, 1); - - updateLayout.mutate({ groupIds, widgetIds, ungroupedServiceIds: ungroupedIds, groupServices }); - return; - } - - // Group reorder - if (isActiveGroup && isOverGroup) { - const newGroupIds = [...groupIds]; - const fromIdx = newGroupIds.indexOf(activeIdStr); - const toIdx = newGroupIds.indexOf(overIdStr); - if (fromIdx !== -1 && toIdx !== -1) { - const [moved] = newGroupIds.splice(fromIdx, 1); - newGroupIds.splice(toIdx, 0, moved); - const groupServices: Record = {}; - for (const g of dashboard.groups) groupServices[g.id] = g.services.map((s) => s.id); - updateLayout.mutate({ groupIds: newGroupIds, widgetIds, ungroupedServiceIds: dashboard.ungroupedServices.map((s) => s.id), groupServices }); - } - return; - } - - // Widget reorder + // Widget reorder only if (isActiveWidget && isOverWidget) { const newWidgetIds = [...widgetIds]; const fromIdx = newWidgetIds.indexOf(activeIdStr); @@ -405,9 +232,9 @@ export default function DashboardPage() { if (error) { return (
-
+
-
+
@@ -434,7 +261,7 @@ export default function DashboardPage() {
{isEmpty ? (
-
+
@@ -459,42 +286,44 @@ export default function DashboardPage() { onDragOver={handleDragOver} onDragEnd={handleDragEnd} > - {/* Widgets strip */} -
-
+ {/* Widgets section */} + +
- Widgets + Widgets
-
- {widgets.length > 0 ? ( -
- w.id)} strategy={rectSortingStrategy}> - {widgets.map((w) => ( - - ))} - -
- ) : ( - - )} -
+ + + {widgets.length > 0 ? ( +
+ w.id)} strategy={rectSortingStrategy}> + {widgets.map((w) => ( + + ))} + +
+ ) : ( + + )} +
+ {/* Apps section */} -
-
+ +
- Apps + Apps
@@ -505,7 +334,7 @@ export default function DashboardPage() { > -
+
-
- - {/* Groups */} - g.id)} strategy={verticalListSortingStrategy}> + + + {/* Groups */} {groups.map((g) => ( - ))} - - {/* Ungrouped services */} - {ungrouped.length > 0 && ( -
- {groups.length > 0 && ( -
-
- Ungrouped - {ungrouped.length} -
- )} - s.id)} strategy={rectSortingStrategy}> + {/* Ungrouped services */} + {ungrouped.length > 0 && ( +
+ {groups.length > 0 && ( +
+
+ Ungrouped + {ungrouped.length} +
+ )} {viewMode === "grid" ? (
{ungrouped.map((s) => ( - + ))}
@@ -563,27 +389,25 @@ export default function DashboardPage() { ))}
)} - -
- )} +
+ )} - {/* In-grid add tile when no ungrouped but groups exist */} - {ungrouped.length === 0 && groups.length > 0 && ( -
+ {/* Add tile when no ungrouped but groups exist */} + {ungrouped.length === 0 && groups.length > 0 && ( -
- )} + )} - {/* No apps at all - show empty state within apps section */} - {groups.length === 0 && ungrouped.length === 0 && ( - - )} -
+ {/* No apps at all - show empty state within apps section */} + {groups.length === 0 && ungrouped.length === 0 && ( + + )} + + {activeId && dashboard ? ( diff --git a/frontend/components/groups/group-section.tsx b/frontend/components/groups/group-section.tsx index ebc1bec..519f4aa 100644 --- a/frontend/components/groups/group-section.tsx +++ b/frontend/components/groups/group-section.tsx @@ -4,27 +4,25 @@ import type { Group, Service } from "@/lib/api/schema"; import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; import { Button } from "@/components/ui/button"; +import { Card } from "@/components/ui/card"; +import { Separator } from "@/components/ui/separator"; import { ServiceCard } from "@/components/services/service-card"; -import { ChevronDown, MoreVertical, Pencil, Trash2, GripVertical, FolderOpen } from "lucide-react"; +import { ChevronDown, MoreVertical, Pencil, Trash2, FolderOpen } from "lucide-react"; import { useUpdateGroup, useDeleteGroup } from "@/lib/api/hooks"; import { cn } from "@/lib/utils"; import { useState } from "react"; -import { useTheme } from "@/components/providers"; interface GroupSectionProps { group: Group; onEditService: (s: Service) => void; onDeleteService: (id: string) => void; onEditGroup: (g: Group) => void; - dragHandleProps?: React.HTMLAttributes; } -export function GroupSection({ group, onEditService, onDeleteService, onEditGroup, dragHandleProps }: GroupSectionProps) { +export function GroupSection({ group, onEditService, onDeleteService, onEditGroup }: GroupSectionProps) { const updateGroup = useUpdateGroup(); const deleteGroup = useDeleteGroup(); const [open, setOpen] = useState(!group.collapsed); - const { theme } = useTheme(); - const isCasaOS = theme === "casaos"; const handleToggle = () => { const next = !open; @@ -42,28 +40,16 @@ export function GroupSection({ group, onEditService, onDeleteService, onEditGrou return ( -
+ {/* Group header */} -
- {dragHandleProps && ( -
- -
- )} - +
diff --git a/frontend/components/shell/theme-toggle.tsx b/frontend/components/shell/theme-toggle.tsx index 1380571..12295c0 100644 --- a/frontend/components/shell/theme-toggle.tsx +++ b/frontend/components/shell/theme-toggle.tsx @@ -1,48 +1,29 @@ "use client"; import { useTheme } from "@/components/providers"; -import { themeLabels, type Theme } from "@/lib/theme/themes"; import { Button } from "@/components/ui/button"; -import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"; -import { Sun, Moon, Sparkles, Check } from "lucide-react"; -import { cn } from "@/lib/utils"; - -const themeIcons: Record = { - light: , - dark: , - casaos: , -}; - -const themeDot: Record = { - light: "bg-amber-400", - dark: "bg-indigo-400", - casaos: "bg-pink-400", -}; +import { Sun, Moon } from "lucide-react"; export function ThemeToggle() { const { theme, setTheme } = useTheme(); + const toggle = () => { + setTheme(theme === "dark" ? "light" : "dark"); + }; + return ( - - - - - - {(["light", "dark", "casaos"] as Theme[]).map((t) => ( - setTheme(t)} className={cn("gap-2.5 rounded-lg cursor-pointer", theme === t && "bg-accent")}> - - {themeIcons[t]} - - {themeLabels[t]} - {theme === t && } - - ))} - - + ); } diff --git a/frontend/components/ui/alert.tsx b/frontend/components/ui/alert.tsx index b166d76..271b57b 100644 --- a/frontend/components/ui/alert.tsx +++ b/frontend/components/ui/alert.tsx @@ -8,7 +8,7 @@ const alertVariants = cva( variants: { variant: { default: "bg-background text-foreground", - destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + destructive: "border-destructive text-destructive [&>svg]:text-destructive", }, }, defaultVariants: { variant: "default" }, diff --git a/frontend/components/ui/badge.tsx b/frontend/components/ui/badge.tsx index 4294b8f..33f51de 100644 --- a/frontend/components/ui/badge.tsx +++ b/frontend/components/ui/badge.tsx @@ -11,9 +11,9 @@ const badgeVariants = cva( secondary: "border-transparent bg-secondary text-secondary-foreground", destructive: "border-transparent bg-destructive text-destructive-foreground", outline: "text-foreground", - local: "border-transparent bg-blue-500/15 text-blue-500", - external: "border-transparent bg-emerald-500/15 text-emerald-500", - custom: "border-transparent bg-amber-500/15 text-amber-500", + local: "border-transparent bg-blue-950 text-blue-400", + external: "border-transparent bg-emerald-950 text-emerald-400", + custom: "border-transparent bg-amber-950 text-amber-400", }, }, defaultVariants: { variant: "default" }, diff --git a/frontend/components/ui/command.tsx b/frontend/components/ui/command.tsx index 9afc00c..1a71bd2 100644 --- a/frontend/components/ui/command.tsx +++ b/frontend/components/ui/command.tsx @@ -25,7 +25,7 @@ const CommandInput = React.forwardRef<
diff --git a/frontend/components/ui/dialog.tsx b/frontend/components/ui/dialog.tsx index f43bcda..c6420e8 100644 --- a/frontend/components/ui/dialog.tsx +++ b/frontend/components/ui/dialog.tsx @@ -17,7 +17,7 @@ const DialogOverlay = React.forwardRef< span]:line-clamp-1", + "flex h-9 w-full items-center justify-between gap-2 rounded-md border border-border bg-background px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1", className, )} {...props} diff --git a/frontend/components/ui/sheet.tsx b/frontend/components/ui/sheet.tsx index 6036ea9..b124981 100644 --- a/frontend/components/ui/sheet.tsx +++ b/frontend/components/ui/sheet.tsx @@ -16,7 +16,7 @@ const SheetOverlay = React.forwardRef< React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( diff --git a/frontend/components/widgets/widget-card.tsx b/frontend/components/widgets/widget-card.tsx index a161fb8..92abc5f 100644 --- a/frontend/components/widgets/widget-card.tsx +++ b/frontend/components/widgets/widget-card.tsx @@ -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 = { clock: , @@ -17,13 +16,6 @@ const widgetTypeIcons: Record = { immich: , }; -const widgetTypeColors: Record = { - 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] || ; - const typeGradient = widgetTypeColors[widget.type] || "from-muted to-muted"; return ( - +
- +
{dragHandleProps && (
- +
)} -
+
{typeIcon}
@@ -75,7 +58,7 @@ export function WidgetCard({ {statusLabel && ( {statusLabel} @@ -83,12 +66,12 @@ export function WidgetCard({
- - @@ -104,7 +87,7 @@ export function WidgetCard({
- + {isLoading ? ( [LOADING...] ) : error || data?.status === "error" ? ( @@ -145,7 +128,7 @@ function ClockContent({ config }: { config: Record; data?: Widg
{localTime}
{localDate}
{timezones.length > 0 && ( -
+
{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 (
-
+
Status
{String(d.status || "unknown")}
-
+
Blocked
{String(d.ads_blocked_today || "0")}
-
+
Queries
{String(d.dns_queries_today || "0")}
-
+
% Blocked
{String(d.ads_percentage_today || "0")}%
@@ -222,8 +205,8 @@ function MemosContent({ data }: { data?: WidgetData }) { return (
{memos.slice(0, 5).map((m, i) => ( -
-
+
+
{String(m.content || m.snippet || "")}
@@ -238,11 +221,11 @@ function ImmichContent({ data }: { data?: WidgetData }) { return (
-
+
Photos
{String(d.photos || "0")}
-
+
Videos
{String(d.videos || "0")}
diff --git a/frontend/components/widgets/widget-form.tsx b/frontend/components/widgets/widget-form.tsx index 1b4f323..4b21e5d 100644 --- a/frontend/components/widgets/widget-form.tsx +++ b/frontend/components/widgets/widget-form.tsx @@ -140,7 +140,7 @@ export function WidgetForm({ widget, open, onOpenChange }: WidgetFormProps) { {tz.split("/").pop()?.replace("_", " ")}