feat: auto system theme

This commit is contained in:
2026-05-16 11:53:38 +07:00
parent b00d9111a9
commit ff6afe5391
4 changed files with 25 additions and 10 deletions
+2 -2
View File
@@ -4,7 +4,7 @@ import React from "react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { MousePointer2, Hand, ScanFace, Type, ImagePlus } from "lucide-react"; import { MousePointer2, Hand, ScanFace, Type, ImagePlus } from "lucide-react";
import { useEditorStore } from "../../../store/editor-store"; import { useEditorStore } from "../../../store/editor-store";
import { useUiStore } from "../../../store/ui-store"; import { useUiStore, isDarkTheme } from "../../../store/ui-store";
import { CanvasSizeModal } from "../../../components/canvas-size-modal"; import { CanvasSizeModal } from "../../../components/canvas-size-modal";
import { EditorCanvasStage } from "../../../components/editor/editor-canvas-stage"; import { EditorCanvasStage } from "../../../components/editor/editor-canvas-stage";
import { EditorHeader } from "../../../components/editor/editor-header"; import { EditorHeader } from "../../../components/editor/editor-header";
@@ -148,7 +148,7 @@ export default function EditorPage() {
t, t,
}); });
const isDark = theme === "dark"; const isDark = isDarkTheme(theme);
const { width: cw, height: ch } = project.canvas; const { width: cw, height: ch } = project.canvas;
const handleSave = React.useCallback(async () => { const handleSave = React.useCallback(async () => {
+2 -2
View File
@@ -6,7 +6,7 @@ import { addLayer, createLayer, createProject, parseProjectFile, setCanvasSize }
import type { Project } from "@pien-studio/types"; import type { Project } from "@pien-studio/types";
import { deleteProject, duplicateProject, loadProjects, upsertProject } from "@pien-studio/storage"; import { deleteProject, duplicateProject, loadProjects, upsertProject } from "@pien-studio/storage";
import { useEditorStore } from "../store/editor-store"; import { useEditorStore } from "../store/editor-store";
import { useUiStore } from "../store/ui-store"; import { useUiStore, isDarkTheme } from "../store/ui-store";
import { accentButtonClass, cx, mutedSurfaceClass, subtleButtonClass, surfaceClass } from "../lib/theme"; import { accentButtonClass, cx, mutedSurfaceClass, subtleButtonClass, surfaceClass } from "../lib/theme";
import { UiPreferences } from "../components/ui-preferences"; import { UiPreferences } from "../components/ui-preferences";
import { useAssetCleanupJob } from "../hooks/use-asset-cleanup-job"; import { useAssetCleanupJob } from "../hooks/use-asset-cleanup-job";
@@ -23,7 +23,7 @@ export default function HomePage() {
const [projectPendingDelete, setProjectPendingDelete] = React.useState<Project | null>(null); const [projectPendingDelete, setProjectPendingDelete] = React.useState<Project | null>(null);
const projectInputRef = React.useRef<HTMLInputElement | null>(null); const projectInputRef = React.useRef<HTMLInputElement | null>(null);
const imageInputRef = React.useRef<HTMLInputElement | null>(null); const imageInputRef = React.useRef<HTMLInputElement | null>(null);
const isDark = theme === "dark"; const isDark = isDarkTheme(theme);
const refreshProjects = React.useCallback(async () => { const refreshProjects = React.useCallback(async () => {
setProjects(await loadProjects()); setProjects(await loadProjects());
+7 -3
View File
@@ -2,13 +2,14 @@
import React from "react"; import React from "react";
import { useUiStore } from "../store/ui-store"; import { useUiStore } from "../store/ui-store";
import { isDarkTheme } from "../store/ui-store";
import { cx, subtleButtonClass } from "../lib/theme"; import { cx, subtleButtonClass } from "../lib/theme";
import { useTranslations } from "../hooks/use-translations"; import { useTranslations } from "../hooks/use-translations";
export function UiPreferences({ compact = false }: { compact?: boolean }) { export function UiPreferences({ compact = false }: { compact?: boolean }) {
const { theme, locale, setTheme, setLocale } = useUiStore((s) => s); const { theme, locale, setTheme, setLocale } = useUiStore((s) => s);
const { t } = useTranslations(); const { t } = useTranslations();
const isDark = theme === "dark"; const isDark = isDarkTheme(theme);
const wrapperHeight = compact ? "h-7" : "h-9"; const wrapperHeight = compact ? "h-7" : "h-9";
const textSize = compact ? "text-[10px]" : "text-xs"; const textSize = compact ? "text-[10px]" : "text-xs";
@@ -33,10 +34,13 @@ export function UiPreferences({ compact = false }: { compact?: boolean }) {
</label> </label>
<button <button
type="button" type="button"
onClick={() => setTheme(theme === "dark" ? "light" : "dark")} onClick={() => {
const next = theme === "light" ? "dark" : theme === "dark" ? "system" : "light";
setTheme(next);
}}
className={cx("rounded border px-3 font-semibold", subtleButtonClass(isDark), wrapperHeight, textSize)} className={cx("rounded border px-3 font-semibold", subtleButtonClass(isDark), wrapperHeight, textSize)}
> >
{theme === "dark" ? t("ui.darkMode") : t("ui.lightMode")} {theme === "dark" ? t("ui.darkMode") : theme === "system" ? t("ui.systemMode") : t("ui.lightMode")}
</button> </button>
</div> </div>
); );
+14 -3
View File
@@ -2,16 +2,22 @@
import { create } from "zustand"; import { create } from "zustand";
export type AppTheme = "light" | "dark"; export type AppTheme = "light" | "dark" | "system";
export type AppLocale = "en" | "th" | "ja"; export type AppLocale = "en" | "th" | "ja";
const THEME_KEY = "pien.ui.theme"; const THEME_KEY = "pien.ui.theme";
const LOCALE_KEY = "pien.ui.locale"; const LOCALE_KEY = "pien.ui.locale";
function getSystemTheme(): "light" | "dark" {
if (typeof window === "undefined") return "light";
return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
}
function readTheme(): AppTheme { function readTheme(): AppTheme {
if (typeof window === "undefined") return "light"; if (typeof window === "undefined") return "light";
const value = window.localStorage.getItem(THEME_KEY); const value = window.localStorage.getItem(THEME_KEY);
return value === "light" ? "light" : "dark"; if (value === "light" || value === "dark" || value === "system") return value;
return "system";
} }
function readLocale(): AppLocale { function readLocale(): AppLocale {
@@ -23,7 +29,12 @@ function readLocale(): AppLocale {
function applyTheme(theme: AppTheme): void { function applyTheme(theme: AppTheme): void {
if (typeof document === "undefined") return; if (typeof document === "undefined") return;
document.documentElement.setAttribute("data-theme", theme); const resolved = theme === "system" ? getSystemTheme() : theme;
document.documentElement.setAttribute("data-theme", resolved);
}
export function isDarkTheme(theme: AppTheme): boolean {
return theme === "system" ? getSystemTheme() === "dark" : theme === "dark";
} }
type UiState = { type UiState = {