@robot-admin/theme 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,167 @@
1
+ # @robot-admin/theme
2
+
3
+ > 主题切换和管理系统 - 为 Robot Admin 提供完整的主题管理能力
4
+
5
+ ## 特性
6
+
7
+ - 🌓 **多模式支持** - Light / Dark / System 三种主题模式
8
+ - 🎨 **View Transition API** - 丝滑流畅的主题切换动画
9
+ - 💾 **持久化存储** - 自动保存用户的主题偏好
10
+ - 🔧 **高度可配置** - 灵活的配置选项
11
+ - 📦 **零 UI 依赖** - 纯逻辑包,UI 由业务方实现
12
+ - 🚀 **TypeScript** - 完整的类型支持
13
+
14
+ ## 安装
15
+
16
+ ```bash
17
+ npm install @robot-admin/theme
18
+ # or
19
+ pnpm add @robot-admin/theme
20
+ # or
21
+ bun add @robot-admin/theme
22
+ ```
23
+
24
+ ## 快速开始
25
+
26
+ ### 1. 初始化 Store
27
+
28
+ ```typescript
29
+ import { createApp } from "vue";
30
+ import { createPinia } from "pinia";
31
+ import { useThemeStore } from "@robot-admin/theme";
32
+ import App from "./App.vue";
33
+
34
+ const app = createApp(App);
35
+ const pinia = createPinia();
36
+ app.use(pinia);
37
+
38
+ // 初始化主题系统
39
+ const themeStore = useThemeStore();
40
+ themeStore.init();
41
+
42
+ app.mount("#app");
43
+ ```
44
+
45
+ ### 2. 在组件中使用
46
+
47
+ ```vue
48
+ <template>
49
+ <button @click="toggleTheme">
50
+ <span v-if="themeStore.isDark">🌙 深色</span>
51
+ <span v-else>☀️ 浅色</span>
52
+ </button>
53
+ </template>
54
+
55
+ <script setup lang="ts">
56
+ import { useThemeStore } from "@robot-admin/theme";
57
+
58
+ const themeStore = useThemeStore();
59
+
60
+ const toggleTheme = () => {
61
+ themeStore.toggleMode(); // 循环切换 light -> dark -> system
62
+ };
63
+ </script>
64
+ ```
65
+
66
+ ## API 文档
67
+
68
+ ### Store
69
+
70
+ #### `createThemeStore(options?)`
71
+
72
+ 创建自定义配置的主题 Store
73
+
74
+ ```typescript
75
+ import { createThemeStore } from "@robot-admin/theme";
76
+
77
+ const useThemeStore = createThemeStore({
78
+ defaultMode: "dark", // 默认主题模式
79
+ storageKey: "my-theme-mode", // localStorage 键名
80
+ enableTransition: true, // 启用过渡动画
81
+ transitionDuration: 500, // 动画时长(毫秒)
82
+ });
83
+ ```
84
+
85
+ #### `useThemeStore()`
86
+
87
+ 获取默认的主题 Store 实例
88
+
89
+ ### Store 属性
90
+
91
+ - `mode` - 当前主题模式 (`'light'` | `'dark'` | `'system'`)
92
+ - `systemIsDark` - 系统是否为暗色模式
93
+ - `isDark` - 当前是否为暗色模式(计算属性)
94
+
95
+ ### Store 方法
96
+
97
+ - `init()` - 初始化主题系统(必须调用)
98
+ - `setMode(mode)` - 设置主题模式
99
+ - `toggleMode()` - 循环切换主题模式(light → dark → system)
100
+ - `toggleDark()` - 在 light 和 dark 之间切换
101
+
102
+ ### Composables
103
+
104
+ #### `useViewTransition(callback, options?)`
105
+
106
+ 使用 View Transition API 执行过渡动画
107
+
108
+ ```typescript
109
+ import { useViewTransition } from "@robot-admin/theme";
110
+
111
+ await useViewTransition(
112
+ () => {
113
+ // 执行 DOM 更新
114
+ document.body.classList.toggle("dark");
115
+ },
116
+ {
117
+ duration: 500,
118
+ transitioningClass: "theme-transitioning",
119
+ },
120
+ );
121
+ ```
122
+
123
+ #### `isViewTransitionSupported()`
124
+
125
+ 检查浏览器是否支持 View Transition API
126
+
127
+ ### 常量
128
+
129
+ - `DEFAULT_THEME_OPTIONS` - 默认配置选项
130
+ - `THEME_MODE_LABELS` - 主题模式显示文本
131
+ - `THEME_MODE_ICONS` - 主题模式图标类名
132
+
133
+ ## CSS 配置
134
+
135
+ 在你的全局样式中添加:
136
+
137
+ ```css
138
+ /* 定义 CSS 变量 */
139
+ [data-theme="light"] {
140
+ --bg-color: #ffffff;
141
+ --text-color: #333333;
142
+ }
143
+
144
+ [data-theme="dark"] {
145
+ --bg-color: #1a1a1a;
146
+ --text-color: #ffffff;
147
+ }
148
+
149
+ /* View Transition API 动画 */
150
+ ::view-transition-old(root),
151
+ ::view-transition-new(root) {
152
+ animation-duration: 0.5s;
153
+ }
154
+
155
+ /* 禁用冲突的 CSS transitions */
156
+ .theme-transitioning * {
157
+ transition: none !important;
158
+ }
159
+ ```
160
+
161
+ ## 完整示例
162
+
163
+ 查看 [Robot Admin](https://github.com/ChenyCHENYU/Robot_Admin) 项目以获取完整的使用示例。
164
+
165
+ ## License
166
+
167
+ MIT © ChenYu
package/dist/index.cjs ADDED
@@ -0,0 +1,135 @@
1
+ 'use strict';
2
+
3
+ var pinia = require('pinia');
4
+ var vue = require('vue');
5
+
6
+ // src/constants/index.ts
7
+ var DEFAULT_THEME_OPTIONS = {
8
+ defaultMode: "system",
9
+ storageKey: "theme-mode",
10
+ enableTransition: true,
11
+ transitionDuration: 500
12
+ };
13
+ var THEME_MODE_LABELS = {
14
+ light: "\u6D45\u8272",
15
+ dark: "\u6DF1\u8272",
16
+ system: "\u81EA\u52A8"
17
+ };
18
+ var THEME_MODE_ICONS = {
19
+ light: "i-mdi:white-balance-sunny",
20
+ dark: "i-mdi:moon-waning-crescent",
21
+ system: "i-mdi:theme-light-dark"
22
+ };
23
+
24
+ // src/composables/useViewTransition.ts
25
+ async function useViewTransition(callback, options = {}) {
26
+ const { transitioningClass = "theme-transitioning" } = options;
27
+ if (!document.startViewTransition) {
28
+ callback();
29
+ return;
30
+ }
31
+ const root = document.documentElement;
32
+ root.classList.add(transitioningClass);
33
+ try {
34
+ const transition = document.startViewTransition(callback);
35
+ await transition.finished;
36
+ } catch (error) {
37
+ console.debug("[ViewTransition] Transition interrupted:", error);
38
+ } finally {
39
+ root.classList.remove(transitioningClass);
40
+ }
41
+ }
42
+ function isViewTransitionSupported() {
43
+ return typeof document !== "undefined" && "startViewTransition" in document;
44
+ }
45
+ function createThemeStore(options = {}) {
46
+ const {
47
+ defaultMode = DEFAULT_THEME_OPTIONS.defaultMode,
48
+ storageKey = DEFAULT_THEME_OPTIONS.storageKey,
49
+ enableTransition = DEFAULT_THEME_OPTIONS.enableTransition,
50
+ transitionDuration = DEFAULT_THEME_OPTIONS.transitionDuration
51
+ } = options;
52
+ return pinia.defineStore("theme", () => {
53
+ const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
54
+ const savedMode = typeof window !== "undefined" ? localStorage.getItem(storageKey) : null;
55
+ const mode = vue.ref(savedMode || defaultMode);
56
+ const systemIsDark = vue.ref(mediaQuery?.matches ?? false);
57
+ const isDark = vue.computed(() => {
58
+ if (mode.value === "system") {
59
+ return systemIsDark.value;
60
+ }
61
+ return mode.value === "dark";
62
+ });
63
+ const syncThemeAttr = () => {
64
+ if (typeof document !== "undefined") {
65
+ const themeValue = isDark.value ? "dark" : "light";
66
+ document.documentElement.setAttribute("data-theme", themeValue);
67
+ }
68
+ };
69
+ const init = () => {
70
+ if (mediaQuery) {
71
+ systemIsDark.value = mediaQuery.matches;
72
+ }
73
+ syncThemeAttr();
74
+ if (mediaQuery) {
75
+ mediaQuery.addEventListener("change", (e) => {
76
+ systemIsDark.value = e.matches;
77
+ if (mode.value === "system") {
78
+ syncThemeAttr();
79
+ }
80
+ });
81
+ }
82
+ };
83
+ const setMode = async (newMode) => {
84
+ const oldDark = isDark.value;
85
+ mode.value = newMode;
86
+ if (typeof window !== "undefined") {
87
+ localStorage.setItem(storageKey, newMode);
88
+ }
89
+ const newDark = isDark.value;
90
+ if (oldDark === newDark) {
91
+ syncThemeAttr();
92
+ return;
93
+ }
94
+ if (enableTransition) {
95
+ await useViewTransition(syncThemeAttr, {
96
+ });
97
+ } else {
98
+ syncThemeAttr();
99
+ }
100
+ };
101
+ const toggleMode = async () => {
102
+ const modes = ["light", "dark", "system"];
103
+ const currentIndex = modes.indexOf(mode.value);
104
+ const nextIndex = (currentIndex + 1) % modes.length;
105
+ await setMode(modes[nextIndex]);
106
+ };
107
+ const toggleDark = async () => {
108
+ const newMode = mode.value === "dark" ? "light" : "dark";
109
+ await setMode(newMode);
110
+ };
111
+ return {
112
+ // State
113
+ mode,
114
+ systemIsDark,
115
+ // Getters
116
+ isDark,
117
+ // Actions
118
+ init,
119
+ setMode,
120
+ toggleMode,
121
+ toggleDark
122
+ };
123
+ });
124
+ }
125
+ var useThemeStore = createThemeStore();
126
+
127
+ exports.DEFAULT_THEME_OPTIONS = DEFAULT_THEME_OPTIONS;
128
+ exports.THEME_MODE_ICONS = THEME_MODE_ICONS;
129
+ exports.THEME_MODE_LABELS = THEME_MODE_LABELS;
130
+ exports.createThemeStore = createThemeStore;
131
+ exports.isViewTransitionSupported = isViewTransitionSupported;
132
+ exports.useThemeStore = useThemeStore;
133
+ exports.useViewTransition = useViewTransition;
134
+ //# sourceMappingURL=index.cjs.map
135
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants/index.ts","../src/composables/useViewTransition.ts","../src/stores/theme.ts"],"names":["defineStore","ref","computed"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\r\n * 默认配置常量\r\n */\r\nexport const DEFAULT_THEME_OPTIONS = {\r\n defaultMode: \"system\" as const,\r\n storageKey: \"theme-mode\",\r\n enableTransition: true,\r\n transitionDuration: 500,\r\n};\r\n\r\n/**\r\n * 主题模式显示文本\r\n */\r\nexport const THEME_MODE_LABELS = {\r\n light: \"浅色\",\r\n dark: \"深色\",\r\n system: \"自动\",\r\n} as const;\r\n\r\n/**\r\n * 主题模式图标\r\n */\r\nexport const THEME_MODE_ICONS = {\r\n light: \"i-mdi:white-balance-sunny\",\r\n dark: \"i-mdi:moon-waning-crescent\",\r\n system: \"i-mdi:theme-light-dark\",\r\n} as const;\r\n","/**\r\n * View Transition API 工具函数\r\n */\r\n\r\nexport interface ViewTransitionOptions {\r\n /** 过渡动画时长(毫秒) */\r\n duration?: number;\r\n /** 过渡中添加的 CSS 类名 */\r\n transitioningClass?: string;\r\n}\r\n\r\n/**\r\n * 使用 View Transition API 执行主题切换\r\n * @param callback - 执行 DOM 更新的回调函数\r\n * @param options - 配置选项\r\n */\r\nexport async function useViewTransition(\r\n callback: () => void,\r\n options: ViewTransitionOptions = {},\r\n): Promise<void> {\r\n const { transitioningClass = \"theme-transitioning\" } = options;\r\n\r\n // 检查浏览器是否支持 View Transition API\r\n if (!document.startViewTransition) {\r\n // 不支持则直接执行回调\r\n callback();\r\n return;\r\n }\r\n\r\n const root = document.documentElement;\r\n\r\n // 添加标记类,用于禁用所有 CSS transitions(防止冲突)\r\n root.classList.add(transitioningClass);\r\n\r\n try {\r\n const transition = document.startViewTransition(callback);\r\n\r\n // 等待过渡完成\r\n await transition.finished;\r\n } catch (error) {\r\n // 忽略用户中断过渡的错误\r\n console.debug(\"[ViewTransition] Transition interrupted:\", error);\r\n } finally {\r\n // 移除标记类\r\n root.classList.remove(transitioningClass);\r\n }\r\n}\r\n\r\n/**\r\n * 检查浏览器是否支持 View Transition API\r\n */\r\nexport function isViewTransitionSupported(): boolean {\r\n return typeof document !== \"undefined\" && \"startViewTransition\" in document;\r\n}\r\n","import { defineStore } from \"pinia\";\r\nimport { ref, computed } from \"vue\";\r\nimport type { ThemeMode, ThemeStoreOptions } from \"../types\";\r\nimport { DEFAULT_THEME_OPTIONS } from \"../constants\";\r\nimport { useViewTransition } from \"../composables/useViewTransition\";\r\n\r\n/**\r\n * 创建主题管理 Store\r\n * @param options - 配置选项\r\n */\r\nexport function createThemeStore(options: ThemeStoreOptions = {}) {\r\n const {\r\n defaultMode = DEFAULT_THEME_OPTIONS.defaultMode,\r\n storageKey = DEFAULT_THEME_OPTIONS.storageKey,\r\n enableTransition = DEFAULT_THEME_OPTIONS.enableTransition,\r\n transitionDuration = DEFAULT_THEME_OPTIONS.transitionDuration,\r\n } = options;\r\n\r\n return defineStore(\"theme\", () => {\r\n // ============ 初始化 ============\r\n\r\n // 获取系统主题偏好\r\n const mediaQuery =\r\n typeof window !== \"undefined\"\r\n ? window.matchMedia(\"(prefers-color-scheme: dark)\")\r\n : null;\r\n\r\n // 从 localStorage 读取保存的模式\r\n const savedMode =\r\n typeof window !== \"undefined\"\r\n ? (localStorage.getItem(storageKey) as ThemeMode)\r\n : null;\r\n\r\n // ============ 状态定义 ============\r\n\r\n /** 当前主题模式 */\r\n const mode = ref<ThemeMode>(savedMode || defaultMode);\r\n\r\n /** 系统是否为暗色模式 */\r\n const systemIsDark = ref(mediaQuery?.matches ?? false);\r\n\r\n // ============ 计算属性 ============\r\n\r\n /** 当前是否为暗色模式 */\r\n const isDark = computed(() => {\r\n if (mode.value === \"system\") {\r\n return systemIsDark.value;\r\n }\r\n return mode.value === \"dark\";\r\n });\r\n\r\n // ============ 内部方法 ============\r\n\r\n /**\r\n * 同步主题属性到 HTML 元素\r\n */\r\n const syncThemeAttr = () => {\r\n if (typeof document !== \"undefined\") {\r\n const themeValue = isDark.value ? \"dark\" : \"light\";\r\n document.documentElement.setAttribute(\"data-theme\", themeValue);\r\n }\r\n };\r\n\r\n // ============ Actions ============\r\n\r\n /**\r\n * 初始化主题系统\r\n */\r\n const init = () => {\r\n // 确保状态同步\r\n if (mediaQuery) {\r\n systemIsDark.value = mediaQuery.matches;\r\n }\r\n\r\n // 同步 data-theme 属性到 html 元素\r\n syncThemeAttr();\r\n\r\n // 监听系统主题变化\r\n if (mediaQuery) {\r\n mediaQuery.addEventListener(\"change\", (e) => {\r\n systemIsDark.value = e.matches;\r\n // 如果当前是 system 模式,需要更新 DOM\r\n if (mode.value === \"system\") {\r\n syncThemeAttr();\r\n }\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * 设置主题模式\r\n * @param newMode - 新的主题模式\r\n */\r\n const setMode = async (newMode: ThemeMode) => {\r\n // 记录切换前的视觉状态\r\n const oldDark = isDark.value;\r\n\r\n // 更新状态\r\n mode.value = newMode;\r\n\r\n // 保存到 localStorage\r\n if (typeof window !== \"undefined\") {\r\n localStorage.setItem(storageKey, newMode);\r\n }\r\n\r\n // 新的视觉状态\r\n const newDark = isDark.value;\r\n\r\n // 如果视觉效果没变化,直接同步 DOM 即可(无需动画)\r\n if (oldDark === newDark) {\r\n syncThemeAttr();\r\n return;\r\n }\r\n\r\n // 视觉有变化,执行过渡动画\r\n if (enableTransition) {\r\n await useViewTransition(syncThemeAttr, {\r\n duration: transitionDuration,\r\n });\r\n } else {\r\n syncThemeAttr();\r\n }\r\n };\r\n\r\n /**\r\n * 切换主题模式(在 light/dark/system 之间循环)\r\n */\r\n const toggleMode = async () => {\r\n const modes: ThemeMode[] = [\"light\", \"dark\", \"system\"];\r\n const currentIndex = modes.indexOf(mode.value);\r\n const nextIndex = (currentIndex + 1) % modes.length;\r\n await setMode(modes[nextIndex]);\r\n };\r\n\r\n /**\r\n * 切换暗色模式(仅在 light 和 dark 之间切换)\r\n */\r\n const toggleDark = async () => {\r\n const newMode = mode.value === \"dark\" ? \"light\" : \"dark\";\r\n await setMode(newMode);\r\n };\r\n\r\n // ============ 返回 ============\r\n\r\n return {\r\n // State\r\n mode,\r\n systemIsDark,\r\n\r\n // Getters\r\n isDark,\r\n\r\n // Actions\r\n init,\r\n setMode,\r\n toggleMode,\r\n toggleDark,\r\n };\r\n });\r\n}\r\n\r\n/**\r\n * 默认的主题 Store(使用默认配置)\r\n */\r\nexport const useThemeStore = createThemeStore();\r\n"]}
@@ -0,0 +1,137 @@
1
+ import * as pinia from 'pinia';
2
+ import * as vue from 'vue';
3
+
4
+ /**
5
+ * 主题模式类型
6
+ */
7
+ type ThemeMode = "light" | "dark" | "system";
8
+ /**
9
+ * 主题配置接口
10
+ */
11
+ interface ThemeConfig {
12
+ /** 当前主题模式 */
13
+ mode: ThemeMode;
14
+ /** 是否为暗色模式 */
15
+ isDark: boolean;
16
+ /** 系统是否为暗色模式 */
17
+ systemIsDark: boolean;
18
+ }
19
+ /**
20
+ * 主题 Store 选项
21
+ */
22
+ interface ThemeStoreOptions {
23
+ /** 默认主题模式 */
24
+ defaultMode?: ThemeMode;
25
+ /** localStorage 键名 */
26
+ storageKey?: string;
27
+ /** 是否启用 View Transition API */
28
+ enableTransition?: boolean;
29
+ /** 过渡动画时长(毫秒) */
30
+ transitionDuration?: number;
31
+ }
32
+
33
+ /**
34
+ * 默认配置常量
35
+ */
36
+ declare const DEFAULT_THEME_OPTIONS: {
37
+ defaultMode: "system";
38
+ storageKey: string;
39
+ enableTransition: boolean;
40
+ transitionDuration: number;
41
+ };
42
+ /**
43
+ * 主题模式显示文本
44
+ */
45
+ declare const THEME_MODE_LABELS: {
46
+ readonly light: "浅色";
47
+ readonly dark: "深色";
48
+ readonly system: "自动";
49
+ };
50
+ /**
51
+ * 主题模式图标
52
+ */
53
+ declare const THEME_MODE_ICONS: {
54
+ readonly light: "i-mdi:white-balance-sunny";
55
+ readonly dark: "i-mdi:moon-waning-crescent";
56
+ readonly system: "i-mdi:theme-light-dark";
57
+ };
58
+
59
+ /**
60
+ * View Transition API 工具函数
61
+ */
62
+ interface ViewTransitionOptions {
63
+ /** 过渡动画时长(毫秒) */
64
+ duration?: number;
65
+ /** 过渡中添加的 CSS 类名 */
66
+ transitioningClass?: string;
67
+ }
68
+ /**
69
+ * 使用 View Transition API 执行主题切换
70
+ * @param callback - 执行 DOM 更新的回调函数
71
+ * @param options - 配置选项
72
+ */
73
+ declare function useViewTransition(callback: () => void, options?: ViewTransitionOptions): Promise<void>;
74
+ /**
75
+ * 检查浏览器是否支持 View Transition API
76
+ */
77
+ declare function isViewTransitionSupported(): boolean;
78
+
79
+ /**
80
+ * 创建主题管理 Store
81
+ * @param options - 配置选项
82
+ */
83
+ declare function createThemeStore(options?: ThemeStoreOptions): pinia.StoreDefinition<"theme", Pick<{
84
+ mode: vue.Ref<ThemeMode, ThemeMode>;
85
+ systemIsDark: vue.Ref<boolean, boolean>;
86
+ isDark: vue.ComputedRef<boolean>;
87
+ init: () => void;
88
+ setMode: (newMode: ThemeMode) => Promise<void>;
89
+ toggleMode: () => Promise<void>;
90
+ toggleDark: () => Promise<void>;
91
+ }, "mode" | "systemIsDark">, Pick<{
92
+ mode: vue.Ref<ThemeMode, ThemeMode>;
93
+ systemIsDark: vue.Ref<boolean, boolean>;
94
+ isDark: vue.ComputedRef<boolean>;
95
+ init: () => void;
96
+ setMode: (newMode: ThemeMode) => Promise<void>;
97
+ toggleMode: () => Promise<void>;
98
+ toggleDark: () => Promise<void>;
99
+ }, "isDark">, Pick<{
100
+ mode: vue.Ref<ThemeMode, ThemeMode>;
101
+ systemIsDark: vue.Ref<boolean, boolean>;
102
+ isDark: vue.ComputedRef<boolean>;
103
+ init: () => void;
104
+ setMode: (newMode: ThemeMode) => Promise<void>;
105
+ toggleMode: () => Promise<void>;
106
+ toggleDark: () => Promise<void>;
107
+ }, "init" | "setMode" | "toggleMode" | "toggleDark">>;
108
+ /**
109
+ * 默认的主题 Store(使用默认配置)
110
+ */
111
+ declare const useThemeStore: pinia.StoreDefinition<"theme", Pick<{
112
+ mode: vue.Ref<ThemeMode, ThemeMode>;
113
+ systemIsDark: vue.Ref<boolean, boolean>;
114
+ isDark: vue.ComputedRef<boolean>;
115
+ init: () => void;
116
+ setMode: (newMode: ThemeMode) => Promise<void>;
117
+ toggleMode: () => Promise<void>;
118
+ toggleDark: () => Promise<void>;
119
+ }, "mode" | "systemIsDark">, Pick<{
120
+ mode: vue.Ref<ThemeMode, ThemeMode>;
121
+ systemIsDark: vue.Ref<boolean, boolean>;
122
+ isDark: vue.ComputedRef<boolean>;
123
+ init: () => void;
124
+ setMode: (newMode: ThemeMode) => Promise<void>;
125
+ toggleMode: () => Promise<void>;
126
+ toggleDark: () => Promise<void>;
127
+ }, "isDark">, Pick<{
128
+ mode: vue.Ref<ThemeMode, ThemeMode>;
129
+ systemIsDark: vue.Ref<boolean, boolean>;
130
+ isDark: vue.ComputedRef<boolean>;
131
+ init: () => void;
132
+ setMode: (newMode: ThemeMode) => Promise<void>;
133
+ toggleMode: () => Promise<void>;
134
+ toggleDark: () => Promise<void>;
135
+ }, "init" | "setMode" | "toggleMode" | "toggleDark">>;
136
+
137
+ export { DEFAULT_THEME_OPTIONS, THEME_MODE_ICONS, THEME_MODE_LABELS, type ThemeConfig, type ThemeMode, type ThemeStoreOptions, createThemeStore, isViewTransitionSupported, useThemeStore, useViewTransition };
@@ -0,0 +1,137 @@
1
+ import * as pinia from 'pinia';
2
+ import * as vue from 'vue';
3
+
4
+ /**
5
+ * 主题模式类型
6
+ */
7
+ type ThemeMode = "light" | "dark" | "system";
8
+ /**
9
+ * 主题配置接口
10
+ */
11
+ interface ThemeConfig {
12
+ /** 当前主题模式 */
13
+ mode: ThemeMode;
14
+ /** 是否为暗色模式 */
15
+ isDark: boolean;
16
+ /** 系统是否为暗色模式 */
17
+ systemIsDark: boolean;
18
+ }
19
+ /**
20
+ * 主题 Store 选项
21
+ */
22
+ interface ThemeStoreOptions {
23
+ /** 默认主题模式 */
24
+ defaultMode?: ThemeMode;
25
+ /** localStorage 键名 */
26
+ storageKey?: string;
27
+ /** 是否启用 View Transition API */
28
+ enableTransition?: boolean;
29
+ /** 过渡动画时长(毫秒) */
30
+ transitionDuration?: number;
31
+ }
32
+
33
+ /**
34
+ * 默认配置常量
35
+ */
36
+ declare const DEFAULT_THEME_OPTIONS: {
37
+ defaultMode: "system";
38
+ storageKey: string;
39
+ enableTransition: boolean;
40
+ transitionDuration: number;
41
+ };
42
+ /**
43
+ * 主题模式显示文本
44
+ */
45
+ declare const THEME_MODE_LABELS: {
46
+ readonly light: "浅色";
47
+ readonly dark: "深色";
48
+ readonly system: "自动";
49
+ };
50
+ /**
51
+ * 主题模式图标
52
+ */
53
+ declare const THEME_MODE_ICONS: {
54
+ readonly light: "i-mdi:white-balance-sunny";
55
+ readonly dark: "i-mdi:moon-waning-crescent";
56
+ readonly system: "i-mdi:theme-light-dark";
57
+ };
58
+
59
+ /**
60
+ * View Transition API 工具函数
61
+ */
62
+ interface ViewTransitionOptions {
63
+ /** 过渡动画时长(毫秒) */
64
+ duration?: number;
65
+ /** 过渡中添加的 CSS 类名 */
66
+ transitioningClass?: string;
67
+ }
68
+ /**
69
+ * 使用 View Transition API 执行主题切换
70
+ * @param callback - 执行 DOM 更新的回调函数
71
+ * @param options - 配置选项
72
+ */
73
+ declare function useViewTransition(callback: () => void, options?: ViewTransitionOptions): Promise<void>;
74
+ /**
75
+ * 检查浏览器是否支持 View Transition API
76
+ */
77
+ declare function isViewTransitionSupported(): boolean;
78
+
79
+ /**
80
+ * 创建主题管理 Store
81
+ * @param options - 配置选项
82
+ */
83
+ declare function createThemeStore(options?: ThemeStoreOptions): pinia.StoreDefinition<"theme", Pick<{
84
+ mode: vue.Ref<ThemeMode, ThemeMode>;
85
+ systemIsDark: vue.Ref<boolean, boolean>;
86
+ isDark: vue.ComputedRef<boolean>;
87
+ init: () => void;
88
+ setMode: (newMode: ThemeMode) => Promise<void>;
89
+ toggleMode: () => Promise<void>;
90
+ toggleDark: () => Promise<void>;
91
+ }, "mode" | "systemIsDark">, Pick<{
92
+ mode: vue.Ref<ThemeMode, ThemeMode>;
93
+ systemIsDark: vue.Ref<boolean, boolean>;
94
+ isDark: vue.ComputedRef<boolean>;
95
+ init: () => void;
96
+ setMode: (newMode: ThemeMode) => Promise<void>;
97
+ toggleMode: () => Promise<void>;
98
+ toggleDark: () => Promise<void>;
99
+ }, "isDark">, Pick<{
100
+ mode: vue.Ref<ThemeMode, ThemeMode>;
101
+ systemIsDark: vue.Ref<boolean, boolean>;
102
+ isDark: vue.ComputedRef<boolean>;
103
+ init: () => void;
104
+ setMode: (newMode: ThemeMode) => Promise<void>;
105
+ toggleMode: () => Promise<void>;
106
+ toggleDark: () => Promise<void>;
107
+ }, "init" | "setMode" | "toggleMode" | "toggleDark">>;
108
+ /**
109
+ * 默认的主题 Store(使用默认配置)
110
+ */
111
+ declare const useThemeStore: pinia.StoreDefinition<"theme", Pick<{
112
+ mode: vue.Ref<ThemeMode, ThemeMode>;
113
+ systemIsDark: vue.Ref<boolean, boolean>;
114
+ isDark: vue.ComputedRef<boolean>;
115
+ init: () => void;
116
+ setMode: (newMode: ThemeMode) => Promise<void>;
117
+ toggleMode: () => Promise<void>;
118
+ toggleDark: () => Promise<void>;
119
+ }, "mode" | "systemIsDark">, Pick<{
120
+ mode: vue.Ref<ThemeMode, ThemeMode>;
121
+ systemIsDark: vue.Ref<boolean, boolean>;
122
+ isDark: vue.ComputedRef<boolean>;
123
+ init: () => void;
124
+ setMode: (newMode: ThemeMode) => Promise<void>;
125
+ toggleMode: () => Promise<void>;
126
+ toggleDark: () => Promise<void>;
127
+ }, "isDark">, Pick<{
128
+ mode: vue.Ref<ThemeMode, ThemeMode>;
129
+ systemIsDark: vue.Ref<boolean, boolean>;
130
+ isDark: vue.ComputedRef<boolean>;
131
+ init: () => void;
132
+ setMode: (newMode: ThemeMode) => Promise<void>;
133
+ toggleMode: () => Promise<void>;
134
+ toggleDark: () => Promise<void>;
135
+ }, "init" | "setMode" | "toggleMode" | "toggleDark">>;
136
+
137
+ export { DEFAULT_THEME_OPTIONS, THEME_MODE_ICONS, THEME_MODE_LABELS, type ThemeConfig, type ThemeMode, type ThemeStoreOptions, createThemeStore, isViewTransitionSupported, useThemeStore, useViewTransition };
package/dist/index.js ADDED
@@ -0,0 +1,127 @@
1
+ import { defineStore } from 'pinia';
2
+ import { ref, computed } from 'vue';
3
+
4
+ // src/constants/index.ts
5
+ var DEFAULT_THEME_OPTIONS = {
6
+ defaultMode: "system",
7
+ storageKey: "theme-mode",
8
+ enableTransition: true,
9
+ transitionDuration: 500
10
+ };
11
+ var THEME_MODE_LABELS = {
12
+ light: "\u6D45\u8272",
13
+ dark: "\u6DF1\u8272",
14
+ system: "\u81EA\u52A8"
15
+ };
16
+ var THEME_MODE_ICONS = {
17
+ light: "i-mdi:white-balance-sunny",
18
+ dark: "i-mdi:moon-waning-crescent",
19
+ system: "i-mdi:theme-light-dark"
20
+ };
21
+
22
+ // src/composables/useViewTransition.ts
23
+ async function useViewTransition(callback, options = {}) {
24
+ const { transitioningClass = "theme-transitioning" } = options;
25
+ if (!document.startViewTransition) {
26
+ callback();
27
+ return;
28
+ }
29
+ const root = document.documentElement;
30
+ root.classList.add(transitioningClass);
31
+ try {
32
+ const transition = document.startViewTransition(callback);
33
+ await transition.finished;
34
+ } catch (error) {
35
+ console.debug("[ViewTransition] Transition interrupted:", error);
36
+ } finally {
37
+ root.classList.remove(transitioningClass);
38
+ }
39
+ }
40
+ function isViewTransitionSupported() {
41
+ return typeof document !== "undefined" && "startViewTransition" in document;
42
+ }
43
+ function createThemeStore(options = {}) {
44
+ const {
45
+ defaultMode = DEFAULT_THEME_OPTIONS.defaultMode,
46
+ storageKey = DEFAULT_THEME_OPTIONS.storageKey,
47
+ enableTransition = DEFAULT_THEME_OPTIONS.enableTransition,
48
+ transitionDuration = DEFAULT_THEME_OPTIONS.transitionDuration
49
+ } = options;
50
+ return defineStore("theme", () => {
51
+ const mediaQuery = typeof window !== "undefined" ? window.matchMedia("(prefers-color-scheme: dark)") : null;
52
+ const savedMode = typeof window !== "undefined" ? localStorage.getItem(storageKey) : null;
53
+ const mode = ref(savedMode || defaultMode);
54
+ const systemIsDark = ref(mediaQuery?.matches ?? false);
55
+ const isDark = computed(() => {
56
+ if (mode.value === "system") {
57
+ return systemIsDark.value;
58
+ }
59
+ return mode.value === "dark";
60
+ });
61
+ const syncThemeAttr = () => {
62
+ if (typeof document !== "undefined") {
63
+ const themeValue = isDark.value ? "dark" : "light";
64
+ document.documentElement.setAttribute("data-theme", themeValue);
65
+ }
66
+ };
67
+ const init = () => {
68
+ if (mediaQuery) {
69
+ systemIsDark.value = mediaQuery.matches;
70
+ }
71
+ syncThemeAttr();
72
+ if (mediaQuery) {
73
+ mediaQuery.addEventListener("change", (e) => {
74
+ systemIsDark.value = e.matches;
75
+ if (mode.value === "system") {
76
+ syncThemeAttr();
77
+ }
78
+ });
79
+ }
80
+ };
81
+ const setMode = async (newMode) => {
82
+ const oldDark = isDark.value;
83
+ mode.value = newMode;
84
+ if (typeof window !== "undefined") {
85
+ localStorage.setItem(storageKey, newMode);
86
+ }
87
+ const newDark = isDark.value;
88
+ if (oldDark === newDark) {
89
+ syncThemeAttr();
90
+ return;
91
+ }
92
+ if (enableTransition) {
93
+ await useViewTransition(syncThemeAttr, {
94
+ });
95
+ } else {
96
+ syncThemeAttr();
97
+ }
98
+ };
99
+ const toggleMode = async () => {
100
+ const modes = ["light", "dark", "system"];
101
+ const currentIndex = modes.indexOf(mode.value);
102
+ const nextIndex = (currentIndex + 1) % modes.length;
103
+ await setMode(modes[nextIndex]);
104
+ };
105
+ const toggleDark = async () => {
106
+ const newMode = mode.value === "dark" ? "light" : "dark";
107
+ await setMode(newMode);
108
+ };
109
+ return {
110
+ // State
111
+ mode,
112
+ systemIsDark,
113
+ // Getters
114
+ isDark,
115
+ // Actions
116
+ init,
117
+ setMode,
118
+ toggleMode,
119
+ toggleDark
120
+ };
121
+ });
122
+ }
123
+ var useThemeStore = createThemeStore();
124
+
125
+ export { DEFAULT_THEME_OPTIONS, THEME_MODE_ICONS, THEME_MODE_LABELS, createThemeStore, isViewTransitionSupported, useThemeStore, useViewTransition };
126
+ //# sourceMappingURL=index.js.map
127
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/constants/index.ts","../src/composables/useViewTransition.ts","../src/stores/theme.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\r\n * 默认配置常量\r\n */\r\nexport const DEFAULT_THEME_OPTIONS = {\r\n defaultMode: \"system\" as const,\r\n storageKey: \"theme-mode\",\r\n enableTransition: true,\r\n transitionDuration: 500,\r\n};\r\n\r\n/**\r\n * 主题模式显示文本\r\n */\r\nexport const THEME_MODE_LABELS = {\r\n light: \"浅色\",\r\n dark: \"深色\",\r\n system: \"自动\",\r\n} as const;\r\n\r\n/**\r\n * 主题模式图标\r\n */\r\nexport const THEME_MODE_ICONS = {\r\n light: \"i-mdi:white-balance-sunny\",\r\n dark: \"i-mdi:moon-waning-crescent\",\r\n system: \"i-mdi:theme-light-dark\",\r\n} as const;\r\n","/**\r\n * View Transition API 工具函数\r\n */\r\n\r\nexport interface ViewTransitionOptions {\r\n /** 过渡动画时长(毫秒) */\r\n duration?: number;\r\n /** 过渡中添加的 CSS 类名 */\r\n transitioningClass?: string;\r\n}\r\n\r\n/**\r\n * 使用 View Transition API 执行主题切换\r\n * @param callback - 执行 DOM 更新的回调函数\r\n * @param options - 配置选项\r\n */\r\nexport async function useViewTransition(\r\n callback: () => void,\r\n options: ViewTransitionOptions = {},\r\n): Promise<void> {\r\n const { transitioningClass = \"theme-transitioning\" } = options;\r\n\r\n // 检查浏览器是否支持 View Transition API\r\n if (!document.startViewTransition) {\r\n // 不支持则直接执行回调\r\n callback();\r\n return;\r\n }\r\n\r\n const root = document.documentElement;\r\n\r\n // 添加标记类,用于禁用所有 CSS transitions(防止冲突)\r\n root.classList.add(transitioningClass);\r\n\r\n try {\r\n const transition = document.startViewTransition(callback);\r\n\r\n // 等待过渡完成\r\n await transition.finished;\r\n } catch (error) {\r\n // 忽略用户中断过渡的错误\r\n console.debug(\"[ViewTransition] Transition interrupted:\", error);\r\n } finally {\r\n // 移除标记类\r\n root.classList.remove(transitioningClass);\r\n }\r\n}\r\n\r\n/**\r\n * 检查浏览器是否支持 View Transition API\r\n */\r\nexport function isViewTransitionSupported(): boolean {\r\n return typeof document !== \"undefined\" && \"startViewTransition\" in document;\r\n}\r\n","import { defineStore } from \"pinia\";\r\nimport { ref, computed } from \"vue\";\r\nimport type { ThemeMode, ThemeStoreOptions } from \"../types\";\r\nimport { DEFAULT_THEME_OPTIONS } from \"../constants\";\r\nimport { useViewTransition } from \"../composables/useViewTransition\";\r\n\r\n/**\r\n * 创建主题管理 Store\r\n * @param options - 配置选项\r\n */\r\nexport function createThemeStore(options: ThemeStoreOptions = {}) {\r\n const {\r\n defaultMode = DEFAULT_THEME_OPTIONS.defaultMode,\r\n storageKey = DEFAULT_THEME_OPTIONS.storageKey,\r\n enableTransition = DEFAULT_THEME_OPTIONS.enableTransition,\r\n transitionDuration = DEFAULT_THEME_OPTIONS.transitionDuration,\r\n } = options;\r\n\r\n return defineStore(\"theme\", () => {\r\n // ============ 初始化 ============\r\n\r\n // 获取系统主题偏好\r\n const mediaQuery =\r\n typeof window !== \"undefined\"\r\n ? window.matchMedia(\"(prefers-color-scheme: dark)\")\r\n : null;\r\n\r\n // 从 localStorage 读取保存的模式\r\n const savedMode =\r\n typeof window !== \"undefined\"\r\n ? (localStorage.getItem(storageKey) as ThemeMode)\r\n : null;\r\n\r\n // ============ 状态定义 ============\r\n\r\n /** 当前主题模式 */\r\n const mode = ref<ThemeMode>(savedMode || defaultMode);\r\n\r\n /** 系统是否为暗色模式 */\r\n const systemIsDark = ref(mediaQuery?.matches ?? false);\r\n\r\n // ============ 计算属性 ============\r\n\r\n /** 当前是否为暗色模式 */\r\n const isDark = computed(() => {\r\n if (mode.value === \"system\") {\r\n return systemIsDark.value;\r\n }\r\n return mode.value === \"dark\";\r\n });\r\n\r\n // ============ 内部方法 ============\r\n\r\n /**\r\n * 同步主题属性到 HTML 元素\r\n */\r\n const syncThemeAttr = () => {\r\n if (typeof document !== \"undefined\") {\r\n const themeValue = isDark.value ? \"dark\" : \"light\";\r\n document.documentElement.setAttribute(\"data-theme\", themeValue);\r\n }\r\n };\r\n\r\n // ============ Actions ============\r\n\r\n /**\r\n * 初始化主题系统\r\n */\r\n const init = () => {\r\n // 确保状态同步\r\n if (mediaQuery) {\r\n systemIsDark.value = mediaQuery.matches;\r\n }\r\n\r\n // 同步 data-theme 属性到 html 元素\r\n syncThemeAttr();\r\n\r\n // 监听系统主题变化\r\n if (mediaQuery) {\r\n mediaQuery.addEventListener(\"change\", (e) => {\r\n systemIsDark.value = e.matches;\r\n // 如果当前是 system 模式,需要更新 DOM\r\n if (mode.value === \"system\") {\r\n syncThemeAttr();\r\n }\r\n });\r\n }\r\n };\r\n\r\n /**\r\n * 设置主题模式\r\n * @param newMode - 新的主题模式\r\n */\r\n const setMode = async (newMode: ThemeMode) => {\r\n // 记录切换前的视觉状态\r\n const oldDark = isDark.value;\r\n\r\n // 更新状态\r\n mode.value = newMode;\r\n\r\n // 保存到 localStorage\r\n if (typeof window !== \"undefined\") {\r\n localStorage.setItem(storageKey, newMode);\r\n }\r\n\r\n // 新的视觉状态\r\n const newDark = isDark.value;\r\n\r\n // 如果视觉效果没变化,直接同步 DOM 即可(无需动画)\r\n if (oldDark === newDark) {\r\n syncThemeAttr();\r\n return;\r\n }\r\n\r\n // 视觉有变化,执行过渡动画\r\n if (enableTransition) {\r\n await useViewTransition(syncThemeAttr, {\r\n duration: transitionDuration,\r\n });\r\n } else {\r\n syncThemeAttr();\r\n }\r\n };\r\n\r\n /**\r\n * 切换主题模式(在 light/dark/system 之间循环)\r\n */\r\n const toggleMode = async () => {\r\n const modes: ThemeMode[] = [\"light\", \"dark\", \"system\"];\r\n const currentIndex = modes.indexOf(mode.value);\r\n const nextIndex = (currentIndex + 1) % modes.length;\r\n await setMode(modes[nextIndex]);\r\n };\r\n\r\n /**\r\n * 切换暗色模式(仅在 light 和 dark 之间切换)\r\n */\r\n const toggleDark = async () => {\r\n const newMode = mode.value === \"dark\" ? \"light\" : \"dark\";\r\n await setMode(newMode);\r\n };\r\n\r\n // ============ 返回 ============\r\n\r\n return {\r\n // State\r\n mode,\r\n systemIsDark,\r\n\r\n // Getters\r\n isDark,\r\n\r\n // Actions\r\n init,\r\n setMode,\r\n toggleMode,\r\n toggleDark,\r\n };\r\n });\r\n}\r\n\r\n/**\r\n * 默认的主题 Store(使用默认配置)\r\n */\r\nexport const useThemeStore = createThemeStore();\r\n"]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@robot-admin/theme",
3
+ "version": "0.1.0",
4
+ "description": "Theme switching and management system for Robot Admin",
5
+ "type": "module",
6
+ "keywords": [
7
+ "vue",
8
+ "theme",
9
+ "dark-mode",
10
+ "pinia",
11
+ "view-transition"
12
+ ],
13
+ "author": "ChenYu <ycyplus@gmail.com>",
14
+ "license": "MIT",
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "https://github.com/ChenyCHENYU/robot-admin-packages",
18
+ "directory": "packages/theme"
19
+ },
20
+ "main": "./dist/index.cjs",
21
+ "module": "./dist/index.js",
22
+ "types": "./dist/index.d.ts",
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js",
27
+ "require": "./dist/index.cjs"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist",
32
+ "README.md"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "scripts": {
39
+ "dev": "tsup --watch",
40
+ "build": "tsup",
41
+ "clean": "rm -rf dist",
42
+ "type-check": "tsc --noEmit",
43
+ "prepublishOnly": "bun run build"
44
+ },
45
+ "peerDependencies": {
46
+ "vue": "^3.4.0",
47
+ "pinia": "^2.0.0 || ^3.0.0",
48
+ "naive-ui": "^2.38.0"
49
+ },
50
+ "devDependencies": {
51
+ "@types/node": "^22.13.9",
52
+ "naive-ui": "^2.41.0",
53
+ "pinia": "^3.0.1",
54
+ "tsup": "^8.3.5",
55
+ "typescript": "~5.8.0",
56
+ "vue": "^3.5.13"
57
+ }
58
+ }