skinsuite 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/CHANGELOG.md ADDED
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ 本包遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/):0.x 阶段的次要版本允许重命名与结构调整(公开出口是显式具名导出,改名即 semver-minor)。
4
+
5
+ > **命名沿革**:2026-09-27 当天两度改名,最终定为 **`skinsuite`**:
6
+ > ① 初版发布名 `mauth-theme-core@0.1.0`(前缀取自宿主的 `mauth-*` 主题层,对外没有意义);
7
+ > ② 改 `multitheme` 时被 npm 拒绝(403:与已存在的 `multi-theme` 过于相似,官方建议改 scope);
8
+ > ③ 定名 `skinsuite`("皮肤 / 配色套件",对应这套机制里 `?skin=` 那一层)。
9
+ > 旧名 `mauth-theme-core` 的 `0.1.0` / `0.1.1` 均已标记 **deprecated** 并保留可安装,不再更新 ——
10
+ > 它们的代码与 `skinsuite@0.1.0` 完全一致,只有包名不同。
11
+
12
+ ## 0.1.0 — 2026-09-27
13
+
14
+ 首个公开发布。内容 = 主题机制里的**纯叶子层**(抽包 Stage 1),从宿主 [`oauth21`](https://github.com/yijiu2025/CoreFlow) 的 `src/theme/` 原样迁出,宿主侧改为零逻辑转发壳。
15
+
16
+ ### 新增
17
+
18
+ - **常量与白名单**(`src/constants.ts`)
19
+ - `THEME_ID_RE` —— 目录名 / id 白名单(主题包名、配色 id、版式 id、页面名共用一条)
20
+ - `DEFAULT_THEME_PACKAGE` / `DEFAULT_THEME_PAGE` / `DEFAULT_THEME_ID` —— 内置包、兜底页面、空记录占位
21
+ - `DEFAULT_THEME_COLOR = 'white'` / `DEFAULT_THEME_DARK_COLOR = 'black'` —— **显式**默认配色与暗系配对(取代原先靠字母序巧合的兜底口径)
22
+ - `BASE_VIEW_ID = 'base'` —— 版式注册表工厂的逻辑 id
23
+ - **明暗 intent**(`src/mode.ts`)—— `ThemeMode` 三态含 `system`、`MODE_CYCLE` 循环顺序、`MODE_LABELS` 文案、`isThemeMode` / `normalizeMode`(容忍大小写与空白,非法返回 `null`)
24
+ - **明暗系别**(`src/tone.ts`)—— `ThemeTone` 两系、`TONE_LABELS` 文案、`isThemeTone` / `normalizeTone`(无 `system`:系别是配色自带属性)
25
+ - **token 安全校验**(`src/tokens.ts`)—— 名字前缀白名单、取值白名单(拒颜色名与 `url()`)、`--mauth-font-family` 字体栈特例、`sanitizeOverrides` 逐条过滤并返回被拒项
26
+ - **契约类型**(`src/types.ts`)—— `MauthThemeMeta` / `MauthThemePackage` / `MauthThemeColor` / `MauthThemeRecord`,含包级 `views` 声明与 `coverage` 范围声明
27
+ - **设备维度参数**(`src/views/params.ts`)—— `readDeviceParam(query, name, device)` 与 `DEVICE_SCOPED_PARAMS`,取值链 `?view.<设备>` → `?view` → 未指定(空串 / 纯空白 / 空数组均视为未指定)
28
+
29
+ ### 交付形态
30
+
31
+ - ESM only(`type: module`),`exports` 指向 `dist/`(含 `.js` + `.d.ts` + sourcemap),同时发布 `src/` 便于跳转源码
32
+ - `sideEffects: false`、零运行时依赖、Node >= 18
33
+ - `src/` 的相对 import 统一带 `.js` 后缀(`moduleResolution: NodeNext` 产物可被原生 ESM 解析)
34
+ - 测试:`node --test` 针对**构建产物**跑(顺带验证产物能被 Node 原生加载),14 项覆盖上述全部公开行为
35
+
36
+ ### 说明
37
+
38
+ - 设备取值集合(`mobile` / `standard` / `mini` …)**不属于本包**:那是宿主应用的词汇表,必须在宿主的设备清单里声明
39
+ - 配套的 Vue 适配层(store / DOM 注入 / `import.meta.glob` 适配器)尚未抽包,计划为 `mauth-theme-vue`,见 README 的 Roadmap
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 qirly
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,273 @@
1
+ # skinsuite
2
+
3
+ 多主题 / 多版式机制的**纯内核**:把「主题包 → 设备 → 页面 → 配色」这套机制里**与业务、与框架都无关**的那一半抽成一个零运行时依赖的 npm 包。
4
+
5
+ > **English TL;DR** — `skinsuite` is the framework-agnostic core of a multi-theme / multi-layout system: whitelisted constants, theme-package contract types, *mode* (light/dark/system) vs *tone* (light/dark color family), safe CSS-variable token validation, and device-scoped URL parameter resolution. Zero runtime dependencies, no DOM, no bundler APIs, no router — usable from Vue, React, plain Node or SSR. Everything DOM/bundler/store related is intentionally **your** adapter layer (see [What this package deliberately does not do](#什么不在这个包里)).
6
+
7
+ ```bash
8
+ npm install skinsuite
9
+ ```
10
+
11
+ ---
12
+
13
+ ## 它解决什么问题
14
+
15
+ 把一个 app 里打磨成熟的多主题机制搬到别的前端时,真正值得复用、也**最容易走样**的是这几件事:
16
+
17
+ | 机制 | 手写时的典型事故 |
18
+ | --- | --- |
19
+ | 目录名 / id 白名单 | 主题目录名直接拼进 `data-*` 属性与 CSS 选择器,一个大写字母就是静默不生效 |
20
+ | 明暗(`mode`)与色系(`tone`)的联动 | 两个维度被混成一个布尔值 →「开夜间」切不到深色配色,或者切过去就再也回不来 |
21
+ | CSS 变量(token)的安全校验 | 后端/父页面下发的值直接 `setProperty`,等于把 CSS 注入和外发请求的口子交出去 |
22
+ | 设备维度的 URL 参数 | `?view.mobile=compact&view.standard=default` 各端各一套的取值链,抄一份就漂移一份 |
23
+
24
+ 本包把这些做成一串**纯函数 + 常量**:零运行时依赖、`sideEffects: false`、可在 Node / SSR / 单测里直接跑,也可以在 Vue 与 React 里共用同一份口径。
25
+
26
+ 它的出身地是 [`oauth21`](https://github.com/yijiu2025/CoreFlow) 的移动端认证页(3 设备 × 3 页面 × 多配色 / 多主题包),但**包里没有任何一行是那个页面特有的**——这就是为什么它可以被你直接用。
27
+
28
+ ---
29
+
30
+ ## 30 秒上手
31
+
32
+ ### 1. 安全地接受外部下发的 CSS 变量
33
+
34
+ ```js
35
+ import { sanitizeOverrides } from 'skinsuite';
36
+
37
+ // 后端配置 / 父应用 postMessage 同步 / URL 参数 —— 一律视为不可信输入
38
+ const { tokens, rejected } = sanitizeOverrides(payload);
39
+
40
+ for (const name of Object.keys(tokens)) {
41
+ document.documentElement.style.setProperty(name, tokens[name]);
42
+ }
43
+ if (rejected.length) reportToSentry('theme: illegal token', rejected);
44
+ ```
45
+
46
+ 非法条目被**逐条丢弃**而非整体拒绝(`rejected` 会告诉你哪几条、为什么),所以后端写错一个色值不会让整站退回默认外观。
47
+
48
+ ### 2. 按设备读取 URL 参数
49
+
50
+ ```js
51
+ import { readDeviceParam } from 'skinsuite';
52
+
53
+ // /login?view=default&view.mobile=compact
54
+ readDeviceParam(route.query, 'view', 'mobile'); // 'compact' 设备专属优先
55
+ readDeviceParam(route.query, 'view', 'standard'); // 'default' 该设备没指定 → 通用键
56
+ readDeviceParam({}, 'view', 'mobile'); // undefined 由调用方决定兜底
57
+ ```
58
+
59
+ ---
60
+
61
+ ## 心智模型
62
+
63
+ ```
64
+ themes/<主题包>/<设备>/<页面>/colors/<配色>/
65
+ │ │ │ │
66
+ │ │ │ └─ 配色 id:一套扁平 token(+ 可选 theme.scss)
67
+ │ │ └──────────── 页面:login / register / forgot-password …
68
+ │ └───────────────────── 设备:**宿主**的词汇表(mobile / standard / mini / tablet …)
69
+ └──────────────────────────────── 一个主题包 = 一种版式
70
+ ```
71
+
72
+ 四条固定口径(它们决定了下面所有 API 的形状):
73
+
74
+ | 维度 | 谁定义 | 说明 |
75
+ | --- | --- | --- |
76
+ | **设备**(device) | **宿主**(你) | 本包刻意**不认识**具体设备名。「加一种设备」只改宿主清单,内核无感 —— 这是它能跨项目复用的前提 |
77
+ | **页面**(page) | 宿主 | 每页一份 `.vue`,只在**当前包 × 当前设备**下查找 |
78
+ | **配色**(color) | 主题包 | 每套配色自带 `tone`(白系 / 黑系),唯一性作用域是「同包 × 同设备 × 同页面」 |
79
+ | **明暗**(mode) | 用户偏好 | 与配色正交,通过 `tone` 联动:切「暗」→ 落到黑系配色,切「明」→ 落回白系 |
80
+
81
+ ⚠️ **明暗 ≡ 色系**,不是两套规则:明暗切换的本质是"切到哪一系配色"。这条是本包 `mode` / `tone` 两个模块分工的依据(`tone` 是配色**自带**的属性,`mode` 是用户的意图)。
82
+
83
+ ---
84
+
85
+ ## API
86
+
87
+ 唯一公开出口是包根 `skinsuite`,采用**显式具名导出**(不用 `export *`)—— 下面这张表就是包的承诺,表里没有的名字不该被依赖。
88
+
89
+ ### 常量与兜底口径
90
+
91
+ ```js
92
+ import {
93
+ THEME_ID_RE,
94
+ DEFAULT_THEME_ID,
95
+ DEFAULT_THEME_PACKAGE,
96
+ DEFAULT_THEME_PAGE,
97
+ DEFAULT_THEME_COLOR,
98
+ DEFAULT_THEME_DARK_COLOR,
99
+ BASE_VIEW_ID
100
+ } from 'skinsuite';
101
+ ```
102
+
103
+ | 导出 | 值 | 用途 |
104
+ | --- | --- | --- |
105
+ | `THEME_ID_RE` | `/^[a-z0-9-]+$/` | 目录名 / id 白名单:主题包名、配色 id、版式 id、页面名都要过这条(它们会进属性选择器) |
106
+ | `DEFAULT_THEME_PACKAGE` | `'default'` | 内置默认**主题包**(首屏静态引入、无需惰性加载的那个包) |
107
+ | `DEFAULT_THEME_PAGE` | `'login'` | 未指定页面时的兜底(认证流程的最常见入口) |
108
+ | `DEFAULT_THEME_COLOR` | `'white'` | **默认配色**(显式值,不是"字母序第一"—— 那样加一个 `amber` 就会静默改掉默认外观) |
109
+ | `DEFAULT_THEME_DARK_COLOR` | `'black'` | 暗系配对值,与上一行成对使用 |
110
+ | `DEFAULT_THEME_ID` | `'default'` | 空记录(注册表还没产出任何记录)的**占位** id,**不是**默认配色 |
111
+ | `BASE_VIEW_ID` | `'base'` | 版式注册表工厂的**逻辑** id;它永远不该出现在配色键里 |
112
+
113
+ ### 明暗 `mode`
114
+
115
+ ```js
116
+ import { MODE_CYCLE, MODE_LABELS, isThemeMode, normalizeMode } from 'skinsuite';
117
+ ```
118
+
119
+ - `ThemeMode = 'system' | 'light' | 'dark'` —— 含 `system`,旧版只有布尔值时"切过就回不来"
120
+ - `MODE_CYCLE = ['system', 'light', 'dark']` —— 切换按钮的循环顺序
121
+ - `normalizeMode(raw)` —— 容忍大小写与首尾空白,非法返回 `null`(是否回退由你决定),而不是抛错
122
+
123
+ ### 色系 `tone`
124
+
125
+ ```js
126
+ import { THEME_TONES, TONE_LABELS, isThemeTone, normalizeTone } from 'skinsuite';
127
+ ```
128
+
129
+ - `ThemeTone = 'light' | 'dark'` —— **没有** `system`:系别是配色自带的属性,不存在"跟随系统"
130
+ - `tone` 不参与渲染(不是 CSS 变量),唯一用途是判断"当前配色与目标明暗是否一致"
131
+
132
+ ### token 安全校验
133
+
134
+ ```js
135
+ import {
136
+ isSafeTokenName,
137
+ isSafeTokenValue,
138
+ isSafeTokenEntry,
139
+ sanitizeOverrides
140
+ } from 'skinsuite';
141
+ import type { ThemeTokenOverrides, SanitizeResult, ThemeTokenLayers } from 'skinsuite';
142
+ ```
143
+
144
+ - token 名必须是 `--mauth-<小写短横线>`(避免误伤 `--background` 这类全局变量)
145
+ - 取值逐条走**白名单**:`#hex` / `rgb()` / `hsl()` / 长度 / `var(--mauth-*)` / 少量关键字
146
+ - `--mauth-font-family` 是唯一的例外(字体栈天然含空格、逗号、引号,且含括号即被拒)
147
+ - `sanitizeOverrides(input)` → `{ tokens, rejected }`
148
+
149
+ 详见下方[安全边界](#安全边界)。
150
+
151
+ ### 契约类型(编译期擦除)
152
+
153
+ ```js
154
+ import type {
155
+ MauthThemeMeta,
156
+ MauthThemePackage,
157
+ MauthThemeColor,
158
+ MauthThemeRecord
159
+ } from 'skinsuite';
160
+ ```
161
+
162
+ 主题包定义、配色定义,以及注册表里一条已解析好的记录。它们的文档注释就是规格说明(我们会保持注释与实现一致)。
163
+
164
+ ### 设备维度参数
165
+
166
+ ```js
167
+ import { DEVICE_SCOPED_PARAMS, readDeviceParam } from 'skinsuite';
168
+ import type { DeviceScopedParam } from 'skinsuite';
169
+
170
+ readDeviceParam(query, name, device);
171
+ // `query['<name>.<device>']` → `query[name]` → undefined
172
+ ```
173
+
174
+ - query key 里带 `.` 合法,但访问必须用下标:`query['view.standard']`
175
+ - **空串 / 纯空白 / 空数组 = 未指定**,不顶掉通用键(部署方习惯把参数留空表示"这台设备不特殊指定")
176
+ - 只决定"取哪个键",**不做合法性校验** —— 校验需要"当前包里有没有这套东西"的知识,那是宿主的事
177
+
178
+ ---
179
+
180
+ ## 什么不在这个包里
181
+
182
+ 内核不该知道宿主 —— 这条原则是它可复用的全部理由。以下都由**你的适配层**提供:
183
+
184
+ | 不在包里 | 原因 |
185
+ | --- | --- |
186
+ | `import.meta.glob` / `import.meta.env` | 构建器专有(webpack 用 `require.context`,测试环境喂数组),同一份内核才能适配多种来源 |
187
+ | `document` / `localStorage` / `matchMedia` | DOM 与存储;SSR 与单测需要一个替代实现 |
188
+ | Pinia store / 路由 | 应用自己的世界观 —— 路由尤其如此(路径语义、`meta` 约定) |
189
+ | 设备取值集合 | `mobile` / `standard` / `mini` 是**某个 app** 的词汇表,不是机制的一部分 |
190
+ | 业务资产(`themes/**`) | 版式实现与配色数据是"你的页面长什么样",随应用走 |
191
+
192
+ 配套的宿主 Vue 适配层(store + DOM 注入 + glob 适配器)计划单发一个包 `skinsuite-vue`,见 [Roadmap](#roadmap)。
193
+
194
+ ### 在自有项目里接入(Vite 示例)
195
+
196
+ 安装后无需任何别名配置:包自带类型声明,`import { … } from 'skinsuite'` 直接可用。
197
+ 真正要你写的是**一层薄适配**——把本包的纯函数接到你的平台上:
198
+
199
+ ```ts
200
+ // src/theme/index.ts —— 你的适配层
201
+ import { THEME_ID_RE, sanitizeOverrides } from 'skinsuite';
202
+
203
+ // ① 主题包:构建期扫出来(Vite 专有,留在宿主)
204
+ const packages = import.meta.glob('./themes/*/index.ts', { eager: true });
205
+
206
+ // ② 写入 DOM:值走本包的白名单,写到哪一层是你自己的活
207
+ export function applyTokens(el: HTMLElement, overrides?: Record<string, string>) {
208
+ const { tokens } = sanitizeOverrides(overrides);
209
+ for (const [k, v] of Object.entries(tokens)) el.style.setProperty(k, v);
210
+ }
211
+ ```
212
+
213
+ 要点:**注入的位置由你定,校验的规则由本包给**。这几件事因此能被复用到任何宿主:名称白名单、明暗 / 系别归一、token 安全、设备维度取值链。
214
+
215
+ ---
216
+
217
+ ## 安全边界
218
+
219
+ CSS 自定义属性是能**直接改变渲染**的输入。未经校验就 `setProperty` 等于把两个口子交给后端或父页面:
220
+
221
+ - **外发请求**:值里塞一个指向第三方的 `url()` → 可用于探测内网存活、追踪用户
222
+ - **样式注入**:值里塞能闭合规则再起新规则的文本 → 注入任意 CSS
223
+
224
+ 所以本包用**白名单**而不是黑名单过滤,并且刻意不放行:
225
+
226
+ | 不放行 | 原因 |
227
+ | --- | --- |
228
+ | CSS 颜色名(`red` / `blue`) | 若放宽成"任意字母",会连带放行 `expression` 这类历史攻击向量 |
229
+ | `url()` | 外发请求的唯一入口;背景图/字体请走构建期就存在于仓库里的 `theme.scss` |
230
+ | 含 `;` `{}` `<>` 反斜杠、注释、`@import` 的值 | 规则闭合与 @ 规则注入 |
231
+
232
+ 配套约定:**值怎么给**
233
+
234
+ | 值 | 走哪条路 |
235
+ | --- | --- |
236
+ | 颜色、圆角、间距、字体族名 | `tokens`(→ `--mauth-*` 变量,过白名单) |
237
+ | 背景图、webfont、动画、伪元素、结构级差异 | `theme.scss`(写真实 CSS,选择器带 `html[data-mauth-theme='<id>']` 前缀) |
238
+
239
+ 判据很简单:**必须写 `url()` 或需要伪元素 / keyframes 的,就不能走 token。**
240
+
241
+ ---
242
+
243
+ ## Roadmap
244
+
245
+ 本包当前只包含"纯叶子层"(Stage 1)。迁移是分期的,每期保持宿主侧的导入面不变,任何一期可独立回滚:
246
+
247
+ | 期 | 内容 | 状态 |
248
+ | --- | --- | --- |
249
+ | Stage 1 | 常量与白名单、`types`、`tone`、`mode`、设备维度参数、token 形态与校验 | ✅ 已发布(本版本) |
250
+ | Stage 2 | 版式注册表工厂 + 版式选择器(四档链:`?view.<设备>` → `?view` → 包声明 → 环境变量 → 当前包) | 计划中 |
251
+ | Stage 3 | 配色注册表与回退链(`createColorRegistry` 等) | 计划中 |
252
+ | Stage 4 | store 的纯逻辑(URL 意图归一、明暗联动规则);DOM 与存储拆成适配器 | 计划中 |
253
+ | Stage 5 | 配套 Vue 适配层 `skinsuite-vue` | 计划中 |
254
+
255
+ 在 1.0 之前(0.x),次要版本可能包含重命名 —— 公开出口是显式具名导出,改名即 semver-minor。
256
+
257
+ ---
258
+
259
+ ## 开发
260
+
261
+ ```bash
262
+ npm run typecheck # tsc --noEmit
263
+ npm run build # tsc → dist/(ESM + .d.ts + sourcemap)
264
+ npm test # 先构建,再用 node:test 跑 dist 产物
265
+ ```
266
+
267
+ ⚠️ `src/` 里的相对 import **必须带 `.js` 后缀**:包以 `moduleResolution: NodeNext` 编译,TS 会把 `.js` 映射回 `.ts` 源码,而产物里的相对说明符是原样保留的 —— 不带后缀浏览器与 Node 的原生 ESM 都解析不到。
268
+
269
+ ---
270
+
271
+ ## License
272
+
273
+ [MIT](./LICENSE) © 2026 qirly
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 主题内核的**常量与白名单** —— 设备 / 页面 / 包 / 配色的取值集合
3
+ *
4
+ * 这些值原先散在 `oauth21/src/theme/index.ts`,2026-09-26 抽包 Stage 1 移到这里。
5
+ * 它们是**跨层共享的词汇表**:配色注册表、版式注册表工厂、参数读取、store、
6
+ * 以及目录口径关卡(`.tmp-probe/verify-theme-dirs.mjs`)全部按这几个常量对齐,
7
+ * 所以必须**只有一个来源** —— 改这里,其余各处跟着变。
8
+ *
9
+ * ⚠️ 本文件**零依赖**(不 import 任何东西),也不碰 DOM / 构建器 / 路由:
10
+ * 这是 `skinsuite` 能被任意前端复用的前提(见
11
+ * docs/frontend/theme-package-extraction.md 的「〇、一条原则」)。
12
+ *
13
+ * @author yijiu2025
14
+ */
15
+ /**
16
+ * 目录名 / id 的白名单正则
17
+ *
18
+ * 同时作用于**主题包名**、**配色 id**、**版式 id**、**页面名** —— 它们都会进
19
+ * `data-*` 属性与 CSS 属性选择器,因此只允许小写字母、数字、短横线。
20
+ * 不合规的目录**静默跳过**(一个写坏的目录不该让整页起不来),所以排查
21
+ * "加了主题没生效"时,第一件事就是核这条正则。
22
+ */
23
+ export declare const THEME_ID_RE: RegExp;
24
+ /**
25
+ * 空记录(registered 目录尚未产出任何记录)的占位配色 id
26
+ *
27
+ * ⚠️ 它**不再**表示"默认配色":自 2026-09-24 起黑白是各设备下**具名的正式配色**
28
+ * (`<页面>/colors/{black,white}/`)。**默认配色**是下面的 `DEFAULT_THEME_COLOR`。
29
+ * 这里只用于空记录占位。
30
+ */
31
+ export declare const DEFAULT_THEME_ID = "default";
32
+ /**
33
+ * 内置默认**主题包** id
34
+ *
35
+ * 它用在版式侧:该包的版式由容器**静态引入**(首屏零请求),其它包才需要惰性加载。
36
+ * 见 `theme/views/registry.ts`。
37
+ */
38
+ export declare const DEFAULT_THEME_PACKAGE = "default";
39
+ /**
40
+ * 设备维度:**取值集合由宿主的设备清单声明**,内核不再自带一份
41
+ *
42
+ * === 为什么从这里搬走(2026-09-27)===
43
+ * 内核原先写着 `THEME_DEVICES = ['mobile','standard','mini']` —— 那是**某个应用的**
44
+ * 词汇表,不是机制的一部分。它带来的实际代价是"加一种设备要改好几处",而其中一处
45
+ * 在包里、另外几处在宿主(判定、容器、路由、面板),漏一处就是静默的半生效。
46
+ * 现在:
47
+ * • 内核只留「设备名是个 `[a-z0-9-]` 字符串」这一条**形态**约定;
48
+ * • 取值集合、中文名、判定规则、容器位置,全部在宿主的设备清单里声明
49
+ * (本仓 = `oauth21/src/theme/devices.ts`)。
50
+ * 内核因此可以在任何前端被复用 —— 那个前端有几台设备、叫什么,与内核无关。
51
+ *
52
+ * ⚠️ 内核里所有"需要知道设备取值集合"的函数都已改成**接收它**(而不是查常量):
53
+ * 白名单校验在宿主(`asThemeDevice`),设备维度参数读取只需要"当前设备是谁"。
54
+ */
55
+ /**
56
+ * 兜底页面:注册表类函数在调用点没给页面时的默认值
57
+ *
58
+ * 取 `login` 是因为它是认证流程的默认入口(未指定页面时最可能问的就是它)。
59
+ * 有明确页面的调用点(容器、面板)都应显式传 `page`,不要依赖这个兜底。
60
+ */
61
+ export declare const DEFAULT_THEME_PAGE = "login";
62
+ /**
63
+ * 兜底**配色** id:该范围内没有更具体的指定时用它(2026-09-26 定)
64
+ *
65
+ * 用户口径:**默认版式 = `default` 包,默认配色 = `white`**。
66
+ * 配色注册表先找它;只有该范围里根本没有这套色(如以后某个版式只提供一套自命名的
67
+ * 配色)才退回「该范围 id 字母序第一」。
68
+ *
69
+ * ⚠️ 在它之前,兜底口径是"字母序第一",而 `black < blue < cyan < rainbow < white`
70
+ * → "默认外观"实际落在**黑系**,再靠首屏系别对齐纠正成白系。"两步走"有两个坑:
71
+ * ① 语义撕裂:基线 SCSS 的 `:root` 是浅底,兜底却是深色配色;
72
+ * ② **静默事故**:新增一个比 `black` 更靠前的配色 id(如 `amber`)会改掉默认外观。
73
+ * 写死成 `white` 后,"默认"是一个**可读的显式值**,不再依赖命名巧合。
74
+ */
75
+ export declare const DEFAULT_THEME_COLOR = "white";
76
+ /**
77
+ * 兜底**暗系**配色 id:明暗联动切到「暗」时的标配
78
+ *
79
+ * 与 `DEFAULT_THEME_COLOR` 成对(用户 2026-09-25 定:切「明」→白系、切「暗」→黑系)。
80
+ * 该范围内没有它时退回**该系别**字母序第一套,绝不跨系别硬凑。
81
+ */
82
+ export declare const DEFAULT_THEME_DARK_COLOR = "black";
83
+ /**
84
+ * 基础版式的**逻辑** id(与版式注册表工厂的默认 `baseId` 是同一约定值)
85
+ *
86
+ * ⚠️ 它**不是**配色注册表里的 view 值:配色记录里的 view 段恒等于包名
87
+ * (一个主题包 = 一种版式),`'base'` 永远不会出现在配色键里。
88
+ * 外部 `?view=base` 由版式选择器归一化到当前包名,到不了配色注册表。
89
+ *
90
+ * 保留它只是为了版式工厂的默认值;新增代码不要拿它当配色查找的 view 参数。
91
+ */
92
+ export declare const BASE_VIEW_ID = "base";
93
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AACH,eAAO,MAAM,WAAW,QAAiB,CAAC;AAE1C;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,YAAY,CAAC;AAE1C;;;;;GAKG;AACH,eAAO,MAAM,qBAAqB,YAAY,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,UAAU,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,mBAAmB,UAAU,CAAC;AAE3C;;;;;GAKG;AACH,eAAO,MAAM,wBAAwB,UAAU,CAAC;AAEhD;;;;;;;;GAQG;AACH,eAAO,MAAM,YAAY,SAAS,CAAC"}
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 主题内核的**常量与白名单** —— 设备 / 页面 / 包 / 配色的取值集合
3
+ *
4
+ * 这些值原先散在 `oauth21/src/theme/index.ts`,2026-09-26 抽包 Stage 1 移到这里。
5
+ * 它们是**跨层共享的词汇表**:配色注册表、版式注册表工厂、参数读取、store、
6
+ * 以及目录口径关卡(`.tmp-probe/verify-theme-dirs.mjs`)全部按这几个常量对齐,
7
+ * 所以必须**只有一个来源** —— 改这里,其余各处跟着变。
8
+ *
9
+ * ⚠️ 本文件**零依赖**(不 import 任何东西),也不碰 DOM / 构建器 / 路由:
10
+ * 这是 `skinsuite` 能被任意前端复用的前提(见
11
+ * docs/frontend/theme-package-extraction.md 的「〇、一条原则」)。
12
+ *
13
+ * @author yijiu2025
14
+ */
15
+ /**
16
+ * 目录名 / id 的白名单正则
17
+ *
18
+ * 同时作用于**主题包名**、**配色 id**、**版式 id**、**页面名** —— 它们都会进
19
+ * `data-*` 属性与 CSS 属性选择器,因此只允许小写字母、数字、短横线。
20
+ * 不合规的目录**静默跳过**(一个写坏的目录不该让整页起不来),所以排查
21
+ * "加了主题没生效"时,第一件事就是核这条正则。
22
+ */
23
+ export const THEME_ID_RE = /^[a-z0-9-]+$/;
24
+ /**
25
+ * 空记录(registered 目录尚未产出任何记录)的占位配色 id
26
+ *
27
+ * ⚠️ 它**不再**表示"默认配色":自 2026-09-24 起黑白是各设备下**具名的正式配色**
28
+ * (`<页面>/colors/{black,white}/`)。**默认配色**是下面的 `DEFAULT_THEME_COLOR`。
29
+ * 这里只用于空记录占位。
30
+ */
31
+ export const DEFAULT_THEME_ID = 'default';
32
+ /**
33
+ * 内置默认**主题包** id
34
+ *
35
+ * 它用在版式侧:该包的版式由容器**静态引入**(首屏零请求),其它包才需要惰性加载。
36
+ * 见 `theme/views/registry.ts`。
37
+ */
38
+ export const DEFAULT_THEME_PACKAGE = 'default';
39
+ /**
40
+ * 设备维度:**取值集合由宿主的设备清单声明**,内核不再自带一份
41
+ *
42
+ * === 为什么从这里搬走(2026-09-27)===
43
+ * 内核原先写着 `THEME_DEVICES = ['mobile','standard','mini']` —— 那是**某个应用的**
44
+ * 词汇表,不是机制的一部分。它带来的实际代价是"加一种设备要改好几处",而其中一处
45
+ * 在包里、另外几处在宿主(判定、容器、路由、面板),漏一处就是静默的半生效。
46
+ * 现在:
47
+ * • 内核只留「设备名是个 `[a-z0-9-]` 字符串」这一条**形态**约定;
48
+ * • 取值集合、中文名、判定规则、容器位置,全部在宿主的设备清单里声明
49
+ * (本仓 = `oauth21/src/theme/devices.ts`)。
50
+ * 内核因此可以在任何前端被复用 —— 那个前端有几台设备、叫什么,与内核无关。
51
+ *
52
+ * ⚠️ 内核里所有"需要知道设备取值集合"的函数都已改成**接收它**(而不是查常量):
53
+ * 白名单校验在宿主(`asThemeDevice`),设备维度参数读取只需要"当前设备是谁"。
54
+ */
55
+ /**
56
+ * 兜底页面:注册表类函数在调用点没给页面时的默认值
57
+ *
58
+ * 取 `login` 是因为它是认证流程的默认入口(未指定页面时最可能问的就是它)。
59
+ * 有明确页面的调用点(容器、面板)都应显式传 `page`,不要依赖这个兜底。
60
+ */
61
+ export const DEFAULT_THEME_PAGE = 'login';
62
+ /**
63
+ * 兜底**配色** id:该范围内没有更具体的指定时用它(2026-09-26 定)
64
+ *
65
+ * 用户口径:**默认版式 = `default` 包,默认配色 = `white`**。
66
+ * 配色注册表先找它;只有该范围里根本没有这套色(如以后某个版式只提供一套自命名的
67
+ * 配色)才退回「该范围 id 字母序第一」。
68
+ *
69
+ * ⚠️ 在它之前,兜底口径是"字母序第一",而 `black < blue < cyan < rainbow < white`
70
+ * → "默认外观"实际落在**黑系**,再靠首屏系别对齐纠正成白系。"两步走"有两个坑:
71
+ * ① 语义撕裂:基线 SCSS 的 `:root` 是浅底,兜底却是深色配色;
72
+ * ② **静默事故**:新增一个比 `black` 更靠前的配色 id(如 `amber`)会改掉默认外观。
73
+ * 写死成 `white` 后,"默认"是一个**可读的显式值**,不再依赖命名巧合。
74
+ */
75
+ export const DEFAULT_THEME_COLOR = 'white';
76
+ /**
77
+ * 兜底**暗系**配色 id:明暗联动切到「暗」时的标配
78
+ *
79
+ * 与 `DEFAULT_THEME_COLOR` 成对(用户 2026-09-25 定:切「明」→白系、切「暗」→黑系)。
80
+ * 该范围内没有它时退回**该系别**字母序第一套,绝不跨系别硬凑。
81
+ */
82
+ export const DEFAULT_THEME_DARK_COLOR = 'black';
83
+ /**
84
+ * 基础版式的**逻辑** id(与版式注册表工厂的默认 `baseId` 是同一约定值)
85
+ *
86
+ * ⚠️ 它**不是**配色注册表里的 view 值:配色记录里的 view 段恒等于包名
87
+ * (一个主题包 = 一种版式),`'base'` 永远不会出现在配色键里。
88
+ * 外部 `?view=base` 由版式选择器归一化到当前包名,到不了配色注册表。
89
+ *
90
+ * 保留它只是为了版式工厂的默认值;新增代码不要拿它当配色查找的 view 参数。
91
+ */
92
+ export const BASE_VIEW_ID = 'base';
93
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,WAAW,GAAG,cAAc,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,SAAS,CAAC;AAE1C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,SAAS,CAAC;AAE/C;;;;;;;;;;;;;;;GAeG;AAEH;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAO,CAAC;AAE1C;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,OAAO,CAAC;AAE3C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,OAAO,CAAC;AAEhD;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,MAAM,CAAC"}
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `skinsuite` 的**唯一公开出口**
3
+ *
4
+ * === 这里装什么 ===
5
+ * 主题机制里**与业务、与框架都无关**的那一半:取值白名单、契约类型、系别/明暗取值、
6
+ * 设备维度参数读取、token 形态与校验。它是纯函数 + 数据,可以在任何前端(Vue / React /
7
+ * 单测 / 将来的 SSR)里被同一套代码驱动。
8
+ *
9
+ * ### 不在这里装什么(重要)
10
+ * • **DOM 与注入**(`documentElement`、`localStorage`、`matchMedia`)→ 由宿主适配器提供
11
+ * • **构建器耦合**(`import.meta.glob` / `import.meta.env`)→ 由宿主适配器提供
12
+ * • **路由**(vue-router)→ 应用自己的世界观
13
+ * • **业务资产**(`themes/**` 的版式与配色实现)→ 随应用走
14
+ * 见 `docs/frontend/theme-package-extraction.md` 的「〇、一条原则」与「五、四个注入点契约」。
15
+ *
16
+ * === 为什么用显式具名导出,而不是 `export *` ===
17
+ * 立项文档 §四 有一张"公开 API 面"的表,本文件就是那张表的**落地**:表里没有的名字
18
+ * 就不该从这里出去(如内部用的正则、解析器)。用 `export *` 会让"包到底承诺了什么"
19
+ * 变成一份隐式清单,加一个内部导出就悄悄扩大了对外承诺。
20
+ * 代价是新增导出要手工补一行 —— 这是刻意的摩擦。
21
+ *
22
+ * ⚠️ 相对 import **必须带 `.js` 后缀**(`from './tone.js'` 而不是 `'./tone'`):
23
+ * 包以 `moduleResolution: NodeNext` 编译到 `dist/`,TS 会把 `.js` 映射回 `.ts` 源码,
24
+ * 而**产物**里的相对说明符是原样保留的 —— 不带后缀的话浏览器与 Node 的原生 ESM
25
+ * 都解析不到。Vite / webpack 侧的别名消费源码时同样认这条 TS 约定,所以不用改两遍。
26
+ *
27
+ * ⚠️ 公开面一旦发出就是承诺:改名/删名要先想清楚下游(当前只有 `oauth21` 一个消费者,
28
+ * 所以现在改是免费的,见立项文档 §八)。
29
+ *
30
+ * @author yijiu2025
31
+ */
32
+ export { THEME_ID_RE, DEFAULT_THEME_ID, DEFAULT_THEME_PACKAGE, DEFAULT_THEME_PAGE, DEFAULT_THEME_COLOR, DEFAULT_THEME_DARK_COLOR, BASE_VIEW_ID } from './constants.js';
33
+ export { THEME_TONES, TONE_LABELS, isThemeTone, normalizeTone } from './tone.js';
34
+ export type { ThemeTone } from './tone.js';
35
+ export { MODE_CYCLE, MODE_LABELS, isThemeMode, normalizeMode } from './mode.js';
36
+ export type { ThemeMode } from './mode.js';
37
+ export { isSafeTokenName, isSafeTokenValue, isSafeTokenEntry, sanitizeOverrides } from './tokens.js';
38
+ export type { ThemeTokenOverrides, SanitizeResult, ThemeTokenLayers } from './tokens.js';
39
+ export type { MauthThemeMeta, MauthThemePackage, MauthThemeColor, MauthThemeRecord } from './types.js';
40
+ export { DEVICE_SCOPED_PARAMS, readDeviceParam } from './views/params.js';
41
+ export type { DeviceScopedParam } from './views/params.js';
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AACjF,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAChF,YAAY,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AACrB,YAAY,EAAE,mBAAmB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAEzF,YAAY,EACV,cAAc,EACd,iBAAiB,EACjB,eAAe,EACf,gBAAgB,EACjB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAC1E,YAAY,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `skinsuite` 的**唯一公开出口**
3
+ *
4
+ * === 这里装什么 ===
5
+ * 主题机制里**与业务、与框架都无关**的那一半:取值白名单、契约类型、系别/明暗取值、
6
+ * 设备维度参数读取、token 形态与校验。它是纯函数 + 数据,可以在任何前端(Vue / React /
7
+ * 单测 / 将来的 SSR)里被同一套代码驱动。
8
+ *
9
+ * ### 不在这里装什么(重要)
10
+ * • **DOM 与注入**(`documentElement`、`localStorage`、`matchMedia`)→ 由宿主适配器提供
11
+ * • **构建器耦合**(`import.meta.glob` / `import.meta.env`)→ 由宿主适配器提供
12
+ * • **路由**(vue-router)→ 应用自己的世界观
13
+ * • **业务资产**(`themes/**` 的版式与配色实现)→ 随应用走
14
+ * 见 `docs/frontend/theme-package-extraction.md` 的「〇、一条原则」与「五、四个注入点契约」。
15
+ *
16
+ * === 为什么用显式具名导出,而不是 `export *` ===
17
+ * 立项文档 §四 有一张"公开 API 面"的表,本文件就是那张表的**落地**:表里没有的名字
18
+ * 就不该从这里出去(如内部用的正则、解析器)。用 `export *` 会让"包到底承诺了什么"
19
+ * 变成一份隐式清单,加一个内部导出就悄悄扩大了对外承诺。
20
+ * 代价是新增导出要手工补一行 —— 这是刻意的摩擦。
21
+ *
22
+ * ⚠️ 相对 import **必须带 `.js` 后缀**(`from './tone.js'` 而不是 `'./tone'`):
23
+ * 包以 `moduleResolution: NodeNext` 编译到 `dist/`,TS 会把 `.js` 映射回 `.ts` 源码,
24
+ * 而**产物**里的相对说明符是原样保留的 —— 不带后缀的话浏览器与 Node 的原生 ESM
25
+ * 都解析不到。Vite / webpack 侧的别名消费源码时同样认这条 TS 约定,所以不用改两遍。
26
+ *
27
+ * ⚠️ 公开面一旦发出就是承诺:改名/删名要先想清楚下游(当前只有 `oauth21` 一个消费者,
28
+ * 所以现在改是免费的,见立项文档 §八)。
29
+ *
30
+ * @author yijiu2025
31
+ */
32
+ export { THEME_ID_RE, DEFAULT_THEME_ID, DEFAULT_THEME_PACKAGE, DEFAULT_THEME_PAGE, DEFAULT_THEME_COLOR, DEFAULT_THEME_DARK_COLOR, BASE_VIEW_ID } from './constants.js';
33
+ export { THEME_TONES, TONE_LABELS, isThemeTone, normalizeTone } from './tone.js';
34
+ export { MODE_CYCLE, MODE_LABELS, isThemeMode, normalizeMode } from './mode.js';
35
+ export { isSafeTokenName, isSafeTokenValue, isSafeTokenEntry, sanitizeOverrides } from './tokens.js';
36
+ export { DEVICE_SCOPED_PARAMS, readDeviceParam } from './views/params.js';
37
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,EACL,WAAW,EACX,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,EAClB,mBAAmB,EACnB,wBAAwB,EACxB,YAAY,EACb,MAAM,gBAAgB,CAAC;AAExB,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAGjF,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAGhF,OAAO,EACL,eAAe,EACf,gBAAgB,EAChB,gBAAgB,EAChB,iBAAiB,EAClB,MAAM,aAAa,CAAC;AAUrB,OAAO,EAAE,oBAAoB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/mode.d.ts ADDED
@@ -0,0 +1,24 @@
1
+ /**
2
+ * 明暗意图(mode)的取值、循环顺序与展示文案
3
+ *
4
+ * 🔴 明暗 ≡ 色系(2026-09-25 定):「明暗切换就是切换白色和黑色这个色系,不是另一套规则」。
5
+ * mode 不是独立于配色的第二个维度,而是「切到哪一系别」的意图:
6
+ * light —— 明 = 白系
7
+ * dark —— 暗 = 黑系
8
+ * system —— 跟随系统偏好(系统暗→黑系、亮→白系)
9
+ * 最终明暗由**当前配色的 tone** 决定(见 `oauth21/src/stores/theme.ts` 的 `isDark`)。
10
+ * 点色卡同步 mode(点黑卡=dark、点白卡=light),切 mode 联动配色 —— 永远一致。
11
+ *
12
+ * @author yijiu2025
13
+ */
14
+ /** 明暗三态:'system' 显式跟随系统(旧版只有布尔值,切过就回不来) */
15
+ export type ThemeMode = 'system' | 'light' | 'dark';
16
+ /** 切换按钮的循环顺序:跟随系统 → 浅色 → 深色 → 跟随系统 */
17
+ export declare const MODE_CYCLE: readonly ThemeMode[];
18
+ /** 界面与无障碍标签用的中文文案(明=白系 / 暗=黑系 / 跟随系统) */
19
+ export declare const MODE_LABELS: Record<ThemeMode, string>;
20
+ /** 是否是合法的明暗取值(用于校验 URL 参数、后端下发这类不可信输入) */
21
+ export declare function isThemeMode(value: unknown): value is ThemeMode;
22
+ /** 归一化外部输入:大小写与首尾空白容忍,非法返回 null(由调用方决定回退) */
23
+ export declare function normalizeMode(raw: unknown): ThemeMode | null;
24
+ //# sourceMappingURL=mode.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mode.d.ts","sourceRoot":"","sources":["../src/mode.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,2CAA2C;AAC3C,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD,sCAAsC;AACtC,eAAO,MAAM,UAAU,EAAE,SAAS,SAAS,EAAgC,CAAC;AAE5E,yCAAyC;AACzC,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAIjD,CAAC;AAEF,0CAA0C;AAC1C,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,SAAS,CAE9D;AAED,6CAA6C;AAC7C,wBAAgB,aAAa,CAAC,GAAG,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAI5D"}