dsh-plugin-shop 0.8.7 → 0.9.0-beta.1
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 +1529 -744
- 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 +15 -5
- package/lib/types/client/page-store.d.ts +255 -0
- package/lib/types/client/present.d.ts +26 -6
- 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/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
|
@@ -4761,17 +4761,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4761
4761
|
};
|
|
4762
4762
|
//#endregion
|
|
4763
4763
|
//#region src/client/locales.ts
|
|
4764
|
-
/** Copy dictionaries for the shop's
|
|
4764
|
+
/** Copy dictionaries for the shop's page in dsh's main column, its row in the
|
|
4765
|
+
* sidebar, and its two buttons on dsh's Plugins page. */
|
|
4765
4766
|
/** Simplified Chinese dictionary and key source of truth. */
|
|
4766
4767
|
const zh = {
|
|
4767
|
-
/** The shop's name
|
|
4768
|
-
*
|
|
4768
|
+
/** The shop's name: its sidebar row and its page title (design
|
|
4769
|
+
* 2026-10-16-shop-sidebar-page §4.1), one key so the two cannot drift
|
|
4770
|
+
* apart. */
|
|
4769
4771
|
title: "插件商店",
|
|
4770
|
-
/** The
|
|
4771
|
-
* as the built-in Models and 内置插件 pages carry one under their title. */
|
|
4772
|
+
/** The page's intro line, under its title, as dsh's own pages carry one. */
|
|
4772
4773
|
intro: "浏览、安装和更新目录中的插件。",
|
|
4774
|
+
/** The Plugins page's button on the shop's own bundle page (design
|
|
4775
|
+
* 2026-10-16-shop-sidebar-page §7). */
|
|
4776
|
+
openShop: "打开插件商店",
|
|
4777
|
+
/** Its button on a page of a bundle the shelf lists. */
|
|
4778
|
+
viewInShop: "在插件商店中查看",
|
|
4773
4779
|
loading: "正在读取目录…",
|
|
4774
4780
|
error: "暂时无法读取目录。",
|
|
4781
|
+
installedLoading: "正在读取已安装列表…",
|
|
4775
4782
|
installedError: "暂时无法读取已安装列表。",
|
|
4776
4783
|
retry: "重试",
|
|
4777
4784
|
/** The shop's own error boundary caught a render error; the error's message
|
|
@@ -4960,8 +4967,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4960
4967
|
const en = {
|
|
4961
4968
|
title: "Plugin shop",
|
|
4962
4969
|
intro: "Browse, install, and update plugins from the catalog.",
|
|
4970
|
+
openShop: "Open Plugin shop",
|
|
4971
|
+
viewInShop: "View in Plugin shop",
|
|
4963
4972
|
loading: "Reading the catalog…",
|
|
4964
4973
|
error: "The catalog is temporarily unavailable.",
|
|
4974
|
+
installedLoading: "Reading the installed list…",
|
|
4965
4975
|
installedError: "The installed list is temporarily unavailable.",
|
|
4966
4976
|
retry: "Retry",
|
|
4967
4977
|
tabCrashed: "The plugin shop hit an error while showing this page.",
|
|
@@ -5284,6 +5294,57 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5284
5294
|
return refineIncompatible(incompatible, withRemoved, timeoutMs);
|
|
5285
5295
|
}
|
|
5286
5296
|
//#endregion
|
|
5297
|
+
//#region src/shared/install-state.ts
|
|
5298
|
+
/** Whether the host is done with this record: it will not change again, and a
|
|
5299
|
+
* poller may stop. */
|
|
5300
|
+
function isTerminalInstallState(state) {
|
|
5301
|
+
return state === "done" || state === "failed";
|
|
5302
|
+
}
|
|
5303
|
+
//#endregion
|
|
5304
|
+
//#region src/client/listeners.ts
|
|
5305
|
+
/**
|
|
5306
|
+
* The listener plumbing the client's external stores share: the page store
|
|
5307
|
+
* and its owed notices (page-store.ts), the flow registries
|
|
5308
|
+
* (flow-registry.ts) and the restart monitor (restart-monitor.ts). Each keeps
|
|
5309
|
+
* its listeners in a plain `Set`. React subscribes through `listen`, as
|
|
5310
|
+
* `useSyncExternalStore` asks, and a change reaches every listener through
|
|
5311
|
+
* `notify`.
|
|
5312
|
+
*
|
|
5313
|
+
* Functions over that `Set`, never an object a store keeps in its place. The
|
|
5314
|
+
* restart monitor's store lives on `globalThis`, and a 0.8.x shop hands it
|
|
5315
|
+
* over (design 2026-10-16-shop-sidebar-page §6). Its `listeners` is a plain
|
|
5316
|
+
* `Set` that the predecessor's own code adds to, deletes from and iterates,
|
|
5317
|
+
* so the field's shape is a contract across versions, and these functions
|
|
5318
|
+
* take that `Set` as it is.
|
|
5319
|
+
*/
|
|
5320
|
+
/** The sets a disposed store forgot (`forget`). Held here, never as a mark
|
|
5321
|
+
* on the `Set` itself: a store's `Set` is its own, and the restart monitor's
|
|
5322
|
+
* is a 0.8.x predecessor's too (above). */
|
|
5323
|
+
const forgotten = /* @__PURE__ */ new WeakSet();
|
|
5324
|
+
/** Add `listener`, and hand back what removes it. A forgotten set takes
|
|
5325
|
+
* none: its store tells nobody anything more, so a listener subscribing in
|
|
5326
|
+
* a hot swap's window would only be held. */
|
|
5327
|
+
function listen(listeners, listener) {
|
|
5328
|
+
if (forgotten.has(listeners)) return () => {};
|
|
5329
|
+
listeners.add(listener);
|
|
5330
|
+
return () => {
|
|
5331
|
+
listeners.delete(listener);
|
|
5332
|
+
};
|
|
5333
|
+
}
|
|
5334
|
+
/** Call every listener as the set stands when the call begins. Over a copy,
|
|
5335
|
+
* so a listener that subscribes or unsubscribes one while it runs (React
|
|
5336
|
+
* re-subscribing in the render the change caused) changes the next notify,
|
|
5337
|
+
* never this one. */
|
|
5338
|
+
function notify(listeners) {
|
|
5339
|
+
for (const listener of [...listeners]) listener();
|
|
5340
|
+
}
|
|
5341
|
+
/** Drop every listener, and take none again: a disposed store tells nobody
|
|
5342
|
+
* anything more. */
|
|
5343
|
+
function forget(listeners) {
|
|
5344
|
+
listeners.clear();
|
|
5345
|
+
forgotten.add(listeners);
|
|
5346
|
+
}
|
|
5347
|
+
//#endregion
|
|
5287
5348
|
//#region src/shared/shop-like.ts
|
|
5288
5349
|
/**
|
|
5289
5350
|
* Whether a package name reads as a competing plugin marketplace.
|
|
@@ -6851,13 +6912,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6851
6912
|
return spec.length > HOLDER_SPEC_CAP ? `${spec.slice(0, 79)}…` : spec;
|
|
6852
6913
|
}
|
|
6853
6914
|
//#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
6915
|
//#region src/client/present.ts
|
|
6862
6916
|
/** The done notice for one activation, as a locale key.
|
|
6863
6917
|
*
|
|
@@ -7187,11 +7241,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7187
7241
|
return previous.installed !== next.installed;
|
|
7188
7242
|
}
|
|
7189
7243
|
/**
|
|
7244
|
+
* Whether an update of the shop has landed and waits for a restart: the
|
|
7245
|
+
* profile holds a version other than the one running. A host older than the
|
|
7246
|
+
* field sends none, and that reads as waiting: a client newer than its host
|
|
7247
|
+
* exists only because the disk holds a newer shop than the process running
|
|
7248
|
+
* (design §7.3, 2026-09-27 amendment). The tab's restart offer and the
|
|
7249
|
+
* hand-over after a hot swap (design 2026-10-16-shop-sidebar-page §6) read
|
|
7250
|
+
* it alike.
|
|
7251
|
+
*/
|
|
7252
|
+
function updateAwaitsRestart(version) {
|
|
7253
|
+
return version.pendingVersion !== null;
|
|
7254
|
+
}
|
|
7255
|
+
/**
|
|
7190
7256
|
* 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
|
|
7257
|
+
* landed and was waiting for exactly that. The page store clears these
|
|
7258
|
+
* (page-store.ts, `noteRestarted`) once the answers come from another
|
|
7259
|
+
* process, and nothing else — a reload is still owed by a page nobody
|
|
7260
|
+
* reloaded, a failure still happened, and a flow still running settles by
|
|
7261
|
+
* its own poll.
|
|
7195
7262
|
*/
|
|
7196
7263
|
function settledByRestart(view) {
|
|
7197
7264
|
return view.kind === "done" && view.activation === "restart";
|
|
@@ -7380,6 +7447,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7380
7447
|
return identityKey(entry);
|
|
7381
7448
|
}
|
|
7382
7449
|
/**
|
|
7450
|
+
* Whether the shelf lists `entry`: a name the catalog's `notAShop` clears,
|
|
7451
|
+
* or one whose name and repository `isShopLike` leaves alone. One rule for
|
|
7452
|
+
* the shelf and for the Plugins page's "View in Plugin shop" (design
|
|
7453
|
+
* 2026-10-16-shop-sidebar-page §7), so that button never sends a reader to a
|
|
7454
|
+
* shelf that hides the plugin.
|
|
7455
|
+
*/
|
|
7456
|
+
function listedOnShelf(entry, cleared) {
|
|
7457
|
+
return cleared.has(entry.name) || !isShopLike(entry.name) && (entry.repo === void 0 || !isShopLike(entry.repo));
|
|
7458
|
+
}
|
|
7459
|
+
/**
|
|
7383
7460
|
* One entry's star count from the sidecar, or undefined when it has none.
|
|
7384
7461
|
*
|
|
7385
7462
|
* The sidecar keys a github entry by its repo full name and an npm entry by
|
|
@@ -7534,28 +7611,258 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7534
7611
|
return `${value.toFixed(1)} ${unit}`;
|
|
7535
7612
|
}
|
|
7536
7613
|
//#endregion
|
|
7614
|
+
//#region src/client/flow-registry.ts
|
|
7615
|
+
/** The one idle view, so a key the registry holds no flow for and a key it
|
|
7616
|
+
* reset to idle hand a memoized consumer the same object. */
|
|
7617
|
+
const IDLE_VIEW = { kind: "idle" };
|
|
7618
|
+
/** `map` without every entry `drop` selects, or null when it selects none.
|
|
7619
|
+
* The copy is made at the first match, so a prune that matches nothing
|
|
7620
|
+
* copies nothing, and its caller, handed null, publishes nothing. The
|
|
7621
|
+
* registry's `resetWhere` and the page store's owed notices share this
|
|
7622
|
+
* contract, so a fourth prune cannot return an empty map instead. */
|
|
7623
|
+
function pruned(map, drop) {
|
|
7624
|
+
let next = null;
|
|
7625
|
+
for (const [key, value] of map) {
|
|
7626
|
+
if (!drop(value, key)) continue;
|
|
7627
|
+
next ??= new Map(map);
|
|
7628
|
+
next.delete(key);
|
|
7629
|
+
}
|
|
7630
|
+
return next;
|
|
7631
|
+
}
|
|
7632
|
+
/**
|
|
7633
|
+
* @param deps read on every call, so an owner can hand over its latest
|
|
7634
|
+
* functions without rebuilding the registry.
|
|
7635
|
+
*/
|
|
7636
|
+
function createFlowRegistry(deps, options) {
|
|
7637
|
+
let snapshot = {
|
|
7638
|
+
views: /* @__PURE__ */ new Map(),
|
|
7639
|
+
starting: /* @__PURE__ */ new Set()
|
|
7640
|
+
};
|
|
7641
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
7642
|
+
const startedWith = /* @__PURE__ */ new Map();
|
|
7643
|
+
let timer = null;
|
|
7644
|
+
let disposed = false;
|
|
7645
|
+
const syncTimer = () => {
|
|
7646
|
+
const wanted = !disposed && (options.pollUnwatched || listeners.size > 0) && [...snapshot.views.values()].some((view) => view.kind === "running");
|
|
7647
|
+
if (wanted && timer === null) timer = setInterval(poll, INSTALL_POLL_MS);
|
|
7648
|
+
else if (!wanted && timer !== null) {
|
|
7649
|
+
clearInterval(timer);
|
|
7650
|
+
timer = null;
|
|
7651
|
+
}
|
|
7652
|
+
};
|
|
7653
|
+
const publish = (next) => {
|
|
7654
|
+
if (disposed) return;
|
|
7655
|
+
snapshot = next;
|
|
7656
|
+
syncTimer();
|
|
7657
|
+
notify(listeners);
|
|
7658
|
+
};
|
|
7659
|
+
const withView = (key, view) => {
|
|
7660
|
+
const views = new Map(snapshot.views);
|
|
7661
|
+
views.set(key, view);
|
|
7662
|
+
return views;
|
|
7663
|
+
};
|
|
7664
|
+
let asked = 0;
|
|
7665
|
+
const folded = /* @__PURE__ */ new Map();
|
|
7666
|
+
function poll() {
|
|
7667
|
+
for (const [key, view] of snapshot.views) {
|
|
7668
|
+
if (view.kind !== "running") continue;
|
|
7669
|
+
const { installId } = view;
|
|
7670
|
+
asked += 1;
|
|
7671
|
+
const number = asked;
|
|
7672
|
+
deps().installStatus({ installId }).then((status) => {
|
|
7673
|
+
const current = snapshot.views.get(key);
|
|
7674
|
+
if (disposed || current === void 0 || current.kind !== "running" || current.installId !== installId) return;
|
|
7675
|
+
if (number <= (folded.get(key) ?? 0)) return;
|
|
7676
|
+
folded.set(key, number);
|
|
7677
|
+
const next = reduceInstall(current, {
|
|
7678
|
+
type: "status",
|
|
7679
|
+
status
|
|
7680
|
+
});
|
|
7681
|
+
if (next !== current) publish({
|
|
7682
|
+
views: withView(key, next),
|
|
7683
|
+
starting: snapshot.starting
|
|
7684
|
+
});
|
|
7685
|
+
if (next.kind === "running") return;
|
|
7686
|
+
folded.delete(key);
|
|
7687
|
+
const args = startedWith.get(key);
|
|
7688
|
+
startedWith.delete(key);
|
|
7689
|
+
if (status.found && isTerminalInstallState(status.state) && args !== void 0) deps().onSettled?.(key, status.state, args);
|
|
7690
|
+
}, () => {});
|
|
7691
|
+
}
|
|
7692
|
+
}
|
|
7693
|
+
return {
|
|
7694
|
+
getSnapshot: () => snapshot,
|
|
7695
|
+
subscribe: (listener) => {
|
|
7696
|
+
const unlisten = listen(listeners, listener);
|
|
7697
|
+
syncTimer();
|
|
7698
|
+
return () => {
|
|
7699
|
+
unlisten();
|
|
7700
|
+
syncTimer();
|
|
7701
|
+
};
|
|
7702
|
+
},
|
|
7703
|
+
start: async (key, args) => {
|
|
7704
|
+
if (disposed || snapshot.starting.has(key)) return;
|
|
7705
|
+
startedWith.set(key, args);
|
|
7706
|
+
folded.delete(key);
|
|
7707
|
+
publish({
|
|
7708
|
+
views: withView(key, IDLE_VIEW),
|
|
7709
|
+
starting: new Set(snapshot.starting).add(key)
|
|
7710
|
+
});
|
|
7711
|
+
try {
|
|
7712
|
+
const view = await deps().begin(args);
|
|
7713
|
+
if (view.kind !== "running") startedWith.delete(key);
|
|
7714
|
+
publish({
|
|
7715
|
+
views: withView(key, view),
|
|
7716
|
+
starting: snapshot.starting
|
|
7717
|
+
});
|
|
7718
|
+
} finally {
|
|
7719
|
+
if (snapshot.starting.has(key)) {
|
|
7720
|
+
const starting = new Set(snapshot.starting);
|
|
7721
|
+
starting.delete(key);
|
|
7722
|
+
publish({
|
|
7723
|
+
views: snapshot.views,
|
|
7724
|
+
starting
|
|
7725
|
+
});
|
|
7726
|
+
}
|
|
7727
|
+
}
|
|
7728
|
+
},
|
|
7729
|
+
reset: (key) => {
|
|
7730
|
+
if (!snapshot.starting.has(key)) startedWith.delete(key);
|
|
7731
|
+
folded.delete(key);
|
|
7732
|
+
if (!snapshot.views.has(key)) return;
|
|
7733
|
+
const views = new Map(snapshot.views);
|
|
7734
|
+
views.delete(key);
|
|
7735
|
+
publish({
|
|
7736
|
+
views,
|
|
7737
|
+
starting: snapshot.starting
|
|
7738
|
+
});
|
|
7739
|
+
},
|
|
7740
|
+
resetWhere: (predicate) => {
|
|
7741
|
+
const views = pruned(snapshot.views, predicate);
|
|
7742
|
+
if (views === null) return;
|
|
7743
|
+
for (const key of snapshot.views.keys()) {
|
|
7744
|
+
if (views.has(key)) continue;
|
|
7745
|
+
folded.delete(key);
|
|
7746
|
+
if (!snapshot.starting.has(key)) startedWith.delete(key);
|
|
7747
|
+
}
|
|
7748
|
+
publish({
|
|
7749
|
+
views,
|
|
7750
|
+
starting: snapshot.starting
|
|
7751
|
+
});
|
|
7752
|
+
},
|
|
7753
|
+
dispose: () => {
|
|
7754
|
+
disposed = true;
|
|
7755
|
+
startedWith.clear();
|
|
7756
|
+
folded.clear();
|
|
7757
|
+
forget(listeners);
|
|
7758
|
+
syncTimer();
|
|
7759
|
+
}
|
|
7760
|
+
};
|
|
7761
|
+
}
|
|
7762
|
+
/** The transport rule the three mappings share: a thrown start is a TRANSPORT
|
|
7763
|
+
* failure (index.ts's `unwrap` throws the prefixed wire code and message), not a
|
|
7764
|
+
* business rejection — `rejected` stays reserved for the host's ShopInstallResult
|
|
7765
|
+
* union (§7.2). The transport detail is private (it can name hosts and ports) and
|
|
7766
|
+
* never rendered, so the view carries an EMPTY detail and the surface falls back
|
|
7767
|
+
* to its localized transport line. Nothing else can reach these catches, because
|
|
7768
|
+
* every business union is a resolved value, never a throw. */
|
|
7769
|
+
const transportFailed = () => ({
|
|
7770
|
+
kind: "failed",
|
|
7771
|
+
detail: "",
|
|
7772
|
+
log: []
|
|
7773
|
+
});
|
|
7774
|
+
/** An install's start: the host's business union mapped to the first view. */
|
|
7775
|
+
function beginInstall(install) {
|
|
7776
|
+
return async (args) => {
|
|
7777
|
+
try {
|
|
7778
|
+
const result = await install(args);
|
|
7779
|
+
if (!result.ok) return {
|
|
7780
|
+
kind: "rejected",
|
|
7781
|
+
code: result.code,
|
|
7782
|
+
detail: result.detail
|
|
7783
|
+
};
|
|
7784
|
+
return {
|
|
7785
|
+
kind: "running",
|
|
7786
|
+
installId: result.installId,
|
|
7787
|
+
log: [],
|
|
7788
|
+
phase: phaseOf(result.state)
|
|
7789
|
+
};
|
|
7790
|
+
} catch {
|
|
7791
|
+
return transportFailed();
|
|
7792
|
+
}
|
|
7793
|
+
};
|
|
7794
|
+
}
|
|
7795
|
+
/** A business start: the uninstall's and the shop's own update's. Its
|
|
7796
|
+
* refusal is a `failed` view with the host's published detail, never an
|
|
7797
|
+
* install `rejected` code: those belong to the install gate, and these
|
|
7798
|
+
* unions' refusals carry none. A throw is the transport rule above. The two
|
|
7799
|
+
* starts differ only in the phase they open on, which is `phaseFor`. */
|
|
7800
|
+
function beginBusiness(rpc, phaseFor) {
|
|
7801
|
+
return async (args) => {
|
|
7802
|
+
try {
|
|
7803
|
+
const result = await rpc(args);
|
|
7804
|
+
if (!result.ok) return {
|
|
7805
|
+
kind: "failed",
|
|
7806
|
+
detail: result.detail,
|
|
7807
|
+
log: []
|
|
7808
|
+
};
|
|
7809
|
+
return {
|
|
7810
|
+
kind: "running",
|
|
7811
|
+
installId: result.installId,
|
|
7812
|
+
log: [],
|
|
7813
|
+
phase: phaseFor(result)
|
|
7814
|
+
};
|
|
7815
|
+
} catch {
|
|
7816
|
+
return transportFailed();
|
|
7817
|
+
}
|
|
7818
|
+
};
|
|
7819
|
+
}
|
|
7820
|
+
/** An uninstall's start. Its refusals: not in the catalog, not installed.
|
|
7821
|
+
* The host reports no phase for an uninstall, which opens on `installing`. */
|
|
7822
|
+
function beginUninstall(uninstall) {
|
|
7823
|
+
return beginBusiness(uninstall, () => "installing");
|
|
7824
|
+
}
|
|
7825
|
+
/** The shop's own update's start. Its refusal: a version that is not plain
|
|
7826
|
+
* semver. */
|
|
7827
|
+
function beginUpdate(updateStart) {
|
|
7828
|
+
return beginBusiness(updateStart, (started) => phaseOf(started.state));
|
|
7829
|
+
}
|
|
7830
|
+
//#endregion
|
|
7537
7831
|
//#region src/client/restart-monitor.ts
|
|
7538
7832
|
/** Registry symbol, so every module instance on the page finds the same store. */
|
|
7539
|
-
const STORE_KEY = Symbol.for("dsh-plugin-shop.restart-monitor");
|
|
7833
|
+
const STORE_KEY$1 = Symbol.for("dsh-plugin-shop.restart-monitor");
|
|
7540
7834
|
function monitorStore() {
|
|
7541
7835
|
const page = globalThis;
|
|
7542
|
-
let stored = page[STORE_KEY];
|
|
7836
|
+
let stored = page[STORE_KEY$1];
|
|
7543
7837
|
if (stored === void 0) {
|
|
7544
7838
|
stored = {
|
|
7545
7839
|
state: null,
|
|
7546
7840
|
listeners: /* @__PURE__ */ new Set(),
|
|
7547
7841
|
stop: null
|
|
7548
7842
|
};
|
|
7549
|
-
page[STORE_KEY] = stored;
|
|
7843
|
+
page[STORE_KEY$1] = stored;
|
|
7550
7844
|
}
|
|
7551
7845
|
return Object.assign(stored, {
|
|
7552
7846
|
carriers: stored.carriers ?? [],
|
|
7553
7847
|
attempt: stored.attempt ?? null
|
|
7554
7848
|
});
|
|
7555
7849
|
}
|
|
7850
|
+
/**
|
|
7851
|
+
* Whether a shop instance on this page has registered its restart path:
|
|
7852
|
+
* every `apply` since 0.8.4 does so before anything can ask for a restart.
|
|
7853
|
+
* Read before this instance's own `apply` registers one, a store here names
|
|
7854
|
+
* a predecessor — a 0.8.x shop when no page store sits beside it
|
|
7855
|
+
* (page-store.ts `readPredecessor`; design 2026-10-16-shop-sidebar-page §6).
|
|
7856
|
+
* The store outlives a carrier's withdrawal: a predecessor's teardown
|
|
7857
|
+
* withdraws its carrier and leaves the store, and the store is what names it.
|
|
7858
|
+
* Only a read: unlike `monitorStore`, it never creates the store.
|
|
7859
|
+
*/
|
|
7860
|
+
function hasRestartMonitorStore() {
|
|
7861
|
+
return globalThis[STORE_KEY$1] !== void 0;
|
|
7862
|
+
}
|
|
7556
7863
|
function settle(store, state) {
|
|
7557
7864
|
store.state = state;
|
|
7558
|
-
|
|
7865
|
+
notify(store.listeners);
|
|
7559
7866
|
}
|
|
7560
7867
|
/** sessionStorage key of the restart this tab last saw committed. Not the
|
|
7561
7868
|
* page store: the verdict belongs to the page the reload produces, and a
|
|
@@ -7606,11 +7913,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7606
7913
|
}
|
|
7607
7914
|
/** Be told when the page's restart changes state. */
|
|
7608
7915
|
function subscribeRestartMonitor(listener) {
|
|
7609
|
-
|
|
7610
|
-
store.listeners.add(listener);
|
|
7611
|
-
return () => {
|
|
7612
|
-
store.listeners.delete(listener);
|
|
7613
|
-
};
|
|
7916
|
+
return listen(monitorStore().listeners, listener);
|
|
7614
7917
|
}
|
|
7615
7918
|
/**
|
|
7616
7919
|
* Watch the origin for the restarted server and reload into it: probe once
|
|
@@ -7823,9 +8126,518 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7823
8126
|
if (store.state?.kind === "not-restarted") settle(store, null);
|
|
7824
8127
|
}
|
|
7825
8128
|
//#endregion
|
|
8129
|
+
//#region src/client/page-store.ts
|
|
8130
|
+
/** The owed map's key for one switch: `<surface>:<identity>`. */
|
|
8131
|
+
function switchKey(surface, identity) {
|
|
8132
|
+
return `${surface}:${identity}`;
|
|
8133
|
+
}
|
|
8134
|
+
/** The identity a switch key names. A surface holds no colon, so it is
|
|
8135
|
+
* everything after the first. */
|
|
8136
|
+
const identityOf = (key) => key.slice(key.indexOf(":") + 1);
|
|
8137
|
+
/** Selects `key`'s receipt: its view, unless that is a flow still running.
|
|
8138
|
+
* A running flow settles by its own poll, and resetting it would drop every
|
|
8139
|
+
* later answer for it — its settle with them. */
|
|
8140
|
+
const receiptOf = (key) => (view, each) => each === key && view.kind !== "running";
|
|
8141
|
+
/** The shelf a page opens on: the star order, every category, one batch. */
|
|
8142
|
+
const INITIAL_VIEW = {
|
|
8143
|
+
query: "",
|
|
8144
|
+
category: null,
|
|
8145
|
+
order: "stars",
|
|
8146
|
+
hideIncompatible: false,
|
|
8147
|
+
visibleCount: 48
|
|
8148
|
+
};
|
|
8149
|
+
/** Whether going from one view to the other changes which cards the shelf
|
|
8150
|
+
* shows, or their order. Growing the shelf and opening a card change
|
|
8151
|
+
* neither. */
|
|
8152
|
+
function reshelves(before, after) {
|
|
8153
|
+
return before.query !== after.query || before.category !== after.category || before.order !== after.order || before.hideIncompatible !== after.hideIncompatible;
|
|
8154
|
+
}
|
|
8155
|
+
/**
|
|
8156
|
+
* @param deps read on every call, so the owner can hand over its latest
|
|
8157
|
+
* functions without rebuilding the store.
|
|
8158
|
+
* @param options the page's own store polls unwatched; a store a tab builds
|
|
8159
|
+
* for itself does not, and stops with the tab (ShopTab).
|
|
8160
|
+
*/
|
|
8161
|
+
function createPageStore(deps, options) {
|
|
8162
|
+
let snapshot = {
|
|
8163
|
+
view: INITIAL_VIEW,
|
|
8164
|
+
reverdictPending: false,
|
|
8165
|
+
reverdicts: 0,
|
|
8166
|
+
settled: 0
|
|
8167
|
+
};
|
|
8168
|
+
let scrollTop = 0;
|
|
8169
|
+
let scrollAnchor = null;
|
|
8170
|
+
let pageMounted = false;
|
|
8171
|
+
let handover = { showing: false };
|
|
8172
|
+
let disposed = false;
|
|
8173
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
8174
|
+
const publish = (next) => {
|
|
8175
|
+
if (disposed) return;
|
|
8176
|
+
snapshot = {
|
|
8177
|
+
...snapshot,
|
|
8178
|
+
...next
|
|
8179
|
+
};
|
|
8180
|
+
notify(listeners);
|
|
8181
|
+
};
|
|
8182
|
+
/** The view with `patch` applied, published only when it differs: the
|
|
8183
|
+
* view's fields are primitives, so a field-by-field compare is the
|
|
8184
|
+
* whole question. One that refilters or reorders the shelf forgets the
|
|
8185
|
+
* card the reader was at: the restore then sets the position, which is
|
|
8186
|
+
* what the reader last saw, and their next scroll writes a card again. */
|
|
8187
|
+
const showView = (patch) => {
|
|
8188
|
+
const view = {
|
|
8189
|
+
...snapshot.view,
|
|
8190
|
+
...patch
|
|
8191
|
+
};
|
|
8192
|
+
if (Object.keys(view).every((field) => view[field] === snapshot.view[field])) return;
|
|
8193
|
+
if (reshelves(snapshot.view, view)) scrollAnchor = null;
|
|
8194
|
+
publish({ view });
|
|
8195
|
+
};
|
|
8196
|
+
let expanded = /* @__PURE__ */ new Set();
|
|
8197
|
+
const expandedListeners = /* @__PURE__ */ new Set();
|
|
8198
|
+
const publishExpanded = (next) => {
|
|
8199
|
+
if (disposed) return;
|
|
8200
|
+
expanded = next;
|
|
8201
|
+
notify(expandedListeners);
|
|
8202
|
+
};
|
|
8203
|
+
/** Open one card, and leave it open: a card's own header toggles it, and
|
|
8204
|
+
* `expand` opens one from outside it (design
|
|
8205
|
+
* 2026-10-16-shop-sidebar-page §5.6). */
|
|
8206
|
+
const openCard = (key) => {
|
|
8207
|
+
if (!expanded.has(key)) publishExpanded(new Set(expanded).add(key));
|
|
8208
|
+
};
|
|
8209
|
+
let owed = /* @__PURE__ */ new Map();
|
|
8210
|
+
const owedListeners = /* @__PURE__ */ new Set();
|
|
8211
|
+
const publishOwed = (next) => {
|
|
8212
|
+
if (disposed) return;
|
|
8213
|
+
owed = next;
|
|
8214
|
+
notify(owedListeners);
|
|
8215
|
+
};
|
|
8216
|
+
/** A reverdict raised: pending, and counted. */
|
|
8217
|
+
const raisedReverdict = () => ({
|
|
8218
|
+
reverdictPending: true,
|
|
8219
|
+
reverdicts: snapshot.reverdicts + 1
|
|
8220
|
+
});
|
|
8221
|
+
/** One settle the page reacts to: re-read the installed list and, when the
|
|
8222
|
+
* catalog's verdicts may have moved, judge them again. */
|
|
8223
|
+
const settle = (reverdict) => {
|
|
8224
|
+
publish({
|
|
8225
|
+
settled: snapshot.settled + 1,
|
|
8226
|
+
...reverdict ? raisedReverdict() : {}
|
|
8227
|
+
});
|
|
8228
|
+
};
|
|
8229
|
+
/** The owed map without one notice kind, or null when it holds none of
|
|
8230
|
+
* them (`pruned`), so a caller publishes nothing for an unchanged map. */
|
|
8231
|
+
const withoutOwed = (kind) => pruned(owed, (value) => value === kind);
|
|
8232
|
+
/** The owed map without any switch of `identities`, on every surface, or
|
|
8233
|
+
* null when it holds none. */
|
|
8234
|
+
const withoutSwitchesOf = (identities) => pruned(owed, (_, key) => identities.has(identityOf(key)));
|
|
8235
|
+
let drops = 0;
|
|
8236
|
+
/** By identity: the stamp of the last drop because it left the profile. */
|
|
8237
|
+
const leftAt = /* @__PURE__ */ new Map();
|
|
8238
|
+
/** The identities the installed list a page last read held: the switches
|
|
8239
|
+
* on screen are theirs, and so is any toggle still out. */
|
|
8240
|
+
let listed = /* @__PURE__ */ new Set();
|
|
8241
|
+
/** `identities` left the profile: their switches owe nothing, and a toggle
|
|
8242
|
+
* of one still out re-creates nothing. */
|
|
8243
|
+
const leave = (identities) => {
|
|
8244
|
+
if (identities.size === 0) return;
|
|
8245
|
+
drops += 1;
|
|
8246
|
+
for (const identity of identities) leftAt.set(identity, drops);
|
|
8247
|
+
const next = withoutSwitchesOf(identities);
|
|
8248
|
+
if (next !== null) publishOwed(next);
|
|
8249
|
+
};
|
|
8250
|
+
const installs = createFlowRegistry(() => ({
|
|
8251
|
+
begin: beginInstall(deps().install),
|
|
8252
|
+
installStatus: deps().installStatus,
|
|
8253
|
+
onSettled: (key, outcome) => {
|
|
8254
|
+
if (outcome === "done") uninstalls.resetWhere(receiptOf(key));
|
|
8255
|
+
settle(outcome === "done");
|
|
8256
|
+
}
|
|
8257
|
+
}), options);
|
|
8258
|
+
const uninstalls = createFlowRegistry(() => ({
|
|
8259
|
+
begin: beginUninstall(deps().uninstall),
|
|
8260
|
+
installStatus: deps().installStatus,
|
|
8261
|
+
onSettled: (key, outcome, args) => {
|
|
8262
|
+
if (outcome !== "done") return;
|
|
8263
|
+
installs.resetWhere(receiptOf(key));
|
|
8264
|
+
leave(/* @__PURE__ */ new Set([key]));
|
|
8265
|
+
deps().noteUninstalled?.(args.name);
|
|
8266
|
+
settle(true);
|
|
8267
|
+
}
|
|
8268
|
+
}), options);
|
|
8269
|
+
const self = createFlowRegistry(() => ({
|
|
8270
|
+
begin: beginUpdate(deps().updateStart),
|
|
8271
|
+
installStatus: deps().installStatus
|
|
8272
|
+
}), options);
|
|
8273
|
+
return {
|
|
8274
|
+
schema: 1,
|
|
8275
|
+
installs,
|
|
8276
|
+
uninstalls,
|
|
8277
|
+
self,
|
|
8278
|
+
getSnapshot: () => snapshot,
|
|
8279
|
+
subscribe: (listener) => listen(listeners, listener),
|
|
8280
|
+
setView: (patch) => {
|
|
8281
|
+
showView(patch);
|
|
8282
|
+
},
|
|
8283
|
+
updateView: (update) => {
|
|
8284
|
+
showView(update(snapshot.view));
|
|
8285
|
+
},
|
|
8286
|
+
isExpanded: (key) => expanded.has(key),
|
|
8287
|
+
subscribeExpanded: (listener) => listen(expandedListeners, listener),
|
|
8288
|
+
toggleExpanded: (key) => {
|
|
8289
|
+
const next = new Set(expanded);
|
|
8290
|
+
if (!next.delete(key)) next.add(key);
|
|
8291
|
+
publishExpanded(next);
|
|
8292
|
+
},
|
|
8293
|
+
expand: openCard,
|
|
8294
|
+
focus: (name, card) => {
|
|
8295
|
+
scrollTop = 0;
|
|
8296
|
+
scrollAnchor = null;
|
|
8297
|
+
showView({
|
|
8298
|
+
query: name,
|
|
8299
|
+
category: null,
|
|
8300
|
+
hideIncompatible: false,
|
|
8301
|
+
visibleCount: 48
|
|
8302
|
+
});
|
|
8303
|
+
if (card !== null) openCard(card);
|
|
8304
|
+
},
|
|
8305
|
+
getOwed: (key) => owed.get(key),
|
|
8306
|
+
subscribeOwed: (listener) => listen(owedListeners, listener),
|
|
8307
|
+
owedEpoch: () => drops,
|
|
8308
|
+
setOwed: (key, notice, since) => {
|
|
8309
|
+
if ((leftAt.get(identityOf(key)) ?? 0) > since) return;
|
|
8310
|
+
const current = owed.get(key);
|
|
8311
|
+
if (current === "restart" || current === notice) return;
|
|
8312
|
+
publishOwed(new Map(owed).set(key, notice));
|
|
8313
|
+
},
|
|
8314
|
+
noteInstalled: (identities) => {
|
|
8315
|
+
const held = new Set(identities);
|
|
8316
|
+
const gone = /* @__PURE__ */ new Set();
|
|
8317
|
+
for (const identity of listed) if (!held.has(identity)) gone.add(identity);
|
|
8318
|
+
for (const key of owed.keys()) if (!held.has(identityOf(key))) gone.add(identityOf(key));
|
|
8319
|
+
listed = held;
|
|
8320
|
+
leave(gone);
|
|
8321
|
+
},
|
|
8322
|
+
clearReverdict: (asked) => {
|
|
8323
|
+
if (snapshot.reverdictPending && asked === snapshot.reverdicts) publish({ reverdictPending: false });
|
|
8324
|
+
},
|
|
8325
|
+
noteRestarted: () => {
|
|
8326
|
+
self.resetWhere(settledByRestart);
|
|
8327
|
+
installs.resetWhere(settledByRestart);
|
|
8328
|
+
uninstalls.resetWhere(settledByRestart);
|
|
8329
|
+
const next = withoutOwed("restart");
|
|
8330
|
+
if (next !== null) publishOwed(next);
|
|
8331
|
+
settle(true);
|
|
8332
|
+
},
|
|
8333
|
+
recordScroll: (top, anchor = null) => {
|
|
8334
|
+
scrollTop = top;
|
|
8335
|
+
scrollAnchor = anchor;
|
|
8336
|
+
},
|
|
8337
|
+
readScroll: () => scrollTop,
|
|
8338
|
+
readAnchor: () => scrollAnchor,
|
|
8339
|
+
notePageMounted: (mounted) => {
|
|
8340
|
+
pageMounted = mounted;
|
|
8341
|
+
},
|
|
8342
|
+
recordTeardown: () => {
|
|
8343
|
+
handover = { showing: pageMounted };
|
|
8344
|
+
},
|
|
8345
|
+
handover: () => handover,
|
|
8346
|
+
dispose: () => {
|
|
8347
|
+
disposed = true;
|
|
8348
|
+
installs.dispose();
|
|
8349
|
+
uninstalls.dispose();
|
|
8350
|
+
self.dispose();
|
|
8351
|
+
forget(listeners);
|
|
8352
|
+
forget(owedListeners);
|
|
8353
|
+
forget(expandedListeners);
|
|
8354
|
+
}
|
|
8355
|
+
};
|
|
8356
|
+
}
|
|
8357
|
+
/** Registry symbol, so every module instance on the page finds one store. */
|
|
8358
|
+
const STORE_KEY = Symbol.for("dsh-plugin-shop.page");
|
|
8359
|
+
function occupantOf(held) {
|
|
8360
|
+
if (held === void 0) return { kind: "none" };
|
|
8361
|
+
if (typeof held === "object" && held !== null && held.schema === 1) return {
|
|
8362
|
+
kind: "store",
|
|
8363
|
+
store: held
|
|
8364
|
+
};
|
|
8365
|
+
return {
|
|
8366
|
+
kind: "other",
|
|
8367
|
+
occupant: held
|
|
8368
|
+
};
|
|
8369
|
+
}
|
|
8370
|
+
/** What a shop applied earlier left on this page: nothing, a store of this
|
|
8371
|
+
* schema, or another schema's object. */
|
|
8372
|
+
function readPageStore() {
|
|
8373
|
+
return occupantOf(globalThis[STORE_KEY]);
|
|
8374
|
+
}
|
|
8375
|
+
/** Make `store` the page's. Returns what it replaces. */
|
|
8376
|
+
function installPageStore(store) {
|
|
8377
|
+
const page = globalThis;
|
|
8378
|
+
const previous = occupantOf(page[STORE_KEY]);
|
|
8379
|
+
page[STORE_KEY] = store;
|
|
8380
|
+
return previous;
|
|
8381
|
+
}
|
|
8382
|
+
/** Read at the very start of `apply`, before this instance puts its own
|
|
8383
|
+
* stores on the page. */
|
|
8384
|
+
function readPredecessor() {
|
|
8385
|
+
const previous = readPageStore();
|
|
8386
|
+
if (previous.kind === "none") return hasRestartMonitorStore() ? { kind: "section" } : { kind: "none" };
|
|
8387
|
+
if (previous.kind === "other") return {
|
|
8388
|
+
kind: "page",
|
|
8389
|
+
showing: false
|
|
8390
|
+
};
|
|
8391
|
+
if (typeof previous.store.handover !== "function") return { kind: "section" };
|
|
8392
|
+
return {
|
|
8393
|
+
kind: "page",
|
|
8394
|
+
showing: previous.store.handover().showing
|
|
8395
|
+
};
|
|
8396
|
+
}
|
|
8397
|
+
/** Stop what the page key held, through the `dispose` it has and no other:
|
|
8398
|
+
* an object that claims this schema and lacks one is stopped no more than a
|
|
8399
|
+
* schema-2 object is, and neither can throw the shop off the page. */
|
|
8400
|
+
function disposeOccupant(occupant) {
|
|
8401
|
+
if (occupant.kind === "none") return;
|
|
8402
|
+
const held = occupant.kind === "store" ? occupant.store : occupant.occupant;
|
|
8403
|
+
if (typeof held?.dispose === "function") held.dispose();
|
|
8404
|
+
}
|
|
8405
|
+
//#endregion
|
|
8406
|
+
//#region \0dsh-plugin-shop-css:5f753521
|
|
8407
|
+
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.sa85938_client.s94d4e5_config.s91da2e_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.sa85938_client.sbb5706_spec.s91da2e_ts` pins this. */\n\n.s6a01b8_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 `.sa16cbe_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.sbc1497_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 `.s61d354_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.s61d354_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.s61d354_page > * {\n width: 100%;\n max-width: 960px;\n}\n\n.s9331e8_pageHead {\n box-sizing: border-box;\n padding-top: calc(28px + var(--dsh-frame-top-clearance, 0px));\n}\n\n.sb4f89f_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.s5b1bea_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.s591d55_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.s591d55_detailAction:hover {\n background: var(--dsw-alias-interactive-bg-hover);\n}\n\n.s591d55_detailAction:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s352f7a_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.s60f2f5_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.s34d5f5_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s11e315_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.s11e315_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.s227d7c_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.s83025b_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.sdb7845_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.sa61117_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.s11e315_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 .s11e315_skeletonCard span { animation: none; opacity: 0.75; }\n .s11e315_skeletonCard::after { content: none; }\n}\n\n.s757132_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.s757132_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.s757132_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sbc1497_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s22d15d_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.s76d3c9_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s390e34_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.s79450a_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.s79450a_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.s79450a_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.sb48cbc_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.sb48cbc_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.sb48cbc_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.sb48cbc_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.s946346_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.s946346_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.s946346_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.s861599_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.sa16cbe_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.sa16cbe_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.seb8329_expansionNote {\n margin: 4px 0 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s31a2f0_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.sab4844_categoryBar {\n display: flex;\n flex-wrap: wrap;\n align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s512425_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.s512425_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 `.s512425_categoryButton.s598384_categoryButtonOn` — TWO classes — so it\n * matches `.s512425_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s598384_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.sa85938_client.sbb5706_spec.s91da2e_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.s512425_categoryButton.s598384_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 .s512425_categoryButton.s598384_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s512425_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 `.s512425_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.s7ce437_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: `.s7ce437_incompatibleFilter:hover .s3a8382_switch` outranks `.s1e392e_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.s7ce437_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.s7ce437_incompatibleFilter:hover .s3a8382_switch:not(.s1e392e_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 `.s512425_categoryButton.s598384_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.s7ce437_incompatibleFilter.sd9fa8d_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.s7ce437_incompatibleFilter .s3a8382_switch:not(.s1e392e_switchOn) .s419ba1_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.s7ce437_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.s320079_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.s923c82_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.s3edd9f_sortGroup {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n margin-left: auto;\n}\n\n.s73feb1_sortLabel {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sad5aba_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 `.s757132_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.s4cca13_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.s4cca13_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.s4cca13_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s4cca13_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc24c97_catalogRefreshFailed {\n font-size: 11px;\n color: var(--shop-warn);\n}\n\n.s1aa6bf_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.sa036b0_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 .sa036b0_cards { animation: none; }\n}\n\n.s88b200_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.s853040_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s239c39_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s88b200_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 .s88b200_card { transition: none; }\n .s88b200_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.sc1c25e_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. `.s512425_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.s88b200_card[data-category='tool'], .s512425_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s88b200_card[data-category='provider'], .s512425_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s88b200_card[data-category='ui'], .s512425_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s88b200_card[data-category='workflow'], .s512425_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s88b200_card[data-category='integration'], .s512425_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s88b200_card[data-category='theme'], .s512425_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s88b200_card[data-category='other'], .s512425_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.s433c76_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.s6bc968_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.s6bc968_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.s704f0d_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.sba9311_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.s9b33f4_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.s9b33f4_cardActions > .sc99cff_gate,\n.s9b33f4_cardActions > .sbd0905_installPanel,\n.s9b33f4_cardActions > .sd562d9_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.s83e5d6_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.s93d06c_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s1fe784_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s1fe784_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.s1fe784_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.s1fe784_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.s1fe784_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.s1fe784_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.s2f456e_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.sc6c968_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s287fda_chevron, .s20b60f_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s20b60f_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s287fda_chevron, .s20b60f_chevronOpen { transition: none; }\n}\n\n.s87fcee_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sf5d0d8_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.sa0923f_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.s20eea8_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb53b34_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.sc2cc78_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.sc7602c_remedy {\n display: block;\n}\n\n.sc7602c_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 `.s895dae_foldBlock` collapsed and `.s6b136d_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.s895dae_foldBlock {\n display: block;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n display: -webkit-box;\n overflow: hidden;\n}\n\n.s6b136d_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.s8c3792_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.s8c3792_foldToggle:hover { border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2)); }\n.s8c3792_foldToggle:focus-visible { outline: 2px solid var(--dsw-alias-brand-primary); outline-offset: 1px; }\n\n.s935bd9_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.se75f45_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.se69fd7_refusal {\n display: block;\n flex-basis: 100%;\n}\n\n.se69fd7_refusal > * + * { margin-top: 6px; }\n\n.sb2edea_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.sa5c8b2_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sb95e1e_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.sb95e1e_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.s1f4436_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.sf6f153_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sbfe178_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sbfe178_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sbfe178_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.sbfe178_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sbfe178_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sbfe178_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 * `.s9b33f4_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.s92d907_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: `.s92d907_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 * `.s6119f4_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.s5889e2_size,\n.s6119f4_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 `.s92d907_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 `.s92d907_cardMeta`. There is no middot:\n * `.s92d907_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.s5efcbe_author {\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sdd3957_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.sbd0905_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.se20cee_installButton,\n.sdf88f9_confirmButton,\n.s330a03_cancelButton,\n.sad029f_installedLabel,\n.s7b7f2d_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.se20cee_installButton, .sdf88f9_confirmButton, .s330a03_cancelButton {\n cursor: pointer;\n}\n\n.se20cee_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.se20cee_installButton:disabled {\n cursor: wait;\n}\n\n.se20cee_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sdf88f9_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.sdf88f9_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s330a03_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.s330a03_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se20cee_installButton:focus-visible, .sdf88f9_confirmButton:focus-visible, .s330a03_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.sad029f_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.s7b7f2d_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.sd562d9_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.s1ef16e_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.s1ef16e_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s1ef16e_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.s912ec7_restartSelfButton {\n /* The version row's third state. Geometry matches .sb48cbc_checkUpdateButton and\n * .s79450a_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.s912ec7_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.sa5d8c1_restartButton,\n.se5bd2c_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.sa5d8c1_restartButton:hover,\n.se5bd2c_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.sa5d8c1_restartButton:focus-visible,\n.se5bd2c_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 * `.sd562d9_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.s76690e_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\n\n\n.sc99cff_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.s58fe9b_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--shop-warn-on-tint);\n}\n\n.sd510f5_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s943dbd_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.sb2e35c_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s407016_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s56c1a5_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.s3d7aca_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s3e1ea0_notice {\n margin: 0;\n font-size: 12px;\n color: var(--shop-success);\n}\n\n.s71965d_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--shop-error);\n}\n\n.s41bb43_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s3b880d_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--shop-error);\n}\n\n.s465924_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.sfccf6f_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s403f9e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.sd9cb61_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.s7b2d89_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s7b2d89_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.sc33878_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 .sba9311_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.sa88998_shadowedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.s98799e_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.saaf7ce_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: `.s7ce437_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 `.s3a8382_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.s3a8382_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.s1e392e_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 55%, transparent);\n}\n\n.s419ba1_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.s1e392e_switchOn .s419ba1_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s3a8382_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 .s3a8382_switch, .s419ba1_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.saa9c93_switchWrap {\n display: inline-flex;\n align-items: center;\n /* Same reason as `.saaf7ce_outdatedActions`: `.s76690e_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sd562d9_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
8408
|
+
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"5f753521\"]")) {
|
|
8409
|
+
const tag = document.createElement("style");
|
|
8410
|
+
tag.dataset.pluginCss = "5f753521";
|
|
8411
|
+
tag.textContent = css;
|
|
8412
|
+
document.head.appendChild(tag);
|
|
8413
|
+
}
|
|
8414
|
+
var _dsh_plugin_shop_css_5f753521_default = {
|
|
8415
|
+
"client": "sa85938_client",
|
|
8416
|
+
"config": "s94d4e5_config",
|
|
8417
|
+
"ts": "s91da2e_ts",
|
|
8418
|
+
"spec": "sbb5706_spec",
|
|
8419
|
+
"panel": "s6a01b8_panel",
|
|
8420
|
+
"searchInput": "sa16cbe_searchInput",
|
|
8421
|
+
"toolbar": "sbc1497_toolbar",
|
|
8422
|
+
"page": "s61d354_page",
|
|
8423
|
+
"pageHead": "s9331e8_pageHead",
|
|
8424
|
+
"pageTitle": "sb4f89f_pageTitle",
|
|
8425
|
+
"pageIntro": "s5b1bea_pageIntro",
|
|
8426
|
+
"detailAction": "s591d55_detailAction",
|
|
8427
|
+
"stateLine": "s352f7a_stateLine",
|
|
8428
|
+
"srOnly": "s60f2f5_srOnly",
|
|
8429
|
+
"skeletonGrid": "s34d5f5_skeletonGrid",
|
|
8430
|
+
"skeletonCard": "s11e315_skeletonCard",
|
|
8431
|
+
"skeletonName": "s227d7c_skeletonName",
|
|
8432
|
+
"skeletonSummary": "s83025b_skeletonSummary",
|
|
8433
|
+
"skeletonSummaryShort": "sdb7845_skeletonSummaryShort",
|
|
8434
|
+
"skeletonActions": "sa61117_skeletonActions",
|
|
8435
|
+
"actionButton": "s757132_actionButton",
|
|
8436
|
+
"toolbarRight": "s22d15d_toolbarRight",
|
|
8437
|
+
"versionBlock": "s76d3c9_versionBlock",
|
|
8438
|
+
"versionText": "s390e34_versionText",
|
|
8439
|
+
"updateSelfButton": "s79450a_updateSelfButton",
|
|
8440
|
+
"checkUpdateButton": "sb48cbc_checkUpdateButton",
|
|
8441
|
+
"githubLink": "s946346_githubLink",
|
|
8442
|
+
"selfUpdatePanel": "s861599_selfUpdatePanel",
|
|
8443
|
+
"expansionNote": "seb8329_expansionNote",
|
|
8444
|
+
"staleBadge": "s31a2f0_staleBadge",
|
|
8445
|
+
"categoryBar": "sab4844_categoryBar",
|
|
8446
|
+
"categoryButton": "s512425_categoryButton",
|
|
8447
|
+
"categoryButtonOn": "s598384_categoryButtonOn",
|
|
8448
|
+
"incompatibleFilter": "s7ce437_incompatibleFilter",
|
|
8449
|
+
"switch": "s3a8382_switch",
|
|
8450
|
+
"switchOn": "s1e392e_switchOn",
|
|
8451
|
+
"incompatibleFilterOn": "sd9fa8d_incompatibleFilterOn",
|
|
8452
|
+
"switchKnob": "s419ba1_switchKnob",
|
|
8453
|
+
"updatableHeading": "s320079_updatableHeading",
|
|
8454
|
+
"catalogStatsRow": "s923c82_catalogStatsRow",
|
|
8455
|
+
"sortGroup": "s3edd9f_sortGroup",
|
|
8456
|
+
"sortLabel": "s73feb1_sortLabel",
|
|
8457
|
+
"catalogStats": "sad5aba_catalogStats",
|
|
8458
|
+
"catalogRefreshButton": "s4cca13_catalogRefreshButton",
|
|
8459
|
+
"catalogRefreshFailed": "sc24c97_catalogRefreshFailed",
|
|
8460
|
+
"emptyLine": "s1aa6bf_emptyLine",
|
|
8461
|
+
"cards": "sa036b0_cards",
|
|
8462
|
+
"card": "s88b200_card",
|
|
8463
|
+
"cardsSentry": "s853040_cardsSentry",
|
|
8464
|
+
"showingLine": "s239c39_showingLine",
|
|
8465
|
+
"cardSpine": "sc1c25e_cardSpine",
|
|
8466
|
+
"categoryBadge": "s433c76_categoryBadge",
|
|
8467
|
+
"entryHeader": "s6bc968_entryHeader",
|
|
8468
|
+
"name": "s704f0d_name",
|
|
8469
|
+
"badges": "sba9311_badges",
|
|
8470
|
+
"cardActions": "s9b33f4_cardActions",
|
|
8471
|
+
"gate": "sc99cff_gate",
|
|
8472
|
+
"installPanel": "sbd0905_installPanel",
|
|
8473
|
+
"installedActions": "sd562d9_installedActions",
|
|
8474
|
+
"cardVersion": "s83e5d6_cardVersion",
|
|
8475
|
+
"sourceBadge": "s93d06c_sourceBadge",
|
|
8476
|
+
"tierBadge": "s1fe784_tierBadge",
|
|
8477
|
+
"incompatibleBadge": "s2f456e_incompatibleBadge",
|
|
8478
|
+
"starsBadge": "sc6c968_starsBadge",
|
|
8479
|
+
"chevron": "s287fda_chevron",
|
|
8480
|
+
"chevronOpen": "s20b60f_chevronOpen",
|
|
8481
|
+
"body": "s87fcee_body",
|
|
8482
|
+
"summary": "sf5d0d8_summary",
|
|
8483
|
+
"summaryZh": "sa0923f_summaryZh",
|
|
8484
|
+
"summaryExpanded": "s20eea8_summaryExpanded",
|
|
8485
|
+
"summaryZhExpanded": "sb53b34_summaryZhExpanded",
|
|
8486
|
+
"incompatibleDetail": "sc2cc78_incompatibleDetail",
|
|
8487
|
+
"remedy": "sc7602c_remedy",
|
|
8488
|
+
"foldBlock": "s895dae_foldBlock",
|
|
8489
|
+
"foldBlockExpanded": "s6b136d_foldBlockExpanded",
|
|
8490
|
+
"foldToggle": "s8c3792_foldToggle",
|
|
8491
|
+
"remedyLead": "s935bd9_remedyLead",
|
|
8492
|
+
"allowCommand": "se75f45_allowCommand",
|
|
8493
|
+
"refusal": "se69fd7_refusal",
|
|
8494
|
+
"capabilitiesBlock": "sb2edea_capabilitiesBlock",
|
|
8495
|
+
"capabilitiesNote": "sa5c8b2_capabilitiesNote",
|
|
8496
|
+
"capabilities": "sb95e1e_capabilities",
|
|
8497
|
+
"detail": "s1f4436_detail",
|
|
8498
|
+
"detailRows": "sf6f153_detailRows",
|
|
8499
|
+
"detailRow": "sbfe178_detailRow",
|
|
8500
|
+
"cardMeta": "s92d907_cardMeta",
|
|
8501
|
+
"shelfDate": "s6119f4_shelfDate",
|
|
8502
|
+
"size": "s5889e2_size",
|
|
8503
|
+
"author": "s5efcbe_author",
|
|
8504
|
+
"reviewedLine": "sdd3957_reviewedLine",
|
|
8505
|
+
"installButton": "se20cee_installButton",
|
|
8506
|
+
"confirmButton": "sdf88f9_confirmButton",
|
|
8507
|
+
"cancelButton": "s330a03_cancelButton",
|
|
8508
|
+
"installedLabel": "sad029f_installedLabel",
|
|
8509
|
+
"refusedLabel": "s7b7f2d_refusedLabel",
|
|
8510
|
+
"uninstallButton": "s1ef16e_uninstallButton",
|
|
8511
|
+
"restartSelfButton": "s912ec7_restartSelfButton",
|
|
8512
|
+
"restartButton": "sa5d8c1_restartButton",
|
|
8513
|
+
"reloadButton": "se5bd2c_reloadButton",
|
|
8514
|
+
"reloadPanel": "s76690e_reloadPanel",
|
|
8515
|
+
"gateTitle": "s58fe9b_gateTitle",
|
|
8516
|
+
"gateBody": "sd510f5_gateBody",
|
|
8517
|
+
"gateWarning": "s943dbd_gateWarning",
|
|
8518
|
+
"gateActions": "sb2e35c_gateActions",
|
|
8519
|
+
"installing": "s407016_installing",
|
|
8520
|
+
"log": "s56c1a5_log",
|
|
8521
|
+
"logLine": "s3d7aca_logLine",
|
|
8522
|
+
"notice": "s3e1ea0_notice",
|
|
8523
|
+
"failedHeading": "s71965d_failedHeading",
|
|
8524
|
+
"failedDetail": "s41bb43_failedDetail",
|
|
8525
|
+
"rejectedCode": "s3b880d_rejectedCode",
|
|
8526
|
+
"rejectedDetail": "s465924_rejectedDetail",
|
|
8527
|
+
"outdatedSection": "sfccf6f_outdatedSection",
|
|
8528
|
+
"outdatedList": "s403f9e_outdatedList",
|
|
8529
|
+
"outdatedRow": "sd9cb61_outdatedRow",
|
|
8530
|
+
"outdatedInfo": "s7b2d89_outdatedInfo",
|
|
8531
|
+
"outdatedVersions": "sc33878_outdatedVersions",
|
|
8532
|
+
"shadowedSection": "sa88998_shadowedSection",
|
|
8533
|
+
"shadowedDetail": "s98799e_shadowedDetail",
|
|
8534
|
+
"outdatedActions": "saaf7ce_outdatedActions",
|
|
8535
|
+
"switchWrap": "saa9c93_switchWrap"
|
|
8536
|
+
};
|
|
8537
|
+
//#endregion
|
|
8538
|
+
//#region src/client/ShopDetailAction.tsx
|
|
8539
|
+
/**
|
|
8540
|
+
* The shop's two buttons on dsh's Plugins page (design
|
|
8541
|
+
* 2026-10-16-shop-sidebar-page §7), in the head of a detail page, before the
|
|
8542
|
+
* page's own switch and uninstall (`plugins.detail.actions`, dsh 0.1.7-rc.2
|
|
8543
|
+
* and later): "Open Plugin shop" on the shop's own bundle page, and "View in
|
|
8544
|
+
* Plugin shop" on the page of any bundle the shelf lists. Any other subject
|
|
8545
|
+
* renders nothing, as the slot's contract asks of an entry.
|
|
8546
|
+
*
|
|
8547
|
+
* It renders inside dsh's page, so it carries its own error boundary: a
|
|
8548
|
+
* throw here costs the reader a button, never dsh's page.
|
|
8549
|
+
*/
|
|
8550
|
+
/** The shop's own package, whose bundle page gets "Open Plugin shop". */
|
|
8551
|
+
const OWN_PACKAGE = "dsh-plugin-shop";
|
|
8552
|
+
function ShopDetailAction(props) {
|
|
8553
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailActionBoundary, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DetailActionBody, { ...props }) });
|
|
8554
|
+
}
|
|
8555
|
+
/** Renders nothing once a child throws: a lost button, never a lost page. A
|
|
8556
|
+
* class, because React offers error boundaries in no other form. */
|
|
8557
|
+
var DetailActionBoundary = class extends react.Component {
|
|
8558
|
+
state = { failed: false };
|
|
8559
|
+
static getDerivedStateFromError() {
|
|
8560
|
+
return { failed: true };
|
|
8561
|
+
}
|
|
8562
|
+
componentDidCatch(error, info) {
|
|
8563
|
+
console.error("dsh-plugin-shop: the Plugins page button crashed", error, info.componentStack);
|
|
8564
|
+
}
|
|
8565
|
+
render() {
|
|
8566
|
+
return this.state.failed ? null : this.props.children;
|
|
8567
|
+
}
|
|
8568
|
+
};
|
|
8569
|
+
function DetailActionBody({ subject, t, listing, canOpen, openShop, page }) {
|
|
8570
|
+
const name = subject.kind === "bundle" ? subject.pkg.name : null;
|
|
8571
|
+
const own = name === OWN_PACKAGE;
|
|
8572
|
+
const [listed, setListed] = (0, react.useState)(null);
|
|
8573
|
+
(0, react.useEffect)(() => {
|
|
8574
|
+
if (name === null || own) return;
|
|
8575
|
+
let cancelled = false;
|
|
8576
|
+
listing().then((result) => {
|
|
8577
|
+
if (cancelled) return;
|
|
8578
|
+
const cleared = new Set(result.notAShop ?? []);
|
|
8579
|
+
setListed({
|
|
8580
|
+
name,
|
|
8581
|
+
keys: result.plugins.filter((entry) => entry.name === name && listedOnShelf(entry, cleared)).map(entryKey)
|
|
8582
|
+
});
|
|
8583
|
+
}).catch(() => {});
|
|
8584
|
+
return () => {
|
|
8585
|
+
cancelled = true;
|
|
8586
|
+
};
|
|
8587
|
+
}, [
|
|
8588
|
+
name,
|
|
8589
|
+
own,
|
|
8590
|
+
listing
|
|
8591
|
+
]);
|
|
8592
|
+
if (name === null || !canOpen()) return null;
|
|
8593
|
+
if (own) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8594
|
+
type: "button",
|
|
8595
|
+
className: _dsh_plugin_shop_css_5f753521_default.detailAction,
|
|
8596
|
+
"data-shop-open-shop": true,
|
|
8597
|
+
onClick: openShop,
|
|
8598
|
+
children: t("openShop")
|
|
8599
|
+
});
|
|
8600
|
+
if (listed === null || listed.name !== name || listed.keys.length === 0) return null;
|
|
8601
|
+
const only = listed.keys.length === 1 ? listed.keys[0] ?? null : null;
|
|
8602
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
8603
|
+
type: "button",
|
|
8604
|
+
className: _dsh_plugin_shop_css_5f753521_default.detailAction,
|
|
8605
|
+
"data-shop-view-in-shop": true,
|
|
8606
|
+
onClick: () => {
|
|
8607
|
+
page.focus(name, only);
|
|
8608
|
+
openShop();
|
|
8609
|
+
},
|
|
8610
|
+
children: t("viewInShop")
|
|
8611
|
+
});
|
|
8612
|
+
}
|
|
8613
|
+
//#endregion
|
|
8614
|
+
//#region src/client/ShopGlyph.tsx
|
|
8615
|
+
function ShopGlyph({ size }) {
|
|
8616
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("svg", {
|
|
8617
|
+
width: size,
|
|
8618
|
+
height: size,
|
|
8619
|
+
viewBox: "0 0 16 16",
|
|
8620
|
+
fill: "none",
|
|
8621
|
+
stroke: "currentColor",
|
|
8622
|
+
strokeWidth: 1,
|
|
8623
|
+
strokeLinecap: "round",
|
|
8624
|
+
strokeLinejoin: "round",
|
|
8625
|
+
"aria-hidden": "true",
|
|
8626
|
+
"data-shop-glyph": true,
|
|
8627
|
+
children: [
|
|
8628
|
+
/* @__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" }),
|
|
8629
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5.75 5.5V4.25a2.25 2.25 0 0 1 4.5 0V5.5" }),
|
|
8630
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 12.75V11.25" }),
|
|
8631
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 11.25C7.4 10.2 6.6 9.8 5.75 9.75" }),
|
|
8632
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M8 11.25C8.6 10.2 9.4 9.8 10.25 9.75" })
|
|
8633
|
+
]
|
|
8634
|
+
});
|
|
8635
|
+
}
|
|
8636
|
+
//#endregion
|
|
7826
8637
|
//#region src/client/useFlows.ts
|
|
7827
8638
|
/**
|
|
7828
|
-
*
|
|
8639
|
+
* React's view of a keyed registry of install-shaped flows (flow-registry.ts):
|
|
8640
|
+
* start, poll to terminal, reset.
|
|
7829
8641
|
*
|
|
7830
8642
|
* Install and uninstall are the same machine. Both fold a
|
|
7831
8643
|
* `ShopInstallStatusResult` through `reduceInstall` into an `InstallView`,
|
|
@@ -7838,108 +8650,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7838
8650
|
* install refusal is a `rejected` view carrying the gate's code; an uninstall
|
|
7839
8651
|
* refusal is a `failed` view carrying the host's published detail, because
|
|
7840
8652
|
* `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.
|
|
8653
|
+
* codes belong to the install gate. That difference is the registry's
|
|
8654
|
+
* `begin`, and it is the whole of it.
|
|
7843
8655
|
*
|
|
7844
8656
|
* The two used to be hand-synced copies — 71 of 84 lines identical. The cost
|
|
7845
8657
|
* was not theoretical: a single review round had to make the same edit twice
|
|
7846
8658
|
* (the settle signature, and its call), and the `pending` set below was added
|
|
7847
8659
|
* to one of them and not the other before this module existed.
|
|
8660
|
+
*
|
|
8661
|
+
* Since 2026-10-16 the registry is its own module, framework-free, and the
|
|
8662
|
+
* polling belongs to it: a component's effect stopped the moment the shop left
|
|
8663
|
+
* the screen (design 2026-10-16-shop-sidebar-page §5). This module only
|
|
8664
|
+
* renders one, through `useRegistryFlows`.
|
|
7848
8665
|
*/
|
|
7849
8666
|
/** Shared empty set, so an idle registry hands out one stable identity. */
|
|
7850
8667
|
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]);
|
|
8668
|
+
/** The keyed-flow face over any registry: one of the page store's, which
|
|
8669
|
+
* outlives the component, or one of the store a tab builds for itself when
|
|
8670
|
+
* none is handed over (ShopTab). */
|
|
8671
|
+
function useRegistryFlows(registry) {
|
|
8672
|
+
const { views, starting } = (0, react.useSyncExternalStore)(registry.subscribe, registry.getSnapshot);
|
|
8673
|
+
const start = (0, react.useCallback)((key, args) => registry.start(key, args), [registry]);
|
|
7905
8674
|
const reset = (0, react.useCallback)((key) => {
|
|
7906
|
-
|
|
7907
|
-
|
|
7908
|
-
const next = new Map(current);
|
|
7909
|
-
next.delete(key);
|
|
7910
|
-
return next;
|
|
7911
|
-
});
|
|
7912
|
-
}, []);
|
|
8675
|
+
registry.reset(key);
|
|
8676
|
+
}, [registry]);
|
|
7913
8677
|
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
|
-
]);
|
|
8678
|
+
registry.resetWhere(predicate);
|
|
8679
|
+
}, [registry]);
|
|
7943
8680
|
const pendingRef = (0, react.useRef)(NO_PENDING);
|
|
7944
8681
|
const pending = (0, react.useMemo)(() => {
|
|
7945
8682
|
const next = /* @__PURE__ */ new Set();
|
|
@@ -7981,154 +8718,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7981
8718
|
start,
|
|
7982
8719
|
reset
|
|
7983
8720
|
]),
|
|
7984
|
-
resetFlow: reset,
|
|
7985
8721
|
resetWhere,
|
|
7986
8722
|
pending
|
|
7987
8723
|
};
|
|
7988
8724
|
}
|
|
7989
8725
|
//#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
8726
|
//#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);
|
|
8727
|
+
/** The one key the self-update registry holds: the shop has one update. */
|
|
8728
|
+
const SELF_UPDATE_KEY = "self";
|
|
8729
|
+
/** The self-update face over a registry: the keyed-flow face
|
|
8730
|
+
* (`useRegistryFlows`) at its one key, so the subscription and the flow's
|
|
8731
|
+
* identity are that hook's and nothing here can drift from it. */
|
|
8732
|
+
function useRegistrySelf(registry) {
|
|
8733
|
+
const { flowFor, resetWhere } = useRegistryFlows(registry);
|
|
8734
|
+
const { view, start } = flowFor(SELF_UPDATE_KEY);
|
|
8132
8735
|
return {
|
|
8133
8736
|
view,
|
|
8134
8737
|
start,
|
|
@@ -9312,148 +9915,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9312
9915
|
};
|
|
9313
9916
|
}
|
|
9314
9917
|
//#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
9918
|
//#region src/client/FoldableRedBlock.tsx
|
|
9446
9919
|
function FoldableRedBlock({ children, t, attrName }) {
|
|
9447
9920
|
const { rootRef, expandable, expanded, toggle } = useFoldableRedBlock();
|
|
9448
9921
|
const kindName = attrName.replace(/^data-shop-/, "");
|
|
9449
9922
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9450
|
-
className: expanded ?
|
|
9923
|
+
className: expanded ? _dsh_plugin_shop_css_5f753521_default.foldBlockExpanded : _dsh_plugin_shop_css_5f753521_default.foldBlock,
|
|
9451
9924
|
[attrName]: true,
|
|
9452
9925
|
ref: rootRef,
|
|
9453
9926
|
children
|
|
9454
9927
|
}), expandable && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9455
9928
|
type: "button",
|
|
9456
|
-
className:
|
|
9929
|
+
className: _dsh_plugin_shop_css_5f753521_default.foldToggle,
|
|
9457
9930
|
"data-shop-fold": kindName,
|
|
9458
9931
|
"aria-expanded": expanded,
|
|
9459
9932
|
onClick: toggle,
|
|
@@ -9475,14 +9948,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9475
9948
|
* those nodes, and React's next commit throws on the ones it no longer finds
|
|
9476
9949
|
* (design 2026-09-26-market-borrowings §4). The tab renders no portals, so its
|
|
9477
9950
|
* root is its whole surface. */
|
|
9478
|
-
const PANEL_ROOT = `${
|
|
9951
|
+
const PANEL_ROOT = `${_dsh_plugin_shop_css_5f753521_default.panel} notranslate`;
|
|
9479
9952
|
function ChevronIcon({ open }) {
|
|
9480
9953
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
9481
9954
|
viewBox: "0 0 16 16",
|
|
9482
9955
|
width: "14",
|
|
9483
9956
|
height: "14",
|
|
9484
9957
|
"aria-hidden": "true",
|
|
9485
|
-
className: open ?
|
|
9958
|
+
className: open ? _dsh_plugin_shop_css_5f753521_default.chevronOpen : _dsh_plugin_shop_css_5f753521_default.chevron,
|
|
9486
9959
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", {
|
|
9487
9960
|
fill: "currentColor",
|
|
9488
9961
|
d: "M3.5 6 8 10.5 12.5 6l1 1L8 12.5 2.5 7l1-1Z"
|
|
@@ -9495,8 +9968,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9495
9968
|
* install controls. An installed plugin's card carries its installed row:
|
|
9496
9969
|
* current → the non-interactive installed label, behind → the update button;
|
|
9497
9970
|
* 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
|
|
9971
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, order, shelfDay, installed, missing, harness, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView, page }) {
|
|
9972
|
+
const key = entryKey(entry);
|
|
9973
|
+
const open = (0, react.useSyncExternalStore)(page.subscribeExpanded, () => page.isExpanded(key));
|
|
9974
|
+
const [, rerender] = (0, react.useReducer)((count) => count + 1, 0);
|
|
9500
9975
|
const blockers = stateBlockers(blockersOf(missing, harness, nameTakenBy), t);
|
|
9501
9976
|
const detailId = (0, react.useId)();
|
|
9502
9977
|
const summary = entry.catalog?.summary;
|
|
@@ -9514,32 +9989,35 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9514
9989
|
subdir: entry.subdir
|
|
9515
9990
|
};
|
|
9516
9991
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9517
|
-
className:
|
|
9992
|
+
className: _dsh_plugin_shop_css_5f753521_default.card,
|
|
9518
9993
|
"data-shop-entry": entry.name,
|
|
9519
9994
|
"data-category": category,
|
|
9520
9995
|
children: [
|
|
9521
9996
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9522
|
-
className:
|
|
9997
|
+
className: _dsh_plugin_shop_css_5f753521_default.cardSpine,
|
|
9523
9998
|
"aria-hidden": "true"
|
|
9524
9999
|
}),
|
|
9525
10000
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
9526
10001
|
type: "button",
|
|
9527
|
-
className:
|
|
10002
|
+
className: _dsh_plugin_shop_css_5f753521_default.entryHeader,
|
|
9528
10003
|
"aria-expanded": open,
|
|
9529
10004
|
"aria-controls": detailId,
|
|
9530
|
-
onClick: () =>
|
|
10005
|
+
onClick: () => {
|
|
10006
|
+
page.toggleExpanded(key);
|
|
10007
|
+
rerender();
|
|
10008
|
+
},
|
|
9531
10009
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9532
|
-
className:
|
|
10010
|
+
className: _dsh_plugin_shop_css_5f753521_default.name,
|
|
9533
10011
|
children: entry.name
|
|
9534
10012
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9535
|
-
className:
|
|
10013
|
+
className: _dsh_plugin_shop_css_5f753521_default.badges,
|
|
9536
10014
|
children: [
|
|
9537
10015
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9538
|
-
className:
|
|
10016
|
+
className: _dsh_plugin_shop_css_5f753521_default.categoryBadge,
|
|
9539
10017
|
children: t(categoryKey(entry))
|
|
9540
10018
|
}),
|
|
9541
10019
|
entry.source === "npm" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9542
|
-
className:
|
|
10020
|
+
className: _dsh_plugin_shop_css_5f753521_default.sourceBadge,
|
|
9543
10021
|
"data-shop-source-npm": true,
|
|
9544
10022
|
role: "img",
|
|
9545
10023
|
"aria-label": t("npmSource"),
|
|
@@ -9556,7 +10034,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9556
10034
|
})
|
|
9557
10035
|
}),
|
|
9558
10036
|
hasGithubHome(entry) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9559
|
-
className:
|
|
10037
|
+
className: _dsh_plugin_shop_css_5f753521_default.sourceBadge,
|
|
9560
10038
|
"data-shop-source-github": true,
|
|
9561
10039
|
role: "img",
|
|
9562
10040
|
"aria-label": t("githubSource"),
|
|
@@ -9570,7 +10048,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9570
10048
|
})
|
|
9571
10049
|
}),
|
|
9572
10050
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9573
|
-
className:
|
|
10051
|
+
className: _dsh_plugin_shop_css_5f753521_default.cardVersion,
|
|
9574
10052
|
"data-card-version": true,
|
|
9575
10053
|
role: "img",
|
|
9576
10054
|
"aria-label": versionName,
|
|
@@ -9578,17 +10056,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9578
10056
|
children: version.text
|
|
9579
10057
|
}),
|
|
9580
10058
|
entry.tier !== "community" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9581
|
-
className:
|
|
10059
|
+
className: _dsh_plugin_shop_css_5f753521_default.tierBadge,
|
|
9582
10060
|
"data-tier": entry.tier,
|
|
9583
10061
|
children: t(tierKey(entry.tier))
|
|
9584
10062
|
}),
|
|
9585
10063
|
entry.archived === true && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9586
|
-
className:
|
|
10064
|
+
className: _dsh_plugin_shop_css_5f753521_default.tierBadge,
|
|
9587
10065
|
"data-tier": "archived",
|
|
9588
10066
|
children: t("archivedBadge")
|
|
9589
10067
|
}),
|
|
9590
10068
|
stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9591
|
-
className:
|
|
10069
|
+
className: _dsh_plugin_shop_css_5f753521_default.starsBadge,
|
|
9592
10070
|
role: "img",
|
|
9593
10071
|
"aria-label": t("stars", { count: stars }),
|
|
9594
10072
|
children: ["★ ", formatStars(stars)]
|
|
@@ -9598,14 +10076,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9598
10076
|
})]
|
|
9599
10077
|
}),
|
|
9600
10078
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9601
|
-
className:
|
|
10079
|
+
className: _dsh_plugin_shop_css_5f753521_default.body,
|
|
9602
10080
|
children: [
|
|
9603
10081
|
summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9604
|
-
className: open ? `${
|
|
10082
|
+
className: open ? `${_dsh_plugin_shop_css_5f753521_default.summary} ${_dsh_plugin_shop_css_5f753521_default.summaryExpanded}` : _dsh_plugin_shop_css_5f753521_default.summary,
|
|
9605
10083
|
children: summary.en
|
|
9606
10084
|
}),
|
|
9607
10085
|
summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9608
|
-
className: open ? `${
|
|
10086
|
+
className: open ? `${_dsh_plugin_shop_css_5f753521_default.summaryZh} ${_dsh_plugin_shop_css_5f753521_default.summaryZhExpanded}` : _dsh_plugin_shop_css_5f753521_default.summaryZh,
|
|
9609
10087
|
children: summary.zh
|
|
9610
10088
|
}),
|
|
9611
10089
|
blockers.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(FoldableRedBlock, {
|
|
@@ -9613,28 +10091,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9613
10091
|
attrName: "data-shop-remedy",
|
|
9614
10092
|
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
9615
10093
|
blocker,
|
|
9616
|
-
className:
|
|
10094
|
+
className: _dsh_plugin_shop_css_5f753521_default.incompatibleDetail,
|
|
9617
10095
|
surface: "detail"
|
|
9618
10096
|
}, blocker.kind))
|
|
9619
10097
|
}),
|
|
9620
10098
|
open && entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9621
|
-
className:
|
|
10099
|
+
className: _dsh_plugin_shop_css_5f753521_default.capabilitiesBlock,
|
|
9622
10100
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9623
|
-
className:
|
|
10101
|
+
className: _dsh_plugin_shop_css_5f753521_default.capabilitiesNote,
|
|
9624
10102
|
children: t("capabilitiesNote")
|
|
9625
10103
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
9626
|
-
className:
|
|
10104
|
+
className: _dsh_plugin_shop_css_5f753521_default.capabilities,
|
|
9627
10105
|
children: entry.catalog.capabilities.map((capability) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: capability }, capability))
|
|
9628
10106
|
})]
|
|
9629
10107
|
}),
|
|
9630
10108
|
open && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
9631
10109
|
id: detailId,
|
|
9632
|
-
className:
|
|
10110
|
+
className: _dsh_plugin_shop_css_5f753521_default.detail,
|
|
9633
10111
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
|
|
9634
|
-
className:
|
|
10112
|
+
className: _dsh_plugin_shop_css_5f753521_default.detailRows,
|
|
9635
10113
|
children: [
|
|
9636
10114
|
npmUrl !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9637
|
-
className:
|
|
10115
|
+
className: _dsh_plugin_shop_css_5f753521_default.detailRow,
|
|
9638
10116
|
"data-shop-npm": true,
|
|
9639
10117
|
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
10118
|
href: npmUrl,
|
|
@@ -9644,7 +10122,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9644
10122
|
}) })]
|
|
9645
10123
|
}),
|
|
9646
10124
|
entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9647
|
-
className:
|
|
10125
|
+
className: _dsh_plugin_shop_css_5f753521_default.detailRow,
|
|
9648
10126
|
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
10127
|
href: entry.repository,
|
|
9650
10128
|
target: "_blank",
|
|
@@ -9653,12 +10131,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9653
10131
|
}) : entry.repository })]
|
|
9654
10132
|
}),
|
|
9655
10133
|
entry.license !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9656
|
-
className:
|
|
10134
|
+
className: _dsh_plugin_shop_css_5f753521_default.detailRow,
|
|
9657
10135
|
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
10136
|
})
|
|
9659
10137
|
]
|
|
9660
10138
|
}), entry.tier === "verified-stale" && entry.review !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9661
|
-
className:
|
|
10139
|
+
className: _dsh_plugin_shop_css_5f753521_default.reviewedLine,
|
|
9662
10140
|
children: entry.review.reviewedVersion !== void 0 ? t("reviewedVersionLine", {
|
|
9663
10141
|
reviewed: versionLabel("npm", entry.review.reviewedVersion).text,
|
|
9664
10142
|
current: version.text
|
|
@@ -9671,7 +10149,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9671
10149
|
]
|
|
9672
10150
|
}),
|
|
9673
10151
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9674
|
-
className:
|
|
10152
|
+
className: _dsh_plugin_shop_css_5f753521_default.cardActions,
|
|
9675
10153
|
"data-shop-actions": true,
|
|
9676
10154
|
children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [!inInstalledView && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
9677
10155
|
target: installTarget,
|
|
@@ -9703,7 +10181,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9703
10181
|
restartBlocked,
|
|
9704
10182
|
reload
|
|
9705
10183
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9706
|
-
className:
|
|
10184
|
+
className: _dsh_plugin_shop_css_5f753521_default.installedLabel,
|
|
9707
10185
|
"data-shop-installed": true,
|
|
9708
10186
|
children: t("installed")
|
|
9709
10187
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerBadge, {
|
|
@@ -9712,12 +10190,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9712
10190
|
})] }),
|
|
9713
10191
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
9714
10192
|
row: installed,
|
|
10193
|
+
surface: "card",
|
|
10194
|
+
page,
|
|
9715
10195
|
t,
|
|
9716
10196
|
setEnabled,
|
|
9717
10197
|
restart,
|
|
9718
10198
|
restartBlocked,
|
|
9719
|
-
reload
|
|
9720
|
-
restarts
|
|
10199
|
+
reload
|
|
9721
10200
|
}),
|
|
9722
10201
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
|
|
9723
10202
|
name: entry.name,
|
|
@@ -9728,15 +10207,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9728
10207
|
flow: uninstallFlow
|
|
9729
10208
|
})
|
|
9730
10209
|
] }), (size !== void 0 || author !== null || shelfDay !== void 0) && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
9731
|
-
className:
|
|
10210
|
+
className: _dsh_plugin_shop_css_5f753521_default.cardMeta,
|
|
9732
10211
|
children: [
|
|
9733
10212
|
shelfDay !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9734
|
-
className:
|
|
10213
|
+
className: _dsh_plugin_shop_css_5f753521_default.shelfDate,
|
|
9735
10214
|
"data-shop-shelf-date": true,
|
|
9736
10215
|
children: t(order === "added" ? "listedOn" : "updatedOn", { date: shelfDay })
|
|
9737
10216
|
}),
|
|
9738
10217
|
size !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9739
|
-
className:
|
|
10218
|
+
className: _dsh_plugin_shop_css_5f753521_default.size,
|
|
9740
10219
|
"data-shop-size": true,
|
|
9741
10220
|
role: "img",
|
|
9742
10221
|
"aria-label": t("sizeLabel", { size }),
|
|
@@ -9744,7 +10223,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9744
10223
|
children: size
|
|
9745
10224
|
}),
|
|
9746
10225
|
author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9747
|
-
className:
|
|
10226
|
+
className: _dsh_plugin_shop_css_5f753521_default.author,
|
|
9748
10227
|
"data-shop-author": true,
|
|
9749
10228
|
children: t("authorLine", { author })
|
|
9750
10229
|
})
|
|
@@ -9794,13 +10273,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9794
10273
|
...attribute,
|
|
9795
10274
|
children: blocker.text
|
|
9796
10275
|
}), blocker.remedy !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9797
|
-
className:
|
|
10276
|
+
className: _dsh_plugin_shop_css_5f753521_default.remedy,
|
|
9798
10277
|
"data-shop-remedy": blocker.kind,
|
|
9799
10278
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9800
|
-
className:
|
|
10279
|
+
className: _dsh_plugin_shop_css_5f753521_default.remedyLead,
|
|
9801
10280
|
children: blocker.remedy.lead
|
|
9802
10281
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
|
|
9803
|
-
className:
|
|
10282
|
+
className: _dsh_plugin_shop_css_5f753521_default.allowCommand,
|
|
9804
10283
|
"data-shop-allow-command": true,
|
|
9805
10284
|
children: blocker.remedy.command
|
|
9806
10285
|
})]
|
|
@@ -9838,7 +10317,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9838
10317
|
if (lead === void 0 || word === null) return null;
|
|
9839
10318
|
const detail = blockers.map((blocker) => blocker.text).join("\n");
|
|
9840
10319
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
9841
|
-
className:
|
|
10320
|
+
className: _dsh_plugin_shop_css_5f753521_default.incompatibleBadge,
|
|
9842
10321
|
"data-shop-blocker": lead.kind,
|
|
9843
10322
|
role: "img",
|
|
9844
10323
|
"aria-label": detail,
|
|
@@ -9848,51 +10327,59 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9848
10327
|
}
|
|
9849
10328
|
/** One entry's install flow: the button (or the refused LABEL that stands in
|
|
9850
10329
|
* 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
|
|
10330
|
+
* rejection detail — driven by the page store's install registry. Shared by
|
|
10331
|
+
* the catalog cards (`variant: 'install'`) and the outdated rows' update
|
|
10332
|
+
* button (`variant: 'update'`, which drives the same install flow for
|
|
10333
|
+
* `name@latest`).
|
|
9854
10334
|
*
|
|
9855
10335
|
* There is no §9.3 dialog. Until 2026-10-14 a community entry opened an
|
|
9856
10336
|
* acknowledgement gate first, and that gate was also the only thing standing
|
|
9857
10337
|
* between a second press and a second RPC — it reopened instead of asking
|
|
9858
10338
|
* 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
|
|
10339
|
+
* the flow registry's in-flight guard (flow-registry.ts, `starting`), which
|
|
10340
|
+
* keeps a second press from becoming a second RPC. The page states the
|
|
10341
|
+
* dialog's facts nowhere since 2026-10-15 (the shop design's §7 amendment of
|
|
10342
|
+
* that date). */
|
|
9862
10343
|
function InstallPanel({ target, tier, blockers, blockersStated = false, variant = "install", flow, t, restart, restartBlocked, reload }) {
|
|
9863
|
-
const { view, start, starting } = flow;
|
|
10344
|
+
const { view, start, starting, reset } = flow;
|
|
10345
|
+
const request = {
|
|
10346
|
+
...target,
|
|
10347
|
+
acknowledged: tier === "verified" ? void 0 : true
|
|
10348
|
+
};
|
|
10349
|
+
const refused = refusesInstall(blockers);
|
|
10350
|
+
const word = blockerBadgeKey(blockers);
|
|
9864
10351
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9865
|
-
className:
|
|
10352
|
+
className: _dsh_plugin_shop_css_5f753521_default.installPanel,
|
|
9866
10353
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9867
|
-
className:
|
|
10354
|
+
className: _dsh_plugin_shop_css_5f753521_default.installing,
|
|
9868
10355
|
children: t(installPhaseKey(view.phase))
|
|
9869
10356
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9870
|
-
className:
|
|
10357
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
9871
10358
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9872
|
-
className:
|
|
10359
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
9873
10360
|
"data-shop-log-line": true,
|
|
9874
10361
|
children: line
|
|
9875
10362
|
}, index))
|
|
9876
10363
|
})]
|
|
9877
10364
|
});
|
|
9878
10365
|
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9879
|
-
className:
|
|
10366
|
+
className: _dsh_plugin_shop_css_5f753521_default.installedActions,
|
|
9880
10367
|
children: [
|
|
9881
10368
|
view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9882
|
-
className:
|
|
10369
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
9883
10370
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9884
|
-
className:
|
|
10371
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
9885
10372
|
"data-shop-log-line": true,
|
|
9886
10373
|
children: line
|
|
9887
10374
|
}, index))
|
|
9888
10375
|
}),
|
|
9889
10376
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9890
|
-
className:
|
|
10377
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
9891
10378
|
"data-shop-restart-notice": true,
|
|
9892
10379
|
children: t(activationNoticeKey(view.activation, view.restartReason))
|
|
9893
10380
|
}),
|
|
9894
10381
|
view.detail !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9895
|
-
className:
|
|
10382
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
9896
10383
|
"data-shop-done-note": true,
|
|
9897
10384
|
children: view.detail
|
|
9898
10385
|
}),
|
|
@@ -9903,46 +10390,77 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9903
10390
|
restartBlocked,
|
|
9904
10391
|
reload,
|
|
9905
10392
|
where: "install"
|
|
10393
|
+
}),
|
|
10394
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10395
|
+
type: "button",
|
|
10396
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
10397
|
+
"data-shop-install-dismiss": true,
|
|
10398
|
+
onClick: reset,
|
|
10399
|
+
children: t("dismiss")
|
|
9906
10400
|
})
|
|
9907
10401
|
]
|
|
9908
10402
|
});
|
|
9909
10403
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9910
|
-
className:
|
|
10404
|
+
className: _dsh_plugin_shop_css_5f753521_default.installPanel,
|
|
9911
10405
|
children: [
|
|
9912
10406
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9913
|
-
className:
|
|
10407
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedHeading,
|
|
9914
10408
|
children: t("installFailed")
|
|
9915
10409
|
}),
|
|
9916
10410
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9917
|
-
className:
|
|
10411
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
9918
10412
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
9919
|
-
className:
|
|
10413
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
9920
10414
|
"data-shop-log-line": true,
|
|
9921
10415
|
children: line
|
|
9922
10416
|
}, index))
|
|
9923
10417
|
}),
|
|
9924
10418
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
9925
|
-
className:
|
|
10419
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
9926
10420
|
children: view.detail === "" ? t("installTransportFailed") : view.detail
|
|
10421
|
+
}),
|
|
10422
|
+
refused && word !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10423
|
+
type: "button",
|
|
10424
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
10425
|
+
"data-shop-install-dismiss": true,
|
|
10426
|
+
onClick: reset,
|
|
10427
|
+
children: t("dismiss")
|
|
10428
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10429
|
+
type: "button",
|
|
10430
|
+
className: _dsh_plugin_shop_css_5f753521_default.installButton,
|
|
10431
|
+
"data-shop-install-retry": true,
|
|
10432
|
+
disabled: starting,
|
|
10433
|
+
onClick: () => {
|
|
10434
|
+
start(request);
|
|
10435
|
+
},
|
|
10436
|
+
children: t("retry")
|
|
9927
10437
|
})
|
|
9928
10438
|
]
|
|
9929
10439
|
});
|
|
9930
10440
|
if (view.kind === "rejected") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9931
|
-
className:
|
|
9932
|
-
children: [
|
|
9933
|
-
|
|
9934
|
-
|
|
9935
|
-
|
|
9936
|
-
|
|
9937
|
-
|
|
9938
|
-
|
|
10441
|
+
className: _dsh_plugin_shop_css_5f753521_default.installPanel,
|
|
10442
|
+
children: [
|
|
10443
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10444
|
+
className: _dsh_plugin_shop_css_5f753521_default.rejectedCode,
|
|
10445
|
+
children: t(rejectionCodeKey(view.code))
|
|
10446
|
+
}),
|
|
10447
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10448
|
+
className: _dsh_plugin_shop_css_5f753521_default.rejectedDetail,
|
|
10449
|
+
children: view.detail
|
|
10450
|
+
}),
|
|
10451
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10452
|
+
type: "button",
|
|
10453
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
10454
|
+
"data-shop-install-dismiss": true,
|
|
10455
|
+
onClick: reset,
|
|
10456
|
+
children: t("dismiss")
|
|
10457
|
+
})
|
|
10458
|
+
]
|
|
9939
10459
|
});
|
|
9940
10460
|
const update = variant === "update";
|
|
9941
|
-
const refused = refusesInstall(blockers);
|
|
9942
|
-
const word = blockerBadgeKey(blockers);
|
|
9943
10461
|
const detail = blockers.map((blocker) => blocker.text).join("\n");
|
|
9944
10462
|
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:
|
|
10463
|
+
className: _dsh_plugin_shop_css_5f753521_default.refusedLabel,
|
|
9946
10464
|
"data-shop-refused": true,
|
|
9947
10465
|
"data-shop-blocker-kind": blockers[0]?.kind,
|
|
9948
10466
|
role: "img",
|
|
@@ -9954,21 +10472,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9954
10472
|
attrName: "data-shop-refusal",
|
|
9955
10473
|
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
9956
10474
|
blocker,
|
|
9957
|
-
className:
|
|
10475
|
+
className: _dsh_plugin_shop_css_5f753521_default.gateWarning,
|
|
9958
10476
|
surface: "warning"
|
|
9959
10477
|
}, blocker.kind))
|
|
9960
10478
|
})] });
|
|
9961
10479
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
9962
10480
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
9963
10481
|
type: "button",
|
|
9964
|
-
className:
|
|
10482
|
+
className: _dsh_plugin_shop_css_5f753521_default.installButton,
|
|
9965
10483
|
disabled: starting,
|
|
9966
10484
|
...update ? { "data-shop-update": true } : { "data-shop-install": true },
|
|
9967
10485
|
onClick: () => {
|
|
9968
|
-
start(
|
|
9969
|
-
...target,
|
|
9970
|
-
acknowledged: tier === "verified" ? void 0 : true
|
|
9971
|
-
});
|
|
10486
|
+
start(request);
|
|
9972
10487
|
},
|
|
9973
10488
|
children: t(update ? "update" : "install")
|
|
9974
10489
|
}),
|
|
@@ -9981,7 +10496,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9981
10496
|
attrName: "data-shop-blocker-lines",
|
|
9982
10497
|
children: blockers.map((blocker) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(BlockerLine, {
|
|
9983
10498
|
blocker,
|
|
9984
|
-
className:
|
|
10499
|
+
className: _dsh_plugin_shop_css_5f753521_default.gateWarning,
|
|
9985
10500
|
surface: "warning"
|
|
9986
10501
|
}, blocker.kind))
|
|
9987
10502
|
})
|
|
@@ -9996,32 +10511,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
9996
10511
|
function UninstallPanel({ name, t, restart, restartBlocked, reload, flow }) {
|
|
9997
10512
|
const { view, start, reset } = flow;
|
|
9998
10513
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
9999
|
-
className:
|
|
10514
|
+
className: _dsh_plugin_shop_css_5f753521_default.installPanel,
|
|
10000
10515
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10001
|
-
className:
|
|
10516
|
+
className: _dsh_plugin_shop_css_5f753521_default.installing,
|
|
10002
10517
|
children: t("uninstalling")
|
|
10003
10518
|
}), view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10004
|
-
className:
|
|
10519
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
10005
10520
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10006
|
-
className:
|
|
10521
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
10007
10522
|
"data-shop-log-line": true,
|
|
10008
10523
|
children: line
|
|
10009
10524
|
}, index))
|
|
10010
10525
|
})]
|
|
10011
10526
|
});
|
|
10012
10527
|
if (view.kind === "done") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10013
|
-
className:
|
|
10528
|
+
className: _dsh_plugin_shop_css_5f753521_default.installedActions,
|
|
10014
10529
|
children: [
|
|
10015
10530
|
view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10016
|
-
className:
|
|
10531
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
10017
10532
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10018
|
-
className:
|
|
10533
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
10019
10534
|
"data-shop-log-line": true,
|
|
10020
10535
|
children: line
|
|
10021
10536
|
}, index))
|
|
10022
10537
|
}),
|
|
10023
10538
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10024
|
-
className:
|
|
10539
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10025
10540
|
"data-shop-uninstall-done": true,
|
|
10026
10541
|
children: t(uninstallActivationNoticeKey(view.activation))
|
|
10027
10542
|
}),
|
|
@@ -10035,7 +10550,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10035
10550
|
}),
|
|
10036
10551
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10037
10552
|
type: "button",
|
|
10038
|
-
className:
|
|
10553
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
10039
10554
|
"data-shop-uninstall-dismiss": true,
|
|
10040
10555
|
onClick: reset,
|
|
10041
10556
|
children: t("dismiss")
|
|
@@ -10043,37 +10558,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10043
10558
|
]
|
|
10044
10559
|
});
|
|
10045
10560
|
if (view.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10046
|
-
className:
|
|
10561
|
+
className: _dsh_plugin_shop_css_5f753521_default.installPanel,
|
|
10047
10562
|
children: [
|
|
10048
10563
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10049
|
-
className:
|
|
10564
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedHeading,
|
|
10050
10565
|
children: t("uninstallFailed")
|
|
10051
10566
|
}),
|
|
10052
10567
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10053
|
-
className:
|
|
10568
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
10054
10569
|
children: view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10055
|
-
className:
|
|
10570
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
10056
10571
|
"data-shop-log-line": true,
|
|
10057
10572
|
children: line
|
|
10058
10573
|
}, index))
|
|
10059
10574
|
}),
|
|
10060
10575
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10061
|
-
className:
|
|
10576
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10062
10577
|
children: view.detail === "" ? t("uninstallTransportFailed") : view.detail
|
|
10063
10578
|
}),
|
|
10064
10579
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10065
10580
|
type: "button",
|
|
10066
|
-
className:
|
|
10581
|
+
className: _dsh_plugin_shop_css_5f753521_default.uninstallButton,
|
|
10067
10582
|
"data-shop-uninstall-retry": true,
|
|
10068
10583
|
onClick: () => void start({ name }),
|
|
10069
10584
|
children: t("retry")
|
|
10585
|
+
}),
|
|
10586
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10587
|
+
type: "button",
|
|
10588
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
10589
|
+
"data-shop-uninstall-dismiss": true,
|
|
10590
|
+
onClick: reset,
|
|
10591
|
+
children: t("dismiss")
|
|
10070
10592
|
})
|
|
10071
10593
|
]
|
|
10072
10594
|
});
|
|
10073
10595
|
if (view.kind !== "idle") return null;
|
|
10074
10596
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10075
10597
|
type: "button",
|
|
10076
|
-
className:
|
|
10598
|
+
className: _dsh_plugin_shop_css_5f753521_default.uninstallButton,
|
|
10077
10599
|
"data-shop-uninstall": true,
|
|
10078
10600
|
onClick: () => void start({ name }),
|
|
10079
10601
|
children: t("uninstall")
|
|
@@ -10114,39 +10636,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10114
10636
|
else if (outcome.kind === "unreachable") setRefusal(t("restartTransportFailed"));
|
|
10115
10637
|
};
|
|
10116
10638
|
if (monitor?.kind === "requesting" || monitor?.kind === "restarting") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10117
|
-
className:
|
|
10639
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10118
10640
|
"data-shop-restarting": true,
|
|
10119
10641
|
children: t("restarting")
|
|
10120
10642
|
});
|
|
10121
10643
|
if (monitor?.kind === "failed") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10122
|
-
className:
|
|
10644
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10123
10645
|
"data-shop-restart-error": true,
|
|
10124
10646
|
children: monitor.logFile === void 0 ? t("restartFailedNotice") : t("restartFailedLogNotice", { log: monitor.logFile })
|
|
10125
10647
|
});
|
|
10126
10648
|
if (monitor?.kind === "not-restarted") return null;
|
|
10127
10649
|
if (gateOpen) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10128
|
-
className:
|
|
10650
|
+
className: _dsh_plugin_shop_css_5f753521_default.gate,
|
|
10129
10651
|
"data-shop-restart-gate": true,
|
|
10130
10652
|
children: [
|
|
10131
10653
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10132
|
-
className:
|
|
10654
|
+
className: _dsh_plugin_shop_css_5f753521_default.gateTitle,
|
|
10133
10655
|
children: t("restartTitle")
|
|
10134
10656
|
}),
|
|
10135
10657
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10136
|
-
className:
|
|
10658
|
+
className: _dsh_plugin_shop_css_5f753521_default.gateBody,
|
|
10137
10659
|
children: t("restartBody")
|
|
10138
10660
|
}),
|
|
10139
10661
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10140
|
-
className:
|
|
10662
|
+
className: _dsh_plugin_shop_css_5f753521_default.gateActions,
|
|
10141
10663
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10142
10664
|
type: "button",
|
|
10143
|
-
className:
|
|
10665
|
+
className: _dsh_plugin_shop_css_5f753521_default.confirmButton,
|
|
10144
10666
|
"data-shop-restart-confirm": true,
|
|
10145
10667
|
onClick: () => void onConfirm(),
|
|
10146
10668
|
children: t("restartConfirm")
|
|
10147
10669
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10148
10670
|
type: "button",
|
|
10149
|
-
className:
|
|
10671
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
10150
10672
|
onClick: () => setGateOpen(false),
|
|
10151
10673
|
children: t("cancel")
|
|
10152
10674
|
})]
|
|
@@ -10154,14 +10676,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10154
10676
|
]
|
|
10155
10677
|
});
|
|
10156
10678
|
const refused = refusal === null ? null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10157
|
-
className:
|
|
10679
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10158
10680
|
"data-shop-restart-error": true,
|
|
10159
10681
|
children: refusal
|
|
10160
10682
|
});
|
|
10161
10683
|
if (gate !== void 0) return refused;
|
|
10162
10684
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [refused, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10163
10685
|
type: "button",
|
|
10164
|
-
className:
|
|
10686
|
+
className: _dsh_plugin_shop_css_5f753521_default.restartButton,
|
|
10165
10687
|
"data-shop-restart": true,
|
|
10166
10688
|
onClick: () => setGateOpen(true),
|
|
10167
10689
|
children: t("restart")
|
|
@@ -10179,13 +10701,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10179
10701
|
*/
|
|
10180
10702
|
function NotRestartedNotice({ t, pid, logFile }) {
|
|
10181
10703
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10182
|
-
className:
|
|
10704
|
+
className: _dsh_plugin_shop_css_5f753521_default.selfUpdatePanel,
|
|
10183
10705
|
"data-shop-not-restarted": true,
|
|
10184
10706
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10185
|
-
className:
|
|
10707
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10186
10708
|
children: pid === void 0 ? t("notRestartedNotice") : t("notRestartedPidNotice", { pid: String(pid) })
|
|
10187
10709
|
}), logFile !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10188
|
-
className:
|
|
10710
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10189
10711
|
children: t("notRestartedLogNotice", { log: logFile })
|
|
10190
10712
|
})]
|
|
10191
10713
|
});
|
|
@@ -10215,7 +10737,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10215
10737
|
restart,
|
|
10216
10738
|
reload
|
|
10217
10739
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10218
|
-
className:
|
|
10740
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10219
10741
|
"data-shop-restart-disabled": true,
|
|
10220
10742
|
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
10221
10743
|
});
|
|
@@ -10226,14 +10748,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10226
10748
|
* would help without knowing that now is a good time. */
|
|
10227
10749
|
function ReloadPanel({ t, reload, where }) {
|
|
10228
10750
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10229
|
-
className:
|
|
10751
|
+
className: _dsh_plugin_shop_css_5f753521_default.reloadPanel,
|
|
10230
10752
|
"data-shop-reload": where,
|
|
10231
10753
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10232
|
-
className:
|
|
10754
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10233
10755
|
children: t("reloadNote")
|
|
10234
10756
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10235
10757
|
type: "button",
|
|
10236
|
-
className:
|
|
10758
|
+
className: _dsh_plugin_shop_css_5f753521_default.reloadButton,
|
|
10237
10759
|
onClick: reload,
|
|
10238
10760
|
children: t("reload")
|
|
10239
10761
|
})]
|
|
@@ -10246,21 +10768,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10246
10768
|
* transport throw renders the localized failure line — its private detail
|
|
10247
10769
|
* (which can name hosts and ports) never reaches the UI.
|
|
10248
10770
|
*/
|
|
10249
|
-
function EnabledSwitch({ row, t, setEnabled, restart, restartBlocked, reload
|
|
10771
|
+
function EnabledSwitch({ row, surface, page, t, setEnabled, restart, restartBlocked, reload }) {
|
|
10250
10772
|
const [enabled, setEnabledState] = (0, react.useState)(row.enabled);
|
|
10251
10773
|
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]);
|
|
10774
|
+
const owedKey = switchKey(surface, identityKey(row));
|
|
10775
|
+
const owed = (0, react.useSyncExternalStore)(page.subscribeOwed, () => page.getOwed(owedKey) ?? "nothing");
|
|
10261
10776
|
const onToggle = async () => {
|
|
10262
10777
|
if (toggle.kind === "saving") return;
|
|
10263
10778
|
const next = !enabled;
|
|
10779
|
+
const since = page.owedEpoch();
|
|
10264
10780
|
setToggle({ kind: "saving" });
|
|
10265
10781
|
try {
|
|
10266
10782
|
const result = await setEnabled({
|
|
@@ -10269,9 +10785,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10269
10785
|
});
|
|
10270
10786
|
if (result.ok) {
|
|
10271
10787
|
setEnabledState(next);
|
|
10272
|
-
setToggle({
|
|
10273
|
-
|
|
10274
|
-
|
|
10788
|
+
setToggle({
|
|
10789
|
+
kind: "saved",
|
|
10790
|
+
live: result.activation === "live"
|
|
10791
|
+
});
|
|
10792
|
+
if (result.activation === "restart") page.setOwed(owedKey, "restart", since);
|
|
10793
|
+
else if (result.activation !== "live") page.setOwed(owedKey, "reload", since);
|
|
10275
10794
|
} else setToggle({
|
|
10276
10795
|
kind: "error",
|
|
10277
10796
|
detail: result.detail || t("toggleFailed")
|
|
@@ -10284,7 +10803,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10284
10803
|
}
|
|
10285
10804
|
};
|
|
10286
10805
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10287
|
-
className:
|
|
10806
|
+
className: _dsh_plugin_shop_css_5f753521_default.switchWrap,
|
|
10288
10807
|
"data-shop-enabled-switch": row.name,
|
|
10289
10808
|
children: [
|
|
10290
10809
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
@@ -10293,18 +10812,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10293
10812
|
"aria-checked": enabled,
|
|
10294
10813
|
"aria-label": t("enabledSwitch"),
|
|
10295
10814
|
"data-shop-toggle": true,
|
|
10296
|
-
className: `${
|
|
10815
|
+
className: `${_dsh_plugin_shop_css_5f753521_default.switch} ${enabled ? _dsh_plugin_shop_css_5f753521_default.switchOn : ""}`,
|
|
10297
10816
|
onClick: () => void onToggle(),
|
|
10298
10817
|
disabled: toggle.kind === "saving",
|
|
10299
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
10818
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_5f753521_default.switchKnob })
|
|
10300
10819
|
}),
|
|
10301
|
-
toggle.kind === "saved" && owed === "nothing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10302
|
-
className:
|
|
10820
|
+
toggle.kind === "saved" && toggle.live && owed === "nothing" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10821
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10303
10822
|
"data-shop-hot-apply": true,
|
|
10304
10823
|
children: t("hotApplyNote")
|
|
10305
10824
|
}),
|
|
10306
10825
|
owed === "restart" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10307
|
-
className:
|
|
10826
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10308
10827
|
"data-shop-toggle-restart": true,
|
|
10309
10828
|
children: t("toggleRestartNote")
|
|
10310
10829
|
}),
|
|
@@ -10317,7 +10836,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10317
10836
|
where: "toggle"
|
|
10318
10837
|
}),
|
|
10319
10838
|
toggle.kind === "error" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10320
|
-
className:
|
|
10839
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10321
10840
|
"data-shop-toggle-error": true,
|
|
10322
10841
|
children: toggle.detail
|
|
10323
10842
|
})
|
|
@@ -10330,29 +10849,30 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10330
10849
|
* carry a `latest` string — a catalog truth to compare against; §C10's
|
|
10331
10850
|
* shadowed rows hold `latest: null` and never reach this control, so the
|
|
10332
10851
|
* field is narrowed at the parameter. */
|
|
10333
|
-
function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload,
|
|
10852
|
+
function OutdatedRow({ row, tier, missing, harness, t, setEnabled, flowFor, restart, restartBlocked, reload, page }) {
|
|
10334
10853
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10335
|
-
className:
|
|
10854
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedRow,
|
|
10336
10855
|
"data-shop-outdated-entry": row.name,
|
|
10337
10856
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10338
|
-
className:
|
|
10857
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedInfo,
|
|
10339
10858
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10340
|
-
className:
|
|
10859
|
+
className: _dsh_plugin_shop_css_5f753521_default.name,
|
|
10341
10860
|
children: row.name
|
|
10342
10861
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
10343
|
-
className:
|
|
10862
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedVersions,
|
|
10344
10863
|
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
10864
|
})]
|
|
10346
10865
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10347
|
-
className:
|
|
10866
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedActions,
|
|
10348
10867
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
10349
10868
|
row,
|
|
10869
|
+
surface: "row",
|
|
10870
|
+
page,
|
|
10350
10871
|
t,
|
|
10351
10872
|
setEnabled,
|
|
10352
10873
|
restart,
|
|
10353
10874
|
restartBlocked,
|
|
10354
|
-
reload
|
|
10355
|
-
restarts
|
|
10875
|
+
reload
|
|
10356
10876
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
10357
10877
|
target: {
|
|
10358
10878
|
name: row.name,
|
|
@@ -10392,25 +10912,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10392
10912
|
* community entry's does); an entry absent from the catalog is read as
|
|
10393
10913
|
* community, so its request acknowledges, which the host accepts for any
|
|
10394
10914
|
* tier. */
|
|
10395
|
-
function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload,
|
|
10915
|
+
function OutdatedSection({ state, entriesByKey, missingByKey, harnessVerdicts, t, setEnabled, flowFor, restart, restartBlocked, reload, page }) {
|
|
10396
10916
|
if (state.kind === "loading") return null;
|
|
10397
10917
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10398
|
-
className:
|
|
10918
|
+
className: _dsh_plugin_shop_css_5f753521_default.stateLine,
|
|
10399
10919
|
"data-shop-outdated-error": true,
|
|
10400
10920
|
children: t("installedError")
|
|
10401
10921
|
});
|
|
10402
10922
|
const outdated = state.entries.filter((entry) => entry.outdated && entry.latest !== null);
|
|
10403
10923
|
if (outdated.length === 0) return null;
|
|
10404
10924
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
10405
|
-
className:
|
|
10925
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedSection,
|
|
10406
10926
|
"data-shop-outdated": true,
|
|
10407
10927
|
"aria-labelledby": UPDATABLE_HEADING_ID,
|
|
10408
10928
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
10409
|
-
className:
|
|
10929
|
+
className: _dsh_plugin_shop_css_5f753521_default.updatableHeading,
|
|
10410
10930
|
id: UPDATABLE_HEADING_ID,
|
|
10411
10931
|
children: t("updatableSection")
|
|
10412
10932
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
10413
|
-
className:
|
|
10933
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedList,
|
|
10414
10934
|
children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
|
|
10415
10935
|
row,
|
|
10416
10936
|
tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
|
|
@@ -10422,7 +10942,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10422
10942
|
restart,
|
|
10423
10943
|
restartBlocked,
|
|
10424
10944
|
reload,
|
|
10425
|
-
|
|
10945
|
+
page
|
|
10426
10946
|
}) }, identityKey(row)))
|
|
10427
10947
|
})]
|
|
10428
10948
|
});
|
|
@@ -10438,37 +10958,37 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10438
10958
|
const shadowed = entries.filter((entry) => entry.shadowed === true);
|
|
10439
10959
|
if (shadowed.length === 0) return null;
|
|
10440
10960
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
10441
|
-
className:
|
|
10961
|
+
className: _dsh_plugin_shop_css_5f753521_default.shadowedSection,
|
|
10442
10962
|
"data-shop-shadowed": true,
|
|
10443
10963
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
10444
|
-
className:
|
|
10964
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedList,
|
|
10445
10965
|
children: shadowed.map((row) => {
|
|
10446
10966
|
const key = identityKey(row);
|
|
10447
10967
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10448
|
-
className:
|
|
10968
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedRow,
|
|
10449
10969
|
"data-shop-shadowed-entry": row.name,
|
|
10450
10970
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10451
|
-
className:
|
|
10971
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedInfo,
|
|
10452
10972
|
children: [
|
|
10453
10973
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10454
|
-
className:
|
|
10974
|
+
className: _dsh_plugin_shop_css_5f753521_default.name,
|
|
10455
10975
|
children: row.name
|
|
10456
10976
|
}),
|
|
10457
10977
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10458
|
-
className:
|
|
10978
|
+
className: _dsh_plugin_shop_css_5f753521_default.badges,
|
|
10459
10979
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10460
|
-
className:
|
|
10980
|
+
className: _dsh_plugin_shop_css_5f753521_default.tierBadge,
|
|
10461
10981
|
"data-tier": "shadowed",
|
|
10462
10982
|
children: t("shadowedBadge")
|
|
10463
10983
|
})
|
|
10464
10984
|
}),
|
|
10465
10985
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10466
|
-
className:
|
|
10986
|
+
className: _dsh_plugin_shop_css_5f753521_default.shadowedDetail,
|
|
10467
10987
|
children: t("shadowedDetail")
|
|
10468
10988
|
})
|
|
10469
10989
|
]
|
|
10470
10990
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10471
|
-
className:
|
|
10991
|
+
className: _dsh_plugin_shop_css_5f753521_default.outdatedActions,
|
|
10472
10992
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UninstallPanel, {
|
|
10473
10993
|
name: row.name,
|
|
10474
10994
|
t,
|
|
@@ -10518,16 +11038,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10518
11038
|
role: "alert",
|
|
10519
11039
|
children: [
|
|
10520
11040
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10521
|
-
className:
|
|
11041
|
+
className: _dsh_plugin_shop_css_5f753521_default.stateLine,
|
|
10522
11042
|
children: say("tabCrashed", en.tabCrashed)
|
|
10523
11043
|
}),
|
|
10524
11044
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10525
|
-
className:
|
|
11045
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
10526
11046
|
children: error.message
|
|
10527
11047
|
}),
|
|
10528
11048
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10529
11049
|
type: "button",
|
|
10530
|
-
className:
|
|
11050
|
+
className: _dsh_plugin_shop_css_5f753521_default.actionButton,
|
|
10531
11051
|
onClick: () => this.setState({ error: null }),
|
|
10532
11052
|
children: say("retry", en.retry)
|
|
10533
11053
|
})
|
|
@@ -10535,10 +11055,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10535
11055
|
});
|
|
10536
11056
|
}
|
|
10537
11057
|
};
|
|
10538
|
-
/** The shop body inside its own error boundary, as `
|
|
10539
|
-
* below the page's
|
|
10540
|
-
*
|
|
10541
|
-
*
|
|
11058
|
+
/** The shop body inside its own error boundary, as `ShopPage` renders it
|
|
11059
|
+
* below the page's head (index.ts registers `ShopPage` in the main column).
|
|
11060
|
+
* Exported under the name the body used to carry, so nothing can render the
|
|
11061
|
+
* body without the boundary. */
|
|
10542
11062
|
function ShopTab(props) {
|
|
10543
11063
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopErrorBoundary, {
|
|
10544
11064
|
t: props.t,
|
|
@@ -10548,7 +11068,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10548
11068
|
/** The shop tab body: browse, search, refresh, and render one card per
|
|
10549
11069
|
* entry. Data attributes on the e2e-relevant nodes follow the Task 3 list. */
|
|
10550
11070
|
function ShopTabBody(props) {
|
|
10551
|
-
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, onRestarted, updateStart, reload: injectedReload } = props;
|
|
11071
|
+
const { t, catalog, install, installStatus, setEnabled, installed, installedSpecs, uninstall, noteUninstalled, restart, version, onRestarted, updateStart, reload: injectedReload, page: injectedPage, onShelfReady } = props;
|
|
10552
11072
|
const [catalogState, setCatalogState] = (0, react.useState)({ kind: "loading" });
|
|
10553
11073
|
const [installedState, setInstalledState] = (0, react.useState)({ kind: "loading" });
|
|
10554
11074
|
/** The install gate's own input, or undefined while it is unknown — loading,
|
|
@@ -10562,63 +11082,64 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10562
11082
|
globalThis.location.reload();
|
|
10563
11083
|
}), [injectedReload]);
|
|
10564
11084
|
const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
|
|
10565
|
-
const
|
|
11085
|
+
const ownDeps = (0, react.useRef)({
|
|
11086
|
+
install,
|
|
11087
|
+
uninstall,
|
|
11088
|
+
updateStart,
|
|
11089
|
+
installStatus,
|
|
11090
|
+
noteUninstalled
|
|
11091
|
+
});
|
|
11092
|
+
ownDeps.current = {
|
|
11093
|
+
install,
|
|
11094
|
+
uninstall,
|
|
11095
|
+
updateStart,
|
|
11096
|
+
installStatus,
|
|
11097
|
+
noteUninstalled
|
|
11098
|
+
};
|
|
11099
|
+
const [ownPage] = (0, react.useState)(() => injectedPage === void 0 ? createPageStore(() => ownDeps.current, { pollUnwatched: false }) : null);
|
|
11100
|
+
const page = injectedPage ?? ownPage;
|
|
11101
|
+
if (page === null) throw new Error("dsh-plugin-shop: the shop face stopped handing over the page store the tab mounted with");
|
|
11102
|
+
const pageState = (0, react.useSyncExternalStore)(page.subscribe, page.getSnapshot);
|
|
11103
|
+
const { query, category, order, hideIncompatible, visibleCount } = pageState.view;
|
|
11104
|
+
const selfUpdate = useRegistrySelf(page.self);
|
|
10566
11105
|
const restartMonitor = useRestartMonitor();
|
|
10567
|
-
const hostPending = selfVersion !== null && selfVersion
|
|
11106
|
+
const hostPending = selfVersion !== null && updateAwaitsRestart(selfVersion);
|
|
10568
11107
|
const selfPending = selfUpdate.view.kind === "done" || selfUpdate.view.kind === "idle" && hostPending;
|
|
10569
|
-
const [request, setRequest] = (0, react.useState)(
|
|
11108
|
+
const [request, setRequest] = (0, react.useState)(() => {
|
|
11109
|
+
const { reverdictPending, reverdicts } = page.getSnapshot();
|
|
11110
|
+
return reverdictPending ? {
|
|
11111
|
+
kind: "reverdict",
|
|
11112
|
+
reverdicts
|
|
11113
|
+
} : { kind: "initial" };
|
|
11114
|
+
});
|
|
11115
|
+
const askedFor = (0, react.useRef)(request.kind === "reverdict" ? request.reverdicts : null);
|
|
11116
|
+
(0, react.useEffect)(() => {
|
|
11117
|
+
if (!pageState.reverdictPending || askedFor.current === pageState.reverdicts) return;
|
|
11118
|
+
askedFor.current = pageState.reverdicts;
|
|
11119
|
+
setRequest({
|
|
11120
|
+
kind: "reverdict",
|
|
11121
|
+
reverdicts: pageState.reverdicts
|
|
11122
|
+
});
|
|
11123
|
+
}, [pageState.reverdictPending, pageState.reverdicts]);
|
|
11124
|
+
const pendingReverdict = () => {
|
|
11125
|
+
const { reverdictPending, reverdicts } = page.getSnapshot();
|
|
11126
|
+
return reverdictPending ? reverdicts : null;
|
|
11127
|
+
};
|
|
10570
11128
|
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;
|
|
11129
|
+
const { settled } = pageState;
|
|
11130
|
+
const flows = useRegistryFlows(page.installs);
|
|
11131
|
+
const uninstallFlows = useRegistryFlows(page.uninstalls);
|
|
10599
11132
|
(0, react.useEffect)(() => onRestarted?.((fresh) => {
|
|
10600
11133
|
setSelfVersion(fresh);
|
|
10601
|
-
|
|
10602
|
-
resetInstalls(settledByRestart);
|
|
10603
|
-
resetUninstalls(settledByRestart);
|
|
10604
|
-
setRestarts((current) => current + 1);
|
|
10605
|
-
noteMutation();
|
|
10606
|
-
setRequest({ kind: "reverdict" });
|
|
11134
|
+
if (injectedPage === void 0) page.noteRestarted();
|
|
10607
11135
|
}), [
|
|
10608
11136
|
onRestarted,
|
|
10609
|
-
|
|
10610
|
-
|
|
10611
|
-
resetUninstalls,
|
|
10612
|
-
noteMutation
|
|
11137
|
+
page,
|
|
11138
|
+
injectedPage
|
|
10613
11139
|
]);
|
|
10614
11140
|
const [reloading, setReloading] = (0, react.useState)(false);
|
|
10615
11141
|
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
11142
|
const incremental = typeof IntersectionObserver !== "undefined";
|
|
10621
|
-
const [visibleCount, setVisibleCount] = (0, react.useState)(48);
|
|
10622
11143
|
const sentinelRef = (0, react.useRef)(null);
|
|
10623
11144
|
const filteredLenRef = (0, react.useRef)(0);
|
|
10624
11145
|
const sortLabelId = (0, react.useId)();
|
|
@@ -10630,17 +11151,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10630
11151
|
const load = async () => {
|
|
10631
11152
|
try {
|
|
10632
11153
|
const result = await catalog(request.kind === "refresh" ? { refresh: true } : request.kind === "reverdict" ? { reverdict: true } : void 0);
|
|
10633
|
-
if (!cancelled)
|
|
10634
|
-
|
|
10635
|
-
|
|
10636
|
-
|
|
11154
|
+
if (!cancelled) {
|
|
11155
|
+
setCatalogState({
|
|
11156
|
+
kind: "ready",
|
|
11157
|
+
result
|
|
11158
|
+
});
|
|
11159
|
+
if (request.kind !== "initial" && request.reverdicts !== null) page.clearReverdict(request.reverdicts);
|
|
11160
|
+
}
|
|
10637
11161
|
} catch {
|
|
10638
11162
|
if (cancelled) {
|
|
10639
11163
|
if (request.kind === "refresh" && pendingRefreshRef.current === request) setReloadFailed(true);
|
|
10640
11164
|
return;
|
|
10641
11165
|
}
|
|
10642
11166
|
setCatalogState((current) => current.kind === "ready" ? current : { kind: "error" });
|
|
10643
|
-
if (request.kind
|
|
11167
|
+
if (request.kind === "reverdict") page.clearReverdict(request.reverdicts);
|
|
11168
|
+
else {
|
|
11169
|
+
setReloadFailed(true);
|
|
11170
|
+
if (request.kind !== "initial" && request.reverdicts !== null) setRequest({
|
|
11171
|
+
kind: "reverdict",
|
|
11172
|
+
reverdicts: request.reverdicts
|
|
11173
|
+
});
|
|
11174
|
+
}
|
|
10644
11175
|
} finally {
|
|
10645
11176
|
if (pendingRefreshRef.current === request) pendingRefreshRef.current = null;
|
|
10646
11177
|
if (!cancelled) setReloading(false);
|
|
@@ -10650,7 +11181,21 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10650
11181
|
return () => {
|
|
10651
11182
|
cancelled = true;
|
|
10652
11183
|
};
|
|
10653
|
-
}, [
|
|
11184
|
+
}, [
|
|
11185
|
+
catalog,
|
|
11186
|
+
request,
|
|
11187
|
+
page
|
|
11188
|
+
]);
|
|
11189
|
+
const shelfShown = (0, react.useRef)(false);
|
|
11190
|
+
(0, react.useLayoutEffect)(() => {
|
|
11191
|
+
if (shelfShown.current || catalogState.kind !== "ready" || installedState.kind === "loading") return;
|
|
11192
|
+
shelfShown.current = true;
|
|
11193
|
+
onShelfReady?.();
|
|
11194
|
+
}, [
|
|
11195
|
+
catalogState,
|
|
11196
|
+
installedState,
|
|
11197
|
+
onShelfReady
|
|
11198
|
+
]);
|
|
10654
11199
|
(0, react.useEffect)(() => {
|
|
10655
11200
|
let cancelled = false;
|
|
10656
11201
|
const load = async () => {
|
|
@@ -10694,6 +11239,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10694
11239
|
entries
|
|
10695
11240
|
});
|
|
10696
11241
|
setSpecs(held ?? void 0);
|
|
11242
|
+
page.noteInstalled(entries.map(identityKey));
|
|
10697
11243
|
} catch {
|
|
10698
11244
|
if (cancelled) return;
|
|
10699
11245
|
setInstalledState({ kind: "error" });
|
|
@@ -10707,8 +11253,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10707
11253
|
}, [
|
|
10708
11254
|
installed,
|
|
10709
11255
|
installedSpecs,
|
|
10710
|
-
|
|
10711
|
-
|
|
11256
|
+
versionReload,
|
|
11257
|
+
settled,
|
|
11258
|
+
page
|
|
10712
11259
|
]);
|
|
10713
11260
|
const installedByKey = (0, react.useMemo)(() => {
|
|
10714
11261
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -10726,7 +11273,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10726
11273
|
const browsable = (0, react.useMemo)(() => {
|
|
10727
11274
|
if (catalogState.kind !== "ready") return [];
|
|
10728
11275
|
const cleared = new Set(catalogState.result.notAShop ?? []);
|
|
10729
|
-
return sortedCatalog.filter((entry) =>
|
|
11276
|
+
return sortedCatalog.filter((entry) => listedOnShelf(entry, cleared));
|
|
10730
11277
|
}, [catalogState, sortedCatalog]);
|
|
10731
11278
|
const missingByKey = (0, react.useMemo)(() => {
|
|
10732
11279
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -10785,14 +11332,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10785
11332
|
const node = sentinelRef.current;
|
|
10786
11333
|
if (node === null) return;
|
|
10787
11334
|
const observer = new IntersectionObserver((entries) => {
|
|
10788
|
-
if (entries.some((entry) => entry.isIntersecting))
|
|
11335
|
+
if (entries.some((entry) => entry.isIntersecting)) page.updateView((view) => ({ visibleCount: nextVisibleCount(view.visibleCount, filteredLenRef.current, 48) }));
|
|
10789
11336
|
}, { rootMargin: "0px 0px 1200px 0px" });
|
|
10790
11337
|
observer.observe(node);
|
|
10791
11338
|
return () => observer.disconnect();
|
|
10792
11339
|
}, [
|
|
10793
11340
|
incremental,
|
|
10794
11341
|
visibleCount,
|
|
10795
|
-
filtered.length
|
|
11342
|
+
filtered.length,
|
|
11343
|
+
page
|
|
10796
11344
|
]);
|
|
10797
11345
|
const visible = incremental ? filtered.slice(0, visibleCount) : filtered;
|
|
10798
11346
|
/** The date a card shows under the current order, or undefined under the
|
|
@@ -10825,19 +11373,19 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10825
11373
|
"data-shop-tab": true,
|
|
10826
11374
|
"aria-busy": "true",
|
|
10827
11375
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10828
|
-
className:
|
|
11376
|
+
className: _dsh_plugin_shop_css_5f753521_default.srOnly,
|
|
10829
11377
|
children: t("loading")
|
|
10830
11378
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10831
|
-
className:
|
|
11379
|
+
className: _dsh_plugin_shop_css_5f753521_default.skeletonGrid,
|
|
10832
11380
|
"data-shop-skeleton": true,
|
|
10833
11381
|
"aria-hidden": "true",
|
|
10834
11382
|
children: Array.from({ length: 8 }, (_, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10835
|
-
className:
|
|
11383
|
+
className: _dsh_plugin_shop_css_5f753521_default.skeletonCard,
|
|
10836
11384
|
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:
|
|
11385
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_5f753521_default.skeletonName }),
|
|
11386
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_5f753521_default.skeletonSummary }),
|
|
11387
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_5f753521_default.skeletonSummaryShort }),
|
|
11388
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_5f753521_default.skeletonActions })
|
|
10841
11389
|
]
|
|
10842
11390
|
}, i))
|
|
10843
11391
|
})]
|
|
@@ -10847,13 +11395,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10847
11395
|
translate: "no",
|
|
10848
11396
|
"data-shop-tab": true,
|
|
10849
11397
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10850
|
-
className:
|
|
11398
|
+
className: _dsh_plugin_shop_css_5f753521_default.stateLine,
|
|
10851
11399
|
children: t("error")
|
|
10852
11400
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10853
11401
|
type: "button",
|
|
10854
|
-
className:
|
|
11402
|
+
className: _dsh_plugin_shop_css_5f753521_default.actionButton,
|
|
10855
11403
|
onClick: () => {
|
|
10856
|
-
setRequest({
|
|
11404
|
+
setRequest({
|
|
11405
|
+
kind: "retry",
|
|
11406
|
+
reverdicts: pendingReverdict()
|
|
11407
|
+
});
|
|
10857
11408
|
setVersionReload((current) => current + 1);
|
|
10858
11409
|
},
|
|
10859
11410
|
children: t("retry")
|
|
@@ -10866,46 +11417,48 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10866
11417
|
"data-shop-tab": true,
|
|
10867
11418
|
children: [
|
|
10868
11419
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10869
|
-
className:
|
|
11420
|
+
className: _dsh_plugin_shop_css_5f753521_default.toolbar,
|
|
10870
11421
|
children: [
|
|
10871
11422
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
10872
11423
|
type: "search",
|
|
10873
|
-
className:
|
|
11424
|
+
className: _dsh_plugin_shop_css_5f753521_default.searchInput,
|
|
10874
11425
|
"aria-label": t("search"),
|
|
10875
11426
|
placeholder: t("search"),
|
|
10876
11427
|
value: query,
|
|
10877
11428
|
onChange: (event) => {
|
|
10878
|
-
|
|
10879
|
-
|
|
11429
|
+
page.setView({
|
|
11430
|
+
query: event.target.value,
|
|
11431
|
+
visibleCount: 48
|
|
11432
|
+
});
|
|
10880
11433
|
}
|
|
10881
11434
|
}),
|
|
10882
11435
|
expansionMemo.intents.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10883
11436
|
"data-shop-expansion-note": true,
|
|
10884
|
-
className:
|
|
11437
|
+
className: _dsh_plugin_shop_css_5f753521_default.expansionNote,
|
|
10885
11438
|
children: t("zhExpansionNote", { terms: expansionMemo.terms.slice(1).map((term) => term.startsWith("#") ? term.slice(1) : term).join(", ") })
|
|
10886
11439
|
}),
|
|
10887
11440
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10888
|
-
className:
|
|
11441
|
+
className: _dsh_plugin_shop_css_5f753521_default.toolbarRight,
|
|
10889
11442
|
children: [
|
|
10890
11443
|
result.stale && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
10891
|
-
className:
|
|
11444
|
+
className: _dsh_plugin_shop_css_5f753521_default.staleBadge,
|
|
10892
11445
|
children: t("staleLabel", { date: result.builtAt.slice(0, 10) })
|
|
10893
11446
|
}),
|
|
10894
11447
|
selfVersion !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10895
|
-
className:
|
|
11448
|
+
className: _dsh_plugin_shop_css_5f753521_default.versionBlock,
|
|
10896
11449
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
10897
|
-
className:
|
|
11450
|
+
className: _dsh_plugin_shop_css_5f753521_default.versionText,
|
|
10898
11451
|
"data-shop-version": true,
|
|
10899
11452
|
children: ["v", selfVersion.installed]
|
|
10900
11453
|
}), restartMonitor !== null ? null : selfPending && restartBlocked === null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10901
11454
|
type: "button",
|
|
10902
|
-
className:
|
|
11455
|
+
className: _dsh_plugin_shop_css_5f753521_default.restartSelfButton,
|
|
10903
11456
|
"data-shop-restart": true,
|
|
10904
11457
|
onClick: () => setSelfRestartGate(true),
|
|
10905
11458
|
children: t("restart")
|
|
10906
11459
|
}) : !selfPending && selfVersion.outdated && selfVersion.latest !== null && selfUpdate.view.kind === "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10907
11460
|
type: "button",
|
|
10908
|
-
className:
|
|
11461
|
+
className: _dsh_plugin_shop_css_5f753521_default.updateSelfButton,
|
|
10909
11462
|
"data-shop-update-self": true,
|
|
10910
11463
|
onClick: () => {
|
|
10911
11464
|
if (selfVersion.latest !== null) selfUpdate.start({ version: selfVersion.latest });
|
|
@@ -10913,7 +11466,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10913
11466
|
children: t("update")
|
|
10914
11467
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
10915
11468
|
type: "button",
|
|
10916
|
-
className:
|
|
11469
|
+
className: _dsh_plugin_shop_css_5f753521_default.checkUpdateButton,
|
|
10917
11470
|
"data-shop-check-update": true,
|
|
10918
11471
|
title: t("checkUpdateTitle"),
|
|
10919
11472
|
disabled: checkState !== "idle",
|
|
@@ -10922,7 +11475,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10922
11475
|
})]
|
|
10923
11476
|
}),
|
|
10924
11477
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
10925
|
-
className:
|
|
11478
|
+
className: _dsh_plugin_shop_css_5f753521_default.githubLink,
|
|
10926
11479
|
href: SHOP_REPO_URL,
|
|
10927
11480
|
target: "_blank",
|
|
10928
11481
|
rel: "noopener noreferrer",
|
|
@@ -10947,28 +11500,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10947
11500
|
logFile: restartMonitor.logFile
|
|
10948
11501
|
}),
|
|
10949
11502
|
selfUpdate.view.kind === "running" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10950
|
-
className:
|
|
11503
|
+
className: _dsh_plugin_shop_css_5f753521_default.selfUpdatePanel,
|
|
10951
11504
|
"data-shop-self-updating": true,
|
|
10952
11505
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10953
|
-
className:
|
|
11506
|
+
className: _dsh_plugin_shop_css_5f753521_default.installing,
|
|
10954
11507
|
children: t(installPhaseKey(selfUpdate.view.phase))
|
|
10955
11508
|
}), selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10956
|
-
className:
|
|
11509
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
10957
11510
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10958
|
-
className:
|
|
11511
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
10959
11512
|
"data-shop-log-line": true,
|
|
10960
11513
|
children: line
|
|
10961
11514
|
}, index))
|
|
10962
11515
|
})]
|
|
10963
11516
|
}),
|
|
10964
11517
|
selfPending && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
10965
|
-
className:
|
|
11518
|
+
className: _dsh_plugin_shop_css_5f753521_default.selfUpdatePanel,
|
|
10966
11519
|
"data-shop-self-update-done": true,
|
|
10967
11520
|
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10968
|
-
className:
|
|
11521
|
+
className: _dsh_plugin_shop_css_5f753521_default.installedActions,
|
|
10969
11522
|
children: [
|
|
10970
11523
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10971
|
-
className:
|
|
11524
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10972
11525
|
children: t("installedRestartNotice")
|
|
10973
11526
|
}),
|
|
10974
11527
|
restartBlocked === null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(RestartPanel, {
|
|
@@ -10981,7 +11534,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10981
11534
|
}
|
|
10982
11535
|
}),
|
|
10983
11536
|
restartBlocked !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10984
|
-
className:
|
|
11537
|
+
className: _dsh_plugin_shop_css_5f753521_default.notice,
|
|
10985
11538
|
"data-shop-restart-disabled": true,
|
|
10986
11539
|
children: t(restartBlockedNoticeKey(restartBlocked))
|
|
10987
11540
|
})
|
|
@@ -10989,39 +11542,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
10989
11542
|
})
|
|
10990
11543
|
}),
|
|
10991
11544
|
selfUpdate.view.kind === "failed" && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
10992
|
-
className:
|
|
11545
|
+
className: _dsh_plugin_shop_css_5f753521_default.selfUpdatePanel,
|
|
10993
11546
|
"data-shop-self-update-failed": true,
|
|
10994
11547
|
children: [
|
|
10995
11548
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
10996
|
-
className:
|
|
11549
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedHeading,
|
|
10997
11550
|
children: t("updateFailed")
|
|
10998
11551
|
}),
|
|
10999
11552
|
selfUpdate.view.log.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11000
|
-
className:
|
|
11553
|
+
className: _dsh_plugin_shop_css_5f753521_default.log,
|
|
11001
11554
|
children: selfUpdate.view.log.map((line, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
11002
|
-
className:
|
|
11555
|
+
className: _dsh_plugin_shop_css_5f753521_default.logLine,
|
|
11003
11556
|
"data-shop-log-line": true,
|
|
11004
11557
|
children: line
|
|
11005
11558
|
}, index))
|
|
11006
11559
|
}),
|
|
11007
11560
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11008
|
-
className:
|
|
11561
|
+
className: _dsh_plugin_shop_css_5f753521_default.failedDetail,
|
|
11009
11562
|
children: selfUpdate.view.detail === "" ? t("updateTransportFailed") : selfUpdate.view.detail
|
|
11563
|
+
}),
|
|
11564
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11565
|
+
type: "button",
|
|
11566
|
+
className: _dsh_plugin_shop_css_5f753521_default.cancelButton,
|
|
11567
|
+
"data-shop-update-self-dismiss": true,
|
|
11568
|
+
onClick: () => {
|
|
11569
|
+
selfUpdate.resetWhere((current) => current.kind === "failed");
|
|
11570
|
+
},
|
|
11571
|
+
children: t("dismiss")
|
|
11010
11572
|
})
|
|
11011
11573
|
]
|
|
11012
11574
|
}),
|
|
11013
11575
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11014
|
-
className:
|
|
11576
|
+
className: _dsh_plugin_shop_css_5f753521_default.categoryBar,
|
|
11015
11577
|
role: "group",
|
|
11016
11578
|
"aria-label": t("catalog"),
|
|
11017
11579
|
children: [
|
|
11018
11580
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11019
11581
|
type: "button",
|
|
11020
|
-
className: category === null ? `${
|
|
11582
|
+
className: category === null ? `${_dsh_plugin_shop_css_5f753521_default.categoryButton} ${_dsh_plugin_shop_css_5f753521_default.categoryButtonOn}` : _dsh_plugin_shop_css_5f753521_default.categoryButton,
|
|
11021
11583
|
"aria-pressed": category === null,
|
|
11022
11584
|
onClick: () => {
|
|
11023
|
-
|
|
11024
|
-
|
|
11585
|
+
page.setView({
|
|
11586
|
+
category: null,
|
|
11587
|
+
visibleCount: 48
|
|
11588
|
+
});
|
|
11025
11589
|
},
|
|
11026
11590
|
"data-shop-category-all": true,
|
|
11027
11591
|
children: [
|
|
@@ -11032,13 +11596,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11032
11596
|
}),
|
|
11033
11597
|
CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11034
11598
|
type: "button",
|
|
11035
|
-
className: category === key ? `${
|
|
11599
|
+
className: category === key ? `${_dsh_plugin_shop_css_5f753521_default.categoryButton} ${_dsh_plugin_shop_css_5f753521_default.categoryButtonOn}` : _dsh_plugin_shop_css_5f753521_default.categoryButton,
|
|
11036
11600
|
"aria-pressed": category === key,
|
|
11037
11601
|
"data-category": key,
|
|
11038
11602
|
"data-shop-category-tab": key,
|
|
11039
11603
|
onClick: () => {
|
|
11040
|
-
|
|
11041
|
-
|
|
11604
|
+
page.setView({
|
|
11605
|
+
category: key,
|
|
11606
|
+
visibleCount: 48
|
|
11607
|
+
});
|
|
11042
11608
|
},
|
|
11043
11609
|
children: [
|
|
11044
11610
|
t(categoryLocaleKey(key)),
|
|
@@ -11048,12 +11614,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11048
11614
|
}, key)),
|
|
11049
11615
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
11050
11616
|
type: "button",
|
|
11051
|
-
className: category === "installed" ? `${
|
|
11617
|
+
className: category === "installed" ? `${_dsh_plugin_shop_css_5f753521_default.categoryButton} ${_dsh_plugin_shop_css_5f753521_default.categoryButtonOn}` : _dsh_plugin_shop_css_5f753521_default.categoryButton,
|
|
11052
11618
|
"aria-pressed": category === "installed",
|
|
11053
11619
|
"data-shop-category-installed": true,
|
|
11054
11620
|
onClick: () => {
|
|
11055
|
-
|
|
11056
|
-
|
|
11621
|
+
page.setView({
|
|
11622
|
+
category: "installed",
|
|
11623
|
+
visibleCount: 48
|
|
11624
|
+
});
|
|
11057
11625
|
},
|
|
11058
11626
|
children: [
|
|
11059
11627
|
t("installed"),
|
|
@@ -11065,26 +11633,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11065
11633
|
type: "button",
|
|
11066
11634
|
role: "switch",
|
|
11067
11635
|
"aria-checked": hideIncompatible,
|
|
11068
|
-
className: hideIncompatible ? `${
|
|
11636
|
+
className: hideIncompatible ? `${_dsh_plugin_shop_css_5f753521_default.incompatibleFilter} ${_dsh_plugin_shop_css_5f753521_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_5f753521_default.incompatibleFilter,
|
|
11069
11637
|
title: t(hideIncompatible ? "showIncompatibleTitle" : "incompatibleFilterTitle"),
|
|
11070
11638
|
"data-shop-hide-incompatible": true,
|
|
11071
11639
|
onClick: () => {
|
|
11072
|
-
|
|
11073
|
-
|
|
11640
|
+
page.updateView((view) => ({
|
|
11641
|
+
hideIncompatible: !view.hideIncompatible,
|
|
11642
|
+
visibleCount: 48
|
|
11643
|
+
}));
|
|
11074
11644
|
},
|
|
11075
11645
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11076
|
-
className: hideIncompatible ? `${
|
|
11646
|
+
className: hideIncompatible ? `${_dsh_plugin_shop_css_5f753521_default.switch} ${_dsh_plugin_shop_css_5f753521_default.switchOn}` : _dsh_plugin_shop_css_5f753521_default.switch,
|
|
11077
11647
|
"aria-hidden": "true",
|
|
11078
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className:
|
|
11648
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_5f753521_default.switchKnob })
|
|
11079
11649
|
}), t("hideIncompatible", { count: incompatibleCount })]
|
|
11080
11650
|
})
|
|
11081
11651
|
]
|
|
11082
11652
|
}),
|
|
11083
11653
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
11084
|
-
className:
|
|
11654
|
+
className: _dsh_plugin_shop_css_5f753521_default.catalogStatsRow,
|
|
11085
11655
|
children: [
|
|
11086
11656
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11087
|
-
className:
|
|
11657
|
+
className: _dsh_plugin_shop_css_5f753521_default.catalogStats,
|
|
11088
11658
|
"data-shop-catalog-stats": true,
|
|
11089
11659
|
children: t("catalogStats", {
|
|
11090
11660
|
count: String(browsable.length),
|
|
@@ -11093,39 +11663,44 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11093
11663
|
}),
|
|
11094
11664
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11095
11665
|
type: "button",
|
|
11096
|
-
className:
|
|
11666
|
+
className: _dsh_plugin_shop_css_5f753521_default.catalogRefreshButton,
|
|
11097
11667
|
"data-shop-catalog-refresh": true,
|
|
11098
11668
|
disabled: reloading,
|
|
11099
11669
|
onClick: () => {
|
|
11100
11670
|
setReloading(true);
|
|
11101
11671
|
setReloadFailed(false);
|
|
11102
|
-
setRequest({
|
|
11672
|
+
setRequest({
|
|
11673
|
+
kind: "refresh",
|
|
11674
|
+
reverdicts: pendingReverdict()
|
|
11675
|
+
});
|
|
11103
11676
|
setVersionReload((current) => current + 1);
|
|
11104
11677
|
},
|
|
11105
11678
|
children: reloading ? t("refreshing") : t("refresh")
|
|
11106
11679
|
}),
|
|
11107
11680
|
reloadFailed && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11108
|
-
className:
|
|
11681
|
+
className: _dsh_plugin_shop_css_5f753521_default.catalogRefreshFailed,
|
|
11109
11682
|
"data-shop-catalog-refresh-failed": true,
|
|
11110
11683
|
children: t("refreshFailed")
|
|
11111
11684
|
}),
|
|
11112
11685
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
11113
|
-
className:
|
|
11686
|
+
className: _dsh_plugin_shop_css_5f753521_default.sortGroup,
|
|
11114
11687
|
role: "group",
|
|
11115
11688
|
"aria-labelledby": sortLabelId,
|
|
11116
11689
|
"data-shop-sort": true,
|
|
11117
11690
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
11118
|
-
className:
|
|
11691
|
+
className: _dsh_plugin_shop_css_5f753521_default.sortLabel,
|
|
11119
11692
|
id: sortLabelId,
|
|
11120
11693
|
children: t("sortLabel")
|
|
11121
11694
|
}), SHELF_ORDERS.map((name) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
11122
11695
|
type: "button",
|
|
11123
|
-
className: order === name ? `${
|
|
11696
|
+
className: order === name ? `${_dsh_plugin_shop_css_5f753521_default.categoryButton} ${_dsh_plugin_shop_css_5f753521_default.categoryButtonOn}` : _dsh_plugin_shop_css_5f753521_default.categoryButton,
|
|
11124
11697
|
"aria-pressed": order === name,
|
|
11125
11698
|
"data-shop-order": name,
|
|
11126
11699
|
onClick: () => {
|
|
11127
|
-
|
|
11128
|
-
|
|
11700
|
+
page.setView({
|
|
11701
|
+
order: name,
|
|
11702
|
+
visibleCount: 48
|
|
11703
|
+
});
|
|
11129
11704
|
},
|
|
11130
11705
|
children: t(shelfOrderLocaleKey(name))
|
|
11131
11706
|
}, name))]
|
|
@@ -11133,43 +11708,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11133
11708
|
]
|
|
11134
11709
|
}),
|
|
11135
11710
|
result.plugins.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11136
|
-
className:
|
|
11711
|
+
className: _dsh_plugin_shop_css_5f753521_default.emptyLine,
|
|
11137
11712
|
children: t("empty")
|
|
11138
|
-
}) : filtered.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11139
|
-
className:
|
|
11713
|
+
}) : filtered.length === 0 ? category === "installed" && installedState.kind !== "ready" ? installedState.kind === "loading" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11714
|
+
className: _dsh_plugin_shop_css_5f753521_default.emptyLine,
|
|
11715
|
+
"data-shop-installed-loading": true,
|
|
11716
|
+
children: t("installedLoading")
|
|
11717
|
+
}) : null : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11718
|
+
className: _dsh_plugin_shop_css_5f753521_default.emptyLine,
|
|
11140
11719
|
"data-shop-empty": true,
|
|
11141
11720
|
children: matched.length > 0 ? t("emptyIncompatibleFiltered") : t("emptySearch")
|
|
11142
11721
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("ul", {
|
|
11143
|
-
className:
|
|
11722
|
+
className: _dsh_plugin_shop_css_5f753521_default.cards,
|
|
11144
11723
|
children: [visible.map((entry) => {
|
|
11145
11724
|
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
|
-
|
|
11725
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
11726
|
+
"data-shop-identity": key,
|
|
11727
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
|
|
11728
|
+
entry,
|
|
11729
|
+
stars: starsOf(entry, stars),
|
|
11730
|
+
order,
|
|
11731
|
+
shelfDay: shelfDayOf(entry),
|
|
11732
|
+
installed: installedByKey.get(key),
|
|
11733
|
+
missing: missingByKey.get(key) ?? [],
|
|
11734
|
+
harness: harnessVerdictOf(harnessVerdicts, key),
|
|
11735
|
+
nameTakenBy: nameTakenByKey.get(key),
|
|
11736
|
+
t,
|
|
11737
|
+
flow: flows.flowFor(key),
|
|
11738
|
+
uninstallFlow: uninstallFlows.flowFor(key),
|
|
11739
|
+
restart,
|
|
11740
|
+
restartBlocked,
|
|
11741
|
+
reload,
|
|
11742
|
+
setEnabled,
|
|
11743
|
+
inInstalledView: category === "installed",
|
|
11744
|
+
page
|
|
11745
|
+
})
|
|
11746
|
+
}, key);
|
|
11165
11747
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
11166
11748
|
ref: sentinelRef,
|
|
11167
|
-
className:
|
|
11749
|
+
className: _dsh_plugin_shop_css_5f753521_default.cardsSentry,
|
|
11168
11750
|
"data-shop-sentry": true,
|
|
11169
11751
|
"aria-hidden": "true"
|
|
11170
11752
|
})]
|
|
11171
11753
|
}), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11172
|
-
className:
|
|
11754
|
+
className: _dsh_plugin_shop_css_5f753521_default.showingLine,
|
|
11173
11755
|
"aria-live": "polite",
|
|
11174
11756
|
children: t("showing", {
|
|
11175
11757
|
shown: String(visibleCount),
|
|
@@ -11187,7 +11769,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11187
11769
|
restart,
|
|
11188
11770
|
restartBlocked,
|
|
11189
11771
|
reload,
|
|
11190
|
-
|
|
11772
|
+
page
|
|
11191
11773
|
}),
|
|
11192
11774
|
category === "installed" && installedState.kind === "ready" && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShadowedSection, {
|
|
11193
11775
|
entries: installedState.entries,
|
|
@@ -11201,35 +11783,172 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11201
11783
|
});
|
|
11202
11784
|
}
|
|
11203
11785
|
//#endregion
|
|
11204
|
-
//#region src/client/
|
|
11205
|
-
|
|
11206
|
-
|
|
11207
|
-
|
|
11208
|
-
|
|
11209
|
-
|
|
11210
|
-
|
|
11211
|
-
|
|
11212
|
-
|
|
11213
|
-
|
|
11214
|
-
|
|
11215
|
-
|
|
11216
|
-
|
|
11217
|
-
|
|
11218
|
-
|
|
11219
|
-
|
|
11220
|
-
|
|
11786
|
+
//#region src/client/ShopPage.tsx
|
|
11787
|
+
/**
|
|
11788
|
+
* The shop's own page in dsh's main column (design
|
|
11789
|
+
* 2026-10-16-shop-sidebar-page §4.2), framed as dsh frames its Plugins page:
|
|
11790
|
+
* the page is its own scroll container, its head carries the title and the
|
|
11791
|
+
* one intro line and clears the desktop title bar, and the body below is
|
|
11792
|
+
* `ShopTab`, unchanged.
|
|
11793
|
+
*
|
|
11794
|
+
* It keeps the reader's scroll position in the page store (design
|
|
11795
|
+
* 2026-10-16-shop-sidebar-page §5.7). The position is written on every
|
|
11796
|
+
* scroll event — a browser fires at most one a frame, and the store takes it
|
|
11797
|
+
* without telling anyone — with the card at the page's top edge. The page
|
|
11798
|
+
* goes back there once, when the shelf is on screen again and the installed
|
|
11799
|
+
* list has answered, unless the reader moved first: the same card at the
|
|
11800
|
+
* same height, held there for a moment while the cards around it render, or
|
|
11801
|
+
* the position when that card is no longer on the shelf or the shelf was
|
|
11802
|
+
* refiltered since.
|
|
11803
|
+
*/
|
|
11804
|
+
/** What each card on the shelf carries: its identity (ShopTab), not its
|
|
11805
|
+
* name, which two listed entries can share — a fork and its original. */
|
|
11806
|
+
const IDENTITY = "data-shop-identity";
|
|
11807
|
+
/** The shelf's cards in document order: the children of the one list that
|
|
11808
|
+
* holds them, a live collection reached through the first card, so a scroll
|
|
11809
|
+
* event never walks the cards' own subtrees. Null while no card is on the
|
|
11810
|
+
* page. */
|
|
11811
|
+
function cardsOf(scroller) {
|
|
11812
|
+
return scroller.querySelector(`[${IDENTITY}]`)?.parentElement?.children ?? null;
|
|
11813
|
+
}
|
|
11814
|
+
/** The shelf's gap between two cards, in px: `.cards`' `gap` in
|
|
11815
|
+
* ShopTab.module.css, as the list's computed row gap reads it. */
|
|
11816
|
+
function gapOf(list) {
|
|
11817
|
+
const gap = list === null ? NaN : Number.parseFloat(getComputedStyle(list).rowGap);
|
|
11818
|
+
return Number.isFinite(gap) ? gap : 0;
|
|
11819
|
+
}
|
|
11820
|
+
/**
|
|
11821
|
+
* Where the reader is on the shelf: the first card whose bottom lies below
|
|
11822
|
+
* the page's top edge, and its top relative to that edge. The cards stand one
|
|
11823
|
+
* per row in document order, so their bottoms rise with their index, and a
|
|
11824
|
+
* binary search reads a handful of their rects on each scroll event, never
|
|
11825
|
+
* one per card.
|
|
11826
|
+
*
|
|
11827
|
+
* A card whose top lies below the edge counts only while it lies no further
|
|
11828
|
+
* below than the shelf's gap between cards, as when the edge falls in that
|
|
11829
|
+
* gap. Further down, the reader is above the shelf, in the head or the
|
|
11830
|
+
* toolbar: nothing there lays out at a placeholder height, so the position
|
|
11831
|
+
* alone is exact, while a card would pull them down the page whenever the
|
|
11832
|
+
* content above the shelf grows meanwhile.
|
|
11833
|
+
*/
|
|
11834
|
+
function anchorAt(scroller) {
|
|
11835
|
+
const cards = cardsOf(scroller);
|
|
11836
|
+
if (cards === null) return null;
|
|
11837
|
+
const edge = scroller.getBoundingClientRect().top;
|
|
11838
|
+
let low = 0;
|
|
11839
|
+
let high = cards.length;
|
|
11840
|
+
while (low < high) {
|
|
11841
|
+
const middle = Math.floor((low + high) / 2);
|
|
11842
|
+
if ((cards.item(middle)?.getBoundingClientRect().bottom ?? edge) > edge) high = middle;
|
|
11843
|
+
else low = middle + 1;
|
|
11844
|
+
}
|
|
11845
|
+
const card = cards.item(low);
|
|
11846
|
+
const key = card?.getAttribute(IDENTITY) ?? null;
|
|
11847
|
+
if (card === null || key === null) return null;
|
|
11848
|
+
const offset = card.getBoundingClientRect().top - edge;
|
|
11849
|
+
if (offset > 0 && offset > gapOf(card.parentElement)) return null;
|
|
11850
|
+
return {
|
|
11851
|
+
key,
|
|
11852
|
+
offset
|
|
11853
|
+
};
|
|
11854
|
+
}
|
|
11855
|
+
/** The card with `key` on the shelf, matched by iteration: an identity
|
|
11856
|
+
* carries `:`, `/` and `#`, which a selector would have to escape. */
|
|
11857
|
+
function cardWith(scroller, key) {
|
|
11858
|
+
const cards = cardsOf(scroller);
|
|
11859
|
+
if (cards === null) return null;
|
|
11860
|
+
for (const card of cards) if (card.getAttribute(IDENTITY) === key) return card;
|
|
11861
|
+
return null;
|
|
11862
|
+
}
|
|
11863
|
+
function ShopPage(props) {
|
|
11864
|
+
const { t, page } = props;
|
|
11865
|
+
const scroller = (0, react.useRef)(null);
|
|
11866
|
+
const moved = (0, react.useRef)(false);
|
|
11867
|
+
const holding = (0, react.useRef)(null);
|
|
11868
|
+
(0, react.useEffect)(() => () => {
|
|
11869
|
+
if (holding.current !== null) cancelAnimationFrame(holding.current);
|
|
11870
|
+
}, []);
|
|
11871
|
+
const restore = (0, react.useCallback)(() => {
|
|
11872
|
+
const node = scroller.current;
|
|
11873
|
+
if (node === null || moved.current || page === void 0) return;
|
|
11874
|
+
const anchor = page.readAnchor();
|
|
11875
|
+
const card = anchor === null ? null : cardWith(node, anchor.key);
|
|
11876
|
+
if (anchor === null || card === null) {
|
|
11877
|
+
node.scrollTop = page.readScroll();
|
|
11878
|
+
return;
|
|
11879
|
+
}
|
|
11880
|
+
const drift = () => card.getBoundingClientRect().top - node.getBoundingClientRect().top - anchor.offset;
|
|
11881
|
+
node.style.overflowAnchor = "none";
|
|
11882
|
+
node.scrollTop += drift();
|
|
11883
|
+
let held = node.scrollTop;
|
|
11884
|
+
const until = performance.now() + 500;
|
|
11885
|
+
const hold = () => {
|
|
11886
|
+
holding.current = null;
|
|
11887
|
+
if (moved.current || Math.abs(node.scrollTop - held) >= 1 || !card.isConnected || performance.now() > until) {
|
|
11888
|
+
node.style.overflowAnchor = "";
|
|
11889
|
+
return;
|
|
11890
|
+
}
|
|
11891
|
+
const off = drift();
|
|
11892
|
+
if (Math.abs(off) >= 1) {
|
|
11893
|
+
node.scrollTop += off;
|
|
11894
|
+
held = node.scrollTop;
|
|
11895
|
+
}
|
|
11896
|
+
holding.current = requestAnimationFrame(hold);
|
|
11897
|
+
};
|
|
11898
|
+
holding.current = requestAnimationFrame(hold);
|
|
11899
|
+
}, [page]);
|
|
11900
|
+
const cancelRestore = () => {
|
|
11901
|
+
moved.current = true;
|
|
11902
|
+
};
|
|
11903
|
+
(0, react.useEffect)(() => {
|
|
11904
|
+
page?.notePageMounted(true);
|
|
11905
|
+
return () => {
|
|
11906
|
+
page?.notePageMounted(false);
|
|
11907
|
+
};
|
|
11908
|
+
}, [page]);
|
|
11909
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
11910
|
+
ref: scroller,
|
|
11911
|
+
className: `${_dsh_plugin_shop_css_5f753521_default.page} notranslate`,
|
|
11912
|
+
translate: "no",
|
|
11913
|
+
"data-shop-page": true,
|
|
11914
|
+
onScroll: (event) => {
|
|
11915
|
+
page?.recordScroll(event.currentTarget.scrollTop, anchorAt(event.currentTarget));
|
|
11916
|
+
},
|
|
11917
|
+
onWheel: cancelRestore,
|
|
11918
|
+
onTouchStart: cancelRestore,
|
|
11919
|
+
onKeyDown: cancelRestore,
|
|
11920
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("header", {
|
|
11921
|
+
className: _dsh_plugin_shop_css_5f753521_default.pageHead,
|
|
11922
|
+
"data-shop-page-head": true,
|
|
11923
|
+
"data-window-drag": true,
|
|
11924
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
|
|
11925
|
+
className: _dsh_plugin_shop_css_5f753521_default.pageTitle,
|
|
11926
|
+
"data-shop-page-title": true,
|
|
11927
|
+
children: t("title")
|
|
11928
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
11929
|
+
className: _dsh_plugin_shop_css_5f753521_default.pageIntro,
|
|
11930
|
+
"data-shop-page-intro": true,
|
|
11931
|
+
children: t("intro")
|
|
11932
|
+
})]
|
|
11933
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopTab, {
|
|
11934
|
+
...props,
|
|
11935
|
+
onShelfReady: restore
|
|
11936
|
+
})]
|
|
11221
11937
|
});
|
|
11222
11938
|
}
|
|
11223
11939
|
//#endregion
|
|
11224
11940
|
//#region src/client/index.ts
|
|
11225
11941
|
/** Dictionary namespace owned by this plugin. */
|
|
11226
11942
|
const NS = "settings.shop";
|
|
11943
|
+
/** The id the shop's sidebar row and its main-column page share (design
|
|
11944
|
+
* 2026-10-16-shop-sidebar-page §4.1). */
|
|
11945
|
+
const SHOP_PANEL = "shop";
|
|
11227
11946
|
/** How long a stashed catalog may satisfy a plain open. The host applies the
|
|
11228
11947
|
* same five-minute freshness window, so the client stash must not outlive it. */
|
|
11229
11948
|
const WARM_TTL_MS = 3e5;
|
|
11230
11949
|
/** The boot-time warm fetch: the catalog the tab wants on its first open.
|
|
11231
11950
|
* Started in apply — the client bundle boots with the web app, long before
|
|
11232
|
-
* the user
|
|
11951
|
+
* the user opens the shop — so the host's slow network fetch runs while
|
|
11233
11952
|
* nobody is looking at the shop, and the tab's mount consumes this promise
|
|
11234
11953
|
* instead of starting its own fetch. A rejection stays stored (the injected
|
|
11235
11954
|
* catalog falls back to a fresh call); the extra catch keeps the
|
|
@@ -11247,7 +11966,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11247
11966
|
* uninstall made from another tab, another window, or the CLI is never added
|
|
11248
11967
|
* here and stays unknown to this page until it reloads. */
|
|
11249
11968
|
let pageRemoved = /* @__PURE__ */ new Set();
|
|
11250
|
-
/** Services required by the
|
|
11969
|
+
/** Services required by the page, row and Plugins page button registrations
|
|
11970
|
+
* and the Remote mount.
|
|
11251
11971
|
*
|
|
11252
11972
|
* `remote.shop` is deliberately ABSENT: this package both mounts its own
|
|
11253
11973
|
* Remote (the assembly does not know it, §7.3) and consumes it, and cordis
|
|
@@ -11262,8 +11982,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11262
11982
|
"slots",
|
|
11263
11983
|
"locale"
|
|
11264
11984
|
];
|
|
11265
|
-
/** Mount the shop Remote and contribute the shop's
|
|
11985
|
+
/** Mount the shop Remote and contribute the shop's page, its sidebar row and
|
|
11986
|
+
* its buttons on dsh's Plugins page. */
|
|
11266
11987
|
async function apply(ctx) {
|
|
11988
|
+
const predecessor = readPredecessor();
|
|
11267
11989
|
ctx.effect(() => ctx.locale.register(NS, {
|
|
11268
11990
|
zh,
|
|
11269
11991
|
en
|
|
@@ -11302,7 +12024,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11302
12024
|
}
|
|
11303
12025
|
return result;
|
|
11304
12026
|
};
|
|
11305
|
-
version()
|
|
12027
|
+
const firstAnswer = version();
|
|
12028
|
+
firstAnswer.catch(() => {});
|
|
11306
12029
|
ctx.on("connection/reset", () => {
|
|
11307
12030
|
version().catch(() => {});
|
|
11308
12031
|
});
|
|
@@ -11335,29 +12058,62 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11335
12058
|
};
|
|
11336
12059
|
return fresh;
|
|
11337
12060
|
};
|
|
12061
|
+
let listingRead = null;
|
|
12062
|
+
const listing = async () => {
|
|
12063
|
+
const warm = warmCatalog;
|
|
12064
|
+
if (warm !== null) try {
|
|
12065
|
+
return await warm.result;
|
|
12066
|
+
} catch {}
|
|
12067
|
+
listingRead ??= hostCatalog().finally(() => {
|
|
12068
|
+
listingRead = null;
|
|
12069
|
+
});
|
|
12070
|
+
return listingRead;
|
|
12071
|
+
};
|
|
11338
12072
|
const handOver = async (result) => ({
|
|
11339
12073
|
...result,
|
|
11340
12074
|
incompatible: await refineAgainstModuleTable(result.incompatible ?? {}, ctx.get("modules"), pageRemoved)
|
|
11341
12075
|
});
|
|
11342
12076
|
const restart = async () => unwrap(await ns.restart());
|
|
11343
12077
|
withdrawRestart = registerRestartCarrier(restart);
|
|
12078
|
+
const install = async (args) => {
|
|
12079
|
+
warmCatalog = null;
|
|
12080
|
+
return unwrap(await ns.installStart(args));
|
|
12081
|
+
};
|
|
12082
|
+
const installStatus = async (args) => unwrap(await ns.installStatus(args));
|
|
12083
|
+
const uninstall = async (args) => {
|
|
12084
|
+
warmCatalog = null;
|
|
12085
|
+
return unwrap(await ns.uninstallStart(args));
|
|
12086
|
+
};
|
|
12087
|
+
const updateStart = async (args) => {
|
|
12088
|
+
warmCatalog = null;
|
|
12089
|
+
return unwrap(await ns.updateStart(args));
|
|
12090
|
+
};
|
|
12091
|
+
const noteUninstalled = (name) => {
|
|
12092
|
+
pageRemoved.add(name);
|
|
12093
|
+
};
|
|
12094
|
+
const page = createPageStore(() => ({
|
|
12095
|
+
install,
|
|
12096
|
+
uninstall,
|
|
12097
|
+
updateStart,
|
|
12098
|
+
installStatus,
|
|
12099
|
+
noteUninstalled
|
|
12100
|
+
}), { pollUnwatched: true });
|
|
12101
|
+
disposeOccupant(installPageStore(page));
|
|
12102
|
+
ctx.effect(() => () => {
|
|
12103
|
+
page.dispose();
|
|
12104
|
+
}, "dsh-plugin-shop: page store");
|
|
12105
|
+
restartListeners.add(() => {
|
|
12106
|
+
page.noteRestarted();
|
|
12107
|
+
});
|
|
11344
12108
|
const injected = () => ({
|
|
11345
12109
|
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)),
|
|
12110
|
+
install,
|
|
12111
|
+
installStatus,
|
|
11351
12112
|
setEnabled: async (args) => unwrap(await ns.setEnabled(args)),
|
|
11352
12113
|
installed: async () => unwrap(await ns.installed()),
|
|
11353
12114
|
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
|
-
},
|
|
12115
|
+
uninstall,
|
|
12116
|
+
noteUninstalled,
|
|
11361
12117
|
restart,
|
|
11362
12118
|
version,
|
|
11363
12119
|
onRestarted: (listener) => {
|
|
@@ -11366,22 +12122,51 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
11366
12122
|
restartListeners.delete(listener);
|
|
11367
12123
|
};
|
|
11368
12124
|
},
|
|
11369
|
-
updateStart
|
|
11370
|
-
|
|
11371
|
-
return unwrap(await ns.updateStart(args));
|
|
11372
|
-
}
|
|
12125
|
+
updateStart,
|
|
12126
|
+
page
|
|
11373
12127
|
});
|
|
11374
|
-
|
|
11375
|
-
|
|
11376
|
-
|
|
11377
|
-
|
|
11378
|
-
|
|
12128
|
+
const layout = () => ctx.get("layout");
|
|
12129
|
+
const selectShopPage = () => {
|
|
12130
|
+
const service = layout();
|
|
12131
|
+
if (typeof service?.selectPanel !== "function") return;
|
|
12132
|
+
try {
|
|
12133
|
+
service.selectPanel(SHOP_PANEL);
|
|
12134
|
+
} catch {}
|
|
12135
|
+
};
|
|
12136
|
+
ctx.slots.inject("main", () => [ctx.slots.register({
|
|
12137
|
+
name: "main",
|
|
12138
|
+
key: SHOP_PANEL,
|
|
11379
12139
|
locale: NS,
|
|
11380
12140
|
inject: injected
|
|
11381
|
-
},
|
|
12141
|
+
}, ShopPage), () => {
|
|
12142
|
+
page.recordTeardown();
|
|
12143
|
+
}]);
|
|
12144
|
+
ctx.slots.inject("sidebar.panellist", () => ctx.slots.register({
|
|
12145
|
+
name: "sidebar.panellist",
|
|
12146
|
+
id: SHOP_PANEL,
|
|
12147
|
+
order: 5,
|
|
12148
|
+
label: () => ctx.locale.bind(NS)("title"),
|
|
12149
|
+
locale: NS
|
|
12150
|
+
}, ShopGlyph));
|
|
12151
|
+
ctx.slots.inject("plugins.detail.actions", () => ctx.slots.register({
|
|
12152
|
+
name: "plugins.detail.actions",
|
|
12153
|
+
id: SHOP_PANEL,
|
|
12154
|
+
order: 0,
|
|
12155
|
+
locale: NS,
|
|
12156
|
+
inject: () => ({
|
|
12157
|
+
listing,
|
|
12158
|
+
canOpen: () => typeof layout()?.selectPanel === "function",
|
|
12159
|
+
openShop: selectShopPage,
|
|
12160
|
+
page
|
|
12161
|
+
})
|
|
12162
|
+
}, ShopDetailAction));
|
|
12163
|
+
if (predecessor.kind === "section" || predecessor.kind === "page" && predecessor.showing) firstAnswer.then((answer) => {
|
|
12164
|
+
if (updateAwaitsRestart(answer)) selectShopPage();
|
|
12165
|
+
}, () => {});
|
|
11382
12166
|
}
|
|
11383
12167
|
//#endregion
|
|
11384
12168
|
exports.NS = NS;
|
|
12169
|
+
exports.SHOP_PANEL = SHOP_PANEL;
|
|
11385
12170
|
exports.WARM_TTL_MS = WARM_TTL_MS;
|
|
11386
12171
|
exports.apply = apply;
|
|
11387
12172
|
exports.inject = inject;
|