dsh-remote-plugin 0.6.14 → 0.6.15

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.
@@ -0,0 +1,41 @@
1
+ /*
2
+ * Local morphicons bridge for the zero-build WebUI.
3
+ *
4
+ * The vendored element keeps a static SVG child as its first-paint fallback.
5
+ * UI code only changes data-morph-state; this bridge turns that state change
6
+ * into a morph without coupling the application code to the vendor API.
7
+ */
8
+ import { defineMorphIcon } from './vendor/morphicons/element.js'
9
+
10
+ defineMorphIcon()
11
+
12
+ function syncMorphIcon(element) {
13
+ if (!element || !('icon' in element)) return
14
+ const state = element.dataset.morphState === 'open' ? 'open' : 'closed'
15
+ const next = state === 'open' ? element.dataset.morphOpen : element.dataset.morphClosed
16
+ if (!next || element.icon === next) return
17
+ element.icon = next
18
+ }
19
+
20
+ function syncMorphIcons(root = document) {
21
+ if (root.matches?.('morph-icon[data-morph-closed][data-morph-open]')) syncMorphIcon(root)
22
+ root.querySelectorAll?.('morph-icon[data-morph-closed][data-morph-open]').forEach(syncMorphIcon)
23
+ }
24
+
25
+ function bootMorphIcons() {
26
+ syncMorphIcons()
27
+ const observer = new MutationObserver(records => {
28
+ for (const record of records) syncMorphIcon(record.target)
29
+ })
30
+ observer.observe(document.documentElement, {
31
+ subtree: true,
32
+ attributes: true,
33
+ attributeFilter: ['data-morph-state']
34
+ })
35
+ }
36
+
37
+ if (document.readyState === 'loading') {
38
+ document.addEventListener('DOMContentLoaded', bootMorphIcons, { once: true })
39
+ } else {
40
+ bootMorphIcons()
41
+ }
@@ -0,0 +1,469 @@
1
+ /* dsh-Remote motion layer: GSAP core + timeline, with reduced-motion and low-cost DOM updates. */
2
+ (function () {
3
+ 'use strict'
4
+
5
+ const gsap = window.gsap
6
+ if (!gsap) return
7
+
8
+ const reduceQuery = window.matchMedia?.('(prefers-reduced-motion: reduce)')
9
+ let reduced = !!reduceQuery?.matches
10
+ reduceQuery?.addEventListener?.('change', event => { reduced = !!event.matches })
11
+ const activePulses = new WeakSet()
12
+
13
+ function clear(targets) {
14
+ gsap.set(targets, { clearProps: 'opacity,visibility,transform,willChange' })
15
+ }
16
+
17
+ function motionKey(node) {
18
+ return node?.dataset?.motionKey || node?.dataset?.id || node?.dataset?.sessionSwipe || node?.dataset?.wbSession || node?.textContent?.slice(0, 80) || ''
19
+ }
20
+
21
+ function motionSignature(items) {
22
+ return items.map(motionKey).join('|')
23
+ }
24
+
25
+ function view(view) {
26
+ if (!view) return
27
+ const children = [...view.children].filter(child => !child.classList.contains('hidden')).slice(0, 6)
28
+ gsap.killTweensOf([view, ...children])
29
+ if (reduced) {
30
+ clear([view, ...children])
31
+ return
32
+ }
33
+ const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
34
+ tl.fromTo(view, { autoAlpha: 0, y: 8 }, { autoAlpha: 1, y: 0, duration: 0.22, clearProps: 'transform' })
35
+ .fromTo(children, { autoAlpha: 0, y: 6 }, { autoAlpha: 1, y: 0, duration: 0.16, stagger: 0.025, clearProps: 'transform' }, '<0.04')
36
+ }
37
+
38
+ function list(container, selector) {
39
+ if (!container) return
40
+ const items = [...container.querySelectorAll(selector)]
41
+ if (!items.length) {
42
+ delete container.dataset.motionListSignature
43
+ return
44
+ }
45
+ const signature = motionSignature(items)
46
+ if (container.dataset.motionListSignature === signature) return
47
+ container.dataset.motionListSignature = signature
48
+ gsap.killTweensOf(items)
49
+ if (reduced) {
50
+ clear(items)
51
+ return
52
+ }
53
+ gsap.fromTo(items, { autoAlpha: 0, y: 8 }, {
54
+ autoAlpha: 1,
55
+ y: 0,
56
+ duration: 0.2,
57
+ ease: 'power2.out',
58
+ stagger: { each: 0.025, from: 'start' },
59
+ clearProps: 'transform'
60
+ })
61
+ }
62
+
63
+ // FLIP-style reflow without the optional Flip plugin: read all positions,
64
+ // let the caller render once, then animate only transform/opacity.
65
+ function relayout(container, selector, render) {
66
+ if (!container || typeof render !== 'function') return false
67
+ const existing = [...container.querySelectorAll(selector)]
68
+ const before = new Map(existing.map(node => [motionKey(node), node.getBoundingClientRect()]))
69
+ gsap.killTweensOf(existing)
70
+ render()
71
+ const next = [...container.querySelectorAll(selector)]
72
+ if (reduced) {
73
+ clear(next)
74
+ return true
75
+ }
76
+ const entering = []
77
+ const moving = []
78
+ for (const node of next) {
79
+ const previous = before.get(motionKey(node))
80
+ if (!previous) {
81
+ entering.push(node)
82
+ continue
83
+ }
84
+ const current = node.getBoundingClientRect()
85
+ const x = previous.left - current.left
86
+ const y = previous.top - current.top
87
+ if (Math.abs(x) > 0.5 || Math.abs(y) > 0.5) {
88
+ gsap.set(node, { x, y })
89
+ moving.push(node)
90
+ }
91
+ }
92
+ if (moving.length) {
93
+ gsap.to(moving, {
94
+ x: 0,
95
+ y: 0,
96
+ duration: 0.28,
97
+ ease: 'power2.out',
98
+ stagger: { each: 0.018, from: 'start' },
99
+ clearProps: 'transform'
100
+ })
101
+ }
102
+ if (entering.length) {
103
+ gsap.fromTo(entering, { autoAlpha: 0, y: 8 }, {
104
+ autoAlpha: 1,
105
+ y: 0,
106
+ duration: 0.2,
107
+ ease: 'power2.out',
108
+ stagger: { each: 0.025, from: 'start' },
109
+ clearProps: 'transform'
110
+ })
111
+ }
112
+ return true
113
+ }
114
+
115
+ function bindLongPressReorder(container, selector, options = {}) {
116
+ if (!container || !selector) return
117
+ const registry = container.__dshReorderRegistry || (container.__dshReorderRegistry = new Map())
118
+ const existing = registry.get(selector)
119
+ if (existing) {
120
+ existing.options = options
121
+ return
122
+ }
123
+ const LONG_PRESS_MS = 300
124
+ const MOVE_TOLERANCE = 28
125
+ const state = { options, press: null, drag: null, suppressClickUntil: 0 }
126
+ const raf = callback => window.requestAnimationFrame ? window.requestAnimationFrame(callback) : setTimeout(callback, 16)
127
+ const caf = id => window.cancelAnimationFrame ? window.cancelAnimationFrame(id) : clearTimeout(id)
128
+ const itemFrom = target => target?.closest?.(selector)
129
+ const listFrom = item => item?.parentElement || container
130
+ const groupFrom = item => state.options.groupSelector ? (item.closest(state.options.groupSelector) || container) : container
131
+ const keyFrom = item => state.options.key ? state.options.key(item) : motionKey(item)
132
+ const itemsFrom = list => [...list.querySelectorAll(selector)].filter(item => item.parentElement === list)
133
+ const scrollTargetsFrom = item => {
134
+ const targets = []
135
+ let node = item?.parentElement
136
+ while (node && node !== document.body) {
137
+ const style = window.getComputedStyle(node)
138
+ if ((style.overflowY === 'auto' || style.overflowY === 'scroll') && node.scrollHeight > node.clientHeight + 1) targets.push(node)
139
+ node = node.parentElement
140
+ }
141
+ if (document.scrollingElement) targets.push(document.scrollingElement)
142
+ return targets
143
+ }
144
+ const reorderDraggedItems = (drag, clientY) => {
145
+ const items = itemsFrom(drag.list).filter(item => item !== drag.item)
146
+ if (!items.length) return
147
+ const firstRects = new Map(items.map(item => [item, item.getBoundingClientRect()]))
148
+ let target = null
149
+ let insertBeforeTarget = false
150
+ for (const item of items) {
151
+ const rect = item.getBoundingClientRect()
152
+ if (clientY < rect.top + rect.height / 2) {
153
+ target = item
154
+ insertBeforeTarget = true
155
+ break
156
+ }
157
+ target = item
158
+ }
159
+ if (!target) return
160
+ const reference = insertBeforeTarget ? target : target.nextElementSibling
161
+ if (reference !== drag.placeholder) drag.list.insertBefore(drag.placeholder, reference || null)
162
+ if (!reduced) {
163
+ for (const item of items) {
164
+ const first = firstRects.get(item)
165
+ const last = item.getBoundingClientRect()
166
+ const x = first.left - last.left
167
+ const y = first.top - last.top
168
+ if (Math.abs(x) > 0.5 || Math.abs(y) > 0.5) {
169
+ gsap.fromTo(item, { x, y }, { x: 0, y: 0, duration: 0.16, ease: 'power2.out', clearProps: 'transform' })
170
+ }
171
+ }
172
+ }
173
+ }
174
+ const autoScroll = drag => {
175
+ if (!drag || state.drag !== drag) return
176
+ const pointerY = drag.pointerY
177
+ const targets = drag.scrollTargets || []
178
+ for (const target of targets) {
179
+ const root = target === document.scrollingElement
180
+ ? { top: 0, bottom: window.innerHeight }
181
+ : target.getBoundingClientRect()
182
+ const threshold = Math.min(78, Math.max(42, (root.bottom - root.top) * 0.14))
183
+ let delta = 0
184
+ if (pointerY < root.top + threshold) {
185
+ const strength = 1 - Math.max(0, pointerY - root.top) / threshold
186
+ if (target.scrollTop > 0) delta = -Math.ceil(4 + strength * 14)
187
+ } else if (pointerY > root.bottom - threshold) {
188
+ const max = target.scrollHeight - target.clientHeight
189
+ const strength = 1 - Math.max(0, root.bottom - pointerY) / threshold
190
+ if (target.scrollTop < max) delta = Math.ceil(4 + strength * 14)
191
+ }
192
+ if (delta) {
193
+ target.scrollTop = Math.max(0, Math.min(target.scrollHeight - target.clientHeight, target.scrollTop + delta))
194
+ reorderDraggedItems(drag, pointerY)
195
+ break
196
+ }
197
+ }
198
+ drag.scrollRaf = raf(() => autoScroll(drag))
199
+ }
200
+ const restoreStyle = (item, style) => {
201
+ if (style == null) item.removeAttribute('style')
202
+ else item.setAttribute('style', style)
203
+ }
204
+ const orderFrom = drag => {
205
+ const order = []
206
+ for (const child of drag.list.children) {
207
+ if (child === drag.placeholder) order.push(keyFrom(drag.item))
208
+ else if (child !== drag.item && child.matches?.(selector)) order.push(keyFrom(child))
209
+ }
210
+ return order
211
+ }
212
+ const animateDrop = (drag, commit) => {
213
+ const floating = drag.item.getBoundingClientRect()
214
+ const order = orderFrom(drag)
215
+ drag.list.insertBefore(drag.item, drag.placeholder)
216
+ drag.placeholder.remove()
217
+ restoreStyle(drag.item, drag.originalStyle)
218
+ const finalRect = drag.item.getBoundingClientRect()
219
+ const dx = floating.left - finalRect.left
220
+ const dy = floating.top - finalRect.top
221
+ if (!reduced && (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5)) {
222
+ gsap.fromTo(drag.item, { x: dx, y: dy, scale: 1.02 }, {
223
+ x: 0,
224
+ y: 0,
225
+ scale: 1,
226
+ duration: 0.24,
227
+ ease: 'power2.out',
228
+ clearProps: 'transform'
229
+ })
230
+ } else if (reduced) clear(drag.item)
231
+ if (commit) {
232
+ const callback = drag.options.onCommit
233
+ const payload = { item: drag.item, list: drag.list, group: drag.group, order }
234
+ if (callback) setTimeout(() => callback(payload), reduced ? 0 : 240)
235
+ }
236
+ }
237
+ const restoreDrag = (drag, commit) => {
238
+ if (!commit) {
239
+ const originalNext = drag.originalNextSibling?.parentElement === drag.list ? drag.originalNextSibling : null
240
+ drag.list.insertBefore(drag.placeholder, originalNext)
241
+ }
242
+ animateDrop(drag, commit)
243
+ }
244
+ const setPageScrollLock = locked => {
245
+ document.documentElement.classList.toggle('reorder-scroll-lock', locked)
246
+ }
247
+
248
+ const cancelPress = () => {
249
+ if (!state.press) return
250
+ clearTimeout(state.press.timer)
251
+ state.press.item.classList.remove('reorder-pressing')
252
+ state.press = null
253
+ }
254
+ const finishDrag = (commit) => {
255
+ const drag = state.drag
256
+ if (!drag) return
257
+ state.drag = null
258
+ drag.item.classList.remove('reorder-dragging')
259
+ drag.item.removeAttribute('aria-grabbed')
260
+ drag.item.releasePointerCapture?.(drag.pointerId)
261
+ if (drag.scrollRaf != null) caf(drag.scrollRaf)
262
+ container.classList.remove('reorder-active')
263
+ setPageScrollLock(false)
264
+ if (commit) state.suppressClickUntil = Date.now() + 680
265
+ restoreDrag(drag, commit)
266
+ }
267
+ const activate = () => {
268
+ const press = state.press
269
+ if (!press) return
270
+ state.press = null
271
+ const rect = press.item.getBoundingClientRect()
272
+ const originalNextSibling = press.item.nextElementSibling
273
+ const placeholder = document.createElement('div')
274
+ placeholder.className = 'reorder-placeholder'
275
+ placeholder.setAttribute('aria-hidden', 'true')
276
+ placeholder.style.height = `${rect.height}px`
277
+ placeholder.style.width = `${rect.width}px`
278
+ press.item.before(placeholder)
279
+ state.drag = {
280
+ ...press,
281
+ options: state.options,
282
+ placeholder,
283
+ originalStyle: press.item.getAttribute('style'),
284
+ originalNextSibling,
285
+ startY: press.y,
286
+ pointerY: press.y,
287
+ scrollTargets: scrollTargetsFrom(press.item)
288
+ }
289
+ press.item.setPointerCapture?.(press.pointerId)
290
+ press.item.classList.remove('reorder-pressing')
291
+ press.item.classList.add('reorder-dragging')
292
+ press.item.setAttribute('aria-grabbed', 'true')
293
+ press.item.style.position = 'fixed'
294
+ press.item.style.left = `${rect.left}px`
295
+ press.item.style.top = `${rect.top}px`
296
+ press.item.style.width = `${rect.width}px`
297
+ press.item.style.zIndex = '20'
298
+ press.item.style.pointerEvents = 'none'
299
+ if (!reduced) {
300
+ gsap.set(press.item, { scale: 1.02 })
301
+ state.drag.yTo = gsap.quickTo(press.item, 'y', { duration: 0.12, ease: 'power2.out' })
302
+ }
303
+ container.classList.add('reorder-active')
304
+ setPageScrollLock(true)
305
+ const drag = state.drag
306
+ drag.scrollRaf = raf(() => autoScroll(drag))
307
+ }
308
+ const onPointerDown = event => {
309
+ if (state.press || state.drag) return
310
+ if (event.button != null && event.button !== 0) return
311
+ const item = itemFrom(event.target)
312
+ if (!item || !container.contains(item)) return
313
+ const currentOptions = state.options || {}
314
+ if (currentOptions.handleSelector && !event.target.closest(currentOptions.handleSelector)) return
315
+ if (currentOptions.excludeSelector && event.target.closest(currentOptions.excludeSelector)) return
316
+ const list = listFrom(item)
317
+ const group = groupFrom(item)
318
+ if (!itemsFrom(list).includes(item)) return
319
+ const press = { item, list, group, pointerId: event.pointerId, x: event.clientX, y: event.clientY, timer: 0 }
320
+ press.timer = setTimeout(activate, LONG_PRESS_MS)
321
+ state.press = press
322
+ item.classList.add('reorder-pressing')
323
+ }
324
+ const onPointerMove = event => {
325
+ const press = state.press
326
+ if (press && !state.drag) {
327
+ if (Math.hypot(event.clientX - press.x, event.clientY - press.y) > MOVE_TOLERANCE) cancelPress()
328
+ return
329
+ }
330
+ const drag = state.drag
331
+ if (!drag || drag.pointerId !== event.pointerId) return
332
+ event.preventDefault()
333
+ drag.pointerY = event.clientY
334
+ if (!reduced) drag.yTo?.(event.clientY - drag.startY)
335
+ reorderDraggedItems(drag, event.clientY)
336
+ }
337
+ const onPointerUp = event => {
338
+ if (state.press?.pointerId === event.pointerId) cancelPress()
339
+ if (state.drag?.pointerId === event.pointerId) finishDrag(true)
340
+ }
341
+ const onPointerCancel = event => {
342
+ if (state.press?.pointerId === event.pointerId) cancelPress()
343
+ if (state.drag?.pointerId === event.pointerId) finishDrag(false)
344
+ }
345
+ const touchPoint = event => event.changedTouches?.[0] || event.touches?.[0]
346
+ const touchPointerEvent = event => {
347
+ const point = touchPoint(event)
348
+ if (!point) return null
349
+ return {
350
+ target: event.target,
351
+ button: 0,
352
+ pointerId: 10000 + point.identifier,
353
+ clientX: point.clientX,
354
+ clientY: point.clientY,
355
+ preventDefault: () => event.preventDefault()
356
+ }
357
+ }
358
+ const onTouchStart = event => {
359
+ const normalized = touchPointerEvent(event)
360
+ if (normalized) onPointerDown(normalized)
361
+ }
362
+ const onTouchMove = event => {
363
+ const normalized = touchPointerEvent(event)
364
+ if (normalized) onPointerMove(normalized)
365
+ }
366
+ const onTouchEnd = event => {
367
+ const normalized = touchPointerEvent(event)
368
+ if (normalized) onPointerUp(normalized)
369
+ }
370
+ const onTouchCancel = event => {
371
+ const normalized = touchPointerEvent(event)
372
+ if (normalized) onPointerCancel(normalized)
373
+ }
374
+ const onDocumentMove = event => {
375
+ if (state.drag) event.preventDefault()
376
+ }
377
+ const pointerIdFromEvent = event => {
378
+ if (event.pointerId != null) return event.pointerId
379
+ const point = touchPoint(event)
380
+ return point ? 10000 + point.identifier : null
381
+ }
382
+ const onDocumentEnd = (event, commit) => {
383
+ const pointerId = pointerIdFromEvent(event)
384
+ if (state.press && (pointerId == null || state.press.pointerId === pointerId)) cancelPress()
385
+ if (state.drag && (pointerId == null || state.drag.pointerId === pointerId)) finishDrag(commit)
386
+ }
387
+ const onWindowBlur = () => onDocumentEnd({}, false)
388
+ const onVisibilityChange = () => {
389
+ if (document.hidden) onDocumentEnd({}, false)
390
+ }
391
+ const onContextMenu = event => {
392
+ if (state.press || state.drag) event.preventDefault()
393
+ }
394
+ const onClick = event => {
395
+ if (Date.now() >= state.suppressClickUntil) return
396
+ if (itemFrom(event.target)) {
397
+ event.preventDefault()
398
+ event.stopPropagation()
399
+ }
400
+ }
401
+ container.addEventListener('pointerdown', onPointerDown)
402
+ container.addEventListener('pointermove', onPointerMove)
403
+ container.addEventListener('pointerup', onPointerUp)
404
+ container.addEventListener('pointercancel', onPointerCancel)
405
+ container.addEventListener('lostpointercapture', onPointerCancel)
406
+ container.addEventListener('touchstart', onTouchStart, { passive: false })
407
+ container.addEventListener('touchmove', onTouchMove, { passive: false })
408
+ container.addEventListener('touchend', onTouchEnd, { passive: false })
409
+ container.addEventListener('touchcancel', onTouchCancel, { passive: false })
410
+ container.addEventListener('contextmenu', onContextMenu)
411
+ container.addEventListener('click', onClick, true)
412
+ document.addEventListener('touchmove', onDocumentMove, { passive: false, capture: true })
413
+ document.addEventListener('pointermove', onDocumentMove, { passive: false, capture: true })
414
+ document.addEventListener('pointerup', event => onDocumentEnd(event, true), { passive: false, capture: true })
415
+ document.addEventListener('pointercancel', event => onDocumentEnd(event, false), { passive: false, capture: true })
416
+ document.addEventListener('touchend', event => onDocumentEnd(event, true), { passive: false, capture: true })
417
+ document.addEventListener('touchcancel', event => onDocumentEnd(event, false), { passive: false, capture: true })
418
+ window.addEventListener('blur', onWindowBlur)
419
+ document.addEventListener('visibilitychange', onVisibilityChange)
420
+ registry.set(selector, state)
421
+ }
422
+
423
+ function overlay(node) {
424
+ if (!node) return
425
+ const card = node.querySelector('.modal-card, .ds-modal-card, .sheet, .ds-drawer') || node
426
+ gsap.killTweensOf([node, card])
427
+ if (reduced) {
428
+ clear([node, card])
429
+ return
430
+ }
431
+ const isSideDrawer = card.classList.contains('ds-drawer')
432
+ const isSheet = card.classList.contains('sheet')
433
+ const tl = gsap.timeline({ defaults: { ease: 'power3.out' } })
434
+ tl.fromTo(node, { autoAlpha: 0 }, { autoAlpha: 1, duration: 0.16 })
435
+ .fromTo(card,
436
+ { autoAlpha: 0, x: isSideDrawer ? 28 : 0, y: isSheet ? 24 : 8, scale: isSideDrawer || isSheet ? 1 : 0.985 },
437
+ { autoAlpha: 1, x: 0, y: 0, scale: 1, duration: isSheet ? 0.24 : 0.2, clearProps: 'transform' },
438
+ '<'
439
+ )
440
+ }
441
+
442
+ function pulse(targets) {
443
+ if (reduced) return clear(targets)
444
+ const nodes = typeof targets === 'string' ? document.querySelectorAll(targets) : targets
445
+ for (const node of nodes || []) {
446
+ if (activePulses.has(node)) continue
447
+ activePulses.add(node)
448
+ gsap.to(node, { scale: 1.12, autoAlpha: 0.62, duration: 0.85, ease: 'sine.inOut', repeat: -1, yoyo: true, transformOrigin: '50% 50%' })
449
+ }
450
+ }
451
+
452
+ window.DshMotion = { gsap, reduced: () => reduced, view, list, relayout, bindLongPressReorder, overlay, pulse }
453
+
454
+ document.addEventListener('DOMContentLoaded', () => {
455
+ document.querySelectorAll('.view:not(.hidden), .ds-view:not(.hidden)').forEach(node => view(node))
456
+ }, { once: true })
457
+
458
+ const observer = new MutationObserver(records => {
459
+ for (const record of records) {
460
+ const node = record.target
461
+ if (!(node instanceof HTMLElement)) continue
462
+ const becameVisible = record.oldValue?.split(/\s+/).includes('hidden') && !node.classList.contains('hidden')
463
+ if (!becameVisible) continue
464
+ if (node.matches('.view, .ds-view')) view(node)
465
+ else if (node.matches('.modal, .ds-modal, .sheet, .ds-drawer')) overlay(node)
466
+ }
467
+ })
468
+ observer.observe(document.documentElement, { subtree: true, attributes: true, attributeFilter: ['class'], attributeOldValue: true })
469
+ })()
@@ -116,7 +116,7 @@
116
116
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="8" y="8" width="11" height="11" rx="2"/><path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"/></svg>复制令牌
117
117
  </button>
118
118
  <button id="plugin-toggle" class="plugin-action" type="button">
119
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v9M7.8 5.4a7 7 0 1 0 8.4 0"/></svg><span id="plugin-toggle-label">启动网关</span>
119
+ <morph-icon id="plugin-toggle-icon" data-morph-state="closed" data-morph-closed="M12 3v9M7.8 5.4a7 7 0 1 0 8.4 0" data-morph-open="M7 7h10v10H7z" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v9M7.8 5.4a7 7 0 1 0 8.4 0"/></svg></morph-icon><span id="plugin-toggle-label">启动网关</span>
120
120
  </button>
121
121
  </section>
122
122
 
@@ -146,6 +146,9 @@
146
146
 
147
147
  <footer class="plugin-foot"><span>状态面板 · 需要深入管理时进入控制台</span><a id="plugin-about" href="/remote/admin/" target="_blank" rel="noopener">关于与支持</a></footer>
148
148
  </main>
149
+ <script src="vendor/gsap/gsap.min.js"></script>
150
+ <script src="motion.js"></script>
151
+ <script type="module" src="morphicons-init.js"></script>
149
152
  <script src="plugin.js"></script>
150
153
  </body>
151
154
  </html>
package/public/plugin.js CHANGED
@@ -99,6 +99,7 @@ function render(st) {
99
99
  primary.textContent = healthy ? '打开控制台' : installed ? '启动网关' : '查看控制台'
100
100
  primary.dataset.action = healthy ? 'console' : installed ? 'start' : 'console'
101
101
  text('plugin-toggle-label', gateway ? '停止网关' : '启动网关')
102
+ $('plugin-toggle-icon')?.setAttribute('data-morph-state', gateway ? 'open' : 'closed')
102
103
  $('plugin-toggle').classList.toggle('hidden', !installed)
103
104
 
104
105
  text('plugin-version', st.version ? 'v' + st.version : '—')
package/public/styles.css CHANGED
@@ -112,8 +112,6 @@ a.mini-btn { text-decoration: none; display: inline-flex; align-items: center; }
112
112
 
113
113
  /* ---------- 主体 ---------- */
114
114
  .main { padding: 12px 12px 20px; max-width: 720px; margin: 0 auto; }
115
- .view { animation: fadein .18s ease; }
116
- @keyframes fadein { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; } }
117
115
 
118
116
  .stat-strip {
119
117
  display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px;
@@ -215,6 +213,12 @@ a.mini-btn { text-decoration: none; display: inline-flex; align-items: center; }
215
213
  box-sizing: border-box; margin: 8px 2px 0; padding: 6px 8px 4px; border-bottom: 1px solid var(--dsr-line);
216
214
  color: var(--dsr-accent-strong); font-size: 11px; font-weight: 700;
217
215
  }
216
+ .session-workspace-group { flex: 0 0 auto; display: flex; flex-direction: column; gap: 0; }
217
+ .session-group-label { touch-action: pan-y; user-select: none; }
218
+ .session-group-drag-handle { flex: 0 0 auto; color: var(--dsr-muted); opacity: .52; font-size: 14px; line-height: 1; letter-spacing: -3px; }
219
+ .session-workspace-group.reorder-pressing { outline: 1px solid var(--dsr-accent-line); border-radius: var(--dsr-radius); }
220
+ .session-workspace-group.reorder-dragging { position: relative; z-index: 5; opacity: .76; outline: 1px solid var(--dsr-accent-strong); box-shadow: 0 10px 24px var(--dsr-shadow); touch-action: none; }
221
+ .session-workspace-group .session-card { touch-action: pan-y; }
218
222
  .session-group-icon { flex: 0 0 auto; color: var(--dsr-accent-2); font-size: 12px; }
219
223
  .session-group-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
220
224
  .session-list.workspace-sorted .sc-workspace { display: none; }
@@ -255,11 +259,18 @@ a.mini-btn { text-decoration: none; display: inline-flex; align-items: center; }
255
259
  .workbench-bar.bound .wb-toggle[aria-expanded="true"] .wb-chevron { transform: rotate(180deg); }
256
260
  .wb-panel { background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-top: none; border-radius: 0 0 var(--dsr-radius) var(--dsr-radius); padding: 8px; display: flex; flex-direction: column; gap: 6px; }
257
261
  .wb-project { background: var(--dsr-panel); border: 1px solid var(--dsr-line); border-radius: 10px; overflow: hidden; }
258
- .wb-project-head { display: flex; align-items: center; gap: 7px; padding: 9px 10px; cursor: pointer; }
262
+ .wb-project-head { display: flex; align-items: center; gap: 7px; padding: 9px 10px; cursor: pointer; touch-action: pan-y; user-select: none; }
263
+ .wb-drag-handle, .wb-session-drag-handle { flex: none; color: var(--dsr-muted); opacity: .52; font-size: 14px; line-height: 1; letter-spacing: -3px; }
264
+ .wb-project.reorder-pressing, .wb-session.reorder-pressing { outline: 1px solid var(--dsr-accent-line); }
265
+ .wb-project.reorder-dragging, .wb-session.reorder-dragging { position: relative; z-index: 5; opacity: .76; outline: 1px solid var(--dsr-accent-strong); box-shadow: 0 10px 24px var(--dsr-shadow); touch-action: none; }
266
+ .wb-panel.reorder-active { user-select: none; }
267
+ .reorder-placeholder { flex: 0 0 auto; box-sizing: border-box; border: 1px dashed var(--dsr-accent-line); border-radius: 8px; background: var(--dsr-accent-soft); opacity: .72; }
268
+ html.reorder-scroll-lock { overscroll-behavior: none; }
269
+ html.reorder-scroll-lock body { overscroll-behavior: none; }
259
270
  .wb-project-title { flex: 1; min-width: 0; font-size: 13.5px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
260
271
  .wb-new { padding: 3px 9px; font-size: 12px; }
261
272
  .wb-sessions { display: flex; flex-direction: column; gap: 4px; padding: 0 6px 6px; }
262
- .wb-session { width: 100%; display: flex; align-items: center; gap: 8px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 8px; padding: 7px 9px; text-align: left; color: var(--dsr-text); font: inherit; }
273
+ .wb-session { width: 100%; display: flex; align-items: center; gap: 8px; background: var(--dsr-bg-2); border: 1px solid var(--dsr-line); border-radius: 8px; padding: 7px 9px; text-align: left; color: var(--dsr-text); font: inherit; touch-action: pan-y; user-select: none; }
263
274
  .wb-session-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
264
275
  .wb-session-meta { flex-shrink: 0; font-size: 11px; color: var(--dsr-muted); }
265
276
  .wb-empty { font-size: 12px; color: var(--dsr-muted); text-align: center; padding: 10px 0; }
@@ -317,6 +328,9 @@ body.in-session .view {
317
328
  font-size: 16px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
318
329
  }
319
330
  .session-sub { font-size: 12px; color: var(--dsr-muted); margin-top: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
331
+ .session-head .mini-btn { flex: none; padding: 7px 9px; }
332
+ .text-input { width: 100%; box-sizing: border-box; min-height: 42px; padding: 10px 12px; border: 1px solid var(--dsr-line); border-radius: 10px; background: var(--dsr-bg-2); color: var(--dsr-text); font: inherit; outline: none; }
333
+ .text-input:focus { border-color: var(--dsr-accent-line); }
320
334
  .cards {
321
335
  display: flex; flex-direction: column; gap: 8px; margin-top: 8px;
322
336
  flex-shrink: 0; max-height: 42vh; overflow-y: auto;
@@ -333,6 +347,8 @@ body.in-session .view {
333
347
  .subagent-toggle { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 10px 12px; border: 0; background: transparent; color: inherit; text-align: left; cursor: pointer; }
334
348
  .subagent-toggle .card-title { margin: 0; }
335
349
  .subagent-toggle-icon { color: var(--dsr-muted); font-size: 16px; line-height: 1; }
350
+ .subagent-toggle-icon morph-icon,
351
+ .subagent-toggle-icon morph-icon svg { width: 16px; height: 16px; display: block; }
336
352
  .subagent-list { padding: 0 12px 10px; border-top: 1px solid var(--dsr-line); }
337
353
  .queue-dock { margin: 0 0 10px; border: 1px solid var(--dsr-line); border-radius: 12px; background: var(--dsr-surface); overflow: hidden; }
338
354
  .queue-dock-head { display: flex; align-items: center; gap: 8px; padding: 9px 12px; color: var(--dsr-muted); font-size: 12px; font-weight: 700; border-bottom: 1px solid var(--dsr-line); }
@@ -845,7 +861,6 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
845
861
  background: var(--dsr-bg-2); border-top: 1px solid var(--dsr-line); border-radius: 20px 20px 0 0;
846
862
  padding: 8px 12px calc(12px + env(safe-area-inset-bottom, 0px));
847
863
  box-shadow: 0 -10px 34px var(--dsr-shadow);
848
- animation: sheet-up .22s cubic-bezier(.2,.8,.3,1);
849
864
  }
850
865
  .sheet-handle { width: 38px; height: 4px; border-radius: 999px; background: var(--dsr-line); margin: 2px auto 10px; }
851
866
  .sheet-title { font-size: 14px; font-weight: 700; padding: 0 6px 8px; }
@@ -867,7 +882,6 @@ button.overview-attention-item, button.overview-session-item { cursor:pointer; }
867
882
  .sheet-item-name { font-size: 14px; font-weight: 600; }
868
883
  .sheet-item-desc { font-size: 12px; color: var(--dsr-muted); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
869
884
  @keyframes sheet-fade { from { opacity: 0 } to { opacity: 1 } }
870
- @keyframes sheet-up { from { transform: translateY(24px); opacity: .6 } to { transform: translateY(0); opacity: 1 } }
871
885
 
872
886
  /* ---------- 应用内选择抽屉 ---------- */
873
887
  .custom-select-native {
@@ -1,10 +1,14 @@
1
1
  {
2
- "version": "0.6.14",
2
+ "version": "0.6.15",
3
3
  "apkUrl": "dsh-remote.apk",
4
- "sha256": "4adbea85b1cf3f1768609a14c70adf3876033d892f7fab3dfa0d7f40959e643c",
5
- "releasedAt": "2026-08-25T06:48:09.119Z",
6
- "notes": "0.6.14-rc.7:将 Android 后台轮询服务接入与前台 App 相同的 client ID,修复 Dalvik 后台请求被显示为第二台未知设备;保留旧记录的顺序无关迁移;补齐文件上传请求的持久设备 ID;实时扫码继续使用 Worker 解码并降低取样负载;修复非用户来源的 DSH user/message 被误显示为用户输入;新增子代理折叠、排队消息插话、运行中提示和按轮次稳定排序;Markdown 支持 GFM 表格。",
4
+ "sha256": "0c32bd959466c73f270b81c263e2f7ca6ab5c21841fced2032f32317bad9e8d4",
5
+ "releasedAt": "2026-08-25T15:34:05.215Z",
6
+ "notes": "0.6.15:修复拖拽候选项拦截普通竖向滑动的问题,卡片中间也可自然滚动;仅在长按真正进入拖拽后锁定页面,并补充指针结束、取消、失焦和页面隐藏时的解锁清理。保留边缘自动滚动、占位符同步重排、主列表/工作台工作区与组内对话拖动。",
7
7
  "history": [
8
+ {
9
+ "version": "0.6.15",
10
+ "notes": "0.6.15:修复拖拽候选项拦截普通竖向滑动的问题,卡片中间也可自然滚动;仅在长按真正进入拖拽后锁定页面,并补充指针结束、取消、失焦和页面隐藏时的解锁清理。保留边缘自动滚动、占位符同步重排、主列表/工作台工作区与组内对话拖动。"
11
+ },
8
12
  {
9
13
  "version": "0.6.14",
10
14
  "notes": "0.6.14-rc.7:将 Android 后台轮询服务接入与前台 App 相同的 client ID,修复 Dalvik 后台请求被显示为第二台未知设备;保留旧记录的顺序无关迁移;补齐文件上传请求的持久设备 ID;实时扫码继续使用 Worker 解码并降低取样负载;修复非用户来源的 DSH user/message 被误显示为用户输入;新增子代理折叠、排队消息插话、运行中提示和按轮次稳定排序;Markdown 支持 GFM 表格。"
@@ -40,10 +44,6 @@
40
44
  {
41
45
  "version": "0.6.6",
42
46
  "notes": "新增新建工作区,按当前文件目录创建文件夹并自动打开会话;优化工作区排序分组、路径显示和小屏布局。"
43
- },
44
- {
45
- "version": "0.6.5",
46
- "notes": "连接稳定性全面优化:25 秒心跳保活(WiFi 切换 / NAT 超时不再假活);60 秒无消息自动重连,指数退避重连(1s → 30s 上限 + 抖动);网关死连接自动清理(60 秒无数据双向销毁);断网自动检测与恢复(离线 / 重连中倒计时 / 连接失败状态可见);降级轮询恢复优化。"
47
47
  }
48
48
  ]
49
49
  }
@@ -0,0 +1,10 @@
1
+ # GSAP vendored runtime
2
+
3
+ - Upstream: https://github.com/greensock/GSAP
4
+ - Version: 3.15.0
5
+ - Browser entry: `dist/gsap.min.js`
6
+ - License: GreenSock standard no-charge license
7
+ - License text: https://gsap.com/standard-license
8
+
9
+ dsh-Remote keeps the fixed browser distribution local so the zero-build WebUI
10
+ does not depend on a CDN or runtime npm resolution.