@chatcode/cco-market 1.45.1
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/README.md +146 -0
- package/README.zh.md +143 -0
- package/UPDATE-API-V1.md +132 -0
- package/client/client.js +11534 -0
- package/cordis.patch.yml +5 -0
- package/lib/accelerate.js +184 -0
- package/lib/agents.js +36 -0
- package/lib/backup.js +572 -0
- package/lib/catalog-local-match.js +126 -0
- package/lib/catalog-npm.js +111 -0
- package/lib/changelog.js +242 -0
- package/lib/channels.js +64 -0
- package/lib/check.js +1067 -0
- package/lib/compatibility.js +198 -0
- package/lib/diagnostics.js +49 -0
- package/lib/discovery-compatibility.js +260 -0
- package/lib/dsh-cli.js +949 -0
- package/lib/dsh-install.js +108 -0
- package/lib/gist.js +352 -0
- package/lib/groups.js +97 -0
- package/lib/home-paths.js +42 -0
- package/lib/hot.js +519 -0
- package/lib/http.js +39 -0
- package/lib/index.js +97 -0
- package/lib/install.js +387 -0
- package/lib/log.js +197 -0
- package/lib/ndjson.js +154 -0
- package/lib/net.js +98 -0
- package/lib/order.js +284 -0
- package/lib/patch.js +521 -0
- package/lib/pnpm-compat.js +490 -0
- package/lib/presets.js +278 -0
- package/lib/profile.js +911 -0
- package/lib/region-probe.js +93 -0
- package/lib/regions.js +253 -0
- package/lib/registry.js +204 -0
- package/lib/restart.js +373 -0
- package/lib/routes.js +4372 -0
- package/lib/settings.js +109 -0
- package/lib/skill-market/api.js +119 -0
- package/lib/skill-market/config.js +29 -0
- package/lib/skill-market/installer.js +352 -0
- package/lib/skill-market/runtime.js +13 -0
- package/lib/skill-market/tui.js +119 -0
- package/lib/skill-market/types.js +1 -0
- package/lib/skill-market/web-routes.js +123 -0
- package/lib/snapshot.js +500 -0
- package/lib/source-migration.js +54 -0
- package/lib/sources.js +596 -0
- package/lib/store.js +91 -0
- package/lib/themes.js +102 -0
- package/lib/trial.js +116 -0
- package/lib/types/accelerate.d.ts +85 -0
- package/lib/types/agents.d.ts +22 -0
- package/lib/types/backup.d.ts +131 -0
- package/lib/types/catalog-local-match.d.ts +50 -0
- package/lib/types/catalog-npm.d.ts +47 -0
- package/lib/types/changelog.d.ts +102 -0
- package/lib/types/channels.d.ts +56 -0
- package/lib/types/check.d.ts +266 -0
- package/lib/types/compatibility.d.ts +64 -0
- package/lib/types/diagnostics.d.ts +29 -0
- package/lib/types/discovery-compatibility.d.ts +80 -0
- package/lib/types/dsh-cli.d.ts +324 -0
- package/lib/types/dsh-install.d.ts +46 -0
- package/lib/types/gist.d.ts +53 -0
- package/lib/types/groups.d.ts +34 -0
- package/lib/types/home-paths.d.ts +16 -0
- package/lib/types/hot.d.ts +221 -0
- package/lib/types/http.d.ts +12 -0
- package/lib/types/index.d.ts +14 -0
- package/lib/types/install.d.ts +172 -0
- package/lib/types/log.d.ts +43 -0
- package/lib/types/ndjson.d.ts +52 -0
- package/lib/types/net.d.ts +58 -0
- package/lib/types/order.d.ts +100 -0
- package/lib/types/patch.d.ts +129 -0
- package/lib/types/pnpm-compat.d.ts +93 -0
- package/lib/types/presets.d.ts +90 -0
- package/lib/types/profile.d.ts +253 -0
- package/lib/types/region-probe.d.ts +50 -0
- package/lib/types/regions.d.ts +122 -0
- package/lib/types/registry.d.ts +86 -0
- package/lib/types/restart.d.ts +194 -0
- package/lib/types/routes.d.ts +67 -0
- package/lib/types/settings.d.ts +78 -0
- package/lib/types/skill-market/api.d.ts +10 -0
- package/lib/types/skill-market/config.d.ts +3 -0
- package/lib/types/skill-market/installer.d.ts +23 -0
- package/lib/types/skill-market/runtime.d.ts +10 -0
- package/lib/types/skill-market/tui.d.ts +4 -0
- package/lib/types/skill-market/types.d.ts +71 -0
- package/lib/types/skill-market/web-routes.d.ts +19 -0
- package/lib/types/snapshot.d.ts +93 -0
- package/lib/types/source-migration.d.ts +11 -0
- package/lib/types/sources.d.ts +216 -0
- package/lib/types/store.d.ts +30 -0
- package/lib/types/themes.d.ts +40 -0
- package/lib/types/trial.d.ts +61 -0
- package/lib/types/update-api-v1.d.ts +66 -0
- package/lib/types/updates.d.ts +123 -0
- package/lib/types/verify.d.ts +139 -0
- package/lib/update-api-v1.js +215 -0
- package/lib/updates.js +361 -0
- package/lib/verify.js +453 -0
- package/package.json +121 -0
- package/src/accelerate.ts +213 -0
- package/src/agents.ts +43 -0
- package/src/backup.ts +583 -0
- package/src/catalog-local-match.ts +144 -0
- package/src/catalog-npm.ts +120 -0
- package/src/changelog.ts +282 -0
- package/src/channels.ts +70 -0
- package/src/check.ts +1239 -0
- package/src/client/CommentsModal.tsx +119 -0
- package/src/client/Diagnostics.tsx +907 -0
- package/src/client/ErrorBoundary.tsx +111 -0
- package/src/client/InstallToast.tsx +31 -0
- package/src/client/Market.module.css +830 -0
- package/src/client/MarketSection.tsx +5301 -0
- package/src/client/OperationsPanel.tsx +365 -0
- package/src/client/SettingsCard.tsx +621 -0
- package/src/client/SkillMarket.module.css +21 -0
- package/src/client/SkillMarketSection.tsx +162 -0
- package/src/client/comments.ts +54 -0
- package/src/client/globals.d.ts +13 -0
- package/src/client/index.ts +180 -0
- package/src/client/locales.ts +1112 -0
- package/src/client/market-data.ts +1321 -0
- package/src/client/operations.ts +201 -0
- package/src/client/preset-panel.tsx +263 -0
- package/src/client/primitives.d.ts +140 -0
- package/src/client/self-check.ts +147 -0
- package/src/client/snapshot-panel.tsx +244 -0
- package/src/compatibility.ts +237 -0
- package/src/diagnostics.ts +84 -0
- package/src/discovery-compatibility.ts +315 -0
- package/src/dsh-cli.ts +1126 -0
- package/src/dsh-install.ts +118 -0
- package/src/gist.ts +362 -0
- package/src/groups.ts +111 -0
- package/src/home-paths.ts +53 -0
- package/src/hot.ts +628 -0
- package/src/http.ts +41 -0
- package/src/index.ts +128 -0
- package/src/install.ts +420 -0
- package/src/log.ts +206 -0
- package/src/ndjson.ts +185 -0
- package/src/net.ts +106 -0
- package/src/order.ts +303 -0
- package/src/patch.ts +522 -0
- package/src/pnpm-compat.ts +527 -0
- package/src/presets.ts +344 -0
- package/src/profile.ts +940 -0
- package/src/region-probe.ts +97 -0
- package/src/regions.ts +310 -0
- package/src/registry.ts +250 -0
- package/src/restart.ts +396 -0
- package/src/routes.ts +4506 -0
- package/src/settings.ts +141 -0
- package/src/skill-market/api.ts +130 -0
- package/src/skill-market/config.ts +32 -0
- package/src/skill-market/installer.ts +337 -0
- package/src/skill-market/runtime.ts +14 -0
- package/src/skill-market/tui.ts +157 -0
- package/src/skill-market/types.ts +80 -0
- package/src/skill-market/web-routes.ts +132 -0
- package/src/snapshot.ts +532 -0
- package/src/source-migration.ts +61 -0
- package/src/sources.ts +565 -0
- package/src/store.ts +89 -0
- package/src/themes.ts +125 -0
- package/src/trial.ts +156 -0
- package/src/update-api-v1.ts +277 -0
- package/src/updates.ts +400 -0
- package/src/verify.ts +492 -0
|
@@ -0,0 +1,621 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The market's card on the plugin configuration page (dsh >= 0.1.0-rc.7).
|
|
3
|
+
*
|
|
4
|
+
* It manages the market ITSELF — version, update, remove — when the current
|
|
5
|
+
* profile owns the dependency. A host-provided market keeps only its version
|
|
6
|
+
* display and download-region controls. This page is where a user goes to deal
|
|
7
|
+
* with a plugin,
|
|
8
|
+
* and "which version am I on / update it / get rid of it" is the part of
|
|
9
|
+
* that anybody can act on without knowing how ChatCode CLI is put together.
|
|
10
|
+
*
|
|
11
|
+
* `allowRestart` deliberately does NOT appear here. It exists for hosts
|
|
12
|
+
* where a supervisor (systemd, launchd, pm2, Docker `restart: always`, a
|
|
13
|
+
* desktop wrapper) owns the process, and its audience is whoever wrote that
|
|
14
|
+
* deployment — a person already editing config, not someone browsing
|
|
15
|
+
* settings. As a switch it read as jargon to everyone else, which is worse
|
|
16
|
+
* than absent: a control you cannot evaluate is a control you cannot safely
|
|
17
|
+
* touch. It remains a config option.
|
|
18
|
+
*
|
|
19
|
+
* ## Why the chrome is hand-built (again), and why it now matches
|
|
20
|
+
*
|
|
21
|
+
* The host's own contract is that "a plugin that ships a browser half owns
|
|
22
|
+
* its own card" — the plugins tab only lays out a flex column and dispatches
|
|
23
|
+
* `settings.plugin.item`. So the container IS ours to draw, and a value
|
|
24
|
+
* import from `dsh-client-ui-settings-plugins` would fail the client
|
|
25
|
+
* bundle-purity gate anyway.
|
|
26
|
+
*
|
|
27
|
+
* What the first version got wrong was drawing something of its own
|
|
28
|
+
* invention: a flat, always-expanded box next to rows that collapse and
|
|
29
|
+
* carry a chevron. The fix is not a different component — `DisclosureRow` is
|
|
30
|
+
* 24px chrome for compact flow rows, a different thing — but the same design
|
|
31
|
+
* tokens, laid out the way the host lays out `PluginCard`. Classes below
|
|
32
|
+
* mirror it one for one, so the market stops looking like it wandered in
|
|
33
|
+
* from another product.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import { createElement as h, Fragment, useCallback, useEffect, useRef, useState } from 'react'
|
|
37
|
+
import type { ReactElement } from 'react'
|
|
38
|
+
import { Button, IconChevronDownOutline14, IconLoadingOutline16 } from '@deepseek-ai/dsh-client-ui-primitives'
|
|
39
|
+
import css from './Market.module.css'
|
|
40
|
+
import { api, applyGithubRouting } from './market-data.ts'
|
|
41
|
+
import type { MarketStatus } from './market-data.ts'
|
|
42
|
+
import type { Translate } from './market-data.ts'
|
|
43
|
+
|
|
44
|
+
/** Keys the market leaves in the browser; cleared when the user purges. */
|
|
45
|
+
const BROWSER_KEYS = ['dshm-webdav', 'dshm-gist-id'] as const
|
|
46
|
+
const SELF_PACKAGE_NAMES = ['@chatcode/cco-market', 'dshmarket', 'dsh-market'] as const
|
|
47
|
+
|
|
48
|
+
export interface SettingsCardProps {
|
|
49
|
+
t: Translate
|
|
50
|
+
/**
|
|
51
|
+
* Retire the market's own entry in the left settings menu.
|
|
52
|
+
*
|
|
53
|
+
* Called once the package is gone. Leaving "插件市场" in the menu after
|
|
54
|
+
* the user removed it is the card asserting something the profile no
|
|
55
|
+
* longer agrees with — and the section behind it can no longer talk to a
|
|
56
|
+
* server that has disposed its routes.
|
|
57
|
+
*/
|
|
58
|
+
onRemoved?: () => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type Channel = 'stable' | 'beta' | 'dev'
|
|
62
|
+
type Region = 'global' | 'china'
|
|
63
|
+
|
|
64
|
+
/** What api('/dsh-market/status') tells this card. */
|
|
65
|
+
interface SelfStatus {
|
|
66
|
+
version: string | null
|
|
67
|
+
restart: boolean
|
|
68
|
+
channel: Channel
|
|
69
|
+
/** The channels the SERVER offers; the card never invents its own list. */
|
|
70
|
+
channels: Channel[]
|
|
71
|
+
/** Which mirrors every outbound request uses. */
|
|
72
|
+
region: Region
|
|
73
|
+
/** The regions the SERVER offers, on the same principle as `channels`. */
|
|
74
|
+
regions: Region[]
|
|
75
|
+
/** The region came from the network check, not from the user. */
|
|
76
|
+
regionAuto: boolean
|
|
77
|
+
/** User-maintained fallback prefix, separate from the active learned route. */
|
|
78
|
+
githubProxyCustom: string | null
|
|
79
|
+
/** Environment-owned prefixes are visible but not editable. */
|
|
80
|
+
githubProxyManaged: boolean
|
|
81
|
+
/** Whether the profile package manager owns this market installation. */
|
|
82
|
+
selfManaged: boolean
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The subset of api('/dsh-market/status') this card reads. */
|
|
86
|
+
interface StatusBody {
|
|
87
|
+
version?: string
|
|
88
|
+
restart?: boolean
|
|
89
|
+
channel?: string
|
|
90
|
+
channels?: string[]
|
|
91
|
+
region?: string
|
|
92
|
+
regions?: string[]
|
|
93
|
+
regionAuto?: boolean
|
|
94
|
+
githubProxy?: string | null
|
|
95
|
+
githubRoutes?: MarketStatus['githubRoutes']
|
|
96
|
+
githubProxyCustom?: string | null
|
|
97
|
+
githubProxyManaged?: boolean
|
|
98
|
+
selfManaged?: boolean
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What api('/dsh-market/updates') says about the market's own row. */
|
|
102
|
+
interface SelfUpdate {
|
|
103
|
+
updateAvailable: boolean
|
|
104
|
+
latest: string | null
|
|
105
|
+
/** The channel points at this version, and it is not newer: a switch. */
|
|
106
|
+
channelSwitch: string | null
|
|
107
|
+
/** The current build came from a local package and must switch to the online release. */
|
|
108
|
+
restoreRequired: boolean
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
type Phase = 'idle' | 'confirming' | 'working' | 'removed' | 'updated' | 'failed'
|
|
112
|
+
|
|
113
|
+
/** The market's own row as api('/dsh-market/updates') sends it. */
|
|
114
|
+
interface RawUpdate { updateAvailable?: boolean; latest?: string; channelSwitch?: string; restoreRequired?: boolean }
|
|
115
|
+
|
|
116
|
+
const CHANNELS: Channel[] = ['stable', 'beta', 'dev']
|
|
117
|
+
const asChannel = (value: unknown): Channel | null =>
|
|
118
|
+
CHANNELS.includes(value as Channel) ? (value as Channel) : null
|
|
119
|
+
|
|
120
|
+
const CHANNEL_LABEL: Record<Channel, string> = {
|
|
121
|
+
stable: 'setChannelStable', beta: 'setChannelBeta', dev: 'setChannelDev',
|
|
122
|
+
}
|
|
123
|
+
const CHANNEL_HINT: Record<Channel, string> = {
|
|
124
|
+
stable: 'setChannelStableHint', beta: 'setChannelBetaHint', dev: 'setChannelDevHint',
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const REGIONS: Region[] = ['global', 'china']
|
|
128
|
+
const asRegion = (value: unknown): Region | null =>
|
|
129
|
+
REGIONS.includes(value as Region) ? (value as Region) : null
|
|
130
|
+
|
|
131
|
+
const REGION_LABEL: Record<Region, string> = {
|
|
132
|
+
global: 'setRegionGlobal', china: 'setRegionChina',
|
|
133
|
+
}
|
|
134
|
+
const REGION_HINT: Record<Region, string> = {
|
|
135
|
+
global: 'setRegionGlobalHint', china: 'setRegionChinaHint',
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Read the server's answer, taking the list of channels FROM it.
|
|
140
|
+
*
|
|
141
|
+
* The card does not decide which channels exist: the server is what accepts
|
|
142
|
+
* or refuses a selection, so a card drawing its own list could only ever
|
|
143
|
+
* disagree with it.
|
|
144
|
+
*/
|
|
145
|
+
function readStatus(body: StatusBody): SelfStatus {
|
|
146
|
+
const offered = (body.channels ?? []).map(asChannel).filter((c): c is Channel => c !== null)
|
|
147
|
+
const regions = (body.regions ?? []).map(asRegion).filter((r): r is Region => r !== null)
|
|
148
|
+
return {
|
|
149
|
+
version: body.version ?? null,
|
|
150
|
+
restart: body.restart === true,
|
|
151
|
+
channel: asChannel(body.channel) ?? 'stable',
|
|
152
|
+
// A host too old to send the list still gets the two that predate it.
|
|
153
|
+
channels: offered.length > 0 ? offered : ['stable', 'beta'],
|
|
154
|
+
region: asRegion(body.region) ?? 'global',
|
|
155
|
+
// A host with no region at all is a host from before this existed. It
|
|
156
|
+
// downloads from the official sources, which is what `global` means, so
|
|
157
|
+
// the row tells the truth about that host rather than hiding.
|
|
158
|
+
regions: regions.length > 0 ? regions : REGIONS,
|
|
159
|
+
regionAuto: body.regionAuto === true,
|
|
160
|
+
githubProxyCustom: typeof body.githubProxyCustom === 'string' ? body.githubProxyCustom : null,
|
|
161
|
+
githubProxyManaged: body.githubProxyManaged === true,
|
|
162
|
+
selfManaged: body.selfManaged !== false,
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function readUpdate(own: RawUpdate): SelfUpdate {
|
|
167
|
+
return {
|
|
168
|
+
updateAvailable: own.updateAvailable === true,
|
|
169
|
+
latest: own.latest ?? null,
|
|
170
|
+
channelSwitch: own.channelSwitch ?? null,
|
|
171
|
+
restoreRequired: own.restoreRequired === true,
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Clear the market's browser-side leftovers.
|
|
177
|
+
*
|
|
178
|
+
* These are the only two things the market keeps in the browser, and the
|
|
179
|
+
* server cannot reach either. Neither holds a credential — the WebDAV
|
|
180
|
+
* password is never persisted and a Gist token is read from the environment,
|
|
181
|
+
* never from disk — so this is tidiness, not a security step, and the copy
|
|
182
|
+
* must not imply otherwise.
|
|
183
|
+
*/
|
|
184
|
+
export function clearBrowserState(storage: Pick<Storage, 'removeItem'>): void {
|
|
185
|
+
for (const key of BROWSER_KEYS) {
|
|
186
|
+
try { storage.removeItem(key) } catch { /* storage unavailable */ }
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function SettingsCard({ t, onRemoved }: SettingsCardProps): ReactElement | null {
|
|
191
|
+
const [open, setOpen] = useState(false)
|
|
192
|
+
const [status, setStatus] = useState<SelfStatus | null>(null)
|
|
193
|
+
const [update, setUpdate] = useState<SelfUpdate | null>(null)
|
|
194
|
+
const [selfPackageName, setSelfPackageName] = useState<string>('@chatcode/cco-market')
|
|
195
|
+
const [phase, setPhase] = useState<Phase>('idle')
|
|
196
|
+
const [restoreConfirming, setRestoreConfirming] = useState(false)
|
|
197
|
+
const [purge, setPurge] = useState(false)
|
|
198
|
+
const [error, setError] = useState<string | null>(null)
|
|
199
|
+
const [proxyEditing, setProxyEditing] = useState(false)
|
|
200
|
+
const [proxyDraft, setProxyDraft] = useState('')
|
|
201
|
+
const [proxySaving, setProxySaving] = useState(false)
|
|
202
|
+
/**
|
|
203
|
+
* The last self-update was refused by pnpm's fresh-release safety wait
|
|
204
|
+
* (#39). Only the market's own card can update the market, so without a
|
|
205
|
+
* retry here there is NO way to take a just-published version — the
|
|
206
|
+
* discover list's retry button covers every plugin except this one (#255).
|
|
207
|
+
*/
|
|
208
|
+
const [stale, setStale] = useState(false)
|
|
209
|
+
|
|
210
|
+
// Only once the row is opened: the plugin configuration page renders every
|
|
211
|
+
// card at once, and an update check costs a registry round trip.
|
|
212
|
+
//
|
|
213
|
+
// `open` is the ONLY dependency, and re-entry is guarded by a ref rather
|
|
214
|
+
// than by `status`. Keying it on state the effect itself sets meant the
|
|
215
|
+
// first `setStatus` re-ran the effect, whose cleanup dropped the still
|
|
216
|
+
// in-flight update check — so the update row never appeared. Measured by
|
|
217
|
+
// the spec, not reasoned about.
|
|
218
|
+
const probed = useRef(false)
|
|
219
|
+
useEffect(() => {
|
|
220
|
+
if (!open || probed.current) return
|
|
221
|
+
probed.current = true
|
|
222
|
+
let live = true
|
|
223
|
+
void (async () => {
|
|
224
|
+
try {
|
|
225
|
+
const response = await fetch(api('/dsh-market/status'), { cache: 'no-store' })
|
|
226
|
+
const body = (await response.json()) as StatusBody
|
|
227
|
+
if (live) setStatus(readStatus(body))
|
|
228
|
+
applyGithubRouting(body)
|
|
229
|
+
} catch {
|
|
230
|
+
if (live) {
|
|
231
|
+
setStatus({
|
|
232
|
+
version: null, restart: false, channel: 'stable', channels: ['stable', 'beta'],
|
|
233
|
+
region: 'global', regions: REGIONS, regionAuto: false,
|
|
234
|
+
githubProxyCustom: null, githubProxyManaged: false, selfManaged: true,
|
|
235
|
+
})
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const response = await fetch(api('/dsh-market/updates'), { cache: 'no-store' })
|
|
240
|
+
const body = (await response.json()) as { updates?: Record<string, RawUpdate> }
|
|
241
|
+
const ownName = SELF_PACKAGE_NAMES.find(name => body.updates?.[name] !== undefined)
|
|
242
|
+
const own = ownName === undefined ? undefined : body.updates?.[ownName]
|
|
243
|
+
if (live && own !== undefined) {
|
|
244
|
+
setSelfPackageName(ownName ?? '@chatcode/cco-market')
|
|
245
|
+
setUpdate(readUpdate(own))
|
|
246
|
+
}
|
|
247
|
+
} catch { /* an update check that fails leaves the row without an offer */ }
|
|
248
|
+
})()
|
|
249
|
+
return () => { live = false }
|
|
250
|
+
}, [open])
|
|
251
|
+
|
|
252
|
+
const post = useCallback(async (path: string, payload: unknown): Promise<{ ok?: boolean; error?: string }> => {
|
|
253
|
+
const response = await fetch(path, {
|
|
254
|
+
method: 'POST',
|
|
255
|
+
headers: { 'content-type': 'application/json' },
|
|
256
|
+
body: JSON.stringify(payload),
|
|
257
|
+
})
|
|
258
|
+
return (await response.json()) as { ok?: boolean; error?: string }
|
|
259
|
+
}, [])
|
|
260
|
+
|
|
261
|
+
const onUpdate = useCallback((force = false) => {
|
|
262
|
+
setRestoreConfirming(false)
|
|
263
|
+
setPhase('working')
|
|
264
|
+
setError(null)
|
|
265
|
+
setStale(false)
|
|
266
|
+
void (async () => {
|
|
267
|
+
try {
|
|
268
|
+
const body = await post(api('/dsh-market/update'), {
|
|
269
|
+
name: selfPackageName,
|
|
270
|
+
...(update?.restoreRequired === true ? { restore: true } : {}),
|
|
271
|
+
...(force ? { force: true } : {}),
|
|
272
|
+
}) as {
|
|
273
|
+
ok?: boolean; error?: string; stale?: boolean
|
|
274
|
+
}
|
|
275
|
+
if (body.ok === true) setPhase('updated')
|
|
276
|
+
else {
|
|
277
|
+
setStale(body.stale === true)
|
|
278
|
+
setError(body.error ?? t('setSelfFailed'))
|
|
279
|
+
setPhase('failed')
|
|
280
|
+
}
|
|
281
|
+
} catch (cause) {
|
|
282
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
283
|
+
setPhase('failed')
|
|
284
|
+
}
|
|
285
|
+
})()
|
|
286
|
+
}, [post, selfPackageName, t, update?.restoreRequired])
|
|
287
|
+
|
|
288
|
+
const onRemove = useCallback(() => {
|
|
289
|
+
setPhase('working')
|
|
290
|
+
setError(null)
|
|
291
|
+
void (async () => {
|
|
292
|
+
try {
|
|
293
|
+
const body = await post(api('/dsh-market/self-uninstall'), { confirm: true, purge })
|
|
294
|
+
if (body.ok === true) {
|
|
295
|
+
if (purge) clearBrowserState(localStorage)
|
|
296
|
+
setPhase('removed')
|
|
297
|
+
onRemoved?.()
|
|
298
|
+
} else { setError(body.error ?? t('setSelfFailed')); setPhase('failed') }
|
|
299
|
+
} catch (cause) {
|
|
300
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
301
|
+
setPhase('failed')
|
|
302
|
+
}
|
|
303
|
+
})()
|
|
304
|
+
}, [onRemoved, post, purge, t])
|
|
305
|
+
|
|
306
|
+
const busy = phase === 'working'
|
|
307
|
+
const version = status?.version ?? null
|
|
308
|
+
// A prerelease says so in its own version string; nothing else has to be
|
|
309
|
+
// consulted to know the running build is one.
|
|
310
|
+
const prerelease = version !== null && version.includes('-')
|
|
311
|
+
|
|
312
|
+
/** Re-ask what this channel offers; the previous answer was for another one. */
|
|
313
|
+
const refreshUpdate = useCallback(async (): Promise<void> => {
|
|
314
|
+
const response = await fetch(api('/dsh-market/updates') + '?force=1', { cache: 'no-store' })
|
|
315
|
+
const body = (await response.json()) as { updates?: Record<string, RawUpdate> }
|
|
316
|
+
const ownName = SELF_PACKAGE_NAMES.find(name => body.updates?.[name] !== undefined)
|
|
317
|
+
const own = ownName === undefined ? undefined : body.updates?.[ownName]
|
|
318
|
+
if (ownName !== undefined) setSelfPackageName(ownName)
|
|
319
|
+
setUpdate(own === undefined ? null : readUpdate(own))
|
|
320
|
+
}, [])
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Select a channel — and show the one the SERVER accepted.
|
|
324
|
+
*
|
|
325
|
+
* This used to move the control first and ignore the answer, which was
|
|
326
|
+
* harmless while every channel was permitted. It stopped being harmless
|
|
327
|
+
* the moment one of them can be refused: a 403 would have left "dev"
|
|
328
|
+
* highlighted on a profile that is not on it.
|
|
329
|
+
*/
|
|
330
|
+
const onChannel = useCallback((next: Channel) => {
|
|
331
|
+
setError(null)
|
|
332
|
+
void (async () => {
|
|
333
|
+
try {
|
|
334
|
+
const body = await post(api('/dsh-market/channel'), { channel: next }) as { ok?: boolean; error?: string; channel?: string }
|
|
335
|
+
if (body.ok !== true) { setError(body.error ?? t('setSelfFailed')); return }
|
|
336
|
+
setStatus(current => (current === null ? current : { ...current, channel: asChannel(body.channel) ?? next }))
|
|
337
|
+
await refreshUpdate()
|
|
338
|
+
} catch (cause) {
|
|
339
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
340
|
+
}
|
|
341
|
+
})()
|
|
342
|
+
}, [post, refreshUpdate, t])
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Select a download region — and show the one the SERVER accepted, on the
|
|
346
|
+
* same reasoning as the channel above.
|
|
347
|
+
*
|
|
348
|
+
* A hand-made choice retires the one-time notice: the market no longer has
|
|
349
|
+
* anything to explain once the user has answered for themselves.
|
|
350
|
+
*/
|
|
351
|
+
const onRegion = useCallback((next: Region) => {
|
|
352
|
+
setError(null)
|
|
353
|
+
void (async () => {
|
|
354
|
+
try {
|
|
355
|
+
const body = await post(api('/dsh-market/region'), { region: next }) as { ok?: boolean; error?: string; region?: string }
|
|
356
|
+
if (body.ok !== true) { setError(body.error ?? t('setSelfFailed')); return }
|
|
357
|
+
const accepted = asRegion(body.region) ?? next
|
|
358
|
+
setStatus(current => (current === null ? current : { ...current, region: accepted, regionAuto: false }))
|
|
359
|
+
// Re-read the resolved proxy rather than deriving it here. The card
|
|
360
|
+
// knows which regions exist because the server told it; it must not
|
|
361
|
+
// start knowing what each one RESOLVES to, or the routing table would
|
|
362
|
+
// have a second copy that can disagree with the first.
|
|
363
|
+
try {
|
|
364
|
+
const fresh = await fetch(api('/dsh-market/status'), { cache: 'no-store' })
|
|
365
|
+
const status = (await fresh.json()) as StatusBody
|
|
366
|
+
applyGithubRouting(status)
|
|
367
|
+
} catch { /* images keep the previous route until the next load */ }
|
|
368
|
+
} catch (cause) {
|
|
369
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
370
|
+
}
|
|
371
|
+
})()
|
|
372
|
+
}, [post, t])
|
|
373
|
+
|
|
374
|
+
/** Save or clear the single user-facing GitHub escape route. */
|
|
375
|
+
const onGithubProxy = useCallback((proxy: string | null) => {
|
|
376
|
+
setError(null)
|
|
377
|
+
setProxySaving(true)
|
|
378
|
+
void (async () => {
|
|
379
|
+
try {
|
|
380
|
+
const body = await post(api('/dsh-market/github-proxy'), { proxy }) as { ok?: boolean; error?: string }
|
|
381
|
+
if (body.ok !== true) { setError(body.error ?? t('setSelfFailed')); return }
|
|
382
|
+
const fresh = await fetch(api('/dsh-market/status'), { cache: 'no-store' })
|
|
383
|
+
const statusBody = (await fresh.json()) as StatusBody
|
|
384
|
+
setStatus(readStatus(statusBody))
|
|
385
|
+
applyGithubRouting(statusBody)
|
|
386
|
+
setProxyEditing(false)
|
|
387
|
+
} catch (cause) {
|
|
388
|
+
setError(cause instanceof Error ? cause.message : String(cause))
|
|
389
|
+
} finally {
|
|
390
|
+
setProxySaving(false)
|
|
391
|
+
}
|
|
392
|
+
})()
|
|
393
|
+
}, [post, t])
|
|
394
|
+
|
|
395
|
+
/** One label + hint block with an optional action, the host's row shape. */
|
|
396
|
+
const row = (label: string, hint: string, action: ReactElement | null): ReactElement =>
|
|
397
|
+
h('div', { className: css.setRow },
|
|
398
|
+
h('div', { className: css.setLabelBox },
|
|
399
|
+
h('div', { className: css.setLabel }, label),
|
|
400
|
+
h('div', { className: css.setHint }, hint),
|
|
401
|
+
),
|
|
402
|
+
action,
|
|
403
|
+
)
|
|
404
|
+
|
|
405
|
+
// The end state REPLACES the controls rather than sitting beside them: the
|
|
406
|
+
// package is gone from disk, so an update button next to "removed" would
|
|
407
|
+
// offer something that cannot happen.
|
|
408
|
+
const body = phase === 'removed'
|
|
409
|
+
? row(t('setSelfRemoved'), t('setSelfRemovedHint'), null)
|
|
410
|
+
: h(Fragment, null,
|
|
411
|
+
status?.selfManaged === true ? row(
|
|
412
|
+
// An older offer is not an update, and calling it one would have
|
|
413
|
+
// the user click "更新" to go backwards. It IS what picking an
|
|
414
|
+
// earlier channel asked for, so it is offered — under its own name.
|
|
415
|
+
update?.updateAvailable === true && update.latest !== null
|
|
416
|
+
? `${t('setSelfUpdateReady')} ${update.latest}`
|
|
417
|
+
: update?.channelSwitch != null
|
|
418
|
+
? `${t('setChannelSwitch')} ${update.channelSwitch}`
|
|
419
|
+
: t('setSelfUpToDate'),
|
|
420
|
+
phase === 'updated'
|
|
421
|
+
? t('setSelfUpdatedHint')
|
|
422
|
+
: update?.channelSwitch != null
|
|
423
|
+
? t('setChannelSwitchHint')
|
|
424
|
+
// Explains what the Update button does — only worth saying
|
|
425
|
+
// when that button is actually on screen. Already up to date,
|
|
426
|
+
// it read as an instruction for an action that wasn't there.
|
|
427
|
+
: update?.updateAvailable === true ? t('setSelfUpdateHint') : t('setSelfUpToDateHint'),
|
|
428
|
+
phase === 'updated'
|
|
429
|
+
? null
|
|
430
|
+
: update?.updateAvailable === true
|
|
431
|
+
? h(Button, {
|
|
432
|
+
variant: 'primary',
|
|
433
|
+
size: 'sm',
|
|
434
|
+
disabled: busy,
|
|
435
|
+
onClick: () => {
|
|
436
|
+
if (update.restoreRequired) setRestoreConfirming(true)
|
|
437
|
+
else onUpdate()
|
|
438
|
+
},
|
|
439
|
+
}, update.restoreRequired ? t('restoreOnline') : t('setSelfUpdate'))
|
|
440
|
+
: update?.channelSwitch != null
|
|
441
|
+
? h(Button, { variant: 'outline', size: 'sm', disabled: busy, onClick: () => onUpdate() }, t('setChannelSwitch'))
|
|
442
|
+
: null,
|
|
443
|
+
) : null,
|
|
444
|
+
status?.selfManaged === true && restoreConfirming
|
|
445
|
+
? h('div', { className: css.setConfirm },
|
|
446
|
+
h('div', { className: css.setHint }, t('restoreHint')),
|
|
447
|
+
h('div', { className: css.setActions },
|
|
448
|
+
h(Button, {
|
|
449
|
+
variant: 'ghost', size: 'sm', onClick: () => { setRestoreConfirming(false) },
|
|
450
|
+
}, t('setSelfCancel')),
|
|
451
|
+
h(Button, {
|
|
452
|
+
variant: 'primary', size: 'sm', onClick: () => onUpdate(),
|
|
453
|
+
}, t('restoreContinue')),
|
|
454
|
+
),
|
|
455
|
+
)
|
|
456
|
+
: null,
|
|
457
|
+
status?.selfManaged === true ? row(t('setChannel'), t(CHANNEL_HINT[status.channel]),
|
|
458
|
+
h('div', { className: css.setSeg },
|
|
459
|
+
// Drawn from what the SERVER says is available, so the control
|
|
460
|
+
// can never offer a channel the route would refuse.
|
|
461
|
+
(status?.channels ?? ['stable', 'beta']).map(id => h('button', {
|
|
462
|
+
key: id,
|
|
463
|
+
type: 'button',
|
|
464
|
+
className: status?.channel === id ? `${css.setSegBtn} ${css.setSegOn}` : css.setSegBtn,
|
|
465
|
+
disabled: busy || status === null,
|
|
466
|
+
// The dev option says what it is on hover rather than being
|
|
467
|
+
// hidden behind a switch. A label a user can read beats a gate
|
|
468
|
+
// plus the machinery that maintains it, and the row's own hint
|
|
469
|
+
// repeats it in full once the channel is selected.
|
|
470
|
+
title: id === 'dev' ? t('setChannelDevHint') : undefined,
|
|
471
|
+
onClick: () => { onChannel(id) },
|
|
472
|
+
}, t(CHANNEL_LABEL[id]))),
|
|
473
|
+
)) : null,
|
|
474
|
+
// Above the channel row and below the update row: this is about
|
|
475
|
+
// getting plugins at all, which is the market's whole job, while the
|
|
476
|
+
// channel is about which build of the market itself arrives.
|
|
477
|
+
row(
|
|
478
|
+
t('setRegion'),
|
|
479
|
+
// The hint carries the explanation of an automatic choice when
|
|
480
|
+
// there is one to make, and the plain description of the selected
|
|
481
|
+
// region otherwise. One line either way — a notice that pushes the
|
|
482
|
+
// description off screen would trade a permanent answer for a
|
|
483
|
+
// temporary one.
|
|
484
|
+
status?.regionAuto === true
|
|
485
|
+
? `${t(REGION_HINT[status.region])} ${t('setRegionAuto')}`
|
|
486
|
+
: t(REGION_HINT[status?.region ?? 'global']),
|
|
487
|
+
h('div', { className: css.setSeg },
|
|
488
|
+
(status?.regions ?? REGIONS).map(id => h('button', {
|
|
489
|
+
key: id,
|
|
490
|
+
type: 'button',
|
|
491
|
+
className: status?.region === id ? `${css.setSegBtn} ${css.setSegOn}` : css.setSegBtn,
|
|
492
|
+
disabled: busy || status === null,
|
|
493
|
+
onClick: () => { onRegion(id) },
|
|
494
|
+
}, t(REGION_LABEL[id]))),
|
|
495
|
+
)),
|
|
496
|
+
row(
|
|
497
|
+
t('setGithubProxy'),
|
|
498
|
+
status?.githubProxyManaged === true
|
|
499
|
+
? t('setGithubProxyManagedHint')
|
|
500
|
+
: status?.githubProxyCustom !== null && status?.githubProxyCustom !== undefined
|
|
501
|
+
? t('setGithubProxyCustomHint')
|
|
502
|
+
: t('setGithubProxyAutoHint'),
|
|
503
|
+
status?.githubProxyManaged === true
|
|
504
|
+
? null
|
|
505
|
+
: status?.githubProxyCustom !== null && status?.githubProxyCustom !== undefined
|
|
506
|
+
? h('div', { className: css.setInlineActions },
|
|
507
|
+
h(Button, {
|
|
508
|
+
variant: 'outline', size: 'sm', disabled: proxySaving,
|
|
509
|
+
onClick: () => { setProxyDraft(status.githubProxyCustom ?? ''); setProxyEditing(true) },
|
|
510
|
+
}, t('setGithubProxyEdit')),
|
|
511
|
+
h(Button, {
|
|
512
|
+
variant: 'ghost', size: 'sm', disabled: proxySaving,
|
|
513
|
+
onClick: () => { onGithubProxy(null) },
|
|
514
|
+
}, t('setGithubProxyAuto')),
|
|
515
|
+
)
|
|
516
|
+
: h(Button, {
|
|
517
|
+
variant: 'outline', size: 'sm', disabled: status === null || proxySaving,
|
|
518
|
+
onClick: () => { setProxyDraft(''); setProxyEditing(true) },
|
|
519
|
+
}, t('setGithubProxyCustom')),
|
|
520
|
+
),
|
|
521
|
+
proxyEditing && status?.githubProxyManaged !== true
|
|
522
|
+
? h('div', { className: css.setProxyEditor },
|
|
523
|
+
h('label', { className: css.setProxyLabel },
|
|
524
|
+
t('setGithubProxyInput'),
|
|
525
|
+
h('input', {
|
|
526
|
+
className: css.setProxyInput,
|
|
527
|
+
type: 'url',
|
|
528
|
+
value: proxyDraft,
|
|
529
|
+
placeholder: 'https://mirror.example',
|
|
530
|
+
'aria-label': t('setGithubProxyInput'),
|
|
531
|
+
disabled: proxySaving,
|
|
532
|
+
onChange: (event: { currentTarget: { value: string } }) => { setProxyDraft(event.currentTarget.value) },
|
|
533
|
+
}),
|
|
534
|
+
),
|
|
535
|
+
h('div', { className: css.setInlineActions },
|
|
536
|
+
h(Button, {
|
|
537
|
+
variant: 'ghost', size: 'sm', disabled: proxySaving,
|
|
538
|
+
onClick: () => { setProxyEditing(false) },
|
|
539
|
+
}, t('setSelfCancel')),
|
|
540
|
+
h(Button, {
|
|
541
|
+
variant: 'primary', size: 'sm', disabled: proxySaving || proxyDraft.trim() === '',
|
|
542
|
+
onClick: () => { onGithubProxy(proxyDraft) },
|
|
543
|
+
}, t('setGithubProxySave')),
|
|
544
|
+
),
|
|
545
|
+
)
|
|
546
|
+
: null,
|
|
547
|
+
status?.selfManaged === true ? row(t('setSelfRemove'), t('setSelfRemoveHint'),
|
|
548
|
+
phase === 'confirming' || busy
|
|
549
|
+
? null
|
|
550
|
+
: h(Button, {
|
|
551
|
+
variant: 'outline',
|
|
552
|
+
size: 'sm',
|
|
553
|
+
className: css.setDanger,
|
|
554
|
+
disabled: busy,
|
|
555
|
+
onClick: () => { setPhase('confirming') },
|
|
556
|
+
}, t('setSelfRemove'))) : null,
|
|
557
|
+
status?.selfManaged === true && (phase === 'confirming' || busy)
|
|
558
|
+
? h('div', { className: css.setConfirm },
|
|
559
|
+
h('div', { className: css.setHint }, t('setSelfConfirm')),
|
|
560
|
+
h('label', { className: css.setCheck },
|
|
561
|
+
h('input', { type: 'checkbox', checked: purge, onChange: () => { setPurge(!purge) } }),
|
|
562
|
+
h('span', null, t('setSelfPurge')),
|
|
563
|
+
),
|
|
564
|
+
// States the consequence of the CURRENT choice, both ways. The
|
|
565
|
+
// default keeps things, and what "keeping" costs — plugins the
|
|
566
|
+
// market switched off stay off, with the UI that could switch
|
|
567
|
+
// them back on about to disappear — is the part a user cannot
|
|
568
|
+
// work out on their own.
|
|
569
|
+
h('div', { className: css.setHint }, purge ? t('setSelfPurgeOn') : t('setSelfPurgeOff')),
|
|
570
|
+
h('div', { className: css.setActions },
|
|
571
|
+
busy
|
|
572
|
+
? null
|
|
573
|
+
: h(Button, {
|
|
574
|
+
variant: 'ghost',
|
|
575
|
+
size: 'sm',
|
|
576
|
+
onClick: () => { setPhase('idle'); setPurge(false) },
|
|
577
|
+
}, t('setSelfCancel')),
|
|
578
|
+
h(Button, {
|
|
579
|
+
variant: 'primary',
|
|
580
|
+
size: 'sm',
|
|
581
|
+
className: css.setDanger,
|
|
582
|
+
disabled: busy,
|
|
583
|
+
icon: busy ? h('span', { className: css.spin }, h(IconLoadingOutline16, { size: 16 })) : undefined,
|
|
584
|
+
onClick: onRemove,
|
|
585
|
+
}, busy ? t('setSelfWorking') : t('setSelfRemoveConfirm')),
|
|
586
|
+
),
|
|
587
|
+
)
|
|
588
|
+
: null,
|
|
589
|
+
error !== null ? h('div', { className: css.err }, error) : null,
|
|
590
|
+
// The one place the market can be forced past pnpm's fresh-release
|
|
591
|
+
// wait. Shown only after that specific refusal, never as a default:
|
|
592
|
+
// the wait exists because a version published minutes ago can still
|
|
593
|
+
// be unpublished (#39).
|
|
594
|
+
stale
|
|
595
|
+
? h('div', { className: css.setActions },
|
|
596
|
+
h(Button, { variant: 'primary', size: 'sm', onClick: () => onUpdate(true) }, t('updateNow')))
|
|
597
|
+
: null,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
return h('div', { className: open ? `${css.setCard} ${css.setCardOpen}` : css.setCard },
|
|
601
|
+
h('button', {
|
|
602
|
+
type: 'button',
|
|
603
|
+
className: css.setHeader,
|
|
604
|
+
'aria-expanded': open,
|
|
605
|
+
onClick: () => { setOpen(!open) },
|
|
606
|
+
},
|
|
607
|
+
h('div', { className: css.setHeadText },
|
|
608
|
+
h('div', { className: css.setName },
|
|
609
|
+
t('nav'),
|
|
610
|
+
version !== null ? h('span', { className: css.version }, ` v${version}`) : null,
|
|
611
|
+
prerelease ? h('span', { className: css.setBetaTag }, t('setChannelBeta')) : null,
|
|
612
|
+
),
|
|
613
|
+
h('div', { className: css.setDesc }, t('setCardDesc')),
|
|
614
|
+
),
|
|
615
|
+
h('span', { className: open ? `${css.setChevron} ${css.setChevronOpen}` : css.setChevron },
|
|
616
|
+
h(IconChevronDownOutline14, { size: 14 }),
|
|
617
|
+
),
|
|
618
|
+
),
|
|
619
|
+
open ? h('div', { className: css.setBody }, body) : null,
|
|
620
|
+
)
|
|
621
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.root{height:100%;overflow:auto;padding:4px;color:var(--dsw-alias-label-primary,#1f2328)}
|
|
2
|
+
.head{display:flex;align-items:flex-start;justify-content:space-between;gap:16px;margin-bottom:8px}
|
|
3
|
+
.head>span{min-width:240px;max-width:420px;flex:1}
|
|
4
|
+
.title{font-size:16px;line-height:24px;font-weight:600;margin:0}
|
|
5
|
+
.subtitle,.workspace,.warning,.meta,.state{font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary,#8b93a1);margin:2px 0}
|
|
6
|
+
.workspace{overflow-wrap:anywhere}
|
|
7
|
+
.warning{padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:8px;margin:10px 0}
|
|
8
|
+
.error,.notice{display:flex;align-items:center;justify-content:space-between;gap:8px;font-size:12px;padding:8px 10px;border-radius:8px;margin:10px 0}
|
|
9
|
+
.error{color:var(--dsw-alias-state-danger-primary,#dc2626);background:var(--dsw-alias-state-danger-bg,#fef2f2)}
|
|
10
|
+
.notice{color:var(--dsw-alias-state-success-primary,#16a34a);background:var(--dsw-alias-state-success-bg,#f0fdf4);overflow-wrap:anywhere}
|
|
11
|
+
.grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px;margin-top:12px}
|
|
12
|
+
.card{display:flex;flex-direction:column;min-height:150px;padding:12px;border:1px solid var(--dsw-alias-border-l2,#e5e7eb);border-radius:10px;background:var(--dsw-alias-bg-layer-1,#fff)}
|
|
13
|
+
.cardHead{display:flex;align-items:center;justify-content:space-between;gap:8px}
|
|
14
|
+
.card h3{font-size:14px;line-height:20px;margin:0;overflow-wrap:anywhere}
|
|
15
|
+
.description{font-size:12px;line-height:18px;margin:8px 0;flex:1;color:var(--dsw-alias-label-secondary,#6b7280);display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
|
|
16
|
+
.meta{min-height:18px}
|
|
17
|
+
.actions{display:flex;gap:8px;margin-top:8px;flex-wrap:wrap}
|
|
18
|
+
.projectInstall.projectInstall{background:var(--dsw-alias-button-primary-fill,#1f2328);color:var(--dsw-alias-label-primary-foreground,#fff)}
|
|
19
|
+
.projectInstall.projectInstall:hover:not(:disabled){background-color:var(--dsw-alias-button-primary-fill,#1f2328)!important;color:var(--dsw-alias-label-primary-foreground,#fff)!important}
|
|
20
|
+
.pager{display:flex;align-items:center;justify-content:center;gap:12px;margin:16px 0;font-size:12px;color:var(--dsw-alias-label-secondary,#6b7280)}
|
|
21
|
+
@media(max-width:640px){.head{flex-direction:column}.head>span{width:100%;max-width:none}.grid{grid-template-columns:1fr}}
|