Openstatus www.openstatus.dev

fix: dark prose and stpg theme (#1484)

* fix: theme dark

* fix: theme styles

* fix: theme styles

* fix: theme skeleton

authored by

Maximilian Kaske and committed by
GitHub
df267293 0e2d7185

+99 -103
+8 -4
apps/dashboard/src/app/(dashboard)/overview/page.tsx
··· 103 103 icon: List, 104 104 }, 105 105 { 106 - title: lastIncident?.resolvedAt ? "Recent Incident" : "Active Incident", 106 + title: 107 + lastIncident?.resolvedAt === undefined && lastIncident 108 + ? "Active Incident" 109 + : "Recent Incident", 107 110 value: incidentDistance, 108 111 disabled: !lastIncident?.monitorId, 109 112 href: `/monitors/${lastIncident?.monitorId}/incidents`, 110 - variant: lastIncident?.resolvedAt 111 - ? ("default" as const) 112 - : ("warning" as const), 113 + variant: 114 + lastIncident?.resolvedAt === undefined && lastIncident 115 + ? ("warning" as const) 116 + : ("default" as const), 113 117 icon: Search, 114 118 }, 115 119 {
+4 -1
apps/dashboard/src/components/data-table/maintenances/columns.tsx
··· 16 16 header: "Title", 17 17 enableSorting: false, 18 18 enableHiding: false, 19 + meta: { 20 + cellClassName: "max-w-[200px] truncate", 21 + }, 19 22 }, 20 23 { 21 24 accessorKey: "message", ··· 25 28 cell: ({ row }) => { 26 29 const value = String(row.getValue("message")); 27 30 return ( 28 - <div className="prose prose-sm line-clamp-3 max-w-[200px] truncate text-muted-foreground"> 31 + <div className="prose dark:prose-invert prose-sm line-clamp-3 max-w-[200px] truncate text-muted-foreground"> 29 32 <ProcessMessage value={value} /> 30 33 </div> 31 34 );
+1 -1
apps/dashboard/src/components/data-table/status-report-updates/data-table.tsx
··· 111 111 </div> 112 112 </TableCell> 113 113 <TableCell> 114 - <div className="prose prose-sm line-clamp-3 text-wrap"> 114 + <div className="prose dark:prose-invert prose-sm line-clamp-3 text-wrap text-muted-foreground"> 115 115 <ProcessMessage value={update.message} /> 116 116 </div> 117 117 </TableCell>
+3
apps/dashboard/src/components/data-table/status-reports/columns.tsx
··· 53 53 header: "Title", 54 54 enableSorting: false, 55 55 enableHiding: false, 56 + meta: { 57 + cellClassName: "max-w-[200px] truncate", 58 + }, 56 59 }, 57 60 { 58 61 accessorKey: "status",
+1 -1
apps/dashboard/src/components/forms/maintenance/form.tsx
··· 370 370 <TabsContent value="tab-2"> 371 371 <div className="grid gap-2"> 372 372 <Label>Preview</Label> 373 - <div className="prose prose-sm rounded-md border px-3 py-2 text-foreground text-sm"> 373 + <div className="prose dark:prose-invert prose-sm rounded-md border px-3 py-2 text-foreground text-sm"> 374 374 <ProcessMessage value={watchMessage} /> 375 375 </div> 376 376 </div>
+1 -1
apps/dashboard/src/components/forms/status-report-update/form.tsx
··· 278 278 <TabsContent value="tab-2"> 279 279 <div className="grid gap-2"> 280 280 <Label>Preview</Label> 281 - <div className="prose prose-sm rounded-md border px-3 py-2 text-foreground text-sm"> 281 + <div className="prose prose-sm dark:prose-invert rounded-md border px-3 py-2 text-foreground text-sm"> 282 282 <ProcessMessage value={watchMessage} /> 283 283 </div> 284 284 </div>
+1 -1
apps/dashboard/src/components/forms/status-report/form.tsx
··· 317 317 <TabsContent value="tab-2"> 318 318 <div className="grid gap-2"> 319 319 <Label>Preview</Label> 320 - <div className="prose prose-sm rounded-md border px-3 py-2 text-foreground text-sm"> 320 + <div className="prose dark:prose-invert prose-sm rounded-md border px-3 py-2 text-foreground text-sm"> 321 321 <ProcessMessage value={watchMessage} /> 322 322 </div> 323 323 </div>
+31 -22
apps/status-page/src/app/(public)/client.tsx
··· 23 23 import { Button } from "@/components/ui/button"; 24 24 import { Input } from "@/components/ui/input"; 25 25 import { Separator } from "@/components/ui/separator"; 26 + import { Skeleton } from "@/components/ui/skeleton"; 26 27 import { 27 28 Tooltip, 28 29 TooltipContent, ··· 32 33 import { monitors } from "@/data/monitors"; 33 34 import { useTRPC } from "@/lib/trpc/client"; 34 35 import { cn } from "@/lib/utils"; 35 - import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; 36 + import { 37 + THEMES, 38 + THEME_KEYS, 39 + generateThemeStyles, 40 + } from "@openstatus/theme-store"; 36 41 import { useQuery } from "@tanstack/react-query"; 37 42 import { useTheme } from "next-themes"; 38 43 import { useQueryStates } from "nuqs"; ··· 61 66 }, []); 62 67 63 68 useEffect(() => { 64 - const theme = resolvedTheme as "dark" | "light"; 65 - if (["dark", "light"].includes(theme)) { 66 - const element = document.documentElement; 67 - element.removeAttribute("style"); // reset the style 68 - Object.keys(THEMES[t][theme]).forEach((key) => { 69 - const value = 70 - THEMES[t][theme][ 71 - key as keyof (typeof THEMES)[typeof t][typeof theme] 72 - ]; 73 - if (value) { 74 - element.style.setProperty(key, value as string); 75 - } 76 - }); 69 + const themeStyles = document.getElementById("theme-styles"); 70 + if (t && themeStyles) { 71 + themeStyles.innerHTML = generateThemeStyles(t); 77 72 } 78 - }, [resolvedTheme, t]); 73 + }, [t]); 79 74 80 75 return ( 81 76 <SectionGroup> ··· 133 128 const style = mounted 134 129 ? theme[resolvedTheme as "dark" | "light"] 135 130 : undefined; 131 + 136 132 return ( 137 133 <li key={k} className="group/theme-card space-y-1.5"> 138 134 <div ··· 149 145 } 150 146 }} 151 147 > 152 - <div 153 - className="absolute h-full w-full bg-background text-foreground" 154 - style={style as React.CSSProperties} 155 - inert 156 - > 157 - <ThemePlaygroundMonitor className="pointer-events-none scale-80" /> 158 - </div> 148 + {mounted ? ( 149 + <div 150 + className="absolute h-full w-full bg-background text-foreground" 151 + style={style as React.CSSProperties} 152 + inert 153 + > 154 + <ThemePlaygroundMonitor className="pointer-events-none scale-80" /> 155 + </div> 156 + ) : ( 157 + <Skeleton className="absolute h-full w-full" /> 158 + )} 159 159 </div> 160 160 <div className="flex items-start justify-between gap-2"> 161 161 <div className="space-y-0.5"> ··· 178 178 const backgroundColor = style 179 179 ? style[color.key] 180 180 : undefined; 181 + 182 + if (!mounted) { 183 + return ( 184 + <Skeleton 185 + key={color.key} 186 + className="size-3.5 rounded-sm" 187 + /> 188 + ); 189 + } 181 190 return ( 182 191 <TooltipProvider key={color.key}> 183 192 <Tooltip>
+6
apps/status-page/src/app/(public)/layout.tsx
··· 1 1 import { Link } from "@/components/common/link"; 2 2 import { ThemeProvider } from "@/components/themes/theme-provider"; 3 3 import { Toaster } from "@/components/ui/sonner"; 4 + import { generateThemeStyles } from "@openstatus/theme-store"; 4 5 import PlausibleProvider from "next-plausible"; 5 6 6 7 export default async function Layout({ ··· 10 11 }) { 11 12 return ( 12 13 <PlausibleProvider domain="themes.openstatus.dev"> 14 + <style 15 + id="theme-styles" 16 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 17 + dangerouslySetInnerHTML={{ __html: generateThemeStyles() }} 18 + /> 13 19 <ThemeProvider attribute="class" enableSystem disableTransitionOnChange> 14 20 <main>{children}</main> 15 21 <footer className="flex items-center justify-center gap-4 p-4 text-center font-mono text-muted-foreground text-sm">
+14 -1
apps/status-page/src/app/(status-page)/[domain]/layout.tsx
··· 7 7 import { ThemeProvider } from "@/components/themes/theme-provider"; 8 8 import { Toaster } from "@/components/ui/sonner"; 9 9 import { HydrateClient, getQueryClient, trpc } from "@/lib/trpc/server"; 10 + import { 11 + THEME_KEYS, 12 + type ThemeKey, 13 + generateThemeStyles, 14 + } from "@openstatus/theme-store"; 10 15 import type { Metadata } from "next"; 11 16 import { notFound } from "next/navigation"; 12 17 import { z } from "zod"; ··· 15 20 value: z.enum(["duration", "requests", "manual"]).default("duration"), 16 21 type: z.enum(["absolute", "manual"]).default("absolute"), 17 22 uptime: z.coerce.boolean().default(true), 18 - theme: z.enum(["default"]).default("default"), 23 + theme: z.enum(THEME_KEYS as [ThemeKey, ...ThemeKey[]]).default("default"), 19 24 }); 20 25 21 26 export default async function Layout({ ··· 32 37 ); 33 38 34 39 const validation = schema.safeParse(page?.configuration); 40 + const communityTheme = validation.data?.theme; 35 41 36 42 return ( 37 43 <HydrateClient> 44 + <style 45 + id="theme-styles" 46 + // biome-ignore lint/security/noDangerouslySetInnerHtml: <explanation> 47 + dangerouslySetInnerHTML={{ 48 + __html: generateThemeStyles(communityTheme), 49 + }} 50 + /> 38 51 <ThemeProvider 39 52 attribute="class" 40 53 defaultTheme={page?.forceTheme ?? "system"}
+10 -71
apps/status-page/src/components/status-page/floating-button.tsx
··· 25 25 } from "@/components/ui/select"; 26 26 import { Separator } from "@/components/ui/separator"; 27 27 import { cn } from "@/lib/utils"; 28 - import { THEMES, THEME_KEYS } from "@openstatus/theme-store"; 28 + import { 29 + THEMES, 30 + THEME_KEYS, 31 + generateThemeStyles, 32 + } from "@openstatus/theme-store"; 29 33 import { Check, ChevronsUpDown, Settings } from "lucide-react"; 30 - import { useTheme } from "next-themes"; 31 34 import { parseAsString, useQueryState } from "nuqs"; 32 35 import type React from "react"; 33 36 import { createContext, useContext, useEffect, useState } from "react"; ··· 46 49 export const COMMUNITY_THEME = THEME_KEYS; 47 50 export type CommunityTheme = (typeof COMMUNITY_THEME)[number]; 48 51 49 - export const RADIUS = ["square", "rounded"] as const; 50 - export type Radius = (typeof RADIUS)[number]; 51 52 interface StatusPageContextType { 52 53 cardType: CardType; 53 54 setCardType: (cardType: CardType) => void; ··· 57 58 setShowUptime: (showUptime: boolean) => void; 58 59 communityTheme: CommunityTheme; 59 60 setCommunityTheme: (communityTheme: CommunityTheme) => void; 60 - radius: Radius; 61 - setRadius: (radius: Radius) => void; 62 61 } 63 62 64 63 const StatusPageContext = createContext<StatusPageContextType | null>(null); ··· 77 76 defaultBarType = "absolute", 78 77 defaultShowUptime = true, 79 78 defaultCommunityTheme = "default", 80 - defaultRadius = "square", 81 79 }: { 82 80 children: React.ReactNode; 83 81 defaultCardType?: CardType; 84 82 defaultBarType?: BarType; 85 83 defaultShowUptime?: boolean; 86 84 defaultCommunityTheme?: CommunityTheme; 87 - defaultRadius?: Radius; 88 85 }) { 89 86 const [cardType, setCardType] = useState<CardType>(defaultCardType); 90 87 const [barType, setBarType] = useState<BarType>(defaultBarType); 91 88 const [showUptime, setShowUptime] = useState<boolean>(defaultShowUptime); 92 - const [radius, setRadius] = useState<Radius>(defaultRadius); 93 - const { resolvedTheme } = useTheme(); 94 89 const [communityTheme, setCommunityTheme] = useState<CommunityTheme>( 95 90 defaultCommunityTheme, 96 91 ); 97 92 98 93 useEffect(() => { 99 - const theme = resolvedTheme as "dark" | "light"; 100 - if (["dark", "light"].includes(theme)) { 101 - const element = document.documentElement; 102 - element.removeAttribute("style"); // reset the style 103 - Object.keys(THEMES[communityTheme][theme]).forEach((key) => { 104 - const value = 105 - THEMES[communityTheme][theme][ 106 - key as keyof (typeof THEMES)[typeof communityTheme][typeof theme] 107 - ]; 108 - console.log(key, value); 109 - if (value) { 110 - element.style.setProperty(key, value as string); 111 - } 112 - }); 94 + const themeStyles = document.getElementById("theme-styles"); 95 + if (themeStyles) { 96 + themeStyles.innerHTML = generateThemeStyles(communityTheme); 113 97 } 114 - }, [resolvedTheme, communityTheme]); 115 - 116 - useEffect(() => { 117 - const computedRadius = getComputedStyle( 118 - document.documentElement, 119 - ).getPropertyValue("--radius"); 120 - if (radius === "square" && computedRadius !== "0rem") { 121 - document.documentElement.style.setProperty("--radius", "0rem"); 122 - } else if (radius === "rounded" && computedRadius !== "0.625rem") { 123 - document.documentElement.style.setProperty("--radius", "0.625rem"); 124 - } 125 - }, [radius]); 98 + }, [communityTheme]); 126 99 127 100 return ( 128 101 <StatusPageContext.Provider ··· 135 108 setShowUptime, 136 109 communityTheme, 137 110 setCommunityTheme, 138 - radius, 139 - setRadius, 140 111 }} 141 112 > 142 - <div 143 - style={ 144 - communityTheme 145 - ? (THEMES[communityTheme][ 146 - resolvedTheme as "dark" | "light" 147 - ] as React.CSSProperties) 148 - : undefined 149 - } 150 - > 151 - {children} 152 - </div> 113 + {children} 153 114 </StatusPageContext.Provider> 154 115 ); 155 116 } ··· 172 133 setShowUptime, 173 134 communityTheme, 174 135 setCommunityTheme, 175 - radius, 176 - setRadius, 177 136 } = useStatusPage(); 178 137 const [display, setDisplay] = useState(false); 179 138 const [configToken, setConfigToken] = useQueryState( ··· 294 253 </SelectContent> 295 254 </Select> 296 255 </div> 297 - {IS_DEV ? ( 298 - <div className="space-y-2"> 299 - <Label htmlFor="radius">Radius</Label> 300 - <Select 301 - value={radius} 302 - onValueChange={(v) => setRadius(v as Radius)} 303 - > 304 - <SelectTrigger id="radius" className="w-full capitalize"> 305 - <SelectValue /> 306 - </SelectTrigger> 307 - <SelectContent> 308 - {RADIUS.map((v) => ( 309 - <SelectItem key={v} value={v} className="capitalize"> 310 - {v} 311 - </SelectItem> 312 - ))} 313 - </SelectContent> 314 - </Select> 315 - </div> 316 - ) : null} 317 256 {IS_DEV ? ( 318 257 <div className="space-y-2"> 319 258 <Label htmlFor="theme">Theme</Label>
+19
packages/theme-store/src/index.ts
··· 19 19 20 20 export const THEME_KEYS = THEMES_LIST.map((theme) => theme.id); 21 21 export type ThemeKey = (typeof THEME_KEYS)[number]; 22 + 23 + export function generateThemeStyles(themeKey: ThemeKey = "default") { 24 + const theme = THEMES[themeKey]; 25 + const lightVars = Object.entries(theme.light) 26 + .map(([key, value]) => `${key}: ${value};`) 27 + .join("\n "); 28 + const darkVars = Object.entries(theme.dark) 29 + .map(([key, value]) => `${key}: ${value};`) 30 + .join("\n "); 31 + 32 + return ` 33 + :root { 34 + ${lightVars} 35 + } 36 + .dark { 37 + ${darkVars} 38 + } 39 + `; 40 + }