skinsuite 0.1.0 → 0.2.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.
Files changed (82) hide show
  1. package/README.md +28 -6
  2. package/dist/constants.d.ts +44 -7
  3. package/dist/constants.d.ts.map +1 -1
  4. package/dist/constants.js +44 -7
  5. package/dist/constants.js.map +1 -1
  6. package/dist/index.d.ts +32 -6
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +25 -4
  9. package/dist/index.js.map +1 -1
  10. package/dist/manifest.d.ts +83 -0
  11. package/dist/manifest.d.ts.map +1 -0
  12. package/dist/manifest.js +90 -0
  13. package/dist/manifest.js.map +1 -0
  14. package/dist/registry.d.ts +73 -0
  15. package/dist/registry.d.ts.map +1 -0
  16. package/dist/registry.js +299 -0
  17. package/dist/registry.js.map +1 -0
  18. package/dist/remote-config.d.ts +25 -0
  19. package/dist/remote-config.d.ts.map +1 -0
  20. package/dist/remote-config.js +43 -0
  21. package/dist/remote-config.js.map +1 -0
  22. package/dist/scheme.d.ts +36 -0
  23. package/dist/scheme.d.ts.map +1 -0
  24. package/dist/scheme.js +47 -0
  25. package/dist/scheme.js.map +1 -0
  26. package/dist/types.d.ts +17 -1
  27. package/dist/types.d.ts.map +1 -1
  28. package/dist/views/device-registry.d.ts +45 -0
  29. package/dist/views/device-registry.d.ts.map +1 -0
  30. package/dist/views/device-registry.js +55 -0
  31. package/dist/views/device-registry.js.map +1 -0
  32. package/dist/views/device.d.ts +63 -0
  33. package/dist/views/device.d.ts.map +1 -0
  34. package/dist/views/device.js +59 -0
  35. package/dist/views/device.js.map +1 -0
  36. package/dist/views/params.d.ts +24 -21
  37. package/dist/views/params.d.ts.map +1 -1
  38. package/dist/views/params.js +11 -11
  39. package/dist/views/params.js.map +1 -1
  40. package/docs/onboarding.md +472 -0
  41. package/package.json +22 -1
  42. package/src/constants.ts +141 -100
  43. package/src/index.ts +128 -65
  44. package/src/manifest.ts +131 -0
  45. package/src/registry.ts +400 -0
  46. package/src/scheme.ts +94 -0
  47. package/src/types.ts +19 -1
  48. package/src/views/device-registry.ts +96 -0
  49. package/src/views/device.ts +107 -0
  50. package/src/views/params.ts +26 -23
  51. package/src/vite/dir.ts +85 -0
  52. package/src/vite/index.ts +41 -0
  53. package/src/vite/plugin.ts +409 -0
  54. package/src/vite/theme.d.ts +49 -0
  55. package/src/vue/ThemeDebugPanel.vue +36 -0
  56. package/src/vue/ThemeDebugPanelBody.vue +505 -0
  57. package/src/vue/ThemeRoot.vue +59 -0
  58. package/src/vue/ThemeView.vue +92 -0
  59. package/src/vue/create-theme-state.ts +116 -0
  60. package/src/vue/define-theme-device.ts +157 -0
  61. package/src/vue/device-containers.ts +73 -0
  62. package/src/vue/device-domain.ts +149 -0
  63. package/src/vue/index.ts +98 -0
  64. package/src/vue/panel.ts +16 -0
  65. package/src/vue/plugin.ts +35 -0
  66. package/src/vue/remote-fetch.ts +74 -0
  67. package/src/vue/theme-intent.ts +157 -0
  68. package/src/vue/theme-persistence.ts +136 -0
  69. package/src/vue/theme-runtime.ts +163 -0
  70. package/src/vue/theme-scope.ts +148 -0
  71. package/src/vue/theme-store.ts +58 -0
  72. package/src/vue/theme-views.ts +39 -0
  73. package/src/vue/token-runtime.ts +81 -0
  74. package/src/vue/types.ts +67 -0
  75. package/src/vue/useParentThemeSync.ts +99 -0
  76. package/src/vue/useThemeDebugPanel.ts +192 -0
  77. package/src/vue/useThemeSources.ts +74 -0
  78. package/src/vue/view-page.ts +99 -0
  79. package/src/vue/view-registry.ts +215 -0
  80. package/src/vue/viewport.ts +54 -0
  81. package/src/mode.ts +0 -38
  82. package/src/tone.ts +0 -56
@@ -0,0 +1,472 @@
1
+ # 接入指南(Onboarding)
2
+
3
+ > 面向:要给一个**新的 Vue + Vite 前端**接上 `skinsuite` 的多主题 / 多版式机制。
4
+ > 本文只讲「接入」,不讲「为什么这么设计」——原理见包根 `README.md` 与仓库里的
5
+ > `theme-package-extraction.md` / `theme-replacement.md` / `theme-view-component-design.md`。
6
+ >
7
+ > 全程以 `oauth21`(登录页)为已落地参照,但**不假设你的业务与它相同**:每一步都标注
8
+ > 哪些是「库的固定 API(照抄)」,哪些是「宿主的业务事实(你自己定)」。
9
+
10
+ ---
11
+
12
+ ## 〇、先想清楚你要用到哪几层
13
+
14
+ 接入前花一分钟确认要哪几层,避免把不需要的也搬进来:
15
+
16
+ | 能力 | 入口 | 你需要的场景 |
17
+ |---|---|---|
18
+ | 多配色(明暗 + 色系,换 CSS 变量) | `skinsuite` + `skinsuite/vue` | 必选 |
19
+ | 多版式(同一页换 DOM / 交互组织) | `skinsuite/vue` | 可选(只有一个版式就只建一个主题包) |
20
+ | 多设备(手机 / 桌面 / 平板各自布局) | `skinsuite/vue` | 可选(单端就不声明视口 match) |
21
+ | 调试面板 | `skinsuite/vue` | 可选(`theme.config.ts` 里 `debug: { enabled: 'url' }`) |
22
+
23
+ **最小接入 = 多配色**:只建一个主题包、一个设备、一个页面,也能完整跑起来。
24
+
25
+ ---
26
+
27
+ ## 一、安装与依赖
28
+
29
+ ```bash
30
+ npm install skinsuite
31
+ ```
32
+
33
+ 库是**零运行时依赖**的纯内核,`vue` / `vite` 都是 `optional` peer dependency——你项目里
34
+ 本就有它们,无需额外安装。
35
+
36
+ 三个入口(包名 → 用途):
37
+
38
+ | 入口 | 作用 | 谁用 |
39
+ |---|---|---|
40
+ | `skinsuite` | 纯内核:常量、类型、token 校验、设备/配色取值 | 任何地方(含 Node 脚本、单测) |
41
+ | `skinsuite/vue` | Vue 绑定:`<ThemeView>` / `<ThemeRoot>` / 状态机 / 设备域 | Vue 组件与 store |
42
+ | `skinsuite/vite` | 构建期插件:`themeAssets` 扫主题目录生成 `virtual:theme` | `vite.config.ts` |
43
+
44
+ ---
45
+
46
+ ## 二、目录约定(固定,不配置)
47
+
48
+ 库要求主题域是一个**自足的目录**,内部三个名字是固定约定:
49
+
50
+ ```
51
+ <主题根目录>/ ← 由 env `VITE_THEME_DIR` 指定(缺省 src/theme)
52
+ ├── theme.config.ts 清单:调试开关(defineThemeManifest)
53
+ ├── themes/ 主题包目录(一个包 = 一整套设计)
54
+ │ └── default/ ← 内置包(default 是保留名,eager 首屏零请求)
55
+ │ ├── index.ts 包定义(MauthThemePackage)
56
+ │ └── <设备>/<页面>/ 版式组件 + 配色
57
+ │ ├── index.vue 该设备该页的版式组件
58
+ │ └── colors/<配色>/
59
+ │ ├── index.ts 配色元信息 + tokens
60
+ │ └── theme.scss 附加样式(可选)
61
+ └── views/ 声明目录(设备 + 页面契约)
62
+ ├── devices.ts defineThemeDevice 声明设备
63
+ └── <page>.ts defineThemePage 声明页面
64
+ ```
65
+
66
+ `themes` 和 `views` 这两个子目录名是**固定的**(插件按名字扫描),只有主题根目录位置可配。
67
+
68
+ ---
69
+
70
+ ## 三、写主题资产(theme 根目录)
71
+
72
+ ### 3.1 包定义 `themes/default/index.ts`
73
+
74
+ ```ts
75
+ import type { MauthThemePackage } from 'skinsuite';
76
+
77
+ const pkg: MauthThemePackage = {
78
+ id: 'default',
79
+ name: '默认版式',
80
+ views: {} // 可选:本包声明各页默认用哪个配色 / 版式
81
+ };
82
+
83
+ export default pkg;
84
+ ```
85
+
86
+ > `MauthThemePackage` 的完整字段以 `skinsuite` 的 `types.ts` 为准;最小可只给 `id` + `name`。
87
+
88
+ ### 3.2 配色 `themes/default/<设备>/<页面>/colors/<配色>/index.ts`
89
+
90
+ ```ts
91
+ import type { MauthThemeColor } from 'skinsuite';
92
+
93
+ const color: MauthThemeColor = {
94
+ id: 'light',
95
+ tone: 'light', // 'light' | 'dark' —— 明暗系别,与 mode 联动的关键
96
+ tokens: {
97
+ '--mauth-bg': '#ffffff',
98
+ '--mauth-text': '#1f2937'
99
+ // ... 你的设计 token
100
+ }
101
+ };
102
+
103
+ export default color;
104
+ ```
105
+
106
+ ### 3.3 版式组件 `themes/default/<设备>/<页面>/index.vue`
107
+
108
+ ```vue
109
+ <script setup lang="ts">
110
+ import type { LoginViewProps } from '@/theme/views/login';
111
+
112
+ // 版式只渲染 ctx、只调 ctx.actions,禁止写业务(请求 / 校验 / store / 路由)
113
+ defineProps<LoginViewProps>();
114
+ </script>
115
+
116
+ <template>
117
+ <div class="login-layout">
118
+ <!-- 用 ctx.fields.email.value / ctx.actions.submit() 等 -->
119
+ </div>
120
+ </template>
121
+ ```
122
+
123
+ ---
124
+
125
+ ## 四、写声明文件(views 目录)
126
+
127
+ ### 4.1 设备声明 `views/devices.ts`
128
+
129
+ ```ts
130
+ import type { Component } from 'vue';
131
+ import { THEME_ID_RE } from 'skinsuite';
132
+ import { defineThemeDevice } from 'skinsuite/vue';
133
+
134
+ // 判定顺序 = 声明顺序,最后一条必须 fallback(兜底)
135
+ defineThemeDevice({
136
+ id: 'mobile',
137
+ label: '手机端',
138
+ urlKey: 'mobile', // URL 参数里用的缩写(?th_mobile=...)
139
+ containers: {
140
+ glob: import.meta.glob<{ default: Component }>('/src/view/app/*/index.vue'),
141
+ fileRe: /^index\.vue$/,
142
+ idRe: THEME_ID_RE
143
+ },
144
+ routePath: page => `m/${page}`,
145
+ match: { explicitMobile: true, viewportMobile: true }
146
+ });
147
+
148
+ defineThemeDevice({
149
+ id: 'standard',
150
+ label: '桌面端',
151
+ urlKey: 'std',
152
+ containers: {
153
+ glob: import.meta.glob<{ default: Component }>('/src/view/web/*/*.vue'),
154
+ fileRe: /^Standard/,
155
+ idRe: THEME_ID_RE
156
+ },
157
+ routePath: page => page,
158
+ match: { fallback: true }
159
+ });
160
+ ```
161
+
162
+ `match` 里的信号是声明式的:`explicitMobile`(`?isMobile=true`)、`viewportMobile`(视口窄)、
163
+ `from` / `fromLogin` / `pathSegment`(URL 来源标记)。`defineThemeDevice` 会自动注册进设备域、
164
+ 自动建容器表,**不要再手动写任何转发函数**。
165
+
166
+ ### 4.2 页面契约 `views/<page>.ts`
167
+
168
+ ```ts
169
+ import { defineThemePage } from 'skinsuite/vue';
170
+
171
+ // —— 版式契约类型(纯类型,无运行时):容器传给版式的 ctx ——
172
+ export interface LoginViewContext {
173
+ t: (key: string, params?: Record<string, unknown> | string) => string;
174
+ fields: Record<'email' | 'password', { value: string; attrs: Record<string, unknown>; invalid: boolean; error?: string }>;
175
+ submitting: boolean;
176
+ actions: {
177
+ submit(): void;
178
+ switchMode(mode: 'email' | 'pwd'): void;
179
+ };
180
+ }
181
+
182
+ export interface LoginViewProps {
183
+ ctx: LoginViewContext;
184
+ }
185
+
186
+ // 一行声明:把本页注册进页面域(<ThemeView> 靠它取版式)
187
+ defineThemePage({ page: 'login' });
188
+ ```
189
+
190
+ **注意**:页面契约文件是**纯副作用 + 纯类型**,不要在这里 import `virtual:theme`、不要自己
191
+ `import.meta.glob`。版式组件清单由内核从 `virtual:theme` 惰性解析。
192
+
193
+ ### 4.3 清单 `theme.config.ts`
194
+
195
+ ```ts
196
+ import { defineThemeManifest } from 'skinsuite';
197
+
198
+ export default defineThemeManifest({
199
+ // 调试面板:'off' | 'url' | 'always'(见 §九)
200
+ debug: { enabled: 'url', query: 'debug', value: 'theme' }
201
+ });
202
+ ```
203
+
204
+ ---
205
+
206
+ ## 五、vite.config.ts(构建期接入)
207
+
208
+ ```ts
209
+ import { defineConfig } from 'vite';
210
+ import vue from '@vitejs/plugin-vue';
211
+ import { themeAssets, resolveThemeDir } from 'skinsuite/vite';
212
+
213
+ export default defineConfig(({ mode }) => ({
214
+ plugins: [
215
+ // 顺序:themeAssets 在前、vue 在后
216
+ themeAssets({ dir: resolveThemeDir(mode) }),
217
+ vue()
218
+ ]
219
+ }));
220
+ ```
221
+
222
+ `themeAssets` 扫 `VITE_THEME_DIR` 指向的目录,产出虚拟模块 `virtual:theme`(`packages` /
223
+ `colors` / `styles` / `themeViews` / `themeManifest`),并在模块被 import 时**自动装配**。
224
+
225
+ 要点:
226
+ - **构建与单测两侧必须用同一份插件**(否则单测的 `virtual:theme` 是空表,用例静默落默认值)。
227
+ 建议抽成一个 `sharedPlugins(mode)` 工厂,`vite.config.ts` 和 `vitest.config.ts` 共用。
228
+ - `resolveThemeDir` 记得显式传 `root`(工程根),不要依赖 `process.cwd()`。
229
+
230
+ ### 5.1 `.env`
231
+
232
+ ```env
233
+ VITE_THEME_DIR=src/theme
234
+ ```
235
+
236
+ 缺省就是 `src/theme`,可不写。**换目录只改这一个变量,不动代码。**
237
+
238
+ ### 5.2 tsconfig / env 类型(可选)
239
+
240
+ `skinsuite/vite` 自带 `virtual:theme` 的类型声明,宿主通过引入 `skinsuite/vite` 即可获得,
241
+ 一般无需自写。若你的 `vue-tsc` 报找不到 `virtual:theme`,确认 `vite.config.ts` 已被 tsconfig
242
+ 纳入检查范围,或补一句:
243
+
244
+ ```ts
245
+ /// <reference types="skinsuite/vite" />
246
+ ```
247
+
248
+ ---
249
+
250
+ ## 六、bootstrap.ts(启动编排,从这开始写)
251
+
252
+ 这是「接入」的核心。启动顺序:pinia → router → i18n → theme。
253
+
254
+ ```ts
255
+ import { createApp } from 'vue';
256
+ import { createPinia } from 'pinia';
257
+ import App from './App.vue';
258
+ import router from './router';
259
+ import i18n from './i18n';
260
+ import { theme } from 'skinsuite/vue';
261
+
262
+ const app = createApp(App);
263
+
264
+ app.use(createPinia());
265
+ app.use(router);
266
+ app.use(i18n);
267
+
268
+ // 设备域启动:传 router + 宿主自己的「设备覆盖」解析。
269
+ // 库只认 router 的最小结构(currentRoute.value.{ query, path, fullPath }),
270
+ // 不 import vue-router,不写死任何 URL 词汇。
271
+ app.use(theme, {
272
+ router,
273
+ // ?device=<id> 是宿主自己的 URL 约定,用注入点交给库(返回 null = 不覆盖)
274
+ resolveDeviceOverride: query => (typeof query.device === 'string' ? query.device : null)
275
+ });
276
+
277
+ router.isReady().then(() => app.mount('#app'));
278
+ ```
279
+
280
+ **`resolveDeviceOverride` 的语义**:宿主想让「某个 URL 参数能锁定设备(压过视口自动判定)」
281
+ 时,通过它注入解析逻辑。参数名(`device`)是宿主词汇,库不认识也不写死;返回合法设备 id
282
+ 即锁定、返回 `null` 或非法 id 走自动判定。**不需要这个能力就不传,字段可选。**
283
+
284
+ ---
285
+
286
+ ## 七、store(宿主壳,pinia 不下沉)
287
+
288
+ ```ts
289
+ import { defineStore } from 'pinia';
290
+ import { createThemeState } from 'skinsuite/vue';
291
+ import 'virtual:theme'; // 副作用:装配版式表 + 注册表 + 清单 + 拉齐 views/*.ts 声明
292
+
293
+ export const useThemeStore = defineStore('theme', () => createThemeState());
294
+ ```
295
+
296
+ 三句话讲清边界:
297
+ 1. **状态机已下沉** `createThemeState`——scope(明暗意图)/ intent(配色意图)/ runtime
298
+ (token 注入)+ 持久化全在库内,宿主一行包成 store。
299
+ 2. **pinia 不下沉**——状态机零 pinia API,库不为此加 peer 依赖;`defineStore` 只是壳。
300
+ 3. **`import 'virtual:theme'` 必须保留**——它触发全部副作用装配,宿主**不要再手动 import
301
+ 任何 `@/theme/views/*`**(bootstrap 里也不 import)。
302
+
303
+ > `createThemeState(options?)` 可选 `ThemeStateOptions`(`storageKeys` / `colorParam`),
304
+ > 用于覆盖 localStorage 键名和 URL 配色参数名;缺省对齐 oauth21 现状。
305
+
306
+ ---
307
+
308
+ ## 八、App.vue(外壳 + 调试面板)
309
+
310
+ ```vue
311
+ <script setup lang="ts">
312
+ import { ThemeRoot, ThemeDebugPanel } from 'skinsuite/vue';
313
+ import { useThemeStore } from '@/stores/theme';
314
+
315
+ const themeStore = useThemeStore();
316
+ </script>
317
+
318
+ <template>
319
+ <ThemeRoot :theme-store="themeStore">
320
+ <router-view />
321
+ <ThemeDebugPanel />
322
+ </ThemeRoot>
323
+ </template>
324
+ ```
325
+
326
+ `<ThemeRoot :themeStore>` 是**纯外观壳**(挂 `.dark` 类 + 用 token 决定 body 底色),同时把
327
+ store `provide` 给子树,`<ThemeView>` 靠它 `inject`。`<ThemeDebugPanel>` 独立挂载,`?debug=theme`
328
+ 才下载 / 显示。
329
+
330
+ ---
331
+
332
+ ## 九、路由与容器
333
+
334
+ ### 9.1 路由表(宿主事实)
335
+
336
+ 路由是**宿主固定声明**的,页面名编码在 path 里(`/<page>` 桌面、`/m/<page>` 手机)。库**不
337
+ 驱动路由、不声明标题**——`document.title` 归路由 `meta.title`。
338
+
339
+ ```ts
340
+ import type { RouteRecordRaw } from 'vue-router';
341
+
342
+ export const authRoutes: RouteRecordRaw[] = [
343
+ { path: '/login', name: 'login', component: () => import('@/view/web/login/index.vue'),
344
+ meta: { title: '登录', requiresAuth: false } },
345
+ { path: '/m/login', name: 'mobile-login', component: () => import('@/view/web/login/index.vue'),
346
+ meta: { title: '登录' } }
347
+ ];
348
+ ```
349
+
350
+ ### 9.2 容器(业务逻辑)与版式(UI)分离
351
+
352
+ 容器只写业务:表单校验、ctx 组装、请求、路由。版式组件由 `<ThemeView>` 按「页面 + 设备」自动
353
+ 选择并渲染:
354
+
355
+ #### 容器放哪:**页面名 = 文件上一级目录名**
356
+
357
+ `pageContainers()` 从 glob 结果的 `a/b/<页面>/<文件>.vue` 里取 `parts[length-2]` 当**页面名**,
358
+ 再用各设备声明的 `fileRe` 筛文件名。于是两份声明各自解读同一批文件:
359
+
360
+ | 设备 | glob(`views/devices.ts`) | `fileRe` | 实际位置约定 |
361
+ |---|---|---|---|
362
+ | `mobile` | `/src/view/app/*/index.vue` | `/^index\.vue$/` | `view/app/<页面>/index.vue` |
363
+ | `tablet` | `/src/view/tablet/*/index.vue` | `/^index\.vue$/` | `view/tablet/<页面>/index.vue` |
364
+ | `standard` | `/src/view/web/*/*.vue` | `/^Standard/` | `view/web/<页面>/Standard<页面>.vue` |
365
+ | `mini` | `/src/view/web/*/*.vue` | `/^Mini/` | `view/web/<页面>/Mini<页面>.vue` |
366
+
367
+ 🔴 **`standard` / `mini` 共用 `view/web/`,靠文件名前缀区分设备、靠目录名区分页面**。把
368
+ `StandardLogin.vue` 直接摊在 `view/web/` 下 ⇒ 页面名被算成 `web` ⇒ 该设备在这一页「没写容器」⇒
369
+ 分发器 `resolveContainerOf()` 取不到(`standard` 是兜底,连兜底也空就 reject)。
370
+
371
+ `import.meta.glob` 的 `*` **不跨 `/`**,所以 `web/*/*.vue` 恰好吃「一层目录 + 一个文件」,不需要 `**`。
372
+
373
+ ```vue
374
+ <script setup lang="ts">
375
+ import { ThemeView } from 'skinsuite/vue';
376
+ import type { LoginViewContext } from '@/theme/views/login';
377
+ import { computed } from 'vue';
378
+
379
+ const ctx = computed<LoginViewContext>(() => ({
380
+ t: k => k,
381
+ fields: { /* ... */ },
382
+ submitting: false,
383
+ actions: { submit() { /* ... */ }, switchMode() { /* ... */ } }
384
+ }));
385
+ </script>
386
+
387
+ <template>
388
+ <ThemeView page="login" device="mobile" :ctx="ctx" />
389
+ </template>
390
+ ```
391
+
392
+ `<ThemeView>` 三个 props:
393
+ - `page`:页面名(与 `defineThemePage({ page })` 一致)
394
+ - `device`:设备名(**容器文件位置即身份**,无需从别处推断)
395
+ - `ctx`:传给版式组件的业务上下文
396
+
397
+ `<ThemeView>` 内部自动完成:选版式(URL `?th[_<设备缩写>]` > 主题包声明 > 当前包名)→ 惰性
398
+ 加载 → epoch 防抖回退 → 通知 store 当前页 / 版式。**不要在容器里再写 `setActiveDevice` /
399
+ `setActivePageView`**——那些是旧版样板,现在都是冗余。
400
+
401
+ ---
402
+
403
+ ## 十、验证清单(接完按这个跑)
404
+
405
+ | 项 | 命令 / 动作 | 期望 |
406
+ |---|---|---|
407
+ | 类型检查 | `vue-tsc -b` | 0 error(`virtual:theme` 有类型、`createThemeState` 面完整) |
408
+ | lint | `eslint .` | 0 error |
409
+ | 单测 | `vitest run` | 全绿(确认 `vitest.config` 也挂了 `themeAssets`) |
410
+ | 构建 | `vite build` | 成功,内置包 eager、非内置包独立 chunk |
411
+ | 手测 | `?th=<包>` / `?th_<设备缩写>=<包>` | 版式切换 |
412
+ | 手测 | `?debug=theme` | 调试面板出现,可切配色 |
413
+ | 手测 | 缩放窗口 / 换 UA | 手机 / 桌面自动切换(若声明了视口 match) |
414
+ | 手测 | `?device=mobile`(若注入 override) | 锁定手机端,刷新后失效 |
415
+
416
+ ---
417
+
418
+ ## 十一、常见坑(每一条都踩过)
419
+
420
+ 1. **页面契约 import 了 `virtual:theme`** → 形成 `store → virtual:theme → 组件 → 容器 → store`
421
+ 循环。页面契约必须是纯声明(只 `defineThemePage({ page })`),版式表由内核惰性解析。
422
+
423
+ 2. **手写 `import.meta.glob('/src/theme/themes/...')`** → 换目录就失效。扫描全交给
424
+ `themeAssets`,主题目录位置只由 `VITE_THEME_DIR` 决定。
425
+
426
+ 3. **单测没挂 `themeAssets`** → `virtual:theme` 是空表,用例不报错、只静默落默认值,极难归因。
427
+ build 与 test 两侧必须共用同一份插件工厂。
428
+
429
+ 4. **设备判定写了两份**(容器一套 `pickDeviceId`、设备域一套)→ 口径不一致时手机端渲染
430
+ 但主题是桌面作用域。设备判定只认设备域 `currentDevice()` 一个来源。
431
+
432
+ 5. **在容器里手写 `setActiveDevice` / `setActivePageView`** → 冗余且会被 `<ThemeView>` 覆盖。
433
+ 全删,交给 `<ThemeView>`。
434
+
435
+ 6. **URL 词汇(`?device=` 等)写进库里** → 边界错。库只提供通用注入点(`resolveDeviceOverride`),
436
+ 参数名是宿主词汇,只写在宿主装配处。
437
+
438
+ 7. **容器摊平在 `view/web/` 下**(`web/StandardLogin.vue`)→ 页面名被算成 `web`,该页在这一设备上
439
+ 等于「没写容器」。`standard` / `mini` 必须各自待在 `view/web/<页面>/` 里,见 §9.2。
440
+ **症状**:类型检查 / lint / build 全绿(glob 是静态的、文件确实被打进产物),只在运行时分发
441
+ 取不到容器才炸。
442
+
443
+ ---
444
+
445
+ ## 附:一个最小可跑的接入骨架(目录总览)
446
+
447
+ ```
448
+ my-app/
449
+ ├── vite.config.ts # themeAssets({ dir: resolveThemeDir(mode) }) + vue()
450
+ ├── .env # VITE_THEME_DIR=src/theme(可省)
451
+ └── src/
452
+ ├── bootstrap.ts # app.use(theme, { router, resolveDeviceOverride? })
453
+ ├── App.vue # <ThemeRoot :theme-store> + <ThemeDebugPanel>
454
+ ├── stores/theme.ts # defineStore('theme', () => createThemeState()) + import 'virtual:theme'
455
+ ├── router/index.ts # 固定声明 /<page>、/m/<page>
456
+ ├── view/ # 容器(业务),模板 <ThemeView page device :ctx>
457
+ │ ├── app/<页面>/index.vue # mobile(目录名 = 页面名)
458
+ │ ├── tablet/<页面>/index.vue # tablet(同上;常薄转发 standard)
459
+ │ └── web/<页面>/Standard<页面>.vue # standard + Mini<页面>.vue 同目录(见 §9.2)
460
+ └── theme/ # 主题域(自足目录)
461
+ ├── theme.config.ts # defineThemeManifest({ debug })
462
+ ├── views/
463
+ │ ├── devices.ts # defineThemeDevice 声明设备
464
+ │ └── login.ts # 契约类型 + defineThemePage({ page: 'login' })
465
+ └── themes/default/
466
+ ├── index.ts # 包定义
467
+ └── mobile/login/
468
+ ├── index.vue # 版式组件(只渲染 ctx)
469
+ └── colors/light/
470
+ ├── index.ts # MauthThemeColor(tokens)
471
+ └── theme.scss
472
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skinsuite",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "多主题 / 多版式机制的纯内核(零运行时依赖):目录名与默认值的白名单常量、主题包契约类型、明暗 mode 与色系 tone、CSS 变量 token 的安全校验、设备维度 URL 参数读取。不碰 DOM / 构建器 / 路由,可在任意前端(Vue / React / Node / SSR)复用。",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -11,11 +11,20 @@
11
11
  "types": "./dist/index.d.ts",
12
12
  "import": "./dist/index.js"
13
13
  },
14
+ "./vue": {
15
+ "types": "./src/vue/index.ts",
16
+ "import": "./src/vue/index.ts"
17
+ },
18
+ "./vite": {
19
+ "types": "./src/vite/index.ts",
20
+ "import": "./src/vite/index.ts"
21
+ },
14
22
  "./package.json": "./package.json"
15
23
  },
16
24
  "files": [
17
25
  "dist",
18
26
  "src",
27
+ "docs",
19
28
  "README.md",
20
29
  "CHANGELOG.md",
21
30
  "LICENSE"
@@ -53,6 +62,18 @@
53
62
  },
54
63
  "author": "qirly",
55
64
  "license": "MIT",
65
+ "peerDependencies": {
66
+ "vue": "^3.4.0",
67
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
68
+ },
69
+ "peerDependenciesMeta": {
70
+ "vue": {
71
+ "optional": true
72
+ },
73
+ "vite": {
74
+ "optional": true
75
+ }
76
+ },
56
77
  "devDependencies": {
57
78
  "typescript": "^6.0.3"
58
79
  }