@objectivex666/dsh-settings-search 1.0.0 → 1.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/README.en.md +91 -0
- package/README.md +93 -2
- package/cordis.patch.yml +9 -0
- package/dsh-settings-search.js +3 -2
- package/lib/client.js +205 -0
- package/lib/index.js +22 -0
- package/package.json +28 -8
- package/scripts/check.mjs +61 -0
package/README.en.md
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# dsh-settings-search
|
|
2
|
+
|
|
3
|
+
> Instant search for the DSH (Cordis) settings panel — quickly locate settings pages and general settings items.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@objectivex666/dsh-settings-search)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
**English** · [中文](./README.md)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
- 🔍 **Live search** — a search box at the top of the settings panel's left navigation filters every registered settings page (`settings.section`) and general item (`settings.general.item`) as you type.
|
|
16
|
+
- 🧭 **Navigation guidance** — clicking a result shows which left-navigation entry owns that setting, so you can jump straight to it.
|
|
17
|
+
- 🌐 **i18n** — built-in Chinese/English dictionaries that follow the DSH localization system and are easy to extend.
|
|
18
|
+
- 🎨 **Theme-aware** — styled entirely with CSS variables, automatically following the DSH theme (light/dark).
|
|
19
|
+
- ⚡ **Reactive** — subscribes to settings entries being added, removed, or changed, and refreshes results automatically.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Installation (command line)
|
|
24
|
+
|
|
25
|
+
This plugin is a standard **DSH profile bundle**: once installed it wires itself into the settings panel — no manual edits to any DSH configuration file.
|
|
26
|
+
|
|
27
|
+
### Prerequisites
|
|
28
|
+
|
|
29
|
+
- **DeepSeek Harness (DSH)** — any release that ships the web UI (`dsh web`).
|
|
30
|
+
- **Node.js** >= 18 (LTS recommended).
|
|
31
|
+
|
|
32
|
+
### From npm (recommended)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
dsh plugin --profile web add @objectivex666/dsh-settings-search
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### From GitHub
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
dsh plugin --profile web add github:objectivex666/dsh-settings-search
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### From a local checkout (development)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
dsh plugin --profile web add /path/to/dsh-settings-search
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
After installing, **restart DSH** (`dsh web`); the search box appears at the top of the settings panel.
|
|
51
|
+
|
|
52
|
+
To uninstall: `dsh plugin --profile web remove @objectivex666/dsh-settings-search`.
|
|
53
|
+
|
|
54
|
+
> For a non-`web` profile, replace `--profile web` with your profile name.
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Manual wiring (when not using the command line)
|
|
59
|
+
|
|
60
|
+
Add the package to your profile's dependencies and declare it as a bundle layer (`dsh plugin add` does both for you):
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# your profile's cordis.patch.yml (or pass it via --patch)
|
|
64
|
+
- insert:
|
|
65
|
+
- id: settings-search
|
|
66
|
+
name: '@objectivex666/dsh-settings-search'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The package ships its own `cordis.patch.yml` (that exact content), which `dsh plugin add` applies automatically.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Package layout
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
lib/index.js Host half (no-op — makes the package a valid Cordis plugin row)
|
|
77
|
+
lib/client.js Browser half (search UI, __ModuleLoader__ format)
|
|
78
|
+
cordis.patch.yml Composition patch: inserts the settings-search plugin row
|
|
79
|
+
scripts/check.mjs Structural self-checks (npm test)
|
|
80
|
+
dsh-settings-search.js Dynamic-plugin source (optional: in-session cordis_define usage)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm test # validates the host half, the dsh.client manifest, and the client bundle handoff
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Dynamic-plugin usage (optional)
|
|
90
|
+
|
|
91
|
+
Legacy usage: register the contents of `dsh-settings-search.js` as `code.client` through DSH's dynamic plugin system (`cordis_define`) — session-only and temporary. For anything persistent, use the profile-bundle install above.
|
package/README.md
CHANGED
|
@@ -1,2 +1,93 @@
|
|
|
1
|
-
# dsh-settings-search
|
|
2
|
-
|
|
1
|
+
# dsh-settings-search
|
|
2
|
+
|
|
3
|
+
> 为 DSH(Cordis)设置面板提供即时搜索能力,快速定位设置页与通用设置项。
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@objectivex666/dsh-settings-search)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
|
|
9
|
+
**中文** · [English](./README.en.md)
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## 功能特性
|
|
14
|
+
|
|
15
|
+
- 🔍 **实时搜索**:在设置面板左侧导航顶部嵌入搜索框,输入关键词立即过滤所有已注册的设置页(`settings.section`)与通用设置项(`settings.general.item`)。
|
|
16
|
+
- 🧭 **导航引导**:点击搜索结果后,显示该设置所在导航项的标签,帮助用户快速定位。
|
|
17
|
+
- 🌐 **多语言支持**:内置中英文国际化,可跟随 DSH 本地化系统扩展。
|
|
18
|
+
- 🎨 **主题适配**:完全使用 CSS 变量,自动跟随 DSH 主题(亮/暗)。
|
|
19
|
+
- ⚡ **动态响应**:实时订阅设置项的新增/删除/变更,搜索结果自动刷新。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 安装(命令行)
|
|
24
|
+
|
|
25
|
+
本插件是一个标准的 **DSH profile bundle**:安装后自动写入设置面板,
|
|
26
|
+
无需手动修改任何 DSH 配置文件。
|
|
27
|
+
|
|
28
|
+
### 前置要求
|
|
29
|
+
|
|
30
|
+
- **DeepSeek Harness (DSH)**:任意包含 Web 界面(`dsh web`)的版本。
|
|
31
|
+
- **Node.js** >= 18(推荐 LTS)。
|
|
32
|
+
|
|
33
|
+
### 通过 npm 安装(推荐)
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
dsh plugin --profile web add @objectivex666/dsh-settings-search
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 通过 GitHub 安装
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
dsh plugin --profile web add github:objectivex666/dsh-settings-search
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 本地源码安装(开发调试)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
dsh plugin --profile web add /path/to/dsh-settings-search
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
安装完成后**重启 DSH**(`dsh web`),打开设置面板即可看到顶部的搜索框。
|
|
52
|
+
卸载:`dsh plugin --profile web remove @objectivex666/dsh-settings-search`。
|
|
53
|
+
|
|
54
|
+
> 非 `web` profile 请把命令中的 `--profile web` 换成你的 profile 名。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 手动接线(不通过命令行安装时)
|
|
59
|
+
|
|
60
|
+
把本包加入 profile 依赖并声明为 bundle 层即可(`dsh plugin add` 会自动完成):
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# 你的 profile 的 cordis.patch.yml(或通过 --patch 传入)
|
|
64
|
+
- insert:
|
|
65
|
+
- id: settings-search
|
|
66
|
+
name: '@objectivex666/dsh-settings-search'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
包内自带 `cordis.patch.yml`(本文件内容),`dsh plugin add` 会自动应用。
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 包结构
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
lib/index.js Host half(空实现,保证作为合法 Cordis 插件行加载)
|
|
77
|
+
lib/client.js Browser half(搜索框 UI,__ModuleLoader__ 格式)
|
|
78
|
+
cordis.patch.yml 组合补丁:插入 settings-search 插件行
|
|
79
|
+
scripts/check.mjs 结构自检(npm test)
|
|
80
|
+
dsh-settings-search.js 动态插件源(可选:DSH 会话内 cordis_define 用法)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 开发
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
npm test # 校验 host 半、dsh.client 清单、client bundle handoff
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 动态插件用法(可选)
|
|
90
|
+
|
|
91
|
+
旧版用法:把 `dsh-settings-search.js` 的内容作为 `code.client` 通过 DSH 的
|
|
92
|
+
动态插件系统(`cordis_define`)注册,仅供会话内临时使用;命令行安装请使用
|
|
93
|
+
上文的 profile bundle 方式。
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# dsh-settings-search bundle layer: applied automatically when the package is
|
|
2
|
+
# installed into a DSH profile (`dsh plugin add @objectivex666/dsh-settings-search`
|
|
3
|
+
# or `dsh plugin add github:objectivex666/dsh-settings-search`), because the
|
|
4
|
+
# manifest declares `dsh.bundle.patch`. No manual cordis.patch.yml edits are
|
|
5
|
+
# needed. Later layers (the profile's own cordis.patch.yml, --patch overlays)
|
|
6
|
+
# override these rows by id.
|
|
7
|
+
- insert:
|
|
8
|
+
- id: settings-search
|
|
9
|
+
name: '@objectivex666/dsh-settings-search'
|
package/dsh-settings-search.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* settings-search
|
|
2
|
+
* settings-search 动态插件源文件(legacy,仅会话内临时使用)
|
|
3
3
|
*
|
|
4
4
|
* 说明:
|
|
5
5
|
* - 这是 DSH 动态 Cordis 插件(Client half)的源码,通过 cordis_define 注册到运行中的
|
|
6
|
-
* DSH
|
|
6
|
+
* DSH 进程,不落盘。仅供会话内临时使用;命令行安装请改用本仓库的 profile bundle
|
|
7
|
+
* 形态(`dsh plugin add`,见 README),其浏览器半位于 lib/client.js,与本文件等价。
|
|
7
8
|
* - 功能:在设置面板左侧导航栏顶部(settings.header 槽,priority:-1 shadow)添加搜索框,
|
|
8
9
|
* 输入关键词实时搜索所有已注册设置页(settings.section)与通用设置项
|
|
9
10
|
* (settings.general.item),浮层展示结果,点击后显示左侧导航定位引导。
|
package/lib/client.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-settings-search — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Registered as a DSH client module: the package's `dsh.client` declaration
|
|
5
|
+
* makes the web app load this bundle (`/plugins/@objectivex666/dsh-settings-search/client.js`),
|
|
6
|
+
* where `window.__ModuleLoader__.load({ id, factory })` registers the code
|
|
7
|
+
* and the client kernel adopts the exported `{ apply, inject }` as the
|
|
8
|
+
* plugin's browser half.
|
|
9
|
+
*
|
|
10
|
+
* Behavior (identical to the dynamic-plugin source, `dsh-settings-search.js`):
|
|
11
|
+
* embed a live search box at the top of the settings panel's left navigation
|
|
12
|
+
* (`settings.header`, priority -1), filtering every registered settings page
|
|
13
|
+
* (`settings.section`) and general item (`settings.general.item`), with a
|
|
14
|
+
* navigation guide on selection.
|
|
15
|
+
*/
|
|
16
|
+
window.__ModuleLoader__.load({
|
|
17
|
+
id: '@objectivex666/dsh-settings-search',
|
|
18
|
+
factory: (require) => {
|
|
19
|
+
const module = { exports: {} }
|
|
20
|
+
const exports = module.exports
|
|
21
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
|
|
22
|
+
const React = require('react')
|
|
23
|
+
|
|
24
|
+
const NS = 'settings-search-v4'
|
|
25
|
+
const zh = {
|
|
26
|
+
title: '设置',
|
|
27
|
+
placeholder: '搜索设置…',
|
|
28
|
+
empty: '没有匹配的设置',
|
|
29
|
+
pages: '设置页',
|
|
30
|
+
rows: '通用设置项',
|
|
31
|
+
guide: '该设置位于左侧导航「{label}」,点击左侧导航项即可打开',
|
|
32
|
+
page: '设置页',
|
|
33
|
+
row: '设置项',
|
|
34
|
+
}
|
|
35
|
+
const en = {
|
|
36
|
+
title: 'Settings',
|
|
37
|
+
placeholder: 'Search settings…',
|
|
38
|
+
empty: 'No matching settings',
|
|
39
|
+
pages: 'Settings pages',
|
|
40
|
+
rows: 'General items',
|
|
41
|
+
guide: 'This setting lives in "{label}" — open it from the left navigation',
|
|
42
|
+
page: 'Page',
|
|
43
|
+
row: 'Item',
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const CSS = `
|
|
47
|
+
.sss-wrap { position: relative; display: flex; flex-direction: column; gap: 8px; }
|
|
48
|
+
.sss-title { font-size: 16px; font-weight: 500; line-height: 24px; }
|
|
49
|
+
.sss-box { position: relative; display: flex; align-items: center; }
|
|
50
|
+
.sss-icon { position: absolute; left: 9px; display: block; opacity: .6; pointer-events: none; }
|
|
51
|
+
.sss-input { width: 100%; box-sizing: border-box; padding: 6px 26px 6px 28px; border-radius: 8px; border: 1px solid var(--dsw-alias-border-l1); background: var(--dsw-alias-bg-layer-1); color: var(--dsw-alias-label-primary); font-size: 13px; line-height: 20px; outline: none; }
|
|
52
|
+
.sss-input:focus { border-color: var(--dsw-alias-brand-primary); }
|
|
53
|
+
.sss-input::placeholder { color: var(--dsw-alias-label-secondary); opacity: .8; }
|
|
54
|
+
.sss-clear { position: absolute; right: 6px; border: none; background: none; color: var(--dsw-alias-label-secondary); cursor: pointer; font-size: 11px; padding: 2px 4px; }
|
|
55
|
+
.sss-clear:hover { color: var(--dsw-alias-label-primary); }
|
|
56
|
+
.sss-pop { position: absolute; top: calc(100% + 6px); left: 0; right: 0; z-index: 20; background: var(--dsw-alias-bg-overlay); border: 1px solid var(--dsw-alias-border-l1); border-radius: 10px; box-shadow: var(--dsw-shadow-lv3); padding: 6px; max-height: 300px; overflow: auto; }
|
|
57
|
+
.sss-groups { display: flex; flex-direction: column; gap: 8px; }
|
|
58
|
+
.sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
|
|
59
|
+
.sss-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
|
|
60
|
+
.sss-item { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
|
|
61
|
+
.sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
|
|
62
|
+
.sss-item-label { flex: 1; min-width: 0; color: var(--dsw-alias-label-primary); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
63
|
+
.sss-item-id { font-size: 10px; color: var(--dsw-alias-label-secondary); flex-shrink: 0; }
|
|
64
|
+
.sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
|
|
65
|
+
.sss-guide { padding: 7px 10px; border-radius: 8px; background: var(--dsw-alias-bg-layer-1); border: 1px solid var(--dsw-alias-border-l1); color: var(--dsw-alias-label-primary); font-size: 12px; line-height: 18px; }
|
|
66
|
+
`
|
|
67
|
+
let stylesInstalled = false
|
|
68
|
+
function installStyles() {
|
|
69
|
+
if (stylesInstalled || typeof document === 'undefined') return
|
|
70
|
+
stylesInstalled = true
|
|
71
|
+
const tag = document.createElement('style')
|
|
72
|
+
tag.dataset.plugin = '@objectivex666/dsh-settings-search'
|
|
73
|
+
tag.dataset.pluginCss = '@objectivex666/dsh-settings-search/panel'
|
|
74
|
+
tag.textContent = CSS
|
|
75
|
+
document.head.appendChild(tag)
|
|
76
|
+
return () => {
|
|
77
|
+
tag.remove()
|
|
78
|
+
stylesInstalled = false
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const resolveLabel = (entry) => {
|
|
83
|
+
const raw = entry.options.label
|
|
84
|
+
if (typeof raw === 'function') return String(raw())
|
|
85
|
+
return raw == null ? '' : String(raw)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function apply(ctx) {
|
|
89
|
+
ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'settings-search: dictionaries')
|
|
90
|
+
const t = ctx.locale.bind(NS)
|
|
91
|
+
const removeStyles = installStyles()
|
|
92
|
+
if (typeof removeStyles === 'function') ctx.effect(removeStyles, 'settings-search: styles')
|
|
93
|
+
|
|
94
|
+
const readSections = () => ctx.slots.entries('settings.section')
|
|
95
|
+
.filter((e) => e.options.id !== 'settings-search')
|
|
96
|
+
.map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
|
|
97
|
+
.sort((a, b) => a.order - b.order)
|
|
98
|
+
const readRows = () => ctx.slots.entries('settings.general.item')
|
|
99
|
+
.map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
|
|
100
|
+
.sort((a, b) => a.order - b.order)
|
|
101
|
+
|
|
102
|
+
const SearchIcon = React.createElement('svg', {
|
|
103
|
+
className: 'sss-icon',
|
|
104
|
+
viewBox: '0 0 16.5 16.5',
|
|
105
|
+
width: 13,
|
|
106
|
+
height: 13,
|
|
107
|
+
xmlns: 'http://www.w3.org/2000/svg',
|
|
108
|
+
'aria-hidden': 'true',
|
|
109
|
+
}, [
|
|
110
|
+
React.createElement('path', {
|
|
111
|
+
d: 'M11.894845 6.647401C11.894845 3.725463 9.534486 1.356779 6.623219 1.35657C3.711786 1.35657 1.351635 3.725338 1.351635 6.647401C1.351843 9.569296 3.711911 11.938273 6.623219 11.938273C9.534361 11.938064 11.894637 9.569171 11.894845 6.647401ZM13.245462 6.647401C13.245254 10.317935 10.280401 13.293613 6.623219 13.293821C2.965871 13.293821 0.000204 10.31806 0 6.647401C0 2.976574 2.965746 0 6.623219 0C10.280526 0.000205 13.245462 2.9767 13.245462 6.647401Z',
|
|
112
|
+
fill: 'currentColor',
|
|
113
|
+
}),
|
|
114
|
+
React.createElement('path', {
|
|
115
|
+
d: 'M16.000417 15.041079L15.044449 16.000433L11.530434 12.473588L12.486298 11.514234L16.000417 15.041079Z',
|
|
116
|
+
fill: 'currentColor',
|
|
117
|
+
}),
|
|
118
|
+
])
|
|
119
|
+
|
|
120
|
+
function SettingsHeaderSearch() {
|
|
121
|
+
const [query, setQuery] = React.useState('')
|
|
122
|
+
const [open, setOpen] = React.useState(false)
|
|
123
|
+
const [sections, setSections] = React.useState(readSections)
|
|
124
|
+
const [rows, setRows] = React.useState(readRows)
|
|
125
|
+
const [selected, setSelected] = React.useState(null)
|
|
126
|
+
React.useEffect(() => {
|
|
127
|
+
const refresh = () => {
|
|
128
|
+
setSections(readSections())
|
|
129
|
+
setRows(readRows())
|
|
130
|
+
}
|
|
131
|
+
const offs = [
|
|
132
|
+
ctx.slots.subscribe('settings.section', refresh),
|
|
133
|
+
ctx.slots.subscribe('settings.general.item', refresh),
|
|
134
|
+
ctx.locale.subscribe(refresh),
|
|
135
|
+
]
|
|
136
|
+
return () => { for (const off of offs) off() }
|
|
137
|
+
}, [])
|
|
138
|
+
|
|
139
|
+
const q = query.trim().toLowerCase()
|
|
140
|
+
const tokens = q.split(/\s+/).filter(Boolean)
|
|
141
|
+
const match = (label, id) => tokens.every((tok) => (label + ' ' + id).toLowerCase().includes(tok))
|
|
142
|
+
const shownSections = tokens.length ? sections.filter((s) => match(s.label, s.id)) : []
|
|
143
|
+
const shownRows = tokens.length ? rows.filter((r) => match(r.label, r.id)) : []
|
|
144
|
+
const none = tokens.length > 0 && shownSections.length === 0 && shownRows.length === 0
|
|
145
|
+
|
|
146
|
+
const renderItem = (item, kind) => React.createElement('li', {
|
|
147
|
+
key: item.id,
|
|
148
|
+
className: 'sss-item',
|
|
149
|
+
onMouseDown: () => { setSelected(item); setOpen(false) },
|
|
150
|
+
}, [
|
|
151
|
+
React.createElement('span', { className: 'sss-item-label' }, item.label || item.id),
|
|
152
|
+
React.createElement('span', { className: 'sss-item-id' }, kind),
|
|
153
|
+
])
|
|
154
|
+
|
|
155
|
+
return React.createElement('div', { className: 'sss-wrap' }, [
|
|
156
|
+
React.createElement('div', { className: 'sss-title' }, t('title')),
|
|
157
|
+
React.createElement('div', { className: 'sss-box' }, [
|
|
158
|
+
SearchIcon,
|
|
159
|
+
React.createElement('input', {
|
|
160
|
+
className: 'sss-input',
|
|
161
|
+
value: query,
|
|
162
|
+
placeholder: t('placeholder'),
|
|
163
|
+
spellCheck: false,
|
|
164
|
+
onChange: (e) => { setQuery(e.target.value); setOpen(true) },
|
|
165
|
+
onFocus: () => setOpen(true),
|
|
166
|
+
onBlur: () => setOpen(false),
|
|
167
|
+
}),
|
|
168
|
+
query !== '' ? React.createElement('button', {
|
|
169
|
+
className: 'sss-clear',
|
|
170
|
+
type: 'button',
|
|
171
|
+
onClick: () => { setQuery(''); setSelected(null); setOpen(false) },
|
|
172
|
+
}, '✕') : null,
|
|
173
|
+
]),
|
|
174
|
+
open && tokens.length > 0 ? React.createElement('div', { className: 'sss-pop' }, [
|
|
175
|
+
none
|
|
176
|
+
? React.createElement('p', { className: 'sss-empty' }, t('empty'))
|
|
177
|
+
: React.createElement('div', { className: 'sss-groups' }, [
|
|
178
|
+
shownSections.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
179
|
+
React.createElement('div', { className: 'sss-group-title' }, t('pages')),
|
|
180
|
+
React.createElement('ul', { className: 'sss-list' }, shownSections.map((s) => renderItem(s, t('page')))),
|
|
181
|
+
]) : null,
|
|
182
|
+
shownRows.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
|
|
183
|
+
React.createElement('div', { className: 'sss-group-title' }, t('rows')),
|
|
184
|
+
React.createElement('ul', { className: 'sss-list' }, shownRows.map((r) => renderItem(r, t('row')))),
|
|
185
|
+
]) : null,
|
|
186
|
+
]),
|
|
187
|
+
]) : null,
|
|
188
|
+
selected !== null
|
|
189
|
+
? React.createElement('div', { className: 'sss-guide' }, t('guide').replace('{label}', selected.label || selected.id))
|
|
190
|
+
: null,
|
|
191
|
+
])
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
ctx.slots.inject('settings.header', () => ctx.slots.register({
|
|
195
|
+
name: 'settings.header',
|
|
196
|
+
priority: -1,
|
|
197
|
+
locale: NS,
|
|
198
|
+
}, SettingsHeaderSearch))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
exports.apply = apply
|
|
202
|
+
exports.inject = ['slots', 'locale']
|
|
203
|
+
return module.exports
|
|
204
|
+
},
|
|
205
|
+
})
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-settings-search — host half.
|
|
3
|
+
*
|
|
4
|
+
* This plugin is client-only: every feature lives in the browser half
|
|
5
|
+
* (`./client` export, `lib/client.js`). This host half exists so the package
|
|
6
|
+
* is a *valid* Cordis plugin row when installed as a DSH profile bundle
|
|
7
|
+
* (`dsh plugin add @objectivex666/dsh-settings-search`): the profile
|
|
8
|
+
* composition loads the package's main entry as a plugin, and the bundle's
|
|
9
|
+
* `dsh.client` declaration tells the web app to fetch and mount the browser
|
|
10
|
+
* half. A no-op host is all that is required here.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Plugin row id used by cordis.patch.yml (also the settings-nav id filter). */
|
|
14
|
+
export const name = 'settings-search'
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Host half entry point. Nothing to do on the host — the search UI lives in
|
|
18
|
+
* the browser and needs no host services or RPC.
|
|
19
|
+
*/
|
|
20
|
+
export function apply() {
|
|
21
|
+
// client-only plugin; intentionally empty on the host
|
|
22
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@objectivex666/dsh-settings-search",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance.",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance. Installable via `dsh plugin add`.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
7
7
|
"cordis",
|
|
@@ -21,23 +21,43 @@
|
|
|
21
21
|
},
|
|
22
22
|
"license": "GPL-3.0",
|
|
23
23
|
"author": "objectivex666",
|
|
24
|
-
"type": "
|
|
25
|
-
"main": "
|
|
24
|
+
"type": "module",
|
|
25
|
+
"main": "lib/index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./lib/index.js",
|
|
28
|
+
"./client": "./lib/client.js",
|
|
29
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
26
32
|
"files": [
|
|
33
|
+
"lib",
|
|
34
|
+
"cordis.patch.yml",
|
|
35
|
+
"scripts",
|
|
27
36
|
"dsh-settings-search.js",
|
|
28
37
|
"README.md",
|
|
38
|
+
"README.en.md",
|
|
29
39
|
"LICENSE"
|
|
30
40
|
],
|
|
31
|
-
"
|
|
32
|
-
"
|
|
41
|
+
"dsh": {
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
},
|
|
45
|
+
"client": {
|
|
46
|
+
"platform": "web",
|
|
47
|
+
"inject": [
|
|
48
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
49
|
+
"@deepseek-ai/dsh-client-locale",
|
|
50
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
51
|
+
]
|
|
52
|
+
}
|
|
33
53
|
},
|
|
34
54
|
"engines": {
|
|
35
55
|
"node": ">=18"
|
|
36
56
|
},
|
|
37
57
|
"scripts": {
|
|
38
|
-
"test": "
|
|
58
|
+
"test": "node scripts/check.mjs"
|
|
39
59
|
},
|
|
40
60
|
"publishConfig": {
|
|
41
61
|
"access": "public"
|
|
42
62
|
}
|
|
43
|
-
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural smoke checks for the installable package (run via `npm test`):
|
|
3
|
+
* 1. the host half loads as ESM and exports a Cordis plugin (`apply`);
|
|
4
|
+
* 2. the manifest carries the DSH bundle wiring (`dsh.bundle.patch`,
|
|
5
|
+
* `dsh.client.platform`, `exports["./client"]`);
|
|
6
|
+
* 3. the client bundle registers the correct module id and its factory
|
|
7
|
+
* materializes to `{ apply, inject }` under a stub module table.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync } from 'node:fs'
|
|
10
|
+
import vm from 'node:vm'
|
|
11
|
+
|
|
12
|
+
const pkgUrl = new URL('../package.json', import.meta.url)
|
|
13
|
+
const pkg = JSON.parse(readFileSync(pkgUrl, 'utf8'))
|
|
14
|
+
let failed = 0
|
|
15
|
+
const check = (cond, msg) => {
|
|
16
|
+
if (cond) return
|
|
17
|
+
failed += 1
|
|
18
|
+
console.error(`FAIL: ${msg}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 1. host half
|
|
22
|
+
const host = await import('../lib/index.js')
|
|
23
|
+
check(typeof host.apply === 'function', 'lib/index.js must export apply(ctx)')
|
|
24
|
+
|
|
25
|
+
// 2. manifest
|
|
26
|
+
check(pkg.dsh?.bundle?.patch, 'dsh.bundle.patch is missing')
|
|
27
|
+
check(pkg.dsh?.client?.platform === 'web', 'dsh.client.platform must be "web"')
|
|
28
|
+
const clientExport = pkg.exports?.['./client']
|
|
29
|
+
check(typeof clientExport === 'string', 'exports["./client"] must be a string path')
|
|
30
|
+
check(typeof pkg.exports?.['./cordis.patch.yml'] === 'string', 'exports["./cordis.patch.yml"] must be a string path')
|
|
31
|
+
check(Array.isArray(pkg.dsh.client.inject) && pkg.dsh.client.inject.every((i) => typeof i === 'string'),
|
|
32
|
+
'dsh.client.inject must be a string array')
|
|
33
|
+
|
|
34
|
+
// 3. client bundle handoff
|
|
35
|
+
const source = readFileSync(new URL(`../${clientExport}`, import.meta.url), 'utf8')
|
|
36
|
+
let handoff = null
|
|
37
|
+
const sandbox = {
|
|
38
|
+
window: {
|
|
39
|
+
__ModuleLoader__: { load: (h) => { handoff = h } },
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
vm.createContext(sandbox)
|
|
43
|
+
vm.runInContext(source, sandbox, { filename: clientExport })
|
|
44
|
+
check(handoff !== null, `client bundle must call window.__ModuleLoader__.load(...)`)
|
|
45
|
+
check(handoff?.id === pkg.name, `client module id must equal the package name (${pkg.name}), got ${handoff?.id}`)
|
|
46
|
+
const stubRequire = (spec) => {
|
|
47
|
+
if (spec === 'react') {
|
|
48
|
+
return { createElement: () => null, useState: () => [], useEffect: () => {} }
|
|
49
|
+
}
|
|
50
|
+
throw new Error(`unexpected require in client factory: ${spec}`)
|
|
51
|
+
}
|
|
52
|
+
const client = handoff.factory(stubRequire)
|
|
53
|
+
check(typeof client.apply === 'function', 'client half must export apply(ctx)')
|
|
54
|
+
check(Array.isArray(client.inject) && client.inject.includes('slots') && client.inject.includes('locale'),
|
|
55
|
+
'client half inject must include "slots" and "locale"')
|
|
56
|
+
|
|
57
|
+
if (failed > 0) {
|
|
58
|
+
console.error(`${failed} check(s) failed`)
|
|
59
|
+
process.exit(1)
|
|
60
|
+
}
|
|
61
|
+
console.log(`ok: ${pkg.name}@${pkg.version} — host half, manifest, and client handoff are valid`)
|