dsh-plugin-shop 0.4.3 → 0.4.5
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 +13 -8
- package/lib/client.js +257 -35
- package/lib/index.js +161 -81
- package/lib/typert.host.js +87 -11
- package/lib/typert.remote-client.d.ts +5 -1
- package/lib/typert.remote-client.js +62 -8
- package/lib/types/client/ShopTab.d.ts +5 -1
- package/lib/types/client/locales.d.ts +6 -0
- package/lib/types/client/present.d.ts +7 -0
- package/lib/types/client/useUpdateSelf.d.ts +20 -0
- package/lib/types/host/index.d.ts +43 -5
- package/lib/types/host/restart.d.ts +27 -24
- package/lib/types/host/self-update.d.ts +9 -0
- package/lib/types/own-version.d.ts +7 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,10 +46,13 @@ install them once with `npm install -g @deepseek-ai/dsh pnpm` and verify with
|
|
|
46
46
|
`dsh --version` and `pnpm --version`.
|
|
47
47
|
|
|
48
48
|
```sh
|
|
49
|
-
# dsh on PATH (global install):
|
|
50
|
-
|
|
49
|
+
# dsh on PATH (global install). The explicit version pin matters: pnpm 11
|
|
50
|
+
# holds back very recent releases, so a bare `add dsh-plugin-shop` can hand
|
|
51
|
+
# you an older version for a while. Pin the current release — refresh it
|
|
52
|
+
# with `npm view dsh-plugin-shop version`.
|
|
53
|
+
dsh plugin --profile web add dsh-plugin-shop@0.4.5
|
|
51
54
|
# or straight through npx, nothing installed:
|
|
52
|
-
npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop
|
|
55
|
+
npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.4.5
|
|
53
56
|
```
|
|
54
57
|
|
|
55
58
|
Replace `web` with your profile if you use another one. Then restart `dsh` once — a
|
|
@@ -75,10 +78,11 @@ profile.
|
|
|
75
78
|
ls -1 "${DSH_HOME:-$HOME/.dsh}/profiles" | grep -v '^node_modules$'
|
|
76
79
|
```
|
|
77
80
|
|
|
78
|
-
**2. Install.**
|
|
81
|
+
**2. Install.** Pin the version — the explicit pin bypasses pnpm's release
|
|
82
|
+
cooldown, and deterministic installs are the point of the agent path.
|
|
79
83
|
|
|
80
84
|
```sh
|
|
81
|
-
dsh plugin --profile <profile> add dsh-plugin-shop
|
|
85
|
+
dsh plugin --profile <profile> add dsh-plugin-shop@0.4.5
|
|
82
86
|
```
|
|
83
87
|
|
|
84
88
|
**3. Verify — do not skip this.** A zero exit from step 2 means pnpm resolved the
|
|
@@ -101,7 +105,7 @@ bundle is not.
|
|
|
101
105
|
| What you see | What it means | What to do |
|
|
102
106
|
|---|---|---|
|
|
103
107
|
| `error: required option '--profile <name>' not specified` | `--profile` was omitted | Pass it; there is no default |
|
|
104
|
-
| A version older than npm's `latest` gets installed | pnpm 11 holds back very recently published versions |
|
|
108
|
+
| A version older than npm's `latest` gets installed | pnpm 11 holds back very recently published versions | Pin the version explicitly: `dsh plugin --profile <p> add dsh-plugin-shop@<version>` |
|
|
105
109
|
| `client bundles not found ... lib/client.js` | the copy on disk was built without its browser half | Install from npm rather than from a source checkout, or run `pnpm build` in that checkout |
|
|
106
110
|
| `dsh: pnpm not found on PATH — install pnpm to manage profile plugins` | `dsh plugin` forwards to pnpm, and pnpm is missing | `npm install -g pnpm` |
|
|
107
111
|
| `no profile directory found above <path>` | the plugin could not locate its profile | Please report it — this is resolved from `ctx.baseUrl` and should not fail |
|
|
@@ -116,6 +120,7 @@ bundle is not.
|
|
|
116
120
|
| **Enable / disable** | Applies to an installed plugin without a restart |
|
|
117
121
|
| **Installed state** | An installed plugin shows an Installed label on its card — or an Update button when the catalog has a newer version — plus an Uninstall button; the Installed filter in the category bar shows only installed plugins |
|
|
118
122
|
| **Restart** | After an install, update, or uninstall, the shop offers to restart dsh — stating the cost first: the page disconnects and in-flight work is interrupted |
|
|
123
|
+
| **Self-update** | The shop shows its own version next to the search box, checks npm for a newer release, and updates itself with the pinned version — then the usual restart |
|
|
119
124
|
|
|
120
125
|
## 🧩 How it is put together
|
|
121
126
|
|
|
@@ -125,9 +130,9 @@ boundary:
|
|
|
125
130
|
| Half | Entry | Can reach | Cannot reach |
|
|
126
131
|
|---|---|---|---|
|
|
127
132
|
| **Host** | `dsh-plugin-shop` | The network (catalog fetch and sha256 verify), the filesystem (cache), `dsh plugin add` under a per-profile mutex | — |
|
|
128
|
-
| **Client** | `dsh-plugin-shop/client` | Exactly
|
|
133
|
+
| **Client** | `dsh-plugin-shop/client` | Exactly nine `shop/*` Remote methods | The network, the filesystem |
|
|
129
134
|
|
|
130
|
-
Compromising the browser half buys an attacker those
|
|
135
|
+
Compromising the browser half buys an attacker those nine calls and nothing more.
|
|
131
136
|
|
|
132
137
|
## ⚙️ Configuration
|
|
133
138
|
|
package/lib/client.js
CHANGED
|
@@ -4350,10 +4350,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4350
4350
|
]).optional(),
|
|
4351
4351
|
"detail": union([_undefined(), string()]).optional()
|
|
4352
4352
|
});
|
|
4353
|
-
const dsh_plugin_shop_shop_restart_result$schema = union([object({
|
|
4354
|
-
"ok": literal(true),
|
|
4355
|
-
"url": string()
|
|
4356
|
-
}), object({
|
|
4353
|
+
const dsh_plugin_shop_shop_restart_result$schema = union([object({ "ok": literal(true) }), object({
|
|
4357
4354
|
"ok": literal(false),
|
|
4358
4355
|
"detail": string()
|
|
4359
4356
|
})]);
|
|
@@ -4373,6 +4370,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4373
4370
|
"ok": literal(false),
|
|
4374
4371
|
"detail": string()
|
|
4375
4372
|
})]);
|
|
4373
|
+
const dsh_plugin_shop_shop_updateStart_parameter_0$schema = object({ "version": string() });
|
|
4374
|
+
const dsh_plugin_shop_shop_updateStart_result$schema = union([object({
|
|
4375
|
+
"ok": literal(true),
|
|
4376
|
+
"installId": string()
|
|
4377
|
+
}), object({
|
|
4378
|
+
"ok": literal(false),
|
|
4379
|
+
"detail": string()
|
|
4380
|
+
})]);
|
|
4381
|
+
const dsh_plugin_shop_shop_version_result$schema = object({
|
|
4382
|
+
"installed": string(),
|
|
4383
|
+
"latest": union([literal(null), string()]),
|
|
4384
|
+
"outdated": boolean()
|
|
4385
|
+
});
|
|
4376
4386
|
const TYPERT_REMOTE = {
|
|
4377
4387
|
package: "dsh-plugin-shop",
|
|
4378
4388
|
descriptors: [
|
|
@@ -4400,7 +4410,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4400
4410
|
},
|
|
4401
4411
|
sourceLocation: {
|
|
4402
4412
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4403
|
-
"line":
|
|
4413
|
+
"line": 244,
|
|
4404
4414
|
"column": 9
|
|
4405
4415
|
}
|
|
4406
4416
|
},
|
|
@@ -4418,7 +4428,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4418
4428
|
},
|
|
4419
4429
|
sourceLocation: {
|
|
4420
4430
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4421
|
-
"line":
|
|
4431
|
+
"line": 319,
|
|
4422
4432
|
"column": 9
|
|
4423
4433
|
}
|
|
4424
4434
|
},
|
|
@@ -4446,7 +4456,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4446
4456
|
},
|
|
4447
4457
|
sourceLocation: {
|
|
4448
4458
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4449
|
-
"line":
|
|
4459
|
+
"line": 270,
|
|
4450
4460
|
"column": 9
|
|
4451
4461
|
}
|
|
4452
4462
|
},
|
|
@@ -4473,7 +4483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4473
4483
|
},
|
|
4474
4484
|
sourceLocation: {
|
|
4475
4485
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4476
|
-
"line":
|
|
4486
|
+
"line": 308,
|
|
4477
4487
|
"column": 3
|
|
4478
4488
|
}
|
|
4479
4489
|
},
|
|
@@ -4491,7 +4501,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4491
4501
|
},
|
|
4492
4502
|
sourceLocation: {
|
|
4493
4503
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4494
|
-
"line":
|
|
4504
|
+
"line": 392,
|
|
4495
4505
|
"column": 9
|
|
4496
4506
|
}
|
|
4497
4507
|
},
|
|
@@ -4518,7 +4528,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4518
4528
|
},
|
|
4519
4529
|
sourceLocation: {
|
|
4520
4530
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4521
|
-
"line":
|
|
4531
|
+
"line": 213,
|
|
4522
4532
|
"column": 3
|
|
4523
4533
|
}
|
|
4524
4534
|
},
|
|
@@ -4546,7 +4556,52 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4546
4556
|
},
|
|
4547
4557
|
sourceLocation: {
|
|
4548
4558
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4549
|
-
"line":
|
|
4559
|
+
"line": 359,
|
|
4560
|
+
"column": 9
|
|
4561
|
+
}
|
|
4562
|
+
},
|
|
4563
|
+
{
|
|
4564
|
+
id: "dsh-plugin-shop#shop/updateStart",
|
|
4565
|
+
service: "shop",
|
|
4566
|
+
namespace: "shop",
|
|
4567
|
+
method: "updateStart",
|
|
4568
|
+
invocation: { kind: "direct" },
|
|
4569
|
+
parameters: [{
|
|
4570
|
+
name: "args",
|
|
4571
|
+
wire: "args",
|
|
4572
|
+
source: "json",
|
|
4573
|
+
codec: {
|
|
4574
|
+
mode: "strict",
|
|
4575
|
+
typeSymbol: "dsh-plugin-shop#shop/updateStart:args",
|
|
4576
|
+
schema: dsh_plugin_shop_shop_updateStart_parameter_0$schema
|
|
4577
|
+
}
|
|
4578
|
+
}],
|
|
4579
|
+
result: {
|
|
4580
|
+
mode: "strict",
|
|
4581
|
+
typeSymbol: "dsh-plugin-shop/types#ShopUpdateResult",
|
|
4582
|
+
schema: dsh_plugin_shop_shop_updateStart_result$schema
|
|
4583
|
+
},
|
|
4584
|
+
sourceLocation: {
|
|
4585
|
+
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4586
|
+
"line": 441,
|
|
4587
|
+
"column": 9
|
|
4588
|
+
}
|
|
4589
|
+
},
|
|
4590
|
+
{
|
|
4591
|
+
id: "dsh-plugin-shop#shop/version",
|
|
4592
|
+
service: "shop",
|
|
4593
|
+
namespace: "shop",
|
|
4594
|
+
method: "version",
|
|
4595
|
+
invocation: { kind: "direct" },
|
|
4596
|
+
parameters: [],
|
|
4597
|
+
result: {
|
|
4598
|
+
mode: "strict",
|
|
4599
|
+
typeSymbol: "dsh-plugin-shop/types#ShopVersionResult",
|
|
4600
|
+
schema: dsh_plugin_shop_shop_version_result$schema
|
|
4601
|
+
},
|
|
4602
|
+
sourceLocation: {
|
|
4603
|
+
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4604
|
+
"line": 426,
|
|
4550
4605
|
"column": 9
|
|
4551
4606
|
}
|
|
4552
4607
|
}
|
|
@@ -4637,6 +4692,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4637
4692
|
"shop",
|
|
4638
4693
|
"marketplace"
|
|
4639
4694
|
];
|
|
4695
|
+
/** Competing marketplaces whose names carry no store/market keyword and so
|
|
4696
|
+
* escape the pattern rules below. Named explicitly rather than guessed:
|
|
4697
|
+
* `dsh-plugin` is the npm package of github.com/dshplugin/dsh-plugin-hub —
|
|
4698
|
+
* a community plugin marketplace for DeepSeek Harness. */
|
|
4699
|
+
const SHOP_LIKE_NAMES = ["dsh-plugin"];
|
|
4640
4700
|
/**
|
|
4641
4701
|
* Whether a package name reads as a plugin shop (a marketplace for dsh
|
|
4642
4702
|
* plugins, e.g. `dsh-plugin-shop`, `dsh-store`, `pluginstore`). The shop
|
|
@@ -4649,6 +4709,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4649
4709
|
* it matches other people's package names, not ours.
|
|
4650
4710
|
*/
|
|
4651
4711
|
function isShopLike(name) {
|
|
4712
|
+
if (SHOP_LIKE_NAMES.includes(name.toLowerCase())) return true;
|
|
4652
4713
|
const segments = name.toLowerCase().split(/[-_.]+/);
|
|
4653
4714
|
const hasPlugin = segments.includes("plugin");
|
|
4654
4715
|
if (segments.some((segment) => /plugin(store|market|mall|shop|marketplace)/.test(segment) || /(store|market|mall|shop|marketplace)plugin/.test(segment))) return true;
|
|
@@ -4763,8 +4824,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4763
4824
|
restartTitle: "确认重启 dsh",
|
|
4764
4825
|
restartBody: "重启会断开当前页面与服务器的连接,正在进行的对话和任务会中断;浏览器会自动跳转到重启后的地址,稍等片刻即可继续使用。",
|
|
4765
4826
|
restartConfirm: "重启",
|
|
4766
|
-
restarting: "
|
|
4827
|
+
restarting: "正在重启,服务器就绪后页面会自动刷新…",
|
|
4828
|
+
restartFailedNotice: "重启后服务器没有回来。请手动启动:dsh web(新进程的日志在商店缓存目录的 restart.log)",
|
|
4767
4829
|
restartTransportFailed: "重启请求未能送达。请稍后重试。",
|
|
4830
|
+
updateFailed: "更新失败",
|
|
4831
|
+
updateTransportFailed: "更新请求未能送达。请稍后重试。",
|
|
4768
4832
|
enabledSwitch: "启用",
|
|
4769
4833
|
toggleFailed: "设置失败,请重试。",
|
|
4770
4834
|
hotApplyNote: "无需重启,立即生效"
|
|
@@ -4828,8 +4892,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4828
4892
|
restartTitle: "Restart dsh?",
|
|
4829
4893
|
restartBody: "Restarting disconnects this page and interrupts any conversation or task in progress; the browser will jump to the restarted address automatically, so give it a few seconds.",
|
|
4830
4894
|
restartConfirm: "Restart",
|
|
4831
|
-
restarting: "Restarting; the
|
|
4895
|
+
restarting: "Restarting; the page will refresh once the server is back…",
|
|
4896
|
+
restartFailedNotice: "The server did not come back after the restart. Start it manually: dsh web (see restart.log in the shop cache directory)",
|
|
4832
4897
|
restartTransportFailed: "The restart request could not be delivered. Please retry.",
|
|
4898
|
+
updateFailed: "Update failed",
|
|
4899
|
+
updateTransportFailed: "The update request could not be delivered. Please retry.",
|
|
4833
4900
|
enabledSwitch: "Enable",
|
|
4834
4901
|
toggleFailed: "Failed to update. Please retry.",
|
|
4835
4902
|
hotApplyNote: "takes effect without a restart"
|
|
@@ -4941,8 +5008,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4941
5008
|
};
|
|
4942
5009
|
}
|
|
4943
5010
|
//#endregion
|
|
5011
|
+
//#region src/client/useUpdateSelf.ts
|
|
5012
|
+
/** Self-update driving hook for the shop itself: start, poll to terminal.
|
|
5013
|
+
* Shares the install view machine and poll loop; the start mapping is the
|
|
5014
|
+
* uninstall one — a business refusal (a non-semver version) lands in the
|
|
5015
|
+
* `failed` view with the host's detail, never in the install `rejected`
|
|
5016
|
+
* codes. */
|
|
5017
|
+
/** Drive one self-update: the host's business union, the shared polling
|
|
5018
|
+
* loop, and terminal states. The `rejected` install state never occurs. */
|
|
5019
|
+
function useUpdateSelf(updateStart, installStatus) {
|
|
5020
|
+
const [view, setView] = (0, react.useState)({ kind: "idle" });
|
|
5021
|
+
const start = (0, react.useCallback)(async (args) => {
|
|
5022
|
+
setView({ kind: "idle" });
|
|
5023
|
+
try {
|
|
5024
|
+
const result = await updateStart(args);
|
|
5025
|
+
if (!result.ok) {
|
|
5026
|
+
setView({
|
|
5027
|
+
kind: "failed",
|
|
5028
|
+
detail: result.detail,
|
|
5029
|
+
log: []
|
|
5030
|
+
});
|
|
5031
|
+
return;
|
|
5032
|
+
}
|
|
5033
|
+
setView({
|
|
5034
|
+
kind: "running",
|
|
5035
|
+
installId: result.installId,
|
|
5036
|
+
log: []
|
|
5037
|
+
});
|
|
5038
|
+
} catch {
|
|
5039
|
+
setView({
|
|
5040
|
+
kind: "failed",
|
|
5041
|
+
detail: "",
|
|
5042
|
+
log: []
|
|
5043
|
+
});
|
|
5044
|
+
}
|
|
5045
|
+
}, [updateStart]);
|
|
5046
|
+
usePollStatus(view, setView, installStatus);
|
|
5047
|
+
return {
|
|
5048
|
+
view,
|
|
5049
|
+
start
|
|
5050
|
+
};
|
|
5051
|
+
}
|
|
5052
|
+
//#endregion
|
|
4944
5053
|
//#region \0dsh-plugin-shop-css:e3675a89
|
|
4945
|
-
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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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.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.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\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 gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\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.s26d58e_categoryButton: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.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sccc4d1_catalogStats {\n margin: 6px 0 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.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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\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(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\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 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.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.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.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\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.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 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";
|
|
5054
|
+
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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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 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.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\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 gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\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.s26d58e_categoryButton: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.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sccc4d1_catalogStats {\n margin: 6px 0 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.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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\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(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\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 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.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.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.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\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.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 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";
|
|
4946
5055
|
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
|
|
4947
5056
|
const tag = document.createElement("style");
|
|
4948
5057
|
tag.dataset.pluginCss = "e3675a89";
|
|
@@ -4965,6 +5074,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4965
5074
|
"skeletonSummaryShort": "sbb0d25_skeletonSummaryShort",
|
|
4966
5075
|
"actionButton": "s73242a_actionButton",
|
|
4967
5076
|
"toolbar": "s646509_toolbar",
|
|
5077
|
+
"toolbarRight": "s745e3c_toolbarRight",
|
|
5078
|
+
"versionBlock": "s4fe61f_versionBlock",
|
|
5079
|
+
"versionText": "sb9b924_versionText",
|
|
5080
|
+
"updateSelfButton": "sa04455_updateSelfButton",
|
|
5081
|
+
"selfUpdatePanel": "sb81ba5_selfUpdatePanel",
|
|
4968
5082
|
"searchInput": "s232ec4_searchInput",
|
|
4969
5083
|
"staleBlock": "s1555aa_staleBlock",
|
|
4970
5084
|
"staleBadge": "s3946f1_staleBadge",
|
|
@@ -5376,22 +5490,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5376
5490
|
/** The §8 restart flow (amendment 2026-08-27): after an install, update, or
|
|
5377
5491
|
* uninstall reports done, this panel offers a restart of dsh. The
|
|
5378
5492
|
* confirmation gate states the cost — the page disconnects and in-flight
|
|
5379
|
-
* conversations/tasks are interrupted — and on confirm the restart RPC
|
|
5380
|
-
* the
|
|
5381
|
-
*
|
|
5493
|
+
* conversations/tasks are interrupted — and on confirm the restart RPC
|
|
5494
|
+
* commits the two-phase handoff: the host exits, a helper re-runs dsh, and
|
|
5495
|
+
* this panel polls the origin after a grace period, refreshing the page
|
|
5496
|
+
* once the NEW server answers. A refused restart renders the host's
|
|
5497
|
+
* published detail; a server that never comes back names the manual
|
|
5498
|
+
* command. */
|
|
5382
5499
|
function RestartPanel({ t, restart }) {
|
|
5383
5500
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5384
5501
|
const [state, setState] = (0, react.useState)({ kind: "idle" });
|
|
5385
5502
|
const onConfirm = async () => {
|
|
5386
5503
|
setGateOpen(false);
|
|
5387
|
-
setState({ kind: "restarting" });
|
|
5388
5504
|
try {
|
|
5389
5505
|
const result = await restart();
|
|
5390
|
-
if (result.ok)
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5506
|
+
if (!result.ok) {
|
|
5507
|
+
setState({
|
|
5508
|
+
kind: "failed",
|
|
5509
|
+
detail: result.detail
|
|
5510
|
+
});
|
|
5511
|
+
return;
|
|
5512
|
+
}
|
|
5513
|
+
setState({ kind: "restarting" });
|
|
5395
5514
|
} catch {
|
|
5396
5515
|
setState({
|
|
5397
5516
|
kind: "failed",
|
|
@@ -5399,6 +5518,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5399
5518
|
});
|
|
5400
5519
|
}
|
|
5401
5520
|
};
|
|
5521
|
+
(0, react.useEffect)(() => {
|
|
5522
|
+
if (state.kind !== "restarting") return;
|
|
5523
|
+
const started = Date.now();
|
|
5524
|
+
const timer = setInterval(() => {
|
|
5525
|
+
const elapsed = Date.now() - started;
|
|
5526
|
+
if (elapsed < 3e3) return;
|
|
5527
|
+
fetch(window.location.href, { cache: "no-store" }).then(() => {
|
|
5528
|
+
window.location.reload();
|
|
5529
|
+
}, () => {});
|
|
5530
|
+
if (elapsed > 3e4) {
|
|
5531
|
+
clearInterval(timer);
|
|
5532
|
+
setState({
|
|
5533
|
+
kind: "failed",
|
|
5534
|
+
detail: t("restartFailedNotice")
|
|
5535
|
+
});
|
|
5536
|
+
}
|
|
5537
|
+
}, INSTALL_POLL_MS);
|
|
5538
|
+
return () => clearInterval(timer);
|
|
5539
|
+
}, [state, t]);
|
|
5402
5540
|
if (state.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5403
5541
|
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5404
5542
|
"data-shop-restarting": true,
|
|
@@ -5572,9 +5710,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5572
5710
|
/** The shop tab root: browse, search, refresh, and render one card per
|
|
5573
5711
|
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
5574
5712
|
function ShopTab(props) {
|
|
5575
|
-
const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart } = props;
|
|
5713
|
+
const { t, catalog, install, installStatus, setEnabled, installed, uninstall, restart, version, updateStart } = props;
|
|
5576
5714
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
5577
5715
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
5716
|
+
const [selfVersion, setSelfVersion] = (0, react.useState)(null);
|
|
5717
|
+
const selfUpdate = useUpdateSelf(updateStart, installStatus);
|
|
5578
5718
|
const [request, setRequest] = (0, react.useState)({ kind: "initial" });
|
|
5579
5719
|
const [query, setQuery] = (0, react.useState)("");
|
|
5580
5720
|
const [category, setCategory] = (0, react.useState)(null);
|
|
@@ -5601,6 +5741,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5601
5741
|
cancelled = true;
|
|
5602
5742
|
};
|
|
5603
5743
|
}, [catalog, request]);
|
|
5744
|
+
(0, react.useEffect)(() => {
|
|
5745
|
+
let cancelled = false;
|
|
5746
|
+
const load = async () => {
|
|
5747
|
+
try {
|
|
5748
|
+
const result = await version();
|
|
5749
|
+
if (!cancelled) setSelfVersion(result);
|
|
5750
|
+
} catch {}
|
|
5751
|
+
};
|
|
5752
|
+
load();
|
|
5753
|
+
return () => {
|
|
5754
|
+
cancelled = true;
|
|
5755
|
+
};
|
|
5756
|
+
}, [version, request]);
|
|
5604
5757
|
(0, react.useEffect)(() => {
|
|
5605
5758
|
let cancelled = false;
|
|
5606
5759
|
const load = async () => {
|
|
@@ -5732,19 +5885,86 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5732
5885
|
setQuery(event.target.value);
|
|
5733
5886
|
setVisibleCount(48);
|
|
5734
5887
|
}
|
|
5735
|
-
}),
|
|
5736
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
5737
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
5738
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
5739
|
-
children:
|
|
5740
|
-
|
|
5741
|
-
|
|
5742
|
-
|
|
5743
|
-
|
|
5744
|
-
|
|
5888
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5889
|
+
className: _dsh_plugin_shop_css_e3675a89_default.toolbarRight,
|
|
5890
|
+
children: [result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5891
|
+
className: _dsh_plugin_shop_css_e3675a89_default.staleBlock,
|
|
5892
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5893
|
+
className: _dsh_plugin_shop_css_e3675a89_default.staleBadge,
|
|
5894
|
+
children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
|
|
5895
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5896
|
+
type: "button",
|
|
5897
|
+
className: _dsh_plugin_shop_css_e3675a89_default.actionButton,
|
|
5898
|
+
onClick: () => setRequest({ kind: "refresh" }),
|
|
5899
|
+
children: t("refresh")
|
|
5900
|
+
})]
|
|
5901
|
+
}), selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5902
|
+
className: _dsh_plugin_shop_css_e3675a89_default.versionBlock,
|
|
5903
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5904
|
+
className: _dsh_plugin_shop_css_e3675a89_default.versionText,
|
|
5905
|
+
"data-shop-version": true,
|
|
5906
|
+
children: ["v", selfVersion.installed]
|
|
5907
|
+
}), selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5908
|
+
type: "button",
|
|
5909
|
+
className: _dsh_plugin_shop_css_e3675a89_default.updateSelfButton,
|
|
5910
|
+
"data-shop-update-self": true,
|
|
5911
|
+
onClick: () => {
|
|
5912
|
+
if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
|
|
5913
|
+
},
|
|
5914
|
+
children: t("update")
|
|
5915
|
+
})]
|
|
5745
5916
|
})]
|
|
5746
5917
|
})]
|
|
5747
5918
|
}),
|
|
5919
|
+
selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5920
|
+
className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
|
|
5921
|
+
"data-shop-self-updating": true,
|
|
5922
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5923
|
+
className: _dsh_plugin_shop_css_e3675a89_default.installing,
|
|
5924
|
+
children: t("installing")
|
|
5925
|
+
}), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5926
|
+
className: _dsh_plugin_shop_css_e3675a89_default.log,
|
|
5927
|
+
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5928
|
+
className: _dsh_plugin_shop_css_e3675a89_default.logLine,
|
|
5929
|
+
children: line
|
|
5930
|
+
}, index))
|
|
5931
|
+
})]
|
|
5932
|
+
}),
|
|
5933
|
+
selfUpdate.view.kind === "done" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5934
|
+
className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
|
|
5935
|
+
"data-shop-self-update-done": true,
|
|
5936
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5937
|
+
className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
|
|
5938
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5939
|
+
className: _dsh_plugin_shop_css_e3675a89_default.notice,
|
|
5940
|
+
children: t("installedRestartNotice")
|
|
5941
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
5942
|
+
t,
|
|
5943
|
+
restart
|
|
5944
|
+
})]
|
|
5945
|
+
})
|
|
5946
|
+
}),
|
|
5947
|
+
selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5948
|
+
className: _dsh_plugin_shop_css_e3675a89_default.selfUpdatePanel,
|
|
5949
|
+
"data-shop-self-update-failed": true,
|
|
5950
|
+
children: [
|
|
5951
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5952
|
+
className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
|
|
5953
|
+
children: t("updateFailed")
|
|
5954
|
+
}),
|
|
5955
|
+
selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5956
|
+
className: _dsh_plugin_shop_css_e3675a89_default.log,
|
|
5957
|
+
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
5958
|
+
className: _dsh_plugin_shop_css_e3675a89_default.logLine,
|
|
5959
|
+
children: line
|
|
5960
|
+
}, index))
|
|
5961
|
+
}),
|
|
5962
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5963
|
+
className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
|
|
5964
|
+
children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
|
|
5965
|
+
})
|
|
5966
|
+
]
|
|
5967
|
+
}),
|
|
5748
5968
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5749
5969
|
className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
|
|
5750
5970
|
role: "group",
|
|
@@ -5884,7 +6104,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5884
6104
|
setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
|
|
5885
6105
|
installed: async () => unwrap(await ns.installed()),
|
|
5886
6106
|
uninstall: async (args) => unwrap(await ns.uninstallStart(args)),
|
|
5887
|
-
restart: async () => unwrap(await ns.restart())
|
|
6107
|
+
restart: async () => unwrap(await ns.restart()),
|
|
6108
|
+
version: async () => unwrap(await ns.version()),
|
|
6109
|
+
updateStart: async (args) => unwrap(await ns.updateStart(args))
|
|
5888
6110
|
});
|
|
5889
6111
|
ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
|
|
5890
6112
|
name: "settings.plugins.tab",
|