dsh-plugin-subscription-oauth 0.2.2 → 0.4.0
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 +13 -0
- package/README.zh.md +371 -0
- package/lib/auth/ark-control.d.ts +180 -0
- package/lib/auth/ark-control.js +304 -0
- package/lib/auth/ark-flow.d.ts +134 -0
- package/lib/auth/ark-flow.js +239 -0
- package/lib/client/UsageChip.d.ts +120 -0
- package/lib/client/UsageChip.js +232 -0
- package/lib/client/index.d.ts +27 -0
- package/lib/client/index.js +29 -0
- package/lib/client/locales.d.ts +4 -0
- package/lib/client/locales.js +4 -0
- package/lib/client/styles.d.ts +15 -1
- package/lib/client/styles.js +128 -1
- package/lib/client.js +413 -5
- package/lib/client.js.map +1 -1
- package/lib/index.d.ts +2 -2
- package/lib/index.js +15 -2
- package/lib/providers/ark-provider.d.ts +42 -0
- package/lib/providers/ark-provider.js +166 -0
- package/lib/providers/ark.d.ts +154 -0
- package/lib/providers/ark.js +412 -0
- package/lib/providers/auto.js +16 -0
- package/lib/providers/usage.js +49 -4
- package/package.json +1 -1
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The usage chip that rides the composer's bottom bar.
|
|
3
|
+
*
|
|
4
|
+
* The bar itself is a slot the shell owns. `conversation.composer.dock` is a
|
|
5
|
+
* session-scoped `list`, and the shell's own stats pills (`tok/s`, cache hit) and its
|
|
6
|
+
* context meter register into it; this chip joins as the last entry, so it reads to the
|
|
7
|
+
* right of the context meter. It is deliberately compact — an abbreviated route name
|
|
8
|
+
* and the percentage — because the bar is a status line, not a dashboard; the full
|
|
9
|
+
* figure, its reset and the meter appear when the chip is clicked.
|
|
10
|
+
*
|
|
11
|
+
* Two silences are deliberate: a provider this plugin does not own shows nothing rather
|
|
12
|
+
* than a fabricated zero, and switching to a foreign provider keeps the last own reading
|
|
13
|
+
* on screen instead of blanking the bar — the shell's own pills already speak for the
|
|
14
|
+
* foreign one.
|
|
15
|
+
*
|
|
16
|
+
* @module dsh-plugin-subscription-oauth/client/UsageChip
|
|
17
|
+
*/
|
|
18
|
+
import type { ReactNode } from 'react';
|
|
19
|
+
import type { SessionStandardProps } from '@deepseek-ai/dsh-client-ui-slots';
|
|
20
|
+
import type { SubscriptionsCall } from './subscriptions-rpc.js';
|
|
21
|
+
import type { ProviderUsage, UsageWindow } from '../protocol.js';
|
|
22
|
+
/** Injected props the chip is registered with. */
|
|
23
|
+
export interface UsageChipInjected {
|
|
24
|
+
/** The plugin's own RPC caller. */
|
|
25
|
+
call: SubscriptionsCall;
|
|
26
|
+
/** Copy for the active locale. */
|
|
27
|
+
t: (key: 'usageChipTitle' | 'usageChipFull', values?: Record<string, string | number>) => string;
|
|
28
|
+
/** Whether the provider is one this plugin serves. */
|
|
29
|
+
isOwnProvider: (provider: string) => boolean;
|
|
30
|
+
}
|
|
31
|
+
/** One model choice, as the host records it. */
|
|
32
|
+
export interface ModelSelection {
|
|
33
|
+
/** Provider route key. */
|
|
34
|
+
readonly provider: string;
|
|
35
|
+
/** Model id. */
|
|
36
|
+
readonly model: string;
|
|
37
|
+
/** Reasoning effort, when one was chosen. */
|
|
38
|
+
readonly reasoningEffort?: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The session's model-selection projection, as the host wires it.
|
|
42
|
+
*
|
|
43
|
+
* `next` is a selection the user has made but not yet used; `lastUsed` is the one a
|
|
44
|
+
* request actually ran with. Reading `next` first is what makes the chip follow the
|
|
45
|
+
* picker immediately rather than only after the next message.
|
|
46
|
+
*/
|
|
47
|
+
export interface ModelSelectionProjection {
|
|
48
|
+
/** The last selection a request ran with. */
|
|
49
|
+
readonly lastUsed: ModelSelection | null;
|
|
50
|
+
/** The selection in force now. */
|
|
51
|
+
readonly next: ModelSelection | null;
|
|
52
|
+
}
|
|
53
|
+
/** The session's provider, from its model-selection projection. */
|
|
54
|
+
export declare function providerOf(projection: ModelSelectionProjection | null | undefined): string | undefined;
|
|
55
|
+
/** What one route's allowance reduces to for a single line. */
|
|
56
|
+
export interface ChipReading {
|
|
57
|
+
/** Route the figures belong to. */
|
|
58
|
+
provider: string;
|
|
59
|
+
/** Name to show, as the route states it. */
|
|
60
|
+
label: string;
|
|
61
|
+
/** Every window that states a figure, in the order the route stated them. */
|
|
62
|
+
windows: readonly UsageWindow[];
|
|
63
|
+
/** The window worth showing in the compact line: the one closest to its limit. */
|
|
64
|
+
window: UsageWindow;
|
|
65
|
+
/** Plan name, when the provider stated one. */
|
|
66
|
+
plan?: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The short form of a route's name.
|
|
70
|
+
*
|
|
71
|
+
* @param provider - the route id.
|
|
72
|
+
* @param label - the route's own display name.
|
|
73
|
+
* @returns an abbreviation that still identifies the route.
|
|
74
|
+
*/
|
|
75
|
+
export declare function shortLabel(provider: string, label: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* The short form of a window's name.
|
|
78
|
+
*
|
|
79
|
+
* @param label - the window's own name, e.g. `weekly`.
|
|
80
|
+
* @returns an abbreviation, or the label itself when it is already short.
|
|
81
|
+
*/
|
|
82
|
+
export declare function shortWindow(label: string): string;
|
|
83
|
+
/**
|
|
84
|
+
* Pick the window a single line should show.
|
|
85
|
+
*
|
|
86
|
+
* A plan can meter several windows at once (a rolling five-hour one and a weekly one).
|
|
87
|
+
* The most constrained is the one that will stop work soonest, so it earns the space;
|
|
88
|
+
* ties fall to the earlier window, which is the order the providers state them in.
|
|
89
|
+
*
|
|
90
|
+
* @param windows - the route's metered windows.
|
|
91
|
+
* @returns the window to show, or undefined when none states a figure.
|
|
92
|
+
*/
|
|
93
|
+
export declare function pickWindow(windows: readonly UsageWindow[]): UsageWindow | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Reduce one route's usage to what the chip renders.
|
|
96
|
+
*
|
|
97
|
+
* @param provider - the route the figures came from.
|
|
98
|
+
* @param label - the route's display name.
|
|
99
|
+
* @param usage - the accounts the route answered for.
|
|
100
|
+
* @returns the reading, or undefined when the route stated no figure.
|
|
101
|
+
*/
|
|
102
|
+
export declare function readingOf(provider: string, label: string, usage: readonly ProviderUsage[]): ChipReading | undefined;
|
|
103
|
+
/**
|
|
104
|
+
* Format a window's reset the way the settings cards do.
|
|
105
|
+
*
|
|
106
|
+
* The rule is shared with the cards on purpose: a figure reading `6d20h` here and
|
|
107
|
+
* something else there would be two answers to one question.
|
|
108
|
+
*
|
|
109
|
+
* @param at - epoch milliseconds, or undefined when the provider states none.
|
|
110
|
+
* @param now - the instant to measure from.
|
|
111
|
+
* @returns a short phrase, or undefined when there is no reset to show.
|
|
112
|
+
*/
|
|
113
|
+
export declare function formatReset(at: number | undefined, now: number): string | undefined;
|
|
114
|
+
/**
|
|
115
|
+
* The composer's usage chip.
|
|
116
|
+
*
|
|
117
|
+
* @param props - injected RPC caller, copy, and the session's provider reader.
|
|
118
|
+
* @returns the chip, or null when there is nothing true to say.
|
|
119
|
+
*/
|
|
120
|
+
export declare function UsageChip({ call, t, isOwnProvider, useProjection }: UsageChipInjected & SessionStandardProps): ReactNode;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The usage chip that rides the composer's bottom bar.
|
|
4
|
+
*
|
|
5
|
+
* The bar itself is a slot the shell owns. `conversation.composer.dock` is a
|
|
6
|
+
* session-scoped `list`, and the shell's own stats pills (`tok/s`, cache hit) and its
|
|
7
|
+
* context meter register into it; this chip joins as the last entry, so it reads to the
|
|
8
|
+
* right of the context meter. It is deliberately compact — an abbreviated route name
|
|
9
|
+
* and the percentage — because the bar is a status line, not a dashboard; the full
|
|
10
|
+
* figure, its reset and the meter appear when the chip is clicked.
|
|
11
|
+
*
|
|
12
|
+
* Two silences are deliberate: a provider this plugin does not own shows nothing rather
|
|
13
|
+
* than a fabricated zero, and switching to a foreign provider keeps the last own reading
|
|
14
|
+
* on screen instead of blanking the bar — the shell's own pills already speak for the
|
|
15
|
+
* foreign one.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-plugin-subscription-oauth/client/UsageChip
|
|
18
|
+
*/
|
|
19
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
20
|
+
import { usageChipCss } from './styles.js';
|
|
21
|
+
/** How often a visible chip refreshes its figures. */
|
|
22
|
+
const REFRESH_INTERVAL_MS = 60_000;
|
|
23
|
+
/**
|
|
24
|
+
* Abbreviations for the routes this plugin serves.
|
|
25
|
+
*
|
|
26
|
+
* The bar is a status line under the composer, where a full product name crowds out the
|
|
27
|
+
* shell's own pills; the full name is one click away. The fallback shortens an unknown
|
|
28
|
+
* route's display name rather than dropping it.
|
|
29
|
+
*/
|
|
30
|
+
const SHORT_LABELS = {
|
|
31
|
+
kimi: 'Kimi',
|
|
32
|
+
grok: 'Grok',
|
|
33
|
+
antigravity: 'Anti',
|
|
34
|
+
glm: 'GLM',
|
|
35
|
+
ark: 'Ark',
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Abbreviations for the window names the providers use.
|
|
39
|
+
*
|
|
40
|
+
* The bar has room for a token or two, so `weekly` becomes `7d` — the same figure in a
|
|
41
|
+
* form that reads at a glance. An unrecognised window keeps its own name, shortened
|
|
42
|
+
* only if it is long.
|
|
43
|
+
*/
|
|
44
|
+
const SHORT_WINDOWS = {
|
|
45
|
+
'weekly': '7d',
|
|
46
|
+
'7d': '7d',
|
|
47
|
+
'5h': '5h',
|
|
48
|
+
'monthly': 'mo',
|
|
49
|
+
'daily': '1d',
|
|
50
|
+
'credits': 'cr',
|
|
51
|
+
'free': 'free',
|
|
52
|
+
'session': 's',
|
|
53
|
+
};
|
|
54
|
+
/** The session's provider, from its model-selection projection. */
|
|
55
|
+
export function providerOf(projection) {
|
|
56
|
+
return (projection?.next ?? projection?.lastUsed)?.provider;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The short form of a route's name.
|
|
60
|
+
*
|
|
61
|
+
* @param provider - the route id.
|
|
62
|
+
* @param label - the route's own display name.
|
|
63
|
+
* @returns an abbreviation that still identifies the route.
|
|
64
|
+
*/
|
|
65
|
+
export function shortLabel(provider, label) {
|
|
66
|
+
const known = SHORT_LABELS[provider];
|
|
67
|
+
if (known !== undefined)
|
|
68
|
+
return known;
|
|
69
|
+
// An unknown route keeps its first word, so the chip names something recognisable
|
|
70
|
+
// rather than a generic placeholder.
|
|
71
|
+
const [first] = label.trim().split(/\s+/);
|
|
72
|
+
return first === undefined || first === '' ? provider : first;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The short form of a window's name.
|
|
76
|
+
*
|
|
77
|
+
* @param label - the window's own name, e.g. `weekly`.
|
|
78
|
+
* @returns an abbreviation, or the label itself when it is already short.
|
|
79
|
+
*/
|
|
80
|
+
export function shortWindow(label) {
|
|
81
|
+
const known = SHORT_WINDOWS[label.toLowerCase()];
|
|
82
|
+
if (known !== undefined)
|
|
83
|
+
return known;
|
|
84
|
+
return label.length <= 4 ? label : label.slice(0, 4);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Pick the window a single line should show.
|
|
88
|
+
*
|
|
89
|
+
* A plan can meter several windows at once (a rolling five-hour one and a weekly one).
|
|
90
|
+
* The most constrained is the one that will stop work soonest, so it earns the space;
|
|
91
|
+
* ties fall to the earlier window, which is the order the providers state them in.
|
|
92
|
+
*
|
|
93
|
+
* @param windows - the route's metered windows.
|
|
94
|
+
* @returns the window to show, or undefined when none states a figure.
|
|
95
|
+
*/
|
|
96
|
+
export function pickWindow(windows) {
|
|
97
|
+
let best;
|
|
98
|
+
for (const window of windows) {
|
|
99
|
+
if (window.usedPercent === undefined)
|
|
100
|
+
continue;
|
|
101
|
+
if (best?.usedPercent === undefined || window.usedPercent > best.usedPercent)
|
|
102
|
+
best = window;
|
|
103
|
+
}
|
|
104
|
+
return best;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reduce one route's usage to what the chip renders.
|
|
108
|
+
*
|
|
109
|
+
* @param provider - the route the figures came from.
|
|
110
|
+
* @param label - the route's display name.
|
|
111
|
+
* @param usage - the accounts the route answered for.
|
|
112
|
+
* @returns the reading, or undefined when the route stated no figure.
|
|
113
|
+
*/
|
|
114
|
+
export function readingOf(provider, label, usage) {
|
|
115
|
+
for (const entry of usage) {
|
|
116
|
+
const window = pickWindow(entry.windows);
|
|
117
|
+
if (window === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
const windows = entry.windows.filter((candidate) => candidate.usedPercent !== undefined);
|
|
120
|
+
return {
|
|
121
|
+
provider,
|
|
122
|
+
label,
|
|
123
|
+
windows,
|
|
124
|
+
window,
|
|
125
|
+
...entry.plan === undefined ? {} : { plan: entry.plan },
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Format a window's reset the way the settings cards do.
|
|
132
|
+
*
|
|
133
|
+
* The rule is shared with the cards on purpose: a figure reading `6d20h` here and
|
|
134
|
+
* something else there would be two answers to one question.
|
|
135
|
+
*
|
|
136
|
+
* @param at - epoch milliseconds, or undefined when the provider states none.
|
|
137
|
+
* @param now - the instant to measure from.
|
|
138
|
+
* @returns a short phrase, or undefined when there is no reset to show.
|
|
139
|
+
*/
|
|
140
|
+
export function formatReset(at, now) {
|
|
141
|
+
if (at === undefined)
|
|
142
|
+
return undefined;
|
|
143
|
+
const remaining = at - now;
|
|
144
|
+
if (remaining <= 0)
|
|
145
|
+
return 'now';
|
|
146
|
+
const hours = remaining / 3_600_000;
|
|
147
|
+
if (hours < 1)
|
|
148
|
+
return `${String(Math.max(1, Math.round(remaining / 60_000)))}m`;
|
|
149
|
+
if (hours < 24)
|
|
150
|
+
return `${String(Math.round(hours))}h`;
|
|
151
|
+
const days = Math.floor(hours / 24);
|
|
152
|
+
return `${String(days)}d${String(Math.round(hours - days * 24))}h`;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* The composer's usage chip.
|
|
156
|
+
*
|
|
157
|
+
* @param props - injected RPC caller, copy, and the session's provider reader.
|
|
158
|
+
* @returns the chip, or null when there is nothing true to say.
|
|
159
|
+
*/
|
|
160
|
+
export function UsageChip({ call, t, isOwnProvider, useProjection }) {
|
|
161
|
+
const [reading, setReading] = useState(undefined);
|
|
162
|
+
const [now, setNow] = useState(() => Date.now());
|
|
163
|
+
const [open, setOpen] = useState(false);
|
|
164
|
+
// The last route that answered, so a switch to a foreign provider leaves the last own
|
|
165
|
+
// reading up rather than blanking the bar.
|
|
166
|
+
const lastOwn = useRef(undefined);
|
|
167
|
+
// A projection read, so the chip follows the picker: changing the model re-renders it
|
|
168
|
+
// with the new provider instead of waiting for the next unrelated render.
|
|
169
|
+
const selected = useProjection('modelSelection', (value) => providerOf(value));
|
|
170
|
+
const load = useCallback(async (target) => {
|
|
171
|
+
try {
|
|
172
|
+
const result = await call('usage', { provider: target });
|
|
173
|
+
const label = result.status.providers.find((entry) => entry.provider === target)?.displayName ?? target;
|
|
174
|
+
return readingOf(target, label, result.usage);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
// A failed read is not worth a chip of its own: the settings page is where a user
|
|
178
|
+
// goes to find out why a route cannot report.
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
}, [call]);
|
|
182
|
+
useEffect(() => {
|
|
183
|
+
const target = selected !== undefined && isOwnProvider(selected)
|
|
184
|
+
? selected
|
|
185
|
+
// Not one of ours (or none selected): keep showing the last own reading.
|
|
186
|
+
: lastOwn.current;
|
|
187
|
+
if (target === undefined) {
|
|
188
|
+
setReading(undefined);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
let live = true;
|
|
192
|
+
const refresh = () => {
|
|
193
|
+
setNow(Date.now());
|
|
194
|
+
void load(target).then((next) => {
|
|
195
|
+
if (!live || next === undefined)
|
|
196
|
+
return;
|
|
197
|
+
lastOwn.current = target;
|
|
198
|
+
setReading(next);
|
|
199
|
+
});
|
|
200
|
+
};
|
|
201
|
+
refresh();
|
|
202
|
+
const timer = setInterval(refresh, REFRESH_INTERVAL_MS);
|
|
203
|
+
return () => {
|
|
204
|
+
live = false;
|
|
205
|
+
clearInterval(timer);
|
|
206
|
+
};
|
|
207
|
+
}, [selected, isOwnProvider, load]);
|
|
208
|
+
// A click anywhere else puts the detail away, which is what a status chip should do.
|
|
209
|
+
useEffect(() => {
|
|
210
|
+
if (!open)
|
|
211
|
+
return;
|
|
212
|
+
const close = () => { setOpen(false); };
|
|
213
|
+
document.addEventListener('click', close);
|
|
214
|
+
return () => { document.removeEventListener('click', close); };
|
|
215
|
+
}, [open]);
|
|
216
|
+
if (reading === undefined)
|
|
217
|
+
return null;
|
|
218
|
+
const percent = reading.window.usedPercent;
|
|
219
|
+
if (percent === undefined)
|
|
220
|
+
return null;
|
|
221
|
+
const reset = formatReset(reading.window.resetsAt, now);
|
|
222
|
+
const clamped = Math.max(0, Math.min(100, percent));
|
|
223
|
+
const short = shortLabel(reading.provider, reading.label);
|
|
224
|
+
return (_jsxs("span", { className: "dsh-subscription-usage-chip", children: [_jsx("style", { children: usageChipCss }), _jsxs("button", { type: "button", className: "dsh-subscription-usage-chip-trigger", "aria-expanded": open, title: t('usageChipTitle', { provider: reading.label }), onClick: (event) => {
|
|
225
|
+
// The document listener would otherwise close what this click just opened.
|
|
226
|
+
event.stopPropagation();
|
|
227
|
+
setOpen((was) => !was);
|
|
228
|
+
}, children: [_jsx("span", { className: "dsh-subscription-usage-chip-label", children: short }), reading.windows.map((window) => (_jsxs("span", { className: "dsh-subscription-usage-chip-cell", children: [_jsx("span", { className: "dsh-subscription-usage-chip-window", children: shortWindow(window.label) }), _jsx("span", { className: "dsh-subscription-usage-chip-value", children: `${String(Math.round(window.usedPercent ?? 0))}%` })] }, window.label)))] }), open && (_jsxs("span", { className: "dsh-subscription-usage-chip-panel", role: "status", children: [_jsx("span", { className: "dsh-subscription-usage-chip-full", children: t('usageChipFull', { provider: reading.label }) }), reading.windows.map((window) => {
|
|
229
|
+
const windowReset = formatReset(window.resetsAt, now);
|
|
230
|
+
return (_jsxs("span", { className: "dsh-subscription-usage-chip-meter", children: [_jsxs("span", { className: "dsh-subscription-usage-chip-meterHead", children: [_jsx("span", { children: window.label }), _jsx("span", { className: "dsh-subscription-usage-chip-detail", children: `${String(Math.round(window.usedPercent ?? 0))}%${windowReset === undefined ? '' : ` · ${windowReset}`}` })] }), _jsx("span", { className: "dsh-subscription-usage-chip-track", "aria-hidden": "true", children: _jsx("span", { className: "dsh-subscription-usage-chip-fill", style: { width: `${String(Math.max(0, Math.min(100, window.usedPercent ?? 0)))}%` } }) })] }, window.label));
|
|
231
|
+
}), reading.plan === undefined ? null : (_jsx("span", { className: "dsh-subscription-usage-chip-detail", children: reading.plan }))] }))] }));
|
|
232
|
+
}
|
package/lib/client/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { Context as ClientContext } from '@deepseek-ai/cordis';
|
|
12
12
|
import type { SubscriptionsKey } from './locales.js';
|
|
13
13
|
export type { SubscriptionsSectionInjected, SubscriptionsSectionProps } from './SubscriptionsSection.js';
|
|
14
|
+
export type { UsageChipInjected } from './UsageChip.js';
|
|
14
15
|
export type { SubscriptionsKey } from './locales.js';
|
|
15
16
|
export { createSubscriptionsCall, SubscriptionsRpcError } from './subscriptions-rpc.js';
|
|
16
17
|
export type { SubscriptionsCall } from './subscriptions-rpc.js';
|
|
@@ -19,6 +20,32 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
|
|
|
19
20
|
/** The Subscriptions settings page copy. */
|
|
20
21
|
'settings.subscriptions': SubscriptionsKey;
|
|
21
22
|
}
|
|
23
|
+
interface SlotMap {
|
|
24
|
+
/**
|
|
25
|
+
* The composer's bottom bar.
|
|
26
|
+
*
|
|
27
|
+
* Declared here as the shell declares it — a session-scoped list — because the
|
|
28
|
+
* owning package ships no type declarations to a plugin that only consumes it.
|
|
29
|
+
* The shell's own stats pills are one entry in this list; this plugin's usage chip
|
|
30
|
+
* is another.
|
|
31
|
+
*/
|
|
32
|
+
'conversation.composer.dock': {
|
|
33
|
+
kind: 'list';
|
|
34
|
+
scope: 'session';
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The session standard kit, of which this plugin reads one member.
|
|
39
|
+
*
|
|
40
|
+
* `ui-session` merges the session identity and the projection reader into every
|
|
41
|
+
* session-scoped component's props, and it ships no declarations a plugin can see, so
|
|
42
|
+
* the member is declared here. It reads one session projection, optionally narrowed
|
|
43
|
+
* by a selector, and re-renders the component when that projection changes — which is
|
|
44
|
+
* what lets the usage chip follow the model picker.
|
|
45
|
+
*/
|
|
46
|
+
interface SessionStandardProps {
|
|
47
|
+
useProjection<T>(key: string, selector: (value: unknown) => T): T;
|
|
48
|
+
}
|
|
22
49
|
}
|
|
23
50
|
/**
|
|
24
51
|
* Required services: `slots` carries the registration seat, `connection` the
|
package/lib/client/index.js
CHANGED
|
@@ -9,13 +9,28 @@
|
|
|
9
9
|
* @module dsh-plugin-subscription-oauth/client
|
|
10
10
|
*/
|
|
11
11
|
import { SubscriptionsSection } from './SubscriptionsSection.js';
|
|
12
|
+
import { UsageChip } from './UsageChip.js';
|
|
12
13
|
import { createSubscriptionsCall } from './subscriptions-rpc.js';
|
|
13
14
|
import { en, zh } from './locales.js';
|
|
14
15
|
export { createSubscriptionsCall, SubscriptionsRpcError } from './subscriptions-rpc.js';
|
|
16
|
+
/**
|
|
17
|
+
* Routes this plugin serves.
|
|
18
|
+
*
|
|
19
|
+
* The chip only reports for these; a provider somebody else registered gets no chip,
|
|
20
|
+
* because this plugin has no way to read its allowance.
|
|
21
|
+
*/
|
|
22
|
+
const OWN_PROVIDER_IDS = new Set(['kimi', 'grok', 'antigravity', 'glm', 'ark']);
|
|
15
23
|
/** Dictionary namespace owned by this plugin. */
|
|
16
24
|
const NS = 'settings.subscriptions';
|
|
17
25
|
/** Settings nav position: after the shipped sections, before plugin tooling. */
|
|
18
26
|
const SECTION_ORDER = 92;
|
|
27
|
+
/**
|
|
28
|
+
* Composer-dock position: after the shell's own stats pills, which sit at 0.
|
|
29
|
+
*
|
|
30
|
+
* The shell owns that bar and registers `tok/s` and the cache-hit pill into it, so the
|
|
31
|
+
* chip follows them rather than displacing them.
|
|
32
|
+
*/
|
|
33
|
+
const CHIP_ORDER = 10;
|
|
19
34
|
/**
|
|
20
35
|
* Required services: `slots` carries the registration seat, `connection` the
|
|
21
36
|
* RPC caller, and `locale` the copy dictionaries.
|
|
@@ -44,4 +59,18 @@ export function apply(ctx) {
|
|
|
44
59
|
label: () => t('nav'),
|
|
45
60
|
inject: () => ({ call, t }),
|
|
46
61
|
}, SubscriptionsSection));
|
|
62
|
+
// The composer's bottom bar is a session-scoped list the shell owns; its own stats
|
|
63
|
+
// pills sit at order 0. This chip rides beside them and shows the allowance of the
|
|
64
|
+
// provider the session is using. The scope hands `inject` the session id, but the chip
|
|
65
|
+
// reads the selection from its own projection prop, so nothing here needs it.
|
|
66
|
+
ctx.slots.inject('conversation.composer.dock', () => ctx.slots.register({
|
|
67
|
+
name: 'conversation.composer.dock',
|
|
68
|
+
id: 'subscription-usage',
|
|
69
|
+
order: CHIP_ORDER,
|
|
70
|
+
inject: () => ({
|
|
71
|
+
call,
|
|
72
|
+
t: t,
|
|
73
|
+
isOwnProvider: (provider) => OWN_PROVIDER_IDS.has(provider),
|
|
74
|
+
}),
|
|
75
|
+
}, UsageChip));
|
|
47
76
|
}
|
package/lib/client/locales.d.ts
CHANGED
|
@@ -96,6 +96,8 @@ export declare const en: {
|
|
|
96
96
|
readonly lastError: "Last login attempt failed: {message}";
|
|
97
97
|
readonly loginFailed: "Could not start the sign-in: {message}";
|
|
98
98
|
readonly actionFailed: "Action failed: {message}";
|
|
99
|
+
readonly usageChipTitle: "{provider} subscription usage";
|
|
100
|
+
readonly usageChipFull: "{provider} allowance";
|
|
99
101
|
};
|
|
100
102
|
/** The Subscriptions namespace key union. */
|
|
101
103
|
export type SubscriptionsKey = keyof typeof en;
|
|
@@ -190,4 +192,6 @@ export declare const zh: {
|
|
|
190
192
|
lastError: string;
|
|
191
193
|
loginFailed: string;
|
|
192
194
|
actionFailed: string;
|
|
195
|
+
usageChipTitle: string;
|
|
196
|
+
usageChipFull: string;
|
|
193
197
|
};
|
package/lib/client/locales.js
CHANGED
|
@@ -96,6 +96,8 @@ export const en = {
|
|
|
96
96
|
lastError: 'Last login attempt failed: {message}',
|
|
97
97
|
loginFailed: 'Could not start the sign-in: {message}',
|
|
98
98
|
actionFailed: 'Action failed: {message}',
|
|
99
|
+
usageChipTitle: '{provider} subscription usage',
|
|
100
|
+
usageChipFull: '{provider} allowance',
|
|
99
101
|
};
|
|
100
102
|
/** Chinese strings. */
|
|
101
103
|
export const zh = {
|
|
@@ -188,4 +190,6 @@ export const zh = {
|
|
|
188
190
|
lastError: '上次登录失败:{message}',
|
|
189
191
|
loginFailed: '无法开始登录:{message}',
|
|
190
192
|
actionFailed: '操作失败:{message}',
|
|
193
|
+
usageChipTitle: '{provider} 订阅用量',
|
|
194
|
+
usageChipFull: '{provider} 额度',
|
|
191
195
|
};
|
package/lib/client/styles.d.ts
CHANGED
|
@@ -15,4 +15,18 @@
|
|
|
15
15
|
* @module dsh-plugin-subscription-oauth/client/styles
|
|
16
16
|
*/
|
|
17
17
|
/** The page's and dialog's stylesheet. */
|
|
18
|
-
export declare const subscriptionsCss = "\n.dsh-subscriptions-settings,\n.dsh-subscription-manager {\n font-family: inherit;\n font-size: 13px;\n line-height: 1.5;\n /* Native controls (checkbox, dialog) follow the host theme, not the OS. */\n color-scheme: light;\n}\nbody[data-ds-dark-theme] :is(.dsh-subscriptions-settings, .dsh-subscription-manager) {\n color-scheme: dark;\n}\n.dsh-subscription-manager {\n box-sizing: border-box;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-panel, 16px);\n background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));\n color: var(--dsw-alias-label-primary);\n box-shadow: var(--dsw-elevation-prominent, 0 20px 70px #0004);\n}\n.dsh-subscription-manager::backdrop {\n background: var(--dsw-alias-bg-mask-1, #0006);\n backdrop-filter: var(--dsw-mask-blur, none);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) h2 {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) strong,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-label {\n font-size: 13px;\n line-height: 1.5;\n font-weight: 500;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) small,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-hint,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) [role=\"status\"] {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary, #b42318);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-success {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary, #067647);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) label:has(> input[type=\"checkbox\"]) {\n cursor: pointer;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"] {\n accent-color: var(--dsw-alias-brand-primary);\n}\n/*\n * A model's on/off control is a switch, not a checkbox: it takes effect\n * immediately and its state is what matters, not a tick. Drawn from the theme\n * tokens with appearance: none, so it follows the host palette in both themes\n * instead of the platform's checkbox chrome.\n */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch {\n appearance: none;\n -webkit-appearance: none;\n position: relative;\n box-sizing: border-box;\n flex-shrink: 0;\n width: 34px;\n height: 20px;\n margin: 0;\n border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch::after {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary-foreground, #fff);\n box-shadow: 0 1px 2px #0003;\n transition: transform 120ms ease;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch:checked {\n border-color: transparent;\n background: var(--dsw-alias-state-success-primary, var(--dsw-alias-brand-primary, #067647));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch:checked::after {\n transform: translateX(14px);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch:disabled {\n opacity: 0.4;\n cursor: default;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-height: 28px;\n padding: 0 10px;\n border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-3));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:active:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-3));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n/* A control that is present but should not read as a bordered button. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-quiet {\n min-height: 22px;\n padding: 0 4px;\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, var(--dsw-alias-label-primary));\n}\n/* The free-text field for a hand-added model id. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input.dsh-subscription-input {\n box-sizing: border-box;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 28px;\n padding: 0 10px;\n border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-radius: var(--dsw-radius-sm, 6px);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input.dsh-subscription-input::placeholder {\n color: var(--dsw-alias-label-tertiary);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input.dsh-subscription-input:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 2px;\n}\n/* A drag handle: no border or fill, so it reads as a grip rather than a button. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-handle {\n min-height: 18px;\n height: 18px;\n padding: 0 2px;\n border-color: transparent;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n font-size: 12px;\n line-height: 18px;\n cursor: grab;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-handle:hover:not(:disabled) {\n background: transparent;\n color: var(--dsw-alias-label-primary);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-handle:active:not(:disabled) { cursor: grabbing; }\n/* Controls inside the manage dialog are one step larger, like a form footer. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-dialog-actions button {\n min-height: 34px;\n padding: 0 14px;\n border-radius: var(--dsw-radius-md, 8px);\n font-size: 13px;\n line-height: 1.5;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-primary {\n border-color: transparent;\n background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-label-primary));\n color: var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-layer-1));\n font-weight: 500;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover, var(--dsw-alias-label-secondary));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:focus-visible,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"]:focus-visible,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-disclosure:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 2px;\n}\n/* The catalog heading is a summary-style disclosure, not a bordered button. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-disclosure {\n border-color: transparent;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n padding: 0 4px;\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary, var(--dsw-alias-label-primary));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-disclosure:hover:not(:disabled) {\n background: transparent;\n color: var(--dsw-alias-label-primary);\n}\n/* A usage meter: the track, with the fill sized by the caller. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-meter {\n height: 6px;\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));\n overflow: hidden;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-meter > span {\n display: block;\n height: 100%;\n background: var(--dsw-alias-state-success-primary, #067647);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-meter > span[data-exhausted=\"true\"] {\n background: var(--dsw-alias-state-error-primary, #b42318);\n}\n";
|
|
18
|
+
export declare const subscriptionsCss = "\n.dsh-subscriptions-settings,\n.dsh-subscription-manager {\n font-family: inherit;\n font-size: 13px;\n line-height: 1.5;\n /* Native controls (checkbox, dialog) follow the host theme, not the OS. */\n color-scheme: light;\n}\nbody[data-ds-dark-theme] :is(.dsh-subscriptions-settings, .dsh-subscription-manager) {\n color-scheme: dark;\n}\n.dsh-subscription-manager {\n box-sizing: border-box;\n padding: 0;\n border: 0;\n border-radius: var(--dsw-radius-panel, 16px);\n background: var(--dsw-alias-bg-layer-2, var(--dsw-alias-bg-layer-1));\n color: var(--dsw-alias-label-primary);\n box-shadow: var(--dsw-elevation-prominent, 0 20px 70px #0004);\n}\n.dsh-subscription-manager::backdrop {\n background: var(--dsw-alias-bg-mask-1, #0006);\n backdrop-filter: var(--dsw-mask-blur, none);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) h2 {\n margin: 0;\n font-size: 16px;\n line-height: 24px;\n font-weight: 500;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) strong,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-label {\n font-size: 13px;\n line-height: 1.5;\n font-weight: 500;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) small,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-hint,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) [role=\"status\"] {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-tertiary);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-error {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-error-primary, #b42318);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-success {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-state-success-primary, #067647);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) label:has(> input[type=\"checkbox\"]) {\n cursor: pointer;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"] {\n accent-color: var(--dsw-alias-brand-primary);\n}\n/*\n * A model's on/off control is a switch, not a checkbox: it takes effect\n * immediately and its state is what matters, not a tick. Drawn from the theme\n * tokens with appearance: none, so it follows the host palette in both themes\n * instead of the platform's checkbox chrome.\n */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch {\n appearance: none;\n -webkit-appearance: none;\n position: relative;\n box-sizing: border-box;\n flex-shrink: 0;\n width: 34px;\n height: 20px;\n margin: 0;\n border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-3, var(--dsw-alias-bg-layer-2));\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch::after {\n content: '';\n position: absolute;\n top: 2px;\n left: 2px;\n width: 14px;\n height: 14px;\n border-radius: 50%;\n background: var(--dsw-alias-label-primary-foreground, #fff);\n box-shadow: 0 1px 2px #0003;\n transition: transform 120ms ease;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch:checked {\n border-color: transparent;\n background: var(--dsw-alias-state-success-primary, var(--dsw-alias-brand-primary, #067647));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch:checked::after {\n transform: translateX(14px);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"].dsh-subscription-switch:disabled {\n opacity: 0.4;\n cursor: default;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex-shrink: 0;\n min-height: 28px;\n padding: 0 10px;\n border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:hover:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-hover, var(--dsw-alias-bg-layer-3));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:active:not(:disabled) {\n background: var(--dsw-alias-interactive-bg-active, var(--dsw-alias-bg-layer-3));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:disabled {\n opacity: 0.4;\n cursor: default;\n}\n/* A control that is present but should not read as a bordered button. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-quiet {\n min-height: 22px;\n padding: 0 4px;\n border-color: transparent;\n color: var(--dsw-alias-label-secondary, var(--dsw-alias-label-primary));\n}\n/* The free-text field for a hand-added model id. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input.dsh-subscription-input {\n box-sizing: border-box;\n flex: 1 1 auto;\n min-width: 0;\n min-height: 28px;\n padding: 0 10px;\n border: 0.5px solid var(--dsw-alias-border-l3, var(--dsw-alias-border-l2));\n border-radius: var(--dsw-radius-sm, 6px);\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font: inherit;\n font-size: 12px;\n line-height: 18px;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input.dsh-subscription-input::placeholder {\n color: var(--dsw-alias-label-tertiary);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input.dsh-subscription-input:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 2px;\n}\n/* A drag handle: no border or fill, so it reads as a grip rather than a button. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-handle {\n min-height: 18px;\n height: 18px;\n padding: 0 2px;\n border-color: transparent;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-tertiary, var(--dsw-alias-label-secondary));\n font-size: 12px;\n line-height: 18px;\n cursor: grab;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-handle:hover:not(:disabled) {\n background: transparent;\n color: var(--dsw-alias-label-primary);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-handle:active:not(:disabled) { cursor: grabbing; }\n/* Controls inside the manage dialog are one step larger, like a form footer. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-dialog-actions button {\n min-height: 34px;\n padding: 0 14px;\n border-radius: var(--dsw-radius-md, 8px);\n font-size: 13px;\n line-height: 1.5;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-primary {\n border-color: transparent;\n background: var(--dsw-alias-button-primary-fill, var(--dsw-alias-label-primary));\n color: var(--dsw-alias-label-primary-foreground, var(--dsw-alias-bg-layer-1));\n font-weight: 500;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button.dsh-subscription-primary:hover:not(:disabled) {\n background: var(--dsw-alias-button-primary-hover, var(--dsw-alias-label-secondary));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) button:focus-visible,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) input[type=\"checkbox\"]:focus-visible,\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-disclosure:focus-visible {\n outline: var(--dsw-focus-ring-width, 2px) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));\n outline-offset: 2px;\n}\n/* The catalog heading is a summary-style disclosure, not a bordered button. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-disclosure {\n border-color: transparent;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n padding: 0 4px;\n font-size: 12px;\n line-height: 18px;\n font-weight: 500;\n color: var(--dsw-alias-label-secondary, var(--dsw-alias-label-primary));\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-disclosure:hover:not(:disabled) {\n background: transparent;\n color: var(--dsw-alias-label-primary);\n}\n/* A usage meter: the track, with the fill sized by the caller. */\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-meter {\n height: 6px;\n border-radius: 999px;\n /* The track must stay visible when nothing is spent \u2014 a 0 % window otherwise reads as\n \"no meter at all\", which is the opposite of \"nothing used\". The layer token falls\n back to the card's own colour on a host that does not define it, so the neutral is\n explicit and translucent, and the inset ring keeps the empty track visible on a\n light theme as well as a dark one. */\n background: #8a8f9826;\n box-shadow: inset 0 0 0 1px #8a8f9833;\n overflow: hidden;\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-meter > span {\n display: block;\n height: 100%;\n background: var(--dsw-alias-state-success-primary, #067647);\n}\n:is(.dsh-subscriptions-settings, .dsh-subscription-manager) .dsh-subscription-meter > span[data-exhausted=\"true\"] {\n background: var(--dsw-alias-state-error-primary, #b42318);\n}\n";
|
|
19
|
+
/**
|
|
20
|
+
* The composer usage chip's stylesheet.
|
|
21
|
+
*
|
|
22
|
+
* Separate from the page's, because the chip renders in the composer's own bar rather
|
|
23
|
+
* than inside either page container — the page stylesheet is scoped to those
|
|
24
|
+
* containers on purpose, so nothing here may leak into the shell.
|
|
25
|
+
*
|
|
26
|
+
* The chip inherits the bar's type and only draws a meter, so it sits beside the shell's
|
|
27
|
+
* own stats pills without looking like a second design. The trigger is a bare control:
|
|
28
|
+
* the host gives plugin markup no control classes, and a default `<button>` would
|
|
29
|
+
* otherwise read as a raised chip. Every token carries a fallback for hosts that do not
|
|
30
|
+
* define it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const usageChipCss = "\n.dsh-subscription-usage-chip {\n position: relative;\n display: inline-flex;\n align-items: center;\n /* The bar is a centred flex row whose children are this slot's entries followed by the\n shell's context meter, and a list slot renders its entries as bare children (no\n wrapper). So the chip is itself a flex item: a higher order places it after the\n context meter, and no auto margin is set, so it sits immediately beside it rather\n than being pushed to the edge of the bar. */\n order: 1;\n min-width: 0;\n font-family: inherit;\n font-size: 12px;\n line-height: 1;\n}\n.dsh-subscription-usage-chip-trigger {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin: 0;\n padding: 2px 6px;\n border: 0;\n border-radius: var(--dsw-radius-sm, 6px);\n background: transparent;\n color: var(--dsw-alias-label-secondary, #8a8f98);\n font: inherit;\n cursor: pointer;\n}\n.dsh-subscription-usage-chip-trigger:hover {\n background: var(--dsw-alias-interactive-bg-hover, #ffffff14);\n}\n.dsh-subscription-usage-chip-trigger:focus-visible {\n outline: 2px solid var(--dsw-alias-border-focus, #4d6bfe);\n outline-offset: 1px;\n}\n.dsh-subscription-usage-chip-label {\n overflow: hidden;\n max-width: 6ch;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n.dsh-subscription-usage-chip-cell {\n display: inline-flex;\n align-items: baseline;\n gap: 2px;\n}\n.dsh-subscription-usage-chip-window {\n opacity: 0.75;\n}\n.dsh-subscription-usage-chip-value {\n color: var(--dsw-alias-label-primary, #e8e8e8);\n font-variant-numeric: tabular-nums;\n}\n.dsh-subscription-usage-chip-panel {\n position: absolute;\n bottom: calc(100% + 6px);\n right: 0;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 8px;\n min-width: 168px;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-secondary, #ffffff1f);\n border-radius: var(--dsw-radius-md, 10px);\n background: var(--dsw-alias-bg-layer-2, #26262a);\n box-shadow: var(--dsw-elevation-prominent, 0 12px 32px #0007);\n color: var(--dsw-alias-label-primary, #e8e8e8);\n white-space: nowrap;\n}\n.dsh-subscription-usage-chip-full {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary, #8a8f98);\n}\n.dsh-subscription-usage-chip-meter {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n.dsh-subscription-usage-chip-meterHead {\n display: flex;\n align-items: baseline;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n.dsh-subscription-usage-chip-detail {\n color: var(--dsw-alias-label-secondary, #8a8f98);\n font-variant-numeric: tabular-nums;\n}\n.dsh-subscription-usage-chip-track {\n display: block;\n width: 100%;\n height: 5px;\n border-radius: 999px;\n /* As on the settings meter: an unspent window has to show its track, so the neutral and\n its ring are explicit rather than a layer token that may match the panel. */\n background: #8a8f9826;\n box-shadow: inset 0 0 0 1px #8a8f9833;\n overflow: hidden;\n}\n.dsh-subscription-usage-chip-fill {\n display: block;\n height: 100%;\n background: var(--dsw-alias-state-success-primary, #067647);\n}\n";
|