dsh-mini-utility-dock 0.1.0 → 0.1.2
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 +8 -0
- package/CHANGELOG.md +8 -0
- package/LICENSE +21 -21
- package/README.en.md +9 -0
- package/README.md +9 -0
- package/dist/bootstrap.js +227 -213
- package/package.json +20 -4
package/CHANGELOG.en.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 - 2026-09-06
|
|
4
|
+
|
|
5
|
+
- Fix the README `npm run dock:embed` examples: add the previously missing `dock:embed` script to `package.json` and switch the usage to `npm run dock:embed -- check|sync path/to/client.js` (npm argument passing needs the `--` separator, and the CLI takes `sync|check` subcommands, not `--check`). A new smoke test extracts the documented commands from both READMEs and runs them, preventing docs/script drift.
|
|
6
|
+
|
|
7
|
+
## 0.1.1 - 2026-09-04
|
|
8
|
+
|
|
9
|
+
- Normalize `label` in `register()`: a missing, blank, or non-string label falls back to `id`, so no item renders `aria-label="undefined"`.
|
|
10
|
+
|
|
3
11
|
## 0.1.0
|
|
4
12
|
|
|
5
13
|
- Add the Mini Utility Dock protocol v1 bootstrap.
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2 - 2026-09-06
|
|
4
|
+
|
|
5
|
+
- 修正 README 的 `npm run dock:embed` 示例:补上 `package.json` 缺失的 `dock:embed` 脚本,用法改为 `npm run dock:embed -- check|sync path/to/client.js`(npm 传参需要 `--` 分隔符,CLI 接受 `sync|check` 子命令而非 `--check`)。新增从双语 README 提取命令逐条执行的 smoke test,防止文档与脚本漂移。
|
|
6
|
+
|
|
7
|
+
## 0.1.1 - 2026-09-04
|
|
8
|
+
|
|
9
|
+
- 归一化 `register()` 的 `label`:缺省、空白或非字符串的 `label` 回退为 `id`,避免渲染出 `aria-label="undefined"`。
|
|
10
|
+
|
|
3
11
|
## 0.1.0
|
|
4
12
|
|
|
5
13
|
- Add the Mini Utility Dock protocol v1 bootstrap.
|
package/LICENSE
CHANGED
|
@@ -1,21 +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.
|
|
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
CHANGED
|
@@ -10,3 +10,12 @@ Put these markers in the client file:
|
|
|
10
10
|
```
|
|
11
11
|
|
|
12
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.
|
|
13
|
+
|
|
14
|
+
You can also embed from this repo's own script (it never hard-codes a consumer path; the target is passed by the caller):
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm run dock:embed -- check path/to/client.js # verify only, non-zero on drift
|
|
18
|
+
npm run dock:embed -- sync path/to/client.js # write between the markers
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
When registering, a missing, blank, or non-string `label` falls back to `id` as the accessible name, avoiding `aria-label="undefined"`.
|
package/README.md
CHANGED
|
@@ -19,3 +19,12 @@ npx dsh-mini-utility-dock check path/to/client.js
|
|
|
19
19
|
```
|
|
20
20
|
|
|
21
21
|
`sync` 保留标记及其缩进;`check` 在内容漂移时以非零状态退出。内嵌脚本通过 global protocol v1 在页面内去重,插件仍可单独运行。
|
|
22
|
+
|
|
23
|
+
也可用本仓库脚本同步(不硬编码任何消费仓路径,目标由调用方传入):
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
npm run dock:embed -- check path/to/client.js # 仅校验,漂移时非零退出
|
|
27
|
+
npm run dock:embed -- sync path/to/client.js # 写入标记之间
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
注册时若 `label` 缺省、空白或非字符串,会回退为 `id` 作为可访问名称,避免 `aria-label="undefined"`。
|
package/dist/bootstrap.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
// Mini Utility Dock bootstrap. DSH client artifacts are self-contained classic
|
|
2
2
|
// scripts, so this fragment is embedded at build time. At runtime the dock is a
|
|
3
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'
|
|
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
21
|
const DOCK_CSS_ID = 'createhelper-utility-dock'
|
|
22
22
|
const DOCK_SNAPSHOT = 'createhelper.utility-dock/1+placement'
|
|
23
23
|
const DOCK_LEFT_FALLBACK_PX = 80
|
|
@@ -26,95 +26,106 @@ const warnDockGeometry = (left) => {
|
|
|
26
26
|
if (typeof console === 'undefined' || typeof console.warn !== 'function') return
|
|
27
27
|
console.warn('[dsh-mini-utility-dock] shell geometry unavailable; falling back to left=' + left + 'px')
|
|
28
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
+
/**
|
|
106
|
+
* A missing, blank, or non-string `label` would reach `render()` as `undefined`
|
|
107
|
+
* and produce aria-label="undefined". `id` is always present (register() rejects
|
|
108
|
+
* an empty one), so it is the safe, meaningful accessible name. Normalize once
|
|
109
|
+
* on store — not at every render — so the stored item is always valid.
|
|
110
|
+
*/
|
|
111
|
+
const normalizeDockLabel = (item) => {
|
|
112
|
+
const label = typeof item.label === 'string' ? item.label.trim() : ''
|
|
113
|
+
return label || item.id
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function getUtilityDock() {
|
|
117
|
+
if (isCompatibleDock(window[DOCK_KEY])) return window[DOCK_KEY]
|
|
118
|
+
ensureUtilityDockStyles()
|
|
119
|
+
const items = new Map()
|
|
120
|
+
let root = null
|
|
121
|
+
let resizeObserver = null
|
|
122
|
+
let mutationObserver = null
|
|
123
|
+
const readPlacement = () => {
|
|
124
|
+
try {
|
|
125
|
+
const value = localStorage.getItem(DOCK_PLACEMENT_KEY)
|
|
126
|
+
if (value === 'main-bottom-right' || value === 'hidden') return value
|
|
127
|
+
} catch (e) { }
|
|
128
|
+
return 'main-bottom-left'
|
|
118
129
|
}
|
|
119
130
|
let placement = readPlacement()
|
|
120
131
|
const findShellFrame = () => {
|
|
@@ -138,112 +149,115 @@ function getUtilityDock() {
|
|
|
138
149
|
return Math.max(16, Math.round(sidebarRect.right + 16))
|
|
139
150
|
}
|
|
140
151
|
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
|
-
}
|
|
152
|
+
if (!root) return
|
|
153
|
+
root.hidden = placement === 'hidden'
|
|
154
|
+
root.dataset.placement = placement
|
|
155
|
+
document.documentElement.dataset.createhelperUtilityDockPlacement = placement
|
|
156
|
+
root.style.right = ''
|
|
157
|
+
root.style.left = ''
|
|
158
|
+
if (placement === 'main-bottom-right') {
|
|
159
|
+
root.style.right = '16px'
|
|
160
|
+
return
|
|
161
|
+
}
|
|
151
162
|
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
|
+
root.style.left = left + 'px'
|
|
164
|
+
document.documentElement.style.setProperty('--createhelper-utility-dock-left', left + 'px')
|
|
165
|
+
}
|
|
166
|
+
const render = () => {
|
|
167
|
+
if (!root) {
|
|
168
|
+
root = document.createElement('nav')
|
|
169
|
+
root.className = 'createhelper-utility-dock'
|
|
170
|
+
root.setAttribute('aria-label', 'DSH utilities')
|
|
171
|
+
document.body.appendChild(root)
|
|
172
|
+
window.addEventListener('resize', updateGeometry)
|
|
173
|
+
const observeLayout = () => {
|
|
163
174
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
175
|
+
if (!frame) return false
|
|
176
|
+
mutationObserver?.disconnect()
|
|
177
|
+
mutationObserver = null
|
|
178
|
+
if (typeof ResizeObserver === 'function' && !resizeObserver) {
|
|
179
|
+
resizeObserver = new ResizeObserver(updateGeometry)
|
|
180
|
+
resizeObserver.observe(frame)
|
|
181
|
+
if (frame.firstElementChild) resizeObserver.observe(frame.firstElementChild)
|
|
182
|
+
}
|
|
183
|
+
updateGeometry()
|
|
184
|
+
return true
|
|
185
|
+
}
|
|
186
|
+
if (!observeLayout() && typeof MutationObserver === 'function') {
|
|
187
|
+
mutationObserver = new MutationObserver(() => { observeLayout() })
|
|
188
|
+
mutationObserver.observe(document.body, { childList: true, subtree: true })
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
root.replaceChildren()
|
|
192
|
+
Array.from(items.values()).sort((a, b) => a.order - b.order || a.id.localeCompare(b.id)).forEach((item) => {
|
|
193
|
+
const button = document.createElement('button')
|
|
194
|
+
button.type = 'button'
|
|
195
|
+
button.className = 'createhelper-utility-dock-item'
|
|
196
|
+
button.dataset.createhelperDockItem = item.id
|
|
197
|
+
button.title = item.label
|
|
198
|
+
button.setAttribute('aria-label', item.label)
|
|
199
|
+
button.setAttribute('aria-pressed', item.active ? 'true' : 'false')
|
|
200
|
+
// Sanitized here, not in register(), so `update({ icon })` cannot be a
|
|
201
|
+
// second way past the gate.
|
|
202
|
+
if (safeDockIcon(item.icon)) button.innerHTML = item.icon
|
|
203
|
+
else button.textContent = dockIconFallback(item)
|
|
204
|
+
button.addEventListener('click', () => {
|
|
205
|
+
if (!item.active) {
|
|
206
|
+
for (const other of items.values()) {
|
|
207
|
+
if (other.id !== item.id && other.active && typeof other.onDeactivate === 'function') {
|
|
208
|
+
other.onDeactivate()
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
item.onActivate()
|
|
213
|
+
})
|
|
214
|
+
root.appendChild(button)
|
|
215
|
+
})
|
|
216
|
+
updateGeometry()
|
|
217
|
+
}
|
|
218
|
+
const api = {
|
|
219
|
+
protocol: DOCK_PROTOCOL,
|
|
220
|
+
version: DOCK_VERSION,
|
|
221
|
+
snapshot: DOCK_SNAPSHOT,
|
|
222
|
+
register(item) {
|
|
223
|
+
if (!item || typeof item.id !== 'string' || !item.id || typeof item.onActivate !== 'function') {
|
|
224
|
+
throw new TypeError('utility dock item requires a non-empty id and onActivate()')
|
|
225
|
+
}
|
|
226
|
+
const registration = Object.freeze({})
|
|
227
|
+
items.set(item.id, { ...item, registration, label: normalizeDockLabel(item), order: Number(item.order) || 0, active: !!item.active })
|
|
228
|
+
render()
|
|
229
|
+
return {
|
|
230
|
+
update(patch) {
|
|
231
|
+
const current = items.get(item.id)
|
|
232
|
+
if (!current || current.registration !== registration) return
|
|
233
|
+
const next = { ...current, ...patch }
|
|
234
|
+
// Keep the stored label valid even when `update({ label })` is passed.
|
|
235
|
+
next.label = normalizeDockLabel(next)
|
|
236
|
+
items.set(item.id, next)
|
|
237
|
+
render()
|
|
238
|
+
},
|
|
239
|
+
dispose() {
|
|
240
|
+
const current = items.get(item.id)
|
|
241
|
+
if (!current || current.registration !== registration) return
|
|
242
|
+
items.delete(item.id)
|
|
243
|
+
if (items.size) { render(); return }
|
|
244
|
+
resizeObserver?.disconnect()
|
|
245
|
+
resizeObserver = null
|
|
246
|
+
mutationObserver?.disconnect()
|
|
247
|
+
mutationObserver = null
|
|
248
|
+
window.removeEventListener('resize', updateGeometry)
|
|
249
|
+
root?.remove()
|
|
250
|
+
root = null
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
},
|
|
254
|
+
setPlacement(next) {
|
|
255
|
+
placement = next === 'main-bottom-right' || next === 'hidden' ? next : 'main-bottom-left'
|
|
256
|
+
try { localStorage.setItem(DOCK_PLACEMENT_KEY, placement) } catch (e) { }
|
|
257
|
+
updateGeometry()
|
|
258
|
+
},
|
|
259
|
+
getPlacement() { return placement }
|
|
260
|
+
}
|
|
261
|
+
window[DOCK_KEY] = api
|
|
262
|
+
return api
|
|
263
|
+
}
|
package/package.json
CHANGED
|
@@ -1,20 +1,36 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mini-utility-dock",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Shared classic-script utility dock protocol for DSH plugins.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"dsh-mini-utility-dock": "bin/dsh-mini-utility-dock.js"
|
|
8
8
|
},
|
|
9
|
-
"files": [
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"README.en.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md",
|
|
16
|
+
"CHANGELOG.en.md"
|
|
17
|
+
],
|
|
10
18
|
"scripts": {
|
|
19
|
+
"dock:embed": "node bin/dsh-mini-utility-dock.js",
|
|
11
20
|
"test": "node --test test/*.test.js"
|
|
12
21
|
},
|
|
13
|
-
"engines": {
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20"
|
|
24
|
+
},
|
|
14
25
|
"repository": {
|
|
15
26
|
"type": "git",
|
|
16
27
|
"url": "git+https://github.com/xswt442-cmd/dsh-mini-utility-dock.git"
|
|
17
28
|
},
|
|
18
|
-
"keywords": [
|
|
29
|
+
"keywords": [
|
|
30
|
+
"dsh",
|
|
31
|
+
"deepseek-harness",
|
|
32
|
+
"dsh-plugin",
|
|
33
|
+
"utility-dock"
|
|
34
|
+
],
|
|
19
35
|
"license": "MIT"
|
|
20
36
|
}
|