dsh-composer-glass 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-composer-glass contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,57 @@
1
+ <div align="center">
2
+
3
+ # dsh-composer-glass
4
+
5
+ **Turns the DSH composer into a single uniformly translucent frosted-glass pane**
6
+
7
+ Toggle lives in Settings → General
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+ [![Type: DSH Plugin](https://img.shields.io/badge/Type-DSH%20Plugin-8A2BE2.svg)](#install)
11
+
12
+ </div>
13
+
14
+ ***
15
+
16
+ ## Preview
17
+
18
+ | Start a session | In conversation |
19
+ | :---------------------------------------------: | :-----------------------------------------------: |
20
+ | ![Start a session](images/01-start-session.png) | ![In conversation](images/02-in-conversation.png) |
21
+
22
+ ## Features
23
+
24
+ - The composer is rendered as **one uniformly translucent frosted-glass material**, not partial blur
25
+ - A single on/off switch under **Settings → General**
26
+
27
+ ![Settings](images/03-settings.png)
28
+
29
+ ## Quick start
30
+
31
+ | Action | Command |
32
+ | :-------- | :------------------------------------------------------- |
33
+ | Install | `dsh plugin --profile web add dsh-composer-glass` |
34
+ | Upgrade | `dsh plugin --profile web add dsh-composer-glass@latest` |
35
+ | Uninstall | `dsh plugin --profile web remove dsh-composer-glass` |
36
+
37
+ Restart DSH, then enable it under **Settings → General**.
38
+
39
+ > \[!NOTE]
40
+ > On Windows, if you clean up leftovers by hand, just delete
41
+ > `node_modules\dsh-composer-glass`.
42
+
43
+ ## Notes
44
+
45
+ - **The switch does not persist**: it resets on page reload — persisting it
46
+ needs a Host-side settings namespace, which is left as a follow-up change.
47
+ - **A material, not a refractor**: the composer sits over flat colour, so
48
+ refracting it produces no visible change. The reasoning and the measurements
49
+ are in [docs/WHY-NOT-REFRACTION.md](docs/WHY-NOT-REFRACTION.md).
50
+
51
+ ## License
52
+
53
+ <div align="center">
54
+
55
+ [MIT](LICENSE)
56
+
57
+ </div>
package/README.md ADDED
@@ -0,0 +1,53 @@
1
+ <div align="center">
2
+
3
+ # dsh-composer-glass
4
+
5
+ **把 DSH 的输入框变成一整块均匀半透明的毛玻璃面板**
6
+
7
+ 开关位于 设置 → 通用
8
+
9
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
10
+ [![Type: DSH Plugin](https://img.shields.io/badge/Type-DSH%20Plugin-8A2BE2.svg)](#安装)
11
+
12
+ </div>
13
+
14
+ ***
15
+
16
+ ## 效果预览
17
+
18
+ | 开始会话 | 会话效果 |
19
+ | :----------------------------------: | :------------------------------------: |
20
+ | ![开始会话](images/01-start-session.png) | ![会话效果](images/02-in-conversation.png) |
21
+
22
+ ## 特性
23
+
24
+ - 输入框整体呈现**均匀半透明的毛玻璃材质**,而非局部磨砂
25
+ - 在 **设置 → 通用** 中提供一键开关
26
+
27
+ ![设置页面](images/03-settings.png)
28
+
29
+ ## 快速开始
30
+
31
+ | 操作 | 命令 |
32
+ | :- | :------------------------------------------------------- |
33
+ | 安装 | `dsh plugin --profile web add dsh-composer-glass` |
34
+ | 升级 | `dsh plugin --profile web add dsh-composer-glass@latest` |
35
+ | 卸载 | `dsh plugin --profile web remove dsh-composer-glass` |
36
+
37
+ 安装完成后重启 DSH,在 **设置 → 通用** 里打开开关。
38
+
39
+ > \[!NOTE]
40
+ > Windows 上如果手动清理残留目录,删掉 `node_modules\dsh-composer-glass` 即可。
41
+
42
+ ## 说明
43
+
44
+ - **开关不持久化**:开关状态刷新页面后会重置 —— 持久化需要 Host 侧 settings 命名空间,留作后续改动。
45
+ - **材质,而非折射**:输入框背后是平坦纯色,折射它不会有任何可见变化。原因与实测记录见 [docs/WHY-NOT-REFRACTION.md](docs/WHY-NOT-REFRACTION.md)。
46
+
47
+ ## 许可
48
+
49
+ <div align="center">
50
+
51
+ [MIT](LICENSE)
52
+
53
+ </div>
@@ -0,0 +1,12 @@
1
+ # dsh-composer-glass — bundle patch
2
+ #
3
+ # Declared through package.json:
4
+ # "dsh": { "bundle": { "patch": "./cordis.patch.yml" } }
5
+ #
6
+ # Loading this package as a profile layer therefore registers its plugin row
7
+ # automatically. Do not also hand-write an `- insert:` row for this package in a
8
+ # profile's own cordis.patch.yml — the id would be inserted twice.
9
+
10
+ - insert:
11
+ - id: composer-glass
12
+ name: 'dsh-composer-glass'
Binary file
Binary file
Binary file
package/lib/client.js ADDED
@@ -0,0 +1,292 @@
1
+ /**
2
+ * dsh-composer-glass — browser half.
3
+ *
4
+ * Bundle format: `window.__ModuleLoader__.load({ id, factory(require) })`. The
5
+ * shell supplies the CJS shim and pre-registered modules (react, ...), so this
6
+ * file needs no bundler and no build step.
7
+ *
8
+ * WHAT IT DOES
9
+ * Turns the composer card into a single uniformly translucent pane. It is a
10
+ * MATERIAL, not a refractor: measured behaviour of the target engine made real
11
+ * backdrop refraction unworkable here, and the reasoning is recorded in the
12
+ * repository notes rather than silently dropped.
13
+ *
14
+ * The one control is a preference row under Settings -> General. That slot's
15
+ * runtime contract states the owner passes NO props, so the row draws its own
16
+ * label, value and write path — the internals below mirror DSH's own General
17
+ * rows (see `EnterBehaviorRow` in dsh-client-ui-conversation) field for field.
18
+ *
19
+ * Assets: none. No images, no fonts, no network. The whole effect is CSS.
20
+ */
21
+
22
+ window.__ModuleLoader__.load({
23
+ id: 'dsh-composer-glass',
24
+ factory: (require) => {
25
+ var module = { exports: {} }
26
+ var exports = module.exports
27
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' })
28
+ const React = require('react')
29
+
30
+ /** Root attribute whose value ("on" / "off") gates every style rule. */
31
+ const ROOT_ATTR = 'data-dsh-glass'
32
+ /** Class-name prefix, so this plugin's rules can never collide. */
33
+ const PREFIX = 'dsh-glass'
34
+ /** Locale namespace owned by this plugin. */
35
+ const NS = 'dsh-composer-glass'
36
+ /** Cell key for the Settings -> General row. */
37
+ const ROW_ID = 'dsh-glass-pane'
38
+ /** Row order: after the shipped rows (… transcript-view 12, composer-enter 20). */
39
+ const ROW_ORDER = 30
40
+
41
+ /**
42
+ * The tuned material. These are the deliverable: frozen constants rather
43
+ * than runtime variables, so the look cannot drift on its own.
44
+ */
45
+ const MATERIAL = {
46
+ blur: 10,
47
+ saturate: 165,
48
+ brightness: 0.91,
49
+ tint: 0.17,
50
+ radius: 22,
51
+ ring: 0.34
52
+ }
53
+
54
+ // ---------------------------------------------------------------- locale
55
+
56
+ /** Bilingual dictionaries. Every shipped locale id must be present. */
57
+ const MESSAGES = {
58
+ zh: {
59
+ 'row.title': '输入框毛玻璃',
60
+ 'row.desc': '让输入框变成整块均匀半透明的毛玻璃面板',
61
+ 'opt.on': '开启',
62
+ 'opt.off': '关闭'
63
+ },
64
+ en: {
65
+ 'row.title': 'Composer frosted glass',
66
+ 'row.desc': 'Turn the composer into a uniformly translucent frosted-glass pane',
67
+ 'opt.on': 'On',
68
+ 'opt.off': 'Off'
69
+ }
70
+ }
71
+
72
+ // ----------------------------------------------------------------- style
73
+
74
+ /**
75
+ * Three preconditions make the pane read as glass. All three are required,
76
+ * and each was learned by breaking it:
77
+ *
78
+ * 1. Clear the opaque chrome. The shipped rule is
79
+ * .wSkVaW_root{background:var(--dsw-alias-bg-base)}
80
+ * A `background` SHORTHAND also sets a background-image layer, so
81
+ * overriding background-color alone leaves that layer behind. Hence
82
+ * `background: none`.
83
+ * 2. Remove the composer seat's downward fade,
84
+ * linear-gradient(180deg, transparent 0px, <base> 36px)
85
+ * which reads transparent -> SOLID and so makes the LOWER part opaque.
86
+ * It is the direct cause of "only the top of the card looks transparent".
87
+ * 3. The pane's own low alpha. `tint` is the pane background-color's alpha
88
+ * and is the pane's own property; it does not depend on what sits behind.
89
+ *
90
+ * FRAGILITY: the `.wSkVaW_*` selectors are build-hashed class names read out
91
+ * of the shipped bundle. They hold for the version this was written against
92
+ * and can change on a DSH upgrade. If the effect silently reverts, check
93
+ * these first. `[data-composer-card]` is a stable data attribute and is
94
+ * preferred wherever one is available.
95
+ */
96
+ const CSS = [
97
+ '.wSkVaW_root, .wSkVaW_scrollBody, .wSkVaW_body, .wSkVaW_viewArea { background: none !important; }',
98
+ '.wSkVaW_composerSeat { background: none !important; }',
99
+
100
+ ':root {',
101
+ ' --dsh-glass-ring: rgba(255,255,255,0.34);',
102
+ ' --dsh-glass-inner: rgba(255,255,255,0.05);',
103
+ ' --dsh-glass-shade: rgba(0,0,0,0.12);',
104
+ ' --dsh-glass-halo: 0 12px 36px rgba(0,0,0,0.28);',
105
+ '}',
106
+ // DSH carries its dark palette on body[data-ds-dark-theme]; a `.dark`
107
+ // class is never used for tokens, so `.dark` selectors would never match.
108
+ 'body[data-ds-dark-theme] {',
109
+ ' --dsh-glass-ring: rgba(255,255,255,0.22);',
110
+ ' --dsh-glass-inner: rgba(255,255,255,0.04);',
111
+ ' --dsh-glass-shade: rgba(0,0,0,0.18);',
112
+ ' --dsh-glass-halo: 0 14px 40px rgba(0,0,0,0.45);',
113
+ '}',
114
+
115
+ ':root[' + ROOT_ATTR + '="on"] [data-composer-card] {',
116
+ ' background-color: color-mix(in srgb, var(--dsw-specific-input-major) calc(' + MATERIAL.tint + ' * 100%), transparent) !important;',
117
+ ' backdrop-filter: blur(' + MATERIAL.blur + 'px) saturate(' + MATERIAL.saturate + '%) brightness(' + MATERIAL.brightness + ');',
118
+ ' -webkit-backdrop-filter: blur(' + MATERIAL.blur + 'px) saturate(' + MATERIAL.saturate + '%) brightness(' + MATERIAL.brightness + ');',
119
+ ' box-shadow:',
120
+ ' inset 0 0 0 1px var(--dsh-glass-ring),',
121
+ ' inset 0 1px 0 0 var(--dsh-glass-inner),',
122
+ ' inset 0 -12px 24px -18px var(--dsh-glass-shade),',
123
+ ' var(--dsh-glass-halo) !important;',
124
+ ' border-radius: ' + MATERIAL.radius + 'px;',
125
+ ' transition: background-color .18s ease, backdrop-filter .18s ease;',
126
+ '}',
127
+ // Exactly one decorative layer. Every extra layer erodes the uniformity
128
+ // that makes the pane read as a single piece of glass.
129
+ ':root[' + ROOT_ATTR + '="on"] [data-composer-card]::after {',
130
+ " content: '';",
131
+ ' position: absolute;',
132
+ ' inset: 0;',
133
+ ' border-radius: inherit;',
134
+ ' pointer-events: none;',
135
+ ' background: linear-gradient(170deg, rgba(255,255,255,.09) 0%, rgba(255,255,255,0) 36%);',
136
+ ' z-index: 0;',
137
+ '}',
138
+
139
+ // DSH ships no global `* { box-sizing: border-box }` reset, so a row with
140
+ // width/padding would overflow without its own.
141
+ '.' + PREFIX + '-row, .' + PREFIX + '-row * { box-sizing: border-box; }',
142
+ '.' + PREFIX + '-row {',
143
+ ' border-bottom: .5px solid var(--dsw-alias-border-l2, rgba(128,128,128,.25));',
144
+ ' align-items: center; gap: 8px; padding: 16px 0; display: flex;',
145
+ '}',
146
+ '.' + PREFIX + '-rowText { flex-direction: column; flex: 1; gap: 4px; min-width: 0; padding-right: 48px; display: flex; }',
147
+ // Key colours are hardcoded with a token fallback: an alias token can be
148
+ // absent or resolve to something unreadable in one of the themes, and a
149
+ // failed var() fails silently.
150
+ '.' + PREFIX + '-title { color: var(--dsw-alias-label-primary, #e8e8ea); font-size: 14px; font-weight: 400; line-height: 22px; }',
151
+ '.' + PREFIX + '-desc { color: var(--dsw-alias-label-tertiary, #9a9aa0); font-size: 12px; font-weight: 400; line-height: 18px; }',
152
+ '.' + PREFIX + '-seg {',
153
+ ' background: var(--dsw-alias-bg-module-platform, rgba(128,128,128,.16));',
154
+ ' border-radius: 18px; height: 36px; padding: 3px; display: inline-flex; gap: 2px; flex: none;',
155
+ '}',
156
+ '.' + PREFIX + '-seg button {',
157
+ ' appearance: none; border: none; cursor: pointer; font: inherit;',
158
+ ' font-size: 14px; line-height: 22px;',
159
+ ' color: var(--dsw-alias-label-secondary, #b9b9c0);',
160
+ ' background: transparent; border-radius: 15px; padding: 0 14px; height: 30px;',
161
+ '}',
162
+ '.' + PREFIX + '-seg button[data-active="true"] {',
163
+ ' background: var(--dsw-alias-bg-overlay, rgba(90,90,100,.55));',
164
+ ' color: var(--dsw-alias-label-primary, #fff);',
165
+ '}'
166
+ ].join('\n')
167
+
168
+ // ------------------------------------------------------------------ state
169
+
170
+ /**
171
+ * Live enable state, owned by `apply`.
172
+ *
173
+ * A settings row unmounts when the user navigates away from the section, so
174
+ * the state cannot live inside the component. It is deliberately module
175
+ * scope rather than a settings namespace: a plugin's preference normally
176
+ * belongs in `settingsScope`, which is the durable, cross-restart home — but
177
+ * that requires a Host-side `settings.register()`, and that is the intended
178
+ * next step for this package rather than something to fake here.
179
+ */
180
+ let enabled = true
181
+ const listeners = []
182
+
183
+ function setEnabled(next) {
184
+ enabled = next
185
+ document.documentElement.setAttribute(ROOT_ATTR, next ? 'on' : 'off')
186
+ for (const notify of listeners) notify(next)
187
+ }
188
+
189
+ // -------------------------------------------------------------------- row
190
+
191
+ function GlassRow(props) {
192
+ // Prefer the renderer's injected `t` seat, and fall back to a locally
193
+ // bound translate so the row still works if the seat is absent.
194
+ const t = props && props.t ? props.t : fallbackT
195
+ const [on, setOn] = React.useState(enabled)
196
+
197
+ React.useEffect(() => {
198
+ const update = (value) => setOn(value)
199
+ listeners.push(update)
200
+ setOn(enabled)
201
+ return () => {
202
+ const at = listeners.indexOf(update)
203
+ if (at >= 0) listeners.splice(at, 1)
204
+ }
205
+ }, [])
206
+
207
+ const option = (label, value) =>
208
+ React.createElement(
209
+ 'button',
210
+ {
211
+ key: label,
212
+ type: 'button',
213
+ 'data-active': String(on === value),
214
+ 'aria-pressed': on === value,
215
+ onClick: () => setEnabled(value)
216
+ },
217
+ label
218
+ )
219
+
220
+ return React.createElement(
221
+ 'div',
222
+ { className: PREFIX + '-row' },
223
+ React.createElement(
224
+ 'div',
225
+ { className: PREFIX + '-rowText' },
226
+ React.createElement('div', { className: PREFIX + '-title' }, t('row.title')),
227
+ React.createElement('div', { className: PREFIX + '-desc' }, t('row.desc'))
228
+ ),
229
+ React.createElement(
230
+ 'div',
231
+ { className: PREFIX + '-seg', role: 'radiogroup', 'aria-label': t('row.title') },
232
+ option(t('opt.on'), true),
233
+ option(t('opt.off'), false)
234
+ )
235
+ )
236
+ }
237
+
238
+ /** Bound translate, replaced in `apply` once the locale service is in hand. */
239
+ let fallbackT = (key) => key
240
+
241
+ // ----------------------------------------------------------------- plugin
242
+
243
+ exports.inject = ['slots', 'locale']
244
+
245
+ exports.apply = function apply(ctx) {
246
+ // A formal package has no style service (the dynamic-plugin `styles`
247
+ // symbol does not exist here), so the stylesheet is a plain tag this
248
+ // plugin owns and removes with its fiber.
249
+ ctx.effect(() => {
250
+ const tag = document.createElement('style')
251
+ tag.dataset.plugin = 'dsh-composer-glass'
252
+ tag.dataset.pluginCss = 'dsh-composer-glass'
253
+ tag.textContent = CSS
254
+ document.head.appendChild(tag)
255
+ return () => tag.remove()
256
+ }, 'composer-glass: styles')
257
+
258
+ // Register one dictionary per shipped locale id. Registration bumps the
259
+ // locale revision, so already-mounted outlets pick the texts up at once.
260
+ ctx.effect(() => {
261
+ const disposers = Object.keys(MESSAGES).map((id) => ctx.locale.register(NS, id, MESSAGES[id]))
262
+ return () => disposers.forEach((dispose) => dispose())
263
+ }, 'composer-glass: dictionaries')
264
+
265
+ fallbackT = ctx.locale.bind(NS)
266
+
267
+ // The pane must be correct on first paint, before Settings is ever opened.
268
+ setEnabled(true)
269
+
270
+ ctx.effect(
271
+ () =>
272
+ ctx.slots.inject('settings.general.item', () =>
273
+ ctx.slots.register(
274
+ { name: 'settings.general.item', id: ROW_ID, order: ROW_ORDER, locale: NS },
275
+ GlassRow
276
+ )
277
+ ),
278
+ 'composer-glass: settings row'
279
+ )
280
+
281
+ ctx.effect(
282
+ () => () => {
283
+ listeners.length = 0
284
+ document.documentElement.removeAttribute(ROOT_ATTR)
285
+ },
286
+ 'composer-glass: teardown'
287
+ )
288
+ }
289
+
290
+ return module.exports
291
+ }
292
+ })
package/lib/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * dsh-composer-glass — Host half.
3
+ *
4
+ * This plugin is purely presentational: every byte of its behaviour is CSS and
5
+ * one React row in the browser. It therefore has nothing to do here, and says so
6
+ * explicitly rather than shipping an empty file that looks unfinished.
7
+ *
8
+ * Why the file exists at all: a bundle declares one plugin row, and the row's
9
+ * package is loaded on the Host as well as served to the browser. Providing a
10
+ * named, inert Host half keeps the composition symmetrical with every other DSH
11
+ * plugin and keeps the door open for a settings namespace later.
12
+ *
13
+ * THE NEXT STEP, IF THE PREFERENCE SHOULD SURVIVE A RESTART
14
+ * The on/off state currently lives in browser module scope, so it resets when
15
+ * the page reloads. Persisting it needs a settings namespace:
16
+ *
17
+ * export const inject = ['settings']
18
+ * export function apply(ctx) {
19
+ * const scope = ctx.settings.register('composer-glass', schema)
20
+ * // …and on the client: ctx.settingsScope.bind({ namespace: 'composer-glass' })
21
+ * }
22
+ *
23
+ * That is deliberately NOT stubbed out here: an unused namespace would show up
24
+ * as a configuration surface in Settings with nothing behind it, and the client
25
+ * half would still need its own binding. It is a real, separable change.
26
+ */
27
+
28
+ /** Stable plugin name, matching `cordis.patch.yml`. */
29
+ export const name = 'composer-glass'
30
+
31
+ /** No Host services are consumed. */
32
+ export const inject = []
33
+
34
+ /** Intentionally a no-op; see the file header. */
35
+ export function apply() {}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "dsh-composer-glass",
3
+ "version": "0.1.0",
4
+ "description": "DSH plugin: turn the composer into a uniformly translucent frosted-glass pane, with an on/off preference row in Settings → General.",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./client": "./lib/client.js",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "files": [
13
+ "lib",
14
+ "images",
15
+ "cordis.patch.yml",
16
+ "README.md",
17
+ "README.en.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=20"
22
+ },
23
+ "keywords": [
24
+ "dsh",
25
+ "cordis",
26
+ "glass",
27
+ "frosted-glass",
28
+ "glassmorphism",
29
+ "composer",
30
+ "theme",
31
+ "ui"
32
+ ],
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Lbunc/dsh-composer-glass.git"
36
+ },
37
+ "dsh": {
38
+ "bundle": {
39
+ "patch": "./cordis.patch.yml"
40
+ },
41
+ "client": {
42
+ "inject": [
43
+ "@deepseek-ai/dsh-client-locale"
44
+ ],
45
+ "platform": "web"
46
+ }
47
+ },
48
+ "peerDependencies": {
49
+ "react": "^18.2.0",
50
+ "@deepseek-ai/dsh-client-locale": "^0.1.5-rc.1"
51
+ },
52
+ "license": "MIT"
53
+ }