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 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 Settings section. */
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 in Settings (design §5.1, amended 2026-10-15): its nav
4768
- * entry and its page title, one key so the two cannot drift apart. */
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 shop's Settings page intro line (design §5.1, amended 2026-10-15),
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 tab clears these once the
7192
- * answers come from another process, and nothing else — a reload is still
7193
- * owed by a page nobody reloaded, a failure still happened, and a flow still
7194
- * running settles by its own poll.
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
- for (const listener of [...store.listeners]) listener();
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
- const store = monitorStore();
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
- * One keyed registry of install-shaped flows: start, poll to terminal, reset.
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 `begin` parameter,
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
- /** Shared idle view, so every key WITHOUT a flow yields one object rather
7852
- * than a fresh literal per call. That is what lets `flowFor` below answer
7853
- * "did this key change?" by identity. */
7854
- const IDLE_VIEW = { kind: "idle" };
7855
- /**
7856
- * @param begin the starting RPC and its answer, mapped to the first view.
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
- setViews((current) => {
7907
- if (!current.has(key)) return current;
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
- setViews((current) => {
7915
- let next = null;
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
- /** Self-update driving hook for the shop itself: start, poll to terminal.
8094
- * Shares the install view machine and poll loop; the start mapping is the
8095
- * uninstall one — a business refusal (a non-semver version) lands in the
8096
- * `failed` view with the host's detail, never in the install `rejected`
8097
- * codes. */
8098
- /** Drive one self-update: the host's business union, the shared polling
8099
- * loop, and terminal states. The `rejected` install state never occurs. */
8100
- function useUpdateSelf(updateStart, installStatus) {
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 ? _dsh_plugin_shop_css_c4b688e1_default.foldBlockExpanded : _dsh_plugin_shop_css_c4b688e1_default.foldBlock,
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: _dsh_plugin_shop_css_c4b688e1_default.foldToggle,
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 = `${_dsh_plugin_shop_css_c4b688e1_default.panel} notranslate`;
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 ? _dsh_plugin_shop_css_c4b688e1_default.chevronOpen : _dsh_plugin_shop_css_c4b688e1_default.chevron,
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, restarts }) {
9499
- const [open, setOpen] = (0, react.useState)(false);
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: _dsh_plugin_shop_css_c4b688e1_default.card,
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: _dsh_plugin_shop_css_c4b688e1_default.cardSpine,
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: _dsh_plugin_shop_css_c4b688e1_default.entryHeader,
10002
+ className: _dsh_plugin_shop_css_5f753521_default.entryHeader,
9528
10003
  "aria-expanded": open,
9529
10004
  "aria-controls": detailId,
9530
- onClick: () => setOpen((current) => !current),
10005
+ onClick: () => {
10006
+ page.toggleExpanded(key);
10007
+ rerender();
10008
+ },
9531
10009
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9532
- className: _dsh_plugin_shop_css_c4b688e1_default.name,
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: _dsh_plugin_shop_css_c4b688e1_default.badges,
10013
+ className: _dsh_plugin_shop_css_5f753521_default.badges,
9536
10014
  children: [
9537
10015
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
9538
- className: _dsh_plugin_shop_css_c4b688e1_default.categoryBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.sourceBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.sourceBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.cardVersion,
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: _dsh_plugin_shop_css_c4b688e1_default.tierBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.tierBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.starsBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.body,
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.summary} ${_dsh_plugin_shop_css_c4b688e1_default.summaryExpanded}` : _dsh_plugin_shop_css_c4b688e1_default.summary,
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.summaryZh} ${_dsh_plugin_shop_css_c4b688e1_default.summaryZhExpanded}` : _dsh_plugin_shop_css_c4b688e1_default.summaryZh,
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: _dsh_plugin_shop_css_c4b688e1_default.incompatibleDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.capabilitiesBlock,
10099
+ className: _dsh_plugin_shop_css_5f753521_default.capabilitiesBlock,
9622
10100
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9623
- className: _dsh_plugin_shop_css_c4b688e1_default.capabilitiesNote,
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: _dsh_plugin_shop_css_c4b688e1_default.capabilities,
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: _dsh_plugin_shop_css_c4b688e1_default.detail,
10110
+ className: _dsh_plugin_shop_css_5f753521_default.detail,
9633
10111
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("dl", {
9634
- className: _dsh_plugin_shop_css_c4b688e1_default.detailRows,
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: _dsh_plugin_shop_css_c4b688e1_default.detailRow,
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: _dsh_plugin_shop_css_c4b688e1_default.detailRow,
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: _dsh_plugin_shop_css_c4b688e1_default.detailRow,
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: _dsh_plugin_shop_css_c4b688e1_default.reviewedLine,
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: _dsh_plugin_shop_css_c4b688e1_default.cardActions,
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: _dsh_plugin_shop_css_c4b688e1_default.installedLabel,
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: _dsh_plugin_shop_css_c4b688e1_default.cardMeta,
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: _dsh_plugin_shop_css_c4b688e1_default.shelfDate,
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: _dsh_plugin_shop_css_c4b688e1_default.size,
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: _dsh_plugin_shop_css_c4b688e1_default.author,
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: _dsh_plugin_shop_css_c4b688e1_default.remedy,
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: _dsh_plugin_shop_css_c4b688e1_default.remedyLead,
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: _dsh_plugin_shop_css_c4b688e1_default.allowCommand,
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: _dsh_plugin_shop_css_c4b688e1_default.incompatibleBadge,
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 `useInstallFlows`. Shared by the catalog cards
9852
- * (`variant: 'install'`) and the outdated rows' update button (`variant:
9853
- * 'update'`, which drives the same install flow for `name@latest`).
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 in `useKeyedFlows`, which keeps a second press from
9860
- * becoming a second RPC. The page states the dialog's facts nowhere since
9861
- * 2026-10-15 (the shop design's §7 amendment of that date). */
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: _dsh_plugin_shop_css_c4b688e1_default.installPanel,
10352
+ className: _dsh_plugin_shop_css_5f753521_default.installPanel,
9866
10353
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9867
- className: _dsh_plugin_shop_css_c4b688e1_default.installing,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.installedActions,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.installPanel,
10404
+ className: _dsh_plugin_shop_css_5f753521_default.installPanel,
9911
10405
  children: [
9912
10406
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9913
- className: _dsh_plugin_shop_css_c4b688e1_default.failedHeading,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.installPanel,
9932
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9933
- className: _dsh_plugin_shop_css_c4b688e1_default.rejectedCode,
9934
- children: t(rejectionCodeKey(view.code))
9935
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
9936
- className: _dsh_plugin_shop_css_c4b688e1_default.rejectedDetail,
9937
- children: view.detail
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: _dsh_plugin_shop_css_c4b688e1_default.refusedLabel,
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: _dsh_plugin_shop_css_c4b688e1_default.gateWarning,
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: _dsh_plugin_shop_css_c4b688e1_default.installButton,
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: _dsh_plugin_shop_css_c4b688e1_default.gateWarning,
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: _dsh_plugin_shop_css_c4b688e1_default.installPanel,
10514
+ className: _dsh_plugin_shop_css_5f753521_default.installPanel,
10000
10515
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10001
- className: _dsh_plugin_shop_css_c4b688e1_default.installing,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.installedActions,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.cancelButton,
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: _dsh_plugin_shop_css_c4b688e1_default.installPanel,
10561
+ className: _dsh_plugin_shop_css_5f753521_default.installPanel,
10047
10562
  children: [
10048
10563
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10049
- className: _dsh_plugin_shop_css_c4b688e1_default.failedHeading,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.uninstallButton,
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: _dsh_plugin_shop_css_c4b688e1_default.uninstallButton,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.gate,
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: _dsh_plugin_shop_css_c4b688e1_default.gateTitle,
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: _dsh_plugin_shop_css_c4b688e1_default.gateBody,
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: _dsh_plugin_shop_css_c4b688e1_default.gateActions,
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: _dsh_plugin_shop_css_c4b688e1_default.confirmButton,
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: _dsh_plugin_shop_css_c4b688e1_default.cancelButton,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.restartButton,
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: _dsh_plugin_shop_css_c4b688e1_default.selfUpdatePanel,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.reloadPanel,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.reloadButton,
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, restarts }) {
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 [owed, setOwed] = (0, react.useState)("nothing");
10253
- const restartsSeen = (0, react.useRef)(restarts);
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({ kind: "saved" });
10273
- if (result.activation === "restart") setOwed("restart");
10274
- else if (result.activation !== "live") setOwed((previous) => previous === "restart" ? previous : "reload");
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: _dsh_plugin_shop_css_c4b688e1_default.switchWrap,
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: `${_dsh_plugin_shop_css_c4b688e1_default.switch} ${enabled ? _dsh_plugin_shop_css_c4b688e1_default.switchOn : ""}`,
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: _dsh_plugin_shop_css_c4b688e1_default.switchKnob })
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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, restarts }) {
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedRow,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedInfo,
10857
+ className: _dsh_plugin_shop_css_5f753521_default.outdatedInfo,
10339
10858
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10340
- className: _dsh_plugin_shop_css_c4b688e1_default.name,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedVersions,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedActions,
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, restarts }) {
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: _dsh_plugin_shop_css_c4b688e1_default.stateLine,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedSection,
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: _dsh_plugin_shop_css_c4b688e1_default.updatableHeading,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedList,
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
- restarts
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: _dsh_plugin_shop_css_c4b688e1_default.shadowedSection,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedList,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedRow,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedInfo,
10971
+ className: _dsh_plugin_shop_css_5f753521_default.outdatedInfo,
10452
10972
  children: [
10453
10973
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10454
- className: _dsh_plugin_shop_css_c4b688e1_default.name,
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: _dsh_plugin_shop_css_c4b688e1_default.badges,
10978
+ className: _dsh_plugin_shop_css_5f753521_default.badges,
10459
10979
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
10460
- className: _dsh_plugin_shop_css_c4b688e1_default.tierBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.shadowedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.outdatedActions,
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: _dsh_plugin_shop_css_c4b688e1_default.stateLine,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.actionButton,
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 `ShopSection` renders it
10539
- * below the page's title and intro (the settings slot registers `ShopSection`,
10540
- * `index.ts`). Exported under the name the body used to carry, so nothing can
10541
- * render the body without the boundary. */
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 selfUpdate = useUpdateSelf(updateStart, installStatus);
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.pendingVersion !== null;
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)({ kind: "initial" });
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 [mutations, setMutations] = (0, react.useState)(0);
10572
- const noteMutation = (0, react.useCallback)(() => {
10573
- setMutations((current) => current + 1);
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
- resetSelfUpdate(settledByRestart);
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
- resetSelfUpdate,
10610
- resetInstalls,
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) setCatalogState({
10634
- kind: "ready",
10635
- result
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 !== "reverdict") setReloadFailed(true);
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
- }, [catalog, request]);
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
- request,
10711
- mutations
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) => cleared.has(entry.name) || !isShopLike(entry.name) && (entry.repo === void 0 || !isShopLike(entry.repo)));
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)) setVisibleCount((current) => nextVisibleCount(current, filteredLenRef.current, 48));
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: _dsh_plugin_shop_css_c4b688e1_default.srOnly,
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: _dsh_plugin_shop_css_c4b688e1_default.skeletonGrid,
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: _dsh_plugin_shop_css_c4b688e1_default.skeletonCard,
11383
+ className: _dsh_plugin_shop_css_5f753521_default.skeletonCard,
10836
11384
  children: [
10837
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4b688e1_default.skeletonName }),
10838
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4b688e1_default.skeletonSummary }),
10839
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4b688e1_default.skeletonSummaryShort }),
10840
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { className: _dsh_plugin_shop_css_c4b688e1_default.skeletonActions })
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: _dsh_plugin_shop_css_c4b688e1_default.stateLine,
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: _dsh_plugin_shop_css_c4b688e1_default.actionButton,
11402
+ className: _dsh_plugin_shop_css_5f753521_default.actionButton,
10855
11403
  onClick: () => {
10856
- setRequest({ kind: "retry" });
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: _dsh_plugin_shop_css_c4b688e1_default.toolbar,
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: _dsh_plugin_shop_css_c4b688e1_default.searchInput,
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
- setQuery(event.target.value);
10879
- setVisibleCount(48);
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: _dsh_plugin_shop_css_c4b688e1_default.expansionNote,
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: _dsh_plugin_shop_css_c4b688e1_default.toolbarRight,
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: _dsh_plugin_shop_css_c4b688e1_default.staleBadge,
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: _dsh_plugin_shop_css_c4b688e1_default.versionBlock,
11448
+ className: _dsh_plugin_shop_css_5f753521_default.versionBlock,
10896
11449
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
10897
- className: _dsh_plugin_shop_css_c4b688e1_default.versionText,
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: _dsh_plugin_shop_css_c4b688e1_default.restartSelfButton,
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: _dsh_plugin_shop_css_c4b688e1_default.updateSelfButton,
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: _dsh_plugin_shop_css_c4b688e1_default.checkUpdateButton,
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: _dsh_plugin_shop_css_c4b688e1_default.githubLink,
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: _dsh_plugin_shop_css_c4b688e1_default.selfUpdatePanel,
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: _dsh_plugin_shop_css_c4b688e1_default.installing,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.selfUpdatePanel,
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: _dsh_plugin_shop_css_c4b688e1_default.installedActions,
11521
+ className: _dsh_plugin_shop_css_5f753521_default.installedActions,
10969
11522
  children: [
10970
11523
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
10971
- className: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.notice,
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: _dsh_plugin_shop_css_c4b688e1_default.selfUpdatePanel,
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: _dsh_plugin_shop_css_c4b688e1_default.failedHeading,
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: _dsh_plugin_shop_css_c4b688e1_default.log,
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: _dsh_plugin_shop_css_c4b688e1_default.logLine,
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: _dsh_plugin_shop_css_c4b688e1_default.failedDetail,
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: _dsh_plugin_shop_css_c4b688e1_default.categoryBar,
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.categoryButton} ${_dsh_plugin_shop_css_c4b688e1_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4b688e1_default.categoryButton,
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
- setCategory(null);
11024
- setVisibleCount(48);
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.categoryButton} ${_dsh_plugin_shop_css_c4b688e1_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4b688e1_default.categoryButton,
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
- setCategory(key);
11041
- setVisibleCount(48);
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" ? `${_dsh_plugin_shop_css_c4b688e1_default.categoryButton} ${_dsh_plugin_shop_css_c4b688e1_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4b688e1_default.categoryButton,
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
- setCategory("installed");
11056
- setVisibleCount(48);
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.incompatibleFilter} ${_dsh_plugin_shop_css_c4b688e1_default.incompatibleFilterOn}` : _dsh_plugin_shop_css_c4b688e1_default.incompatibleFilter,
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
- setHideIncompatible((current) => !current);
11073
- setVisibleCount(48);
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.switch} ${_dsh_plugin_shop_css_c4b688e1_default.switchOn}` : _dsh_plugin_shop_css_c4b688e1_default.switch,
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: _dsh_plugin_shop_css_c4b688e1_default.switchKnob })
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: _dsh_plugin_shop_css_c4b688e1_default.catalogStatsRow,
11654
+ className: _dsh_plugin_shop_css_5f753521_default.catalogStatsRow,
11085
11655
  children: [
11086
11656
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
11087
- className: _dsh_plugin_shop_css_c4b688e1_default.catalogStats,
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: _dsh_plugin_shop_css_c4b688e1_default.catalogRefreshButton,
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({ kind: "refresh" });
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: _dsh_plugin_shop_css_c4b688e1_default.catalogRefreshFailed,
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: _dsh_plugin_shop_css_c4b688e1_default.sortGroup,
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: _dsh_plugin_shop_css_c4b688e1_default.sortLabel,
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 ? `${_dsh_plugin_shop_css_c4b688e1_default.categoryButton} ${_dsh_plugin_shop_css_c4b688e1_default.categoryButtonOn}` : _dsh_plugin_shop_css_c4b688e1_default.categoryButton,
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
- setOrder(name);
11128
- setVisibleCount(48);
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: _dsh_plugin_shop_css_c4b688e1_default.emptyLine,
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: _dsh_plugin_shop_css_c4b688e1_default.emptyLine,
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: _dsh_plugin_shop_css_c4b688e1_default.cards,
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", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
11147
- entry,
11148
- stars: starsOf(entry, stars),
11149
- order,
11150
- shelfDay: shelfDayOf(entry),
11151
- installed: installedByKey.get(key),
11152
- missing: missingByKey.get(key) ?? [],
11153
- harness: harnessVerdictOf(harnessVerdicts, key),
11154
- nameTakenBy: nameTakenByKey.get(key),
11155
- t,
11156
- flow: flows.flowFor(key),
11157
- uninstallFlow: uninstallFlows.flowFor(key),
11158
- restart,
11159
- restartBlocked,
11160
- reload,
11161
- setEnabled,
11162
- inInstalledView: category === "installed",
11163
- restarts
11164
- }) }, key);
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: _dsh_plugin_shop_css_c4b688e1_default.cardsSentry,
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: _dsh_plugin_shop_css_c4b688e1_default.showingLine,
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
- restarts
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/ShopSection.tsx
11205
- function ShopSection(props) {
11206
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
11207
- className: _dsh_plugin_shop_css_c4b688e1_default.section,
11208
- children: [
11209
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
11210
- className: _dsh_plugin_shop_css_c4b688e1_default.sectionTitle,
11211
- "data-shop-section-title": true,
11212
- children: props.t("title")
11213
- }),
11214
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
11215
- className: _dsh_plugin_shop_css_c4b688e1_default.sectionIntro,
11216
- "data-shop-section-intro": true,
11217
- children: props.t("intro")
11218
- }),
11219
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ShopTab, { ...props })
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 reaches Settings — so the host's slow network fetch runs while
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 tab registration and the Remote mount.
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 Settings section. */
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().catch(() => {});
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: async (args) => {
11347
- warmCatalog = null;
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: async (args) => {
11355
- warmCatalog = null;
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: async (args) => {
11370
- warmCatalog = null;
11371
- return unwrap(await ns.updateStart(args));
11372
- }
12125
+ updateStart,
12126
+ page
11373
12127
  });
11374
- ctx.slots.inject("settings.section", () => ctx.slots.register({
11375
- name: "settings.section",
11376
- id: "shop",
11377
- order: 17,
11378
- label: () => ctx.locale.bind(NS)("title"),
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
- }, ShopSection));
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;