Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d19ddfd96 | |||
| 79a6567530 |
@@ -3,6 +3,8 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
<title>Collection</title>
|
||||||
<script>
|
<script>
|
||||||
try {
|
try {
|
||||||
@@ -12,6 +14,9 @@
|
|||||||
(t === "system" &&
|
(t === "system" &&
|
||||||
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
||||||
document.documentElement.classList.toggle("dark", dark);
|
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) {}
|
} catch (e) {}
|
||||||
</script>
|
</script>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { join, relative } from "node:path";
|
|||||||
const root = "src";
|
const root = "src";
|
||||||
const excludeDir = join("src", "components", "ui");
|
const excludeDir = join("src", "components", "ui");
|
||||||
const RAW_COLOR =
|
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) {
|
function walk(dir) {
|
||||||
const files = [];
|
const files = [];
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ function ComboboxClear({ className, ...props }: ComboboxPrimitive.Clear.Props) {
|
|||||||
<ComboboxPrimitive.Clear
|
<ComboboxPrimitive.Clear
|
||||||
data-slot="combobox-clear"
|
data-slot="combobox-clear"
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute right-6 text-neutral-400 hover:text-neutral-700",
|
"absolute right-6 text-muted-foreground hover:text-foreground",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -43,7 +43,7 @@ function ComboboxTrigger({ className, ...props }: ComboboxPrimitive.Trigger.Prop
|
|||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Trigger
|
<ComboboxPrimitive.Trigger
|
||||||
data-slot="combobox-trigger"
|
data-slot="combobox-trigger"
|
||||||
className={cn("absolute right-1 text-neutral-500", className)}
|
className={cn("absolute right-1 text-muted-foreground", className)}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -56,7 +56,7 @@ function ComboboxPopup({ className, ...props }: ComboboxPrimitive.Popup.Props) {
|
|||||||
<ComboboxPrimitive.Popup
|
<ComboboxPrimitive.Popup
|
||||||
data-slot="combobox-popup"
|
data-slot="combobox-popup"
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -81,7 +81,7 @@ function ComboboxItem({ className, ...props }: ComboboxPrimitive.Item.Props) {
|
|||||||
<ComboboxPrimitive.Item
|
<ComboboxPrimitive.Item
|
||||||
data-slot="combobox-item"
|
data-slot="combobox-item"
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -103,7 +103,7 @@ function ComboboxEmpty({ className, ...props }: ComboboxPrimitive.Empty.Props) {
|
|||||||
return (
|
return (
|
||||||
<ComboboxPrimitive.Empty
|
<ComboboxPrimitive.Empty
|
||||||
data-slot="combobox-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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type * as React from "react";
|
import type * as React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Toast as ToastPrimitive } from "@base-ui/react/toast";
|
import { Toast as ToastPrimitive } from "@base-ui/react/toast";
|
||||||
|
import { X } from "lucide-react";
|
||||||
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { toastManager } from "@/toast/toast-manager";
|
import { toastManager } from "@/toast/toast-manager";
|
||||||
@@ -14,9 +15,9 @@ function ToastList() {
|
|||||||
toast={toast}
|
toast={toast}
|
||||||
data-slot="toast"
|
data-slot="toast"
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex items-start gap-2 rounded-md border bg-white p-3 text-sm shadow-md",
|
"flex items-start gap-2 rounded-md border bg-popover p-3 text-sm text-popover-foreground shadow-md",
|
||||||
toast.type === "error" && "border-red-300",
|
toast.type === "error" && "border-destructive",
|
||||||
toast.type === "success" && "border-green-300",
|
toast.type === "success" && "border-success",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
@@ -28,15 +29,15 @@ function ToastList() {
|
|||||||
)}
|
)}
|
||||||
<ToastPrimitive.Description
|
<ToastPrimitive.Description
|
||||||
data-slot="toast-description"
|
data-slot="toast-description"
|
||||||
className="text-neutral-700"
|
className="text-muted-foreground"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ToastPrimitive.Close
|
<ToastPrimitive.Close
|
||||||
data-slot="toast-close"
|
data-slot="toast-close"
|
||||||
aria-label={t("common.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.Close>
|
||||||
</ToastPrimitive.Root>
|
</ToastPrimitive.Root>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ function TooltipPopup({ className, ...props }: TooltipPrimitive.Popup.Props) {
|
|||||||
<TooltipPrimitive.Popup
|
<TooltipPrimitive.Popup
|
||||||
data-slot="tooltip-popup"
|
data-slot="tooltip-popup"
|
||||||
className={cn(
|
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,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
|||||||
@@ -35,6 +35,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
|
color-scheme: light;
|
||||||
--background: oklch(1 0 0);
|
--background: oklch(1 0 0);
|
||||||
--foreground: oklch(0.145 0 0);
|
--foreground: oklch(0.145 0 0);
|
||||||
--card: oklch(1 0 0);
|
--card: oklch(1 0 0);
|
||||||
@@ -63,6 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
--background: oklch(0.145 0 0);
|
--background: oklch(0.145 0 0);
|
||||||
--foreground: oklch(0.985 0 0);
|
--foreground: oklch(0.985 0 0);
|
||||||
--card: oklch(0.205 0 0);
|
--card: oklch(0.205 0 0);
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export function ObjectsTable() {
|
|||||||
value={limit}
|
value={limit}
|
||||||
onChange={(event) => setLimit(Number(event.target.value))}
|
onChange={(event) => setLimit(Number(event.target.value))}
|
||||||
aria-label={t("objects.pageSize")}
|
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) => (
|
{PAGE_SIZES.map((size) => (
|
||||||
<option key={size} value={size}>
|
<option key={size} value={size}>
|
||||||
|
|||||||
@@ -40,6 +40,21 @@ test("readTheme defaults to system when unset or invalid", () => {
|
|||||||
expect(readTheme()).toBe("dark");
|
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", () => {
|
test("applyTheme toggles the dark class on documentElement", () => {
|
||||||
mockMatchMedia(false);
|
mockMatchMedia(false);
|
||||||
applyTheme("dark");
|
applyTheme("dark");
|
||||||
|
|||||||
@@ -26,8 +26,16 @@ export function readTheme(): Theme {
|
|||||||
return THEMES.includes(stored as Theme) ? (stored as Theme) : "system";
|
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 {
|
export function applyTheme(theme: Theme): void {
|
||||||
if (typeof document === "undefined") return;
|
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