dsh-plugin-shop 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4306,6 +4306,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4306
4306
  })),
4307
4307
  "stars": record(string(), number())
4308
4308
  });
4309
+ const dsh_plugin_shop_shop_installed_result$schema = array(object({
4310
+ "name": string(),
4311
+ "installed": string(),
4312
+ "latest": string(),
4313
+ "outdated": boolean()
4314
+ }));
4309
4315
  const dsh_plugin_shop_shop_installStart_parameter_0$schema = object({
4310
4316
  "name": string(),
4311
4317
  "version": string(),
@@ -4344,11 +4350,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4344
4350
  ]).optional(),
4345
4351
  "detail": union([_undefined(), string()]).optional()
4346
4352
  });
4347
- const dsh_plugin_shop_shop_outdated_result$schema = array(object({
4348
- "name": string(),
4349
- "installed": string(),
4350
- "latest": string()
4351
- }));
4352
4353
  const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = object({
4353
4354
  "name": string(),
4354
4355
  "enabled": boolean()
@@ -4357,6 +4358,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4357
4358
  "ok": boolean(),
4358
4359
  "detail": union([_undefined(), string()]).optional()
4359
4360
  });
4361
+ const dsh_plugin_shop_shop_uninstallStart_parameter_0$schema = object({ "name": string() });
4362
+ const dsh_plugin_shop_shop_uninstallStart_result$schema = union([object({
4363
+ "ok": literal(true),
4364
+ "installId": string()
4365
+ }), object({
4366
+ "ok": literal(false),
4367
+ "detail": string()
4368
+ })]);
4360
4369
  const TYPERT_REMOTE = {
4361
4370
  package: "dsh-plugin-shop",
4362
4371
  descriptors: [
@@ -4384,7 +4393,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4384
4393
  },
4385
4394
  sourceLocation: {
4386
4395
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4387
- "line": 180,
4396
+ "line": 190,
4397
+ "column": 9
4398
+ }
4399
+ },
4400
+ {
4401
+ id: "dsh-plugin-shop#shop/installed",
4402
+ service: "shop",
4403
+ namespace: "shop",
4404
+ method: "installed",
4405
+ invocation: { kind: "direct" },
4406
+ parameters: [],
4407
+ result: {
4408
+ mode: "strict",
4409
+ typeSymbol: "dsh-plugin-shop#shop/installed:result",
4410
+ schema: dsh_plugin_shop_shop_installed_result$schema
4411
+ },
4412
+ sourceLocation: {
4413
+ "file": "packages/dsh-plugin-shop/src/host/index.ts",
4414
+ "line": 265,
4388
4415
  "column": 9
4389
4416
  }
4390
4417
  },
@@ -4412,7 +4439,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4412
4439
  },
4413
4440
  sourceLocation: {
4414
4441
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4415
- "line": 206,
4442
+ "line": 216,
4416
4443
  "column": 9
4417
4444
  }
4418
4445
  },
@@ -4439,33 +4466,43 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4439
4466
  },
4440
4467
  sourceLocation: {
4441
4468
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4442
- "line": 244,
4469
+ "line": 254,
4443
4470
  "column": 3
4444
4471
  }
4445
4472
  },
4446
4473
  {
4447
- id: "dsh-plugin-shop#shop/outdated",
4474
+ id: "dsh-plugin-shop#shop/setEnabled",
4448
4475
  service: "shop",
4449
4476
  namespace: "shop",
4450
- method: "outdated",
4477
+ method: "setEnabled",
4451
4478
  invocation: { kind: "direct" },
4452
- parameters: [],
4479
+ parameters: [{
4480
+ name: "args",
4481
+ wire: "args",
4482
+ source: "json",
4483
+ codec: {
4484
+ mode: "strict",
4485
+ typeSymbol: "dsh-plugin-shop#shop/setEnabled:args",
4486
+ schema: dsh_plugin_shop_shop_setEnabled_parameter_0$schema
4487
+ }
4488
+ }],
4453
4489
  result: {
4454
4490
  mode: "strict",
4455
- typeSymbol: "dsh-plugin-shop#shop/outdated:result",
4456
- schema: dsh_plugin_shop_shop_outdated_result$schema
4491
+ typeSymbol: "dsh-plugin-shop/types#ShopSetEnabledResult",
4492
+ schema: dsh_plugin_shop_shop_setEnabled_result$schema
4457
4493
  },
4458
4494
  sourceLocation: {
4459
4495
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4460
- "line": 252,
4461
- "column": 9
4496
+ "line": 159,
4497
+ "column": 3
4462
4498
  }
4463
4499
  },
4464
4500
  {
4465
- id: "dsh-plugin-shop#shop/setEnabled",
4501
+ id: "dsh-plugin-shop#shop/uninstallStart",
4466
4502
  service: "shop",
4467
4503
  namespace: "shop",
4468
- method: "setEnabled",
4504
+ method: "uninstallStart",
4505
+ implementation: "uninstall",
4469
4506
  invocation: { kind: "direct" },
4470
4507
  parameters: [{
4471
4508
  name: "args",
@@ -4473,19 +4510,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4473
4510
  source: "json",
4474
4511
  codec: {
4475
4512
  mode: "strict",
4476
- typeSymbol: "dsh-plugin-shop#shop/setEnabled:args",
4477
- schema: dsh_plugin_shop_shop_setEnabled_parameter_0$schema
4513
+ typeSymbol: "dsh-plugin-shop#shop/uninstallStart:args",
4514
+ schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema
4478
4515
  }
4479
4516
  }],
4480
4517
  result: {
4481
4518
  mode: "strict",
4482
- typeSymbol: "dsh-plugin-shop/types#ShopSetEnabledResult",
4483
- schema: dsh_plugin_shop_shop_setEnabled_result$schema
4519
+ typeSymbol: "dsh-plugin-shop/types#ShopUninstallResult",
4520
+ schema: dsh_plugin_shop_shop_uninstallStart_result$schema
4484
4521
  },
4485
4522
  sourceLocation: {
4486
4523
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4487
- "line": 149,
4488
- "column": 3
4524
+ "line": 305,
4525
+ "column": 9
4489
4526
  }
4490
4527
  }
4491
4528
  ]
@@ -4691,6 +4728,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4691
4728
  installedVersion: "已安装 v{version}",
4692
4729
  latestVersion: "最新 v{version}",
4693
4730
  update: "更新",
4731
+ installed: "已安装",
4732
+ uninstall: "卸载",
4733
+ uninstalling: "正在卸载…",
4734
+ uninstallFailed: "卸载失败",
4735
+ uninstallTransportFailed: "卸载请求未能送达。请稍后重试。",
4736
+ uninstalledRestartNotice: "已卸载;重启 dsh 后生效",
4694
4737
  enabledSwitch: "启用",
4695
4738
  toggleFailed: "设置失败,请重试。",
4696
4739
  hotApplyNote: "无需重启,立即生效"
@@ -4744,6 +4787,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4744
4787
  installedVersion: "installed v{version}",
4745
4788
  latestVersion: "latest v{version}",
4746
4789
  update: "Update",
4790
+ installed: "Installed",
4791
+ uninstall: "Uninstall",
4792
+ uninstalling: "Uninstalling…",
4793
+ uninstallFailed: "Uninstall failed",
4794
+ uninstallTransportFailed: "The uninstall request could not be delivered. Please retry.",
4795
+ uninstalledRestartNotice: "uninstalled; restart dsh to activate",
4747
4796
  enabledSwitch: "Enable",
4748
4797
  toggleFailed: "Failed to update. Please retry.",
4749
4798
  hotApplyNote: "takes effect without a restart"
@@ -4751,6 +4800,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4751
4800
  //#endregion
4752
4801
  //#region src/client/useInstall.ts
4753
4802
  /** Install driving hook for one entry: start, poll to terminal, reset. */
4803
+ /** The poll loop shared by the install and uninstall drivers: while the view
4804
+ * is `running`, poll once per second and fold each status through the
4805
+ * reducer. A poll failure is transient — the host retains the record, so the
4806
+ * next tick finds it. The rejection handler must be present: an unhandled
4807
+ * rejection here would escape the poll loop. */
4808
+ function usePollStatus(view, setView, installStatus) {
4809
+ (0, react.useEffect)(() => {
4810
+ if (view.kind !== "running") return;
4811
+ const timer = setInterval(() => {
4812
+ installStatus({ installId: view.installId }).then((status) => {
4813
+ setView((current) => reduceInstall(current, {
4814
+ type: "status",
4815
+ status
4816
+ }));
4817
+ }, () => {});
4818
+ }, INSTALL_POLL_MS);
4819
+ return () => clearInterval(timer);
4820
+ }, [
4821
+ view,
4822
+ setView,
4823
+ installStatus
4824
+ ]);
4825
+ }
4754
4826
  /** Drive one install: rejections, the polling loop at INSTALL_POLL_MS, and
4755
4827
  * terminal states. The interval is cleared on unmount and whenever the view
4756
4828
  * leaves `running`, so a done/failed/rejected install never polls again. */
@@ -4781,18 +4853,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4781
4853
  });
4782
4854
  }
4783
4855
  }, [install]);
4784
- (0, react.useEffect)(() => {
4785
- if (view.kind !== "running") return;
4786
- const timer = setInterval(() => {
4787
- installStatus({ installId: view.installId }).then((status) => {
4788
- setView((current) => reduceInstall(current, {
4789
- type: "status",
4790
- status
4791
- }));
4792
- }, () => {});
4793
- }, INSTALL_POLL_MS);
4794
- return () => clearInterval(timer);
4795
- }, [view, installStatus]);
4856
+ usePollStatus(view, setView, installStatus);
4796
4857
  return {
4797
4858
  view,
4798
4859
  start,
@@ -4800,8 +4861,51 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4800
4861
  };
4801
4862
  }
4802
4863
  //#endregion
4864
+ //#region src/client/useUninstall.ts
4865
+ /** Uninstall driving hook for one entry: start, poll to terminal. Shares the
4866
+ * install view machine and poll loop; the only difference is the start
4867
+ * mapping — an uninstall business failure (not in the catalog / not
4868
+ * installed) lands in the `failed` view with the host's published detail,
4869
+ * never in the install `rejected` codes, which belong to the install gate
4870
+ * (§7.2). */
4871
+ /** Drive one uninstall: the host's business union, the shared polling loop,
4872
+ * and terminal states. The `rejected` install state never occurs here. */
4873
+ function useUninstall(uninstall, installStatus) {
4874
+ const [view, setView] = (0, react.useState)({ kind: "idle" });
4875
+ const start = (0, react.useCallback)(async (args) => {
4876
+ setView({ kind: "idle" });
4877
+ try {
4878
+ const result = await uninstall(args);
4879
+ if (!result.ok) {
4880
+ setView({
4881
+ kind: "failed",
4882
+ detail: result.detail,
4883
+ log: []
4884
+ });
4885
+ return;
4886
+ }
4887
+ setView({
4888
+ kind: "running",
4889
+ installId: result.installId,
4890
+ log: []
4891
+ });
4892
+ } catch {
4893
+ setView({
4894
+ kind: "failed",
4895
+ detail: "",
4896
+ log: []
4897
+ });
4898
+ }
4899
+ }, [uninstall]);
4900
+ usePollStatus(view, setView, installStatus);
4901
+ return {
4902
+ view,
4903
+ start
4904
+ };
4905
+ }
4906
+ //#endregion
4803
4907
  //#region \0dsh-plugin-shop-css:e3675a89
4804
- 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: 10px;\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: -8px 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.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";
4908
+ 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.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";
4805
4909
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
4806
4910
  const tag = document.createElement("style");
4807
4911
  tag.dataset.pluginCss = "e3675a89";
@@ -4864,6 +4968,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4864
4968
  "installButton": "se7aaed_installButton",
4865
4969
  "confirmButton": "sb43a49_confirmButton",
4866
4970
  "cancelButton": "s6e45d1_cancelButton",
4971
+ "installedLabel": "sb6fc74_installedLabel",
4972
+ "installedActions": "sf53ca0_installedActions",
4973
+ "uninstallButton": "s96967f_uninstallButton",
4867
4974
  "gate": "sc4f2ac_gate",
4868
4975
  "gateTitle": "s559c38_gateTitle",
4869
4976
  "gateBody": "s3c0ace_gateBody",
@@ -4909,8 +5016,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4909
5016
  /** One entry card: the category spine and cover band, the expandable header
4910
5017
  * (name, tier, category, unclaimed marker), the plain-text summary in both
4911
5018
  * languages, the self-declared capabilities, the detail section, and the
4912
- * install controls. */
4913
- const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, t, install, installStatus }) {
5019
+ * install controls. An installed plugin's card carries its installed row:
5020
+ * current the non-interactive installed label, behind the update button;
5021
+ * uninstalled → the install button. */
5022
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, t, install, installStatus, uninstall }) {
4914
5023
  const [open, setOpen] = (0, react.useState)(false);
4915
5024
  const detailId = (0, react.useId)();
4916
5025
  const summary = entry.catalog?.summary;
@@ -5016,13 +5125,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5016
5125
  })
5017
5126
  })]
5018
5127
  }),
5019
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5128
+ installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5020
5129
  name: entry.name,
5021
5130
  version: entry.version,
5022
5131
  tier: entry.tier,
5023
5132
  t,
5024
5133
  install,
5025
5134
  installStatus
5135
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5136
+ className: _dsh_plugin_shop_css_e3675a89_default.installedActions,
5137
+ children: [installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5138
+ name: entry.name,
5139
+ version: entry.version,
5140
+ tier: entry.tier,
5141
+ variant: "update",
5142
+ t,
5143
+ install,
5144
+ installStatus
5145
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5146
+ className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
5147
+ "data-shop-installed": true,
5148
+ children: t("installed")
5149
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
5150
+ name: entry.name,
5151
+ t,
5152
+ uninstall,
5153
+ installStatus
5154
+ })]
5026
5155
  })
5027
5156
  ]
5028
5157
  })
@@ -5136,6 +5265,61 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5136
5265
  children: t(update ? "update" : "install")
5137
5266
  });
5138
5267
  }
5268
+ /** One installed entry's uninstall flow: remove from the profile through the
5269
+ * same executor records and poll loop as installs. Uninstalling revokes
5270
+ * privilege rather than granting it, so there is no acknowledgement gate —
5271
+ * §9.3 is about granting. A business failure (not in the catalog / not
5272
+ * installed) lands in the failed view with the host's published detail; a
5273
+ * transport failure carries the empty detail and the localized fallback. */
5274
+ function UninstallPanel({ name, t, uninstall, installStatus }) {
5275
+ const { view, start } = useUninstall(uninstall, installStatus);
5276
+ if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5277
+ className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
5278
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5279
+ className: _dsh_plugin_shop_css_e3675a89_default.installing,
5280
+ children: t("uninstalling")
5281
+ }), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5282
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5283
+ children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5284
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5285
+ children: line
5286
+ }, index))
5287
+ })]
5288
+ });
5289
+ if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5290
+ className: _dsh_plugin_shop_css_e3675a89_default.notice,
5291
+ "data-shop-uninstall-done": true,
5292
+ children: t("uninstalledRestartNotice")
5293
+ });
5294
+ if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5295
+ className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
5296
+ children: [
5297
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5298
+ className: _dsh_plugin_shop_css_e3675a89_default.failedHeading,
5299
+ children: t("uninstallFailed")
5300
+ }),
5301
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5302
+ className: _dsh_plugin_shop_css_e3675a89_default.log,
5303
+ children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5304
+ className: _dsh_plugin_shop_css_e3675a89_default.logLine,
5305
+ children: line
5306
+ }, index))
5307
+ }),
5308
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5309
+ className: _dsh_plugin_shop_css_e3675a89_default.failedDetail,
5310
+ children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
5311
+ })
5312
+ ]
5313
+ });
5314
+ if (view.kind !== "idle") return null;
5315
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5316
+ type: "button",
5317
+ className: _dsh_plugin_shop_css_e3675a89_default.uninstallButton,
5318
+ "data-shop-uninstall": true,
5319
+ onClick: () => void start({ name }),
5320
+ children: t("uninstall")
5321
+ });
5322
+ }
5139
5323
  /** One outdated install row (§7.3): the name, the installed and latest
5140
5324
  * versions, an optimistic enable/disable switch, and the update button (the
5141
5325
  * install flow for `name@latest`, reusing `InstallPanel`). The switch has no
@@ -5223,10 +5407,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5223
5407
  ]
5224
5408
  });
5225
5409
  }
5226
- /** The §7.3 outdated list, rendered as the "installed" section: each row shows
5227
- * both versions, a switch, and an update button. The tier for the update gate
5228
- * is looked up from the catalog by name (community acknowledgement); an entry
5229
- * absent from the catalog defaults to the community gate (the safer read). */
5410
+ /** The §7.3 installed list, rendered as the "installed" section: each row
5411
+ * shows both versions, a switch, and an update button. The rows are the
5412
+ * installed entries filtered to `outdated` a current install is already
5413
+ * spoken for by its shelf card's installed label, and has no row here. The
5414
+ * tier for the update gate is looked up from the catalog by name (community →
5415
+ * acknowledgement); an entry absent from the catalog defaults to the
5416
+ * community gate (the safer read). */
5230
5417
  function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus }) {
5231
5418
  if (state.kind === "loading") return null;
5232
5419
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5234,7 +5421,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5234
5421
  "data-shop-outdated-error": true,
5235
5422
  children: t("error")
5236
5423
  });
5237
- if (state.entries.length === 0) return null;
5424
+ const outdated = state.entries.filter((entry) => entry.outdated);
5425
+ if (outdated.length === 0) return null;
5238
5426
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5239
5427
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedSection,
5240
5428
  "data-shop-outdated": true,
@@ -5243,7 +5431,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5243
5431
  children: t("installedSection")
5244
5432
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5245
5433
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
5246
- children: state.entries.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
5434
+ children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
5247
5435
  row,
5248
5436
  tier: tiers.get(row.name) ?? "community",
5249
5437
  t,
@@ -5257,9 +5445,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5257
5445
  /** The shop tab root: browse, search, refresh, and render one card per
5258
5446
  * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
5259
5447
  function ShopTab(props) {
5260
- const { t, catalog, install, installStatus, setEnabled, outdated } = props;
5448
+ const { t, catalog, install, installStatus, setEnabled, installed, uninstall } = props;
5261
5449
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
5262
- const [outdatedState, setOutdatedState] = (0, react.useState)({ kind: "loading" });
5450
+ const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
5263
5451
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
5264
5452
  const [query, setQuery] = (0, react.useState)("");
5265
5453
  const [category, setCategory] = (0, react.useState)(null);
@@ -5290,26 +5478,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5290
5478
  let cancelled = false;
5291
5479
  const load = async () => {
5292
5480
  try {
5293
- const entries = await outdated();
5294
- if (!cancelled) setOutdatedState({
5481
+ const entries = await installed();
5482
+ if (!cancelled) setInstalledState({
5295
5483
  kind: "ready",
5296
5484
  entries
5297
5485
  });
5298
5486
  } catch {
5299
- if (!cancelled) setOutdatedState({ kind: "error" });
5487
+ if (!cancelled) setInstalledState({ kind: "error" });
5300
5488
  }
5301
5489
  };
5302
5490
  load();
5303
5491
  return () => {
5304
5492
  cancelled = true;
5305
5493
  };
5306
- }, [outdated, request]);
5494
+ }, [installed, request]);
5495
+ const installedByName = (0, react.useMemo)(() => {
5496
+ const map = /* @__PURE__ */ new Map();
5497
+ if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(entry.name, entry);
5498
+ return map;
5499
+ }, [installedState]);
5307
5500
  const filtered = (0, react.useMemo)(() => {
5308
5501
  if (catalogState.kind !== "ready") return [];
5309
5502
  const q = query.trim().toLowerCase();
5310
5503
  return catalogState.result.plugins.filter((entry) => {
5311
5504
  if (isShopLike(entry.name)) return false;
5312
- if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
5505
+ if (category === "installed") {
5506
+ if (!installedByName.has(entry.name)) return false;
5507
+ } else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
5313
5508
  if (q === "") return true;
5314
5509
  const summaryEn = entry.catalog?.summary.en ?? "";
5315
5510
  const summaryZh = entry.catalog?.summary.zh ?? "";
@@ -5318,7 +5513,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5318
5513
  }, [
5319
5514
  catalogState,
5320
5515
  query,
5321
- category
5516
+ category,
5517
+ installedByName
5322
5518
  ]);
5323
5519
  filteredLenRef.current = filtered.length;
5324
5520
  const stars = catalogState.kind === "ready" ? catalogState.result.stars : {};
@@ -5348,6 +5544,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5348
5544
  }
5349
5545
  return counts;
5350
5546
  }, [catalogState]);
5547
+ const installedCount = (0, react.useMemo)(() => {
5548
+ if (installedState.kind !== "ready") return 0;
5549
+ return installedState.entries.filter((entry) => !isShopLike(entry.name)).length;
5550
+ }, [installedState]);
5351
5551
  const tiers = (0, react.useMemo)(() => {
5352
5552
  const map = /* @__PURE__ */ new Map();
5353
5553
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
@@ -5422,37 +5622,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5422
5622
  className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
5423
5623
  role: "group",
5424
5624
  "aria-label": t("catalog"),
5425
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5426
- type: "button",
5427
- className: category === null ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5428
- "aria-pressed": category === null,
5429
- onClick: () => {
5430
- setCategory(null);
5431
- setVisibleCount(48);
5432
- },
5433
- children: [
5434
- t("all"),
5435
- " ",
5436
- [...categoryCounts.values()].reduce((a, b) => a + b, 0)
5437
- ]
5438
- }), CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5439
- type: "button",
5440
- className: category === key ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5441
- "aria-pressed": category === key,
5442
- onClick: () => {
5443
- setCategory(key);
5444
- setVisibleCount(48);
5445
- },
5446
- children: [
5447
- t(categoryLocaleKey(key)),
5448
- " ",
5449
- categoryCounts.get(key) ?? 0
5450
- ]
5451
- }, key))]
5452
- }),
5453
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5454
- className: _dsh_plugin_shop_css_e3675a89_default.catalogHeading,
5455
- children: t("catalog")
5625
+ children: [
5626
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5627
+ type: "button",
5628
+ className: category === null ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5629
+ "aria-pressed": category === null,
5630
+ onClick: () => {
5631
+ setCategory(null);
5632
+ setVisibleCount(48);
5633
+ },
5634
+ children: [
5635
+ t("all"),
5636
+ " ",
5637
+ [...categoryCounts.values()].reduce((a, b) => a + b, 0)
5638
+ ]
5639
+ }),
5640
+ CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5641
+ type: "button",
5642
+ className: category === key ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5643
+ "aria-pressed": category === key,
5644
+ onClick: () => {
5645
+ setCategory(key);
5646
+ setVisibleCount(48);
5647
+ },
5648
+ children: [
5649
+ t(categoryLocaleKey(key)),
5650
+ " ",
5651
+ categoryCounts.get(key) ?? 0
5652
+ ]
5653
+ }, key)),
5654
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5655
+ type: "button",
5656
+ className: category === "installed" ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5657
+ "aria-pressed": category === "installed",
5658
+ onClick: () => {
5659
+ setCategory("installed");
5660
+ setVisibleCount(48);
5661
+ },
5662
+ children: [
5663
+ t("installed"),
5664
+ " ",
5665
+ installedCount
5666
+ ]
5667
+ })
5668
+ ]
5456
5669
  }),
5457
5670
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5458
5671
  className: _dsh_plugin_shop_css_e3675a89_default.catalogStats,
@@ -5472,9 +5685,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5472
5685
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
5473
5686
  entry,
5474
5687
  stars: stars[entry.name],
5688
+ installed: installedByName.get(entry.name),
5475
5689
  t,
5476
5690
  install,
5477
- installStatus
5691
+ installStatus,
5692
+ uninstall
5478
5693
  }) }, entry.name)), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
5479
5694
  ref: sentinelRef,
5480
5695
  className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
@@ -5490,7 +5705,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5490
5705
  })
5491
5706
  })] }),
5492
5707
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
5493
- state: outdatedState,
5708
+ state: installedState,
5494
5709
  tiers,
5495
5710
  t,
5496
5711
  setEnabled,
@@ -5538,7 +5753,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5538
5753
  install: async (args) => unwrap(await ns.installStart(args)),
5539
5754
  installStatus: async (args) => unwrap(await ns.installStatus(args)),
5540
5755
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
5541
- outdated: async () => unwrap(await ns.outdated())
5756
+ installed: async () => unwrap(await ns.installed()),
5757
+ uninstall: async (args) => unwrap(await ns.uninstallStart(args))
5542
5758
  });
5543
5759
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
5544
5760
  name: "settings.plugins.tab",