dsh-mini-utility-dock 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/CHANGELOG.en.md +7 -0
- package/CHANGELOG.md +7 -0
- package/LICENSE +21 -0
- package/README.en.md +12 -0
- package/README.md +21 -0
- package/bin/dsh-mini-utility-dock.js +72 -0
- package/dist/bootstrap.js +249 -0
- package/package.json +20 -0
package/CHANGELOG.en.md
ADDED
package/CHANGELOG.md
ADDED
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 PracticalIssue
|
|
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,12 @@
|
|
|
1
|
+
# dsh-mini-utility-dock
|
|
2
|
+
|
|
3
|
+
The shared DSH utility dock package. It provides a canonical, self-contained classic-script fragment and a build-time CLI for embedding it into a plugin `client.js`.
|
|
4
|
+
|
|
5
|
+
Put these markers in the client file:
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// <dsh-mini-utility-dock>
|
|
9
|
+
// </dsh-mini-utility-dock>
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Run `npx dsh-mini-utility-dock sync path/to/client.js` to embed the current fragment, or `... check ...` in CI. Marker indentation is preserved. The fragment deduplicates itself through the page-local global protocol v1.
|
package/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# dsh-mini-utility-dock
|
|
2
|
+
|
|
3
|
+
DSH 插件共享的 utility dock:提供一个 canonical classic-script 片段,并在构建时嵌入插件的 `client.js`。
|
|
4
|
+
|
|
5
|
+
## 使用
|
|
6
|
+
|
|
7
|
+
在目标文件中放置标记:
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
// <dsh-mini-utility-dock>
|
|
11
|
+
// </dsh-mini-utility-dock>
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
然后运行:
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npx dsh-mini-utility-dock sync path/to/client.js
|
|
18
|
+
npx dsh-mini-utility-dock check path/to/client.js
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`sync` 保留标记及其缩进;`check` 在内容漂移时以非零状态退出。内嵌脚本通过 global protocol v1 在页面内去重,插件仍可单独运行。
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFile, writeFile, stat } from 'node:fs/promises'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
const START = '// <dsh-mini-utility-dock>'
|
|
7
|
+
const END = '// </dsh-mini-utility-dock>'
|
|
8
|
+
const packageRoot = resolve(fileURLToPath(new URL('..', import.meta.url)))
|
|
9
|
+
const bootstrapPath = resolve(packageRoot, 'dist', 'bootstrap.js')
|
|
10
|
+
|
|
11
|
+
function usage() {
|
|
12
|
+
return 'Usage: dsh-mini-utility-dock <sync|check> <client-file>'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function error(message) {
|
|
16
|
+
console.error(`dsh-mini-utility-dock: ${message}`)
|
|
17
|
+
process.exitCode = 1
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function loadTarget(fileName) {
|
|
21
|
+
if (!fileName || fileName.startsWith('-')) throw new Error('client-file is required')
|
|
22
|
+
const target = resolve(fileName)
|
|
23
|
+
const info = await stat(target).catch(() => null)
|
|
24
|
+
if (!info) throw new Error(`file does not exist: ${target}`)
|
|
25
|
+
if (!info.isFile()) throw new Error(`client-file is not a regular file: ${target}`)
|
|
26
|
+
return { target, source: await readFile(target, 'utf8') }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function indentation(line) {
|
|
30
|
+
return (/^[ \t]*/.exec(line) || [''])[0]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function locate(source) {
|
|
34
|
+
const lines = source.split(/\r?\n/)
|
|
35
|
+
const starts = lines.reduce((found, line, index) => line.trim() === START ? [...found, index] : found, [])
|
|
36
|
+
const ends = lines.reduce((found, line, index) => line.trim() === END ? [...found, index] : found, [])
|
|
37
|
+
if (starts.length !== 1 || ends.length !== 1 || ends[0] <= starts[0]) {
|
|
38
|
+
throw new Error(`expected exactly one marked block (${START} ... ${END})`)
|
|
39
|
+
}
|
|
40
|
+
return { lines, start: starts[0], end: ends[0], indent: indentation(lines[starts[0]]) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function rendered(source, bootstrap) {
|
|
44
|
+
const newline = source.includes('\r\n') ? '\r\n' : '\n'
|
|
45
|
+
const block = locate(source)
|
|
46
|
+
const body = bootstrap.replace(/\r?\n$/, '').split(/\r?\n/)
|
|
47
|
+
.map((line) => line ? block.indent + line : '')
|
|
48
|
+
const output = [...block.lines.slice(0, block.start + 1), ...body, ...block.lines.slice(block.end)]
|
|
49
|
+
.join(newline)
|
|
50
|
+
return { output, block }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function main() {
|
|
54
|
+
const [command, fileName, ...rest] = process.argv.slice(2)
|
|
55
|
+
if (!['sync', 'check'].includes(command) || !fileName || rest.length) throw new Error(usage())
|
|
56
|
+
const { target, source } = await loadTarget(fileName)
|
|
57
|
+
const bootstrap = await readFile(bootstrapPath, 'utf8')
|
|
58
|
+
const result = rendered(source, bootstrap)
|
|
59
|
+
if (command === 'check') {
|
|
60
|
+
if (source !== result.output) throw new Error(`marked block is out of date: ${target}`)
|
|
61
|
+
console.log(`ok: ${target}`)
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
if (source === result.output) {
|
|
65
|
+
console.log(`unchanged: ${target}`)
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
await writeFile(target, result.output, 'utf8')
|
|
69
|
+
console.log(`synced: ${target}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
main().catch((cause) => error(cause instanceof Error ? cause.message : String(cause)))
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// Mini Utility Dock bootstrap. DSH client artifacts are self-contained classic
|
|
2
|
+
// scripts, so this fragment is embedded at build time. At runtime the dock is a
|
|
3
|
+
// page-local protocol with no package or plugin dependency.
|
|
4
|
+
//
|
|
5
|
+
// Protocol invariants (createhelper.dsh.utility-dock v1), all page-local:
|
|
6
|
+
// - Exactly one dock container in the page. Whoever loads first creates it;
|
|
7
|
+
// everyone else joins. Joining never takes over an existing dock.
|
|
8
|
+
// - register() requires a non-empty `id` and an `onActivate()`; a dock item
|
|
9
|
+
// is a launcher, and each plugin owns and renders its own panel.
|
|
10
|
+
// - Activating one item deactivates the others.
|
|
11
|
+
// - An item's `icon` is untrusted markup: only a presentational inline SVG
|
|
12
|
+
// reaches the page, anything else renders the label as text.
|
|
13
|
+
// - The registration disposer carries an ownership token, so a stale HMR
|
|
14
|
+
// disposer cannot delete a newer registration for the same id.
|
|
15
|
+
// - Placement is shared and persisted; `hidden` keeps a recovery entry.
|
|
16
|
+
|
|
17
|
+
const DOCK_KEY = '__CREATEHELPER_DSH_UTILITY_DOCK_V1__'
|
|
18
|
+
const DOCK_PROTOCOL = 'createhelper.dsh.utility-dock'
|
|
19
|
+
const DOCK_VERSION = 1
|
|
20
|
+
const DOCK_PLACEMENT_KEY = 'createhelper.utilityDock.placement'
|
|
21
|
+
const DOCK_CSS_ID = 'createhelper-utility-dock'
|
|
22
|
+
const DOCK_SNAPSHOT = 'createhelper.utility-dock/1+placement'
|
|
23
|
+
const DOCK_LEFT_FALLBACK_PX = 80
|
|
24
|
+
|
|
25
|
+
const warnDockGeometry = (left) => {
|
|
26
|
+
if (typeof console === 'undefined' || typeof console.warn !== 'function') return
|
|
27
|
+
console.warn('[dsh-mini-utility-dock] shell geometry unavailable; falling back to left=' + left + 'px')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const isCompatibleDock = (value) => !!value &&
|
|
31
|
+
typeof value.register === 'function' &&
|
|
32
|
+
typeof value.setPlacement === 'function' &&
|
|
33
|
+
typeof value.getPlacement === 'function' &&
|
|
34
|
+
// Builds before the protocol metadata shipped already implemented v1.
|
|
35
|
+
(value.protocol === undefined ||
|
|
36
|
+
(value.protocol === DOCK_PROTOCOL && value.version === DOCK_VERSION))
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Dock chrome styles live here because the creator owns the container. Both
|
|
40
|
+
* shipped plugins previously carried their own copy of these five rules, so a
|
|
41
|
+
* dock created by the plugin that happens to load second still painted.
|
|
42
|
+
*/
|
|
43
|
+
function ensureUtilityDockStyles() {
|
|
44
|
+
if (typeof document === 'undefined') return
|
|
45
|
+
if (document.querySelector('style[data-plugin-css="' + DOCK_CSS_ID + '"]') !== null) return
|
|
46
|
+
const styleEl = document.createElement('style')
|
|
47
|
+
styleEl.setAttribute('data-plugin-css', DOCK_CSS_ID)
|
|
48
|
+
styleEl.textContent =
|
|
49
|
+
'.createhelper-utility-dock{position:fixed;bottom:16px;z-index:9997;display:flex;align-items:center;gap:2px;padding:3px;border:1px solid var(--dsw-alias-border-l1);border-radius:12px;background:var(--dsw-alias-bg-overlay);box-shadow:0 6px 22px rgba(0,0,0,.24);pointer-events:auto}' +
|
|
50
|
+
'.createhelper-utility-dock[hidden]{display:none}' +
|
|
51
|
+
'.createhelper-utility-dock-item{width:32px;height:32px;display:inline-flex;align-items:center;justify-content:center;padding:0;border:0;border-radius:9px;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}' +
|
|
52
|
+
'.createhelper-utility-dock-item:hover,.createhelper-utility-dock-item[aria-pressed="true"]{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}' +
|
|
53
|
+
'.createhelper-utility-dock-item svg{display:block}'
|
|
54
|
+
document.head.appendChild(styleEl)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* An item's `icon` is markup another plugin hands to `innerHTML`, so the dock —
|
|
59
|
+
* not each registrant — owns what reaches the page. Admit a single inline SVG
|
|
60
|
+
* whose tags and attributes are presentational; `href`, `style`, `on*`,
|
|
61
|
+
* `<script>` and `<foreignObject>` are exactly the shapes that turn an icon
|
|
62
|
+
* into a script, and none of them draws a glyph.
|
|
63
|
+
*/
|
|
64
|
+
const DOCK_ICON_TAGS = /^(svg|g|path|rect|circle|ellipse|line|polyline|polygon)$/i
|
|
65
|
+
const DOCK_ICON_ATTRS = /^(width|height|viewBox|preserveAspectRatio|fill|fill-rule|fill-opacity|stroke|stroke-width|stroke-linecap|stroke-linejoin|stroke-miterlimit|stroke-opacity|stroke-dasharray|stroke-dashoffset|opacity|d|x|y|x1|y1|x2|y2|rx|ry|cx|cy|r|points|transform|role|aria-hidden|focusable|class)$/i
|
|
66
|
+
|
|
67
|
+
function safeDockIcon(icon) {
|
|
68
|
+
if (typeof icon !== 'string') return false
|
|
69
|
+
const markup = icon.trim()
|
|
70
|
+
if (!/^<svg(?:\s|>)/i.test(markup) || !/<\/svg>$/i.test(markup)) return false
|
|
71
|
+
// A comment, CDATA or processing instruction can carry markup the scans below
|
|
72
|
+
// never look at.
|
|
73
|
+
if (/<!--|<!\[CDATA\[|<\?|]]>/.test(markup)) return false
|
|
74
|
+
// Splitting on `"` pairs the quotes up: an even segment count is an unbalanced
|
|
75
|
+
// quote, and only the odd-index segments are quoted values. A value holding a
|
|
76
|
+
// tag boundary would move `>` past what the scans below can see.
|
|
77
|
+
const quoted = markup.split('"')
|
|
78
|
+
if (quoted.length % 2 === 0) return false
|
|
79
|
+
for (let i = 1; i < quoted.length; i += 2) {
|
|
80
|
+
if (/[<>]/.test(quoted[i])) return false
|
|
81
|
+
}
|
|
82
|
+
if (/[\s"']on[a-z]+\s*=/i.test(markup)) return false
|
|
83
|
+
if (/javascript\s*:/i.test(markup)) return false
|
|
84
|
+
// A same-document fragment reference is how a gradient is painted; anything
|
|
85
|
+
// else turns a presentational attribute into a network read.
|
|
86
|
+
if (/url\(\s*(?!#)/i.test(markup)) return false
|
|
87
|
+
const tags = markup.match(/<\/?[a-zA-Z][^>]*>/g)
|
|
88
|
+
if (!tags) return false
|
|
89
|
+
for (const tag of tags) {
|
|
90
|
+
const name = /^<\/?\s*([^/>\s]+)/.exec(tag)
|
|
91
|
+
if (!name || !DOCK_ICON_TAGS.test(name[1])) return false
|
|
92
|
+
for (const raw of tag.match(/[^=<>\s]+\s*=/g) || []) {
|
|
93
|
+
if (!DOCK_ICON_ATTRS.test(raw.replace(/\s*=$/, ''))) return false
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return true
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Two characters stand in for an icon the dock could not admit. */
|
|
100
|
+
function dockIconFallback(item) {
|
|
101
|
+
const label = String(item.label || item.id || '')
|
|
102
|
+
return label.slice(0, 2)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getUtilityDock() {
|
|
106
|
+
if (isCompatibleDock(window[DOCK_KEY])) return window[DOCK_KEY]
|
|
107
|
+
ensureUtilityDockStyles()
|
|
108
|
+
const items = new Map()
|
|
109
|
+
let root = null
|
|
110
|
+
let resizeObserver = null
|
|
111
|
+
let mutationObserver = null
|
|
112
|
+
const readPlacement = () => {
|
|
113
|
+
try {
|
|
114
|
+
const value = localStorage.getItem(DOCK_PLACEMENT_KEY)
|
|
115
|
+
if (value === 'main-bottom-right' || value === 'hidden') return value
|
|
116
|
+
} catch (e) { }
|
|
117
|
+
return 'main-bottom-left'
|
|
118
|
+
}
|
|
119
|
+
let placement = readPlacement()
|
|
120
|
+
const findShellFrame = () => {
|
|
121
|
+
const overlay = document.querySelector('[data-shell-overlay]')
|
|
122
|
+
return (overlay && overlay.parentElement) || null
|
|
123
|
+
}
|
|
124
|
+
let geometryWarned = false
|
|
125
|
+
const measureDockLeft = () => {
|
|
126
|
+
const frame = findShellFrame()
|
|
127
|
+
const sidebar = frame && frame.firstElementChild
|
|
128
|
+
const sidebarRect = sidebar && typeof sidebar.getBoundingClientRect === 'function'
|
|
129
|
+
? sidebar.getBoundingClientRect()
|
|
130
|
+
: null
|
|
131
|
+
if (!sidebarRect) {
|
|
132
|
+
if (!geometryWarned) {
|
|
133
|
+
geometryWarned = true
|
|
134
|
+
warnDockGeometry(DOCK_LEFT_FALLBACK_PX)
|
|
135
|
+
}
|
|
136
|
+
return DOCK_LEFT_FALLBACK_PX
|
|
137
|
+
}
|
|
138
|
+
return Math.max(16, Math.round(sidebarRect.right + 16))
|
|
139
|
+
}
|
|
140
|
+
const updateGeometry = () => {
|
|
141
|
+
if (!root) return
|
|
142
|
+
root.hidden = placement === 'hidden'
|
|
143
|
+
root.dataset.placement = placement
|
|
144
|
+
document.documentElement.dataset.createhelperUtilityDockPlacement = placement
|
|
145
|
+
root.style.right = ''
|
|
146
|
+
root.style.left = ''
|
|
147
|
+
if (placement === 'main-bottom-right') {
|
|
148
|
+
root.style.right = '16px'
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
const left = measureDockLeft()
|
|
152
|
+
root.style.left = left + 'px'
|
|
153
|
+
document.documentElement.style.setProperty('--createhelper-utility-dock-left', left + 'px')
|
|
154
|
+
}
|
|
155
|
+
const render = () => {
|
|
156
|
+
if (!root) {
|
|
157
|
+
root = document.createElement('nav')
|
|
158
|
+
root.className = 'createhelper-utility-dock'
|
|
159
|
+
root.setAttribute('aria-label', 'DSH utilities')
|
|
160
|
+
document.body.appendChild(root)
|
|
161
|
+
window.addEventListener('resize', updateGeometry)
|
|
162
|
+
const observeLayout = () => {
|
|
163
|
+
const frame = findShellFrame()
|
|
164
|
+
if (!frame) return false
|
|
165
|
+
mutationObserver?.disconnect()
|
|
166
|
+
mutationObserver = null
|
|
167
|
+
if (typeof ResizeObserver === 'function' && !resizeObserver) {
|
|
168
|
+
resizeObserver = new ResizeObserver(updateGeometry)
|
|
169
|
+
resizeObserver.observe(frame)
|
|
170
|
+
if (frame.firstElementChild) resizeObserver.observe(frame.firstElementChild)
|
|
171
|
+
}
|
|
172
|
+
updateGeometry()
|
|
173
|
+
return true
|
|
174
|
+
}
|
|
175
|
+
if (!observeLayout() && typeof MutationObserver === 'function') {
|
|
176
|
+
mutationObserver = new MutationObserver(() => { observeLayout() })
|
|
177
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true })
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
root.replaceChildren()
|
|
181
|
+
Array.from(items.values()).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id)).forEach((item) => {
|
|
182
|
+
const button = document.createElement('button')
|
|
183
|
+
button.type = 'button'
|
|
184
|
+
button.className = 'createhelper-utility-dock-item'
|
|
185
|
+
button.dataset.createhelperDockItem = item.id
|
|
186
|
+
button.title = item.label
|
|
187
|
+
button.setAttribute('aria-label', item.label)
|
|
188
|
+
button.setAttribute('aria-pressed', item.active ? 'true' : 'false')
|
|
189
|
+
// Sanitized here, not in register(), so `update({ icon })` cannot be a
|
|
190
|
+
// second way past the gate.
|
|
191
|
+
if (safeDockIcon(item.icon)) button.innerHTML = item.icon
|
|
192
|
+
else button.textContent = dockIconFallback(item)
|
|
193
|
+
button.addEventListener('click', () => {
|
|
194
|
+
if (!item.active) {
|
|
195
|
+
for (const other of items.values()) {
|
|
196
|
+
if (other.id !== item.id && other.active && typeof other.onDeactivate === 'function') {
|
|
197
|
+
other.onDeactivate()
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
item.onActivate()
|
|
202
|
+
})
|
|
203
|
+
root.appendChild(button)
|
|
204
|
+
})
|
|
205
|
+
updateGeometry()
|
|
206
|
+
}
|
|
207
|
+
const api = {
|
|
208
|
+
protocol: DOCK_PROTOCOL,
|
|
209
|
+
version: DOCK_VERSION,
|
|
210
|
+
snapshot: DOCK_SNAPSHOT,
|
|
211
|
+
register(item) {
|
|
212
|
+
if (!item || typeof item.id !== 'string' || !item.id || typeof item.onActivate !== 'function') {
|
|
213
|
+
throw new TypeError('utility dock item requires a non-empty id and onActivate()')
|
|
214
|
+
}
|
|
215
|
+
const registration = Object.freeze({})
|
|
216
|
+
items.set(item.id, { ...item, registration, order: Number(item.order) || 0, active: !!item.active })
|
|
217
|
+
render()
|
|
218
|
+
return {
|
|
219
|
+
update(patch) {
|
|
220
|
+
const current = items.get(item.id)
|
|
221
|
+
if (!current || current.registration !== registration) return
|
|
222
|
+
items.set(item.id, { ...current, ...patch })
|
|
223
|
+
render()
|
|
224
|
+
},
|
|
225
|
+
dispose() {
|
|
226
|
+
const current = items.get(item.id)
|
|
227
|
+
if (!current || current.registration !== registration) return
|
|
228
|
+
items.delete(item.id)
|
|
229
|
+
if (items.size) { render(); return }
|
|
230
|
+
resizeObserver?.disconnect()
|
|
231
|
+
resizeObserver = null
|
|
232
|
+
mutationObserver?.disconnect()
|
|
233
|
+
mutationObserver = null
|
|
234
|
+
window.removeEventListener('resize', updateGeometry)
|
|
235
|
+
root?.remove()
|
|
236
|
+
root = null
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
},
|
|
240
|
+
setPlacement(next) {
|
|
241
|
+
placement = next === 'main-bottom-right' || next === 'hidden' ? next : 'main-bottom-left'
|
|
242
|
+
try { localStorage.setItem(DOCK_PLACEMENT_KEY, placement) } catch (e) { }
|
|
243
|
+
updateGeometry()
|
|
244
|
+
},
|
|
245
|
+
getPlacement() { return placement }
|
|
246
|
+
}
|
|
247
|
+
window[DOCK_KEY] = api
|
|
248
|
+
return api
|
|
249
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-mini-utility-dock",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared classic-script utility dock protocol for DSH plugins.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"dsh-mini-utility-dock": "bin/dsh-mini-utility-dock.js"
|
|
8
|
+
},
|
|
9
|
+
"files": ["bin", "dist", "README.md", "README.en.md", "LICENSE", "CHANGELOG.md", "CHANGELOG.en.md"],
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "node --test test/*.test.js"
|
|
12
|
+
},
|
|
13
|
+
"engines": { "node": ">=20" },
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/xswt442-cmd/dsh-mini-utility-dock.git"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["dsh", "deepseek-harness", "dsh-plugin", "utility-dock"],
|
|
19
|
+
"license": "MIT"
|
|
20
|
+
}
|