dsh-plugin-shop 0.8.1 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -5
- package/lib/client.js +455 -197
- package/lib/index.js +712 -164
- package/lib/typert.host.js +38 -25
- package/lib/typert.remote-client.js +23 -18
- package/lib/types/client/ShopTab.d.ts +4 -0
- package/lib/types/client/locales.d.ts +20 -2
- package/lib/types/client/present.d.ts +60 -9
- package/lib/types/client/useFlows.d.ts +66 -0
- package/lib/types/client/useInstall.d.ts +17 -27
- package/lib/types/client/useUninstall.d.ts +24 -17
- package/lib/types/client/useUpdateSelf.d.ts +1 -1
- package/lib/types/host/activation.d.ts +55 -0
- package/lib/types/host/client-half.d.ts +28 -0
- package/lib/types/host/dsh-cli.d.ts +11 -0
- package/lib/types/host/executor.d.ts +18 -5
- package/lib/types/host/hot.d.ts +16 -5
- package/lib/types/host/index.d.ts +111 -16
- package/lib/types/host/peers.d.ts +5 -3
- package/lib/types/host/prefetch.d.ts +67 -0
- package/lib/types/shared/install-state.d.ts +16 -0
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -4355,7 +4355,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4355
4355
|
});
|
|
4356
4356
|
const dsh_plugin_shop_shop_installStart_result$schema = union([object({
|
|
4357
4357
|
"ok": literal(true),
|
|
4358
|
-
"installId": string()
|
|
4358
|
+
"installId": string(),
|
|
4359
|
+
"state": union([
|
|
4360
|
+
literal("downloading"),
|
|
4361
|
+
literal("running"),
|
|
4362
|
+
literal("done"),
|
|
4363
|
+
literal("failed")
|
|
4364
|
+
])
|
|
4359
4365
|
}), object({
|
|
4360
4366
|
"ok": literal(false),
|
|
4361
4367
|
"code": union([
|
|
@@ -4373,15 +4379,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4373
4379
|
const dsh_plugin_shop_shop_installStatus_result$schema = object({
|
|
4374
4380
|
"found": boolean(),
|
|
4375
4381
|
"state": union([
|
|
4382
|
+
literal("downloading"),
|
|
4376
4383
|
literal("running"),
|
|
4377
4384
|
literal("done"),
|
|
4378
4385
|
literal("failed")
|
|
4379
4386
|
]),
|
|
4380
4387
|
"log": array(string()),
|
|
4381
|
-
"
|
|
4388
|
+
"activation": union([
|
|
4382
4389
|
_undefined(),
|
|
4383
|
-
literal(
|
|
4384
|
-
literal(
|
|
4390
|
+
literal("live"),
|
|
4391
|
+
literal("reload"),
|
|
4392
|
+
literal("restart")
|
|
4385
4393
|
]).optional(),
|
|
4386
4394
|
"restartReason": union([
|
|
4387
4395
|
_undefined(),
|
|
@@ -4389,7 +4397,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4389
4397
|
literal("not-simple"),
|
|
4390
4398
|
literal("host-unsupported"),
|
|
4391
4399
|
literal("timeout"),
|
|
4392
|
-
literal("mount-failed")
|
|
4400
|
+
literal("mount-failed"),
|
|
4401
|
+
literal("client-half")
|
|
4393
4402
|
]).optional(),
|
|
4394
4403
|
"detail": union([_undefined(), string()]).optional()
|
|
4395
4404
|
});
|
|
@@ -4401,10 +4410,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4401
4410
|
"name": string(),
|
|
4402
4411
|
"enabled": boolean()
|
|
4403
4412
|
});
|
|
4404
|
-
const dsh_plugin_shop_shop_setEnabled_result$schema = object({
|
|
4405
|
-
"ok":
|
|
4406
|
-
"
|
|
4407
|
-
|
|
4413
|
+
const dsh_plugin_shop_shop_setEnabled_result$schema = union([object({
|
|
4414
|
+
"ok": literal(true),
|
|
4415
|
+
"activation": union([
|
|
4416
|
+
literal("live"),
|
|
4417
|
+
literal("reload"),
|
|
4418
|
+
literal("restart")
|
|
4419
|
+
])
|
|
4420
|
+
}), object({
|
|
4421
|
+
"ok": literal(false),
|
|
4422
|
+
"detail": string()
|
|
4423
|
+
})]);
|
|
4408
4424
|
const dsh_plugin_shop_shop_uninstallStart_parameter_0$schema = object({ "name": string() });
|
|
4409
4425
|
const dsh_plugin_shop_shop_uninstallStart_result$schema = union([object({
|
|
4410
4426
|
"ok": literal(true),
|
|
@@ -4416,7 +4432,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4416
4432
|
const dsh_plugin_shop_shop_updateStart_parameter_0$schema = object({ "version": string() });
|
|
4417
4433
|
const dsh_plugin_shop_shop_updateStart_result$schema = union([object({
|
|
4418
4434
|
"ok": literal(true),
|
|
4419
|
-
"installId": string()
|
|
4435
|
+
"installId": string(),
|
|
4436
|
+
"state": union([
|
|
4437
|
+
literal("downloading"),
|
|
4438
|
+
literal("running"),
|
|
4439
|
+
literal("done"),
|
|
4440
|
+
literal("failed")
|
|
4441
|
+
])
|
|
4420
4442
|
}), object({
|
|
4421
4443
|
"ok": literal(false),
|
|
4422
4444
|
"detail": string()
|
|
@@ -4425,7 +4447,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4425
4447
|
"installed": string(),
|
|
4426
4448
|
"latest": union([literal(null), string()]),
|
|
4427
4449
|
"outdated": boolean(),
|
|
4428
|
-
"
|
|
4450
|
+
"restartBlocked": union([
|
|
4451
|
+
literal(null),
|
|
4452
|
+
literal("windows"),
|
|
4453
|
+
literal("systemd"),
|
|
4454
|
+
literal("port-zero")
|
|
4455
|
+
])
|
|
4429
4456
|
});
|
|
4430
4457
|
const TYPERT_REMOTE = {
|
|
4431
4458
|
package: "dsh-plugin-shop",
|
|
@@ -4454,7 +4481,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4454
4481
|
},
|
|
4455
4482
|
sourceLocation: {
|
|
4456
4483
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4457
|
-
"line":
|
|
4484
|
+
"line": 867,
|
|
4458
4485
|
"column": 9
|
|
4459
4486
|
}
|
|
4460
4487
|
},
|
|
@@ -4472,7 +4499,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4472
4499
|
},
|
|
4473
4500
|
sourceLocation: {
|
|
4474
4501
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4475
|
-
"line":
|
|
4502
|
+
"line": 1140,
|
|
4476
4503
|
"column": 9
|
|
4477
4504
|
}
|
|
4478
4505
|
},
|
|
@@ -4490,7 +4517,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4490
4517
|
},
|
|
4491
4518
|
sourceLocation: {
|
|
4492
4519
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4493
|
-
"line":
|
|
4520
|
+
"line": 1131,
|
|
4494
4521
|
"column": 9
|
|
4495
4522
|
}
|
|
4496
4523
|
},
|
|
@@ -4518,7 +4545,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4518
4545
|
},
|
|
4519
4546
|
sourceLocation: {
|
|
4520
4547
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4521
|
-
"line":
|
|
4548
|
+
"line": 915,
|
|
4522
4549
|
"column": 9
|
|
4523
4550
|
}
|
|
4524
4551
|
},
|
|
@@ -4545,7 +4572,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4545
4572
|
},
|
|
4546
4573
|
sourceLocation: {
|
|
4547
4574
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4548
|
-
"line":
|
|
4575
|
+
"line": 1107,
|
|
4549
4576
|
"column": 3
|
|
4550
4577
|
}
|
|
4551
4578
|
},
|
|
@@ -4563,7 +4590,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4563
4590
|
},
|
|
4564
4591
|
sourceLocation: {
|
|
4565
4592
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4566
|
-
"line":
|
|
4593
|
+
"line": 1311,
|
|
4567
4594
|
"column": 9
|
|
4568
4595
|
}
|
|
4569
4596
|
},
|
|
@@ -4590,7 +4617,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4590
4617
|
},
|
|
4591
4618
|
sourceLocation: {
|
|
4592
4619
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4593
|
-
"line":
|
|
4620
|
+
"line": 692,
|
|
4594
4621
|
"column": 9
|
|
4595
4622
|
}
|
|
4596
4623
|
},
|
|
@@ -4618,7 +4645,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4618
4645
|
},
|
|
4619
4646
|
sourceLocation: {
|
|
4620
4647
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4621
|
-
"line":
|
|
4648
|
+
"line": 1233,
|
|
4622
4649
|
"column": 9
|
|
4623
4650
|
}
|
|
4624
4651
|
},
|
|
@@ -4645,7 +4672,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4645
4672
|
},
|
|
4646
4673
|
sourceLocation: {
|
|
4647
4674
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4648
|
-
"line":
|
|
4675
|
+
"line": 1379,
|
|
4649
4676
|
"column": 9
|
|
4650
4677
|
}
|
|
4651
4678
|
},
|
|
@@ -4663,7 +4690,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4663
4690
|
},
|
|
4664
4691
|
sourceLocation: {
|
|
4665
4692
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4666
|
-
"line":
|
|
4693
|
+
"line": 1363,
|
|
4667
4694
|
"column": 9
|
|
4668
4695
|
}
|
|
4669
4696
|
}
|
|
@@ -6237,20 +6264,71 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6237
6264
|
return spec.length > HOLDER_SPEC_CAP ? `${spec.slice(0, 79)}…` : spec;
|
|
6238
6265
|
}
|
|
6239
6266
|
//#endregion
|
|
6267
|
+
//#region src/shared/install-state.ts
|
|
6268
|
+
/** Whether the host is done with this record: it will not change again, and a
|
|
6269
|
+
* poller may stop. */
|
|
6270
|
+
function isTerminalInstallState(state) {
|
|
6271
|
+
return state === "done" || state === "failed";
|
|
6272
|
+
}
|
|
6273
|
+
//#endregion
|
|
6240
6274
|
//#region src/client/present.ts
|
|
6241
|
-
/**
|
|
6242
|
-
*
|
|
6243
|
-
*
|
|
6244
|
-
|
|
6245
|
-
|
|
6275
|
+
/** The done notice for one activation, as a locale key.
|
|
6276
|
+
*
|
|
6277
|
+
* A hot-mount reason code names WHY a restart is needed and is meaningful
|
|
6278
|
+
* only under `restart`; `live` and `reload` ignore any reason riding along,
|
|
6279
|
+
* because rendering one there would tell a reader their reload had failed.
|
|
6280
|
+
* An absent or unrecognized reason keeps the generic restart line rather
|
|
6281
|
+
* than showing a bare code. */
|
|
6282
|
+
function activationNoticeKey(activation, restartReason) {
|
|
6283
|
+
if (activation === "live") return "installedNoRestartNotice";
|
|
6284
|
+
if (activation === "reload") return "installedReloadNotice";
|
|
6285
|
+
switch (restartReason) {
|
|
6246
6286
|
case "no-patch": return "hotNoPatchNotice";
|
|
6247
6287
|
case "not-simple": return "hotNotSimpleNotice";
|
|
6248
6288
|
case "host-unsupported": return "hotHostUnsupportedNotice";
|
|
6249
6289
|
case "timeout": return "hotTimeoutNotice";
|
|
6250
6290
|
case "mount-failed": return "hotMountFailedNotice";
|
|
6291
|
+
case "client-half": return "hotClientHalfNotice";
|
|
6251
6292
|
default: return "installedRestartNotice";
|
|
6252
6293
|
}
|
|
6253
6294
|
}
|
|
6295
|
+
/** The same mapping for an UNINSTALL's done notice.
|
|
6296
|
+
*
|
|
6297
|
+
* A sibling rather than a `prefix` parameter on `activationNoticeKey`: the
|
|
6298
|
+
* two key sets are not parallel — `installedNoRestartNotice` against
|
|
6299
|
+
* `uninstalledLiveNotice` — so no prefix derives one from the other. It takes
|
|
6300
|
+
* no reason code, because a hot-mount reason explains why a MOUNT could not
|
|
6301
|
+
* happen and an uninstall mounts nothing.
|
|
6302
|
+
*
|
|
6303
|
+
* Here rather than in JSX because this is a pure Activation -> copy decision,
|
|
6304
|
+
* and in a component no fixture can reach it: the install side was covered
|
|
6305
|
+
* and this one was not, for the whole time both existed. */
|
|
6306
|
+
function uninstallActivationNoticeKey(activation) {
|
|
6307
|
+
if (activation === "restart") return "uninstalledRestartNotice";
|
|
6308
|
+
return activation === "reload" ? "uninstalledReloadNotice" : "uninstalledLiveNotice";
|
|
6309
|
+
}
|
|
6310
|
+
/** The copy key for one install phase. A function rather than an inline
|
|
6311
|
+
* ternary in the JSX so it is unit-testable: the tab's only test seam is a
|
|
6312
|
+
* whole-tab render driven by injected RPCs, which cannot be handed one view. */
|
|
6313
|
+
function installPhaseKey(phase) {
|
|
6314
|
+
return phase === "downloading" ? "downloading" : "installing";
|
|
6315
|
+
}
|
|
6316
|
+
/** Why the shop cannot restart dsh, as a locale key.
|
|
6317
|
+
*
|
|
6318
|
+
* A sibling of `activationNoticeKey` and exhaustive for the same reason: the
|
|
6319
|
+
* switch has no default, so a fourth reason added to `RestartBlockedReason` is
|
|
6320
|
+
* a type error here rather than a card that silently keeps the third one's
|
|
6321
|
+
* copy. That is the whole point of the type — the single boolean this replaced
|
|
6322
|
+
* had ONE string for three causes, and it named systemd, so a Windows reader
|
|
6323
|
+
* was told to restart a service they do not run and to set an override that
|
|
6324
|
+
* could not have helped. */
|
|
6325
|
+
function restartBlockedNoticeKey(reason) {
|
|
6326
|
+
switch (reason) {
|
|
6327
|
+
case "windows": return "restartBlockedWindowsNotice";
|
|
6328
|
+
case "systemd": return "restartBlockedSystemdNotice";
|
|
6329
|
+
case "port-zero": return "restartBlockedPortZeroNotice";
|
|
6330
|
+
}
|
|
6331
|
+
}
|
|
6254
6332
|
/** Tier → locale key, for the entry-card tier badge (§6.2). */
|
|
6255
6333
|
function tierKey(tier) {
|
|
6256
6334
|
switch (tier) {
|
|
@@ -6327,6 +6405,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6327
6405
|
function isCustomLicense(license) {
|
|
6328
6406
|
return license !== null && /^SEE LICENSE IN /i.test(license);
|
|
6329
6407
|
}
|
|
6408
|
+
/** The phase a non-terminal state renders as: the WIRE state the host
|
|
6409
|
+
* reported (`InstallState`) mapped onto the phase the view carries. Exported
|
|
6410
|
+
* because every hook that turns a start or a poll result into a view spells
|
|
6411
|
+
* this same mapping, and three hand-copied ternaries are three chances for
|
|
6412
|
+
* one of them to drift.
|
|
6413
|
+
*
|
|
6414
|
+
* Not to be confused with `installPhaseKey`, which is the OTHER direction and
|
|
6415
|
+
* a different pair of values: view phase → the locale key to render. They
|
|
6416
|
+
* happen to share two spellings today and that is a coincidence of naming,
|
|
6417
|
+
* not a reason to fuse them — the wire union and the locale key set are free
|
|
6418
|
+
* to diverge. */
|
|
6419
|
+
function phaseOf(state) {
|
|
6420
|
+
return state === "downloading" ? "downloading" : "installing";
|
|
6421
|
+
}
|
|
6330
6422
|
/** §7.2 once-per-second poll cadence, as a named constant. */
|
|
6331
6423
|
const INSTALL_POLL_MS = 1e3;
|
|
6332
6424
|
/** How long the check-update button reports "up to date" after a re-check
|
|
@@ -6345,7 +6437,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6345
6437
|
case "started": return {
|
|
6346
6438
|
kind: "running",
|
|
6347
6439
|
installId: event.installId,
|
|
6348
|
-
log: []
|
|
6440
|
+
log: [],
|
|
6441
|
+
phase: phaseOf(event.state)
|
|
6349
6442
|
};
|
|
6350
6443
|
case "rejected": return {
|
|
6351
6444
|
kind: "rejected",
|
|
@@ -6360,14 +6453,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6360
6453
|
detail: status.detail ?? "install record lost",
|
|
6361
6454
|
log: status.log
|
|
6362
6455
|
};
|
|
6363
|
-
if (status.state
|
|
6456
|
+
if (!isTerminalInstallState(status.state)) return {
|
|
6364
6457
|
kind: "running",
|
|
6365
6458
|
installId: state.installId,
|
|
6366
|
-
log: status.log
|
|
6459
|
+
log: status.log,
|
|
6460
|
+
phase: phaseOf(status.state)
|
|
6367
6461
|
};
|
|
6368
6462
|
if (status.state === "done") return {
|
|
6369
6463
|
kind: "done",
|
|
6370
|
-
|
|
6464
|
+
activation: status.activation ?? "restart",
|
|
6371
6465
|
log: status.log,
|
|
6372
6466
|
...status.restartReason !== void 0 ? { restartReason: status.restartReason } : {}
|
|
6373
6467
|
};
|
|
@@ -6596,9 +6690,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6596
6690
|
install: "安装",
|
|
6597
6691
|
confirm: "确认",
|
|
6598
6692
|
cancel: "取消",
|
|
6693
|
+
dismiss: "知道了",
|
|
6599
6694
|
acknowledgementTitle: "需要确认",
|
|
6600
6695
|
acknowledgementBody: ACKNOWLEDGEMENT_ZH,
|
|
6601
6696
|
installing: "正在安装…",
|
|
6697
|
+
downloading: "正在下载…",
|
|
6602
6698
|
installedRestartNotice: "已安装;重启 dsh 后生效",
|
|
6603
6699
|
incompatibleBadge: "不兼容",
|
|
6604
6700
|
incompatibleDetail: "缺少组件/插件:{modules}\n此插件可能是为其他版本的 DSH 设计的。",
|
|
@@ -6607,7 +6703,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6607
6703
|
hotHostUnsupportedNotice: "当前环境不支持热挂载;重启 dsh 后生效",
|
|
6608
6704
|
hotTimeoutNotice: "热挂载超时;重启 dsh 后生效",
|
|
6609
6705
|
hotMountFailedNotice: "热挂载失败;重启 dsh 后生效",
|
|
6706
|
+
hotClientHalfNotice: "已安装并在运行;它的界面要重启 dsh 才会更新——刷新页面取不到",
|
|
6610
6707
|
installedNoRestartNotice: "已安装并热挂载;现在即可使用,无需重启",
|
|
6708
|
+
installedReloadNotice: "已生效;刷新页面即可看到",
|
|
6709
|
+
reload: "刷新页面",
|
|
6710
|
+
reloadNote: "服务器已经是新状态,当前页面显示的还是刷新前的内容。刷新会丢弃这个页面上未完成的内容。",
|
|
6611
6711
|
installFailed: "安装失败",
|
|
6612
6712
|
installTransportFailed: "安装请求未能送达。请稍后重试。",
|
|
6613
6713
|
refresh: "刷新",
|
|
@@ -6654,6 +6754,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6654
6754
|
uninstallTransportFailed: "卸载请求未能送达。请稍后重试。",
|
|
6655
6755
|
uninstalledLiveNotice: "已卸载并立即停止。profile 的启动组合将在下次重启时应用该移除。",
|
|
6656
6756
|
uninstalledRestartNotice: "已卸载;重启 dsh 后生效",
|
|
6757
|
+
uninstalledReloadNotice: "已卸载;刷新页面即可看到它消失。profile 的启动组合将在下次重启时应用该移除。",
|
|
6657
6758
|
restart: "重启 dsh",
|
|
6658
6759
|
restartTitle: "确认重启 dsh",
|
|
6659
6760
|
restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
|
|
@@ -6661,7 +6762,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6661
6762
|
restarting: "正在重启,服务器就绪后页面会自动刷新…",
|
|
6662
6763
|
restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
|
|
6663
6764
|
restartTransportFailed: "重启请求未能送达。请稍后重试。",
|
|
6664
|
-
|
|
6765
|
+
restartBlockedWindowsNotice: "商店暂不支持在 Windows 上重启 dsh。请退出 dsh 后重新启动,以应用更改。",
|
|
6766
|
+
restartBlockedSystemdNotice: "当前 dsh 由 systemd 服务托管,商店无法安全地重启它。请手动重启服务,或在商店行配置中设置 allowRestart: true。",
|
|
6767
|
+
restartBlockedPortZeroNotice: "此 dsh 以 --port 0 启动,重启后会绑定到另一个端口,本页面无法跟上。请手动重启 dsh,以应用更改。",
|
|
6665
6768
|
updateFailed: "更新失败",
|
|
6666
6769
|
updateTransportFailed: "更新请求未能送达。请稍后重试。",
|
|
6667
6770
|
checkUpdate: "检查更新",
|
|
@@ -6670,7 +6773,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6670
6773
|
github: "GitHub",
|
|
6671
6774
|
enabledSwitch: "启用",
|
|
6672
6775
|
toggleFailed: "设置失败,请重试。",
|
|
6673
|
-
hotApplyNote: "
|
|
6776
|
+
hotApplyNote: "已生效,无需重启"
|
|
6674
6777
|
};
|
|
6675
6778
|
/** English dictionary checked against the Chinese key set. */
|
|
6676
6779
|
const en = {
|
|
@@ -6699,9 +6802,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6699
6802
|
install: "Install",
|
|
6700
6803
|
confirm: "Confirm",
|
|
6701
6804
|
cancel: "Cancel",
|
|
6805
|
+
dismiss: "Dismiss",
|
|
6702
6806
|
acknowledgementTitle: "Acknowledgement required",
|
|
6703
6807
|
acknowledgementBody: ACKNOWLEDGEMENT_EN,
|
|
6704
6808
|
installing: "Installing…",
|
|
6809
|
+
downloading: "Downloading…",
|
|
6705
6810
|
installedRestartNotice: "installed; restart dsh to activate",
|
|
6706
6811
|
incompatibleBadge: "Incompatible",
|
|
6707
6812
|
incompatibleDetail: "Missing components/plugins: {modules}\nThis plugin may be designed for another DSH version.",
|
|
@@ -6710,7 +6815,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6710
6815
|
hotHostUnsupportedNotice: "this harness cannot hot-mount; restart dsh to activate",
|
|
6711
6816
|
hotTimeoutNotice: "hot-mount timed out; restart dsh to activate",
|
|
6712
6817
|
hotMountFailedNotice: "hot-mount failed; restart dsh to activate",
|
|
6818
|
+
hotClientHalfNotice: "installed and running; its interface needs a restart of dsh — a reload cannot fetch it",
|
|
6713
6819
|
installedNoRestartNotice: "installed and hot-mounted; running now, no restart needed",
|
|
6820
|
+
installedReloadNotice: "already applied; reload the page to see it",
|
|
6821
|
+
reload: "Reload the page",
|
|
6822
|
+
reloadNote: "The server is already in the new state; this page is still showing what came before. Reloading discards anything in flight on this page.",
|
|
6714
6823
|
installFailed: "Install failed",
|
|
6715
6824
|
installTransportFailed: "The install request could not be delivered. Please retry.",
|
|
6716
6825
|
refresh: "Refresh",
|
|
@@ -6752,6 +6861,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6752
6861
|
uninstallTransportFailed: "The uninstall request could not be delivered. Please retry.",
|
|
6753
6862
|
uninstalledLiveNotice: "Removed and stopped immediately. The profile's boot composition picks up the removal at the next restart.",
|
|
6754
6863
|
uninstalledRestartNotice: "uninstalled; restart dsh to activate",
|
|
6864
|
+
uninstalledReloadNotice: "removed; reload the page to see it gone. The profile's boot composition picks up the removal at the next restart.",
|
|
6755
6865
|
restart: "Restart dsh",
|
|
6756
6866
|
restartTitle: "Restart dsh?",
|
|
6757
6867
|
restartBody: "Restarting disconnects this page and interrupts any conversation or task in progress; the browser will jump to the restarted address automatically, so give it a few seconds.",
|
|
@@ -6759,7 +6869,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6759
6869
|
restarting: "Restarting; the page will refresh once the server is back…",
|
|
6760
6870
|
restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
|
|
6761
6871
|
restartTransportFailed: "The restart request could not be delivered. Please retry.",
|
|
6762
|
-
|
|
6872
|
+
restartBlockedWindowsNotice: "The shop cannot restart dsh on Windows yet. Quit dsh and start it again to apply the change.",
|
|
6873
|
+
restartBlockedSystemdNotice: "This dsh process runs as a systemd service, so the shop cannot restart it safely. Restart the service manually, or set allowRestart: true in the shop row config.",
|
|
6874
|
+
restartBlockedPortZeroNotice: "This dsh was launched with --port 0, so a restart would bind a different port and this page could not follow it. Restart dsh manually to apply the change.",
|
|
6763
6875
|
updateFailed: "Update failed",
|
|
6764
6876
|
updateTransportFailed: "The update request could not be delivered. Please retry.",
|
|
6765
6877
|
checkUpdate: "Check",
|
|
@@ -6768,40 +6880,45 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6768
6880
|
github: "GitHub",
|
|
6769
6881
|
enabledSwitch: "Enable",
|
|
6770
6882
|
toggleFailed: "Failed to update. Please retry.",
|
|
6771
|
-
hotApplyNote: "
|
|
6883
|
+
hotApplyNote: "applied without a restart"
|
|
6772
6884
|
};
|
|
6773
6885
|
//#endregion
|
|
6774
|
-
//#region src/client/
|
|
6775
|
-
/** Install driving hook for one entry: start, poll to terminal, reset. */
|
|
6776
|
-
/** The poll loop shared by the install and uninstall drivers: while the view
|
|
6777
|
-
* is `running`, poll once per second and fold each status through the
|
|
6778
|
-
* reducer. A poll failure is transient — the host retains the record, so the
|
|
6779
|
-
* next tick finds it. The rejection handler must be present: an unhandled
|
|
6780
|
-
* rejection here would escape the poll loop. */
|
|
6781
|
-
function usePollStatus(view, setView, installStatus) {
|
|
6782
|
-
(0, react.useEffect)(() => {
|
|
6783
|
-
if (view.kind !== "running") return;
|
|
6784
|
-
const timer = setInterval(() => {
|
|
6785
|
-
installStatus({ installId: view.installId }).then((status) => {
|
|
6786
|
-
setView((current) => reduceInstall(current, {
|
|
6787
|
-
type: "status",
|
|
6788
|
-
status
|
|
6789
|
-
}));
|
|
6790
|
-
}, () => {});
|
|
6791
|
-
}, INSTALL_POLL_MS);
|
|
6792
|
-
return () => clearInterval(timer);
|
|
6793
|
-
}, [
|
|
6794
|
-
view,
|
|
6795
|
-
setView,
|
|
6796
|
-
installStatus
|
|
6797
|
-
]);
|
|
6798
|
-
}
|
|
6886
|
+
//#region src/client/useFlows.ts
|
|
6799
6887
|
/**
|
|
6800
|
-
*
|
|
6801
|
-
*
|
|
6802
|
-
*
|
|
6888
|
+
* One keyed registry of install-shaped flows: start, poll to terminal, reset.
|
|
6889
|
+
*
|
|
6890
|
+
* Install and uninstall are the same machine. Both fold a
|
|
6891
|
+
* `ShopInstallStatusResult` through `reduceInstall` into an `InstallView`,
|
|
6892
|
+
* both poll every running identity from one interval, and both must keep a
|
|
6893
|
+
* settled outcome after the installed projection that follows it drops the
|
|
6894
|
+
* row the panel was mounted on (I-1, §7.2) — which is why the state is keyed
|
|
6895
|
+
* by identity here rather than held by the card.
|
|
6896
|
+
*
|
|
6897
|
+
* They differ in exactly one place: what the STARTING RPC's answer means. An
|
|
6898
|
+
* install refusal is a `rejected` view carrying the gate's code; an uninstall
|
|
6899
|
+
* refusal is a `failed` view carrying the host's published detail, because
|
|
6900
|
+
* `ShopUninstallResult`'s failure variant has no code to populate and those
|
|
6901
|
+
* codes belong to the install gate. That difference is the `begin` parameter,
|
|
6902
|
+
* and it is the whole of it.
|
|
6903
|
+
*
|
|
6904
|
+
* The two used to be hand-synced copies — 71 of 84 lines identical. The cost
|
|
6905
|
+
* was not theoretical: a single review round had to make the same edit twice
|
|
6906
|
+
* (the settle signature, and its call), and the `pending` set below was added
|
|
6907
|
+
* to one of them and not the other before this module existed.
|
|
6803
6908
|
*/
|
|
6804
|
-
|
|
6909
|
+
/** Shared empty set, so an idle registry hands out one stable identity. */
|
|
6910
|
+
const NO_PENDING = /* @__PURE__ */ new Set();
|
|
6911
|
+
/**
|
|
6912
|
+
* @param begin the starting RPC and its answer, mapped to the first view.
|
|
6913
|
+
* Never rejects: a transport throw is the caller's to fold into a view,
|
|
6914
|
+
* because only the caller knows which localized line stands in for a detail
|
|
6915
|
+
* too private to render.
|
|
6916
|
+
* @param onSettled called once per identity that reaches a terminal state,
|
|
6917
|
+
* with WHICH terminal state. The outcome is load-bearing: a failed flow
|
|
6918
|
+
* changed nothing on the server, so a caller acting on "settled" alone
|
|
6919
|
+
* discards outcomes that are still true.
|
|
6920
|
+
*/
|
|
6921
|
+
function useKeyedFlows(begin, installStatus, onSettled) {
|
|
6805
6922
|
const [views, setViews] = (0, react.useState)(() => /* @__PURE__ */ new Map());
|
|
6806
6923
|
const settled = (0, react.useRef)(onSettled);
|
|
6807
6924
|
settled.current = onSettled;
|
|
@@ -6824,29 +6941,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6824
6941
|
}, []);
|
|
6825
6942
|
const start = (0, react.useCallback)(async (key, args) => {
|
|
6826
6943
|
put(key, { kind: "idle" });
|
|
6827
|
-
|
|
6828
|
-
|
|
6829
|
-
if (!result.ok) {
|
|
6830
|
-
put(key, {
|
|
6831
|
-
kind: "rejected",
|
|
6832
|
-
code: result.code,
|
|
6833
|
-
detail: result.detail
|
|
6834
|
-
});
|
|
6835
|
-
return;
|
|
6836
|
-
}
|
|
6837
|
-
put(key, {
|
|
6838
|
-
kind: "running",
|
|
6839
|
-
installId: result.installId,
|
|
6840
|
-
log: []
|
|
6841
|
-
});
|
|
6842
|
-
} catch {
|
|
6843
|
-
put(key, {
|
|
6844
|
-
kind: "failed",
|
|
6845
|
-
detail: "",
|
|
6846
|
-
log: []
|
|
6847
|
-
});
|
|
6848
|
-
}
|
|
6849
|
-
}, [install, put]);
|
|
6944
|
+
put(key, await begin(args));
|
|
6945
|
+
}, [begin, put]);
|
|
6850
6946
|
const reset = (0, react.useCallback)((key) => {
|
|
6851
6947
|
setViews((current) => {
|
|
6852
6948
|
if (!current.has(key)) return current;
|
|
@@ -6865,7 +6961,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6865
6961
|
type: "status",
|
|
6866
6962
|
status
|
|
6867
6963
|
});
|
|
6868
|
-
if (status.found && status.state
|
|
6964
|
+
if (status.found && isTerminalInstallState(status.state)) settled.current?.(key, status.state);
|
|
6869
6965
|
}, () => {});
|
|
6870
6966
|
}, INSTALL_POLL_MS);
|
|
6871
6967
|
return () => clearInterval(timer);
|
|
@@ -6874,58 +6970,130 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6874
6970
|
installStatus,
|
|
6875
6971
|
apply
|
|
6876
6972
|
]);
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
|
|
6885
|
-
])
|
|
6973
|
+
const pendingRef = (0, react.useRef)(NO_PENDING);
|
|
6974
|
+
const pending = (0, react.useMemo)(() => {
|
|
6975
|
+
const next = /* @__PURE__ */ new Set();
|
|
6976
|
+
for (const [key, view] of views) if (view.kind !== "idle") next.add(key);
|
|
6977
|
+
const previous = pendingRef.current;
|
|
6978
|
+
if (previous.size === next.size && [...next].every((key) => previous.has(key))) return previous;
|
|
6979
|
+
pendingRef.current = next;
|
|
6980
|
+
return next;
|
|
6981
|
+
}, [views]);
|
|
6982
|
+
return {
|
|
6983
|
+
flowFor: (0, react.useCallback)((key) => ({
|
|
6984
|
+
view: views.get(key) ?? { kind: "idle" },
|
|
6985
|
+
start: (args) => start(key, args),
|
|
6986
|
+
reset: () => reset(key)
|
|
6987
|
+
}), [
|
|
6988
|
+
views,
|
|
6989
|
+
start,
|
|
6990
|
+
reset
|
|
6991
|
+
]),
|
|
6992
|
+
resetFlow: reset,
|
|
6993
|
+
pending
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
//#endregion
|
|
6997
|
+
//#region src/client/useInstall.ts
|
|
6998
|
+
/** Install driving hooks: the shared poll loop, and the tab's keyed registry. */
|
|
6999
|
+
/** The single-view poll loop: while the view is `running`, poll once per
|
|
7000
|
+
* second and fold each status through the reducer. A poll failure is
|
|
7001
|
+
* transient — the host retains the record, so the next tick finds it. The
|
|
7002
|
+
* rejection handler must be present: an unhandled rejection here would
|
|
7003
|
+
* escape the poll loop.
|
|
7004
|
+
*
|
|
7005
|
+
* Kept for the self-update (`useUpdateSelf`), which drives ONE flow with no
|
|
7006
|
+
* identity to key it by. Everything per-plugin goes through `useKeyedFlows`,
|
|
7007
|
+
* whose own loop polls every running identity from one interval. */
|
|
7008
|
+
function usePollStatus(view, setView, installStatus) {
|
|
7009
|
+
(0, react.useEffect)(() => {
|
|
7010
|
+
if (view.kind !== "running") return;
|
|
7011
|
+
const timer = setInterval(() => {
|
|
7012
|
+
installStatus({ installId: view.installId }).then((status) => {
|
|
7013
|
+
setView((current) => reduceInstall(current, {
|
|
7014
|
+
type: "status",
|
|
7015
|
+
status
|
|
7016
|
+
}));
|
|
7017
|
+
}, () => {});
|
|
7018
|
+
}, INSTALL_POLL_MS);
|
|
7019
|
+
return () => clearInterval(timer);
|
|
7020
|
+
}, [
|
|
7021
|
+
view,
|
|
7022
|
+
setView,
|
|
7023
|
+
installStatus
|
|
7024
|
+
]);
|
|
7025
|
+
}
|
|
7026
|
+
/**
|
|
7027
|
+
* Install flows owned by the tab and keyed by install identity. Keeping the
|
|
7028
|
+
* state above individual cards preserves a running operation when filtering
|
|
7029
|
+
* unmounts its card, and makes the shelf and Outdated panels agree.
|
|
7030
|
+
*
|
|
7031
|
+
* The registry itself is `useKeyedFlows`; this supplies the one thing that is
|
|
7032
|
+
* install-specific — what the starting RPC's answer means.
|
|
7033
|
+
*/
|
|
7034
|
+
function useInstallFlows(install, installStatus, onSettled) {
|
|
7035
|
+
return useKeyedFlows((0, react.useCallback)(async (args) => {
|
|
7036
|
+
try {
|
|
7037
|
+
const result = await install(args);
|
|
7038
|
+
if (!result.ok) return {
|
|
7039
|
+
kind: "rejected",
|
|
7040
|
+
code: result.code,
|
|
7041
|
+
detail: result.detail
|
|
7042
|
+
};
|
|
7043
|
+
return {
|
|
7044
|
+
kind: "running",
|
|
7045
|
+
installId: result.installId,
|
|
7046
|
+
log: [],
|
|
7047
|
+
phase: phaseOf(result.state)
|
|
7048
|
+
};
|
|
7049
|
+
} catch {
|
|
7050
|
+
return {
|
|
7051
|
+
kind: "failed",
|
|
7052
|
+
detail: "",
|
|
7053
|
+
log: []
|
|
7054
|
+
};
|
|
7055
|
+
}
|
|
7056
|
+
}, [install]), installStatus, onSettled);
|
|
6886
7057
|
}
|
|
6887
7058
|
//#endregion
|
|
6888
7059
|
//#region src/client/useUninstall.ts
|
|
6889
|
-
/** Uninstall driving hook
|
|
6890
|
-
|
|
6891
|
-
*
|
|
6892
|
-
*
|
|
6893
|
-
*
|
|
6894
|
-
*
|
|
6895
|
-
|
|
6896
|
-
*
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
7060
|
+
/** Uninstall driving hook: the tab's keyed registry, one start mapping. */
|
|
7061
|
+
/**
|
|
7062
|
+
* Uninstall flows owned by the tab and keyed by install identity. Lifting the
|
|
7063
|
+
* state above individual cards is what lets a completed uninstall's outcome
|
|
7064
|
+
* survive the installed-projection refresh that follows it — the row backing
|
|
7065
|
+
* the card disappears, but the flow keyed by identity does not.
|
|
7066
|
+
*
|
|
7067
|
+
* The registry itself is `useKeyedFlows`; this supplies the one thing that is
|
|
7068
|
+
* uninstall-specific — what the starting RPC's answer means. An uninstall
|
|
7069
|
+
* business failure (not in the catalog / not installed) lands in the `failed`
|
|
7070
|
+
* view with the host's published detail, never in the install `rejected`
|
|
7071
|
+
* codes: those belong to the install gate, and `ShopUninstallResult`'s
|
|
7072
|
+
* failure variant has no `code` field to populate.
|
|
7073
|
+
*/
|
|
7074
|
+
function useUninstallFlows(uninstall, installStatus, onSettled) {
|
|
7075
|
+
return useKeyedFlows((0, react.useCallback)(async (args) => {
|
|
6901
7076
|
try {
|
|
6902
7077
|
const result = await uninstall(args);
|
|
6903
|
-
if (!result.ok) {
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6908
|
-
|
|
6909
|
-
return;
|
|
6910
|
-
}
|
|
6911
|
-
setView({
|
|
7078
|
+
if (!result.ok) return {
|
|
7079
|
+
kind: "failed",
|
|
7080
|
+
detail: result.detail,
|
|
7081
|
+
log: []
|
|
7082
|
+
};
|
|
7083
|
+
return {
|
|
6912
7084
|
kind: "running",
|
|
6913
7085
|
installId: result.installId,
|
|
6914
|
-
log: []
|
|
6915
|
-
|
|
7086
|
+
log: [],
|
|
7087
|
+
phase: "installing"
|
|
7088
|
+
};
|
|
6916
7089
|
} catch {
|
|
6917
|
-
|
|
7090
|
+
return {
|
|
6918
7091
|
kind: "failed",
|
|
6919
7092
|
detail: "",
|
|
6920
7093
|
log: []
|
|
6921
|
-
}
|
|
7094
|
+
};
|
|
6922
7095
|
}
|
|
6923
|
-
}, [uninstall]);
|
|
6924
|
-
usePollStatus(view, setView, installStatus);
|
|
6925
|
-
return {
|
|
6926
|
-
view,
|
|
6927
|
-
start
|
|
6928
|
-
};
|
|
7096
|
+
}, [uninstall]), installStatus, onSettled);
|
|
6929
7097
|
}
|
|
6930
7098
|
//#endregion
|
|
6931
7099
|
//#region src/client/useUpdateSelf.ts
|
|
@@ -6953,7 +7121,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6953
7121
|
setView({
|
|
6954
7122
|
kind: "running",
|
|
6955
7123
|
installId: result.installId,
|
|
6956
|
-
log: []
|
|
7124
|
+
log: [],
|
|
7125
|
+
phase: phaseOf(result.state)
|
|
6957
7126
|
});
|
|
6958
7127
|
} catch {
|
|
6959
7128
|
setView({
|
|
@@ -6971,7 +7140,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6971
7140
|
}
|
|
6972
7141
|
//#endregion
|
|
6973
7142
|
//#region \0dsh-plugin-shop-css:e3675a89
|
|
6974
|
-
const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s26d58e_categoryButton.s5f68c4_categoryButtonOn` — TWO classes — so it\n * matches `.s26d58e_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s5f68c4_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour or a shadow: a pressed tab must occupy\n * exactly the same box as an unpressed one, so the bar does not reflow — and\n * with ~9 pills on a wrapping row, one that grows can push the rest onto\n * another line and move the tab out from under the pointer that just clicked\n * it. That is what `font-weight: 600` did (bold metrics are wider than\n * regular), and it is why nothing may be added here that resolves to a length:\n * no padding, no border-width, no font-size, no letter-spacing.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` fails the build on one.\n *\n * `box-shadow` is the exception that earns its place: it paints outside the\n * layout box, so an inset ring doubles the border's weight at zero cost to the\n * metrics. It is what `font-weight: 600` used to be — a second, thickness-\n * based signal that hover does not also produce — and the `other` tab needs it\n * most: its hue IS the neutral gray this file uses for \"no category\", so on\n * the light theme hue alone barely separates pressed from unpressed. */\n.s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n border-color: var(--category-hue);\n background: color-mix(in srgb, var(--category-hue) 14%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n box-shadow: inset 0 0 0 1px var(--category-hue);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours. `box-shadow` is dropped in this mode\n * too, so the ring has to come back as an outline. */\n@media (forced-colors: active) {\n .s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s26d58e_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sfd887b_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sfd887b_incompatibleFilter:hover .s11c019_switch` outranks `.s7580e8_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sfd887b_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sfd887b_incompatibleFilter:hover .s11c019_switch:not(.s7580e8_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s26d58e_categoryButton.s5f68c4_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sfd887b_incompatibleFilter.s6e4cba_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sfd887b_incompatibleFilter .s11c019_switch:not(.s7580e8_switchOn) .saab2b3_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sfd887b_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s73242a_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. */\n.s64a5ff_reloadButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.s64a5ff_reloadButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s64a5ff_reloadButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s4453f3_reloadFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s26d58e_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.s65f383_card[data-category='tool'], .s26d58e_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'], .s26d58e_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s65f383_card[data-category='ui'], .s26d58e_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'], .s26d58e_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s65f383_card[data-category='integration'], .s26d58e_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s65f383_card[data-category='theme'], .s26d58e_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s65f383_card[data-category='other'], .s26d58e_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value:\n * a github entry has no npm size, and a packument that named no account has no\n * author. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s5fbaf9_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The unpacked size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s5fbaf9_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row. */\n.s2d3c05_size {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s5fbaf9_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s5fbaf9_cardMeta`. There is no middot:\n * `.s5fbaf9_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s1ff66f_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sfd887b_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.s11c019_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.s11c019_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s7580e8_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
|
|
7143
|
+
const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s26d58e_categoryButton.s5f68c4_categoryButtonOn` — TWO classes — so it\n * matches `.s26d58e_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s5f68c4_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour or a shadow: a pressed tab must occupy\n * exactly the same box as an unpressed one, so the bar does not reflow — and\n * with ~9 pills on a wrapping row, one that grows can push the rest onto\n * another line and move the tab out from under the pointer that just clicked\n * it. That is what `font-weight: 600` did (bold metrics are wider than\n * regular), and it is why nothing may be added here that resolves to a length:\n * no padding, no border-width, no font-size, no letter-spacing.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` fails the build on one.\n *\n * `box-shadow` is the exception that earns its place: it paints outside the\n * layout box, so an inset ring doubles the border's weight at zero cost to the\n * metrics. It is what `font-weight: 600` used to be — a second, thickness-\n * based signal that hover does not also produce — and the `other` tab needs it\n * most: its hue IS the neutral gray this file uses for \"no category\", so on\n * the light theme hue alone barely separates pressed from unpressed. */\n.s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n border-color: var(--category-hue);\n background: color-mix(in srgb, var(--category-hue) 14%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n box-shadow: inset 0 0 0 1px var(--category-hue);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours. `box-shadow` is dropped in this mode\n * too, so the ring has to come back as an outline. */\n@media (forced-colors: active) {\n .s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s26d58e_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sfd887b_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sfd887b_incompatibleFilter:hover .s11c019_switch` outranks `.s7580e8_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sfd887b_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sfd887b_incompatibleFilter:hover .s11c019_switch:not(.s7580e8_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s26d58e_categoryButton.s5f68c4_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sfd887b_incompatibleFilter.s6e4cba_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sfd887b_incompatibleFilter .s11c019_switch:not(.s7580e8_switchOn) .saab2b3_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sfd887b_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s73242a_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.sff243d_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.sff243d_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.sff243d_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.sff243d_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s194168_catalogRefreshFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s26d58e_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.s65f383_card[data-category='tool'], .s26d58e_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'], .s26d58e_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s65f383_card[data-category='ui'], .s26d58e_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'], .s26d58e_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s65f383_card[data-category='integration'], .s26d58e_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s65f383_card[data-category='theme'], .s26d58e_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s65f383_card[data-category='other'], .s26d58e_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value:\n * a github entry has no npm size, and a packument that named no account has no\n * author. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s5fbaf9_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The unpacked size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s5fbaf9_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row. */\n.s2d3c05_size {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s5fbaf9_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s5fbaf9_cardMeta`. There is no middot:\n * `.s5fbaf9_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s1ff66f_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s6ac237_restartButton,\n.s64a5ff_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover,\n.s64a5ff_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible,\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.sf53ca0_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.sf79aef_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflows the settings dialog. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sfd887b_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.s11c019_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.s11c019_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s7580e8_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.sa1f556_outdatedActions`: `.sf79aef_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sf53ca0_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
6975
7144
|
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
|
|
6976
7145
|
const tag = document.createElement("style");
|
|
6977
7146
|
tag.dataset.pluginCss = "e3675a89";
|
|
@@ -7014,8 +7183,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7014
7183
|
"catalogHeading": "s668774_catalogHeading",
|
|
7015
7184
|
"catalogStatsRow": "seb0a1e_catalogStatsRow",
|
|
7016
7185
|
"catalogStats": "sccc4d1_catalogStats",
|
|
7017
|
-
"
|
|
7018
|
-
"
|
|
7186
|
+
"catalogRefreshButton": "sff243d_catalogRefreshButton",
|
|
7187
|
+
"catalogRefreshFailed": "s194168_catalogRefreshFailed",
|
|
7019
7188
|
"emptyLine": "s6de1ab_emptyLine",
|
|
7020
7189
|
"cards": "sf657f5_cards",
|
|
7021
7190
|
"card": "s65f383_card",
|
|
@@ -7060,6 +7229,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7060
7229
|
"uninstallButton": "s96967f_uninstallButton",
|
|
7061
7230
|
"restartSelfButton": "sb00c36_restartSelfButton",
|
|
7062
7231
|
"restartButton": "s6ac237_restartButton",
|
|
7232
|
+
"reloadButton": "s64a5ff_reloadButton",
|
|
7233
|
+
"reloadPanel": "sf79aef_reloadPanel",
|
|
7063
7234
|
"gateTitle": "s559c38_gateTitle",
|
|
7064
7235
|
"gateBody": "s3c0ace_gateBody",
|
|
7065
7236
|
"gateWarning": "sc323e5_gateWarning",
|
|
@@ -7108,7 +7279,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7108
7279
|
* install controls. An installed plugin's card carries its installed row:
|
|
7109
7280
|
* current → the non-interactive installed label, behind → the update button;
|
|
7110
7281
|
* uninstalled → the install button. */
|
|
7111
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, nameTakenBy, t, flowFor,
|
|
7282
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, nameTakenBy, t, flowFor, uninstallFlowFor, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
|
|
7112
7283
|
const [open, setOpen] = (0, react.useState)(false);
|
|
7113
7284
|
const blockers = blockersOf(missing, nameTakenBy, t);
|
|
7114
7285
|
const detailId = (0, react.useId)();
|
|
@@ -7125,10 +7296,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7125
7296
|
subdir: entry.subdir
|
|
7126
7297
|
};
|
|
7127
7298
|
const flow = flowFor(entryKey(entry));
|
|
7128
|
-
const
|
|
7129
|
-
flow.reset();
|
|
7130
|
-
onSettled();
|
|
7131
|
-
}, [flow, onSettled]);
|
|
7299
|
+
const uninstallFlow = uninstallFlowFor(entryKey(entry));
|
|
7132
7300
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7133
7301
|
className: _dsh_plugin_shop_css_e3675a89_default.card,
|
|
7134
7302
|
"data-shop-entry": entry.name,
|
|
@@ -7277,7 +7445,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7277
7445
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7278
7446
|
className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
|
|
7279
7447
|
"data-shop-actions": true,
|
|
7280
|
-
children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
7448
|
+
children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!inInstalledView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
7281
7449
|
target: installTarget,
|
|
7282
7450
|
tier: entry.tier,
|
|
7283
7451
|
missing,
|
|
@@ -7286,8 +7454,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7286
7454
|
flow,
|
|
7287
7455
|
t,
|
|
7288
7456
|
restart,
|
|
7289
|
-
|
|
7290
|
-
|
|
7457
|
+
restartBlocked,
|
|
7458
|
+
reload
|
|
7459
|
+
}), uninstallFlow.view.kind !== "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
|
|
7460
|
+
name: entry.name,
|
|
7461
|
+
t,
|
|
7462
|
+
restart,
|
|
7463
|
+
restartBlocked,
|
|
7464
|
+
reload,
|
|
7465
|
+
flow: uninstallFlow
|
|
7466
|
+
})] }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7291
7467
|
installed.outdated || flow.view.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
7292
7468
|
target: installTarget,
|
|
7293
7469
|
tier: entry.tier,
|
|
@@ -7298,7 +7474,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7298
7474
|
flow,
|
|
7299
7475
|
t,
|
|
7300
7476
|
restart,
|
|
7301
|
-
|
|
7477
|
+
restartBlocked,
|
|
7478
|
+
reload
|
|
7302
7479
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7303
7480
|
className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
|
|
7304
7481
|
"data-shop-installed": true,
|
|
@@ -7310,16 +7487,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7310
7487
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
7311
7488
|
row: installed,
|
|
7312
7489
|
t,
|
|
7313
|
-
setEnabled
|
|
7490
|
+
setEnabled,
|
|
7491
|
+
reload
|
|
7314
7492
|
}),
|
|
7315
7493
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
|
|
7316
7494
|
name: entry.name,
|
|
7317
7495
|
t,
|
|
7318
|
-
uninstall,
|
|
7319
|
-
installStatus,
|
|
7320
7496
|
restart,
|
|
7321
|
-
|
|
7322
|
-
|
|
7497
|
+
restartBlocked,
|
|
7498
|
+
reload,
|
|
7499
|
+
flow: uninstallFlow
|
|
7323
7500
|
})
|
|
7324
7501
|
] }), (size !== void 0 || author !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7325
7502
|
className: _dsh_plugin_shop_css_e3675a89_default.cardMeta,
|
|
@@ -7380,17 +7557,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7380
7557
|
}
|
|
7381
7558
|
/** One entry's install flow: the button, the §9.3 acknowledgement gate for
|
|
7382
7559
|
* community-tier entries, and the live view — running log, restart notice,
|
|
7383
|
-
* failure detail, rejection detail — driven by `
|
|
7560
|
+
* failure detail, rejection detail — driven by `useInstallFlows`. Shared by the
|
|
7384
7561
|
* catalog cards (`variant: 'install'`) and the outdated rows' update button
|
|
7385
7562
|
* (`variant: 'update'`, which drives the same install flow for `name@latest`). */
|
|
7386
|
-
function InstallPanel({ target, tier, missing, blockers, missingStated = false, variant = "install", flow, t, restart,
|
|
7563
|
+
function InstallPanel({ target, tier, missing, blockers, missingStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
|
|
7387
7564
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
7388
7565
|
const { view, start } = flow;
|
|
7389
7566
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7390
7567
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
7391
7568
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7392
7569
|
className: _dsh_plugin_shop_css_e3675a89_default.installing,
|
|
7393
|
-
children: t(
|
|
7570
|
+
children: t(installPhaseKey(view.phase))
|
|
7394
7571
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7395
7572
|
className: _dsh_plugin_shop_css_e3675a89_default.log,
|
|
7396
7573
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -7412,16 +7589,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7412
7589
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7413
7590
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
7414
7591
|
"data-shop-restart-notice": true,
|
|
7415
|
-
children:
|
|
7592
|
+
children: t(activationNoticeKey(view.activation, view.restartReason))
|
|
7416
7593
|
}),
|
|
7417
|
-
|
|
7594
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActivationOffer, {
|
|
7595
|
+
activation: view.activation,
|
|
7418
7596
|
t,
|
|
7419
|
-
restart
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
|
|
7423
|
-
"data-shop-restart-disabled": true,
|
|
7424
|
-
children: t("restartDisabledNotice")
|
|
7597
|
+
restart,
|
|
7598
|
+
restartBlocked,
|
|
7599
|
+
reload,
|
|
7600
|
+
where: "install"
|
|
7425
7601
|
})
|
|
7426
7602
|
]
|
|
7427
7603
|
});
|
|
@@ -7521,13 +7697,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7521
7697
|
* §9.3 is about granting. A business failure (not in the catalog / not
|
|
7522
7698
|
* installed) lands in the failed view with the host's published detail; a
|
|
7523
7699
|
* transport failure carries the empty detail and the localized fallback. */
|
|
7524
|
-
function UninstallPanel({ name, t,
|
|
7525
|
-
const { view, start } =
|
|
7526
|
-
const settled = (0, react.useRef)(onSettled);
|
|
7527
|
-
settled.current = onSettled;
|
|
7528
|
-
(0, react.useEffect)(() => {
|
|
7529
|
-
if (view.kind === "done") settled.current();
|
|
7530
|
-
}, [view.kind]);
|
|
7700
|
+
function UninstallPanel({ name, t, restart, restartBlocked, reload, flow }) {
|
|
7701
|
+
const { view, start, reset } = flow;
|
|
7531
7702
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7532
7703
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
7533
7704
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -7547,16 +7718,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7547
7718
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7548
7719
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
7549
7720
|
"data-shop-uninstall-done": true,
|
|
7550
|
-
children:
|
|
7721
|
+
children: t(uninstallActivationNoticeKey(view.activation))
|
|
7551
7722
|
}),
|
|
7552
|
-
|
|
7723
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActivationOffer, {
|
|
7724
|
+
activation: view.activation,
|
|
7553
7725
|
t,
|
|
7554
|
-
restart
|
|
7726
|
+
restart,
|
|
7727
|
+
restartBlocked,
|
|
7728
|
+
reload,
|
|
7729
|
+
where: "uninstall"
|
|
7555
7730
|
}),
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7731
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7732
|
+
type: "button",
|
|
7733
|
+
className: _dsh_plugin_shop_css_e3675a89_default.cancelButton,
|
|
7734
|
+
"data-shop-uninstall-dismiss": true,
|
|
7735
|
+
onClick: reset,
|
|
7736
|
+
children: t("dismiss")
|
|
7560
7737
|
})
|
|
7561
7738
|
]
|
|
7562
7739
|
});
|
|
@@ -7577,6 +7754,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7577
7754
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7578
7755
|
className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
|
|
7579
7756
|
children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
|
|
7757
|
+
}),
|
|
7758
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7759
|
+
type: "button",
|
|
7760
|
+
className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
|
|
7761
|
+
"data-shop-uninstall-retry": true,
|
|
7762
|
+
onClick: () => void start({ name }),
|
|
7763
|
+
children: t("retry")
|
|
7580
7764
|
})
|
|
7581
7765
|
]
|
|
7582
7766
|
});
|
|
@@ -7692,15 +7876,64 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7692
7876
|
});
|
|
7693
7877
|
}
|
|
7694
7878
|
/**
|
|
7879
|
+
* The §4/§8 offer under a settled install, update or uninstall: a reload
|
|
7880
|
+
* when the change is already live on the server and only this page is
|
|
7881
|
+
* stale, a restart when the host actually needs one — and can restart at
|
|
7882
|
+
* all (§C-1), otherwise the disabled notice says why — and neither when the
|
|
7883
|
+
* change landed needing neither.
|
|
7884
|
+
*
|
|
7885
|
+
* One component rather than the same three lines in each panel. The
|
|
7886
|
+
* precedence between the three values is a single rule, and it was written
|
|
7887
|
+
* out twice: two places to find when a fourth activation value arrives or
|
|
7888
|
+
* the `restartBlocked` gate changes, with nothing to make the second one
|
|
7889
|
+
* fail if it were missed.
|
|
7890
|
+
*/
|
|
7891
|
+
function ActivationOffer({ activation, t, restart, restartBlocked, reload, where }) {
|
|
7892
|
+
if (activation === "reload") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReloadPanel, {
|
|
7893
|
+
t,
|
|
7894
|
+
reload,
|
|
7895
|
+
where
|
|
7896
|
+
});
|
|
7897
|
+
if (activation !== "restart") return null;
|
|
7898
|
+
return restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
7899
|
+
t,
|
|
7900
|
+
restart
|
|
7901
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7902
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
7903
|
+
"data-shop-restart-disabled": true,
|
|
7904
|
+
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
7905
|
+
});
|
|
7906
|
+
}
|
|
7907
|
+
/** The §4 reload offer: the server already holds the new state and this tab
|
|
7908
|
+
* does not. Never automatic — a reload discards whatever the reader had in
|
|
7909
|
+
* flight (a conversation, a form, an upload), and the shop knows a reload
|
|
7910
|
+
* would help without knowing that now is a good time. */
|
|
7911
|
+
function ReloadPanel({ t, reload, where }) {
|
|
7912
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7913
|
+
className: _dsh_plugin_shop_css_e3675a89_default.reloadPanel,
|
|
7914
|
+
"data-shop-reload": where,
|
|
7915
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7916
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
7917
|
+
children: t("reloadNote")
|
|
7918
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7919
|
+
type: "button",
|
|
7920
|
+
className: _dsh_plugin_shop_css_e3675a89_default.reloadButton,
|
|
7921
|
+
onClick: reload,
|
|
7922
|
+
children: t("reload")
|
|
7923
|
+
})]
|
|
7924
|
+
});
|
|
7925
|
+
}
|
|
7926
|
+
/**
|
|
7695
7927
|
* The hot enable/disable switch (§8). The initial state is the Host's
|
|
7696
7928
|
* inventory verdict carried on the installed row; the click is optimistic
|
|
7697
7929
|
* (the value flips on success) and a success renders the §8 hot note. A
|
|
7698
7930
|
* transport throw renders the localized failure line — its private detail
|
|
7699
7931
|
* (which can name hosts and ports) never reaches the UI.
|
|
7700
7932
|
*/
|
|
7701
|
-
function EnabledSwitch({ row, t, setEnabled }) {
|
|
7933
|
+
function EnabledSwitch({ row, t, setEnabled, reload }) {
|
|
7702
7934
|
const [enabled, setEnabledState] = (0, react.useState)(row.enabled);
|
|
7703
7935
|
const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
|
|
7936
|
+
const [needsReload, setNeedsReload] = (0, react.useState)(false);
|
|
7704
7937
|
const onToggle = async () => {
|
|
7705
7938
|
if (toggle.kind === "saving") return;
|
|
7706
7939
|
const next = !enabled;
|
|
@@ -7713,9 +7946,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7713
7946
|
if (result.ok) {
|
|
7714
7947
|
setEnabledState(next);
|
|
7715
7948
|
setToggle({ kind: "saved" });
|
|
7949
|
+
if (result.activation !== "live") setNeedsReload(true);
|
|
7716
7950
|
} else setToggle({
|
|
7717
7951
|
kind: "error",
|
|
7718
|
-
detail: result.detail
|
|
7952
|
+
detail: result.detail || t("toggleFailed")
|
|
7719
7953
|
});
|
|
7720
7954
|
} catch {
|
|
7721
7955
|
setToggle({
|
|
@@ -7739,11 +7973,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7739
7973
|
disabled: toggle.kind === "saving",
|
|
7740
7974
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_e3675a89_default.switchKnob })
|
|
7741
7975
|
}),
|
|
7742
|
-
toggle.kind === "saved" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7976
|
+
toggle.kind === "saved" && !needsReload && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7743
7977
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
7744
7978
|
"data-shop-hot-apply": true,
|
|
7745
7979
|
children: t("hotApplyNote")
|
|
7746
7980
|
}),
|
|
7981
|
+
needsReload && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ReloadPanel, {
|
|
7982
|
+
t,
|
|
7983
|
+
reload,
|
|
7984
|
+
where: "toggle"
|
|
7985
|
+
}),
|
|
7747
7986
|
toggle.kind === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7748
7987
|
className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
|
|
7749
7988
|
"data-shop-toggle-error": true,
|
|
@@ -7755,7 +7994,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7755
7994
|
/** One outdated install row (§7.3): the name, the installed and latest
|
|
7756
7995
|
* versions, the hot enable/disable switch, and the update button (the
|
|
7757
7996
|
* install flow for `name@latest`, reusing `InstallPanel`). */
|
|
7758
|
-
function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart,
|
|
7997
|
+
function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
7759
7998
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7760
7999
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedRow,
|
|
7761
8000
|
"data-shop-outdated-entry": row.name,
|
|
@@ -7773,7 +8012,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7773
8012
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
7774
8013
|
row,
|
|
7775
8014
|
t,
|
|
7776
|
-
setEnabled
|
|
8015
|
+
setEnabled,
|
|
8016
|
+
reload
|
|
7777
8017
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
7778
8018
|
target: {
|
|
7779
8019
|
name: row.name,
|
|
@@ -7789,7 +8029,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7789
8029
|
flow: flowFor(identityKey(row)),
|
|
7790
8030
|
t,
|
|
7791
8031
|
restart,
|
|
7792
|
-
|
|
8032
|
+
restartBlocked,
|
|
8033
|
+
reload
|
|
7793
8034
|
})]
|
|
7794
8035
|
})]
|
|
7795
8036
|
});
|
|
@@ -7801,7 +8042,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7801
8042
|
* tier for the update gate is looked up from the catalog by name (community →
|
|
7802
8043
|
* acknowledgement); an entry absent from the catalog defaults to the
|
|
7803
8044
|
* community gate (the safer read). */
|
|
7804
|
-
function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart,
|
|
8045
|
+
function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
7805
8046
|
if (state.kind === "loading") return null;
|
|
7806
8047
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7807
8048
|
className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
|
|
@@ -7826,7 +8067,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7826
8067
|
setEnabled,
|
|
7827
8068
|
flowFor,
|
|
7828
8069
|
restart,
|
|
7829
|
-
|
|
8070
|
+
restartBlocked,
|
|
8071
|
+
reload
|
|
7830
8072
|
}) }, identityKey(row)))
|
|
7831
8073
|
})]
|
|
7832
8074
|
});
|
|
@@ -7834,7 +8076,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7834
8076
|
/** The shop tab root: browse, search, refresh, and render one card per
|
|
7835
8077
|
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
7836
8078
|
function ShopTab(props) {
|
|
7837
|
-
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, restart, version, updateStart } = props;
|
|
8079
|
+
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, restart, version, updateStart, reload: injectedReload } = props;
|
|
7838
8080
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
7839
8081
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
7840
8082
|
/** The install gate's own input, or undefined while it is unknown — loading,
|
|
@@ -7843,7 +8085,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7843
8085
|
* about a conflict it could not check. */
|
|
7844
8086
|
const [specs, setSpecs] = (0, react.useState)(void 0);
|
|
7845
8087
|
const [selfVersion, setSelfVersion] = (0, react.useState)(null);
|
|
7846
|
-
const
|
|
8088
|
+
const restartBlocked = selfVersion?.restartBlocked ?? null;
|
|
8089
|
+
const reload = (0, react.useMemo)(() => injectedReload ?? (() => {
|
|
8090
|
+
globalThis.location.reload();
|
|
8091
|
+
}), [injectedReload]);
|
|
7847
8092
|
const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
|
|
7848
8093
|
const selfUpdate = useUpdateSelf(updateStart, installStatus);
|
|
7849
8094
|
const [request, setRequest] = (0, react.useState)({ kind: "initial" });
|
|
@@ -7851,7 +8096,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7851
8096
|
const noteMutation = (0, react.useCallback)(() => {
|
|
7852
8097
|
setMutations((current) => current + 1);
|
|
7853
8098
|
}, []);
|
|
7854
|
-
const
|
|
8099
|
+
const uninstallResetRef = (0, react.useRef)(() => {});
|
|
8100
|
+
const flows = useInstallFlows(install, installStatus, (0, react.useCallback)((key, outcome) => {
|
|
8101
|
+
if (outcome === "done") uninstallResetRef.current(key);
|
|
8102
|
+
noteMutation();
|
|
8103
|
+
}, [noteMutation]));
|
|
8104
|
+
const uninstallFlows = useUninstallFlows(uninstall, installStatus, (0, react.useCallback)((key, outcome) => {
|
|
8105
|
+
if (outcome !== "done") return;
|
|
8106
|
+
flows.flowFor(key).reset();
|
|
8107
|
+
noteMutation();
|
|
8108
|
+
}, [flows, noteMutation]));
|
|
8109
|
+
uninstallResetRef.current = uninstallFlows.resetFlow;
|
|
7855
8110
|
const [reloading, setReloading] = (0, react.useState)(false);
|
|
7856
8111
|
const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
|
|
7857
8112
|
const [query, setQuery] = (0, react.useState)("");
|
|
@@ -7974,7 +8229,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7974
8229
|
const q = query.trim().toLowerCase();
|
|
7975
8230
|
return sortedBrowsable.filter((entry) => {
|
|
7976
8231
|
if (category === "installed") {
|
|
7977
|
-
|
|
8232
|
+
const key = entryKey(entry);
|
|
8233
|
+
if (!installedByKey.has(key) && !uninstallFlows.pending.has(key)) return false;
|
|
7978
8234
|
} else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
|
|
7979
8235
|
if (q === "") return true;
|
|
7980
8236
|
const summaryEn = entry.catalog?.summary.en ?? "";
|
|
@@ -7985,7 +8241,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7985
8241
|
sortedBrowsable,
|
|
7986
8242
|
query,
|
|
7987
8243
|
category,
|
|
7988
|
-
installedByKey
|
|
8244
|
+
installedByKey,
|
|
8245
|
+
uninstallFlows.pending
|
|
7989
8246
|
]);
|
|
7990
8247
|
const filtered = (0, react.useMemo)(() => hideIncompatible && category !== "installed" ? matched.filter((entry) => !badgedIncompatible(entryKey(entry))) : matched, [
|
|
7991
8248
|
matched,
|
|
@@ -8093,7 +8350,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8093
8350
|
className: _dsh_plugin_shop_css_e3675a89_default.versionText,
|
|
8094
8351
|
"data-shop-version": true,
|
|
8095
8352
|
children: ["v", selfVersion.installed]
|
|
8096
|
-
}), selfUpdate.view.kind === "done" &&
|
|
8353
|
+
}), selfUpdate.view.kind === "done" && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8097
8354
|
type: "button",
|
|
8098
8355
|
className: _dsh_plugin_shop_css_e3675a89_default.restartSelfButton,
|
|
8099
8356
|
"data-shop-restart": true,
|
|
@@ -8141,7 +8398,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8141
8398
|
"data-shop-self-updating": true,
|
|
8142
8399
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8143
8400
|
className: _dsh_plugin_shop_css_e3675a89_default.installing,
|
|
8144
|
-
children: t(
|
|
8401
|
+
children: t(installPhaseKey(selfUpdate.view.phase))
|
|
8145
8402
|
}), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8146
8403
|
className: _dsh_plugin_shop_css_e3675a89_default.log,
|
|
8147
8404
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
@@ -8160,7 +8417,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8160
8417
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
8161
8418
|
children: t("installedRestartNotice")
|
|
8162
8419
|
}),
|
|
8163
|
-
|
|
8420
|
+
restartBlocked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
8164
8421
|
t,
|
|
8165
8422
|
restart,
|
|
8166
8423
|
gate: {
|
|
@@ -8168,10 +8425,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8168
8425
|
close: () => setSelfRestartGate(false)
|
|
8169
8426
|
}
|
|
8170
8427
|
}),
|
|
8171
|
-
|
|
8428
|
+
restartBlocked !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8172
8429
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
8173
8430
|
"data-shop-restart-disabled": true,
|
|
8174
|
-
children: t(
|
|
8431
|
+
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
8175
8432
|
})
|
|
8176
8433
|
]
|
|
8177
8434
|
})
|
|
@@ -8280,8 +8537,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8280
8537
|
}),
|
|
8281
8538
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8282
8539
|
type: "button",
|
|
8283
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
8284
|
-
"data-shop-
|
|
8540
|
+
className: _dsh_plugin_shop_css_e3675a89_default.catalogRefreshButton,
|
|
8541
|
+
"data-shop-catalog-refresh": true,
|
|
8285
8542
|
disabled: reloading,
|
|
8286
8543
|
onClick: () => {
|
|
8287
8544
|
setReloading(true);
|
|
@@ -8291,8 +8548,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8291
8548
|
children: reloading ? t("refreshing") : t("refresh")
|
|
8292
8549
|
}),
|
|
8293
8550
|
reloadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8294
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
8295
|
-
"data-shop-
|
|
8551
|
+
className: _dsh_plugin_shop_css_e3675a89_default.catalogRefreshFailed,
|
|
8552
|
+
"data-shop-catalog-refresh-failed": true,
|
|
8296
8553
|
children: t("refreshFailed")
|
|
8297
8554
|
})
|
|
8298
8555
|
]
|
|
@@ -8316,12 +8573,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8316
8573
|
nameTakenBy: nameTakenByKey.get(key),
|
|
8317
8574
|
t,
|
|
8318
8575
|
flowFor: flows.flowFor,
|
|
8319
|
-
|
|
8320
|
-
uninstall,
|
|
8576
|
+
uninstallFlowFor: uninstallFlows.flowFor,
|
|
8321
8577
|
restart,
|
|
8322
|
-
|
|
8578
|
+
restartBlocked,
|
|
8579
|
+
reload,
|
|
8323
8580
|
setEnabled,
|
|
8324
|
-
|
|
8581
|
+
inInstalledView: category === "installed"
|
|
8325
8582
|
}) }, key);
|
|
8326
8583
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
8327
8584
|
ref: sentinelRef,
|
|
@@ -8345,7 +8602,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8345
8602
|
setEnabled,
|
|
8346
8603
|
flowFor: flows.flowFor,
|
|
8347
8604
|
restart,
|
|
8348
|
-
|
|
8605
|
+
restartBlocked,
|
|
8606
|
+
reload
|
|
8349
8607
|
})
|
|
8350
8608
|
]
|
|
8351
8609
|
});
|