From ff6afe5391abd16c5e3a5bdb5a5f9d0e067306f1 Mon Sep 17 00:00:00 2001 From: Yuzu Date: Sat, 16 May 2026 11:50:53 +0700 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20auto=20system=20theme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/web/app/editor/[projectId]/page.tsx | 4 ++-- apps/web/app/page.tsx | 4 ++-- apps/web/components/ui-preferences.tsx | 10 +++++++--- apps/web/store/ui-store.ts | 17 ++++++++++++++--- 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/apps/web/app/editor/[projectId]/page.tsx b/apps/web/app/editor/[projectId]/page.tsx index 0edc0ba..632f580 100644 --- a/apps/web/app/editor/[projectId]/page.tsx +++ b/apps/web/app/editor/[projectId]/page.tsx @@ -4,7 +4,7 @@ import React from "react"; import { useParams } from "next/navigation"; import { MousePointer2, Hand, ScanFace, Type, ImagePlus } from "lucide-react"; 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 { EditorCanvasStage } from "../../../components/editor/editor-canvas-stage"; import { EditorHeader } from "../../../components/editor/editor-header"; @@ -148,7 +148,7 @@ export default function EditorPage() { t, }); - const isDark = theme === "dark"; + const isDark = isDarkTheme(theme); const { width: cw, height: ch } = project.canvas; const handleSave = React.useCallback(async () => { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 4e20cc8..bbcaf3a 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -6,7 +6,7 @@ import { addLayer, createLayer, createProject, parseProjectFile, setCanvasSize } import type { Project } from "@pien-studio/types"; import { deleteProject, duplicateProject, loadProjects, upsertProject } from "@pien-studio/storage"; 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 { UiPreferences } from "../components/ui-preferences"; import { useAssetCleanupJob } from "../hooks/use-asset-cleanup-job"; @@ -23,7 +23,7 @@ export default function HomePage() { const [projectPendingDelete, setProjectPendingDelete] = React.useState(null); const projectInputRef = React.useRef(null); const imageInputRef = React.useRef(null); - const isDark = theme === "dark"; + const isDark = isDarkTheme(theme); const refreshProjects = React.useCallback(async () => { setProjects(await loadProjects()); diff --git a/apps/web/components/ui-preferences.tsx b/apps/web/components/ui-preferences.tsx index 811b777..5f9e527 100644 --- a/apps/web/components/ui-preferences.tsx +++ b/apps/web/components/ui-preferences.tsx @@ -2,13 +2,14 @@ import React from "react"; import { useUiStore } from "../store/ui-store"; +import { isDarkTheme } from "../store/ui-store"; import { cx, subtleButtonClass } from "../lib/theme"; import { useTranslations } from "../hooks/use-translations"; export function UiPreferences({ compact = false }: { compact?: boolean }) { const { theme, locale, setTheme, setLocale } = useUiStore((s) => s); const { t } = useTranslations(); - const isDark = theme === "dark"; + const isDark = isDarkTheme(theme); const wrapperHeight = compact ? "h-7" : "h-9"; const textSize = compact ? "text-[10px]" : "text-xs"; @@ -33,10 +34,13 @@ export function UiPreferences({ compact = false }: { compact?: boolean }) { ); diff --git a/apps/web/store/ui-store.ts b/apps/web/store/ui-store.ts index 543d925..2970808 100644 --- a/apps/web/store/ui-store.ts +++ b/apps/web/store/ui-store.ts @@ -2,16 +2,22 @@ import { create } from "zustand"; -export type AppTheme = "light" | "dark"; +export type AppTheme = "light" | "dark" | "system"; export type AppLocale = "en" | "th" | "ja"; const THEME_KEY = "pien.ui.theme"; 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 { if (typeof window === "undefined") return "light"; 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 { @@ -23,7 +29,12 @@ function readLocale(): AppLocale { function applyTheme(theme: AppTheme): void { 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 = {