dsh-plugin-shop 0.7.4 → 0.8.0-beta.0
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 +251 -123
- package/lib/index.js +648 -142
- package/lib/typert.host.js +19 -13
- package/lib/typert.remote-client.js +16 -10
- package/lib/types/client/index.d.ts +3 -0
- package/lib/types/client/locales.d.ts +2 -0
- package/lib/types/client/present.d.ts +4 -2
- package/lib/types/client/useInstall.d.ts +18 -0
- package/lib/types/host/executor.d.ts +18 -0
- package/lib/types/host/hot.d.ts +5 -0
- package/lib/types/host/index.d.ts +44 -10
- package/lib/types/host/install.d.ts +9 -6
- package/lib/types/host/npm-origin.d.ts +4 -0
- package/lib/types/host/origin.d.ts +7 -1
- package/lib/types/host/peers.d.ts +75 -4
- package/lib/types/host/repo-pins.d.ts +1 -1
- package/lib/types/host/restart.d.ts +22 -2
- package/lib/types/host/self-update.d.ts +12 -4
- package/lib/types/own-version.d.ts +10 -0
- package/lib/types/shared/identity.d.ts +27 -0
- package/package.json +9 -9
package/lib/client.js
CHANGED
|
@@ -4324,6 +4324,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4324
4324
|
});
|
|
4325
4325
|
const dsh_plugin_shop_shop_installed_result$schema = array(object({
|
|
4326
4326
|
"name": string(),
|
|
4327
|
+
"source": union([literal("npm"), literal("github")]),
|
|
4328
|
+
"repo": union([_undefined(), string()]).optional(),
|
|
4329
|
+
"subdir": union([_undefined(), string()]).optional(),
|
|
4327
4330
|
"installed": string(),
|
|
4328
4331
|
"latest": string(),
|
|
4329
4332
|
"outdated": boolean(),
|
|
@@ -4336,7 +4339,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4336
4339
|
_undefined(),
|
|
4337
4340
|
literal(false),
|
|
4338
4341
|
literal(true)
|
|
4339
|
-
]).optional()
|
|
4342
|
+
]).optional(),
|
|
4343
|
+
"source": union([
|
|
4344
|
+
_undefined(),
|
|
4345
|
+
literal("npm"),
|
|
4346
|
+
literal("github")
|
|
4347
|
+
]).optional(),
|
|
4348
|
+
"repo": union([_undefined(), string()]).optional(),
|
|
4349
|
+
"subdir": union([_undefined(), string()]).optional()
|
|
4340
4350
|
});
|
|
4341
4351
|
const dsh_plugin_shop_shop_installStart_result$schema = union([object({
|
|
4342
4352
|
"ok": literal(true),
|
|
@@ -4348,7 +4358,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4348
4358
|
literal("denied"),
|
|
4349
4359
|
literal("version-mismatch"),
|
|
4350
4360
|
literal("needs-acknowledgement"),
|
|
4351
|
-
literal("tarball-integrity")
|
|
4361
|
+
literal("tarball-integrity"),
|
|
4362
|
+
literal("ambiguous-identity")
|
|
4352
4363
|
]),
|
|
4353
4364
|
"detail": string()
|
|
4354
4365
|
})]);
|
|
@@ -4437,7 +4448,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4437
4448
|
},
|
|
4438
4449
|
sourceLocation: {
|
|
4439
4450
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4440
|
-
"line":
|
|
4451
|
+
"line": 678,
|
|
4441
4452
|
"column": 9
|
|
4442
4453
|
}
|
|
4443
4454
|
},
|
|
@@ -4455,7 +4466,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4455
4466
|
},
|
|
4456
4467
|
sourceLocation: {
|
|
4457
4468
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4458
|
-
"line":
|
|
4469
|
+
"line": 863,
|
|
4459
4470
|
"column": 9
|
|
4460
4471
|
}
|
|
4461
4472
|
},
|
|
@@ -4483,7 +4494,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4483
4494
|
},
|
|
4484
4495
|
sourceLocation: {
|
|
4485
4496
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4486
|
-
"line":
|
|
4497
|
+
"line": 726,
|
|
4487
4498
|
"column": 9
|
|
4488
4499
|
}
|
|
4489
4500
|
},
|
|
@@ -4510,7 +4521,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4510
4521
|
},
|
|
4511
4522
|
sourceLocation: {
|
|
4512
4523
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4513
|
-
"line":
|
|
4524
|
+
"line": 852,
|
|
4514
4525
|
"column": 3
|
|
4515
4526
|
}
|
|
4516
4527
|
},
|
|
@@ -4528,7 +4539,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4528
4539
|
},
|
|
4529
4540
|
sourceLocation: {
|
|
4530
4541
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4531
|
-
"line":
|
|
4542
|
+
"line": 1021,
|
|
4532
4543
|
"column": 9
|
|
4533
4544
|
}
|
|
4534
4545
|
},
|
|
@@ -4555,7 +4566,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4555
4566
|
},
|
|
4556
4567
|
sourceLocation: {
|
|
4557
4568
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4558
|
-
"line":
|
|
4569
|
+
"line": 535,
|
|
4559
4570
|
"column": 9
|
|
4560
4571
|
}
|
|
4561
4572
|
},
|
|
@@ -4583,7 +4594,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4583
4594
|
},
|
|
4584
4595
|
sourceLocation: {
|
|
4585
4596
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4586
|
-
"line":
|
|
4597
|
+
"line": 956,
|
|
4587
4598
|
"column": 9
|
|
4588
4599
|
}
|
|
4589
4600
|
},
|
|
@@ -4610,7 +4621,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4610
4621
|
},
|
|
4611
4622
|
sourceLocation: {
|
|
4612
4623
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4613
|
-
"line":
|
|
4624
|
+
"line": 1106,
|
|
4614
4625
|
"column": 9
|
|
4615
4626
|
}
|
|
4616
4627
|
},
|
|
@@ -4628,7 +4639,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4628
4639
|
},
|
|
4629
4640
|
sourceLocation: {
|
|
4630
4641
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4631
|
-
"line":
|
|
4642
|
+
"line": 1089,
|
|
4632
4643
|
"column": 9
|
|
4633
4644
|
}
|
|
4634
4645
|
}
|
|
@@ -4709,6 +4720,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4709
4720
|
return hasPlugin || SHOP_KEYWORDS.includes(segments.at(-1));
|
|
4710
4721
|
}
|
|
4711
4722
|
//#endregion
|
|
4723
|
+
//#region src/shared/identity.ts
|
|
4724
|
+
/** The registry's uniqueness rule, shared with the presentation layer. */
|
|
4725
|
+
function identityKey(identity) {
|
|
4726
|
+
return identity.source === "npm" ? `npm:${identity.name}` : `github:${identity.repo ?? identity.name}#${identity.subdir ?? ""}`;
|
|
4727
|
+
}
|
|
4728
|
+
/** `owner/slug`, lowercased, or null when this is not a repository reference. */
|
|
4729
|
+
const REPO = "([\\w.-]+)\\/([\\w.-]+?)";
|
|
4730
|
+
new RegExp(`^github:${REPO}(?:[#&].*)?$`);
|
|
4731
|
+
new RegExp(`^(?:git\\+)?https?:\\/\\/(?:www\\.)?github\\.com\\/${REPO}(?:\\.git)?(?:[\\/?#].*)?$`);
|
|
4732
|
+
//#endregion
|
|
4712
4733
|
//#region src/client/present.ts
|
|
4713
4734
|
/** Hot-mount reason code → locale key, so the notice reads in the language
|
|
4714
4735
|
* the person set in dsh. An absent (or unrecognized) reason keeps the generic
|
|
@@ -4733,9 +4754,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4733
4754
|
}
|
|
4734
4755
|
/** The peers the Host said this installation does not provide, or none when it
|
|
4735
4756
|
* said nothing — a plugin that runs here and one the Host could not judge are
|
|
4736
|
-
* both rendered as no warning at all.
|
|
4737
|
-
|
|
4738
|
-
|
|
4757
|
+
* both rendered as no warning at all. The map is keyed by install identity,
|
|
4758
|
+
* never by name, because two entries can share a name. */
|
|
4759
|
+
function missingPeersOf(incompatible, key) {
|
|
4760
|
+
return incompatible[key] ?? [];
|
|
4739
4761
|
}
|
|
4740
4762
|
/** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
|
|
4741
4763
|
* states the same facts in its own register; never soften this wording. */
|
|
@@ -4751,6 +4773,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4751
4773
|
case "version-mismatch": return "versionMismatchCode";
|
|
4752
4774
|
case "needs-acknowledgement": return "needsAcknowledgementCode";
|
|
4753
4775
|
case "tarball-integrity": return "tarballIntegrityCode";
|
|
4776
|
+
case "ambiguous-identity": return "ambiguousIdentityCode";
|
|
4754
4777
|
}
|
|
4755
4778
|
}
|
|
4756
4779
|
/** The version a card displays: full for npm, the short commit for github. */
|
|
@@ -4934,7 +4957,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4934
4957
|
* This mirrors the registry's identity verbatim; the two must not drift.
|
|
4935
4958
|
*/
|
|
4936
4959
|
function entryKey(entry) {
|
|
4937
|
-
return entry
|
|
4960
|
+
return identityKey(entry);
|
|
4938
4961
|
}
|
|
4939
4962
|
/**
|
|
4940
4963
|
* One entry's star count from the sidecar, or undefined when it has none.
|
|
@@ -4951,8 +4974,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4951
4974
|
* repo, one the catalog never even listed.
|
|
4952
4975
|
*/
|
|
4953
4976
|
function starsOf(entry, stars) {
|
|
4954
|
-
|
|
4955
|
-
|
|
4977
|
+
const key = entry.repo ?? entry.name;
|
|
4978
|
+
if (!Object.hasOwn(stars, key)) return void 0;
|
|
4979
|
+
return stars[key];
|
|
4980
|
+
}
|
|
4981
|
+
/** One collator for the whole shelf. Passing an options object to
|
|
4982
|
+
* localeCompare creates a collator per comparison, which dominates the sort
|
|
4983
|
+
* for a catalog with thousands of entries. */
|
|
4984
|
+
const NAME_COLLATOR = new Intl.Collator(void 0, { sensitivity: "base" });
|
|
4956
4985
|
/** Sort the shelf: stars descending, un-starred entries last, name ascending
|
|
4957
4986
|
* (case-insensitive) on ties (spec 2026-08-26-github-stars-design.md D1).
|
|
4958
4987
|
* Display-time only — the catalog's own name sort is untouched. */
|
|
@@ -4961,7 +4990,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4961
4990
|
return [...entries].sort((a, b) => {
|
|
4962
4991
|
const byStars = count(b) - count(a);
|
|
4963
4992
|
if (byStars !== 0) return byStars;
|
|
4964
|
-
return a.name
|
|
4993
|
+
return NAME_COLLATOR.compare(a.name, b.name);
|
|
4965
4994
|
});
|
|
4966
4995
|
}
|
|
4967
4996
|
/** 999 → "999"; 1000 → "1k"; 1234 → "1.2k"; 1500 → "1.5k"; 99999 → "100k". */
|
|
@@ -5030,6 +5059,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5030
5059
|
versionMismatchCode: "版本不匹配",
|
|
5031
5060
|
needsAcknowledgementCode: "需要确认",
|
|
5032
5061
|
tarballIntegrityCode: "压缩包完整性校验失败",
|
|
5062
|
+
ambiguousIdentityCode: "无法确定是哪一个条目",
|
|
5033
5063
|
npmSource: "npm 包",
|
|
5034
5064
|
githubSource: "源码在 GitHub",
|
|
5035
5065
|
installedSection: "已安装",
|
|
@@ -5119,6 +5149,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5119
5149
|
versionMismatchCode: "Version mismatch",
|
|
5120
5150
|
needsAcknowledgementCode: "Acknowledgement required",
|
|
5121
5151
|
tarballIntegrityCode: "tarball integrity check failed",
|
|
5152
|
+
ambiguousIdentityCode: "Ambiguous entry",
|
|
5122
5153
|
npmSource: "npm package",
|
|
5123
5154
|
githubSource: "source on GitHub",
|
|
5124
5155
|
installedSection: "Installed",
|
|
@@ -5176,42 +5207,93 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5176
5207
|
installStatus
|
|
5177
5208
|
]);
|
|
5178
5209
|
}
|
|
5179
|
-
/**
|
|
5180
|
-
*
|
|
5181
|
-
*
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5210
|
+
/**
|
|
5211
|
+
* Install flows owned by the tab and keyed by install identity. Keeping the
|
|
5212
|
+
* state above individual cards preserves a running operation when filtering
|
|
5213
|
+
* unmounts its card, and makes the shelf and Outdated panels agree.
|
|
5214
|
+
*/
|
|
5215
|
+
function useInstallFlows(install, installStatus, onSettled) {
|
|
5216
|
+
const [views, setViews] = (0, react.useState)(() => /* @__PURE__ */ new Map());
|
|
5217
|
+
const settled = (0, react.useRef)(onSettled);
|
|
5218
|
+
settled.current = onSettled;
|
|
5219
|
+
const put = (0, react.useCallback)((key, view) => {
|
|
5220
|
+
setViews((current) => {
|
|
5221
|
+
const next = new Map(current);
|
|
5222
|
+
next.set(key, view);
|
|
5223
|
+
return next;
|
|
5224
|
+
});
|
|
5225
|
+
}, []);
|
|
5226
|
+
const apply = (0, react.useCallback)((key, event) => {
|
|
5227
|
+
setViews((current) => {
|
|
5228
|
+
const before = current.get(key) ?? { kind: "idle" };
|
|
5229
|
+
const after = reduceInstall(before, event);
|
|
5230
|
+
if (after === before) return current;
|
|
5231
|
+
const next = new Map(current);
|
|
5232
|
+
next.set(key, after);
|
|
5233
|
+
return next;
|
|
5234
|
+
});
|
|
5235
|
+
}, []);
|
|
5236
|
+
const start = (0, react.useCallback)(async (key, args) => {
|
|
5237
|
+
put(key, { kind: "idle" });
|
|
5186
5238
|
try {
|
|
5187
5239
|
const result = await install(args);
|
|
5188
5240
|
if (!result.ok) {
|
|
5189
|
-
|
|
5241
|
+
put(key, {
|
|
5190
5242
|
kind: "rejected",
|
|
5191
5243
|
code: result.code,
|
|
5192
5244
|
detail: result.detail
|
|
5193
5245
|
});
|
|
5194
5246
|
return;
|
|
5195
5247
|
}
|
|
5196
|
-
|
|
5248
|
+
put(key, {
|
|
5197
5249
|
kind: "running",
|
|
5198
5250
|
installId: result.installId,
|
|
5199
5251
|
log: []
|
|
5200
5252
|
});
|
|
5201
5253
|
} catch {
|
|
5202
|
-
|
|
5254
|
+
put(key, {
|
|
5203
5255
|
kind: "failed",
|
|
5204
5256
|
detail: "",
|
|
5205
5257
|
log: []
|
|
5206
5258
|
});
|
|
5207
5259
|
}
|
|
5208
|
-
}, [install]);
|
|
5209
|
-
|
|
5210
|
-
|
|
5211
|
-
|
|
5260
|
+
}, [install, put]);
|
|
5261
|
+
const reset = (0, react.useCallback)((key) => {
|
|
5262
|
+
setViews((current) => {
|
|
5263
|
+
if (!current.has(key)) return current;
|
|
5264
|
+
const next = new Map(current);
|
|
5265
|
+
next.delete(key);
|
|
5266
|
+
return next;
|
|
5267
|
+
});
|
|
5268
|
+
}, []);
|
|
5269
|
+
(0, react.useEffect)(() => {
|
|
5270
|
+
const running = [];
|
|
5271
|
+
for (const [key, view] of views) if (view.kind === "running") running.push([key, view.installId]);
|
|
5272
|
+
if (running.length === 0) return;
|
|
5273
|
+
const timer = setInterval(() => {
|
|
5274
|
+
for (const [key, installId] of running) installStatus({ installId }).then((status) => {
|
|
5275
|
+
apply(key, {
|
|
5276
|
+
type: "status",
|
|
5277
|
+
status
|
|
5278
|
+
});
|
|
5279
|
+
if (status.found && status.state !== "running") settled.current?.(key);
|
|
5280
|
+
}, () => {});
|
|
5281
|
+
}, INSTALL_POLL_MS);
|
|
5282
|
+
return () => clearInterval(timer);
|
|
5283
|
+
}, [
|
|
5284
|
+
views,
|
|
5285
|
+
installStatus,
|
|
5286
|
+
apply
|
|
5287
|
+
]);
|
|
5288
|
+
return { flowFor: (0, react.useCallback)((key) => ({
|
|
5289
|
+
view: views.get(key) ?? { kind: "idle" },
|
|
5290
|
+
start: (args) => start(key, args),
|
|
5291
|
+
reset: () => reset(key)
|
|
5292
|
+
}), [
|
|
5293
|
+
views,
|
|
5212
5294
|
start,
|
|
5213
|
-
reset
|
|
5214
|
-
};
|
|
5295
|
+
reset
|
|
5296
|
+
]) };
|
|
5215
5297
|
}
|
|
5216
5298
|
//#endregion
|
|
5217
5299
|
//#region src/client/useUninstall.ts
|
|
@@ -5300,7 +5382,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5300
5382
|
}
|
|
5301
5383
|
//#endregion
|
|
5302
5384
|
//#region \0dsh-plugin-shop-css:e3675a89
|
|
5303
|
-
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.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* 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 `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_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.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_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.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 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.s96e910_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.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_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.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_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.sa04455_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.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_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.s614a38_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.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_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.sa987f7_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.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_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.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_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 `.s73242a_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. */\n.s64a5ff_reloadButton {\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.s64a5ff_reloadButton: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.s64a5ff_reloadButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s4453f3_reloadFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_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 .sf657f5_cards { animation: none; }\n}\n\n.s65f383_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.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='theme'] { --spine-hue: #F472B6; }\n.s65f383_card[data-category='other'] { --spine-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.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--spine-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(--spine-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_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.sa97234_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.sc3f149_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.s513026_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.s9b5048_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.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_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.sc6ec6e_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}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n color: var(--dsw-alias-state-error-primary);\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.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `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.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The publishing account, beside the npm link. Secondary label colour and a\n * middot separator carry the distinction — never a background fill, which is\n * invisible in the light theme where every bg-layer token is the same white. */\n/* Who published the entry, at the right edge of the action row. `margin-left:\n * auto` is what pushes it there; the row is a wrapping flex, so an active\n * install flow (which claims the full width) drops this to the next line\n * rather than squeezing it. */\n.s1ff66f_author {\n margin-left: auto;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_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.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s6ac237_restartButton {\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.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* 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 .s513026_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.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
|
|
5385
|
+
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.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* 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 `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_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.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_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.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 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.s96e910_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.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_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.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_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.sa04455_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.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_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.s614a38_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.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_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.sa987f7_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.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_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.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_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 `.s73242a_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. */\n.s64a5ff_reloadButton {\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.s64a5ff_reloadButton: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.s64a5ff_reloadButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s4453f3_reloadFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_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 .sf657f5_cards { animation: none; }\n}\n\n.s65f383_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.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='theme'] { --spine-hue: #F472B6; }\n.s65f383_card[data-category='other'] { --spine-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.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--spine-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(--spine-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_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.sa97234_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.sc3f149_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.s513026_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.s9b5048_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.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_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.sc6ec6e_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}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n color: var(--dsw-alias-state-error-primary);\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.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `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.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The publishing account, beside the npm link. Secondary label colour and a\n * middot separator carry the distinction — never a background fill, which is\n * invisible in the light theme where every bg-layer token is the same white. */\n/* Who published the entry, at the right edge of the action row. `margin-left:\n * auto` is what pushes it there; the row is a wrapping flex, so an active\n * install flow (which claims the full width) drops this to the next line\n * rather than squeezing it. */\n.s1ff66f_author {\n margin-left: auto;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_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.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s6ac237_restartButton {\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.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* 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 .s513026_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.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
|
|
5304
5386
|
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
|
|
5305
5387
|
const tag = document.createElement("style");
|
|
5306
5388
|
tag.dataset.pluginCss = "e3675a89";
|
|
@@ -5433,13 +5515,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5433
5515
|
* install controls. An installed plugin's card carries its installed row:
|
|
5434
5516
|
* current → the non-interactive installed label, behind → the update button;
|
|
5435
5517
|
* uninstalled → the install button. */
|
|
5436
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, t,
|
|
5518
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, t, flowFor, installStatus, uninstall, restart, restartSupported, setEnabled, onSettled }) {
|
|
5437
5519
|
const [open, setOpen] = (0, react.useState)(false);
|
|
5438
5520
|
const detailId = (0, react.useId)();
|
|
5439
5521
|
const summary = entry.catalog?.summary;
|
|
5440
5522
|
const npmUrl = npmPageUrl(entry);
|
|
5441
5523
|
const author = authorOf(entry);
|
|
5442
5524
|
const category = entry.catalog?.category ?? "other";
|
|
5525
|
+
const installTarget = {
|
|
5526
|
+
name: entry.name,
|
|
5527
|
+
version: entry.version,
|
|
5528
|
+
source: entry.source,
|
|
5529
|
+
repo: entry.repo,
|
|
5530
|
+
subdir: entry.subdir
|
|
5531
|
+
};
|
|
5532
|
+
const flow = flowFor(entryKey(entry));
|
|
5533
|
+
const uninstallSettled = (0, react.useCallback)(() => {
|
|
5534
|
+
flow.reset();
|
|
5535
|
+
onSettled();
|
|
5536
|
+
}, [flow, onSettled]);
|
|
5443
5537
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5444
5538
|
className: _dsh_plugin_shop_css_e3675a89_default.card,
|
|
5445
5539
|
"data-shop-entry": entry.name,
|
|
@@ -5506,12 +5600,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5506
5600
|
"data-tier": entry.tier,
|
|
5507
5601
|
children: t(tierKey(entry.tier))
|
|
5508
5602
|
}),
|
|
5509
|
-
missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5510
|
-
className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
|
|
5511
|
-
"data-shop-incompatible": true,
|
|
5512
|
-
title: t("incompatibleDetail", { modules: missing.join(", ") }),
|
|
5513
|
-
children: t("incompatibleBadge")
|
|
5514
|
-
}),
|
|
5515
5603
|
stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
5516
5604
|
className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
|
|
5517
5605
|
role: "img",
|
|
@@ -5595,34 +5683,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5595
5683
|
className: _dsh_plugin_shop_css_e3675a89_default.cardActions,
|
|
5596
5684
|
"data-shop-actions": true,
|
|
5597
5685
|
children: [installed === void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
5598
|
-
|
|
5599
|
-
version: entry.version,
|
|
5686
|
+
target: installTarget,
|
|
5600
5687
|
tier: entry.tier,
|
|
5601
5688
|
missing,
|
|
5602
5689
|
missingStated: true,
|
|
5690
|
+
flow,
|
|
5603
5691
|
t,
|
|
5604
|
-
install,
|
|
5605
|
-
installStatus,
|
|
5606
5692
|
restart,
|
|
5607
5693
|
restartSupported
|
|
5608
5694
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
|
|
5609
|
-
installed.outdated ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
5610
|
-
|
|
5611
|
-
version: entry.version,
|
|
5695
|
+
installed.outdated || flow.view.kind !== "idle" ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
5696
|
+
target: installTarget,
|
|
5612
5697
|
tier: entry.tier,
|
|
5613
|
-
variant: "update",
|
|
5698
|
+
variant: installed.outdated ? "update" : "install",
|
|
5614
5699
|
missing,
|
|
5615
5700
|
missingStated: true,
|
|
5701
|
+
flow,
|
|
5616
5702
|
t,
|
|
5617
|
-
install,
|
|
5618
|
-
installStatus,
|
|
5619
5703
|
restart,
|
|
5620
5704
|
restartSupported
|
|
5621
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5705
|
+
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
5622
5706
|
className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
|
|
5623
5707
|
"data-shop-installed": true,
|
|
5624
5708
|
children: t("installed")
|
|
5625
|
-
}),
|
|
5709
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IncompatibleBadge, {
|
|
5710
|
+
missing,
|
|
5711
|
+
t
|
|
5712
|
+
})] }),
|
|
5626
5713
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
|
|
5627
5714
|
row: installed,
|
|
5628
5715
|
t,
|
|
@@ -5634,7 +5721,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5634
5721
|
uninstall,
|
|
5635
5722
|
installStatus,
|
|
5636
5723
|
restart,
|
|
5637
|
-
restartSupported
|
|
5724
|
+
restartSupported,
|
|
5725
|
+
onSettled: uninstallSettled
|
|
5638
5726
|
})
|
|
5639
5727
|
] }), author !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5640
5728
|
className: _dsh_plugin_shop_css_e3675a89_default.author,
|
|
@@ -5645,14 +5733,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5645
5733
|
]
|
|
5646
5734
|
});
|
|
5647
5735
|
});
|
|
5736
|
+
/** The harness-compatibility verdict, rendered beside the control it
|
|
5737
|
+
* qualifies. It answers "what happens if I press this", not "what is this",
|
|
5738
|
+
* so it belongs to the action row and not among the identity badges in the
|
|
5739
|
+
* header — where it also sat inside a <button>, competing with that button's
|
|
5740
|
+
* own hit area for the tooltip. Renders nothing when the harness provides
|
|
5741
|
+
* everything. */
|
|
5742
|
+
function IncompatibleBadge({ missing, t }) {
|
|
5743
|
+
if (missing.length === 0) return null;
|
|
5744
|
+
const detail = t("incompatibleDetail", { modules: missing.join(", ") });
|
|
5745
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
5746
|
+
className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
|
|
5747
|
+
"data-shop-incompatible": true,
|
|
5748
|
+
role: "img",
|
|
5749
|
+
"aria-label": detail,
|
|
5750
|
+
title: detail,
|
|
5751
|
+
children: t("incompatibleBadge")
|
|
5752
|
+
});
|
|
5753
|
+
}
|
|
5648
5754
|
/** One entry's install flow: the button, the §9.3 acknowledgement gate for
|
|
5649
5755
|
* community-tier entries, and the live view — running log, restart notice,
|
|
5650
5756
|
* failure detail, rejection detail — driven by `useInstall`. Shared by the
|
|
5651
5757
|
* catalog cards (`variant: 'install'`) and the outdated rows' update button
|
|
5652
5758
|
* (`variant: 'update'`, which drives the same install flow for `name@latest`). */
|
|
5653
|
-
function InstallPanel({
|
|
5759
|
+
function InstallPanel({ target, tier, missing, missingStated = false, variant = "install", flow, t, restart, restartSupported }) {
|
|
5654
5760
|
const [gateOpen, setGateOpen] = (0, react.useState)(false);
|
|
5655
|
-
const { view, start } =
|
|
5761
|
+
const { view, start } = flow;
|
|
5656
5762
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5657
5763
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
5658
5764
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -5740,8 +5846,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5740
5846
|
onClick: () => {
|
|
5741
5847
|
setGateOpen(false);
|
|
5742
5848
|
start({
|
|
5743
|
-
|
|
5744
|
-
version,
|
|
5849
|
+
...target,
|
|
5745
5850
|
acknowledged: true
|
|
5746
5851
|
});
|
|
5747
5852
|
},
|
|
@@ -5756,20 +5861,22 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5756
5861
|
]
|
|
5757
5862
|
});
|
|
5758
5863
|
const update = variant === "update";
|
|
5759
|
-
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5864
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
5760
5865
|
type: "button",
|
|
5761
5866
|
className: _dsh_plugin_shop_css_e3675a89_default.installButton,
|
|
5762
5867
|
...update ? { "data-shop-update": true } : { "data-shop-install": true },
|
|
5763
5868
|
onClick: () => {
|
|
5764
5869
|
if (tier === "verified") start({
|
|
5765
|
-
|
|
5766
|
-
version,
|
|
5870
|
+
...target,
|
|
5767
5871
|
acknowledged: void 0
|
|
5768
5872
|
});
|
|
5769
5873
|
else setGateOpen(true);
|
|
5770
5874
|
},
|
|
5771
5875
|
children: t(update ? "update" : "install")
|
|
5772
|
-
})
|
|
5876
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IncompatibleBadge, {
|
|
5877
|
+
missing,
|
|
5878
|
+
t
|
|
5879
|
+
})] });
|
|
5773
5880
|
}
|
|
5774
5881
|
/** One installed entry's uninstall flow: remove from the profile through the
|
|
5775
5882
|
* same executor records and poll loop as installs. Uninstalling revokes
|
|
@@ -5777,8 +5884,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
5777
5884
|
* §9.3 is about granting. A business failure (not in the catalog / not
|
|
5778
5885
|
* installed) lands in the failed view with the host's published detail; a
|
|
5779
5886
|
* transport failure carries the empty detail and the localized fallback. */
|
|
5780
|
-
function UninstallPanel({ name, t, uninstall, installStatus, restart, restartSupported }) {
|
|
5887
|
+
function UninstallPanel({ name, t, uninstall, installStatus, restart, restartSupported, onSettled }) {
|
|
5781
5888
|
const { view, start } = useUninstall(uninstall, installStatus);
|
|
5889
|
+
const settled = (0, react.useRef)(onSettled);
|
|
5890
|
+
settled.current = onSettled;
|
|
5891
|
+
(0, react.useEffect)(() => {
|
|
5892
|
+
if (view.kind === "done") settled.current();
|
|
5893
|
+
}, [view.kind]);
|
|
5782
5894
|
if (view.kind === "running") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
5783
5895
|
className: _dsh_plugin_shop_css_e3675a89_default.installPanel,
|
|
5784
5896
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -6006,26 +6118,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6006
6118
|
/** One outdated install row (§7.3): the name, the installed and latest
|
|
6007
6119
|
* versions, the hot enable/disable switch, and the update button (the
|
|
6008
6120
|
* install flow for `name@latest`, reusing `InstallPanel`). */
|
|
6009
|
-
function OutdatedRow({ row, tier,
|
|
6121
|
+
function OutdatedRow({ row, tier, missing, t, setEnabled, flowFor, restart, restartSupported }) {
|
|
6010
6122
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6011
6123
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedRow,
|
|
6012
6124
|
"data-shop-outdated-entry": row.name,
|
|
6013
6125
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6014
6126
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedInfo,
|
|
6015
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
6016
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
6017
|
-
children:
|
|
6018
|
-
className: _dsh_plugin_shop_css_e3675a89_default.name,
|
|
6019
|
-
children: row.name
|
|
6020
|
-
}), missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6021
|
-
className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
|
|
6022
|
-
"data-shop-incompatible": true,
|
|
6023
|
-
title: t("incompatibleDetail", { modules: missing.join(", ") }),
|
|
6024
|
-
children: t("incompatibleBadge")
|
|
6025
|
-
})]
|
|
6127
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
6128
|
+
className: _dsh_plugin_shop_css_e3675a89_default.name,
|
|
6129
|
+
children: row.name
|
|
6026
6130
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
6027
6131
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
|
|
6028
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
|
|
6132
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: row.source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
|
|
6029
6133
|
})]
|
|
6030
6134
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
6031
6135
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedActions,
|
|
@@ -6034,14 +6138,18 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6034
6138
|
t,
|
|
6035
6139
|
setEnabled
|
|
6036
6140
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(InstallPanel, {
|
|
6037
|
-
|
|
6038
|
-
|
|
6141
|
+
target: {
|
|
6142
|
+
name: row.name,
|
|
6143
|
+
version: row.latest,
|
|
6144
|
+
source: row.source,
|
|
6145
|
+
repo: row.repo,
|
|
6146
|
+
subdir: row.subdir
|
|
6147
|
+
},
|
|
6039
6148
|
tier,
|
|
6040
6149
|
variant: "update",
|
|
6041
6150
|
missing,
|
|
6151
|
+
flow: flowFor(identityKey(row)),
|
|
6042
6152
|
t,
|
|
6043
|
-
install,
|
|
6044
|
-
installStatus,
|
|
6045
6153
|
restart,
|
|
6046
6154
|
restartSupported
|
|
6047
6155
|
})]
|
|
@@ -6055,7 +6163,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6055
6163
|
* tier for the update gate is looked up from the catalog by name (community →
|
|
6056
6164
|
* acknowledgement); an entry absent from the catalog defaults to the
|
|
6057
6165
|
* community gate (the safer read). */
|
|
6058
|
-
function OutdatedSection({ state,
|
|
6166
|
+
function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartSupported }) {
|
|
6059
6167
|
if (state.kind === "loading") return null;
|
|
6060
6168
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
6061
6169
|
className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
|
|
@@ -6074,16 +6182,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6074
6182
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
|
|
6075
6183
|
children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
|
|
6076
6184
|
row,
|
|
6077
|
-
tier:
|
|
6078
|
-
|
|
6079
|
-
missing: missingByName.get(row.name) ?? [],
|
|
6185
|
+
tier: entriesByKey.get(identityKey(row))?.tier ?? "community",
|
|
6186
|
+
missing: missingByKey.get(identityKey(row)) ?? [],
|
|
6080
6187
|
t,
|
|
6081
6188
|
setEnabled,
|
|
6082
|
-
|
|
6083
|
-
installStatus,
|
|
6189
|
+
flowFor,
|
|
6084
6190
|
restart,
|
|
6085
6191
|
restartSupported
|
|
6086
|
-
}) }, row
|
|
6192
|
+
}) }, identityKey(row)))
|
|
6087
6193
|
})]
|
|
6088
6194
|
});
|
|
6089
6195
|
}
|
|
@@ -6098,6 +6204,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6098
6204
|
const [selfRestartGate, setSelfRestartGate] = (0, react.useState)(false);
|
|
6099
6205
|
const selfUpdate = useUpdateSelf(updateStart, installStatus);
|
|
6100
6206
|
const [request, setRequest] = (0, react.useState)({ kind: "initial" });
|
|
6207
|
+
const [mutations, setMutations] = (0, react.useState)(0);
|
|
6208
|
+
const noteMutation = (0, react.useCallback)(() => {
|
|
6209
|
+
setMutations((current) => current + 1);
|
|
6210
|
+
}, []);
|
|
6211
|
+
const flows = useInstallFlows(install, installStatus, noteMutation);
|
|
6101
6212
|
const [reloading, setReloading] = (0, react.useState)(false);
|
|
6102
6213
|
const [reloadFailed, setReloadFailed] = (0, react.useState)(false);
|
|
6103
6214
|
const [query, setQuery] = (0, react.useState)("");
|
|
@@ -6178,10 +6289,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6178
6289
|
return () => {
|
|
6179
6290
|
cancelled = true;
|
|
6180
6291
|
};
|
|
6181
|
-
}, [
|
|
6182
|
-
|
|
6292
|
+
}, [
|
|
6293
|
+
installed,
|
|
6294
|
+
request,
|
|
6295
|
+
mutations
|
|
6296
|
+
]);
|
|
6297
|
+
const installedByKey = (0, react.useMemo)(() => {
|
|
6183
6298
|
const map = /* @__PURE__ */ new Map();
|
|
6184
|
-
if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(entry
|
|
6299
|
+
if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(identityKey(entry), entry);
|
|
6185
6300
|
return map;
|
|
6186
6301
|
}, [installedState]);
|
|
6187
6302
|
const browsable = (0, react.useMemo)(() => {
|
|
@@ -6189,11 +6304,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6189
6304
|
const cleared = new Set(catalogState.result.notAShop ?? []);
|
|
6190
6305
|
return catalogState.result.plugins.filter((entry) => cleared.has(entry.name) || !isShopLike(entry.name) && (entry.repo === void 0 || !isShopLike(entry.repo)));
|
|
6191
6306
|
}, [catalogState]);
|
|
6307
|
+
const stars = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.stars : {}, [catalogState]);
|
|
6308
|
+
const sortedBrowsable = (0, react.useMemo)(() => sortByStars(browsable, stars), [browsable, stars]);
|
|
6192
6309
|
const filtered = (0, react.useMemo)(() => {
|
|
6193
6310
|
const q = query.trim().toLowerCase();
|
|
6194
|
-
return
|
|
6311
|
+
return sortedBrowsable.filter((entry) => {
|
|
6195
6312
|
if (category === "installed") {
|
|
6196
|
-
if (!
|
|
6313
|
+
if (!installedByKey.has(entryKey(entry))) return false;
|
|
6197
6314
|
} else if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
|
|
6198
6315
|
if (q === "") return true;
|
|
6199
6316
|
const summaryEn = entry.catalog?.summary.en ?? "";
|
|
@@ -6201,14 +6318,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6201
6318
|
return entry.name.toLowerCase().includes(q) || summaryEn.toLowerCase().includes(q) || summaryZh.toLowerCase().includes(q);
|
|
6202
6319
|
});
|
|
6203
6320
|
}, [
|
|
6204
|
-
|
|
6321
|
+
sortedBrowsable,
|
|
6205
6322
|
query,
|
|
6206
6323
|
category,
|
|
6207
|
-
|
|
6324
|
+
installedByKey
|
|
6208
6325
|
]);
|
|
6209
6326
|
filteredLenRef.current = filtered.length;
|
|
6210
|
-
const stars = catalogState.kind === "ready" ? catalogState.result.stars : {};
|
|
6211
|
-
const sorted = (0, react.useMemo)(() => sortByStars(filtered, stars), [filtered, stars]);
|
|
6212
6327
|
(0, react.useEffect)(() => {
|
|
6213
6328
|
if (!incremental) return;
|
|
6214
6329
|
const node = sentinelRef.current;
|
|
@@ -6223,7 +6338,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6223
6338
|
visibleCount,
|
|
6224
6339
|
filtered.length
|
|
6225
6340
|
]);
|
|
6226
|
-
const visible = incremental ?
|
|
6341
|
+
const visible = incremental ? filtered.slice(0, visibleCount) : filtered;
|
|
6227
6342
|
const categoryCounts = (0, react.useMemo)(() => {
|
|
6228
6343
|
const counts = /* @__PURE__ */ new Map();
|
|
6229
6344
|
for (const entry of browsable) {
|
|
@@ -6237,19 +6352,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6237
6352
|
if (installedState.kind !== "ready") return 0;
|
|
6238
6353
|
return installedState.entries.filter((entry) => !isShopLike(entry.name)).length;
|
|
6239
6354
|
}, [installedState]);
|
|
6240
|
-
const
|
|
6241
|
-
const map = /* @__PURE__ */ new Map();
|
|
6242
|
-
if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
|
|
6243
|
-
return map;
|
|
6244
|
-
}, [catalogState]);
|
|
6245
|
-
const sources = (0, react.useMemo)(() => {
|
|
6355
|
+
const entriesByKey = (0, react.useMemo)(() => {
|
|
6246
6356
|
const map = /* @__PURE__ */ new Map();
|
|
6247
|
-
if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry
|
|
6357
|
+
if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entryKey(entry), entry);
|
|
6248
6358
|
return map;
|
|
6249
6359
|
}, [catalogState]);
|
|
6250
|
-
const
|
|
6360
|
+
const missingByKey = (0, react.useMemo)(() => {
|
|
6251
6361
|
const map = /* @__PURE__ */ new Map();
|
|
6252
|
-
if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry
|
|
6362
|
+
if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entryKey(entry), missingPeersOf(catalogState.result.incompatible, entryKey(entry)));
|
|
6253
6363
|
return map;
|
|
6254
6364
|
}, [catalogState]);
|
|
6255
6365
|
if (catalogState.kind === "loading") return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -6512,15 +6622,16 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6512
6622
|
children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
|
|
6513
6623
|
entry,
|
|
6514
6624
|
stars: starsOf(entry, stars),
|
|
6515
|
-
installed:
|
|
6516
|
-
missing:
|
|
6625
|
+
installed: installedByKey.get(entryKey(entry)),
|
|
6626
|
+
missing: missingByKey.get(entryKey(entry)) ?? [],
|
|
6517
6627
|
t,
|
|
6518
|
-
|
|
6628
|
+
flowFor: flows.flowFor,
|
|
6519
6629
|
installStatus,
|
|
6520
6630
|
uninstall,
|
|
6521
6631
|
restart,
|
|
6522
6632
|
restartSupported,
|
|
6523
|
-
setEnabled
|
|
6633
|
+
setEnabled,
|
|
6634
|
+
onSettled: noteMutation
|
|
6524
6635
|
}) }, entryKey(entry))), incremental && visibleCount < filtered.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", {
|
|
6525
6636
|
ref: sentinelRef,
|
|
6526
6637
|
className: _dsh_plugin_shop_css_e3675a89_default.cardsSentry,
|
|
@@ -6537,13 +6648,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6537
6648
|
})] }),
|
|
6538
6649
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedSection, {
|
|
6539
6650
|
state: installedState,
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
missingByName,
|
|
6651
|
+
entriesByKey,
|
|
6652
|
+
missingByKey,
|
|
6543
6653
|
t,
|
|
6544
6654
|
setEnabled,
|
|
6545
|
-
|
|
6546
|
-
installStatus,
|
|
6655
|
+
flowFor: flows.flowFor,
|
|
6547
6656
|
restart,
|
|
6548
6657
|
restartSupported
|
|
6549
6658
|
})
|
|
@@ -6554,13 +6663,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6554
6663
|
//#region src/client/index.ts
|
|
6555
6664
|
/** Dictionary namespace owned by this plugin. */
|
|
6556
6665
|
const NS = "settings.shop";
|
|
6666
|
+
/** How long a stashed catalog may satisfy a plain open. The host applies the
|
|
6667
|
+
* same five-minute freshness window, so the client stash must not outlive it. */
|
|
6668
|
+
const WARM_TTL_MS = 3e5;
|
|
6557
6669
|
/** The boot-time warm fetch: the catalog the tab wants on its first open.
|
|
6558
6670
|
* Started in apply — the client bundle boots with the web app, long before
|
|
6559
6671
|
* the user reaches Settings — so the host's slow network fetch runs while
|
|
6560
6672
|
* nobody is looking at the shop, and the tab's mount consumes this promise
|
|
6561
6673
|
* instead of starting its own fetch. A rejection stays stored (the injected
|
|
6562
6674
|
* catalog falls back to a fresh call); the extra catch keeps the
|
|
6563
|
-
* fire-and-forget from surfacing as an unhandled rejection.
|
|
6675
|
+
* fire-and-forget from surfacing as an unhandled rejection. Every later
|
|
6676
|
+
* result, including an explicit refresh, replaces this timestamped stash. */
|
|
6564
6677
|
let warmCatalog = null;
|
|
6565
6678
|
/** Services required by the tab registration and the Remote mount.
|
|
6566
6679
|
*
|
|
@@ -6592,19 +6705,33 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6592
6705
|
return result.value;
|
|
6593
6706
|
};
|
|
6594
6707
|
warmCatalog = null;
|
|
6595
|
-
|
|
6596
|
-
warmCatalog
|
|
6708
|
+
const warmed = Promise.resolve(ns.catalog(void 0)).then((result) => unwrap(result));
|
|
6709
|
+
warmCatalog = {
|
|
6710
|
+
at: Date.now(),
|
|
6711
|
+
result: warmed
|
|
6712
|
+
};
|
|
6713
|
+
warmed.catch(() => {});
|
|
6597
6714
|
Promise.resolve(ns.installed()).then((result) => unwrap(result)).catch(() => {});
|
|
6598
6715
|
Promise.resolve(ns.version()).then((result) => unwrap(result)).catch(() => {});
|
|
6599
6716
|
const injected = () => ({
|
|
6600
6717
|
catalog: async (args) => {
|
|
6601
|
-
if (args?.refresh === true)
|
|
6718
|
+
if (args?.refresh === true) {
|
|
6719
|
+
const refreshed = unwrap(await ns.catalog(args));
|
|
6720
|
+
warmCatalog = {
|
|
6721
|
+
at: Date.now(),
|
|
6722
|
+
result: Promise.resolve(refreshed)
|
|
6723
|
+
};
|
|
6724
|
+
return refreshed;
|
|
6725
|
+
}
|
|
6602
6726
|
const warm = warmCatalog;
|
|
6603
|
-
if (warm !== null) try {
|
|
6604
|
-
return await warm;
|
|
6727
|
+
if (warm !== null && Date.now() - warm.at < 3e5) try {
|
|
6728
|
+
return await warm.result;
|
|
6605
6729
|
} catch {}
|
|
6606
6730
|
const fresh = unwrap(await ns.catalog(args));
|
|
6607
|
-
warmCatalog =
|
|
6731
|
+
warmCatalog = {
|
|
6732
|
+
at: Date.now(),
|
|
6733
|
+
result: Promise.resolve(fresh)
|
|
6734
|
+
};
|
|
6608
6735
|
return fresh;
|
|
6609
6736
|
},
|
|
6610
6737
|
install: async (args) => unwrap(await ns.installStart(args)),
|
|
@@ -6627,6 +6754,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6627
6754
|
}
|
|
6628
6755
|
//#endregion
|
|
6629
6756
|
exports.NS = NS;
|
|
6757
|
+
exports.WARM_TTL_MS = WARM_TTL_MS;
|
|
6630
6758
|
exports.apply = apply;
|
|
6631
6759
|
exports.inject = inject;
|
|
6632
6760
|
return module.exports;
|