dsh-plugin-shop 0.8.7 → 0.9.0-beta.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 +1561 -762
- package/lib/index.js +210 -34
- package/lib/typert.host.js +12 -12
- package/lib/typert.remote-client.js +11 -11
- package/lib/types/client/ShopDetailAction.d.ts +46 -0
- package/lib/types/client/ShopGlyph.d.ts +12 -0
- package/lib/types/client/ShopPage.d.ts +27 -0
- package/lib/types/client/ShopTab.d.ts +27 -10
- package/lib/types/client/flow-registry.d.ts +89 -0
- package/lib/types/client/index.d.ts +52 -7
- package/lib/types/client/listeners.d.ts +27 -0
- package/lib/types/client/locales.d.ts +17 -5
- package/lib/types/client/page-store.d.ts +255 -0
- package/lib/types/client/present.d.ts +35 -13
- package/lib/types/client/restart-monitor.d.ts +11 -0
- package/lib/types/client/useFlows.d.ts +34 -26
- package/lib/types/client/useUpdateSelf.d.ts +19 -16
- package/lib/types/host/harness.d.ts +26 -2
- package/lib/types/host/index.d.ts +39 -6
- package/lib/types/host/manager-runner.d.ts +24 -0
- package/lib/types/host/plugin-manager.d.ts +5 -0
- package/package.json +1 -1
- package/lib/types/client/ShopSection.d.ts +0 -10
- package/lib/types/client/useInstall.d.ts +0 -31
- package/lib/types/client/useUninstall.d.ts +0 -28
package/lib/client.js
CHANGED
|
@@ -4490,6 +4490,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4490
4490
|
"restartBlocked": union([
|
|
4491
4491
|
literal(null),
|
|
4492
4492
|
literal("desktop"),
|
|
4493
|
+
literal("from-default-profile"),
|
|
4493
4494
|
literal("windows"),
|
|
4494
4495
|
literal("systemd"),
|
|
4495
4496
|
literal("port-zero")
|
|
@@ -4530,7 +4531,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4530
4531
|
},
|
|
4531
4532
|
sourceLocation: {
|
|
4532
4533
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4533
|
-
"line":
|
|
4534
|
+
"line": 1301,
|
|
4534
4535
|
"column": 9
|
|
4535
4536
|
}
|
|
4536
4537
|
},
|
|
@@ -4549,7 +4550,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4549
4550
|
},
|
|
4550
4551
|
sourceLocation: {
|
|
4551
4552
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4552
|
-
"line":
|
|
4553
|
+
"line": 1804,
|
|
4553
4554
|
"column": 9
|
|
4554
4555
|
}
|
|
4555
4556
|
},
|
|
@@ -4568,7 +4569,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4568
4569
|
},
|
|
4569
4570
|
sourceLocation: {
|
|
4570
4571
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4571
|
-
"line":
|
|
4572
|
+
"line": 1795,
|
|
4572
4573
|
"column": 9
|
|
4573
4574
|
}
|
|
4574
4575
|
},
|
|
@@ -4598,7 +4599,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4598
4599
|
},
|
|
4599
4600
|
sourceLocation: {
|
|
4600
4601
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4601
|
-
"line":
|
|
4602
|
+
"line": 1427,
|
|
4602
4603
|
"column": 9
|
|
4603
4604
|
}
|
|
4604
4605
|
},
|
|
@@ -4627,7 +4628,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4627
4628
|
},
|
|
4628
4629
|
sourceLocation: {
|
|
4629
4630
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4630
|
-
"line":
|
|
4631
|
+
"line": 1771,
|
|
4631
4632
|
"column": 3
|
|
4632
4633
|
}
|
|
4633
4634
|
},
|
|
@@ -4646,7 +4647,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4646
4647
|
},
|
|
4647
4648
|
sourceLocation: {
|
|
4648
4649
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4649
|
-
"line":
|
|
4650
|
+
"line": 2055,
|
|
4650
4651
|
"column": 9
|
|
4651
4652
|
}
|
|
4652
4653
|
},
|
|
@@ -4675,7 +4676,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4675
4676
|
},
|
|
4676
4677
|
sourceLocation: {
|
|
4677
4678
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4678
|
-
"line":
|
|
4679
|
+
"line": 953,
|
|
4679
4680
|
"column": 9
|
|
4680
4681
|
}
|
|
4681
4682
|
},
|
|
@@ -4705,7 +4706,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4705
4706
|
},
|
|
4706
4707
|
sourceLocation: {
|
|
4707
4708
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4708
|
-
"line":
|
|
4709
|
+
"line": 1949,
|
|
4709
4710
|
"column": 9
|
|
4710
4711
|
}
|
|
4711
4712
|
},
|
|
@@ -4734,7 +4735,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4734
4735
|
},
|
|
4735
4736
|
sourceLocation: {
|
|
4736
4737
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4737
|
-
"line":
|
|
4738
|
+
"line": 2162,
|
|
4738
4739
|
"column": 9
|
|
4739
4740
|
}
|
|
4740
4741
|
},
|
|
@@ -4753,7 +4754,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4753
4754
|
},
|
|
4754
4755
|
sourceLocation: {
|
|
4755
4756
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4756
|
-
"line":
|
|
4757
|
+
"line": 2119,
|
|
4757
4758
|
"column": 9
|
|
4758
4759
|
}
|
|
4759
4760
|
}
|
|
@@ -4761,17 +4762,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4761
4762
|
};
|
|
4762
4763
|
//#endregion
|
|
4763
4764
|
//#region src/client/locales.ts
|
|
4764
|
-
/** Copy dictionaries for the shop's
|
|
4765
|
+
/** Copy dictionaries for the shop's page in dsh's main column, its row in the
|
|
4766
|
+
* sidebar, and its two buttons on dsh's Plugins page. */
|
|
4765
4767
|
/** Simplified Chinese dictionary and key source of truth. */
|
|
4766
4768
|
const zh = {
|
|
4767
|
-
/** The shop's name
|
|
4768
|
-
*
|
|
4769
|
+
/** The shop's name: its sidebar row and its page title (design
|
|
4770
|
+
* 2026-10-16-shop-sidebar-page §4.1), one key so the two cannot drift
|
|
4771
|
+
* apart. */
|
|
4769
4772
|
title: "插件商店",
|
|
4770
|
-
/** The
|
|
4771
|
-
* as the built-in Models and 内置插件 pages carry one under their title. */
|
|
4773
|
+
/** The page's intro line, under its title, as dsh's own pages carry one. */
|
|
4772
4774
|
intro: "浏览、安装和更新目录中的插件。",
|
|
4775
|
+
/** The Plugins page's button on the shop's own bundle page (design
|
|
4776
|
+
* 2026-10-16-shop-sidebar-page §7). */
|
|
4777
|
+
openShop: "打开插件商店",
|
|
4778
|
+
/** Its button on a page of a bundle the shelf lists. */
|
|
4779
|
+
viewInShop: "在插件商店中查看",
|
|
4773
4780
|
loading: "正在读取目录…",
|
|
4774
4781
|
error: "暂时无法读取目录。",
|
|
4782
|
+
installedLoading: "正在读取已安装列表…",
|
|
4775
4783
|
installedError: "暂时无法读取已安装列表。",
|
|
4776
4784
|
retry: "重试",
|
|
4777
4785
|
/** The shop's own error boundary caught a render error; the error's message
|
|
@@ -4942,6 +4950,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4942
4950
|
notRestartedPidNotice: "dsh 没有重启:页面刷新后,回答的仍是被要求重启的那个进程(pid {pid}),所以更改还没有生效。请手动重启:运行 kill {pid}(进程仍不退出时用 kill -9 {pid}),再运行 dsh web。",
|
|
4943
4951
|
notRestartedLogNotice: "这次重启的经过记录在 {log}。",
|
|
4944
4952
|
restartBlockedDesktopNotice: "此 profile 由 DeepSeek Harness 桌面版管理,商店无法重启它。请重启桌面版,以应用更改。",
|
|
4953
|
+
restartBlockedFromDefaultProfileNotice: "此 dsh 以 --from-default-profile 启动。从 dsh 0.2.1 起,重启会按模板重建 profile,删除其中安装的全部插件,包括商店。请去掉该参数后手动重启 dsh,以应用更改。",
|
|
4945
4954
|
restartBlockedWindowsNotice: "商店暂不支持在 Windows 上重启 dsh。请退出 dsh 后重新启动,以应用更改。",
|
|
4946
4955
|
restartBlockedSystemdNotice: "当前 dsh 由 systemd 服务托管,商店无法安全地重启它。请手动重启服务,或在商店行配置中设置 allowRestart: true。",
|
|
4947
4956
|
restartBlockedPortZeroNotice: "此 dsh 以 --port 0 启动,重启后会绑定到另一个端口,本页面无法跟上。请手动重启 dsh,以应用更改。",
|
|
@@ -4960,8 +4969,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4960
4969
|
const en = {
|
|
4961
4970
|
title: "Plugin shop",
|
|
4962
4971
|
intro: "Browse, install, and update plugins from the catalog.",
|
|
4972
|
+
openShop: "Open Plugin shop",
|
|
4973
|
+
viewInShop: "View in Plugin shop",
|
|
4963
4974
|
loading: "Reading the catalog…",
|
|
4964
4975
|
error: "The catalog is temporarily unavailable.",
|
|
4976
|
+
installedLoading: "Reading the installed list…",
|
|
4965
4977
|
installedError: "The installed list is temporarily unavailable.",
|
|
4966
4978
|
retry: "Retry",
|
|
4967
4979
|
tabCrashed: "The plugin shop hit an error while showing this page.",
|
|
@@ -5082,6 +5094,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5082
5094
|
notRestartedPidNotice: "dsh did not restart: after the reload, the process that was asked to restart (pid {pid}) is still the one answering, so the change has not taken effect. Restart it by hand: run kill {pid} (kill -9 {pid} if it does not exit), then run dsh web.",
|
|
5083
5095
|
notRestartedLogNotice: "What this restart did is logged in {log}.",
|
|
5084
5096
|
restartBlockedDesktopNotice: "This profile is managed by the DeepSeek Harness desktop app, so the shop cannot restart it. Restart the app to apply the change.",
|
|
5097
|
+
restartBlockedFromDefaultProfileNotice: "This dsh was launched with --from-default-profile. From dsh 0.2.1 a restart would recreate the profile from its template and delete every plugin installed in it, this shop included. Restart dsh manually without that flag to apply the change.",
|
|
5085
5098
|
restartBlockedWindowsNotice: "The shop cannot restart dsh on Windows yet. Quit dsh and start it again to apply the change.",
|
|
5086
5099
|
restartBlockedSystemdNotice: "This dsh process runs as a systemd service, so the shop cannot restart it safely. Restart the service manually, or set allowRestart: true in the shop row config.",
|
|
5087
5100
|
restartBlockedPortZeroNotice: "This dsh was launched with --port 0, so a restart would bind a different port and this page could not follow it. Restart dsh manually to apply the change.",
|
|
@@ -5284,6 +5297,57 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5284
5297
|
return refineIncompatible(incompatible, withRemoved, timeoutMs);
|
|
5285
5298
|
}
|
|
5286
5299
|
//#endregion
|
|
5300
|
+
//#region src/shared/install-state.ts
|
|
5301
|
+
/** Whether the host is done with this record: it will not change again, and a
|
|
5302
|
+
* poller may stop. */
|
|
5303
|
+
function isTerminalInstallState(state) {
|
|
5304
|
+
return state === "done" || state === "failed";
|
|
5305
|
+
}
|
|
5306
|
+
//#endregion
|
|
5307
|
+
//#region src/client/listeners.ts
|
|
5308
|
+
/**
|
|
5309
|
+
* The listener plumbing the client's external stores share: the page store
|
|
5310
|
+
* and its owed notices (page-store.ts), the flow registries
|
|
5311
|
+
* (flow-registry.ts) and the restart monitor (restart-monitor.ts). Each keeps
|
|
5312
|
+
* its listeners in a plain `Set`. React subscribes through `listen`, as
|
|
5313
|
+
* `useSyncExternalStore` asks, and a change reaches every listener through
|
|
5314
|
+
* `notify`.
|
|
5315
|
+
*
|
|
5316
|
+
* Functions over that `Set`, never an object a store keeps in its place. The
|
|
5317
|
+
* restart monitor's store lives on `globalThis`, and a 0.8.x shop hands it
|
|
5318
|
+
* over (design 2026-10-16-shop-sidebar-page §6). Its `listeners` is a plain
|
|
5319
|
+
* `Set` that the predecessor's own code adds to, deletes from and iterates,
|
|
5320
|
+
* so the field's shape is a contract across versions, and these functions
|
|
5321
|
+
* take that `Set` as it is.
|
|
5322
|
+
*/
|
|
5323
|
+
/** The sets a disposed store forgot (`forget`). Held here, never as a mark
|
|
5324
|
+
* on the `Set` itself: a store's `Set` is its own, and the restart monitor's
|
|
5325
|
+
* is a 0.8.x predecessor's too (above). */
|
|
5326
|
+
const forgotten = /* @__PURE__ */ new WeakSet();
|
|
5327
|
+
/** Add `listener`, and hand back what removes it. A forgotten set takes
|
|
5328
|
+
* none: its store tells nobody anything more, so a listener subscribing in
|
|
5329
|
+
* a hot swap's window would only be held. */
|
|
5330
|
+
function listen(listeners, listener) {
|
|
5331
|
+
if (forgotten.has(listeners)) return () => {};
|
|
5332
|
+
listeners.add(listener);
|
|
5333
|
+
return () => {
|
|
5334
|
+
listeners.delete(listener);
|
|
5335
|
+
};
|
|
5336
|
+
}
|
|
5337
|
+
/** Call every listener as the set stands when the call begins. Over a copy,
|
|
5338
|
+
* so a listener that subscribes or unsubscribes one while it runs (React
|
|
5339
|
+
* re-subscribing in the render the change caused) changes the next notify,
|
|
5340
|
+
* never this one. */
|
|
5341
|
+
function notify(listeners) {
|
|
5342
|
+
for (const listener of [...listeners]) listener();
|
|
5343
|
+
}
|
|
5344
|
+
/** Drop every listener, and take none again: a disposed store tells nobody
|
|
5345
|
+
* anything more. */
|
|
5346
|
+
function forget(listeners) {
|
|
5347
|
+
listeners.clear();
|
|
5348
|
+
forgotten.add(listeners);
|
|
5349
|
+
}
|
|
5350
|
+
//#endregion
|
|
5287
5351
|
//#region src/shared/shop-like.ts
|
|
5288
5352
|
/**
|
|
5289
5353
|
* Whether a package name reads as a competing plugin marketplace.
|
|
@@ -6851,13 +6915,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6851
6915
|
return spec.length > HOLDER_SPEC_CAP ? `${spec.slice(0, 79)}…` : spec;
|
|
6852
6916
|
}
|
|
6853
6917
|
//#endregion
|
|
6854
|
-
//#region src/shared/install-state.ts
|
|
6855
|
-
/** Whether the host is done with this record: it will not change again, and a
|
|
6856
|
-
* poller may stop. */
|
|
6857
|
-
function isTerminalInstallState(state) {
|
|
6858
|
-
return state === "done" || state === "failed";
|
|
6859
|
-
}
|
|
6860
|
-
//#endregion
|
|
6861
6918
|
//#region src/client/present.ts
|
|
6862
6919
|
/** The done notice for one activation, as a locale key.
|
|
6863
6920
|
*
|
|
@@ -6903,15 +6960,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6903
6960
|
/** Why the shop cannot restart dsh, as a locale key.
|
|
6904
6961
|
*
|
|
6905
6962
|
* A sibling of `activationNoticeKey` and exhaustive for the same reason: the
|
|
6906
|
-
* switch has no default, so a
|
|
6907
|
-
*
|
|
6908
|
-
*
|
|
6963
|
+
* switch has no default, so a reason added to `RestartBlockedReason` is a type
|
|
6964
|
+
* error here rather than a card that silently keeps a neighbour's copy. That
|
|
6965
|
+
* is the whole point of the type — the single boolean this replaced
|
|
6909
6966
|
* had ONE string for three causes, and it named systemd, so a Windows reader
|
|
6910
6967
|
* was told to restart a service they do not run and to set an override that
|
|
6911
6968
|
* could not have helped. */
|
|
6912
6969
|
function restartBlockedNoticeKey(reason) {
|
|
6913
6970
|
switch (reason) {
|
|
6914
6971
|
case "desktop": return "restartBlockedDesktopNotice";
|
|
6972
|
+
case "from-default-profile": return "restartBlockedFromDefaultProfileNotice";
|
|
6915
6973
|
case "windows": return "restartBlockedWindowsNotice";
|
|
6916
6974
|
case "systemd": return "restartBlockedSystemdNotice";
|
|
6917
6975
|
case "port-zero": return "restartBlockedPortZeroNotice";
|
|
@@ -7025,14 +7083,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7025
7083
|
function refusesInstall(blockers) {
|
|
7026
7084
|
return blockers.some((blocker) => blocker.kind === "name-taken" || blocker.kind === "harness-peers");
|
|
7027
7085
|
}
|
|
7086
|
+
/** The key dsh 0.2.1 lists a package's own `engines.dsh` range under, among
|
|
7087
|
+
* the requirements it refuses (design 2026-10-19-dsh-021-readiness, section
|
|
7088
|
+
* 4). No card receives it yet: the prediction judges the catalog's `dshPeers`
|
|
7089
|
+
* alone, and the catalog carries no `engines` (that section's known gap). A
|
|
7090
|
+
* refusal dsh raises at install time names the key in the install's own detail
|
|
7091
|
+
* (`versionRefusalDetail`), which never passes through here. So look for no
|
|
7092
|
+
* producer in `peerVerdictsOf`; this waits for the catalog to carry `engines`. */
|
|
7093
|
+
const ENGINES_DSH = "engines.dsh";
|
|
7028
7094
|
/**
|
|
7029
|
-
* The refused
|
|
7030
|
-
* the package declared, in the order dsh reported them. A
|
|
7031
|
-
*
|
|
7032
|
-
* it
|
|
7095
|
+
* The refused requirements as the refusal's copy names them: each one and the
|
|
7096
|
+
* range the package declared, in the order dsh reported them. A peer is named
|
|
7097
|
+
* by its package. `engines.dsh` is a manifest path, not a name anyone asked
|
|
7098
|
+
* for, so it reads as `dsh` and its range, the way the sentence says "it
|
|
7099
|
+
* requires dsh >=0.3.0". A blank range — which dsh refuses — is quoted, or the
|
|
7100
|
+
* sentence would name the requirement and nothing it asked for.
|
|
7033
7101
|
*/
|
|
7034
7102
|
function refusedPeersText(refused) {
|
|
7035
|
-
return Object.entries(refused).map(([name, range]) => `${name} ${range.trim() === "" ? JSON.stringify(range) : range}`).join(", ");
|
|
7103
|
+
return Object.entries(refused).map(([name, range]) => `${name === ENGINES_DSH ? "dsh" : name} ${range.trim() === "" ? JSON.stringify(range) : range}`).join(", ");
|
|
7036
7104
|
}
|
|
7037
7105
|
/**
|
|
7038
7106
|
* The plugin already holding this entry's bundle name, if a DIFFERENT one is,
|
|
@@ -7187,11 +7255,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7187
7255
|
return previous.installed !== next.installed;
|
|
7188
7256
|
}
|
|
7189
7257
|
/**
|
|
7258
|
+
* Whether an update of the shop has landed and waits for a restart: the
|
|
7259
|
+
* profile holds a version other than the one running. A host older than the
|
|
7260
|
+
* field sends none, and that reads as waiting: a client newer than its host
|
|
7261
|
+
* exists only because the disk holds a newer shop than the process running
|
|
7262
|
+
* (design §7.3, 2026-09-27 amendment). The tab's restart offer and the
|
|
7263
|
+
* hand-over after a hot swap (design 2026-10-16-shop-sidebar-page §6) read
|
|
7264
|
+
* it alike.
|
|
7265
|
+
*/
|
|
7266
|
+
function updateAwaitsRestart(version) {
|
|
7267
|
+
return version.pendingVersion !== null;
|
|
7268
|
+
}
|
|
7269
|
+
/**
|
|
7190
7270
|
* Whether dsh restarting under the page settles this receipt: a change that
|
|
7191
|
-
* landed and was waiting for exactly that. The
|
|
7192
|
-
*
|
|
7193
|
-
*
|
|
7194
|
-
* running settles by
|
|
7271
|
+
* landed and was waiting for exactly that. The page store clears these
|
|
7272
|
+
* (page-store.ts, `noteRestarted`) once the answers come from another
|
|
7273
|
+
* process, and nothing else — a reload is still owed by a page nobody
|
|
7274
|
+
* reloaded, a failure still happened, and a flow still running settles by
|
|
7275
|
+
* its own poll.
|
|
7195
7276
|
*/
|
|
7196
7277
|
function settledByRestart(view) {
|
|
7197
7278
|
return view.kind === "done" && view.activation === "restart";
|
|
@@ -7380,6 +7461,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7380
7461
|
return identityKey(entry);
|
|
7381
7462
|
}
|
|
7382
7463
|
/**
|
|
7464
|
+
* Whether the shelf lists `entry`: a name the catalog's `notAShop` clears,
|
|
7465
|
+
* or one whose name and repository `isShopLike` leaves alone. One rule for
|
|
7466
|
+
* the shelf and for the Plugins page's "View in Plugin shop" (design
|
|
7467
|
+
* 2026-10-16-shop-sidebar-page §7), so that button never sends a reader to a
|
|
7468
|
+
* shelf that hides the plugin.
|
|
7469
|
+
*/
|
|
7470
|
+
function listedOnShelf(entry, cleared) {
|
|
7471
|
+
return cleared.has(entry.name) || !isShopLike(entry.name) && (entry.repo === void 0 || !isShopLike(entry.repo));
|
|
7472
|
+
}
|
|
7473
|
+
/**
|
|
7383
7474
|
* One entry's star count from the sidecar, or undefined when it has none.
|
|
7384
7475
|
*
|
|
7385
7476
|
* The sidecar keys a github entry by its repo full name and an npm entry by
|
|
@@ -7534,28 +7625,258 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7534
7625
|
return `${value.toFixed(1)} ${unit}`;
|
|
7535
7626
|
}
|
|
7536
7627
|
//#endregion
|
|
7628
|
+
//#region src/client/flow-registry.ts
|
|
7629
|
+
/** The one idle view, so a key the registry holds no flow for and a key it
|
|
7630
|
+
* reset to idle hand a memoized consumer the same object. */
|
|
7631
|
+
const IDLE_VIEW = { kind: "idle" };
|
|
7632
|
+
/** `map` without every entry `drop` selects, or null when it selects none.
|
|
7633
|
+
* The copy is made at the first match, so a prune that matches nothing
|
|
7634
|
+
* copies nothing, and its caller, handed null, publishes nothing. The
|
|
7635
|
+
* registry's `resetWhere` and the page store's owed notices share this
|
|
7636
|
+
* contract, so a fourth prune cannot return an empty map instead. */
|
|
7637
|
+
function pruned(map, drop) {
|
|
7638
|
+
let next = null;
|
|
7639
|
+
for (const [key, value] of map) {
|
|
7640
|
+
if (!drop(value, key)) continue;
|
|
7641
|
+
next ??= new Map(map);
|
|
7642
|
+
next.delete(key);
|
|
7643
|
+
}
|
|
7644
|
+
return next;
|
|
7645
|
+
}
|
|
7646
|
+
/**
|
|
7647
|
+
* @param deps read on every call, so an owner can hand over its latest
|
|
7648
|
+
* functions without rebuilding the registry.
|
|
7649
|
+
*/
|
|
7650
|
+
function createFlowRegistry(deps, options) {
|
|
7651
|
+
let snapshot = {
|
|
7652
|
+
views: /* @__PURE__ */ new Map(),
|
|
7653
|
+
starting: /* @__PURE__ */ new Set()
|
|
7654
|
+
};
|
|
7655
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7656
|
+
const startedWith = /* @__PURE__ */ new Map();
|
|
7657
|
+
let timer = null;
|
|
7658
|
+
let disposed = false;
|
|
7659
|
+
const syncTimer = () => {
|
|
7660
|
+
const wanted = !disposed && (options.pollUnwatched || listeners.size > 0) && [...snapshot.views.values()].some((view) => view.kind === "running");
|
|
7661
|
+
if (wanted && timer === null) timer = setInterval(poll, INSTALL_POLL_MS);
|
|
7662
|
+
else if (!wanted && timer !== null) {
|
|
7663
|
+
clearInterval(timer);
|
|
7664
|
+
timer = null;
|
|
7665
|
+
}
|
|
7666
|
+
};
|
|
7667
|
+
const publish = (next) => {
|
|
7668
|
+
if (disposed) return;
|
|
7669
|
+
snapshot = next;
|
|
7670
|
+
syncTimer();
|
|
7671
|
+
notify(listeners);
|
|
7672
|
+
};
|
|
7673
|
+
const withView = (key, view) => {
|
|
7674
|
+
const views = new Map(snapshot.views);
|
|
7675
|
+
views.set(key, view);
|
|
7676
|
+
return views;
|
|
7677
|
+
};
|
|
7678
|
+
let asked = 0;
|
|
7679
|
+
const folded = /* @__PURE__ */ new Map();
|
|
7680
|
+
function poll() {
|
|
7681
|
+
for (const [key, view] of snapshot.views) {
|
|
7682
|
+
if (view.kind !== "running") continue;
|
|
7683
|
+
const { installId } = view;
|
|
7684
|
+
asked += 1;
|
|
7685
|
+
const number = asked;
|
|
7686
|
+
deps().installStatus({ installId }).then((status) => {
|
|
7687
|
+
const current = snapshot.views.get(key);
|
|
7688
|
+
if (disposed || current === void 0 || current.kind !== "running" || current.installId !== installId) return;
|
|
7689
|
+
if (number <= (folded.get(key) ?? 0)) return;
|
|
7690
|
+
folded.set(key, number);
|
|
7691
|
+
const next = reduceInstall(current, {
|
|
7692
|
+
type: "status",
|
|
7693
|
+
status
|
|
7694
|
+
});
|
|
7695
|
+
if (next !== current) publish({
|
|
7696
|
+
views: withView(key, next),
|
|
7697
|
+
starting: snapshot.starting
|
|
7698
|
+
});
|
|
7699
|
+
if (next.kind === "running") return;
|
|
7700
|
+
folded.delete(key);
|
|
7701
|
+
const args = startedWith.get(key);
|
|
7702
|
+
startedWith.delete(key);
|
|
7703
|
+
if (status.found && isTerminalInstallState(status.state) && args !== void 0) deps().onSettled?.(key, status.state, args);
|
|
7704
|
+
}, () => {});
|
|
7705
|
+
}
|
|
7706
|
+
}
|
|
7707
|
+
return {
|
|
7708
|
+
getSnapshot: () => snapshot,
|
|
7709
|
+
subscribe: (listener) => {
|
|
7710
|
+
const unlisten = listen(listeners, listener);
|
|
7711
|
+
syncTimer();
|
|
7712
|
+
return () => {
|
|
7713
|
+
unlisten();
|
|
7714
|
+
syncTimer();
|
|
7715
|
+
};
|
|
7716
|
+
},
|
|
7717
|
+
start: async (key, args) => {
|
|
7718
|
+
if (disposed || snapshot.starting.has(key)) return;
|
|
7719
|
+
startedWith.set(key, args);
|
|
7720
|
+
folded.delete(key);
|
|
7721
|
+
publish({
|
|
7722
|
+
views: withView(key, IDLE_VIEW),
|
|
7723
|
+
starting: new Set(snapshot.starting).add(key)
|
|
7724
|
+
});
|
|
7725
|
+
try {
|
|
7726
|
+
const view = await deps().begin(args);
|
|
7727
|
+
if (view.kind !== "running") startedWith.delete(key);
|
|
7728
|
+
publish({
|
|
7729
|
+
views: withView(key, view),
|
|
7730
|
+
starting: snapshot.starting
|
|
7731
|
+
});
|
|
7732
|
+
} finally {
|
|
7733
|
+
if (snapshot.starting.has(key)) {
|
|
7734
|
+
const starting = new Set(snapshot.starting);
|
|
7735
|
+
starting.delete(key);
|
|
7736
|
+
publish({
|
|
7737
|
+
views: snapshot.views,
|
|
7738
|
+
starting
|
|
7739
|
+
});
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
},
|
|
7743
|
+
reset: (key) => {
|
|
7744
|
+
if (!snapshot.starting.has(key)) startedWith.delete(key);
|
|
7745
|
+
folded.delete(key);
|
|
7746
|
+
if (!snapshot.views.has(key)) return;
|
|
7747
|
+
const views = new Map(snapshot.views);
|
|
7748
|
+
views.delete(key);
|
|
7749
|
+
publish({
|
|
7750
|
+
views,
|
|
7751
|
+
starting: snapshot.starting
|
|
7752
|
+
});
|
|
7753
|
+
},
|
|
7754
|
+
resetWhere: (predicate) => {
|
|
7755
|
+
const views = pruned(snapshot.views, predicate);
|
|
7756
|
+
if (views === null) return;
|
|
7757
|
+
for (const key of snapshot.views.keys()) {
|
|
7758
|
+
if (views.has(key)) continue;
|
|
7759
|
+
folded.delete(key);
|
|
7760
|
+
if (!snapshot.starting.has(key)) startedWith.delete(key);
|
|
7761
|
+
}
|
|
7762
|
+
publish({
|
|
7763
|
+
views,
|
|
7764
|
+
starting: snapshot.starting
|
|
7765
|
+
});
|
|
7766
|
+
},
|
|
7767
|
+
dispose: () => {
|
|
7768
|
+
disposed = true;
|
|
7769
|
+
startedWith.clear();
|
|
7770
|
+
folded.clear();
|
|
7771
|
+
forget(listeners);
|
|
7772
|
+
syncTimer();
|
|
7773
|
+
}
|
|
7774
|
+
};
|
|
7775
|
+
}
|
|
7776
|
+
/** The transport rule the three mappings share: a thrown start is a TRANSPORT
|
|
7777
|
+
* failure (index.ts's `unwrap` throws the prefixed wire code and message), not a
|
|
7778
|
+
* business rejection — `rejected` stays reserved for the host's ShopInstallResult
|
|
7779
|
+
* union (§7.2). The transport detail is private (it can name hosts and ports) and
|
|
7780
|
+
* never rendered, so the view carries an EMPTY detail and the surface falls back
|
|
7781
|
+
* to its localized transport line. Nothing else can reach these catches, because
|
|
7782
|
+
* every business union is a resolved value, never a throw. */
|
|
7783
|
+
const transportFailed = () => ({
|
|
7784
|
+
kind: "failed",
|
|
7785
|
+
detail: "",
|
|
7786
|
+
log: []
|
|
7787
|
+
});
|
|
7788
|
+
/** An install's start: the host's business union mapped to the first view. */
|
|
7789
|
+
function beginInstall(install) {
|
|
7790
|
+
return async (args) => {
|
|
7791
|
+
try {
|
|
7792
|
+
const result = await install(args);
|
|
7793
|
+
if (!result.ok) return {
|
|
7794
|
+
kind: "rejected",
|
|
7795
|
+
code: result.code,
|
|
7796
|
+
detail: result.detail
|
|
7797
|
+
};
|
|
7798
|
+
return {
|
|
7799
|
+
kind: "running",
|
|
7800
|
+
installId: result.installId,
|
|
7801
|
+
log: [],
|
|
7802
|
+
phase: phaseOf(result.state)
|
|
7803
|
+
};
|
|
7804
|
+
} catch {
|
|
7805
|
+
return transportFailed();
|
|
7806
|
+
}
|
|
7807
|
+
};
|
|
7808
|
+
}
|
|
7809
|
+
/** A business start: the uninstall's and the shop's own update's. Its
|
|
7810
|
+
* refusal is a `failed` view with the host's published detail, never an
|
|
7811
|
+
* install `rejected` code: those belong to the install gate, and these
|
|
7812
|
+
* unions' refusals carry none. A throw is the transport rule above. The two
|
|
7813
|
+
* starts differ only in the phase they open on, which is `phaseFor`. */
|
|
7814
|
+
function beginBusiness(rpc, phaseFor) {
|
|
7815
|
+
return async (args) => {
|
|
7816
|
+
try {
|
|
7817
|
+
const result = await rpc(args);
|
|
7818
|
+
if (!result.ok) return {
|
|
7819
|
+
kind: "failed",
|
|
7820
|
+
detail: result.detail,
|
|
7821
|
+
log: []
|
|
7822
|
+
};
|
|
7823
|
+
return {
|
|
7824
|
+
kind: "running",
|
|
7825
|
+
installId: result.installId,
|
|
7826
|
+
log: [],
|
|
7827
|
+
phase: phaseFor(result)
|
|
7828
|
+
};
|
|
7829
|
+
} catch {
|
|
7830
|
+
return transportFailed();
|
|
7831
|
+
}
|
|
7832
|
+
};
|
|
7833
|
+
}
|
|
7834
|
+
/** An uninstall's start. Its refusals: not in the catalog, not installed.
|
|
7835
|
+
* The host reports no phase for an uninstall, which opens on `installing`. */
|
|
7836
|
+
function beginUninstall(uninstall) {
|
|
7837
|
+
return beginBusiness(uninstall, () => "installing");
|
|
7838
|
+
}
|
|
7839
|
+
/** The shop's own update's start. Its refusal: a version that is not plain
|
|
7840
|
+
* semver. */
|
|
7841
|
+
function beginUpdate(updateStart) {
|
|
7842
|
+
return beginBusiness(updateStart, (started) => phaseOf(started.state));
|
|
7843
|
+
}
|
|
7844
|
+
//#endregion
|
|
7537
7845
|
//#region src/client/restart-monitor.ts
|
|
7538
7846
|
/** Registry symbol, so every module instance on the page finds the same store. */
|
|
7539
|
-
const STORE_KEY = Symbol.for("dsh-plugin-shop.restart-monitor");
|
|
7847
|
+
const STORE_KEY$1 = Symbol.for("dsh-plugin-shop.restart-monitor");
|
|
7540
7848
|
function monitorStore() {
|
|
7541
7849
|
const page = globalThis;
|
|
7542
|
-
let stored = page[STORE_KEY];
|
|
7850
|
+
let stored = page[STORE_KEY$1];
|
|
7543
7851
|
if (stored === void 0) {
|
|
7544
7852
|
stored = {
|
|
7545
7853
|
state: null,
|
|
7546
7854
|
listeners: /* @__PURE__ */ new Set(),
|
|
7547
7855
|
stop: null
|
|
7548
7856
|
};
|
|
7549
|
-
page[STORE_KEY] = stored;
|
|
7857
|
+
page[STORE_KEY$1] = stored;
|
|
7550
7858
|
}
|
|
7551
7859
|
return Object.assign(stored, {
|
|
7552
7860
|
carriers: stored.carriers ?? [],
|
|
7553
7861
|
attempt: stored.attempt ?? null
|
|
7554
7862
|
});
|
|
7555
7863
|
}
|
|
7864
|
+
/**
|
|
7865
|
+
* Whether a shop instance on this page has registered its restart path:
|
|
7866
|
+
* every `apply` since 0.8.4 does so before anything can ask for a restart.
|
|
7867
|
+
* Read before this instance's own `apply` registers one, a store here names
|
|
7868
|
+
* a predecessor — a 0.8.x shop when no page store sits beside it
|
|
7869
|
+
* (page-store.ts `readPredecessor`; design 2026-10-16-shop-sidebar-page §6).
|
|
7870
|
+
* The store outlives a carrier's withdrawal: a predecessor's teardown
|
|
7871
|
+
* withdraws its carrier and leaves the store, and the store is what names it.
|
|
7872
|
+
* Only a read: unlike `monitorStore`, it never creates the store.
|
|
7873
|
+
*/
|
|
7874
|
+
function hasRestartMonitorStore() {
|
|
7875
|
+
return globalThis[STORE_KEY$1] !== void 0;
|
|
7876
|
+
}
|
|
7556
7877
|
function settle(store, state) {
|
|
7557
7878
|
store.state = state;
|
|
7558
|
-
|
|
7879
|
+
notify(store.listeners);
|
|
7559
7880
|
}
|
|
7560
7881
|
/** sessionStorage key of the restart this tab last saw committed. Not the
|
|
7561
7882
|
* page store: the verdict belongs to the page the reload produces, and a
|
|
@@ -7606,11 +7927,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7606
7927
|
}
|
|
7607
7928
|
/** Be told when the page's restart changes state. */
|
|
7608
7929
|
function subscribeRestartMonitor(listener) {
|
|
7609
|
-
|
|
7610
|
-
store.listeners.add(listener);
|
|
7611
|
-
return () => {
|
|
7612
|
-
store.listeners.delete(listener);
|
|
7613
|
-
};
|
|
7930
|
+
return listen(monitorStore().listeners, listener);
|
|
7614
7931
|
}
|
|
7615
7932
|
/**
|
|
7616
7933
|
* Watch the origin for the restarted server and reload into it: probe once
|
|
@@ -7823,9 +8140,518 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7823
8140
|
if (store.state?.kind === "not-restarted") settle(store, null);
|
|
7824
8141
|
}
|
|
7825
8142
|
//#endregion
|
|
8143
|
+
//#region src/client/page-store.ts
|
|
8144
|
+
/** The owed map's key for one switch: `<surface>:<identity>`. */
|
|
8145
|
+
function switchKey(surface, identity) {
|
|
8146
|
+
return `${surface}:${identity}`;
|
|
8147
|
+
}
|
|
8148
|
+
/** The identity a switch key names. A surface holds no colon, so it is
|
|
8149
|
+
* everything after the first. */
|
|
8150
|
+
const identityOf = (key) => key.slice(key.indexOf(":") + 1);
|
|
8151
|
+
/** Selects `key`'s receipt: its view, unless that is a flow still running.
|
|
8152
|
+
* A running flow settles by its own poll, and resetting it would drop every
|
|
8153
|
+
* later answer for it — its settle with them. */
|
|
8154
|
+
const receiptOf = (key) => (view, each) => each === key && view.kind !== "running";
|
|
8155
|
+
/** The shelf a page opens on: the star order, every category, one batch. */
|
|
8156
|
+
const INITIAL_VIEW = {
|
|
8157
|
+
query: "",
|
|
8158
|
+
category: null,
|
|
8159
|
+
order: "stars",
|
|
8160
|
+
hideIncompatible: false,
|
|
8161
|
+
visibleCount: 48
|
|
8162
|
+
};
|
|
8163
|
+
/** Whether going from one view to the other changes which cards the shelf
|
|
8164
|
+
* shows, or their order. Growing the shelf and opening a card change
|
|
8165
|
+
* neither. */
|
|
8166
|
+
function reshelves(before, after) {
|
|
8167
|
+
return before.query !== after.query || before.category !== after.category || before.order !== after.order || before.hideIncompatible !== after.hideIncompatible;
|
|
8168
|
+
}
|
|
8169
|
+
/**
|
|
8170
|
+
* @param deps read on every call, so the owner can hand over its latest
|
|
8171
|
+
* functions without rebuilding the store.
|
|
8172
|
+
* @param options the page's own store polls unwatched; a store a tab builds
|
|
8173
|
+
* for itself does not, and stops with the tab (ShopTab).
|
|
8174
|
+
*/
|
|
8175
|
+
function createPageStore(deps, options) {
|
|
8176
|
+
let snapshot = {
|
|
8177
|
+
view: INITIAL_VIEW,
|
|
8178
|
+
reverdictPending: false,
|
|
8179
|
+
reverdicts: 0,
|
|
8180
|
+
settled: 0
|
|
8181
|
+
};
|
|
8182
|
+
let scrollTop = 0;
|
|
8183
|
+
let scrollAnchor = null;
|
|
8184
|
+
let pageMounted = false;
|
|
8185
|
+
let handover = { showing: false };
|
|
8186
|
+
let disposed = false;
|
|
8187
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8188
|
+
const publish = (next) => {
|
|
8189
|
+
if (disposed) return;
|
|
8190
|
+
snapshot = {
|
|
8191
|
+
...snapshot,
|
|
8192
|
+
...next
|
|
8193
|
+
};
|
|
8194
|
+
notify(listeners);
|
|
8195
|
+
};
|
|
8196
|
+
/** The view with `patch` applied, published only when it differs: the
|
|
8197
|
+
* view's fields are primitives, so a field-by-field compare is the
|
|
8198
|
+
* whole question. One that refilters or reorders the shelf forgets the
|
|
8199
|
+
* card the reader was at: the restore then sets the position, which is
|
|
8200
|
+
* what the reader last saw, and their next scroll writes a card again. */
|
|
8201
|
+
const showView = (patch) => {
|
|
8202
|
+
const view = {
|
|
8203
|
+
...snapshot.view,
|
|
8204
|
+
...patch
|
|
8205
|
+
};
|
|
8206
|
+
if (Object.keys(view).every((field) => view[field] === snapshot.view[field])) return;
|
|
8207
|
+
if (reshelves(snapshot.view, view)) scrollAnchor = null;
|
|
8208
|
+
publish({ view });
|
|
8209
|
+
};
|
|
8210
|
+
let expanded = /* @__PURE__ */ new Set();
|
|
8211
|
+
const expandedListeners = /* @__PURE__ */ new Set();
|
|
8212
|
+
const publishExpanded = (next) => {
|
|
8213
|
+
if (disposed) return;
|
|
8214
|
+
expanded = next;
|
|
8215
|
+
notify(expandedListeners);
|
|
8216
|
+
};
|
|
8217
|
+
/** Open one card, and leave it open: a card's own header toggles it, and
|
|
8218
|
+
* `expand` opens one from outside it (design
|
|
8219
|
+
* 2026-10-16-shop-sidebar-page §5.6). */
|
|
8220
|
+
const openCard = (key) => {
|
|
8221
|
+
if (!expanded.has(key)) publishExpanded(new Set(expanded).add(key));
|
|
8222
|
+
};
|
|
8223
|
+
let owed = /* @__PURE__ */ new Map();
|
|
8224
|
+
const owedListeners = /* @__PURE__ */ new Set();
|
|
8225
|
+
const publishOwed = (next) => {
|
|
8226
|
+
if (disposed) return;
|
|
8227
|
+
owed = next;
|
|
8228
|
+
notify(owedListeners);
|
|
8229
|
+
};
|
|
8230
|
+
/** A reverdict raised: pending, and counted. */
|
|
8231
|
+
const raisedReverdict = () => ({
|
|
8232
|
+
reverdictPending: true,
|
|
8233
|
+
reverdicts: snapshot.reverdicts + 1
|
|
8234
|
+
});
|
|
8235
|
+
/** One settle the page reacts to: re-read the installed list and, when the
|
|
8236
|
+
* catalog's verdicts may have moved, judge them again. */
|
|
8237
|
+
const settle = (reverdict) => {
|
|
8238
|
+
publish({
|
|
8239
|
+
settled: snapshot.settled + 1,
|
|
8240
|
+
...reverdict ? raisedReverdict() : {}
|
|
8241
|
+
});
|
|
8242
|
+
};
|
|
8243
|
+
/** The owed map without one notice kind, or null when it holds none of
|
|
8244
|
+
* them (`pruned`), so a caller publishes nothing for an unchanged map. */
|
|
8245
|
+
const withoutOwed = (kind) => pruned(owed, (value) => value === kind);
|
|
8246
|
+
/** The owed map without any switch of `identities`, on every surface, or
|
|
8247
|
+
* null when it holds none. */
|
|
8248
|
+
const withoutSwitchesOf = (identities) => pruned(owed, (_, key) => identities.has(identityOf(key)));
|
|
8249
|
+
let drops = 0;
|
|
8250
|
+
/** By identity: the stamp of the last drop because it left the profile. */
|
|
8251
|
+
const leftAt = /* @__PURE__ */ new Map();
|
|
8252
|
+
/** The identities the installed list a page last read held: the switches
|
|
8253
|
+
* on screen are theirs, and so is any toggle still out. */
|
|
8254
|
+
let listed = /* @__PURE__ */ new Set();
|
|
8255
|
+
/** `identities` left the profile: their switches owe nothing, and a toggle
|
|
8256
|
+
* of one still out re-creates nothing. */
|
|
8257
|
+
const leave = (identities) => {
|
|
8258
|
+
if (identities.size === 0) return;
|
|
8259
|
+
drops += 1;
|
|
8260
|
+
for (const identity of identities) leftAt.set(identity, drops);
|
|
8261
|
+
const next = withoutSwitchesOf(identities);
|
|
8262
|
+
if (next !== null) publishOwed(next);
|
|
8263
|
+
};
|
|
8264
|
+
const installs = createFlowRegistry(() => ({
|
|
8265
|
+
begin: beginInstall(deps().install),
|
|
8266
|
+
installStatus: deps().installStatus,
|
|
8267
|
+
onSettled: (key, outcome) => {
|
|
8268
|
+
if (outcome === "done") uninstalls.resetWhere(receiptOf(key));
|
|
8269
|
+
settle(outcome === "done");
|
|
8270
|
+
}
|
|
8271
|
+
}), options);
|
|
8272
|
+
const uninstalls = createFlowRegistry(() => ({
|
|
8273
|
+
begin: beginUninstall(deps().uninstall),
|
|
8274
|
+
installStatus: deps().installStatus,
|
|
8275
|
+
onSettled: (key, outcome, args) => {
|
|
8276
|
+
if (outcome !== "done") return;
|
|
8277
|
+
installs.resetWhere(receiptOf(key));
|
|
8278
|
+
leave(/* @__PURE__ */ new Set([key]));
|
|
8279
|
+
deps().noteUninstalled?.(args.name);
|
|
8280
|
+
settle(true);
|
|
8281
|
+
}
|
|
8282
|
+
}), options);
|
|
8283
|
+
const self = createFlowRegistry(() => ({
|
|
8284
|
+
begin: beginUpdate(deps().updateStart),
|
|
8285
|
+
installStatus: deps().installStatus
|
|
8286
|
+
}), options);
|
|
8287
|
+
return {
|
|
8288
|
+
schema: 1,
|
|
8289
|
+
installs,
|
|
8290
|
+
uninstalls,
|
|
8291
|
+
self,
|
|
8292
|
+
getSnapshot: () => snapshot,
|
|
8293
|
+
subscribe: (listener) => listen(listeners, listener),
|
|
8294
|
+
setView: (patch) => {
|
|
8295
|
+
showView(patch);
|
|
8296
|
+
},
|
|
8297
|
+
updateView: (update) => {
|
|
8298
|
+
showView(update(snapshot.view));
|
|
8299
|
+
},
|
|
8300
|
+
isExpanded: (key) => expanded.has(key),
|
|
8301
|
+
subscribeExpanded: (listener) => listen(expandedListeners, listener),
|
|
8302
|
+
toggleExpanded: (key) => {
|
|
8303
|
+
const next = new Set(expanded);
|
|
8304
|
+
if (!next.delete(key)) next.add(key);
|
|
8305
|
+
publishExpanded(next);
|
|
8306
|
+
},
|
|
8307
|
+
expand: openCard,
|
|
8308
|
+
focus: (name, card) => {
|
|
8309
|
+
scrollTop = 0;
|
|
8310
|
+
scrollAnchor = null;
|
|
8311
|
+
showView({
|
|
8312
|
+
query: name,
|
|
8313
|
+
category: null,
|
|
8314
|
+
hideIncompatible: false,
|
|
8315
|
+
visibleCount: 48
|
|
8316
|
+
});
|
|
8317
|
+
if (card !== null) openCard(card);
|
|
8318
|
+
},
|
|
8319
|
+
getOwed: (key) => owed.get(key),
|
|
8320
|
+
subscribeOwed: (listener) => listen(owedListeners, listener),
|
|
8321
|
+
owedEpoch: () => drops,
|
|
8322
|
+
setOwed: (key, notice, since) => {
|
|
8323
|
+
if ((leftAt.get(identityOf(key)) ?? 0) > since) return;
|
|
8324
|
+
const current = owed.get(key);
|
|
8325
|
+
if (current === "restart" || current === notice) return;
|
|
8326
|
+
publishOwed(new Map(owed).set(key, notice));
|
|
8327
|
+
},
|
|
8328
|
+
noteInstalled: (identities) => {
|
|
8329
|
+
const held = new Set(identities);
|
|
8330
|
+
const gone = /* @__PURE__ */ new Set();
|
|
8331
|
+
for (const identity of listed) if (!held.has(identity)) gone.add(identity);
|
|
8332
|
+
for (const key of owed.keys()) if (!held.has(identityOf(key))) gone.add(identityOf(key));
|
|
8333
|
+
listed = held;
|
|
8334
|
+
leave(gone);
|
|
8335
|
+
},
|
|
8336
|
+
clearReverdict: (asked) => {
|
|
8337
|
+
if (snapshot.reverdictPending && asked === snapshot.reverdicts) publish({ reverdictPending: false });
|
|
8338
|
+
},
|
|
8339
|
+
noteRestarted: () => {
|
|
8340
|
+
self.resetWhere(settledByRestart);
|
|
8341
|
+
installs.resetWhere(settledByRestart);
|
|
8342
|
+
uninstalls.resetWhere(settledByRestart);
|
|
8343
|
+
const next = withoutOwed("restart");
|
|
8344
|
+
if (next !== null) publishOwed(next);
|
|
8345
|
+
settle(true);
|
|
8346
|
+
},
|
|
8347
|
+
recordScroll: (top, anchor = null) => {
|
|
8348
|
+
scrollTop = top;
|
|
8349
|
+
scrollAnchor = anchor;
|
|
8350
|
+
},
|
|
8351
|
+
readScroll: () => scrollTop,
|
|
8352
|
+
readAnchor: () => scrollAnchor,
|
|
8353
|
+
notePageMounted: (mounted) => {
|
|
8354
|
+
pageMounted = mounted;
|
|
8355
|
+
},
|
|
8356
|
+
recordTeardown: () => {
|
|
8357
|
+
handover = { showing: pageMounted };
|
|
8358
|
+
},
|
|
8359
|
+
handover: () => handover,
|
|
8360
|
+
dispose: () => {
|
|
8361
|
+
disposed = true;
|
|
8362
|
+
installs.dispose();
|
|
8363
|
+
uninstalls.dispose();
|
|
8364
|
+
self.dispose();
|
|
8365
|
+
forget(listeners);
|
|
8366
|
+
forget(owedListeners);
|
|
8367
|
+
forget(expandedListeners);
|
|
8368
|
+
}
|
|
8369
|
+
};
|
|
8370
|
+
}
|
|
8371
|
+
/** Registry symbol, so every module instance on the page finds one store. */
|
|
8372
|
+
const STORE_KEY = Symbol.for("dsh-plugin-shop.page");
|
|
8373
|
+
function occupantOf(held) {
|
|
8374
|
+
if (held === void 0) return { kind: "none" };
|
|
8375
|
+
if (typeof held === "object" && held !== null && held.schema === 1) return {
|
|
8376
|
+
kind: "store",
|
|
8377
|
+
store: held
|
|
8378
|
+
};
|
|
8379
|
+
return {
|
|
8380
|
+
kind: "other",
|
|
8381
|
+
occupant: held
|
|
8382
|
+
};
|
|
8383
|
+
}
|
|
8384
|
+
/** What a shop applied earlier left on this page: nothing, a store of this
|
|
8385
|
+
* schema, or another schema's object. */
|
|
8386
|
+
function readPageStore() {
|
|
8387
|
+
return occupantOf(globalThis[STORE_KEY]);
|
|
8388
|
+
}
|
|
8389
|
+
/** Make `store` the page's. Returns what it replaces. */
|
|
8390
|
+
function installPageStore(store) {
|
|
8391
|
+
const page = globalThis;
|
|
8392
|
+
const previous = occupantOf(page[STORE_KEY]);
|
|
8393
|
+
page[STORE_KEY] = store;
|
|
8394
|
+
return previous;
|
|
8395
|
+
}
|
|
8396
|
+
/** Read at the very start of `apply`, before this instance puts its own
|
|
8397
|
+
* stores on the page. */
|
|
8398
|
+
function readPredecessor() {
|
|
8399
|
+
const previous = readPageStore();
|
|
8400
|
+
if (previous.kind === "none") return hasRestartMonitorStore() ? { kind: "section" } : { kind: "none" };
|
|
8401
|
+
if (previous.kind === "other") return {
|
|
8402
|
+
kind: "page",
|
|
8403
|
+
showing: false
|
|
8404
|
+
};
|
|
8405
|
+
if (typeof previous.store.handover !== "function") return { kind: "section" };
|
|
8406
|
+
return {
|
|
8407
|
+
kind: "page",
|
|
8408
|
+
showing: previous.store.handover().showing
|
|
8409
|
+
};
|
|
8410
|
+
}
|
|
8411
|
+
/** Stop what the page key held, through the `dispose` it has and no other:
|
|
8412
|
+
* an object that claims this schema and lacks one is stopped no more than a
|
|
8413
|
+
* schema-2 object is, and neither can throw the shop off the page. */
|
|
8414
|
+
function disposeOccupant(occupant) {
|
|
8415
|
+
if (occupant.kind === "none") return;
|
|
8416
|
+
const held = occupant.kind === "store" ? occupant.store : occupant.occupant;
|
|
8417
|
+
if (typeof held?.dispose === "function") held.dispose();
|
|
8418
|
+
}
|
|
8419
|
+
//#endregion
|
|
8420
|
+
//#region \0dsh-plugin-shop-css:a037354d
|
|
8421
|
+
const css = "/* Shop styles — the shop's own page with its market shelf, and the shop's two\n * buttons on dsh's Plugins page. Every variable read here is a dsh theme\n * alias token (`--dsw-alias-*`), one of the shop's own properties declared in\n * this file (`--shop-*`, `--category-hue`, `--switch-hue`, `--track-hue`), or\n * one of two dsh variables outside the alias set, each read where the shop\n * copies dsh: `--dsh-frame-top-clearance`, so the page head clears the\n * desktop frame as dsh's own pages do, and `--dsw-radius-sm`, because the\n * Plugins page buttons copy dsh's Button (0.1.5-rc.3 lacks that token, and\n * has no Plugins page to draw them on). `state-error` and\n * `state-success` do not exist, so the state-*-primary set stands in for\n * them (measured 2026-10-17 on dsh 0.1.5-rc.3, 0.1.7-rc.2 and 0.2.0-rc.2,\n * which do define `bg-layer-3` and `interactive-bg-hover`, once listed here\n * as missing). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.sa584e5_client.s18f7ee_config.s39f74c_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.sa584e5_client.sf107cf_spec.s39f74c_ts` pins this. */\n\n.s8de901_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The two reds every red text in this tab is painted with. dsh's error red\n * is #ec1313 in the light theme, which measures 4.498:1 as text on white —\n * a miss of two thousandths — and 3.57:1 on the badge's own 14% red fill\n * (4.00:1 in dark). Mixing it toward label-primary fixes both, because\n * label-primary is the theme's foreground pole: it darkens the red on light\n * surfaces and lightens it on dark ones. Measured in the live app: 90% of\n * the red is 5.30:1 on white and 5.29:1 on the dark card, and 80% is\n * 4.98:1 on the badge's tint, 4.92:1 in dark. Computed: 90% is 6.14:1 on\n * the dark page itself (#151517, `bg-base`), where the self-update's\n * failure heading sits (4.69:1 on Settings' #2c2c2e, its ground through\n * 0.8.7), and 80% is 5.23:1 on the uninstall button's 10% hover fill.\n * Declared here, on the tab root, because a custom property inherits: one\n * declaration reaches every red element, and a new one cannot be\n * forgotten. */\n --shop-error: color-mix(in srgb, var(--dsw-alias-state-error-primary) 90%, var(--dsw-alias-label-primary));\n --shop-error-on-tint: color-mix(in srgb, var(--dsw-alias-state-error-primary) 80%, var(--dsw-alias-label-primary));\n /* The amber and the green, mixed the same way for the same reason. dsh's\n * warn #f59e0b and success #22c55e are one value in both themes and pass\n * in the dark one, but as text on white they measure 2.15:1 and 2.28:1.\n * Each derived colour keeps the largest multiple of ten of its hue that\n * clears every surface it paints, as the reds do. Computed from the live\n * tokens over every bg layer in both themes: amber at 60% is 4.95:1 on a\n * plain surface but 4.44:1 on its own 14% tint, so text on an amber tint\n * takes 50% (5.66:1); green at 60% is 5.18:1 plain and 4.60:1 on its 14%\n * tint, so one green serves both. */\n --shop-warn: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 60%, var(--dsw-alias-label-primary));\n --shop-warn-on-tint: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 50%, var(--dsw-alias-label-primary));\n --shop-success: color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary));\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.s3e432c_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s9a1d55_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n/* The shop's own page in dsh's main column (design\n * 2026-10-16-shop-sidebar-page §4.2), framed as dsh 0.2.0-rc.2 frames its\n * Plugins page — read from that page's stylesheet: the page is the scroll\n * container, its children one centred column of at most 960px, and the head\n * clears the desktop title bar. One declaration is the shop's own, `color`:\n * dsh's `.sa6404f_page` inherits its colour from the shell around it, and the shop's\n * root is not inside dsh's panel stylesheet, so the shop's page states it. */\n.sa6404f_page {\n box-sizing: border-box;\n height: 100%;\n overflow: auto;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 32px;\n padding: 0 clamp(24px, 4vw, 48px) 48px;\n color: var(--dsw-alias-label-primary);\n}\n\n.sa6404f_page > * {\n width: 100%;\n max-width: 960px;\n}\n\n.scab2f9_pageHead {\n box-sizing: border-box;\n padding-top: calc(28px + var(--dsh-frame-top-clearance, 0px));\n}\n\n.sc4a729_pageTitle {\n margin: 0;\n font-size: 20px;\n font-weight: 500;\n line-height: 28px;\n}\n\n/* Secondary, as dsh's own intro: 5.80:1 on the light theme's white. */\n.s14e198_pageIntro {\n margin: 4px 0 0;\n font-size: 13px;\n line-height: 20px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The two buttons on dsh's Plugins page (design 2026-10-16-shop-sidebar-page\n * §7), drawn as that page draws its own small outline buttons beside them —\n * dsh 0.2.0-rc.2's Button, sm + outline: 28px tall, 12px text on an 18px\n * line, 10px of side padding, the small radius, the neutral stroke. That\n * stroke is 0.5px in dsh and 1px here: every border in this stylesheet is,\n * and Chromium paints 0.5px as 1px at every ratio measured. */\n.sd04f34_detailAction {\n box-sizing: border-box;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n height: 28px;\n padding: 0 10px;\n border: 1px solid var(--dsw-alias-border-l3);\n border-radius: var(--dsw-radius-sm);\n background: transparent;\n color: var(--dsw-alias-label-primary);\n font-size: 12px;\n line-height: 18px;\n cursor: pointer;\n}\n\n.sd04f34_detailAction:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.sd04f34_detailAction:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sfb4615_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.s43702a_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.se66364_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s41d8a9_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.s41d8a9_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.sbad351_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sb2be83_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.s67c868_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.sbcd6ff_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s41d8a9_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 .s41d8a9_skeletonCard span { animation: none; opacity: 0.75; }\n .s41d8a9_skeletonCard::after { content: none; }\n}\n\n.s602b18_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.s602b18_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.s602b18_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s9a1d55_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.se2add9_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.sf1bc7d_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.seba9a7_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s8c1ada_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.s8c1ada_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.s8c1ada_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s82f241_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s82f241_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s82f241_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s82f241_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.s1e95bf_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s1e95bf_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.s1e95bf_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.s91bcc4_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s3e432c_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.s3e432c_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* Subdued line under the search box when a dictionary intent fired (design\n * 2026-10-08-search-query-expansion §4). One fact, no badge. */\n.s712943_expansionNote {\n margin: 4px 0 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf2024f_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(--shop-warn-on-tint);\n font-size: 12px;\n}\n\n.s630e95_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s4575ba_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s4575ba_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s4575ba_categoryButton.s7c5955_categoryButtonOn` — TWO classes — so it\n * matches `.s4575ba_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s7c5955_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour: a pressed tab must occupy exactly the\n * same box as an unpressed one, so the bar does not reflow — and with ~9 pills\n * on a wrapping row, one that grows can push the rest onto another line and\n * move the tab out from under the pointer that just clicked it. That is what\n * `font-weight: 600` did (bold metrics are wider than regular), and it is why\n * nothing may be added here that resolves to a length: no padding, no\n * border-width, no font-size, no letter-spacing. `css-tokens.sa584e5_client.sf107cf_spec.s39f74c_ts`\n * fails the build on one.\n *\n * Nor a second line. An inset ring once doubled this border, and every control\n * in the stylesheet now draws exactly one 1px line, which is also the thinnest\n * the host paints (Chromium draws 0.5px as 1px). What separates pressed from\n * unpressed is lightness instead of thickness: the border is the hue mixed 30%\n * toward label-primary, the theme's foreground pole, so it clears 3:1 against\n * the bar in both themes for every hue — the raw hue measured 1.86:1 on white\n * for `ui` — and the fill under it is 22% of the hue. The `other` tab is the\n * case this is for: its hue IS the neutral gray this file uses for \"no\n * category\", so on the light theme the hue alone barely separates the two. */\n.s4575ba_categoryButton.s7c5955_categoryButtonOn {\n border-color: color-mix(in srgb, var(--category-hue) 70%, var(--dsw-alias-label-primary));\n background: color-mix(in srgb, var(--category-hue) 22%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours, so this mode alone gets an outline. */\n@media (forced-colors: active) {\n .s4575ba_categoryButton.s7c5955_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s4575ba_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s4575ba_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sc56af2_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sc56af2_incompatibleFilter:hover .s247428_switch` outranks `.se62fac_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sc56af2_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sc56af2_incompatibleFilter:hover .s247428_switch:not(.se62fac_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s4575ba_categoryButton.s7c5955_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sc56af2_incompatibleFilter.s46063b_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sc56af2_incompatibleFilter .s247428_switch:not(.se62fac_switchOn) .se0ac0b_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sc56af2_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s622eb2_updatableHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.s7ef365_catalogStatsRow {\n display: flex;\n align-items: center;\n /* The row holds the catalog line, the refresh button and the shelf order's\n * three pills (design 2026-10-12-shelf-orders §4). Without wrapping, the\n * last of them overflows the tab at a narrow width. */\n flex-wrap: wrap;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n/* The shelf's order control, at the right end of the stats row. `margin-left:\n * auto` is what puts it there while the line and the refresh button stay\n * left — and it is safe on a wrapping row, unlike a width. */\n.sa2f85b_sortGroup {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n}\n\n.s9d1f07_sortLabel {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd5812b_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s602b18_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.sc01148_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.sc01148_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.sc01148_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.sc01148_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s7acae8_catalogRefreshFailed {\n font-size: 11px;\n color: var(--shop-warn);\n}\n\n.saec8bc_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sc932e7_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sc932e7_cards { animation: none; }\n}\n\n.s341f40_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd43e97_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.sb9f4b3_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s341f40_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 .s341f40_card { transition: none; }\n .s341f40_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.s3a0d45_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s4575ba_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.s341f40_card[data-category='tool'], .s4575ba_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s341f40_card[data-category='provider'], .s4575ba_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s341f40_card[data-category='ui'], .s4575ba_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s341f40_card[data-category='workflow'], .s4575ba_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s341f40_card[data-category='integration'], .s4575ba_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s341f40_card[data-category='theme'], .s4575ba_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s341f40_card[data-category='other'], .s4575ba_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s5e7fbd_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.s72c02a_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.s72c02a_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.s54a911_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.sa76f21_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.sca19d2_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.sca19d2_cardActions > .s14145c_gate,\n.sca19d2_cardActions > .s170b01_installPanel,\n.sca19d2_cardActions > .sd9c8c5_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sf0e287_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n /* A release tag prints as tagged, never cut to seven characters (market\n * borrowings §11), and a tag can run to any length. The cap keeps every\n * tag of up to fourteen characters whole, prerelease suffix included\n * (`v0.1.0-alpha.0`): fourteen glyphs plus the letter-spacing above come to\n * about 14.5ch, so the cap is 15ch, not 14. A longer tag ends in an ellipsis,\n * and the tooltip carries the whole of it. */\n max-width: 15ch;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* The octocat marking a source the reader can read on GitHub, beside the\n * card's version. */\n.s75ac5c_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s861686_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s861686_tierBadge[data-tier='verified'] {\n color: var(--shop-success);\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.s861686_tierBadge[data-tier='verified-stale'] {\n color: var(--shop-warn-on-tint);\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.s861686_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/* The §C10 shadowed badge: an installed github entry the catalog refuses\n * shadowed-by-npm. Subdued/secondary rather than a warn tint — nothing is\n * wrong with the install itself; the shelf just cannot show it. */\n.s861686_tierBadge[data-tier='shadowed'] {\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/* The §C5 archived badge: a github repository the owner has frozen.\n * Subdued register, same as shadowed — nothing is wrong with the install;\n * the pinned commit still serves, so the mark is informational only. */\n.s861686_tierBadge[data-tier='archived'] {\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.s8474c5_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n /* On the tint, not on the plain surface: this text reads on its own 14% red\n * fill, where the 90% red would measure 4.21:1 (light) and 4.43:1 (dark). */\n color: var(--shop-error-on-tint);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.s3269e3_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sb3efb0_chevron, .s712bfa_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s712bfa_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .sb3efb0_chevron, .s712bfa_chevronOpen { transition: none; }\n}\n\n.sa23bba_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.s330087_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.sc86a78_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.sce8fac_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.s644c89_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s9ba4c6_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--shop-error);\n}\n\n/* The way out of dsh's own refusal: the lead-in, then the exemption command\n * on its own line. */\n/* Block layout rather than flex: Firefox 153 will not line-clamp a container\n * that holds a display:flex child (Chromium 151 does), and the fold's\n * measured goal — \"the whole red block, remedy included, as one unit\" —\n * needs the container to clamp in BOTH engines the shop targets. The 4px\n * rhythm between the lead-in and the command is kept by the margins of the\n * children. */\n.s2149d8_remedy {\n display: block;\n}\n\n.s2149d8_remedy > * + * { margin-top: 4px; }\n\n/* The fold under a card's whole red block: two lines by default, a small\n * Refresh-pill-shaped toggle under it. Two containers, one rhythm: the shelf\n * uses `.sc15f91_foldBlock` collapsed and `.s876a53_foldBlockExpanded` open, and they share\n * one `display: block` (Firefox clamps no flex child inside a -webkit-box).\n * The clamp is on the BLOCK rather than any one line, so a 43-line remedy\n * counts as a unit instead of squashing the harness-peers line to a sliver\n * and hiding the command the reader actually came for. Both containers\n * still render the full block to a screen reader — `white-space: normal`\n * instead of pre-line keeps the copy's own `\\n` visible without forcing the\n * browser to lay the block's pieces like a text node. */\n.sc15f91_foldBlock {\n display: block;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n display: -webkit-box;\n overflow: hidden;\n}\n\n.s876a53_foldBlockExpanded { display: block; }\n\n/* The toggle. Geometry and quiet voice are the Refresh pill's\n * (`catalogStatsRow` right beside it on the same screen): one 1px border,\n * 11px text, secondary label, no fill. It is the only one of its kind here,\n * so a class of its own means one function of its own. */\n.s285921_foldToggle {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.s285921_foldToggle:hover { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2)); }\n.s285921_foldToggle:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }\n\n.s7e63ed_remedyLead {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The exemption command, bound for a terminal unchanged: one click selects\n * all of it, and it wraps anywhere rather than widening the card. A border,\n * not a fill, marks the box: the bg layers are one white in the light theme. */\n.sa4cc33_allowCommand {\n display: block;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-all;\n user-select: all;\n color: var(--dsw-alias-label-primary);\n}\n\n/* A refused install on a surface that does not state its blockers — the\n * outdated row — writes them out on their own full-width line under the\n * refused label, the command that clears dsh's refusal included. Block,\n * rather than flex: Firefox will not line-clamp a -webkit-box that holds a\n * display:flex child, and the fold this element can sit inside of carries\n * that clamp. The 6px rhythm between the block's lines is carried by the\n * children's margins. */\n.s7c5f1b_refusal {\n display: block;\n flex-basis: 100%;\n}\n\n.s7c5f1b_refusal > * + * { margin-top: 6px; }\n\n.s12e4f9_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s9e429f_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sb6cd77_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.sb6cd77_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.sbc0e0a_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.s67086c_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sb858f4_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sb858f4_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sb858f4_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.sb858f4_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sb858f4_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sb858f4_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value —\n * a packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.sca19d2_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s936159_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The on-disk size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s936159_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row.\n *\n * `.sa62ae0_shelfDate` shares this rule because it is the same kind of fact — a\n * fixed-width figure a reader compares down the shelf (design\n * 2026-10-12-shelf-orders §4) — and two copies of these declarations would\n * drift. */\n.s63fc2d_size,\n.sa62ae0_shelfDate {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s936159_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s936159_cardMeta`. There is no middot:\n * `.s936159_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s5d52b1_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sc2ddce_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(--shop-warn-on-tint);\n}\n\n/* Install flow. */\n.s170b01_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n/* ONE geometry for everything that occupies the install button's place: the\n * button itself, the confirm/cancel pair beside the restart gate, the installed\n * label, and the refused label. Stated once because a refused label HAS to look\n * like the button it replaces apart from its colour and its text, and four\n * copies of the same five declarations is how they stop looking alike.\n * `cursor: pointer` is NOT here: only the controls that can be pressed take it,\n * while a label renders with the default cursor. */\n.s30cc02_installButton,\n.s4732ec_confirmButton,\n.s6dfe5d_cancelButton,\n.s9f738a_installedLabel,\n.se35352_refusedLabel {\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}\n\n.s30cc02_installButton, .s4732ec_confirmButton, .s6dfe5d_cancelButton {\n cursor: pointer;\n}\n\n.s30cc02_installButton {\n border-color: var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n/* Between the click and the host's answer. The flow ignores a repeat start for\n * the same identity, so this is the visible half of the same fact: a control\n * that says \"not now\" rather than one that looks ready to send a second\n * install. Only the button can be in this state — a label is never pressed. */\n.s30cc02_installButton:disabled {\n cursor: wait;\n}\n\n.s30cc02_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s4732ec_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.s4732ec_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6dfe5d_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.s6dfe5d_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s30cc02_installButton:focus-visible, .s4732ec_confirmButton:focus-visible, .s6dfe5d_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.s9f738a_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/* The refused Install/Update button's replacement: the same box as the button\n * (the geometry rule above), filled solid red, not pressable. The fill is the\n * derived red rather than the raw token because text sits on it — bg-base on\n * `--shop-error` measures 5.31:1 light and 6.14:1 dark — and the raw error red\n * under bg-base text measures 4.498:1, the miss this whole family of fixes\n * exists for. Nothing here changes on hover: it is a statement, not a control,\n * so it keeps the default cursor and its colours. */\n.se35352_refusedLabel {\n border-color: var(--shop-error);\n background: var(--shop-error);\n color: var(--dsw-alias-bg-base);\n white-space: nowrap;\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.sd9c8c5_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.s1157ec_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 /* The on-tint red: hovering this button paints a 10% red fill under the\n * word, where the plain red measures 4.50:1 (light) — the same two\n * thousandths short of the floor the raw token shipped at. */\n color: var(--shop-error-on-tint);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s1157ec_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s1157ec_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.s748113_restartSelfButton {\n /* The version row's third state. Geometry matches .s82f241_checkUpdateButton and\n * .s8c1ada_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.s748113_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s1784cd_restartButton,\n.s55f001_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s1784cd_restartButton:hover,\n.s55f001_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s1784cd_restartButton:focus-visible,\n.s55f001_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.sd9c8c5_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.s3e0556_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.s14145c_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.s1c22f6_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--shop-warn-on-tint);\n}\n\n.sdd4bac_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s1c4d38_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--shop-error);\n}\n\n.s7a3c61_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s9bf897_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s8e495e_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.sec6c41_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sd27ebf_notice {\n margin: 0;\n font-size: 12px;\n color: var(--shop-success);\n}\n\n.sced88c_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--shop-error);\n}\n\n.s7f8226_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s66949c_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--shop-error);\n}\n\n.sda36fc_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\n.se5be3e_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sf32898_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s70aac4_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.sc30749_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.sc30749_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.s735987_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .sa76f21_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The §C10 shadowed section. Reuses the outdated list's visual language; only\n * the section spacing and the per-row explanation paragraph are its own. */\n.sbf0d60_shadowedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s9b143a_shadowedDetail {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s204154_outdatedActions {\n display: flex;\n align-items: center;\n /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflowed the Settings dialog the shop\n * sat in through 0.8.7, and the page's column narrows with the window. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sc56af2_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.s247428_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.s247428_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.se62fac_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.se0ac0b_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.se62fac_switchOn .se0ac0b_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s247428_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 .s247428_switch, .se0ac0b_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.s7dd801_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.s204154_outdatedActions`: `.s3e0556_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sd9c8c5_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
8422
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"a037354d\"]")) {
|
|
8423
|
+
const tag = document.createElement("style");
|
|
8424
|
+
tag.dataset.pluginCss = "a037354d";
|
|
8425
|
+
tag.textContent = css;
|
|
8426
|
+
document.head.appendChild(tag);
|
|
8427
|
+
}
|
|
8428
|
+
var _dsh_plugin_shop_css_a037354d_default = {
|
|
8429
|
+
"client": "sa584e5_client",
|
|
8430
|
+
"config": "s18f7ee_config",
|
|
8431
|
+
"ts": "s39f74c_ts",
|
|
8432
|
+
"spec": "sf107cf_spec",
|
|
8433
|
+
"panel": "s8de901_panel",
|
|
8434
|
+
"searchInput": "s3e432c_searchInput",
|
|
8435
|
+
"toolbar": "s9a1d55_toolbar",
|
|
8436
|
+
"page": "sa6404f_page",
|
|
8437
|
+
"pageHead": "scab2f9_pageHead",
|
|
8438
|
+
"pageTitle": "sc4a729_pageTitle",
|
|
8439
|
+
"pageIntro": "s14e198_pageIntro",
|
|
8440
|
+
"detailAction": "sd04f34_detailAction",
|
|
8441
|
+
"stateLine": "sfb4615_stateLine",
|
|
8442
|
+
"srOnly": "s43702a_srOnly",
|
|
8443
|
+
"skeletonGrid": "se66364_skeletonGrid",
|
|
8444
|
+
"skeletonCard": "s41d8a9_skeletonCard",
|
|
8445
|
+
"skeletonName": "sbad351_skeletonName",
|
|
8446
|
+
"skeletonSummary": "sb2be83_skeletonSummary",
|
|
8447
|
+
"skeletonSummaryShort": "s67c868_skeletonSummaryShort",
|
|
8448
|
+
"skeletonActions": "sbcd6ff_skeletonActions",
|
|
8449
|
+
"actionButton": "s602b18_actionButton",
|
|
8450
|
+
"toolbarRight": "se2add9_toolbarRight",
|
|
8451
|
+
"versionBlock": "sf1bc7d_versionBlock",
|
|
8452
|
+
"versionText": "seba9a7_versionText",
|
|
8453
|
+
"updateSelfButton": "s8c1ada_updateSelfButton",
|
|
8454
|
+
"checkUpdateButton": "s82f241_checkUpdateButton",
|
|
8455
|
+
"githubLink": "s1e95bf_githubLink",
|
|
8456
|
+
"selfUpdatePanel": "s91bcc4_selfUpdatePanel",
|
|
8457
|
+
"expansionNote": "s712943_expansionNote",
|
|
8458
|
+
"staleBadge": "sf2024f_staleBadge",
|
|
8459
|
+
"categoryBar": "s630e95_categoryBar",
|
|
8460
|
+
"categoryButton": "s4575ba_categoryButton",
|
|
8461
|
+
"categoryButtonOn": "s7c5955_categoryButtonOn",
|
|
8462
|
+
"incompatibleFilter": "sc56af2_incompatibleFilter",
|
|
8463
|
+
"switch": "s247428_switch",
|
|
8464
|
+
"switchOn": "se62fac_switchOn",
|
|
8465
|
+
"incompatibleFilterOn": "s46063b_incompatibleFilterOn",
|
|
8466
|
+
"switchKnob": "se0ac0b_switchKnob",
|
|
8467
|
+
"updatableHeading": "s622eb2_updatableHeading",
|
|
8468
|
+
"catalogStatsRow": "s7ef365_catalogStatsRow",
|
|
8469
|
+
"sortGroup": "sa2f85b_sortGroup",
|
|
8470
|
+
"sortLabel": "s9d1f07_sortLabel",
|
|
8471
|
+
"catalogStats": "sd5812b_catalogStats",
|
|
8472
|
+
"catalogRefreshButton": "sc01148_catalogRefreshButton",
|
|
8473
|
+
"catalogRefreshFailed": "s7acae8_catalogRefreshFailed",
|
|
8474
|
+
"emptyLine": "saec8bc_emptyLine",
|
|
8475
|
+
"cards": "sc932e7_cards",
|
|
8476
|
+
"card": "s341f40_card",
|
|
8477
|
+
"cardsSentry": "sd43e97_cardsSentry",
|
|
8478
|
+
"showingLine": "sb9f4b3_showingLine",
|
|
8479
|
+
"cardSpine": "s3a0d45_cardSpine",
|
|
8480
|
+
"categoryBadge": "s5e7fbd_categoryBadge",
|
|
8481
|
+
"entryHeader": "s72c02a_entryHeader",
|
|
8482
|
+
"name": "s54a911_name",
|
|
8483
|
+
"badges": "sa76f21_badges",
|
|
8484
|
+
"cardActions": "sca19d2_cardActions",
|
|
8485
|
+
"gate": "s14145c_gate",
|
|
8486
|
+
"installPanel": "s170b01_installPanel",
|
|
8487
|
+
"installedActions": "sd9c8c5_installedActions",
|
|
8488
|
+
"cardVersion": "sf0e287_cardVersion",
|
|
8489
|
+
"sourceBadge": "s75ac5c_sourceBadge",
|
|
8490
|
+
"tierBadge": "s861686_tierBadge",
|
|
8491
|
+
"incompatibleBadge": "s8474c5_incompatibleBadge",
|
|
8492
|
+
"starsBadge": "s3269e3_starsBadge",
|
|
8493
|
+
"chevron": "sb3efb0_chevron",
|
|
8494
|
+
"chevronOpen": "s712bfa_chevronOpen",
|
|
8495
|
+
"body": "sa23bba_body",
|
|
8496
|
+
"summary": "s330087_summary",
|
|
8497
|
+
"summaryZh": "sc86a78_summaryZh",
|
|
8498
|
+
"summaryExpanded": "sce8fac_summaryExpanded",
|
|
8499
|
+
"summaryZhExpanded": "s644c89_summaryZhExpanded",
|
|
8500
|
+
"incompatibleDetail": "s9ba4c6_incompatibleDetail",
|
|
8501
|
+
"remedy": "s2149d8_remedy",
|
|
8502
|
+
"foldBlock": "sc15f91_foldBlock",
|
|
8503
|
+
"foldBlockExpanded": "s876a53_foldBlockExpanded",
|
|
8504
|
+
"foldToggle": "s285921_foldToggle",
|
|
8505
|
+
"remedyLead": "s7e63ed_remedyLead",
|
|
8506
|
+
"allowCommand": "sa4cc33_allowCommand",
|
|
8507
|
+
"refusal": "s7c5f1b_refusal",
|
|
8508
|
+
"capabilitiesBlock": "s12e4f9_capabilitiesBlock",
|
|
8509
|
+
"capabilitiesNote": "s9e429f_capabilitiesNote",
|
|
8510
|
+
"capabilities": "sb6cd77_capabilities",
|
|
8511
|
+
"detail": "sbc0e0a_detail",
|
|
8512
|
+
"detailRows": "s67086c_detailRows",
|
|
8513
|
+
"detailRow": "sb858f4_detailRow",
|
|
8514
|
+
"cardMeta": "s936159_cardMeta",
|
|
8515
|
+
"shelfDate": "sa62ae0_shelfDate",
|
|
8516
|
+
"size": "s63fc2d_size",
|
|
8517
|
+
"author": "s5d52b1_author",
|
|
8518
|
+
"reviewedLine": "sc2ddce_reviewedLine",
|
|
8519
|
+
"installButton": "s30cc02_installButton",
|
|
8520
|
+
"confirmButton": "s4732ec_confirmButton",
|
|
8521
|
+
"cancelButton": "s6dfe5d_cancelButton",
|
|
8522
|
+
"installedLabel": "s9f738a_installedLabel",
|
|
8523
|
+
"refusedLabel": "se35352_refusedLabel",
|
|
8524
|
+
"uninstallButton": "s1157ec_uninstallButton",
|
|
8525
|
+
"restartSelfButton": "s748113_restartSelfButton",
|
|
8526
|
+
"restartButton": "s1784cd_restartButton",
|
|
8527
|
+
"reloadButton": "s55f001_reloadButton",
|
|
8528
|
+
"reloadPanel": "s3e0556_reloadPanel",
|
|
8529
|
+
"gateTitle": "s1c22f6_gateTitle",
|
|
8530
|
+
"gateBody": "sdd4bac_gateBody",
|
|
8531
|
+
"gateWarning": "s1c4d38_gateWarning",
|
|
8532
|
+
"gateActions": "s7a3c61_gateActions",
|
|
8533
|
+
"installing": "s9bf897_installing",
|
|
8534
|
+
"log": "s8e495e_log",
|
|
8535
|
+
"logLine": "sec6c41_logLine",
|
|
8536
|
+
"notice": "sd27ebf_notice",
|
|
8537
|
+
"failedHeading": "sced88c_failedHeading",
|
|
8538
|
+
"failedDetail": "s7f8226_failedDetail",
|
|
8539
|
+
"rejectedCode": "s66949c_rejectedCode",
|
|
8540
|
+
"rejectedDetail": "sda36fc_rejectedDetail",
|
|
8541
|
+
"outdatedSection": "se5be3e_outdatedSection",
|
|
8542
|
+
"outdatedList": "sf32898_outdatedList",
|
|
8543
|
+
"outdatedRow": "s70aac4_outdatedRow",
|
|
8544
|
+
"outdatedInfo": "sc30749_outdatedInfo",
|
|
8545
|
+
"outdatedVersions": "s735987_outdatedVersions",
|
|
8546
|
+
"shadowedSection": "sbf0d60_shadowedSection",
|
|
8547
|
+
"shadowedDetail": "s9b143a_shadowedDetail",
|
|
8548
|
+
"outdatedActions": "s204154_outdatedActions",
|
|
8549
|
+
"switchWrap": "s7dd801_switchWrap"
|
|
8550
|
+
};
|
|
8551
|
+
//#endregion
|
|
8552
|
+
//#region src/client/ShopDetailAction.tsx
|
|
8553
|
+
/**
|
|
8554
|
+
* The shop's two buttons on dsh's Plugins page (design
|
|
8555
|
+
* 2026-10-16-shop-sidebar-page §7), in the head of a detail page, before the
|
|
8556
|
+
* page's own switch and uninstall (`plugins.detail.actions`, dsh 0.1.7-rc.2
|
|
8557
|
+
* and later): "Open Plugin shop" on the shop's own bundle page, and "View in
|
|
8558
|
+
* Plugin shop" on the page of any bundle the shelf lists. Any other subject
|
|
8559
|
+
* renders nothing, as the slot's contract asks of an entry.
|
|
8560
|
+
*
|
|
8561
|
+
* It renders inside dsh's page, so it carries its own error boundary: a
|
|
8562
|
+
* throw here costs the reader a button, never dsh's page.
|
|
8563
|
+
*/
|
|
8564
|
+
/** The shop's own package, whose bundle page gets "Open Plugin shop". */
|
|
8565
|
+
const OWN_PACKAGE = "dsh-plugin-shop";
|
|
8566
|
+
function ShopDetailAction(props) {
|
|
8567
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailActionBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailActionBody, { ...props }) });
|
|
8568
|
+
}
|
|
8569
|
+
/** Renders nothing once a child throws: a lost button, never a lost page. A
|
|
8570
|
+
* class, because React offers error boundaries in no other form. */
|
|
8571
|
+
var DetailActionBoundary = class extends react.Component {
|
|
8572
|
+
state = { failed: false };
|
|
8573
|
+
static getDerivedStateFromError() {
|
|
8574
|
+
return { failed: true };
|
|
8575
|
+
}
|
|
8576
|
+
componentDidCatch(error, info) {
|
|
8577
|
+
console.error("dsh-plugin-shop: the Plugins page button crashed", error, info.componentStack);
|
|
8578
|
+
}
|
|
8579
|
+
render() {
|
|
8580
|
+
return this.state.failed ? null : this.props.children;
|
|
8581
|
+
}
|
|
8582
|
+
};
|
|
8583
|
+
function DetailActionBody({ subject, t, listing, canOpen, openShop, page }) {
|
|
8584
|
+
const name = subject.kind === "bundle" ? subject.pkg.name : null;
|
|
8585
|
+
const own = name === OWN_PACKAGE;
|
|
8586
|
+
const [listed, setListed] = (0, react.useState)(null);
|
|
8587
|
+
(0, react.useEffect)(() => {
|
|
8588
|
+
if (name === null || own) return;
|
|
8589
|
+
let cancelled = false;
|
|
8590
|
+
listing().then((result) => {
|
|
8591
|
+
if (cancelled) return;
|
|
8592
|
+
const cleared = new Set(result.notAShop ?? []);
|
|
8593
|
+
setListed({
|
|
8594
|
+
name,
|
|
8595
|
+
keys: result.plugins.filter((entry) => entry.name === name && listedOnShelf(entry, cleared)).map(entryKey)
|
|
8596
|
+
});
|
|
8597
|
+
}).catch(() => {});
|
|
8598
|
+
return () => {
|
|
8599
|
+
cancelled = true;
|
|
8600
|
+
};
|
|
8601
|
+
}, [
|
|
8602
|
+
name,
|
|
8603
|
+
own,
|
|
8604
|
+
listing
|
|
8605
|
+
]);
|
|
8606
|
+
if (name === null || !canOpen()) return null;
|
|
8607
|
+
if (own) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8608
|
+
type: "button",
|
|
8609
|
+
className: _dsh_plugin_shop_css_a037354d_default.detailAction,
|
|
8610
|
+
"data-shop-open-shop": true,
|
|
8611
|
+
onClick: openShop,
|
|
8612
|
+
children: t("openShop")
|
|
8613
|
+
});
|
|
8614
|
+
if (listed === null || listed.name !== name || listed.keys.length === 0) return null;
|
|
8615
|
+
const only = listed.keys.length === 1 ? listed.keys[0] ?? null : null;
|
|
8616
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8617
|
+
type: "button",
|
|
8618
|
+
className: _dsh_plugin_shop_css_a037354d_default.detailAction,
|
|
8619
|
+
"data-shop-view-in-shop": true,
|
|
8620
|
+
onClick: () => {
|
|
8621
|
+
page.focus(name, only);
|
|
8622
|
+
openShop();
|
|
8623
|
+
},
|
|
8624
|
+
children: t("viewInShop")
|
|
8625
|
+
});
|
|
8626
|
+
}
|
|
8627
|
+
//#endregion
|
|
8628
|
+
//#region src/client/ShopGlyph.tsx
|
|
8629
|
+
function ShopGlyph({ size }) {
|
|
8630
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
8631
|
+
width: size,
|
|
8632
|
+
height: size,
|
|
8633
|
+
viewBox: "0 0 16 16",
|
|
8634
|
+
fill: "none",
|
|
8635
|
+
stroke: "currentColor",
|
|
8636
|
+
strokeWidth: 1,
|
|
8637
|
+
strokeLinecap: "round",
|
|
8638
|
+
strokeLinejoin: "round",
|
|
8639
|
+
"aria-hidden": "true",
|
|
8640
|
+
"data-shop-glyph": true,
|
|
8641
|
+
children: [
|
|
8642
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M4.7 5.5H11.3Q12.5 5.5 12.632 6.693L13.368 13.307Q13.5 14.5 12.3 14.5H3.7Q2.5 14.5 2.632 13.307L3.368 6.693Q3.5 5.5 4.7 5.5Z" }),
|
|
8643
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.75 5.5V4.25a2.25 2.25 0 0 1 4.5 0V5.5" }),
|
|
8644
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 12.75V11.25" }),
|
|
8645
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 11.25C7.4 10.2 6.6 9.8 5.75 9.75" }),
|
|
8646
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 11.25C8.6 10.2 9.4 9.8 10.25 9.75" })
|
|
8647
|
+
]
|
|
8648
|
+
});
|
|
8649
|
+
}
|
|
8650
|
+
//#endregion
|
|
7826
8651
|
//#region src/client/useFlows.ts
|
|
7827
8652
|
/**
|
|
7828
|
-
*
|
|
8653
|
+
* React's view of a keyed registry of install-shaped flows (flow-registry.ts):
|
|
8654
|
+
* start, poll to terminal, reset.
|
|
7829
8655
|
*
|
|
7830
8656
|
* Install and uninstall are the same machine. Both fold a
|
|
7831
8657
|
* `ShopInstallStatusResult` through `reduceInstall` into an `InstallView`,
|
|
@@ -7838,108 +8664,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7838
8664
|
* install refusal is a `rejected` view carrying the gate's code; an uninstall
|
|
7839
8665
|
* refusal is a `failed` view carrying the host's published detail, because
|
|
7840
8666
|
* `ShopUninstallResult`'s failure variant has no code to populate and those
|
|
7841
|
-
* codes belong to the install gate. That difference is the
|
|
7842
|
-
* and it is the whole of it.
|
|
8667
|
+
* codes belong to the install gate. That difference is the registry's
|
|
8668
|
+
* `begin`, and it is the whole of it.
|
|
7843
8669
|
*
|
|
7844
8670
|
* The two used to be hand-synced copies — 71 of 84 lines identical. The cost
|
|
7845
8671
|
* was not theoretical: a single review round had to make the same edit twice
|
|
7846
8672
|
* (the settle signature, and its call), and the `pending` set below was added
|
|
7847
8673
|
* to one of them and not the other before this module existed.
|
|
8674
|
+
*
|
|
8675
|
+
* Since 2026-10-16 the registry is its own module, framework-free, and the
|
|
8676
|
+
* polling belongs to it: a component's effect stopped the moment the shop left
|
|
8677
|
+
* the screen (design 2026-10-16-shop-sidebar-page §5). This module only
|
|
8678
|
+
* renders one, through `useRegistryFlows`.
|
|
7848
8679
|
*/
|
|
7849
8680
|
/** Shared empty set, so an idle registry hands out one stable identity. */
|
|
7850
8681
|
const NO_PENDING = /* @__PURE__ */ new Set();
|
|
7851
|
-
/**
|
|
7852
|
-
*
|
|
7853
|
-
*
|
|
7854
|
-
|
|
7855
|
-
|
|
7856
|
-
|
|
7857
|
-
* Never rejects: a transport throw is the caller's to fold into a view,
|
|
7858
|
-
* because only the caller knows which localized line stands in for a detail
|
|
7859
|
-
* too private to render.
|
|
7860
|
-
* @param onSettled called once per identity that reaches a terminal state,
|
|
7861
|
-
* with WHICH terminal state. The outcome is load-bearing: a failed flow
|
|
7862
|
-
* changed nothing on the server, so a caller acting on "settled" alone
|
|
7863
|
-
* discards outcomes that are still true.
|
|
7864
|
-
*/
|
|
7865
|
-
function useKeyedFlows(begin, installStatus, onSettled) {
|
|
7866
|
-
const [views, setViews] = (0, react.useState)(() => /* @__PURE__ */ new Map());
|
|
7867
|
-
const [starting, setStarting] = (0, react.useState)(() => /* @__PURE__ */ new Set());
|
|
7868
|
-
const inFlight = (0, react.useRef)(/* @__PURE__ */ new Set());
|
|
7869
|
-
const settled = (0, react.useRef)(onSettled);
|
|
7870
|
-
settled.current = onSettled;
|
|
7871
|
-
const put = (0, react.useCallback)((key, view) => {
|
|
7872
|
-
setViews((current) => {
|
|
7873
|
-
const next = new Map(current);
|
|
7874
|
-
next.set(key, view);
|
|
7875
|
-
return next;
|
|
7876
|
-
});
|
|
7877
|
-
}, []);
|
|
7878
|
-
const apply = (0, react.useCallback)((key, event) => {
|
|
7879
|
-
setViews((current) => {
|
|
7880
|
-
const before = current.get(key) ?? { kind: "idle" };
|
|
7881
|
-
const after = reduceInstall(before, event);
|
|
7882
|
-
if (after === before) return current;
|
|
7883
|
-
const next = new Map(current);
|
|
7884
|
-
next.set(key, after);
|
|
7885
|
-
return next;
|
|
7886
|
-
});
|
|
7887
|
-
}, []);
|
|
7888
|
-
const start = (0, react.useCallback)(async (key, args) => {
|
|
7889
|
-
if (inFlight.current.has(key)) return;
|
|
7890
|
-
inFlight.current.add(key);
|
|
7891
|
-
setStarting((current) => new Set(current).add(key));
|
|
7892
|
-
try {
|
|
7893
|
-
put(key, { kind: "idle" });
|
|
7894
|
-
put(key, await begin(args));
|
|
7895
|
-
} finally {
|
|
7896
|
-
inFlight.current.delete(key);
|
|
7897
|
-
setStarting((current) => {
|
|
7898
|
-
if (!current.has(key)) return current;
|
|
7899
|
-
const next = new Set(current);
|
|
7900
|
-
next.delete(key);
|
|
7901
|
-
return next;
|
|
7902
|
-
});
|
|
7903
|
-
}
|
|
7904
|
-
}, [begin, put]);
|
|
8682
|
+
/** The keyed-flow face over any registry: one of the page store's, which
|
|
8683
|
+
* outlives the component, or one of the store a tab builds for itself when
|
|
8684
|
+
* none is handed over (ShopTab). */
|
|
8685
|
+
function useRegistryFlows(registry) {
|
|
8686
|
+
const { views, starting } = (0, react.useSyncExternalStore)(registry.subscribe, registry.getSnapshot);
|
|
8687
|
+
const start = (0, react.useCallback)((key, args) => registry.start(key, args), [registry]);
|
|
7905
8688
|
const reset = (0, react.useCallback)((key) => {
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
const next = new Map(current);
|
|
7909
|
-
next.delete(key);
|
|
7910
|
-
return next;
|
|
7911
|
-
});
|
|
7912
|
-
}, []);
|
|
8689
|
+
registry.reset(key);
|
|
8690
|
+
}, [registry]);
|
|
7913
8691
|
const resetWhere = (0, react.useCallback)((predicate) => {
|
|
7914
|
-
|
|
7915
|
-
|
|
7916
|
-
for (const [key, view] of current) {
|
|
7917
|
-
if (!predicate(view)) continue;
|
|
7918
|
-
next ??= new Map(current);
|
|
7919
|
-
next.delete(key);
|
|
7920
|
-
}
|
|
7921
|
-
return next ?? current;
|
|
7922
|
-
});
|
|
7923
|
-
}, []);
|
|
7924
|
-
(0, react.useEffect)(() => {
|
|
7925
|
-
const running = [];
|
|
7926
|
-
for (const [key, view] of views) if (view.kind === "running") running.push([key, view.installId]);
|
|
7927
|
-
if (running.length === 0) return;
|
|
7928
|
-
const timer = setInterval(() => {
|
|
7929
|
-
for (const [key, installId] of running) installStatus({ installId }).then((status) => {
|
|
7930
|
-
apply(key, {
|
|
7931
|
-
type: "status",
|
|
7932
|
-
status
|
|
7933
|
-
});
|
|
7934
|
-
if (status.found && isTerminalInstallState(status.state)) settled.current?.(key, status.state);
|
|
7935
|
-
}, () => {});
|
|
7936
|
-
}, INSTALL_POLL_MS);
|
|
7937
|
-
return () => clearInterval(timer);
|
|
7938
|
-
}, [
|
|
7939
|
-
views,
|
|
7940
|
-
installStatus,
|
|
7941
|
-
apply
|
|
7942
|
-
]);
|
|
8692
|
+
registry.resetWhere(predicate);
|
|
8693
|
+
}, [registry]);
|
|
7943
8694
|
const pendingRef = (0, react.useRef)(NO_PENDING);
|
|
7944
8695
|
const pending = (0, react.useMemo)(() => {
|
|
7945
8696
|
const next = /* @__PURE__ */ new Set();
|
|
@@ -7981,154 +8732,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7981
8732
|
start,
|
|
7982
8733
|
reset
|
|
7983
8734
|
]),
|
|
7984
|
-
resetFlow: reset,
|
|
7985
8735
|
resetWhere,
|
|
7986
8736
|
pending
|
|
7987
8737
|
};
|
|
7988
8738
|
}
|
|
7989
8739
|
//#endregion
|
|
7990
|
-
//#region src/client/useInstall.ts
|
|
7991
|
-
/** Install driving hooks: the shared poll loop, and the tab's keyed registry. */
|
|
7992
|
-
/** The single-view poll loop: while the view is `running`, poll once per
|
|
7993
|
-
* second and fold each status through the reducer. A poll failure is
|
|
7994
|
-
* transient — the host retains the record, so the next tick finds it. The
|
|
7995
|
-
* rejection handler must be present: an unhandled rejection here would
|
|
7996
|
-
* escape the poll loop.
|
|
7997
|
-
*
|
|
7998
|
-
* Kept for the self-update (`useUpdateSelf`), which drives ONE flow with no
|
|
7999
|
-
* identity to key it by. Everything per-plugin goes through `useKeyedFlows`,
|
|
8000
|
-
* whose own loop polls every running identity from one interval. */
|
|
8001
|
-
function usePollStatus(view, setView, installStatus) {
|
|
8002
|
-
(0, react.useEffect)(() => {
|
|
8003
|
-
if (view.kind !== "running") return;
|
|
8004
|
-
const timer = setInterval(() => {
|
|
8005
|
-
installStatus({ installId: view.installId }).then((status) => {
|
|
8006
|
-
setView((current) => reduceInstall(current, {
|
|
8007
|
-
type: "status",
|
|
8008
|
-
status
|
|
8009
|
-
}));
|
|
8010
|
-
}, () => {});
|
|
8011
|
-
}, INSTALL_POLL_MS);
|
|
8012
|
-
return () => clearInterval(timer);
|
|
8013
|
-
}, [
|
|
8014
|
-
view,
|
|
8015
|
-
setView,
|
|
8016
|
-
installStatus
|
|
8017
|
-
]);
|
|
8018
|
-
}
|
|
8019
|
-
/**
|
|
8020
|
-
* Install flows owned by the tab and keyed by install identity. Keeping the
|
|
8021
|
-
* state above individual cards preserves a running operation when filtering
|
|
8022
|
-
* unmounts its card, and makes the shelf and Outdated panels agree.
|
|
8023
|
-
*
|
|
8024
|
-
* The registry itself is `useKeyedFlows`; this supplies the one thing that is
|
|
8025
|
-
* install-specific — what the starting RPC's answer means.
|
|
8026
|
-
*/
|
|
8027
|
-
function useInstallFlows(install, installStatus, onSettled) {
|
|
8028
|
-
return useKeyedFlows((0, react.useCallback)(async (args) => {
|
|
8029
|
-
try {
|
|
8030
|
-
const result = await install(args);
|
|
8031
|
-
if (!result.ok) return {
|
|
8032
|
-
kind: "rejected",
|
|
8033
|
-
code: result.code,
|
|
8034
|
-
detail: result.detail
|
|
8035
|
-
};
|
|
8036
|
-
return {
|
|
8037
|
-
kind: "running",
|
|
8038
|
-
installId: result.installId,
|
|
8039
|
-
log: [],
|
|
8040
|
-
phase: phaseOf(result.state)
|
|
8041
|
-
};
|
|
8042
|
-
} catch {
|
|
8043
|
-
return {
|
|
8044
|
-
kind: "failed",
|
|
8045
|
-
detail: "",
|
|
8046
|
-
log: []
|
|
8047
|
-
};
|
|
8048
|
-
}
|
|
8049
|
-
}, [install]), installStatus, onSettled);
|
|
8050
|
-
}
|
|
8051
|
-
//#endregion
|
|
8052
|
-
//#region src/client/useUninstall.ts
|
|
8053
|
-
/** Uninstall driving hook: the tab's keyed registry, one start mapping. */
|
|
8054
|
-
/**
|
|
8055
|
-
* Uninstall flows owned by the tab and keyed by install identity. Lifting the
|
|
8056
|
-
* state above individual cards is what lets a completed uninstall's outcome
|
|
8057
|
-
* survive the installed-projection refresh that follows it — the row backing
|
|
8058
|
-
* the card disappears, but the flow keyed by identity does not.
|
|
8059
|
-
*
|
|
8060
|
-
* The registry itself is `useKeyedFlows`; this supplies the one thing that is
|
|
8061
|
-
* uninstall-specific — what the starting RPC's answer means. An uninstall
|
|
8062
|
-
* business failure (not in the catalog / not installed) lands in the `failed`
|
|
8063
|
-
* view with the host's published detail, never in the install `rejected`
|
|
8064
|
-
* codes: those belong to the install gate, and `ShopUninstallResult`'s
|
|
8065
|
-
* failure variant has no `code` field to populate.
|
|
8066
|
-
*/
|
|
8067
|
-
function useUninstallFlows(uninstall, installStatus, onSettled) {
|
|
8068
|
-
return useKeyedFlows((0, react.useCallback)(async (args) => {
|
|
8069
|
-
try {
|
|
8070
|
-
const result = await uninstall(args);
|
|
8071
|
-
if (!result.ok) return {
|
|
8072
|
-
kind: "failed",
|
|
8073
|
-
detail: result.detail,
|
|
8074
|
-
log: []
|
|
8075
|
-
};
|
|
8076
|
-
return {
|
|
8077
|
-
kind: "running",
|
|
8078
|
-
installId: result.installId,
|
|
8079
|
-
log: [],
|
|
8080
|
-
phase: "installing"
|
|
8081
|
-
};
|
|
8082
|
-
} catch {
|
|
8083
|
-
return {
|
|
8084
|
-
kind: "failed",
|
|
8085
|
-
detail: "",
|
|
8086
|
-
log: []
|
|
8087
|
-
};
|
|
8088
|
-
}
|
|
8089
|
-
}, [uninstall]), installStatus, onSettled);
|
|
8090
|
-
}
|
|
8091
|
-
//#endregion
|
|
8092
8740
|
//#region src/client/useUpdateSelf.ts
|
|
8093
|
-
/**
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
* `
|
|
8097
|
-
*
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
const [view, setView] = (0, react.useState)({ kind: "idle" });
|
|
8102
|
-
const start = (0, react.useCallback)(async (args) => {
|
|
8103
|
-
setView({ kind: "idle" });
|
|
8104
|
-
try {
|
|
8105
|
-
const result = await updateStart(args);
|
|
8106
|
-
if (!result.ok) {
|
|
8107
|
-
setView({
|
|
8108
|
-
kind: "failed",
|
|
8109
|
-
detail: result.detail,
|
|
8110
|
-
log: []
|
|
8111
|
-
});
|
|
8112
|
-
return;
|
|
8113
|
-
}
|
|
8114
|
-
setView({
|
|
8115
|
-
kind: "running",
|
|
8116
|
-
installId: result.installId,
|
|
8117
|
-
log: [],
|
|
8118
|
-
phase: phaseOf(result.state)
|
|
8119
|
-
});
|
|
8120
|
-
} catch {
|
|
8121
|
-
setView({
|
|
8122
|
-
kind: "failed",
|
|
8123
|
-
detail: "",
|
|
8124
|
-
log: []
|
|
8125
|
-
});
|
|
8126
|
-
}
|
|
8127
|
-
}, [updateStart]);
|
|
8128
|
-
const resetWhere = (0, react.useCallback)((predicate) => {
|
|
8129
|
-
setView((current) => predicate(current) ? { kind: "idle" } : current);
|
|
8130
|
-
}, []);
|
|
8131
|
-
usePollStatus(view, setView, installStatus);
|
|
8741
|
+
/** The one key the self-update registry holds: the shop has one update. */
|
|
8742
|
+
const SELF_UPDATE_KEY = "self";
|
|
8743
|
+
/** The self-update face over a registry: the keyed-flow face
|
|
8744
|
+
* (`useRegistryFlows`) at its one key, so the subscription and the flow's
|
|
8745
|
+
* identity are that hook's and nothing here can drift from it. */
|
|
8746
|
+
function useRegistrySelf(registry) {
|
|
8747
|
+
const { flowFor, resetWhere } = useRegistryFlows(registry);
|
|
8748
|
+
const { view, start } = flowFor(SELF_UPDATE_KEY);
|
|
8132
8749
|
return {
|
|
8133
8750
|
view,
|
|
8134
8751
|
start,
|
|
@@ -9312,148 +9929,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9312
9929
|
};
|
|
9313
9930
|
}
|
|
9314
9931
|
//#endregion
|
|
9315
|
-
//#region \0dsh-plugin-shop-css:c4b688e1
|
|
9316
|
-
const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s3ce4ec_client.s2bd96f_config.s26cef9_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.s3ce4ec_client.sfb0a9a_spec.s26cef9_ts` pins this. */\n\n.s8d78da_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The two reds every red text in this tab is painted with. dsh's error red\n * is #ec1313 in the light theme, which measures 4.498:1 as text on white —\n * a miss of two thousandths — and 3.57:1 on the badge's own 14% red fill\n * (4.00:1 in dark). Mixing it toward label-primary fixes both, because\n * label-primary is the theme's foreground pole: it darkens the red on light\n * surfaces and lightens it on dark ones. Measured in the live app: 90% of\n * the red is 5.30:1 on white and 5.29:1 on the dark card, and 80% is\n * 4.98:1 on the badge's tint, 4.92:1 in dark. Computed: 90% is 4.69:1 on\n * the dark panel itself (#2c2c2e), where the self-update's failure heading\n * sits, and 80% is 5.23:1 on the uninstall button's 10% hover fill.\n * Declared here, on the tab root, because a custom property inherits: one\n * declaration reaches every red element, and a new one cannot be\n * forgotten. */\n --shop-error: color-mix(in srgb, var(--dsw-alias-state-error-primary) 90%, var(--dsw-alias-label-primary));\n --shop-error-on-tint: color-mix(in srgb, var(--dsw-alias-state-error-primary) 80%, var(--dsw-alias-label-primary));\n /* The amber and the green, mixed the same way for the same reason. dsh's\n * warn #f59e0b and success #22c55e are one value in both themes and pass\n * in the dark one, but as text on white they measure 2.15:1 and 2.28:1.\n * Each derived colour keeps the largest multiple of ten of its hue that\n * clears every surface it paints, as the reds do. Computed from the live\n * tokens over every bg layer in both themes: amber at 60% is 4.95:1 on a\n * plain surface but 4.44:1 on its own 14% tint, so text on an amber tint\n * takes 50% (5.66:1); green at 60% is 5.18:1 plain and 4.60:1 on its 14%\n * tint, so one green serves both. */\n --shop-warn: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 60%, var(--dsw-alias-label-primary));\n --shop-warn-on-tint: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 50%, var(--dsw-alias-label-primary));\n --shop-success: color-mix(in srgb, var(--dsw-alias-state-success-primary) 60%, var(--dsw-alias-label-primary));\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.se2e462_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s8c0536_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n/* The shop's Settings page (design §5.1, amended 2026-10-15): the frame dsh's\n * Built-in plugins page draws around its body, where the shop sat as a tab,\n * measured from dsh 0.2.0-rc.2 — a 760px column, 12px gaps, an 18px semibold\n * title and a 13px intro line. The intro takes label-secondary, not the\n * label-tertiary dsh's own intro lines use: tertiary measures 3.71:1 on the\n * light theme's white, secondary 5.80:1. */\n.s2318c0_section {\n max-width: 760px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n color: var(--dsw-alias-label-primary);\n}\n\n.sf8f08d_sectionTitle {\n margin: 0;\n font-size: 18px;\n font-weight: 600;\n}\n\n.s73f38e_sectionIntro {\n margin: 0;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa2fe9b_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.s3d84d8_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.s23fd5f_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.se54361_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.se54361_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.sf859c5_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sb926ac_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.s2196b0_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s9cc9e0_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.se54361_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 .se54361_skeletonCard span { animation: none; opacity: 0.75; }\n .se54361_skeletonCard::after { content: none; }\n}\n\n.s67a61e_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.s67a61e_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.s67a61e_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s8c0536_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s5d1722_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s18aa1e_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s59f33b_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sab9c34_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sab9c34_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sab9c34_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.sbc6d1e_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.sbc6d1e_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.sbc6d1e_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sbc6d1e_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.s2bb87e_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s2bb87e_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.s2bb87e_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.s86377e_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.se2e462_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.se2e462_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* Subdued line under the search box when a dictionary intent fired (design\n * 2026-10-08-search-query-expansion §4). One fact, no badge. */\n.sbc236e_expansionNote {\n margin: 4px 0 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sbbc122_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(--shop-warn-on-tint);\n font-size: 12px;\n}\n\n.s127242_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.sc61628_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.sc61628_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.sc61628_categoryButton.sd09021_categoryButtonOn` — TWO classes — so it\n * matches `.sc61628_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.sd09021_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour: a pressed tab must occupy exactly the\n * same box as an unpressed one, so the bar does not reflow — and with ~9 pills\n * on a wrapping row, one that grows can push the rest onto another line and\n * move the tab out from under the pointer that just clicked it. That is what\n * `font-weight: 600` did (bold metrics are wider than regular), and it is why\n * nothing may be added here that resolves to a length: no padding, no\n * border-width, no font-size, no letter-spacing. `css-tokens.s3ce4ec_client.sfb0a9a_spec.s26cef9_ts`\n * fails the build on one.\n *\n * Nor a second line. An inset ring once doubled this border, and every control\n * in the stylesheet now draws exactly one 1px line, which is also the thinnest\n * the host paints (Chromium draws 0.5px as 1px). What separates pressed from\n * unpressed is lightness instead of thickness: the border is the hue mixed 30%\n * toward label-primary, the theme's foreground pole, so it clears 3:1 against\n * the bar in both themes for every hue — the raw hue measured 1.86:1 on white\n * for `ui` — and the fill under it is 22% of the hue. The `other` tab is the\n * case this is for: its hue IS the neutral gray this file uses for \"no\n * category\", so on the light theme the hue alone barely separates the two. */\n.sc61628_categoryButton.sd09021_categoryButtonOn {\n border-color: color-mix(in srgb, var(--category-hue) 70%, var(--dsw-alias-label-primary));\n background: color-mix(in srgb, var(--category-hue) 22%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours, so this mode alone gets an outline. */\n@media (forced-colors: active) {\n .sc61628_categoryButton.sd09021_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.sc61628_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.sc61628_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.s917a01_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.s917a01_incompatibleFilter:hover .se85a58_switch` outranks `.s7b2df9_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.s917a01_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.s917a01_incompatibleFilter:hover .se85a58_switch:not(.s7b2df9_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.sc61628_categoryButton.sd09021_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.s917a01_incompatibleFilter.s438aff_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.s917a01_incompatibleFilter .se85a58_switch:not(.s7b2df9_switchOn) .s894f35_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.s917a01_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s6d0e29_updatableHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.s81108c_catalogStatsRow {\n display: flex;\n align-items: center;\n /* The row holds the catalog line, the refresh button and the shelf order's\n * three pills (design 2026-10-12-shelf-orders §4). Without wrapping, the\n * last of them overflows the tab at a narrow width. */\n flex-wrap: wrap;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n/* The shelf's order control, at the right end of the stats row. `margin-left:\n * auto` is what puts it there while the line and the refresh button stay\n * left — and it is safe on a wrapping row, unlike a width. */\n.s6ca63b_sortGroup {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n}\n\n.sb4b816_sortLabel {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sb4fd92_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s67a61e_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.s8745d6_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.s8745d6_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s8745d6_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s8745d6_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s8ac1d5_catalogRefreshFailed {\n font-size: 11px;\n color: var(--shop-warn);\n}\n\n.s5bfb21_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sa7e6b4_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sa7e6b4_cards { animation: none; }\n}\n\n.sc86593_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.s1ecd69_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.sc06acb_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sc86593_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 .sc86593_card { transition: none; }\n .sc86593_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.s7480b4_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.sc61628_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.sc86593_card[data-category='tool'], .sc61628_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.sc86593_card[data-category='provider'], .sc61628_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.sc86593_card[data-category='ui'], .sc61628_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.sc86593_card[data-category='workflow'], .sc61628_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.sc86593_card[data-category='integration'], .sc61628_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.sc86593_card[data-category='theme'], .sc61628_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.sc86593_card[data-category='other'], .sc61628_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s91e3d3_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.s7c9d85_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.s7c9d85_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc94f56_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s6ae450_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s3529d3_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s3529d3_cardActions > .s202471_gate,\n.s3529d3_cardActions > .sd26f3f_installPanel,\n.s3529d3_cardActions > .sff3c46_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sf1425b_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n /* A release tag prints as tagged, never cut to seven characters (market\n * borrowings §11), and a tag can run to any length. The cap keeps every\n * tag of up to fourteen characters whole, prerelease suffix included\n * (`v0.1.0-alpha.0`): fourteen glyphs plus the letter-spacing above come to\n * about 14.5ch, so the cap is 15ch, not 14. A longer tag ends in an ellipsis,\n * and the tooltip carries the whole of it. */\n max-width: 15ch;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n/* The octocat marking a source the reader can read on GitHub, beside the\n * card's version. */\n.s8e3025_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s71e1b7_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s71e1b7_tierBadge[data-tier='verified'] {\n color: var(--shop-success);\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.s71e1b7_tierBadge[data-tier='verified-stale'] {\n color: var(--shop-warn-on-tint);\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.s71e1b7_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/* The §C10 shadowed badge: an installed github entry the catalog refuses\n * shadowed-by-npm. Subdued/secondary rather than a warn tint — nothing is\n * wrong with the install itself; the shelf just cannot show it. */\n.s71e1b7_tierBadge[data-tier='shadowed'] {\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/* The §C5 archived badge: a github repository the owner has frozen.\n * Subdued register, same as shadowed — nothing is wrong with the install;\n * the pinned commit still serves, so the mark is informational only. */\n.s71e1b7_tierBadge[data-tier='archived'] {\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.s36b613_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n /* On the tint, not on the plain surface: this text reads on its own 14% red\n * fill, where the 90% red would measure 4.21:1 (light) and 4.43:1 (dark). */\n color: var(--shop-error-on-tint);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.s63d9f6_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s1faef8_chevron, .s7a4b1f_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s7a4b1f_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s1faef8_chevron, .s7a4b1f_chevronOpen { transition: none; }\n}\n\n.sc53619_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.s617f51_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.se22e0f_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.s12a8f7_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sf04c34_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s8f5da6_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--shop-error);\n}\n\n/* The way out of dsh's own refusal: the lead-in, then the exemption command\n * on its own line. */\n/* Block layout rather than flex: Firefox 153 will not line-clamp a container\n * that holds a display:flex child (Chromium 151 does), and the fold's\n * measured goal — \"the whole red block, remedy included, as one unit\" —\n * needs the container to clamp in BOTH engines the shop targets. The 4px\n * rhythm between the lead-in and the command is kept by the margins of the\n * children. */\n.sb71cf3_remedy {\n display: block;\n}\n\n.sb71cf3_remedy > * + * { margin-top: 4px; }\n\n/* The fold under a card's whole red block: two lines by default, a small\n * Refresh-pill-shaped toggle under it. Two containers, one rhythm: the shelf\n * uses `.se8b041_foldBlock` collapsed and `.sd5e94d_foldBlockExpanded` open, and they share\n * one `display: block` (Firefox clamps no flex child inside a -webkit-box).\n * The clamp is on the BLOCK rather than any one line, so a 43-line remedy\n * counts as a unit instead of squashing the harness-peers line to a sliver\n * and hiding the command the reader actually came for. Both containers\n * still render the full block to a screen reader — `white-space: normal`\n * instead of pre-line keeps the copy's own `\\n` visible without forcing the\n * browser to lay the block's pieces like a text node. */\n.se8b041_foldBlock {\n display: block;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n display: -webkit-box;\n overflow: hidden;\n}\n\n.sd5e94d_foldBlockExpanded { display: block; }\n\n/* The toggle. Geometry and quiet voice are the Refresh pill's\n * (`catalogStatsRow` right beside it on the same screen): one 1px border,\n * 11px text, secondary label, no fill. It is the only one of its kind here,\n * so a class of its own means one function of its own. */\n.sd63d48_foldToggle {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.sd63d48_foldToggle:hover { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2)); }\n.sd63d48_foldToggle:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }\n\n.s9e108c_remedyLead {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The exemption command, bound for a terminal unchanged: one click selects\n * all of it, and it wraps anywhere rather than widening the card. A border,\n * not a fill, marks the box: the bg layers are one white in the light theme. */\n.s51d415_allowCommand {\n display: block;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n white-space: pre-wrap;\n word-break: break-all;\n user-select: all;\n color: var(--dsw-alias-label-primary);\n}\n\n/* A refused install on a surface that does not state its blockers — the\n * outdated row — writes them out on their own full-width line under the\n * refused label, the command that clears dsh's refusal included. Block,\n * rather than flex: Firefox will not line-clamp a -webkit-box that holds a\n * display:flex child, and the fold this element can sit inside of carries\n * that clamp. The 6px rhythm between the block's lines is carried by the\n * children's margins. */\n.sf48b40_refusal {\n display: block;\n flex-basis: 100%;\n}\n\n.sf48b40_refusal > * + * { margin-top: 6px; }\n\n.s24b64d_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s1cdba4_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s4bc6d7_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s4bc6d7_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.se00d68_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.seadb7e_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.s80932b_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.s80932b_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.s80932b_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.s80932b_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.s80932b_detailRow a:hover {\n text-decoration: underline;\n}\n\n.s80932b_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value —\n * a packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.s3529d3_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s2c7916_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The on-disk size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s2c7916_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row.\n *\n * `.sa2b5c4_shelfDate` shares this rule because it is the same kind of fact — a\n * fixed-width figure a reader compares down the shelf (design\n * 2026-10-12-shelf-orders §4) — and two copies of these declarations would\n * drift. */\n.s983d76_size,\n.sa2b5c4_shelfDate {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s2c7916_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s2c7916_cardMeta`. There is no middot:\n * `.s2c7916_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s787e2e_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.se111f0_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(--shop-warn-on-tint);\n}\n\n/* Install flow. */\n.sd26f3f_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n/* ONE geometry for everything that occupies the install button's place: the\n * button itself, the confirm/cancel pair beside the restart gate, the installed\n * label, and the refused label. Stated once because a refused label HAS to look\n * like the button it replaces apart from its colour and its text, and four\n * copies of the same five declarations is how they stop looking alike.\n * `cursor: pointer` is NOT here: only the controls that can be pressed take it,\n * while a label renders with the default cursor. */\n.s62735d_installButton,\n.s53e10c_confirmButton,\n.sd2236a_cancelButton,\n.s9eb0bd_installedLabel,\n.s6c656b_refusedLabel {\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}\n\n.s62735d_installButton, .s53e10c_confirmButton, .sd2236a_cancelButton {\n cursor: pointer;\n}\n\n.s62735d_installButton {\n border-color: var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n/* Between the click and the host's answer. The flow ignores a repeat start for\n * the same identity, so this is the visible half of the same fact: a control\n * that says \"not now\" rather than one that looks ready to send a second\n * install. Only the button can be in this state — a label is never pressed. */\n.s62735d_installButton:disabled {\n cursor: wait;\n}\n\n.s62735d_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s53e10c_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.s53e10c_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sd2236a_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.sd2236a_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s62735d_installButton:focus-visible, .s53e10c_confirmButton:focus-visible, .sd2236a_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.s9eb0bd_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/* The refused Install/Update button's replacement: the same box as the button\n * (the geometry rule above), filled solid red, not pressable. The fill is the\n * derived red rather than the raw token because text sits on it — bg-base on\n * `--shop-error` measures 5.31:1 light and 6.14:1 dark — and the raw error red\n * under bg-base text measures 4.498:1, the miss this whole family of fixes\n * exists for. Nothing here changes on hover: it is a statement, not a control,\n * so it keeps the default cursor and its colours. */\n.s6c656b_refusedLabel {\n border-color: var(--shop-error);\n background: var(--shop-error);\n color: var(--dsw-alias-bg-base);\n white-space: nowrap;\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.sff3c46_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.sd20046_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 /* The on-tint red: hovering this button paints a 10% red fill under the\n * word, where the plain red measures 4.50:1 (light) — the same two\n * thousandths short of the floor the raw token shipped at. */\n color: var(--shop-error-on-tint);\n font-size: 13px;\n cursor: pointer;\n}\n\n.sd20046_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.sd20046_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sac213f_restartSelfButton {\n /* The version row's third state. Geometry matches .sbc6d1e_checkUpdateButton and\n * .sab9c34_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sac213f_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s34ad1e_restartButton,\n.s234464_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s34ad1e_restartButton:hover,\n.s234464_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s34ad1e_restartButton:focus-visible,\n.s234464_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.sff3c46_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.s55782e_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.s202471_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.s656d08_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--shop-warn-on-tint);\n}\n\n.s83f089_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s26dca1_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--shop-error);\n}\n\n.sda785e_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.sf3a5fc_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s941e64_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.sc0d45c_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s2ddb67_notice {\n margin: 0;\n font-size: 12px;\n color: var(--shop-success);\n}\n\n.sff109f_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--shop-error);\n}\n\n.sb51cd3_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s7287ac_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--shop-error);\n}\n\n.sa26465_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\n.s438beb_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s6e16fa_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s46b13b_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.sc25007_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.sc25007_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.s1b342c_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s6ae450_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The §C10 shadowed section. Reuses the outdated list's visual language; only\n * the section spacing and the per-row explanation paragraph are its own. */\n.s6eace4_shadowedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s653245_shadowedDetail {\n margin: 4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s7d34ef_outdatedActions {\n display: flex;\n align-items: center;\n /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflows the settings dialog. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.s917a01_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.se85a58_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.se85a58_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s7b2df9_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.s894f35_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.s7b2df9_switchOn .s894f35_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.se85a58_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 .se85a58_switch, .s894f35_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sf25027_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.s7d34ef_outdatedActions`: `.s55782e_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sff3c46_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
9317
|
-
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"c4b688e1\"]")) {
|
|
9318
|
-
const tag = document.createElement("style");
|
|
9319
|
-
tag.dataset.pluginCss = "c4b688e1";
|
|
9320
|
-
tag.textContent = css;
|
|
9321
|
-
document.head.appendChild(tag);
|
|
9322
|
-
}
|
|
9323
|
-
var _dsh_plugin_shop_css_c4b688e1_default = {
|
|
9324
|
-
"client": "s3ce4ec_client",
|
|
9325
|
-
"config": "s2bd96f_config",
|
|
9326
|
-
"ts": "s26cef9_ts",
|
|
9327
|
-
"spec": "sfb0a9a_spec",
|
|
9328
|
-
"panel": "s8d78da_panel",
|
|
9329
|
-
"searchInput": "se2e462_searchInput",
|
|
9330
|
-
"toolbar": "s8c0536_toolbar",
|
|
9331
|
-
"section": "s2318c0_section",
|
|
9332
|
-
"sectionTitle": "sf8f08d_sectionTitle",
|
|
9333
|
-
"sectionIntro": "s73f38e_sectionIntro",
|
|
9334
|
-
"stateLine": "sa2fe9b_stateLine",
|
|
9335
|
-
"srOnly": "s3d84d8_srOnly",
|
|
9336
|
-
"skeletonGrid": "s23fd5f_skeletonGrid",
|
|
9337
|
-
"skeletonCard": "se54361_skeletonCard",
|
|
9338
|
-
"skeletonName": "sf859c5_skeletonName",
|
|
9339
|
-
"skeletonSummary": "sb926ac_skeletonSummary",
|
|
9340
|
-
"skeletonSummaryShort": "s2196b0_skeletonSummaryShort",
|
|
9341
|
-
"skeletonActions": "s9cc9e0_skeletonActions",
|
|
9342
|
-
"actionButton": "s67a61e_actionButton",
|
|
9343
|
-
"toolbarRight": "s5d1722_toolbarRight",
|
|
9344
|
-
"versionBlock": "s18aa1e_versionBlock",
|
|
9345
|
-
"versionText": "s59f33b_versionText",
|
|
9346
|
-
"updateSelfButton": "sab9c34_updateSelfButton",
|
|
9347
|
-
"checkUpdateButton": "sbc6d1e_checkUpdateButton",
|
|
9348
|
-
"githubLink": "s2bb87e_githubLink",
|
|
9349
|
-
"selfUpdatePanel": "s86377e_selfUpdatePanel",
|
|
9350
|
-
"expansionNote": "sbc236e_expansionNote",
|
|
9351
|
-
"staleBadge": "sbbc122_staleBadge",
|
|
9352
|
-
"categoryBar": "s127242_categoryBar",
|
|
9353
|
-
"categoryButton": "sc61628_categoryButton",
|
|
9354
|
-
"categoryButtonOn": "sd09021_categoryButtonOn",
|
|
9355
|
-
"incompatibleFilter": "s917a01_incompatibleFilter",
|
|
9356
|
-
"switch": "se85a58_switch",
|
|
9357
|
-
"switchOn": "s7b2df9_switchOn",
|
|
9358
|
-
"incompatibleFilterOn": "s438aff_incompatibleFilterOn",
|
|
9359
|
-
"switchKnob": "s894f35_switchKnob",
|
|
9360
|
-
"updatableHeading": "s6d0e29_updatableHeading",
|
|
9361
|
-
"catalogStatsRow": "s81108c_catalogStatsRow",
|
|
9362
|
-
"sortGroup": "s6ca63b_sortGroup",
|
|
9363
|
-
"sortLabel": "sb4b816_sortLabel",
|
|
9364
|
-
"catalogStats": "sb4fd92_catalogStats",
|
|
9365
|
-
"catalogRefreshButton": "s8745d6_catalogRefreshButton",
|
|
9366
|
-
"catalogRefreshFailed": "s8ac1d5_catalogRefreshFailed",
|
|
9367
|
-
"emptyLine": "s5bfb21_emptyLine",
|
|
9368
|
-
"cards": "sa7e6b4_cards",
|
|
9369
|
-
"card": "sc86593_card",
|
|
9370
|
-
"cardsSentry": "s1ecd69_cardsSentry",
|
|
9371
|
-
"showingLine": "sc06acb_showingLine",
|
|
9372
|
-
"cardSpine": "s7480b4_cardSpine",
|
|
9373
|
-
"categoryBadge": "s91e3d3_categoryBadge",
|
|
9374
|
-
"entryHeader": "s7c9d85_entryHeader",
|
|
9375
|
-
"name": "sc94f56_name",
|
|
9376
|
-
"badges": "s6ae450_badges",
|
|
9377
|
-
"cardActions": "s3529d3_cardActions",
|
|
9378
|
-
"gate": "s202471_gate",
|
|
9379
|
-
"installPanel": "sd26f3f_installPanel",
|
|
9380
|
-
"installedActions": "sff3c46_installedActions",
|
|
9381
|
-
"cardVersion": "sf1425b_cardVersion",
|
|
9382
|
-
"sourceBadge": "s8e3025_sourceBadge",
|
|
9383
|
-
"tierBadge": "s71e1b7_tierBadge",
|
|
9384
|
-
"incompatibleBadge": "s36b613_incompatibleBadge",
|
|
9385
|
-
"starsBadge": "s63d9f6_starsBadge",
|
|
9386
|
-
"chevron": "s1faef8_chevron",
|
|
9387
|
-
"chevronOpen": "s7a4b1f_chevronOpen",
|
|
9388
|
-
"body": "sc53619_body",
|
|
9389
|
-
"summary": "s617f51_summary",
|
|
9390
|
-
"summaryZh": "se22e0f_summaryZh",
|
|
9391
|
-
"summaryExpanded": "s12a8f7_summaryExpanded",
|
|
9392
|
-
"summaryZhExpanded": "sf04c34_summaryZhExpanded",
|
|
9393
|
-
"incompatibleDetail": "s8f5da6_incompatibleDetail",
|
|
9394
|
-
"remedy": "sb71cf3_remedy",
|
|
9395
|
-
"foldBlock": "se8b041_foldBlock",
|
|
9396
|
-
"foldBlockExpanded": "sd5e94d_foldBlockExpanded",
|
|
9397
|
-
"foldToggle": "sd63d48_foldToggle",
|
|
9398
|
-
"remedyLead": "s9e108c_remedyLead",
|
|
9399
|
-
"allowCommand": "s51d415_allowCommand",
|
|
9400
|
-
"refusal": "sf48b40_refusal",
|
|
9401
|
-
"capabilitiesBlock": "s24b64d_capabilitiesBlock",
|
|
9402
|
-
"capabilitiesNote": "s1cdba4_capabilitiesNote",
|
|
9403
|
-
"capabilities": "s4bc6d7_capabilities",
|
|
9404
|
-
"detail": "se00d68_detail",
|
|
9405
|
-
"detailRows": "seadb7e_detailRows",
|
|
9406
|
-
"detailRow": "s80932b_detailRow",
|
|
9407
|
-
"cardMeta": "s2c7916_cardMeta",
|
|
9408
|
-
"shelfDate": "sa2b5c4_shelfDate",
|
|
9409
|
-
"size": "s983d76_size",
|
|
9410
|
-
"author": "s787e2e_author",
|
|
9411
|
-
"reviewedLine": "se111f0_reviewedLine",
|
|
9412
|
-
"installButton": "s62735d_installButton",
|
|
9413
|
-
"confirmButton": "s53e10c_confirmButton",
|
|
9414
|
-
"cancelButton": "sd2236a_cancelButton",
|
|
9415
|
-
"installedLabel": "s9eb0bd_installedLabel",
|
|
9416
|
-
"refusedLabel": "s6c656b_refusedLabel",
|
|
9417
|
-
"uninstallButton": "sd20046_uninstallButton",
|
|
9418
|
-
"restartSelfButton": "sac213f_restartSelfButton",
|
|
9419
|
-
"restartButton": "s34ad1e_restartButton",
|
|
9420
|
-
"reloadButton": "s234464_reloadButton",
|
|
9421
|
-
"reloadPanel": "s55782e_reloadPanel",
|
|
9422
|
-
"gateTitle": "s656d08_gateTitle",
|
|
9423
|
-
"gateBody": "s83f089_gateBody",
|
|
9424
|
-
"gateWarning": "s26dca1_gateWarning",
|
|
9425
|
-
"gateActions": "sda785e_gateActions",
|
|
9426
|
-
"installing": "sf3a5fc_installing",
|
|
9427
|
-
"log": "s941e64_log",
|
|
9428
|
-
"logLine": "sc0d45c_logLine",
|
|
9429
|
-
"notice": "s2ddb67_notice",
|
|
9430
|
-
"failedHeading": "sff109f_failedHeading",
|
|
9431
|
-
"failedDetail": "sb51cd3_failedDetail",
|
|
9432
|
-
"rejectedCode": "s7287ac_rejectedCode",
|
|
9433
|
-
"rejectedDetail": "sa26465_rejectedDetail",
|
|
9434
|
-
"outdatedSection": "s438beb_outdatedSection",
|
|
9435
|
-
"outdatedList": "s6e16fa_outdatedList",
|
|
9436
|
-
"outdatedRow": "s46b13b_outdatedRow",
|
|
9437
|
-
"outdatedInfo": "sc25007_outdatedInfo",
|
|
9438
|
-
"outdatedVersions": "s1b342c_outdatedVersions",
|
|
9439
|
-
"shadowedSection": "s6eace4_shadowedSection",
|
|
9440
|
-
"shadowedDetail": "s653245_shadowedDetail",
|
|
9441
|
-
"outdatedActions": "s7d34ef_outdatedActions",
|
|
9442
|
-
"switchWrap": "sf25027_switchWrap"
|
|
9443
|
-
};
|
|
9444
|
-
//#endregion
|
|
9445
9932
|
//#region src/client/FoldableRedBlock.tsx
|
|
9446
9933
|
function FoldableRedBlock({ children, t, attrName }) {
|
|
9447
9934
|
const { rootRef, expandable, expanded, toggle } = useFoldableRedBlock();
|
|
9448
9935
|
const kindName = attrName.replace(/^data-shop-/, "");
|
|
9449
9936
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9450
|
-
className: expanded ?
|
|
9937
|
+
className: expanded ? _dsh_plugin_shop_css_a037354d_default.foldBlockExpanded : _dsh_plugin_shop_css_a037354d_default.foldBlock,
|
|
9451
9938
|
[attrName]: true,
|
|
9452
9939
|
ref: rootRef,
|
|
9453
9940
|
children
|
|
9454
9941
|
}), expandable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9455
9942
|
type: "button",
|
|
9456
|
-
className:
|
|
9943
|
+
className: _dsh_plugin_shop_css_a037354d_default.foldToggle,
|
|
9457
9944
|
"data-shop-fold": kindName,
|
|
9458
9945
|
"aria-expanded": expanded,
|
|
9459
9946
|
onClick: toggle,
|
|
@@ -9475,14 +9962,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9475
9962
|
* those nodes, and React's next commit throws on the ones it no longer finds
|
|
9476
9963
|
* (design 2026-09-26-market-borrowings §4). The tab renders no portals, so its
|
|
9477
9964
|
* root is its whole surface. */
|
|
9478
|
-
const PANEL_ROOT = `${
|
|
9965
|
+
const PANEL_ROOT = `${_dsh_plugin_shop_css_a037354d_default.panel} notranslate`;
|
|
9479
9966
|
function ChevronIcon({ open }) {
|
|
9480
9967
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
9481
9968
|
viewBox: "0 0 16 16",
|
|
9482
9969
|
width: "14",
|
|
9483
9970
|
height: "14",
|
|
9484
9971
|
"aria-hidden": "true",
|
|
9485
|
-
className: open ?
|
|
9972
|
+
className: open ? _dsh_plugin_shop_css_a037354d_default.chevronOpen : _dsh_plugin_shop_css_a037354d_default.chevron,
|
|
9486
9973
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
9487
9974
|
fill: "currentColor",
|
|
9488
9975
|
d: "M3.5 6 8 10.5 12.5 6l1 1L8 12.5 2.5 7l1-1Z"
|
|
@@ -9495,8 +9982,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9495
9982
|
* install controls. An installed plugin's card carries its installed row:
|
|
9496
9983
|
* current → the non-interactive installed label, behind → the update button;
|
|
9497
9984
|
* uninstalled → the install button. */
|
|
9498
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, order, shelfDay, installed, missing, harness, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView,
|
|
9499
|
-
const
|
|
9985
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, order, shelfDay, installed, missing, harness, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView, page }) {
|
|
9986
|
+
const key = entryKey(entry);
|
|
9987
|
+
const open = (0, react.useSyncExternalStore)(page.subscribeExpanded, () => page.isExpanded(key));
|
|
9988
|
+
const [, rerender] = (0, react.useReducer)((count) => count + 1, 0);
|
|
9500
9989
|
const blockers = stateBlockers(blockersOf(missing, harness, nameTakenBy), t);
|
|
9501
9990
|
const detailId = (0, react.useId)();
|
|
9502
9991
|
const summary = entry.catalog?.summary;
|
|
@@ -9514,32 +10003,35 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9514
10003
|
subdir: entry.subdir
|
|
9515
10004
|
};
|
|
9516
10005
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9517
|
-
className:
|
|
10006
|
+
className: _dsh_plugin_shop_css_a037354d_default.card,
|
|
9518
10007
|
"data-shop-entry": entry.name,
|
|
9519
10008
|
"data-category": category,
|
|
9520
10009
|
children: [
|
|
9521
10010
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9522
|
-
className:
|
|
10011
|
+
className: _dsh_plugin_shop_css_a037354d_default.cardSpine,
|
|
9523
10012
|
"aria-hidden": "true"
|
|
9524
10013
|
}),
|
|
9525
10014
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
9526
10015
|
type: "button",
|
|
9527
|
-
className:
|
|
10016
|
+
className: _dsh_plugin_shop_css_a037354d_default.entryHeader,
|
|
9528
10017
|
"aria-expanded": open,
|
|
9529
10018
|
"aria-controls": detailId,
|
|
9530
|
-
onClick: () =>
|
|
10019
|
+
onClick: () => {
|
|
10020
|
+
page.toggleExpanded(key);
|
|
10021
|
+
rerender();
|
|
10022
|
+
},
|
|
9531
10023
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9532
|
-
className:
|
|
10024
|
+
className: _dsh_plugin_shop_css_a037354d_default.name,
|
|
9533
10025
|
children: entry.name
|
|
9534
10026
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9535
|
-
className:
|
|
10027
|
+
className: _dsh_plugin_shop_css_a037354d_default.badges,
|
|
9536
10028
|
children: [
|
|
9537
10029
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9538
|
-
className:
|
|
10030
|
+
className: _dsh_plugin_shop_css_a037354d_default.categoryBadge,
|
|
9539
10031
|
children: t(categoryKey(entry))
|
|
9540
10032
|
}),
|
|
9541
10033
|
entry.source === "npm" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9542
|
-
className:
|
|
10034
|
+
className: _dsh_plugin_shop_css_a037354d_default.sourceBadge,
|
|
9543
10035
|
"data-shop-source-npm": true,
|
|
9544
10036
|
role: "img",
|
|
9545
10037
|
"aria-label": t("npmSource"),
|
|
@@ -9556,7 +10048,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9556
10048
|
})
|
|
9557
10049
|
}),
|
|
9558
10050
|
hasGithubHome(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9559
|
-
className:
|
|
10051
|
+
className: _dsh_plugin_shop_css_a037354d_default.sourceBadge,
|
|
9560
10052
|
"data-shop-source-github": true,
|
|
9561
10053
|
role: "img",
|
|
9562
10054
|
"aria-label": t("githubSource"),
|
|
@@ -9570,7 +10062,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9570
10062
|
})
|
|
9571
10063
|
}),
|
|
9572
10064
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9573
|
-
className:
|
|
10065
|
+
className: _dsh_plugin_shop_css_a037354d_default.cardVersion,
|
|
9574
10066
|
"data-card-version": true,
|
|
9575
10067
|
role: "img",
|
|
9576
10068
|
"aria-label": versionName,
|
|
@@ -9578,17 +10070,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9578
10070
|
children: version.text
|
|
9579
10071
|
}),
|
|
9580
10072
|
entry.tier !== "community" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9581
|
-
className:
|
|
10073
|
+
className: _dsh_plugin_shop_css_a037354d_default.tierBadge,
|
|
9582
10074
|
"data-tier": entry.tier,
|
|
9583
10075
|
children: t(tierKey(entry.tier))
|
|
9584
10076
|
}),
|
|
9585
10077
|
entry.archived === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9586
|
-
className:
|
|
10078
|
+
className: _dsh_plugin_shop_css_a037354d_default.tierBadge,
|
|
9587
10079
|
"data-tier": "archived",
|
|
9588
10080
|
children: t("archivedBadge")
|
|
9589
10081
|
}),
|
|
9590
10082
|
stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9591
|
-
className:
|
|
10083
|
+
className: _dsh_plugin_shop_css_a037354d_default.starsBadge,
|
|
9592
10084
|
role: "img",
|
|
9593
10085
|
"aria-label": t("stars", { count: stars }),
|
|
9594
10086
|
children: ["★ ", formatStars(stars)]
|
|
@@ -9598,14 +10090,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9598
10090
|
})]
|
|
9599
10091
|
}),
|
|
9600
10092
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9601
|
-
className:
|
|
10093
|
+
className: _dsh_plugin_shop_css_a037354d_default.body,
|
|
9602
10094
|
children: [
|
|
9603
10095
|
summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9604
|
-
className: open ? `${
|
|
10096
|
+
className: open ? `${_dsh_plugin_shop_css_a037354d_default.summary} ${_dsh_plugin_shop_css_a037354d_default.summaryExpanded}` : _dsh_plugin_shop_css_a037354d_default.summary,
|
|
9605
10097
|
children: summary.en
|
|
9606
10098
|
}),
|
|
9607
10099
|
summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9608
|
-
className: open ? `${
|
|
10100
|
+
className: open ? `${_dsh_plugin_shop_css_a037354d_default.summaryZh} ${_dsh_plugin_shop_css_a037354d_default.summaryZhExpanded}` : _dsh_plugin_shop_css_a037354d_default.summaryZh,
|
|
9609
10101
|
children: summary.zh
|
|
9610
10102
|
}),
|
|
9611
10103
|
blockers.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FoldableRedBlock, {
|
|
@@ -9613,28 +10105,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9613
10105
|
attrName: "data-shop-remedy",
|
|
9614
10106
|
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
9615
10107
|
blocker,
|
|
9616
|
-
className:
|
|
10108
|
+
className: _dsh_plugin_shop_css_a037354d_default.incompatibleDetail,
|
|
9617
10109
|
surface: "detail"
|
|
9618
10110
|
}, blocker.kind))
|
|
9619
10111
|
}),
|
|
9620
10112
|
open && entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9621
|
-
className:
|
|
10113
|
+
className: _dsh_plugin_shop_css_a037354d_default.capabilitiesBlock,
|
|
9622
10114
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9623
|
-
className:
|
|
10115
|
+
className: _dsh_plugin_shop_css_a037354d_default.capabilitiesNote,
|
|
9624
10116
|
children: t("capabilitiesNote")
|
|
9625
10117
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
9626
|
-
className:
|
|
10118
|
+
className: _dsh_plugin_shop_css_a037354d_default.capabilities,
|
|
9627
10119
|
children: entry.catalog.capabilities.map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capability }, capability))
|
|
9628
10120
|
})]
|
|
9629
10121
|
}),
|
|
9630
10122
|
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
9631
10123
|
id: detailId,
|
|
9632
|
-
className:
|
|
10124
|
+
className: _dsh_plugin_shop_css_a037354d_default.detail,
|
|
9633
10125
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
9634
|
-
className:
|
|
10126
|
+
className: _dsh_plugin_shop_css_a037354d_default.detailRows,
|
|
9635
10127
|
children: [
|
|
9636
10128
|
npmUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9637
|
-
className:
|
|
10129
|
+
className: _dsh_plugin_shop_css_a037354d_default.detailRow,
|
|
9638
10130
|
"data-shop-npm": true,
|
|
9639
10131
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("npmPage") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
9640
10132
|
href: npmUrl,
|
|
@@ -9644,7 +10136,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9644
10136
|
}) })]
|
|
9645
10137
|
}),
|
|
9646
10138
|
entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9647
|
-
className:
|
|
10139
|
+
className: _dsh_plugin_shop_css_a037354d_default.detailRow,
|
|
9648
10140
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("repository") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /^https?:\/\//.test(entry.repository) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
9649
10141
|
href: entry.repository,
|
|
9650
10142
|
target: "_blank",
|
|
@@ -9653,12 +10145,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9653
10145
|
}) : entry.repository })]
|
|
9654
10146
|
}),
|
|
9655
10147
|
entry.license !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9656
|
-
className:
|
|
10148
|
+
className: _dsh_plugin_shop_css_a037354d_default.detailRow,
|
|
9657
10149
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("license") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: isCustomLicense(entry.license) ? t("customLicense") : entry.license })]
|
|
9658
10150
|
})
|
|
9659
10151
|
]
|
|
9660
10152
|
}), entry.tier === "verified-stale" && entry.review !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9661
|
-
className:
|
|
10153
|
+
className: _dsh_plugin_shop_css_a037354d_default.reviewedLine,
|
|
9662
10154
|
children: entry.review.reviewedVersion !== void 0 ? t("reviewedVersionLine", {
|
|
9663
10155
|
reviewed: versionLabel("npm", entry.review.reviewedVersion).text,
|
|
9664
10156
|
current: version.text
|
|
@@ -9671,7 +10163,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9671
10163
|
]
|
|
9672
10164
|
}),
|
|
9673
10165
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9674
|
-
className:
|
|
10166
|
+
className: _dsh_plugin_shop_css_a037354d_default.cardActions,
|
|
9675
10167
|
"data-shop-actions": true,
|
|
9676
10168
|
children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!inInstalledView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
9677
10169
|
target: installTarget,
|
|
@@ -9703,7 +10195,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9703
10195
|
restartBlocked,
|
|
9704
10196
|
reload
|
|
9705
10197
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9706
|
-
className:
|
|
10198
|
+
className: _dsh_plugin_shop_css_a037354d_default.installedLabel,
|
|
9707
10199
|
"data-shop-installed": true,
|
|
9708
10200
|
children: t("installed")
|
|
9709
10201
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
|
|
@@ -9712,12 +10204,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9712
10204
|
})] }),
|
|
9713
10205
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
9714
10206
|
row: installed,
|
|
10207
|
+
surface: "card",
|
|
10208
|
+
page,
|
|
9715
10209
|
t,
|
|
9716
10210
|
setEnabled,
|
|
9717
10211
|
restart,
|
|
9718
10212
|
restartBlocked,
|
|
9719
|
-
reload
|
|
9720
|
-
restarts
|
|
10213
|
+
reload
|
|
9721
10214
|
}),
|
|
9722
10215
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
|
|
9723
10216
|
name: entry.name,
|
|
@@ -9728,15 +10221,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9728
10221
|
flow: uninstallFlow
|
|
9729
10222
|
})
|
|
9730
10223
|
] }), (size !== void 0 || author !== null || shelfDay !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9731
|
-
className:
|
|
10224
|
+
className: _dsh_plugin_shop_css_a037354d_default.cardMeta,
|
|
9732
10225
|
children: [
|
|
9733
10226
|
shelfDay !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9734
|
-
className:
|
|
10227
|
+
className: _dsh_plugin_shop_css_a037354d_default.shelfDate,
|
|
9735
10228
|
"data-shop-shelf-date": true,
|
|
9736
10229
|
children: t(order === "added" ? "listedOn" : "updatedOn", { date: shelfDay })
|
|
9737
10230
|
}),
|
|
9738
10231
|
size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9739
|
-
className:
|
|
10232
|
+
className: _dsh_plugin_shop_css_a037354d_default.size,
|
|
9740
10233
|
"data-shop-size": true,
|
|
9741
10234
|
role: "img",
|
|
9742
10235
|
"aria-label": t("sizeLabel", { size }),
|
|
@@ -9744,7 +10237,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9744
10237
|
children: size
|
|
9745
10238
|
}),
|
|
9746
10239
|
author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9747
|
-
className:
|
|
10240
|
+
className: _dsh_plugin_shop_css_a037354d_default.author,
|
|
9748
10241
|
"data-shop-author": true,
|
|
9749
10242
|
children: t("authorLine", { author })
|
|
9750
10243
|
})
|
|
@@ -9794,13 +10287,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9794
10287
|
...attribute,
|
|
9795
10288
|
children: blocker.text
|
|
9796
10289
|
}), blocker.remedy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9797
|
-
className:
|
|
10290
|
+
className: _dsh_plugin_shop_css_a037354d_default.remedy,
|
|
9798
10291
|
"data-shop-remedy": blocker.kind,
|
|
9799
10292
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9800
|
-
className:
|
|
10293
|
+
className: _dsh_plugin_shop_css_a037354d_default.remedyLead,
|
|
9801
10294
|
children: blocker.remedy.lead
|
|
9802
10295
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
9803
|
-
className:
|
|
10296
|
+
className: _dsh_plugin_shop_css_a037354d_default.allowCommand,
|
|
9804
10297
|
"data-shop-allow-command": true,
|
|
9805
10298
|
children: blocker.remedy.command
|
|
9806
10299
|
})]
|
|
@@ -9838,7 +10331,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9838
10331
|
if (lead === void 0 || word === null) return null;
|
|
9839
10332
|
const detail = blockers.map((blocker) => blocker.text).join("\n");
|
|
9840
10333
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9841
|
-
className:
|
|
10334
|
+
className: _dsh_plugin_shop_css_a037354d_default.incompatibleBadge,
|
|
9842
10335
|
"data-shop-blocker": lead.kind,
|
|
9843
10336
|
role: "img",
|
|
9844
10337
|
"aria-label": detail,
|
|
@@ -9848,51 +10341,59 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9848
10341
|
}
|
|
9849
10342
|
/** One entry's install flow: the button (or the refused LABEL that stands in
|
|
9850
10343
|
* its place), and the live view — running log, restart notice, failure detail,
|
|
9851
|
-
* rejection detail — driven by
|
|
9852
|
-
* (`variant: 'install'`) and the outdated rows' update
|
|
9853
|
-
* 'update'`, which drives the same install flow for
|
|
10344
|
+
* rejection detail — driven by the page store's install registry. Shared by
|
|
10345
|
+
* the catalog cards (`variant: 'install'`) and the outdated rows' update
|
|
10346
|
+
* button (`variant: 'update'`, which drives the same install flow for
|
|
10347
|
+
* `name@latest`).
|
|
9854
10348
|
*
|
|
9855
10349
|
* There is no §9.3 dialog. Until 2026-10-14 a community entry opened an
|
|
9856
10350
|
* acknowledgement gate first, and that gate was also the only thing standing
|
|
9857
10351
|
* between a second press and a second RPC — it reopened instead of asking
|
|
9858
10352
|
* again. What replaced it is one click that carries `acknowledged: true`, and
|
|
9859
|
-
* the in-flight guard
|
|
9860
|
-
* becoming a second RPC. The page states the
|
|
9861
|
-
* 2026-10-15 (the shop design's §7 amendment of
|
|
10353
|
+
* the flow registry's in-flight guard (flow-registry.ts, `starting`), which
|
|
10354
|
+
* keeps a second press from becoming a second RPC. The page states the
|
|
10355
|
+
* dialog's facts nowhere since 2026-10-15 (the shop design's §7 amendment of
|
|
10356
|
+
* that date). */
|
|
9862
10357
|
function InstallPanel({ target, tier, blockers, blockersStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
|
|
9863
|
-
const { view, start, starting } = flow;
|
|
10358
|
+
const { view, start, starting, reset } = flow;
|
|
10359
|
+
const request = {
|
|
10360
|
+
...target,
|
|
10361
|
+
acknowledged: tier === "verified" ? void 0 : true
|
|
10362
|
+
};
|
|
10363
|
+
const refused = refusesInstall(blockers);
|
|
10364
|
+
const word = blockerBadgeKey(blockers);
|
|
9864
10365
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9865
|
-
className:
|
|
10366
|
+
className: _dsh_plugin_shop_css_a037354d_default.installPanel,
|
|
9866
10367
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9867
|
-
className:
|
|
10368
|
+
className: _dsh_plugin_shop_css_a037354d_default.installing,
|
|
9868
10369
|
children: t(installPhaseKey(view.phase))
|
|
9869
10370
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9870
|
-
className:
|
|
10371
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
9871
10372
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9872
|
-
className:
|
|
10373
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
9873
10374
|
"data-shop-log-line": true,
|
|
9874
10375
|
children: line
|
|
9875
10376
|
}, index))
|
|
9876
10377
|
})]
|
|
9877
10378
|
});
|
|
9878
10379
|
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9879
|
-
className:
|
|
10380
|
+
className: _dsh_plugin_shop_css_a037354d_default.installedActions,
|
|
9880
10381
|
children: [
|
|
9881
10382
|
view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9882
|
-
className:
|
|
10383
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
9883
10384
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9884
|
-
className:
|
|
10385
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
9885
10386
|
"data-shop-log-line": true,
|
|
9886
10387
|
children: line
|
|
9887
10388
|
}, index))
|
|
9888
10389
|
}),
|
|
9889
10390
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9890
|
-
className:
|
|
10391
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
9891
10392
|
"data-shop-restart-notice": true,
|
|
9892
10393
|
children: t(activationNoticeKey(view.activation, view.restartReason))
|
|
9893
10394
|
}),
|
|
9894
10395
|
view.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9895
|
-
className:
|
|
10396
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
9896
10397
|
"data-shop-done-note": true,
|
|
9897
10398
|
children: view.detail
|
|
9898
10399
|
}),
|
|
@@ -9903,46 +10404,77 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9903
10404
|
restartBlocked,
|
|
9904
10405
|
reload,
|
|
9905
10406
|
where: "install"
|
|
10407
|
+
}),
|
|
10408
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10409
|
+
type: "button",
|
|
10410
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
10411
|
+
"data-shop-install-dismiss": true,
|
|
10412
|
+
onClick: reset,
|
|
10413
|
+
children: t("dismiss")
|
|
9906
10414
|
})
|
|
9907
10415
|
]
|
|
9908
10416
|
});
|
|
9909
10417
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9910
|
-
className:
|
|
10418
|
+
className: _dsh_plugin_shop_css_a037354d_default.installPanel,
|
|
9911
10419
|
children: [
|
|
9912
10420
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9913
|
-
className:
|
|
10421
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedHeading,
|
|
9914
10422
|
children: t("installFailed")
|
|
9915
10423
|
}),
|
|
9916
10424
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9917
|
-
className:
|
|
10425
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
9918
10426
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9919
|
-
className:
|
|
10427
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
9920
10428
|
"data-shop-log-line": true,
|
|
9921
10429
|
children: line
|
|
9922
10430
|
}, index))
|
|
9923
10431
|
}),
|
|
9924
10432
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9925
|
-
className:
|
|
10433
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
9926
10434
|
children: view.detail === "" ? t("installTransportFailed") : view.detail
|
|
10435
|
+
}),
|
|
10436
|
+
refused && word !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10437
|
+
type: "button",
|
|
10438
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
10439
|
+
"data-shop-install-dismiss": true,
|
|
10440
|
+
onClick: reset,
|
|
10441
|
+
children: t("dismiss")
|
|
10442
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10443
|
+
type: "button",
|
|
10444
|
+
className: _dsh_plugin_shop_css_a037354d_default.installButton,
|
|
10445
|
+
"data-shop-install-retry": true,
|
|
10446
|
+
disabled: starting,
|
|
10447
|
+
onClick: () => {
|
|
10448
|
+
start(request);
|
|
10449
|
+
},
|
|
10450
|
+
children: t("retry")
|
|
9927
10451
|
})
|
|
9928
10452
|
]
|
|
9929
10453
|
});
|
|
9930
10454
|
if (view.kind === "rejected") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9931
|
-
className:
|
|
9932
|
-
children: [
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
10455
|
+
className: _dsh_plugin_shop_css_a037354d_default.installPanel,
|
|
10456
|
+
children: [
|
|
10457
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10458
|
+
className: _dsh_plugin_shop_css_a037354d_default.rejectedCode,
|
|
10459
|
+
children: t(rejectionCodeKey(view.code))
|
|
10460
|
+
}),
|
|
10461
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10462
|
+
className: _dsh_plugin_shop_css_a037354d_default.rejectedDetail,
|
|
10463
|
+
children: view.detail
|
|
10464
|
+
}),
|
|
10465
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10466
|
+
type: "button",
|
|
10467
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
10468
|
+
"data-shop-install-dismiss": true,
|
|
10469
|
+
onClick: reset,
|
|
10470
|
+
children: t("dismiss")
|
|
10471
|
+
})
|
|
10472
|
+
]
|
|
9939
10473
|
});
|
|
9940
10474
|
const update = variant === "update";
|
|
9941
|
-
const refused = refusesInstall(blockers);
|
|
9942
|
-
const word = blockerBadgeKey(blockers);
|
|
9943
10475
|
const detail = blockers.map((blocker) => blocker.text).join("\n");
|
|
9944
10476
|
if (refused && word !== null) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9945
|
-
className:
|
|
10477
|
+
className: _dsh_plugin_shop_css_a037354d_default.refusedLabel,
|
|
9946
10478
|
"data-shop-refused": true,
|
|
9947
10479
|
"data-shop-blocker-kind": blockers[0]?.kind,
|
|
9948
10480
|
role: "img",
|
|
@@ -9954,21 +10486,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9954
10486
|
attrName: "data-shop-refusal",
|
|
9955
10487
|
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
9956
10488
|
blocker,
|
|
9957
|
-
className:
|
|
10489
|
+
className: _dsh_plugin_shop_css_a037354d_default.gateWarning,
|
|
9958
10490
|
surface: "warning"
|
|
9959
10491
|
}, blocker.kind))
|
|
9960
10492
|
})] });
|
|
9961
10493
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
9962
10494
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9963
10495
|
type: "button",
|
|
9964
|
-
className:
|
|
10496
|
+
className: _dsh_plugin_shop_css_a037354d_default.installButton,
|
|
9965
10497
|
disabled: starting,
|
|
9966
10498
|
...update ? { "data-shop-update": true } : { "data-shop-install": true },
|
|
9967
10499
|
onClick: () => {
|
|
9968
|
-
start(
|
|
9969
|
-
...target,
|
|
9970
|
-
acknowledged: tier === "verified" ? void 0 : true
|
|
9971
|
-
});
|
|
10500
|
+
start(request);
|
|
9972
10501
|
},
|
|
9973
10502
|
children: t(update ? "update" : "install")
|
|
9974
10503
|
}),
|
|
@@ -9981,7 +10510,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9981
10510
|
attrName: "data-shop-blocker-lines",
|
|
9982
10511
|
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
9983
10512
|
blocker,
|
|
9984
|
-
className:
|
|
10513
|
+
className: _dsh_plugin_shop_css_a037354d_default.gateWarning,
|
|
9985
10514
|
surface: "warning"
|
|
9986
10515
|
}, blocker.kind))
|
|
9987
10516
|
})
|
|
@@ -9996,32 +10525,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9996
10525
|
function UninstallPanel({ name, t, restart, restartBlocked, reload, flow }) {
|
|
9997
10526
|
const { view, start, reset } = flow;
|
|
9998
10527
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9999
|
-
className:
|
|
10528
|
+
className: _dsh_plugin_shop_css_a037354d_default.installPanel,
|
|
10000
10529
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10001
|
-
className:
|
|
10530
|
+
className: _dsh_plugin_shop_css_a037354d_default.installing,
|
|
10002
10531
|
children: t("uninstalling")
|
|
10003
10532
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10004
|
-
className:
|
|
10533
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
10005
10534
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10006
|
-
className:
|
|
10535
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
10007
10536
|
"data-shop-log-line": true,
|
|
10008
10537
|
children: line
|
|
10009
10538
|
}, index))
|
|
10010
10539
|
})]
|
|
10011
10540
|
});
|
|
10012
10541
|
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10013
|
-
className:
|
|
10542
|
+
className: _dsh_plugin_shop_css_a037354d_default.installedActions,
|
|
10014
10543
|
children: [
|
|
10015
10544
|
view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10016
|
-
className:
|
|
10545
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
10017
10546
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10018
|
-
className:
|
|
10547
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
10019
10548
|
"data-shop-log-line": true,
|
|
10020
10549
|
children: line
|
|
10021
10550
|
}, index))
|
|
10022
10551
|
}),
|
|
10023
10552
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10024
|
-
className:
|
|
10553
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10025
10554
|
"data-shop-uninstall-done": true,
|
|
10026
10555
|
children: t(uninstallActivationNoticeKey(view.activation))
|
|
10027
10556
|
}),
|
|
@@ -10035,7 +10564,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10035
10564
|
}),
|
|
10036
10565
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10037
10566
|
type: "button",
|
|
10038
|
-
className:
|
|
10567
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
10039
10568
|
"data-shop-uninstall-dismiss": true,
|
|
10040
10569
|
onClick: reset,
|
|
10041
10570
|
children: t("dismiss")
|
|
@@ -10043,37 +10572,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10043
10572
|
]
|
|
10044
10573
|
});
|
|
10045
10574
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10046
|
-
className:
|
|
10575
|
+
className: _dsh_plugin_shop_css_a037354d_default.installPanel,
|
|
10047
10576
|
children: [
|
|
10048
10577
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10049
|
-
className:
|
|
10578
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedHeading,
|
|
10050
10579
|
children: t("uninstallFailed")
|
|
10051
10580
|
}),
|
|
10052
10581
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10053
|
-
className:
|
|
10582
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
10054
10583
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10055
|
-
className:
|
|
10584
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
10056
10585
|
"data-shop-log-line": true,
|
|
10057
10586
|
children: line
|
|
10058
10587
|
}, index))
|
|
10059
10588
|
}),
|
|
10060
10589
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10061
|
-
className:
|
|
10590
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10062
10591
|
children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
|
|
10063
10592
|
}),
|
|
10064
10593
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10065
10594
|
type: "button",
|
|
10066
|
-
className:
|
|
10595
|
+
className: _dsh_plugin_shop_css_a037354d_default.uninstallButton,
|
|
10067
10596
|
"data-shop-uninstall-retry": true,
|
|
10068
10597
|
onClick: () => void start({ name }),
|
|
10069
10598
|
children: t("retry")
|
|
10599
|
+
}),
|
|
10600
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10601
|
+
type: "button",
|
|
10602
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
10603
|
+
"data-shop-uninstall-dismiss": true,
|
|
10604
|
+
onClick: reset,
|
|
10605
|
+
children: t("dismiss")
|
|
10070
10606
|
})
|
|
10071
10607
|
]
|
|
10072
10608
|
});
|
|
10073
10609
|
if (view.kind !== "idle") return null;
|
|
10074
10610
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10075
10611
|
type: "button",
|
|
10076
|
-
className:
|
|
10612
|
+
className: _dsh_plugin_shop_css_a037354d_default.uninstallButton,
|
|
10077
10613
|
"data-shop-uninstall": true,
|
|
10078
10614
|
onClick: () => void start({ name }),
|
|
10079
10615
|
children: t("uninstall")
|
|
@@ -10114,39 +10650,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10114
10650
|
else if (outcome.kind === "unreachable") setRefusal(t("restartTransportFailed"));
|
|
10115
10651
|
};
|
|
10116
10652
|
if (monitor?.kind === "requesting" || monitor?.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10117
|
-
className:
|
|
10653
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10118
10654
|
"data-shop-restarting": true,
|
|
10119
10655
|
children: t("restarting")
|
|
10120
10656
|
});
|
|
10121
10657
|
if (monitor?.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10122
|
-
className:
|
|
10658
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10123
10659
|
"data-shop-restart-error": true,
|
|
10124
10660
|
children: monitor.logFile === void 0 ? t("restartFailedNotice") : t("restartFailedLogNotice", { log: monitor.logFile })
|
|
10125
10661
|
});
|
|
10126
10662
|
if (monitor?.kind === "not-restarted") return null;
|
|
10127
10663
|
if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10128
|
-
className:
|
|
10664
|
+
className: _dsh_plugin_shop_css_a037354d_default.gate,
|
|
10129
10665
|
"data-shop-restart-gate": true,
|
|
10130
10666
|
children: [
|
|
10131
10667
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10132
|
-
className:
|
|
10668
|
+
className: _dsh_plugin_shop_css_a037354d_default.gateTitle,
|
|
10133
10669
|
children: t("restartTitle")
|
|
10134
10670
|
}),
|
|
10135
10671
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10136
|
-
className:
|
|
10672
|
+
className: _dsh_plugin_shop_css_a037354d_default.gateBody,
|
|
10137
10673
|
children: t("restartBody")
|
|
10138
10674
|
}),
|
|
10139
10675
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10140
|
-
className:
|
|
10676
|
+
className: _dsh_plugin_shop_css_a037354d_default.gateActions,
|
|
10141
10677
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10142
10678
|
type: "button",
|
|
10143
|
-
className:
|
|
10679
|
+
className: _dsh_plugin_shop_css_a037354d_default.confirmButton,
|
|
10144
10680
|
"data-shop-restart-confirm": true,
|
|
10145
10681
|
onClick: () => void onConfirm(),
|
|
10146
10682
|
children: t("restartConfirm")
|
|
10147
10683
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10148
10684
|
type: "button",
|
|
10149
|
-
className:
|
|
10685
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
10150
10686
|
onClick: () => setGateOpen(false),
|
|
10151
10687
|
children: t("cancel")
|
|
10152
10688
|
})]
|
|
@@ -10154,14 +10690,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10154
10690
|
]
|
|
10155
10691
|
});
|
|
10156
10692
|
const refused = refusal === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10157
|
-
className:
|
|
10693
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10158
10694
|
"data-shop-restart-error": true,
|
|
10159
10695
|
children: refusal
|
|
10160
10696
|
});
|
|
10161
10697
|
if (gate !== void 0) return refused;
|
|
10162
10698
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [refused, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10163
10699
|
type: "button",
|
|
10164
|
-
className:
|
|
10700
|
+
className: _dsh_plugin_shop_css_a037354d_default.restartButton,
|
|
10165
10701
|
"data-shop-restart": true,
|
|
10166
10702
|
onClick: () => setGateOpen(true),
|
|
10167
10703
|
children: t("restart")
|
|
@@ -10179,13 +10715,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10179
10715
|
*/
|
|
10180
10716
|
function NotRestartedNotice({ t, pid, logFile }) {
|
|
10181
10717
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10182
|
-
className:
|
|
10718
|
+
className: _dsh_plugin_shop_css_a037354d_default.selfUpdatePanel,
|
|
10183
10719
|
"data-shop-not-restarted": true,
|
|
10184
10720
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10185
|
-
className:
|
|
10721
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10186
10722
|
children: pid === void 0 ? t("notRestartedNotice") : t("notRestartedPidNotice", { pid: String(pid) })
|
|
10187
10723
|
}), logFile !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10188
|
-
className:
|
|
10724
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10189
10725
|
children: t("notRestartedLogNotice", { log: logFile })
|
|
10190
10726
|
})]
|
|
10191
10727
|
});
|
|
@@ -10215,7 +10751,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10215
10751
|
restart,
|
|
10216
10752
|
reload
|
|
10217
10753
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10218
|
-
className:
|
|
10754
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10219
10755
|
"data-shop-restart-disabled": true,
|
|
10220
10756
|
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
10221
10757
|
});
|
|
@@ -10226,14 +10762,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10226
10762
|
* would help without knowing that now is a good time. */
|
|
10227
10763
|
function ReloadPanel({ t, reload, where }) {
|
|
10228
10764
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10229
|
-
className:
|
|
10765
|
+
className: _dsh_plugin_shop_css_a037354d_default.reloadPanel,
|
|
10230
10766
|
"data-shop-reload": where,
|
|
10231
10767
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10232
|
-
className:
|
|
10768
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10233
10769
|
children: t("reloadNote")
|
|
10234
10770
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10235
10771
|
type: "button",
|
|
10236
|
-
className:
|
|
10772
|
+
className: _dsh_plugin_shop_css_a037354d_default.reloadButton,
|
|
10237
10773
|
onClick: reload,
|
|
10238
10774
|
children: t("reload")
|
|
10239
10775
|
})]
|
|
@@ -10246,21 +10782,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10246
10782
|
* transport throw renders the localized failure line — its private detail
|
|
10247
10783
|
* (which can name hosts and ports) never reaches the UI.
|
|
10248
10784
|
*/
|
|
10249
|
-
function EnabledSwitch({ row, t, setEnabled, restart, restartBlocked, reload
|
|
10785
|
+
function EnabledSwitch({ row, surface, page, t, setEnabled, restart, restartBlocked, reload }) {
|
|
10250
10786
|
const [enabled, setEnabledState] = (0, react.useState)(row.enabled);
|
|
10251
10787
|
const [toggle, setToggle] = (0, react.useState)({ kind: "idle" });
|
|
10252
|
-
const
|
|
10253
|
-
const
|
|
10254
|
-
(0, react.useEffect)(() => {
|
|
10255
|
-
if (restartsSeen.current === restarts) return;
|
|
10256
|
-
restartsSeen.current = restarts;
|
|
10257
|
-
if (owed !== "restart") return;
|
|
10258
|
-
setOwed("nothing");
|
|
10259
|
-
setToggle((current) => current.kind === "saved" ? { kind: "idle" } : current);
|
|
10260
|
-
}, [restarts, owed]);
|
|
10788
|
+
const owedKey = switchKey(surface, identityKey(row));
|
|
10789
|
+
const owed = (0, react.useSyncExternalStore)(page.subscribeOwed, () => page.getOwed(owedKey) ?? "nothing");
|
|
10261
10790
|
const onToggle = async () => {
|
|
10262
10791
|
if (toggle.kind === "saving") return;
|
|
10263
10792
|
const next = !enabled;
|
|
10793
|
+
const since = page.owedEpoch();
|
|
10264
10794
|
setToggle({ kind: "saving" });
|
|
10265
10795
|
try {
|
|
10266
10796
|
const result = await setEnabled({
|
|
@@ -10269,9 +10799,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10269
10799
|
});
|
|
10270
10800
|
if (result.ok) {
|
|
10271
10801
|
setEnabledState(next);
|
|
10272
|
-
setToggle({
|
|
10273
|
-
|
|
10274
|
-
|
|
10802
|
+
setToggle({
|
|
10803
|
+
kind: "saved",
|
|
10804
|
+
live: result.activation === "live"
|
|
10805
|
+
});
|
|
10806
|
+
if (result.activation === "restart") page.setOwed(owedKey, "restart", since);
|
|
10807
|
+
else if (result.activation !== "live") page.setOwed(owedKey, "reload", since);
|
|
10275
10808
|
} else setToggle({
|
|
10276
10809
|
kind: "error",
|
|
10277
10810
|
detail: result.detail || t("toggleFailed")
|
|
@@ -10284,7 +10817,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10284
10817
|
}
|
|
10285
10818
|
};
|
|
10286
10819
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10287
|
-
className:
|
|
10820
|
+
className: _dsh_plugin_shop_css_a037354d_default.switchWrap,
|
|
10288
10821
|
"data-shop-enabled-switch": row.name,
|
|
10289
10822
|
children: [
|
|
10290
10823
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -10293,18 +10826,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10293
10826
|
"aria-checked": enabled,
|
|
10294
10827
|
"aria-label": t("enabledSwitch"),
|
|
10295
10828
|
"data-shop-toggle": true,
|
|
10296
|
-
className: `${
|
|
10829
|
+
className: `${_dsh_plugin_shop_css_a037354d_default.switch} ${enabled ? _dsh_plugin_shop_css_a037354d_default.switchOn : ""}`,
|
|
10297
10830
|
onClick: () => void onToggle(),
|
|
10298
10831
|
disabled: toggle.kind === "saving",
|
|
10299
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
10832
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_a037354d_default.switchKnob })
|
|
10300
10833
|
}),
|
|
10301
|
-
toggle.kind === "saved" && owed === "nothing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10302
|
-
className:
|
|
10834
|
+
toggle.kind === "saved" && toggle.live && owed === "nothing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10835
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10303
10836
|
"data-shop-hot-apply": true,
|
|
10304
10837
|
children: t("hotApplyNote")
|
|
10305
10838
|
}),
|
|
10306
10839
|
owed === "restart" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10307
|
-
className:
|
|
10840
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10308
10841
|
"data-shop-toggle-restart": true,
|
|
10309
10842
|
children: t("toggleRestartNote")
|
|
10310
10843
|
}),
|
|
@@ -10317,7 +10850,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10317
10850
|
where: "toggle"
|
|
10318
10851
|
}),
|
|
10319
10852
|
toggle.kind === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10320
|
-
className:
|
|
10853
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10321
10854
|
"data-shop-toggle-error": true,
|
|
10322
10855
|
children: toggle.detail
|
|
10323
10856
|
})
|
|
@@ -10330,29 +10863,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10330
10863
|
* carry a `latest` string — a catalog truth to compare against; §C10's
|
|
10331
10864
|
* shadowed rows hold `latest: null` and never reach this control, so the
|
|
10332
10865
|
* field is narrowed at the parameter. */
|
|
10333
|
-
function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload,
|
|
10866
|
+
function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload, page }) {
|
|
10334
10867
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10335
|
-
className:
|
|
10868
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedRow,
|
|
10336
10869
|
"data-shop-outdated-entry": row.name,
|
|
10337
10870
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10338
|
-
className:
|
|
10871
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedInfo,
|
|
10339
10872
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10340
|
-
className:
|
|
10873
|
+
className: _dsh_plugin_shop_css_a037354d_default.name,
|
|
10341
10874
|
children: row.name
|
|
10342
10875
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
10343
|
-
className:
|
|
10876
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedVersions,
|
|
10344
10877
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: versionLabel(row.source, row.installed).text }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: versionLabel(row.source, row.latest).text }) })]
|
|
10345
10878
|
})]
|
|
10346
10879
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10347
|
-
className:
|
|
10880
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedActions,
|
|
10348
10881
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
10349
10882
|
row,
|
|
10883
|
+
surface: "row",
|
|
10884
|
+
page,
|
|
10350
10885
|
t,
|
|
10351
10886
|
setEnabled,
|
|
10352
10887
|
restart,
|
|
10353
10888
|
restartBlocked,
|
|
10354
|
-
reload
|
|
10355
|
-
restarts
|
|
10889
|
+
reload
|
|
10356
10890
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
10357
10891
|
target: {
|
|
10358
10892
|
name: row.name,
|
|
@@ -10392,25 +10926,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10392
10926
|
* community entry's does); an entry absent from the catalog is read as
|
|
10393
10927
|
* community, so its request acknowledges, which the host accepts for any
|
|
10394
10928
|
* tier. */
|
|
10395
|
-
function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload,
|
|
10929
|
+
function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload, page }) {
|
|
10396
10930
|
if (state.kind === "loading") return null;
|
|
10397
10931
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10398
|
-
className:
|
|
10932
|
+
className: _dsh_plugin_shop_css_a037354d_default.stateLine,
|
|
10399
10933
|
"data-shop-outdated-error": true,
|
|
10400
10934
|
children: t("installedError")
|
|
10401
10935
|
});
|
|
10402
10936
|
const outdated = state.entries.filter((entry) => entry.outdated && entry.latest !== null);
|
|
10403
10937
|
if (outdated.length === 0) return null;
|
|
10404
10938
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
10405
|
-
className:
|
|
10939
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedSection,
|
|
10406
10940
|
"data-shop-outdated": true,
|
|
10407
10941
|
"aria-labelledby": UPDATABLE_HEADING_ID,
|
|
10408
10942
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
10409
|
-
className:
|
|
10943
|
+
className: _dsh_plugin_shop_css_a037354d_default.updatableHeading,
|
|
10410
10944
|
id: UPDATABLE_HEADING_ID,
|
|
10411
10945
|
children: t("updatableSection")
|
|
10412
10946
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
10413
|
-
className:
|
|
10947
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedList,
|
|
10414
10948
|
children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
|
|
10415
10949
|
row,
|
|
10416
10950
|
tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
|
|
@@ -10422,7 +10956,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10422
10956
|
restart,
|
|
10423
10957
|
restartBlocked,
|
|
10424
10958
|
reload,
|
|
10425
|
-
|
|
10959
|
+
page
|
|
10426
10960
|
}) }, identityKey(row)))
|
|
10427
10961
|
})]
|
|
10428
10962
|
});
|
|
@@ -10438,37 +10972,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10438
10972
|
const shadowed = entries.filter((entry) => entry.shadowed === true);
|
|
10439
10973
|
if (shadowed.length === 0) return null;
|
|
10440
10974
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
10441
|
-
className:
|
|
10975
|
+
className: _dsh_plugin_shop_css_a037354d_default.shadowedSection,
|
|
10442
10976
|
"data-shop-shadowed": true,
|
|
10443
10977
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
10444
|
-
className:
|
|
10978
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedList,
|
|
10445
10979
|
children: shadowed.map((row) => {
|
|
10446
10980
|
const key = identityKey(row);
|
|
10447
10981
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10448
|
-
className:
|
|
10982
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedRow,
|
|
10449
10983
|
"data-shop-shadowed-entry": row.name,
|
|
10450
10984
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10451
|
-
className:
|
|
10985
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedInfo,
|
|
10452
10986
|
children: [
|
|
10453
10987
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10454
|
-
className:
|
|
10988
|
+
className: _dsh_plugin_shop_css_a037354d_default.name,
|
|
10455
10989
|
children: row.name
|
|
10456
10990
|
}),
|
|
10457
10991
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10458
|
-
className:
|
|
10992
|
+
className: _dsh_plugin_shop_css_a037354d_default.badges,
|
|
10459
10993
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10460
|
-
className:
|
|
10994
|
+
className: _dsh_plugin_shop_css_a037354d_default.tierBadge,
|
|
10461
10995
|
"data-tier": "shadowed",
|
|
10462
10996
|
children: t("shadowedBadge")
|
|
10463
10997
|
})
|
|
10464
10998
|
}),
|
|
10465
10999
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10466
|
-
className:
|
|
11000
|
+
className: _dsh_plugin_shop_css_a037354d_default.shadowedDetail,
|
|
10467
11001
|
children: t("shadowedDetail")
|
|
10468
11002
|
})
|
|
10469
11003
|
]
|
|
10470
11004
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10471
|
-
className:
|
|
11005
|
+
className: _dsh_plugin_shop_css_a037354d_default.outdatedActions,
|
|
10472
11006
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
|
|
10473
11007
|
name: row.name,
|
|
10474
11008
|
t,
|
|
@@ -10518,16 +11052,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10518
11052
|
role: "alert",
|
|
10519
11053
|
children: [
|
|
10520
11054
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10521
|
-
className:
|
|
11055
|
+
className: _dsh_plugin_shop_css_a037354d_default.stateLine,
|
|
10522
11056
|
children: say("tabCrashed", en.tabCrashed)
|
|
10523
11057
|
}),
|
|
10524
11058
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10525
|
-
className:
|
|
11059
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
10526
11060
|
children: error.message
|
|
10527
11061
|
}),
|
|
10528
11062
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10529
11063
|
type: "button",
|
|
10530
|
-
className:
|
|
11064
|
+
className: _dsh_plugin_shop_css_a037354d_default.actionButton,
|
|
10531
11065
|
onClick: () => this.setState({ error: null }),
|
|
10532
11066
|
children: say("retry", en.retry)
|
|
10533
11067
|
})
|
|
@@ -10535,10 +11069,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10535
11069
|
});
|
|
10536
11070
|
}
|
|
10537
11071
|
};
|
|
10538
|
-
/** The shop body inside its own error boundary, as `
|
|
10539
|
-
* below the page's
|
|
10540
|
-
*
|
|
10541
|
-
*
|
|
11072
|
+
/** The shop body inside its own error boundary, as `ShopPage` renders it
|
|
11073
|
+
* below the page's head (index.ts registers `ShopPage` in the main column).
|
|
11074
|
+
* Exported under the name the body used to carry, so nothing can render the
|
|
11075
|
+
* body without the boundary. */
|
|
10542
11076
|
function ShopTab(props) {
|
|
10543
11077
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopErrorBoundary, {
|
|
10544
11078
|
t: props.t,
|
|
@@ -10548,7 +11082,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10548
11082
|
/** The shop tab body: browse, search, refresh, and render one card per
|
|
10549
11083
|
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
10550
11084
|
function ShopTabBody(props) {
|
|
10551
|
-
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, onRestarted, updateStart, reload: injectedReload } = props;
|
|
11085
|
+
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, onRestarted, updateStart, reload: injectedReload, page: injectedPage, onShelfReady } = props;
|
|
10552
11086
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
10553
11087
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
10554
11088
|
/** The install gate's own input, or undefined while it is unknown — loading,
|
|
@@ -10562,63 +11096,64 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10562
11096
|
globalThis.location.reload();
|
|
10563
11097
|
}), [injectedReload]);
|
|
10564
11098
|
const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
|
|
10565
|
-
const
|
|
11099
|
+
const ownDeps = (0, react.useRef)({
|
|
11100
|
+
install,
|
|
11101
|
+
uninstall,
|
|
11102
|
+
updateStart,
|
|
11103
|
+
installStatus,
|
|
11104
|
+
noteUninstalled
|
|
11105
|
+
});
|
|
11106
|
+
ownDeps.current = {
|
|
11107
|
+
install,
|
|
11108
|
+
uninstall,
|
|
11109
|
+
updateStart,
|
|
11110
|
+
installStatus,
|
|
11111
|
+
noteUninstalled
|
|
11112
|
+
};
|
|
11113
|
+
const [ownPage] = (0, react.useState)(() => injectedPage === void 0 ? createPageStore(() => ownDeps.current, { pollUnwatched: false }) : null);
|
|
11114
|
+
const page = injectedPage ?? ownPage;
|
|
11115
|
+
if (page === null) throw new Error("dsh-plugin-shop: the shop face stopped handing over the page store the tab mounted with");
|
|
11116
|
+
const pageState = (0, react.useSyncExternalStore)(page.subscribe, page.getSnapshot);
|
|
11117
|
+
const { query, category, order, hideIncompatible, visibleCount } = pageState.view;
|
|
11118
|
+
const selfUpdate = useRegistrySelf(page.self);
|
|
10566
11119
|
const restartMonitor = useRestartMonitor();
|
|
10567
|
-
const hostPending = selfVersion !== null && selfVersion
|
|
11120
|
+
const hostPending = selfVersion !== null && updateAwaitsRestart(selfVersion);
|
|
10568
11121
|
const selfPending = selfUpdate.view.kind === "done" || selfUpdate.view.kind === "idle" && hostPending;
|
|
10569
|
-
const [request, setRequest] = (0, react.useState)(
|
|
11122
|
+
const [request, setRequest] = (0, react.useState)(() => {
|
|
11123
|
+
const { reverdictPending, reverdicts } = page.getSnapshot();
|
|
11124
|
+
return reverdictPending ? {
|
|
11125
|
+
kind: "reverdict",
|
|
11126
|
+
reverdicts
|
|
11127
|
+
} : { kind: "initial" };
|
|
11128
|
+
});
|
|
11129
|
+
const askedFor = (0, react.useRef)(request.kind === "reverdict" ? request.reverdicts : null);
|
|
11130
|
+
(0, react.useEffect)(() => {
|
|
11131
|
+
if (!pageState.reverdictPending || askedFor.current === pageState.reverdicts) return;
|
|
11132
|
+
askedFor.current = pageState.reverdicts;
|
|
11133
|
+
setRequest({
|
|
11134
|
+
kind: "reverdict",
|
|
11135
|
+
reverdicts: pageState.reverdicts
|
|
11136
|
+
});
|
|
11137
|
+
}, [pageState.reverdictPending, pageState.reverdicts]);
|
|
11138
|
+
const pendingReverdict = () => {
|
|
11139
|
+
const { reverdictPending, reverdicts } = page.getSnapshot();
|
|
11140
|
+
return reverdictPending ? reverdicts : null;
|
|
11141
|
+
};
|
|
10570
11142
|
const [versionReload, setVersionReload] = (0, react.useState)(0);
|
|
10571
|
-
const
|
|
10572
|
-
const
|
|
10573
|
-
|
|
10574
|
-
}, []);
|
|
10575
|
-
const uninstallResetRef = (0, react.useRef)(() => {});
|
|
10576
|
-
const flows = useInstallFlows(install, installStatus, (0, react.useCallback)((key, outcome) => {
|
|
10577
|
-
if (outcome === "done") uninstallResetRef.current(key);
|
|
10578
|
-
noteMutation();
|
|
10579
|
-
if (outcome === "done") setRequest({ kind: "reverdict" });
|
|
10580
|
-
}, [noteMutation]));
|
|
10581
|
-
const uninstallFlows = useUninstallFlows(uninstall, installStatus, (0, react.useCallback)((key, outcome) => {
|
|
10582
|
-
if (outcome !== "done") return;
|
|
10583
|
-
flows.flowFor(key).reset();
|
|
10584
|
-
noteMutation();
|
|
10585
|
-
const name = catalogState.kind === "ready" ? catalogState.result.plugins.find((entry) => entryKey(entry) === key)?.name : void 0;
|
|
10586
|
-
if (name !== void 0) noteUninstalled?.(name);
|
|
10587
|
-
setRequest({ kind: "reverdict" });
|
|
10588
|
-
}, [
|
|
10589
|
-
flows,
|
|
10590
|
-
noteMutation,
|
|
10591
|
-
catalogState,
|
|
10592
|
-
noteUninstalled
|
|
10593
|
-
]));
|
|
10594
|
-
uninstallResetRef.current = uninstallFlows.resetFlow;
|
|
10595
|
-
const [restarts, setRestarts] = (0, react.useState)(0);
|
|
10596
|
-
const resetSelfUpdate = selfUpdate.resetWhere;
|
|
10597
|
-
const resetInstalls = flows.resetWhere;
|
|
10598
|
-
const resetUninstalls = uninstallFlows.resetWhere;
|
|
11143
|
+
const { settled } = pageState;
|
|
11144
|
+
const flows = useRegistryFlows(page.installs);
|
|
11145
|
+
const uninstallFlows = useRegistryFlows(page.uninstalls);
|
|
10599
11146
|
(0, react.useEffect)(() => onRestarted?.((fresh) => {
|
|
10600
11147
|
setSelfVersion(fresh);
|
|
10601
|
-
|
|
10602
|
-
resetInstalls(settledByRestart);
|
|
10603
|
-
resetUninstalls(settledByRestart);
|
|
10604
|
-
setRestarts((current) => current + 1);
|
|
10605
|
-
noteMutation();
|
|
10606
|
-
setRequest({ kind: "reverdict" });
|
|
11148
|
+
if (injectedPage === void 0) page.noteRestarted();
|
|
10607
11149
|
}), [
|
|
10608
11150
|
onRestarted,
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
resetUninstalls,
|
|
10612
|
-
noteMutation
|
|
11151
|
+
page,
|
|
11152
|
+
injectedPage
|
|
10613
11153
|
]);
|
|
10614
11154
|
const [reloading, setReloading] = (0, react.useState)(false);
|
|
10615
11155
|
const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
|
|
10616
|
-
const [query, setQuery] = (0, react.useState)("");
|
|
10617
|
-
const [category, setCategory] = (0, react.useState)(null);
|
|
10618
|
-
const [order, setOrder] = (0, react.useState)("stars");
|
|
10619
|
-
const [hideIncompatible, setHideIncompatible] = (0, react.useState)(false);
|
|
10620
11156
|
const incremental = typeof IntersectionObserver !== "undefined";
|
|
10621
|
-
const [visibleCount, setVisibleCount] = (0, react.useState)(48);
|
|
10622
11157
|
const sentinelRef = (0, react.useRef)(null);
|
|
10623
11158
|
const filteredLenRef = (0, react.useRef)(0);
|
|
10624
11159
|
const sortLabelId = (0, react.useId)();
|
|
@@ -10630,17 +11165,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10630
11165
|
const load = async () => {
|
|
10631
11166
|
try {
|
|
10632
11167
|
const result = await catalog(request.kind === "refresh" ? { refresh: true } : request.kind === "reverdict" ? { reverdict: true } : void 0);
|
|
10633
|
-
if (!cancelled)
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
11168
|
+
if (!cancelled) {
|
|
11169
|
+
setCatalogState({
|
|
11170
|
+
kind: "ready",
|
|
11171
|
+
result
|
|
11172
|
+
});
|
|
11173
|
+
if (request.kind !== "initial" && request.reverdicts !== null) page.clearReverdict(request.reverdicts);
|
|
11174
|
+
}
|
|
10637
11175
|
} catch {
|
|
10638
11176
|
if (cancelled) {
|
|
10639
11177
|
if (request.kind === "refresh" && pendingRefreshRef.current === request) setReloadFailed(true);
|
|
10640
11178
|
return;
|
|
10641
11179
|
}
|
|
10642
11180
|
setCatalogState((current) => current.kind === "ready" ? current : { kind: "error" });
|
|
10643
|
-
if (request.kind
|
|
11181
|
+
if (request.kind === "reverdict") page.clearReverdict(request.reverdicts);
|
|
11182
|
+
else {
|
|
11183
|
+
setReloadFailed(true);
|
|
11184
|
+
if (request.kind !== "initial" && request.reverdicts !== null) setRequest({
|
|
11185
|
+
kind: "reverdict",
|
|
11186
|
+
reverdicts: request.reverdicts
|
|
11187
|
+
});
|
|
11188
|
+
}
|
|
10644
11189
|
} finally {
|
|
10645
11190
|
if (pendingRefreshRef.current === request) pendingRefreshRef.current = null;
|
|
10646
11191
|
if (!cancelled) setReloading(false);
|
|
@@ -10650,7 +11195,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10650
11195
|
return () => {
|
|
10651
11196
|
cancelled = true;
|
|
10652
11197
|
};
|
|
10653
|
-
}, [
|
|
11198
|
+
}, [
|
|
11199
|
+
catalog,
|
|
11200
|
+
request,
|
|
11201
|
+
page
|
|
11202
|
+
]);
|
|
11203
|
+
const shelfShown = (0, react.useRef)(false);
|
|
11204
|
+
(0, react.useLayoutEffect)(() => {
|
|
11205
|
+
if (shelfShown.current || catalogState.kind !== "ready" || installedState.kind === "loading") return;
|
|
11206
|
+
shelfShown.current = true;
|
|
11207
|
+
onShelfReady?.();
|
|
11208
|
+
}, [
|
|
11209
|
+
catalogState,
|
|
11210
|
+
installedState,
|
|
11211
|
+
onShelfReady
|
|
11212
|
+
]);
|
|
10654
11213
|
(0, react.useEffect)(() => {
|
|
10655
11214
|
let cancelled = false;
|
|
10656
11215
|
const load = async () => {
|
|
@@ -10694,6 +11253,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10694
11253
|
entries
|
|
10695
11254
|
});
|
|
10696
11255
|
setSpecs(held ?? void 0);
|
|
11256
|
+
page.noteInstalled(entries.map(identityKey));
|
|
10697
11257
|
} catch {
|
|
10698
11258
|
if (cancelled) return;
|
|
10699
11259
|
setInstalledState({ kind: "error" });
|
|
@@ -10707,8 +11267,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10707
11267
|
}, [
|
|
10708
11268
|
installed,
|
|
10709
11269
|
installedSpecs,
|
|
10710
|
-
|
|
10711
|
-
|
|
11270
|
+
versionReload,
|
|
11271
|
+
settled,
|
|
11272
|
+
page
|
|
10712
11273
|
]);
|
|
10713
11274
|
const installedByKey = (0, react.useMemo)(() => {
|
|
10714
11275
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -10726,7 +11287,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10726
11287
|
const browsable = (0, react.useMemo)(() => {
|
|
10727
11288
|
if (catalogState.kind !== "ready") return [];
|
|
10728
11289
|
const cleared = new Set(catalogState.result.notAShop ?? []);
|
|
10729
|
-
return sortedCatalog.filter((entry) =>
|
|
11290
|
+
return sortedCatalog.filter((entry) => listedOnShelf(entry, cleared));
|
|
10730
11291
|
}, [catalogState, sortedCatalog]);
|
|
10731
11292
|
const missingByKey = (0, react.useMemo)(() => {
|
|
10732
11293
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -10785,14 +11346,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10785
11346
|
const node = sentinelRef.current;
|
|
10786
11347
|
if (node === null) return;
|
|
10787
11348
|
const observer = new IntersectionObserver((entries) => {
|
|
10788
|
-
if (entries.some((entry) => entry.isIntersecting))
|
|
11349
|
+
if (entries.some((entry) => entry.isIntersecting)) page.updateView((view) => ({ visibleCount: nextVisibleCount(view.visibleCount, filteredLenRef.current, 48) }));
|
|
10789
11350
|
}, { rootMargin: "0px 0px 1200px 0px" });
|
|
10790
11351
|
observer.observe(node);
|
|
10791
11352
|
return () => observer.disconnect();
|
|
10792
11353
|
}, [
|
|
10793
11354
|
incremental,
|
|
10794
11355
|
visibleCount,
|
|
10795
|
-
filtered.length
|
|
11356
|
+
filtered.length,
|
|
11357
|
+
page
|
|
10796
11358
|
]);
|
|
10797
11359
|
const visible = incremental ? filtered.slice(0, visibleCount) : filtered;
|
|
10798
11360
|
/** The date a card shows under the current order, or undefined under the
|
|
@@ -10825,19 +11387,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10825
11387
|
"data-shop-tab": true,
|
|
10826
11388
|
"aria-busy": "true",
|
|
10827
11389
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10828
|
-
className:
|
|
11390
|
+
className: _dsh_plugin_shop_css_a037354d_default.srOnly,
|
|
10829
11391
|
children: t("loading")
|
|
10830
11392
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10831
|
-
className:
|
|
11393
|
+
className: _dsh_plugin_shop_css_a037354d_default.skeletonGrid,
|
|
10832
11394
|
"data-shop-skeleton": true,
|
|
10833
11395
|
"aria-hidden": "true",
|
|
10834
11396
|
children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10835
|
-
className:
|
|
11397
|
+
className: _dsh_plugin_shop_css_a037354d_default.skeletonCard,
|
|
10836
11398
|
children: [
|
|
10837
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
10838
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
10839
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
10840
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
11399
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_a037354d_default.skeletonName }),
|
|
11400
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_a037354d_default.skeletonSummary }),
|
|
11401
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_a037354d_default.skeletonSummaryShort }),
|
|
11402
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_a037354d_default.skeletonActions })
|
|
10841
11403
|
]
|
|
10842
11404
|
}, i))
|
|
10843
11405
|
})]
|
|
@@ -10847,13 +11409,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10847
11409
|
translate: "no",
|
|
10848
11410
|
"data-shop-tab": true,
|
|
10849
11411
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10850
|
-
className:
|
|
11412
|
+
className: _dsh_plugin_shop_css_a037354d_default.stateLine,
|
|
10851
11413
|
children: t("error")
|
|
10852
11414
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10853
11415
|
type: "button",
|
|
10854
|
-
className:
|
|
11416
|
+
className: _dsh_plugin_shop_css_a037354d_default.actionButton,
|
|
10855
11417
|
onClick: () => {
|
|
10856
|
-
setRequest({
|
|
11418
|
+
setRequest({
|
|
11419
|
+
kind: "retry",
|
|
11420
|
+
reverdicts: pendingReverdict()
|
|
11421
|
+
});
|
|
10857
11422
|
setVersionReload((current) => current + 1);
|
|
10858
11423
|
},
|
|
10859
11424
|
children: t("retry")
|
|
@@ -10866,46 +11431,48 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10866
11431
|
"data-shop-tab": true,
|
|
10867
11432
|
children: [
|
|
10868
11433
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10869
|
-
className:
|
|
11434
|
+
className: _dsh_plugin_shop_css_a037354d_default.toolbar,
|
|
10870
11435
|
children: [
|
|
10871
11436
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
10872
11437
|
type: "search",
|
|
10873
|
-
className:
|
|
11438
|
+
className: _dsh_plugin_shop_css_a037354d_default.searchInput,
|
|
10874
11439
|
"aria-label": t("search"),
|
|
10875
11440
|
placeholder: t("search"),
|
|
10876
11441
|
value: query,
|
|
10877
11442
|
onChange: (event) => {
|
|
10878
|
-
|
|
10879
|
-
|
|
11443
|
+
page.setView({
|
|
11444
|
+
query: event.target.value,
|
|
11445
|
+
visibleCount: 48
|
|
11446
|
+
});
|
|
10880
11447
|
}
|
|
10881
11448
|
}),
|
|
10882
11449
|
expansionMemo.intents.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10883
11450
|
"data-shop-expansion-note": true,
|
|
10884
|
-
className:
|
|
11451
|
+
className: _dsh_plugin_shop_css_a037354d_default.expansionNote,
|
|
10885
11452
|
children: t("zhExpansionNote", { terms: expansionMemo.terms.slice(1).map((term) => term.startsWith("#") ? term.slice(1) : term).join(", ") })
|
|
10886
11453
|
}),
|
|
10887
11454
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10888
|
-
className:
|
|
11455
|
+
className: _dsh_plugin_shop_css_a037354d_default.toolbarRight,
|
|
10889
11456
|
children: [
|
|
10890
11457
|
result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10891
|
-
className:
|
|
11458
|
+
className: _dsh_plugin_shop_css_a037354d_default.staleBadge,
|
|
10892
11459
|
children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
|
|
10893
11460
|
}),
|
|
10894
11461
|
selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10895
|
-
className:
|
|
11462
|
+
className: _dsh_plugin_shop_css_a037354d_default.versionBlock,
|
|
10896
11463
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
10897
|
-
className:
|
|
11464
|
+
className: _dsh_plugin_shop_css_a037354d_default.versionText,
|
|
10898
11465
|
"data-shop-version": true,
|
|
10899
11466
|
children: ["v", selfVersion.installed]
|
|
10900
11467
|
}), restartMonitor !== null ? null : selfPending && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10901
11468
|
type: "button",
|
|
10902
|
-
className:
|
|
11469
|
+
className: _dsh_plugin_shop_css_a037354d_default.restartSelfButton,
|
|
10903
11470
|
"data-shop-restart": true,
|
|
10904
11471
|
onClick: () => setSelfRestartGate(true),
|
|
10905
11472
|
children: t("restart")
|
|
10906
11473
|
}) : !selfPending && selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10907
11474
|
type: "button",
|
|
10908
|
-
className:
|
|
11475
|
+
className: _dsh_plugin_shop_css_a037354d_default.updateSelfButton,
|
|
10909
11476
|
"data-shop-update-self": true,
|
|
10910
11477
|
onClick: () => {
|
|
10911
11478
|
if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
|
|
@@ -10913,7 +11480,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10913
11480
|
children: t("update")
|
|
10914
11481
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10915
11482
|
type: "button",
|
|
10916
|
-
className:
|
|
11483
|
+
className: _dsh_plugin_shop_css_a037354d_default.checkUpdateButton,
|
|
10917
11484
|
"data-shop-check-update": true,
|
|
10918
11485
|
title: t("checkUpdateTitle"),
|
|
10919
11486
|
disabled: checkState !== "idle",
|
|
@@ -10922,7 +11489,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10922
11489
|
})]
|
|
10923
11490
|
}),
|
|
10924
11491
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
10925
|
-
className:
|
|
11492
|
+
className: _dsh_plugin_shop_css_a037354d_default.githubLink,
|
|
10926
11493
|
href: SHOP_REPO_URL,
|
|
10927
11494
|
target: "_blank",
|
|
10928
11495
|
rel: "noopener noreferrer",
|
|
@@ -10947,28 +11514,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10947
11514
|
logFile: restartMonitor.logFile
|
|
10948
11515
|
}),
|
|
10949
11516
|
selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10950
|
-
className:
|
|
11517
|
+
className: _dsh_plugin_shop_css_a037354d_default.selfUpdatePanel,
|
|
10951
11518
|
"data-shop-self-updating": true,
|
|
10952
11519
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10953
|
-
className:
|
|
11520
|
+
className: _dsh_plugin_shop_css_a037354d_default.installing,
|
|
10954
11521
|
children: t(installPhaseKey(selfUpdate.view.phase))
|
|
10955
11522
|
}), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10956
|
-
className:
|
|
11523
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
10957
11524
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10958
|
-
className:
|
|
11525
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
10959
11526
|
"data-shop-log-line": true,
|
|
10960
11527
|
children: line
|
|
10961
11528
|
}, index))
|
|
10962
11529
|
})]
|
|
10963
11530
|
}),
|
|
10964
11531
|
selfPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10965
|
-
className:
|
|
11532
|
+
className: _dsh_plugin_shop_css_a037354d_default.selfUpdatePanel,
|
|
10966
11533
|
"data-shop-self-update-done": true,
|
|
10967
11534
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10968
|
-
className:
|
|
11535
|
+
className: _dsh_plugin_shop_css_a037354d_default.installedActions,
|
|
10969
11536
|
children: [
|
|
10970
11537
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10971
|
-
className:
|
|
11538
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10972
11539
|
children: t("installedRestartNotice")
|
|
10973
11540
|
}),
|
|
10974
11541
|
restartBlocked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
@@ -10981,7 +11548,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10981
11548
|
}
|
|
10982
11549
|
}),
|
|
10983
11550
|
restartBlocked !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10984
|
-
className:
|
|
11551
|
+
className: _dsh_plugin_shop_css_a037354d_default.notice,
|
|
10985
11552
|
"data-shop-restart-disabled": true,
|
|
10986
11553
|
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
10987
11554
|
})
|
|
@@ -10989,39 +11556,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10989
11556
|
})
|
|
10990
11557
|
}),
|
|
10991
11558
|
selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10992
|
-
className:
|
|
11559
|
+
className: _dsh_plugin_shop_css_a037354d_default.selfUpdatePanel,
|
|
10993
11560
|
"data-shop-self-update-failed": true,
|
|
10994
11561
|
children: [
|
|
10995
11562
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10996
|
-
className:
|
|
11563
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedHeading,
|
|
10997
11564
|
children: t("updateFailed")
|
|
10998
11565
|
}),
|
|
10999
11566
|
selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11000
|
-
className:
|
|
11567
|
+
className: _dsh_plugin_shop_css_a037354d_default.log,
|
|
11001
11568
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11002
|
-
className:
|
|
11569
|
+
className: _dsh_plugin_shop_css_a037354d_default.logLine,
|
|
11003
11570
|
"data-shop-log-line": true,
|
|
11004
11571
|
children: line
|
|
11005
11572
|
}, index))
|
|
11006
11573
|
}),
|
|
11007
11574
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11008
|
-
className:
|
|
11575
|
+
className: _dsh_plugin_shop_css_a037354d_default.failedDetail,
|
|
11009
11576
|
children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
|
|
11577
|
+
}),
|
|
11578
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11579
|
+
type: "button",
|
|
11580
|
+
className: _dsh_plugin_shop_css_a037354d_default.cancelButton,
|
|
11581
|
+
"data-shop-update-self-dismiss": true,
|
|
11582
|
+
onClick: () => {
|
|
11583
|
+
selfUpdate.resetWhere((current) => current.kind === "failed");
|
|
11584
|
+
},
|
|
11585
|
+
children: t("dismiss")
|
|
11010
11586
|
})
|
|
11011
11587
|
]
|
|
11012
11588
|
}),
|
|
11013
11589
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11014
|
-
className:
|
|
11590
|
+
className: _dsh_plugin_shop_css_a037354d_default.categoryBar,
|
|
11015
11591
|
role: "group",
|
|
11016
11592
|
"aria-label": t("catalog"),
|
|
11017
11593
|
children: [
|
|
11018
11594
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11019
11595
|
type: "button",
|
|
11020
|
-
className: category === null ? `${
|
|
11596
|
+
className: category === null ? `${_dsh_plugin_shop_css_a037354d_default.categoryButton} ${_dsh_plugin_shop_css_a037354d_default.categoryButtonOn}` : _dsh_plugin_shop_css_a037354d_default.categoryButton,
|
|
11021
11597
|
"aria-pressed": category === null,
|
|
11022
11598
|
onClick: () => {
|
|
11023
|
-
|
|
11024
|
-
|
|
11599
|
+
page.setView({
|
|
11600
|
+
category: null,
|
|
11601
|
+
visibleCount: 48
|
|
11602
|
+
});
|
|
11025
11603
|
},
|
|
11026
11604
|
"data-shop-category-all": true,
|
|
11027
11605
|
children: [
|
|
@@ -11032,13 +11610,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11032
11610
|
}),
|
|
11033
11611
|
CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11034
11612
|
type: "button",
|
|
11035
|
-
className: category === key ? `${
|
|
11613
|
+
className: category === key ? `${_dsh_plugin_shop_css_a037354d_default.categoryButton} ${_dsh_plugin_shop_css_a037354d_default.categoryButtonOn}` : _dsh_plugin_shop_css_a037354d_default.categoryButton,
|
|
11036
11614
|
"aria-pressed": category === key,
|
|
11037
11615
|
"data-category": key,
|
|
11038
11616
|
"data-shop-category-tab": key,
|
|
11039
11617
|
onClick: () => {
|
|
11040
|
-
|
|
11041
|
-
|
|
11618
|
+
page.setView({
|
|
11619
|
+
category: key,
|
|
11620
|
+
visibleCount: 48
|
|
11621
|
+
});
|
|
11042
11622
|
},
|
|
11043
11623
|
children: [
|
|
11044
11624
|
t(categoryLocaleKey(key)),
|
|
@@ -11048,12 +11628,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11048
11628
|
}, key)),
|
|
11049
11629
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11050
11630
|
type: "button",
|
|
11051
|
-
className: category === "installed" ? `${
|
|
11631
|
+
className: category === "installed" ? `${_dsh_plugin_shop_css_a037354d_default.categoryButton} ${_dsh_plugin_shop_css_a037354d_default.categoryButtonOn}` : _dsh_plugin_shop_css_a037354d_default.categoryButton,
|
|
11052
11632
|
"aria-pressed": category === "installed",
|
|
11053
11633
|
"data-shop-category-installed": true,
|
|
11054
11634
|
onClick: () => {
|
|
11055
|
-
|
|
11056
|
-
|
|
11635
|
+
page.setView({
|
|
11636
|
+
category: "installed",
|
|
11637
|
+
visibleCount: 48
|
|
11638
|
+
});
|
|
11057
11639
|
},
|
|
11058
11640
|
children: [
|
|
11059
11641
|
t("installed"),
|
|
@@ -11065,26 +11647,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11065
11647
|
type: "button",
|
|
11066
11648
|
role: "switch",
|
|
11067
11649
|
"aria-checked": hideIncompatible,
|
|
11068
|
-
className: hideIncompatible ? `${
|
|
11650
|
+
className: hideIncompatible ? `${_dsh_plugin_shop_css_a037354d_default.incompatibleFilter} ${_dsh_plugin_shop_css_a037354d_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_a037354d_default.incompatibleFilter,
|
|
11069
11651
|
title: t(hideIncompatible ? "showIncompatibleTitle" : "incompatibleFilterTitle"),
|
|
11070
11652
|
"data-shop-hide-incompatible": true,
|
|
11071
11653
|
onClick: () => {
|
|
11072
|
-
|
|
11073
|
-
|
|
11654
|
+
page.updateView((view) => ({
|
|
11655
|
+
hideIncompatible: !view.hideIncompatible,
|
|
11656
|
+
visibleCount: 48
|
|
11657
|
+
}));
|
|
11074
11658
|
},
|
|
11075
11659
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11076
|
-
className: hideIncompatible ? `${
|
|
11660
|
+
className: hideIncompatible ? `${_dsh_plugin_shop_css_a037354d_default.switch} ${_dsh_plugin_shop_css_a037354d_default.switchOn}` : _dsh_plugin_shop_css_a037354d_default.switch,
|
|
11077
11661
|
"aria-hidden": "true",
|
|
11078
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
11662
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_a037354d_default.switchKnob })
|
|
11079
11663
|
}), t("hideIncompatible", { count: incompatibleCount })]
|
|
11080
11664
|
})
|
|
11081
11665
|
]
|
|
11082
11666
|
}),
|
|
11083
11667
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11084
|
-
className:
|
|
11668
|
+
className: _dsh_plugin_shop_css_a037354d_default.catalogStatsRow,
|
|
11085
11669
|
children: [
|
|
11086
11670
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11087
|
-
className:
|
|
11671
|
+
className: _dsh_plugin_shop_css_a037354d_default.catalogStats,
|
|
11088
11672
|
"data-shop-catalog-stats": true,
|
|
11089
11673
|
children: t("catalogStats", {
|
|
11090
11674
|
count: String(browsable.length),
|
|
@@ -11093,39 +11677,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11093
11677
|
}),
|
|
11094
11678
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11095
11679
|
type: "button",
|
|
11096
|
-
className:
|
|
11680
|
+
className: _dsh_plugin_shop_css_a037354d_default.catalogRefreshButton,
|
|
11097
11681
|
"data-shop-catalog-refresh": true,
|
|
11098
11682
|
disabled: reloading,
|
|
11099
11683
|
onClick: () => {
|
|
11100
11684
|
setReloading(true);
|
|
11101
11685
|
setReloadFailed(false);
|
|
11102
|
-
setRequest({
|
|
11686
|
+
setRequest({
|
|
11687
|
+
kind: "refresh",
|
|
11688
|
+
reverdicts: pendingReverdict()
|
|
11689
|
+
});
|
|
11103
11690
|
setVersionReload((current) => current + 1);
|
|
11104
11691
|
},
|
|
11105
11692
|
children: reloading ? t("refreshing") : t("refresh")
|
|
11106
11693
|
}),
|
|
11107
11694
|
reloadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11108
|
-
className:
|
|
11695
|
+
className: _dsh_plugin_shop_css_a037354d_default.catalogRefreshFailed,
|
|
11109
11696
|
"data-shop-catalog-refresh-failed": true,
|
|
11110
11697
|
children: t("refreshFailed")
|
|
11111
11698
|
}),
|
|
11112
11699
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
11113
|
-
className:
|
|
11700
|
+
className: _dsh_plugin_shop_css_a037354d_default.sortGroup,
|
|
11114
11701
|
role: "group",
|
|
11115
11702
|
"aria-labelledby": sortLabelId,
|
|
11116
11703
|
"data-shop-sort": true,
|
|
11117
11704
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11118
|
-
className:
|
|
11705
|
+
className: _dsh_plugin_shop_css_a037354d_default.sortLabel,
|
|
11119
11706
|
id: sortLabelId,
|
|
11120
11707
|
children: t("sortLabel")
|
|
11121
11708
|
}), SHELF_ORDERS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11122
11709
|
type: "button",
|
|
11123
|
-
className: order === name ? `${
|
|
11710
|
+
className: order === name ? `${_dsh_plugin_shop_css_a037354d_default.categoryButton} ${_dsh_plugin_shop_css_a037354d_default.categoryButtonOn}` : _dsh_plugin_shop_css_a037354d_default.categoryButton,
|
|
11124
11711
|
"aria-pressed": order === name,
|
|
11125
11712
|
"data-shop-order": name,
|
|
11126
11713
|
onClick: () => {
|
|
11127
|
-
|
|
11128
|
-
|
|
11714
|
+
page.setView({
|
|
11715
|
+
order: name,
|
|
11716
|
+
visibleCount: 48
|
|
11717
|
+
});
|
|
11129
11718
|
},
|
|
11130
11719
|
children: t(shelfOrderLocaleKey(name))
|
|
11131
11720
|
}, name))]
|
|
@@ -11133,43 +11722,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11133
11722
|
]
|
|
11134
11723
|
}),
|
|
11135
11724
|
result.plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11136
|
-
className:
|
|
11725
|
+
className: _dsh_plugin_shop_css_a037354d_default.emptyLine,
|
|
11137
11726
|
children: t("empty")
|
|
11138
|
-
}) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11139
|
-
className:
|
|
11727
|
+
}) : filtered.length === 0 ? category === "installed" && installedState.kind !== "ready" ? installedState.kind === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11728
|
+
className: _dsh_plugin_shop_css_a037354d_default.emptyLine,
|
|
11729
|
+
"data-shop-installed-loading": true,
|
|
11730
|
+
children: t("installedLoading")
|
|
11731
|
+
}) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11732
|
+
className: _dsh_plugin_shop_css_a037354d_default.emptyLine,
|
|
11140
11733
|
"data-shop-empty": true,
|
|
11141
11734
|
children: matched.length > 0 ? t("emptyIncompatibleFiltered") : t("emptySearch")
|
|
11142
11735
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
11143
|
-
className:
|
|
11736
|
+
className: _dsh_plugin_shop_css_a037354d_default.cards,
|
|
11144
11737
|
children: [visible.map((entry) => {
|
|
11145
11738
|
const key = entryKey(entry);
|
|
11146
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
11147
|
-
|
|
11148
|
-
|
|
11149
|
-
|
|
11150
|
-
|
|
11151
|
-
|
|
11152
|
-
|
|
11153
|
-
|
|
11154
|
-
|
|
11155
|
-
|
|
11156
|
-
|
|
11157
|
-
|
|
11158
|
-
|
|
11159
|
-
|
|
11160
|
-
|
|
11161
|
-
|
|
11162
|
-
|
|
11163
|
-
|
|
11164
|
-
|
|
11739
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
11740
|
+
"data-shop-identity": key,
|
|
11741
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
|
|
11742
|
+
entry,
|
|
11743
|
+
stars: starsOf(entry, stars),
|
|
11744
|
+
order,
|
|
11745
|
+
shelfDay: shelfDayOf(entry),
|
|
11746
|
+
installed: installedByKey.get(key),
|
|
11747
|
+
missing: missingByKey.get(key) ?? [],
|
|
11748
|
+
harness: harnessVerdictOf(harnessVerdicts, key),
|
|
11749
|
+
nameTakenBy: nameTakenByKey.get(key),
|
|
11750
|
+
t,
|
|
11751
|
+
flow: flows.flowFor(key),
|
|
11752
|
+
uninstallFlow: uninstallFlows.flowFor(key),
|
|
11753
|
+
restart,
|
|
11754
|
+
restartBlocked,
|
|
11755
|
+
reload,
|
|
11756
|
+
setEnabled,
|
|
11757
|
+
inInstalledView: category === "installed",
|
|
11758
|
+
page
|
|
11759
|
+
})
|
|
11760
|
+
}, key);
|
|
11165
11761
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
11166
11762
|
ref: sentinelRef,
|
|
11167
|
-
className:
|
|
11763
|
+
className: _dsh_plugin_shop_css_a037354d_default.cardsSentry,
|
|
11168
11764
|
"data-shop-sentry": true,
|
|
11169
11765
|
"aria-hidden": "true"
|
|
11170
11766
|
})]
|
|
11171
11767
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11172
|
-
className:
|
|
11768
|
+
className: _dsh_plugin_shop_css_a037354d_default.showingLine,
|
|
11173
11769
|
"aria-live": "polite",
|
|
11174
11770
|
children: t("showing", {
|
|
11175
11771
|
shown: String(visibleCount),
|
|
@@ -11187,7 +11783,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11187
11783
|
restart,
|
|
11188
11784
|
restartBlocked,
|
|
11189
11785
|
reload,
|
|
11190
|
-
|
|
11786
|
+
page
|
|
11191
11787
|
}),
|
|
11192
11788
|
category === "installed" && installedState.kind === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShadowedSection, {
|
|
11193
11789
|
entries: installedState.entries,
|
|
@@ -11201,35 +11797,172 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11201
11797
|
});
|
|
11202
11798
|
}
|
|
11203
11799
|
//#endregion
|
|
11204
|
-
//#region src/client/
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11800
|
+
//#region src/client/ShopPage.tsx
|
|
11801
|
+
/**
|
|
11802
|
+
* The shop's own page in dsh's main column (design
|
|
11803
|
+
* 2026-10-16-shop-sidebar-page §4.2), framed as dsh frames its Plugins page:
|
|
11804
|
+
* the page is its own scroll container, its head carries the title and the
|
|
11805
|
+
* one intro line and clears the desktop title bar, and the body below is
|
|
11806
|
+
* `ShopTab`, unchanged.
|
|
11807
|
+
*
|
|
11808
|
+
* It keeps the reader's scroll position in the page store (design
|
|
11809
|
+
* 2026-10-16-shop-sidebar-page §5.7). The position is written on every
|
|
11810
|
+
* scroll event — a browser fires at most one a frame, and the store takes it
|
|
11811
|
+
* without telling anyone — with the card at the page's top edge. The page
|
|
11812
|
+
* goes back there once, when the shelf is on screen again and the installed
|
|
11813
|
+
* list has answered, unless the reader moved first: the same card at the
|
|
11814
|
+
* same height, held there for a moment while the cards around it render, or
|
|
11815
|
+
* the position when that card is no longer on the shelf or the shelf was
|
|
11816
|
+
* refiltered since.
|
|
11817
|
+
*/
|
|
11818
|
+
/** What each card on the shelf carries: its identity (ShopTab), not its
|
|
11819
|
+
* name, which two listed entries can share — a fork and its original. */
|
|
11820
|
+
const IDENTITY = "data-shop-identity";
|
|
11821
|
+
/** The shelf's cards in document order: the children of the one list that
|
|
11822
|
+
* holds them, a live collection reached through the first card, so a scroll
|
|
11823
|
+
* event never walks the cards' own subtrees. Null while no card is on the
|
|
11824
|
+
* page. */
|
|
11825
|
+
function cardsOf(scroller) {
|
|
11826
|
+
return scroller.querySelector(`[${IDENTITY}]`)?.parentElement?.children ?? null;
|
|
11827
|
+
}
|
|
11828
|
+
/** The shelf's gap between two cards, in px: `.cards`' `gap` in
|
|
11829
|
+
* ShopTab.module.css, as the list's computed row gap reads it. */
|
|
11830
|
+
function gapOf(list) {
|
|
11831
|
+
const gap = list === null ? NaN : Number.parseFloat(getComputedStyle(list).rowGap);
|
|
11832
|
+
return Number.isFinite(gap) ? gap : 0;
|
|
11833
|
+
}
|
|
11834
|
+
/**
|
|
11835
|
+
* Where the reader is on the shelf: the first card whose bottom lies below
|
|
11836
|
+
* the page's top edge, and its top relative to that edge. The cards stand one
|
|
11837
|
+
* per row in document order, so their bottoms rise with their index, and a
|
|
11838
|
+
* binary search reads a handful of their rects on each scroll event, never
|
|
11839
|
+
* one per card.
|
|
11840
|
+
*
|
|
11841
|
+
* A card whose top lies below the edge counts only while it lies no further
|
|
11842
|
+
* below than the shelf's gap between cards, as when the edge falls in that
|
|
11843
|
+
* gap. Further down, the reader is above the shelf, in the head or the
|
|
11844
|
+
* toolbar: nothing there lays out at a placeholder height, so the position
|
|
11845
|
+
* alone is exact, while a card would pull them down the page whenever the
|
|
11846
|
+
* content above the shelf grows meanwhile.
|
|
11847
|
+
*/
|
|
11848
|
+
function anchorAt(scroller) {
|
|
11849
|
+
const cards = cardsOf(scroller);
|
|
11850
|
+
if (cards === null) return null;
|
|
11851
|
+
const edge = scroller.getBoundingClientRect().top;
|
|
11852
|
+
let low = 0;
|
|
11853
|
+
let high = cards.length;
|
|
11854
|
+
while (low < high) {
|
|
11855
|
+
const middle = Math.floor((low + high) / 2);
|
|
11856
|
+
if ((cards.item(middle)?.getBoundingClientRect().bottom ?? edge) > edge) high = middle;
|
|
11857
|
+
else low = middle + 1;
|
|
11858
|
+
}
|
|
11859
|
+
const card = cards.item(low);
|
|
11860
|
+
const key = card?.getAttribute(IDENTITY) ?? null;
|
|
11861
|
+
if (card === null || key === null) return null;
|
|
11862
|
+
const offset = card.getBoundingClientRect().top - edge;
|
|
11863
|
+
if (offset > 0 && offset > gapOf(card.parentElement)) return null;
|
|
11864
|
+
return {
|
|
11865
|
+
key,
|
|
11866
|
+
offset
|
|
11867
|
+
};
|
|
11868
|
+
}
|
|
11869
|
+
/** The card with `key` on the shelf, matched by iteration: an identity
|
|
11870
|
+
* carries `:`, `/` and `#`, which a selector would have to escape. */
|
|
11871
|
+
function cardWith(scroller, key) {
|
|
11872
|
+
const cards = cardsOf(scroller);
|
|
11873
|
+
if (cards === null) return null;
|
|
11874
|
+
for (const card of cards) if (card.getAttribute(IDENTITY) === key) return card;
|
|
11875
|
+
return null;
|
|
11876
|
+
}
|
|
11877
|
+
function ShopPage(props) {
|
|
11878
|
+
const { t, page } = props;
|
|
11879
|
+
const scroller = (0, react.useRef)(null);
|
|
11880
|
+
const moved = (0, react.useRef)(false);
|
|
11881
|
+
const holding = (0, react.useRef)(null);
|
|
11882
|
+
(0, react.useEffect)(() => () => {
|
|
11883
|
+
if (holding.current !== null) cancelAnimationFrame(holding.current);
|
|
11884
|
+
}, []);
|
|
11885
|
+
const restore = (0, react.useCallback)(() => {
|
|
11886
|
+
const node = scroller.current;
|
|
11887
|
+
if (node === null || moved.current || page === void 0) return;
|
|
11888
|
+
const anchor = page.readAnchor();
|
|
11889
|
+
const card = anchor === null ? null : cardWith(node, anchor.key);
|
|
11890
|
+
if (anchor === null || card === null) {
|
|
11891
|
+
node.scrollTop = page.readScroll();
|
|
11892
|
+
return;
|
|
11893
|
+
}
|
|
11894
|
+
const drift = () => card.getBoundingClientRect().top - node.getBoundingClientRect().top - anchor.offset;
|
|
11895
|
+
node.style.overflowAnchor = "none";
|
|
11896
|
+
node.scrollTop += drift();
|
|
11897
|
+
let held = node.scrollTop;
|
|
11898
|
+
const until = performance.now() + 500;
|
|
11899
|
+
const hold = () => {
|
|
11900
|
+
holding.current = null;
|
|
11901
|
+
if (moved.current || Math.abs(node.scrollTop - held) >= 1 || !card.isConnected || performance.now() > until) {
|
|
11902
|
+
node.style.overflowAnchor = "";
|
|
11903
|
+
return;
|
|
11904
|
+
}
|
|
11905
|
+
const off = drift();
|
|
11906
|
+
if (Math.abs(off) >= 1) {
|
|
11907
|
+
node.scrollTop += off;
|
|
11908
|
+
held = node.scrollTop;
|
|
11909
|
+
}
|
|
11910
|
+
holding.current = requestAnimationFrame(hold);
|
|
11911
|
+
};
|
|
11912
|
+
holding.current = requestAnimationFrame(hold);
|
|
11913
|
+
}, [page]);
|
|
11914
|
+
const cancelRestore = () => {
|
|
11915
|
+
moved.current = true;
|
|
11916
|
+
};
|
|
11917
|
+
(0, react.useEffect)(() => {
|
|
11918
|
+
page?.notePageMounted(true);
|
|
11919
|
+
return () => {
|
|
11920
|
+
page?.notePageMounted(false);
|
|
11921
|
+
};
|
|
11922
|
+
}, [page]);
|
|
11923
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
11924
|
+
ref: scroller,
|
|
11925
|
+
className: `${_dsh_plugin_shop_css_a037354d_default.page} notranslate`,
|
|
11926
|
+
translate: "no",
|
|
11927
|
+
"data-shop-page": true,
|
|
11928
|
+
onScroll: (event) => {
|
|
11929
|
+
page?.recordScroll(event.currentTarget.scrollTop, anchorAt(event.currentTarget));
|
|
11930
|
+
},
|
|
11931
|
+
onWheel: cancelRestore,
|
|
11932
|
+
onTouchStart: cancelRestore,
|
|
11933
|
+
onKeyDown: cancelRestore,
|
|
11934
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
11935
|
+
className: _dsh_plugin_shop_css_a037354d_default.pageHead,
|
|
11936
|
+
"data-shop-page-head": true,
|
|
11937
|
+
"data-window-drag": true,
|
|
11938
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
11939
|
+
className: _dsh_plugin_shop_css_a037354d_default.pageTitle,
|
|
11940
|
+
"data-shop-page-title": true,
|
|
11941
|
+
children: t("title")
|
|
11942
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11943
|
+
className: _dsh_plugin_shop_css_a037354d_default.pageIntro,
|
|
11944
|
+
"data-shop-page-intro": true,
|
|
11945
|
+
children: t("intro")
|
|
11946
|
+
})]
|
|
11947
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopTab, {
|
|
11948
|
+
...props,
|
|
11949
|
+
onShelfReady: restore
|
|
11950
|
+
})]
|
|
11221
11951
|
});
|
|
11222
11952
|
}
|
|
11223
11953
|
//#endregion
|
|
11224
11954
|
//#region src/client/index.ts
|
|
11225
11955
|
/** Dictionary namespace owned by this plugin. */
|
|
11226
11956
|
const NS = "settings.shop";
|
|
11957
|
+
/** The id the shop's sidebar row and its main-column page share (design
|
|
11958
|
+
* 2026-10-16-shop-sidebar-page §4.1). */
|
|
11959
|
+
const SHOP_PANEL = "shop";
|
|
11227
11960
|
/** How long a stashed catalog may satisfy a plain open. The host applies the
|
|
11228
11961
|
* same five-minute freshness window, so the client stash must not outlive it. */
|
|
11229
11962
|
const WARM_TTL_MS = 3e5;
|
|
11230
11963
|
/** The boot-time warm fetch: the catalog the tab wants on its first open.
|
|
11231
11964
|
* Started in apply — the client bundle boots with the web app, long before
|
|
11232
|
-
* the user
|
|
11965
|
+
* the user opens the shop — so the host's slow network fetch runs while
|
|
11233
11966
|
* nobody is looking at the shop, and the tab's mount consumes this promise
|
|
11234
11967
|
* instead of starting its own fetch. A rejection stays stored (the injected
|
|
11235
11968
|
* catalog falls back to a fresh call); the extra catch keeps the
|
|
@@ -11247,7 +11980,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11247
11980
|
* uninstall made from another tab, another window, or the CLI is never added
|
|
11248
11981
|
* here and stays unknown to this page until it reloads. */
|
|
11249
11982
|
let pageRemoved = /* @__PURE__ */ new Set();
|
|
11250
|
-
/** Services required by the
|
|
11983
|
+
/** Services required by the page, row and Plugins page button registrations
|
|
11984
|
+
* and the Remote mount.
|
|
11251
11985
|
*
|
|
11252
11986
|
* `remote.shop` is deliberately ABSENT: this package both mounts its own
|
|
11253
11987
|
* Remote (the assembly does not know it, §7.3) and consumes it, and cordis
|
|
@@ -11262,8 +11996,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11262
11996
|
"slots",
|
|
11263
11997
|
"locale"
|
|
11264
11998
|
];
|
|
11265
|
-
/** Mount the shop Remote and contribute the shop's
|
|
11999
|
+
/** Mount the shop Remote and contribute the shop's page, its sidebar row and
|
|
12000
|
+
* its buttons on dsh's Plugins page. */
|
|
11266
12001
|
async function apply(ctx) {
|
|
12002
|
+
const predecessor = readPredecessor();
|
|
11267
12003
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
11268
12004
|
zh,
|
|
11269
12005
|
en
|
|
@@ -11302,7 +12038,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11302
12038
|
}
|
|
11303
12039
|
return result;
|
|
11304
12040
|
};
|
|
11305
|
-
version()
|
|
12041
|
+
const firstAnswer = version();
|
|
12042
|
+
firstAnswer.catch(() => {});
|
|
11306
12043
|
ctx.on("connection/reset", () => {
|
|
11307
12044
|
version().catch(() => {});
|
|
11308
12045
|
});
|
|
@@ -11335,29 +12072,62 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11335
12072
|
};
|
|
11336
12073
|
return fresh;
|
|
11337
12074
|
};
|
|
12075
|
+
let listingRead = null;
|
|
12076
|
+
const listing = async () => {
|
|
12077
|
+
const warm = warmCatalog;
|
|
12078
|
+
if (warm !== null) try {
|
|
12079
|
+
return await warm.result;
|
|
12080
|
+
} catch {}
|
|
12081
|
+
listingRead ??= hostCatalog().finally(() => {
|
|
12082
|
+
listingRead = null;
|
|
12083
|
+
});
|
|
12084
|
+
return listingRead;
|
|
12085
|
+
};
|
|
11338
12086
|
const handOver = async (result) => ({
|
|
11339
12087
|
...result,
|
|
11340
12088
|
incompatible: await refineAgainstModuleTable(result.incompatible ?? {}, ctx.get("modules"), pageRemoved)
|
|
11341
12089
|
});
|
|
11342
12090
|
const restart = async () => unwrap(await ns.restart());
|
|
11343
12091
|
withdrawRestart = registerRestartCarrier(restart);
|
|
12092
|
+
const install = async (args) => {
|
|
12093
|
+
warmCatalog = null;
|
|
12094
|
+
return unwrap(await ns.installStart(args));
|
|
12095
|
+
};
|
|
12096
|
+
const installStatus = async (args) => unwrap(await ns.installStatus(args));
|
|
12097
|
+
const uninstall = async (args) => {
|
|
12098
|
+
warmCatalog = null;
|
|
12099
|
+
return unwrap(await ns.uninstallStart(args));
|
|
12100
|
+
};
|
|
12101
|
+
const updateStart = async (args) => {
|
|
12102
|
+
warmCatalog = null;
|
|
12103
|
+
return unwrap(await ns.updateStart(args));
|
|
12104
|
+
};
|
|
12105
|
+
const noteUninstalled = (name) => {
|
|
12106
|
+
pageRemoved.add(name);
|
|
12107
|
+
};
|
|
12108
|
+
const page = createPageStore(() => ({
|
|
12109
|
+
install,
|
|
12110
|
+
uninstall,
|
|
12111
|
+
updateStart,
|
|
12112
|
+
installStatus,
|
|
12113
|
+
noteUninstalled
|
|
12114
|
+
}), { pollUnwatched: true });
|
|
12115
|
+
disposeOccupant(installPageStore(page));
|
|
12116
|
+
ctx.effect(() => () => {
|
|
12117
|
+
page.dispose();
|
|
12118
|
+
}, "dsh-plugin-shop: page store");
|
|
12119
|
+
restartListeners.add(() => {
|
|
12120
|
+
page.noteRestarted();
|
|
12121
|
+
});
|
|
11344
12122
|
const injected = () => ({
|
|
11345
12123
|
catalog: async (args) => handOver(await hostCatalog(args)),
|
|
11346
|
-
install
|
|
11347
|
-
|
|
11348
|
-
return unwrap(await ns.installStart(args));
|
|
11349
|
-
},
|
|
11350
|
-
installStatus: async (args) => unwrap(await ns.installStatus(args)),
|
|
12124
|
+
install,
|
|
12125
|
+
installStatus,
|
|
11351
12126
|
setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
|
|
11352
12127
|
installed: async () => unwrap(await ns.installed()),
|
|
11353
12128
|
installedSpecs: async () => unwrap(await ns.installedSpecs()),
|
|
11354
|
-
uninstall
|
|
11355
|
-
|
|
11356
|
-
return unwrap(await ns.uninstallStart(args));
|
|
11357
|
-
},
|
|
11358
|
-
noteUninstalled: (name) => {
|
|
11359
|
-
pageRemoved.add(name);
|
|
11360
|
-
},
|
|
12129
|
+
uninstall,
|
|
12130
|
+
noteUninstalled,
|
|
11361
12131
|
restart,
|
|
11362
12132
|
version,
|
|
11363
12133
|
onRestarted: (listener) => {
|
|
@@ -11366,22 +12136,51 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11366
12136
|
restartListeners.delete(listener);
|
|
11367
12137
|
};
|
|
11368
12138
|
},
|
|
11369
|
-
updateStart
|
|
11370
|
-
|
|
11371
|
-
return unwrap(await ns.updateStart(args));
|
|
11372
|
-
}
|
|
12139
|
+
updateStart,
|
|
12140
|
+
page
|
|
11373
12141
|
});
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
12142
|
+
const layout = () => ctx.get("layout");
|
|
12143
|
+
const selectShopPage = () => {
|
|
12144
|
+
const service = layout();
|
|
12145
|
+
if (typeof service?.selectPanel !== "function") return;
|
|
12146
|
+
try {
|
|
12147
|
+
service.selectPanel(SHOP_PANEL);
|
|
12148
|
+
} catch {}
|
|
12149
|
+
};
|
|
12150
|
+
ctx.slots.inject("main", () => [ctx.slots.register({
|
|
12151
|
+
name: "main",
|
|
12152
|
+
key: SHOP_PANEL,
|
|
11379
12153
|
locale: NS,
|
|
11380
12154
|
inject: injected
|
|
11381
|
-
},
|
|
12155
|
+
}, ShopPage), () => {
|
|
12156
|
+
page.recordTeardown();
|
|
12157
|
+
}]);
|
|
12158
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
12159
|
+
name: "sidebar.panellist",
|
|
12160
|
+
id: SHOP_PANEL,
|
|
12161
|
+
order: 5,
|
|
12162
|
+
label: () => ctx.locale.bind(NS)("title"),
|
|
12163
|
+
locale: NS
|
|
12164
|
+
}, ShopGlyph));
|
|
12165
|
+
ctx.slots.inject("plugins.detail.actions", () => ctx.slots.register({
|
|
12166
|
+
name: "plugins.detail.actions",
|
|
12167
|
+
id: SHOP_PANEL,
|
|
12168
|
+
order: 0,
|
|
12169
|
+
locale: NS,
|
|
12170
|
+
inject: () => ({
|
|
12171
|
+
listing,
|
|
12172
|
+
canOpen: () => typeof layout()?.selectPanel === "function",
|
|
12173
|
+
openShop: selectShopPage,
|
|
12174
|
+
page
|
|
12175
|
+
})
|
|
12176
|
+
}, ShopDetailAction));
|
|
12177
|
+
if (predecessor.kind === "section" || predecessor.kind === "page" && predecessor.showing) firstAnswer.then((answer) => {
|
|
12178
|
+
if (updateAwaitsRestart(answer)) selectShopPage();
|
|
12179
|
+
}, () => {});
|
|
11382
12180
|
}
|
|
11383
12181
|
//#endregion
|
|
11384
12182
|
exports.NS = NS;
|
|
12183
|
+
exports.SHOP_PANEL = SHOP_PANEL;
|
|
11385
12184
|
exports.WARM_TTL_MS = WARM_TTL_MS;
|
|
11386
12185
|
exports.apply = apply;
|
|
11387
12186
|
exports.inject = inject;
|