dsh-coding-subscription-oauth 0.5.4 → 0.5.5

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-coding-subscription-oauth",
3
3
  "description": "DeepSeek Harness coding-subscription OAuth: SuperGrok/Grok Build, ChatGPT Plus Codex, Kimi Code, Claude Code. Fixes AUTH API key is invalid, INVALID_REPLAY_STATE, grok-4.6 xhigh, Kimi Bearer vs x-api-key.",
4
- "version": "0.5.4",
4
+ "version": "0.5.5",
5
5
  "publishConfig": {
6
6
  "access": "public",
7
7
  "registry": "https://registry.npmjs.org/"
@@ -1,11 +1,12 @@
1
1
  /** Plugin-owned coding subscription account section inside the dsh Settings shell. */
2
2
 
3
- import { useCallback, useEffect, useRef, useState } from "react";
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
4
4
  import { cancelPreviewTicket, copyText, isConflictError, isConsumedPreviewError, jsonRequest } from "./api.ts";
5
5
  import { AboutTab } from "./components/AboutTab.tsx";
6
6
  import { AccountsTab } from "./components/AccountsTab.tsx";
7
7
  import { CapabilitiesTab } from "./components/CapabilitiesTab.tsx";
8
8
  import { GatewayTab } from "./components/GatewayTab.tsx";
9
+ import type { SettingsTabHint } from "./components/SettingsTabs.tsx";
9
10
  import { SettingsTabs } from "./components/SettingsTabs.tsx";
10
11
  import {
11
12
  CAPABILITIES_PATH,
@@ -28,6 +29,7 @@ import {
28
29
  STATUS_PATH,
29
30
  } from "./constants.ts";
30
31
  import { isLikelyRemoteHost } from "./display.ts";
32
+ import { ensureMicroStyles } from "./microStyles.ts";
31
33
  import {
32
34
  emptyCapabilitySettings,
33
35
  mergeSources,
@@ -185,6 +187,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
185
187
 
186
188
  // Accounts: status immediately; sources shortly after (non-blocking for first paint).
187
189
  useEffect(() => {
190
+ ensureMicroStyles();
188
191
  void refresh();
189
192
  const timer = window.setTimeout(() => {
190
193
  void refreshSources();
@@ -441,6 +444,22 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
441
444
 
442
445
  const showUsage = capabilities?.value.codexUsage === true && status?.providers.codex.status === "signed-in";
443
446
 
447
+ const tabHints = useMemo((): readonly SettingsTabHint[] => {
448
+ const hints: SettingsTabHint[] = [];
449
+ if (status !== undefined) {
450
+ const signedInCount = Object.values(status.providers).filter(
451
+ (provider) => provider.status === "signed-in",
452
+ ).length;
453
+ if (signedInCount > 0) {
454
+ hints.push({ id: "accounts", suffix: String(signedInCount) });
455
+ }
456
+ }
457
+ if (gateway?.running === true) {
458
+ hints.push({ id: "gateway", suffix: t("tabGatewayActive") });
459
+ }
460
+ return hints;
461
+ }, [gateway?.running, status, t]);
462
+
444
463
  const markCopied = (field: CopyField): void => {
445
464
  if (copiedTimerRef.current !== undefined) window.clearTimeout(copiedTimerRef.current);
446
465
  setCopiedField(field);
@@ -570,7 +589,7 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
570
589
  {requestError}
571
590
  </p>
572
591
  )}
573
- <SettingsTabs t={t} activeTab={activeTab} onChange={setActiveTab} />
592
+ <SettingsTabs t={t} activeTab={activeTab} onChange={setActiveTab} hints={tabHints} />
574
593
  <div
575
594
  id={`coding-oauth-panel-${activeTab}`}
576
595
  role="tabpanel"
@@ -635,6 +654,9 @@ export function GrokBuildSettings({ t }: GrokBuildSettingsProps) {
635
654
  onRefreshSources={() => {
636
655
  void refreshSources();
637
656
  }}
657
+ onDismissSourcesNotice={() => {
658
+ setSourcesNotice(undefined);
659
+ }}
638
660
  />
639
661
  ) : null}
640
662
  {activeTab === "capabilities" ? (
@@ -5,16 +5,13 @@ import { allOfficialCliMissing, anyOfficialCliAvailable } from "../display.ts";
5
5
  import {
6
6
  accountGridStyle,
7
7
  bodyStyle,
8
- buttonStyle,
9
8
  cardStyle,
10
9
  dotStyle,
11
- errorStyle,
12
10
  hintStyle,
13
11
  monoStyle,
14
12
  rowStyle,
15
13
  skeletonStyle,
16
14
  statusStyle,
17
- tipStyle,
18
15
  titleStyle,
19
16
  } from "../styles.ts";
20
17
  import type {
@@ -26,7 +23,9 @@ import type {
26
23
  SourceStatus,
27
24
  UsageView,
28
25
  } from "../types.ts";
26
+ import { Badge } from "./Badge.tsx";
29
27
  import { CliPullPreview } from "./CliPullPreview.tsx";
28
+ import { NoticeBanner } from "./NoticeBanner.tsx";
30
29
  import { ProviderCard } from "./ProviderCard.tsx";
31
30
 
32
31
  export interface AccountsTabProps {
@@ -61,6 +60,7 @@ export interface AccountsTabProps {
61
60
  onCommitSource: () => void;
62
61
  onCancelSourcePreview: () => void;
63
62
  onRefreshSources: () => void;
63
+ onDismissSourcesNotice: () => void;
64
64
  }
65
65
 
66
66
  export function AccountsTab({
@@ -95,6 +95,7 @@ export function AccountsTab({
95
95
  onCommitSource,
96
96
  onCancelSourcePreview,
97
97
  onRefreshSources,
98
+ onDismissSourcesNotice,
98
99
  }: AccountsTabProps) {
99
100
  if (status === undefined) {
100
101
  return (
@@ -110,37 +111,35 @@ export function AccountsTab({
110
111
  return (
111
112
  <>
112
113
  {remote && !remoteTipDismissed ? (
113
- <div
114
- style={{ ...tipStyle, display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12 }}
115
- >
116
- <p style={{ ...bodyStyle, margin: 0, color: "var(--dsw-alias-label-primary)" }}>{t("remoteAccountsTip")}</p>
117
- <button type="button" style={buttonStyle} onClick={onDismissRemoteTip}>
118
- {t("remoteTipDismiss")}
119
- </button>
120
- </div>
114
+ <NoticeBanner
115
+ message={t("remoteAccountsTip")}
116
+ dismissLabel={t("remoteTipDismiss")}
117
+ onDismiss={onDismissRemoteTip}
118
+ />
121
119
  ) : null}
122
120
  {allOfficialCliMissing(sources) ? (
123
- <div
124
- style={{ ...tipStyle, display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 12 }}
125
- >
126
- <p style={{ ...bodyStyle, margin: 0, color: "var(--dsw-alias-label-primary)" }}>
127
- {t("sourcesAllMissingHint")}
128
- </p>
129
- <button type="button" style={buttonStyle} disabled={sourcesBusy} onClick={onRefreshSources}>
130
- {t("sourcesCheckAgain")}
131
- </button>
132
- </div>
121
+ <NoticeBanner
122
+ message={t("sourcesAllMissingHint")}
123
+ dismissLabel={t("sourcesCheckAgain")}
124
+ onDismiss={() => {
125
+ onRefreshSources();
126
+ }}
127
+ />
133
128
  ) : null}
134
129
  {anyOfficialCliAvailable(sources) ? <p style={hintStyle}>{t("sourcesAvailableHint")}</p> : null}
135
130
  {sourcesError === undefined ? null : (
136
- <p style={errorStyle} role="alert">
131
+ <p style={{ ...bodyStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
137
132
  {sourcesError}
138
133
  </p>
139
134
  )}
140
135
  {sourcesNotice === undefined ? null : (
141
- <p style={bodyStyle} role="status">
142
- {sourcesNotice}
143
- </p>
136
+ <NoticeBanner
137
+ key={sourcesNotice}
138
+ message={sourcesNotice}
139
+ tone="success"
140
+ autoHideMs={5000}
141
+ onDismiss={onDismissSourcesNotice}
142
+ />
144
143
  )}
145
144
  <div style={accountGridStyle}>
146
145
  {PROVIDERS.map((definition) => {
@@ -199,10 +198,11 @@ export function AccountsTab({
199
198
  <span style={monoStyle}>{status.antigravity.route}</span>
200
199
  </p>
201
200
  </div>
202
- <div style={statusStyle} role="status">
203
- <span aria-hidden="true" style={dotStyle("signed-out", status.antigravity.installed)} />
204
- <span>{status.antigravity.installed ? t("antigravityInstalled") : t("antigravityMissing")}</span>
205
- </div>
201
+ <Badge
202
+ label={status.antigravity.installed ? t("antigravityInstalled") : t("antigravityMissing")}
203
+ tone={status.antigravity.installed ? "success" : "neutral"}
204
+ installed={status.antigravity.installed}
205
+ />
206
206
  </div>
207
207
  <p style={bodyStyle}>{t("antigravityCliHint")}</p>
208
208
  <code style={{ ...monoStyle, fontSize: 12, overflowWrap: "anywhere" }}>{t("antigravityCliCommand")}</code>
@@ -0,0 +1,33 @@
1
+ /** Status capsule badge with semantic tone colors. */
2
+
3
+ import { badgeStyle, dotStyle, providerStatusTone, type StatusTone } from "../styles.ts";
4
+ import type { ProviderStatus } from "../types.ts";
5
+
6
+ export interface BadgeProps {
7
+ label: string;
8
+ tone?: StatusTone;
9
+ /** When set, derives tone from provider login status. */
10
+ providerStatus?: ProviderStatus["status"];
11
+ installed?: boolean;
12
+ showDot?: boolean;
13
+ }
14
+
15
+ export function Badge({ label, tone, providerStatus, installed = true, showDot = true }: BadgeProps) {
16
+ const resolvedTone =
17
+ tone ?? (providerStatus !== undefined ? providerStatusTone(providerStatus, installed) : "neutral");
18
+ return (
19
+ <span style={badgeStyle(resolvedTone)} role="status">
20
+ {showDot ? (
21
+ <span
22
+ aria-hidden="true"
23
+ style={{
24
+ ...dotStyle(providerStatus ?? (resolvedTone === "success" ? "signed-in" : "signed-out"), installed),
25
+ width: 7,
26
+ height: 7,
27
+ }}
28
+ />
29
+ ) : null}
30
+ <span>{label}</span>
31
+ </span>
32
+ );
33
+ }
@@ -5,7 +5,6 @@ import { imagineSourceLabel } from "../parsers.ts";
5
5
  import {
6
6
  bodyStyle,
7
7
  cardStyle,
8
- checkRowStyle,
9
8
  dotStyle,
10
9
  errorStyle,
11
10
  hintStyle,
@@ -23,6 +22,8 @@ import type {
23
22
  GrokBuildSettingsInjected,
24
23
  ImagineCredentialView,
25
24
  } from "../types.ts";
25
+ import { Badge } from "./Badge.tsx";
26
+ import { ToggleSwitch } from "./ToggleSwitch.tsx";
26
27
 
27
28
  export interface CapabilitiesTabProps {
28
29
  t: GrokBuildSettingsInjected["t"];
@@ -68,10 +69,10 @@ export function CapabilitiesTab({
68
69
  </div>
69
70
  ) : imagine === undefined ? null : (
70
71
  <div style={nestedStyle}>
71
- <p style={statusStyle} role="status">
72
- <span aria-hidden="true" style={dotStyle(imagine.configured ? "available" : "unavailable")} />
73
- <span>{imagine.configured ? t("imagineConfigured") : t("imagineNotConfigured")}</span>
74
- </p>
72
+ <Badge
73
+ label={imagine.configured ? t("imagineConfigured") : t("imagineNotConfigured")}
74
+ tone={imagine.configured ? "success" : "neutral"}
75
+ />
75
76
  <p style={hintStyle}>{t("imagineSource", { source: imagineSourceLabel(imagine.source, t) })}</p>
76
77
  </div>
77
78
  )}
@@ -98,25 +99,34 @@ export function CapabilitiesTab({
98
99
  const checked = capabilities.value[item.key];
99
100
  const imagesOff = item.requiresImages === true && !capabilities.value.codexImages;
100
101
  const disabled = capabilitiesBusy || !capabilities.writable || imagesOff;
102
+ const switchId = `cap-switch-${item.key}`;
101
103
  return (
102
- <li key={item.key}>
103
- <label style={checkRowStyle}>
104
- <input
105
- type="checkbox"
104
+ <li
105
+ key={item.key}
106
+ style={{
107
+ opacity: imagesOff ? 0.5 : 1,
108
+ transition: "opacity 0.15s ease",
109
+ }}
110
+ >
111
+ <div style={{ ...rowStyle, alignItems: "flex-start" }}>
112
+ <label htmlFor={switchId} style={{ flex: "1 1 360px", cursor: disabled ? "default" : "pointer" }}>
113
+ <span style={{ display: "block", fontSize: 14, color: "var(--dsw-alias-label-primary)" }}>
114
+ {t(item.label)}
115
+ </span>
116
+ <span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
117
+ {imagesOff ? t("requiresCodexImages") : t(item.hint)}
118
+ </span>
119
+ </label>
120
+ <ToggleSwitch
121
+ id={switchId}
106
122
  checked={checked}
107
123
  disabled={disabled}
108
- aria-describedby={`cap-hint-${item.key}`}
109
- onChange={(event) => {
110
- void onPatchCapability(item.key, event.target.checked);
124
+ ariaLabel={t(item.label)}
125
+ onChange={(next) => {
126
+ void onPatchCapability(item.key, next);
111
127
  }}
112
128
  />
113
- <span>
114
- <span style={{ display: "block" }}>{t(item.label)}</span>
115
- <span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
116
- {t(item.hint)}
117
- </span>
118
- </span>
119
- </label>
129
+ </div>
120
130
  </li>
121
131
  );
122
132
  })}
@@ -126,25 +136,28 @@ export function CapabilitiesTab({
126
136
  {imagineToggles.map((item) => {
127
137
  const checked = capabilities.value[item.key];
128
138
  const disabled = capabilitiesBusy || !capabilities.writable;
139
+ const switchId = `cap-switch-${item.key}`;
129
140
  return (
130
141
  <li key={item.key}>
131
- <label style={checkRowStyle}>
132
- <input
133
- type="checkbox"
142
+ <div style={{ ...rowStyle, alignItems: "flex-start" }}>
143
+ <label htmlFor={switchId} style={{ flex: "1 1 360px", cursor: disabled ? "default" : "pointer" }}>
144
+ <span style={{ display: "block", fontSize: 14, color: "var(--dsw-alias-label-primary)" }}>
145
+ {t(item.label)}
146
+ </span>
147
+ <span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
148
+ {t(item.hint)}
149
+ </span>
150
+ </label>
151
+ <ToggleSwitch
152
+ id={switchId}
134
153
  checked={checked}
135
154
  disabled={disabled}
136
- aria-describedby={`cap-hint-${item.key}`}
137
- onChange={(event) => {
138
- void onPatchCapability(item.key, event.target.checked);
155
+ ariaLabel={t(item.label)}
156
+ onChange={(next) => {
157
+ void onPatchCapability(item.key, next);
139
158
  }}
140
159
  />
141
- <span>
142
- <span style={{ display: "block" }}>{t(item.label)}</span>
143
- <span id={`cap-hint-${item.key}`} style={{ display: "block", ...hintStyle }}>
144
- {t(item.hint)}
145
- </span>
146
- </span>
147
- </label>
160
+ </div>
148
161
  </li>
149
162
  );
150
163
  })}
@@ -0,0 +1,57 @@
1
+ /** Compact copy button with copied / failed feedback. */
2
+
3
+ import { useCallback, useEffect, useRef, useState } from "react";
4
+ import { copyText } from "../api.ts";
5
+ import { compactButtonStyle, primaryButtonStyle } from "../styles.ts";
6
+
7
+ export interface CopyButtonProps {
8
+ text: string;
9
+ idleLabel: string;
10
+ copiedLabel: string;
11
+ failedLabel: string;
12
+ primary?: boolean;
13
+ disabled?: boolean;
14
+ }
15
+
16
+ export function CopyButton({
17
+ text,
18
+ idleLabel,
19
+ copiedLabel,
20
+ failedLabel,
21
+ primary = false,
22
+ disabled = false,
23
+ }: CopyButtonProps) {
24
+ const [state, setState] = useState<"idle" | "copied" | "failed">("idle");
25
+ const timerRef = useRef<number | undefined>(undefined);
26
+
27
+ useEffect(() => {
28
+ return () => {
29
+ if (timerRef.current !== undefined) window.clearTimeout(timerRef.current);
30
+ };
31
+ }, []);
32
+
33
+ const handleClick = useCallback(async () => {
34
+ const ok = await copyText(text);
35
+ setState(ok ? "copied" : "failed");
36
+ if (timerRef.current !== undefined) window.clearTimeout(timerRef.current);
37
+ timerRef.current = window.setTimeout(() => {
38
+ setState("idle");
39
+ timerRef.current = undefined;
40
+ }, 2000);
41
+ }, [text]);
42
+
43
+ const label = state === "copied" ? copiedLabel : state === "failed" ? failedLabel : idleLabel;
44
+
45
+ return (
46
+ <button
47
+ type="button"
48
+ style={primary ? primaryButtonStyle : compactButtonStyle}
49
+ disabled={disabled || text.length === 0}
50
+ onClick={() => {
51
+ void handleClick();
52
+ }}
53
+ >
54
+ {label}
55
+ </button>
56
+ );
57
+ }
@@ -1,7 +1,8 @@
1
1
  /** Local API gateway settings tab. */
2
2
 
3
- import { useState } from "react";
3
+ import { useMemo, useState } from "react";
4
4
  import { GATEWAY_PORT_MAX, GATEWAY_PORT_MIN } from "../constants.ts";
5
+ import { buildGatewaySnippets, type GatewaySnippetId } from "../gatewaySnippets.ts";
5
6
  import { formatGatewayBaseUrl, parseGatewayPort, randomGatewayPort } from "../parsers.ts";
6
7
  import {
7
8
  bodyStyle,
@@ -16,12 +17,18 @@ import {
16
17
  monoStyle,
17
18
  nestedStyle,
18
19
  primaryButtonStyle,
20
+ segmentedNavStyle,
21
+ segmentedTabActiveStyle,
22
+ segmentedTabStyle,
19
23
  skeletonStyle,
24
+ snippetStyle,
20
25
  statusStyle,
21
26
  titleStyle,
22
27
  warningStyle,
23
28
  } from "../styles.ts";
24
29
  import type { CopyField, GatewayView, GrokBuildSettingsInjected } from "../types.ts";
30
+ import { Badge } from "./Badge.tsx";
31
+ import { CopyButton } from "./CopyButton.tsx";
25
32
 
26
33
  export interface GatewayTabProps {
27
34
  t: GrokBuildSettingsInjected["t"];
@@ -47,6 +54,15 @@ export interface GatewayTabProps {
47
54
  onRotate: () => void;
48
55
  }
49
56
 
57
+ const SNIPPET_TABS: readonly {
58
+ id: GatewaySnippetId;
59
+ labelKey: "gatewaySnippetCurl" | "gatewaySnippetPython" | "gatewaySnippetIde";
60
+ }[] = [
61
+ { id: "curl", labelKey: "gatewaySnippetCurl" },
62
+ { id: "python", labelKey: "gatewaySnippetPython" },
63
+ { id: "ide", labelKey: "gatewaySnippetIde" },
64
+ ];
65
+
50
66
  export function GatewayTab({
51
67
  t,
52
68
  gateway,
@@ -71,6 +87,23 @@ export function GatewayTab({
71
87
  onRotate,
72
88
  }: GatewayTabProps) {
73
89
  const [enableConfirm, setEnableConfirm] = useState(false);
90
+ const [activeSnippet, setActiveSnippet] = useState<GatewaySnippetId>("curl");
91
+
92
+ const portValid = parseGatewayPort(portDraft) !== undefined;
93
+ const portChanged = gateway !== undefined && portDraft !== String(gateway.port);
94
+
95
+ const snippets = useMemo(() => {
96
+ if (gateway === undefined) return undefined;
97
+ const openAi = `${formatGatewayBaseUrl(gateway.bind, gateway.port)}/v1`;
98
+ const anthropic = formatGatewayBaseUrl(gateway.bind, gateway.port);
99
+ const key =
100
+ gatewayKeyVisible && gatewayOnceKey !== undefined
101
+ ? gatewayOnceKey
102
+ : gateway.keyHint.length > 0
103
+ ? gateway.keyHint
104
+ : "";
105
+ return buildGatewaySnippets(openAi, anthropic, key);
106
+ }, [gateway, gatewayKeyVisible, gatewayOnceKey]);
74
107
 
75
108
  const copyLabel = (field: CopyField, idle?: string): string => {
76
109
  if (copyFailedField === field) return t("copyFailed");
@@ -107,10 +140,10 @@ export function GatewayTab({
107
140
  </div>
108
141
  ) : gateway === undefined ? null : (
109
142
  <div style={nestedStyle}>
110
- <p style={statusStyle} role="status">
111
- <span aria-hidden="true" style={dotStyle(gateway.running ? "available" : "unavailable")} />
112
- <span>{gateway.running ? t("gatewayRunning") : t("gatewayStopped")}</span>
113
- </p>
143
+ <Badge
144
+ label={gateway.running ? t("gatewayRunning") : t("gatewayStopped")}
145
+ tone={gateway.running ? "success" : "neutral"}
146
+ />
114
147
  {enableConfirm ? (
115
148
  <div style={nestedStyle}>
116
149
  <p style={bodyStyle}>{t("gatewayEnableConfirm")}</p>
@@ -166,6 +199,11 @@ export function GatewayTab({
166
199
  <p id="coding-oauth-gateway-port-hint" style={hintStyle}>
167
200
  {t("gatewayPortHint")}
168
201
  </p>
202
+ {!portValid && portDraft.length > 0 ? (
203
+ <p style={{ ...hintStyle, color: "var(--dsw-alias-state-error-primary)" }} role="alert">
204
+ {t("gatewayPortInvalid")}
205
+ </p>
206
+ ) : null}
169
207
  <div style={{ display: "flex", flexWrap: "wrap", gap: 8, marginTop: 8 }}>
170
208
  <input
171
209
  id="coding-oauth-gateway-port"
@@ -177,7 +215,16 @@ export function GatewayTab({
177
215
  value={portDraft}
178
216
  disabled={gatewayBusy}
179
217
  aria-describedby="coding-oauth-gateway-port-hint"
180
- style={{ ...inputStyle, width: 112, flex: "0 0 112px" }}
218
+ aria-invalid={!portValid && portDraft.length > 0}
219
+ style={{
220
+ ...inputStyle,
221
+ width: 112,
222
+ flex: "0 0 112px",
223
+ borderColor:
224
+ !portValid && portDraft.length > 0
225
+ ? "var(--dsw-alias-state-error-primary)"
226
+ : "var(--dsw-alias-border-l2)",
227
+ }}
181
228
  onChange={(event) => {
182
229
  onPortDraftChange(event.target.value);
183
230
  }}
@@ -194,9 +241,7 @@ export function GatewayTab({
194
241
  <button
195
242
  type="button"
196
243
  style={primaryButtonStyle}
197
- disabled={
198
- gatewayBusy || portDraft === String(gateway.port) || parseGatewayPort(portDraft) === undefined
199
- }
244
+ disabled={gatewayBusy || !portChanged || !portValid}
200
245
  onClick={onApplyPort}
201
246
  >
202
247
  {t("gatewayPortApply")}
@@ -262,6 +307,38 @@ export function GatewayTab({
262
307
  </span>
263
308
  </p>
264
309
  <p style={hintStyle}>{t("gatewayKeyCopyHint")}</p>
310
+ {gateway.enabled && snippets !== undefined ? (
311
+ <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
312
+ <h4 style={{ ...titleStyle, fontSize: 14 }}>{t("gatewaySnippetsTitle")}</h4>
313
+ <div role="tablist" aria-label={t("gatewaySnippetsTitle")} style={segmentedNavStyle}>
314
+ {SNIPPET_TABS.map((tab) => {
315
+ const selected = activeSnippet === tab.id;
316
+ return (
317
+ <button
318
+ key={tab.id}
319
+ type="button"
320
+ role="tab"
321
+ aria-selected={selected}
322
+ style={selected ? segmentedTabActiveStyle : segmentedTabStyle}
323
+ onClick={() => {
324
+ setActiveSnippet(tab.id);
325
+ }}
326
+ >
327
+ {t(tab.labelKey)}
328
+ </button>
329
+ );
330
+ })}
331
+ </div>
332
+ <code style={snippetStyle}>{snippets[activeSnippet]}</code>
333
+ <CopyButton
334
+ text={snippets[activeSnippet]}
335
+ idleLabel={t("copy")}
336
+ copiedLabel={t("copied")}
337
+ failedLabel={t("copyFailed")}
338
+ primary
339
+ />
340
+ </div>
341
+ ) : null}
265
342
  {gatewayRotateConfirm ? (
266
343
  <div style={nestedStyle}>
267
344
  <p style={bodyStyle}>{t("gatewayRotateConfirm")}</p>
@@ -0,0 +1,46 @@
1
+ /** Dismissible notice banner with optional auto-hide. */
2
+
3
+ import { useEffect } from "react";
4
+ import { bodyStyle, buttonStyle, noticeStyle } from "../styles.ts";
5
+
6
+ export interface NoticeBannerProps {
7
+ message: string;
8
+ dismissLabel?: string;
9
+ onDismiss?: () => void;
10
+ autoHideMs?: number;
11
+ tone?: "info" | "success";
12
+ }
13
+
14
+ export function NoticeBanner({ message, dismissLabel, onDismiss, autoHideMs, tone = "info" }: NoticeBannerProps) {
15
+ useEffect(() => {
16
+ if (autoHideMs === undefined || onDismiss === undefined) return;
17
+ const timer = window.setTimeout(onDismiss, autoHideMs);
18
+ return () => {
19
+ window.clearTimeout(timer);
20
+ };
21
+ }, [autoHideMs, onDismiss]);
22
+
23
+ const borderColor =
24
+ tone === "success" ? "var(--dsw-alias-state-success-primary, #22a06b)" : "var(--dsw-alias-brand-primary, #1677ff)";
25
+
26
+ return (
27
+ <div
28
+ style={{
29
+ ...noticeStyle,
30
+ display: "flex",
31
+ alignItems: "flex-start",
32
+ justifyContent: "space-between",
33
+ gap: 12,
34
+ borderLeft: `3px solid ${borderColor}`,
35
+ }}
36
+ role="status"
37
+ >
38
+ <p style={{ ...bodyStyle, margin: 0, color: "var(--dsw-alias-label-primary)" }}>{message}</p>
39
+ {onDismiss === undefined || dismissLabel === undefined ? null : (
40
+ <button type="button" style={buttonStyle} onClick={onDismiss}>
41
+ {dismissLabel}
42
+ </button>
43
+ )}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,52 @@
1
+ /** Usage quota progress bar with threshold-based color. */
2
+
3
+ import { hintStyle } from "../styles.ts";
4
+
5
+ export interface ProgressBarProps {
6
+ /** 0–100 used percentage. */
7
+ value: number;
8
+ label?: string;
9
+ meta?: string;
10
+ }
11
+
12
+ function barColor(percent: number): string {
13
+ if (percent >= 90) return "var(--dsw-alias-state-error-primary, #d92d20)";
14
+ if (percent >= 75) return "var(--dsw-alias-state-warning-primary, #e06c00)";
15
+ return "var(--dsw-alias-brand-primary, #1677ff)";
16
+ }
17
+
18
+ export function ProgressBar({ value, label, meta }: ProgressBarProps) {
19
+ const clamped = Math.max(0, Math.min(100, value));
20
+ return (
21
+ <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
22
+ {label === undefined ? null : (
23
+ <div style={{ display: "flex", justifyContent: "space-between", gap: 8, flexWrap: "wrap" }}>
24
+ <span style={{ ...hintStyle, color: "var(--dsw-alias-label-primary)" }}>{label}</span>
25
+ {meta === undefined ? null : <span style={hintStyle}>{meta}</span>}
26
+ </div>
27
+ )}
28
+ <div
29
+ role="progressbar"
30
+ aria-valuenow={clamped}
31
+ aria-valuemin={0}
32
+ aria-valuemax={100}
33
+ style={{
34
+ height: 8,
35
+ borderRadius: 4,
36
+ background: "var(--dsw-alias-border-l2)",
37
+ overflow: "hidden",
38
+ }}
39
+ >
40
+ <div
41
+ style={{
42
+ width: `${String(clamped)}%`,
43
+ height: "100%",
44
+ borderRadius: 4,
45
+ background: barColor(clamped),
46
+ transition: "width 0.3s ease, background 0.3s ease",
47
+ }}
48
+ />
49
+ </div>
50
+ </div>
51
+ );
52
+ }