@linxin666/dsh-usage 0.4.9 → 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.
@@ -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: 12px;
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
- .tabs {
54
- display: flex;
55
- gap: 4px;
56
- border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent);
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
- .tab {
60
- appearance: none;
61
- border: none;
62
- background: transparent;
63
- color: inherit;
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:hover {
74
- opacity: 0.9;
80
+ .tabs > .tab {
81
+ flex: 1 1 0;
82
+ min-width: max-content;
83
+ text-align: center;
75
84
  }
76
85
 
77
- .tab:focus-visible {
78
- outline: none;
79
- box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 45%, transparent);
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
- .tabActive {
83
- opacity: 1;
94
+ .groupTitle {
95
+ margin: 0 0 2px;
96
+ font-size: 12px;
84
97
  font-weight: 600;
85
- border-bottom-color: currentColor;
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. Both the day picker and the settings-row select therefore give
139
- their option list one explicit surface/label pair, read from the tokens the
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: flex;
151
- flex-wrap: wrap;
152
- gap: 18px;
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: 2px;
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: flex;
222
- flex-direction: column;
223
- gap: 12px;
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
+ }