@linxin666/dsh-remote-web-ui 0.1.1
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/LICENSE +28 -0
- package/README.md +346 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +2817 -0
- package/lib/index.js +1455 -0
- package/lib/invariant.js +21 -0
- package/lib/types/client/PairFailedNotice.d.ts +12 -0
- package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
- package/lib/types/client/PhoneIcon.d.ts +19 -0
- package/lib/types/client/PhoneIcon.d.ts.map +1 -0
- package/lib/types/client/PluginSettingsCard.d.ts +78 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
- package/lib/types/client/RemoteEntry.d.ts +10 -0
- package/lib/types/client/RemoteEntry.d.ts.map +1 -0
- package/lib/types/client/RemotePanel.d.ts +50 -0
- package/lib/types/client/RemotePanel.d.ts.map +1 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
- package/lib/types/client/deep-link.d.ts +40 -0
- package/lib/types/client/deep-link.d.ts.map +1 -0
- package/lib/types/client/index.d.ts +54 -0
- package/lib/types/client/index.d.ts.map +1 -0
- package/lib/types/client/locales.d.ts +144 -0
- package/lib/types/client/locales.d.ts.map +1 -0
- package/lib/types/client/pair-api.d.ts +100 -0
- package/lib/types/client/pair-api.d.ts.map +1 -0
- package/lib/types/client/settings-form.d.ts +119 -0
- package/lib/types/client/settings-form.d.ts.map +1 -0
- package/lib/types/gate.d.ts +58 -0
- package/lib/types/gate.d.ts.map +1 -0
- package/lib/types/index.d.ts +78 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.d.ts.map +1 -0
- package/lib/types/lan.d.ts +15 -0
- package/lib/types/lan.d.ts.map +1 -0
- package/lib/types/mobile/api.d.ts +64 -0
- package/lib/types/mobile/api.d.ts.map +1 -0
- package/lib/types/mobile/index.d.ts +10 -0
- package/lib/types/mobile/index.d.ts.map +1 -0
- package/lib/types/mobile/messages.d.ts +103 -0
- package/lib/types/mobile/messages.d.ts.map +1 -0
- package/lib/types/mobile/messages.test.d.ts +2 -0
- package/lib/types/mobile/messages.test.d.ts.map +1 -0
- package/lib/types/mobile/mobile-styles.d.ts +6 -0
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
- package/lib/types/mobile/mobile-theme.d.ts +22 -0
- package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
- package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
- package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
- package/lib/types/mobile/mux.d.ts +52 -0
- package/lib/types/mobile/mux.d.ts.map +1 -0
- package/lib/types/mobile/rpc.d.ts +38 -0
- package/lib/types/mobile/rpc.d.ts.map +1 -0
- package/lib/types/mobile/theme-toggle.d.ts +12 -0
- package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
- package/lib/types/mobile/views/App.d.ts +59 -0
- package/lib/types/mobile/views/App.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.d.ts +39 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
- package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
- package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/SessionListView.d.ts +27 -0
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
- package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
- package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
- package/lib/types/mobile-api.d.ts +41 -0
- package/lib/types/mobile-api.d.ts.map +1 -0
- package/lib/types/mobile-routes.d.ts +16 -0
- package/lib/types/mobile-routes.d.ts.map +1 -0
- package/lib/types/pairing.d.ts +215 -0
- package/lib/types/pairing.d.ts.map +1 -0
- package/lib/types/routes.d.ts +58 -0
- package/lib/types/routes.d.ts.map +1 -0
- package/lib/types/tunnel.d.ts +99 -0
- package/lib/types/tunnel.d.ts.map +1 -0
- package/package.json +104 -0
- package/src/client/PairFailedNotice.tsx +33 -0
- package/src/client/PhoneIcon.tsx +31 -0
- package/src/client/PluginSettingsCard.tsx +207 -0
- package/src/client/RemoteEntry.tsx +202 -0
- package/src/client/RemotePanel.tsx +186 -0
- package/src/client/RemoteSettingsCard.tsx +220 -0
- package/src/client/deep-link.ts +137 -0
- package/src/client/index.ts +167 -0
- package/src/client/locales.ts +149 -0
- package/src/client/pair-api.ts +176 -0
- package/src/client/remote.module.css +422 -0
- package/src/client/settings-card.module.css +296 -0
- package/src/client/settings-form.ts +294 -0
- package/src/css-modules.d.ts +6 -0
- package/src/gate.ts +121 -0
- package/src/index.ts +299 -0
- package/src/invariant.ts +32 -0
- package/src/lan.ts +21 -0
- package/src/mobile/api.ts +106 -0
- package/src/mobile/index.tsx +27 -0
- package/src/mobile/messages.test.ts +222 -0
- package/src/mobile/messages.ts +555 -0
- package/src/mobile/mobile-styles.ts +853 -0
- package/src/mobile/mobile-theme.test.ts +84 -0
- package/src/mobile/mobile-theme.ts +76 -0
- package/src/mobile/mux.ts +126 -0
- package/src/mobile/rpc.ts +90 -0
- package/src/mobile/theme-toggle.tsx +47 -0
- package/src/mobile/views/App.tsx +152 -0
- package/src/mobile/views/ChatView.test.tsx +243 -0
- package/src/mobile/views/ChatView.tsx +650 -0
- package/src/mobile/views/SessionListView.test.tsx +139 -0
- package/src/mobile/views/SessionListView.tsx +186 -0
- package/src/mobile/views/WorkspaceView.tsx +108 -0
- package/src/mobile-api.ts +271 -0
- package/src/mobile-routes.ts +77 -0
- package/src/pairing.ts +390 -0
- package/src/routes.ts +346 -0
- package/src/tunnel.ts +264 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routes.d.ts","sourceRoot":"","sources":["../../src/routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,EAA0B,KAAK,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,cAAc,CAAA;AAmDhG;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAOxE;AAKD,8CAA8C;AAC9C,eAAO,MAAM,UAAU;;;;;;;CAOb,CAAA;AAiCV,gDAAgD;AAChD,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0B;IAElD;;OAEG;gBACS,OAAO,EAAE,cAAc;IAInC,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,eAAe,EAAE,GAAG,EAAE,cAAc,GAAG,IAAI;IAiBrD,kEAAkE;IAClE,IAAI,CAAC,QAAQ,EAAE,eAAe,GAAG,IAAI;IAYrC,wCAAwC;IACxC,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF;AAED,6CAA6C;AAC7C,MAAM,WAAW,cAAc;IAC7B,2BAA2B;IAC3B,OAAO,EAAE,cAAc,CAAA;IACvB,0EAA0E;IAC1E,YAAY,EAAE,SAAS,MAAM,EAAE,CAAA;CAChC;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,QAAQ,EAAE,CAgK3D"}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-tunnel manager: spawns a Cloudflare quick tunnel (`cloudflared
|
|
3
|
+
* tunnel --url <local>`) through the `cloudflared` npm package — its
|
|
4
|
+
* postinstall downloads the platform binary, so no user-side tooling is
|
|
5
|
+
* involved — surfaces the minted `https://xxx.trycloudflare.com` URL, and
|
|
6
|
+
* restarts the process after unexpected exits with exponential backoff.
|
|
7
|
+
*
|
|
8
|
+
* The cloudflared package's Tunnel is a thin spawn wrapper; this manager
|
|
9
|
+
* owns the lifecycle policy (binary readiness, URL timeout, restart
|
|
10
|
+
* backoff) around it. All seams — the tunnel factory, binary readiness,
|
|
11
|
+
* timers — are injectable so the whole lifecycle is unit-testable without
|
|
12
|
+
* a real binary or network.
|
|
13
|
+
*/
|
|
14
|
+
/** The observable tunnel lifecycle the settings/panel surfaces render. */
|
|
15
|
+
export type TunnelPhase = 'stopped' | 'starting' | 'running' | 'failed';
|
|
16
|
+
/** One tunnel status frame. */
|
|
17
|
+
export interface TunnelInfo {
|
|
18
|
+
phase: TunnelPhase;
|
|
19
|
+
/** The minted public URL, once the tunnel reports it. */
|
|
20
|
+
url?: string;
|
|
21
|
+
/** Human-readable failure detail (binary install, URL timeout, spawn error). */
|
|
22
|
+
error?: string;
|
|
23
|
+
}
|
|
24
|
+
/** The tunnel handle subset this manager uses (the package's Tunnel fits). */
|
|
25
|
+
export interface TunnelHandle {
|
|
26
|
+
on(event: string, listener: (...args: any[]) => void): unknown;
|
|
27
|
+
off(event: string, listener: (...args: any[]) => void): unknown;
|
|
28
|
+
stop(): boolean;
|
|
29
|
+
}
|
|
30
|
+
/** Injectable seams (defaults are the real cloudflared package + node timers). */
|
|
31
|
+
export interface TunnelManagerOptions {
|
|
32
|
+
/** Spawn one quick tunnel toward the local target URL. */
|
|
33
|
+
factory?: (targetUrl: string) => TunnelHandle;
|
|
34
|
+
/** Make sure the cloudflared binary exists, downloading it when absent. */
|
|
35
|
+
ensureBinary?: () => Promise<void>;
|
|
36
|
+
/** Wait up to this long for the tunnel URL before failing the attempt. */
|
|
37
|
+
urlTimeoutMs?: number;
|
|
38
|
+
/** First restart delay after an unexpected failure (exponential base). */
|
|
39
|
+
restartBaseMs?: number;
|
|
40
|
+
/** Cap on the exponential restart delay. */
|
|
41
|
+
restartMaxMs?: number;
|
|
42
|
+
/** Timer source (injected in tests). */
|
|
43
|
+
timer?: {
|
|
44
|
+
setTimeout(fn: () => void, ms: number): unknown;
|
|
45
|
+
clearTimeout(t: unknown): void;
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Own the lifecycle of one auto-tunnel: start/stop, URL surfacing, and
|
|
50
|
+
* crash-restart backoff.
|
|
51
|
+
*/
|
|
52
|
+
export declare class TunnelManager {
|
|
53
|
+
private readonly factory;
|
|
54
|
+
private readonly ensureBinary;
|
|
55
|
+
private readonly urlTimeoutMs;
|
|
56
|
+
private readonly restartBaseMs;
|
|
57
|
+
private readonly restartMaxMs;
|
|
58
|
+
private readonly timer;
|
|
59
|
+
private phase;
|
|
60
|
+
private url;
|
|
61
|
+
private error;
|
|
62
|
+
private targetUrl;
|
|
63
|
+
private handle;
|
|
64
|
+
private urlTimer;
|
|
65
|
+
private restartTimer;
|
|
66
|
+
private attempts;
|
|
67
|
+
private stopping;
|
|
68
|
+
private readonly urlListeners;
|
|
69
|
+
private readonly phaseListeners;
|
|
70
|
+
/**
|
|
71
|
+
* @param options - seams; defaults spawn the real quick tunnel.
|
|
72
|
+
*/
|
|
73
|
+
constructor(options?: TunnelManagerOptions);
|
|
74
|
+
/** The current status frame. */
|
|
75
|
+
get info(): TunnelInfo;
|
|
76
|
+
/**
|
|
77
|
+
* Start (or keep) a quick tunnel toward `targetUrl`. Restarting with a
|
|
78
|
+
* different target tears the old tunnel down first; restarting with the
|
|
79
|
+
* same target while running is a no-op.
|
|
80
|
+
* @param targetUrl - the local URL to expose, e.g. `http://127.0.0.1:3080`.
|
|
81
|
+
*/
|
|
82
|
+
start(targetUrl: string): void;
|
|
83
|
+
/** Stop the tunnel for good: no restarts, no state. */
|
|
84
|
+
stop(): void;
|
|
85
|
+
/** Alias of {@link stop} for plugin-effect disposal. */
|
|
86
|
+
dispose(): void;
|
|
87
|
+
/** Subscribe to minted tunnel URLs (fire-and-forget duplicates dropped). */
|
|
88
|
+
onUrl(listener: (url: string) => void): () => void;
|
|
89
|
+
/** Subscribe to every phase change. */
|
|
90
|
+
onPhase(listener: (info: TunnelInfo) => void): () => void;
|
|
91
|
+
private attempt;
|
|
92
|
+
private handleUrl;
|
|
93
|
+
private handleExit;
|
|
94
|
+
private fail;
|
|
95
|
+
/** Stop the current process and cancel every pending timer (no phase change). */
|
|
96
|
+
private teardown;
|
|
97
|
+
private setPhase;
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=tunnel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@linxin666/dsh-remote-web-ui",
|
|
3
|
+
"description": "Mobile remote control for the dsh web GUI: scan-to-pair QR entry beside the settings button, one-time pairing tokens, live device status, and revocable mobile sessions",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
8
|
+
},
|
|
9
|
+
"main": "lib/index.js",
|
|
10
|
+
"types": "lib/types/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./lib/types/index.d.ts",
|
|
14
|
+
"default": "./lib/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./invariant": {
|
|
17
|
+
"types": "./lib/types/invariant.d.ts",
|
|
18
|
+
"default": "./lib/invariant.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"types": "./lib/types/client/index.d.ts",
|
|
22
|
+
"default": "./lib/client.js"
|
|
23
|
+
},
|
|
24
|
+
"./src/*": "./src/*",
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"lib/index.js",
|
|
29
|
+
"lib/invariant.js",
|
|
30
|
+
"lib/client.js",
|
|
31
|
+
"lib/types/**/*.d.ts",
|
|
32
|
+
"lib/types/**/*.d.ts.map",
|
|
33
|
+
"src",
|
|
34
|
+
"cordis.patch.yml"
|
|
35
|
+
],
|
|
36
|
+
"license": "BSD-3-Clause",
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"client": {
|
|
42
|
+
"inject": [
|
|
43
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
44
|
+
"@deepseek-ai/dsh-client-connection",
|
|
45
|
+
"@deepseek-ai/dsh-client-ui-settings",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-sidebar"
|
|
47
|
+
],
|
|
48
|
+
"platform": "web"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"dependencies": {
|
|
52
|
+
"cloudflared": "^0.7.3",
|
|
53
|
+
"clsx": "^2.1.1",
|
|
54
|
+
"qrcode.react": "^4.2.0",
|
|
55
|
+
"schemastery": "^3.18.0",
|
|
56
|
+
"zod": "^4.4.3"
|
|
57
|
+
},
|
|
58
|
+
"peerDependencies": {
|
|
59
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
61
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
62
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
63
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
64
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
|
|
65
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
66
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
67
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
68
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
69
|
+
"react": "^18.2.0",
|
|
70
|
+
"react-dom": "^18.2.0"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
74
|
+
"@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
|
|
75
|
+
"@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
|
|
76
|
+
"@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
|
|
77
|
+
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
|
|
78
|
+
"@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
|
|
79
|
+
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
|
|
80
|
+
"@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
|
|
81
|
+
"@deepseek-ai/dsh-host-apiproxy": "^0.1.0-rc.6",
|
|
82
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
83
|
+
"@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
|
|
84
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
85
|
+
"@testing-library/dom": "^10.4.1",
|
|
86
|
+
"@testing-library/react": "^16.3.2",
|
|
87
|
+
"@types/node": "^22.20.0",
|
|
88
|
+
"@types/react": "~18.3.1",
|
|
89
|
+
"@types/react-dom": "~18.3.0",
|
|
90
|
+
"jsdom": "29.1.1",
|
|
91
|
+
"lightningcss": "^1.32.0",
|
|
92
|
+
"react": "^18.2.0",
|
|
93
|
+
"react-dom": "^18.2.0",
|
|
94
|
+
"tsdown": "^0.22.2",
|
|
95
|
+
"typescript": "^6.0.3",
|
|
96
|
+
"vite-tsconfig-paths": "^6.1.1",
|
|
97
|
+
"vitest": "^4.1.8"
|
|
98
|
+
},
|
|
99
|
+
"scripts": {
|
|
100
|
+
"build": "tsc -b && tsdown",
|
|
101
|
+
"test": "vitest run",
|
|
102
|
+
"typecheck": "tsc -b --pretty false"
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-time failed-pairing notice: a fixed toast rendered on the phone after
|
|
3
|
+
* a QR accept failed (invalid/used token or a network error). Mounted by
|
|
4
|
+
* the client apply with a plain React root — no slot machinery for a
|
|
5
|
+
* transient diagnostic.
|
|
6
|
+
*/
|
|
7
|
+
import { useEffect, useState } from 'react'
|
|
8
|
+
import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
|
|
9
|
+
import css from './remote.module.css'
|
|
10
|
+
|
|
11
|
+
/** Notice props: localized copy. */
|
|
12
|
+
export interface PairFailedNoticeProps {
|
|
13
|
+
t: TranslateNS<'remote'>
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render the failed-pair toast (auto-dismisses).
|
|
18
|
+
* @param props - localized copy.
|
|
19
|
+
* @returns the toast element.
|
|
20
|
+
*/
|
|
21
|
+
export function PairFailedNotice({ t }: PairFailedNoticeProps) { const [visible, setVisible] = useState(true)
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
const timer = window.setTimeout(() => { setVisible(false) }, 8000)
|
|
24
|
+
return () => { window.clearTimeout(timer) }
|
|
25
|
+
}, [])
|
|
26
|
+
if (!visible) return null
|
|
27
|
+
return (
|
|
28
|
+
<div className={css.notice} role="alert">
|
|
29
|
+
<p className={css.noticeTitle}>{t('pair.failed.title')}</p>
|
|
30
|
+
<p className={css.noticeDetail}>{t('pair.failed.detail')}</p>
|
|
31
|
+
</div>
|
|
32
|
+
)
|
|
33
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phone glyph for the sidebar remote-control trigger. Not part of the
|
|
3
|
+
* shared icon set (ui-primitives has no phone icon); matches the outline
|
|
4
|
+
* style with currentColor strokes.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Icon props mirroring ui-primitives' IconProps. */
|
|
8
|
+
export interface PhoneIconProps {
|
|
9
|
+
/** Glyph size in px (default 16). */
|
|
10
|
+
size?: number
|
|
11
|
+
/** Extra class for layout placement. */
|
|
12
|
+
className?: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render the phone icon.
|
|
17
|
+
* @param props - size and optional class.
|
|
18
|
+
* @returns the svg element.
|
|
19
|
+
*/
|
|
20
|
+
export function PhoneIcon({ size = 16, className }: PhoneIconProps) {
|
|
21
|
+
return (
|
|
22
|
+
<svg width={size} height={size} className={className} viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
|
23
|
+
<path
|
|
24
|
+
d="M4.2 1.8h2.4l.9 2.4-1.5 1.1a7.4 7.4 0 0 0 4.7 4.7l1.1-1.5 2.4.9v2.4a.9.9 0 0 1-1 .9A11.4 11.4 0 0 1 3.3 2.8a.9.9 0 0 1 .9-1Z"
|
|
25
|
+
stroke="currentColor"
|
|
26
|
+
strokeWidth="1.3"
|
|
27
|
+
strokeLinejoin="round"
|
|
28
|
+
/>
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared chrome for the plugin settings card: a disclosure header naming the
|
|
3
|
+
* plugin and what its settings govern, the controls inside, and the save that
|
|
4
|
+
* writes them. Renders nothing while the namespace is unavailable — a
|
|
5
|
+
* deployment that does not compose the owning plugin should show no trace of
|
|
6
|
+
* it. Mirrors the official ui-plugin-config PluginCard in a self-contained
|
|
7
|
+
* slice (this package must not depend on a sibling UI package).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { useState, type ReactNode } from 'react'
|
|
11
|
+
import type { CardShell } from './settings-form.ts'
|
|
12
|
+
import type { SettingsCardKey } from './locales.ts'
|
|
13
|
+
import css from './settings-card.module.css'
|
|
14
|
+
|
|
15
|
+
/** Card chrome shared by every plugin settings card. */
|
|
16
|
+
export interface PluginSettingsCardProps {
|
|
17
|
+
/** Locale reader for this card's copy. */
|
|
18
|
+
t: (key: SettingsCardKey) => string
|
|
19
|
+
/** Locale key of the plugin's name. */
|
|
20
|
+
titleKey: SettingsCardKey
|
|
21
|
+
/** Locale key of the line describing what this plugin's settings govern. */
|
|
22
|
+
descriptionKey: SettingsCardKey
|
|
23
|
+
/** The card's form state: availability, writability, and what a save would do. */
|
|
24
|
+
state: CardShell
|
|
25
|
+
/** Write every staged edit. */
|
|
26
|
+
onSave: () => void
|
|
27
|
+
/** Drop every staged edit. */
|
|
28
|
+
onDiscard: () => void
|
|
29
|
+
/** The plugin's controls. */
|
|
30
|
+
children: ReactNode
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Render one plugin settings card.
|
|
35
|
+
* @param props - the plugin's copy keys, its form state, and its controls.
|
|
36
|
+
* @returns the card, or nothing when the namespace is unavailable.
|
|
37
|
+
*/
|
|
38
|
+
export function PluginSettingsCard(props: PluginSettingsCardProps) {
|
|
39
|
+
const [open, setOpen] = useState(false)
|
|
40
|
+
const { state } = props
|
|
41
|
+
if (!state.available) return null
|
|
42
|
+
const title = props.t(props.titleKey)
|
|
43
|
+
const blocked = !state.dirty || state.invalid || state.saving
|
|
44
|
+
return (
|
|
45
|
+
<li className={css.card}>
|
|
46
|
+
<button
|
|
47
|
+
type="button"
|
|
48
|
+
className={css.header}
|
|
49
|
+
aria-expanded={open}
|
|
50
|
+
aria-label={`${props.t(open ? 'settings.collapse' : 'settings.expand')}: ${title}`}
|
|
51
|
+
onClick={() => { setOpen(!open) }}
|
|
52
|
+
>
|
|
53
|
+
<span className={css.headText}>
|
|
54
|
+
<span className={css.name}>{title}</span>
|
|
55
|
+
<span className={css.description}>{props.t(props.descriptionKey)}</span>
|
|
56
|
+
</span>
|
|
57
|
+
{state.dirty ? <span className={css.pending}>{props.t('settings.unsaved')}</span> : null}
|
|
58
|
+
<span className={open ? css.chevronOpen : css.chevron}>▾</span>
|
|
59
|
+
</button>
|
|
60
|
+
{open
|
|
61
|
+
? (
|
|
62
|
+
<div className={css.body}>
|
|
63
|
+
{!state.writable ? <p className={css.readOnly} role="status">{props.t('settings.readOnly')}</p> : null}
|
|
64
|
+
{props.children}
|
|
65
|
+
<div className={css.footer}>
|
|
66
|
+
{state.failed ? <p className={css.failed} role="status">{props.t('settings.saveFailed')}</p> : null}
|
|
67
|
+
<button
|
|
68
|
+
type="button"
|
|
69
|
+
className={css.discard}
|
|
70
|
+
disabled={!state.dirty || state.saving}
|
|
71
|
+
onClick={props.onDiscard}
|
|
72
|
+
>
|
|
73
|
+
{props.t('settings.discard')}
|
|
74
|
+
</button>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
className={css.save}
|
|
78
|
+
disabled={blocked}
|
|
79
|
+
onClick={props.onSave}
|
|
80
|
+
>
|
|
81
|
+
{props.t(!state.saving ? 'settings.save' : 'settings.saving')}
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
)
|
|
86
|
+
: null}
|
|
87
|
+
</li>
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Props every field control needs regardless of its value type. */
|
|
92
|
+
export interface FieldProps {
|
|
93
|
+
/** Stable id associating the label with its control. */
|
|
94
|
+
id: string
|
|
95
|
+
/** Visible label. */
|
|
96
|
+
label: string
|
|
97
|
+
/** One-line explanation rendered under the control. */
|
|
98
|
+
hint: string
|
|
99
|
+
/** Draft text this control renders. */
|
|
100
|
+
text: string
|
|
101
|
+
/** True when saving would leave a user-layer entry for this field. */
|
|
102
|
+
overridden: boolean
|
|
103
|
+
/** True when the draft is not a value this field accepts. */
|
|
104
|
+
invalid: boolean
|
|
105
|
+
/** Copy for the overridden badge. */
|
|
106
|
+
overriddenLabel: string
|
|
107
|
+
/** Copy for the reset control. */
|
|
108
|
+
resetLabel: string
|
|
109
|
+
/** Copy shown in place of the hint while the draft is invalid. */
|
|
110
|
+
invalidLabel: string
|
|
111
|
+
/** Disables every control (read-only document, or an unavailable namespace). */
|
|
112
|
+
disabled: boolean
|
|
113
|
+
/** Stage draft text. */
|
|
114
|
+
onEdit: (text: string) => void
|
|
115
|
+
/** Stage a clear so the field re-inherits the composition layer. */
|
|
116
|
+
onReset: () => void
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** A staged value field. `numeric` only hints the keypad: which drafts a field accepts is decided by its spec. */
|
|
120
|
+
export function ValueField(props: FieldProps & {
|
|
121
|
+
/** Hints a numeric keypad without narrowing what the control accepts. */
|
|
122
|
+
numeric?: boolean
|
|
123
|
+
/** Placeholder shown while the draft is empty. */
|
|
124
|
+
placeholder?: string
|
|
125
|
+
}) {
|
|
126
|
+
return (
|
|
127
|
+
<div className={css.field}>
|
|
128
|
+
<div className={css.head}>
|
|
129
|
+
<label className={css.label} htmlFor={props.id}>{props.label}</label>
|
|
130
|
+
{props.overridden
|
|
131
|
+
? (
|
|
132
|
+
<span className={css.badges}>
|
|
133
|
+
<span className={css.badge}>{props.overriddenLabel}</span>
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
className={css.reset}
|
|
137
|
+
disabled={props.disabled}
|
|
138
|
+
onClick={props.onReset}
|
|
139
|
+
>
|
|
140
|
+
{props.resetLabel}
|
|
141
|
+
</button>
|
|
142
|
+
</span>
|
|
143
|
+
)
|
|
144
|
+
: null}
|
|
145
|
+
</div>
|
|
146
|
+
<input
|
|
147
|
+
id={props.id}
|
|
148
|
+
className={props.invalid ? css.inputInvalid : css.input}
|
|
149
|
+
type="text"
|
|
150
|
+
{...props.numeric === true ? { inputMode: 'numeric' as const } : {}}
|
|
151
|
+
{...props.invalid ? { 'aria-invalid': true } : {}}
|
|
152
|
+
value={props.text}
|
|
153
|
+
placeholder={props.placeholder ?? ''}
|
|
154
|
+
disabled={props.disabled}
|
|
155
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
156
|
+
/>
|
|
157
|
+
<p className={props.invalid ? css.invalid : css.hint}>
|
|
158
|
+
{props.invalid ? props.invalidLabel : props.hint}
|
|
159
|
+
</p>
|
|
160
|
+
</div>
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** A staged boolean field: 继承 / 开 / 关. */
|
|
165
|
+
export function BooleanField(props: FieldProps & {
|
|
166
|
+
/** Copy for the inherit option. */
|
|
167
|
+
inheritLabel: string
|
|
168
|
+
/** Copy for the on option. */
|
|
169
|
+
onLabel: string
|
|
170
|
+
/** Copy for the off option. */
|
|
171
|
+
offLabel: string
|
|
172
|
+
}) {
|
|
173
|
+
return (
|
|
174
|
+
<div className={css.field}>
|
|
175
|
+
<div className={css.head}>
|
|
176
|
+
<label className={css.label} htmlFor={props.id}>{props.label}</label>
|
|
177
|
+
{props.overridden
|
|
178
|
+
? (
|
|
179
|
+
<span className={css.badges}>
|
|
180
|
+
<span className={css.badge}>{props.overriddenLabel}</span>
|
|
181
|
+
<button
|
|
182
|
+
type="button"
|
|
183
|
+
className={css.reset}
|
|
184
|
+
disabled={props.disabled}
|
|
185
|
+
onClick={props.onReset}
|
|
186
|
+
>
|
|
187
|
+
{props.resetLabel}
|
|
188
|
+
</button>
|
|
189
|
+
</span>
|
|
190
|
+
)
|
|
191
|
+
: null}
|
|
192
|
+
</div>
|
|
193
|
+
<select
|
|
194
|
+
id={props.id}
|
|
195
|
+
className={css.select}
|
|
196
|
+
value={props.text}
|
|
197
|
+
disabled={props.disabled}
|
|
198
|
+
onChange={(event) => { props.onEdit(event.target.value) }}
|
|
199
|
+
>
|
|
200
|
+
<option value="">{props.inheritLabel}</option>
|
|
201
|
+
<option value="true">{props.onLabel}</option>
|
|
202
|
+
<option value="false">{props.offLabel}</option>
|
|
203
|
+
</select>
|
|
204
|
+
<p className={css.hint}>{props.hint}</p>
|
|
205
|
+
</div>
|
|
206
|
+
)
|
|
207
|
+
}
|