dsh-fonttune 0.2.3 → 0.2.5
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.en.md +18 -0
- package/CHANGELOG.md +18 -0
- package/README.en.md +16 -4
- package/README.md +16 -4
- package/lib/client.js +431 -28
- package/lib/index.js +138 -12
- package/package.json +10 -6
package/CHANGELOG.en.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to **dsh-fonttune** are documented here. Chinese version: [CHANGELOG.md](CHANGELOG.md).
|
|
4
4
|
|
|
5
|
+
## [0.2.5] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
- **Installed as a link to the source directory, the card and the fonts disappeared together on 0.1.5-rc.x**: the plugin used the copy of the schema library in its own directory instead of the one DSH provides, and the newer copy marked the configuration in a way the rc line's settings controller cannot handle — the settings namespace was therefore never registered: no card in the settings sheet, an empty injected stylesheet, no typography change at all. It now always uses the copy DSH ships, so a linked install and a normal install behave identically.
|
|
9
|
+
|
|
10
|
+
## [0.2.4] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **One package for both host lines**: it installs and works as-is on **DSH 0.1.5-rc.1 – rc.3** and on **0.1.7-alpha.1 – alpha.2**, with no version to pick. At runtime the plugin uses whichever settings interface the host offers (the rc line's and the alpha's are different); when neither is there it only injects the font and shows no card.
|
|
14
|
+
- **A configuration entry point on the alpha**: on 0.1.7-alpha.x the card lives in the sidebar's Plugins page, under the plugin entry "Font tune"; on rc.x it stays in Settings → Plugins → Plugin configuration.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- **On the alpha the card's first render was a column of empty controls**: it was reading the host's internal references instead of the values, so the injected stylesheet came out empty; the first frame now carries the right family and readouts.
|
|
18
|
+
- **On the alpha an edit reported "The setting was not saved"**: the plugin could write to a place that was not its own; it now writes only after confirming the configuration is its own.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
- The compatibility statement and the compatibility table in both READMEs are updated to 0.2.4, listing the five host lines that were actually tested (0.1.5-rc.1 / rc.2 / rc.3 and 0.1.7-alpha.1 / alpha.2) and where each line's settings live.
|
|
22
|
+
|
|
5
23
|
## [0.2.3] - 2026-09-22
|
|
6
24
|
|
|
7
25
|
### Fixed
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
**dsh-fonttune** 的重要变更都记录在这里。英文版见 [CHANGELOG.en.md](CHANGELOG.en.md)。
|
|
4
4
|
|
|
5
|
+
## [0.2.5] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
- **以「链接到源码目录」的方式安装时,0.1.5-rc.x 上的配置卡片与字体一起消失**:插件会用自己目录里的 schema 库副本,而不是 DSH 自带的那一份;副本的较新版本给配置加了 rc 线宿主处理不了的标记,设置命名空间因此根本没有注册出去——设置页看不到卡片、注入的样式是空的、字体毫无变化。现在插件始终使用 DSH 自带的那一份,链接安装与正常安装在行为上完全一致。
|
|
9
|
+
|
|
10
|
+
## [0.2.4] - 2026-09-23
|
|
11
|
+
|
|
12
|
+
### 新增
|
|
13
|
+
- **同一个安装包同时支持两条宿主线**:DSH **0.1.5-rc.1 – rc.3** 与 **0.1.7-alpha.1 – alpha.2** 上都能直接安装使用,不需要挑版本。插件在运行时按宿主实际提供的那一套设置接口工作(rc 线一套、alpha 线一套),两条都不在时只注入字体、不显示卡片。
|
|
14
|
+
- **alpha 上的配置入口**:在 0.1.7-alpha.x 上,卡片出现在侧栏「插件」页的插件条目「字体增强」里;rc.x 上仍在 设置 → 插件 → 插件配置。
|
|
15
|
+
|
|
16
|
+
### 修复
|
|
17
|
+
- **alpha 上卡片第一次打开是一排空控件**:此前读到的是宿主的内部引用而不是配置值,注入的样式因此为空;现在首帧就是正确的字体与读数。
|
|
18
|
+
- **alpha 上改设置提示「设置没有保存成功」**:插件有几率把改动写到不属于自己的位置;现在只在确认是自己的配置之后才写入。
|
|
19
|
+
|
|
20
|
+
### 变更
|
|
21
|
+
- 兼容声明与中英 README 的兼容表更新到 0.2.4,逐条列出五条实测过的宿主线(0.1.5-rc.1 / rc.2 / rc.3、0.1.7-alpha.1 / alpha.2),并写清两条线各自的设置入口。
|
|
22
|
+
|
|
5
23
|
## [0.2.3] - 2026-09-22
|
|
6
24
|
|
|
7
25
|
### 修复
|
package/README.en.md
CHANGED
|
@@ -22,14 +22,25 @@
|
|
|
22
22
|
|
|
23
23
|
## Compatibility
|
|
24
24
|
|
|
25
|
+
**One package serves both host lines, 0.1.5-rc.x and 0.1.7-alpha.x**, and the install is the same on either: both were verified for "the settings card opens, the stylesheet really reaches the page, and a change made in the card is written through".
|
|
26
|
+
|
|
25
27
|
| Plugin version | Supported DSH versions |
|
|
26
28
|
| --- | --- |
|
|
27
|
-
| **0.2.
|
|
29
|
+
| **0.2.4** (latest) | 0.1.5-rc.1 / rc.2 / rc.3, 0.1.7-alpha.1 / alpha.2 |
|
|
30
|
+
| 0.2.3 | 0.1.5-rc.2 |
|
|
28
31
|
| 0.2.2 | 0.1.5-rc.2 |
|
|
29
32
|
| 0.2.1 | 0.1.5-rc.2 |
|
|
30
33
|
| 0.2.0 | 0.1.5-rc.2 |
|
|
31
34
|
|
|
32
|
-
`engines.dsh`
|
|
35
|
+
`engines.dsh` is declared as **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`** (a prerelease needs its own branch: node-semver admits one only when some comparator in the range sits on that version's exact `major.minor.patch` tuple and carries a prerelease tag of its own, so a plain `>=0.1.5-rc.1` never matches `0.1.7-alpha.1`). Every difference between the lines is settled at runtime rather than by version number:
|
|
36
|
+
|
|
37
|
+
- **The settings service**: up to 0.1.5-rc.x it is `settingsScope.bind({namespace})` (by namespace); from 0.1.7-alpha.x it is `configForms.get(<profile entry id>)` (by entry id, which the installing profile's patch decides — so the plugin claims the form whose served schema names its own fields). Both expose the same `getSnapshot`/`subscribe`/`set`/`unset` face, so one implementation covers them.
|
|
38
|
+
- **The configuration card's seat**: the rc line has a keyed cell under Settings → Plugins → Plugin configuration (`settings.plugin.item`); the alpha line dropped that slot and instead contributes an entry to the Plugins page's official list (`plugins.item`, which is how the built-in settings pages are added there), plus a per-package page for a profile that installed this plugin as a bundle (`plugins.bundle.config`). All three are registered; the one a host never declares is inert.
|
|
39
|
+
- **The host half's config values**: once the alpha marks the schema `.volatile()`, the fields the host half receives are cosmokit volatile references (`{get(),[write]}`) rather than plain values, which left the first-frame stylesheet row empty; the references are unwrapped before use.
|
|
40
|
+
- **`inject` declares only services both lines have** (`slots`, `locale`): declaring one a host does not provide parks the whole package — on the alpha a declared `settingsScope` kept the entire Web UI from booting. The settings service is looked up with `ctx.get(name)` instead.
|
|
41
|
+
- **The alpha's form is addressed by entry id**: while the host's `describe()` view has not arrived, the plugin does not guess a name (a wrong guess means every write is refused) — it stands in with a pending scope, adopts the real form as soon as the view is served, and re-reads immediately.
|
|
42
|
+
|
|
43
|
+
The market entry also states these requirements (`engines.dsh`, `dsh.compatibility.dshReleases` and `peerDependencies` in `package.json`), so the host a version needs can be checked before installing.
|
|
33
44
|
|
|
34
45
|
## Install
|
|
35
46
|
|
|
@@ -73,8 +84,9 @@ Restart DSH once afterwards and open the Web UI; changing settings needs no rest
|
|
|
73
84
|
- `src/index.mjs` — host half: registers the `dsh-fonttune` settings namespace (schemastery schema with length and range validation) and ships a **stamped** `<style>` into the served index through `webserver/index-inject`, so the first paint already uses the saved values; the browser half then **adopts and keeps that element** (there is only ever one copy of the stylesheet, so rules can be added *and* removed)
|
|
74
85
|
- `src/client.js` — browser half: the settings card, the font picker panel, font enumeration and style injection
|
|
75
86
|
- `build.mjs` — zero-dependency build: inlines the shared core, wraps the bundle in the `window.__ModuleLoader__.load({id, factory})` shell, copies the host half, and enforces that the client bundle only requires shell-held modules
|
|
76
|
-
- `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity, the picker's grouping rules, the token-refresh power gate); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class), fires **every control's callback** (an unwired handler only fails when someone actually clicks it) and **renders a second time** to cover a hook order that changes with state; `test/artifacts.mjs` proves `lib/` matches the current `src/` byte for byte (`npm run verify` runs all three)
|
|
77
|
-
- Checks that need a real page run through `npm run verify:browser -- "<url-with-token>"` (start a managed instance first: `dsh web --port 0 --no-open` prints the URL). The default group is read-only (`browser-probe` / `ui-walk` / `style-verify`); `--writers` adds the scripts that **write the settings namespace** (`slider-walk` / `split-walk` / `split-verify` / `weight-verify`, each snapshots the user layer first and restores it before exiting), and `--only a,b` picks a subset. Every script's exit code is its verdict; `test/set-user-layer.mjs <url> '<json>'` puts the layer back after an interrupted run
|
|
87
|
+
- `test/run.mjs` — offline checks (hand-built DOM / cordis / settings-surface doubles, the real schemastery schema, CSS generation and injection, sanitizer adversarial cases, dictionary key parity, the picker's grouping rules, the token-refresh power gate); `test/render-card.mjs` renders the card component through a mini React hooks runtime (including forced-open and forced-state renders, covering the 0.1.0 release-blocking crash class), fires **every control's callback** (an unwired handler only fails when someone actually clicks it) and **renders a second time** to cover a hook order that changes with state; `test/artifacts.mjs` proves `lib/` matches the current `src/` byte for byte (`npm run verify` runs all three). The devDependency is the **newest** schemastery (`^3.18.4`) — the alpha's dialect, whose `.volatile()` makes a resolved field a **reference** rather than a value — so these suites read values through the `plainConfigValue` the host half exports, the same path the plugin takes; at runtime the **host's own copy** is used (3.18.2 on rc.2 / rc.3, with no such modifier, a shape the live probe covers)
|
|
88
|
+
- Checks that need a real page run through `npm run verify:browser -- "<url-with-token>"` (start a managed instance first: `dsh web --port 0 --no-open` prints the URL). The default group is read-only (`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`); `--writers` adds the scripts that **write the settings namespace** (`slider-walk` / `split-walk` / `split-verify` / `weight-verify`, each snapshots the user layer first and restores it before exiting), and `--only a,b` picks a subset. Every script's exit code is its verdict; `test/set-user-layer.mjs <url> '<json>'` puts the layer back after an interrupted run
|
|
89
|
+
- `test/host-line-probe.mjs` is the **host-line-independent** check: it asserts only what must hold on every supported line — the page boots without errors, the plugin owns exactly ONE stylesheet element, the settings really reach the document (`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` also assert the computed values when set), the configuration card is reachable from the settings sheet and renders (`.dfp-card`, whichever seat that line declares), and changing a control there really writes through. Run it first after switching host lines (rc ↔ alpha); set `DFP_PROBE=1` when debugging "the card's writes do not stick on the alpha" — the plugin then logs one `[dfp-probe]` line (which namespaces are served, which one was claimed, and what state the seat is in)
|
|
78
90
|
- `test/weight-verify.mjs` measures all three weight axes (interface / conversation / code) in a live page, including that there is only one stylesheet element and that switching following off drops the followed weight without a reload; `test/split-verify.mjs` measures the conversation and code size axes plus the **retired interface size axis moving nothing**; `test/slider-walk.mjs` and `test/split-walk.mjs` drive the real card (a drag writes nothing, a release does; the simple-mode slots and the whole stack round-trip); the market-entry tooling is `test/market-pr.mjs` (status / update / refresh / reopen / open / about) and the diagnostic `test/market-inspect.mjs` (PR state, comments, CI and branch diff)
|
|
79
91
|
- `docs/` — the 0.2.0 [research](docs/research-0.2.0.md) and [specification](docs/spec-0.2.0.md) records (written during design, with the trade-offs of that moment; where they differ from what shipped, README/CHANGELOG win — see the note at the top of each). **Not part of the npm `files` list, so they are not published**
|
|
80
92
|
- The client bundle may only require modules from the shell's static table (`react`, `react/jsx-runtime`, `react-dom`, `@deepseek-ai/cordis`, `@deepseek-ai/dsh-client-*`…); `dsh.client.inject` declares load order, not require permission
|
package/README.md
CHANGED
|
@@ -22,14 +22,25 @@
|
|
|
22
22
|
|
|
23
23
|
## 兼容性
|
|
24
24
|
|
|
25
|
+
**一份包同时支持 0.1.5-rc.x 与 0.1.7-alpha.x 两条宿主线**,安装方式与宿主线无关:两条线都验证过「设置卡片可用、样式真的作用到页面、卡片上的改动写得进去」。
|
|
26
|
+
|
|
25
27
|
| 插件版本 | 支持的 DSH 版本 |
|
|
26
28
|
| --- | --- |
|
|
27
|
-
| **0.2.
|
|
29
|
+
| **0.2.4**(最新) | 0.1.5-rc.1 / rc.2 / rc.3、0.1.7-alpha.1 / alpha.2 |
|
|
30
|
+
| 0.2.3 | 0.1.5-rc.2 |
|
|
28
31
|
| 0.2.2 | 0.1.5-rc.2 |
|
|
29
32
|
| 0.2.1 | 0.1.5-rc.2 |
|
|
30
33
|
| 0.2.0 | 0.1.5-rc.2 |
|
|
31
34
|
|
|
32
|
-
`engines.dsh`
|
|
35
|
+
`engines.dsh` 声明为 **`>=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0`**(预发布版本必须显式留分支:node-semver 只在一个范围的某个比较符与该版本落在同一个 `major.minor.patch` 元组、且自身带预发布标签时才放行,写成 `>=0.1.5-rc.1` 匹配不到 `0.1.7-alpha.1`)。两条线的差别**全部在运行时二选一,不看版本号**:
|
|
36
|
+
|
|
37
|
+
- **设置服务**:0.1.5-rc.x 是 `settingsScope.bind({namespace})`(按命名空间),0.1.7-alpha.x 换成了 `configForms.get(<profile 条目 id>)`(按条目 id,id 由安装方的 profile patch 决定,所以插件靠「宿主服务的那份 schema 里有没有本插件自己的字段」来认领)。两者都是 `getSnapshot/subscribe/set/unset` 同一张脸,插件只写一套。
|
|
38
|
+
- **配置卡片座位**:rc 线是 设置 → 插件 → 插件配置 里的一个 keyed 单元(`settings.plugin.item`);alpha 线删掉了这个槽位,改成在「插件」页的官方插件列表里贡献一个条目(`plugins.item`,内置那些设置页就是这么挂的),并另外提供「已安装成 bundle 时」的每包页面(`plugins.bundle.config`)。三个座位都注册,宿主不声明的那个是惰性的。
|
|
39
|
+
- **宿主半的配置来源**:alpha 把 schema 标成 `.volatile()` 之后,宿主半拿到的配置字段是 cosmokit 的 volatile 引用(`{get(),[write]}`)而不是普通值,首帧样式行会因此变空;插件先把引用解出来再用。
|
|
40
|
+
- **`inject` 只声明两条线都有的服务**(`slots`、`locale`):声明一个宿主没有的服务会把整包 park 住——alpha 上声明 `settingsScope` 会让整个 Web 界面起不来。设置服务一律用 `ctx.get(name)` 现查。
|
|
41
|
+
- **alpha 的表单是「条目 id + 描述视图」**:宿主 `describe()` 还没答话时,插件不会去猜一个条目名(猜错的话每次写入都会被告主拒绝),而是先用一个等待态座位顶住,等宿主把视图发出来再接管并立刻重读。
|
|
42
|
+
|
|
43
|
+
市场条目同时声明这些兼容信息(`package.json` 的 `engines.dsh`、`dsh.compatibility.dshReleases` 与 `peerDependencies`),安装前可据此判断这一版需要的宿主版本。
|
|
33
44
|
|
|
34
45
|
## 安装
|
|
35
46
|
|
|
@@ -73,8 +84,9 @@ dsh plugin --profile web add <插件目录路径>
|
|
|
73
84
|
- `src/index.mjs` —— 宿主半:注册 `dsh-fonttune` 设置命名空间(schemastery schema,含长度与取值范围校验),并通过 `webserver/index-inject` 把一个打过标记的 `<style>` 随首页下发,首帧即是设置里的字体;那个元素随后由浏览器半**认领并接管**(页面里只有这一份样式表,规则可增可删)
|
|
74
85
|
- `src/client.js` —— 浏览器半:设置卡片、选字体面板、字体枚举与样式注入
|
|
75
86
|
- `build.mjs` —— 零依赖构建:内联共享核心、套上 `window.__ModuleLoader__.load({id, factory})` 外壳、拷贝宿主半,并校验客户端 bundle 只 require shell 预注入的模块
|
|
76
|
-
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify`
|
|
77
|
-
- 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
|
|
87
|
+
- `test/run.mjs` —— 离线检查(自建 DOM / cordis / 设置面替身、真实 schemastery schema、CSS 生成与注入、消毒对抗用例、双语文案键一致性、选字体面板的分组规则、令牌轮询的省电闸门);`test/render-card.mjs` 用 mini React hooks 运行时真实渲染卡片组件(含强制展开态与强制状态,覆盖 0.1.0 那类发布阻断崩溃),把**每个控件的回调都点一遍**(未接线的回调只有真点下去才会暴露),并**渲染第二次**覆盖「状态变化后 hook 顺序改变」这一类崩溃;`test/artifacts.mjs` 校验 `lib/` 与当前 `src/` 逐字节一致(`npm run verify` 跑全部三套)。devDependency 钉的是**最新的** schemastery(`^3.18.4`),也就是 alpha 那套方言——它带 `.volatile()`,解析出来的字段是**引用**而不是值,所以这几套检查都通过宿主半导出的 `plainConfigValue` 取值(和插件读配置同一条路);运行时用的是**宿主自带的那份**(rc.2/rc.3 是 3.18.2,没有该修饰符,那条形态由真机探针覆盖)
|
|
88
|
+
- 需要真实页面的检查走 `npm run verify:browser -- "<带 token 的地址>"`(先起一个受管实例:`dsh web --port 0 --no-open`,它会打印地址)。默认只跑**只读**的几支(`browser-probe` / `ui-walk` / `host-line-probe` / `style-verify`);加 `--writers` 再跑会**写设置命名空间**的几支(`slider-walk` / `split-walk` / `split-verify` / `weight-verify`,各自先快照用户层、退出前还原),`--only a,b` 挑子集;每支的退出码就是判定。`test/set-user-layer.mjs <url> '<json>'` 是在被中断的运行之后还原用户层的工具
|
|
89
|
+
- `test/host-line-probe.mjs` 是**与宿主线无关**的那一支:只断言两条线都必须成立的事——页面无报错、插件只维护**一份**样式表、设置真的作用到文档(`DFP_EXPECT_FAMILY` / `DFP_EXPECT_WEIGHT` 给了就顺带断言计算样式)、设置页里能找到并渲染出 `.dfp-card`(哪条线的座位都行)、以及从卡片上改一个值真的写进去。换宿主线(rc ↔ alpha)后先跑它;调试「alpha 上卡片写不进去」这类问题时设 `DFP_PROBE=1`,插件会往控制台打一行 `[dfp-probe]`(服务了哪些命名空间、认领了哪个、座位当前是什么状态)
|
|
78
90
|
- `test/weight-verify.mjs` 在真实页面里量三条字重轴(界面 / 对话 / 代码)的独立性与「样式表只有一份、不刷新关掉跟随立即回落」;`test/split-verify.mjs` 量对话/代码两条字号轴的独立性,外加**退役的界面字号轴什么都不动**;`test/slider-walk.mjs` 与 `test/split-walk.mjs` 驱动真实卡片(拖动不写、松手才写;简单模式两格与整条栈的往返);另有市场条目的维护脚本 `test/market-pr.mjs`(status / update / refresh / reopen / open / about)与诊断脚本 `test/market-inspect.mjs`(PR 状态、评论、CI 与分支差异)
|
|
79
91
|
- `docs/` —— 0.2.0 的[背景调研](docs/research-0.2.0.md)与[功能规格](docs/spec-0.2.0.md)(设计阶段的记录,含当时的取舍理由;与最终实现不同的地方以 README/CHANGELOG 为准,两份文件文首都有说明)。**不在 npm `files` 清单内,不随包发布**
|
|
80
92
|
- 客户端模块能 require 的只有 shell 静态表里的模块(`react`、`react/jsx-runtime`、`react-dom`、`@deepseek-ai/cordis`、`@deepseek-ai/dsh-client-*` 等);`dsh.client.inject` 只是加载顺序声明,不是 require 许可
|
package/lib/client.js
CHANGED
|
@@ -2266,7 +2266,6 @@ if (typeof module !== "undefined" && module.exports) module.exports = shared;
|
|
|
2266
2266
|
|
|
2267
2267
|
var React = require("react");
|
|
2268
2268
|
var createPortal = require("react-dom").createPortal;
|
|
2269
|
-
var primitives = require("@deepseek-ai/dsh-client-ui-primitives");
|
|
2270
2269
|
|
|
2271
2270
|
var h = React.createElement;
|
|
2272
2271
|
var useCallback = React.useCallback;
|
|
@@ -2334,8 +2333,24 @@ var setWestEntry = shared.setWestEntry;
|
|
|
2334
2333
|
var setEastEntry = shared.setEastEntry;
|
|
2335
2334
|
var removeStackEntry = shared.removeStackEntry;
|
|
2336
2335
|
|
|
2337
|
-
/**
|
|
2338
|
-
|
|
2336
|
+
/**
|
|
2337
|
+
* Services this bundle waits for before it applies.
|
|
2338
|
+
*
|
|
2339
|
+
* `settingsScope` is deliberately absent: a host that does not provide a
|
|
2340
|
+
* declared service parks the whole package until it appears, and DSH
|
|
2341
|
+
* 0.1.7-alpha.x replaced that service with `configForms` — a plugin asking for
|
|
2342
|
+
* it never activates there. Both settings dialects are looked up at apply time
|
|
2343
|
+
* with `ctx.get` instead (see `pickSettingsScope`).
|
|
2344
|
+
*/
|
|
2345
|
+
var inject = ["slots", "locale"];
|
|
2346
|
+
|
|
2347
|
+
/**
|
|
2348
|
+
* The settings scope and translator `apply` bound, for the seat that renders
|
|
2349
|
+
* the card without handing it either: the alpha line's bundle page renders the
|
|
2350
|
+
* registered component with its own props, so the card falls back to these.
|
|
2351
|
+
*/
|
|
2352
|
+
var cardScope = null;
|
|
2353
|
+
var cardT = null;
|
|
2339
2354
|
|
|
2340
2355
|
/** The weight DSH uses for body text; choosing it means "leave it alone". */
|
|
2341
2356
|
var NEUTRAL_WEIGHT = 400;
|
|
@@ -2364,6 +2379,80 @@ var CHEVRON_PATH =
|
|
|
2364
2379
|
/** Panel height cap, used to decide whether it opens up or down. */
|
|
2365
2380
|
var PANEL_HEIGHT = 380;
|
|
2366
2381
|
|
|
2382
|
+
/**
|
|
2383
|
+
* The translator the card falls back to when the host hands it no `t`: the
|
|
2384
|
+
* built-in English table says something useful where the key alone would not.
|
|
2385
|
+
* @param {string} key - dictionary key.
|
|
2386
|
+
* @param {Record<string, string>} [params] - `{name}` substitutions.
|
|
2387
|
+
* @returns {string} the text.
|
|
2388
|
+
*/
|
|
2389
|
+
function fallbackTranslate(key, params) {
|
|
2390
|
+
return translate("en", key, params);
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
/**
|
|
2394
|
+
* The settings seat a host without any settings service leaves the card: every
|
|
2395
|
+
* control renders disabled over an empty document, exactly like a read-only
|
|
2396
|
+
* deployment. It keeps the card — and therefore the whole configuration page —
|
|
2397
|
+
* from failing to render on a composition this plugin does not know.
|
|
2398
|
+
*/
|
|
2399
|
+
var MEMORY_SCOPE = {
|
|
2400
|
+
getSnapshot: function () {
|
|
2401
|
+
return {
|
|
2402
|
+
status: "ready",
|
|
2403
|
+
value: {},
|
|
2404
|
+
base: {},
|
|
2405
|
+
user: {},
|
|
2406
|
+
revision: 0,
|
|
2407
|
+
writable: false,
|
|
2408
|
+
mode: "memory",
|
|
2409
|
+
};
|
|
2410
|
+
},
|
|
2411
|
+
subscribe: function () {
|
|
2412
|
+
return function () {};
|
|
2413
|
+
},
|
|
2414
|
+
set: function () {
|
|
2415
|
+
return Promise.reject(new Error("settings are not available in this deployment"));
|
|
2416
|
+
},
|
|
2417
|
+
unset: function () {
|
|
2418
|
+
return Promise.reject(new Error("settings are not available in this deployment"));
|
|
2419
|
+
},
|
|
2420
|
+
};
|
|
2421
|
+
|
|
2422
|
+
/* ------------------------------------------------------------------ *
|
|
2423
|
+
* inline icons
|
|
2424
|
+
* ------------------------------------------------------------------ */
|
|
2425
|
+
|
|
2426
|
+
/**
|
|
2427
|
+
* The 16px "plus" glyph of the "add a font" button.
|
|
2428
|
+
*
|
|
2429
|
+
* Drawn here rather than taken from `@deepseek-ai/dsh-client-ui-primitives`
|
|
2430
|
+
* because that module's icon names are not stable across the host lines this
|
|
2431
|
+
* plugin supports: up to 0.1.5-rc.x the shell exports `IconPlusOutline16`,
|
|
2432
|
+
* while 0.1.7-alpha.x ships the same artwork as `IconPlusOutlineRegular` /
|
|
2433
|
+
* `IconPlusOutlineMedium` — and requiring a name the shell does not have turns
|
|
2434
|
+
* the whole card into a render error. The path is the shipped one, byte for
|
|
2435
|
+
* byte, so the button looks identical on both.
|
|
2436
|
+
* @returns {object} the svg element.
|
|
2437
|
+
*/
|
|
2438
|
+
function plusIcon() {
|
|
2439
|
+
return h(
|
|
2440
|
+
"svg",
|
|
2441
|
+
{
|
|
2442
|
+
width: 16,
|
|
2443
|
+
height: 16,
|
|
2444
|
+
viewBox: "0 0 16 16",
|
|
2445
|
+
fill: "none",
|
|
2446
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
2447
|
+
"aria-hidden": "true",
|
|
2448
|
+
},
|
|
2449
|
+
h("path", {
|
|
2450
|
+
d: "M8.64453 1.5V7.34961H14.5V8.65039H8.64453V14.5H7.34473V8.65039H1.5V7.34961H7.34473V1.5H8.64453Z",
|
|
2451
|
+
fill: "currentColor",
|
|
2452
|
+
})
|
|
2453
|
+
);
|
|
2454
|
+
}
|
|
2455
|
+
|
|
2367
2456
|
/* ------------------------------------------------------------------ *
|
|
2368
2457
|
* copy
|
|
2369
2458
|
* ------------------------------------------------------------------ */
|
|
@@ -4013,7 +4102,7 @@ function StackPicker(props) {
|
|
|
4013
4102
|
setOpen(!open);
|
|
4014
4103
|
},
|
|
4015
4104
|
},
|
|
4016
|
-
|
|
4105
|
+
plusIcon(),
|
|
4017
4106
|
h("span", null, t("stack.add"))
|
|
4018
4107
|
)
|
|
4019
4108
|
)
|
|
@@ -4186,6 +4275,24 @@ function firstFamily(stack) {
|
|
|
4186
4275
|
* the card
|
|
4187
4276
|
* ------------------------------------------------------------------ */
|
|
4188
4277
|
|
|
4278
|
+
/**
|
|
4279
|
+
* The card as DSH 0.1.7-alpha.x's Plugins page renders it.
|
|
4280
|
+
*
|
|
4281
|
+
* That page contributes one entry per plugin (`plugins.item`) and asks that one
|
|
4282
|
+
* component for two different views: a one-line `summary` for the card in the
|
|
4283
|
+
* list, and the `page` body once the entry is opened. The summary is answered
|
|
4284
|
+
* with the card's own description, and every other view is the card itself, so
|
|
4285
|
+
* the same UI serves the rc line's configuration cell and the alpha's plugin
|
|
4286
|
+
* page without a second implementation.
|
|
4287
|
+
* @param {object} props - the view asked for plus the injected face.
|
|
4288
|
+
* @returns {object|string} the card, or the one-line summary.
|
|
4289
|
+
*/
|
|
4290
|
+
function FontCardPage(props) {
|
|
4291
|
+
var t = typeof props.t === "function" ? props.t : cardT || fallbackTranslate;
|
|
4292
|
+
if (props.view === "summary") return t("card.description");
|
|
4293
|
+
return h(FontCard, { scope: props.scope || cardScope || MEMORY_SCOPE, t: t });
|
|
4294
|
+
}
|
|
4295
|
+
|
|
4189
4296
|
/**
|
|
4190
4297
|
* Render the plugin's card: the settings section dispatches this component
|
|
4191
4298
|
* under the namespace key, and the host has to serve that namespace for it to
|
|
@@ -4203,8 +4310,11 @@ function firstFamily(stack) {
|
|
|
4203
4310
|
* @returns {object} the card element.
|
|
4204
4311
|
*/
|
|
4205
4312
|
function FontCard(props) {
|
|
4206
|
-
|
|
4207
|
-
|
|
4313
|
+
// The registration hands the card its face on the line that supports
|
|
4314
|
+
// `inject`; the alpha's bundle page renders it with its own props, so the
|
|
4315
|
+
// values `apply` bound are the fallback.
|
|
4316
|
+
var t = typeof props.t === "function" ? props.t : cardT || fallbackTranslate;
|
|
4317
|
+
var scope = props.scope || cardScope || MEMORY_SCOPE;
|
|
4208
4318
|
var [open, setOpen] = useState(false);
|
|
4209
4319
|
var [view, setView] = useState(readViewMode);
|
|
4210
4320
|
var [expanded, setExpanded] = useState(null);
|
|
@@ -4306,13 +4416,48 @@ function FontCard(props) {
|
|
|
4306
4416
|
* Report a write the settings document refused. A silent failure is the
|
|
4307
4417
|
* worst outcome here: the control goes back to showing the stored value and
|
|
4308
4418
|
* the user is left believing the change was saved.
|
|
4309
|
-
* @param {unknown} error - the rejection reason.
|
|
4419
|
+
* @param {unknown} error - the rejection reason, or a boolean refusal.
|
|
4310
4420
|
*/
|
|
4311
4421
|
var writeFailed = function (error) {
|
|
4312
|
-
var message =
|
|
4422
|
+
var message =
|
|
4423
|
+
error === false || error === undefined
|
|
4424
|
+
? "the host refused the write"
|
|
4425
|
+
: error && error.message
|
|
4426
|
+
? String(error.message)
|
|
4427
|
+
: String(error);
|
|
4313
4428
|
if (message.length > 160) message = message.slice(0, 160) + "…";
|
|
4314
4429
|
showStatus(t("preset.writeFailed", { message: message }));
|
|
4315
4430
|
};
|
|
4431
|
+
/**
|
|
4432
|
+
* Follow one queued settings write.
|
|
4433
|
+
*
|
|
4434
|
+
* The two host lines answer differently: up to 0.1.5-rc.x the scope resolves
|
|
4435
|
+
* a promise that rejects on a transport failure, while 0.1.7-alpha.x resolves
|
|
4436
|
+
* `false` for a write the host refused (a stale revision, say). Both are
|
|
4437
|
+
* reported, and a refused write is never left looking saved.
|
|
4438
|
+
* @param {unknown} result - what the scope returned.
|
|
4439
|
+
*/
|
|
4440
|
+
var followWrite = function (result) {
|
|
4441
|
+
if (result === false) {
|
|
4442
|
+
writeFailed(result);
|
|
4443
|
+
return;
|
|
4444
|
+
}
|
|
4445
|
+
if (result && typeof result.then === "function") {
|
|
4446
|
+
result.then(function (accepted) {
|
|
4447
|
+
if (accepted === false) writeFailed(accepted);
|
|
4448
|
+
}, function (error) {
|
|
4449
|
+
// The scope reloads the Host state itself; the status row is what
|
|
4450
|
+
// keeps the user from thinking the change stuck.
|
|
4451
|
+
writeFailed(error);
|
|
4452
|
+
});
|
|
4453
|
+
return;
|
|
4454
|
+
}
|
|
4455
|
+
if (result && typeof result.catch === "function") {
|
|
4456
|
+
result.catch(function (error) {
|
|
4457
|
+
writeFailed(error);
|
|
4458
|
+
});
|
|
4459
|
+
}
|
|
4460
|
+
};
|
|
4316
4461
|
var submit = function (field, value) {
|
|
4317
4462
|
var result;
|
|
4318
4463
|
try {
|
|
@@ -4321,13 +4466,7 @@ function FontCard(props) {
|
|
|
4321
4466
|
writeFailed(error);
|
|
4322
4467
|
return;
|
|
4323
4468
|
}
|
|
4324
|
-
|
|
4325
|
-
result.catch(function (error) {
|
|
4326
|
-
// The scope reloads the Host state itself; the status row is what
|
|
4327
|
-
// keeps the user from thinking the change stuck.
|
|
4328
|
-
writeFailed(error);
|
|
4329
|
-
});
|
|
4330
|
-
}
|
|
4469
|
+
followWrite(result);
|
|
4331
4470
|
};
|
|
4332
4471
|
var clear = function (field) {
|
|
4333
4472
|
var result;
|
|
@@ -4337,11 +4476,7 @@ function FontCard(props) {
|
|
|
4337
4476
|
writeFailed(error);
|
|
4338
4477
|
return;
|
|
4339
4478
|
}
|
|
4340
|
-
|
|
4341
|
-
result.catch(function (error) {
|
|
4342
|
-
writeFailed(error);
|
|
4343
|
-
});
|
|
4344
|
-
}
|
|
4479
|
+
followWrite(result);
|
|
4345
4480
|
};
|
|
4346
4481
|
var overridden = function (field) {
|
|
4347
4482
|
return Object.prototype.hasOwnProperty.call(user, field);
|
|
@@ -5282,7 +5417,231 @@ function apply(ctx) {
|
|
|
5282
5417
|
var applyCss = createStylesheet(function () {
|
|
5283
5418
|
return tokens;
|
|
5284
5419
|
});
|
|
5285
|
-
|
|
5420
|
+
|
|
5421
|
+
/**
|
|
5422
|
+
* Look a service up without declaring it.
|
|
5423
|
+
*
|
|
5424
|
+
* Declaring a name the running host does not provide parks the whole package
|
|
5425
|
+
* until it appears — on DSH 0.1.7-alpha.x a declared `settingsScope` left the
|
|
5426
|
+
* Web UI waiting on a service that no longer exists. Both routes are tried:
|
|
5427
|
+
* `ctx.get(name)` first, then the service property older lines also expose.
|
|
5428
|
+
* @param {string} name - the service name.
|
|
5429
|
+
* @returns {object|null} the service, or null when this host has none.
|
|
5430
|
+
*/
|
|
5431
|
+
var getService = function (name) {
|
|
5432
|
+
try {
|
|
5433
|
+
if (typeof ctx.get === "function") {
|
|
5434
|
+
var found = ctx.get(name);
|
|
5435
|
+
if (found) return found;
|
|
5436
|
+
}
|
|
5437
|
+
} catch (error) {
|
|
5438
|
+
// fall through to the property read
|
|
5439
|
+
}
|
|
5440
|
+
try {
|
|
5441
|
+
return ctx[name] || null;
|
|
5442
|
+
} catch (error) {
|
|
5443
|
+
return null;
|
|
5444
|
+
}
|
|
5445
|
+
};
|
|
5446
|
+
|
|
5447
|
+
/**
|
|
5448
|
+
* Whether one served namespace is THIS plugin's form.
|
|
5449
|
+
*
|
|
5450
|
+
* The alpha keys a form by profile entry id, which the installing profile's
|
|
5451
|
+
* patch decides, so the entry's own fields are what identify us: either the
|
|
5452
|
+
* served schema or the values it carries name them.
|
|
5453
|
+
* @param {object} entry - one namespace from the describe view.
|
|
5454
|
+
* @returns {boolean} true when the entry is this plugin's.
|
|
5455
|
+
*/
|
|
5456
|
+
var isOurNamespace = function (entry) {
|
|
5457
|
+
var schema = "";
|
|
5458
|
+
try {
|
|
5459
|
+
schema = JSON.stringify((entry && entry.schema) || {});
|
|
5460
|
+
} catch (error) {
|
|
5461
|
+
schema = "";
|
|
5462
|
+
}
|
|
5463
|
+
if (schema.indexOf(UI_FOLLOWS_FIELD) >= 0 && schema.indexOf(STACK_DIALOG_FIELD) >= 0) {
|
|
5464
|
+
return true;
|
|
5465
|
+
}
|
|
5466
|
+
var values = (entry && (entry.value || entry.user || entry.base)) || {};
|
|
5467
|
+
return (
|
|
5468
|
+
Object.prototype.hasOwnProperty.call(values, UI_FOLLOWS_FIELD) &&
|
|
5469
|
+
Object.prototype.hasOwnProperty.call(values, STACK_DIALOG_FIELD)
|
|
5470
|
+
);
|
|
5471
|
+
};
|
|
5472
|
+
|
|
5473
|
+
/**
|
|
5474
|
+
* The profile entry id the `configForms` dialect addresses settings by.
|
|
5475
|
+
*
|
|
5476
|
+
* Up to 0.1.5-rc.x a namespace is a name the plugin registers; from
|
|
5477
|
+
* 0.1.7-alpha.x the settings service derives a form per composed entry and
|
|
5478
|
+
* keys it by that entry's id — which the installing profile's patch decides,
|
|
5479
|
+
* so nothing here may hard-code it. The served view is the only honest
|
|
5480
|
+
* answer: while it has not arrived this returns null, and the caller waits
|
|
5481
|
+
* for it instead of guessing a name whose form could never be written.
|
|
5482
|
+
* @param {object} forms - the `configForms` service.
|
|
5483
|
+
* @returns {string|null} the entry id, or null while the host has not served it.
|
|
5484
|
+
*/
|
|
5485
|
+
var formsNamespace = function (forms) {
|
|
5486
|
+
var view = null;
|
|
5487
|
+
try {
|
|
5488
|
+
view = forms.describe ? forms.describe().getSnapshot().view : null;
|
|
5489
|
+
} catch (error) {
|
|
5490
|
+
view = null;
|
|
5491
|
+
}
|
|
5492
|
+
var served = (view && view.namespaces) || [];
|
|
5493
|
+
if (globalThis.__DFP_PROBE__ === true) {
|
|
5494
|
+
console.log(
|
|
5495
|
+
"[dfp-probe] served namespaces " +
|
|
5496
|
+
JSON.stringify(
|
|
5497
|
+
served.map(function (item) {
|
|
5498
|
+
return item && item.ns;
|
|
5499
|
+
})
|
|
5500
|
+
)
|
|
5501
|
+
);
|
|
5502
|
+
}
|
|
5503
|
+
for (var index = 0; index < served.length; index += 1) {
|
|
5504
|
+
if (isOurNamespace(served[index])) {
|
|
5505
|
+
if (globalThis.__DFP_PROBE__ === true) {
|
|
5506
|
+
console.log("[dfp-probe] matched by served schema: " + String(served[index].ns));
|
|
5507
|
+
}
|
|
5508
|
+
return served[index].ns;
|
|
5509
|
+
}
|
|
5510
|
+
}
|
|
5511
|
+
return null;
|
|
5512
|
+
};
|
|
5513
|
+
|
|
5514
|
+
/**
|
|
5515
|
+
* A scope that adopts the host's scope the moment the host serves it.
|
|
5516
|
+
*
|
|
5517
|
+
* The alpha's `configForms` can be asked for a form before its describe view
|
|
5518
|
+
* has arrived — `get(id)` then answers with a not-ready controller for that
|
|
5519
|
+
* id, and every write through it is refused. This stands in for the scope
|
|
5520
|
+
* until the real one is identifiable, forwards the card's subscription to it
|
|
5521
|
+
* on adoption, and lets the card re-read immediately afterwards.
|
|
5522
|
+
* @returns {object} the pending scope, with an `adopt` seat for the caller.
|
|
5523
|
+
*/
|
|
5524
|
+
var createPendingScope = function () {
|
|
5525
|
+
var target = null;
|
|
5526
|
+
var listeners = [];
|
|
5527
|
+
return {
|
|
5528
|
+
getSnapshot: function () {
|
|
5529
|
+
return target === null
|
|
5530
|
+
? {
|
|
5531
|
+
status: "loading",
|
|
5532
|
+
value: undefined,
|
|
5533
|
+
base: undefined,
|
|
5534
|
+
user: undefined,
|
|
5535
|
+
revision: undefined,
|
|
5536
|
+
writable: false,
|
|
5537
|
+
mode: "pending",
|
|
5538
|
+
}
|
|
5539
|
+
: target.getSnapshot();
|
|
5540
|
+
},
|
|
5541
|
+
subscribe: function (listener) {
|
|
5542
|
+
if (target !== null) return target.subscribe(listener);
|
|
5543
|
+
listeners.push(listener);
|
|
5544
|
+
return function () {
|
|
5545
|
+
var at = listeners.indexOf(listener);
|
|
5546
|
+
if (at >= 0) listeners.splice(at, 1);
|
|
5547
|
+
};
|
|
5548
|
+
},
|
|
5549
|
+
set: function (field, value) {
|
|
5550
|
+
if (target === null) {
|
|
5551
|
+
return Promise.reject(new Error("the settings service is still starting"));
|
|
5552
|
+
}
|
|
5553
|
+
return target.set(field, value);
|
|
5554
|
+
},
|
|
5555
|
+
unset: function (field) {
|
|
5556
|
+
if (target === null) {
|
|
5557
|
+
return Promise.reject(new Error("the settings service is still starting"));
|
|
5558
|
+
}
|
|
5559
|
+
return target.unset(field);
|
|
5560
|
+
},
|
|
5561
|
+
/** Hand over the real scope once it is identifiable; true when taken. */
|
|
5562
|
+
adopt: function (next) {
|
|
5563
|
+
if (target !== null || next === null || next === undefined) return false;
|
|
5564
|
+
target = next;
|
|
5565
|
+
var pending = listeners.slice();
|
|
5566
|
+
listeners.length = 0;
|
|
5567
|
+
for (var index = 0; index < pending.length; index += 1) {
|
|
5568
|
+
target.subscribe(pending[index]);
|
|
5569
|
+
pending[index]();
|
|
5570
|
+
}
|
|
5571
|
+
return true;
|
|
5572
|
+
},
|
|
5573
|
+
};
|
|
5574
|
+
};
|
|
5575
|
+
|
|
5576
|
+
/**
|
|
5577
|
+
* Bind the settings scope in whichever dialect the running host speaks: the
|
|
5578
|
+
* `settingsScope` service up to 0.1.5-rc.x (`bind({namespace})`), the
|
|
5579
|
+
* `configForms` service from 0.1.7-alpha.x (`get(entryId)`). Both expose the
|
|
5580
|
+
* same snapshot/`set`/`unset` face, so one card covers them.
|
|
5581
|
+
* @returns {object|null} the bound scope, or null when neither dialect answered.
|
|
5582
|
+
*/
|
|
5583
|
+
var pickSettingsScope = function () {
|
|
5584
|
+
var settingsScope = getService("settingsScope");
|
|
5585
|
+
if (settingsScope && typeof settingsScope.bind === "function") {
|
|
5586
|
+
try {
|
|
5587
|
+
var bound = settingsScope.bind({ namespace: NAMESPACE });
|
|
5588
|
+
if (bound) return bound;
|
|
5589
|
+
} catch (error) {
|
|
5590
|
+
// fall through to the newer dialect
|
|
5591
|
+
}
|
|
5592
|
+
}
|
|
5593
|
+
var forms = getService("configForms");
|
|
5594
|
+
if (forms && typeof forms.get === "function") {
|
|
5595
|
+
var bindAlphaForm = function () {
|
|
5596
|
+
var ns = formsNamespace(forms);
|
|
5597
|
+
if (ns === null) return null;
|
|
5598
|
+
try {
|
|
5599
|
+
var form = forms.get(ns);
|
|
5600
|
+
return form && typeof form.getSnapshot === "function" ? form : null;
|
|
5601
|
+
} catch (error) {
|
|
5602
|
+
return null;
|
|
5603
|
+
}
|
|
5604
|
+
};
|
|
5605
|
+
var immediate = bindAlphaForm();
|
|
5606
|
+
if (immediate !== null) return immediate;
|
|
5607
|
+
var pending = createPendingScope();
|
|
5608
|
+
var mirror = null;
|
|
5609
|
+
try {
|
|
5610
|
+
mirror = typeof forms.describe === "function" ? forms.describe() : null;
|
|
5611
|
+
} catch (error) {
|
|
5612
|
+
mirror = null;
|
|
5613
|
+
}
|
|
5614
|
+
var offMirror = null;
|
|
5615
|
+
var adopt = function () {
|
|
5616
|
+
var form = bindAlphaForm();
|
|
5617
|
+
if (form === null) return;
|
|
5618
|
+
if (!pending.adopt(form)) return;
|
|
5619
|
+
if (typeof offMirror === "function") offMirror();
|
|
5620
|
+
};
|
|
5621
|
+
if (mirror && typeof mirror.subscribe === "function") {
|
|
5622
|
+
offMirror = mirror.subscribe(adopt);
|
|
5623
|
+
if (typeof mirror.ensure === "function") mirror.ensure();
|
|
5624
|
+
}
|
|
5625
|
+
adopt();
|
|
5626
|
+
return pending;
|
|
5627
|
+
}
|
|
5628
|
+
return null;
|
|
5629
|
+
};
|
|
5630
|
+
|
|
5631
|
+
var scope = pickSettingsScope() || MEMORY_SCOPE;
|
|
5632
|
+
if (globalThis.__DFP_PROBE__ === true) {
|
|
5633
|
+
var probeSnapshot = scope.getSnapshot ? scope.getSnapshot() : {};
|
|
5634
|
+
console.log(
|
|
5635
|
+
"[dfp-probe] scope " +
|
|
5636
|
+
JSON.stringify({
|
|
5637
|
+
mode: probeSnapshot.mode,
|
|
5638
|
+
status: probeSnapshot.status,
|
|
5639
|
+
revision: probeSnapshot.revision,
|
|
5640
|
+
writable: probeSnapshot.writable,
|
|
5641
|
+
value: probeSnapshot.value,
|
|
5642
|
+
})
|
|
5643
|
+
);
|
|
5644
|
+
}
|
|
5286
5645
|
var appliedOverrides = "";
|
|
5287
5646
|
|
|
5288
5647
|
/**
|
|
@@ -5436,6 +5795,9 @@ function apply(ctx) {
|
|
|
5436
5795
|
}
|
|
5437
5796
|
return translate(locale, key, params);
|
|
5438
5797
|
};
|
|
5798
|
+
// The card reads these when a seat renders it without the injected face.
|
|
5799
|
+
cardScope = scope;
|
|
5800
|
+
cardT = t;
|
|
5439
5801
|
ctx.effect(
|
|
5440
5802
|
function () {
|
|
5441
5803
|
return ctx.locale.register(NAMESPACE, DICTS);
|
|
@@ -5443,18 +5805,59 @@ function apply(ctx) {
|
|
|
5443
5805
|
"dsh-fonttune: dictionaries"
|
|
5444
5806
|
);
|
|
5445
5807
|
|
|
5446
|
-
|
|
5447
|
-
|
|
5448
|
-
|
|
5449
|
-
|
|
5450
|
-
|
|
5808
|
+
/**
|
|
5809
|
+
* Offer the card in one slot seat.
|
|
5810
|
+
*
|
|
5811
|
+
* Which seat a host declares is a line difference, not a preference, and the
|
|
5812
|
+
* three are not alternatives to pick from — each is simply inert where a host
|
|
5813
|
+
* never declares it:
|
|
5814
|
+
*
|
|
5815
|
+
* * `settings.plugin.item` — up to 0.1.5-rc.x, a keyed cell under
|
|
5816
|
+
* Settings → Plugins → Plugin configuration (keyed by the namespace);
|
|
5817
|
+
* * `plugins.item` — 0.1.7-alpha.x's own entry in the Plugins page's
|
|
5818
|
+
* official list, which is how EVERY plugin page is contributed there (the
|
|
5819
|
+
* built-in settings pages do the same), and the seat that needs no
|
|
5820
|
+
* installed package behind it;
|
|
5821
|
+
* * `plugins.bundle.config` — the alpha's per-package page, keyed by the npm
|
|
5822
|
+
* package name, for a profile that installed this plugin as a bundle.
|
|
5823
|
+
*
|
|
5824
|
+
* @param {string} seat - the slot name.
|
|
5825
|
+
* @param {object} options - the registration options that seat looks up.
|
|
5826
|
+
* @param {Function} [component] - the component to register; the card itself
|
|
5827
|
+
* unless a seat hands it props the card does not read.
|
|
5828
|
+
*/
|
|
5829
|
+
var offerCard = function (seat, options, component) {
|
|
5830
|
+
ctx.slots.inject(seat, function () {
|
|
5831
|
+
var registration = {
|
|
5832
|
+
name: seat,
|
|
5833
|
+
locale: NAMESPACE,
|
|
5451
5834
|
inject: function () {
|
|
5452
5835
|
return { scope: scope, t: t };
|
|
5453
5836
|
},
|
|
5837
|
+
};
|
|
5838
|
+
for (var field in options) {
|
|
5839
|
+
if (Object.prototype.hasOwnProperty.call(options, field)) {
|
|
5840
|
+
registration[field] = options[field];
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
return ctx.slots.register(registration, component === undefined ? FontCard : component);
|
|
5844
|
+
});
|
|
5845
|
+
};
|
|
5846
|
+
if (scope !== MEMORY_SCOPE) {
|
|
5847
|
+
offerCard("settings.plugin.item", { key: NAMESPACE });
|
|
5848
|
+
offerCard("plugins.bundle.config", { key: "dsh-fonttune" });
|
|
5849
|
+
offerCard(
|
|
5850
|
+
"plugins.item",
|
|
5851
|
+
{
|
|
5852
|
+
id: "fonttune",
|
|
5853
|
+
order: 40,
|
|
5854
|
+
label: function () {
|
|
5855
|
+
return t("card.title");
|
|
5856
|
+
},
|
|
5454
5857
|
},
|
|
5455
|
-
|
|
5858
|
+
FontCardPage
|
|
5456
5859
|
);
|
|
5457
|
-
}
|
|
5860
|
+
}
|
|
5458
5861
|
}
|
|
5459
5862
|
|
|
5460
5863
|
exports.apply = apply;
|
package/lib/index.js
CHANGED
|
@@ -20,12 +20,54 @@
|
|
|
20
20
|
*
|
|
21
21
|
* @module dsh-fonttune
|
|
22
22
|
*/
|
|
23
|
-
import
|
|
23
|
+
import { createRequire } from "node:module";
|
|
24
|
+
import { dirname, sep } from "node:path";
|
|
25
|
+
import { fileURLToPath } from "node:url";
|
|
24
26
|
// Default import, not named: `shared.cjs` is a CommonJS module, and Node's
|
|
25
27
|
// CJS-to-ESM interop only guarantees `module.exports` as the default binding
|
|
26
28
|
// (statically-detected named exports would break this half at boot).
|
|
27
29
|
import shared from "./shared.cjs";
|
|
28
30
|
|
|
31
|
+
/**
|
|
32
|
+
* The schema library, taken from the HOST whenever the host can be reached.
|
|
33
|
+
*
|
|
34
|
+
* `@deepseek-ai/schemastery` is an optional peer dependency — DSH supplies the
|
|
35
|
+
* schema library, this package never ships its own copy into a profile. Node's
|
|
36
|
+
* bare-specifier resolution does not know that: when this package is linked or
|
|
37
|
+
* junctioned into a profile (the development setup) the specifier resolves
|
|
38
|
+
* inside this package's own `node_modules`, i.e. to the devDependency. That is
|
|
39
|
+
* a SECOND instance of the library, and — worse — a newer one than the running
|
|
40
|
+
* host may carry: its `.volatile()` marking lands on a schema the rc line's
|
|
41
|
+
* settings controller cannot digest, so the namespace is never served (no card
|
|
42
|
+
* in the settings sheet, an empty first-frame stylesheet). Measured on
|
|
43
|
+
* 0.1.5-rc.3: linked → nothing, installed → works.
|
|
44
|
+
*
|
|
45
|
+
* Resolving from the host first keeps one instance and one dialect, which is
|
|
46
|
+
* exactly what an installed package gets. `process.argv[1]` is the host's own
|
|
47
|
+
* entry (its `node_modules` holds the authoritative copy); anything resolving
|
|
48
|
+
* back inside THIS package is skipped, so the offline suites — which have no
|
|
49
|
+
* host — still fall through to the devDependency.
|
|
50
|
+
* @returns {object} the schemastery export.
|
|
51
|
+
*/
|
|
52
|
+
function loadSchemastery() {
|
|
53
|
+
const own = dirname(dirname(fileURLToPath(import.meta.url))) + sep;
|
|
54
|
+
const bases = [process.argv[1], fileURLToPath(import.meta.url)];
|
|
55
|
+
for (const base of bases) {
|
|
56
|
+
if (typeof base !== "string" || base === "") continue;
|
|
57
|
+
try {
|
|
58
|
+
const from = createRequire(base);
|
|
59
|
+
if (!from.resolve("@deepseek-ai/schemastery").startsWith(own)) {
|
|
60
|
+
return from("@deepseek-ai/schemastery");
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// Not resolvable from that anchor: try the next one.
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return createRequire(import.meta.url)("@deepseek-ai/schemastery");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const z = loadSchemastery();
|
|
70
|
+
|
|
29
71
|
const {
|
|
30
72
|
buildFontCss,
|
|
31
73
|
CODE_LINE_HEIGHT_FIELD,
|
|
@@ -71,6 +113,30 @@ const MAX_STACK = 200;
|
|
|
71
113
|
*/
|
|
72
114
|
const SAFE_FEATURES = /^[a-zA-Z0-9"' ,]*$/;
|
|
73
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Mark a schema editable without reloading, where the running schemastery
|
|
118
|
+
* knows the modifier.
|
|
119
|
+
*
|
|
120
|
+
* DSH 0.1.7-alpha.x derives a settings page from one form per composed entry
|
|
121
|
+
* and only projects the fields under a node marked `.volatile()` — an entry
|
|
122
|
+
* whose schema has no such node is skipped entirely, so the plugin would have
|
|
123
|
+
* no configuration page there. The modifier arrived in schemastery 3.18.x for
|
|
124
|
+
* the alpha line; the 0.1.5-rc.x line ships 3.18.2, where calling it while
|
|
125
|
+
* this module is imported would throw. Feature-detecting it is therefore the
|
|
126
|
+
* whole compatibility shim: the rc line keeps the plain schema it has always
|
|
127
|
+
* used, and the alpha gets the marking it needs.
|
|
128
|
+
*
|
|
129
|
+
* Marking the ROOT object makes every field below it live-editable; schemastery
|
|
130
|
+
* refuses a volatile node nested inside another volatile one, so nothing below
|
|
131
|
+
* may carry the modifier as well.
|
|
132
|
+
*
|
|
133
|
+
* @param {object} schema - the object schema.
|
|
134
|
+
* @returns {object} the schema, marked when the modifier exists.
|
|
135
|
+
*/
|
|
136
|
+
function volatileWhenSupported(schema) {
|
|
137
|
+
return typeof schema.volatile === "function" ? schema.volatile() : schema;
|
|
138
|
+
}
|
|
139
|
+
|
|
74
140
|
/**
|
|
75
141
|
* The durable settings section.
|
|
76
142
|
*
|
|
@@ -82,7 +148,7 @@ const SAFE_FEATURES = /^[a-zA-Z0-9"' ,]*$/;
|
|
|
82
148
|
* value. Installing the plugin therefore changes nothing until the user asks
|
|
83
149
|
* for something.
|
|
84
150
|
*/
|
|
85
|
-
export const Config = z.object({
|
|
151
|
+
export const Config = volatileWhenSupported(z.object({
|
|
86
152
|
[SANS_FIELD]: z
|
|
87
153
|
.string()
|
|
88
154
|
.max(MAX_STACK)
|
|
@@ -220,13 +286,46 @@ export const Config = z.object({
|
|
|
220
286
|
.max(shared.MAX_PRESET_NAME)
|
|
221
287
|
.default("")
|
|
222
288
|
.description("Name of the preset the card currently edits (edits auto-save into it)"),
|
|
223
|
-
});
|
|
289
|
+
}));
|
|
224
290
|
|
|
225
291
|
/**
|
|
226
292
|
* One row of the structured index injection table.
|
|
227
293
|
* @typedef {{kind: "style", text: string} | {kind: "html", placement: "head", html: string}} StyleRow
|
|
228
294
|
*/
|
|
229
295
|
|
|
296
|
+
/**
|
|
297
|
+
* The cosmokit volatile-reference protocol: a parsed config field may be a
|
|
298
|
+
* frozen `{ get(), [write](value) }` reference rather than a plain value.
|
|
299
|
+
*
|
|
300
|
+
* DSH 0.1.7-alpha.x marks the plugin's whole schema `.volatile()` so a settings
|
|
301
|
+
* write can land without remounting the entry, and the loader then hands this
|
|
302
|
+
* plugin a config whose fields ARE those references. Reading them as plain data
|
|
303
|
+
* (which `normalizeConfig` expects) would see objects where strings belong and
|
|
304
|
+
* emit nothing, so the references are unwrapped first. On the 0.1.5-rc.x line
|
|
305
|
+
* no field is a reference and this is a plain deep copy.
|
|
306
|
+
*/
|
|
307
|
+
const VOLATILE_WRITE = Symbol.for("cosmokit.volatile.write");
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Replace every volatile reference in a parsed config with its current value.
|
|
311
|
+
*
|
|
312
|
+
* Exported because it is half of this plugin's host-line contract: a caller that
|
|
313
|
+
* wants to read a resolved config — a test, or a later composition — has to go
|
|
314
|
+
* through it, since on the alpha a parsed field is a reference rather than data.
|
|
315
|
+
* @param {unknown} value - a parsed config value, or a reference to one.
|
|
316
|
+
* @returns {unknown} the same shape, references unwrapped.
|
|
317
|
+
*/
|
|
318
|
+
export function plainConfigValue(value) {
|
|
319
|
+
if (value === null || typeof value !== "object") return value;
|
|
320
|
+
if (VOLATILE_WRITE in value && typeof value.get === "function") {
|
|
321
|
+
return plainConfigValue(value.get());
|
|
322
|
+
}
|
|
323
|
+
if (Array.isArray(value)) return value.map(plainConfigValue);
|
|
324
|
+
const out = {};
|
|
325
|
+
for (const [key, child] of Object.entries(value)) out[key] = plainConfigValue(child);
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
|
|
230
329
|
/**
|
|
231
330
|
* The style row carrying one configuration.
|
|
232
331
|
*
|
|
@@ -240,7 +339,7 @@ export const Config = z.object({
|
|
|
240
339
|
* @returns {StyleRow} the row the web server renders into `<head>`.
|
|
241
340
|
*/
|
|
242
341
|
function styleRow(resolved) {
|
|
243
|
-
const cfg = normalizeConfig(resolved);
|
|
342
|
+
const cfg = normalizeConfig(plainConfigValue(resolved));
|
|
244
343
|
// 0.2.0 ships the per-theme editor out; light and dark stay one value set
|
|
245
344
|
// (the dark fields remain in the schema, dormant, for a future release).
|
|
246
345
|
cfg[PER_THEME_FIELD] = false;
|
|
@@ -258,20 +357,47 @@ function styleRow(resolved) {
|
|
|
258
357
|
|
|
259
358
|
/**
|
|
260
359
|
* Register the settings section and keep the index in sync with it.
|
|
360
|
+
*
|
|
361
|
+
* TWO HOST DIALECTS. Up to DSH 0.1.5-rc.x the settings service registers a
|
|
362
|
+
* named section with a schema and reads it back by that name
|
|
363
|
+
* (`settings.installSection`), and the section name is what the browser half
|
|
364
|
+
* binds. From 0.1.7-alpha.x the service derives one form per composed entry and
|
|
365
|
+
* addresses it by the PROFILE ENTRY ID — there is no `installSection` at all,
|
|
366
|
+
* and an instance announces its page policy with `settings.configure` instead.
|
|
367
|
+
* Both are pure registrations that the browser half then reads through its own
|
|
368
|
+
* service, so which one ran does not change anything else here.
|
|
369
|
+
*
|
|
261
370
|
* @param {object} ctx - host cordis context.
|
|
262
371
|
* @param {unknown} config - the plugin entry's composition config (base layer).
|
|
263
372
|
*/
|
|
264
373
|
export function apply(ctx, config) {
|
|
265
374
|
let current = () => config;
|
|
266
375
|
ctx.inject(["settings"], (settingsCtx) => {
|
|
267
|
-
settingsCtx.settings
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
376
|
+
const settings = settingsCtx.settings;
|
|
377
|
+
if (settings && typeof settings.installSection === "function") {
|
|
378
|
+
settings.installSection(ctx, NAMESPACE, Config, config ?? {}, {
|
|
379
|
+
setSource: (source) => {
|
|
380
|
+
current = source;
|
|
381
|
+
},
|
|
382
|
+
// The row table is rebuilt on every index render and every worker boot
|
|
383
|
+
// payload, so each read is already fresh; nothing to invalidate here.
|
|
384
|
+
onChange: () => {},
|
|
385
|
+
});
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
if (settings && typeof settings.configure === "function") {
|
|
389
|
+
// `auto: true` keeps the host's generated page as the fallback wherever
|
|
390
|
+
// no custom page of ours is reachable.
|
|
391
|
+
settingsCtx.effect(() => settings.configure({ auto: true }, ctx.fiber));
|
|
392
|
+
// This dialect has no registered reader: the entry's live, merged value
|
|
393
|
+
// is the fiber's own resolved config, which is what the settings service
|
|
394
|
+
// shows a form as well. The browser half adopts and rewrites the served
|
|
395
|
+
// element on activation either way, so this only decides the first frame.
|
|
396
|
+
current = () => {
|
|
397
|
+
const live = ctx.fiber && ctx.fiber.config;
|
|
398
|
+
return live === undefined || live === null ? config : live;
|
|
399
|
+
};
|
|
400
|
+
}
|
|
275
401
|
});
|
|
276
402
|
ctx.on("webserver/index-inject", (table) => {
|
|
277
403
|
table.push(styleRow(current()));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-fonttune",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Font plugin for the DeepSeek Harness Web GUI: the conversation gets its own font, size, line height and weight, the interface font and weight follow it, code keeps its own axis with ligature control, and whole setups save as presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"LICENSE"
|
|
20
20
|
],
|
|
21
21
|
"engines": {
|
|
22
|
-
"dsh": ">=0.1.5-rc.2"
|
|
22
|
+
"dsh": ">=0.1.5-rc.1 <0.1.7-0 || >=0.1.7-alpha.1 <0.2.0-0"
|
|
23
23
|
},
|
|
24
24
|
"dsh": {
|
|
25
25
|
"bundle": {
|
|
@@ -30,13 +30,17 @@
|
|
|
30
30
|
},
|
|
31
31
|
"compatibility": {
|
|
32
32
|
"dshReleases": {
|
|
33
|
-
"0.1.5-rc.
|
|
33
|
+
"0.1.5-rc.1": "compatible",
|
|
34
|
+
"0.1.5-rc.2": "compatible",
|
|
35
|
+
"0.1.5-rc.3": "compatible",
|
|
36
|
+
"0.1.7-alpha.1": "compatible",
|
|
37
|
+
"0.1.7-alpha.2": "compatible"
|
|
34
38
|
}
|
|
35
39
|
}
|
|
36
40
|
},
|
|
37
41
|
"peerDependencies": {
|
|
38
|
-
"
|
|
39
|
-
"
|
|
42
|
+
"@deepseek-ai/schemastery": "^3.18.0",
|
|
43
|
+
"react": "^18.2.0"
|
|
40
44
|
},
|
|
41
45
|
"peerDependenciesMeta": {
|
|
42
46
|
"@deepseek-ai/schemastery": {
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
}
|
|
45
49
|
},
|
|
46
50
|
"devDependencies": {
|
|
47
|
-
"@deepseek-ai/schemastery": "^3.18.
|
|
51
|
+
"@deepseek-ai/schemastery": "^3.18.4"
|
|
48
52
|
},
|
|
49
53
|
"scripts": {
|
|
50
54
|
"build": "node build.mjs",
|