dsh-zen-remote 2.2.7 → 2.2.9
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.md +1 -1
- package/README.zh-CN.md +1 -1
- package/docs/interface.md +6 -4
- package/docs/remote-access.md +1 -0
- package/lib/client-routes.js +52 -0
- package/lib/client-routes.js.map +1 -1
- package/lib/client.js +506 -280
- package/lib/relay-client.js +71 -0
- package/lib/relay-client.js.map +1 -1
- package/lib/types/client/MobileAttachButton.d.ts +15 -7
- package/lib/types/client/MobileAttachButton.d.ts.map +1 -1
- package/lib/types/client/MobileAttachChips.d.ts.map +1 -1
- package/lib/types/client/effects/model-sheet-extras.d.ts +29 -25
- package/lib/types/client/effects/model-sheet-extras.d.ts.map +1 -1
- package/lib/types/client/styles/composer.css.d.ts +1 -1
- package/lib/types/client/styles/composer.css.d.ts.map +1 -1
- package/lib/types/client-data/remote-status.d.ts +39 -2
- package/lib/types/client-data/remote-status.d.ts.map +1 -1
- package/lib/types/client-routes.d.ts +19 -0
- package/lib/types/client-routes.d.ts.map +1 -1
- package/lib/types/relay-client.d.ts +10 -0
- package/lib/types/relay-client.d.ts.map +1 -1
- package/package.json +1 -1
- package/scripts/check-attach-upload.mjs +42 -0
- package/src/client/MobileAttachButton.tsx +23 -12
- package/src/client/MobileAttachChips.tsx +0 -7
- package/src/client/effects/model-sheet-extras.ts +233 -111
- package/src/client/styles/composer.css.ts +142 -83
- package/src/client-data/remote-status.ts +128 -2
- package/src/client-routes.ts +53 -0
- package/src/relay-client.ts +76 -0
- package/lib/types/client/host-attach.d.ts +0 -38
- package/lib/types/client/host-attach.d.ts.map +0 -1
- package/src/client/host-attach.ts +0 -54
|
@@ -3,163 +3,285 @@ import type { ClientContext } from '../compat/types.ts'
|
|
|
3
3
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
4
4
|
const PHONE_QUERY = '(max-width: 767px)'
|
|
5
5
|
|
|
6
|
-
/** The
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
/** The model pill's own popup, which section 4 of composer.css.ts turns into
|
|
10
|
-
* a bottom sheet — inline under the pill on ≤ 0.1.2, portaled to body on
|
|
11
|
-
* 0.1.5+ (same two spellings as composer.css.ts's MODEL_MENU; the body form
|
|
12
|
-
* is the only body-level `role=menu` with an `-menu` id, measured on
|
|
13
|
-
* 0.1.5-rc.2). Appending into the portal root is no different from the
|
|
14
|
-
* inline case: React leaves nodes it did not create alone, and the sheet
|
|
15
|
-
* still unmounts on close, which is what the detached-menu path handles. */
|
|
16
|
-
const MENU_SELECTOR = '[data-slot="conversation.input.model"] [class$="_menu"], body > [id$="-menu"][role="menu"]'
|
|
6
|
+
/** The composer bar slot: the scan root, and the anchor the synthetic dock
|
|
7
|
+
* rail hangs beside (hero stage). */
|
|
8
|
+
const BAR_SELECTOR = '[data-slot="conversation.composer.bar"]'
|
|
17
9
|
|
|
18
10
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
11
|
+
* Where a secondary control can legally sit in the composer, as ONE selector
|
|
12
|
+
* list scanned inside the bar. Slots first (input.right / input.left), then
|
|
13
|
+
* the official flex groups, then the row itself — a third party may inject
|
|
14
|
+
* at any of the six (subscriptions and vision-router register into
|
|
15
|
+
* input.right; dsh-workbuddy's credits and dsh-skills-ui's chips hang off
|
|
16
|
+
* _row directly). Every position excludes `[data-mobile-nav]` so this
|
|
17
|
+
* plugin's own nodes (the strip itself, and anything else we mount) are
|
|
18
|
+
* never collected, and the official controls by name, so the whitelist
|
|
19
|
+
* never fights the host. opt-in by marker, never `:not()`-hide unclaimed
|
|
20
|
+
* nodes — the failure direction is "extra control stays visible", never
|
|
21
|
+
* "control hidden from both places".
|
|
22
22
|
*/
|
|
23
|
-
const EXTRAS_SELECTOR =
|
|
23
|
+
const EXTRAS_SELECTOR = [
|
|
24
|
+
'[data-slot="conversation.input.right"] > *:not([data-mobile-nav])',
|
|
25
|
+
'[data-slot="conversation.input.left"] > *:not([data-mobile-nav])',
|
|
26
|
+
'[class$="_tools"] > *:not(button[class$="_add"]):not(input[type="file"]):not([class$="_modes"]):not([data-slot="conversation.input.left"]):not([data-mobile-nav])',
|
|
27
|
+
'[class$="_standardControls"] > *:not([data-slot="conversation.input.model"]):not([data-slot="conversation.input.right"]):not([data-mobile-nav])',
|
|
28
|
+
'[class$="_trailing"] > *:not([class$="_standardControls"]):not([class$="_activity"]):not([class$="_primary"]):not([data-mobile-nav])',
|
|
29
|
+
'[class$="_row"] > *:not([class$="_tools"]):not([class$="_trailing"]):not([data-mobile-nav])',
|
|
30
|
+
].join(', ')
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
|
-
* dsh-vision-router's own mount marker. Used
|
|
27
|
-
*
|
|
33
|
+
* dsh-vision-router's own mount marker. Used only to answer "is the single
|
|
34
|
+
* collected control the vision toggle" — see `worthMoving`.
|
|
28
35
|
*/
|
|
29
36
|
const VISION_SELECTOR = '[data-vision-router-mode-toggle]'
|
|
30
37
|
|
|
31
38
|
/**
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
39
|
+
* On the input.right CONTAINER while the effect manages any of its children,
|
|
40
|
+
* and on the strip itself. It is the styling hook for both halves in
|
|
41
|
+
* composer.css.ts (the emptied container hides without leaving a flex gap;
|
|
42
|
+
* an inline `[role="menu"]` opened by a parked chip escapes the strip's
|
|
43
|
+
* overflow clip through it). It is deliberately what the row's
|
|
44
|
+
* `display: none` keys off: without the marker the controls stay exactly
|
|
45
|
+
* where the host put them, so a version of this file that declines to act —
|
|
46
|
+
* or never runs at all — cannot leave them hidden in the row and absent from
|
|
47
|
+
* the strip, i.e. unreachable.
|
|
38
48
|
*/
|
|
39
49
|
const MANAGED = 'data-zen-sheet-extras'
|
|
40
50
|
|
|
41
51
|
/**
|
|
42
|
-
*
|
|
52
|
+
* On every collected NODE while it is parked in the strip. Purely opt-in:
|
|
53
|
+
* the marker's absence never hides anything, and `releaseAll` removes it —
|
|
54
|
+
* an unmarked control is the host's own business again.
|
|
55
|
+
*/
|
|
56
|
+
const EXTRA = 'data-zen-composer-extra'
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Is there anything collected that actually costs the row width?
|
|
43
60
|
*
|
|
44
61
|
* The row is one nowrap flex line whose only elastic item is the model pill
|
|
45
62
|
* (composer.css.ts section 1), so entries here are paid for out of the model
|
|
46
|
-
* name. But that is only a problem once something WIDE lands
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* accommodates fine, and moving it on its own would cost
|
|
50
|
-
*
|
|
63
|
+
* name. But that is only a problem once something WIDE lands there — today
|
|
64
|
+
* dsh-plugin-subscriptions' speed chip, which renders "速度 · 标准" and eats
|
|
65
|
+
* ~70px. dsh-vision-router's toggle alone is a 28px icon that the row
|
|
66
|
+
* accommodates fine, and moving it on its own would cost nothing visually
|
|
67
|
+
* but would hide a one-tap control behind a scroll (user review, 2026-09-06).
|
|
51
68
|
*
|
|
52
|
-
* So:
|
|
53
|
-
* toggle. With no such entry — subscriptions not
|
|
54
|
-
* not contributing on this model — nothing moves
|
|
55
|
-
*
|
|
69
|
+
* So: move only when the collection holds an entry that is not (or does not
|
|
70
|
+
* contain) the vision toggle. With no such entry — subscriptions not
|
|
71
|
+
* installed, or installed but not contributing on this model — nothing moves
|
|
72
|
+
* and the row keeps its one icon. Why width rather than sniffing for
|
|
73
|
+
* subscriptions by name: the slot system adds no per-registration marker, so
|
|
74
|
+
* naming the plugin would anchor on a marker we'd have to add to our own
|
|
75
|
+
* fork of it, and the feature would die the day the fork is dropped. The
|
|
76
|
+
* width question is the real question anyway, and this asks it directly.
|
|
56
77
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* marker dies the day the fork is dropped, and the feature would go quiet
|
|
62
|
-
* with nothing to catch it. The width question is the real question anyway,
|
|
63
|
-
* and this asks it directly.
|
|
78
|
+
* Unlike the pre-#30 design, NOTHING here depends on the official model
|
|
79
|
+
* pill existing: replaced model slots (…codex-ui) and subagent sessions —
|
|
80
|
+
* which have no pill at all — collect and relocate exactly like everyone
|
|
81
|
+
* else.
|
|
64
82
|
*/
|
|
65
|
-
function worthMoving(
|
|
66
|
-
|
|
83
|
+
function worthMoving(controls: readonly Element[]): boolean {
|
|
84
|
+
if (controls.length === 0) return false
|
|
85
|
+
if (controls.length === 1) {
|
|
86
|
+
const child = controls[0]
|
|
87
|
+
if (child === undefined) return false
|
|
88
|
+
// "is or contains" — some plugins wrap their toggle in a positioning node.
|
|
89
|
+
if (child.matches(VISION_SELECTOR) || child.querySelector(VISION_SELECTOR) !== null) return false
|
|
90
|
+
}
|
|
91
|
+
return true
|
|
67
92
|
}
|
|
68
93
|
|
|
69
94
|
/**
|
|
70
|
-
* Phone:
|
|
95
|
+
* Phone: move the composer row's secondary third-party controls into a
|
|
96
|
+
* 26px horizontally-scrollable strip inside the dock rail above the card —
|
|
97
|
+
* always visible, one tap from anywhere, no sheet in between.
|
|
71
98
|
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
99
|
+
* Why move the real controls instead of drawing our own rows that drive
|
|
100
|
+
* them: the same reason native-trigger-overlay.ts moves the real trigger. A
|
|
101
|
+
* stand-in has to script the original, which is a road this plugin does not
|
|
102
|
+
* go down — beyond the trusted-input wall that effect documents, our own
|
|
103
|
+
* rows would have to mirror every piece of state (speed tier, disabled,
|
|
104
|
+
* aria-pressed, whether a vision twin exists for the current model) and
|
|
105
|
+
* would rot on the next upstream restyle. Moving the node keeps one source
|
|
106
|
+
* of truth: their React roots go on owning and updating these elements, we
|
|
107
|
+
* only change where they hang — never cloneNode, and the move is anchored
|
|
108
|
+
* with a Comment at the original spot so `unparkAll` can put each node back
|
|
109
|
+
* exactly where React left it.
|
|
76
110
|
*
|
|
77
|
-
*
|
|
78
|
-
* the
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
* a vision twin exists for the current model) and would rot on the next
|
|
83
|
-
* upstream restyle. Moving the node keeps one source of truth: their React
|
|
84
|
-
* roots go on owning and updating these elements, we only change where they
|
|
85
|
-
* hang.
|
|
111
|
+
* Measured 2026-09-06 (DSH 0.1.2, 390px) before the original version of
|
|
112
|
+
* this effect — the two things that would have killed the approach both
|
|
113
|
+
* hold: a relocated control survives host re-renders (typing into the
|
|
114
|
+
* composer re-renders the row and does not yank it back) and stays
|
|
115
|
+
* hit-testable at its new position.
|
|
86
116
|
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* query can no longer find it.
|
|
117
|
+
* Known hazard, inherent to relocating React-owned nodes: a control whose
|
|
118
|
+
* REGISTRATION unmounts while parked (plugin disabled, slot re-keyed) is
|
|
119
|
+
* removed by React through its ORIGINAL parent — `parent.removeChild(child)`
|
|
120
|
+
* throws when the child moved, and the exception lands in React's commit.
|
|
121
|
+
* Updates that keep the registration mounted (state-driven re-renders,
|
|
122
|
+
* text/attribute changes, a control's own inner nodes unmounting) are safe:
|
|
123
|
+
* those removals run inside the parked subtree, where every parent-child
|
|
124
|
+
* link React recorded still holds. Parking is also fully reversible — any
|
|
125
|
+
* decline path (desktop, nothing worth moving, teardown) hands every node
|
|
126
|
+
* back before anything is removed.
|
|
98
127
|
*/
|
|
99
128
|
export function installModelSheetExtras(ctx: ClientContext): void {
|
|
100
129
|
ctx.effect(() => {
|
|
101
130
|
const phone = window.matchMedia(PHONE_QUERY)
|
|
102
|
-
/**
|
|
103
|
-
let parked: { node: Element,
|
|
131
|
+
/** Parked nodes with the Comment anchoring each one's original spot. */
|
|
132
|
+
let parked: Array<{ node: Element, placeholder: Comment }> = []
|
|
133
|
+
let syntheticDock: HTMLElement | null = null
|
|
134
|
+
|
|
135
|
+
/** Collected controls, document order; parked ones appended so the set
|
|
136
|
+
* stays stable while they live in the strip. */
|
|
137
|
+
const collect = (bar: Element): Element[] => {
|
|
138
|
+
const controls = Array.from(bar.querySelectorAll(EXTRAS_SELECTOR))
|
|
139
|
+
for (const { node } of parked) {
|
|
140
|
+
if (node.isConnected && !controls.includes(node)) controls.push(node)
|
|
141
|
+
}
|
|
142
|
+
return controls
|
|
143
|
+
}
|
|
104
144
|
|
|
105
|
-
const park = (node: Element,
|
|
106
|
-
if (
|
|
107
|
-
|
|
108
|
-
|
|
145
|
+
const park = (node: Element, strip: HTMLElement): void => {
|
|
146
|
+
if (node.parentElement === null) return
|
|
147
|
+
const placeholder = document.createComment('zen-composer-extra')
|
|
148
|
+
node.parentElement.insertBefore(placeholder, node)
|
|
149
|
+
node.setAttribute(EXTRA, 'open')
|
|
150
|
+
strip.appendChild(node)
|
|
151
|
+
parked.push({ node, placeholder })
|
|
109
152
|
}
|
|
110
153
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
154
|
+
const unparkAll = (): void => {
|
|
155
|
+
for (const { node, placeholder } of parked) {
|
|
156
|
+
// Re-home only while the anchor still marks the spot AND that home
|
|
157
|
+
// is on the page — when the whole composer went away, the owning
|
|
158
|
+
// React roots rebuild the slot on the next mount.
|
|
159
|
+
if (placeholder.parentNode !== null && placeholder.parentNode.isConnected) {
|
|
160
|
+
placeholder.parentNode.insertBefore(node, placeholder)
|
|
161
|
+
}
|
|
162
|
+
placeholder.remove()
|
|
163
|
+
node.removeAttribute(EXTRA)
|
|
164
|
+
}
|
|
165
|
+
parked = []
|
|
120
166
|
}
|
|
121
167
|
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
168
|
+
/** Everything the effect owns, back to host-placed defaults: parked
|
|
169
|
+
* nodes re-homed, strip, synthetic rail and every marker gone.
|
|
170
|
+
* Idempotent — teardown and every decline path go through here. */
|
|
171
|
+
const releaseAll = (): void => {
|
|
172
|
+
unparkAll()
|
|
173
|
+
document.querySelector('[data-mobile-nav="composer-chips"]')?.remove()
|
|
174
|
+
syntheticDock?.remove()
|
|
175
|
+
syntheticDock = null
|
|
176
|
+
const bar = document.querySelector(BAR_SELECTOR)
|
|
177
|
+
const scope: ParentNode = bar ?? document
|
|
178
|
+
for (const node of scope.querySelectorAll(`[${EXTRA}]`)) node.removeAttribute(EXTRA)
|
|
179
|
+
bar?.querySelector('[data-slot="conversation.input.right"]')?.removeAttribute(MANAGED)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** The rail the strip lives in — the host's own dock, or a synthetic
|
|
183
|
+
* stand-in while the hero composer has rendered none yet. */
|
|
184
|
+
const adoptDock = (bar: Element): Element => {
|
|
185
|
+
// :not([data-zen-synthetic-dock]) is load-bearing: the synthetic rail
|
|
186
|
+
// is bar's SIBLING carrying the same data-slot, so an unqualified
|
|
187
|
+
// query would find it as the "real" dock, delete it as stale, recreate
|
|
188
|
+
// it after — the observer would oscillate forever.
|
|
189
|
+
const dock = bar.parentElement?.querySelector('[data-slot="conversation.input.dock"]:not([data-zen-synthetic-dock])') ?? null
|
|
190
|
+
if (dock !== null) {
|
|
191
|
+
if (syntheticDock !== null) {
|
|
192
|
+
// Real dock appeared: hand every parked node back before the
|
|
193
|
+
// synthetic rail (and the strip inside it) is destroyed — removing
|
|
194
|
+
// a rail full of React-owned controls would detach them from the
|
|
195
|
+
// page. sync re-parks them into the new strip right after.
|
|
196
|
+
unparkAll()
|
|
197
|
+
syntheticDock.remove()
|
|
198
|
+
syntheticDock = null
|
|
199
|
+
}
|
|
200
|
+
return dock
|
|
201
|
+
}
|
|
202
|
+
if (syntheticDock === null || !syntheticDock.isConnected) {
|
|
203
|
+
syntheticDock = document.createElement('div')
|
|
204
|
+
syntheticDock.setAttribute('data-slot', 'conversation.input.dock')
|
|
205
|
+
syntheticDock.setAttribute('data-zen-synthetic-dock', '')
|
|
206
|
+
bar.parentNode?.insertBefore(syntheticDock, bar)
|
|
207
|
+
}
|
|
208
|
+
return syntheticDock
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const ensureStrip = (home: Element): HTMLElement => {
|
|
212
|
+
let strip = home.querySelector<HTMLElement>('[data-mobile-nav="composer-chips"]')
|
|
213
|
+
if (strip === null) {
|
|
214
|
+
strip = document.createElement('div')
|
|
215
|
+
strip.setAttribute('data-mobile-nav', 'composer-chips')
|
|
216
|
+
strip.setAttribute(MANAGED, '')
|
|
217
|
+
// Document-order first; CSS `order: 0` keeps it the rail's leftmost
|
|
218
|
+
// pill line among the order-0 ties.
|
|
219
|
+
home.prepend(strip)
|
|
220
|
+
}
|
|
221
|
+
return strip
|
|
126
222
|
}
|
|
127
223
|
|
|
128
224
|
const sync = (): void => {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
//
|
|
132
|
-
//
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
|
|
225
|
+
const bar = document.querySelector(BAR_SELECTOR)
|
|
226
|
+
if (bar === null) { releaseAll(); return }
|
|
227
|
+
// A parked node its own React root unmounted is dead weight; drop it
|
|
228
|
+
// and its anchor before collecting, so it neither blocks nor hides.
|
|
229
|
+
parked = parked.filter(({ node, placeholder }) => {
|
|
230
|
+
if (node.isConnected) return true
|
|
231
|
+
placeholder.remove()
|
|
232
|
+
return false
|
|
233
|
+
})
|
|
234
|
+
const controls = collect(bar)
|
|
235
|
+
if (!phone.matches || !worthMoving(controls)) { releaseAll(); return }
|
|
236
|
+
|
|
237
|
+
const home = adoptDock(bar)
|
|
238
|
+
const strip = ensureStrip(home)
|
|
239
|
+
|
|
240
|
+
// Parked entries the set has outgrown (an uninstalled plugin, a
|
|
241
|
+
// re-render that moved a control into a claimed position) go home and
|
|
242
|
+
// lose their marker.
|
|
243
|
+
parked = parked.filter(({ node, placeholder }) => {
|
|
244
|
+
if (controls.includes(node) && node.isConnected) return true
|
|
245
|
+
if (node.isConnected && placeholder.parentNode !== null && placeholder.parentNode.isConnected) {
|
|
246
|
+
placeholder.parentNode.insertBefore(node, placeholder)
|
|
247
|
+
}
|
|
248
|
+
placeholder.remove()
|
|
249
|
+
node.removeAttribute(EXTRA)
|
|
250
|
+
return false
|
|
251
|
+
})
|
|
252
|
+
for (const node of controls) {
|
|
253
|
+
if (!parked.some((entry) => entry.node === node)) park(node, strip)
|
|
254
|
+
}
|
|
255
|
+
// Stale markers anywhere in the bar — parked ones are in `controls`,
|
|
256
|
+
// so they survive this sweep.
|
|
257
|
+
for (const node of bar.querySelectorAll(`[${EXTRA}]`)) {
|
|
258
|
+
if (!controls.includes(node)) node.removeAttribute(EXTRA)
|
|
259
|
+
}
|
|
260
|
+
const right = bar.querySelector('[data-slot="conversation.input.right"]')
|
|
261
|
+
if (right !== null) {
|
|
262
|
+
const rightManaged = right.children.length > 0 ||
|
|
263
|
+
parked.some(({ placeholder }) => placeholder.parentNode === right)
|
|
264
|
+
if (rightManaged) right.setAttribute(MANAGED, '')
|
|
265
|
+
else right.removeAttribute(MANAGED)
|
|
266
|
+
}
|
|
141
267
|
}
|
|
142
268
|
|
|
143
269
|
// Synchronous in the observer callback, never behind rAF: the callback is
|
|
144
|
-
// a microtask and runs before this frame paints, so the
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
270
|
+
// a microtask and runs before this frame paints, so the strip and the
|
|
271
|
+
// parked nodes are in place the first time their frame is drawn. A rAF
|
|
272
|
+
// would land a frame late (AGENTS.md's "晚一帧" rule). Body-level
|
|
273
|
+
// childList is the same shape modal-back.ts uses. The observer also sees
|
|
274
|
+
// this effect's own moves; sync is idempotent, so the extra pass is a
|
|
275
|
+
// no-op.
|
|
149
276
|
const observer = new MutationObserver(sync)
|
|
150
|
-
observer.observe(document.body, {
|
|
151
|
-
childList: true,
|
|
152
|
-
subtree: true,
|
|
153
|
-
attributes: true,
|
|
154
|
-
attributeFilter: ['aria-expanded'],
|
|
155
|
-
})
|
|
277
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
156
278
|
phone.addEventListener('change', sync)
|
|
157
279
|
sync()
|
|
158
280
|
|
|
159
281
|
return () => {
|
|
160
282
|
observer.disconnect()
|
|
161
283
|
phone.removeEventListener('change', sync)
|
|
162
|
-
|
|
284
|
+
releaseAll()
|
|
163
285
|
}
|
|
164
|
-
}, 'dsh-mobile-nav:
|
|
286
|
+
}, 'dsh-mobile-nav: composer chips extras')
|
|
165
287
|
}
|