@huaqiu/dsh-tool-plugin-update 0.0.0-stage → 0.5.22
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/LICENSE +21 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1869 -0
- package/lib/client.js.map +1 -0
- package/lib/index.d.mts +32 -0
- package/lib/index.mjs +178 -0
- package/package.json +50 -3
- package/src/client/api.ts +98 -0
- package/src/client/copy.ts +120 -0
- package/src/client/icons.ts +91 -0
- package/src/client/index.ts +1052 -0
- package/src/client/update-view.ts +619 -0
- package/src/index.ts +108 -0
- package/src/routes.ts +149 -0
- package/README.md +0 -3
|
@@ -0,0 +1,1052 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@huaqiu/dsh-tool-plugin-update` — browser half.
|
|
3
|
+
*
|
|
4
|
+
* Mounts one sidebar toolbutton in the Huaqiu family block ("插件更新") with a
|
|
5
|
+
* numeric badge equal to the actionable plugin-update count (spec §6.3/§7.1:
|
|
6
|
+
* only compatible `update` + `install` items after host/HQ-Edge filtering and
|
|
7
|
+
* after excluding the ignored offer; runtime updates are NOT counted). Click
|
|
8
|
+
* opens the review/confirm/ignore modal.
|
|
9
|
+
*
|
|
10
|
+
* The authoritative state is the hq-edge report fetched through the plugin's
|
|
11
|
+
* same-origin forwarding routes. A tiny in-memory store owns the latest report
|
|
12
|
+
* + installing flag; the sidebar badge and the modal subscribe to it. No
|
|
13
|
+
* timers beyond the install-poll and an idle 60 s refresh.
|
|
14
|
+
*
|
|
15
|
+
* @module
|
|
16
|
+
*/
|
|
17
|
+
import {
|
|
18
|
+
HUAQIU_SIDEBAR_ENTRY_FAMILY,
|
|
19
|
+
detectHostLocale,
|
|
20
|
+
mountSidebarEntry,
|
|
21
|
+
observeHostLocale,
|
|
22
|
+
type HostLocale,
|
|
23
|
+
} from '@huaqiu/dsh-sidebar-entry'
|
|
24
|
+
import { checkUpdates, fetchReport, type PluginUpdateReport } from './api.js'
|
|
25
|
+
import { COPY } from './copy.js'
|
|
26
|
+
import { PLUGIN_ICON } from './icons.js'
|
|
27
|
+
import { openUpdateView, type UpdateStoreLike } from './update-view.js'
|
|
28
|
+
|
|
29
|
+
/** Stable data attributes identifying the injected entry row. */
|
|
30
|
+
export const UPDATE_ENTRY_SELECTOR = '[data-hq-plugin-update-entry]'
|
|
31
|
+
export const UPDATE_ENTRY_ATTRIBUTE = 'data-hq-plugin-update-entry'
|
|
32
|
+
|
|
33
|
+
/** L2 semantic plugin id: rows carry data-dsh-plugin + data-dsh-part. */
|
|
34
|
+
const PLUGIN_ID = 'huaqiu-plugin-update'
|
|
35
|
+
|
|
36
|
+
/** Row class names (defined by {@link ENTRY_CSS_TEXT}). */
|
|
37
|
+
const ENTRY_CSS: Record<string, string> = {
|
|
38
|
+
entry: 'hqpu-sidebar-entry',
|
|
39
|
+
entryIcon: 'hqpu-sidebar-entry__icon',
|
|
40
|
+
entryLabel: 'hqpu-sidebar-entry__label',
|
|
41
|
+
entryBadge: 'hqpu-sidebar-entry__badge',
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Localized row copy: zh is the fallback, matching the family convention. */
|
|
45
|
+
function entryCopy(lang: HostLocale | undefined): { label: string; tooltip: string } {
|
|
46
|
+
return lang === 'en'
|
|
47
|
+
? { label: COPY.en.entryLabel, tooltip: COPY.en.entryTooltip }
|
|
48
|
+
: { label: COPY.zh.entryLabel, tooltip: COPY.zh.entryTooltip }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** The sidebar glyph — the same pinwheel the shell's own Plugins entry draws
|
|
52
|
+
* (`IconPluginPinwheelOutlineRegular`), so both rows read as one feature. */
|
|
53
|
+
export const UPDATE_ICON = PLUGIN_ICON(16)
|
|
54
|
+
|
|
55
|
+
/** Sidebar entry styles — matched to the family rows (same inset/padding/gap
|
|
56
|
+
* as the shell's own Plugins entry, `SidebarRoot.module.css` `.panelRow`).
|
|
57
|
+
* Every colour rides a `--dsw-alias-*` token so the row follows the shell's
|
|
58
|
+
* light/dark theme; the badge foreground uses the toast label, the shell's
|
|
59
|
+
* "always light ink on a saturated surface" token. */
|
|
60
|
+
const ENTRY_CSS_TEXT = `
|
|
61
|
+
.hqpu-sidebar-entry {
|
|
62
|
+
box-sizing: border-box;
|
|
63
|
+
display: flex;
|
|
64
|
+
align-items: center;
|
|
65
|
+
gap: 8px;
|
|
66
|
+
width: 100%;
|
|
67
|
+
margin: 0 2px;
|
|
68
|
+
min-height: 36px;
|
|
69
|
+
padding: 7px 8px;
|
|
70
|
+
background: transparent;
|
|
71
|
+
border: none;
|
|
72
|
+
border-radius: var(--dsw-radius-md);
|
|
73
|
+
color: var(--dsw-alias-label-primary);
|
|
74
|
+
cursor: pointer;
|
|
75
|
+
font: inherit;
|
|
76
|
+
line-height: 22px;
|
|
77
|
+
text-align: left;
|
|
78
|
+
white-space: nowrap;
|
|
79
|
+
}
|
|
80
|
+
.hqpu-sidebar-entry:hover {
|
|
81
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
82
|
+
}
|
|
83
|
+
.hqpu-sidebar-entry[data-active] {
|
|
84
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
85
|
+
color: var(--dsw-alias-label-primary);
|
|
86
|
+
}
|
|
87
|
+
.hqpu-sidebar-entry:focus-visible {
|
|
88
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
89
|
+
outline-offset: -2px;
|
|
90
|
+
}
|
|
91
|
+
.hqpu-sidebar-entry__icon {
|
|
92
|
+
display: inline-flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
justify-content: center;
|
|
95
|
+
flex: none;
|
|
96
|
+
}
|
|
97
|
+
.hqpu-sidebar-entry__icon svg {
|
|
98
|
+
display: block;
|
|
99
|
+
width: 16px;
|
|
100
|
+
height: 16px;
|
|
101
|
+
}
|
|
102
|
+
.hqpu-sidebar-entry__label {
|
|
103
|
+
min-width: 0;
|
|
104
|
+
overflow: hidden;
|
|
105
|
+
text-overflow: ellipsis;
|
|
106
|
+
flex: 1;
|
|
107
|
+
}
|
|
108
|
+
.hqpu-sidebar-entry__badge {
|
|
109
|
+
flex: none;
|
|
110
|
+
min-width: 16px;
|
|
111
|
+
height: 16px;
|
|
112
|
+
padding: 0 5px;
|
|
113
|
+
border-radius: 8px;
|
|
114
|
+
background: var(--dsw-alias-state-error-primary);
|
|
115
|
+
color: var(--dsw-alias-toast-label);
|
|
116
|
+
font-size: 11px;
|
|
117
|
+
line-height: 16px;
|
|
118
|
+
font-weight: 500;
|
|
119
|
+
text-align: center;
|
|
120
|
+
font-variant-numeric: tabular-nums;
|
|
121
|
+
}
|
|
122
|
+
/* Collapsed rail: icon-only, centered — same 36px square the shell uses. */
|
|
123
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry {
|
|
124
|
+
justify-content: center;
|
|
125
|
+
width: 36px;
|
|
126
|
+
height: 36px;
|
|
127
|
+
margin: 0;
|
|
128
|
+
padding: 0;
|
|
129
|
+
}
|
|
130
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__icon svg {
|
|
131
|
+
width: 18px;
|
|
132
|
+
height: 18px;
|
|
133
|
+
}
|
|
134
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__label,
|
|
135
|
+
[data-sidebar-collapsed] .hqpu-sidebar-entry__badge {
|
|
136
|
+
display: none;
|
|
137
|
+
}
|
|
138
|
+
`
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* View styles (hqpu-*). Plain overlay + panel, drawn with the SAME design
|
|
142
|
+
* tokens and geometry the shell's own plugin manager uses
|
|
143
|
+
* (`ui-plugin-manager/src/client/PluginManagerPage.module.css` +
|
|
144
|
+
* `ui-primitives/src/{Modal,Toast,Tag,StateDot}.module.css`), so this page
|
|
145
|
+
* reads as part of the product rather than a bolt-on.
|
|
146
|
+
*
|
|
147
|
+
* Two rules keep it honest across themes:
|
|
148
|
+
* 1. every colour is a `--dsw-alias-*` token — no literal hex/rgb, so light
|
|
149
|
+
* and dark both come out right (the old sheet invented
|
|
150
|
+
* `--dsw-alias-bg-surface` / `--dsw-alias-line-border` / `--dsw-alias-bg-badge`,
|
|
151
|
+
* none of which exist, and so always fell back to a white panel);
|
|
152
|
+
* 2. `color-mix()` tints come from the state token itself, so a palette
|
|
153
|
+
* change moves fill and ink together.
|
|
154
|
+
*
|
|
155
|
+
* The chrome (panel, scrim, header, footer) follows `ui-primitives/Modal`,
|
|
156
|
+
* because this view IS a dialog; the body follows `PluginManagerPage`, because
|
|
157
|
+
* that is the content it heads. The one token the bundled shell does not
|
|
158
|
+
* publish is backfilled by {@link ensureDesignTokens} — see the note there.
|
|
159
|
+
*/
|
|
160
|
+
const VIEW_CSS_TEXT = `
|
|
161
|
+
/* ── Overlay + panel (ui-primitives Modal.module.css) ─────────────────── */
|
|
162
|
+
.hqpu-overlay {
|
|
163
|
+
position: fixed;
|
|
164
|
+
inset: 0;
|
|
165
|
+
z-index: 1000;
|
|
166
|
+
display: flex;
|
|
167
|
+
align-items: center;
|
|
168
|
+
justify-content: center;
|
|
169
|
+
/* The frame's own top inset keeps a tall card clear of the window chrome. */
|
|
170
|
+
padding: max(24px, var(--dsh-frame-overlay-top, 24px)) 24px;
|
|
171
|
+
}
|
|
172
|
+
.hqpu-scrim {
|
|
173
|
+
position: absolute;
|
|
174
|
+
/* Only paint excludes the native caption; the root still blocks clicks. */
|
|
175
|
+
inset: var(--dsh-frame-chrome-top, 0px) 0 0;
|
|
176
|
+
background: var(--dsw-alias-bg-mask-1);
|
|
177
|
+
animation: hqpu-fade var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);
|
|
178
|
+
}
|
|
179
|
+
.hqpu-panel {
|
|
180
|
+
position: relative;
|
|
181
|
+
z-index: 1;
|
|
182
|
+
box-sizing: border-box;
|
|
183
|
+
display: flex;
|
|
184
|
+
flex-direction: column;
|
|
185
|
+
gap: 20px;
|
|
186
|
+
width: min(920px, 100%);
|
|
187
|
+
max-height: 100%;
|
|
188
|
+
overflow: hidden;
|
|
189
|
+
border: 0;
|
|
190
|
+
border-radius: var(--dsw-radius-panel);
|
|
191
|
+
background: var(--dsw-alias-bg-layer-2);
|
|
192
|
+
box-shadow: var(--dsw-elevation-prominent);
|
|
193
|
+
color: var(--dsw-alias-label-primary);
|
|
194
|
+
animation: hqpu-panel-in var(--ds-transition-duration, 0.2s) var(--ds-ease-in-out, ease);
|
|
195
|
+
}
|
|
196
|
+
.hqpu-panel:focus {
|
|
197
|
+
outline: none;
|
|
198
|
+
}
|
|
199
|
+
@keyframes hqpu-fade {
|
|
200
|
+
from { opacity: 0; }
|
|
201
|
+
to { opacity: 1; }
|
|
202
|
+
}
|
|
203
|
+
@keyframes hqpu-panel-in {
|
|
204
|
+
from { opacity: 0; }
|
|
205
|
+
to { opacity: 1; }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/* ── Head row (ui-primitives Modal .header) ─────────────────────────────── */
|
|
209
|
+
.hqpu-head {
|
|
210
|
+
display: flex;
|
|
211
|
+
flex: none;
|
|
212
|
+
flex-wrap: wrap;
|
|
213
|
+
align-items: flex-start;
|
|
214
|
+
justify-content: space-between;
|
|
215
|
+
gap: 16px;
|
|
216
|
+
box-sizing: border-box;
|
|
217
|
+
/* The official Title row: 22/14/12/24 — the toolbar sits 10px further right
|
|
218
|
+
* than the body content on purpose, matching the shell's own close button. */
|
|
219
|
+
padding: 22px 14px 12px 24px;
|
|
220
|
+
}
|
|
221
|
+
.hqpu-head-main {
|
|
222
|
+
min-width: 0;
|
|
223
|
+
max-width: 100%;
|
|
224
|
+
}
|
|
225
|
+
.hqpu-title {
|
|
226
|
+
margin: 0;
|
|
227
|
+
font-size: 20px;
|
|
228
|
+
font-weight: 500;
|
|
229
|
+
line-height: 28px;
|
|
230
|
+
color: var(--dsw-alias-label-primary);
|
|
231
|
+
}
|
|
232
|
+
.hqpu-intro {
|
|
233
|
+
display: flex;
|
|
234
|
+
align-items: center;
|
|
235
|
+
gap: 4px;
|
|
236
|
+
margin: 4px 0 0;
|
|
237
|
+
font-size: 13px;
|
|
238
|
+
line-height: 20px;
|
|
239
|
+
color: var(--dsw-alias-label-secondary);
|
|
240
|
+
}
|
|
241
|
+
.hqpu-info {
|
|
242
|
+
display: inline-flex;
|
|
243
|
+
align-items: center;
|
|
244
|
+
justify-content: center;
|
|
245
|
+
flex: none;
|
|
246
|
+
width: 20px;
|
|
247
|
+
height: 20px;
|
|
248
|
+
padding: 0;
|
|
249
|
+
border: 0;
|
|
250
|
+
border-radius: var(--dsw-radius-sm);
|
|
251
|
+
background: transparent;
|
|
252
|
+
color: var(--dsw-alias-label-caption);
|
|
253
|
+
cursor: pointer;
|
|
254
|
+
}
|
|
255
|
+
.hqpu-info:hover {
|
|
256
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
257
|
+
color: var(--dsw-alias-label-secondary);
|
|
258
|
+
}
|
|
259
|
+
.hqpu-info:focus-visible {
|
|
260
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
261
|
+
outline-offset: 1px;
|
|
262
|
+
}
|
|
263
|
+
.hqpu-toolbar {
|
|
264
|
+
display: flex;
|
|
265
|
+
flex: none;
|
|
266
|
+
align-items: center;
|
|
267
|
+
gap: 16px;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* ── Scrolling body: the page's centred column ────────────────────────── */
|
|
271
|
+
.hqpu-scroll {
|
|
272
|
+
flex: 1;
|
|
273
|
+
min-height: 0;
|
|
274
|
+
overflow-y: auto;
|
|
275
|
+
box-sizing: border-box;
|
|
276
|
+
/* The panel's 20px gap already separates this from the head and footer. */
|
|
277
|
+
padding: 0 clamp(16px, 3vw, 32px) 4px;
|
|
278
|
+
/* Anything that scrolls inside the panel draws the l2 thumb. */
|
|
279
|
+
--dsh-scrollbar-thumb: var(--dsw-alias-scrollbar-bg-l2);
|
|
280
|
+
--dsh-scrollbar-thumb-hover: var(--dsw-alias-scrollbar-hover-l2);
|
|
281
|
+
}
|
|
282
|
+
.hqpu-column {
|
|
283
|
+
display: flex;
|
|
284
|
+
flex-direction: column;
|
|
285
|
+
gap: 24px;
|
|
286
|
+
width: 100%;
|
|
287
|
+
max-width: 880px;
|
|
288
|
+
margin: 0 auto;
|
|
289
|
+
}
|
|
290
|
+
/* A stack of groups (or the single toast host) inside the column. */
|
|
291
|
+
.hqpu-stack {
|
|
292
|
+
display: flex;
|
|
293
|
+
flex-direction: column;
|
|
294
|
+
gap: 24px;
|
|
295
|
+
}
|
|
296
|
+
.hqpu-toast-host:empty {
|
|
297
|
+
display: none;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ── Groups (PluginManagerPage .group) ────────────────────────────────── */
|
|
301
|
+
.hqpu-group {
|
|
302
|
+
display: flex;
|
|
303
|
+
flex-direction: column;
|
|
304
|
+
gap: 8px;
|
|
305
|
+
}
|
|
306
|
+
.hqpu-group-head {
|
|
307
|
+
display: flex;
|
|
308
|
+
align-items: center;
|
|
309
|
+
gap: 8px;
|
|
310
|
+
}
|
|
311
|
+
.hqpu-group-title {
|
|
312
|
+
flex: 1;
|
|
313
|
+
min-width: 0;
|
|
314
|
+
margin: 0;
|
|
315
|
+
font-size: 14px;
|
|
316
|
+
font-weight: 500;
|
|
317
|
+
line-height: 22px;
|
|
318
|
+
color: var(--dsw-alias-label-primary);
|
|
319
|
+
}
|
|
320
|
+
.hqpu-group-count {
|
|
321
|
+
flex: none;
|
|
322
|
+
font-size: 12px;
|
|
323
|
+
line-height: 18px;
|
|
324
|
+
color: var(--dsw-alias-label-tertiary);
|
|
325
|
+
font-variant-numeric: tabular-nums;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/* ── Cards (PluginManagerPage .card) ──────────────────────────────────── */
|
|
329
|
+
.hqpu-cards {
|
|
330
|
+
margin: 0;
|
|
331
|
+
padding: 0;
|
|
332
|
+
list-style: none;
|
|
333
|
+
display: flex;
|
|
334
|
+
flex-direction: column;
|
|
335
|
+
gap: 2px;
|
|
336
|
+
}
|
|
337
|
+
.hqpu-card {
|
|
338
|
+
min-width: 0;
|
|
339
|
+
/* The hover surface reaches 8px past the content on both sides. */
|
|
340
|
+
margin: 0 -8px;
|
|
341
|
+
border-radius: var(--dsw-radius-xl);
|
|
342
|
+
}
|
|
343
|
+
.hqpu-card:hover {
|
|
344
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
345
|
+
}
|
|
346
|
+
.hqpu-card-head {
|
|
347
|
+
display: flex;
|
|
348
|
+
align-items: center;
|
|
349
|
+
gap: 14px;
|
|
350
|
+
padding: 8px;
|
|
351
|
+
}
|
|
352
|
+
.hqpu-card-icon {
|
|
353
|
+
display: inline-flex;
|
|
354
|
+
flex: none;
|
|
355
|
+
align-items: center;
|
|
356
|
+
justify-content: center;
|
|
357
|
+
width: 48px;
|
|
358
|
+
height: 48px;
|
|
359
|
+
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
360
|
+
border-radius: var(--dsw-radius-lg);
|
|
361
|
+
color: var(--dsw-alias-label-secondary);
|
|
362
|
+
}
|
|
363
|
+
.hqpu-card-icon svg {
|
|
364
|
+
display: block;
|
|
365
|
+
width: 24px;
|
|
366
|
+
height: 24px;
|
|
367
|
+
}
|
|
368
|
+
.hqpu-card-main {
|
|
369
|
+
flex: 1;
|
|
370
|
+
min-width: 0;
|
|
371
|
+
display: flex;
|
|
372
|
+
flex-direction: column;
|
|
373
|
+
gap: 4px;
|
|
374
|
+
}
|
|
375
|
+
.hqpu-title-row {
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
flex-wrap: wrap;
|
|
379
|
+
gap: 8px;
|
|
380
|
+
min-width: 0;
|
|
381
|
+
}
|
|
382
|
+
.hqpu-card-title {
|
|
383
|
+
min-width: 0;
|
|
384
|
+
font-size: 14px;
|
|
385
|
+
font-weight: 500;
|
|
386
|
+
line-height: 20px;
|
|
387
|
+
color: var(--dsw-alias-label-primary);
|
|
388
|
+
overflow: hidden;
|
|
389
|
+
text-overflow: ellipsis;
|
|
390
|
+
white-space: nowrap;
|
|
391
|
+
}
|
|
392
|
+
.hqpu-card-desc {
|
|
393
|
+
font-size: 13px;
|
|
394
|
+
line-height: 18px;
|
|
395
|
+
color: var(--dsw-alias-label-tertiary);
|
|
396
|
+
display: -webkit-box;
|
|
397
|
+
-webkit-box-orient: vertical;
|
|
398
|
+
-webkit-line-clamp: 1;
|
|
399
|
+
overflow: hidden;
|
|
400
|
+
overflow-wrap: anywhere;
|
|
401
|
+
}
|
|
402
|
+
.hqpu-card-end {
|
|
403
|
+
position: relative;
|
|
404
|
+
z-index: 1;
|
|
405
|
+
display: inline-flex;
|
|
406
|
+
flex: none;
|
|
407
|
+
align-items: center;
|
|
408
|
+
gap: 8px;
|
|
409
|
+
}
|
|
410
|
+
.hqpu-card-reason {
|
|
411
|
+
margin: 0;
|
|
412
|
+
padding: 0 8px 8px 70px;
|
|
413
|
+
font-size: 12px;
|
|
414
|
+
line-height: 18px;
|
|
415
|
+
color: var(--dsw-alias-state-error-primary);
|
|
416
|
+
overflow-wrap: anywhere;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* ── Tags (ui-primitives Tag.module.css) ──────────────────────────────── */
|
|
420
|
+
.hqpu-tag {
|
|
421
|
+
display: inline-flex;
|
|
422
|
+
align-items: center;
|
|
423
|
+
flex: none;
|
|
424
|
+
border-radius: 999px;
|
|
425
|
+
corner-shape: round;
|
|
426
|
+
padding: 1px 8px;
|
|
427
|
+
font-size: 11px;
|
|
428
|
+
font-weight: 500;
|
|
429
|
+
line-height: 17px;
|
|
430
|
+
white-space: nowrap;
|
|
431
|
+
}
|
|
432
|
+
.hqpu-tag[data-tone='success'] {
|
|
433
|
+
background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 10%, transparent);
|
|
434
|
+
color: var(--dsw-alias-state-success-primary);
|
|
435
|
+
}
|
|
436
|
+
.hqpu-tag[data-tone='info'] {
|
|
437
|
+
background: color-mix(in srgb, var(--dsw-alias-state-business-primary) 10%, transparent);
|
|
438
|
+
color: var(--dsw-alias-state-business-primary);
|
|
439
|
+
}
|
|
440
|
+
.hqpu-tag[data-tone='warning'] {
|
|
441
|
+
background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);
|
|
442
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
443
|
+
}
|
|
444
|
+
.hqpu-tag[data-tone='danger'] {
|
|
445
|
+
background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);
|
|
446
|
+
color: var(--dsw-alias-state-error-primary);
|
|
447
|
+
}
|
|
448
|
+
.hqpu-tag[data-tone='neutral'] {
|
|
449
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
450
|
+
color: var(--dsw-alias-label-secondary);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/* ── Fact rows (PluginManagerPage .facts / .rows) ─────────────────────── */
|
|
454
|
+
.hqpu-facts {
|
|
455
|
+
margin: 0;
|
|
456
|
+
display: flex;
|
|
457
|
+
flex-direction: column;
|
|
458
|
+
}
|
|
459
|
+
.hqpu-facts > div {
|
|
460
|
+
display: flex;
|
|
461
|
+
align-items: center;
|
|
462
|
+
justify-content: space-between;
|
|
463
|
+
gap: 16px;
|
|
464
|
+
min-height: 44px;
|
|
465
|
+
padding: 8px 2px;
|
|
466
|
+
border-bottom: 0.5px solid var(--dsw-alias-border-l2);
|
|
467
|
+
}
|
|
468
|
+
.hqpu-facts > div:last-child {
|
|
469
|
+
border-bottom: 0;
|
|
470
|
+
}
|
|
471
|
+
.hqpu-facts dt {
|
|
472
|
+
flex: none;
|
|
473
|
+
font-size: 13px;
|
|
474
|
+
line-height: 20px;
|
|
475
|
+
color: var(--dsw-alias-label-secondary);
|
|
476
|
+
}
|
|
477
|
+
.hqpu-facts dd {
|
|
478
|
+
min-width: 0;
|
|
479
|
+
margin: 0;
|
|
480
|
+
font-size: 13px;
|
|
481
|
+
line-height: 20px;
|
|
482
|
+
text-align: end;
|
|
483
|
+
overflow-wrap: anywhere;
|
|
484
|
+
color: var(--dsw-alias-label-primary);
|
|
485
|
+
}
|
|
486
|
+
.hqpu-facts code {
|
|
487
|
+
padding: 2px 6px;
|
|
488
|
+
border-radius: var(--dsw-radius-xs);
|
|
489
|
+
background: var(--dsw-alias-bg-module-platform);
|
|
490
|
+
font-family: var(--dsw-font-mono, ui-monospace, SFMono-Regular, Menlo, monospace);
|
|
491
|
+
font-size: 12px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/* ── Banner (PluginManagerPage .installSafety) ────────────────────────── */
|
|
495
|
+
.hqpu-banner {
|
|
496
|
+
display: flex;
|
|
497
|
+
align-items: flex-start;
|
|
498
|
+
gap: 8px;
|
|
499
|
+
padding: 10px 12px;
|
|
500
|
+
border-radius: var(--dsw-radius-md);
|
|
501
|
+
background: var(--dsw-alias-state-warn-tertiary);
|
|
502
|
+
font-size: 12.5px;
|
|
503
|
+
line-height: 18px;
|
|
504
|
+
color: var(--dsw-alias-label-secondary);
|
|
505
|
+
}
|
|
506
|
+
.hqpu-banner > svg {
|
|
507
|
+
flex: none;
|
|
508
|
+
margin-top: 2px;
|
|
509
|
+
color: var(--dsw-alias-state-warn-primary);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* ── Status, empty, failure (PluginManagerPage .status / .empty / .failure) */
|
|
513
|
+
.hqpu-status {
|
|
514
|
+
margin: 0;
|
|
515
|
+
font-size: 13px;
|
|
516
|
+
line-height: 20px;
|
|
517
|
+
color: var(--dsw-alias-label-tertiary);
|
|
518
|
+
}
|
|
519
|
+
.hqpu-status--dot {
|
|
520
|
+
display: inline-flex;
|
|
521
|
+
align-items: center;
|
|
522
|
+
gap: 6px;
|
|
523
|
+
}
|
|
524
|
+
.hqpu-failure {
|
|
525
|
+
display: flex;
|
|
526
|
+
align-items: center;
|
|
527
|
+
gap: 10px;
|
|
528
|
+
color: var(--dsw-alias-state-error-primary);
|
|
529
|
+
}
|
|
530
|
+
.hqpu-empty {
|
|
531
|
+
padding: 32px 0;
|
|
532
|
+
text-align: center;
|
|
533
|
+
font-size: 13px;
|
|
534
|
+
line-height: 20px;
|
|
535
|
+
color: var(--dsw-alias-label-tertiary);
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/* ── State dot + spinner (ui-primitives StateDot.module.css) ──────────── */
|
|
539
|
+
.hqpu-dot {
|
|
540
|
+
position: relative;
|
|
541
|
+
display: inline-block;
|
|
542
|
+
flex: none;
|
|
543
|
+
width: 10px;
|
|
544
|
+
height: 10px;
|
|
545
|
+
}
|
|
546
|
+
.hqpu-dot::after {
|
|
547
|
+
content: '';
|
|
548
|
+
position: absolute;
|
|
549
|
+
inset: 20%;
|
|
550
|
+
border-radius: 50%;
|
|
551
|
+
corner-shape: round;
|
|
552
|
+
background: currentColor;
|
|
553
|
+
}
|
|
554
|
+
.hqpu-dot[data-state='done'] { color: var(--dsw-alias-state-success-primary); }
|
|
555
|
+
.hqpu-dot[data-state='warning'] { color: var(--dsw-alias-state-warn-primary); }
|
|
556
|
+
.hqpu-dot[data-state='error'] { color: var(--dsw-alias-state-error-primary); }
|
|
557
|
+
.hqpu-dot[data-state='idle'] { color: var(--dsw-alias-state-idle-primary); }
|
|
558
|
+
.hqpu-spinner {
|
|
559
|
+
flex: none;
|
|
560
|
+
width: 14px;
|
|
561
|
+
height: 14px;
|
|
562
|
+
border-radius: 50%;
|
|
563
|
+
border: 1.5px solid color-mix(in srgb, currentColor 25%, transparent);
|
|
564
|
+
border-top-color: currentColor;
|
|
565
|
+
animation: hqpu-spin 1.5s linear infinite;
|
|
566
|
+
}
|
|
567
|
+
@keyframes hqpu-spin {
|
|
568
|
+
to { transform: rotate(360deg); }
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* ── Skeleton (PluginManagerPage .skeleton*) ──────────────────────────── */
|
|
572
|
+
.hqpu-skeleton-fill {
|
|
573
|
+
background: var(--dsw-alias-bg-skeleton);
|
|
574
|
+
animation: hqpu-skeleton 2s cubic-bezier(0.36, 0, 0.64, 1) infinite;
|
|
575
|
+
}
|
|
576
|
+
.hqpu-skeleton-text {
|
|
577
|
+
display: flex;
|
|
578
|
+
align-items: center;
|
|
579
|
+
height: 1lh;
|
|
580
|
+
}
|
|
581
|
+
.hqpu-skeleton-bar {
|
|
582
|
+
width: 100%;
|
|
583
|
+
height: 12px;
|
|
584
|
+
border-radius: var(--dsw-radius-xs);
|
|
585
|
+
}
|
|
586
|
+
.hqpu-skeleton-heading { width: 96px; }
|
|
587
|
+
.hqpu-skeleton-title { width: min(144px, 60%); }
|
|
588
|
+
.hqpu-skeleton-desc { width: min(280px, 85%); }
|
|
589
|
+
.hqpu-skeleton-icon { border-color: transparent; }
|
|
590
|
+
@keyframes hqpu-skeleton {
|
|
591
|
+
0% { opacity: 1; }
|
|
592
|
+
40% { opacity: 0.6; }
|
|
593
|
+
80%, 100% { opacity: 1; }
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/* ── Toast (ui-primitives Toast.module.css, held inside the panel) ────── */
|
|
597
|
+
.hqpu-toast {
|
|
598
|
+
display: flex;
|
|
599
|
+
align-items: center;
|
|
600
|
+
gap: 10px;
|
|
601
|
+
padding: 12px 16px;
|
|
602
|
+
border-radius: var(--dsw-radius-lg);
|
|
603
|
+
background: var(--dsw-alias-toast-bg);
|
|
604
|
+
color: var(--dsw-alias-toast-label);
|
|
605
|
+
font-size: 14px;
|
|
606
|
+
line-height: 22px;
|
|
607
|
+
box-shadow: var(--dsw-shadow-lv3);
|
|
608
|
+
animation: hqpu-toast-in 160ms ease-out;
|
|
609
|
+
}
|
|
610
|
+
.hqpu-toast-icon {
|
|
611
|
+
display: grid;
|
|
612
|
+
place-items: center;
|
|
613
|
+
flex: none;
|
|
614
|
+
color: var(--dsw-alias-state-warn-label);
|
|
615
|
+
}
|
|
616
|
+
.hqpu-toast-icon[data-tone='success'] { color: var(--dsw-alias-state-success-primary); }
|
|
617
|
+
.hqpu-toast-text { min-width: 0; }
|
|
618
|
+
@keyframes hqpu-toast-in {
|
|
619
|
+
from { opacity: 0; transform: translateY(-6px); }
|
|
620
|
+
to { opacity: 1; transform: none; }
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/* ── Buttons (PluginManagerPage .addButton / .iconButton) ─────────────── */
|
|
624
|
+
.hqpu-icon-btn {
|
|
625
|
+
display: inline-flex;
|
|
626
|
+
align-items: center;
|
|
627
|
+
justify-content: center;
|
|
628
|
+
flex: none;
|
|
629
|
+
width: 28px;
|
|
630
|
+
height: 28px;
|
|
631
|
+
padding: 0;
|
|
632
|
+
border: 0;
|
|
633
|
+
border-radius: var(--dsw-radius-sm);
|
|
634
|
+
background: transparent;
|
|
635
|
+
color: var(--dsw-alias-label-caption);
|
|
636
|
+
cursor: pointer;
|
|
637
|
+
}
|
|
638
|
+
.hqpu-icon-btn svg { display: block; }
|
|
639
|
+
.hqpu-icon-btn:hover:not(:disabled) {
|
|
640
|
+
background: var(--dsw-alias-interactive-bg-hover);
|
|
641
|
+
color: var(--dsw-alias-label-secondary);
|
|
642
|
+
}
|
|
643
|
+
.hqpu-icon-btn:disabled {
|
|
644
|
+
opacity: 0.5;
|
|
645
|
+
cursor: default;
|
|
646
|
+
}
|
|
647
|
+
.hqpu-icon-btn:focus-visible {
|
|
648
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
649
|
+
outline-offset: 1px;
|
|
650
|
+
}
|
|
651
|
+
.hqpu-btn {
|
|
652
|
+
box-sizing: border-box;
|
|
653
|
+
display: inline-flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
justify-content: center;
|
|
656
|
+
gap: 6px;
|
|
657
|
+
height: 32px;
|
|
658
|
+
padding: 0 12px;
|
|
659
|
+
border: 0;
|
|
660
|
+
border-radius: var(--dsw-radius-md);
|
|
661
|
+
background: var(--dsw-alias-bg-layer-3);
|
|
662
|
+
color: var(--dsw-alias-label-primary);
|
|
663
|
+
font: inherit;
|
|
664
|
+
font-size: 13px;
|
|
665
|
+
line-height: 20px;
|
|
666
|
+
cursor: pointer;
|
|
667
|
+
}
|
|
668
|
+
.hqpu-btn svg { display: block; }
|
|
669
|
+
.hqpu-btn:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover-solid); }
|
|
670
|
+
.hqpu-btn:disabled { opacity: 0.5; cursor: default; }
|
|
671
|
+
.hqpu-btn:focus-visible {
|
|
672
|
+
outline: var(--dsw-focus-ring-width) solid var(--dsw-focus-ring-color, var(--dsw-alias-state-business-primary));
|
|
673
|
+
outline-offset: 1px;
|
|
674
|
+
}
|
|
675
|
+
.hqpu-btn--primary {
|
|
676
|
+
background: var(--dsw-alias-button-primary-fill);
|
|
677
|
+
color: var(--dsw-alias-label-primary-foreground);
|
|
678
|
+
}
|
|
679
|
+
.hqpu-btn--primary:hover:not(:disabled) { background: var(--dsw-alias-button-primary-hover); }
|
|
680
|
+
.hqpu-btn--outline {
|
|
681
|
+
background: transparent;
|
|
682
|
+
border: 0.5px solid var(--dsw-alias-border-l4);
|
|
683
|
+
color: var(--dsw-alias-label-secondary);
|
|
684
|
+
}
|
|
685
|
+
.hqpu-btn--outline:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
686
|
+
.hqpu-btn--ghost { background: transparent; color: var(--dsw-alias-label-secondary); }
|
|
687
|
+
.hqpu-btn--ghost:hover:not(:disabled) { background: var(--dsw-alias-interactive-bg-hover); }
|
|
688
|
+
.hqpu-btn--danger { color: var(--dsw-alias-state-error-primary); }
|
|
689
|
+
.hqpu-btn--sm { height: 28px; padding: 0 10px; font-size: 12.5px; }
|
|
690
|
+
.hqpu-actions {
|
|
691
|
+
display: flex;
|
|
692
|
+
align-items: center;
|
|
693
|
+
flex-wrap: wrap;
|
|
694
|
+
gap: 8px;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/* ── Footer action row (ui-primitives Modal .footer) ───────────────────── */
|
|
698
|
+
.hqpu-footer {
|
|
699
|
+
display: flex;
|
|
700
|
+
flex: none;
|
|
701
|
+
align-items: center;
|
|
702
|
+
justify-content: flex-end;
|
|
703
|
+
gap: 8px;
|
|
704
|
+
padding: 0 24px 20px;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
.hqpu-visually-hidden {
|
|
708
|
+
position: absolute;
|
|
709
|
+
width: 1px;
|
|
710
|
+
height: 1px;
|
|
711
|
+
margin: -1px;
|
|
712
|
+
padding: 0;
|
|
713
|
+
overflow: hidden;
|
|
714
|
+
clip-path: inset(50%);
|
|
715
|
+
white-space: nowrap;
|
|
716
|
+
border: 0;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/* ── Reduced motion: drop every decorative animation ──────────────────── */
|
|
720
|
+
@media (prefers-reduced-motion: reduce) {
|
|
721
|
+
.hqpu-scrim,
|
|
722
|
+
.hqpu-panel,
|
|
723
|
+
.hqpu-skeleton-fill,
|
|
724
|
+
.hqpu-spinner,
|
|
725
|
+
.hqpu-toast {
|
|
726
|
+
animation: none;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
`
|
|
730
|
+
|
|
731
|
+
const STYLE_TAG_ID = 'data-plugin-css="huaqiu-plugin-update"'
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Design tokens the bundled DSH shell may not publish.
|
|
735
|
+
*
|
|
736
|
+
* hq-edge ships an older `ui-theme` build. A live probe of the running shell
|
|
737
|
+
* (both themes) shows 13 of the tokens this stylesheet reads are absent:
|
|
738
|
+
* `--dsw-radius-{xs,sm,md,lg,xl,panel}`, `--dsw-focus-ring-width`,
|
|
739
|
+
* `--dsw-focus-ring-color`, `--dsw-alias-toast-label`,
|
|
740
|
+
* `--dsw-alias-state-idle-primary`, `--dsw-font-mono`,
|
|
741
|
+
* `--dsh-frame-overlay-top` and `--dsh-frame-chrome-top`.
|
|
742
|
+
*
|
|
743
|
+
* An unresolved `var()` is not an error — it silently degrades the property to
|
|
744
|
+
* its initial value. That is how this panel once rendered with
|
|
745
|
+
* `border-radius: 0px` and a 3px `auto` focus ring while every colour was
|
|
746
|
+
* already correct.
|
|
747
|
+
*
|
|
748
|
+
* The last three (`--dsw-font-mono`, `--dsh-frame-*`) are deliberately NOT
|
|
749
|
+
* backfilled: the shell's own primitives rely on the inline `var(…, fallback)`
|
|
750
|
+
* form for those (`Modal.module.css`, `PluginManagerPage.module.css`), so this
|
|
751
|
+
* sheet does too. Everything else is backfilled below with the canonical value
|
|
752
|
+
* from `ui-theme/src/styles/{base,focus,design-platform}.css`.
|
|
753
|
+
*
|
|
754
|
+
* Values are feature-detected at runtime and scoped to this plugin's own roots
|
|
755
|
+
* rather than `:root`, so a newer shell always wins and filling a gap can never
|
|
756
|
+
* restyle the host.
|
|
757
|
+
*/
|
|
758
|
+
|
|
759
|
+
/** The two roots this plugin styles: the view overlay (built in
|
|
760
|
+
* update-view.ts) and the sidebar row ({@link ENTRY_CSS}.entry). */
|
|
761
|
+
const STYLE_ROOTS = '.hqpu-overlay, .hqpu-sidebar-entry'
|
|
762
|
+
|
|
763
|
+
/** The shell's dark-theme carrier — audited live as `body[data-ds-dark-theme]`. */
|
|
764
|
+
const DARK_THEME_SELECTOR = 'body[data-ds-dark-theme]'
|
|
765
|
+
|
|
766
|
+
/** Same roots, one rule per theme, for tokens whose official value flips. */
|
|
767
|
+
const DARK_STYLE_ROOTS = STYLE_ROOTS.split(',')
|
|
768
|
+
.map((root) => `${DARK_THEME_SELECTOR} ${root.trim()}`)
|
|
769
|
+
.join(',')
|
|
770
|
+
|
|
771
|
+
type DesignTokenFallback =
|
|
772
|
+
| { token: string; value: string }
|
|
773
|
+
| { token: string; value: { light: string; dark: string } }
|
|
774
|
+
|
|
775
|
+
const DESIGN_TOKEN_FALLBACKS: DesignTokenFallback[] = [
|
|
776
|
+
// base.css — geometry; theme-invariant.
|
|
777
|
+
{ token: '--dsw-radius-xs', value: '4px' },
|
|
778
|
+
{ token: '--dsw-radius-sm', value: '8px' },
|
|
779
|
+
{ token: '--dsw-radius-md', value: '12px' },
|
|
780
|
+
{ token: '--dsw-radius-lg', value: '16px' },
|
|
781
|
+
{ token: '--dsw-radius-xl', value: '20px' },
|
|
782
|
+
{ token: '--dsw-radius-panel', value: '28px' },
|
|
783
|
+
// focus.css — the standard ring geometry. The colour already falls back
|
|
784
|
+
// inline in the sheet; publishing it too keeps the ring intact if that
|
|
785
|
+
// fallback is ever edited away.
|
|
786
|
+
{ token: '--dsw-focus-ring-width', value: '2px' },
|
|
787
|
+
{ token: '--dsw-focus-ring-color', value: 'var(--dsw-alias-state-business-primary, #C7000B)' },
|
|
788
|
+
// design-platform.css — `--dsw-static-neutral-bluish-00` is white in both
|
|
789
|
+
// themes, which is exactly what the official alias resolves to.
|
|
790
|
+
{ token: '--dsw-alias-toast-label', value: 'var(--dsw-static-neutral-bluish-00, #fff)' },
|
|
791
|
+
// The one token whose official value flips with the theme. `--dsw-static-*`
|
|
792
|
+
// are theme-invariant, so one rule per carrier reproduces neutral-300
|
|
793
|
+
// (light) / neutral-600 (dark) without knowing the current theme.
|
|
794
|
+
{
|
|
795
|
+
token: '--dsw-alias-state-idle-primary',
|
|
796
|
+
value: {
|
|
797
|
+
light: 'var(--dsw-static-neutral-300, #d4d4d4)',
|
|
798
|
+
dark: 'var(--dsw-static-neutral-600, #545557)',
|
|
799
|
+
},
|
|
800
|
+
},
|
|
801
|
+
]
|
|
802
|
+
|
|
803
|
+
const TOKEN_STYLE_TAG_ID = 'data-plugin-css="huaqiu-plugin-update-tokens"'
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Publish canonical values for the design tokens the shell is missing.
|
|
807
|
+
*
|
|
808
|
+
* Idempotent and cheap: safe to call on every open, so a shell that publishes
|
|
809
|
+
* its tokens late still wins (the stale fallbacks are removed in that case).
|
|
810
|
+
*/
|
|
811
|
+
export function ensureDesignTokens(): void {
|
|
812
|
+
if (typeof document === 'undefined' || document.body === null) return
|
|
813
|
+
const probe = getComputedStyle(document.body)
|
|
814
|
+
const missing = DESIGN_TOKEN_FALLBACKS.filter(
|
|
815
|
+
(entry) => probe.getPropertyValue(entry.token).trim() === '',
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
const existing = document.querySelector<HTMLStyleElement>(`style[${TOKEN_STYLE_TAG_ID}]`)
|
|
819
|
+
if (missing.length === 0) {
|
|
820
|
+
existing?.remove()
|
|
821
|
+
return
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
const flat = missing.filter(
|
|
825
|
+
(entry): entry is { token: string; value: string } => typeof entry.value === 'string',
|
|
826
|
+
)
|
|
827
|
+
const themed = missing.filter(
|
|
828
|
+
(entry): entry is { token: string; value: { light: string; dark: string } } =>
|
|
829
|
+
typeof entry.value !== 'string',
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
const rules: string[] = []
|
|
833
|
+
const lightDecls = [
|
|
834
|
+
...flat.map((entry) => ` ${entry.token}: ${entry.value};`),
|
|
835
|
+
...themed.map((entry) => ` ${entry.token}: ${entry.value.light};`),
|
|
836
|
+
]
|
|
837
|
+
rules.push(`${STYLE_ROOTS} {\n${lightDecls.join('\n')}\n}`)
|
|
838
|
+
if (themed.length > 0) {
|
|
839
|
+
const darkDecls = themed.map((entry) => ` ${entry.token}: ${entry.value.dark};`)
|
|
840
|
+
rules.push(`${DARK_STYLE_ROOTS} {\n${darkDecls.join('\n')}\n}`)
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const tag = existing ?? document.createElement('style')
|
|
844
|
+
tag.dataset.pluginCss = 'huaqiu-plugin-update-tokens'
|
|
845
|
+
tag.textContent = rules.join('\n')
|
|
846
|
+
if (tag.parentNode === null) document.head.appendChild(tag)
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
/** Inject styles once (idempotent). */
|
|
850
|
+
export function injectUpdateStyles(): void {
|
|
851
|
+
if (typeof document === 'undefined') return
|
|
852
|
+
if (document.querySelector(`style[${STYLE_TAG_ID}]`) !== null) return
|
|
853
|
+
const tag = document.createElement('style')
|
|
854
|
+
tag.dataset.pluginCss = 'huaqiu-plugin-update'
|
|
855
|
+
tag.textContent = `${ENTRY_CSS_TEXT}\n${VIEW_CSS_TEXT}`
|
|
856
|
+
document.head.appendChild(tag)
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/** Minimal structural client context (dsh-client-runtime provides these). */
|
|
860
|
+
export interface ClientContext {
|
|
861
|
+
get<T = unknown>(name: string): T | undefined
|
|
862
|
+
effect?(fn: () => (() => void) | void, label?: string): void
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
/** In-memory update-report store shared by the badge and the modal. */
|
|
866
|
+
class UpdateStore implements UpdateStoreLike {
|
|
867
|
+
private current: PluginUpdateReport | null = null
|
|
868
|
+
private installingFlag = false
|
|
869
|
+
private readonly listeners = new Set<() => void>()
|
|
870
|
+
private refreshTimer: number | null = null
|
|
871
|
+
|
|
872
|
+
report(): PluginUpdateReport | null {
|
|
873
|
+
return this.current
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
installing(): boolean {
|
|
877
|
+
return this.installingFlag
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
subscribe(listener: () => void): () => void {
|
|
881
|
+
this.listeners.add(listener)
|
|
882
|
+
return () => {
|
|
883
|
+
this.listeners.delete(listener)
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/** Badge count per spec §6.3: actionable `update` + `install`, excluding the
|
|
888
|
+
* ignored offer (the server already folds ignore into item states; keep the
|
|
889
|
+
* client-side filter as a defensive second pass). */
|
|
890
|
+
badgeCount(): number {
|
|
891
|
+
const report = this.current
|
|
892
|
+
if (report === null) return 0
|
|
893
|
+
return report.items.filter(
|
|
894
|
+
(item) => item.state === 'update' || item.state === 'install',
|
|
895
|
+
).length
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
private emit(): void {
|
|
899
|
+
for (const listener of [...this.listeners]) {
|
|
900
|
+
try {
|
|
901
|
+
listener()
|
|
902
|
+
} catch {
|
|
903
|
+
/* one bad listener must not strand the rest */
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
async refresh(): Promise<PluginUpdateReport | null> {
|
|
909
|
+
try {
|
|
910
|
+
const report = await fetchReport()
|
|
911
|
+
this.current = report
|
|
912
|
+
this.emit()
|
|
913
|
+
return report
|
|
914
|
+
} catch {
|
|
915
|
+
return this.current
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
async check(): Promise<PluginUpdateReport | null> {
|
|
920
|
+
try {
|
|
921
|
+
const report = await checkUpdates()
|
|
922
|
+
this.current = report
|
|
923
|
+
this.emit()
|
|
924
|
+
return report
|
|
925
|
+
} catch {
|
|
926
|
+
return this.current
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
/** Mark the set as installing (set by the modal before the restart). */
|
|
931
|
+
setInstalling(flag: boolean): void {
|
|
932
|
+
this.installingFlag = flag
|
|
933
|
+
this.emit()
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
/** Idle refresh while mounted (60 s) — no overlapping poll loop. */
|
|
937
|
+
startIdleRefresh(): void {
|
|
938
|
+
if (this.refreshTimer !== null) return
|
|
939
|
+
this.refreshTimer = window.setInterval(() => {
|
|
940
|
+
void this.refresh()
|
|
941
|
+
}, 60_000) as unknown as number
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
stop(): void {
|
|
945
|
+
if (this.refreshTimer !== null) {
|
|
946
|
+
window.clearInterval(this.refreshTimer)
|
|
947
|
+
this.refreshTimer = null
|
|
948
|
+
}
|
|
949
|
+
this.listeners.clear()
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/**
|
|
954
|
+
* Client cordis inject: no service dependencies — the plugin reads the
|
|
955
|
+
* hq-edge report purely through same-origin fetch (forwarded by its own node
|
|
956
|
+
* half). `inject` stays empty so the loader never waits on another service.
|
|
957
|
+
*/
|
|
958
|
+
export const inject: string[] = []
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* Client plugin body — mount the sidebar toolbutton + review modal.
|
|
962
|
+
* @param ctx - real cordis client context.
|
|
963
|
+
* @returns disposer removing the row and all subscriptions.
|
|
964
|
+
*/
|
|
965
|
+
export function apply(ctx: ClientContext): () => void {
|
|
966
|
+
if (typeof document === 'undefined') return () => {}
|
|
967
|
+
injectUpdateStyles()
|
|
968
|
+
ensureDesignTokens()
|
|
969
|
+
|
|
970
|
+
const store = new UpdateStore()
|
|
971
|
+
const copy = { zh: COPY.zh, en: COPY.en }
|
|
972
|
+
const copyFor = (): typeof COPY.zh => (locale === 'en' ? copy.en : copy.zh)
|
|
973
|
+
|
|
974
|
+
// Locale: re-render the row copy when <html lang> changes. The sidebar
|
|
975
|
+
// helper subscribes via `refresh.subscribe`; we fan out to its listener.
|
|
976
|
+
let locale: HostLocale | undefined = detectHostLocale()
|
|
977
|
+
const localeSubscribers = new Set<() => void>()
|
|
978
|
+
const refreshSubscribe = (listener: () => void): (() => void) => {
|
|
979
|
+
localeSubscribers.add(listener)
|
|
980
|
+
return () => {
|
|
981
|
+
localeSubscribers.delete(listener)
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
const unsubscribeLocale = observeHostLocale((next) => {
|
|
985
|
+
locale = next
|
|
986
|
+
for (const fn of [...localeSubscribers]) {
|
|
987
|
+
try {
|
|
988
|
+
fn()
|
|
989
|
+
} catch {
|
|
990
|
+
/* one bad subscriber must not strand the rest */
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
})
|
|
994
|
+
const labelState = { label: entryCopy(locale).label, tooltip: entryCopy(locale).tooltip }
|
|
995
|
+
const applyLabel = (): void => {
|
|
996
|
+
const c = entryCopy(locale)
|
|
997
|
+
labelState.label = c.label
|
|
998
|
+
labelState.tooltip = c.tooltip
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
const openView = (): void => {
|
|
1002
|
+
// Re-detect the shell's design tokens on every open: a shell that
|
|
1003
|
+
// publishes them after this plugin mounted must win, and one that never
|
|
1004
|
+
// does must keep the canonical fallbacks.
|
|
1005
|
+
ensureDesignTokens()
|
|
1006
|
+
// Always re-check on open so the review screen never shows stale data.
|
|
1007
|
+
void store.check().then((report) => {
|
|
1008
|
+
if (report !== null) openUpdateView(store, copyFor())
|
|
1009
|
+
})
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
const disposeEntry = mountSidebarEntry({
|
|
1013
|
+
rowAttribute: UPDATE_ENTRY_ATTRIBUTE,
|
|
1014
|
+
rowSelector: UPDATE_ENTRY_SELECTOR,
|
|
1015
|
+
plugin: PLUGIN_ID,
|
|
1016
|
+
icon: UPDATE_ICON,
|
|
1017
|
+
css: ENTRY_CSS,
|
|
1018
|
+
label: () => labelState.label,
|
|
1019
|
+
tooltip: () => labelState.tooltip,
|
|
1020
|
+
refresh: { subscribe: refreshSubscribe },
|
|
1021
|
+
badge: {
|
|
1022
|
+
subscribe: (listener) => store.subscribe(listener),
|
|
1023
|
+
count: () => store.badgeCount(),
|
|
1024
|
+
},
|
|
1025
|
+
onToggle: openView,
|
|
1026
|
+
position: 'after',
|
|
1027
|
+
familySelectors: HUAQIU_SIDEBAR_ENTRY_FAMILY,
|
|
1028
|
+
})
|
|
1029
|
+
|
|
1030
|
+
store.startIdleRefresh()
|
|
1031
|
+
void store.refresh()
|
|
1032
|
+
|
|
1033
|
+
const dispose = (): void => {
|
|
1034
|
+
try {
|
|
1035
|
+
disposeEntry()
|
|
1036
|
+
} catch {
|
|
1037
|
+
/* already disposed */
|
|
1038
|
+
}
|
|
1039
|
+
unsubscribeLocale()
|
|
1040
|
+
store.stop()
|
|
1041
|
+
}
|
|
1042
|
+
if (typeof ctx.effect === 'function') {
|
|
1043
|
+
// `ctx.effect` takes a SETUP function and calls it immediately, keeping
|
|
1044
|
+
// whatever it returns as the teardown. Passing `dispose` itself would run
|
|
1045
|
+
// the teardown synchronously — disconnecting the sidebar-entry observers
|
|
1046
|
+
// and stopping the store before the shell ever renders the sidebar, so the
|
|
1047
|
+
// row could never be placed. Hand it a factory that returns the teardown
|
|
1048
|
+
// instead (the same shape the sibling tool plugins use).
|
|
1049
|
+
ctx.effect(() => dispose, 'plugin-update')
|
|
1050
|
+
}
|
|
1051
|
+
return dispose
|
|
1052
|
+
}
|