@linxin666/dsh-usage 0.4.10 → 0.4.11
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.i18n.yaml +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/lib/client.js +1762 -366
- package/lib/client.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +251 -0
- package/lib/types/client/RangeBubble.d.ts +46 -0
- package/lib/types/client/SegmentGroup.d.ts +28 -0
- package/lib/types/client/UsagePage.d.ts +19 -0
- package/lib/types/client/UsageSectionCard.d.ts +11 -18
- package/lib/types/client/locales.d.ts +13 -2
- package/lib/types/client/section-integration.d.ts +47 -0
- package/lib/types/client/settings-form.d.ts +235 -0
- package/lib/types/client/usage-settings-card.d.ts +48 -0
- package/lib/types/client/web-extension-page.d.ts +50 -0
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +817 -0
- package/src/client/RangeBubble.tsx +164 -0
- package/src/client/SegmentGroup.tsx +93 -0
- package/src/client/UsagePage.tsx +108 -0
- package/src/client/UsageSectionCard.tsx +173 -270
- package/src/client/index.ts +19 -30
- package/src/client/locales.ts +46 -24
- package/src/client/section-integration.ts +88 -0
- package/src/client/settings-card.module.css +821 -0
- package/src/client/settings-form.ts +510 -0
- package/src/client/usage-settings-card.ts +72 -0
- package/src/client/usage.module.css +74 -82
- package/src/client/web-extension-page.ts +151 -0
|
@@ -3,17 +3,29 @@
|
|
|
3
3
|
section reads on light and dark themes alike. */
|
|
4
4
|
|
|
5
5
|
.section {
|
|
6
|
+
container-name: usage-section;
|
|
7
|
+
container-type: inline-size;
|
|
6
8
|
display: flex;
|
|
7
9
|
flex-direction: column;
|
|
8
10
|
gap: 16px;
|
|
11
|
+
min-width: 0;
|
|
9
12
|
color: inherit;
|
|
10
13
|
}
|
|
11
14
|
|
|
12
15
|
.header {
|
|
13
16
|
display: flex;
|
|
17
|
+
flex-wrap: wrap;
|
|
14
18
|
align-items: center;
|
|
15
19
|
justify-content: space-between;
|
|
16
|
-
gap:
|
|
20
|
+
gap: 8px 16px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.headerActions {
|
|
24
|
+
display: flex;
|
|
25
|
+
flex-wrap: wrap;
|
|
26
|
+
align-items: center;
|
|
27
|
+
justify-content: flex-end;
|
|
28
|
+
gap: 8px;
|
|
17
29
|
}
|
|
18
30
|
|
|
19
31
|
.currentProvider {
|
|
@@ -50,39 +62,40 @@
|
|
|
50
62
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);
|
|
51
63
|
}
|
|
52
64
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
65
|
+
/* The page card (PluginSettingsCard) sits in a bare list, as on the sibling
|
|
66
|
+
Web Extensions pages. */
|
|
67
|
+
.sectionList {
|
|
68
|
+
margin: 0;
|
|
69
|
+
padding: 0;
|
|
70
|
+
list-style: none;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
font: inherit;
|
|
65
|
-
font-size: 13px;
|
|
66
|
-
padding: 6px 14px;
|
|
67
|
-
cursor: pointer;
|
|
68
|
-
opacity: 0.65;
|
|
69
|
-
border-bottom: 2px solid transparent;
|
|
70
|
-
margin-bottom: -1px;
|
|
73
|
+
/* The statistics tabs ride the family's segmented track
|
|
74
|
+
(settings-card.module.css), spread across the card. */
|
|
75
|
+
.tabs {
|
|
76
|
+
display: flex;
|
|
77
|
+
align-self: stretch;
|
|
71
78
|
}
|
|
72
79
|
|
|
73
|
-
.tab
|
|
74
|
-
|
|
80
|
+
.tabs > .tab {
|
|
81
|
+
flex: 1 1 0;
|
|
82
|
+
min-width: max-content;
|
|
83
|
+
text-align: center;
|
|
75
84
|
}
|
|
76
85
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
86
|
+
/* The display rows close the card body under a quiet heading, above the
|
|
87
|
+
hairline the rows draw between themselves. */
|
|
88
|
+
.settingsGroup {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
margin-top: 16px;
|
|
80
92
|
}
|
|
81
93
|
|
|
82
|
-
.
|
|
83
|
-
|
|
94
|
+
.groupTitle {
|
|
95
|
+
margin: 0 0 2px;
|
|
96
|
+
font-size: 12px;
|
|
84
97
|
font-weight: 600;
|
|
85
|
-
|
|
98
|
+
opacity: 0.6;
|
|
86
99
|
}
|
|
87
100
|
|
|
88
101
|
.card {
|
|
@@ -135,27 +148,30 @@
|
|
|
135
148
|
control's background, so an unset option background falls back to the
|
|
136
149
|
browser's own (light) list surface while the option text still inherits the
|
|
137
150
|
section's light text color — light text on a light list, unreadable on a
|
|
138
|
-
dark skin.
|
|
139
|
-
|
|
151
|
+
dark skin. The day picker therefore gives its option list one explicit
|
|
152
|
+
surface/label pair, read from the tokens the
|
|
140
153
|
platform and the skins ship (a skin that omits the surface token falls back
|
|
141
154
|
to the platform's, a host with no theme layer to Canvas). Text and
|
|
142
155
|
background always travel together, per theme half. */
|
|
143
|
-
.daySelect option
|
|
144
|
-
.settingItem select option {
|
|
156
|
+
.daySelect option {
|
|
145
157
|
background-color: var(--dsw-alias-bg-multi-select, Canvas);
|
|
146
158
|
color: var(--dsw-alias-label-primary, CanvasText);
|
|
147
159
|
}
|
|
148
160
|
|
|
149
161
|
.statRow {
|
|
150
|
-
display:
|
|
151
|
-
|
|
152
|
-
gap:
|
|
162
|
+
display: grid;
|
|
163
|
+
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
164
|
+
gap: 10px;
|
|
153
165
|
}
|
|
154
166
|
|
|
155
167
|
.stat {
|
|
156
168
|
display: flex;
|
|
157
169
|
flex-direction: column;
|
|
158
|
-
gap:
|
|
170
|
+
gap: 3px;
|
|
171
|
+
min-width: 0;
|
|
172
|
+
padding: 9px 10px;
|
|
173
|
+
border-radius: 8px;
|
|
174
|
+
background: color-mix(in srgb, currentColor 4%, transparent);
|
|
159
175
|
}
|
|
160
176
|
|
|
161
177
|
.statValue {
|
|
@@ -218,9 +234,31 @@
|
|
|
218
234
|
/* 近 30 天提供方/模型条形图:provider 行主导(条宽按窗口内最大 provider
|
|
219
235
|
归一),模型子行缩进细条(行内归一)。 */
|
|
220
236
|
.chart {
|
|
221
|
-
display:
|
|
222
|
-
|
|
223
|
-
gap:
|
|
237
|
+
display: grid;
|
|
238
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
239
|
+
gap: 14px 20px;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
.usageSummaryGrid {
|
|
243
|
+
display: grid;
|
|
244
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
245
|
+
gap: 16px;
|
|
246
|
+
align-items: start;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
.trendAxis {
|
|
250
|
+
grid-column: 1 / -1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
@container usage-section (max-width: 680px) {
|
|
254
|
+
.usageSummaryGrid,
|
|
255
|
+
.chart {
|
|
256
|
+
grid-template-columns: minmax(0, 1fr);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.statRow {
|
|
260
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
261
|
+
}
|
|
224
262
|
}
|
|
225
263
|
|
|
226
264
|
.chartProvider {
|
|
@@ -384,52 +422,6 @@
|
|
|
384
422
|
font-variant-numeric: tabular-nums;
|
|
385
423
|
}
|
|
386
424
|
|
|
387
|
-
.settingsGrid {
|
|
388
|
-
display: flex;
|
|
389
|
-
flex-wrap: wrap;
|
|
390
|
-
gap: 16px;
|
|
391
|
-
align-items: center;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
.settingItem {
|
|
395
|
-
display: flex;
|
|
396
|
-
align-items: center;
|
|
397
|
-
gap: 8px;
|
|
398
|
-
font-size: 13px;
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
.settingItem input[type='checkbox'] {
|
|
402
|
-
accent-color: currentColor;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
.settingItem input[type='number'] {
|
|
406
|
-
width: 90px;
|
|
407
|
-
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
|
408
|
-
border-radius: 6px;
|
|
409
|
-
background: transparent;
|
|
410
|
-
color: inherit;
|
|
411
|
-
font: inherit;
|
|
412
|
-
font-size: 13px;
|
|
413
|
-
padding: 3px 8px;
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
.settingItem select {
|
|
417
|
-
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
|
|
418
|
-
border-radius: 6px;
|
|
419
|
-
background: transparent;
|
|
420
|
-
color: inherit;
|
|
421
|
-
font: inherit;
|
|
422
|
-
font-size: 13px;
|
|
423
|
-
padding: 3px 8px;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
.settingItem input:focus-visible,
|
|
427
|
-
.settingItem select:focus-visible {
|
|
428
|
-
outline: none;
|
|
429
|
-
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
425
|
/* ---------------------------------------------------------------------------
|
|
434
426
|
Sidebar foot card: the compact usage glance the mount seats as the shell
|
|
435
427
|
foot area's last child, directly below the Settings row. The chrome is an
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Generated by scripts/sync-shared.mjs from shared/client/settings/web-extension-page.ts. Do not edit this copy; edit the shared source and run "node scripts/sync-shared.mjs".
|
|
2
|
+
/**
|
|
3
|
+
* One settings page's seat in either the Web Extensions parent or the legacy
|
|
4
|
+
* first-level settings navigation.
|
|
5
|
+
*
|
|
6
|
+
* The parent supplies the page contract; a child does not know which slot the
|
|
7
|
+
* parent uses internally. While `webUiExtensions` is live the page is handed
|
|
8
|
+
* to its service, which owns keyed mounting and disposal. Without the parent,
|
|
9
|
+
* the same component registers as its original first-level settings section,
|
|
10
|
+
* so a standalone install keeps a working entry.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** The parent service face this helper consumes. */
|
|
14
|
+
export interface WebExtensionPageService {
|
|
15
|
+
readonly sectionId: string
|
|
16
|
+
registerPage(page: WebExtensionPageSeat): () => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Seat-independent page contribution. */
|
|
20
|
+
export interface WebExtensionPageSeat {
|
|
21
|
+
key: string
|
|
22
|
+
order: number
|
|
23
|
+
locale: string
|
|
24
|
+
label(): string
|
|
25
|
+
inject(): object
|
|
26
|
+
component: unknown
|
|
27
|
+
children?: Record<string, { kind: 'keyed' | 'list'; scope: 'root' }>
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The slot-registry slice used for the standalone fallback. */
|
|
31
|
+
export interface WebExtensionPageSlots {
|
|
32
|
+
register(options: never, component: never): unknown
|
|
33
|
+
spec?(key: string): unknown
|
|
34
|
+
entries?(key: string): readonly { component?: unknown; options: Record<string, unknown> }[]
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The context slice needed by the migration. */
|
|
38
|
+
export interface WebExtensionPageContext {
|
|
39
|
+
slots: WebExtensionPageSlots
|
|
40
|
+
get?(name: string): unknown
|
|
41
|
+
on?(event: string, listener: () => void): unknown
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const SETTINGS_SECTION = 'settings.section'
|
|
45
|
+
const WEB_EXTENSION_SLOT = 'web-ui.extension'
|
|
46
|
+
|
|
47
|
+
function serviceOf(ctx: WebExtensionPageContext): WebExtensionPageService | undefined {
|
|
48
|
+
if (typeof ctx.get !== 'function') return undefined
|
|
49
|
+
try {
|
|
50
|
+
const candidate = ctx.get('webUiExtensions') as Partial<WebExtensionPageService> | undefined
|
|
51
|
+
return candidate !== null
|
|
52
|
+
&& typeof candidate === 'object'
|
|
53
|
+
&& typeof candidate.sectionId === 'string'
|
|
54
|
+
&& typeof candidate.registerPage === 'function'
|
|
55
|
+
? candidate as WebExtensionPageService
|
|
56
|
+
: undefined
|
|
57
|
+
} catch {
|
|
58
|
+
return undefined
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function warn(seat: string, error: unknown): void {
|
|
63
|
+
try {
|
|
64
|
+
console.warn(`[dsh-web-settings] page "${seat}" could not be mounted`, error)
|
|
65
|
+
} catch {
|
|
66
|
+
// A restricted console must not drop the remaining entry.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Keep exactly one reachable page contribution while the parent loads or
|
|
72
|
+
* unloads. The parent service owns integrated mounting; only the fallback uses
|
|
73
|
+
* this plugin's own slot registry. Slot disposal is synchronous, so the latch
|
|
74
|
+
* prevents a move from recursively registering a duplicate.
|
|
75
|
+
*/
|
|
76
|
+
export function installWebExtensionPage(
|
|
77
|
+
ctx: WebExtensionPageContext,
|
|
78
|
+
page: WebExtensionPageSeat,
|
|
79
|
+
): () => void {
|
|
80
|
+
let dispose: (() => void) | undefined
|
|
81
|
+
let current: 'integrated' | 'legacy' | undefined
|
|
82
|
+
let reconciling = false
|
|
83
|
+
let disposed = false
|
|
84
|
+
|
|
85
|
+
const present = (seat: 'integrated' | 'legacy'): boolean => {
|
|
86
|
+
if (typeof ctx.slots.entries !== 'function') return current === seat
|
|
87
|
+
const slot = seat === 'integrated' ? WEB_EXTENSION_SLOT : SETTINGS_SECTION
|
|
88
|
+
return ctx.slots.entries(slot).some(entry =>
|
|
89
|
+
entry.component === page.component
|
|
90
|
+
&& (seat === 'integrated' ? entry.options.key : entry.options.id) === page.key)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const registerSeat = (seat: 'integrated' | 'legacy'): (() => void) => {
|
|
94
|
+
if (seat === 'integrated') {
|
|
95
|
+
const service = serviceOf(ctx)
|
|
96
|
+
if (service === undefined) throw new Error('webUiExtensions service is unavailable')
|
|
97
|
+
return service.registerPage(page)
|
|
98
|
+
}
|
|
99
|
+
return ctx.slots.register({
|
|
100
|
+
name: SETTINGS_SECTION,
|
|
101
|
+
id: page.key,
|
|
102
|
+
order: page.order,
|
|
103
|
+
label: page.label,
|
|
104
|
+
locale: page.locale,
|
|
105
|
+
inject: page.inject as never,
|
|
106
|
+
...(page.children === undefined ? {} : { children: page.children }),
|
|
107
|
+
} as never, page.component as never) as () => void
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const reconcile = (): void => {
|
|
111
|
+
if (disposed || reconciling) return
|
|
112
|
+
const target: 'integrated' | 'legacy' = serviceOf(ctx) === undefined ? 'legacy' : 'integrated'
|
|
113
|
+
if (current === target && present(target)) return
|
|
114
|
+
const slot = target === 'integrated' ? WEB_EXTENSION_SLOT : SETTINGS_SECTION
|
|
115
|
+
if (typeof ctx.slots.spec === 'function' && ctx.slots.spec(slot) === undefined) return
|
|
116
|
+
reconciling = true
|
|
117
|
+
const previous = current
|
|
118
|
+
const disposePrevious = dispose
|
|
119
|
+
current = undefined
|
|
120
|
+
dispose = undefined
|
|
121
|
+
disposePrevious?.()
|
|
122
|
+
try {
|
|
123
|
+
dispose = registerSeat(target)
|
|
124
|
+
current = target
|
|
125
|
+
} catch (error) {
|
|
126
|
+
warn(target, error)
|
|
127
|
+
if (previous !== undefined && previous !== target) {
|
|
128
|
+
try {
|
|
129
|
+
dispose = registerSeat(previous)
|
|
130
|
+
current = previous
|
|
131
|
+
} catch (restoreError) {
|
|
132
|
+
warn(previous, restoreError)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
} finally {
|
|
136
|
+
reconciling = false
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof ctx.on === 'function') {
|
|
141
|
+
try { ctx.on('slots/changed', reconcile) } catch { /* the initial entry remains usable */ }
|
|
142
|
+
}
|
|
143
|
+
reconcile()
|
|
144
|
+
return () => {
|
|
145
|
+
if (disposed) return
|
|
146
|
+
disposed = true
|
|
147
|
+
dispose?.()
|
|
148
|
+
dispose = undefined
|
|
149
|
+
current = undefined
|
|
150
|
+
}
|
|
151
|
+
}
|