dsh-plugin-shop 0.8.3 → 0.8.4-beta.0
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 +11 -8
- package/lib/client.js +1133 -421
- package/lib/index.js +1400 -178
- package/lib/typert.host.js +69 -22
- package/lib/typert.remote-client.js +52 -13
- package/lib/types/client/ShopTab.d.ts +19 -2
- package/lib/types/client/locales.d.ts +42 -2
- package/lib/types/client/module-table.d.ts +131 -0
- package/lib/types/client/present.d.ts +126 -2
- package/lib/types/client/restart-monitor.d.ts +58 -0
- package/lib/types/host/activation.d.ts +12 -9
- package/lib/types/host/bundle-patch.d.ts +35 -0
- package/lib/types/host/catalog.d.ts +9 -3
- package/lib/types/host/compatibility.d.ts +223 -0
- package/lib/types/host/dsh-cli.d.ts +8 -0
- package/lib/types/host/harness.d.ts +109 -0
- package/lib/types/host/hot.d.ts +8 -8
- package/lib/types/host/index.d.ts +197 -28
- package/lib/types/host/install.d.ts +1 -1
- package/lib/types/host/peers.d.ts +181 -10
- package/lib/types/host/profile.d.ts +61 -4
- package/lib/types/host/restart.d.ts +5 -1
- package/lib/types/host/types.d.ts +27 -1
- package/lib/types/own-version.d.ts +18 -2
- package/package.json +2 -1
package/lib/client.js
CHANGED
|
@@ -4316,6 +4316,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4316
4316
|
})]).optional(),
|
|
4317
4317
|
"added": union([_undefined(), string()]).optional(),
|
|
4318
4318
|
"peers": union([_undefined(), array(string())]).optional(),
|
|
4319
|
+
"compatibility": union([_undefined(), object({
|
|
4320
|
+
"dsh": union([_undefined(), string()]).optional(),
|
|
4321
|
+
"profiles": union([_undefined(), array(string())]).optional()
|
|
4322
|
+
})]).optional(),
|
|
4323
|
+
"dshPeers": union([_undefined(), record(string(), string())]).optional(),
|
|
4319
4324
|
"unpackedSize": union([_undefined(), number()]).optional(),
|
|
4320
4325
|
"installSize": union([_undefined(), number()]).optional()
|
|
4321
4326
|
})),
|
|
@@ -4326,7 +4331,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4326
4331
|
})),
|
|
4327
4332
|
"notAShop": union([_undefined(), array(string())]).optional(),
|
|
4328
4333
|
"stars": record(string(), number()),
|
|
4329
|
-
"incompatible": record(string(), array(string()))
|
|
4334
|
+
"incompatible": record(string(), array(string())),
|
|
4335
|
+
"incompatibleHarness": record(string(), object({
|
|
4336
|
+
"dsh": union([_undefined(), object({
|
|
4337
|
+
"range": string(),
|
|
4338
|
+
"running": string()
|
|
4339
|
+
})]).optional(),
|
|
4340
|
+
"profile": union([_undefined(), object({
|
|
4341
|
+
"declared": array(string()),
|
|
4342
|
+
"running": string()
|
|
4343
|
+
})]).optional(),
|
|
4344
|
+
"peers": union([_undefined(), object({
|
|
4345
|
+
"refused": record(string(), string()),
|
|
4346
|
+
"running": string(),
|
|
4347
|
+
"allowCommand": union([literal(null), string()])
|
|
4348
|
+
})]).optional()
|
|
4349
|
+
}))
|
|
4330
4350
|
});
|
|
4331
4351
|
const dsh_plugin_shop_shop_installed_result$schema = array(object({
|
|
4332
4352
|
"name": string(),
|
|
@@ -4373,7 +4393,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4373
4393
|
literal("needs-acknowledgement"),
|
|
4374
4394
|
literal("tarball-integrity"),
|
|
4375
4395
|
literal("ambiguous-identity"),
|
|
4376
|
-
literal("name-taken")
|
|
4396
|
+
literal("name-taken"),
|
|
4397
|
+
literal("desktop-profile")
|
|
4377
4398
|
]),
|
|
4378
4399
|
"detail": string()
|
|
4379
4400
|
})]);
|
|
@@ -4400,11 +4421,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4400
4421
|
literal("host-unsupported"),
|
|
4401
4422
|
literal("timeout"),
|
|
4402
4423
|
literal("mount-failed"),
|
|
4403
|
-
literal("
|
|
4424
|
+
literal("already-loaded")
|
|
4404
4425
|
]).optional(),
|
|
4405
4426
|
"detail": union([_undefined(), string()]).optional()
|
|
4406
4427
|
});
|
|
4407
|
-
const dsh_plugin_shop_shop_restart_result$schema = union([object({
|
|
4428
|
+
const dsh_plugin_shop_shop_restart_result$schema = union([object({
|
|
4429
|
+
"ok": literal(true),
|
|
4430
|
+
"logFile": union([_undefined(), string()]).optional()
|
|
4431
|
+
}), object({
|
|
4408
4432
|
"ok": literal(false),
|
|
4409
4433
|
"detail": string()
|
|
4410
4434
|
})]);
|
|
@@ -4451,10 +4475,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4451
4475
|
"outdated": boolean(),
|
|
4452
4476
|
"restartBlocked": union([
|
|
4453
4477
|
literal(null),
|
|
4478
|
+
literal("desktop"),
|
|
4454
4479
|
literal("windows"),
|
|
4455
4480
|
literal("systemd"),
|
|
4456
4481
|
literal("port-zero")
|
|
4457
|
-
])
|
|
4482
|
+
]),
|
|
4483
|
+
"pendingVersion": union([
|
|
4484
|
+
_undefined(),
|
|
4485
|
+
literal(null),
|
|
4486
|
+
string()
|
|
4487
|
+
]).optional()
|
|
4458
4488
|
});
|
|
4459
4489
|
const TYPERT_REMOTE = {
|
|
4460
4490
|
package: "dsh-plugin-shop",
|
|
@@ -4473,17 +4503,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4473
4503
|
codec: {
|
|
4474
4504
|
mode: "strict",
|
|
4475
4505
|
typeSymbol: "dsh-plugin-shop#shop/catalog:args",
|
|
4476
|
-
schema: dsh_plugin_shop_shop_catalog_parameter_0$schema
|
|
4506
|
+
schema: dsh_plugin_shop_shop_catalog_parameter_0$schema,
|
|
4507
|
+
create: () => dsh_plugin_shop_shop_catalog_parameter_0$schema
|
|
4477
4508
|
}
|
|
4478
4509
|
}],
|
|
4479
4510
|
result: {
|
|
4480
4511
|
mode: "strict",
|
|
4481
4512
|
typeSymbol: "dsh-plugin-shop/types#ShopCatalogResult",
|
|
4482
|
-
schema: dsh_plugin_shop_shop_catalog_result$schema
|
|
4513
|
+
schema: dsh_plugin_shop_shop_catalog_result$schema,
|
|
4514
|
+
create: () => dsh_plugin_shop_shop_catalog_result$schema
|
|
4483
4515
|
},
|
|
4484
4516
|
sourceLocation: {
|
|
4485
4517
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4486
|
-
"line":
|
|
4518
|
+
"line": 1134,
|
|
4487
4519
|
"column": 9
|
|
4488
4520
|
}
|
|
4489
4521
|
},
|
|
@@ -4497,11 +4529,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4497
4529
|
result: {
|
|
4498
4530
|
mode: "strict",
|
|
4499
4531
|
typeSymbol: "dsh-plugin-shop#shop/installed:result",
|
|
4500
|
-
schema: dsh_plugin_shop_shop_installed_result$schema
|
|
4532
|
+
schema: dsh_plugin_shop_shop_installed_result$schema,
|
|
4533
|
+
create: () => dsh_plugin_shop_shop_installed_result$schema
|
|
4501
4534
|
},
|
|
4502
4535
|
sourceLocation: {
|
|
4503
4536
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4504
|
-
"line":
|
|
4537
|
+
"line": 1494,
|
|
4505
4538
|
"column": 9
|
|
4506
4539
|
}
|
|
4507
4540
|
},
|
|
@@ -4515,11 +4548,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4515
4548
|
result: {
|
|
4516
4549
|
mode: "strict",
|
|
4517
4550
|
typeSymbol: "dsh-plugin-shop#shop/installedSpecs:result",
|
|
4518
|
-
schema: dsh_plugin_shop_shop_installedSpecs_result$schema
|
|
4551
|
+
schema: dsh_plugin_shop_shop_installedSpecs_result$schema,
|
|
4552
|
+
create: () => dsh_plugin_shop_shop_installedSpecs_result$schema
|
|
4519
4553
|
},
|
|
4520
4554
|
sourceLocation: {
|
|
4521
4555
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4522
|
-
"line":
|
|
4556
|
+
"line": 1485,
|
|
4523
4557
|
"column": 9
|
|
4524
4558
|
}
|
|
4525
4559
|
},
|
|
@@ -4537,17 +4571,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4537
4571
|
codec: {
|
|
4538
4572
|
mode: "strict",
|
|
4539
4573
|
typeSymbol: "dsh-plugin-shop/types#InstallArgs",
|
|
4540
|
-
schema: dsh_plugin_shop_shop_installStart_parameter_0$schema
|
|
4574
|
+
schema: dsh_plugin_shop_shop_installStart_parameter_0$schema,
|
|
4575
|
+
create: () => dsh_plugin_shop_shop_installStart_parameter_0$schema
|
|
4541
4576
|
}
|
|
4542
4577
|
}],
|
|
4543
4578
|
result: {
|
|
4544
4579
|
mode: "strict",
|
|
4545
4580
|
typeSymbol: "dsh-plugin-shop/types#ShopInstallResult",
|
|
4546
|
-
schema: dsh_plugin_shop_shop_installStart_result$schema
|
|
4581
|
+
schema: dsh_plugin_shop_shop_installStart_result$schema,
|
|
4582
|
+
create: () => dsh_plugin_shop_shop_installStart_result$schema
|
|
4547
4583
|
},
|
|
4548
4584
|
sourceLocation: {
|
|
4549
4585
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4550
|
-
"line":
|
|
4586
|
+
"line": 1260,
|
|
4551
4587
|
"column": 9
|
|
4552
4588
|
}
|
|
4553
4589
|
},
|
|
@@ -4564,17 +4600,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4564
4600
|
codec: {
|
|
4565
4601
|
mode: "strict",
|
|
4566
4602
|
typeSymbol: "dsh-plugin-shop#shop/installStatus:args",
|
|
4567
|
-
schema: dsh_plugin_shop_shop_installStatus_parameter_0$schema
|
|
4603
|
+
schema: dsh_plugin_shop_shop_installStatus_parameter_0$schema,
|
|
4604
|
+
create: () => dsh_plugin_shop_shop_installStatus_parameter_0$schema
|
|
4568
4605
|
}
|
|
4569
4606
|
}],
|
|
4570
4607
|
result: {
|
|
4571
4608
|
mode: "strict",
|
|
4572
4609
|
typeSymbol: "dsh-plugin-shop/types#ShopInstallStatusResult",
|
|
4573
|
-
schema: dsh_plugin_shop_shop_installStatus_result$schema
|
|
4610
|
+
schema: dsh_plugin_shop_shop_installStatus_result$schema,
|
|
4611
|
+
create: () => dsh_plugin_shop_shop_installStatus_result$schema
|
|
4574
4612
|
},
|
|
4575
4613
|
sourceLocation: {
|
|
4576
4614
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4577
|
-
"line":
|
|
4615
|
+
"line": 1461,
|
|
4578
4616
|
"column": 3
|
|
4579
4617
|
}
|
|
4580
4618
|
},
|
|
@@ -4588,11 +4626,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4588
4626
|
result: {
|
|
4589
4627
|
mode: "strict",
|
|
4590
4628
|
typeSymbol: "dsh-plugin-shop/types#ShopRestartResult",
|
|
4591
|
-
schema: dsh_plugin_shop_shop_restart_result$schema
|
|
4629
|
+
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
4630
|
+
create: () => dsh_plugin_shop_shop_restart_result$schema
|
|
4592
4631
|
},
|
|
4593
4632
|
sourceLocation: {
|
|
4594
4633
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4595
|
-
"line":
|
|
4634
|
+
"line": 1680,
|
|
4596
4635
|
"column": 9
|
|
4597
4636
|
}
|
|
4598
4637
|
},
|
|
@@ -4609,17 +4648,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4609
4648
|
codec: {
|
|
4610
4649
|
mode: "strict",
|
|
4611
4650
|
typeSymbol: "dsh-plugin-shop#shop/setEnabled:args",
|
|
4612
|
-
schema: dsh_plugin_shop_shop_setEnabled_parameter_0$schema
|
|
4651
|
+
schema: dsh_plugin_shop_shop_setEnabled_parameter_0$schema,
|
|
4652
|
+
create: () => dsh_plugin_shop_shop_setEnabled_parameter_0$schema
|
|
4613
4653
|
}
|
|
4614
4654
|
}],
|
|
4615
4655
|
result: {
|
|
4616
4656
|
mode: "strict",
|
|
4617
4657
|
typeSymbol: "dsh-plugin-shop/types#ShopSetEnabledResult",
|
|
4618
|
-
schema: dsh_plugin_shop_shop_setEnabled_result$schema
|
|
4658
|
+
schema: dsh_plugin_shop_shop_setEnabled_result$schema,
|
|
4659
|
+
create: () => dsh_plugin_shop_shop_setEnabled_result$schema
|
|
4619
4660
|
},
|
|
4620
4661
|
sourceLocation: {
|
|
4621
4662
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4622
|
-
"line":
|
|
4663
|
+
"line": 887,
|
|
4623
4664
|
"column": 9
|
|
4624
4665
|
}
|
|
4625
4666
|
},
|
|
@@ -4637,17 +4678,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4637
4678
|
codec: {
|
|
4638
4679
|
mode: "strict",
|
|
4639
4680
|
typeSymbol: "dsh-plugin-shop#shop/uninstallStart:args",
|
|
4640
|
-
schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema
|
|
4681
|
+
schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema,
|
|
4682
|
+
create: () => dsh_plugin_shop_shop_uninstallStart_parameter_0$schema
|
|
4641
4683
|
}
|
|
4642
4684
|
}],
|
|
4643
4685
|
result: {
|
|
4644
4686
|
mode: "strict",
|
|
4645
4687
|
typeSymbol: "dsh-plugin-shop/types#ShopUninstallResult",
|
|
4646
|
-
schema: dsh_plugin_shop_shop_uninstallStart_result$schema
|
|
4688
|
+
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
4689
|
+
create: () => dsh_plugin_shop_shop_uninstallStart_result$schema
|
|
4647
4690
|
},
|
|
4648
4691
|
sourceLocation: {
|
|
4649
4692
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4650
|
-
"line":
|
|
4693
|
+
"line": 1601,
|
|
4651
4694
|
"column": 9
|
|
4652
4695
|
}
|
|
4653
4696
|
},
|
|
@@ -4664,17 +4707,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4664
4707
|
codec: {
|
|
4665
4708
|
mode: "strict",
|
|
4666
4709
|
typeSymbol: "dsh-plugin-shop#shop/updateStart:args",
|
|
4667
|
-
schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema
|
|
4710
|
+
schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema,
|
|
4711
|
+
create: () => dsh_plugin_shop_shop_updateStart_parameter_0$schema
|
|
4668
4712
|
}
|
|
4669
4713
|
}],
|
|
4670
4714
|
result: {
|
|
4671
4715
|
mode: "strict",
|
|
4672
4716
|
typeSymbol: "dsh-plugin-shop/types#ShopUpdateResult",
|
|
4673
|
-
schema: dsh_plugin_shop_shop_updateStart_result$schema
|
|
4717
|
+
schema: dsh_plugin_shop_shop_updateStart_result$schema,
|
|
4718
|
+
create: () => dsh_plugin_shop_shop_updateStart_result$schema
|
|
4674
4719
|
},
|
|
4675
4720
|
sourceLocation: {
|
|
4676
4721
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4677
|
-
"line":
|
|
4722
|
+
"line": 1777,
|
|
4678
4723
|
"column": 9
|
|
4679
4724
|
}
|
|
4680
4725
|
},
|
|
@@ -4688,11 +4733,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4688
4733
|
result: {
|
|
4689
4734
|
mode: "strict",
|
|
4690
4735
|
typeSymbol: "dsh-plugin-shop/types#ShopVersionResult",
|
|
4691
|
-
schema: dsh_plugin_shop_shop_version_result$schema
|
|
4736
|
+
schema: dsh_plugin_shop_shop_version_result$schema,
|
|
4737
|
+
create: () => dsh_plugin_shop_shop_version_result$schema
|
|
4692
4738
|
},
|
|
4693
4739
|
sourceLocation: {
|
|
4694
4740
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4695
|
-
"line":
|
|
4741
|
+
"line": 1735,
|
|
4696
4742
|
"column": 9
|
|
4697
4743
|
}
|
|
4698
4744
|
}
|
|
@@ -6290,7 +6336,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6290
6336
|
case "host-unsupported": return "hotHostUnsupportedNotice";
|
|
6291
6337
|
case "timeout": return "hotTimeoutNotice";
|
|
6292
6338
|
case "mount-failed": return "hotMountFailedNotice";
|
|
6293
|
-
case "
|
|
6339
|
+
case "already-loaded": return "hotAlreadyLoadedNotice";
|
|
6294
6340
|
default: return "installedRestartNotice";
|
|
6295
6341
|
}
|
|
6296
6342
|
}
|
|
@@ -6326,6 +6372,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6326
6372
|
* could not have helped. */
|
|
6327
6373
|
function restartBlockedNoticeKey(reason) {
|
|
6328
6374
|
switch (reason) {
|
|
6375
|
+
case "desktop": return "restartBlockedDesktopNotice";
|
|
6329
6376
|
case "windows": return "restartBlockedWindowsNotice";
|
|
6330
6377
|
case "systemd": return "restartBlockedSystemdNotice";
|
|
6331
6378
|
case "port-zero": return "restartBlockedPortZeroNotice";
|
|
@@ -6346,6 +6393,105 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6346
6393
|
function missingPeersOf(incompatible, key) {
|
|
6347
6394
|
return incompatible[key] ?? [];
|
|
6348
6395
|
}
|
|
6396
|
+
/** The author's own `dsh.compatibility` that this installation does not meet
|
|
6397
|
+
* (design 2026-09-01 §8.2), or undefined when the host gave no verdict. Keyed
|
|
6398
|
+
* by install identity, never by name, for the reason `missingPeersOf` is.
|
|
6399
|
+
*
|
|
6400
|
+
* The map itself may be absent, whatever the result type says: a tab can
|
|
6401
|
+
* outlive a shop self-update by one reload and then be talking to the OLD
|
|
6402
|
+
* in-process host until dsh restarts, which answers the shape from before the
|
|
6403
|
+
* key existed. That is "no verdict", never a crash. */
|
|
6404
|
+
function harnessVerdictOf(verdicts, key) {
|
|
6405
|
+
return verdicts !== void 0 && Object.hasOwn(verdicts, key) ? verdicts[key] : void 0;
|
|
6406
|
+
}
|
|
6407
|
+
/**
|
|
6408
|
+
* Everything standing in one entry's way, in the order every surface renders
|
|
6409
|
+
* it — the card's detail lines, the badge's accessible name, the outdated
|
|
6410
|
+
* row's refusal — and the one list the incompatible filter reads, through
|
|
6411
|
+
* `readsIncompatible`, and the install button, through `refusesInstall`.
|
|
6412
|
+
*
|
|
6413
|
+
* The refusals read first. The name conflict leads: it is about a plugin the
|
|
6414
|
+
* reader chose and would lose. Then dsh's own refusal, then what this machine
|
|
6415
|
+
* is measured to lack, then what the author declared, range before profiles —
|
|
6416
|
+
* the order the host's verdict carries its halves.
|
|
6417
|
+
*/
|
|
6418
|
+
function blockersOf(missing, harness, nameTakenBy) {
|
|
6419
|
+
const blockers = [];
|
|
6420
|
+
if (nameTakenBy !== void 0) blockers.push({
|
|
6421
|
+
kind: "name-taken",
|
|
6422
|
+
holder: nameTakenBy
|
|
6423
|
+
});
|
|
6424
|
+
if (harness?.peers !== void 0) {
|
|
6425
|
+
const { refused, running, allowCommand } = harness.peers;
|
|
6426
|
+
blockers.push({
|
|
6427
|
+
kind: "harness-peers",
|
|
6428
|
+
refused,
|
|
6429
|
+
running,
|
|
6430
|
+
allowCommand
|
|
6431
|
+
});
|
|
6432
|
+
}
|
|
6433
|
+
if (missing.length > 0) blockers.push({
|
|
6434
|
+
kind: "missing-peers",
|
|
6435
|
+
modules: missing
|
|
6436
|
+
});
|
|
6437
|
+
if (harness?.dsh !== void 0) blockers.push({
|
|
6438
|
+
kind: "harness-range",
|
|
6439
|
+
range: harness.dsh.range,
|
|
6440
|
+
running: harness.dsh.running
|
|
6441
|
+
});
|
|
6442
|
+
if (harness?.profile !== void 0) blockers.push({
|
|
6443
|
+
kind: "harness-profile",
|
|
6444
|
+
declared: harness.profile.declared,
|
|
6445
|
+
running: harness.profile.running
|
|
6446
|
+
});
|
|
6447
|
+
return blockers;
|
|
6448
|
+
}
|
|
6449
|
+
/**
|
|
6450
|
+
* The badge's visible word for these blockers, or null when there are none.
|
|
6451
|
+
*
|
|
6452
|
+
* "Incompatible" for anything about this harness, whichever reason put it
|
|
6453
|
+
* there. A taken name is the more serious condition and names a different
|
|
6454
|
+
* remedy, so it decides the word whenever it holds — the card is perfectly
|
|
6455
|
+
* compatible, and a different plugin is in the way.
|
|
6456
|
+
*/
|
|
6457
|
+
function blockerBadgeKey(blockers) {
|
|
6458
|
+
if (blockers.length === 0) return null;
|
|
6459
|
+
return blockers.some((blocker) => blocker.kind === "name-taken") ? "nameTakenBadge" : "incompatibleBadge";
|
|
6460
|
+
}
|
|
6461
|
+
/** Whether the badge reads "Incompatible" — exactly the set the incompatible
|
|
6462
|
+
* filter counts and takes away. Derived from the badge's own rule, never
|
|
6463
|
+
* restated beside it: the filter used to test the peer list by hand, which
|
|
6464
|
+
* held only while missing peers were the one reason for the word — a card
|
|
6465
|
+
* badged for its declared range would have read "Incompatible" and survived
|
|
6466
|
+
* the filter that names it. */
|
|
6467
|
+
function readsIncompatible(blockers) {
|
|
6468
|
+
return blockerBadgeKey(blockers) === "incompatibleBadge";
|
|
6469
|
+
}
|
|
6470
|
+
/**
|
|
6471
|
+
* Whether this install is refused before it starts, so its button is
|
|
6472
|
+
* disabled rather than opening the acknowledgement gate for an install that
|
|
6473
|
+
* cannot proceed: by the host, for a taken name, or by the running dsh, for
|
|
6474
|
+
* its harness peers.
|
|
6475
|
+
*
|
|
6476
|
+
* The second is the one harness reason that blocks (design
|
|
6477
|
+
* 2026-09-26-dsh-017-readiness, B1, amending "warn, never block"): dsh 0.1.7
|
|
6478
|
+
* refuses the install itself, before pnpm runs, and the shop cannot install
|
|
6479
|
+
* past it. The verdict is dsh's own rule on the profile's own exemptions, so
|
|
6480
|
+
* a disabled button is dsh's answer given early, never the shop's opinion —
|
|
6481
|
+
* and recording the exemption the card shows is what enables it again.
|
|
6482
|
+
*/
|
|
6483
|
+
function refusesInstall(blockers) {
|
|
6484
|
+
return blockers.some((blocker) => blocker.kind === "name-taken" || blocker.kind === "harness-peers");
|
|
6485
|
+
}
|
|
6486
|
+
/**
|
|
6487
|
+
* The refused peers as the refusal's copy names them: each peer and the range
|
|
6488
|
+
* the package declared, in the order dsh reported them. A blank range — which
|
|
6489
|
+
* dsh refuses — is quoted, or the sentence would name the peer and nothing
|
|
6490
|
+
* it asked for.
|
|
6491
|
+
*/
|
|
6492
|
+
function refusedPeersText(refused) {
|
|
6493
|
+
return Object.entries(refused).map(([name, range]) => `${name} ${range.trim() === "" ? JSON.stringify(range) : range}`).join(", ");
|
|
6494
|
+
}
|
|
6349
6495
|
/**
|
|
6350
6496
|
* The plugin already holding this entry's bundle name, if a DIFFERENT one is,
|
|
6351
6497
|
* named the way the host's refusal names it.
|
|
@@ -6384,6 +6530,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6384
6530
|
case "tarball-integrity": return "tarballIntegrityCode";
|
|
6385
6531
|
case "ambiguous-identity": return "ambiguousIdentityCode";
|
|
6386
6532
|
case "name-taken": return "nameTakenCode";
|
|
6533
|
+
case "desktop-profile": return "desktopProfileCode";
|
|
6387
6534
|
}
|
|
6388
6535
|
}
|
|
6389
6536
|
/** The version a card displays: full for npm, the short commit for github. */
|
|
@@ -6423,6 +6570,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6423
6570
|
}
|
|
6424
6571
|
/** §7.2 once-per-second poll cadence, as a named constant. */
|
|
6425
6572
|
const INSTALL_POLL_MS = 1e3;
|
|
6573
|
+
/**
|
|
6574
|
+
* The restart monitor's decision after a probe of the origin.
|
|
6575
|
+
*
|
|
6576
|
+
* `stableSinceMs` is when the current unbroken run of good answers began —
|
|
6577
|
+
* null when the last probe failed, or none has succeeded yet — and both times
|
|
6578
|
+
* count from the moment the restart was committed. One answer is not enough:
|
|
6579
|
+
* reloading on it is what landed a reader on a boot that died a moment later,
|
|
6580
|
+
* a blank page in place of the notice naming the log. So the page reloads
|
|
6581
|
+
* only once the run has lasted `RESTART_STABLE_MS`, and the monitor gives up
|
|
6582
|
+
* only when no run is under way past `RESTART_WAIT_MS` — which bounds the
|
|
6583
|
+
* whole wait at the sum of the two, since a run that began in time either
|
|
6584
|
+
* completes or breaks after the deadline.
|
|
6585
|
+
*/
|
|
6586
|
+
function restartMonitorVerdict(input) {
|
|
6587
|
+
if (input.stableSinceMs !== null && input.elapsedMs - input.stableSinceMs >= 8e3) return "reload";
|
|
6588
|
+
if (input.stableSinceMs === null && input.elapsedMs > 3e4) return "failed";
|
|
6589
|
+
return "wait";
|
|
6590
|
+
}
|
|
6426
6591
|
/** How long the check-update button reports "up to date" after a re-check
|
|
6427
6592
|
* finds no newer release, before reverting to its idle label. */
|
|
6428
6593
|
const CHECK_UP_TO_DATE_MS = 3e3;
|
|
@@ -6680,6 +6845,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6680
6845
|
error: "暂时无法读取目录。",
|
|
6681
6846
|
installedError: "暂时无法读取已安装列表。",
|
|
6682
6847
|
retry: "重试",
|
|
6848
|
+
/** The shop's own error boundary caught a render error; the error's message
|
|
6849
|
+
* follows on its own line, and Retry remounts the tab (design
|
|
6850
|
+
* 2026-09-26-market-borrowings §4). */
|
|
6851
|
+
tabCrashed: "插件商店在显示这一页时出错了。",
|
|
6683
6852
|
search: "搜索插件",
|
|
6684
6853
|
all: "全部",
|
|
6685
6854
|
catalog: "插件目录",
|
|
@@ -6709,12 +6878,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6709
6878
|
installedRestartNotice: "已安装;重启 dsh 后生效",
|
|
6710
6879
|
incompatibleBadge: "不兼容",
|
|
6711
6880
|
incompatibleDetail: "缺少组件/插件:{modules}\n此插件可能是为其他版本的 DSH 设计的。",
|
|
6881
|
+
/** The author's own `dsh.compatibility`, unmet on this machine (design
|
|
6882
|
+
* 2026-09-01 §8.2). Each names BOTH sides — what the author declared and
|
|
6883
|
+
* what this dsh runs — because the host sends both, and a bare
|
|
6884
|
+
* "unsupported" leaves the reader guessing which side is off. */
|
|
6885
|
+
harnessRangeDetail: "作者声明支持的 dsh 版本:{range};本机运行的是 {running}。",
|
|
6886
|
+
harnessProfileDetail: "作者声明支持的 profile:{declared};此 dsh 以 {running} profile 启动。",
|
|
6887
|
+
/** dsh's OWN refusal (0.1.7 on), not the author's claim: the install button
|
|
6888
|
+
* is disabled beside it (design 2026-09-26-dsh-017-readiness, B1). `{peers}`
|
|
6889
|
+
* is each refused peer with the range the package declared. The remedy
|
|
6890
|
+
* leads into the exemption command, rendered on its own line after it, and
|
|
6891
|
+
* names the Refresh button by its label. */
|
|
6892
|
+
harnessPeersDetail: "本机的 dsh {running} 会拒绝安装此插件:它要求 {peers}。",
|
|
6893
|
+
harnessPeersRemedy: "若愿意承担崩溃或数据丢失的风险,可在终端运行以下命令,为这个确切版本授予豁免,然后点击“刷新”:",
|
|
6712
6894
|
hotNoPatchNotice: "该插件没有可热挂载的补丁文件;重启 dsh 后生效",
|
|
6713
6895
|
hotNotSimpleNotice: "该插件的补丁包含无法热挂载的配置;重启 dsh 后生效",
|
|
6714
6896
|
hotHostUnsupportedNotice: "当前环境不支持热挂载;重启 dsh 后生效",
|
|
6715
6897
|
hotTimeoutNotice: "热挂载超时;重启 dsh 后生效",
|
|
6716
6898
|
hotMountFailedNotice: "热挂载失败;重启 dsh 后生效",
|
|
6717
|
-
|
|
6899
|
+
/** No mount was tried: this dsh already loaded the package once, and Node
|
|
6900
|
+
* will not load a second copy from the same place (design
|
|
6901
|
+
* 2026-09-26-market-borrowings §1). Covers an update and a reinstall alike,
|
|
6902
|
+
* so it names the new files rather than a "new version". */
|
|
6903
|
+
hotAlreadyLoadedNotice: "已安装;这次运行的 dsh 已经加载过这个插件,新装的文件要重启 dsh 后才会生效",
|
|
6718
6904
|
installedNoRestartNotice: "已安装并热挂载;现在即可使用,无需重启",
|
|
6719
6905
|
installedReloadNotice: "已生效;刷新页面即可看到",
|
|
6720
6906
|
reload: "刷新页面",
|
|
@@ -6739,9 +6925,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6739
6925
|
/** The incompatible filter's label. FIXED — the switch beside it carries the
|
|
6740
6926
|
* state, so the words name what the switch does and never flip with it. */
|
|
6741
6927
|
hideIncompatible: "隐藏不兼容 {count}",
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6928
|
+
/** What the filter takes away is every card whose badge reads 不兼容 —
|
|
6929
|
+
* missing components OR an author's declaration this dsh does not meet —
|
|
6930
|
+
* so these three name the badge, not one of its reasons. */
|
|
6931
|
+
incompatibleFilterTitle: "不显示标为不兼容的插件",
|
|
6932
|
+
showIncompatibleTitle: "把标为不兼容的插件放回列表",
|
|
6933
|
+
emptyIncompatibleFiltered: "匹配的插件都标为不兼容,已被「隐藏不兼容」筛掉。",
|
|
6745
6934
|
license: "许可",
|
|
6746
6935
|
customLicense: "自定义许可",
|
|
6747
6936
|
reviewedVersionLine: "已审核 v{reviewed} / 当前 v{current} 未审核",
|
|
@@ -6753,6 +6942,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6753
6942
|
tarballIntegrityCode: "压缩包完整性校验失败",
|
|
6754
6943
|
ambiguousIdentityCode: "无法确定是哪一个条目",
|
|
6755
6944
|
nameTakenCode: "同名的另一个插件已安装",
|
|
6945
|
+
desktopProfileCode: "由桌面版管理的 profile",
|
|
6756
6946
|
nameTakenBadge: "名称被占用",
|
|
6757
6947
|
nameTakenDetail: "已安装同名的另一个插件({holder})。\n两个同名插件声明相同的加载项 id,无法共存;安装它会替换掉已装的那个。",
|
|
6758
6948
|
npmSource: "npm 包",
|
|
@@ -6775,7 +6965,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6775
6965
|
restartConfirm: "重启",
|
|
6776
6966
|
restarting: "正在重启,服务器就绪后页面会自动刷新…",
|
|
6777
6967
|
restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
|
|
6968
|
+
/** The same failure, when the host said where the new process writes —
|
|
6969
|
+
* which is where a boot that died explains itself (design
|
|
6970
|
+
* 2026-09-26-market-borrowings §3). "Or did not stay up": the page now
|
|
6971
|
+
* waits for the new server to keep answering, so this also covers a boot
|
|
6972
|
+
* that answered and then died. */
|
|
6973
|
+
restartFailedLogNotice: "重启后服务器没有回来,或回来后没能保持运行。新进程的日志在 {log};也可以手动启动:dsh web",
|
|
6778
6974
|
restartTransportFailed: "重启请求未能送达。请稍后重试。",
|
|
6975
|
+
restartBlockedDesktopNotice: "此 profile 由 DeepSeek Harness 桌面版管理,商店无法重启它。请重启桌面版,以应用更改。",
|
|
6779
6976
|
restartBlockedWindowsNotice: "商店暂不支持在 Windows 上重启 dsh。请退出 dsh 后重新启动,以应用更改。",
|
|
6780
6977
|
restartBlockedSystemdNotice: "当前 dsh 由 systemd 服务托管,商店无法安全地重启它。请手动重启服务,或在商店行配置中设置 allowRestart: true。",
|
|
6781
6978
|
restartBlockedPortZeroNotice: "此 dsh 以 --port 0 启动,重启后会绑定到另一个端口,本页面无法跟上。请手动重启 dsh,以应用更改。",
|
|
@@ -6796,6 +6993,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6796
6993
|
error: "The catalog is temporarily unavailable.",
|
|
6797
6994
|
installedError: "The installed list is temporarily unavailable.",
|
|
6798
6995
|
retry: "Retry",
|
|
6996
|
+
tabCrashed: "The plugin shop hit an error while showing this page.",
|
|
6799
6997
|
search: "Search plugins",
|
|
6800
6998
|
all: "All",
|
|
6801
6999
|
catalog: "Plugin catalog",
|
|
@@ -6825,12 +7023,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6825
7023
|
installedRestartNotice: "installed; restart dsh to activate",
|
|
6826
7024
|
incompatibleBadge: "Incompatible",
|
|
6827
7025
|
incompatibleDetail: "Missing components/plugins: {modules}\nThis plugin may be designed for another DSH version.",
|
|
7026
|
+
harnessRangeDetail: "The author declares support for dsh {range}; this machine runs {running}.",
|
|
7027
|
+
harnessProfileDetail: "The author declares support for the {declared} profile(s); this dsh was launched with the {running} profile.",
|
|
7028
|
+
harnessPeersDetail: "dsh {running} on this machine refuses to install this plugin: it requires {peers}.",
|
|
7029
|
+
harnessPeersRemedy: "To accept the risk of crashes or data loss for this exact version, run this in a terminal, then press Refresh:",
|
|
6828
7030
|
hotNoPatchNotice: "no patch file to hot-mount; restart dsh to activate",
|
|
6829
7031
|
hotNotSimpleNotice: "the patch has config rows that cannot be hot-mounted; restart dsh to activate",
|
|
6830
7032
|
hotHostUnsupportedNotice: "this harness cannot hot-mount; restart dsh to activate",
|
|
6831
7033
|
hotTimeoutNotice: "hot-mount timed out; restart dsh to activate",
|
|
6832
7034
|
hotMountFailedNotice: "hot-mount failed; restart dsh to activate",
|
|
6833
|
-
|
|
7035
|
+
hotAlreadyLoadedNotice: "installed; this dsh has already loaded the plugin once, so the files just installed take effect after a restart of dsh",
|
|
6834
7036
|
installedNoRestartNotice: "installed and hot-mounted; running now, no restart needed",
|
|
6835
7037
|
installedReloadNotice: "already applied; reload the page to see it",
|
|
6836
7038
|
reload: "Reload the page",
|
|
@@ -6847,9 +7049,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6847
7049
|
authorLine: "Author: {author}",
|
|
6848
7050
|
sizeLabel: "{size} on disk",
|
|
6849
7051
|
hideIncompatible: "Hide incompatible {count}",
|
|
6850
|
-
incompatibleFilterTitle: "Leave out plugins
|
|
6851
|
-
showIncompatibleTitle: "Put plugins
|
|
6852
|
-
emptyIncompatibleFiltered: "Every matching plugin is
|
|
7052
|
+
incompatibleFilterTitle: "Leave out plugins marked incompatible with this dsh",
|
|
7053
|
+
showIncompatibleTitle: "Put plugins marked incompatible back on the shelf",
|
|
7054
|
+
emptyIncompatibleFiltered: "Every matching plugin is marked incompatible, and the incompatible filter is leaving them out.",
|
|
6853
7055
|
license: "License",
|
|
6854
7056
|
customLicense: "Custom license",
|
|
6855
7057
|
reviewedVersionLine: "reviewed v{reviewed} / current v{current} unreviewed",
|
|
@@ -6861,6 +7063,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6861
7063
|
tarballIntegrityCode: "tarball integrity check failed",
|
|
6862
7064
|
ambiguousIdentityCode: "Ambiguous entry",
|
|
6863
7065
|
nameTakenCode: "Another plugin holds this name",
|
|
7066
|
+
desktopProfileCode: "Managed by the desktop app",
|
|
6864
7067
|
nameTakenBadge: "Name taken",
|
|
6865
7068
|
nameTakenDetail: "A different plugin of this name is installed ({holder}).\nTwo plugins of one name declare the same loader entry id and cannot both load; installing this one would replace it.",
|
|
6866
7069
|
npmSource: "npm package",
|
|
@@ -6883,7 +7086,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6883
7086
|
restartConfirm: "Restart",
|
|
6884
7087
|
restarting: "Restarting; the page will refresh once the server is back…",
|
|
6885
7088
|
restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
|
|
7089
|
+
restartFailedLogNotice: "The server did not come back after the restart, or did not stay up. The new process logs to {log}; you can also start it manually: dsh web",
|
|
6886
7090
|
restartTransportFailed: "The restart request could not be delivered. Please retry.",
|
|
7091
|
+
restartBlockedDesktopNotice: "This profile is managed by the DeepSeek Harness desktop app, so the shop cannot restart it. Restart the app to apply the change.",
|
|
6887
7092
|
restartBlockedWindowsNotice: "The shop cannot restart dsh on Windows yet. Quit dsh and start it again to apply the change.",
|
|
6888
7093
|
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.",
|
|
6889
7094
|
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.",
|
|
@@ -6898,6 +7103,314 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6898
7103
|
hotApplyNote: "applied without a restart"
|
|
6899
7104
|
};
|
|
6900
7105
|
//#endregion
|
|
7106
|
+
//#region src/client/module-table.ts
|
|
7107
|
+
/**
|
|
7108
|
+
* How long one probe may take before its answer counts as unknown.
|
|
7109
|
+
*
|
|
7110
|
+
* The work a probe can actually reach is a microtask: a seed-word lookup, or a
|
|
7111
|
+
* factory this page already registered. So this bounds a harness that never
|
|
7112
|
+
* answers, not work — and it exists because the cost of the alternative is the
|
|
7113
|
+
* whole shelf. `refineAgainstModuleTable` promises that a table in any state
|
|
7114
|
+
* cannot cost the reader the catalog, and an `import()` that never settles
|
|
7115
|
+
* would have made that promise false by leaving the tab with nothing to render
|
|
7116
|
+
* (§9's degradation rule: an unavailable fact is silence, never an accusation).
|
|
7117
|
+
* A probe per name, not one deadline for the whole refinement: every other
|
|
7118
|
+
* name is still judged, and the tab waits at most this long however many names
|
|
7119
|
+
* there are.
|
|
7120
|
+
*/
|
|
7121
|
+
const MODULE_PROBE_TIMEOUT_MS = 2e3;
|
|
7122
|
+
/** `outcome`, or null once `ms` has passed — whichever settles first. The
|
|
7123
|
+
* timer is cleared either way, so a settled probe leaves nothing pending. */
|
|
7124
|
+
function withDeadline(outcome, ms) {
|
|
7125
|
+
let timer;
|
|
7126
|
+
const deadline = new Promise((resolve) => {
|
|
7127
|
+
timer = setTimeout(() => resolve(null), ms);
|
|
7128
|
+
});
|
|
7129
|
+
return Promise.race([outcome, deadline]).finally(() => {
|
|
7130
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
7131
|
+
});
|
|
7132
|
+
}
|
|
7133
|
+
/**
|
|
7134
|
+
* The host's verdict less every name the oracle provides.
|
|
7135
|
+
*
|
|
7136
|
+
* An entry whose list empties is dropped, and an entry ANY of whose names
|
|
7137
|
+
* comes back unknown gets no verdict at all — the rule `incompatibilityMap`
|
|
7138
|
+
* follows on the host, for the host's reason: an unavailable fact must never
|
|
7139
|
+
* read as an accusation, and keeping the rest of a list whose one unknown name
|
|
7140
|
+
* was dropped would publish a partial answer as the whole one. Each distinct
|
|
7141
|
+
* name is asked once, however many entries declare it. Never rejects: any
|
|
7142
|
+
* failure of the oracle is an unknown.
|
|
7143
|
+
*/
|
|
7144
|
+
async function refineIncompatible(incompatible, provides, timeoutMs = MODULE_PROBE_TIMEOUT_MS) {
|
|
7145
|
+
const answers = /* @__PURE__ */ new Map();
|
|
7146
|
+
const ask = (spec) => {
|
|
7147
|
+
const cached = answers.get(spec);
|
|
7148
|
+
if (cached !== void 0) return cached;
|
|
7149
|
+
const answer = withDeadline(new Promise((resolve) => {
|
|
7150
|
+
resolve(provides(spec));
|
|
7151
|
+
}).catch(() => null), timeoutMs);
|
|
7152
|
+
answers.set(spec, answer);
|
|
7153
|
+
return answer;
|
|
7154
|
+
};
|
|
7155
|
+
const judged = await Promise.all(Object.entries(incompatible).map(async ([key, names]) => {
|
|
7156
|
+
const verdicts = await Promise.all(names.map(ask));
|
|
7157
|
+
if (verdicts.some((verdict) => verdict !== true && verdict !== false)) return null;
|
|
7158
|
+
const missing = names.filter((_, index) => verdicts[index] === false);
|
|
7159
|
+
return missing.length > 0 ? {
|
|
7160
|
+
key,
|
|
7161
|
+
missing
|
|
7162
|
+
} : null;
|
|
7163
|
+
}));
|
|
7164
|
+
const refined = {};
|
|
7165
|
+
for (const row of judged) if (row !== null) refined[row.key] = row.missing;
|
|
7166
|
+
return refined;
|
|
7167
|
+
}
|
|
7168
|
+
/** The harness's own normalization (`stripClientSuffix`): a plugin bundle IS
|
|
7169
|
+
* its package's client half, so `x/client` and `x` name one row and one
|
|
7170
|
+
* record. Mirrored rather than imported — this package does not depend on
|
|
7171
|
+
* `@deepseek-ai/dsh-client-modules`. */
|
|
7172
|
+
function stripClientSuffix(spec) {
|
|
7173
|
+
return spec.endsWith("/client") ? spec.slice(0, -7) : spec;
|
|
7174
|
+
}
|
|
7175
|
+
/** The prefix of the one rejection that means "absent":
|
|
7176
|
+
* `ClientModuleSystem.import`'s own `client-modules: cannot resolve "<spec>" —
|
|
7177
|
+
* not a seed word, not a materialized module, and not a row in the boot
|
|
7178
|
+
* graph`. A harness that rewords it makes every probe unknown, which renders
|
|
7179
|
+
* as silence — the safe direction, never a false accusation. */
|
|
7180
|
+
const CANNOT_RESOLVE = "client-modules: cannot resolve";
|
|
7181
|
+
/**
|
|
7182
|
+
* An oracle over the live `modules` service, or null when the page offers no
|
|
7183
|
+
* usable table.
|
|
7184
|
+
*
|
|
7185
|
+
* The service is the harness's `ClientModuleLoader`, published to client
|
|
7186
|
+
* plugins as `ctx.reflect.provide('modules', …)`. It arrives as unknown-typed
|
|
7187
|
+
* foreign state, so only its PUBLIC contract is read, and only after
|
|
7188
|
+
* narrowing: `manifest.modules` must be an array, `loadCache` a Map and
|
|
7189
|
+
* `import` a function. `seed`, `factories` and `graphRows` exist on the
|
|
7190
|
+
* instance too, but they are TypeScript-`private` in the harness's own
|
|
7191
|
+
* declarations, and nothing here names them.
|
|
7192
|
+
*
|
|
7193
|
+
* Per spec, in the table's own order but with every side effect kept out:
|
|
7194
|
+
* 1. A graph row provides it, and `import` is NOT called — for a row it
|
|
7195
|
+
* fetches the bundle and runs the module body, and a probe must never be
|
|
7196
|
+
* what loads a plugin. `manifest.modules` is the row index, but it is not
|
|
7197
|
+
* fixed for the page's whole life on every harness build. On 0.1.5-rc.3
|
|
7198
|
+
* `this.manifest = options.manifest` is the constructor's only write, so a
|
|
7199
|
+
* row placed at boot stands unchanged for as long as the page lives. On
|
|
7200
|
+
* 0.1.7-rc.2 (`next`) `updateManifest` REPLACES `this.manifest` outright
|
|
7201
|
+
* with a new manifest that no longer lists an uninstalled package, and
|
|
7202
|
+
* `reconcile` removes the plugin from the loader — so UNTIL this page's own
|
|
7203
|
+
* uninstall reconciles, the OLD row still stands in `manifest.modules` and
|
|
7204
|
+
* this step clears the entry falsely; AFTER it reconciles, this step can no
|
|
7205
|
+
* longer be the false clear, because `manifest.modules` has already dropped
|
|
7206
|
+
* the package. The false clear then comes from `prune`, which keeps the
|
|
7207
|
+
* removed id only in the PRIVATE `graphRows` and in `loadCache`, and only
|
|
7208
|
+
* while some retained module still references it — that is step 2 below,
|
|
7209
|
+
* or the side effect of `import` in step 3 (a private graph row this
|
|
7210
|
+
* function cannot see).
|
|
7211
|
+
* 2. A `loadCache` record provides it, again without `import`.
|
|
7212
|
+
* 3. Otherwise `import(spec, '', {})`, with the spec as written — the seed
|
|
7213
|
+
* holds `react-dom/client` itself, not its stripped form. Resolving means a
|
|
7214
|
+
* seed word, whose lookup has no side effect; rejecting with the
|
|
7215
|
+
* "cannot resolve" message means absent; any other rejection is unknown.
|
|
7216
|
+
*
|
|
7217
|
+
* A kept-but-unmaterialized row is this function's own remaining blind spot:
|
|
7218
|
+
* asked about here, it still reaches step 3's `import` probe, which fetches
|
|
7219
|
+
* and runs it — the one side effect this module promises never to cause on
|
|
7220
|
+
* its own account. `refineAgainstModuleTable`'s page-removed set is the fix
|
|
7221
|
+
* for the one case a page can know about on its own — a name uninstalled
|
|
7222
|
+
* through THIS shop, on THIS page, since it loaded — by bypassing this oracle
|
|
7223
|
+
* for that name entirely rather than trusting a row that may only be kept. An
|
|
7224
|
+
* uninstall made from another tab, another window, or the CLI is not in that
|
|
7225
|
+
* set and stays unknown to this page until it reloads.
|
|
7226
|
+
*
|
|
7227
|
+
* One side-effect path remains: a registered page-local factory that is
|
|
7228
|
+
* neither a graph row nor yet materialized, which `import` materializes. That
|
|
7229
|
+
* is reachable only for a name the host could not resolve AND the page
|
|
7230
|
+
* registered without a row — the shell's own page-local modules are
|
|
7231
|
+
* node-resolvable, so the host never lists them. And what it does is what the
|
|
7232
|
+
* first real `require` of that name does anyway: run a factory this page
|
|
7233
|
+
* registered precisely so it would be materialized, once, memoized in
|
|
7234
|
+
* `loadCache`, fetching nothing. It moves that moment earlier; it runs nothing
|
|
7235
|
+
* the page did not already hold.
|
|
7236
|
+
*/
|
|
7237
|
+
function moduleTableOracle(service) {
|
|
7238
|
+
if (typeof service !== "object" || service === null) return null;
|
|
7239
|
+
let rows;
|
|
7240
|
+
let loadCache;
|
|
7241
|
+
let probe;
|
|
7242
|
+
try {
|
|
7243
|
+
const { manifest, loadCache: cache, import: load } = service;
|
|
7244
|
+
const modules = typeof manifest === "object" && manifest !== null ? manifest.modules : void 0;
|
|
7245
|
+
if (!Array.isArray(modules) || !(cache instanceof Map) || typeof load !== "function") return null;
|
|
7246
|
+
rows = /* @__PURE__ */ new Set();
|
|
7247
|
+
for (const row of modules) {
|
|
7248
|
+
const id = typeof row === "object" && row !== null ? row.id : void 0;
|
|
7249
|
+
if (typeof id === "string") rows.add(id);
|
|
7250
|
+
}
|
|
7251
|
+
loadCache = cache;
|
|
7252
|
+
probe = (spec) => Promise.resolve(load.call(service, spec, "", {}));
|
|
7253
|
+
} catch {
|
|
7254
|
+
return null;
|
|
7255
|
+
}
|
|
7256
|
+
return async (spec) => {
|
|
7257
|
+
const id = stripClientSuffix(spec);
|
|
7258
|
+
if (rows.has(id) || loadCache.has(id)) return true;
|
|
7259
|
+
try {
|
|
7260
|
+
await probe(spec);
|
|
7261
|
+
return true;
|
|
7262
|
+
} catch (error) {
|
|
7263
|
+
return error instanceof Error && error.message.startsWith(CANNOT_RESOLVE) ? false : null;
|
|
7264
|
+
}
|
|
7265
|
+
};
|
|
7266
|
+
}
|
|
7267
|
+
/**
|
|
7268
|
+
* The host's verdict as this page can stand behind it: refined against the
|
|
7269
|
+
* live table, or `{}` — no verdicts at all — when there is no usable table.
|
|
7270
|
+
*
|
|
7271
|
+
* Never the host's list unrefined. Measured, that list is majority-false on
|
|
7272
|
+
* this harness line (see the header), so showing it whenever the table cannot
|
|
7273
|
+
* be read would hand the reader a verdict already known to be unreliable;
|
|
7274
|
+
* silence is this project's documented degradation — an unavailable fact must
|
|
7275
|
+
* never read as an accusation. Never rejects, so a table in any state cannot
|
|
7276
|
+
* cost the reader the catalog.
|
|
7277
|
+
*
|
|
7278
|
+
* `removed` is the page-removed set (design 2026-09-01-harness-compatibility
|
|
7279
|
+
* section 9.1): package names this page has uninstalled since it loaded.
|
|
7280
|
+
* Every name in it is reported missing exactly
|
|
7281
|
+
* as the host said, WITHOUT ever reaching the table — not the rows check, not
|
|
7282
|
+
* `loadCache`, not `import` — because a row or a cache record can outlive a
|
|
7283
|
+
* restart-free uninstall (`moduleTableOracle`'s header), and the table's own
|
|
7284
|
+
* "provided" would be precisely the false clear this set exists to prevent.
|
|
7285
|
+
*/
|
|
7286
|
+
async function refineAgainstModuleTable(incompatible, service, removed = /* @__PURE__ */ new Set(), timeoutMs = MODULE_PROBE_TIMEOUT_MS) {
|
|
7287
|
+
const oracle = moduleTableOracle(service);
|
|
7288
|
+
if (oracle === null) return {};
|
|
7289
|
+
const withRemoved = async (spec) => removed.has(stripClientSuffix(spec)) ? false : oracle(spec);
|
|
7290
|
+
return refineIncompatible(incompatible, withRemoved, timeoutMs);
|
|
7291
|
+
}
|
|
7292
|
+
//#endregion
|
|
7293
|
+
//#region src/client/restart-monitor.ts
|
|
7294
|
+
/**
|
|
7295
|
+
* The restart handoff's browser half (design §8), held by the page rather
|
|
7296
|
+
* than by the tab that confirmed the restart.
|
|
7297
|
+
*
|
|
7298
|
+
* It lived in a RestartPanel effect until 2026-09-27, and a self-update
|
|
7299
|
+
* killed it. dsh's client HMR (`@deepseek-ai/dsh-client-hmr`, mounted by the
|
|
7300
|
+
* web bundle unconditionally) stat-polls every client bundle and hot-swaps
|
|
7301
|
+
* one whose file changed; a self-update rewrites this package's own
|
|
7302
|
+
* `lib/client.js`, so the swap disposes the tab's fiber and mounts a fresh
|
|
7303
|
+
* instance of a freshly imported module. The effect's cleanup then stopped
|
|
7304
|
+
* the probe, and once the restart finished nothing reloaded the page: it sat
|
|
7305
|
+
* on the old process's answers while dsh's socket quietly reconnected to the
|
|
7306
|
+
* new one. Measured on dsh 0.1.5-rc.3 updating 0.8.2 to 0.8.3; with the
|
|
7307
|
+
* `/plugins/events` channel blocked the same flow reloaded.
|
|
7308
|
+
*
|
|
7309
|
+
* So the state sits on `globalThis` under a registry symbol — the one thing a
|
|
7310
|
+
* swapped-in module instance shares with the one it replaced — and the probe
|
|
7311
|
+
* loop is plain timers owned by no fiber. Any tab reads the state, so the
|
|
7312
|
+
* replacement renders the restart under way instead of offering another.
|
|
7313
|
+
*/
|
|
7314
|
+
/** Registry symbol, so every module instance on the page finds the same store. */
|
|
7315
|
+
const STORE_KEY = Symbol.for("dsh-plugin-shop.restart-monitor");
|
|
7316
|
+
function monitorStore() {
|
|
7317
|
+
const page = globalThis;
|
|
7318
|
+
let store = page[STORE_KEY];
|
|
7319
|
+
if (store === void 0) {
|
|
7320
|
+
store = {
|
|
7321
|
+
state: null,
|
|
7322
|
+
listeners: /* @__PURE__ */ new Set(),
|
|
7323
|
+
stop: null
|
|
7324
|
+
};
|
|
7325
|
+
page[STORE_KEY] = store;
|
|
7326
|
+
}
|
|
7327
|
+
return store;
|
|
7328
|
+
}
|
|
7329
|
+
function settle(store, state) {
|
|
7330
|
+
store.state = state;
|
|
7331
|
+
for (const listener of [...store.listeners]) listener();
|
|
7332
|
+
}
|
|
7333
|
+
/** The page's restart, or null when it has committed none. The same object
|
|
7334
|
+
* until the state changes, as `useSyncExternalStore` requires. */
|
|
7335
|
+
function readRestartMonitor() {
|
|
7336
|
+
return monitorStore().state;
|
|
7337
|
+
}
|
|
7338
|
+
/** Be told when the page's restart changes state. */
|
|
7339
|
+
function subscribeRestartMonitor(listener) {
|
|
7340
|
+
const store = monitorStore();
|
|
7341
|
+
store.listeners.add(listener);
|
|
7342
|
+
return () => {
|
|
7343
|
+
store.listeners.delete(listener);
|
|
7344
|
+
};
|
|
7345
|
+
}
|
|
7346
|
+
/**
|
|
7347
|
+
* Watch the origin for the restarted server and reload into it: probe once
|
|
7348
|
+
* the grace period is over (the host exits within it, so an answer is the NEW
|
|
7349
|
+
* server), and reload only after it has KEPT answering for the stable window.
|
|
7350
|
+
* A boot that is about to fail can bind the port and answer before its plugin
|
|
7351
|
+
* tree is audited and it exits; reloading on that one answer left the reader
|
|
7352
|
+
* on a blank page instead of the notice naming the log (design
|
|
7353
|
+
* 2026-09-26-market-borrowings §3). `restartMonitorVerdict` decides; this
|
|
7354
|
+
* loop only probes. Probes never overlap: each is scheduled one poll interval
|
|
7355
|
+
* after the previous one settled.
|
|
7356
|
+
*
|
|
7357
|
+
* A restart already being watched is left alone — every offer on the page
|
|
7358
|
+
* renders this state, so a second press can only come from a panel that has
|
|
7359
|
+
* not re-rendered yet, and a second loop would probe the same origin twice.
|
|
7360
|
+
*/
|
|
7361
|
+
function startRestartMonitor(options) {
|
|
7362
|
+
const store = monitorStore();
|
|
7363
|
+
if (store.state?.kind === "restarting") return;
|
|
7364
|
+
store.stop?.();
|
|
7365
|
+
const { reload, logFile } = options;
|
|
7366
|
+
const started = Date.now();
|
|
7367
|
+
let stableSince = null;
|
|
7368
|
+
let stopped = false;
|
|
7369
|
+
let timer;
|
|
7370
|
+
const probe = async () => {
|
|
7371
|
+
if (Date.now() - started >= 3e3) {
|
|
7372
|
+
let up = false;
|
|
7373
|
+
try {
|
|
7374
|
+
up = (await fetch(globalThis.location.href, { cache: "no-store" })).ok;
|
|
7375
|
+
} catch {}
|
|
7376
|
+
if (stopped) return;
|
|
7377
|
+
const elapsed = Date.now() - started;
|
|
7378
|
+
stableSince = up ? stableSince ?? elapsed : null;
|
|
7379
|
+
const verdict = restartMonitorVerdict({
|
|
7380
|
+
elapsedMs: elapsed,
|
|
7381
|
+
stableSinceMs: stableSince
|
|
7382
|
+
});
|
|
7383
|
+
if (verdict === "reload") {
|
|
7384
|
+
store.stop = null;
|
|
7385
|
+
reload();
|
|
7386
|
+
return;
|
|
7387
|
+
}
|
|
7388
|
+
if (verdict === "failed") {
|
|
7389
|
+
store.stop = null;
|
|
7390
|
+
settle(store, logFile === void 0 ? { kind: "failed" } : {
|
|
7391
|
+
kind: "failed",
|
|
7392
|
+
logFile
|
|
7393
|
+
});
|
|
7394
|
+
return;
|
|
7395
|
+
}
|
|
7396
|
+
}
|
|
7397
|
+
timer = setTimeout(() => {
|
|
7398
|
+
probe();
|
|
7399
|
+
}, INSTALL_POLL_MS);
|
|
7400
|
+
};
|
|
7401
|
+
store.stop = () => {
|
|
7402
|
+
stopped = true;
|
|
7403
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
7404
|
+
};
|
|
7405
|
+
timer = setTimeout(() => {
|
|
7406
|
+
probe();
|
|
7407
|
+
}, INSTALL_POLL_MS);
|
|
7408
|
+
settle(store, logFile === void 0 ? { kind: "restarting" } : {
|
|
7409
|
+
kind: "restarting",
|
|
7410
|
+
logFile
|
|
7411
|
+
});
|
|
7412
|
+
}
|
|
7413
|
+
//#endregion
|
|
6901
7414
|
//#region src/client/useFlows.ts
|
|
6902
7415
|
/**
|
|
6903
7416
|
* One keyed registry of install-shaped flows: start, poll to terminal, reset.
|
|
@@ -7177,117 +7690,121 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7177
7690
|
};
|
|
7178
7691
|
}
|
|
7179
7692
|
//#endregion
|
|
7180
|
-
//#region \0dsh-plugin-shop-css:
|
|
7181
|
-
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.sebf017_updatableHeading {\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 packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.s9b5048_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. 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 on-disk 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/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\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";
|
|
7182
|
-
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"
|
|
7693
|
+
//#region \0dsh-plugin-shop-css:c4028823
|
|
7694
|
+
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.sc389ac_client.sc41672_config.s8eefa1_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.sc389ac_client.sfe36ad_spec.s8eefa1_ts` pins this. */\n\n.s688753_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 `.s9f28cb_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.sbc2e10_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.s7237a9_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.s601d42_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.sa26d1f_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.sceffd1_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.sceffd1_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.s6f1efd_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.s4db475_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.se9c3a9_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.s3cbb84_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.sceffd1_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 .sceffd1_skeletonCard span { animation: none; opacity: 0.75; }\n .sceffd1_skeletonCard::after { content: none; }\n}\n\n.se4623a_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.se4623a_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.se4623a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sbc2e10_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.sb9d854_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.s17e39d_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sff4480_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.sf22160_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.sf22160_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sf22160_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.s1abadc_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.s1abadc_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s1abadc_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s1abadc_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.se9af4e_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.se9af4e_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.se9af4e_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.s11efce_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s9f28cb_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.s9f28cb_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.scfe278_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.sfd2800_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.saa7982_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.saa7982_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 `.saa7982_categoryButton.s47a604_categoryButtonOn` — TWO classes — so it\n * matches `.saa7982_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s47a604_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.sc389ac_client.sfe36ad_spec.s8eefa1_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.saa7982_categoryButton.s47a604_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 .saa7982_categoryButton.s47a604_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.saa7982_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 `.saa7982_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.s6000bb_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: `.s6000bb_incompatibleFilter:hover .s21d1d6_switch` outranks `.sfaf305_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.s6000bb_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.s6000bb_incompatibleFilter:hover .s21d1d6_switch:not(.sfaf305_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 `.saa7982_categoryButton.s47a604_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.s6000bb_incompatibleFilter.s9fb743_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.s6000bb_incompatibleFilter .s21d1d6_switch:not(.sfaf305_switchOn) .s3e9566_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.s6000bb_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s3627e7_updatableHeading {\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.s879949_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.s7f1200_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 `.se4623a_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.s7f50e0_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.s7f50e0_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.s7f50e0_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s7f50e0_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s284f4c_catalogRefreshFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s894d35_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.s73be4c_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 .s73be4c_cards { animation: none; }\n}\n\n.s7ce0b5_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.se9f199_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s571a2d_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s7ce0b5_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 .s7ce0b5_card { transition: none; }\n .s7ce0b5_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.s3afd93_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. `.saa7982_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.s7ce0b5_card[data-category='tool'], .saa7982_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s7ce0b5_card[data-category='provider'], .saa7982_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s7ce0b5_card[data-category='ui'], .saa7982_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s7ce0b5_card[data-category='workflow'], .saa7982_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s7ce0b5_card[data-category='integration'], .saa7982_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s7ce0b5_card[data-category='theme'], .saa7982_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s7ce0b5_card[data-category='other'], .saa7982_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.sda4302_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.sf9a029_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.sf9a029_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.scde6f0_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.s5b12f5_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.sbfb66e_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.sbfb66e_cardActions > .sbf0f49_gate,\n.sbfb66e_cardActions > .se45c46_installPanel,\n.sbfb66e_cardActions > .s6a7b50_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.sa3f608_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.s908192_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sda8b40_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.sda8b40_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.sda8b40_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.sda8b40_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.s6eb4e5_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.s3e85ad_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sef80ea_chevron, .sc0d8c8_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.sc0d8c8_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .sef80ea_chevron, .sc0d8c8_chevronOpen { transition: none; }\n}\n\n.safe8c4_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sbd2517_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.s15225a_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.s273e04_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.s20dbc5_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.s927eb9_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/* The way out of dsh's own refusal: the lead-in, then the exemption command\n * on its own line. */\n.sa35ed7_remedy {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s2fcacb_remedyLead {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The exemption command, bound for a terminal unchanged: one click selects\n * all of it, and it wraps anywhere rather than widening the card. A border,\n * not a fill, marks the box: the bg layers are one white in the light theme. */\n.se87ea1_allowCommand {\n display: block;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-all;\n user-select: all;\n color: var(--dsw-alias-label-primary);\n}\n\n/* A refused install on a surface that does not state its blockers — the\n * outdated row — writes them out on their own full-width line under the\n * disabled button, the command that clears dsh's refusal included. */\n.sbccf5d_refusal {\n display: flex;\n flex-direction: column;\n gap: 6px;\n flex-basis: 100%;\n}\n\n.sca4074_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3ed23b_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sbd7053_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.sbd7053_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.se69ccd_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.s71ed47_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.s488064_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.s488064_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.s488064_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.s488064_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.s488064_detailRow a:hover {\n text-decoration: underline;\n}\n\n.s488064_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 packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.sbfb66e_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. 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.sd941dc_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The on-disk 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: `.sd941dc_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.sff4055_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 `.sd941dc_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 `.sd941dc_cardMeta`. There is no middot:\n * `.sd941dc_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.s74b808_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.s5a1b52_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.se45c46_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.s7bcc5e_installButton, .sb977c6_confirmButton, .s3cfd9a_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.s7bcc5e_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.s7bcc5e_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb977c6_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.sb977c6_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s3cfd9a_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.s3cfd9a_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s7bcc5e_installButton:focus-visible, .sb977c6_confirmButton:focus-visible, .s3cfd9a_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.sfaf388_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.s6a7b50_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.sb0c03c_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.sb0c03c_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.sb0c03c_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.se1e373_restartSelfButton {\n /* The version row's third state. Geometry matches .s1abadc_checkUpdateButton and\n * .sf22160_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.se1e373_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.s785196_restartButton,\n.s578d0b_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.s785196_restartButton:hover,\n.s578d0b_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s785196_restartButton:focus-visible,\n.s578d0b_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 * `.s6a7b50_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.s168719_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.sbf0f49_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.s62b4ea_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s8a8304_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s9a546e_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.secbb11_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s2fe11f_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s597ed4_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.s2fbc07_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.se1ef74_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.s878e49_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.scaf26f_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sb46af7_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se50b2b_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\n.s103f71_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s2ba5be_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s2283c2_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.se41dca_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.se41dca_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.s970b51_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 .s5b12f5_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.sfe5725_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: `.s6000bb_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 `.s21d1d6_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.s21d1d6_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.sfaf305_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.s3e9566_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.sfaf305_switchOn .s3e9566_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s21d1d6_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 .s21d1d6_switch, .s3e9566_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.s98de45_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.sfe5725_outdatedActions`: `.s168719_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.s6a7b50_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
7695
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"c4028823\"]")) {
|
|
7183
7696
|
const tag = document.createElement("style");
|
|
7184
|
-
tag.dataset.pluginCss = "
|
|
7697
|
+
tag.dataset.pluginCss = "c4028823";
|
|
7185
7698
|
tag.textContent = css;
|
|
7186
7699
|
document.head.appendChild(tag);
|
|
7187
7700
|
}
|
|
7188
|
-
var
|
|
7189
|
-
"client": "
|
|
7190
|
-
"config": "
|
|
7191
|
-
"ts": "
|
|
7192
|
-
"spec": "
|
|
7193
|
-
"panel": "
|
|
7194
|
-
"searchInput": "
|
|
7195
|
-
"toolbar": "
|
|
7196
|
-
"stateLine": "
|
|
7197
|
-
"srOnly": "
|
|
7198
|
-
"skeletonGrid": "
|
|
7199
|
-
"skeletonCard": "
|
|
7200
|
-
"skeletonName": "
|
|
7201
|
-
"skeletonSummary": "
|
|
7202
|
-
"skeletonSummaryShort": "
|
|
7203
|
-
"skeletonActions": "
|
|
7204
|
-
"actionButton": "
|
|
7205
|
-
"toolbarRight": "
|
|
7206
|
-
"versionBlock": "
|
|
7207
|
-
"versionText": "
|
|
7208
|
-
"updateSelfButton": "
|
|
7209
|
-
"checkUpdateButton": "
|
|
7210
|
-
"githubLink": "
|
|
7211
|
-
"selfUpdatePanel": "
|
|
7212
|
-
"staleBadge": "
|
|
7213
|
-
"categoryBar": "
|
|
7214
|
-
"categoryButton": "
|
|
7215
|
-
"categoryButtonOn": "
|
|
7216
|
-
"incompatibleFilter": "
|
|
7217
|
-
"switch": "
|
|
7218
|
-
"switchOn": "
|
|
7219
|
-
"incompatibleFilterOn": "
|
|
7220
|
-
"switchKnob": "
|
|
7221
|
-
"updatableHeading": "
|
|
7222
|
-
"catalogStatsRow": "
|
|
7223
|
-
"catalogStats": "
|
|
7224
|
-
"catalogRefreshButton": "
|
|
7225
|
-
"catalogRefreshFailed": "
|
|
7226
|
-
"emptyLine": "
|
|
7227
|
-
"cards": "
|
|
7228
|
-
"card": "
|
|
7229
|
-
"cardsSentry": "
|
|
7230
|
-
"showingLine": "
|
|
7231
|
-
"cardSpine": "
|
|
7232
|
-
"categoryBadge": "
|
|
7233
|
-
"entryHeader": "
|
|
7234
|
-
"name": "
|
|
7235
|
-
"badges": "
|
|
7236
|
-
"cardActions": "
|
|
7237
|
-
"gate": "
|
|
7238
|
-
"installPanel": "
|
|
7239
|
-
"installedActions": "
|
|
7240
|
-
"cardVersion": "
|
|
7241
|
-
"sourceBadge": "
|
|
7242
|
-
"tierBadge": "
|
|
7243
|
-
"incompatibleBadge": "
|
|
7244
|
-
"starsBadge": "
|
|
7245
|
-
"chevron": "
|
|
7246
|
-
"chevronOpen": "
|
|
7247
|
-
"body": "
|
|
7248
|
-
"summary": "
|
|
7249
|
-
"summaryZh": "
|
|
7250
|
-
"summaryExpanded": "
|
|
7251
|
-
"summaryZhExpanded": "
|
|
7252
|
-
"incompatibleDetail": "
|
|
7253
|
-
"
|
|
7254
|
-
"
|
|
7255
|
-
"
|
|
7256
|
-
"
|
|
7257
|
-
"
|
|
7258
|
-
"
|
|
7259
|
-
"
|
|
7260
|
-
"
|
|
7261
|
-
"
|
|
7262
|
-
"
|
|
7263
|
-
"
|
|
7264
|
-
"
|
|
7265
|
-
"
|
|
7266
|
-
"
|
|
7267
|
-
"
|
|
7268
|
-
"
|
|
7269
|
-
"
|
|
7270
|
-
"
|
|
7271
|
-
"
|
|
7272
|
-
"
|
|
7273
|
-
"
|
|
7274
|
-
"
|
|
7275
|
-
"
|
|
7276
|
-
"
|
|
7277
|
-
"
|
|
7278
|
-
"
|
|
7279
|
-
"
|
|
7280
|
-
"
|
|
7281
|
-
"
|
|
7282
|
-
"
|
|
7283
|
-
"
|
|
7284
|
-
"
|
|
7285
|
-
"
|
|
7286
|
-
"
|
|
7287
|
-
"
|
|
7288
|
-
"
|
|
7289
|
-
"
|
|
7290
|
-
"
|
|
7701
|
+
var _dsh_plugin_shop_css_c4028823_default = {
|
|
7702
|
+
"client": "sc389ac_client",
|
|
7703
|
+
"config": "sc41672_config",
|
|
7704
|
+
"ts": "s8eefa1_ts",
|
|
7705
|
+
"spec": "sfe36ad_spec",
|
|
7706
|
+
"panel": "s688753_panel",
|
|
7707
|
+
"searchInput": "s9f28cb_searchInput",
|
|
7708
|
+
"toolbar": "sbc2e10_toolbar",
|
|
7709
|
+
"stateLine": "s7237a9_stateLine",
|
|
7710
|
+
"srOnly": "s601d42_srOnly",
|
|
7711
|
+
"skeletonGrid": "sa26d1f_skeletonGrid",
|
|
7712
|
+
"skeletonCard": "sceffd1_skeletonCard",
|
|
7713
|
+
"skeletonName": "s6f1efd_skeletonName",
|
|
7714
|
+
"skeletonSummary": "s4db475_skeletonSummary",
|
|
7715
|
+
"skeletonSummaryShort": "se9c3a9_skeletonSummaryShort",
|
|
7716
|
+
"skeletonActions": "s3cbb84_skeletonActions",
|
|
7717
|
+
"actionButton": "se4623a_actionButton",
|
|
7718
|
+
"toolbarRight": "sb9d854_toolbarRight",
|
|
7719
|
+
"versionBlock": "s17e39d_versionBlock",
|
|
7720
|
+
"versionText": "sff4480_versionText",
|
|
7721
|
+
"updateSelfButton": "sf22160_updateSelfButton",
|
|
7722
|
+
"checkUpdateButton": "s1abadc_checkUpdateButton",
|
|
7723
|
+
"githubLink": "se9af4e_githubLink",
|
|
7724
|
+
"selfUpdatePanel": "s11efce_selfUpdatePanel",
|
|
7725
|
+
"staleBadge": "scfe278_staleBadge",
|
|
7726
|
+
"categoryBar": "sfd2800_categoryBar",
|
|
7727
|
+
"categoryButton": "saa7982_categoryButton",
|
|
7728
|
+
"categoryButtonOn": "s47a604_categoryButtonOn",
|
|
7729
|
+
"incompatibleFilter": "s6000bb_incompatibleFilter",
|
|
7730
|
+
"switch": "s21d1d6_switch",
|
|
7731
|
+
"switchOn": "sfaf305_switchOn",
|
|
7732
|
+
"incompatibleFilterOn": "s9fb743_incompatibleFilterOn",
|
|
7733
|
+
"switchKnob": "s3e9566_switchKnob",
|
|
7734
|
+
"updatableHeading": "s3627e7_updatableHeading",
|
|
7735
|
+
"catalogStatsRow": "s879949_catalogStatsRow",
|
|
7736
|
+
"catalogStats": "s7f1200_catalogStats",
|
|
7737
|
+
"catalogRefreshButton": "s7f50e0_catalogRefreshButton",
|
|
7738
|
+
"catalogRefreshFailed": "s284f4c_catalogRefreshFailed",
|
|
7739
|
+
"emptyLine": "s894d35_emptyLine",
|
|
7740
|
+
"cards": "s73be4c_cards",
|
|
7741
|
+
"card": "s7ce0b5_card",
|
|
7742
|
+
"cardsSentry": "se9f199_cardsSentry",
|
|
7743
|
+
"showingLine": "s571a2d_showingLine",
|
|
7744
|
+
"cardSpine": "s3afd93_cardSpine",
|
|
7745
|
+
"categoryBadge": "sda4302_categoryBadge",
|
|
7746
|
+
"entryHeader": "sf9a029_entryHeader",
|
|
7747
|
+
"name": "scde6f0_name",
|
|
7748
|
+
"badges": "s5b12f5_badges",
|
|
7749
|
+
"cardActions": "sbfb66e_cardActions",
|
|
7750
|
+
"gate": "sbf0f49_gate",
|
|
7751
|
+
"installPanel": "se45c46_installPanel",
|
|
7752
|
+
"installedActions": "s6a7b50_installedActions",
|
|
7753
|
+
"cardVersion": "sa3f608_cardVersion",
|
|
7754
|
+
"sourceBadge": "s908192_sourceBadge",
|
|
7755
|
+
"tierBadge": "sda8b40_tierBadge",
|
|
7756
|
+
"incompatibleBadge": "s6eb4e5_incompatibleBadge",
|
|
7757
|
+
"starsBadge": "s3e85ad_starsBadge",
|
|
7758
|
+
"chevron": "sef80ea_chevron",
|
|
7759
|
+
"chevronOpen": "sc0d8c8_chevronOpen",
|
|
7760
|
+
"body": "safe8c4_body",
|
|
7761
|
+
"summary": "sbd2517_summary",
|
|
7762
|
+
"summaryZh": "s15225a_summaryZh",
|
|
7763
|
+
"summaryExpanded": "s273e04_summaryExpanded",
|
|
7764
|
+
"summaryZhExpanded": "s20dbc5_summaryZhExpanded",
|
|
7765
|
+
"incompatibleDetail": "s927eb9_incompatibleDetail",
|
|
7766
|
+
"remedy": "sa35ed7_remedy",
|
|
7767
|
+
"remedyLead": "s2fcacb_remedyLead",
|
|
7768
|
+
"allowCommand": "se87ea1_allowCommand",
|
|
7769
|
+
"refusal": "sbccf5d_refusal",
|
|
7770
|
+
"capabilitiesBlock": "sca4074_capabilitiesBlock",
|
|
7771
|
+
"capabilitiesNote": "s3ed23b_capabilitiesNote",
|
|
7772
|
+
"capabilities": "sbd7053_capabilities",
|
|
7773
|
+
"detail": "se69ccd_detail",
|
|
7774
|
+
"detailRows": "s71ed47_detailRows",
|
|
7775
|
+
"detailRow": "s488064_detailRow",
|
|
7776
|
+
"cardMeta": "sd941dc_cardMeta",
|
|
7777
|
+
"size": "sff4055_size",
|
|
7778
|
+
"author": "s74b808_author",
|
|
7779
|
+
"reviewedLine": "s5a1b52_reviewedLine",
|
|
7780
|
+
"installButton": "s7bcc5e_installButton",
|
|
7781
|
+
"confirmButton": "sb977c6_confirmButton",
|
|
7782
|
+
"cancelButton": "s3cfd9a_cancelButton",
|
|
7783
|
+
"installedLabel": "sfaf388_installedLabel",
|
|
7784
|
+
"uninstallButton": "sb0c03c_uninstallButton",
|
|
7785
|
+
"restartSelfButton": "se1e373_restartSelfButton",
|
|
7786
|
+
"restartButton": "s785196_restartButton",
|
|
7787
|
+
"reloadButton": "s578d0b_reloadButton",
|
|
7788
|
+
"reloadPanel": "s168719_reloadPanel",
|
|
7789
|
+
"gateTitle": "s62b4ea_gateTitle",
|
|
7790
|
+
"gateBody": "s8a8304_gateBody",
|
|
7791
|
+
"gateWarning": "s9a546e_gateWarning",
|
|
7792
|
+
"gateActions": "secbb11_gateActions",
|
|
7793
|
+
"installing": "s2fe11f_installing",
|
|
7794
|
+
"log": "s597ed4_log",
|
|
7795
|
+
"logLine": "s2fbc07_logLine",
|
|
7796
|
+
"notice": "se1ef74_notice",
|
|
7797
|
+
"failedHeading": "s878e49_failedHeading",
|
|
7798
|
+
"failedDetail": "scaf26f_failedDetail",
|
|
7799
|
+
"rejectedCode": "sb46af7_rejectedCode",
|
|
7800
|
+
"rejectedDetail": "se50b2b_rejectedDetail",
|
|
7801
|
+
"outdatedSection": "s103f71_outdatedSection",
|
|
7802
|
+
"outdatedList": "s2ba5be_outdatedList",
|
|
7803
|
+
"outdatedRow": "s2283c2_outdatedRow",
|
|
7804
|
+
"outdatedInfo": "se41dca_outdatedInfo",
|
|
7805
|
+
"outdatedVersions": "s970b51_outdatedVersions",
|
|
7806
|
+
"outdatedActions": "sfe5725_outdatedActions",
|
|
7807
|
+
"switchWrap": "s98de45_switchWrap"
|
|
7291
7808
|
};
|
|
7292
7809
|
//#endregion
|
|
7293
7810
|
//#region src/client/ShopTab.tsx
|
|
@@ -7298,13 +7815,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7298
7815
|
* §11.3.4): no render path here may ever use dangerouslySetInnerHTML. */
|
|
7299
7816
|
/** The project's home on GitHub, linked from the toolbar. */
|
|
7300
7817
|
const SHOP_REPO_URL = "https://github.com/LivXue/dsh-plugin-shop";
|
|
7818
|
+
/** The class of every root the tab renders, the crash fallback's included:
|
|
7819
|
+
* the panel, plus `notranslate`, which with `translate="no"` keeps a browser's
|
|
7820
|
+
* page translation out of the text nodes React owns. Translation rewrites
|
|
7821
|
+
* those nodes, and React's next commit throws on the ones it no longer finds
|
|
7822
|
+
* (design 2026-09-26-market-borrowings §4). The tab renders no portals, so its
|
|
7823
|
+
* root is its whole surface. */
|
|
7824
|
+
const PANEL_ROOT = `${_dsh_plugin_shop_css_c4028823_default.panel} notranslate`;
|
|
7301
7825
|
function ChevronIcon({ open }) {
|
|
7302
7826
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
7303
7827
|
viewBox: "0 0 16 16",
|
|
7304
7828
|
width: "14",
|
|
7305
7829
|
height: "14",
|
|
7306
7830
|
"aria-hidden": "true",
|
|
7307
|
-
className: open ?
|
|
7831
|
+
className: open ? _dsh_plugin_shop_css_c4028823_default.chevronOpen : _dsh_plugin_shop_css_c4028823_default.chevron,
|
|
7308
7832
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
7309
7833
|
fill: "currentColor",
|
|
7310
7834
|
d: "M3.5 6 8 10.5 12.5 6l1 1L8 12.5 2.5 7l1-1Z"
|
|
@@ -7317,9 +7841,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7317
7841
|
* install controls. An installed plugin's card carries its installed row:
|
|
7318
7842
|
* current → the non-interactive installed label, behind → the update button;
|
|
7319
7843
|
* uninstalled → the install button. */
|
|
7320
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
|
|
7844
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, harness, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
|
|
7321
7845
|
const [open, setOpen] = (0, react.useState)(false);
|
|
7322
|
-
const blockers = blockersOf(missing, nameTakenBy, t);
|
|
7846
|
+
const blockers = stateBlockers(blockersOf(missing, harness, nameTakenBy), t);
|
|
7323
7847
|
const detailId = (0, react.useId)();
|
|
7324
7848
|
const summary = entry.catalog?.summary;
|
|
7325
7849
|
const npmUrl = npmPageUrl(entry);
|
|
@@ -7334,32 +7858,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7334
7858
|
subdir: entry.subdir
|
|
7335
7859
|
};
|
|
7336
7860
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7337
|
-
className:
|
|
7861
|
+
className: _dsh_plugin_shop_css_c4028823_default.card,
|
|
7338
7862
|
"data-shop-entry": entry.name,
|
|
7339
7863
|
"data-category": category,
|
|
7340
7864
|
children: [
|
|
7341
7865
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7342
|
-
className:
|
|
7866
|
+
className: _dsh_plugin_shop_css_c4028823_default.cardSpine,
|
|
7343
7867
|
"aria-hidden": "true"
|
|
7344
7868
|
}),
|
|
7345
7869
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
7346
7870
|
type: "button",
|
|
7347
|
-
className:
|
|
7871
|
+
className: _dsh_plugin_shop_css_c4028823_default.entryHeader,
|
|
7348
7872
|
"aria-expanded": open,
|
|
7349
7873
|
"aria-controls": detailId,
|
|
7350
7874
|
onClick: () => setOpen((current) => !current),
|
|
7351
7875
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7352
|
-
className:
|
|
7876
|
+
className: _dsh_plugin_shop_css_c4028823_default.name,
|
|
7353
7877
|
children: entry.name
|
|
7354
7878
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7355
|
-
className:
|
|
7879
|
+
className: _dsh_plugin_shop_css_c4028823_default.badges,
|
|
7356
7880
|
children: [
|
|
7357
7881
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7358
|
-
className:
|
|
7882
|
+
className: _dsh_plugin_shop_css_c4028823_default.categoryBadge,
|
|
7359
7883
|
children: t(categoryKey(entry))
|
|
7360
7884
|
}),
|
|
7361
7885
|
entry.source === "npm" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7362
|
-
className:
|
|
7886
|
+
className: _dsh_plugin_shop_css_c4028823_default.sourceBadge,
|
|
7363
7887
|
"data-shop-source-npm": true,
|
|
7364
7888
|
role: "img",
|
|
7365
7889
|
"aria-label": t("npmSource"),
|
|
@@ -7376,7 +7900,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7376
7900
|
})
|
|
7377
7901
|
}),
|
|
7378
7902
|
hasGithubHome(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7379
|
-
className:
|
|
7903
|
+
className: _dsh_plugin_shop_css_c4028823_default.sourceBadge,
|
|
7380
7904
|
"data-shop-source-github": true,
|
|
7381
7905
|
role: "img",
|
|
7382
7906
|
"aria-label": t("githubSource"),
|
|
@@ -7390,17 +7914,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7390
7914
|
})
|
|
7391
7915
|
}),
|
|
7392
7916
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7393
|
-
className:
|
|
7917
|
+
className: _dsh_plugin_shop_css_c4028823_default.cardVersion,
|
|
7394
7918
|
"data-card-version": true,
|
|
7395
7919
|
children: ["v", displayVersion(entry)]
|
|
7396
7920
|
}),
|
|
7397
7921
|
entry.tier !== "community" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7398
|
-
className:
|
|
7922
|
+
className: _dsh_plugin_shop_css_c4028823_default.tierBadge,
|
|
7399
7923
|
"data-tier": entry.tier,
|
|
7400
7924
|
children: t(tierKey(entry.tier))
|
|
7401
7925
|
}),
|
|
7402
7926
|
stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7403
|
-
className:
|
|
7927
|
+
className: _dsh_plugin_shop_css_c4028823_default.starsBadge,
|
|
7404
7928
|
role: "img",
|
|
7405
7929
|
"aria-label": t("stars", { count: stars }),
|
|
7406
7930
|
children: ["★ ", formatStars(stars)]
|
|
@@ -7410,39 +7934,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7410
7934
|
})]
|
|
7411
7935
|
}),
|
|
7412
7936
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7413
|
-
className:
|
|
7937
|
+
className: _dsh_plugin_shop_css_c4028823_default.body,
|
|
7414
7938
|
children: [
|
|
7415
7939
|
summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7416
|
-
className: open ? `${
|
|
7940
|
+
className: open ? `${_dsh_plugin_shop_css_c4028823_default.summary} ${_dsh_plugin_shop_css_c4028823_default.summaryExpanded}` : _dsh_plugin_shop_css_c4028823_default.summary,
|
|
7417
7941
|
children: summary.en
|
|
7418
7942
|
}),
|
|
7419
7943
|
summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7420
|
-
className: open ? `${
|
|
7944
|
+
className: open ? `${_dsh_plugin_shop_css_c4028823_default.summaryZh} ${_dsh_plugin_shop_css_c4028823_default.summaryZhExpanded}` : _dsh_plugin_shop_css_c4028823_default.summaryZh,
|
|
7421
7945
|
children: summary.zh
|
|
7422
7946
|
}),
|
|
7423
|
-
blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
7424
|
-
|
|
7425
|
-
|
|
7426
|
-
|
|
7947
|
+
blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
7948
|
+
blocker,
|
|
7949
|
+
className: _dsh_plugin_shop_css_c4028823_default.incompatibleDetail,
|
|
7950
|
+
surface: "detail"
|
|
7427
7951
|
}, blocker.kind)),
|
|
7428
7952
|
open && entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7429
|
-
className:
|
|
7953
|
+
className: _dsh_plugin_shop_css_c4028823_default.capabilitiesBlock,
|
|
7430
7954
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7431
|
-
className:
|
|
7955
|
+
className: _dsh_plugin_shop_css_c4028823_default.capabilitiesNote,
|
|
7432
7956
|
children: t("capabilitiesNote")
|
|
7433
7957
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
7434
|
-
className:
|
|
7958
|
+
className: _dsh_plugin_shop_css_c4028823_default.capabilities,
|
|
7435
7959
|
children: entry.catalog.capabilities.map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capability }, capability))
|
|
7436
7960
|
})]
|
|
7437
7961
|
}),
|
|
7438
7962
|
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
7439
7963
|
id: detailId,
|
|
7440
|
-
className:
|
|
7964
|
+
className: _dsh_plugin_shop_css_c4028823_default.detail,
|
|
7441
7965
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
7442
|
-
className:
|
|
7966
|
+
className: _dsh_plugin_shop_css_c4028823_default.detailRows,
|
|
7443
7967
|
children: [
|
|
7444
7968
|
npmUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7445
|
-
className:
|
|
7969
|
+
className: _dsh_plugin_shop_css_c4028823_default.detailRow,
|
|
7446
7970
|
"data-shop-npm": true,
|
|
7447
7971
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("npmPage") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
7448
7972
|
href: npmUrl,
|
|
@@ -7452,7 +7976,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7452
7976
|
}) })]
|
|
7453
7977
|
}),
|
|
7454
7978
|
entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7455
|
-
className:
|
|
7979
|
+
className: _dsh_plugin_shop_css_c4028823_default.detailRow,
|
|
7456
7980
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("repository") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /^https?:\/\//.test(entry.repository) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
7457
7981
|
href: entry.repository,
|
|
7458
7982
|
target: "_blank",
|
|
@@ -7461,12 +7985,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7461
7985
|
}) : entry.repository })]
|
|
7462
7986
|
}),
|
|
7463
7987
|
entry.license !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7464
|
-
className:
|
|
7988
|
+
className: _dsh_plugin_shop_css_c4028823_default.detailRow,
|
|
7465
7989
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("license") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: isCustomLicense(entry.license) ? t("customLicense") : entry.license })]
|
|
7466
7990
|
})
|
|
7467
7991
|
]
|
|
7468
7992
|
}), entry.tier === "verified-stale" && entry.review !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7469
|
-
className:
|
|
7993
|
+
className: _dsh_plugin_shop_css_c4028823_default.reviewedLine,
|
|
7470
7994
|
children: entry.review.reviewedVersion !== void 0 ? t("reviewedVersionLine", {
|
|
7471
7995
|
reviewed: entry.review.reviewedVersion,
|
|
7472
7996
|
current: entry.version
|
|
@@ -7479,14 +8003,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7479
8003
|
]
|
|
7480
8004
|
}),
|
|
7481
8005
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7482
|
-
className:
|
|
8006
|
+
className: _dsh_plugin_shop_css_c4028823_default.cardActions,
|
|
7483
8007
|
"data-shop-actions": true,
|
|
7484
8008
|
children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!inInstalledView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
7485
8009
|
target: installTarget,
|
|
7486
8010
|
tier: entry.tier,
|
|
7487
|
-
missing,
|
|
7488
8011
|
blockers,
|
|
7489
|
-
|
|
8012
|
+
blockersStated: true,
|
|
7490
8013
|
flow,
|
|
7491
8014
|
t,
|
|
7492
8015
|
restart,
|
|
@@ -7504,16 +8027,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7504
8027
|
target: installTarget,
|
|
7505
8028
|
tier: entry.tier,
|
|
7506
8029
|
variant: installed.outdated ? "update" : "install",
|
|
7507
|
-
missing,
|
|
7508
8030
|
blockers,
|
|
7509
|
-
|
|
8031
|
+
blockersStated: true,
|
|
7510
8032
|
flow,
|
|
7511
8033
|
t,
|
|
7512
8034
|
restart,
|
|
7513
8035
|
restartBlocked,
|
|
7514
8036
|
reload
|
|
7515
8037
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7516
|
-
className:
|
|
8038
|
+
className: _dsh_plugin_shop_css_c4028823_default.installedLabel,
|
|
7517
8039
|
"data-shop-installed": true,
|
|
7518
8040
|
children: t("installed")
|
|
7519
8041
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
|
|
@@ -7535,16 +8057,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7535
8057
|
flow: uninstallFlow
|
|
7536
8058
|
})
|
|
7537
8059
|
] }), (size !== void 0 || author !== null) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
7538
|
-
className:
|
|
8060
|
+
className: _dsh_plugin_shop_css_c4028823_default.cardMeta,
|
|
7539
8061
|
children: [size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7540
|
-
className:
|
|
8062
|
+
className: _dsh_plugin_shop_css_c4028823_default.size,
|
|
7541
8063
|
"data-shop-size": true,
|
|
7542
8064
|
role: "img",
|
|
7543
8065
|
"aria-label": t("sizeLabel", { size }),
|
|
7544
8066
|
title: t("sizeLabel", { size }),
|
|
7545
8067
|
children: size
|
|
7546
8068
|
}), author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7547
|
-
className:
|
|
8069
|
+
className: _dsh_plugin_shop_css_c4028823_default.author,
|
|
7548
8070
|
"data-shop-author": true,
|
|
7549
8071
|
children: t("authorLine", { author })
|
|
7550
8072
|
})]
|
|
@@ -7554,41 +8076,95 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7554
8076
|
});
|
|
7555
8077
|
});
|
|
7556
8078
|
/**
|
|
7557
|
-
* Everything standing in this entry's way,
|
|
8079
|
+
* Everything standing in this entry's way, localized ONCE per surface: the
|
|
8080
|
+
* detail paragraphs and the badge summarizing them read this one list, so
|
|
8081
|
+
* they can never be built from different inputs.
|
|
7558
8082
|
*
|
|
7559
|
-
* The
|
|
7560
|
-
*
|
|
7561
|
-
*
|
|
8083
|
+
* The host sent facts — module names, the declared range and the running
|
|
8084
|
+
* version, the declared and the running profile — and the sentences are this
|
|
8085
|
+
* dictionary's, in the reader's own language (§4: no copy crosses the RPC).
|
|
7562
8086
|
*/
|
|
7563
|
-
function
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
kind
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
8087
|
+
function stateBlockers(blockers, t) {
|
|
8088
|
+
return blockers.map((blocker) => {
|
|
8089
|
+
const text = blockerText(blocker, t);
|
|
8090
|
+
return blocker.kind === "harness-peers" && blocker.allowCommand !== null ? {
|
|
8091
|
+
kind: blocker.kind,
|
|
8092
|
+
text,
|
|
8093
|
+
remedy: {
|
|
8094
|
+
lead: t("harnessPeersRemedy"),
|
|
8095
|
+
command: blocker.allowCommand
|
|
8096
|
+
}
|
|
8097
|
+
} : {
|
|
8098
|
+
kind: blocker.kind,
|
|
8099
|
+
text
|
|
8100
|
+
};
|
|
7572
8101
|
});
|
|
7573
|
-
|
|
8102
|
+
}
|
|
8103
|
+
/**
|
|
8104
|
+
* One blocker as a surface prints it: its sentence in the surface's own
|
|
8105
|
+
* paragraph style, and after it, for a refusal the reader can clear, the
|
|
8106
|
+
* lead-in and the command on a line of its own — selectable as a whole, since
|
|
8107
|
+
* it goes to a terminal unchanged. `surface` names the attribute a locator
|
|
8108
|
+
* reads: the card's detail line, or the warning the gate and the outdated
|
|
8109
|
+
* row's refusal print.
|
|
8110
|
+
*/
|
|
8111
|
+
function BlockerLine({ blocker, className, surface }) {
|
|
8112
|
+
const attribute = surface === "detail" ? { "data-shop-incompatible-detail": blocker.kind } : { "data-shop-incompatible-warning": blocker.kind };
|
|
8113
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8114
|
+
className,
|
|
8115
|
+
...attribute,
|
|
8116
|
+
children: blocker.text
|
|
8117
|
+
}), blocker.remedy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8118
|
+
className: _dsh_plugin_shop_css_c4028823_default.remedy,
|
|
8119
|
+
"data-shop-remedy": blocker.kind,
|
|
8120
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8121
|
+
className: _dsh_plugin_shop_css_c4028823_default.remedyLead,
|
|
8122
|
+
children: blocker.remedy.lead
|
|
8123
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
8124
|
+
className: _dsh_plugin_shop_css_c4028823_default.allowCommand,
|
|
8125
|
+
"data-shop-allow-command": true,
|
|
8126
|
+
children: blocker.remedy.command
|
|
8127
|
+
})]
|
|
8128
|
+
})] });
|
|
8129
|
+
}
|
|
8130
|
+
/** One blocker's sentence. Exhaustive with no default, so a kind added
|
|
8131
|
+
* to `Blocker` is a type error here rather than a line that renders nothing. */
|
|
8132
|
+
function blockerText(blocker, t) {
|
|
8133
|
+
switch (blocker.kind) {
|
|
8134
|
+
case "name-taken": return t("nameTakenDetail", { holder: blocker.holder });
|
|
8135
|
+
case "harness-peers": return t("harnessPeersDetail", {
|
|
8136
|
+
running: blocker.running,
|
|
8137
|
+
peers: refusedPeersText(blocker.refused)
|
|
8138
|
+
});
|
|
8139
|
+
case "missing-peers": return t("incompatibleDetail", { modules: blocker.modules.join(", ") });
|
|
8140
|
+
case "harness-range": return t("harnessRangeDetail", {
|
|
8141
|
+
range: blocker.range,
|
|
8142
|
+
running: blocker.running
|
|
8143
|
+
});
|
|
8144
|
+
case "harness-profile": return t("harnessProfileDetail", {
|
|
8145
|
+
declared: blocker.declared.join(", "),
|
|
8146
|
+
running: blocker.running
|
|
8147
|
+
});
|
|
8148
|
+
}
|
|
7574
8149
|
}
|
|
7575
8150
|
/** The harness-compatibility verdict, rendered beside the control it
|
|
7576
8151
|
* qualifies. It answers "what happens if I press this", not "what is this",
|
|
7577
8152
|
* so it belongs to the action row and not among the identity badges in the
|
|
7578
8153
|
* header — where it also sat inside a <button>, competing with that button's
|
|
7579
|
-
* own hit area for the tooltip. Renders nothing when
|
|
7580
|
-
*
|
|
8154
|
+
* own hit area for the tooltip. Renders nothing when nothing stands in the
|
|
8155
|
+
* way. */
|
|
7581
8156
|
function BlockerBadge({ blockers, t }) {
|
|
7582
|
-
|
|
8157
|
+
const lead = blockers[0];
|
|
8158
|
+
const word = blockerBadgeKey(blockers);
|
|
8159
|
+
if (lead === void 0 || word === null) return null;
|
|
7583
8160
|
const detail = blockers.map((blocker) => blocker.text).join("\n");
|
|
7584
|
-
const taken = blockers.some((blocker) => blocker.kind === "name-taken");
|
|
7585
8161
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
7586
|
-
className:
|
|
7587
|
-
"data-shop-blocker":
|
|
8162
|
+
className: _dsh_plugin_shop_css_c4028823_default.incompatibleBadge,
|
|
8163
|
+
"data-shop-blocker": lead.kind,
|
|
7588
8164
|
role: "img",
|
|
7589
8165
|
"aria-label": detail,
|
|
7590
8166
|
title: detail,
|
|
7591
|
-
children: t(
|
|
8167
|
+
children: t(word)
|
|
7592
8168
|
});
|
|
7593
8169
|
}
|
|
7594
8170
|
/** One entry's install flow: the button, the §9.3 acknowledgement gate for
|
|
@@ -7596,34 +8172,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7596
8172
|
* failure detail, rejection detail — driven by `useInstallFlows`. Shared by the
|
|
7597
8173
|
* catalog cards (`variant: 'install'`) and the outdated rows' update button
|
|
7598
8174
|
* (`variant: 'update'`, which drives the same install flow for `name@latest`). */
|
|
7599
|
-
function InstallPanel({ target, tier,
|
|
8175
|
+
function InstallPanel({ target, tier, blockers, blockersStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
|
|
7600
8176
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
7601
8177
|
const { view, start } = flow;
|
|
7602
8178
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7603
|
-
className:
|
|
8179
|
+
className: _dsh_plugin_shop_css_c4028823_default.installPanel,
|
|
7604
8180
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7605
|
-
className:
|
|
8181
|
+
className: _dsh_plugin_shop_css_c4028823_default.installing,
|
|
7606
8182
|
children: t(installPhaseKey(view.phase))
|
|
7607
8183
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7608
|
-
className:
|
|
8184
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
7609
8185
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7610
|
-
className:
|
|
8186
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
7611
8187
|
children: line
|
|
7612
8188
|
}, index))
|
|
7613
8189
|
})]
|
|
7614
8190
|
});
|
|
7615
8191
|
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7616
|
-
className:
|
|
8192
|
+
className: _dsh_plugin_shop_css_c4028823_default.installedActions,
|
|
7617
8193
|
children: [
|
|
7618
8194
|
view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7619
|
-
className:
|
|
8195
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
7620
8196
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7621
|
-
className:
|
|
8197
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
7622
8198
|
children: line
|
|
7623
8199
|
}, index))
|
|
7624
8200
|
}),
|
|
7625
8201
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7626
|
-
className:
|
|
8202
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
7627
8203
|
"data-shop-restart-notice": true,
|
|
7628
8204
|
children: t(activationNoticeKey(view.activation, view.restartReason))
|
|
7629
8205
|
}),
|
|
@@ -7638,56 +8214,56 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7638
8214
|
]
|
|
7639
8215
|
});
|
|
7640
8216
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7641
|
-
className:
|
|
8217
|
+
className: _dsh_plugin_shop_css_c4028823_default.installPanel,
|
|
7642
8218
|
children: [
|
|
7643
8219
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7644
|
-
className:
|
|
8220
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedHeading,
|
|
7645
8221
|
children: t("installFailed")
|
|
7646
8222
|
}),
|
|
7647
8223
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7648
|
-
className:
|
|
8224
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
7649
8225
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7650
|
-
className:
|
|
8226
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
7651
8227
|
children: line
|
|
7652
8228
|
}, index))
|
|
7653
8229
|
}),
|
|
7654
8230
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7655
|
-
className:
|
|
8231
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
7656
8232
|
children: view.detail === "" ? t("installTransportFailed") : view.detail
|
|
7657
8233
|
})
|
|
7658
8234
|
]
|
|
7659
8235
|
});
|
|
7660
8236
|
if (view.kind === "rejected") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7661
|
-
className:
|
|
8237
|
+
className: _dsh_plugin_shop_css_c4028823_default.installPanel,
|
|
7662
8238
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7663
|
-
className:
|
|
8239
|
+
className: _dsh_plugin_shop_css_c4028823_default.rejectedCode,
|
|
7664
8240
|
children: t(rejectionCodeKey(view.code))
|
|
7665
8241
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7666
|
-
className:
|
|
8242
|
+
className: _dsh_plugin_shop_css_c4028823_default.rejectedDetail,
|
|
7667
8243
|
children: view.detail
|
|
7668
8244
|
})]
|
|
7669
8245
|
});
|
|
7670
8246
|
if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7671
|
-
className:
|
|
8247
|
+
className: _dsh_plugin_shop_css_c4028823_default.gate,
|
|
7672
8248
|
children: [
|
|
7673
8249
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7674
|
-
className:
|
|
8250
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateTitle,
|
|
7675
8251
|
children: t("acknowledgementTitle")
|
|
7676
8252
|
}),
|
|
7677
8253
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7678
|
-
className:
|
|
8254
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateBody,
|
|
7679
8255
|
children: t("acknowledgementBody")
|
|
7680
8256
|
}),
|
|
7681
|
-
!
|
|
7682
|
-
|
|
7683
|
-
|
|
7684
|
-
|
|
8257
|
+
!blockersStated && blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
8258
|
+
blocker,
|
|
8259
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateWarning,
|
|
8260
|
+
surface: "warning"
|
|
7685
8261
|
}, blocker.kind)),
|
|
7686
8262
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7687
|
-
className:
|
|
8263
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateActions,
|
|
7688
8264
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7689
8265
|
type: "button",
|
|
7690
|
-
className:
|
|
8266
|
+
className: _dsh_plugin_shop_css_c4028823_default.confirmButton,
|
|
7691
8267
|
"data-shop-confirm": true,
|
|
7692
8268
|
onClick: () => {
|
|
7693
8269
|
setGateOpen(false);
|
|
@@ -7699,7 +8275,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7699
8275
|
children: t("confirm")
|
|
7700
8276
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7701
8277
|
type: "button",
|
|
7702
|
-
className:
|
|
8278
|
+
className: _dsh_plugin_shop_css_c4028823_default.cancelButton,
|
|
7703
8279
|
onClick: () => setGateOpen(false),
|
|
7704
8280
|
children: t("cancel")
|
|
7705
8281
|
})]
|
|
@@ -7707,25 +8283,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7707
8283
|
]
|
|
7708
8284
|
});
|
|
7709
8285
|
const update = variant === "update";
|
|
7710
|
-
const refused = blockers
|
|
7711
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
7712
|
-
|
|
7713
|
-
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
|
|
7718
|
-
|
|
7719
|
-
|
|
7720
|
-
|
|
7721
|
-
|
|
7722
|
-
|
|
7723
|
-
|
|
7724
|
-
|
|
7725
|
-
|
|
7726
|
-
|
|
7727
|
-
|
|
7728
|
-
|
|
8286
|
+
const refused = refusesInstall(blockers);
|
|
8287
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
8288
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8289
|
+
type: "button",
|
|
8290
|
+
className: _dsh_plugin_shop_css_c4028823_default.installButton,
|
|
8291
|
+
disabled: refused,
|
|
8292
|
+
...refused ? { "data-shop-refused": true } : {},
|
|
8293
|
+
...update ? { "data-shop-update": true } : { "data-shop-install": true },
|
|
8294
|
+
onClick: () => {
|
|
8295
|
+
if (tier === "verified") start({
|
|
8296
|
+
...target,
|
|
8297
|
+
acknowledged: void 0
|
|
8298
|
+
});
|
|
8299
|
+
else setGateOpen(true);
|
|
8300
|
+
},
|
|
8301
|
+
children: t(update ? "update" : "install")
|
|
8302
|
+
}),
|
|
8303
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
|
|
8304
|
+
blockers,
|
|
8305
|
+
t
|
|
8306
|
+
}),
|
|
8307
|
+
refused && !blockersStated && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8308
|
+
className: _dsh_plugin_shop_css_c4028823_default.refusal,
|
|
8309
|
+
"data-shop-refusal": true,
|
|
8310
|
+
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
8311
|
+
blocker,
|
|
8312
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateWarning,
|
|
8313
|
+
surface: "warning"
|
|
8314
|
+
}, blocker.kind))
|
|
8315
|
+
})
|
|
8316
|
+
] });
|
|
7729
8317
|
}
|
|
7730
8318
|
/** One installed entry's uninstall flow: remove from the profile through the
|
|
7731
8319
|
* same executor records and poll loop as installs. Uninstalling revokes
|
|
@@ -7736,23 +8324,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7736
8324
|
function UninstallPanel({ name, t, restart, restartBlocked, reload, flow }) {
|
|
7737
8325
|
const { view, start, reset } = flow;
|
|
7738
8326
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7739
|
-
className:
|
|
8327
|
+
className: _dsh_plugin_shop_css_c4028823_default.installPanel,
|
|
7740
8328
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7741
|
-
className:
|
|
8329
|
+
className: _dsh_plugin_shop_css_c4028823_default.installing,
|
|
7742
8330
|
children: t("uninstalling")
|
|
7743
8331
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7744
|
-
className:
|
|
8332
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
7745
8333
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7746
|
-
className:
|
|
8334
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
7747
8335
|
children: line
|
|
7748
8336
|
}, index))
|
|
7749
8337
|
})]
|
|
7750
8338
|
});
|
|
7751
8339
|
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7752
|
-
className:
|
|
8340
|
+
className: _dsh_plugin_shop_css_c4028823_default.installedActions,
|
|
7753
8341
|
children: [
|
|
7754
8342
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7755
|
-
className:
|
|
8343
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
7756
8344
|
"data-shop-uninstall-done": true,
|
|
7757
8345
|
children: t(uninstallActivationNoticeKey(view.activation))
|
|
7758
8346
|
}),
|
|
@@ -7766,7 +8354,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7766
8354
|
}),
|
|
7767
8355
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7768
8356
|
type: "button",
|
|
7769
|
-
className:
|
|
8357
|
+
className: _dsh_plugin_shop_css_c4028823_default.cancelButton,
|
|
7770
8358
|
"data-shop-uninstall-dismiss": true,
|
|
7771
8359
|
onClick: reset,
|
|
7772
8360
|
children: t("dismiss")
|
|
@@ -7774,26 +8362,26 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7774
8362
|
]
|
|
7775
8363
|
});
|
|
7776
8364
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7777
|
-
className:
|
|
8365
|
+
className: _dsh_plugin_shop_css_c4028823_default.installPanel,
|
|
7778
8366
|
children: [
|
|
7779
8367
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7780
|
-
className:
|
|
8368
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedHeading,
|
|
7781
8369
|
children: t("uninstallFailed")
|
|
7782
8370
|
}),
|
|
7783
8371
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7784
|
-
className:
|
|
8372
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
7785
8373
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
7786
|
-
className:
|
|
8374
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
7787
8375
|
children: line
|
|
7788
8376
|
}, index))
|
|
7789
8377
|
}),
|
|
7790
8378
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7791
|
-
className:
|
|
8379
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
7792
8380
|
children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
|
|
7793
8381
|
}),
|
|
7794
8382
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7795
8383
|
type: "button",
|
|
7796
|
-
className:
|
|
8384
|
+
className: _dsh_plugin_shop_css_c4028823_default.uninstallButton,
|
|
7797
8385
|
"data-shop-uninstall-retry": true,
|
|
7798
8386
|
onClick: () => void start({ name }),
|
|
7799
8387
|
children: t("retry")
|
|
@@ -7803,99 +8391,89 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7803
8391
|
if (view.kind !== "idle") return null;
|
|
7804
8392
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7805
8393
|
type: "button",
|
|
7806
|
-
className:
|
|
8394
|
+
className: _dsh_plugin_shop_css_c4028823_default.uninstallButton,
|
|
7807
8395
|
"data-shop-uninstall": true,
|
|
7808
8396
|
onClick: () => void start({ name }),
|
|
7809
8397
|
children: t("uninstall")
|
|
7810
8398
|
});
|
|
7811
8399
|
}
|
|
8400
|
+
/** The page's restart, re-rendering whenever it changes state. Shared by
|
|
8401
|
+
* every restart offer and by the version row, in this tab and in whatever
|
|
8402
|
+
* tab an HMR swap mounts in its place. */
|
|
8403
|
+
function useRestartMonitor() {
|
|
8404
|
+
return (0, react.useSyncExternalStore)(subscribeRestartMonitor, readRestartMonitor);
|
|
8405
|
+
}
|
|
7812
8406
|
/** The §8 restart flow (amendment 2026-08-27): after an install, update, or
|
|
7813
8407
|
* uninstall reports done, this panel offers a restart of dsh. The
|
|
7814
8408
|
* confirmation gate states the cost — the page disconnects and in-flight
|
|
7815
8409
|
* conversations/tasks are interrupted — and on confirm the restart RPC
|
|
7816
8410
|
* commits the two-phase handoff: the host exits, a helper re-runs dsh, and
|
|
7817
|
-
*
|
|
7818
|
-
*
|
|
7819
|
-
* published detail; a server that
|
|
7820
|
-
* command. */
|
|
7821
|
-
function RestartPanel({ t, restart, gate }) {
|
|
8411
|
+
* the page's restart monitor (restart-monitor.ts) polls the origin after a
|
|
8412
|
+
* grace period, refreshing the page once the NEW server keeps answering.
|
|
8413
|
+
* A refused restart renders the host's published detail; a server that
|
|
8414
|
+
* never comes back names the manual command. */
|
|
8415
|
+
function RestartPanel({ t, restart, reload, gate }) {
|
|
7822
8416
|
const [ownGateOpen, setOwnGateOpen] = (0, react.useState)(false);
|
|
7823
8417
|
const gateOpen = gate === void 0 ? ownGateOpen : gate.open;
|
|
7824
8418
|
const setGateOpen = (open) => {
|
|
7825
8419
|
if (gate === void 0) setOwnGateOpen(open);
|
|
7826
8420
|
else if (!open) gate.close();
|
|
7827
8421
|
};
|
|
7828
|
-
const [
|
|
8422
|
+
const [refusal, setRefusal] = (0, react.useState)(null);
|
|
8423
|
+
const monitor = useRestartMonitor();
|
|
7829
8424
|
const onConfirm = async () => {
|
|
7830
8425
|
setGateOpen(false);
|
|
7831
8426
|
try {
|
|
7832
8427
|
const result = await restart();
|
|
7833
8428
|
if (!result.ok) {
|
|
7834
|
-
|
|
7835
|
-
kind: "failed",
|
|
7836
|
-
detail: result.detail
|
|
7837
|
-
});
|
|
8429
|
+
setRefusal(result.detail);
|
|
7838
8430
|
return;
|
|
7839
8431
|
}
|
|
7840
|
-
|
|
7841
|
-
|
|
7842
|
-
|
|
7843
|
-
kind: "failed",
|
|
7844
|
-
detail: t("restartTransportFailed")
|
|
8432
|
+
startRestartMonitor({
|
|
8433
|
+
reload,
|
|
8434
|
+
...typeof result.logFile === "string" ? { logFile: result.logFile } : {}
|
|
7845
8435
|
});
|
|
8436
|
+
} catch {
|
|
8437
|
+
setRefusal(t("restartTransportFailed"));
|
|
7846
8438
|
}
|
|
7847
8439
|
};
|
|
7848
|
-
(0,
|
|
7849
|
-
|
|
7850
|
-
const started = Date.now();
|
|
7851
|
-
const timer = setInterval(() => {
|
|
7852
|
-
const elapsed = Date.now() - started;
|
|
7853
|
-
if (elapsed < 3e3) return;
|
|
7854
|
-
fetch(window.location.href, { cache: "no-store" }).then(() => {
|
|
7855
|
-
window.location.reload();
|
|
7856
|
-
}, () => {});
|
|
7857
|
-
if (elapsed > 3e4) {
|
|
7858
|
-
clearInterval(timer);
|
|
7859
|
-
setState({
|
|
7860
|
-
kind: "failed",
|
|
7861
|
-
detail: t("restartFailedNotice")
|
|
7862
|
-
});
|
|
7863
|
-
}
|
|
7864
|
-
}, INSTALL_POLL_MS);
|
|
7865
|
-
return () => clearInterval(timer);
|
|
7866
|
-
}, [state, t]);
|
|
7867
|
-
if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7868
|
-
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
8440
|
+
if (monitor?.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8441
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
7869
8442
|
"data-shop-restarting": true,
|
|
7870
8443
|
children: t("restarting")
|
|
7871
8444
|
});
|
|
7872
|
-
if (
|
|
7873
|
-
className:
|
|
8445
|
+
if (monitor?.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8446
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
8447
|
+
"data-shop-restart-error": true,
|
|
8448
|
+
children: monitor.logFile === void 0 ? t("restartFailedNotice") : t("restartFailedLogNotice", { log: monitor.logFile })
|
|
8449
|
+
});
|
|
8450
|
+
if (refusal !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8451
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
7874
8452
|
"data-shop-restart-error": true,
|
|
7875
|
-
children:
|
|
8453
|
+
children: refusal
|
|
7876
8454
|
});
|
|
7877
8455
|
if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7878
|
-
className:
|
|
8456
|
+
className: _dsh_plugin_shop_css_c4028823_default.gate,
|
|
7879
8457
|
children: [
|
|
7880
8458
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7881
|
-
className:
|
|
8459
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateTitle,
|
|
7882
8460
|
children: t("restartTitle")
|
|
7883
8461
|
}),
|
|
7884
8462
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7885
|
-
className:
|
|
8463
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateBody,
|
|
7886
8464
|
children: t("restartBody")
|
|
7887
8465
|
}),
|
|
7888
8466
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7889
|
-
className:
|
|
8467
|
+
className: _dsh_plugin_shop_css_c4028823_default.gateActions,
|
|
7890
8468
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7891
8469
|
type: "button",
|
|
7892
|
-
className:
|
|
8470
|
+
className: _dsh_plugin_shop_css_c4028823_default.confirmButton,
|
|
7893
8471
|
"data-shop-restart-confirm": true,
|
|
7894
8472
|
onClick: () => void onConfirm(),
|
|
7895
8473
|
children: t("restartConfirm")
|
|
7896
8474
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7897
8475
|
type: "button",
|
|
7898
|
-
className:
|
|
8476
|
+
className: _dsh_plugin_shop_css_c4028823_default.cancelButton,
|
|
7899
8477
|
onClick: () => setGateOpen(false),
|
|
7900
8478
|
children: t("cancel")
|
|
7901
8479
|
})]
|
|
@@ -7905,7 +8483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7905
8483
|
if (gate !== void 0) return null;
|
|
7906
8484
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7907
8485
|
type: "button",
|
|
7908
|
-
className:
|
|
8486
|
+
className: _dsh_plugin_shop_css_c4028823_default.restartButton,
|
|
7909
8487
|
"data-shop-restart": true,
|
|
7910
8488
|
onClick: () => setGateOpen(true),
|
|
7911
8489
|
children: t("restart")
|
|
@@ -7933,9 +8511,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7933
8511
|
if (activation !== "restart") return null;
|
|
7934
8512
|
return restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
7935
8513
|
t,
|
|
7936
|
-
restart
|
|
8514
|
+
restart,
|
|
8515
|
+
reload
|
|
7937
8516
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7938
|
-
className:
|
|
8517
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
7939
8518
|
"data-shop-restart-disabled": true,
|
|
7940
8519
|
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
7941
8520
|
});
|
|
@@ -7946,14 +8525,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7946
8525
|
* would help without knowing that now is a good time. */
|
|
7947
8526
|
function ReloadPanel({ t, reload, where }) {
|
|
7948
8527
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7949
|
-
className:
|
|
8528
|
+
className: _dsh_plugin_shop_css_c4028823_default.reloadPanel,
|
|
7950
8529
|
"data-shop-reload": where,
|
|
7951
8530
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
7952
|
-
className:
|
|
8531
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
7953
8532
|
children: t("reloadNote")
|
|
7954
8533
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
7955
8534
|
type: "button",
|
|
7956
|
-
className:
|
|
8535
|
+
className: _dsh_plugin_shop_css_c4028823_default.reloadButton,
|
|
7957
8536
|
onClick: reload,
|
|
7958
8537
|
children: t("reload")
|
|
7959
8538
|
})]
|
|
@@ -7995,7 +8574,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7995
8574
|
}
|
|
7996
8575
|
};
|
|
7997
8576
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7998
|
-
className:
|
|
8577
|
+
className: _dsh_plugin_shop_css_c4028823_default.switchWrap,
|
|
7999
8578
|
"data-shop-enabled-switch": row.name,
|
|
8000
8579
|
children: [
|
|
8001
8580
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -8004,13 +8583,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8004
8583
|
"aria-checked": enabled,
|
|
8005
8584
|
"aria-label": t("enabledSwitch"),
|
|
8006
8585
|
"data-shop-toggle": true,
|
|
8007
|
-
className: `${
|
|
8586
|
+
className: `${_dsh_plugin_shop_css_c4028823_default.switch} ${enabled ? _dsh_plugin_shop_css_c4028823_default.switchOn : ""}`,
|
|
8008
8587
|
onClick: () => void onToggle(),
|
|
8009
8588
|
disabled: toggle.kind === "saving",
|
|
8010
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
8589
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.switchKnob })
|
|
8011
8590
|
}),
|
|
8012
8591
|
toggle.kind === "saved" && !needsReload && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8013
|
-
className:
|
|
8592
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
8014
8593
|
"data-shop-hot-apply": true,
|
|
8015
8594
|
children: t("hotApplyNote")
|
|
8016
8595
|
}),
|
|
@@ -8020,7 +8599,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8020
8599
|
where: "toggle"
|
|
8021
8600
|
}),
|
|
8022
8601
|
toggle.kind === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8023
|
-
className:
|
|
8602
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
8024
8603
|
"data-shop-toggle-error": true,
|
|
8025
8604
|
children: toggle.detail
|
|
8026
8605
|
})
|
|
@@ -8030,21 +8609,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8030
8609
|
/** One outdated install row (§7.3): the name, the installed and latest
|
|
8031
8610
|
* versions, the hot enable/disable switch, and the update button (the
|
|
8032
8611
|
* install flow for `name@latest`, reusing `InstallPanel`). */
|
|
8033
|
-
function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
8612
|
+
function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
8034
8613
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8035
|
-
className:
|
|
8614
|
+
className: _dsh_plugin_shop_css_c4028823_default.outdatedRow,
|
|
8036
8615
|
"data-shop-outdated-entry": row.name,
|
|
8037
8616
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8038
|
-
className:
|
|
8617
|
+
className: _dsh_plugin_shop_css_c4028823_default.outdatedInfo,
|
|
8039
8618
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8040
|
-
className:
|
|
8619
|
+
className: _dsh_plugin_shop_css_c4028823_default.name,
|
|
8041
8620
|
children: row.name
|
|
8042
8621
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8043
|
-
className:
|
|
8622
|
+
className: _dsh_plugin_shop_css_c4028823_default.outdatedVersions,
|
|
8044
8623
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: row.source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
|
|
8045
8624
|
})]
|
|
8046
8625
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8047
|
-
className:
|
|
8626
|
+
className: _dsh_plugin_shop_css_c4028823_default.outdatedActions,
|
|
8048
8627
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
8049
8628
|
row,
|
|
8050
8629
|
t,
|
|
@@ -8060,8 +8639,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8060
8639
|
},
|
|
8061
8640
|
tier,
|
|
8062
8641
|
variant: "update",
|
|
8063
|
-
missing,
|
|
8064
|
-
blockers: blockersOf(missing, void 0, t),
|
|
8642
|
+
blockers: stateBlockers(blockersOf(missing, harness, void 0), t),
|
|
8065
8643
|
flow: flowFor(identityKey(row)),
|
|
8066
8644
|
t,
|
|
8067
8645
|
restart,
|
|
@@ -8088,29 +8666,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8088
8666
|
* tier for the update gate is looked up from the catalog by install IDENTITY,
|
|
8089
8667
|
* never by name (community → acknowledgement); an entry absent from the
|
|
8090
8668
|
* catalog defaults to the community gate (the safer read). */
|
|
8091
|
-
function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
8669
|
+
function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
8092
8670
|
if (state.kind === "loading") return null;
|
|
8093
8671
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8094
|
-
className:
|
|
8672
|
+
className: _dsh_plugin_shop_css_c4028823_default.stateLine,
|
|
8095
8673
|
"data-shop-outdated-error": true,
|
|
8096
8674
|
children: t("installedError")
|
|
8097
8675
|
});
|
|
8098
8676
|
const outdated = state.entries.filter((entry) => entry.outdated);
|
|
8099
8677
|
if (outdated.length === 0) return null;
|
|
8100
8678
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
8101
|
-
className:
|
|
8679
|
+
className: _dsh_plugin_shop_css_c4028823_default.outdatedSection,
|
|
8102
8680
|
"data-shop-outdated": true,
|
|
8103
8681
|
"aria-labelledby": UPDATABLE_HEADING_ID,
|
|
8104
8682
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
8105
|
-
className:
|
|
8683
|
+
className: _dsh_plugin_shop_css_c4028823_default.updatableHeading,
|
|
8106
8684
|
id: UPDATABLE_HEADING_ID,
|
|
8107
8685
|
children: t("updatableSection")
|
|
8108
8686
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
8109
|
-
className:
|
|
8687
|
+
className: _dsh_plugin_shop_css_c4028823_default.outdatedList,
|
|
8110
8688
|
children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
|
|
8111
8689
|
row,
|
|
8112
8690
|
tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
|
|
8113
8691
|
missing: missingByKey.get(identityKey(row)) ?? [],
|
|
8692
|
+
harness: harnessVerdictOf(harnessVerdicts, identityKey(row)),
|
|
8114
8693
|
t,
|
|
8115
8694
|
setEnabled,
|
|
8116
8695
|
flowFor,
|
|
@@ -8121,10 +8700,71 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8121
8700
|
})]
|
|
8122
8701
|
});
|
|
8123
8702
|
}
|
|
8124
|
-
/**
|
|
8125
|
-
*
|
|
8703
|
+
/**
|
|
8704
|
+
* The shop's own error boundary (design 2026-09-26-market-borrowings §4).
|
|
8705
|
+
*
|
|
8706
|
+
* The harness catches a crashing slot entry as well, but renders an empty
|
|
8707
|
+
* `<div data-slot-error>` that stays until the page reloads: a blank tab, with
|
|
8708
|
+
* no message and no way back. This one says what happened and offers a Retry
|
|
8709
|
+
* that remounts the tab from scratch. Its fallback carries no `data-shop-tab`,
|
|
8710
|
+
* so a crashed tab cannot satisfy anything waiting for a working one. A class,
|
|
8711
|
+
* because React offers error boundaries in no other form.
|
|
8712
|
+
*/
|
|
8713
|
+
var ShopErrorBoundary = class extends react.Component {
|
|
8714
|
+
state = { error: null };
|
|
8715
|
+
static getDerivedStateFromError(error) {
|
|
8716
|
+
return { error: error instanceof Error ? error : new Error(String(error)) };
|
|
8717
|
+
}
|
|
8718
|
+
componentDidCatch(error, info) {
|
|
8719
|
+
console.error("dsh-plugin-shop: the shop tab crashed", error, info.componentStack);
|
|
8720
|
+
}
|
|
8721
|
+
render() {
|
|
8722
|
+
const { error } = this.state;
|
|
8723
|
+
if (error === null) return this.props.children;
|
|
8724
|
+
const say = (key, fallback) => {
|
|
8725
|
+
try {
|
|
8726
|
+
return this.props.t(key);
|
|
8727
|
+
} catch {
|
|
8728
|
+
return fallback;
|
|
8729
|
+
}
|
|
8730
|
+
};
|
|
8731
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8732
|
+
className: PANEL_ROOT,
|
|
8733
|
+
translate: "no",
|
|
8734
|
+
"data-shop-crashed": true,
|
|
8735
|
+
role: "alert",
|
|
8736
|
+
children: [
|
|
8737
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8738
|
+
className: _dsh_plugin_shop_css_c4028823_default.stateLine,
|
|
8739
|
+
children: say("tabCrashed", en.tabCrashed)
|
|
8740
|
+
}),
|
|
8741
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8742
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
8743
|
+
children: error.message
|
|
8744
|
+
}),
|
|
8745
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8746
|
+
type: "button",
|
|
8747
|
+
className: _dsh_plugin_shop_css_c4028823_default.actionButton,
|
|
8748
|
+
onClick: () => this.setState({ error: null }),
|
|
8749
|
+
children: say("retry", en.retry)
|
|
8750
|
+
})
|
|
8751
|
+
]
|
|
8752
|
+
});
|
|
8753
|
+
}
|
|
8754
|
+
};
|
|
8755
|
+
/** The tab as the settings slot registers it (`index.ts`): the body inside the
|
|
8756
|
+
* shop's own error boundary. Exported under the name the body used to carry,
|
|
8757
|
+
* so nothing can register the tab without the boundary. */
|
|
8126
8758
|
function ShopTab(props) {
|
|
8127
|
-
|
|
8759
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopErrorBoundary, {
|
|
8760
|
+
t: props.t,
|
|
8761
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopTabBody, { ...props })
|
|
8762
|
+
});
|
|
8763
|
+
}
|
|
8764
|
+
/** The shop tab body: browse, search, refresh, and render one card per
|
|
8765
|
+
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
8766
|
+
function ShopTabBody(props) {
|
|
8767
|
+
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, updateStart, reload: injectedReload } = props;
|
|
8128
8768
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
8129
8769
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
8130
8770
|
/** The install gate's own input, or undefined while it is unknown — loading,
|
|
@@ -8139,7 +8779,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8139
8779
|
}), [injectedReload]);
|
|
8140
8780
|
const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
|
|
8141
8781
|
const selfUpdate = useUpdateSelf(updateStart, installStatus);
|
|
8782
|
+
const restartMonitor = useRestartMonitor();
|
|
8783
|
+
const hostPending = selfVersion !== null && selfVersion.pendingVersion !== null;
|
|
8784
|
+
const selfPending = selfUpdate.view.kind === "done" || selfUpdate.view.kind === "idle" && hostPending;
|
|
8142
8785
|
const [request, setRequest] = (0, react.useState)({ kind: "initial" });
|
|
8786
|
+
const [versionReload, setVersionReload] = (0, react.useState)(0);
|
|
8143
8787
|
const [mutations, setMutations] = (0, react.useState)(0);
|
|
8144
8788
|
const noteMutation = (0, react.useCallback)(() => {
|
|
8145
8789
|
setMutations((current) => current + 1);
|
|
@@ -8148,12 +8792,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8148
8792
|
const flows = useInstallFlows(install, installStatus, (0, react.useCallback)((key, outcome) => {
|
|
8149
8793
|
if (outcome === "done") uninstallResetRef.current(key);
|
|
8150
8794
|
noteMutation();
|
|
8795
|
+
if (outcome === "done") setRequest({ kind: "reverdict" });
|
|
8151
8796
|
}, [noteMutation]));
|
|
8152
8797
|
const uninstallFlows = useUninstallFlows(uninstall, installStatus, (0, react.useCallback)((key, outcome) => {
|
|
8153
8798
|
if (outcome !== "done") return;
|
|
8154
8799
|
flows.flowFor(key).reset();
|
|
8155
8800
|
noteMutation();
|
|
8156
|
-
|
|
8801
|
+
const name = catalogState.kind === "ready" ? catalogState.result.plugins.find((entry) => entryKey(entry) === key)?.name : void 0;
|
|
8802
|
+
if (name !== void 0) noteUninstalled?.(name);
|
|
8803
|
+
setRequest({ kind: "reverdict" });
|
|
8804
|
+
}, [
|
|
8805
|
+
flows,
|
|
8806
|
+
noteMutation,
|
|
8807
|
+
catalogState,
|
|
8808
|
+
noteUninstalled
|
|
8809
|
+
]));
|
|
8157
8810
|
uninstallResetRef.current = uninstallFlows.resetFlow;
|
|
8158
8811
|
const [reloading, setReloading] = (0, react.useState)(false);
|
|
8159
8812
|
const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
|
|
@@ -8164,21 +8817,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8164
8817
|
const [visibleCount, setVisibleCount] = (0, react.useState)(48);
|
|
8165
8818
|
const sentinelRef = (0, react.useRef)(null);
|
|
8166
8819
|
const filteredLenRef = (0, react.useRef)(0);
|
|
8820
|
+
const pendingRefreshRef = (0, react.useRef)(null);
|
|
8167
8821
|
(0, react.useEffect)(() => {
|
|
8168
8822
|
let cancelled = false;
|
|
8169
|
-
if (request.kind !== "refresh") setCatalogState((current) => current.kind === "ready" ? current : { kind: "loading" });
|
|
8823
|
+
if (request.kind !== "refresh" && request.kind !== "reverdict") setCatalogState((current) => current.kind === "ready" ? current : { kind: "loading" });
|
|
8824
|
+
if (request.kind === "refresh") pendingRefreshRef.current = request;
|
|
8170
8825
|
const load = async () => {
|
|
8171
8826
|
try {
|
|
8172
|
-
const result = await catalog(request.kind === "refresh" ? { refresh: true } : void 0);
|
|
8827
|
+
const result = await catalog(request.kind === "refresh" ? { refresh: true } : request.kind === "reverdict" ? { reverdict: true } : void 0);
|
|
8173
8828
|
if (!cancelled) setCatalogState({
|
|
8174
8829
|
kind: "ready",
|
|
8175
8830
|
result
|
|
8176
8831
|
});
|
|
8177
8832
|
} catch {
|
|
8178
|
-
if (cancelled)
|
|
8833
|
+
if (cancelled) {
|
|
8834
|
+
if (request.kind === "refresh" && pendingRefreshRef.current === request) setReloadFailed(true);
|
|
8835
|
+
return;
|
|
8836
|
+
}
|
|
8179
8837
|
setCatalogState((current) => current.kind === "ready" ? current : { kind: "error" });
|
|
8180
|
-
setReloadFailed(true);
|
|
8838
|
+
if (request.kind !== "reverdict") setReloadFailed(true);
|
|
8181
8839
|
} finally {
|
|
8840
|
+
if (pendingRefreshRef.current === request) pendingRefreshRef.current = null;
|
|
8182
8841
|
if (!cancelled) setReloading(false);
|
|
8183
8842
|
}
|
|
8184
8843
|
};
|
|
@@ -8199,7 +8858,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8199
8858
|
return () => {
|
|
8200
8859
|
cancelled = true;
|
|
8201
8860
|
};
|
|
8202
|
-
}, [version,
|
|
8861
|
+
}, [version, versionReload]);
|
|
8203
8862
|
const [checkState, setCheckState] = (0, react.useState)("idle");
|
|
8204
8863
|
const upToDateTimer = (0, react.useRef)(null);
|
|
8205
8864
|
(0, react.useEffect)(() => () => {
|
|
@@ -8272,7 +8931,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8272
8931
|
}
|
|
8273
8932
|
return map;
|
|
8274
8933
|
}, [catalogState, specs]);
|
|
8275
|
-
const
|
|
8934
|
+
const harnessVerdicts = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.incompatibleHarness : void 0, [catalogState]);
|
|
8935
|
+
const badgedIncompatible = (0, react.useCallback)((key) => readsIncompatible(blockersOf(missingByKey.get(key) ?? [], harnessVerdictOf(harnessVerdicts, key), nameTakenByKey.get(key))), [
|
|
8936
|
+
missingByKey,
|
|
8937
|
+
harnessVerdicts,
|
|
8938
|
+
nameTakenByKey
|
|
8939
|
+
]);
|
|
8276
8940
|
const matched = (0, react.useMemo)(() => {
|
|
8277
8941
|
const q = query.trim().toLowerCase();
|
|
8278
8942
|
return (category === "installed" ? sortedCatalog : browsable).filter((entry) => {
|
|
@@ -8332,50 +8996,56 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8332
8996
|
return map;
|
|
8333
8997
|
}, [catalogState]);
|
|
8334
8998
|
if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8335
|
-
className:
|
|
8999
|
+
className: PANEL_ROOT,
|
|
9000
|
+
translate: "no",
|
|
8336
9001
|
"data-shop-tab": true,
|
|
8337
9002
|
"aria-busy": "true",
|
|
8338
9003
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8339
|
-
className:
|
|
9004
|
+
className: _dsh_plugin_shop_css_c4028823_default.srOnly,
|
|
8340
9005
|
children: t("loading")
|
|
8341
9006
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8342
|
-
className:
|
|
9007
|
+
className: _dsh_plugin_shop_css_c4028823_default.skeletonGrid,
|
|
8343
9008
|
"data-shop-skeleton": true,
|
|
8344
9009
|
"aria-hidden": "true",
|
|
8345
9010
|
children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8346
|
-
className:
|
|
9011
|
+
className: _dsh_plugin_shop_css_c4028823_default.skeletonCard,
|
|
8347
9012
|
children: [
|
|
8348
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
8349
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
8350
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
8351
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
9013
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonName }),
|
|
9014
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonSummary }),
|
|
9015
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonSummaryShort }),
|
|
9016
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.skeletonActions })
|
|
8352
9017
|
]
|
|
8353
9018
|
}, i))
|
|
8354
9019
|
})]
|
|
8355
9020
|
});
|
|
8356
9021
|
if (catalogState.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8357
|
-
className:
|
|
9022
|
+
className: PANEL_ROOT,
|
|
9023
|
+
translate: "no",
|
|
8358
9024
|
"data-shop-tab": true,
|
|
8359
9025
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8360
|
-
className:
|
|
9026
|
+
className: _dsh_plugin_shop_css_c4028823_default.stateLine,
|
|
8361
9027
|
children: t("error")
|
|
8362
9028
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8363
9029
|
type: "button",
|
|
8364
|
-
className:
|
|
8365
|
-
onClick: () =>
|
|
9030
|
+
className: _dsh_plugin_shop_css_c4028823_default.actionButton,
|
|
9031
|
+
onClick: () => {
|
|
9032
|
+
setRequest({ kind: "retry" });
|
|
9033
|
+
setVersionReload((current) => current + 1);
|
|
9034
|
+
},
|
|
8366
9035
|
children: t("retry")
|
|
8367
9036
|
})]
|
|
8368
9037
|
});
|
|
8369
9038
|
const { result } = catalogState;
|
|
8370
9039
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8371
|
-
className:
|
|
9040
|
+
className: PANEL_ROOT,
|
|
9041
|
+
translate: "no",
|
|
8372
9042
|
"data-shop-tab": true,
|
|
8373
9043
|
children: [
|
|
8374
9044
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8375
|
-
className:
|
|
9045
|
+
className: _dsh_plugin_shop_css_c4028823_default.toolbar,
|
|
8376
9046
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
8377
9047
|
type: "search",
|
|
8378
|
-
className:
|
|
9048
|
+
className: _dsh_plugin_shop_css_c4028823_default.searchInput,
|
|
8379
9049
|
"aria-label": t("search"),
|
|
8380
9050
|
placeholder: t("search"),
|
|
8381
9051
|
value: query,
|
|
@@ -8384,27 +9054,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8384
9054
|
setVisibleCount(48);
|
|
8385
9055
|
}
|
|
8386
9056
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8387
|
-
className:
|
|
9057
|
+
className: _dsh_plugin_shop_css_c4028823_default.toolbarRight,
|
|
8388
9058
|
children: [
|
|
8389
9059
|
result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8390
|
-
className:
|
|
9060
|
+
className: _dsh_plugin_shop_css_c4028823_default.staleBadge,
|
|
8391
9061
|
children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
|
|
8392
9062
|
}),
|
|
8393
9063
|
selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8394
|
-
className:
|
|
9064
|
+
className: _dsh_plugin_shop_css_c4028823_default.versionBlock,
|
|
8395
9065
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
8396
|
-
className:
|
|
9066
|
+
className: _dsh_plugin_shop_css_c4028823_default.versionText,
|
|
8397
9067
|
"data-shop-version": true,
|
|
8398
9068
|
children: ["v", selfVersion.installed]
|
|
8399
|
-
}),
|
|
9069
|
+
}), restartMonitor !== null ? null : selfPending && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8400
9070
|
type: "button",
|
|
8401
|
-
className:
|
|
9071
|
+
className: _dsh_plugin_shop_css_c4028823_default.restartSelfButton,
|
|
8402
9072
|
"data-shop-restart": true,
|
|
8403
9073
|
onClick: () => setSelfRestartGate(true),
|
|
8404
9074
|
children: t("restart")
|
|
8405
|
-
}) : selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9075
|
+
}) : !selfPending && selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8406
9076
|
type: "button",
|
|
8407
|
-
className:
|
|
9077
|
+
className: _dsh_plugin_shop_css_c4028823_default.updateSelfButton,
|
|
8408
9078
|
"data-shop-update-self": true,
|
|
8409
9079
|
onClick: () => {
|
|
8410
9080
|
if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
|
|
@@ -8412,7 +9082,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8412
9082
|
children: t("update")
|
|
8413
9083
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8414
9084
|
type: "button",
|
|
8415
|
-
className:
|
|
9085
|
+
className: _dsh_plugin_shop_css_c4028823_default.checkUpdateButton,
|
|
8416
9086
|
"data-shop-check-update": true,
|
|
8417
9087
|
title: t("checkUpdateTitle"),
|
|
8418
9088
|
disabled: checkState !== "idle",
|
|
@@ -8421,7 +9091,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8421
9091
|
})]
|
|
8422
9092
|
}),
|
|
8423
9093
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
8424
|
-
className:
|
|
9094
|
+
className: _dsh_plugin_shop_css_c4028823_default.githubLink,
|
|
8425
9095
|
href: SHOP_REPO_URL,
|
|
8426
9096
|
target: "_blank",
|
|
8427
9097
|
rel: "noopener noreferrer",
|
|
@@ -8440,39 +9110,40 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8440
9110
|
})]
|
|
8441
9111
|
}),
|
|
8442
9112
|
selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8443
|
-
className:
|
|
9113
|
+
className: _dsh_plugin_shop_css_c4028823_default.selfUpdatePanel,
|
|
8444
9114
|
"data-shop-self-updating": true,
|
|
8445
9115
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8446
|
-
className:
|
|
9116
|
+
className: _dsh_plugin_shop_css_c4028823_default.installing,
|
|
8447
9117
|
children: t(installPhaseKey(selfUpdate.view.phase))
|
|
8448
9118
|
}), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8449
|
-
className:
|
|
9119
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
8450
9120
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8451
|
-
className:
|
|
9121
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
8452
9122
|
children: line
|
|
8453
9123
|
}, index))
|
|
8454
9124
|
})]
|
|
8455
9125
|
}),
|
|
8456
|
-
|
|
8457
|
-
className:
|
|
9126
|
+
selfPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9127
|
+
className: _dsh_plugin_shop_css_c4028823_default.selfUpdatePanel,
|
|
8458
9128
|
"data-shop-self-update-done": true,
|
|
8459
9129
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8460
|
-
className:
|
|
9130
|
+
className: _dsh_plugin_shop_css_c4028823_default.installedActions,
|
|
8461
9131
|
children: [
|
|
8462
9132
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8463
|
-
className:
|
|
9133
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
8464
9134
|
children: t("installedRestartNotice")
|
|
8465
9135
|
}),
|
|
8466
9136
|
restartBlocked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
8467
9137
|
t,
|
|
8468
9138
|
restart,
|
|
9139
|
+
reload,
|
|
8469
9140
|
gate: {
|
|
8470
9141
|
open: selfRestartGate,
|
|
8471
9142
|
close: () => setSelfRestartGate(false)
|
|
8472
9143
|
}
|
|
8473
9144
|
}),
|
|
8474
9145
|
restartBlocked !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8475
|
-
className:
|
|
9146
|
+
className: _dsh_plugin_shop_css_c4028823_default.notice,
|
|
8476
9147
|
"data-shop-restart-disabled": true,
|
|
8477
9148
|
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
8478
9149
|
})
|
|
@@ -8480,34 +9151,34 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8480
9151
|
})
|
|
8481
9152
|
}),
|
|
8482
9153
|
selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8483
|
-
className:
|
|
9154
|
+
className: _dsh_plugin_shop_css_c4028823_default.selfUpdatePanel,
|
|
8484
9155
|
"data-shop-self-update-failed": true,
|
|
8485
9156
|
children: [
|
|
8486
9157
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8487
|
-
className:
|
|
9158
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedHeading,
|
|
8488
9159
|
children: t("updateFailed")
|
|
8489
9160
|
}),
|
|
8490
9161
|
selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8491
|
-
className:
|
|
9162
|
+
className: _dsh_plugin_shop_css_c4028823_default.log,
|
|
8492
9163
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
8493
|
-
className:
|
|
9164
|
+
className: _dsh_plugin_shop_css_c4028823_default.logLine,
|
|
8494
9165
|
children: line
|
|
8495
9166
|
}, index))
|
|
8496
9167
|
}),
|
|
8497
9168
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8498
|
-
className:
|
|
9169
|
+
className: _dsh_plugin_shop_css_c4028823_default.failedDetail,
|
|
8499
9170
|
children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
|
|
8500
9171
|
})
|
|
8501
9172
|
]
|
|
8502
9173
|
}),
|
|
8503
9174
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8504
|
-
className:
|
|
9175
|
+
className: _dsh_plugin_shop_css_c4028823_default.categoryBar,
|
|
8505
9176
|
role: "group",
|
|
8506
9177
|
"aria-label": t("catalog"),
|
|
8507
9178
|
children: [
|
|
8508
9179
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
8509
9180
|
type: "button",
|
|
8510
|
-
className: category === null ? `${
|
|
9181
|
+
className: category === null ? `${_dsh_plugin_shop_css_c4028823_default.categoryButton} ${_dsh_plugin_shop_css_c4028823_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4028823_default.categoryButton,
|
|
8511
9182
|
"aria-pressed": category === null,
|
|
8512
9183
|
onClick: () => {
|
|
8513
9184
|
setCategory(null);
|
|
@@ -8522,7 +9193,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8522
9193
|
}),
|
|
8523
9194
|
CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
8524
9195
|
type: "button",
|
|
8525
|
-
className: category === key ? `${
|
|
9196
|
+
className: category === key ? `${_dsh_plugin_shop_css_c4028823_default.categoryButton} ${_dsh_plugin_shop_css_c4028823_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4028823_default.categoryButton,
|
|
8526
9197
|
"aria-pressed": category === key,
|
|
8527
9198
|
"data-category": key,
|
|
8528
9199
|
"data-shop-category-tab": key,
|
|
@@ -8538,7 +9209,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8538
9209
|
}, key)),
|
|
8539
9210
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
8540
9211
|
type: "button",
|
|
8541
|
-
className: category === "installed" ? `${
|
|
9212
|
+
className: category === "installed" ? `${_dsh_plugin_shop_css_c4028823_default.categoryButton} ${_dsh_plugin_shop_css_c4028823_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4028823_default.categoryButton,
|
|
8542
9213
|
"aria-pressed": category === "installed",
|
|
8543
9214
|
"data-shop-category-installed": true,
|
|
8544
9215
|
onClick: () => {
|
|
@@ -8555,7 +9226,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8555
9226
|
type: "button",
|
|
8556
9227
|
role: "switch",
|
|
8557
9228
|
"aria-checked": hideIncompatible,
|
|
8558
|
-
className: hideIncompatible ? `${
|
|
9229
|
+
className: hideIncompatible ? `${_dsh_plugin_shop_css_c4028823_default.incompatibleFilter} ${_dsh_plugin_shop_css_c4028823_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_c4028823_default.incompatibleFilter,
|
|
8559
9230
|
title: t(hideIncompatible ? "showIncompatibleTitle" : "incompatibleFilterTitle"),
|
|
8560
9231
|
"data-shop-hide-incompatible": true,
|
|
8561
9232
|
onClick: () => {
|
|
@@ -8563,18 +9234,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8563
9234
|
setVisibleCount(48);
|
|
8564
9235
|
},
|
|
8565
9236
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8566
|
-
className: hideIncompatible ? `${
|
|
9237
|
+
className: hideIncompatible ? `${_dsh_plugin_shop_css_c4028823_default.switch} ${_dsh_plugin_shop_css_c4028823_default.switchOn}` : _dsh_plugin_shop_css_c4028823_default.switch,
|
|
8567
9238
|
"aria-hidden": "true",
|
|
8568
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
9239
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4028823_default.switchKnob })
|
|
8569
9240
|
}), t("hideIncompatible", { count: incompatibleCount })]
|
|
8570
9241
|
})
|
|
8571
9242
|
]
|
|
8572
9243
|
}),
|
|
8573
9244
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
8574
|
-
className:
|
|
9245
|
+
className: _dsh_plugin_shop_css_c4028823_default.catalogStatsRow,
|
|
8575
9246
|
children: [
|
|
8576
9247
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8577
|
-
className:
|
|
9248
|
+
className: _dsh_plugin_shop_css_c4028823_default.catalogStats,
|
|
8578
9249
|
"data-shop-catalog-stats": true,
|
|
8579
9250
|
children: t("catalogStats", {
|
|
8580
9251
|
count: String(browsable.length),
|
|
@@ -8583,32 +9254,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8583
9254
|
}),
|
|
8584
9255
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8585
9256
|
type: "button",
|
|
8586
|
-
className:
|
|
9257
|
+
className: _dsh_plugin_shop_css_c4028823_default.catalogRefreshButton,
|
|
8587
9258
|
"data-shop-catalog-refresh": true,
|
|
8588
9259
|
disabled: reloading,
|
|
8589
9260
|
onClick: () => {
|
|
8590
9261
|
setReloading(true);
|
|
8591
9262
|
setReloadFailed(false);
|
|
8592
9263
|
setRequest({ kind: "refresh" });
|
|
9264
|
+
setVersionReload((current) => current + 1);
|
|
8593
9265
|
},
|
|
8594
9266
|
children: reloading ? t("refreshing") : t("refresh")
|
|
8595
9267
|
}),
|
|
8596
9268
|
reloadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
8597
|
-
className:
|
|
9269
|
+
className: _dsh_plugin_shop_css_c4028823_default.catalogRefreshFailed,
|
|
8598
9270
|
"data-shop-catalog-refresh-failed": true,
|
|
8599
9271
|
children: t("refreshFailed")
|
|
8600
9272
|
})
|
|
8601
9273
|
]
|
|
8602
9274
|
}),
|
|
8603
9275
|
result.plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8604
|
-
className:
|
|
9276
|
+
className: _dsh_plugin_shop_css_c4028823_default.emptyLine,
|
|
8605
9277
|
children: t("empty")
|
|
8606
9278
|
}) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8607
|
-
className:
|
|
9279
|
+
className: _dsh_plugin_shop_css_c4028823_default.emptyLine,
|
|
8608
9280
|
"data-shop-empty": true,
|
|
8609
9281
|
children: matched.length > 0 ? t("emptyIncompatibleFiltered") : t("emptySearch")
|
|
8610
9282
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
8611
|
-
className:
|
|
9283
|
+
className: _dsh_plugin_shop_css_c4028823_default.cards,
|
|
8612
9284
|
children: [visible.map((entry) => {
|
|
8613
9285
|
const key = entryKey(entry);
|
|
8614
9286
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
|
|
@@ -8616,6 +9288,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8616
9288
|
stars: starsOf(entry, stars),
|
|
8617
9289
|
installed: installedByKey.get(key),
|
|
8618
9290
|
missing: missingByKey.get(key) ?? [],
|
|
9291
|
+
harness: harnessVerdictOf(harnessVerdicts, key),
|
|
8619
9292
|
nameTakenBy: nameTakenByKey.get(key),
|
|
8620
9293
|
t,
|
|
8621
9294
|
flow: flows.flowFor(key),
|
|
@@ -8628,12 +9301,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8628
9301
|
}) }, key);
|
|
8629
9302
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
8630
9303
|
ref: sentinelRef,
|
|
8631
|
-
className:
|
|
9304
|
+
className: _dsh_plugin_shop_css_c4028823_default.cardsSentry,
|
|
8632
9305
|
"data-shop-sentry": true,
|
|
8633
9306
|
"aria-hidden": "true"
|
|
8634
9307
|
})]
|
|
8635
9308
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8636
|
-
className:
|
|
9309
|
+
className: _dsh_plugin_shop_css_c4028823_default.showingLine,
|
|
8637
9310
|
"aria-live": "polite",
|
|
8638
9311
|
children: t("showing", {
|
|
8639
9312
|
shown: String(visibleCount),
|
|
@@ -8644,6 +9317,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8644
9317
|
state: installedState,
|
|
8645
9318
|
entriesByKey,
|
|
8646
9319
|
missingByKey,
|
|
9320
|
+
harnessVerdicts,
|
|
8647
9321
|
t,
|
|
8648
9322
|
setEnabled,
|
|
8649
9323
|
flowFor: flows.flowFor,
|
|
@@ -8670,6 +9344,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8670
9344
|
* fire-and-forget from surfacing as an unhandled rejection. Every later
|
|
8671
9345
|
* result, including an explicit refresh, replaces this timestamped stash. */
|
|
8672
9346
|
let warmCatalog = null;
|
|
9347
|
+
/** Package names THIS PAGE has uninstalled since it loaded (design
|
|
9348
|
+
* 2026-09-01-harness-compatibility section 9.1): the module table can still
|
|
9349
|
+
* hold a graph row or a `loadCache` record for one after a restart-free
|
|
9350
|
+
* uninstall (module-table.ts's header explains why), so
|
|
9351
|
+
* the refinement must never clear a name here on the table's say-so — the
|
|
9352
|
+
* host's verdict stands unconditionally. Module-level so it survives the tab
|
|
9353
|
+
* closing and reopening; reset in `apply` beside `warmCatalog`, because a
|
|
9354
|
+
* re-applied bundle is a new page that has uninstalled nothing yet. An
|
|
9355
|
+
* uninstall made from another tab, another window, or the CLI is never added
|
|
9356
|
+
* here and stays unknown to this page until it reloads. */
|
|
9357
|
+
let pageRemoved = /* @__PURE__ */ new Set();
|
|
8673
9358
|
/** Services required by the tab registration and the Remote mount.
|
|
8674
9359
|
*
|
|
8675
9360
|
* `remote.shop` is deliberately ABSENT: this package both mounts its own
|
|
@@ -8700,6 +9385,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8700
9385
|
return result.value;
|
|
8701
9386
|
};
|
|
8702
9387
|
warmCatalog = null;
|
|
9388
|
+
pageRemoved = /* @__PURE__ */ new Set();
|
|
8703
9389
|
const warmed = Promise.resolve(ns.catalog(void 0)).then((result) => unwrap(result));
|
|
8704
9390
|
warmCatalog = {
|
|
8705
9391
|
at: Date.now(),
|
|
@@ -8708,36 +9394,62 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8708
9394
|
warmed.catch(() => {});
|
|
8709
9395
|
Promise.resolve(ns.installed()).then((result) => unwrap(result)).catch(() => {});
|
|
8710
9396
|
Promise.resolve(ns.version()).then((result) => unwrap(result)).catch(() => {});
|
|
8711
|
-
const
|
|
8712
|
-
|
|
8713
|
-
|
|
8714
|
-
const refreshed = unwrap(await ns.catalog(args));
|
|
8715
|
-
warmCatalog = {
|
|
8716
|
-
at: Date.now(),
|
|
8717
|
-
result: Promise.resolve(refreshed)
|
|
8718
|
-
};
|
|
8719
|
-
return refreshed;
|
|
8720
|
-
}
|
|
8721
|
-
const warm = warmCatalog;
|
|
8722
|
-
if (warm !== null && Date.now() - warm.at < 3e5) try {
|
|
8723
|
-
return await warm.result;
|
|
8724
|
-
} catch {}
|
|
8725
|
-
const fresh = unwrap(await ns.catalog(args));
|
|
9397
|
+
const hostCatalog = async (args) => {
|
|
9398
|
+
if (args?.refresh === true) {
|
|
9399
|
+
const refreshed = unwrap(await ns.catalog({ refresh: true }));
|
|
8726
9400
|
warmCatalog = {
|
|
8727
9401
|
at: Date.now(),
|
|
8728
|
-
result: Promise.resolve(
|
|
9402
|
+
result: Promise.resolve(refreshed)
|
|
8729
9403
|
};
|
|
8730
|
-
return
|
|
9404
|
+
return refreshed;
|
|
9405
|
+
}
|
|
9406
|
+
if (args?.reverdict === true) {
|
|
9407
|
+
warmCatalog = null;
|
|
9408
|
+
const judged = unwrap(await ns.catalog(void 0));
|
|
9409
|
+
warmCatalog = {
|
|
9410
|
+
at: Date.now(),
|
|
9411
|
+
result: Promise.resolve(judged)
|
|
9412
|
+
};
|
|
9413
|
+
return judged;
|
|
9414
|
+
}
|
|
9415
|
+
const warm = warmCatalog;
|
|
9416
|
+
if (warm !== null && Date.now() - warm.at < 3e5) try {
|
|
9417
|
+
return await warm.result;
|
|
9418
|
+
} catch {}
|
|
9419
|
+
const fresh = unwrap(await ns.catalog(void 0));
|
|
9420
|
+
warmCatalog = {
|
|
9421
|
+
at: Date.now(),
|
|
9422
|
+
result: Promise.resolve(fresh)
|
|
9423
|
+
};
|
|
9424
|
+
return fresh;
|
|
9425
|
+
};
|
|
9426
|
+
const handOver = async (result) => ({
|
|
9427
|
+
...result,
|
|
9428
|
+
incompatible: await refineAgainstModuleTable(result.incompatible ?? {}, ctx.get("modules"), pageRemoved)
|
|
9429
|
+
});
|
|
9430
|
+
const injected = () => ({
|
|
9431
|
+
catalog: async (args) => handOver(await hostCatalog(args)),
|
|
9432
|
+
install: async (args) => {
|
|
9433
|
+
warmCatalog = null;
|
|
9434
|
+
return unwrap(await ns.installStart(args));
|
|
8731
9435
|
},
|
|
8732
|
-
install: async (args) => unwrap(await ns.installStart(args)),
|
|
8733
9436
|
installStatus: async (args) => unwrap(await ns.installStatus(args)),
|
|
8734
9437
|
setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
|
|
8735
9438
|
installed: async () => unwrap(await ns.installed()),
|
|
8736
9439
|
installedSpecs: async () => unwrap(await ns.installedSpecs()),
|
|
8737
|
-
uninstall: async (args) =>
|
|
9440
|
+
uninstall: async (args) => {
|
|
9441
|
+
warmCatalog = null;
|
|
9442
|
+
return unwrap(await ns.uninstallStart(args));
|
|
9443
|
+
},
|
|
9444
|
+
noteUninstalled: (name) => {
|
|
9445
|
+
pageRemoved.add(name);
|
|
9446
|
+
},
|
|
8738
9447
|
restart: async () => unwrap(await ns.restart()),
|
|
8739
9448
|
version: async () => unwrap(await ns.version()),
|
|
8740
|
-
updateStart: async (args) =>
|
|
9449
|
+
updateStart: async (args) => {
|
|
9450
|
+
warmCatalog = null;
|
|
9451
|
+
return unwrap(await ns.updateStart(args));
|
|
9452
|
+
}
|
|
8741
9453
|
});
|
|
8742
9454
|
ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
|
|
8743
9455
|
name: "settings.plugins.tab",
|