@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.
Files changed (118) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +346 -0
  3. package/cordis.patch.yml +10 -0
  4. package/lib/client.js +2817 -0
  5. package/lib/index.js +1455 -0
  6. package/lib/invariant.js +21 -0
  7. package/lib/types/client/PairFailedNotice.d.ts +12 -0
  8. package/lib/types/client/PairFailedNotice.d.ts.map +1 -0
  9. package/lib/types/client/PhoneIcon.d.ts +19 -0
  10. package/lib/types/client/PhoneIcon.d.ts.map +1 -0
  11. package/lib/types/client/PluginSettingsCard.d.ts +78 -0
  12. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -0
  13. package/lib/types/client/RemoteEntry.d.ts +10 -0
  14. package/lib/types/client/RemoteEntry.d.ts.map +1 -0
  15. package/lib/types/client/RemotePanel.d.ts +50 -0
  16. package/lib/types/client/RemotePanel.d.ts.map +1 -0
  17. package/lib/types/client/RemoteSettingsCard.d.ts +75 -0
  18. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -0
  19. package/lib/types/client/deep-link.d.ts +40 -0
  20. package/lib/types/client/deep-link.d.ts.map +1 -0
  21. package/lib/types/client/index.d.ts +54 -0
  22. package/lib/types/client/index.d.ts.map +1 -0
  23. package/lib/types/client/locales.d.ts +144 -0
  24. package/lib/types/client/locales.d.ts.map +1 -0
  25. package/lib/types/client/pair-api.d.ts +100 -0
  26. package/lib/types/client/pair-api.d.ts.map +1 -0
  27. package/lib/types/client/settings-form.d.ts +119 -0
  28. package/lib/types/client/settings-form.d.ts.map +1 -0
  29. package/lib/types/gate.d.ts +58 -0
  30. package/lib/types/gate.d.ts.map +1 -0
  31. package/lib/types/index.d.ts +78 -0
  32. package/lib/types/index.d.ts.map +1 -0
  33. package/lib/types/invariant.d.ts +16 -0
  34. package/lib/types/invariant.d.ts.map +1 -0
  35. package/lib/types/lan.d.ts +15 -0
  36. package/lib/types/lan.d.ts.map +1 -0
  37. package/lib/types/mobile/api.d.ts +64 -0
  38. package/lib/types/mobile/api.d.ts.map +1 -0
  39. package/lib/types/mobile/index.d.ts +10 -0
  40. package/lib/types/mobile/index.d.ts.map +1 -0
  41. package/lib/types/mobile/messages.d.ts +103 -0
  42. package/lib/types/mobile/messages.d.ts.map +1 -0
  43. package/lib/types/mobile/messages.test.d.ts +2 -0
  44. package/lib/types/mobile/messages.test.d.ts.map +1 -0
  45. package/lib/types/mobile/mobile-styles.d.ts +6 -0
  46. package/lib/types/mobile/mobile-styles.d.ts.map +1 -0
  47. package/lib/types/mobile/mobile-theme.d.ts +22 -0
  48. package/lib/types/mobile/mobile-theme.d.ts.map +1 -0
  49. package/lib/types/mobile/mobile-theme.test.d.ts +2 -0
  50. package/lib/types/mobile/mobile-theme.test.d.ts.map +1 -0
  51. package/lib/types/mobile/mux.d.ts +52 -0
  52. package/lib/types/mobile/mux.d.ts.map +1 -0
  53. package/lib/types/mobile/rpc.d.ts +38 -0
  54. package/lib/types/mobile/rpc.d.ts.map +1 -0
  55. package/lib/types/mobile/theme-toggle.d.ts +12 -0
  56. package/lib/types/mobile/theme-toggle.d.ts.map +1 -0
  57. package/lib/types/mobile/views/App.d.ts +59 -0
  58. package/lib/types/mobile/views/App.d.ts.map +1 -0
  59. package/lib/types/mobile/views/ChatView.d.ts +39 -0
  60. package/lib/types/mobile/views/ChatView.d.ts.map +1 -0
  61. package/lib/types/mobile/views/ChatView.test.d.ts +2 -0
  62. package/lib/types/mobile/views/ChatView.test.d.ts.map +1 -0
  63. package/lib/types/mobile/views/SessionListView.d.ts +27 -0
  64. package/lib/types/mobile/views/SessionListView.d.ts.map +1 -0
  65. package/lib/types/mobile/views/SessionListView.test.d.ts +2 -0
  66. package/lib/types/mobile/views/SessionListView.test.d.ts.map +1 -0
  67. package/lib/types/mobile/views/WorkspaceView.d.ts +19 -0
  68. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -0
  69. package/lib/types/mobile-api.d.ts +41 -0
  70. package/lib/types/mobile-api.d.ts.map +1 -0
  71. package/lib/types/mobile-routes.d.ts +16 -0
  72. package/lib/types/mobile-routes.d.ts.map +1 -0
  73. package/lib/types/pairing.d.ts +215 -0
  74. package/lib/types/pairing.d.ts.map +1 -0
  75. package/lib/types/routes.d.ts +58 -0
  76. package/lib/types/routes.d.ts.map +1 -0
  77. package/lib/types/tunnel.d.ts +99 -0
  78. package/lib/types/tunnel.d.ts.map +1 -0
  79. package/package.json +104 -0
  80. package/src/client/PairFailedNotice.tsx +33 -0
  81. package/src/client/PhoneIcon.tsx +31 -0
  82. package/src/client/PluginSettingsCard.tsx +207 -0
  83. package/src/client/RemoteEntry.tsx +202 -0
  84. package/src/client/RemotePanel.tsx +186 -0
  85. package/src/client/RemoteSettingsCard.tsx +220 -0
  86. package/src/client/deep-link.ts +137 -0
  87. package/src/client/index.ts +167 -0
  88. package/src/client/locales.ts +149 -0
  89. package/src/client/pair-api.ts +176 -0
  90. package/src/client/remote.module.css +422 -0
  91. package/src/client/settings-card.module.css +296 -0
  92. package/src/client/settings-form.ts +294 -0
  93. package/src/css-modules.d.ts +6 -0
  94. package/src/gate.ts +121 -0
  95. package/src/index.ts +299 -0
  96. package/src/invariant.ts +32 -0
  97. package/src/lan.ts +21 -0
  98. package/src/mobile/api.ts +106 -0
  99. package/src/mobile/index.tsx +27 -0
  100. package/src/mobile/messages.test.ts +222 -0
  101. package/src/mobile/messages.ts +555 -0
  102. package/src/mobile/mobile-styles.ts +853 -0
  103. package/src/mobile/mobile-theme.test.ts +84 -0
  104. package/src/mobile/mobile-theme.ts +76 -0
  105. package/src/mobile/mux.ts +126 -0
  106. package/src/mobile/rpc.ts +90 -0
  107. package/src/mobile/theme-toggle.tsx +47 -0
  108. package/src/mobile/views/App.tsx +152 -0
  109. package/src/mobile/views/ChatView.test.tsx +243 -0
  110. package/src/mobile/views/ChatView.tsx +650 -0
  111. package/src/mobile/views/SessionListView.test.tsx +139 -0
  112. package/src/mobile/views/SessionListView.tsx +186 -0
  113. package/src/mobile/views/WorkspaceView.tsx +108 -0
  114. package/src/mobile-api.ts +271 -0
  115. package/src/mobile-routes.ts +77 -0
  116. package/src/pairing.ts +390 -0
  117. package/src/routes.ts +346 -0
  118. 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
+ }