merge: dark-mode tokens for popup primitives + theme-color/color-scheme sync (#68)
CI / web (push) Successful in 5m47s
CI / web (push) Successful in 5m47s
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="color-scheme" content="light dark" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<title>Collection</title>
|
||||
<script>
|
||||
try {
|
||||
@@ -12,6 +14,9 @@
|
||||
(t === "system" &&
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||
document.documentElement.classList.toggle("dark", dark);
|
||||
// Keep in sync with THEME_COLORS in src/theme/theme.ts.
|
||||
var meta = document.querySelector('meta[name="theme-color"]');
|
||||
if (meta) meta.setAttribute("content", dark ? "#0a0a0a" : "#ffffff");
|
||||
} catch (e) {}
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { join, relative } from "node:path";
|
||||
const root = "src";
|
||||
const excludeDir = join("src", "components", "ui");
|
||||
const RAW_COLOR =
|
||||
/(?:text|bg|border|ring|fill|stroke|from|to|via|decoration|outline|divide|placeholder)-(?:neutral|gray|slate|zinc|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950)\b/;
|
||||
/(?:text|bg|border|ring|fill|stroke|from|to|via|decoration|outline|divide|placeholder)-(?:(?:neutral|gray|slate|zinc|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(?:50|100|200|300|400|500|600|700|800|900|950)|white|black)\b/;
|
||||
|
||||
function walk(dir) {
|
||||
const files = [];
|
||||
|
||||
@@ -31,7 +31,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
||||
<ComboboxPrimitive.Clear
|
||||
data-slot="combobox-clear"
|
||||
className={cn(
|
||||
"absolute right-6 text-neutral-400 hover:text-neutral-700",
|
||||
"absolute right-6 text-muted-foreground hover:text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -43,7 +43,7 @@ function ComboboxTrigger({ className, ...props }: ComboboxPrimitive.Trigger.Prop
|
||||
return (
|
||||
<ComboboxPrimitive.Trigger
|
||||
data-slot="combobox-trigger"
|
||||
className={cn("absolute right-1 text-neutral-500", className)}
|
||||
className={cn("absolute right-1 text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -56,7 +56,7 @@ function ComboboxPopup({ className, ...props }: ComboboxPrimitive.Popup.Props) {
|
||||
<ComboboxPrimitive.Popup
|
||||
data-slot="combobox-popup"
|
||||
className={cn(
|
||||
"max-h-64 min-w-48 overflow-auto rounded border bg-white p-1 text-sm shadow-md",
|
||||
"max-h-64 min-w-48 overflow-auto rounded border bg-popover p-1 text-sm text-popover-foreground shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -81,7 +81,7 @@ function ComboboxItem({ className, ...props }: ComboboxPrimitive.Item.Props) {
|
||||
<ComboboxPrimitive.Item
|
||||
data-slot="combobox-item"
|
||||
className={cn(
|
||||
"flex cursor-default items-center gap-2 rounded px-2 py-1 data-[highlighted]:bg-indigo-50",
|
||||
"flex cursor-default items-center gap-2 rounded px-2 py-1 data-[highlighted]:bg-accent data-[highlighted]:text-accent-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
@@ -103,7 +103,7 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
||||
return (
|
||||
<ComboboxPrimitive.Empty
|
||||
data-slot="combobox-empty"
|
||||
className={cn("px-2 py-1 text-neutral-500", className)}
|
||||
className={cn("px-2 py-1 text-muted-foreground", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Toast as ToastPrimitive } from "@base-ui/react/toast";
|
||||
import { X } from "lucide-react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toastManager } from "@/toast/toast-manager";
|
||||
@@ -14,9 +15,9 @@ function ToastList() {
|
||||
toast={toast}
|
||||
data-slot="toast"
|
||||
className={cn(
|
||||
"flex items-start gap-2 rounded-md border bg-white p-3 text-sm shadow-md",
|
||||
toast.type === "error" && "border-red-300",
|
||||
toast.type === "success" && "border-green-300",
|
||||
"flex items-start gap-2 rounded-md border bg-popover p-3 text-sm text-popover-foreground shadow-md",
|
||||
toast.type === "error" && "border-destructive",
|
||||
toast.type === "success" && "border-success",
|
||||
)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
@@ -28,15 +29,15 @@ function ToastList() {
|
||||
)}
|
||||
<ToastPrimitive.Description
|
||||
data-slot="toast-description"
|
||||
className="text-neutral-700"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<ToastPrimitive.Close
|
||||
data-slot="toast-close"
|
||||
aria-label={t("common.close")}
|
||||
className="text-neutral-400 hover:text-neutral-700"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
×
|
||||
<X className="size-4" aria-hidden="true" />
|
||||
</ToastPrimitive.Close>
|
||||
</ToastPrimitive.Root>
|
||||
));
|
||||
|
||||
@@ -22,7 +22,7 @@ function TooltipPopup({ className, ...props }: TooltipPrimitive.Popup.Props) {
|
||||
<TooltipPrimitive.Popup
|
||||
data-slot="tooltip-popup"
|
||||
className={cn(
|
||||
"rounded border bg-white px-2 py-1 text-sm shadow-md",
|
||||
"rounded border bg-popover px-2 py-1 text-sm text-popover-foreground shadow-md",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
}
|
||||
|
||||
:root {
|
||||
color-scheme: light;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.145 0 0);
|
||||
--card: oklch(1 0 0);
|
||||
@@ -63,6 +64,7 @@
|
||||
}
|
||||
|
||||
.dark {
|
||||
color-scheme: dark;
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
|
||||
@@ -287,7 +287,7 @@ export function ObjectsTable() {
|
||||
value={limit}
|
||||
onChange={(event) => setLimit(Number(event.target.value))}
|
||||
aria-label={t("objects.pageSize")}
|
||||
className="rounded-md border bg-white px-1 py-0.5"
|
||||
className="rounded-md border bg-background px-1 py-0.5"
|
||||
>
|
||||
{PAGE_SIZES.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
|
||||
@@ -40,6 +40,21 @@ test("readTheme defaults to system when unset or invalid", () => {
|
||||
expect(readTheme()).toBe("dark");
|
||||
});
|
||||
|
||||
test("applyTheme syncs the theme-color meta when present", () => {
|
||||
const meta = document.createElement("meta");
|
||||
meta.setAttribute("name", "theme-color");
|
||||
document.head.appendChild(meta);
|
||||
try {
|
||||
mockMatchMedia(false);
|
||||
applyTheme("dark");
|
||||
expect(meta.getAttribute("content")).toBe("#0a0a0a");
|
||||
applyTheme("light");
|
||||
expect(meta.getAttribute("content")).toBe("#ffffff");
|
||||
} finally {
|
||||
meta.remove();
|
||||
}
|
||||
});
|
||||
|
||||
test("applyTheme toggles the dark class on documentElement", () => {
|
||||
mockMatchMedia(false);
|
||||
applyTheme("dark");
|
||||
|
||||
@@ -26,8 +26,16 @@ export function readTheme(): Theme {
|
||||
return THEMES.includes(stored as Theme) ? (stored as Theme) : "system";
|
||||
}
|
||||
|
||||
/** Browser-chrome colors per resolved theme; must match `--background` in index.css. */
|
||||
const THEME_COLORS = { light: "#ffffff", dark: "#0a0a0a" } as const;
|
||||
|
||||
export function applyTheme(theme: Theme): void {
|
||||
if (typeof document === "undefined") return;
|
||||
|
||||
document.documentElement.classList.toggle("dark", resolveTheme(theme) === "dark");
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
document.documentElement.classList.toggle("dark", resolved === "dark");
|
||||
document
|
||||
.querySelector('meta[name="theme-color"]')
|
||||
?.setAttribute("content", THEME_COLORS[resolved]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user