@linxin666/dsh-remote-web-ui 0.1.12 → 0.1.13

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 (64) hide show
  1. package/README.i18n.yaml +6 -0
  2. package/README.md +12 -1
  3. package/README.zh.md +171 -0
  4. package/lib/client.js +169 -98
  5. package/lib/client.js.map +1 -1
  6. package/lib/index.js +224 -4
  7. package/lib/mobile.js +109 -33
  8. package/lib/mobile.js.map +1 -1
  9. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  10. package/lib/types/client/PluginSettingsCard.js +3 -2
  11. package/lib/types/client/RemoteSettingsCard.d.ts +4 -0
  12. package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
  13. package/lib/types/client/RemoteSettingsCard.js +3 -1
  14. package/lib/types/client/UpdateEntry.d.ts.map +1 -1
  15. package/lib/types/client/UpdateEntry.js +10 -2
  16. package/lib/types/client/deep-link.js +10 -5
  17. package/lib/types/client/locales.d.ts +8 -0
  18. package/lib/types/client/locales.d.ts.map +1 -1
  19. package/lib/types/client/locales.js +8 -0
  20. package/lib/types/client/update-api.d.ts +10 -0
  21. package/lib/types/client/update-api.d.ts.map +1 -1
  22. package/lib/types/client/update-api.js +22 -2
  23. package/lib/types/index.d.ts +7 -0
  24. package/lib/types/index.d.ts.map +1 -1
  25. package/lib/types/index.js +7 -1
  26. package/lib/types/mobile/api.d.ts +9 -2
  27. package/lib/types/mobile/api.d.ts.map +1 -1
  28. package/lib/types/mobile/api.js +7 -3
  29. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  30. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  31. package/lib/types/mobile/mobile-styles.js +3 -0
  32. package/lib/types/mobile/views/App.d.ts +3 -1
  33. package/lib/types/mobile/views/App.d.ts.map +1 -1
  34. package/lib/types/mobile/views/App.js +18 -3
  35. package/lib/types/mobile/views/ChatView.d.ts +6 -0
  36. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/ChatView.js +95 -7
  38. package/lib/types/mobile/views/ChatView.test.js +161 -3
  39. package/lib/types/mobile/views/WorkspaceView.d.ts +3 -1
  40. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  41. package/lib/types/mobile/views/WorkspaceView.js +9 -2
  42. package/lib/types/mobile/views/WorkspaceView.test.d.ts +2 -0
  43. package/lib/types/mobile/views/WorkspaceView.test.d.ts.map +1 -0
  44. package/lib/types/mobile/views/WorkspaceView.test.js +56 -0
  45. package/lib/types/mobile-api.d.ts +2 -0
  46. package/lib/types/mobile-api.d.ts.map +1 -1
  47. package/lib/types/mobile-api.js +17 -2
  48. package/package.json +1 -11
  49. package/src/client/PluginSettingsCard.tsx +30 -5
  50. package/src/client/RemoteSettingsCard.tsx +18 -0
  51. package/src/client/UpdateEntry.tsx +10 -2
  52. package/src/client/deep-link.ts +10 -4
  53. package/src/client/locales.ts +8 -0
  54. package/src/client/settings-card.module.css +139 -144
  55. package/src/client/update-api.ts +22 -2
  56. package/src/index.ts +14 -1
  57. package/src/mobile/api.ts +14 -2
  58. package/src/mobile/mobile-styles.ts +3 -0
  59. package/src/mobile/views/App.tsx +22 -3
  60. package/src/mobile/views/ChatView.test.tsx +185 -3
  61. package/src/mobile/views/ChatView.tsx +102 -8
  62. package/src/mobile/views/WorkspaceView.test.tsx +66 -0
  63. package/src/mobile/views/WorkspaceView.tsx +11 -2
  64. package/src/mobile-api.ts +20 -2
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@linxin666/dsh-remote-web-ui",
3
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.12",
4
+ "version": "0.1.13",
5
5
  "type": "module",
6
6
  "engines": {
7
7
  "node": "^22.19.0 || >=24.0.0"
@@ -55,16 +55,6 @@
55
55
  "zod": "^4.4.3"
56
56
  },
57
57
  "peerDependencies": {
58
- "@deepseek-ai/dsh-client-connection": "^0.1.0-rc.6",
59
- "@deepseek-ai/dsh-client-locale": "^0.1.0-rc.6",
60
- "@deepseek-ai/dsh-client-runtime": "^0.1.0-rc.6",
61
- "@deepseek-ai/dsh-client-ui-primitives": "^0.1.0-rc.6",
62
- "@deepseek-ai/dsh-client-ui-settings": "^0.1.0-rc.6",
63
- "@deepseek-ai/dsh-client-ui-sidebar": "^0.1.0-rc.6",
64
- "@deepseek-ai/dsh-client-ui-slots": "^0.1.0-rc.6",
65
- "@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
66
- "@deepseek-ai/dsh-invariants": "^0.1.0-rc.6",
67
- "@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
68
58
  "react": "^18.2.0",
69
59
  "react-dom": "^18.2.0"
70
60
  },
@@ -41,13 +41,14 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
41
41
  if (!state.available) return null
42
42
  const title = props.t(props.titleKey)
43
43
  const blocked = !state.dirty || state.invalid || state.saving
44
+ const cardClass = open ? `${css.cardOpen} ${css.card}` : css.card
44
45
  // The namespace exists but the Host does not serve it to this client (the
45
46
  // official settings allowlist omits third-party namespaces): show a card
46
47
  // that explains the gap instead of vanishing, so a missing card never
47
48
  // reads as a missing plugin.
48
49
  if (!state.exposed) {
49
50
  return (
50
- <li className={css.card}>
51
+ <li className={cardClass}>
51
52
  <button
52
53
  type="button"
53
54
  className={css.header}
@@ -59,7 +60,19 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
59
60
  <span className={css.name}>{title}</span>
60
61
  <span className={css.description}>{props.t(props.descriptionKey)}</span>
61
62
  </span>
62
- <span className={open ? css.chevronOpen : css.chevron}>▾</span>
63
+ <svg
64
+ width="14"
65
+ height="14"
66
+ viewBox="0 0 14 14"
67
+ fill="none"
68
+ xmlns="http://www.w3.org/2000/svg"
69
+ className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
70
+ >
71
+ <path
72
+ d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
73
+ fill="currentColor"
74
+ />
75
+ </svg>
63
76
  </button>
64
77
  {open
65
78
  ? (
@@ -72,7 +85,7 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
72
85
  )
73
86
  }
74
87
  return (
75
- <li className={css.card}>
88
+ <li className={cardClass}>
76
89
  <button
77
90
  type="button"
78
91
  className={css.header}
@@ -85,7 +98,19 @@ export function PluginSettingsCard(props: PluginSettingsCardProps) {
85
98
  <span className={css.description}>{props.t(props.descriptionKey)}</span>
86
99
  </span>
87
100
  {state.dirty ? <span className={css.pending}>{props.t('settings.unsaved')}</span> : null}
88
- <span className={open ? css.chevronOpen : css.chevron}>▾</span>
101
+ <svg
102
+ width="14"
103
+ height="14"
104
+ viewBox="0 0 14 14"
105
+ fill="none"
106
+ xmlns="http://www.w3.org/2000/svg"
107
+ className={open ? `${css.chevron} ${css.chevronOpen}` : css.chevron}
108
+ >
109
+ <path
110
+ d="M11.8486 5.5L11.4238 5.92383L8.69727 8.65137C8.44157 8.90706 8.21562 9.13382 8.01172 9.29785C7.79912 9.46883 7.55595 9.61756 7.25 9.66602C7.08435 9.69222 6.91565 9.69222 6.75 9.66602C6.44405 9.61756 6.20088 9.46883 5.98828 9.29785C5.78438 9.13382 5.55843 8.90706 5.30273 8.65137L2.57617 5.92383L2.15137 5.5L3 4.65137L3.42383 5.07617L6.15137 7.80273C6.42595 8.07732 6.59876 8.24849 6.74023 8.3623C6.87291 8.46904 6.92272 8.47813 6.9375 8.48047C6.97895 8.48703 7.02105 8.48703 7.0625 8.48047C7.07728 8.47813 7.12709 8.46904 7.25977 8.3623C7.40124 8.24849 7.57405 8.07732 7.84863 7.80273L10.5762 5.07617L11 4.65137L11.8486 5.5Z"
111
+ fill="currentColor"
112
+ />
113
+ </svg>
89
114
  </button>
90
115
  {open
91
116
  ? (
@@ -234,4 +259,4 @@ export function BooleanField(props: FieldProps & {
234
259
  <p className={css.hint}>{props.hint}</p>
235
260
  </div>
236
261
  )
237
- }
262
+ }
@@ -27,6 +27,8 @@ export interface RemoteSettings {
27
27
  publicBaseUrl?: string
28
28
  /** When on, the plugin runs its own Cloudflare quick tunnel automatically. */
29
29
  autoTunnel?: boolean
30
+ /** Mobile composer: plain Enter sends; off means Enter inserts a newline. */
31
+ mobileEnterToSend?: boolean
30
32
  }
31
33
 
32
34
  /** What the remote-control card renders. */
@@ -47,6 +49,8 @@ export interface RemoteSettingsCardState extends CardShell {
47
49
  publicBaseUrl: CardFieldState
48
50
  /** Auto public tunnel switch. */
49
51
  autoTunnel: CardFieldState
52
+ /** Mobile composer Enter-to-send switch. */
53
+ mobileEnterToSend: CardFieldState
50
54
  }
51
55
 
52
56
  /** The registration-side face the card's slot entry injects. */
@@ -73,6 +77,7 @@ export class RemoteSettingsCardController {
73
77
  booleanField('requirePairingForLan'),
74
78
  textField('publicBaseUrl'),
75
79
  booleanField('autoTunnel'),
80
+ booleanField('mobileEnterToSend'),
76
81
  ])
77
82
  this.store = this.form.bind(() => this.projection())
78
83
  }
@@ -88,6 +93,7 @@ export class RemoteSettingsCardController {
88
93
  requirePairingForLan: this.form.field('requirePairingForLan'),
89
94
  publicBaseUrl: this.form.field('publicBaseUrl'),
90
95
  autoTunnel: this.form.field('autoTunnel'),
96
+ mobileEnterToSend: this.form.field('mobileEnterToSend'),
91
97
  }
92
98
  }
93
99
 
@@ -215,6 +221,18 @@ export function RemoteSettingsCard(props: RemoteSettingsCardProps) {
215
221
  onEdit={(text) => { props.edit('autoTunnel', text) }}
216
222
  onReset={() => { props.resetField('autoTunnel') }}
217
223
  />
224
+ <BooleanField
225
+ id="settings-remote-mobile-enter"
226
+ label={t('settings.mobileEnterToSend')}
227
+ hint={t('settings.mobileEnterToSendHint')}
228
+ inheritLabel={t('settings.inherit')}
229
+ onLabel={t('settings.on')}
230
+ offLabel={t('settings.off')}
231
+ {...fieldProps}
232
+ {...state.mobileEnterToSend}
233
+ onEdit={(text) => { props.edit('mobileEnterToSend', text) }}
234
+ onReset={() => { props.resetField('mobileEnterToSend') }}
235
+ />
218
236
  </PluginSettingsCard>
219
237
  )
220
238
  }
@@ -10,7 +10,7 @@ import { createPortal } from "react-dom"
10
10
  import type { TranslateNS } from '@deepseek-ai/dsh-client-ui-slots'
11
11
  import { IconDownloadOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
12
12
  import type { UpdateStatus } from '../update.ts'
13
- import { fetchUpdateStatus, runUpdate } from "./update-api.ts"
13
+ import { fetchUpdateStatus, runUpdate, UpdateStatusError } from "./update-api.ts"
14
14
  import { UpdatePanel, type UpdateView } from "./UpdatePanel.tsx"
15
15
  import css from "./remote.module.css"
16
16
 
@@ -36,7 +36,15 @@ export function UpdateEntry({ wide, t }: UpdateEntryProps) {
36
36
  let status: UpdateStatus
37
37
  try {
38
38
  status = await fetchUpdateStatus()
39
- } catch {
39
+ } catch (error) {
40
+ // HTTP 404: the update route is not mounted — the host process runs an
41
+ // older plugin build (client refreshed, host did not). Restarting dsh
42
+ // web loads the new plugin; a plain network failure gets the generic
43
+ // offline copy instead of a misleading "cannot reach update source".
44
+ if (error instanceof UpdateStatusError && error.status === 404) {
45
+ setView({ kind: "error", message: t("update.unmounted"), detail: t("update.unmountedDetail") })
46
+ return
47
+ }
40
48
  setView({ kind: "error", message: t("update.offline"), detail: t("update.offlineDetail") })
41
49
  return
42
50
  }
@@ -74,7 +74,7 @@ export function runPairBootFlow(ctx: Context, search: string, page: PageSurface
74
74
  }
75
75
  }
76
76
 
77
- /** Accept the token, then reload (the workspace param survives the reload). */
77
+ /** Accept the token, then enter the matching desktop/mobile surface. */
78
78
  async function runAccept(token: string, page: PageSurface): Promise<void> {
79
79
  let ok = false
80
80
  try {
@@ -91,9 +91,15 @@ async function runAccept(token: string, page: PageSurface): Promise<void> {
91
91
  page.replaceState(`${url.pathname}${url.search}${url.hash}`)
92
92
  if (ok) {
93
93
  // Phones land on the standalone simplified surface (the full desktop UI
94
- // is not built for small screens); desktops stay on the full UI.
95
- if (isMobileSurface()) page.navigate('/m')
96
- else page.reload()
94
+ // is not built for small screens). Keep the workspace target so the
95
+ // mobile surface can open the intended workspace instead of losing the
96
+ // QR context at this navigation boundary.
97
+ if (isMobileSurface()) {
98
+ url.pathname = '/m'
99
+ page.navigate(`${url.pathname}${url.search}${url.hash}`)
100
+ } else {
101
+ page.reload()
102
+ }
97
103
  }
98
104
  }
99
105
 
@@ -55,6 +55,8 @@ export const zh = {
55
55
  'settings.publicBaseUrlHint': '内网穿透的公网地址,如 Cloudflare Tunnel:https://xxx.trycloudflare.com。填写后二维码将生成公网链接,手机不在同一网络也能配对;留空则仅局域网可用。',
56
56
  'settings.autoTunnel': '自动公网隧道',
57
57
  'settings.autoTunnelHint': '开启后插件自动启动 Cloudflare quick tunnel(无需安装任何工具),并自动更新公网地址与信任配置,手机随时可用公网配对;关闭时使用上方手动填写的公网地址。',
58
+ 'settings.mobileEnterToSend': '移动端回车发送',
59
+ 'settings.mobileEnterToSendHint': '开启:移动端输入框按 Enter 直接发送,Shift+Enter 换行;关闭:Enter 换行,仅点发送按钮发送。',
58
60
  'settings.inherit': '继承',
59
61
  'settings.on': '开',
60
62
  'settings.off': '关',
@@ -89,6 +91,8 @@ export const zh = {
89
91
  'update.missingDetail': '无法定位已安装的 dsh-web-ui 聚合包。请先运行 dsh plugin --profile <profile> add @linxin666/dsh-web-ui-all。',
90
92
  'update.offline': '无法连接更新源',
91
93
  'update.offlineDetail': '网络不可用或更新服务异常,请稍后重试。',
94
+ 'update.unmounted': '更新服务未加载',
95
+ 'update.unmountedDetail': '当前 dsh web 进程运行的插件版本不含更新服务,请重启 dsh web(Ctrl+C 后重新运行)后再试。',
92
96
  'update.error': '更新失败',
93
97
  'update.packages': '组件版本',
94
98
  'update.retry': '重新检查',
@@ -162,6 +166,8 @@ export const en = {
162
166
  'settings.publicBaseUrlHint': 'The public URL of a tunnel in front of this server, e.g. a Cloudflare Tunnel: https://xxx.trycloudflare.com. When set, the QR link is built from it so a phone anywhere can pair; leave blank for LAN-only usage.',
163
167
  'settings.autoTunnel': 'Auto public tunnel',
164
168
  'settings.autoTunnelHint': 'When on, the plugin runs its own Cloudflare quick tunnel (no tool installation needed) and keeps the public address and trust config in sync automatically, so a phone anywhere can pair at any time; when off, the manually entered public address above applies.',
169
+ 'settings.mobileEnterToSend': 'Enter to send on mobile',
170
+ 'settings.mobileEnterToSendHint': 'On: Enter in the mobile composer sends immediately and Shift+Enter inserts a newline. Off: Enter inserts a newline and only the send button sends.',
165
171
  'settings.inherit': 'Inherit',
166
172
  'settings.on': 'On',
167
173
  'settings.off': 'Off',
@@ -196,6 +202,8 @@ export const en = {
196
202
  'update.missingDetail': 'Could not locate the installed dsh-web-ui aggregate package. Install it with: dsh plugin --profile <profile> add @linxin666/dsh-web-ui-all',
197
203
  'update.offline': 'Cannot reach the update source',
198
204
  'update.offlineDetail': 'The network is unavailable or the update service is failing; try again later.',
205
+ 'update.unmounted': 'The update service is not loaded',
206
+ 'update.unmountedDetail': 'The running dsh web process loads a plugin build without the update service; restart dsh web (Ctrl+C, then run dsh web again) and retry.',
199
207
  'update.error': 'Update failed',
200
208
  'update.packages': 'Component versions',
201
209
  'update.retry': 'Check again',
@@ -1,76 +1,83 @@
1
+ /* Plugin settings card chrome + staged form fields.
2
+ * Aligned with the official ui-settings-plugins PluginCard / fields CSS:
3
+ * same semantic tokens, radius, typography and states so family cards read
4
+ * as siblings of the built-in Shell / Agent loop / Web search cards. */
5
+
1
6
  .card {
2
- list-style: none;
3
7
  border: 1px solid var(--dsw-alias-border-l2);
4
- border-radius: 8px;
5
8
  background: var(--dsw-alias-bg-layer-3);
6
- overflow: hidden;
9
+ border-radius: 12px;
10
+ list-style: none;
7
11
  transition: border-color 0.16s, background 0.16s;
8
12
  }
9
13
 
14
+ .card:hover {
15
+ border-color: var(--dsw-alias-label-dimmed);
16
+ }
17
+
10
18
  .cardOpen {
11
19
  background: var(--dsw-alias-bg-layer-2);
12
20
  border-color: var(--dsw-alias-label-dimmed);
13
21
  }
14
22
 
15
23
  .header {
16
- display: flex;
17
- align-items: center;
18
- gap: 8px;
24
+ appearance: none;
19
25
  width: 100%;
20
- padding: 10px 14px;
21
- border: 0;
22
- background: transparent;
23
- cursor: pointer;
24
- text-align: left;
25
26
  font: inherit;
26
- border-radius: 8px;
27
- transition: background-color 120ms ease, box-shadow 120ms ease;
28
- }
29
-
30
- .header:hover:not(:disabled) {
31
- background: var(--dsw-alias-interactive-bg-hover);
32
- }
33
-
34
- .header:active:not(:disabled) {
35
- background: var(--dsw-alias-interactive-bg-active);
27
+ color: inherit;
28
+ text-align: left;
29
+ cursor: pointer;
30
+ background: transparent;
31
+ border: 0;
32
+ border-radius: 12px;
33
+ align-items: center;
34
+ gap: 12px;
35
+ padding: 14px 16px;
36
+ display: flex;
36
37
  }
37
38
 
38
39
  .header:focus-visible {
39
- outline: none;
40
- box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);
41
- }
42
-
43
- .header:disabled {
44
- opacity: 0.5;
45
- cursor: default;
40
+ outline: 2px solid var(--dsw-alias-brand-primary);
41
+ outline-offset: -2px;
46
42
  }
47
43
 
48
44
  .headText {
49
- display: flex;
50
45
  flex-direction: column;
51
- gap: 2px;
52
46
  flex: 1;
47
+ gap: 4px;
53
48
  min-width: 0;
49
+ display: flex;
54
50
  }
55
51
 
56
52
  .name {
57
- font-weight: 600;
58
53
  color: var(--dsw-alias-label-primary);
54
+ font-size: 15px;
55
+ font-weight: 600;
56
+ line-height: 1.4;
59
57
  }
60
58
 
61
59
  .description {
62
- font-size: 12px;
63
60
  color: var(--dsw-alias-label-tertiary);
61
+ font-size: 13px;
62
+ line-height: 1.5;
64
63
  }
65
64
 
66
65
  .pending {
67
- font-size: 12px;
68
- color: var(--dsw-alias-state-warn-primary);
66
+ white-space: nowrap;
67
+ background: var(--dsw-alias-bg-module-platform);
68
+ color: var(--dsw-alias-label-secondary);
69
+ border-radius: 999px;
70
+ flex: none;
71
+ padding: 1px 8px;
72
+ font-size: 11px;
73
+ font-weight: 500;
74
+ line-height: 17px;
69
75
  }
70
76
 
71
77
  .chevron {
72
- transition: transform 120ms ease;
73
78
  color: var(--dsw-alias-label-tertiary);
79
+ flex: none;
80
+ transition: transform 0.16s;
74
81
  }
75
82
 
76
83
  .chevronOpen {
@@ -78,52 +85,59 @@
78
85
  }
79
86
 
80
87
  .body {
81
- padding: 0 14px 14px;
82
- display: flex;
83
- flex-direction: column;
84
- gap: 14px;
88
+ border-top: 1px solid var(--dsw-alias-border-l2);
89
+ margin: 0 16px;
90
+ padding-bottom: 8px;
85
91
  }
86
92
 
87
93
  .readOnly {
88
- margin: 0;
89
- font-size: 12px;
90
94
  color: var(--dsw-alias-label-tertiary);
95
+ margin: 12px 0 0;
96
+ font-size: 12px;
97
+ line-height: 1.5;
91
98
  }
92
99
 
93
100
  .notExposed {
94
- margin: 0;
101
+ color: var(--dsw-alias-state-warn-primary);
102
+ margin: 12px 0 0;
95
103
  font-size: 12px;
96
104
  line-height: 1.5;
97
- color: var(--dsw-alias-state-warn-primary);
98
105
  }
99
106
 
100
107
  .footer {
101
- display: flex;
102
- align-items: center;
108
+ border-top: 1px solid var(--dsw-alias-border-l2);
103
109
  justify-content: flex-end;
110
+ align-items: center;
104
111
  gap: 8px;
112
+ padding: 12px 0 4px;
113
+ display: flex;
105
114
  }
106
115
 
107
116
  .failed {
108
- margin: 0 auto 0 0;
117
+ min-width: 0;
118
+ color: var(--dsw-alias-label-error);
119
+ flex: 1;
120
+ margin: 0;
109
121
  font-size: 12px;
110
- color: var(--dsw-alias-state-error-primary);
122
+ line-height: 1.5;
111
123
  }
112
124
 
113
125
  .discard,
114
126
  .save {
115
- padding: 5px 12px;
116
- border-radius: 6px;
127
+ appearance: none;
117
128
  font: inherit;
118
- font-size: 13px;
119
129
  cursor: pointer;
120
- transition: background-color 120ms ease, border-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
130
+ border: 1px solid transparent;
131
+ border-radius: 8px;
132
+ padding: 5px 14px;
133
+ font-size: 13px;
134
+ line-height: 1.5;
121
135
  }
122
136
 
123
137
  .discard {
124
- border: 1px solid var(--dsw-alias-border-l2);
125
- background: transparent;
138
+ border-color: var(--dsw-alias-border-l2);
126
139
  color: var(--dsw-alias-label-secondary);
140
+ background: transparent;
127
141
  }
128
142
 
129
143
  .discard:hover:not(:disabled) {
@@ -131,173 +145,154 @@
131
145
  border-color: var(--dsw-alias-label-dimmed);
132
146
  }
133
147
 
134
- .discard:active:not(:disabled) {
135
- background: var(--dsw-alias-interactive-bg-active);
136
- }
137
-
138
- .discard:focus-visible {
139
- outline: none;
140
- box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);
141
- }
142
-
143
148
  .save {
144
- border: 1px solid var(--dsw-alias-button-info-fill);
145
- background: var(--dsw-alias-button-info-fill);
146
- color: var(--dsw-alias-label-primary-foreground);
147
- }
148
-
149
- .save:hover:not(:disabled) {
150
- border-color: var(--dsw-alias-button-info-hover);
151
- background: var(--dsw-alias-button-info-hover);
152
- }
153
-
154
- .save:active:not(:disabled) {
155
- border-color: var(--dsw-alias-button-info-hover);
156
- background: var(--dsw-alias-button-info-hover);
157
- filter: brightness(0.96);
158
- }
159
-
160
- .save:focus-visible {
161
- outline: none;
162
- box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);
149
+ background: var(--dsw-alias-label-primary);
150
+ color: var(--dsw-alias-bg-layer-3);
163
151
  }
164
152
 
165
153
  .discard:disabled,
166
154
  .save:disabled {
167
- opacity: 0.5;
155
+ opacity: 0.4;
168
156
  cursor: default;
169
157
  }
170
158
 
159
+ .discard:focus-visible,
160
+ .save:focus-visible {
161
+ outline: 2px solid var(--dsw-alias-brand-primary);
162
+ outline-offset: 1px;
163
+ }
164
+
171
165
  .field {
172
- display: flex;
173
166
  flex-direction: column;
174
- gap: 4px;
167
+ gap: 6px;
168
+ padding: 12px 0;
169
+ display: flex;
170
+ }
171
+
172
+ .field + .field {
173
+ border-top: 1px solid var(--dsw-alias-border-l2);
175
174
  }
176
175
 
177
176
  .head {
178
- display: flex;
179
177
  align-items: center;
180
178
  gap: 8px;
179
+ display: flex;
181
180
  }
182
181
 
183
182
  .label {
183
+ min-width: 0;
184
+ color: var(--dsw-alias-label-primary);
185
+ flex: 1;
184
186
  font-size: 13px;
185
187
  font-weight: 500;
186
- color: var(--dsw-alias-label-primary);
188
+ line-height: 1.5;
187
189
  }
188
190
 
189
191
  .badges {
190
- display: flex;
191
192
  align-items: center;
192
- gap: 6px;
193
+ gap: 8px;
194
+ display: inline-flex;
193
195
  }
194
196
 
195
197
  .badge {
196
- flex: none;
197
- min-width: 0;
198
- font-size: 11px;
199
- padding: 1px 6px;
200
- border-radius: 999px;
201
- background: var(--dsw-alias-interactive-bg-hover-accent);
202
- color: var(--dsw-alias-state-business-primary);
203
198
  white-space: nowrap;
199
+ background: var(--dsw-alias-bg-module-platform);
200
+ color: var(--dsw-alias-label-secondary);
201
+ border-radius: 999px;
202
+ padding: 1px 8px;
203
+ font-size: 11px;
204
+ font-weight: 500;
205
+ line-height: 17px;
204
206
  }
205
207
 
206
208
  .reset {
207
- flex: none;
208
- font-size: 11px;
209
- border: 0;
210
- border-radius: 4px;
211
- background: transparent;
212
- color: var(--dsw-alias-state-business-primary);
209
+ font: inherit;
210
+ color: var(--dsw-alias-label-secondary);
213
211
  cursor: pointer;
214
- padding: 1px 2px;
215
- transition: background-color 120ms ease, color 120ms ease, box-shadow 120ms ease;
212
+ background: transparent;
213
+ border: none;
214
+ padding: 0;
215
+ font-size: 12px;
216
+ line-height: 1.5;
216
217
  }
217
218
 
218
219
  .reset:hover:not(:disabled) {
219
220
  color: var(--dsw-alias-label-primary);
220
- background: var(--dsw-alias-interactive-bg-hover);
221
221
  }
222
222
 
223
- .reset:active:not(:disabled) {
224
- background: var(--dsw-alias-interactive-bg-active);
223
+ .reset:disabled {
224
+ cursor: default;
225
225
  }
226
226
 
227
227
  .reset:focus-visible {
228
- outline: none;
229
- box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);
230
- }
231
-
232
- .reset:disabled {
233
- opacity: 0.5;
234
- cursor: default;
228
+ outline: 2px solid var(--dsw-alias-brand-primary);
229
+ outline-offset: 2px;
235
230
  }
236
231
 
237
232
  .input,
238
233
  .select {
239
- padding: 6px 8px;
240
234
  border: 1px solid var(--dsw-alias-border-l2);
241
- border-radius: 6px;
235
+ background: var(--dsw-alias-bg-layer-3);
236
+ height: 34px;
242
237
  font: inherit;
243
- font-size: 13px;
244
238
  color: var(--dsw-alias-label-primary);
245
- background: var(--dsw-specific-input-major);
246
- transition: border-color 120ms ease, box-shadow 120ms ease;
239
+ border-radius: 8px;
240
+ padding: 0 12px;
241
+ font-size: 13px;
242
+ line-height: 1.5;
247
243
  }
248
244
 
249
245
  .input:focus-visible,
250
246
  .select:focus-visible {
251
- outline: none;
252
247
  border-color: var(--dsw-alias-brand-primary);
253
- box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-brand-primary);
248
+ outline: none;
249
+ }
250
+
251
+ .input:disabled,
252
+ .select:disabled {
253
+ color: var(--dsw-alias-label-tertiary);
254
+ cursor: default;
254
255
  }
255
256
 
256
257
  .inputInvalid {
257
- padding: 6px 8px;
258
- border: 1px solid var(--dsw-alias-state-error-primary);
259
- border-radius: 6px;
258
+ border: 1px solid var(--dsw-alias-label-error);
259
+ background: var(--dsw-alias-bg-layer-3);
260
+ height: 34px;
260
261
  font: inherit;
261
- font-size: 13px;
262
262
  color: var(--dsw-alias-label-primary);
263
- transition: border-color 120ms ease, box-shadow 120ms ease;
263
+ border-radius: 8px;
264
+ padding: 0 12px;
265
+ font-size: 13px;
266
+ line-height: 1.5;
264
267
  }
265
268
 
266
269
  .inputInvalid:focus-visible {
267
- outline: none;
268
- border-color: var(--dsw-alias-state-error-primary);
269
- box-shadow: 0 0 0 2px var(--dsw-alias-bg-layer-3), 0 0 0 4px var(--dsw-alias-state-error-primary);
270
+ outline: 2px solid var(--dsw-alias-label-error);
271
+ outline-offset: 1px;
272
+ border-color: var(--dsw-alias-label-error);
270
273
  }
271
274
 
272
- .input:disabled,
273
- .select:disabled {
274
- opacity: 0.6;
275
- }
276
-
277
- .hint {
275
+ .invalid {
276
+ color: var(--dsw-alias-label-error);
278
277
  margin: 0;
279
278
  font-size: 12px;
280
- color: var(--dsw-alias-label-secondary);
279
+ line-height: 1.5;
281
280
  }
282
281
 
283
- .invalid {
282
+ .hint {
283
+ color: var(--dsw-alias-label-tertiary);
284
284
  margin: 0;
285
285
  font-size: 12px;
286
- color: var(--dsw-alias-state-error-primary);
286
+ line-height: 1.5;
287
287
  }
288
288
 
289
- /* Accessibility: collapse the interaction transitions for reduced-motion
290
- users; the card chrome itself stays static. */
291
289
  @media (prefers-reduced-motion: reduce) {
292
290
  .card,
293
291
  .header,
294
292
  .chevron,
293
+ .chevronOpen,
295
294
  .discard,
296
- .save,
297
- .reset,
298
- .input,
299
- .select,
300
- .inputInvalid {
295
+ .save {
301
296
  transition: none;
302
297
  }
303
298
  }