@tixxin/nuxt-theme-engine 0.0.1

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.
@@ -0,0 +1,52 @@
1
+ declare module '#build/theme-engine.options.mjs' {
2
+ export const themeEngineOptions: {
3
+ themesDir: string
4
+ defaultTheme: string
5
+ cookieKey: string
6
+ lazyLoadThemes: boolean
7
+ requiredCssVars: string[]
8
+ contractsEntry: string
9
+ contractsImportId: string
10
+ }
11
+
12
+ export const themeEngineThemeNames: string[]
13
+
14
+ export const themeEngineThemes: Array<{
15
+ name: string
16
+ label: string
17
+ extends: string | null
18
+ inheritanceChain: string[]
19
+ }>
20
+ }
21
+
22
+ declare module '#build/theme-engine.registry.mjs' {
23
+ import type { Component } from 'vue'
24
+
25
+ export const themeComponentRegistry: Record<string, Record<string, {
26
+ alias: string
27
+ sourceTheme: string
28
+ }>>
29
+
30
+ export const themeComponentLoaders: Record<string, Record<string, () => Promise<{ default: Component }>>>
31
+ }
32
+
33
+ declare module '#build/theme-engine.contracts.mjs' {
34
+ export const themeEngineContracts: {
35
+ entry: string
36
+ importId: string
37
+ }
38
+
39
+ export type GeneratedThemeComponentName = string
40
+ export type GeneratedThemeComponentContracts = Record<string, Record<string, unknown>>
41
+ export type ThemeComponentDiscriminatedProps = {
42
+ name: GeneratedThemeComponentName
43
+ } & Record<string, unknown>
44
+ }
45
+
46
+ declare module '#build/theme-engine.css-report.mjs' {
47
+ export const themeCssVariableReport: Array<{
48
+ theme: string
49
+ provided: string[]
50
+ missing: string[]
51
+ }>
52
+ }
@@ -0,0 +1,10 @@
1
+ type GeneratedThemeComponentName = import('#build/theme-engine.contracts.mjs').GeneratedThemeComponentName;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
4
+ declare const __VLS_export: import("vue").DefineComponent<{
5
+ name: GeneratedThemeComponentName;
6
+ }, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
7
+ [key: string]: any;
8
+ }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
9
+ name: GeneratedThemeComponentName;
10
+ }> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,46 @@
1
+ <script>
2
+ import { computed, defineAsyncComponent, defineComponent, h, useAttrs, useSlots } from "vue";
3
+ import { themeComponentLoaders, themeComponentRegistry } from "#build/theme-engine.registry.mjs";
4
+ import { useThemeEngine } from "../composables/useThemeEngine";
5
+ const asyncComponentCache = /* @__PURE__ */ new Map();
6
+ export default defineComponent({
7
+ name: "ThemeComponent",
8
+ inheritAttrs: false,
9
+ props: {
10
+ name: {
11
+ type: String,
12
+ required: true
13
+ }
14
+ },
15
+ setup(props) {
16
+ const attrs = useAttrs();
17
+ const slots = useSlots();
18
+ const { currentTheme } = useThemeEngine();
19
+ const firstThemeName = Object.keys(themeComponentRegistry)[0] ?? "";
20
+ const entry = computed(() => {
21
+ return themeComponentRegistry[currentTheme.value]?.[props.name] ?? themeComponentRegistry[firstThemeName]?.[props.name];
22
+ });
23
+ const resolvedComponent = computed(() => {
24
+ const resolvedEntry = entry.value;
25
+ if (!resolvedEntry) {
26
+ return null;
27
+ }
28
+ const loader = themeComponentLoaders[currentTheme.value]?.[props.name] ?? themeComponentLoaders[firstThemeName]?.[props.name];
29
+ if (!loader) {
30
+ return null;
31
+ }
32
+ const cacheKey = `${resolvedEntry.sourceTheme}:${props.name}`;
33
+ if (!asyncComponentCache.has(cacheKey)) {
34
+ asyncComponentCache.set(cacheKey, defineAsyncComponent(loader));
35
+ }
36
+ return asyncComponentCache.get(cacheKey) ?? null;
37
+ });
38
+ return () => {
39
+ if (!resolvedComponent.value) {
40
+ return null;
41
+ }
42
+ return h(resolvedComponent.value, attrs, slots);
43
+ };
44
+ }
45
+ });
46
+ </script>
@@ -0,0 +1,10 @@
1
+ type GeneratedThemeComponentName = import('#build/theme-engine.contracts.mjs').GeneratedThemeComponentName;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
4
+ declare const __VLS_export: import("vue").DefineComponent<{
5
+ name: GeneratedThemeComponentName;
6
+ }, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
7
+ [key: string]: any;
8
+ }> | null, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
9
+ name: GeneratedThemeComponentName;
10
+ }> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
@@ -0,0 +1,10 @@
1
+ type ThemeConfigValue = string | number | boolean | null | Record<string, unknown> | unknown[];
2
+ type ThemeConfigRecord = Record<string, ThemeConfigValue>;
3
+ export declare function useThemeConfig(): {
4
+ activeThemeConfig: import("vue").ComputedRef<ThemeConfigRecord>;
5
+ getThemeOption: <T = ThemeConfigValue>(key: string, fallback?: T) => T | undefined;
6
+ setThemeOption: (key: string, value: ThemeConfigValue) => void;
7
+ removeThemeOption: (key: string) => void;
8
+ replaceThemeConfig: (nextConfig: ThemeConfigRecord) => void;
9
+ };
10
+ export {};
@@ -0,0 +1,88 @@
1
+ import { computed, watch } from "vue";
2
+ import { useCookie, useState } from "nuxt/app";
3
+ import { themeEngineOptions } from "#build/theme-engine.options.mjs";
4
+ import { useThemeEngine } from "./useThemeEngine.js";
5
+ const CONFIG_STATE_KEY = "theme-engine:config-store";
6
+ const CONFIG_STORAGE_KEY = "theme-engine:theme-config";
7
+ const CONFIG_COOKIE_KEY_SUFFIX = "-config";
8
+ let themeConfigInitialized = false;
9
+ let themeConfigWatchRegistered = false;
10
+ function parseStoredConfig(raw) {
11
+ if (!raw) {
12
+ return {};
13
+ }
14
+ try {
15
+ const parsed = JSON.parse(raw);
16
+ return parsed && typeof parsed === "object" ? parsed : {};
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+ export function useThemeConfig() {
22
+ const { currentTheme } = useThemeEngine();
23
+ const cookie = useCookie(`${themeEngineOptions.cookieKey}${CONFIG_COOKIE_KEY_SUFFIX}`, {
24
+ default: () => ({})
25
+ });
26
+ const store = useState(CONFIG_STATE_KEY, () => cookie.value ?? {});
27
+ if (import.meta.client && !themeConfigInitialized) {
28
+ const storageValue = parseStoredConfig(window.localStorage.getItem(CONFIG_STORAGE_KEY));
29
+ store.value = Object.keys(storageValue).length > 0 ? storageValue : cookie.value ?? {};
30
+ themeConfigInitialized = true;
31
+ }
32
+ if (import.meta.client && !themeConfigWatchRegistered) {
33
+ watch(store, (value) => {
34
+ cookie.value = value;
35
+ if (import.meta.client) {
36
+ window.localStorage.setItem(CONFIG_STORAGE_KEY, JSON.stringify(value));
37
+ }
38
+ }, {
39
+ deep: true,
40
+ immediate: true
41
+ });
42
+ themeConfigWatchRegistered = true;
43
+ }
44
+ const activeThemeConfig = computed(() => {
45
+ return store.value[currentTheme.value] ?? {};
46
+ });
47
+ function getThemeOption(key, fallback) {
48
+ const value = activeThemeConfig.value[key];
49
+ return value ?? fallback;
50
+ }
51
+ function setThemeOption(key, value) {
52
+ const themeName = currentTheme.value;
53
+ const currentConfig = store.value[themeName] ?? {};
54
+ store.value = {
55
+ ...store.value,
56
+ [themeName]: {
57
+ ...currentConfig,
58
+ [key]: value
59
+ }
60
+ };
61
+ }
62
+ function removeThemeOption(key) {
63
+ const themeName = currentTheme.value;
64
+ const currentConfig = {
65
+ ...store.value[themeName] ?? {}
66
+ };
67
+ delete currentConfig[key];
68
+ store.value = {
69
+ ...store.value,
70
+ [themeName]: currentConfig
71
+ };
72
+ }
73
+ function replaceThemeConfig(nextConfig) {
74
+ store.value = {
75
+ ...store.value,
76
+ [currentTheme.value]: {
77
+ ...nextConfig
78
+ }
79
+ };
80
+ }
81
+ return {
82
+ activeThemeConfig,
83
+ getThemeOption,
84
+ setThemeOption,
85
+ removeThemeOption,
86
+ replaceThemeConfig
87
+ };
88
+ }
@@ -0,0 +1,6 @@
1
+ export declare function useThemeEngine(): {
2
+ currentTheme: Readonly<import("vue").Ref<string, string>>;
3
+ availableThemes: import("vue").ComputedRef<any[]>;
4
+ themeDefinitions: import("vue").ComputedRef<any[]>;
5
+ setTheme: (themeName: string) => boolean;
6
+ };
@@ -0,0 +1,68 @@
1
+ import { computed, readonly, watch } from "vue";
2
+ import { useCookie, useRoute, useState } from "nuxt/app";
3
+ import { themeEngineOptions, themeEngineThemeNames, themeEngineThemes } from "#build/theme-engine.options.mjs";
4
+ const CURRENT_THEME_KEY = "theme-engine:current-theme";
5
+ const STORAGE_KEY = "theme-engine:storage";
6
+ let themeEngineInitialized = false;
7
+ let themeEngineWatchRegistered = false;
8
+ function isThemeName(value) {
9
+ return Boolean(value) && themeEngineThemeNames.includes(value);
10
+ }
11
+ function resolveRouteTheme() {
12
+ const route = useRoute();
13
+ const queryTheme = route.query.theme;
14
+ return typeof queryTheme === "string" ? queryTheme : void 0;
15
+ }
16
+ function resolveInitialTheme(cookieKey, fallbackTheme) {
17
+ const routeTheme = resolveRouteTheme();
18
+ if (isThemeName(routeTheme)) {
19
+ return routeTheme;
20
+ }
21
+ const cookie = useCookie(cookieKey);
22
+ if (isThemeName(cookie.value)) {
23
+ return cookie.value;
24
+ }
25
+ if (import.meta.client) {
26
+ const storageTheme = window.localStorage.getItem(STORAGE_KEY);
27
+ if (isThemeName(storageTheme)) {
28
+ return storageTheme;
29
+ }
30
+ }
31
+ return fallbackTheme;
32
+ }
33
+ export function useThemeEngine() {
34
+ const currentTheme = useState(CURRENT_THEME_KEY, () => themeEngineOptions.defaultTheme);
35
+ if (import.meta.server || !themeEngineInitialized) {
36
+ currentTheme.value = resolveInitialTheme(themeEngineOptions.cookieKey, themeEngineOptions.defaultTheme);
37
+ if (import.meta.client) {
38
+ themeEngineInitialized = true;
39
+ }
40
+ }
41
+ const cookie = useCookie(themeEngineOptions.cookieKey);
42
+ if (import.meta.client && !themeEngineWatchRegistered) {
43
+ watch(currentTheme, (themeName) => {
44
+ cookie.value = themeName;
45
+ if (import.meta.client) {
46
+ window.localStorage.setItem(STORAGE_KEY, themeName);
47
+ }
48
+ }, {
49
+ immediate: true
50
+ });
51
+ themeEngineWatchRegistered = true;
52
+ }
53
+ const availableThemes = computed(() => [...themeEngineThemeNames]);
54
+ const themeDefinitions = computed(() => [...themeEngineThemes]);
55
+ function setTheme(themeName) {
56
+ if (!isThemeName(themeName)) {
57
+ return false;
58
+ }
59
+ currentTheme.value = themeName;
60
+ return true;
61
+ }
62
+ return {
63
+ currentTheme: readonly(currentTheme),
64
+ availableThemes,
65
+ themeDefinitions,
66
+ setTheme
67
+ };
68
+ }
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,64 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import { themeCssVariableReport } from "#build/theme-engine.css-report.mjs";
4
+ import { themeComponentRegistry } from "#build/theme-engine.registry.mjs";
5
+ import { themeEngineOptions } from "#build/theme-engine.options.mjs";
6
+ import { useThemeConfig } from "../composables/useThemeConfig";
7
+ import { useThemeEngine } from "../composables/useThemeEngine";
8
+ const { currentTheme, availableThemes, themeDefinitions, setTheme } = useThemeEngine();
9
+ const { activeThemeConfig } = useThemeConfig();
10
+ const currentRegistryEntries = computed(() => {
11
+ return Object.entries(themeComponentRegistry[currentTheme.value] ?? {});
12
+ });
13
+ </script>
14
+
15
+ <template>
16
+ <main class="theme-engine-devtools">
17
+ <section class="theme-engine-card">
18
+ <h1>Theme Engine</h1>
19
+ <p>当前主题:<strong>{{ currentTheme }}</strong></p>
20
+ <div class="theme-engine-actions">
21
+ <button
22
+ v-for="themeName in availableThemes"
23
+ :key="themeName"
24
+ :class="{ active: themeName === currentTheme }"
25
+ type="button"
26
+ @click="setTheme(themeName)"
27
+ >
28
+ {{ themeName }}
29
+ </button>
30
+ </div>
31
+ </section>
32
+
33
+ <section class="theme-engine-grid">
34
+ <article class="theme-engine-card">
35
+ <h2>模块选项</h2>
36
+ <pre>{{ themeEngineOptions }}</pre>
37
+ </article>
38
+
39
+ <article class="theme-engine-card">
40
+ <h2>主题定义</h2>
41
+ <pre>{{ themeDefinitions }}</pre>
42
+ </article>
43
+
44
+ <article class="theme-engine-card">
45
+ <h2>当前主题组件映射</h2>
46
+ <pre>{{ currentRegistryEntries }}</pre>
47
+ </article>
48
+
49
+ <article class="theme-engine-card">
50
+ <h2>主题配置 KV</h2>
51
+ <pre>{{ activeThemeConfig }}</pre>
52
+ </article>
53
+
54
+ <article class="theme-engine-card">
55
+ <h2>CSS 变量报告</h2>
56
+ <pre>{{ themeCssVariableReport }}</pre>
57
+ </article>
58
+ </section>
59
+ </main>
60
+ </template>
61
+
62
+ <style scoped>
63
+ .theme-engine-devtools{background:#0f172a;color:#e2e8f0;font-family:Inter,system-ui,sans-serif;min-height:100vh;padding:24px}.theme-engine-grid{display:grid;gap:16px;grid-template-columns:repeat(auto-fit,minmax(320px,1fr))}.theme-engine-card{background:rgba(15,23,42,.85);border:1px solid rgba(148,163,184,.3);border-radius:12px;padding:16px}.theme-engine-card pre{background:rgba(2,6,23,.85);border-radius:8px;overflow:auto;padding:12px;white-space:pre-wrap;word-break:break-word}.theme-engine-actions{display:flex;flex-wrap:wrap;gap:8px}.theme-engine-actions button{background:transparent;border:1px solid rgba(148,163,184,.35);border-radius:999px;color:inherit;cursor:pointer;padding:8px 12px}.theme-engine-actions button.active{background:rgba(56,189,248,.2);border-color:rgba(56,189,248,.7)}
64
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: import("vue").DefineComponent<{}, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,2 @@
1
+ declare const _default: import("nuxt/app").Plugin<Record<string, unknown>> & import("nuxt/app").ObjectPlugin<Record<string, unknown>>;
2
+ export default _default;
@@ -0,0 +1,9 @@
1
+ import { watchEffect } from "vue";
2
+ import { defineNuxtPlugin } from "nuxt/app";
3
+ import { useThemeEngine } from "../composables/useThemeEngine.js";
4
+ export default defineNuxtPlugin(() => {
5
+ const { currentTheme } = useThemeEngine();
6
+ watchEffect(() => {
7
+ document.documentElement.dataset.theme = currentTheme.value;
8
+ });
9
+ });
@@ -0,0 +1,13 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "baseUrl": "../.."
5
+ },
6
+ "include": [
7
+ "./**/*.ts",
8
+ "./**/*.vue",
9
+ "./**/*.d.ts",
10
+ "../types.ts",
11
+ "../../packages/theme-contracts/src/**/*.ts"
12
+ ]
13
+ }
@@ -0,0 +1,7 @@
1
+ import type { NuxtModule } from '@nuxt/schema'
2
+
3
+ import type { default as Module } from './module.mjs'
4
+
5
+ export type ModuleOptions = typeof Module extends NuxtModule<infer O> ? Partial<O> : Record<string, any>
6
+
7
+ export { default } from './module.mjs'
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@tixxin/nuxt-theme-engine",
3
+ "version": "0.0.1",
4
+ "type": "module",
5
+ "description": "A Nuxt 4 theme engine with layered themes, typed contracts, and runtime theme switching.",
6
+ "license": "MIT",
7
+ "main": "./dist/module.mjs",
8
+ "types": "./dist/types.d.mts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/types.d.mts",
12
+ "import": "./dist/module.mjs"
13
+ },
14
+ "./runtime/*": "./dist/runtime/*"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "dev": "nuxi dev playground",
21
+ "dev:prepare": "nuxi prepare playground",
22
+ "build": "pnpm --filter @tixxin/theme-contracts build && nuxt-module-build build",
23
+ "prepack": "pnpm build",
24
+ "lint": "eslint .",
25
+ "typecheck": "pnpm dev:prepare && tsc --noEmit -p tsconfig.json && vue-tsc --noEmit -p playground/.nuxt/tsconfig.json"
26
+ },
27
+ "peerDependencies": {
28
+ "nuxt": "^4.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@tixxin/theme-contracts": "workspace:*",
32
+ "defu": "^6.1.6",
33
+ "fast-glob": "^3.3.3",
34
+ "pathe": "^2.0.3",
35
+ "zod": "^4.3.6"
36
+ },
37
+ "devDependencies": {
38
+ "@nuxt/devtools-kit": "4.0.0-alpha.3",
39
+ "@nuxt/kit": "^4.4.2",
40
+ "@nuxt/module-builder": "^1.0.2",
41
+ "@types/node": "^25.5.0",
42
+ "@unocss/nuxt": "^66.6.7",
43
+ "@unocss/transformer-directives": "^66.6.7",
44
+ "eslint": "^10.1.0",
45
+ "nuxt": "^4.4.2",
46
+ "typescript": "^5.9.3",
47
+ "unbuild": "^3.6.1",
48
+ "unocss": "^66.6.7",
49
+ "vue": "^3.5.31",
50
+ "vue-tsc": "^3.2.6"
51
+ }
52
+ }