mirror of
https://github.com/YuzuZensai/Pien-Studio.git
synced 2026-09-02 14:18:35 +00:00
✨ feat: auto system theme
This commit is contained in:
@@ -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 () => {
|
||||||
|
|||||||
@@ -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());
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
Reference in New Issue
Block a user