dsh-plugin-shop 0.8.2-beta.0 → 0.8.3-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/README.md +3 -3
- package/lib/client.js +99 -53
- package/lib/index.js +8 -3
- package/lib/typert.host.js +8 -6
- package/lib/typert.remote-client.js +6 -4
- package/lib/types/client/locales.d.ts +9 -4
- package/lib/types/client/present.d.ts +17 -9
- package/lib/types/client/useFlows.d.ts +5 -1
- package/lib/types/host/index.d.ts +1 -1
- package/lib/types/host/types.d.ts +28 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,9 +35,9 @@ install them once with `npm install -g @deepseek-ai/dsh pnpm` and verify with
|
|
|
35
35
|
# holds back very recent releases, so a bare `add dsh-plugin-shop` can hand
|
|
36
36
|
# you an older version for a while. Pin the current release — refresh it
|
|
37
37
|
# with `npm view dsh-plugin-shop version`.
|
|
38
|
-
dsh plugin --profile web add dsh-plugin-shop@0.8.
|
|
38
|
+
dsh plugin --profile web add dsh-plugin-shop@0.8.2
|
|
39
39
|
# or straight through npx, nothing installed:
|
|
40
|
-
npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.8.
|
|
40
|
+
npx -y @deepseek-ai/dsh plugin --profile web add dsh-plugin-shop@0.8.2
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Replace `web` with your profile if you use another one. Then restart `dsh` once — a
|
|
@@ -67,7 +67,7 @@ ls -1 "${DSH_HOME:-$HOME/.dsh}/profiles" | grep -v '^node_modules$'
|
|
|
67
67
|
cooldown, and deterministic installs are the point of the agent path.
|
|
68
68
|
|
|
69
69
|
```sh
|
|
70
|
-
dsh plugin --profile <profile> add dsh-plugin-shop@0.8.
|
|
70
|
+
dsh plugin --profile <profile> add dsh-plugin-shop@0.8.2
|
|
71
71
|
```
|
|
72
72
|
|
|
73
73
|
**3. Verify — do not skip this.** A zero exit from step 2 means pnpm resolved the
|
package/lib/client.js
CHANGED
|
@@ -4285,6 +4285,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4285
4285
|
"reviewedVersion": union([_undefined(), string()]).optional(),
|
|
4286
4286
|
"reviewedCommit": union([_undefined(), string()]).optional(),
|
|
4287
4287
|
"reviewedSha256": union([_undefined(), string()]).optional(),
|
|
4288
|
+
"repo": union([_undefined(), string()]).optional(),
|
|
4288
4289
|
"reviewer": string(),
|
|
4289
4290
|
"reviewCommit": string(),
|
|
4290
4291
|
"notes": string()
|
|
@@ -4315,7 +4316,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4315
4316
|
})]).optional(),
|
|
4316
4317
|
"added": union([_undefined(), string()]).optional(),
|
|
4317
4318
|
"peers": union([_undefined(), array(string())]).optional(),
|
|
4318
|
-
"unpackedSize": union([_undefined(), number()]).optional()
|
|
4319
|
+
"unpackedSize": union([_undefined(), number()]).optional(),
|
|
4320
|
+
"installSize": union([_undefined(), number()]).optional()
|
|
4319
4321
|
})),
|
|
4320
4322
|
"denied": array(object({
|
|
4321
4323
|
"name": string(),
|
|
@@ -4590,7 +4592,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4590
4592
|
},
|
|
4591
4593
|
sourceLocation: {
|
|
4592
4594
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4593
|
-
"line":
|
|
4595
|
+
"line": 1319,
|
|
4594
4596
|
"column": 9
|
|
4595
4597
|
}
|
|
4596
4598
|
},
|
|
@@ -4645,7 +4647,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4645
4647
|
},
|
|
4646
4648
|
sourceLocation: {
|
|
4647
4649
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4648
|
-
"line":
|
|
4650
|
+
"line": 1241,
|
|
4649
4651
|
"column": 9
|
|
4650
4652
|
}
|
|
4651
4653
|
},
|
|
@@ -4672,7 +4674,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4672
4674
|
},
|
|
4673
4675
|
sourceLocation: {
|
|
4674
4676
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4675
|
-
"line":
|
|
4677
|
+
"line": 1387,
|
|
4676
4678
|
"column": 9
|
|
4677
4679
|
}
|
|
4678
4680
|
},
|
|
@@ -4690,7 +4692,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
4690
4692
|
},
|
|
4691
4693
|
sourceLocation: {
|
|
4692
4694
|
"file": "packages/dsh-plugin-shop/src/host/index.ts",
|
|
4693
|
-
"line":
|
|
4695
|
+
"line": 1371,
|
|
4694
4696
|
"column": 9
|
|
4695
4697
|
}
|
|
4696
4698
|
}
|
|
@@ -6625,24 +6627,32 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6625
6627
|
return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
|
|
6626
6628
|
}
|
|
6627
6629
|
/**
|
|
6628
|
-
* One entry's
|
|
6630
|
+
* One entry's on-disk size as a label: 0 → "0 B", 812 → "812 B",
|
|
6629
6631
|
* 25283 → "25.3 kB", 847407 → "847.4 kB", 179562863 → "179.6 MB".
|
|
6630
6632
|
*
|
|
6631
|
-
* Decimal kB/MB, not KiB/MiB, because the number
|
|
6633
|
+
* Decimal kB/MB, not KiB/MiB, because on the npm side the number IS npm's
|
|
6632
6634
|
* `dist.unpackedSize` and npm's own package page shows it decimal — a reader
|
|
6633
6635
|
* comparing the shelf against npmjs.com must not find two different figures
|
|
6634
|
-
* for one package.
|
|
6635
|
-
*
|
|
6636
|
-
*
|
|
6637
|
-
*
|
|
6636
|
+
* for one package. A github figure is a different measurement (its git tree's
|
|
6637
|
+
* blobs) but not a different unit, so one rule serves both. One fractional
|
|
6638
|
+
* digit at every magnitude above a kilobyte, the same rule `formatStars`
|
|
6639
|
+
* follows: a column of sizes is read by comparing them down the shelf, and
|
|
6640
|
+
* switching precision by magnitude makes that column ragged for no gain.
|
|
6638
6641
|
*
|
|
6639
6642
|
* Locale-free by construction (`toFixed`, not `toLocaleString`): the shelf's
|
|
6640
6643
|
* two languages must not disagree about what a size is, and a decimal comma
|
|
6641
6644
|
* in one of them would read as a thousands separator in the other.
|
|
6642
6645
|
*
|
|
6643
|
-
* Undefined in, undefined out —
|
|
6644
|
-
*
|
|
6645
|
-
*
|
|
6646
|
+
* Undefined in, undefined out — an npm publish predating npm 5.6, or an entry
|
|
6647
|
+
* the registry could not measure honestly (`tree-size.ts` enumerates each way
|
|
6648
|
+
* that happens), and the card renders no label rather than a guess. The caller
|
|
6649
|
+
* need not check first.
|
|
6650
|
+
*
|
|
6651
|
+
* This once read "a github entry", which was true only while the shelf read
|
|
6652
|
+
* `unpackedSize` alone. It is not a source distinction any more: every live
|
|
6653
|
+
* github entry carries an `installSize`, and the sizeless count the build
|
|
6654
|
+
* report prints is owned by `emit.ts` — read it there rather than from a copy
|
|
6655
|
+
* here, because it tracks the daily build.
|
|
6646
6656
|
*/
|
|
6647
6657
|
function formatSize(bytes) {
|
|
6648
6658
|
if (bytes === void 0) return void 0;
|
|
@@ -6668,6 +6678,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6668
6678
|
tab: "插件商店",
|
|
6669
6679
|
loading: "正在读取目录…",
|
|
6670
6680
|
error: "暂时无法读取目录。",
|
|
6681
|
+
installedError: "暂时无法读取已安装列表。",
|
|
6671
6682
|
retry: "重试",
|
|
6672
6683
|
search: "搜索插件",
|
|
6673
6684
|
all: "全部",
|
|
@@ -6719,9 +6730,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6719
6730
|
npmPage: "npm 页面",
|
|
6720
6731
|
authorLine: "作者:{author}",
|
|
6721
6732
|
/** The accessible name and tooltip of the size label; the label itself shows
|
|
6722
|
-
* the bare figure. "
|
|
6723
|
-
*
|
|
6724
|
-
|
|
6733
|
+
* the bare figure. "磁盘占用" is the load-bearing phrase — the number is what
|
|
6734
|
+
* the install lands on disk, not the download. It replaced "解包后", which
|
|
6735
|
+
* was npm's word: a commit-pinned github entry unpacks nothing, its figure
|
|
6736
|
+
* being the sum of its git tree's blobs. One phrase now covers both sources
|
|
6737
|
+
* and stays true of npm, whose number is unchanged. */
|
|
6738
|
+
sizeLabel: "磁盘占用 {size}",
|
|
6725
6739
|
/** The incompatible filter's label. FIXED — the switch beside it carries the
|
|
6726
6740
|
* state, so the words name what the switch does and never flip with it. */
|
|
6727
6741
|
hideIncompatible: "隐藏不兼容 {count}",
|
|
@@ -6735,7 +6749,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6735
6749
|
deniedCode: "已拒绝",
|
|
6736
6750
|
notInCatalogCode: "不在目录中",
|
|
6737
6751
|
versionMismatchCode: "版本不匹配",
|
|
6738
|
-
needsAcknowledgementCode: "
|
|
6752
|
+
needsAcknowledgementCode: "未确认",
|
|
6739
6753
|
tarballIntegrityCode: "压缩包完整性校验失败",
|
|
6740
6754
|
ambiguousIdentityCode: "无法确定是哪一个条目",
|
|
6741
6755
|
nameTakenCode: "同名的另一个插件已安装",
|
|
@@ -6743,7 +6757,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6743
6757
|
nameTakenDetail: "已安装同名的另一个插件({holder})。\n两个同名插件声明相同的加载项 id,无法共存;安装它会替换掉已装的那个。",
|
|
6744
6758
|
npmSource: "npm 包",
|
|
6745
6759
|
githubSource: "源码在 GitHub",
|
|
6746
|
-
|
|
6760
|
+
updatableSection: "可更新",
|
|
6747
6761
|
installedVersion: "已安装 v{version}",
|
|
6748
6762
|
latestVersion: "最新 v{version}",
|
|
6749
6763
|
update: "更新",
|
|
@@ -6768,7 +6782,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6768
6782
|
updateFailed: "更新失败",
|
|
6769
6783
|
updateTransportFailed: "更新请求未能送达。请稍后重试。",
|
|
6770
6784
|
checkUpdate: "检查更新",
|
|
6771
|
-
checkUpdateTitle: "
|
|
6785
|
+
checkUpdateTitle: "检查是否有新版本",
|
|
6772
6786
|
upToDate: "已是最新版本",
|
|
6773
6787
|
github: "GitHub",
|
|
6774
6788
|
enabledSwitch: "启用",
|
|
@@ -6780,6 +6794,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6780
6794
|
tab: "Plugin shop",
|
|
6781
6795
|
loading: "Reading the catalog…",
|
|
6782
6796
|
error: "The catalog is temporarily unavailable.",
|
|
6797
|
+
installedError: "The installed list is temporarily unavailable.",
|
|
6783
6798
|
retry: "Retry",
|
|
6784
6799
|
search: "Search plugins",
|
|
6785
6800
|
all: "All",
|
|
@@ -6830,7 +6845,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6830
6845
|
repository: "Repository",
|
|
6831
6846
|
npmPage: "npm page",
|
|
6832
6847
|
authorLine: "Author: {author}",
|
|
6833
|
-
sizeLabel: "{size}
|
|
6848
|
+
sizeLabel: "{size} on disk",
|
|
6834
6849
|
hideIncompatible: "Hide incompatible {count}",
|
|
6835
6850
|
incompatibleFilterTitle: "Leave out plugins whose components are missing",
|
|
6836
6851
|
showIncompatibleTitle: "Put plugins whose components are missing back on the shelf",
|
|
@@ -6842,7 +6857,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6842
6857
|
deniedCode: "Denied",
|
|
6843
6858
|
notInCatalogCode: "Not in the catalog",
|
|
6844
6859
|
versionMismatchCode: "Version mismatch",
|
|
6845
|
-
needsAcknowledgementCode: "
|
|
6860
|
+
needsAcknowledgementCode: "Not acknowledged",
|
|
6846
6861
|
tarballIntegrityCode: "tarball integrity check failed",
|
|
6847
6862
|
ambiguousIdentityCode: "Ambiguous entry",
|
|
6848
6863
|
nameTakenCode: "Another plugin holds this name",
|
|
@@ -6850,7 +6865,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6850
6865
|
nameTakenDetail: "A different plugin of this name is installed ({holder}).\nTwo plugins of one name declare the same loader entry id and cannot both load; installing this one would replace it.",
|
|
6851
6866
|
npmSource: "npm package",
|
|
6852
6867
|
githubSource: "source on GitHub",
|
|
6853
|
-
|
|
6868
|
+
updatableSection: "Updatable",
|
|
6854
6869
|
installedVersion: "installed v{version}",
|
|
6855
6870
|
latestVersion: "latest v{version}",
|
|
6856
6871
|
update: "Update",
|
|
@@ -6908,6 +6923,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6908
6923
|
*/
|
|
6909
6924
|
/** Shared empty set, so an idle registry hands out one stable identity. */
|
|
6910
6925
|
const NO_PENDING = /* @__PURE__ */ new Set();
|
|
6926
|
+
/** Shared idle view, so every key WITHOUT a flow yields one object rather
|
|
6927
|
+
* than a fresh literal per call. That is what lets `flowFor` below answer
|
|
6928
|
+
* "did this key change?" by identity. */
|
|
6929
|
+
const IDLE_VIEW = { kind: "idle" };
|
|
6911
6930
|
/**
|
|
6912
6931
|
* @param begin the starting RPC and its answer, mapped to the first view.
|
|
6913
6932
|
* Never rejects: a transport throw is the caller's to fold into a view,
|
|
@@ -6979,12 +6998,31 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
6979
6998
|
pendingRef.current = next;
|
|
6980
6999
|
return next;
|
|
6981
7000
|
}, [views]);
|
|
7001
|
+
const flowCache = (0, react.useRef)(/* @__PURE__ */ new Map());
|
|
7002
|
+
const flowClosures = (0, react.useRef)({
|
|
7003
|
+
start,
|
|
7004
|
+
reset
|
|
7005
|
+
});
|
|
6982
7006
|
return {
|
|
6983
|
-
flowFor: (0, react.useCallback)((key) =>
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
|
|
6987
|
-
|
|
7007
|
+
flowFor: (0, react.useCallback)((key) => {
|
|
7008
|
+
if (flowClosures.current.start !== start || flowClosures.current.reset !== reset) {
|
|
7009
|
+
flowCache.current = /* @__PURE__ */ new Map();
|
|
7010
|
+
flowClosures.current = {
|
|
7011
|
+
start,
|
|
7012
|
+
reset
|
|
7013
|
+
};
|
|
7014
|
+
}
|
|
7015
|
+
const view = views.get(key) ?? IDLE_VIEW;
|
|
7016
|
+
const cached = flowCache.current.get(key);
|
|
7017
|
+
if (cached !== void 0 && cached.view === view) return cached;
|
|
7018
|
+
const flow = {
|
|
7019
|
+
view,
|
|
7020
|
+
start: (args) => start(key, args),
|
|
7021
|
+
reset: () => reset(key)
|
|
7022
|
+
};
|
|
7023
|
+
flowCache.current.set(key, flow);
|
|
7024
|
+
return flow;
|
|
7025
|
+
}, [
|
|
6988
7026
|
views,
|
|
6989
7027
|
start,
|
|
6990
7028
|
reset
|
|
@@ -7140,7 +7178,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7140
7178
|
}
|
|
7141
7179
|
//#endregion
|
|
7142
7180
|
//#region \0dsh-plugin-shop-css:e3675a89
|
|
7143
|
-
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 align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s26d58e_categoryButton.s5f68c4_categoryButtonOn` — TWO classes — so it\n * matches `.s26d58e_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s5f68c4_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour or a shadow: a pressed tab must occupy\n * exactly the same box as an unpressed one, so the bar does not reflow — and\n * with ~9 pills on a wrapping row, one that grows can push the rest onto\n * another line and move the tab out from under the pointer that just clicked\n * it. That is what `font-weight: 600` did (bold metrics are wider than\n * regular), and it is why nothing may be added here that resolves to a length:\n * no padding, no border-width, no font-size, no letter-spacing.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` fails the build on one.\n *\n * `box-shadow` is the exception that earns its place: it paints outside the\n * layout box, so an inset ring doubles the border's weight at zero cost to the\n * metrics. It is what `font-weight: 600` used to be — a second, thickness-\n * based signal that hover does not also produce — and the `other` tab needs it\n * most: its hue IS the neutral gray this file uses for \"no category\", so on\n * the light theme hue alone barely separates pressed from unpressed. */\n.s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n border-color: var(--category-hue);\n background: color-mix(in srgb, var(--category-hue) 14%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n box-shadow: inset 0 0 0 1px var(--category-hue);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours. `box-shadow` is dropped in this mode\n * too, so the ring has to come back as an outline. */\n@media (forced-colors: active) {\n .s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s26d58e_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sfd887b_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sfd887b_incompatibleFilter:hover .s11c019_switch` outranks `.s7580e8_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sfd887b_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sfd887b_incompatibleFilter:hover .s11c019_switch:not(.s7580e8_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s26d58e_categoryButton.s5f68c4_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sfd887b_incompatibleFilter.s6e4cba_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sfd887b_incompatibleFilter .s11c019_switch:not(.s7580e8_switchOn) .saab2b3_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sfd887b_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\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. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.sff243d_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.sff243d_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.sff243d_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.sff243d_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s194168_catalogRefreshFailed {\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(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s26d58e_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.s65f383_card[data-category='tool'], .s26d58e_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'], .s26d58e_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s65f383_card[data-category='ui'], .s26d58e_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'], .s26d58e_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s65f383_card[data-category='integration'], .s26d58e_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s65f383_card[data-category='theme'], .s26d58e_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s65f383_card[data-category='other'], .s26d58e_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.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/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value:\n * a github entry has no npm size, and a packument that named no account has no\n * author. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s5fbaf9_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The unpacked size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s5fbaf9_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row. */\n.s2d3c05_size {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s5fbaf9_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s5fbaf9_cardMeta`. There is no middot:\n * `.s5fbaf9_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s1ff66f_author {\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/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s6ac237_restartButton,\n.s64a5ff_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover,\n.s64a5ff_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible,\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.sf53ca0_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.sf79aef_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\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 /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflows the settings dialog. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sfd887b_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.s11c019_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.s11c019_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s7580e8_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 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 /* Same reason as `.sa1f556_outdatedActions`: `.sf79aef_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sf53ca0_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
7181
|
+
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 align-items: center;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n /* The hue this button carries when selected. `All` and `Installed` select by\n * something other than a category and keep the brand token; the six category\n * buttons override it from the shared hue table below, so a pressed Tool tab\n * turns the same blue as a tool card's spine. */\n --category-hue: var(--dsw-alias-brand-primary);\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Blend text with the theme foreground: darker on light backgrounds, lighter\n * on dark ones. The unblended category hue belongs on the border and fill. */\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-border-l2));\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Selected. Written `.s26d58e_categoryButton.s5f68c4_categoryButtonOn` — TWO classes — so it\n * matches `.s26d58e_categoryButton:hover`'s specificity and, coming after it, wins.\n * As a bare `.s5f68c4_categoryButtonOn` it lost both `border-color` and `color` to the\n * hover rule for as long as the pointer stayed on the tab that had just been\n * clicked, which is precisely when a reader looks for confirmation.\n *\n * EVERY declaration here is a colour or a shadow: a pressed tab must occupy\n * exactly the same box as an unpressed one, so the bar does not reflow — and\n * with ~9 pills on a wrapping row, one that grows can push the rest onto\n * another line and move the tab out from under the pointer that just clicked\n * it. That is what `font-weight: 600` did (bold metrics are wider than\n * regular), and it is why nothing may be added here that resolves to a length:\n * no padding, no border-width, no font-size, no letter-spacing.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` fails the build on one.\n *\n * `box-shadow` is the exception that earns its place: it paints outside the\n * layout box, so an inset ring doubles the border's weight at zero cost to the\n * metrics. It is what `font-weight: 600` used to be — a second, thickness-\n * based signal that hover does not also produce — and the `other` tab needs it\n * most: its hue IS the neutral gray this file uses for \"no category\", so on\n * the light theme hue alone barely separates pressed from unpressed. */\n.s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n border-color: var(--category-hue);\n background: color-mix(in srgb, var(--category-hue) 14%, transparent);\n color: color-mix(in srgb, var(--category-hue) 45%, var(--dsw-alias-label-primary) 55%);\n box-shadow: inset 0 0 0 1px var(--category-hue);\n}\n\n/* Forced colours replace every colour above with the system palette, which\n * would leave the three colour declarations saying nothing and the pressed tab\n * indistinguishable from its neighbours. `box-shadow` is dropped in this mode\n * too, so the ring has to come back as an outline. */\n@media (forced-colors: active) {\n .s26d58e_categoryButton.s5f68c4_categoryButtonOn {\n outline: 2px solid ButtonText;\n outline-offset: -4px;\n }\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The incompatible filter, at the right edge of the category bar: `margin-left:\n * auto` pushes it past the pills, and the bar wraps, so on a narrow panel it\n * drops to its own line rather than squeezing them. It is a MODIFIER, not a\n * ninth category — the categories select what to show and this one subtracts\n * from whatever they selected — so it takes the error hue it filters on rather\n * than joining the hue table.\n *\n * And it is a SWITCH, which is the difference the shape now carries. It used\n * to wear `.s26d58e_categoryButton`: same pill, same row, same size as the eight tabs,\n * differing only in being red. That reads as a ninth tab, and it is not one —\n * the tabs are choose-one and this is on-or-off. So it drops the pill chrome\n * entirely (no border, no fill, nothing to compete with the row it sits\n * beside) and puts the state in a track, the same one every installed card\n * carries. Nothing here restates the switch geometry; `--switch-hue` is the\n * whole of what this control says about it, and it reaches the label too, so\n * one declaration keeps the word and the track the same colour. */\n.sfd887b_incompatibleFilter {\n --switch-hue: var(--dsw-alias-state-error-primary);\n margin-left: auto;\n display: inline-flex;\n align-items: center;\n gap: 6px;\n /* Matches the pills' vertical padding so the bar keeps one line box whether\n * this control wraps onto a row of its own or shares theirs. */\n padding: 3px 0;\n border: 0;\n background: none;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n/* Hover lifts the word to full contrast and warms the track's border. Scoped\n * to the OFF track: `.sfd887b_incompatibleFilter:hover .s11c019_switch` outranks `.s7580e8_switchOn`\n * by two classes, so unscoped it would strip the hue from the border at\n * exactly the moment a reader has the pointer on it to check the state. */\n.sfd887b_incompatibleFilter:hover {\n color: var(--dsw-alias-label-primary);\n}\n\n.sfd887b_incompatibleFilter:hover .s11c019_switch:not(.s7580e8_switchOn) {\n border-color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-border-l2));\n}\n\n/* On. Written with both classes for the reason `.s26d58e_categoryButton.s5f68c4_categoryButtonOn`\n * is: it has to match `:hover`'s specificity and come after it, or the colour\n * reverts under a pointer that has not moved since the click. */\n.sfd887b_incompatibleFilter.s6e4cba_incompatibleFilterOn {\n color: color-mix(in srgb, var(--switch-hue) 45%, var(--dsw-alias-label-primary) 55%);\n}\n\n/* Off here means \"nothing is being taken away\" — the resting state — so the\n * knob steps back to the secondary label. The enable switch keeps the full\n * strength dot, where off means \"this plugin is disabled\" and is worth\n * noticing. In this bar it would be the heaviest mark in a row of hairline\n * pills, and on the light theme it is the WHOLE control: bg-layer-2 is the\n * same white as the panel there, so an off track is a hairline outline with a\n * near-black dot sitting in it. */\n.sfd887b_incompatibleFilter .s11c019_switch:not(.s7580e8_switchOn) .saab2b3_switchKnob {\n background: var(--dsw-alias-label-secondary);\n}\n\n/* The ring has no border to trace, so it takes an offset and the pill radius\n * of the tabs it sits beside rather than hugging the text. */\n.sfd887b_incompatibleFilter:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 2px;\n border-radius: 999px;\n}\n\n.sebf017_updatableHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.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. Named\n * `catalogRefresh*` (not `reload*`) so it cannot collide with the §4 page\n * reload offer below, which reloads the whole tab rather than refetching\n * the catalog data. */\n.sff243d_catalogRefreshButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.sff243d_catalogRefreshButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.sff243d_catalogRefreshButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.sff243d_catalogRefreshButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s194168_catalogRefreshFailed {\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(--category-hue, #8B8E96);\n}\n\n/* The hue table, keyed by `data-category` and read by BOTH surfaces that\n * speak in category colour: a card's spine and badge, and the category bar's\n * own tab. One table, so a tab can never disagree with the cards it filters to\n * — which is the whole point of colouring the tab. `.s26d58e_categoryButton` sets the\n * brand fallback for `All`/`Installed`; these attribute selectors are one\n * specificity step above it and win wherever the attribute is present. */\n.s65f383_card[data-category='tool'], .s26d58e_categoryButton[data-category='tool'] { --category-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'], .s26d58e_categoryButton[data-category='provider'] { --category-hue: #A78BFA; }\n.s65f383_card[data-category='ui'], .s26d58e_categoryButton[data-category='ui'] { --category-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'], .s26d58e_categoryButton[data-category='workflow'] { --category-hue: #F59E0B; }\n.s65f383_card[data-category='integration'], .s26d58e_categoryButton[data-category='integration'] { --category-hue: #34D399; }\n.s65f383_card[data-category='theme'], .s26d58e_categoryButton[data-category='theme'] { --category-hue: #F472B6; }\n.s65f383_card[data-category='other'], .s26d58e_categoryButton[data-category='other'] { --category-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--category-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--category-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.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/* What the entry IS rather than what you can do with it — how big it is and\n * who published it — at the right edge of the action row. `margin-left: auto`\n * is what pushes it there, and it lives on this wrapper rather than on either\n * label so the group stays flush right when only one of the two has a value —\n * a packument that named no account has no author, and an entry the registry\n * could not measure honestly has no size. Rarer than it was (every live entry\n * now carries a size, github included), but the wrapper is not what makes it\n * rare: without one, an EMPTY meta group still costs 8px, because\n * `.s9b5048_cardActions` is a flex row with `gap: 8px` and a zero-width item is still\n * an item. The row is a wrapping flex, so an active install flow (which claims\n * the full width) drops the whole group to the next line rather than squeezing\n * it. */\n.s5fbaf9_cardMeta {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n margin-left: auto;\n min-width: 0;\n}\n\n/* The on-disk size, left of the author. Mono and tabular so the digits of one\n * figure line up with the digits of another WHEREVER the two land: `.s5fbaf9_cardMeta`\n * is flush right and the author sits outside the size, so a size's x position\n * moves with the author's width and is different again on the many entries\n * that carry no author at all. There is no column to align to, which is why\n * the reason given here is per-figure legibility rather than the column the\n * first version of this comment claimed. Secondary colour, because it\n * qualifies the entry rather than announcing it.\n *\n * `font-variant-numeric` is safe HERE and would not be on a pill: this element\n * has no state rules, so nothing it does can reflow a wrapping row. */\n.s2d3c05_size {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-variant-numeric: tabular-nums;\n font-size: 12px;\n letter-spacing: 0.02em;\n white-space: nowrap;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The publishing account, at the outer end of `.s5fbaf9_cardMeta` on the action row —\n * NOT beside the npm link in the expanded detail, which is where it used to\n * live and what this comment described until the accurate half was deleted\n * with the `margin-left: auto` that moved to `.s5fbaf9_cardMeta`. There is no middot:\n * `.s5fbaf9_cardMeta`'s 8px gap separates it from the size.\n *\n * Secondary label colour carries the distinction — never a background fill,\n * which is invisible in the light theme where every bg-layer token is the\n * same white. */\n.s1ff66f_author {\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/* The two activation offers share one recipe: a border in an alias token,\n * never a `bg-layer` fill alone — all four bg-layer tokens collapse to the\n * same white under `body` in the light theme, and a fill-only button would\n * vanish there. Grouped rather than copied, because a copy is a rule that\n * can be corrected on one button and left wrong on the other, silently. */\n.s6ac237_restartButton,\n.s64a5ff_reloadButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover,\n.s64a5ff_reloadButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible,\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The §4 reload offer: a notice plus its button, one item of\n * `.sf53ca0_installedActions` the way the restart gate is — a column of its own\n * rather than inline with the row. */\n.sf79aef_reloadPanel {\n display: flex;\n flex-direction: column;\n align-items: flex-start;\n gap: 6px;\n}\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/* The Updatable section. The class keeps the Host's field name; only the\n heading the reader sees was renamed. */\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 /* The reload offer (§4) mounts inside the switch beside the Update button\n * and is a two-line block, not a control: without wrapping, a row reading\n * [switch][notice + Reload][Update] overflows the settings dialog. */\n flex-wrap: wrap;\n gap: 10px;\n}\n\n/* The shop's one switch. It is worn by two controls that mean different\n * things — the enable/disable toggle on an installed card, and the shelf's\n * incompatible filter — so the geometry lives here once and each caller states\n * only its hue.\n *\n * `--switch-hue` is that knob, and it is read from the ANCESTOR rather than\n * declared per state: `.sfd887b_incompatibleFilter` sets it once and it reaches the\n * track and the label together, which is what keeps a red switch from sitting\n * under a neutral word. A declaration on `.s11c019_switch` itself would shadow the\n * inherited value (an element's own rule beats what it inherits), so the\n * default rides in the `var()` fallback and is resolved once into\n * `--track-hue` instead of being spelled out at each use.\n *\n * `box-sizing` and `padding` are stated because the two callers are different\n * elements — a `<button>`, which the UA gives padding, and a `<span>`, which\n * it does not. Without them the same class renders two sizes, and the knob\n * sits 2px above centre in the taller one. */\n.s11c019_switch {\n --track-hue: var(--switch-hue, var(--dsw-alias-state-success-primary));\n box-sizing: border-box;\n display: inline-block;\n position: relative;\n width: 32px;\n height: 18px;\n padding: 0;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n/* The knob's POSITION is the state, which is the reason this control needs no\n * `forced-colors` fallback the way the category pills do: a system palette\n * replaces every colour below and moves nothing. */\n.s7580e8_switchOn {\n border-color: var(--track-hue);\n background: color-mix(in srgb, var(--track-hue) 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 /* Same reason as `.sa1f556_outdatedActions`: `.sf79aef_reloadPanel`'s own comment calls\n * itself \"a column of its own rather than inline with the row\", which held\n * for `.sf53ca0_installedActions` and not here until this line. */\n flex-wrap: wrap;\n gap: 8px;\n}\n";
|
|
7144
7182
|
if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
|
|
7145
7183
|
const tag = document.createElement("style");
|
|
7146
7184
|
tag.dataset.pluginCss = "e3675a89";
|
|
@@ -7180,7 +7218,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7180
7218
|
"switchOn": "s7580e8_switchOn",
|
|
7181
7219
|
"incompatibleFilterOn": "s6e4cba_incompatibleFilterOn",
|
|
7182
7220
|
"switchKnob": "saab2b3_switchKnob",
|
|
7183
|
-
"
|
|
7221
|
+
"updatableHeading": "sebf017_updatableHeading",
|
|
7184
7222
|
"catalogStatsRow": "seb0a1e_catalogStatsRow",
|
|
7185
7223
|
"catalogStats": "sccc4d1_catalogStats",
|
|
7186
7224
|
"catalogRefreshButton": "sff243d_catalogRefreshButton",
|
|
@@ -7279,14 +7317,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7279
7317
|
* install controls. An installed plugin's card carries its installed row:
|
|
7280
7318
|
* current → the non-interactive installed label, behind → the update button;
|
|
7281
7319
|
* uninstalled → the install button. */
|
|
7282
|
-
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, nameTakenBy, t,
|
|
7320
|
+
const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, installed, missing, nameTakenBy, t, flow, uninstallFlow, restart, restartBlocked, reload, setEnabled, inInstalledView }) {
|
|
7283
7321
|
const [open, setOpen] = (0, react.useState)(false);
|
|
7284
7322
|
const blockers = blockersOf(missing, nameTakenBy, t);
|
|
7285
7323
|
const detailId = (0, react.useId)();
|
|
7286
7324
|
const summary = entry.catalog?.summary;
|
|
7287
7325
|
const npmUrl = npmPageUrl(entry);
|
|
7288
7326
|
const author = authorOf(entry);
|
|
7289
|
-
const size = formatSize(entry.
|
|
7327
|
+
const size = formatSize(entry.installSize);
|
|
7290
7328
|
const category = entry.catalog?.category ?? "other";
|
|
7291
7329
|
const installTarget = {
|
|
7292
7330
|
name: entry.name,
|
|
@@ -7295,8 +7333,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
7295
7333
|
repo: entry.repo,
|
|
7296
7334
|
subdir: entry.subdir
|
|
7297
7335
|
};
|
|
7298
|
-
const flow = flowFor(entryKey(entry));
|
|
7299
|
-
const uninstallFlow = uninstallFlowFor(entryKey(entry));
|
|
7300
7336
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
7301
7337
|
className: _dsh_plugin_shop_css_e3675a89_default.card,
|
|
7302
7338
|
"data-shop-entry": entry.name,
|
|
@@ -8035,28 +8071,40 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8035
8071
|
})]
|
|
8036
8072
|
});
|
|
8037
8073
|
}
|
|
8038
|
-
/** The
|
|
8074
|
+
/** The id the section points its `aria-labelledby` at. A bare `<section>`
|
|
8075
|
+
* maps to `generic`, not `region`: without an accessible name landmark
|
|
8076
|
+
* navigation skips the update rows, and this `<h2>` — the tab's only
|
|
8077
|
+
* heading, rendered last, below the whole shelf — reads as a continuation
|
|
8078
|
+
* of the cards above it. */
|
|
8079
|
+
const UPDATABLE_HEADING_ID = "dsh-shop-updatable-heading";
|
|
8080
|
+
/** The §7.3 installed list, rendered as the "Updatable" section: each row
|
|
8039
8081
|
* shows both versions, a switch, and an update button. The rows are the
|
|
8040
8082
|
* installed entries filtered to `outdated` — a current install is already
|
|
8041
8083
|
* spoken for by its shelf card's installed label, and has no row here. The
|
|
8042
|
-
*
|
|
8043
|
-
*
|
|
8044
|
-
*
|
|
8084
|
+
* heading names that filtered content, not the list it came from: it read
|
|
8085
|
+
* "Installed" until 2026-09-16, which was the Installed filter's label
|
|
8086
|
+
* character for character in both languages, over a strict subset of what
|
|
8087
|
+
* that filter selects. Internals stay `outdated` — the Host field name. The
|
|
8088
|
+
* tier for the update gate is looked up from the catalog by install IDENTITY,
|
|
8089
|
+
* never by name (community → acknowledgement); an entry absent from the
|
|
8090
|
+
* catalog defaults to the community gate (the safer read). */
|
|
8045
8091
|
function OutdatedSection({ state, entriesByKey, missingByKey, t, setEnabled, flowFor, restart, restartBlocked, reload }) {
|
|
8046
8092
|
if (state.kind === "loading") return null;
|
|
8047
8093
|
if (state.kind === "error") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
8048
8094
|
className: _dsh_plugin_shop_css_e3675a89_default.stateLine,
|
|
8049
8095
|
"data-shop-outdated-error": true,
|
|
8050
|
-
children: t("
|
|
8096
|
+
children: t("installedError")
|
|
8051
8097
|
});
|
|
8052
8098
|
const outdated = state.entries.filter((entry) => entry.outdated);
|
|
8053
8099
|
if (outdated.length === 0) return null;
|
|
8054
8100
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
8055
8101
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedSection,
|
|
8056
8102
|
"data-shop-outdated": true,
|
|
8103
|
+
"aria-labelledby": UPDATABLE_HEADING_ID,
|
|
8057
8104
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
|
|
8058
|
-
className: _dsh_plugin_shop_css_e3675a89_default.
|
|
8059
|
-
|
|
8105
|
+
className: _dsh_plugin_shop_css_e3675a89_default.updatableHeading,
|
|
8106
|
+
id: UPDATABLE_HEADING_ID,
|
|
8107
|
+
children: t("updatableSection")
|
|
8060
8108
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("ul", {
|
|
8061
8109
|
className: _dsh_plugin_shop_css_e3675a89_default.outdatedList,
|
|
8062
8110
|
children: outdated.map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(OutdatedRow, {
|
|
@@ -8203,13 +8251,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8203
8251
|
if (installedState.kind === "ready") for (const entry of installedState.entries) map.set(identityKey(entry), entry);
|
|
8204
8252
|
return map;
|
|
8205
8253
|
}, [installedState]);
|
|
8254
|
+
const stars = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.stars : {}, [catalogState]);
|
|
8255
|
+
const sortedCatalog = (0, react.useMemo)(() => catalogState.kind === "ready" ? sortByStars(catalogState.result.plugins, stars) : [], [catalogState, stars]);
|
|
8206
8256
|
const browsable = (0, react.useMemo)(() => {
|
|
8207
8257
|
if (catalogState.kind !== "ready") return [];
|
|
8208
8258
|
const cleared = new Set(catalogState.result.notAShop ?? []);
|
|
8209
|
-
return
|
|
8210
|
-
}, [catalogState]);
|
|
8211
|
-
const stars = (0, react.useMemo)(() => catalogState.kind === "ready" ? catalogState.result.stars : {}, [catalogState]);
|
|
8212
|
-
const sortedBrowsable = (0, react.useMemo)(() => sortByStars(browsable, stars), [browsable, stars]);
|
|
8259
|
+
return sortedCatalog.filter((entry) => cleared.has(entry.name) || !isShopLike(entry.name) && (entry.repo === void 0 || !isShopLike(entry.repo)));
|
|
8260
|
+
}, [catalogState, sortedCatalog]);
|
|
8213
8261
|
const missingByKey = (0, react.useMemo)(() => {
|
|
8214
8262
|
const map = /* @__PURE__ */ new Map();
|
|
8215
8263
|
if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entryKey(entry), missingPeersOf(catalogState.result.incompatible, entryKey(entry)));
|
|
@@ -8227,7 +8275,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8227
8275
|
const badgedIncompatible = (0, react.useCallback)((key) => (missingByKey.get(key) ?? []).length > 0 && !nameTakenByKey.has(key), [missingByKey, nameTakenByKey]);
|
|
8228
8276
|
const matched = (0, react.useMemo)(() => {
|
|
8229
8277
|
const q = query.trim().toLowerCase();
|
|
8230
|
-
return
|
|
8278
|
+
return (category === "installed" ? sortedCatalog : browsable).filter((entry) => {
|
|
8231
8279
|
if (category === "installed") {
|
|
8232
8280
|
const key = entryKey(entry);
|
|
8233
8281
|
if (!installedByKey.has(key) && !uninstallFlows.pending.has(key)) return false;
|
|
@@ -8238,7 +8286,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8238
8286
|
return entry.name.toLowerCase().includes(q) || summaryEn.toLowerCase().includes(q) || summaryZh.toLowerCase().includes(q);
|
|
8239
8287
|
});
|
|
8240
8288
|
}, [
|
|
8241
|
-
|
|
8289
|
+
sortedCatalog,
|
|
8290
|
+
browsable,
|
|
8242
8291
|
query,
|
|
8243
8292
|
category,
|
|
8244
8293
|
installedByKey,
|
|
@@ -8275,10 +8324,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8275
8324
|
}
|
|
8276
8325
|
return counts;
|
|
8277
8326
|
}, [browsable]);
|
|
8278
|
-
const installedCount = (0, react.useMemo)(() =>
|
|
8279
|
-
if (installedState.kind !== "ready") return 0;
|
|
8280
|
-
return installedState.entries.filter((entry) => !isShopLike(entry.name)).length;
|
|
8281
|
-
}, [installedState]);
|
|
8327
|
+
const installedCount = (0, react.useMemo)(() => sortedCatalog.filter((entry) => installedByKey.has(entryKey(entry))).length, [sortedCatalog, installedByKey]);
|
|
8282
8328
|
const incompatibleCount = (0, react.useMemo)(() => browsable.filter((entry) => badgedIncompatible(entryKey(entry))).length, [browsable, badgedIncompatible]);
|
|
8283
8329
|
const entriesByKey = (0, react.useMemo)(() => {
|
|
8284
8330
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -8572,8 +8618,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
|
|
|
8572
8618
|
missing: missingByKey.get(key) ?? [],
|
|
8573
8619
|
nameTakenBy: nameTakenByKey.get(key),
|
|
8574
8620
|
t,
|
|
8575
|
-
|
|
8576
|
-
|
|
8621
|
+
flow: flows.flowFor(key),
|
|
8622
|
+
uninstallFlow: uninstallFlows.flowFor(key),
|
|
8577
8623
|
restart,
|
|
8578
8624
|
restartBlocked,
|
|
8579
8625
|
reload,
|
package/lib/index.js
CHANGED
|
@@ -578,6 +578,7 @@ const entrySchema = z.object({
|
|
|
578
578
|
reviewedVersion: z.string().optional(),
|
|
579
579
|
reviewedCommit: z.string().optional(),
|
|
580
580
|
reviewedSha256: z.string().optional(),
|
|
581
|
+
repo: z.string().optional(),
|
|
581
582
|
reviewer: z.string(),
|
|
582
583
|
reviewCommit: z.string(),
|
|
583
584
|
notes: z.string()
|
|
@@ -608,7 +609,8 @@ const entrySchema = z.object({
|
|
|
608
609
|
}).optional(),
|
|
609
610
|
publisher: z.string().optional(),
|
|
610
611
|
peers: z.array(z.string()).optional(),
|
|
611
|
-
unpackedSize: z.number().int().nonnegative().optional()
|
|
612
|
+
unpackedSize: z.number().int().nonnegative().optional(),
|
|
613
|
+
installSize: z.number().int().nonnegative().optional()
|
|
612
614
|
}).superRefine((entry, ctx) => {
|
|
613
615
|
if (entry.source === "npm") {
|
|
614
616
|
if (!NPM_NAME.test(entry.name)) ctx.addIssue({
|
|
@@ -638,7 +640,10 @@ const entrySchema = z.object({
|
|
|
638
640
|
path: ["version"],
|
|
639
641
|
message: `github entry version ${JSON.stringify(entry.version)} is neither a 40-character commit sha nor a release tag`
|
|
640
642
|
});
|
|
641
|
-
})
|
|
643
|
+
}).transform((entry) => entry.installSize === void 0 && entry.unpackedSize !== void 0 ? {
|
|
644
|
+
...entry,
|
|
645
|
+
installSize: entry.unpackedSize
|
|
646
|
+
} : entry);
|
|
642
647
|
const dataSchema = z.object({
|
|
643
648
|
schemaVersion: z.number(),
|
|
644
649
|
plugins: z.array(entrySchema),
|
|
@@ -3827,7 +3832,7 @@ let ShopGateway = (() => {
|
|
|
3827
3832
|
}
|
|
3828
3833
|
/** Installed catalog plugins (§7.3): every entry of the snapshot the profile
|
|
3829
3834
|
* manifest declares as a dependency, with the Host's `outdated` verdict
|
|
3830
|
-
* attached. The tab's shelf cards and its
|
|
3835
|
+
* attached. The tab's shelf cards and its Updatable section both derive
|
|
3831
3836
|
* from this one list. */
|
|
3832
3837
|
async installed() {
|
|
3833
3838
|
const snapshot = await this.snapshotNow();
|
package/lib/typert.host.js
CHANGED
|
@@ -21,6 +21,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
|
|
|
21
21
|
'reviewedVersion': z.union([z.undefined(), z.string()]).optional(),
|
|
22
22
|
'reviewedCommit': z.union([z.undefined(), z.string()]).optional(),
|
|
23
23
|
'reviewedSha256': z.union([z.undefined(), z.string()]).optional(),
|
|
24
|
+
'repo': z.union([z.undefined(), z.string()]).optional(),
|
|
24
25
|
'reviewer': z.string(),
|
|
25
26
|
'reviewCommit': z.string(),
|
|
26
27
|
'notes': z.string(),
|
|
@@ -44,6 +45,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
|
|
|
44
45
|
'added': z.union([z.undefined(), z.string()]).optional(),
|
|
45
46
|
'peers': z.union([z.undefined(), z.array(z.string())]).optional(),
|
|
46
47
|
'unpackedSize': z.union([z.undefined(), z.number()]).optional(),
|
|
48
|
+
'installSize': z.union([z.undefined(), z.number()]).optional(),
|
|
47
49
|
})),
|
|
48
50
|
'denied': z.array(z.object({
|
|
49
51
|
'name': z.string(),
|
|
@@ -264,7 +266,7 @@ export const TYPERT = {
|
|
|
264
266
|
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
265
267
|
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
266
268
|
},
|
|
267
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
269
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1319,"column":9},
|
|
268
270
|
},
|
|
269
271
|
{
|
|
270
272
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -315,7 +317,7 @@ export const TYPERT = {
|
|
|
315
317
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
316
318
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
317
319
|
},
|
|
318
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
320
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1241,"column":9},
|
|
319
321
|
},
|
|
320
322
|
{
|
|
321
323
|
id: 'dsh-plugin-shop#shop/updateStart',
|
|
@@ -340,7 +342,7 @@ export const TYPERT = {
|
|
|
340
342
|
typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
|
|
341
343
|
schema: dsh_plugin_shop_shop_updateStart_result$schema,
|
|
342
344
|
},
|
|
343
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
345
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1387,"column":9},
|
|
344
346
|
},
|
|
345
347
|
{
|
|
346
348
|
id: 'dsh-plugin-shop#shop/version',
|
|
@@ -355,7 +357,7 @@ export const TYPERT = {
|
|
|
355
357
|
typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
|
|
356
358
|
schema: dsh_plugin_shop_shop_version_result$schema,
|
|
357
359
|
},
|
|
358
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
360
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1371,"column":9},
|
|
359
361
|
},
|
|
360
362
|
],
|
|
361
363
|
model: {
|
|
@@ -414,7 +416,7 @@ export const TYPERT = {
|
|
|
414
416
|
"name": "installed",
|
|
415
417
|
"signature": "@Remote('installed') async installed(): Promise<ShopInstalledEntry[]>",
|
|
416
418
|
"summary": "Installed catalog plugins (§7.3): every entry of the snapshot the profile manifest declares as a dependency, with the Host's `outdated` verdict attached.",
|
|
417
|
-
"jsDoc": "/** Installed catalog plugins (§7.3): every entry of the snapshot the profile\n * manifest declares as a dependency, with the Host's `outdated` verdict\n * attached. The tab's shelf cards and its
|
|
419
|
+
"jsDoc": "/** Installed catalog plugins (§7.3): every entry of the snapshot the profile\n * manifest declares as a dependency, with the Host's `outdated` verdict\n * attached. The tab's shelf cards and its Updatable section both derive\n * from this one list. */"
|
|
418
420
|
},
|
|
419
421
|
{
|
|
420
422
|
"kind": "method",
|
|
@@ -452,7 +454,7 @@ export const TYPERT = {
|
|
|
452
454
|
},
|
|
453
455
|
{
|
|
454
456
|
"name": "CatalogEntry",
|
|
455
|
-
"declaration": "export interface CatalogEntry {\n name: string;\n version: string;\n integrity: string | null;\n publishedAt: string | null;\n repository: string | null;\n license: string | null;\n tier: 'verified' | 'verified-stale' | 'community';\n metadata: 'declared' | 'derived';\n review?: { reviewedVersion?: string; reviewedCommit?: string; reviewedSha256?: string; reviewer: string; reviewCommit: string; notes: string; };\n catalog?: CatalogSection;\n source: 'npm' | 'github';\n publisher?: string;\n repo?: string;\n subdir?: string;\n tarball?: { url: string; sha256: string; };\n added?: string;\n peers?: string[];\n unpackedSize?: number;\n}"
|
|
457
|
+
"declaration": "export interface CatalogEntry {\n name: string;\n version: string;\n integrity: string | null;\n publishedAt: string | null;\n repository: string | null;\n license: string | null;\n tier: 'verified' | 'verified-stale' | 'community';\n metadata: 'declared' | 'derived';\n review?: { reviewedVersion?: string; reviewedCommit?: string; reviewedSha256?: string; repo?: string; reviewer: string; reviewCommit: string; notes: string; };\n catalog?: CatalogSection;\n source: 'npm' | 'github';\n publisher?: string;\n repo?: string;\n subdir?: string;\n tarball?: { url: string; sha256: string; };\n added?: string;\n peers?: string[];\n unpackedSize?: number;\n installSize?: number;\n}"
|
|
456
458
|
},
|
|
457
459
|
{
|
|
458
460
|
"name": "CatalogSection",
|
|
@@ -21,6 +21,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
|
|
|
21
21
|
'reviewedVersion': z.union([z.undefined(), z.string()]).optional(),
|
|
22
22
|
'reviewedCommit': z.union([z.undefined(), z.string()]).optional(),
|
|
23
23
|
'reviewedSha256': z.union([z.undefined(), z.string()]).optional(),
|
|
24
|
+
'repo': z.union([z.undefined(), z.string()]).optional(),
|
|
24
25
|
'reviewer': z.string(),
|
|
25
26
|
'reviewCommit': z.string(),
|
|
26
27
|
'notes': z.string(),
|
|
@@ -44,6 +45,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
|
|
|
44
45
|
'added': z.union([z.undefined(), z.string()]).optional(),
|
|
45
46
|
'peers': z.union([z.undefined(), z.array(z.string())]).optional(),
|
|
46
47
|
'unpackedSize': z.union([z.undefined(), z.number()]).optional(),
|
|
48
|
+
'installSize': z.union([z.undefined(), z.number()]).optional(),
|
|
47
49
|
})),
|
|
48
50
|
'denied': z.array(z.object({
|
|
49
51
|
'name': z.string(),
|
|
@@ -261,7 +263,7 @@ export const TYPERT_REMOTE = {
|
|
|
261
263
|
typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
|
|
262
264
|
schema: dsh_plugin_shop_shop_restart_result$schema,
|
|
263
265
|
},
|
|
264
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
266
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1319,"column":9},
|
|
265
267
|
},
|
|
266
268
|
{
|
|
267
269
|
id: 'dsh-plugin-shop#shop/setEnabled',
|
|
@@ -312,7 +314,7 @@ export const TYPERT_REMOTE = {
|
|
|
312
314
|
typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
|
|
313
315
|
schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
|
|
314
316
|
},
|
|
315
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
317
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1241,"column":9},
|
|
316
318
|
},
|
|
317
319
|
{
|
|
318
320
|
id: 'dsh-plugin-shop#shop/updateStart',
|
|
@@ -337,7 +339,7 @@ export const TYPERT_REMOTE = {
|
|
|
337
339
|
typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
|
|
338
340
|
schema: dsh_plugin_shop_shop_updateStart_result$schema,
|
|
339
341
|
},
|
|
340
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
342
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1387,"column":9},
|
|
341
343
|
},
|
|
342
344
|
{
|
|
343
345
|
id: 'dsh-plugin-shop#shop/version',
|
|
@@ -352,7 +354,7 @@ export const TYPERT_REMOTE = {
|
|
|
352
354
|
typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
|
|
353
355
|
schema: dsh_plugin_shop_shop_version_result$schema,
|
|
354
356
|
},
|
|
355
|
-
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":
|
|
357
|
+
sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1371,"column":9},
|
|
356
358
|
},
|
|
357
359
|
],
|
|
358
360
|
}
|
|
@@ -4,6 +4,7 @@ export declare const zh: {
|
|
|
4
4
|
tab: string;
|
|
5
5
|
loading: string;
|
|
6
6
|
error: string;
|
|
7
|
+
installedError: string;
|
|
7
8
|
retry: string;
|
|
8
9
|
search: string;
|
|
9
10
|
all: string;
|
|
@@ -55,8 +56,11 @@ export declare const zh: {
|
|
|
55
56
|
npmPage: string;
|
|
56
57
|
authorLine: string;
|
|
57
58
|
/** The accessible name and tooltip of the size label; the label itself shows
|
|
58
|
-
* the bare figure. "
|
|
59
|
-
*
|
|
59
|
+
* the bare figure. "磁盘占用" is the load-bearing phrase — the number is what
|
|
60
|
+
* the install lands on disk, not the download. It replaced "解包后", which
|
|
61
|
+
* was npm's word: a commit-pinned github entry unpacks nothing, its figure
|
|
62
|
+
* being the sum of its git tree's blobs. One phrase now covers both sources
|
|
63
|
+
* and stays true of npm, whose number is unchanged. */
|
|
60
64
|
sizeLabel: string;
|
|
61
65
|
/** The incompatible filter's label. FIXED — the switch beside it carries the
|
|
62
66
|
* state, so the words name what the switch does and never flip with it. */
|
|
@@ -79,7 +83,7 @@ export declare const zh: {
|
|
|
79
83
|
nameTakenDetail: string;
|
|
80
84
|
npmSource: string;
|
|
81
85
|
githubSource: string;
|
|
82
|
-
|
|
86
|
+
updatableSection: string;
|
|
83
87
|
installedVersion: string;
|
|
84
88
|
latestVersion: string;
|
|
85
89
|
update: string;
|
|
@@ -118,6 +122,7 @@ export declare const en: {
|
|
|
118
122
|
tab: string;
|
|
119
123
|
loading: string;
|
|
120
124
|
error: string;
|
|
125
|
+
installedError: string;
|
|
121
126
|
retry: string;
|
|
122
127
|
search: string;
|
|
123
128
|
all: string;
|
|
@@ -188,7 +193,7 @@ export declare const en: {
|
|
|
188
193
|
nameTakenDetail: string;
|
|
189
194
|
npmSource: string;
|
|
190
195
|
githubSource: string;
|
|
191
|
-
|
|
196
|
+
updatableSection: string;
|
|
192
197
|
installedVersion: string;
|
|
193
198
|
latestVersion: string;
|
|
194
199
|
update: string;
|
|
@@ -285,23 +285,31 @@ export declare function sortByStars(entries: CatalogEntry[], stars: Record<strin
|
|
|
285
285
|
/** 999 → "999"; 1000 → "1k"; 1234 → "1.2k"; 1500 → "1.5k"; 99999 → "100k". */
|
|
286
286
|
export declare function formatStars(n: number): string;
|
|
287
287
|
/**
|
|
288
|
-
* One entry's
|
|
288
|
+
* One entry's on-disk size as a label: 0 → "0 B", 812 → "812 B",
|
|
289
289
|
* 25283 → "25.3 kB", 847407 → "847.4 kB", 179562863 → "179.6 MB".
|
|
290
290
|
*
|
|
291
|
-
* Decimal kB/MB, not KiB/MiB, because the number
|
|
291
|
+
* Decimal kB/MB, not KiB/MiB, because on the npm side the number IS npm's
|
|
292
292
|
* `dist.unpackedSize` and npm's own package page shows it decimal — a reader
|
|
293
293
|
* comparing the shelf against npmjs.com must not find two different figures
|
|
294
|
-
* for one package.
|
|
295
|
-
*
|
|
296
|
-
*
|
|
297
|
-
*
|
|
294
|
+
* for one package. A github figure is a different measurement (its git tree's
|
|
295
|
+
* blobs) but not a different unit, so one rule serves both. One fractional
|
|
296
|
+
* digit at every magnitude above a kilobyte, the same rule `formatStars`
|
|
297
|
+
* follows: a column of sizes is read by comparing them down the shelf, and
|
|
298
|
+
* switching precision by magnitude makes that column ragged for no gain.
|
|
298
299
|
*
|
|
299
300
|
* Locale-free by construction (`toFixed`, not `toLocaleString`): the shelf's
|
|
300
301
|
* two languages must not disagree about what a size is, and a decimal comma
|
|
301
302
|
* in one of them would read as a thousands separator in the other.
|
|
302
303
|
*
|
|
303
|
-
* Undefined in, undefined out —
|
|
304
|
-
*
|
|
305
|
-
*
|
|
304
|
+
* Undefined in, undefined out — an npm publish predating npm 5.6, or an entry
|
|
305
|
+
* the registry could not measure honestly (`tree-size.ts` enumerates each way
|
|
306
|
+
* that happens), and the card renders no label rather than a guess. The caller
|
|
307
|
+
* need not check first.
|
|
308
|
+
*
|
|
309
|
+
* This once read "a github entry", which was true only while the shelf read
|
|
310
|
+
* `unpackedSize` alone. It is not a source distinction any more: every live
|
|
311
|
+
* github entry carries an `installSize`, and the sizeless count the build
|
|
312
|
+
* report prints is owned by `emit.ts` — read it there rather than from a copy
|
|
313
|
+
* here, because it tracks the daily build.
|
|
306
314
|
*/
|
|
307
315
|
export declare function formatSize(bytes: number | undefined): string | undefined;
|
|
@@ -29,7 +29,11 @@ export interface KeyedFlow<TArgs> {
|
|
|
29
29
|
reset: () => void;
|
|
30
30
|
}
|
|
31
31
|
export interface UseKeyedFlows<TArgs> {
|
|
32
|
-
/** Two panels asking for the same identity receive one flow
|
|
32
|
+
/** Two panels asking for the same identity receive one flow — the SAME
|
|
33
|
+
* object, for as long as that identity's view is unchanged, so a
|
|
34
|
+
* memoized consumer handed the result re-renders only when its own flow
|
|
35
|
+
* moves. The accessor's identity still tracks `views`; the result's does
|
|
36
|
+
* not, and the result is what a card should be given. */
|
|
33
37
|
flowFor: (key: string) => KeyedFlow<TArgs>;
|
|
34
38
|
/** Return one identity to `idle` without building a flow for it. Stable
|
|
35
39
|
* across renders (unlike `flowFor`, whose identity tracks `views`), so one
|
|
@@ -506,7 +506,7 @@ export declare class ShopGateway extends TypertRemoteService {
|
|
|
506
506
|
installedSpecs(): Promise<Record<string, string> | null>;
|
|
507
507
|
/** Installed catalog plugins (§7.3): every entry of the snapshot the profile
|
|
508
508
|
* manifest declares as a dependency, with the Host's `outdated` verdict
|
|
509
|
-
* attached. The tab's shelf cards and its
|
|
509
|
+
* attached. The tab's shelf cards and its Updatable section both derive
|
|
510
510
|
* from this one list. */
|
|
511
511
|
installed(): Promise<ShopInstalledEntry[]>;
|
|
512
512
|
/** Whether an installed dependency spec sits behind the catalog's version.
|
|
@@ -24,6 +24,10 @@ export interface CatalogEntry {
|
|
|
24
24
|
* the content-addressed identity; the tag is display only, a mutable ref
|
|
25
25
|
* that must never carry the trust. */
|
|
26
26
|
reviewedSha256?: string;
|
|
27
|
+
/** Which repository was reviewed. A github review binds (`repo`,
|
|
28
|
+
* `reviewedCommit`) and never a name, because 83 live bundle names are
|
|
29
|
+
* claimed by both a fork and an original. */
|
|
30
|
+
repo?: string;
|
|
27
31
|
reviewer: string;
|
|
28
32
|
reviewCommit: string;
|
|
29
33
|
notes: string;
|
|
@@ -54,12 +58,31 @@ export interface CatalogEntry {
|
|
|
54
58
|
added?: string;
|
|
55
59
|
/** The package's declared peer dependency names (schemaVersion 6). */
|
|
56
60
|
peers?: string[];
|
|
57
|
-
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
61
|
+
/** Wire-compatibility key, not a field to read — see {@link installSize},
|
|
62
|
+
* which the catalog parse fills from this one. It names npm's OWN quantity
|
|
63
|
+
* (`dist.unpackedSize`), so it is npm-only and the parse REFUSES one on a
|
|
64
|
+
* github entry: a github figure is a different measurement and would be
|
|
65
|
+
* wrong under this key, not merely absent (registry `Entry.unpackedSize`).
|
|
66
|
+
* The registry keeps emitting it so that a client older than this one still
|
|
67
|
+
* shows npm sizes; it retires with the transform in `catalog.ts`. Additive
|
|
68
|
+
* and optional, so it rides every schemaVersion. */
|
|
62
69
|
unpackedSize?: number;
|
|
70
|
+
/** What installing this puts on disk, in bytes — the same quantity as
|
|
71
|
+
* `unpackedSize`, for every source (registry `Entry.installSize`). npm
|
|
72
|
+
* repeats its packument figure here; a github entry sums its git tree's
|
|
73
|
+
* blobs, or the release tarball it was rescued from. Absent wherever the
|
|
74
|
+
* measurement could not be made honestly: a size is a decoration, so a
|
|
75
|
+
* missing one costs the label and never the listing. Additive and optional,
|
|
76
|
+
* so it rides every schemaVersion.
|
|
77
|
+
*
|
|
78
|
+
* THE size field: read this one and never `unpackedSize`. The catalog parse
|
|
79
|
+
* fills it from `unpackedSize` when an older catalog carries only that key,
|
|
80
|
+
* so every consumer of a parsed entry sees one field whatever the data
|
|
81
|
+
* predates (`catalog.ts`, the transform below the entry superRefine). For a
|
|
82
|
+
* github entry the figure is scoped to `subdir` when it has one — 239 of the
|
|
83
|
+
* 6,492 live github entries — which is the plugin's own cost and not the
|
|
84
|
+
* repository's; registry `Entry.installSize` carries the measured caveat. */
|
|
85
|
+
installSize?: number;
|
|
63
86
|
}
|
|
64
87
|
export interface DeniedEntry {
|
|
65
88
|
name: string;
|
package/package.json
CHANGED