dsh-subscriptions 0.1.0 → 0.1.2
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/README.md +99 -6
- package/icon.svg +28 -0
- package/lib/client/CursorCard.js +15 -11
- package/lib/client/ExternalUsageCards.js +11 -11
- package/lib/client/SubscriptionUsageBadge.d.ts +6 -2
- package/lib/client/SubscriptionUsageBadge.js +56 -24
- package/lib/client/SubscriptionsSection.d.ts +7 -2
- package/lib/client/SubscriptionsSection.js +9 -13
- package/lib/client/UsageBadgeDisplaySetting.js +18 -1
- package/lib/client/UsageMeter.d.ts +8 -0
- package/lib/client/UsageMeter.js +51 -0
- package/lib/client/host-contract.d.ts +1 -1
- package/lib/client/host-contract.js +1 -0
- package/lib/client/locales.d.ts +48 -0
- package/lib/client/locales.js +48 -0
- package/lib/client/subscription-card-styles.js +0 -5
- package/lib/client/usage-color-preferences.d.ts +6 -0
- package/lib/client/usage-color-preferences.js +38 -0
- package/lib/client/usage-pace.d.ts +29 -0
- package/lib/client/usage-pace.js +58 -0
- package/lib/client.js +396 -114
- package/lib/client.js.map +1 -1
- package/lib/index.js +225 -35
- package/lib/providers/claude.js +7 -0
- package/lib/providers/codex.js +5 -0
- package/lib/providers/common.d.ts +10 -0
- package/lib/providers/cursor-usage.js +6 -2
- package/lib/providers/external-usage-controller.d.ts +1 -1
- package/lib/providers/external-usage-controller.js +8 -3
- package/lib/providers/external-usage.d.ts +1 -1
- package/lib/providers/external-usage.js +62 -2
- package/lib/providers/grok.js +3 -1
- package/lib/providers/minimax-usage.d.ts +3 -0
- package/lib/providers/minimax-usage.js +55 -0
- package/lib/providers/pool-usage.js +6 -4
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +10 -6
|
@@ -18,6 +18,8 @@ import { en } from './locales.js';
|
|
|
18
18
|
import { ProviderAccountManager } from './ProviderAccountManager.js';
|
|
19
19
|
import { ExternalUsageCards } from './ExternalUsageCards.js';
|
|
20
20
|
import { CursorCard } from './CursorCard.js';
|
|
21
|
+
import { UsageMeter } from './UsageMeter.js';
|
|
22
|
+
import { displayUsedPercent } from './usage-pace.js';
|
|
21
23
|
import { UsageBadgeDisplaySetting } from './UsageBadgeDisplaySetting.js';
|
|
22
24
|
import { USAGE_BADGE_REFRESH_EVENT } from './usage-badge-preferences.js';
|
|
23
25
|
import { subscriptionCardStyles as cardStyles } from './subscription-card-styles.js';
|
|
@@ -102,8 +104,6 @@ const styles = {
|
|
|
102
104
|
font: 'inherit', fontSize: 14, lineHeight: '20px', cursor: 'pointer',
|
|
103
105
|
color: 'var(--dsw-alias-state-warn-label)',
|
|
104
106
|
},
|
|
105
|
-
usageTrack: cardStyles.usageTrack,
|
|
106
|
-
usageFill: cardStyles.usageFill,
|
|
107
107
|
deviceCode: {
|
|
108
108
|
marginTop: 4, display: 'flex', flexDirection: 'column', gap: 6,
|
|
109
109
|
border: '1px solid var(--dsw-alias-border-l2)', borderRadius: 8,
|
|
@@ -183,14 +183,8 @@ function usageWindowLabel(t, window) {
|
|
|
183
183
|
: window.kind === 'weekly' ? t('usageWeekly') : t('usageWindow');
|
|
184
184
|
return window.scope !== undefined && window.scope !== '' ? `${base} · ${window.scope}` : base;
|
|
185
185
|
}
|
|
186
|
-
/**
|
|
187
|
-
export
|
|
188
|
-
if (usedPercent >= 95)
|
|
189
|
-
return 'var(--dsw-alias-state-error-primary)';
|
|
190
|
-
if (usedPercent >= 80)
|
|
191
|
-
return 'var(--dsw-alias-state-warn-label)';
|
|
192
|
-
return 'var(--dsw-alias-state-success-primary)';
|
|
193
|
-
}
|
|
186
|
+
/** Meter fill color lives with the pace logic: fresh 90%+ is red, pace presets add a yellow lead threshold. */
|
|
187
|
+
export { usageBarColor } from './usage-pace.js';
|
|
194
188
|
/**
|
|
195
189
|
* Derive one provider's default-effort section from its catalog and filter.
|
|
196
190
|
* Pure so the collapsed-header counts and the filter stay testable without a
|
|
@@ -268,6 +262,7 @@ export function SubscriptionsSection(props) {
|
|
|
268
262
|
const [copiedCode, setCopiedCode] = useState(undefined);
|
|
269
263
|
/** Usage snapshots keyed `${provider}:${accountKey}` — every account tracks its own windows. */
|
|
270
264
|
const [usages, setUsages] = useState({});
|
|
265
|
+
const [observedAt, setObservedAt] = useState({});
|
|
271
266
|
const [usageErrors, setUsageErrors] = useState({});
|
|
272
267
|
const [usageLoading, setUsageLoading] = useState({});
|
|
273
268
|
const mountedRef = useRef(true);
|
|
@@ -377,6 +372,7 @@ export function SubscriptionsSection(props) {
|
|
|
377
372
|
if (!mountedRef.current)
|
|
378
373
|
return;
|
|
379
374
|
setUsages(prev => ({ ...prev, [key]: usage }));
|
|
375
|
+
setObservedAt(prev => ({ ...prev, [key]: usage.observedAt ?? Date.now() }));
|
|
380
376
|
window.dispatchEvent(new Event(USAGE_BADGE_REFRESH_EVENT));
|
|
381
377
|
setUsageErrors((prev) => {
|
|
382
378
|
const next = { ...prev };
|
|
@@ -535,9 +531,9 @@ export function SubscriptionsSection(props) {
|
|
|
535
531
|
if (!account.isDefault)
|
|
536
532
|
void setDefault(id, account.key);
|
|
537
533
|
}, children: account.isDefault ? '★' : '☆' }), _jsx("span", { style: styles.accountName, children: display }), account.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: account.plan })), account.expiresAt !== undefined && (_jsx("span", { style: styles.statusLine, children: t('accountExpires', { date: new Date(account.expiresAt).toLocaleString() }) })), _jsx("button", { type: "button", style: { ...styles.button, marginLeft: 'auto', flexShrink: 0 }, onClick: () => { void logout(id, account.key, display, name); }, children: t('logout') })] }), showUsage && (_jsxs("div", { style: styles.usage, children: [_jsxs("div", { style: styles.usageHeader, children: [_jsx("span", { style: styles.usageTitle, children: t('usageTitle') }), usage?.plan !== undefined && (_jsx("span", { style: styles.usagePlan, children: t('usagePlan', { plan: usage.plan }) })), _jsx("button", { type: "button", style: { ...styles.usageRefresh, ...usageLoading[usageKey] === true ? { opacity: 0.5, cursor: 'default' } : {} }, disabled: usageLoading[usageKey] === true, onClick: () => { void loadUsage(id, account.key, true); }, children: t('usageRefresh') })] }), usage === undefined && usageError === undefined && (_jsx("p", { style: styles.statusLine, children: t('usageLoading') })), usageError !== undefined && (_jsx("p", { style: styles.errorLine, children: t('usageError', { message: usageError }) })), usage?.windows !== undefined && usage.windows.length === 0 && (_jsx("p", { style: styles.statusLine, children: t('usageEmpty') })), (usage?.windows ?? []).map((window, index) => {
|
|
538
|
-
const percent =
|
|
539
|
-
return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [`${
|
|
540
|
-
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(
|
|
534
|
+
const percent = displayUsedPercent(window.usedPercent);
|
|
535
|
+
return (_jsxs("div", { style: styles.usageRow, children: [_jsxs("div", { style: styles.usageMeta, children: [_jsx("span", { children: usageWindowLabel(t, window) }), _jsxs("span", { children: [percent === undefined ? t('usageMeterInvalid') : `${percent}%`, window.resetsAt !== undefined
|
|
536
|
+
&& ` · ${t('usageResets', { date: new Date(window.resetsAt).toLocaleString() })}`] })] }), _jsx(UsageMeter, { window: window, t: t, observedAt: observedAt[usageKey], stale: usageError !== undefined || usage.stale === true })] }, index));
|
|
541
537
|
})] }))] }, account.key));
|
|
542
538
|
}), _jsxs("div", { style: styles.actions, children: [!busy && accounts.length === 0 && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('login') })), !busy && accounts.length > 0 && id === 'claude' && (_jsxs(_Fragment, { children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'oauth'); }, children: t('addAccountOAuth') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id, 'keychain'); }, children: t('addAccountKeychain') })] })), !busy && accounts.length > 0 && id !== 'claude' && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void login(id); }, children: t('addAccount') })), _jsx("button", { type: "button", style: styles.button, "aria-haspopup": "dialog", onClick: () => setManagedProvider({ id, name }), children: t('accountsManage') }), busy && (_jsx("button", { type: "button", style: styles.button, onClick: () => { void cancel(id); }, children: t('cancel') }))] }), !busy && accounts.length > 0 && (_jsx("p", { style: styles.statusLine, children: t('addAccountHint') })), busy && deviceCode !== undefined && (_jsxs("div", { style: styles.deviceCode, children: [_jsx("span", { style: styles.statusLine, children: t('deviceCodePrompt') }), _jsx("span", { style: styles.deviceCodeText, children: deviceCode.userCode }), _jsxs("div", { style: styles.actions, children: [_jsx("button", { type: "button", style: styles.button, onClick: () => { copyDeviceCode(id, deviceCode.userCode); }, children: copiedCode === id ? t('deviceCodeCopied') : t('deviceCodeCopy') }), _jsx("button", { type: "button", style: styles.button, onClick: () => { window.open(deviceCode.verificationUrl, '_blank', 'noopener'); }, children: t('deviceCodeOpenPage') })] })] })), busy && deviceCode === undefined && (_jsxs("details", { style: styles.manual, children: [_jsx("summary", { children: t('manualSummary') }), _jsxs("div", { style: styles.manualRow, children: [_jsx("input", { style: styles.manualInput, value: manualDrafts[id], placeholder: t('manualPlaceholder'), onChange: event => setManualDrafts(prev => ({ ...prev, [id]: event.target.value })) }), _jsx("button", { type: "button", style: styles.button, onClick: () => { void submitManual(id); }, children: t('submit') })] })] }))] }, id));
|
|
543
539
|
}), _jsx(CursorCard, { rpc: rpc, t: t }), _jsx(ExternalUsageCards, { rpc: rpc, t: t }), managedProvider && _jsx(ProviderAccountManager, { provider: managedProvider.id, name: managedProvider.name, rpc: rpc, t: t, onClose: () => setManagedProvider(undefined) })] }));
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
3
|
import { setUsageBadgeMode, useUsageBadgeMode } from './usage-badge-preferences.js';
|
|
4
|
+
import { isUsageColorPreset, setUsageColorPreset, useUsageColorPreset } from './usage-color-preferences.js';
|
|
4
5
|
/** Browser-local display preference; never changes provider or account settings. */
|
|
5
6
|
export function UsageBadgeDisplaySetting({ t }) {
|
|
6
7
|
const mode = useUsageBadgeMode();
|
|
8
|
+
const preset = useUsageColorPreset();
|
|
7
9
|
const [failed, setFailed] = useState(false);
|
|
8
10
|
return (_jsxs("div", { style: styles.card, children: [_jsxs("label", { style: styles.field, children: [_jsx("span", { children: t('usageBadgeDisplay') }), _jsxs("select", { style: styles.select, "aria-label": t('usageBadgeDisplay'), value: mode, onChange: event => {
|
|
9
11
|
const value = event.currentTarget.value;
|
|
@@ -18,7 +20,22 @@ export function UsageBadgeDisplaySetting({ t }) {
|
|
|
18
20
|
throw error;
|
|
19
21
|
setFailed(true);
|
|
20
22
|
}
|
|
21
|
-
}, children: [_jsx("option", { value: "recent", children: t('usageBadgeDisplayRecent') }), _jsx("option", { value: "hidden", children: t('usageBadgeDisplayHidden') })] })] }), _jsx("p", { style: styles.hint, children: t('usageBadgeDisplayHint') }),
|
|
23
|
+
}, children: [_jsx("option", { value: "recent", children: t('usageBadgeDisplayRecent') }), _jsx("option", { value: "hidden", children: t('usageBadgeDisplayHidden') })] })] }), _jsx("p", { style: styles.hint, children: t('usageBadgeDisplayHint') }), _jsxs("label", { style: styles.field, children: [_jsx("span", { children: t('usageColorLabel') }), _jsxs("select", { style: styles.select, "aria-label": t('usageColorLabel'), value: preset, onChange: event => {
|
|
24
|
+
const value = event.currentTarget.value;
|
|
25
|
+
if (!isUsageColorPreset(value))
|
|
26
|
+
return;
|
|
27
|
+
try {
|
|
28
|
+
setUsageColorPreset(value);
|
|
29
|
+
setFailed(false);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!(error instanceof DOMException) || !['SecurityError', 'QuotaExceededError'].includes(error.name))
|
|
33
|
+
throw error;
|
|
34
|
+
setFailed(true);
|
|
35
|
+
}
|
|
36
|
+
}, children: [_jsx("option", { value: "standard", children: t('usageColorStandard') }), _jsx("option", { value: "relaxed", children: t('usageColorRelaxed') }), _jsx("option", { value: "remaining", children: t('usageColorRemaining') })] })] }), _jsx("p", { style: styles.hint, children: t(preset === 'remaining' ? 'usageColorRemainingHint' : 'usageColorPaceHint', {
|
|
37
|
+
allowance: preset === 'relaxed' ? 15 : 10,
|
|
38
|
+
}) }), failed && _jsx("p", { role: "alert", style: { ...styles.hint, color: 'var(--dsw-alias-state-error-primary)' }, children: t('usageBadgeDisplaySaveFailed') })] }));
|
|
22
39
|
}
|
|
23
40
|
const styles = {
|
|
24
41
|
card: {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
import type { UsageWindow, SubscriptionsSectionInjected } from './SubscriptionsSection.js';
|
|
3
|
+
import type { UsageObservation } from './usage-pace.js';
|
|
4
|
+
export declare function UsageMeter({ window: quota, t, style, observedAt, stale }: {
|
|
5
|
+
window: UsageWindow;
|
|
6
|
+
t: SubscriptionsSectionInjected['t'];
|
|
7
|
+
style?: CSSProperties;
|
|
8
|
+
} & UsageObservation): import("react").JSX.Element;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useState } from 'react';
|
|
3
|
+
import { elapsedPercent, isUsageFresh, resetCountdownParts, usageBarColor, usageColorState, validUsage } from './usage-pace.js';
|
|
4
|
+
import { useUsageColorPreset } from './usage-color-preferences.js';
|
|
5
|
+
/** Singular/plural locale keys per countdown unit (zh maps both to one string). */
|
|
6
|
+
const UNIT_KEYS = {
|
|
7
|
+
day: ['usageUnitDay', 'usageUnitDays'],
|
|
8
|
+
hour: ['usageUnitHour', 'usageUnitHours'],
|
|
9
|
+
minute: ['usageUnitMinute', 'usageUnitMinutes'],
|
|
10
|
+
};
|
|
11
|
+
export function UsageMeter({ window: quota, t, style, observedAt, stale }) {
|
|
12
|
+
const preset = useUsageColorPreset();
|
|
13
|
+
const [now, setNow] = useState(Date.now);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
const timer = setInterval(() => setNow(Date.now()), 60_000);
|
|
16
|
+
return () => clearInterval(timer);
|
|
17
|
+
}, []);
|
|
18
|
+
// Use render time for new readings, without trusting a future observation timestamp.
|
|
19
|
+
const current = Math.max(now, Date.now());
|
|
20
|
+
const valid = validUsage(quota.usedPercent);
|
|
21
|
+
const used = valid ? quota.usedPercent : 0;
|
|
22
|
+
const fresh = isUsageFresh(quota, { observedAt, stale }, current);
|
|
23
|
+
const elapsed = fresh ? elapsedPercent(quota, current) : undefined;
|
|
24
|
+
const state = usageColorState(quota.usedPercent, elapsed, preset, fresh);
|
|
25
|
+
const lead = elapsed === undefined ? 0 : Math.round(used - elapsed);
|
|
26
|
+
const label = [
|
|
27
|
+
valid ? t('usageMeterUsed', { used: Math.round(used), remaining: Math.round(100 - used) }) : t('usageMeterInvalid'),
|
|
28
|
+
// An invalid reading already states unavailability; the stale line would repeat it.
|
|
29
|
+
!valid ? '' : !fresh ? t('usageMeterStale') : state === 'red' ? t('usageMeterNearLimit')
|
|
30
|
+
: elapsed === undefined && preset !== 'remaining' ? t('usageMeterUnknownTime')
|
|
31
|
+
: state === 'yellow' ? t('usageMeterAhead') : t('usageMeterNoWarning'),
|
|
32
|
+
elapsed === undefined ? '' : t('usageMeterPace', { elapsed: Math.round(elapsed) }),
|
|
33
|
+
elapsed === undefined ? '' : lead > 0 ? t('usageMeterPaceAhead', { points: lead })
|
|
34
|
+
: lead < 0 ? t('usageMeterPaceBehind', { points: -lead }) : t('usageMeterPaceEven'),
|
|
35
|
+
quota.resetsAt !== undefined && quota.resetsAt > current ? t('usageMeterReset', {
|
|
36
|
+
duration: resetCountdownParts(quota.resetsAt - current)
|
|
37
|
+
.map(part => t(UNIT_KEYS[part.unit][part.count === 1 ? 0 : 1], { count: part.count }))
|
|
38
|
+
.join(' '),
|
|
39
|
+
}) : '',
|
|
40
|
+
].filter(Boolean).join(' · ');
|
|
41
|
+
return _jsxs("div", { role: "img", "aria-label": label, title: label, "data-usage-color": state, style: {
|
|
42
|
+
height: 6, borderRadius: 3, overflow: 'hidden',
|
|
43
|
+
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
44
|
+
...style, position: 'relative',
|
|
45
|
+
}, children: [_jsx("div", { style: { height: '100%', borderRadius: 'inherit', width: `${used}%`,
|
|
46
|
+
background: usageBarColor(quota.usedPercent, elapsed, preset, fresh) } }), elapsed !== undefined && _jsx("span", { "aria-hidden": "true", "data-usage-time-marker": true, style: {
|
|
47
|
+
position: 'absolute', left: `clamp(0px, calc(${elapsed}% - 1px), calc(100% - 2px))`,
|
|
48
|
+
top: 0, bottom: 0, width: 2, background: 'var(--dsw-alias-label-primary)',
|
|
49
|
+
boxShadow: '0 0 0 1px var(--dsw-alias-bg-layer-1)',
|
|
50
|
+
} })] });
|
|
51
|
+
}
|
|
@@ -83,7 +83,7 @@ export declare const HOST_CONTRACT: {
|
|
|
83
83
|
/** Design tokens the client styles with; the host theme must define each one. */
|
|
84
84
|
readonly tokens: {
|
|
85
85
|
readonly package: "@deepseek-ai/dsh-client-ui-theme";
|
|
86
|
-
readonly names: readonly ["--dsw-alias-bg-layer-1", "--dsw-alias-bg-skeleton", "--dsw-alias-border-l2", "--dsw-alias-border-l2-darkmode-thin", "--dsw-alias-interactive-bg-hover", "--dsw-alias-interactive-bg-hover-solid", "--dsw-alias-label-dimmed", "--dsw-alias-label-primary", "--dsw-alias-label-secondary", "--dsw-alias-label-tertiary", "--dsw-alias-state-error-primary", "--dsw-alias-state-success-primary", "--dsw-alias-state-warn-label", "--dsw-elevation-prominent", "--dsw-specific-menu"];
|
|
86
|
+
readonly names: readonly ["--dsw-alias-bg-layer-1", "--dsw-alias-bg-skeleton", "--dsw-alias-border-l2", "--dsw-alias-border-l2-darkmode-thin", "--dsw-alias-interactive-bg-hover", "--dsw-alias-interactive-bg-hover-solid", "--dsw-alias-label-dimmed", "--dsw-alias-label-primary", "--dsw-alias-label-secondary", "--dsw-alias-label-tertiary", "--dsw-alias-state-error-primary", "--dsw-alias-state-success-primary", "--dsw-alias-state-warn-label", "--dsw-elevation-prominent", "--dsw-menu-backdrop-filter", "--dsw-specific-menu"];
|
|
87
87
|
};
|
|
88
88
|
};
|
|
89
89
|
/** A glyph name listed in {@link HOST_CONTRACT}; hostIcon() accepts no other. */
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -79,6 +79,30 @@ export declare const en: {
|
|
|
79
79
|
usageMonthly: string;
|
|
80
80
|
usageWindow: string;
|
|
81
81
|
usageResets: string;
|
|
82
|
+
usageMeterUsed: string;
|
|
83
|
+
usageMeterPace: string;
|
|
84
|
+
usageMeterPaceAhead: string;
|
|
85
|
+
usageMeterPaceBehind: string;
|
|
86
|
+
usageMeterPaceEven: string;
|
|
87
|
+
usageMeterInvalid: string;
|
|
88
|
+
usageMeterStale: string;
|
|
89
|
+
usageMeterNearLimit: string;
|
|
90
|
+
usageMeterAhead: string;
|
|
91
|
+
usageMeterNoWarning: string;
|
|
92
|
+
usageMeterUnknownTime: string;
|
|
93
|
+
usageMeterReset: string;
|
|
94
|
+
usageUnitDay: string;
|
|
95
|
+
usageUnitDays: string;
|
|
96
|
+
usageUnitHour: string;
|
|
97
|
+
usageUnitHours: string;
|
|
98
|
+
usageUnitMinute: string;
|
|
99
|
+
usageUnitMinutes: string;
|
|
100
|
+
usageColorLabel: string;
|
|
101
|
+
usageColorStandard: string;
|
|
102
|
+
usageColorRelaxed: string;
|
|
103
|
+
usageColorRemaining: string;
|
|
104
|
+
usageColorPaceHint: string;
|
|
105
|
+
usageColorRemainingHint: string;
|
|
82
106
|
usagePlan: string;
|
|
83
107
|
externalUsageTitle: string;
|
|
84
108
|
externalUsageConnected: string;
|
|
@@ -226,6 +250,30 @@ export declare const zh: {
|
|
|
226
250
|
usageMonthly: string;
|
|
227
251
|
usageWindow: string;
|
|
228
252
|
usageResets: string;
|
|
253
|
+
usageMeterUsed: string;
|
|
254
|
+
usageMeterPace: string;
|
|
255
|
+
usageMeterPaceAhead: string;
|
|
256
|
+
usageMeterPaceBehind: string;
|
|
257
|
+
usageMeterPaceEven: string;
|
|
258
|
+
usageMeterInvalid: string;
|
|
259
|
+
usageMeterStale: string;
|
|
260
|
+
usageMeterNearLimit: string;
|
|
261
|
+
usageMeterAhead: string;
|
|
262
|
+
usageMeterNoWarning: string;
|
|
263
|
+
usageMeterUnknownTime: string;
|
|
264
|
+
usageMeterReset: string;
|
|
265
|
+
usageUnitDay: string;
|
|
266
|
+
usageUnitDays: string;
|
|
267
|
+
usageUnitHour: string;
|
|
268
|
+
usageUnitHours: string;
|
|
269
|
+
usageUnitMinute: string;
|
|
270
|
+
usageUnitMinutes: string;
|
|
271
|
+
usageColorLabel: string;
|
|
272
|
+
usageColorStandard: string;
|
|
273
|
+
usageColorRelaxed: string;
|
|
274
|
+
usageColorRemaining: string;
|
|
275
|
+
usageColorPaceHint: string;
|
|
276
|
+
usageColorRemainingHint: string;
|
|
229
277
|
usagePlan: string;
|
|
230
278
|
externalUsageTitle: string;
|
|
231
279
|
externalUsageConnected: string;
|
package/lib/client/locales.js
CHANGED
|
@@ -79,6 +79,30 @@ export const en = {
|
|
|
79
79
|
usageMonthly: 'Monthly',
|
|
80
80
|
usageWindow: 'Window',
|
|
81
81
|
usageResets: 'resets {date}',
|
|
82
|
+
usageMeterUsed: 'Used {used}% · Remaining {remaining}%',
|
|
83
|
+
usageMeterPace: 'Time elapsed {elapsed}% (vertical marker)',
|
|
84
|
+
usageMeterPaceAhead: '{points} points ahead of pace',
|
|
85
|
+
usageMeterPaceBehind: '{points} points behind pace',
|
|
86
|
+
usageMeterPaceEven: 'On pace',
|
|
87
|
+
usageMeterInvalid: 'Quota unavailable',
|
|
88
|
+
usageMeterStale: 'Reading is stale or unavailable; refresh to confirm',
|
|
89
|
+
usageMeterNearLimit: 'Near exhaustion',
|
|
90
|
+
usageMeterAhead: 'Usage ahead of pace',
|
|
91
|
+
usageMeterNoWarning: 'No quota warning',
|
|
92
|
+
usageMeterUnknownTime: 'Time progress unknown',
|
|
93
|
+
usageMeterReset: 'Resets in {duration}',
|
|
94
|
+
usageUnitDay: '{count} day',
|
|
95
|
+
usageUnitDays: '{count} days',
|
|
96
|
+
usageUnitHour: '{count} hour',
|
|
97
|
+
usageUnitHours: '{count} hours',
|
|
98
|
+
usageUnitMinute: '{count} minute',
|
|
99
|
+
usageUnitMinutes: '{count} minutes',
|
|
100
|
+
usageColorLabel: 'Usage coloring',
|
|
101
|
+
usageColorStandard: 'Standard (default · 10-point lead)',
|
|
102
|
+
usageColorRelaxed: 'Relaxed (15-point lead)',
|
|
103
|
+
usageColorRemaining: 'Remaining quota only (no yellow)',
|
|
104
|
+
usageColorPaceHint: 'Yellow when usage leads elapsed time by {allowance} points; red at 90% used takes priority. Saved in this browser; only colors change.',
|
|
105
|
+
usageColorRemainingHint: 'Red at 90% used; no yellow pace warnings. Reliable time markers remain visible. Saved in this browser.',
|
|
82
106
|
usagePlan: 'Plan: {plan}',
|
|
83
107
|
externalUsageTitle: 'Built-in provider usage',
|
|
84
108
|
externalUsageConnected: 'API key configured',
|
|
@@ -226,6 +250,30 @@ export const zh = {
|
|
|
226
250
|
usageMonthly: '每月',
|
|
227
251
|
usageWindow: '窗口',
|
|
228
252
|
usageResets: '{date} 重置',
|
|
253
|
+
usageMeterUsed: '已用 {used}% · 剩余 {remaining}%',
|
|
254
|
+
usageMeterPace: '时间已过 {elapsed}%(竖线标记)',
|
|
255
|
+
usageMeterPaceAhead: '领先时间进度 {points} 个百分点',
|
|
256
|
+
usageMeterPaceBehind: '落后时间进度 {points} 个百分点',
|
|
257
|
+
usageMeterPaceEven: '与时间进度一致',
|
|
258
|
+
usageMeterInvalid: '额度数据不可用',
|
|
259
|
+
usageMeterStale: '读数已陈旧或不可用,请刷新确认',
|
|
260
|
+
usageMeterNearLimit: '额度接近耗尽',
|
|
261
|
+
usageMeterAhead: '用量领先时间进度',
|
|
262
|
+
usageMeterNoWarning: '未触发额度警告',
|
|
263
|
+
usageMeterUnknownTime: '时间进度未知',
|
|
264
|
+
usageMeterReset: '{duration}后重置',
|
|
265
|
+
usageUnitDay: '{count} 天',
|
|
266
|
+
usageUnitDays: '{count} 天',
|
|
267
|
+
usageUnitHour: '{count} 小时',
|
|
268
|
+
usageUnitHours: '{count} 小时',
|
|
269
|
+
usageUnitMinute: '{count} 分钟',
|
|
270
|
+
usageUnitMinutes: '{count} 分钟',
|
|
271
|
+
usageColorLabel: '用量配色',
|
|
272
|
+
usageColorStandard: '标准(默认 · 领先 10 个百分点)',
|
|
273
|
+
usageColorRelaxed: '宽松(领先 15 个百分点)',
|
|
274
|
+
usageColorRemaining: '仅剩余额度(不显示黄色)',
|
|
275
|
+
usageColorPaceHint: '用量领先时间进度 {allowance} 个百分点时变黄;已用达到 90% 时优先变红。自动保存到当前浏览器,仅改变配色。',
|
|
276
|
+
usageColorRemainingHint: '已用达到 90% 时变红,不显示黄色节奏警告。可靠的时间标记仍保留,自动保存到当前浏览器。',
|
|
229
277
|
usagePlan: '计划:{plan}',
|
|
230
278
|
externalUsageTitle: '内置供应商用量',
|
|
231
279
|
externalUsageConnected: '已配置 API 密钥',
|
|
@@ -45,9 +45,4 @@ export const subscriptionCardStyles = {
|
|
|
45
45
|
display: 'flex', justifyContent: 'space-between', gap: 8,
|
|
46
46
|
fontSize: 12, lineHeight: '18px', color: 'var(--dsw-alias-label-tertiary)',
|
|
47
47
|
},
|
|
48
|
-
usageTrack: {
|
|
49
|
-
height: 6, borderRadius: 3, overflow: 'hidden',
|
|
50
|
-
background: 'var(--dsw-alias-bg-layer-1)', border: '1px solid var(--dsw-alias-border-l2)',
|
|
51
|
-
},
|
|
52
|
-
usageFill: { height: '100%', borderRadius: 3 },
|
|
53
48
|
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type UsageColorPreset = 'standard' | 'relaxed' | 'remaining';
|
|
2
|
+
export declare function isUsageColorPreset(value: unknown): value is UsageColorPreset;
|
|
3
|
+
export declare function readUsageColorPreset(): UsageColorPreset;
|
|
4
|
+
export declare function setUsageColorPreset(value: UsageColorPreset): void;
|
|
5
|
+
export declare function subscribeUsageColorPreset(notify: () => void): () => void;
|
|
6
|
+
export declare function useUsageColorPreset(): UsageColorPreset;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { useSyncExternalStore } from 'react';
|
|
2
|
+
const KEY = 'dsh.subscriptions.usageColorPreset';
|
|
3
|
+
const EVENT = 'dsh:subscriptions:usage-color-preset';
|
|
4
|
+
export function isUsageColorPreset(value) {
|
|
5
|
+
return value === 'standard' || value === 'relaxed' || value === 'remaining';
|
|
6
|
+
}
|
|
7
|
+
export function readUsageColorPreset() {
|
|
8
|
+
if (typeof window === 'undefined')
|
|
9
|
+
return 'standard';
|
|
10
|
+
try {
|
|
11
|
+
const value = window.localStorage.getItem(KEY);
|
|
12
|
+
return isUsageColorPreset(value) ? value : 'standard';
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (!(error instanceof DOMException) || error.name !== 'SecurityError')
|
|
16
|
+
throw error;
|
|
17
|
+
return 'standard';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function setUsageColorPreset(value) {
|
|
21
|
+
if (!isUsageColorPreset(value))
|
|
22
|
+
throw new TypeError('Invalid usage coloring preset');
|
|
23
|
+
window.localStorage.setItem(KEY, value);
|
|
24
|
+
window.dispatchEvent(new Event(EVENT));
|
|
25
|
+
}
|
|
26
|
+
export function subscribeUsageColorPreset(notify) {
|
|
27
|
+
const storage = (event) => { if (event.key === KEY || event.key === null)
|
|
28
|
+
notify(); };
|
|
29
|
+
window.addEventListener(EVENT, notify);
|
|
30
|
+
window.addEventListener('storage', storage);
|
|
31
|
+
return () => {
|
|
32
|
+
window.removeEventListener(EVENT, notify);
|
|
33
|
+
window.removeEventListener('storage', storage);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function useUsageColorPreset() {
|
|
37
|
+
return useSyncExternalStore(subscribeUsageColorPreset, readUsageColorPreset, () => 'standard');
|
|
38
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { UsageWindow } from './SubscriptionsSection.js';
|
|
2
|
+
import type { UsageColorPreset } from './usage-color-preferences.js';
|
|
3
|
+
export interface UsageObservation {
|
|
4
|
+
observedAt?: number | undefined;
|
|
5
|
+
stale?: boolean | undefined;
|
|
6
|
+
}
|
|
7
|
+
export type UsageColorState = 'neutral' | 'red' | 'yellow' | 'green';
|
|
8
|
+
export declare const USAGE_FRESHNESS_MS: number;
|
|
9
|
+
export declare function validUsage(used: number): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Rounded share for display, or undefined when the provider's number is
|
|
12
|
+
* unusable. Callers must show the unavailable state rather than clamp an
|
|
13
|
+
* out-of-range reading into 0% or 100%.
|
|
14
|
+
*/
|
|
15
|
+
export declare function displayUsedPercent(used: number): number | undefined;
|
|
16
|
+
export interface CountdownPart {
|
|
17
|
+
unit: 'day' | 'hour' | 'minute';
|
|
18
|
+
count: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Reset countdown as the two largest nonzero whole units, rounded up so a
|
|
22
|
+
* window never announces zero remaining time. Always at least one minute.
|
|
23
|
+
*/
|
|
24
|
+
export declare function resetCountdownParts(ms: number): CountdownPart[];
|
|
25
|
+
export declare function isUsageFresh(window: UsageWindow, observation: UsageObservation, now?: number): boolean;
|
|
26
|
+
/** Only explicit intervals or provider-verified fixed durations support linear pace. */
|
|
27
|
+
export declare function elapsedPercent(window: UsageWindow, now?: number): number | undefined;
|
|
28
|
+
export declare function usageColorState(used: number, elapsed: number | undefined, preset: UsageColorPreset, fresh: boolean): UsageColorState;
|
|
29
|
+
export declare function usageBarColor(used: number, elapsed?: number, preset?: UsageColorPreset, fresh?: boolean): string;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
export const USAGE_FRESHNESS_MS = 5 * 60_000;
|
|
2
|
+
export function validUsage(used) {
|
|
3
|
+
return Number.isFinite(used) && used >= 0 && used <= 100;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Rounded share for display, or undefined when the provider's number is
|
|
7
|
+
* unusable. Callers must show the unavailable state rather than clamp an
|
|
8
|
+
* out-of-range reading into 0% or 100%.
|
|
9
|
+
*/
|
|
10
|
+
export function displayUsedPercent(used) {
|
|
11
|
+
return validUsage(used) ? Math.round(used) : undefined;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Reset countdown as the two largest nonzero whole units, rounded up so a
|
|
15
|
+
* window never announces zero remaining time. Always at least one minute.
|
|
16
|
+
*/
|
|
17
|
+
export function resetCountdownParts(ms) {
|
|
18
|
+
const total = Math.max(1, Math.ceil(ms / 60_000));
|
|
19
|
+
const parts = [
|
|
20
|
+
{ unit: 'day', count: Math.floor(total / 1440) },
|
|
21
|
+
{ unit: 'hour', count: Math.floor((total % 1440) / 60) },
|
|
22
|
+
{ unit: 'minute', count: total % 60 },
|
|
23
|
+
];
|
|
24
|
+
return parts.filter(part => part.count > 0).slice(0, 2);
|
|
25
|
+
}
|
|
26
|
+
export function isUsageFresh(window, observation, now = Date.now()) {
|
|
27
|
+
return validUsage(window.usedPercent) && observation.stale !== true
|
|
28
|
+
&& observation.observedAt !== undefined && Number.isFinite(observation.observedAt)
|
|
29
|
+
&& observation.observedAt <= now && now - observation.observedAt <= USAGE_FRESHNESS_MS
|
|
30
|
+
&& (window.resetsAt === undefined || Number.isFinite(window.resetsAt) && window.resetsAt > now);
|
|
31
|
+
}
|
|
32
|
+
/** Only explicit intervals or provider-verified fixed durations support linear pace. */
|
|
33
|
+
export function elapsedPercent(window, now = Date.now()) {
|
|
34
|
+
const end = window.resetsAt;
|
|
35
|
+
if (end === undefined || !Number.isFinite(end) || end <= now)
|
|
36
|
+
return undefined;
|
|
37
|
+
const duration = window.windowDurationMs;
|
|
38
|
+
const start = window.startsAt ?? (window.fixedWindow === true && duration !== undefined && Number.isFinite(duration) && duration > 0 ? end - duration : undefined);
|
|
39
|
+
if (start === undefined || !Number.isFinite(start) || start >= end || start > now)
|
|
40
|
+
return undefined;
|
|
41
|
+
return Math.min(100, Math.max(0, (now - start) / (end - start) * 100));
|
|
42
|
+
}
|
|
43
|
+
export function usageColorState(used, elapsed, preset, fresh) {
|
|
44
|
+
if (!validUsage(used))
|
|
45
|
+
return 'neutral';
|
|
46
|
+
if (used >= 90)
|
|
47
|
+
return 'red';
|
|
48
|
+
// Missing pace or stale observations retain the last reported absolute color.
|
|
49
|
+
if (!fresh || preset === 'remaining' || elapsed === undefined)
|
|
50
|
+
return 'green';
|
|
51
|
+
return used - elapsed >= (preset === 'relaxed' ? 15 : 10) ? 'yellow' : 'green';
|
|
52
|
+
}
|
|
53
|
+
export function usageBarColor(used, elapsed, preset = 'standard', fresh = true) {
|
|
54
|
+
const state = usageColorState(used, elapsed, preset, fresh);
|
|
55
|
+
return state === 'red' ? 'var(--dsw-alias-state-error-primary)'
|
|
56
|
+
: state === 'yellow' ? 'var(--dsw-alias-state-warn-label)'
|
|
57
|
+
: 'var(--dsw-alias-state-success-primary)';
|
|
58
|
+
}
|