@deepseek-ai/dsh-client-ui-theme 0.0.1-rc.2 → 0.0.1-rc.3
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.i18n.yaml +2 -2
- package/README.md +2 -2
- package/README.zh.md +4 -4
- package/lib/client.js +189 -9
- package/lib/index.js +2 -2
- package/lib/styles/design-platform.css +2 -0
- package/lib/types/client/index.d.ts +69 -5
- package/package.json +21 -21
package/README.i18n.yaml
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
3
|
# after editing either side, bring the other along and re-record with:
|
|
4
4
|
# pnpm run verify-translation-pairing --write packages/client/ui-theme/README.md
|
|
5
|
-
README.md:
|
|
6
|
-
README.zh.md:
|
|
5
|
+
README.md: 9388d5c05e5625d1a38b27c4565edf3ed47b3b6d
|
|
6
|
+
README.zh.md: 9995980e4a64b07c376f90e223c1ec83b5881b73
|
package/README.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
English | [中文](README.zh.md)
|
|
4
4
|
|
|
5
|
-
Theme plugin:
|
|
5
|
+
Theme plugin: ThemeRuntime over the --dsw-* token base stylesheets (static scale + alias semantic layers). The service owns the live theme preference (`light`/`dark`/`system`), resolves `system` through `prefers-color-scheme`, and publishes immutable `ThemeSnapshot`s on the `theme/change` event; it never touches the DOM — ui-layout's presenter applies the resolved snapshot (`html { color-scheme }`, `body[data-ds-dark-theme]`, and inline alias tokens). A loopback browser provides the service immediately with `system`, then loads `ui-theme.preference` in the background and writes each built-in selection through the Host settings API, whose local provider stores it in `$DSH_HOME/settings.yaml` by default; pushed settings changes and reconnects refetch it, rapid selections are serialized in gesture order with namespace revisions, and a rejected latest write reloads the durable value. A remote browser cannot access the privileged settings API, so its selection remains process-local. Third-party registered theme ids remain an in-process extension and do not cross the built-in settings schema; removing one never overwrites the last durable built-in preference. The [Host-backed preferences decision](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md) owns the persistence boundary.
|
|
6
6
|
|
|
7
|
-
When the host composition includes an HTTP server, the host half injects a synchronous bootstrap immediately after the opening `<body>` tag. Each index response embeds the registered Host setting for `ui-theme.preference`, or `system` when no settings provider is present; the browser resolves `system` from the OS scheme, then sets `color-scheme` and `body[data-ds-dark-theme]` before the shell loading page renders. Compositions without an HTTP server remain unaffected, and
|
|
7
|
+
When the host composition includes an HTTP server, the host half injects a synchronous bootstrap immediately after the opening `<body>` tag. Each index response embeds the registered Host setting for `ui-theme.preference`, or `system` when no settings provider is present; the browser resolves `system` from the OS scheme, then sets `color-scheme` and `body[data-ds-dark-theme]` before the shell loading page renders. Compositions without an HTTP server remain unaffected, and ThemeRuntime and ui-layout remain authoritative for client state and subsequent DOM updates after the plugin tree activates.
|
|
8
8
|
|
|
9
9
|
`src/styles/` holds five sheets, all imported by the web shell's `base.css`: `base.css`, `design-platform.css`, `scrollbar.css`, `gradient-shadow-text.css`, and `shiki.css`. `scrollbar.css` is the sole consumer of the `--dsw-alias-scrollbar-*` tokens and must follow `design-platform.css`, which declares them.
|
|
10
10
|
|
package/README.zh.md
CHANGED
|
@@ -2,15 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
[English](README.md) | 中文
|
|
4
4
|
|
|
5
|
-
主题插件:基于 --dsw-* token 基础样式表(静态尺度 + 别名语义层)的
|
|
5
|
+
主题插件:基于 --dsw-* token 基础样式表(静态尺度 + 别名语义层)的 ThemeRuntime。该服务拥有实时主题偏好(`light`/`dark`/`system`),将 `system` 通过 `prefers-color-scheme` 解析为实际主题,并发布不可变的 `ThemeSnapshot`,通过 `theme/change` 事件通知变化;它绝不接触 DOM:ui-layout 的呈现器会应用解析后的快照(`html { color-scheme }`、`body[data-ds-dark-theme]`,以及主题的别名 token 内联变量)。来自回环地址的浏览器会先以 `system` 立即提供该服务,随后在后台加载 `ui-theme.preference`,并将每次内置主题选择通过 Host settings API 写入;其本地提供方默认将设置存入 `$DSH_HOME/settings.yaml`。收到推送的 settings 变更时或重连后,浏览器都会重新拉取该设置;连续快速选择会按操作顺序携带 namespace revision 串行写入,最新写入被拒时则重新加载持久化值。远程浏览器无法访问特权 settings API,因此它的选择仅保留在进程内。已注册的第三方主题 id 仍是进程内扩展,不会跨越内置 settings schema;移除其中任意一个都绝不会覆盖最后一个持久化的内置偏好。该持久化边界由[Host settings 支撑的偏好决策](../../../.agents/notes/implemented/bug-fix/2026-08-06-host-backed-web-preferences.md)拥有。
|
|
6
6
|
|
|
7
|
-
当主机组合包含 HTTP 服务器时,主机侧紧接 `<body>` 起始标签注入同步引导代码。每份 index 响应会嵌入已注册的 Host 设置 `ui-theme.preference`,没有 settings provider 时则嵌入 `system`;浏览器按操作系统配色解析 `system`,随后在外壳加载页面渲染前设置 `color-scheme` 和 `body[data-ds-dark-theme]`。不含 HTTP 服务器的组合不受影响,插件树激活后,
|
|
7
|
+
当主机组合包含 HTTP 服务器时,主机侧紧接 `<body>` 起始标签注入同步引导代码。每份 index 响应会嵌入已注册的 Host 设置 `ui-theme.preference`,没有 settings provider 时则嵌入 `system`;浏览器按操作系统配色解析 `system`,随后在外壳加载页面渲染前设置 `color-scheme` 和 `body[data-ds-dark-theme]`。不含 HTTP 服务器的组合不受影响,插件树激活后,ThemeRuntime 与 ui-layout 仍分别是客户端状态和后续 DOM 更新的权威来源。
|
|
8
8
|
|
|
9
9
|
`src/styles/` 下有五张样式表,全部由 web 壳的 `base.css` 导入:`base.css`、`design-platform.css`、`scrollbar.css`、`gradient-shadow-text.css` 与 `shiki.css`。`scrollbar.css` 是 `--dsw-alias-scrollbar-*` token 的唯一消费方,必须排在声明这些 token 的 `design-platform.css` 之后。
|
|
10
10
|
|
|
11
11
|
滚动条重新绑定约定:`scrollbar.css` 在 `body` 上把 `--dsh-scrollbar-thumb` 与 `--dsh-scrollbar-thumb-hover` 绑定到 l1(基础表面)token,两条渲染路径都读取这一组变量。高层级表面(菜单、浮层、对话框)在自己的容器上设置 `--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2)` 与 `--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2)`;一次重新绑定即可为引擎实际走的那条路径换色。这组变量的另一个合法目标是 `transparent`,即完全不绘制滑块——[ui-sidebar](../ui-sidebar/README.md) 在指针不在栏内时就这样重新绑定自己的列。绑回 l1 那组不算重新绑定,它只是重述基础表面的默认值。
|
|
12
12
|
|
|
13
|
-
两条路径在构造上互斥。`scrollbar-width`/`scrollbar-color` 写在 `@supports not selector(::-webkit-scrollbar)` 之内,因为这两个属性中的任一个只要取非 `auto` 值,Chromium 与 Safari 就会丢弃该元素上的全部 `::-webkit-scrollbar*` 规则,`::-webkit-scrollbar-thumb:hover` 也在其中——若无条件地同时声明,`--dsh-scrollbar-thumb-hover` 在任何引擎上都不会被渲染。因此 Firefox 走标准属性,WebKit 系引擎走伪元素,hover token
|
|
13
|
+
两条路径在构造上互斥。`scrollbar-width`/`scrollbar-color` 写在 `@supports not selector(::-webkit-scrollbar)` 之内,因为这两个属性中的任一个只要取非 `auto` 值,Chromium 与 Safari 就会丢弃该元素上的全部 `::-webkit-scrollbar*` 规则,`::-webkit-scrollbar-thumb:hover` 也在其中——若无条件地同时声明,`--dsh-scrollbar-thumb-hover` 在任何引擎上都不会被渲染。因此 Firefox 走标准属性,WebKit 系引擎走伪元素,hover token 只经由伪元素这条路径渲染。相关原理与实测计算值见[滚动条 Agent Note](../../../.agents/notes/implemented/bug-fix/2026-07-28-themed-scrollbars-and-reserved-gutter.md)。
|
|
14
14
|
|
|
15
15
|
## 模型体验
|
|
16
16
|
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
#### KV Cache 影响
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
无;该包既不组装也不发送提供方请求。
|
|
22
22
|
|
|
23
23
|
## 已知限制与暂缓事项
|
|
24
24
|
|
package/lib/client.js
CHANGED
|
@@ -33,11 +33,11 @@ window.__ModuleLoader__.load({
|
|
|
33
33
|
document.head.appendChild(tag);
|
|
34
34
|
}
|
|
35
35
|
var AppearanceRow_module_css_default = {
|
|
36
|
-
"themeCube": "_8HJdBW_themeCube",
|
|
37
36
|
"cubeRow": "_8HJdBW_cubeRow",
|
|
37
|
+
"title": "_8HJdBW_title",
|
|
38
|
+
"themeCube": "_8HJdBW_themeCube",
|
|
38
39
|
"selected": "_8HJdBW_selected",
|
|
39
|
-
"group": "_8HJdBW_group"
|
|
40
|
-
"title": "_8HJdBW_title"
|
|
40
|
+
"group": "_8HJdBW_group"
|
|
41
41
|
};
|
|
42
42
|
//#endregion
|
|
43
43
|
//#region lib/types/client/AppearanceRow.js
|
|
@@ -962,16 +962,111 @@ window.__ModuleLoader__.load({
|
|
|
962
962
|
colorScheme: "dark",
|
|
963
963
|
tokens: Object.freeze({})
|
|
964
964
|
})]);
|
|
965
|
+
const BUILTIN_INSPECT_TOKENS = Object.freeze([
|
|
966
|
+
{
|
|
967
|
+
name: "--dsw-alias-bg-base",
|
|
968
|
+
description: "Application base background.",
|
|
969
|
+
valueType: "CSS color",
|
|
970
|
+
requiresLightAndDark: true,
|
|
971
|
+
cssVariable: "--dsw-alias-bg-base"
|
|
972
|
+
},
|
|
973
|
+
{
|
|
974
|
+
name: "--dsw-alias-bg-layer-1",
|
|
975
|
+
description: "Primary raised surface background.",
|
|
976
|
+
valueType: "CSS color",
|
|
977
|
+
requiresLightAndDark: true,
|
|
978
|
+
cssVariable: "--dsw-alias-bg-layer-1"
|
|
979
|
+
},
|
|
980
|
+
{
|
|
981
|
+
name: "--dsw-alias-bg-layer-2",
|
|
982
|
+
description: "Secondary nested surface background.",
|
|
983
|
+
valueType: "CSS color",
|
|
984
|
+
requiresLightAndDark: true,
|
|
985
|
+
cssVariable: "--dsw-alias-bg-layer-2"
|
|
986
|
+
},
|
|
987
|
+
{
|
|
988
|
+
name: "--dsw-alias-bg-overlay",
|
|
989
|
+
description: "Overlay and popover background.",
|
|
990
|
+
valueType: "CSS color",
|
|
991
|
+
requiresLightAndDark: true,
|
|
992
|
+
cssVariable: "--dsw-alias-bg-overlay"
|
|
993
|
+
},
|
|
994
|
+
{
|
|
995
|
+
name: "--dsw-alias-border-l1",
|
|
996
|
+
description: "Primary subtle border.",
|
|
997
|
+
valueType: "CSS color",
|
|
998
|
+
requiresLightAndDark: true,
|
|
999
|
+
cssVariable: "--dsw-alias-border-l1"
|
|
1000
|
+
},
|
|
1001
|
+
{
|
|
1002
|
+
name: "--dsw-alias-border-l2",
|
|
1003
|
+
description: "Secondary stronger border.",
|
|
1004
|
+
valueType: "CSS color",
|
|
1005
|
+
requiresLightAndDark: true,
|
|
1006
|
+
cssVariable: "--dsw-alias-border-l2"
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "--dsw-alias-brand-primary",
|
|
1010
|
+
description: "Primary brand accent.",
|
|
1011
|
+
valueType: "CSS color",
|
|
1012
|
+
requiresLightAndDark: true,
|
|
1013
|
+
cssVariable: "--dsw-alias-brand-primary"
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
name: "--dsw-alias-label-primary",
|
|
1017
|
+
description: "Primary text color.",
|
|
1018
|
+
valueType: "CSS color",
|
|
1019
|
+
requiresLightAndDark: true,
|
|
1020
|
+
cssVariable: "--dsw-alias-label-primary"
|
|
1021
|
+
},
|
|
1022
|
+
{
|
|
1023
|
+
name: "--dsw-alias-label-secondary",
|
|
1024
|
+
description: "Secondary text color.",
|
|
1025
|
+
valueType: "CSS color",
|
|
1026
|
+
requiresLightAndDark: true,
|
|
1027
|
+
cssVariable: "--dsw-alias-label-secondary"
|
|
1028
|
+
},
|
|
1029
|
+
{
|
|
1030
|
+
name: "--dsw-alias-state-error-primary",
|
|
1031
|
+
description: "Primary error state color.",
|
|
1032
|
+
valueType: "CSS color",
|
|
1033
|
+
requiresLightAndDark: true,
|
|
1034
|
+
cssVariable: "--dsw-alias-state-error-primary"
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: "--dsw-alias-state-success-primary",
|
|
1038
|
+
description: "Primary success state color.",
|
|
1039
|
+
valueType: "CSS color",
|
|
1040
|
+
requiresLightAndDark: true,
|
|
1041
|
+
cssVariable: "--dsw-alias-state-success-primary"
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
name: "--dsw-alias-state-warn-primary",
|
|
1045
|
+
description: "Primary warning state color.",
|
|
1046
|
+
valueType: "CSS color",
|
|
1047
|
+
requiresLightAndDark: true,
|
|
1048
|
+
cssVariable: "--dsw-alias-state-warn-primary"
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
name: "--dsw-specific-sidebar-fill",
|
|
1052
|
+
description: "Sidebar column and title-row background.",
|
|
1053
|
+
valueType: "CSS color",
|
|
1054
|
+
requiresLightAndDark: true,
|
|
1055
|
+
cssVariable: "--dsw-specific-sidebar-fill"
|
|
1056
|
+
}
|
|
1057
|
+
]);
|
|
965
1058
|
/**
|
|
966
1059
|
* Theme registry and preference owner. `light`/`dark` are built in (the base
|
|
967
1060
|
* stylesheets carry both palettes); third-party themes register alias-layer
|
|
968
|
-
* overrides. Reads go through {@link getTheme}; writes only
|
|
969
|
-
* {@link setTheme}; continuous sync only through the `theme/change`
|
|
1061
|
+
* overrides. Reads go through {@link getTheme}; preference writes only
|
|
1062
|
+
* through {@link setTheme}; continuous sync only through the `theme/change`
|
|
1063
|
+
* event. {@link overrideTokens} stacks partial token layers over the active
|
|
1064
|
+
* theme without touching the registry.
|
|
970
1065
|
* The service holds the `prefers-color-scheme` media query (environment
|
|
971
1066
|
* sensing, not presentation) and re-emits when the OS scheme flips while the
|
|
972
1067
|
* preference is `system`.
|
|
973
1068
|
*/
|
|
974
|
-
var
|
|
1069
|
+
var ThemeRuntime = class {
|
|
975
1070
|
ctx;
|
|
976
1071
|
host;
|
|
977
1072
|
themes = [...BUILTIN_THEMES];
|
|
@@ -979,6 +1074,9 @@ window.__ModuleLoader__.load({
|
|
|
979
1074
|
revision = 0;
|
|
980
1075
|
snapshot;
|
|
981
1076
|
media;
|
|
1077
|
+
/** Override layers by source; seq (monotonic) is the stacking order. */
|
|
1078
|
+
overrides = /* @__PURE__ */ new Map();
|
|
1079
|
+
overrideSeq = 0;
|
|
982
1080
|
/**
|
|
983
1081
|
* @param ctx - owning context (change events are emitted on it; the
|
|
984
1082
|
* media-query and scope listeners are released through ctx.effect on dispose).
|
|
@@ -1016,6 +1114,16 @@ window.__ModuleLoader__.load({
|
|
|
1016
1114
|
return this.snapshot;
|
|
1017
1115
|
}
|
|
1018
1116
|
/**
|
|
1117
|
+
* Export the current token directory without reading DOM or computed styles.
|
|
1118
|
+
* @returns stable JSON-safe token descriptions, including registered and override-only names.
|
|
1119
|
+
*/
|
|
1120
|
+
exportInspectTokens() {
|
|
1121
|
+
const tokens = new Map(BUILTIN_INSPECT_TOKENS.map((token) => [token.name, token]));
|
|
1122
|
+
for (const theme of this.themes) for (const name of Object.keys(theme.tokens)) if (!tokens.has(name)) tokens.set(name, dynamicToken(name));
|
|
1123
|
+
for (const layer of this.overrides.values()) for (const name of Object.keys(layer.tokens)) if (!tokens.has(name)) tokens.set(name, dynamicToken(name));
|
|
1124
|
+
return [...tokens.values()].map((token) => ({ ...token })).sort((left, right) => left.name.localeCompare(right.name));
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1019
1127
|
* Switch the theme preference — the only user preference write entry.
|
|
1020
1128
|
* Built-in preferences are written through the settings scope and every
|
|
1021
1129
|
* accepted value emits `theme/change`.
|
|
@@ -1055,6 +1163,35 @@ window.__ModuleLoader__.load({
|
|
|
1055
1163
|
this.publish();
|
|
1056
1164
|
};
|
|
1057
1165
|
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Stack a token override layer on top of the active theme — the token-level
|
|
1168
|
+
* analogue of slot shading: the base theme stays untouched, layers compose
|
|
1169
|
+
* in seq order with later layers winning per-token, and removing a layer
|
|
1170
|
+
* restores whatever it covered. Calling again with the same source replaces
|
|
1171
|
+
* that source's whole layer and restacks it on top (effect re-registration
|
|
1172
|
+
* semantics). Emits `theme/change` with the recomposed snapshot.
|
|
1173
|
+
* @param source - layer identity; one layer per source (dynamic packages
|
|
1174
|
+
* pass their package id — the façade pins it, so it also names the layer's
|
|
1175
|
+
* origin for inspection).
|
|
1176
|
+
* @param tokens - token-name → `{ light, dark }` value pairs. Validated at
|
|
1177
|
+
* runtime (model-authored callers reach this boundary with untyped JS);
|
|
1178
|
+
* a bare string value throws a teaching error.
|
|
1179
|
+
* @returns disposer removing exactly the layer this call created; a no-op
|
|
1180
|
+
* once the source has re-overridden (the newer layer is not torn down).
|
|
1181
|
+
*/
|
|
1182
|
+
overrideTokens(source, tokens) {
|
|
1183
|
+
const layer = {
|
|
1184
|
+
seq: this.overrideSeq++,
|
|
1185
|
+
tokens: validateOverrides(source, tokens)
|
|
1186
|
+
};
|
|
1187
|
+
this.overrides.set(source, layer);
|
|
1188
|
+
this.publish();
|
|
1189
|
+
return () => {
|
|
1190
|
+
if (this.overrides.get(source) !== layer) return;
|
|
1191
|
+
this.overrides.delete(source);
|
|
1192
|
+
this.publish();
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1058
1195
|
buildSnapshot() {
|
|
1059
1196
|
const resolvedId = this.preference === "system" ? this.media?.matches === true ? "dark" : "light" : this.preference;
|
|
1060
1197
|
const active = this.themes.find((t) => t.id === resolvedId);
|
|
@@ -1062,11 +1199,26 @@ window.__ModuleLoader__.load({
|
|
|
1062
1199
|
if (active === void 0) throw new Error(`theme registry lost "${resolvedId}"`);
|
|
1063
1200
|
return Object.freeze({
|
|
1064
1201
|
preference: this.preference,
|
|
1065
|
-
active,
|
|
1202
|
+
active: this.composeActive(active),
|
|
1066
1203
|
themes: Object.freeze([...this.themes]),
|
|
1067
1204
|
revision: this.revision
|
|
1068
1205
|
});
|
|
1069
1206
|
}
|
|
1207
|
+
/**
|
|
1208
|
+
* Fold the override layers into the active definition: seq order, later
|
|
1209
|
+
* layers win per-token, each value picked for the active color scheme (the
|
|
1210
|
+
* presenter consumes the composed snapshot and needs no override awareness).
|
|
1211
|
+
* Without layers the registered definition passes through by identity.
|
|
1212
|
+
*/
|
|
1213
|
+
composeActive(active) {
|
|
1214
|
+
if (this.overrides.size === 0) return active;
|
|
1215
|
+
const tokens = { ...active.tokens };
|
|
1216
|
+
for (const layer of [...this.overrides.values()].sort((a, b) => a.seq - b.seq)) for (const [name, modes] of Object.entries(layer.tokens)) tokens[name] = modes[active.colorScheme];
|
|
1217
|
+
return Object.freeze({
|
|
1218
|
+
...active,
|
|
1219
|
+
tokens: Object.freeze(tokens)
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1070
1222
|
publish() {
|
|
1071
1223
|
this.revision += 1;
|
|
1072
1224
|
this.snapshot = this.buildSnapshot();
|
|
@@ -1074,6 +1226,34 @@ window.__ModuleLoader__.load({
|
|
|
1074
1226
|
}
|
|
1075
1227
|
};
|
|
1076
1228
|
/**
|
|
1229
|
+
* Runtime shape check for one override layer (model-authored callers pass
|
|
1230
|
+
* untyped JS through the dynamic-package façade, so the static type cannot
|
|
1231
|
+
* enforce the pair shape there). Returns a defensive per-token copy so later
|
|
1232
|
+
* caller mutation cannot reach the stored layer.
|
|
1233
|
+
*/
|
|
1234
|
+
function validateOverrides(source, tokens) {
|
|
1235
|
+
const validated = {};
|
|
1236
|
+
for (const [name, value] of Object.entries(tokens)) {
|
|
1237
|
+
if (typeof value === "string") throw new TypeError(`theme override "${name}" from "${source}" is a bare string — pass { light: ${JSON.stringify(value)}, dark: ${JSON.stringify(value)} } (repeat the value when it is the same in both palettes); a single value goes illegible when the user switches color scheme`);
|
|
1238
|
+
if (typeof value !== "object" || value === null || typeof value.light !== "string" || typeof value.dark !== "string") throw new TypeError(`theme override "${name}" from "${source}" must map to a { light, dark } pair of strings — one value per color scheme`);
|
|
1239
|
+
const modes = value;
|
|
1240
|
+
validated[name] = {
|
|
1241
|
+
light: modes.light,
|
|
1242
|
+
dark: modes.dark
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
return validated;
|
|
1246
|
+
}
|
|
1247
|
+
function dynamicToken(name) {
|
|
1248
|
+
return {
|
|
1249
|
+
name,
|
|
1250
|
+
description: "Theme token registered by the current Client composition.",
|
|
1251
|
+
valueType: "CSS value",
|
|
1252
|
+
requiresLightAndDark: true,
|
|
1253
|
+
...name.startsWith("--") ? { cssVariable: name } : {}
|
|
1254
|
+
};
|
|
1255
|
+
}
|
|
1256
|
+
/**
|
|
1077
1257
|
* Required services: settings transport plus slots/locale for the Appearance
|
|
1078
1258
|
* row. `remote` carries the forwarded settings invalidation that
|
|
1079
1259
|
* `bindSettingsScope` subscribes to on this context.
|
|
@@ -1092,7 +1272,7 @@ window.__ModuleLoader__.load({
|
|
|
1092
1272
|
* @param ctx - client cordis context.
|
|
1093
1273
|
*/
|
|
1094
1274
|
function apply(ctx) {
|
|
1095
|
-
const theme = new
|
|
1275
|
+
const theme = new ThemeRuntime(ctx, ctx.settingsScope.bind({ namespace: THEME_SETTINGS_NAMESPACE }));
|
|
1096
1276
|
ctx.provide("theme", theme);
|
|
1097
1277
|
ctx.effect(() => ctx.locale.register(SETTINGS_NS, {
|
|
1098
1278
|
zh,
|
|
@@ -1122,7 +1302,7 @@ window.__ModuleLoader__.load({
|
|
|
1122
1302
|
}
|
|
1123
1303
|
//#endregion
|
|
1124
1304
|
exports.SETTINGS_NS = SETTINGS_NS;
|
|
1125
|
-
exports.
|
|
1305
|
+
exports.ThemeRuntime = ThemeRuntime;
|
|
1126
1306
|
exports.apply = apply;
|
|
1127
1307
|
exports.inject = inject;
|
|
1128
1308
|
return module.exports;
|
package/lib/index.js
CHANGED
|
@@ -72,8 +72,8 @@ function apply(ctx) {
|
|
|
72
72
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
73
73
|
settingsCtx.settings.register(THEME_NAMESPACE, ThemeSettingsSchema);
|
|
74
74
|
});
|
|
75
|
-
ctx.inject(["
|
|
76
|
-
httpCtx.effect(() => httpCtx.
|
|
75
|
+
ctx.inject(["webServer"], (httpCtx) => {
|
|
76
|
+
httpCtx.effect(() => httpCtx.webServer.tapIndex((html) => injectBootTheme(html, readPreference(ctx))), "client-ui-theme: initial theme bootstrap");
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
//#endregion
|
|
@@ -162,6 +162,7 @@ body {
|
|
|
162
162
|
--dsw-alias-bg-mask-2: rgba(0, 0, 0, 0.12);
|
|
163
163
|
--dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.48);
|
|
164
164
|
--dsw-alias-bg-mask-photo: rgba(0, 0, 0, 0.88);
|
|
165
|
+
--dsw-alias-bg-mask-drop: rgba(255, 255, 255, 0.7);
|
|
165
166
|
--dsw-alias-bg-module-platform: var(--dsw-static-neutral-bluish-60);
|
|
166
167
|
--dsw-alias-bg-multi-select: var(--dsw-static-neutral-bluish-60);
|
|
167
168
|
--dsw-alias-bg-overlay: var(--dsw-static-neutral-bluish-150);
|
|
@@ -253,6 +254,7 @@ body[data-ds-dark-theme] {
|
|
|
253
254
|
--dsw-alias-bg-mask-2: rgba(0, 0, 0, 0.2);
|
|
254
255
|
--dsw-alias-bg-mask-3: rgba(0, 0, 0, 0.48);
|
|
255
256
|
--dsw-alias-bg-mask-photo: rgba(0, 0, 0, 0.88);
|
|
257
|
+
--dsw-alias-bg-mask-drop: rgba(39, 39, 48, 0.7);
|
|
256
258
|
--dsw-alias-bg-module-platform: var(--dsw-static-neutral-bluish-800);
|
|
257
259
|
--dsw-alias-bg-multi-select: var(--dsw-static-neutral-850);
|
|
258
260
|
--dsw-alias-bg-overlay: var(--dsw-static-neutral-bluish-700);
|
|
@@ -25,6 +25,19 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
25
25
|
}
|
|
26
26
|
/** Theme token dictionary: --dsw-alias-* overrides keyed by variable name. */
|
|
27
27
|
export type ThemeTokens = Record<string, string>;
|
|
28
|
+
/**
|
|
29
|
+
* One override-layer token value: both palette modes are mandatory (repeat
|
|
30
|
+
* the same value when the token is scheme-invariant) so an override never
|
|
31
|
+
* goes illegible when the user switches to the other scheme.
|
|
32
|
+
*/
|
|
33
|
+
export interface ThemeTokenModes {
|
|
34
|
+
/** Value applied while the light base palette is active. */
|
|
35
|
+
light: string;
|
|
36
|
+
/** Value applied while the dark base palette is active. */
|
|
37
|
+
dark: string;
|
|
38
|
+
}
|
|
39
|
+
/** Override-layer dictionary: token names to per-mode value pairs. */
|
|
40
|
+
export type ThemeTokenOverrides = Record<string, ThemeTokenModes>;
|
|
28
41
|
/** One selectable theme: id, dark/light semantics, and alias-token overrides. */
|
|
29
42
|
export interface ThemeDefinition {
|
|
30
43
|
/** Theme id (the setTheme argument for concrete themes). */
|
|
@@ -41,16 +54,33 @@ export interface ThemeDefinition {
|
|
|
41
54
|
export interface ThemeSnapshot {
|
|
42
55
|
/** The persisted preference (may be `system`). */
|
|
43
56
|
preference: ThemePreference;
|
|
44
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* The resolved active theme (`system` resolved via prefers-color-scheme)
|
|
59
|
+
* with override layers folded into its tokens (seq order, later layers win
|
|
60
|
+
* per-token; each value picked for the active color scheme).
|
|
61
|
+
*/
|
|
45
62
|
active: ThemeDefinition;
|
|
46
63
|
/** Registered themes in registration order. */
|
|
47
64
|
themes: readonly ThemeDefinition[];
|
|
48
65
|
/** Monotonic change counter (registry or active changes). */
|
|
49
66
|
revision: number;
|
|
50
67
|
}
|
|
68
|
+
/** One theme token exposed to pre-definition Cordis inspection. */
|
|
69
|
+
export interface ThemeTokenInspection {
|
|
70
|
+
/** Token name accepted by {@link ThemeService.overrideTokens}. */
|
|
71
|
+
name: string;
|
|
72
|
+
/** Intended visual role. */
|
|
73
|
+
description: string;
|
|
74
|
+
/** CSS value category. */
|
|
75
|
+
valueType: string;
|
|
76
|
+
/** Whether override layers must supply both palette modes. */
|
|
77
|
+
requiresLightAndDark: boolean;
|
|
78
|
+
/** CSS custom property consumed by UI styles. */
|
|
79
|
+
cssVariable?: string;
|
|
80
|
+
}
|
|
51
81
|
declare module '@deepseek-ai/cordis' {
|
|
52
82
|
interface Context {
|
|
53
|
-
theme:
|
|
83
|
+
theme: ThemeRuntime;
|
|
54
84
|
}
|
|
55
85
|
interface Events {
|
|
56
86
|
/**
|
|
@@ -65,13 +95,15 @@ declare module '@deepseek-ai/cordis' {
|
|
|
65
95
|
/**
|
|
66
96
|
* Theme registry and preference owner. `light`/`dark` are built in (the base
|
|
67
97
|
* stylesheets carry both palettes); third-party themes register alias-layer
|
|
68
|
-
* overrides. Reads go through {@link getTheme}; writes only
|
|
69
|
-
* {@link setTheme}; continuous sync only through the `theme/change`
|
|
98
|
+
* overrides. Reads go through {@link getTheme}; preference writes only
|
|
99
|
+
* through {@link setTheme}; continuous sync only through the `theme/change`
|
|
100
|
+
* event. {@link overrideTokens} stacks partial token layers over the active
|
|
101
|
+
* theme without touching the registry.
|
|
70
102
|
* The service holds the `prefers-color-scheme` media query (environment
|
|
71
103
|
* sensing, not presentation) and re-emits when the OS scheme flips while the
|
|
72
104
|
* preference is `system`.
|
|
73
105
|
*/
|
|
74
|
-
export declare class
|
|
106
|
+
export declare class ThemeRuntime {
|
|
75
107
|
private readonly ctx;
|
|
76
108
|
private readonly host;
|
|
77
109
|
private themes;
|
|
@@ -79,6 +111,9 @@ export declare class ThemeService {
|
|
|
79
111
|
private revision;
|
|
80
112
|
private snapshot;
|
|
81
113
|
private readonly media;
|
|
114
|
+
/** Override layers by source; seq (monotonic) is the stacking order. */
|
|
115
|
+
private readonly overrides;
|
|
116
|
+
private overrideSeq;
|
|
82
117
|
/**
|
|
83
118
|
* @param ctx - owning context (change events are emitted on it; the
|
|
84
119
|
* media-query and scope listeners are released through ctx.effect on dispose).
|
|
@@ -90,6 +125,11 @@ export declare class ThemeService {
|
|
|
90
125
|
* @returns the current snapshot (stable reference until the next change).
|
|
91
126
|
*/
|
|
92
127
|
getTheme(): ThemeSnapshot;
|
|
128
|
+
/**
|
|
129
|
+
* Export the current token directory without reading DOM or computed styles.
|
|
130
|
+
* @returns stable JSON-safe token descriptions, including registered and override-only names.
|
|
131
|
+
*/
|
|
132
|
+
exportInspectTokens(): ThemeTokenInspection[];
|
|
93
133
|
/**
|
|
94
134
|
* Switch the theme preference — the only user preference write entry.
|
|
95
135
|
* Built-in preferences are written through the settings scope and every
|
|
@@ -108,7 +148,31 @@ export declare class ThemeService {
|
|
|
108
148
|
* unregistered theme.
|
|
109
149
|
*/
|
|
110
150
|
register(definition: ThemeDefinition): () => void;
|
|
151
|
+
/**
|
|
152
|
+
* Stack a token override layer on top of the active theme — the token-level
|
|
153
|
+
* analogue of slot shading: the base theme stays untouched, layers compose
|
|
154
|
+
* in seq order with later layers winning per-token, and removing a layer
|
|
155
|
+
* restores whatever it covered. Calling again with the same source replaces
|
|
156
|
+
* that source's whole layer and restacks it on top (effect re-registration
|
|
157
|
+
* semantics). Emits `theme/change` with the recomposed snapshot.
|
|
158
|
+
* @param source - layer identity; one layer per source (dynamic packages
|
|
159
|
+
* pass their package id — the façade pins it, so it also names the layer's
|
|
160
|
+
* origin for inspection).
|
|
161
|
+
* @param tokens - token-name → `{ light, dark }` value pairs. Validated at
|
|
162
|
+
* runtime (model-authored callers reach this boundary with untyped JS);
|
|
163
|
+
* a bare string value throws a teaching error.
|
|
164
|
+
* @returns disposer removing exactly the layer this call created; a no-op
|
|
165
|
+
* once the source has re-overridden (the newer layer is not torn down).
|
|
166
|
+
*/
|
|
167
|
+
overrideTokens(source: string, tokens: ThemeTokenOverrides): () => void;
|
|
111
168
|
private buildSnapshot;
|
|
169
|
+
/**
|
|
170
|
+
* Fold the override layers into the active definition: seq order, later
|
|
171
|
+
* layers win per-token, each value picked for the active color scheme (the
|
|
172
|
+
* presenter consumes the composed snapshot and needs no override awareness).
|
|
173
|
+
* Without layers the registered definition passes through by identity.
|
|
174
|
+
*/
|
|
175
|
+
private composeActive;
|
|
112
176
|
private publish;
|
|
113
177
|
}
|
|
114
178
|
/**
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deepseek-ai/dsh-client-ui-theme",
|
|
3
|
-
"description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free
|
|
4
|
-
"version": "0.0.1-rc.
|
|
3
|
+
"description": "Theme plugin: Host bootstrap for the pre-plugin palette; DOM-free ThemeRuntime for light/dark/system state; --dsw-* token styles and Appearance settings row",
|
|
4
|
+
"version": "0.0.1-rc.3",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "restricted"
|
|
7
7
|
},
|
|
@@ -47,29 +47,29 @@
|
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"react": "^18.2.0",
|
|
49
49
|
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
50
|
-
"@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.
|
|
51
|
-
"@deepseek-ai/dsh-client-
|
|
52
|
-
"@deepseek-ai/dsh-client-
|
|
53
|
-
"@deepseek-ai/dsh-client-
|
|
54
|
-
"@deepseek-ai/dsh-client-
|
|
55
|
-
"@deepseek-ai/dsh-client-ui-
|
|
56
|
-
"@deepseek-ai/dsh-
|
|
57
|
-
"@deepseek-ai/dsh-
|
|
58
|
-
"@deepseek-ai/dsh-
|
|
50
|
+
"@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.3",
|
|
51
|
+
"@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
|
|
52
|
+
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
|
|
53
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
|
|
54
|
+
"@deepseek-ai/dsh-client-connection": "^0.0.1-rc.3",
|
|
55
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.3",
|
|
57
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.3",
|
|
58
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.0.1-rc.3"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"@types/react": "~18.3.1",
|
|
62
62
|
"react": "^18.2.0",
|
|
63
63
|
"@deepseek-ai/cordis": "^4.0.1-rc.1",
|
|
64
|
-
"@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.
|
|
65
|
-
"@deepseek-ai/dsh-client-locale": "^0.0.1-rc.
|
|
66
|
-
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.
|
|
67
|
-
"@deepseek-ai/dsh-client-test-runtime": "^0.0.1-rc.
|
|
68
|
-
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.
|
|
69
|
-
"@deepseek-ai/dsh-client-ui-settings": "^0.0.1-rc.
|
|
70
|
-
"@deepseek-ai/dsh-
|
|
71
|
-
"@deepseek-ai/dsh-
|
|
72
|
-
"@deepseek-ai/dsh-
|
|
64
|
+
"@deepseek-ai/dsh-api-remotes": "^0.0.1-rc.3",
|
|
65
|
+
"@deepseek-ai/dsh-client-locale": "^0.0.1-rc.3",
|
|
66
|
+
"@deepseek-ai/dsh-client-runtime": "^0.0.1-rc.3",
|
|
67
|
+
"@deepseek-ai/dsh-client-test-runtime": "^0.0.1-rc.3",
|
|
68
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.0.1-rc.3",
|
|
69
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.0.1-rc.3",
|
|
70
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.0.1-rc.3",
|
|
71
|
+
"@deepseek-ai/dsh-host-webserver": "^0.0.1-rc.3",
|
|
72
|
+
"@deepseek-ai/dsh-invariants": "^0.0.1-rc.3"
|
|
73
73
|
},
|
|
74
74
|
"files": [
|
|
75
75
|
"lib/index.js",
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
],
|
|
81
81
|
"dependencies": {
|
|
82
82
|
"clsx": "^2.0.0",
|
|
83
|
-
"@deepseek-ai/dsh-settings": "^0.0.1-rc.
|
|
83
|
+
"@deepseek-ai/dsh-settings": "^0.0.1-rc.3",
|
|
84
84
|
"@deepseek-ai/schemastery": "^3.18.1-rc.1"
|
|
85
85
|
},
|
|
86
86
|
"scripts": {
|