@linxin666/dsh-pet 0.1.11 → 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.
@@ -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
+ }
@@ -0,0 +1,138 @@
1
+ // @vitest-environment jsdom
2
+ /**
3
+ * WhalePet rename-box keyboard handling. The rename input must treat
4
+ * Enter/Escape keydowns that arrive during IME composition (candidate
5
+ * selection) as composition input, never as submit/cancel (issue #89).
6
+ */
7
+ import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
8
+ import { cleanup, fireEvent, render, screen } from '@testing-library/react'
9
+ import { WhalePet, type WhalePetProps } from './WhalePet.tsx'
10
+ import { t } from './locales.ts'
11
+ import type { PetStateView } from '../service.ts'
12
+
13
+ /** Snapshot fixture: idle whale girl named 泡泡. */
14
+ const snapshot: PetStateView = {
15
+ animation: 'idle',
16
+ phase: 'idle',
17
+ sessionActive: true,
18
+ affinity: {
19
+ points: 0,
20
+ rank: '幼鲸',
21
+ rankEmoji: '*',
22
+ pets: 0,
23
+ feeds: 0,
24
+ turns: 0,
25
+ petCooldown: false,
26
+ feedCooldown: false,
27
+ },
28
+ display: { visible: true, size: 160, right: 24, bottom: 20 },
29
+ name: '泡泡',
30
+ treats: { stocked: 3, max: 5 },
31
+ }
32
+
33
+ beforeAll(() => {
34
+ // Deterministic zh copy for button labels.
35
+ document.documentElement.lang = 'zh'
36
+ // Prefer-reduced-motion matches: the sprite loop then never schedules
37
+ // requestAnimationFrame, keeping the test free of animation timers.
38
+ Object.defineProperty(window, 'matchMedia', {
39
+ configurable: true,
40
+ value: (query: string) => ({
41
+ matches: query.includes('prefers-reduced-motion'),
42
+ media: query,
43
+ onchange: null,
44
+ addEventListener: () => {},
45
+ removeEventListener: () => {},
46
+ addListener: () => {},
47
+ removeListener: () => {},
48
+ dispatchEvent: () => false,
49
+ }),
50
+ })
51
+ })
52
+
53
+ afterEach(() => {
54
+ cleanup()
55
+ })
56
+
57
+ /** Render the pet with mocked callbacks; returns the rename spy. */
58
+ function renderPet(): { onRename: ReturnType<typeof vi.fn> } {
59
+ const onRename = vi.fn()
60
+ const props: WhalePetProps = {
61
+ snapshot,
62
+ display: snapshot.display,
63
+ feedback: null,
64
+ onPet: vi.fn(),
65
+ onFeed: vi.fn(),
66
+ onHide: vi.fn(),
67
+ onDragEnd: vi.fn(),
68
+ onRename,
69
+ onFeedbackDone: vi.fn(),
70
+ t,
71
+ }
72
+ render(<WhalePet {...props} />)
73
+ return { onRename }
74
+ }
75
+
76
+ /** Hover the sprite to open the panel, then click the rename button. */
77
+ function openRename(): HTMLInputElement {
78
+ fireEvent.pointerOver(screen.getByRole('button', { name: 'whale girl' }))
79
+ fireEvent.click(screen.getByText('改名'))
80
+ return screen.getByPlaceholderText('输入新名字') as HTMLInputElement
81
+ }
82
+
83
+ /**
84
+ * Fire a keydown whose native event reports an active IME composition, the
85
+ * way Chromium marks Enter/Escape pressed to select or dismiss a candidate.
86
+ */
87
+ function fireComposingKeydown(target: Element, key: string): void {
88
+ fireEvent.compositionStart(target)
89
+ const native = new window.KeyboardEvent('keydown', {
90
+ key,
91
+ bubbles: true,
92
+ cancelable: true,
93
+ isComposing: true,
94
+ })
95
+ // jsdom does not implement KeyboardEvent.isComposing, so pin the flag on
96
+ // the dispatched native event exactly as the browser would report it.
97
+ Object.defineProperty(native, 'isComposing', { value: true })
98
+ fireEvent(target, native)
99
+ fireEvent.compositionEnd(target)
100
+ }
101
+
102
+ describe('WhalePet rename input', () => {
103
+ it('submits the draft on Enter outside composition', () => {
104
+ const { onRename } = renderPet()
105
+ const input = openRename()
106
+ fireEvent.change(input, { target: { value: ' 小鲸 ' } })
107
+ fireEvent.keyDown(input, { key: 'Enter' })
108
+ expect(onRename).toHaveBeenCalledTimes(1)
109
+ expect(onRename).toHaveBeenCalledWith('小鲸')
110
+ expect(screen.queryByPlaceholderText('输入新名字')).toBeNull()
111
+ })
112
+
113
+ it('ignores Enter while an IME composition is active', () => {
114
+ const { onRename } = renderPet()
115
+ const input = openRename()
116
+ fireEvent.change(input, { target: { value: '泡泡酱' } })
117
+ fireComposingKeydown(input, 'Enter')
118
+ expect(onRename).not.toHaveBeenCalled()
119
+ expect(screen.getByPlaceholderText('输入新名字')).toBe(input)
120
+ expect(input.value).toBe('泡泡酱')
121
+ // Once the composition is over, Enter submits normally.
122
+ fireEvent.keyDown(input, { key: 'Enter' })
123
+ expect(onRename).toHaveBeenCalledWith('泡泡酱')
124
+ expect(screen.queryByPlaceholderText('输入新名字')).toBeNull()
125
+ })
126
+
127
+ it('ignores Escape while an IME composition is active', () => {
128
+ const { onRename } = renderPet()
129
+ const input = openRename()
130
+ fireEvent.change(input, { target: { value: 'abc' } })
131
+ fireComposingKeydown(input, 'Escape')
132
+ expect(screen.getByPlaceholderText('输入新名字')).toBe(input)
133
+ // A real Escape outside composition closes the box without renaming.
134
+ fireEvent.keyDown(input, { key: 'Escape' })
135
+ expect(onRename).not.toHaveBeenCalled()
136
+ expect(screen.queryByPlaceholderText('输入新名字')).toBeNull()
137
+ })
138
+ })
@@ -301,6 +301,11 @@ export function WhalePet(props: WhalePetProps): ReactPortal {
301
301
  autoFocus
302
302
  onChange={(e) => setNameDraft(e.target.value)}
303
303
  onKeyDown={(e) => {
304
+ // While an IME composition is active (e.g. selecting a
305
+ // Chinese candidate), Enter/Escape keydowns belong to the
306
+ // input method: ignore them so candidate selection can
307
+ // neither submit the draft nor close the rename box.
308
+ if (e.nativeEvent.isComposing) return
304
309
  if (e.key === 'Enter') {
305
310
  const trimmed = nameDraft.trim()
306
311
  if (trimmed !== '') {
@@ -10,7 +10,7 @@
10
10
  * @module @linxin666/dsh-pet/client
11
11
  */
12
12
 
13
- import type { ClientContext } from '@deepseek-ai/dsh-client-runtime/client'
13
+ import type { ClientContext, SettingsScope, SettingsScopeSpec } from '@deepseek-ai/dsh-client-runtime/client'
14
14
  // Type-only: pulls the locale plugin's Context merge (ctx.locale).
15
15
  import type {} from '@deepseek-ai/dsh-client-locale/client'
16
16
  // Type-only: pulls the settings-surface Context merge (ctx.settingsScope).
@@ -92,6 +92,18 @@ export interface SettingsPluginItemOwnerProps {
92
92
  children?: never
93
93
  }
94
94
 
95
+ declare module '@deepseek-ai/cordis' {
96
+ interface Context {
97
+ /**
98
+ * Optional rc.6 compatibility binder provided by dsh-web-ui-settings;
99
+ * absent when that group plugin is not installed, so callers fall back to
100
+ * the official settings scope.
101
+ */
102
+ webUiSettings?: { bind<S>(spec: SettingsScopeSpec<S>): SettingsScope<S> }
103
+ }
104
+ }
105
+
106
+
95
107
  /**
96
108
  * Client plugin body: register dictionaries, mount the global pet entry and
97
109
  * poll loop while the plugin is enabled, and seat the settings card in the
@@ -101,7 +113,8 @@ export interface SettingsPluginItemOwnerProps {
101
113
  export function apply(ctx: ClientContext): void {
102
114
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'pet: dictionaries')
103
115
 
104
- const settingsScope = ctx.settingsScope.bind<PetSettings>({ namespace: PET_SETTINGS_NS })
116
+ const binder = ctx.get('webUiSettings') ?? ctx.settingsScope
117
+ const settingsScope = binder.bind<PetSettings>({ namespace: PET_SETTINGS_NS })
105
118
  const enabled = (): boolean => {
106
119
  const snapshot = settingsScope.getSnapshot()
107
120
  return snapshot.status === 'ready'
@@ -1,71 +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
- transition: background 120ms ease;
27
- }
28
-
29
- .header:hover {
30
- background: var(--dsw-alias-interactive-bg-hover);
31
- }
32
-
33
- .header:active {
34
- background: var(--dsw-alias-interactive-bg-hover-solid);
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;
35
37
  }
36
38
 
37
39
  .header:focus-visible {
38
- outline: none;
39
- /* Inset ring so it is not clipped by the card's overflow:hidden. */
40
- box-shadow: inset 0 0 0 2px var(--dsw-alias-button-info-fill);
40
+ outline: 2px solid var(--dsw-alias-brand-primary);
41
+ outline-offset: -2px;
41
42
  }
42
43
 
43
44
  .headText {
44
- display: flex;
45
45
  flex-direction: column;
46
- gap: 2px;
47
46
  flex: 1;
47
+ gap: 4px;
48
48
  min-width: 0;
49
+ display: flex;
49
50
  }
50
51
 
51
52
  .name {
52
- font-weight: 600;
53
53
  color: var(--dsw-alias-label-primary);
54
+ font-size: 15px;
55
+ font-weight: 600;
56
+ line-height: 1.4;
54
57
  }
55
58
 
56
59
  .description {
57
- font-size: 12px;
58
60
  color: var(--dsw-alias-label-tertiary);
61
+ font-size: 13px;
62
+ line-height: 1.5;
59
63
  }
60
64
 
61
65
  .pending {
62
- font-size: 12px;
63
- 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;
64
75
  }
65
76
 
66
77
  .chevron {
67
- transition: transform 120ms ease;
68
78
  color: var(--dsw-alias-label-tertiary);
79
+ flex: none;
80
+ transition: transform 0.16s;
69
81
  }
70
82
 
71
83
  .chevronOpen {
@@ -73,53 +85,59 @@
73
85
  }
74
86
 
75
87
  .body {
76
- padding: 0 14px 14px;
77
- display: flex;
78
- flex-direction: column;
79
- gap: 14px;
88
+ border-top: 1px solid var(--dsw-alias-border-l2);
89
+ margin: 0 16px;
90
+ padding-bottom: 8px;
80
91
  }
81
92
 
82
93
  .readOnly {
83
- margin: 0;
84
- font-size: 12px;
85
94
  color: var(--dsw-alias-label-tertiary);
95
+ margin: 12px 0 0;
96
+ font-size: 12px;
97
+ line-height: 1.5;
86
98
  }
87
99
 
88
100
  .notExposed {
89
- margin: 0;
101
+ color: var(--dsw-alias-state-warn-primary);
102
+ margin: 12px 0 0;
90
103
  font-size: 12px;
91
104
  line-height: 1.5;
92
- color: var(--dsw-alias-state-warn-primary);
93
105
  }
94
106
 
95
107
  .footer {
96
- display: flex;
97
- align-items: center;
108
+ border-top: 1px solid var(--dsw-alias-border-l2);
98
109
  justify-content: flex-end;
110
+ align-items: center;
99
111
  gap: 8px;
112
+ padding: 12px 0 4px;
113
+ display: flex;
100
114
  }
101
115
 
102
116
  .failed {
103
- margin: 0 auto 0 0;
117
+ min-width: 0;
118
+ color: var(--dsw-alias-label-error);
119
+ flex: 1;
120
+ margin: 0;
104
121
  font-size: 12px;
105
- color: var(--dsw-alias-state-error-primary);
122
+ line-height: 1.5;
106
123
  }
107
124
 
108
125
  .discard,
109
126
  .save {
110
- padding: 5px 12px;
111
- border-radius: 6px;
127
+ appearance: none;
112
128
  font: inherit;
113
- font-size: 13px;
114
129
  cursor: pointer;
115
- transition: color 120ms ease, border-color 120ms ease,
116
- background 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;
117
135
  }
118
136
 
119
137
  .discard {
120
- border: 1px solid var(--dsw-alias-border-l2);
121
- background: transparent;
138
+ border-color: var(--dsw-alias-border-l2);
122
139
  color: var(--dsw-alias-label-secondary);
140
+ background: transparent;
123
141
  }
124
142
 
125
143
  .discard:hover:not(:disabled) {
@@ -127,156 +145,152 @@
127
145
  border-color: var(--dsw-alias-label-dimmed);
128
146
  }
129
147
 
130
- .discard:active:not(:disabled) {
131
- background: var(--dsw-alias-interactive-bg-hover);
132
- }
133
-
134
148
  .save {
135
- border: 1px solid var(--dsw-alias-button-info-fill);
136
- background: var(--dsw-alias-button-info-fill);
137
- color: var(--dsw-alias-label-primary-foreground);
138
- }
139
-
140
- .save:hover:not(:disabled) {
141
- border-color: var(--dsw-alias-button-info-hover);
142
- background: var(--dsw-alias-button-info-hover);
143
- }
144
-
145
- .save:active:not(:disabled) {
146
- filter: brightness(0.94);
147
- }
148
-
149
- .discard:focus-visible:not(:disabled),
150
- .save:focus-visible:not(:disabled) {
151
- outline: none;
152
- box-shadow: 0 0 0 2px var(--dsw-alias-button-info-fill);
149
+ background: var(--dsw-alias-label-primary);
150
+ color: var(--dsw-alias-bg-layer-3);
153
151
  }
154
152
 
155
153
  .discard:disabled,
156
154
  .save:disabled {
157
- opacity: 0.5;
155
+ opacity: 0.4;
158
156
  cursor: default;
159
157
  }
160
158
 
159
+ .discard:focus-visible,
160
+ .save:focus-visible {
161
+ outline: 2px solid var(--dsw-alias-brand-primary);
162
+ outline-offset: 1px;
163
+ }
164
+
161
165
  .field {
162
- display: flex;
163
166
  flex-direction: column;
164
- 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);
165
174
  }
166
175
 
167
176
  .head {
168
- display: flex;
169
177
  align-items: center;
170
178
  gap: 8px;
179
+ display: flex;
171
180
  }
172
181
 
173
182
  .label {
183
+ min-width: 0;
184
+ color: var(--dsw-alias-label-primary);
185
+ flex: 1;
174
186
  font-size: 13px;
175
187
  font-weight: 500;
176
- color: var(--dsw-alias-label-primary);
188
+ line-height: 1.5;
177
189
  }
178
190
 
179
191
  .badges {
180
- display: flex;
181
192
  align-items: center;
182
- gap: 6px;
193
+ gap: 8px;
194
+ display: inline-flex;
183
195
  }
184
196
 
185
197
  .badge {
186
- font-size: 11px;
187
- padding: 1px 6px;
198
+ white-space: nowrap;
199
+ background: var(--dsw-alias-bg-module-platform);
200
+ color: var(--dsw-alias-label-secondary);
188
201
  border-radius: 999px;
189
- background: var(--dsw-alias-interactive-bg-hover-accent);
190
- color: var(--dsw-alias-state-business-primary);
202
+ padding: 1px 8px;
203
+ font-size: 11px;
204
+ font-weight: 500;
205
+ line-height: 17px;
191
206
  }
192
207
 
193
208
  .reset {
194
- font-size: 11px;
195
- border: 0;
196
- background: transparent;
197
- color: var(--dsw-alias-state-business-primary);
209
+ font: inherit;
210
+ color: var(--dsw-alias-label-secondary);
198
211
  cursor: pointer;
212
+ background: transparent;
213
+ border: none;
199
214
  padding: 0;
200
- border-radius: 3px;
201
- transition: color 120ms ease, box-shadow 120ms ease;
215
+ font-size: 12px;
216
+ line-height: 1.5;
202
217
  }
203
218
 
204
219
  .reset:hover:not(:disabled) {
205
- color: var(--dsw-alias-label-primary-bluish);
206
- text-decoration: underline;
207
- }
208
-
209
- .reset:active:not(:disabled) {
210
- color: var(--dsw-alias-state-business-primary);
211
- }
212
-
213
- .reset:focus-visible:not(:disabled) {
214
- outline: none;
215
- box-shadow: 0 0 0 2px var(--dsw-alias-button-info-fill);
220
+ color: var(--dsw-alias-label-primary);
216
221
  }
217
222
 
218
223
  .reset:disabled {
219
- opacity: 0.5;
220
224
  cursor: default;
221
225
  }
222
226
 
227
+ .reset:focus-visible {
228
+ outline: 2px solid var(--dsw-alias-brand-primary);
229
+ outline-offset: 2px;
230
+ }
231
+
223
232
  .input,
224
233
  .select {
225
- padding: 6px 8px;
226
234
  border: 1px solid var(--dsw-alias-border-l2);
227
- border-radius: 6px;
235
+ background: var(--dsw-alias-bg-layer-3);
236
+ height: 34px;
228
237
  font: inherit;
229
- font-size: 13px;
230
238
  color: var(--dsw-alias-label-primary);
231
- background: var(--dsw-specific-input-major);
239
+ border-radius: 8px;
240
+ padding: 0 12px;
241
+ font-size: 13px;
242
+ line-height: 1.5;
232
243
  }
233
244
 
234
- .inputInvalid {
235
- padding: 6px 8px;
236
- border: 1px solid var(--dsw-alias-state-error-primary);
237
- border-radius: 6px;
238
- font: inherit;
239
- font-size: 13px;
240
- color: var(--dsw-alias-label-primary);
245
+ .input:focus-visible,
246
+ .select:focus-visible {
247
+ border-color: var(--dsw-alias-brand-primary);
248
+ outline: none;
241
249
  }
242
250
 
243
251
  .input:disabled,
244
252
  .select:disabled {
245
- opacity: 0.6;
253
+ color: var(--dsw-alias-label-tertiary);
254
+ cursor: default;
246
255
  }
247
256
 
248
- .input:focus,
249
- .select:focus {
250
- outline: none;
251
- border-color: var(--dsw-alias-button-info-fill);
252
- box-shadow: 0 0 0 2px var(--dsw-alias-button-info-fill);
257
+ .inputInvalid {
258
+ border: 1px solid var(--dsw-alias-label-error);
259
+ background: var(--dsw-alias-bg-layer-3);
260
+ height: 34px;
261
+ font: inherit;
262
+ color: var(--dsw-alias-label-primary);
263
+ border-radius: 8px;
264
+ padding: 0 12px;
265
+ font-size: 13px;
266
+ line-height: 1.5;
253
267
  }
254
268
 
255
- .inputInvalid:focus {
256
- outline: none;
257
- box-shadow: 0 0 0 2px var(--dsw-alias-state-error-primary);
269
+ .inputInvalid:focus-visible {
270
+ outline: 2px solid var(--dsw-alias-label-error);
271
+ outline-offset: 1px;
272
+ border-color: var(--dsw-alias-label-error);
258
273
  }
259
274
 
260
- .hint {
275
+ .invalid {
276
+ color: var(--dsw-alias-label-error);
261
277
  margin: 0;
262
278
  font-size: 12px;
263
- color: var(--dsw-alias-label-secondary);
279
+ line-height: 1.5;
264
280
  }
265
281
 
266
- .invalid {
282
+ .hint {
283
+ color: var(--dsw-alias-label-tertiary);
267
284
  margin: 0;
268
285
  font-size: 12px;
269
- color: var(--dsw-alias-state-error-primary);
286
+ line-height: 1.5;
270
287
  }
271
288
 
272
- /* Reduced motion: drop the card/button transitions. Token colors and the
273
- focus rings stay, so interactive states remain visible without motion. */
274
289
  @media (prefers-reduced-motion: reduce) {
275
290
  .card,
276
291
  .header,
277
292
  .chevron,
278
293
  .chevronOpen,
279
- .reset,
280
294
  .discard,
281
295
  .save {
282
296
  transition: none;