@objectivex666/dsh-settings-search 1.0.0 → 1.1.1

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 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
+ [![npm version](https://img.shields.io/npm/v/@objectivex666/dsh-settings-search)](https://www.npmjs.com/package/@objectivex666/dsh-settings-search)
6
+ [![license](https://img.shields.io/npm/l/@objectivex666/dsh-settings-search)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/@objectivex666/dsh-settings-search)](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
- A plugin that adds a search box to the DSH settings panel.
1
+ # dsh-settings-search
2
+
3
+ > 为 DSH(Cordis)设置面板提供即时搜索能力,快速定位设置页与通用设置项。
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@objectivex666/dsh-settings-search)](https://www.npmjs.com/package/@objectivex666/dsh-settings-search)
6
+ [![license](https://img.shields.io/npm/l/@objectivex666/dsh-settings-search)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/@objectivex666/dsh-settings-search)](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 方式。
@@ -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'
@@ -1,9 +1,10 @@
1
1
  /**
2
- * settings-search 插件源文件(set-1 / pkg-4,正在运行)
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),浮层展示结果,点击后显示左侧导航定位引导。
@@ -45,14 +46,13 @@ return {
45
46
 
46
47
  styles.insert(`
47
48
  .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); }
49
+ .sss-title { font-size: 16px; font-weight: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
50
+ .sss-box { display: flex; align-items: center; gap: 6px; width: 100%; box-sizing: border-box; height: 32px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); }
51
+ .sss-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; color: var(--dsw-alias-label-tertiary); }
52
+ .sss-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); }
53
+ .sss-input::placeholder { color: var(--dsw-alias-label-dimmed); }
54
+ .sss-clear { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1; cursor: pointer; }
55
+ .sss-clear:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
56
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
57
  .sss-groups { display: flex; flex-direction: column; gap: 8px; }
58
58
  .sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
@@ -81,8 +81,8 @@ return {
81
81
  const SearchIcon = React.createElement('svg', {
82
82
  className: 'sss-icon',
83
83
  viewBox: '0 0 16.5 16.5',
84
- width: 13,
85
- height: 13,
84
+ width: 16,
85
+ height: 16,
86
86
  xmlns: 'http://www.w3.org/2000/svg',
87
87
  'aria-hidden': 'true',
88
88
  }, [
package/lib/client.js ADDED
@@ -0,0 +1,204 @@
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: 600; line-height: 24px; color: var(--dsw-alias-label-primary); }
49
+ .sss-box { display: flex; align-items: center; gap: 6px; width: 100%; box-sizing: border-box; height: 32px; padding: 0 8px; border: 1px solid var(--dsw-alias-border-l2); border-radius: 8px; background: var(--dsw-alias-bg-layer-1); }
50
+ .sss-icon { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; color: var(--dsw-alias-label-tertiary); }
51
+ .sss-input { flex: 1; min-width: 0; border: none; outline: none; background: transparent; font-size: 14px; line-height: 22px; color: var(--dsw-alias-label-primary); }
52
+ .sss-input::placeholder { color: var(--dsw-alias-label-dimmed); }
53
+ .sss-clear { flex: none; display: inline-flex; align-items: center; justify-content: center; width: 18px; height: 18px; padding: 0; border: none; border-radius: 4px; background: transparent; color: var(--dsw-alias-label-secondary); font-size: 12px; line-height: 1; cursor: pointer; }
54
+ .sss-clear:hover { color: var(--dsw-alias-label-primary); background: var(--dsw-alias-interactive-bg-hover); }
55
+ .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; }
56
+ .sss-groups { display: flex; flex-direction: column; gap: 8px; }
57
+ .sss-group-title { font-size: 11px; color: var(--dsw-alias-label-secondary); letter-spacing: .05em; padding: 2px 6px; }
58
+ .sss-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
59
+ .sss-item { display: flex; align-items: center; gap: 6px; padding: 7px 8px; border-radius: 7px; cursor: pointer; }
60
+ .sss-item:hover { background: var(--dsw-alias-interactive-bg-hover); }
61
+ .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; }
62
+ .sss-item-id { font-size: 10px; color: var(--dsw-alias-label-secondary); flex-shrink: 0; }
63
+ .sss-empty { margin: 0; padding: 10px 8px; color: var(--dsw-alias-label-secondary); font-size: 13px; }
64
+ .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; }
65
+ `
66
+ let stylesInstalled = false
67
+ function installStyles() {
68
+ if (stylesInstalled || typeof document === 'undefined') return
69
+ stylesInstalled = true
70
+ const tag = document.createElement('style')
71
+ tag.dataset.plugin = '@objectivex666/dsh-settings-search'
72
+ tag.dataset.pluginCss = '@objectivex666/dsh-settings-search/panel'
73
+ tag.textContent = CSS
74
+ document.head.appendChild(tag)
75
+ return () => {
76
+ tag.remove()
77
+ stylesInstalled = false
78
+ }
79
+ }
80
+
81
+ const resolveLabel = (entry) => {
82
+ const raw = entry.options.label
83
+ if (typeof raw === 'function') return String(raw())
84
+ return raw == null ? '' : String(raw)
85
+ }
86
+
87
+ function apply(ctx) {
88
+ ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'settings-search: dictionaries')
89
+ const t = ctx.locale.bind(NS)
90
+ const removeStyles = installStyles()
91
+ if (typeof removeStyles === 'function') ctx.effect(removeStyles, 'settings-search: styles')
92
+
93
+ const readSections = () => ctx.slots.entries('settings.section')
94
+ .filter((e) => e.options.id !== 'settings-search')
95
+ .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
96
+ .sort((a, b) => a.order - b.order)
97
+ const readRows = () => ctx.slots.entries('settings.general.item')
98
+ .map((e) => ({ id: e.options.id, order: e.options.order ?? 0, label: resolveLabel(e) }))
99
+ .sort((a, b) => a.order - b.order)
100
+
101
+ const SearchIcon = React.createElement('svg', {
102
+ className: 'sss-icon',
103
+ viewBox: '0 0 16.5 16.5',
104
+ width: 16,
105
+ height: 16,
106
+ xmlns: 'http://www.w3.org/2000/svg',
107
+ 'aria-hidden': 'true',
108
+ }, [
109
+ React.createElement('path', {
110
+ 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',
111
+ fill: 'currentColor',
112
+ }),
113
+ React.createElement('path', {
114
+ d: 'M16.000417 15.041079L15.044449 16.000433L11.530434 12.473588L12.486298 11.514234L16.000417 15.041079Z',
115
+ fill: 'currentColor',
116
+ }),
117
+ ])
118
+
119
+ function SettingsHeaderSearch() {
120
+ const [query, setQuery] = React.useState('')
121
+ const [open, setOpen] = React.useState(false)
122
+ const [sections, setSections] = React.useState(readSections)
123
+ const [rows, setRows] = React.useState(readRows)
124
+ const [selected, setSelected] = React.useState(null)
125
+ React.useEffect(() => {
126
+ const refresh = () => {
127
+ setSections(readSections())
128
+ setRows(readRows())
129
+ }
130
+ const offs = [
131
+ ctx.slots.subscribe('settings.section', refresh),
132
+ ctx.slots.subscribe('settings.general.item', refresh),
133
+ ctx.locale.subscribe(refresh),
134
+ ]
135
+ return () => { for (const off of offs) off() }
136
+ }, [])
137
+
138
+ const q = query.trim().toLowerCase()
139
+ const tokens = q.split(/\s+/).filter(Boolean)
140
+ const match = (label, id) => tokens.every((tok) => (label + ' ' + id).toLowerCase().includes(tok))
141
+ const shownSections = tokens.length ? sections.filter((s) => match(s.label, s.id)) : []
142
+ const shownRows = tokens.length ? rows.filter((r) => match(r.label, r.id)) : []
143
+ const none = tokens.length > 0 && shownSections.length === 0 && shownRows.length === 0
144
+
145
+ const renderItem = (item, kind) => React.createElement('li', {
146
+ key: item.id,
147
+ className: 'sss-item',
148
+ onMouseDown: () => { setSelected(item); setOpen(false) },
149
+ }, [
150
+ React.createElement('span', { className: 'sss-item-label' }, item.label || item.id),
151
+ React.createElement('span', { className: 'sss-item-id' }, kind),
152
+ ])
153
+
154
+ return React.createElement('div', { className: 'sss-wrap' }, [
155
+ React.createElement('div', { className: 'sss-title' }, t('title')),
156
+ React.createElement('div', { className: 'sss-box' }, [
157
+ SearchIcon,
158
+ React.createElement('input', {
159
+ className: 'sss-input',
160
+ value: query,
161
+ placeholder: t('placeholder'),
162
+ spellCheck: false,
163
+ onChange: (e) => { setQuery(e.target.value); setOpen(true) },
164
+ onFocus: () => setOpen(true),
165
+ onBlur: () => setOpen(false),
166
+ }),
167
+ query !== '' ? React.createElement('button', {
168
+ className: 'sss-clear',
169
+ type: 'button',
170
+ onClick: () => { setQuery(''); setSelected(null); setOpen(false) },
171
+ }, '✕') : null,
172
+ ]),
173
+ open && tokens.length > 0 ? React.createElement('div', { className: 'sss-pop' }, [
174
+ none
175
+ ? React.createElement('p', { className: 'sss-empty' }, t('empty'))
176
+ : React.createElement('div', { className: 'sss-groups' }, [
177
+ shownSections.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
178
+ React.createElement('div', { className: 'sss-group-title' }, t('pages')),
179
+ React.createElement('ul', { className: 'sss-list' }, shownSections.map((s) => renderItem(s, t('page')))),
180
+ ]) : null,
181
+ shownRows.length > 0 ? React.createElement('div', { className: 'sss-group' }, [
182
+ React.createElement('div', { className: 'sss-group-title' }, t('rows')),
183
+ React.createElement('ul', { className: 'sss-list' }, shownRows.map((r) => renderItem(r, t('row')))),
184
+ ]) : null,
185
+ ]),
186
+ ]) : null,
187
+ selected !== null
188
+ ? React.createElement('div', { className: 'sss-guide' }, t('guide').replace('{label}', selected.label || selected.id))
189
+ : null,
190
+ ])
191
+ }
192
+
193
+ ctx.slots.inject('settings.header', () => ctx.slots.register({
194
+ name: 'settings.header',
195
+ priority: -1,
196
+ locale: NS,
197
+ }, SettingsHeaderSearch))
198
+ }
199
+
200
+ exports.apply = apply
201
+ exports.inject = ['slots', 'locale']
202
+ return module.exports
203
+ },
204
+ })
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.0.0",
4
- "description": "DSH settings panel search plugin – instantly filter setting sections and general items with navigation guidance.",
3
+ "version": "1.1.1",
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": "commonjs",
25
- "main": "dsh-settings-search.js",
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
- "peerDependencies": {
32
- "cordis": "^3.0.0 || ^4.0.0"
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": "echo \"Error: no test specified\" && exit 1"
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`)