dsh-zen-remote 1.1.2 → 1.1.3
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 +26 -9
- package/docs/interface.md +5 -5
- package/lib/client.js +2094 -1875
- package/lib/index.js +21 -1
- package/lib/index.js.map +1 -1
- package/lib/types/client/client-config.d.ts +51 -0
- package/lib/types/client/client-config.d.ts.map +1 -0
- package/lib/types/client/effects/keyboard-avoid.d.ts +14 -7
- package/lib/types/client/effects/keyboard-avoid.d.ts.map +1 -1
- package/lib/types/client/effects/turn-fold.d.ts +14 -0
- package/lib/types/client/effects/turn-fold.d.ts.map +1 -1
- package/lib/types/client/styles/compat.css.d.ts +1 -1
- package/lib/types/client/styles/compat.css.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/styles/turn-fold.css.d.ts.map +1 -1
- package/lib/types/index.d.ts +25 -0
- package/lib/types/index.d.ts.map +1 -1
- package/package.json +11 -11
- package/scripts/check-keyboard-avoid.mjs +39 -0
- package/src/client/client-config.ts +86 -0
- package/src/client/effects/keyboard-avoid.ts +34 -14
- package/src/client/effects/turn-fold.ts +22 -17
- package/src/client/styles/compat.css.ts +33 -0
- package/src/client/styles/composer.css.ts +61 -1
- package/src/client/styles/turn-fold.css.ts +51 -8
- package/src/index.ts +35 -1
package/lib/types/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAIlD,6DAA6D;AAC7D,eAAO,MAAM,YAAY,4BAA4B,CAAA;AAErD;;kEAEkE;AAClE,eAAO,MAAM,mBAAmB,mCAAmC,CAAA;AAEnE,+FAA+F;AAC/F,eAAO,MAAM,UAAU,iBAAiB,CAAA;AAExC,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,QAAmB,CAAA;AAQxD,wBAAwB;AACxB,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;mEAE+D;IAC/D,eAAe,CAAC,EAAE,OAAO,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAEhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAIlD,6DAA6D;AAC7D,eAAO,MAAM,YAAY,4BAA4B,CAAA;AAErD;;kEAEkE;AAClE,eAAO,MAAM,mBAAmB,mCAAmC,CAAA;AAEnE,+FAA+F;AAC/F,eAAO,MAAM,UAAU,iBAAiB,CAAA;AAExC,6DAA6D;AAC7D,eAAO,MAAM,wBAAwB,QAAmB,CAAA;AAQxD,wBAAwB;AACxB,MAAM,WAAW,eAAe;IAC9B,iGAAiG;IACjG,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;mEAE+D;IAC/D,eAAe,CAAC,EAAE,OAAO,CAAA;IACzB;;;;;;;gCAO4B;IAC5B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,+EAA+E;IAC/E,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;yCACqC;IACrC,mBAAmB,CAAC,EAAE,MAAM,CAAA;CAC7B;AAED;;;;;;;;;GASG;AACH,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAGhH;AAwCD;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAa5D;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAKnE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAgBlD;AA6HD;;;;;;;;;GASG;AACH,wBAAsB,YAAY,CAChC,GAAG,EAAE,OAAO,EACZ,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,IAAI,CAAC,CA+Bf;AAED;;;;;;;GAOG;AACH,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,GAAE,eAAoB,GAAG,IAAI,CA8BtE"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-zen-remote",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "把 DeepSeek Harness 变成手机 App:移动端界面重排(两级页面栈、会话列表主屏、composer 重排、手势、附件上传)+ 配对码认证网关 + PWA 安装 + 真 Web Push。Complete mobile plugin for DeepSeek Harness (DSH): app-shell mobile UI, pairing-code gateway for public-internet access behind your own reverse proxy, installable PWA, offline service worker, and real Web Push agent-done notifications.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -81,16 +81,16 @@
|
|
|
81
81
|
},
|
|
82
82
|
"peerDependencies": {
|
|
83
83
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
84
|
-
"@deepseek-ai/dsh-client-locale": "
|
|
85
|
-
"@deepseek-ai/dsh-client-runtime": "
|
|
86
|
-
"@deepseek-ai/dsh-client-ui-conversation": "
|
|
87
|
-
"@deepseek-ai/dsh-client-ui-layout": "
|
|
88
|
-
"@deepseek-ai/dsh-client-ui-primitives": "
|
|
89
|
-
"@deepseek-ai/dsh-client-ui-sidebar": "
|
|
90
|
-
"@deepseek-ai/dsh-client-ui-slots": "
|
|
91
|
-
"@deepseek-ai/dsh-host-webserver": "
|
|
92
|
-
"@deepseek-ai/dsh-session": "
|
|
93
|
-
"@deepseek-ai/dsh-session-log-export": "
|
|
84
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
85
|
+
"@deepseek-ai/dsh-client-runtime": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
86
|
+
"@deepseek-ai/dsh-client-ui-conversation": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
87
|
+
"@deepseek-ai/dsh-client-ui-layout": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
88
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
89
|
+
"@deepseek-ai/dsh-client-ui-sidebar": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
90
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
91
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
92
|
+
"@deepseek-ai/dsh-session": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
93
|
+
"@deepseek-ai/dsh-session-log-export": ">=0.1.0-rc.7 <0.2.0-0 || >=0.1.1-rc.0 <0.2.0-0",
|
|
94
94
|
"react": "^18.2.0",
|
|
95
95
|
"@deepseek-ai/dsh-tools": ">=0.0.1-rc.1 <0.1.0 || >=0.1.0-rc.1 <0.2.0-0"
|
|
96
96
|
},
|
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
import assert from 'node:assert/strict'
|
|
6
6
|
import { readFileSync } from 'node:fs'
|
|
7
7
|
import { composerLift, estimatedLift, keyboardLift as lift, safetyPad } from '../src/client/effects/keyboard-avoid.ts'
|
|
8
|
+
import { KEYBOARD_DEFAULTS, parseClientConfig } from '../src/client/client-config.ts'
|
|
9
|
+
import { clamped } from '../src/index.ts'
|
|
8
10
|
|
|
9
11
|
// The reported class of bug: keyboard shrinks the visual viewport, browser
|
|
10
12
|
// does not pan it — the whole shrink is an occluded band at the bottom.
|
|
@@ -88,4 +90,41 @@ assert.match(
|
|
|
88
90
|
'keyboard-avoid transform must hit the box-generating _root child',
|
|
89
91
|
)
|
|
90
92
|
|
|
93
|
+
// --- the tuning knobs (2026-08-22) -----------------------------------------
|
|
94
|
+
// The two estimates above are guesses about someone else's hardware, so the
|
|
95
|
+
// plugin row can retune them. What must hold: an install that sets nothing
|
|
96
|
+
// behaves EXACTLY as it did before the knobs existed, and a set value cannot
|
|
97
|
+
// push the composer somewhere there is no way back from.
|
|
98
|
+
|
|
99
|
+
// Shipped defaults are the values the assertions above were written against.
|
|
100
|
+
assert.deepEqual(KEYBOARD_DEFAULTS, { liftRatio: 0.42, liftMaxPx: 400, safetyPadPx: 15 })
|
|
101
|
+
assert.equal(estimatedLift(858, KEYBOARD_DEFAULTS), estimatedLift(858), 'defaults are the no-config path')
|
|
102
|
+
assert.equal(safetyPad(ANDROID, KEYBOARD_DEFAULTS.safetyPadPx), safetyPad(ANDROID))
|
|
103
|
+
|
|
104
|
+
// A retuned row moves both halves of the estimate, and the pad.
|
|
105
|
+
assert.equal(estimatedLift(700, { liftRatio: 0.5, liftMaxPx: 400, safetyPadPx: 15 }), 350)
|
|
106
|
+
assert.equal(estimatedLift(858, { liftRatio: 0.5, liftMaxPx: 500, safetyPadPx: 15 }), 429)
|
|
107
|
+
assert.equal(estimatedLift(858, { liftRatio: 0.5, liftMaxPx: 380, safetyPadPx: 15 }), 380, 'cap still wins')
|
|
108
|
+
assert.equal(safetyPad(ANDROID, 40), 40)
|
|
109
|
+
assert.equal(safetyPad(IPHONE, 40), 0, 'a configured pad stays Android-only')
|
|
110
|
+
|
|
111
|
+
// An empty / failed / garbage route body leaves every default in place — the
|
|
112
|
+
// client half is where the defaults live, so this is the whole contract.
|
|
113
|
+
for (const body of [undefined, null, {}, 'not json', { keyboardLiftRatio: 'big' }, { keyboardLiftRatio: NaN }]) {
|
|
114
|
+
assert.deepEqual(parseClientConfig(body).keyboard, KEYBOARD_DEFAULTS, `body ${JSON.stringify(body) ?? 'undefined'} keeps defaults`)
|
|
115
|
+
}
|
|
116
|
+
assert.equal(parseClientConfig({ keyboardLiftRatio: 0.5 }).keyboard.liftRatio, 0.5)
|
|
117
|
+
assert.equal(parseClientConfig({ keyboardSafetyPadPx: 0 }).keyboard.safetyPadPx, 0, 'zero is a real value, not "unset"')
|
|
118
|
+
assert.equal(parseClientConfig({ turnFoldDesktop: true }).turnFoldDesktop, true)
|
|
119
|
+
|
|
120
|
+
// The host clamps before publishing: a typo in the YAML must not be able to
|
|
121
|
+
// push the composer off-screen with no way back except editing the YAML.
|
|
122
|
+
assert.deepEqual(clamped('keyboardLiftRatio', 5, 0, 1), { keyboardLiftRatio: 1 })
|
|
123
|
+
assert.deepEqual(clamped('keyboardLiftRatio', -2, 0, 1), { keyboardLiftRatio: 0 })
|
|
124
|
+
assert.deepEqual(clamped('keyboardLiftMaxPx', 9000, 0, 2000), { keyboardLiftMaxPx: 2000 })
|
|
125
|
+
assert.deepEqual(clamped('keyboardLiftRatio', undefined, 0, 1), {}, 'unset stays absent so the client default wins')
|
|
126
|
+
for (const bad of [Number.NaN, Infinity, '0.5', null]) {
|
|
127
|
+
assert.deepEqual(clamped('keyboardLiftRatio', bad, 0, 1), {}, `${String(bad)} is not a number`)
|
|
128
|
+
}
|
|
129
|
+
|
|
91
130
|
console.log('KEYBOARD AVOID CHECK OK')
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The plugin row's client-facing knobs, fetched once per page.
|
|
3
|
+
*
|
|
4
|
+
* The client bundle ships statically and never sees the row config, so the
|
|
5
|
+
* host republishes the client-relevant subset at {@link CLIENT_CONFIG_ROUTE}
|
|
6
|
+
* (src/index.ts). Two effects read it — turn-fold's desktop switch and
|
|
7
|
+
* keyboard-avoid's fallback tuning — so the request is memoized here rather
|
|
8
|
+
* than fired once per reader.
|
|
9
|
+
*
|
|
10
|
+
* Defaults live in THIS file, not in the host: the route omits a knob the
|
|
11
|
+
* row never set, so there is exactly one place each default is written and
|
|
12
|
+
* no way for the two halves to drift apart on the value. The host's job is
|
|
13
|
+
* only to validate and clamp what a user actually typed.
|
|
14
|
+
*
|
|
15
|
+
* Every read is total — a failed request, a non-JSON body, or a garbage
|
|
16
|
+
* value all fall back to the defaults rather than throwing into an effect.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/** Mirror of src/index.ts CLIENT_CONFIG_ROUTE (client tsconfig cannot reach it). */
|
|
20
|
+
export const CLIENT_CONFIG_ROUTE = '/_dsh/mobile-nav/client-config'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Tuning for the estimated-lift path in effects/keyboard-avoid.ts — the
|
|
24
|
+
* fallback for a keyboard the browser cannot see at all. There is nothing to
|
|
25
|
+
* measure on those devices, so these are estimates, and estimates about
|
|
26
|
+
* hardware need a knob: IME toolbars, foldables, and PWA shells all sit at
|
|
27
|
+
* different heights, and the person holding the phone is the only one who
|
|
28
|
+
* can see whether the composer cleared the keys.
|
|
29
|
+
*/
|
|
30
|
+
export interface KeyboardTuning {
|
|
31
|
+
/** Share of the layout viewport the estimated lift starts from. */
|
|
32
|
+
liftRatio: number
|
|
33
|
+
/** Ceiling on that estimate, so a tall viewport cannot strand the composer mid-screen. */
|
|
34
|
+
liftMaxPx: number
|
|
35
|
+
/** Extra clearance above a keyboard the browser DID react to (Android only). */
|
|
36
|
+
safetyPadPx: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Shipped tuning: the values measured on the one reported device (issue #1,
|
|
40
|
+
* WeType ~315 CSS px on an 858px viewport) plus the Android IME under-report
|
|
41
|
+
* pad. Changing these changes the default for every install — a per-device
|
|
42
|
+
* fix belongs in that install's plugin row instead. */
|
|
43
|
+
export const KEYBOARD_DEFAULTS: KeyboardTuning = {
|
|
44
|
+
liftRatio: 0.42,
|
|
45
|
+
liftMaxPx: 400,
|
|
46
|
+
safetyPadPx: 15,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Everything the client half reads off the plugin row. */
|
|
50
|
+
export interface ClientConfig {
|
|
51
|
+
/** Fold each turn's process at every width, not just below the phone breakpoint. */
|
|
52
|
+
turnFoldDesktop: boolean
|
|
53
|
+
keyboard: KeyboardTuning
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** One published number, or the shipped default when the row left it unset
|
|
57
|
+
* (the route omits the key) or the body is not what it claims to be. */
|
|
58
|
+
function num(value: unknown, fallback: number): number {
|
|
59
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Parse one route body. Exported for scripts/check-client-config.mjs. */
|
|
63
|
+
export function parseClientConfig(body: unknown): ClientConfig {
|
|
64
|
+
const row: Record<string, unknown> = typeof body === 'object' && body !== null
|
|
65
|
+
? body as Record<string, unknown>
|
|
66
|
+
: {}
|
|
67
|
+
return {
|
|
68
|
+
turnFoldDesktop: row['turnFoldDesktop'] === true,
|
|
69
|
+
keyboard: {
|
|
70
|
+
liftRatio: num(row['keyboardLiftRatio'], KEYBOARD_DEFAULTS.liftRatio),
|
|
71
|
+
liftMaxPx: num(row['keyboardLiftMaxPx'], KEYBOARD_DEFAULTS.liftMaxPx),
|
|
72
|
+
safetyPadPx: num(row['keyboardSafetyPadPx'], KEYBOARD_DEFAULTS.safetyPadPx),
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
let cached: Promise<ClientConfig> | undefined
|
|
78
|
+
|
|
79
|
+
/** The row config, fetched at most once per page load. Never rejects. */
|
|
80
|
+
export function clientConfig(): Promise<ClientConfig> {
|
|
81
|
+
cached ??= fetch(CLIENT_CONFIG_ROUTE, { cache: 'no-store' })
|
|
82
|
+
.then((res) => (res.ok ? res.json() as Promise<unknown> : undefined))
|
|
83
|
+
.catch(() => undefined)
|
|
84
|
+
.then(parseClientConfig)
|
|
85
|
+
return cached
|
|
86
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
import { clientConfig, KEYBOARD_DEFAULTS, type KeyboardTuning } from '../client-config.ts'
|
|
2
3
|
|
|
3
4
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
4
5
|
const PHONE_QUERY = '(max-width: 767px)'
|
|
@@ -27,14 +28,19 @@ const MAX_SCALE = 1.01
|
|
|
27
28
|
*
|
|
28
29
|
* ANDROID ONLY ({@link safetyPad}). The under-reporting is an Android IME
|
|
29
30
|
* behaviour; iOS has one system keyboard whose height WebKit reports exactly,
|
|
30
|
-
* so padding there would be a visible gap bought for nothing.
|
|
31
|
-
|
|
31
|
+
* so padding there would be a visible gap bought for nothing.
|
|
32
|
+
*
|
|
33
|
+
* The 15px default is what shipped; a device whose IME hides more of the
|
|
34
|
+
* composer than that can raise it through the plugin row's
|
|
35
|
+
* `config.keyboardSafetyPadPx` ({@link KeyboardTuning}). */
|
|
36
|
+
const SAFETY_PAD_PX = KEYBOARD_DEFAULTS.safetyPadPx
|
|
32
37
|
|
|
33
|
-
/** The safety pad this platform gets: the clearance
|
|
34
|
-
* Android. UA sniffing is the right tool here — the target is a platform
|
|
35
|
-
* defect, not a feature that could be detected.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
+
/** The safety pad this platform gets: the configured clearance, or nothing
|
|
39
|
+
* off Android. UA sniffing is the right tool here — the target is a platform
|
|
40
|
+
* defect, not a feature that could be detected.
|
|
41
|
+
* @param padPx - the row's clearance; defaults to the shipped 15px. */
|
|
42
|
+
export function safetyPad(userAgent: string, padPx: number = SAFETY_PAD_PX): number {
|
|
43
|
+
return /Android/iu.test(userAgent) ? padPx : 0
|
|
38
44
|
}
|
|
39
45
|
|
|
40
46
|
/** A viewport that lost at least this much against its no-keyboard baseline
|
|
@@ -66,11 +72,16 @@ const PROBE_EPSILON_PX = 8
|
|
|
66
72
|
* There is no signal to measure, so this is an estimate: the reporter's
|
|
67
73
|
* measured WeType is ~315 CSS px on a 858px viewport (~37%); 42% capped at
|
|
68
74
|
* 400px covers taller IME toolbars without stranding the composer mid-screen.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* Both numbers are a guess about someone else's hardware, so both are knobs:
|
|
77
|
+
* `config.keyboardLiftRatio` / `config.keyboardLiftMaxPx` on the plugin row
|
|
78
|
+
* ({@link KeyboardTuning}). The shipped pair stays the default — an install
|
|
79
|
+
* that never touches them behaves exactly as before.
|
|
80
|
+
*
|
|
81
|
+
* @param tuning - the row's tuning; defaults to the shipped estimate.
|
|
71
82
|
*/
|
|
72
|
-
export function estimatedLift(innerHeight: number): number {
|
|
73
|
-
return Math.min(Math.round(innerHeight *
|
|
83
|
+
export function estimatedLift(innerHeight: number, tuning: KeyboardTuning = KEYBOARD_DEFAULTS): number {
|
|
84
|
+
return Math.min(Math.round(innerHeight * tuning.liftRatio), tuning.liftMaxPx)
|
|
74
85
|
}
|
|
75
86
|
|
|
76
87
|
/** One visualViewport reading, in CSS pixels. */
|
|
@@ -188,7 +199,16 @@ export function installKeyboardAvoid(ctx: ClientContext): void {
|
|
|
188
199
|
* it. Re-read on every sync that finds the composer unfocused, so a
|
|
189
200
|
* rotation or a URL-bar change rebases it without extra listeners. */
|
|
190
201
|
let baseline = 0
|
|
191
|
-
|
|
202
|
+
/** Row tuning for the estimate path. Starts on the shipped defaults and
|
|
203
|
+
* is replaced once the config request lands (a few ms into the page, and
|
|
204
|
+
* always long before a touch focus can raise a keyboard) — no re-sync is
|
|
205
|
+
* forced, the next focus or viewport event picks the new values up. */
|
|
206
|
+
let tuning = KEYBOARD_DEFAULTS
|
|
207
|
+
let pad = safetyPad(navigator.userAgent)
|
|
208
|
+
void clientConfig().then((loaded) => {
|
|
209
|
+
tuning = loaded.keyboard
|
|
210
|
+
pad = safetyPad(navigator.userAgent, tuning.safetyPadPx)
|
|
211
|
+
})
|
|
192
212
|
|
|
193
213
|
const composerTextarea = (node: unknown): boolean =>
|
|
194
214
|
node instanceof HTMLElement && node.tagName === 'TEXTAREA' && node.closest(COMPOSER) !== null
|
|
@@ -265,7 +285,7 @@ export function installKeyboardAvoid(ctx: ClientContext): void {
|
|
|
265
285
|
// the probe below still runs and revokes the verdict if the viewport
|
|
266
286
|
// turns out to react after all.
|
|
267
287
|
if (localStorage.getItem(DUMB_KEY) === '1') {
|
|
268
|
-
estimate = estimatedLift(window.innerHeight)
|
|
288
|
+
estimate = estimatedLift(window.innerHeight, tuning)
|
|
269
289
|
sync()
|
|
270
290
|
}
|
|
271
291
|
const h0 = viewport.height
|
|
@@ -297,7 +317,7 @@ export function installKeyboardAvoid(ctx: ClientContext): void {
|
|
|
297
317
|
// remember the verdict so the next focus skips the wait.
|
|
298
318
|
if (!composerTextarea(document.activeElement)) return
|
|
299
319
|
localStorage.setItem(DUMB_KEY, '1')
|
|
300
|
-
estimate = estimatedLift(window.innerHeight)
|
|
320
|
+
estimate = estimatedLift(window.innerHeight, tuning)
|
|
301
321
|
sync()
|
|
302
322
|
}
|
|
303
323
|
probeTimer = window.setTimeout(step, PROBE_INTERVAL_MS)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
|
|
2
|
+
import { clientConfig } from '../client-config.ts'
|
|
2
3
|
import { NS } from '../locales.ts'
|
|
3
4
|
|
|
4
5
|
/** Phone breakpoint — same query every phone-only effect in this plugin uses. */
|
|
@@ -15,8 +16,6 @@ const DESKTOP_KEY = 'dsh-mobile-nav.turn-fold-desktop'
|
|
|
15
16
|
const DESKTOP_PARAM = 'mobile-nav-turn-fold'
|
|
16
17
|
/** Root attribute the stylesheet keys the width-independent rules on. */
|
|
17
18
|
const DESKTOP_ATTR = 'data-mnav-desktop-fold'
|
|
18
|
-
/** Mirror of src/index.ts CLIENT_CONFIG_ROUTE (client tsconfig cannot reach it). */
|
|
19
|
-
const CLIENT_CONFIG_ROUTE = '/_dsh/mobile-nav/client-config'
|
|
20
19
|
|
|
21
20
|
/** Read (and, when the URL param is present, persist) the per-browser opt-in. */
|
|
22
21
|
function desktopOptIn(): boolean {
|
|
@@ -26,17 +25,10 @@ function desktopOptIn(): boolean {
|
|
|
26
25
|
return localStorage.getItem(DESKTOP_KEY) === '1'
|
|
27
26
|
}
|
|
28
27
|
|
|
29
|
-
/** True when the plugin row asks for desktop fold; false on any failure
|
|
28
|
+
/** True when the plugin row asks for desktop fold; false on any failure —
|
|
29
|
+
* the shared reader in client-config.ts already swallows those. */
|
|
30
30
|
async function desktopConfigured(): Promise<boolean> {
|
|
31
|
-
|
|
32
|
-
const res = await fetch(CLIENT_CONFIG_ROUTE, { cache: 'no-store' })
|
|
33
|
-
if (!res.ok) return false
|
|
34
|
-
const body: unknown = await res.json()
|
|
35
|
-
return typeof body === 'object' && body !== null
|
|
36
|
-
&& (body as { turnFoldDesktop?: unknown }).turnFoldDesktop === true
|
|
37
|
-
} catch {
|
|
38
|
-
return false
|
|
39
|
-
}
|
|
31
|
+
return (await clientConfig()).turnFoldDesktop
|
|
40
32
|
}
|
|
41
33
|
|
|
42
34
|
/** ChatView's flow column (dsh-client-ui-conversation lib/client.js:5512).
|
|
@@ -56,19 +48,30 @@ const CHIP_SELECTOR = '[data-mobile-nav="turn-fold"]'
|
|
|
56
48
|
* per-block below), `turn-tail` (the footer with its actions), and the
|
|
57
49
|
* error notices `turn-error` / `turn-max-tokens` / `model-retry`, which are
|
|
58
50
|
* exactly what a reader must not have to hunt for. */
|
|
59
|
-
const PROCESS_KINDS =
|
|
51
|
+
export const PROCESS_KINDS = ['context', 'tool-call', 'command'] as const
|
|
52
|
+
const PROCESS_KIND_SET: ReadonlySet<string> = new Set(PROCESS_KINDS)
|
|
60
53
|
|
|
61
54
|
/** Node kinds that open a new turn group. */
|
|
62
55
|
const TURN_HEAD_KINDS = new Set(['user', 'steering'])
|
|
63
56
|
|
|
64
57
|
/** ReasoningRow's own marker (lib/client.js:8966) — the Think disclosure
|
|
65
58
|
* rendered inside an assistant-step row, beside that step's prose. */
|
|
66
|
-
const THINK = '[data-variant="think"]'
|
|
59
|
+
export const THINK = '[data-variant="think"]'
|
|
67
60
|
|
|
68
|
-
/** Attribute the phone stylesheet turns into `display: none`.
|
|
61
|
+
/** Attribute the phone stylesheet turns into `display: none`. Still written
|
|
62
|
+
* per element for the one case the stylesheet cannot express on its own: an
|
|
63
|
+
* assistant-step row whose entire body is reasoning, which has to go as a
|
|
64
|
+
* whole row (see {@link foldsWholeRow}). */
|
|
69
65
|
const FOLD = 'data-mnav-fold'
|
|
70
66
|
/** Per-item override written while its turn is expanded. */
|
|
71
|
-
const OPEN = 'data-mnav-fold-open'
|
|
67
|
+
export const OPEN = 'data-mnav-fold-open'
|
|
68
|
+
/** Root attribute written for as long as this effect is attached — i.e. for
|
|
69
|
+
* exactly the widths where the fold is live. The stylesheet keys its
|
|
70
|
+
* born-folded rules on it, which is what makes a newly inserted tool call
|
|
71
|
+
* hidden at its FIRST paint instead of after the next rescan (the "row
|
|
72
|
+
* flashes in, then folds away" report, 2026-08-22); it also keeps the
|
|
73
|
+
* stylesheet a no-op — content visible — if this effect never runs. */
|
|
74
|
+
const ACTIVE_ATTR = 'data-mnav-fold-on'
|
|
72
75
|
|
|
73
76
|
/**
|
|
74
77
|
* True when the reasoning rows are the ONLY thing this row renders, so the
|
|
@@ -98,7 +101,7 @@ function foldsWholeRow(row: Element, thinks: readonly Element[]): boolean {
|
|
|
98
101
|
function processItems(row: Element): readonly Element[] {
|
|
99
102
|
const kind = row.getAttribute('data-chat-flow-kind')
|
|
100
103
|
if (kind === null) return []
|
|
101
|
-
if (
|
|
104
|
+
if (PROCESS_KIND_SET.has(kind)) return [row]
|
|
102
105
|
if (kind !== 'assistant-step') return []
|
|
103
106
|
const thinks = [...row.querySelectorAll(THINK)]
|
|
104
107
|
if (thinks.length === 0) return []
|
|
@@ -280,12 +283,14 @@ export function installTurnFold(ctx: ClientContext): void {
|
|
|
280
283
|
|
|
281
284
|
const attach = (): void => {
|
|
282
285
|
if (observer !== null) return
|
|
286
|
+
document.documentElement.setAttribute(ACTIVE_ATTR, '')
|
|
283
287
|
observer = new MutationObserver(schedule)
|
|
284
288
|
observer.observe(document.body, { childList: true, subtree: true })
|
|
285
289
|
document.addEventListener('click', onClick)
|
|
286
290
|
scan()
|
|
287
291
|
}
|
|
288
292
|
const detach = (): void => {
|
|
293
|
+
document.documentElement.removeAttribute(ACTIVE_ATTR)
|
|
289
294
|
observer?.disconnect()
|
|
290
295
|
observer = null
|
|
291
296
|
document.removeEventListener('click', onClick)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// compat — split from src/client/mobile.css.ts (2026-08-16), order preserved.
|
|
2
2
|
// Do not reorder: styles/index.ts concatenates in this exact order.
|
|
3
3
|
|
|
4
|
+
import { UPLOAD_DIR } from '../attach-upload.ts'
|
|
5
|
+
|
|
4
6
|
export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility ----------
|
|
5
7
|
The linxin666 plugin suite extends the shell frame directly:
|
|
6
8
|
- aionui-panel appends two trailing grid columns (explorer / preview)
|
|
@@ -736,4 +738,35 @@ export const COMPAT_CSS = ` /* ---------- dsh-web-ui family compatibility -----
|
|
|
736
738
|
display: none !important;
|
|
737
739
|
}
|
|
738
740
|
}
|
|
741
|
+
|
|
742
|
+
/* ---------- dsh-at-file compatibility ----------
|
|
743
|
+
dsh-at-file registers its own \`conversation.input.dock\` entry
|
|
744
|
+
([data-at-file-dock]) and renders one row per \`@path\` token in the
|
|
745
|
+
draft. That is exactly the same source this plugin's attachment chips
|
|
746
|
+
read, so on a phone a single uploaded image produced TWO entries on the
|
|
747
|
+
composer rail: our 48px thumbnail tile AND at-file's file-name row for
|
|
748
|
+
the same token (reported 2026-08-22).
|
|
749
|
+
|
|
750
|
+
Drop at-file's row for the upload directory only. Everything else the
|
|
751
|
+
user @-mentions by hand is at-file's own job and stays — this is not a
|
|
752
|
+
blanket hide. The path is readable from the row's own title attribute
|
|
753
|
+
(FilesDock passes \`title={mention.relative}\`), which is a rendered
|
|
754
|
+
contract rather than a hashed class name.
|
|
755
|
+
|
|
756
|
+
Phone only, and deliberately so: at \u003e= 768px our chips are hidden by
|
|
757
|
+
composer.css.ts, which leaves at-file's row as the ONLY thing on screen
|
|
758
|
+
representing the attachment. Hiding it there would make an uploaded file
|
|
759
|
+
invisible.
|
|
760
|
+
|
|
761
|
+
When every row was an upload the container itself is left empty, and an
|
|
762
|
+
empty flex item still claims the rail's 6px gap — so the dock goes too
|
|
763
|
+
unless at least one non-upload row survives. */
|
|
764
|
+
@media (max-width: 767px) {
|
|
765
|
+
[data-at-file-dock] [data-at-file-row]:has([title^="${UPLOAD_DIR}/"]) {
|
|
766
|
+
display: none !important;
|
|
767
|
+
}
|
|
768
|
+
[data-at-file-dock]:not(:has([data-at-file-row] [title]:not([title^="${UPLOAD_DIR}/"]))) {
|
|
769
|
+
display: none !important;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
739
772
|
`
|
|
@@ -315,9 +315,18 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
|
|
|
315
315
|
/* --- 7b. attachment chips (S7.1) ---
|
|
316
316
|
Our own dock entry opts OUT of the 26px pill cage above: an image chip is
|
|
317
317
|
a 48px tile. The row sits tight under the card's top edge, so the chips
|
|
318
|
-
read as part of the composer rather than as a floating strip.
|
|
318
|
+
read as part of the composer rather than as a floating strip.
|
|
319
|
+
|
|
320
|
+
It also takes the LAST line of its own (line policy in 7c): a 48px tile
|
|
321
|
+
beside the to-do chip on one nowrap line squeezed that chip down to an
|
|
322
|
+
unreadable stub, with a long file name pushing the rest off-screen
|
|
323
|
+
(reported 2026-08-22). Its own chips wrap rather than overflow, so
|
|
324
|
+
several attachments stack instead of scrolling out of reach. */
|
|
319
325
|
[data-slot="conversation.input.dock"] > [data-mobile-nav="attach-chips"] {
|
|
320
326
|
display: flex !important;
|
|
327
|
+
flex: 1 0 100% !important;
|
|
328
|
+
flex-wrap: wrap !important;
|
|
329
|
+
order: 2 !important;
|
|
321
330
|
align-items: center !important;
|
|
322
331
|
gap: 6px !important;
|
|
323
332
|
max-width: none !important;
|
|
@@ -327,6 +336,57 @@ export const COMPOSER_CSS = `/* ---------- phone composer (< 768px) ---------- *
|
|
|
327
336
|
overflow: visible !important;
|
|
328
337
|
margin-bottom: -2px;
|
|
329
338
|
}
|
|
339
|
+
|
|
340
|
+
/* --- 7c. the queue strip opts OUT of the cage, on a line of its own ---
|
|
341
|
+
Third entry that is not a pill. conversation.input.dock is a \`list\` slot
|
|
342
|
+
and DSH's own QueueDock registers into it at order 20 (id \`queue\`,
|
|
343
|
+
dsh-client-ui-conversation) — a 36px row per queued message, or a
|
|
344
|
+
collapsible "N 条排队消息" header once there is more than one. The 26px
|
|
345
|
+
cage above clipped it to a sliver: on a phone a queued message showed as
|
|
346
|
+
a grey stub poking out from behind the input card, unreadable and with
|
|
347
|
+
its edit / delete / send buttons cut off (reported 2026-08-22, same
|
|
348
|
+
failure the to-do card hit in 7a).
|
|
349
|
+
|
|
350
|
+
Two things are restored. Geometry: the official row height back, and the
|
|
351
|
+
dock's own composer-column sizing dropped — its width/max-width are
|
|
352
|
+
calc()s over the composer card variables and its bottom margin is
|
|
353
|
+
negative by design (upstream tucks the strip UNDER the card), none of
|
|
354
|
+
which survives being a flex item of our chip rail.
|
|
355
|
+
|
|
356
|
+
Layout — the line policy for the whole rail, now that three things want
|
|
357
|
+
room on it. The pills (the to-do card, a branch chip, whatever else
|
|
358
|
+
registers) keep order 0 and share the top line, still scrolling
|
|
359
|
+
horizontally. The queue takes the next line (order 1). The attachment
|
|
360
|
+
chips take the last one (order 2, 7b), so what the user is about to send
|
|
361
|
+
sits closest to the input it will be sent from. The rail only wraps
|
|
362
|
+
while one of the two full-width entries exists, so a session with
|
|
363
|
+
neither keeps the original single scrolling chip line. */
|
|
364
|
+
[data-slot="conversation.input.dock"]:has(> [data-queue-dock], > [data-mobile-nav="attach-chips"]) {
|
|
365
|
+
flex-wrap: wrap !important;
|
|
366
|
+
}
|
|
367
|
+
[data-slot="conversation.input.dock"] > [data-queue-dock] {
|
|
368
|
+
flex: 1 0 100% !important;
|
|
369
|
+
order: 1 !important;
|
|
370
|
+
max-width: none !important;
|
|
371
|
+
min-height: 0 !important;
|
|
372
|
+
max-height: none !important;
|
|
373
|
+
margin: 0 !important;
|
|
374
|
+
padding: 0 !important;
|
|
375
|
+
border-radius: 0 !important;
|
|
376
|
+
overflow: visible !important;
|
|
377
|
+
font-size: 13px !important;
|
|
378
|
+
line-height: normal !important;
|
|
379
|
+
}
|
|
380
|
+
/* The panel is the dock's only child; upstream rounds its top corners only
|
|
381
|
+
and drops the bottom border, because there it is half-hidden behind the
|
|
382
|
+
card. On its own line it is a free-standing strip, so close it up. */
|
|
383
|
+
[data-slot="conversation.input.dock"] > [data-queue-dock] > * {
|
|
384
|
+
border-radius: 12px !important;
|
|
385
|
+
}
|
|
386
|
+
[data-slot="conversation.input.dock"] > [data-queue-dock] > *::after {
|
|
387
|
+
border-bottom: 1px solid var(--dsw-alias-border-l1) !important;
|
|
388
|
+
}
|
|
389
|
+
|
|
330
390
|
[data-mobile-nav="attach-chip"] {
|
|
331
391
|
position: relative;
|
|
332
392
|
display: inline-flex;
|
|
@@ -1,16 +1,53 @@
|
|
|
1
1
|
// turn-fold — the folded turn process and its summary row (S8, 2026-08-17).
|
|
2
|
-
//
|
|
3
|
-
// html[data-mnav-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// (
|
|
2
|
+
// Two blocks, both inert unless the fold is actually running:
|
|
3
|
+
// * BORN_FOLDED, keyed on html[data-mnav-fold-on] — the root attribute
|
|
4
|
+
// effects/turn-fold.ts holds for as long as it is attached, so the block
|
|
5
|
+
// is live at exactly the widths the fold is. It hides process rows by
|
|
6
|
+
// the HOST's own markers, which means a row is folded at its first paint
|
|
7
|
+
// rather than a frame after the effect notices it (2026-08-22).
|
|
8
|
+
// * the `rules()` block below, scoped to (max-width: 767px) OR
|
|
9
|
+
// html[data-mnav-desktop-fold] (the per-browser desktop opt-in, issue
|
|
10
|
+
// #2), reading markers this plugin writes: data-mnav-fold on a process
|
|
11
|
+
// element, data-mobile-nav="turn-fold" on the injected summary row.
|
|
12
|
+
// effects/turn-fold.ts wipes both when it detaches, so a non-opted-in
|
|
13
|
+
// tablet/desktop is a no-op twice over, by attribute and by scope. It is
|
|
14
|
+
// authored once and emitted twice so the two scopes can never drift.
|
|
10
15
|
// Appended last in styles/index.ts; it shares no selector with any other
|
|
11
16
|
// file, the position just keeps the "phone files come after the shared
|
|
12
17
|
// <=1023px block" ordering intact.
|
|
13
18
|
|
|
19
|
+
import { OPEN, PROCESS_KINDS, THINK } from '../effects/turn-fold.ts'
|
|
20
|
+
|
|
21
|
+
/** Mirror of the root attribute effects/turn-fold.ts holds while attached. */
|
|
22
|
+
const ACTIVE = 'data-mnav-fold-on'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Born folded — the reason a new tool call no longer blinks into view.
|
|
26
|
+
*
|
|
27
|
+
* These selectors read the HOST's own markers (ChatNodeSeat's
|
|
28
|
+
* `data-chat-flow-kind`, ReasoningRow's `data-variant`), which React sets in
|
|
29
|
+
* the same commit that inserts the row, so the row is already
|
|
30
|
+
* `display: none` at its first paint. effects/turn-fold.ts still rescans on
|
|
31
|
+
* the next animation frame, but by then it is only writing the per-turn
|
|
32
|
+
* {@link OPEN} override and the summary chip — it is no longer what hides
|
|
33
|
+
* anything, which is what the reader used to see as a one-frame flash
|
|
34
|
+
* (2026-08-22).
|
|
35
|
+
*
|
|
36
|
+
* Keyed on the root attribute the effect holds while it is attached, so this
|
|
37
|
+
* block is live for exactly the widths the fold is, and hides nothing at all
|
|
38
|
+
* when the effect never ran.
|
|
39
|
+
*
|
|
40
|
+
* One case stays with the effect: an assistant-step row whose whole body is
|
|
41
|
+
* reasoning has to be hidden as a row (an emptied-out flex item still claims
|
|
42
|
+
* the flow column's 16px gap), and "this row has nothing but Think in it" is
|
|
43
|
+
* not a selector. Its rows therefore still fold a frame late — as blank
|
|
44
|
+
* space, not as content, since the Think rows below are hidden here.
|
|
45
|
+
*/
|
|
46
|
+
const BORN_FOLDED = [
|
|
47
|
+
...PROCESS_KINDS.map((kind) => `html[${ACTIVE}] [data-chat-flow] > [data-chat-flow-kind="${kind}"]:not([${OPEN}])`),
|
|
48
|
+
`html[${ACTIVE}] [data-chat-flow] > [data-chat-flow-kind="assistant-step"] ${THINK}:not([${OPEN}])`,
|
|
49
|
+
].join(',\n')
|
|
50
|
+
|
|
14
51
|
/** The fold + chip rules, with every selector prefixed by `scope`. */
|
|
15
52
|
const rules = (scope: string): string => `
|
|
16
53
|
/* Folded process: tool-call / context / command rows and the Think
|
|
@@ -97,6 +134,12 @@ const rules = (scope: string): string => `
|
|
|
97
134
|
|
|
98
135
|
export const TURN_FOLD_CSS = `/* ---------- turn process fold (< 768px, or desktop opt-in) ---------- */
|
|
99
136
|
|
|
137
|
+
/* Process rows and Think disclosures start folded, before any script has
|
|
138
|
+
looked at them — see BORN_FOLDED above. */
|
|
139
|
+
${BORN_FOLDED} {
|
|
140
|
+
display: none !important;
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
/* The summary row never paints outside its active scope, even if a
|
|
101
144
|
media-query change raced the effect's own cleanup. */
|
|
102
145
|
[data-mobile-nav="turn-fold"] {
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,35 @@ export interface MobileNavConfig {
|
|
|
51
51
|
* phone breakpoint. Default false (phone-only). A browser can still opt
|
|
52
52
|
* itself in via `?mobile-nav-turn-fold=1` when this is off. */
|
|
53
53
|
turnFoldDesktop?: boolean
|
|
54
|
+
/** Calibration for the composer lift used when a phone's keyboard is
|
|
55
|
+
* invisible to the browser (src/client/effects/keyboard-avoid.ts). Leave
|
|
56
|
+
* every one of these unset to keep the shipped estimate — the route omits
|
|
57
|
+
* what the row never set, and the client half owns the defaults, so there
|
|
58
|
+
* is one place each default is written.
|
|
59
|
+
*
|
|
60
|
+
* Share of the layout viewport the estimated lift starts from (shipped
|
|
61
|
+
* 0.42). Clamped to 0-1. */
|
|
62
|
+
keyboardLiftRatio?: number
|
|
63
|
+
/** Ceiling on that estimate in CSS pixels (shipped 400). Clamped to 0-2000. */
|
|
64
|
+
keyboardLiftMaxPx?: number
|
|
65
|
+
/** Extra clearance above a keyboard the browser DID react to, Android only
|
|
66
|
+
* (shipped 15). Clamped to 0-200. */
|
|
67
|
+
keyboardSafetyPadPx?: number
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* One configured number on its way to the browser, clamped into a band that
|
|
72
|
+
* cannot break the composer — a ratio of 5 or a 9000px lift would push the
|
|
73
|
+
* input clean off the screen with no way back except editing the YAML again.
|
|
74
|
+
* Absent from the result when the row left it unset or wrote something that
|
|
75
|
+
* is not a finite number, which leaves the client on its shipped default.
|
|
76
|
+
*
|
|
77
|
+
* Exported for scripts/check-keyboard-avoid.mjs — this is the trust boundary
|
|
78
|
+
* between a hand-edited YAML row and the browser, so it is asserted directly.
|
|
79
|
+
*/
|
|
80
|
+
export function clamped(key: string, value: number | undefined, min: number, max: number): Record<string, number> {
|
|
81
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) return {}
|
|
82
|
+
return { [key]: Math.min(Math.max(value, min), max) }
|
|
54
83
|
}
|
|
55
84
|
|
|
56
85
|
/**
|
|
@@ -361,7 +390,12 @@ export function apply(ctx: Context, config: MobileNavConfig = {}): void {
|
|
|
361
390
|
responseJson(res, 405, { ok: false, error: { code: 'method-not-allowed', message: 'Use GET' } })
|
|
362
391
|
return
|
|
363
392
|
}
|
|
364
|
-
responseJson(res, 200, {
|
|
393
|
+
responseJson(res, 200, {
|
|
394
|
+
turnFoldDesktop: config.turnFoldDesktop === true,
|
|
395
|
+
...clamped('keyboardLiftRatio', config.keyboardLiftRatio, 0, 1),
|
|
396
|
+
...clamped('keyboardLiftMaxPx', config.keyboardLiftMaxPx, 0, 2000),
|
|
397
|
+
...clamped('keyboardSafetyPadPx', config.keyboardSafetyPadPx, 0, 200),
|
|
398
|
+
})
|
|
365
399
|
},
|
|
366
400
|
}), 'dsh-mobile-nav: client config route')
|
|
367
401
|
})
|