dsh-plugin-shop 0.4.0 → 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
  ]
@@ -4590,6 +4627,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4590
4627
  const segments = name.toLowerCase().split(/[-_.]+/);
4591
4628
  const hasPlugin = segments.includes("plugin");
4592
4629
  if (segments.some((segment) => /plugin(store|market|mall|shop|marketplace)/.test(segment) || /(store|market|mall|shop|marketplace)plugin/.test(segment))) return true;
4630
+ if (segments.some((segment) => segment.startsWith("dsh") && SHOP_KEYWORDS.some((keyword) => segment === `dsh${keyword}`))) return true;
4631
+ if (segments.length >= 2 && (segments[0] === "dsh" || segments[0]?.endsWith("/dsh") === true) && SHOP_KEYWORDS.includes(segments[1]) && (segments.length === 2 || segments[2] !== void 0 && [
4632
+ "plus",
4633
+ "pro",
4634
+ "hub",
4635
+ "center",
4636
+ "centre",
4637
+ "max",
4638
+ "free",
4639
+ "lite"
4640
+ ].includes(segments[2]))) return true;
4593
4641
  if (segments.filter((segment) => SHOP_KEYWORDS.includes(segment)).length === 0) return false;
4594
4642
  return hasPlugin || SHOP_KEYWORDS.includes(segments.at(-1));
4595
4643
  }
@@ -4606,6 +4654,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4606
4654
  function categoryKey(entry) {
4607
4655
  return CATEGORY_KEYS[entry.catalog?.category ?? "other"];
4608
4656
  }
4657
+ /** The six categories in display order (map insertion order). */
4658
+ const CATEGORY_ORDER = Object.keys(CATEGORY_KEYS);
4659
+ /** The locale key for one bare category value (the filter buttons). */
4660
+ function categoryLocaleKey(category) {
4661
+ return CATEGORY_KEYS[category];
4662
+ }
4609
4663
  /** Sort the shelf: stars descending, un-starred entries last, name ascending
4610
4664
  * (case-insensitive) on ties (spec 2026-08-26-github-stars-design.md D1).
4611
4665
  * Display-time only — the catalog's own name sort is untouched. */
@@ -4632,6 +4686,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4632
4686
  error: "暂时无法读取目录。",
4633
4687
  retry: "重试",
4634
4688
  search: "搜索插件",
4689
+ all: "全部",
4635
4690
  catalog: "插件目录",
4636
4691
  catalogStats: "{count} 个插件 · 构建于 {date}",
4637
4692
  stars: "{count} 星",
@@ -4673,6 +4728,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4673
4728
  installedVersion: "已安装 v{version}",
4674
4729
  latestVersion: "最新 v{version}",
4675
4730
  update: "更新",
4731
+ installed: "已安装",
4732
+ uninstall: "卸载",
4733
+ uninstalling: "正在卸载…",
4734
+ uninstallFailed: "卸载失败",
4735
+ uninstallTransportFailed: "卸载请求未能送达。请稍后重试。",
4736
+ uninstalledRestartNotice: "已卸载;重启 dsh 后生效",
4676
4737
  enabledSwitch: "启用",
4677
4738
  toggleFailed: "设置失败,请重试。",
4678
4739
  hotApplyNote: "无需重启,立即生效"
@@ -4684,6 +4745,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4684
4745
  error: "The catalog is temporarily unavailable.",
4685
4746
  retry: "Retry",
4686
4747
  search: "Search plugins",
4748
+ all: "All",
4687
4749
  catalog: "Plugin catalog",
4688
4750
  catalogStats: "{count} packages · built {date}",
4689
4751
  stars: "{count} stars",
@@ -4725,6 +4787,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4725
4787
  installedVersion: "installed v{version}",
4726
4788
  latestVersion: "latest v{version}",
4727
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",
4728
4796
  enabledSwitch: "Enable",
4729
4797
  toggleFailed: "Failed to update. Please retry.",
4730
4798
  hotApplyNote: "takes effect without a restart"
@@ -4732,6 +4800,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4732
4800
  //#endregion
4733
4801
  //#region src/client/useInstall.ts
4734
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
+ }
4735
4826
  /** Drive one install: rejections, the polling loop at INSTALL_POLL_MS, and
4736
4827
  * terminal states. The interval is cleared on unmount and whenever the view
4737
4828
  * leaves `running`, so a done/failed/rejected install never polls again. */
@@ -4762,18 +4853,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4762
4853
  });
4763
4854
  }
4764
4855
  }, [install]);
4765
- (0, react.useEffect)(() => {
4766
- if (view.kind !== "running") return;
4767
- const timer = setInterval(() => {
4768
- installStatus({ installId: view.installId }).then((status) => {
4769
- setView((current) => reduceInstall(current, {
4770
- type: "status",
4771
- status
4772
- }));
4773
- }, () => {});
4774
- }, INSTALL_POLL_MS);
4775
- return () => clearInterval(timer);
4776
- }, [view, installStatus]);
4856
+ usePollStatus(view, setView, installStatus);
4777
4857
  return {
4778
4858
  view,
4779
4859
  start,
@@ -4781,8 +4861,51 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4781
4861
  };
4782
4862
  }
4783
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
4784
4907
  //#region \0dsh-plugin-shop-css:e3675a89
4785
- 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.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.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";
4786
4909
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
4787
4910
  const tag = document.createElement("style");
4788
4911
  tag.dataset.pluginCss = "e3675a89";
@@ -4808,6 +4931,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4808
4931
  "searchInput": "s232ec4_searchInput",
4809
4932
  "staleBlock": "s1555aa_staleBlock",
4810
4933
  "staleBadge": "s3946f1_staleBadge",
4934
+ "categoryBar": "sb2c859_categoryBar",
4935
+ "categoryButton": "s26d58e_categoryButton",
4936
+ "categoryButtonOn": "s5f68c4_categoryButtonOn",
4811
4937
  "catalogHeading": "s668774_catalogHeading",
4812
4938
  "catalogStats": "sccc4d1_catalogStats",
4813
4939
  "emptyLine": "s6de1ab_emptyLine",
@@ -4829,6 +4955,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4829
4955
  "body": "s820ebf_body",
4830
4956
  "summary": "sb8fbc8_summary",
4831
4957
  "summaryZh": "se507db_summaryZh",
4958
+ "summaryExpanded": "s10baf0_summaryExpanded",
4959
+ "summaryZhExpanded": "sb9dba9_summaryZhExpanded",
4832
4960
  "capabilitiesBlock": "s9d2029_capabilitiesBlock",
4833
4961
  "capabilitiesNote": "s3bdbd5_capabilitiesNote",
4834
4962
  "capabilities": "s441702_capabilities",
@@ -4840,6 +4968,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4840
4968
  "installButton": "se7aaed_installButton",
4841
4969
  "confirmButton": "sb43a49_confirmButton",
4842
4970
  "cancelButton": "s6e45d1_cancelButton",
4971
+ "installedLabel": "sb6fc74_installedLabel",
4972
+ "installedActions": "sf53ca0_installedActions",
4973
+ "uninstallButton": "s96967f_uninstallButton",
4843
4974
  "gate": "sc4f2ac_gate",
4844
4975
  "gateTitle": "s559c38_gateTitle",
4845
4976
  "gateBody": "s3c0ace_gateBody",
@@ -4885,8 +5016,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4885
5016
  /** One entry card: the category spine and cover band, the expandable header
4886
5017
  * (name, tier, category, unclaimed marker), the plain-text summary in both
4887
5018
  * languages, the self-declared capabilities, the detail section, and the
4888
- * install controls. */
4889
- 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 }) {
4890
5023
  const [open, setOpen] = (0, react.useState)(false);
4891
5024
  const detailId = (0, react.useId)();
4892
5025
  const summary = entry.catalog?.summary;
@@ -4943,11 +5076,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4943
5076
  className: _dsh_plugin_shop_css_e3675a89_default.body,
4944
5077
  children: [
4945
5078
  summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4946
- className: _dsh_plugin_shop_css_e3675a89_default.summary,
5079
+ className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summary} ${_dsh_plugin_shop_css_e3675a89_default.summaryExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summary,
4947
5080
  children: summary.en
4948
5081
  }),
4949
5082
  summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4950
- className: _dsh_plugin_shop_css_e3675a89_default.summaryZh,
5083
+ className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summaryZh,
4951
5084
  children: summary.zh
4952
5085
  }),
4953
5086
  entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4992,13 +5125,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4992
5125
  })
4993
5126
  })]
4994
5127
  }),
4995
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
5128
+ installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
4996
5129
  name: entry.name,
4997
5130
  version: entry.version,
4998
5131
  tier: entry.tier,
4999
5132
  t,
5000
5133
  install,
5001
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
+ })]
5002
5155
  })
5003
5156
  ]
5004
5157
  })
@@ -5112,6 +5265,61 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5112
5265
  children: t(update ? "update" : "install")
5113
5266
  });
5114
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
+ }
5115
5323
  /** One outdated install row (§7.3): the name, the installed and latest
5116
5324
  * versions, an optimistic enable/disable switch, and the update button (the
5117
5325
  * install flow for `name@latest`, reusing `InstallPanel`). The switch has no
@@ -5199,10 +5407,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5199
5407
  ]
5200
5408
  });
5201
5409
  }
5202
- /** The §7.3 outdated list, rendered as the "installed" section: each row shows
5203
- * both versions, a switch, and an update button. The tier for the update gate
5204
- * is looked up from the catalog by name (community acknowledgement); an entry
5205
- * 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). */
5206
5417
  function OutdatedSection({ state, tiers, t, setEnabled, install, installStatus }) {
5207
5418
  if (state.kind === "loading") return null;
5208
5419
  if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -5210,7 +5421,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5210
5421
  "data-shop-outdated-error": true,
5211
5422
  children: t("error")
5212
5423
  });
5213
- if (state.entries.length === 0) return null;
5424
+ const outdated = state.entries.filter((entry) => entry.outdated);
5425
+ if (outdated.length === 0) return null;
5214
5426
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
5215
5427
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedSection,
5216
5428
  "data-shop-outdated": true,
@@ -5219,7 +5431,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5219
5431
  children: t("installedSection")
5220
5432
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
5221
5433
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
5222
- 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, {
5223
5435
  row,
5224
5436
  tier: tiers.get(row.name) ?? "community",
5225
5437
  t,
@@ -5233,11 +5445,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5233
5445
  /** The shop tab root: browse, search, refresh, and render one card per
5234
5446
  * entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
5235
5447
  function ShopTab(props) {
5236
- const { t, catalog, install, installStatus, setEnabled, outdated } = props;
5448
+ const { t, catalog, install, installStatus, setEnabled, installed, uninstall } = props;
5237
5449
  const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
5238
- const [outdatedState, setOutdatedState] = (0, react.useState)({ kind: "loading" });
5450
+ const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
5239
5451
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
5240
5452
  const [query, setQuery] = (0, react.useState)("");
5453
+ const [category, setCategory] = (0, react.useState)(null);
5241
5454
  const incremental = typeof IntersectionObserver !== "undefined";
5242
5455
  const [visibleCount, setVisibleCount] = (0, react.useState)(48);
5243
5456
  const sentinelRef = (0, react.useRef)(null);
@@ -5265,31 +5478,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5265
5478
  let cancelled = false;
5266
5479
  const load = async () => {
5267
5480
  try {
5268
- const entries = await outdated();
5269
- if (!cancelled) setOutdatedState({
5481
+ const entries = await installed();
5482
+ if (!cancelled) setInstalledState({
5270
5483
  kind: "ready",
5271
5484
  entries
5272
5485
  });
5273
5486
  } catch {
5274
- if (!cancelled) setOutdatedState({ kind: "error" });
5487
+ if (!cancelled) setInstalledState({ kind: "error" });
5275
5488
  }
5276
5489
  };
5277
5490
  load();
5278
5491
  return () => {
5279
5492
  cancelled = true;
5280
5493
  };
5281
- }, [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]);
5282
5500
  const filtered = (0, react.useMemo)(() => {
5283
5501
  if (catalogState.kind !== "ready") return [];
5284
5502
  const q = query.trim().toLowerCase();
5285
5503
  return catalogState.result.plugins.filter((entry) => {
5286
5504
  if (isShopLike(entry.name)) 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;
5287
5508
  if (q === "") return true;
5288
5509
  const summaryEn = entry.catalog?.summary.en ?? "";
5289
5510
  const summaryZh = entry.catalog?.summary.zh ?? "";
5290
5511
  return entry.name.toLowerCase().includes(q) || summaryEn.toLowerCase().includes(q) || summaryZh.toLowerCase().includes(q);
5291
5512
  });
5292
- }, [catalogState, query]);
5513
+ }, [
5514
+ catalogState,
5515
+ query,
5516
+ category,
5517
+ installedByName
5518
+ ]);
5293
5519
  filteredLenRef.current = filtered.length;
5294
5520
  const stars = catalogState.kind === "ready" ? catalogState.result.stars : {};
5295
5521
  const sorted = (0, react.useMemo)(() => sortByStars(filtered, stars), [filtered, stars]);
@@ -5308,6 +5534,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5308
5534
  filtered.length
5309
5535
  ]);
5310
5536
  const visible = incremental ? sorted.slice(0, visibleCount) : sorted;
5537
+ const categoryCounts = (0, react.useMemo)(() => {
5538
+ const counts = /* @__PURE__ */ new Map();
5539
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) {
5540
+ if (isShopLike(entry.name)) continue;
5541
+ const key = categoryKey(entry);
5542
+ const bare = CATEGORY_ORDER.find((c) => categoryLocaleKey(c) === key);
5543
+ if (bare !== void 0) counts.set(bare, (counts.get(bare) ?? 0) + 1);
5544
+ }
5545
+ return counts;
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]);
5311
5551
  const tiers = (0, react.useMemo)(() => {
5312
5552
  const map = /* @__PURE__ */ new Map();
5313
5553
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
@@ -5378,9 +5618,54 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5378
5618
  })]
5379
5619
  })]
5380
5620
  }),
5381
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5382
- className: _dsh_plugin_shop_css_e3675a89_default.catalogHeading,
5383
- children: t("catalog")
5621
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5622
+ className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
5623
+ role: "group",
5624
+ "aria-label": 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
+ ]
5384
5669
  }),
5385
5670
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5386
5671
  className: _dsh_plugin_shop_css_e3675a89_default.catalogStats,
@@ -5400,9 +5685,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5400
5685
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
5401
5686
  entry,
5402
5687
  stars: stars[entry.name],
5688
+ installed: installedByName.get(entry.name),
5403
5689
  t,
5404
5690
  install,
5405
- installStatus
5691
+ installStatus,
5692
+ uninstall
5406
5693
  }) }, entry.name)), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
5407
5694
  ref: sentinelRef,
5408
5695
  className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
@@ -5418,7 +5705,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5418
5705
  })
5419
5706
  })] }),
5420
5707
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
5421
- state: outdatedState,
5708
+ state: installedState,
5422
5709
  tiers,
5423
5710
  t,
5424
5711
  setEnabled,
@@ -5466,7 +5753,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5466
5753
  install: async (args) => unwrap(await ns.installStart(args)),
5467
5754
  installStatus: async (args) => unwrap(await ns.installStatus(args)),
5468
5755
  setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
5469
- outdated: async () => unwrap(await ns.outdated())
5756
+ installed: async () => unwrap(await ns.installed()),
5757
+ uninstall: async (args) => unwrap(await ns.uninstallStart(args))
5470
5758
  });
5471
5759
  ctx.slots.inject("settings.plugins.tab", () => ctx.slots.register({
5472
5760
  name: "settings.plugins.tab",