dsh-plugin-shop 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -1890,6 +1890,115 @@ window.__ModuleLoader__.load({
1890
1890
  result.value = merged.data;
1891
1891
  return result;
1892
1892
  }
1893
+ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
1894
+ $ZodType.init(inst, def);
1895
+ inst._zod.parse = (payload, ctx) => {
1896
+ const input = payload.value;
1897
+ if (!isPlainObject(input)) {
1898
+ payload.issues.push({
1899
+ expected: "record",
1900
+ code: "invalid_type",
1901
+ input,
1902
+ inst
1903
+ });
1904
+ return payload;
1905
+ }
1906
+ const proms = [];
1907
+ const values = def.keyType._zod.values;
1908
+ if (values) {
1909
+ payload.value = {};
1910
+ const recordKeys = /* @__PURE__ */ new Set();
1911
+ for (const key of values) if (typeof key === "string" || typeof key === "number" || typeof key === "symbol") {
1912
+ recordKeys.add(typeof key === "number" ? key.toString() : key);
1913
+ const keyResult = def.keyType._zod.run({
1914
+ value: key,
1915
+ issues: []
1916
+ }, ctx);
1917
+ if (keyResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
1918
+ if (keyResult.issues.length) {
1919
+ payload.issues.push({
1920
+ code: "invalid_key",
1921
+ origin: "record",
1922
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
1923
+ input: key,
1924
+ path: [key],
1925
+ inst
1926
+ });
1927
+ continue;
1928
+ }
1929
+ const outKey = keyResult.value;
1930
+ const result = def.valueType._zod.run({
1931
+ value: input[key],
1932
+ issues: []
1933
+ }, ctx);
1934
+ if (result instanceof Promise) proms.push(result.then((result) => {
1935
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
1936
+ payload.value[outKey] = result.value;
1937
+ }));
1938
+ else {
1939
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
1940
+ payload.value[outKey] = result.value;
1941
+ }
1942
+ }
1943
+ let unrecognized;
1944
+ for (const key in input) if (!recordKeys.has(key)) {
1945
+ unrecognized = unrecognized ?? [];
1946
+ unrecognized.push(key);
1947
+ }
1948
+ if (unrecognized && unrecognized.length > 0) payload.issues.push({
1949
+ code: "unrecognized_keys",
1950
+ input,
1951
+ inst,
1952
+ keys: unrecognized
1953
+ });
1954
+ } else {
1955
+ payload.value = {};
1956
+ for (const key of Reflect.ownKeys(input)) {
1957
+ if (key === "__proto__") continue;
1958
+ if (!Object.prototype.propertyIsEnumerable.call(input, key)) continue;
1959
+ let keyResult = def.keyType._zod.run({
1960
+ value: key,
1961
+ issues: []
1962
+ }, ctx);
1963
+ if (keyResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
1964
+ if (typeof key === "string" && number$1.test(key) && keyResult.issues.length) {
1965
+ const retryResult = def.keyType._zod.run({
1966
+ value: Number(key),
1967
+ issues: []
1968
+ }, ctx);
1969
+ if (retryResult instanceof Promise) throw new Error("Async schemas not supported in object keys currently");
1970
+ if (retryResult.issues.length === 0) keyResult = retryResult;
1971
+ }
1972
+ if (keyResult.issues.length) {
1973
+ if (def.mode === "loose") payload.value[key] = input[key];
1974
+ else payload.issues.push({
1975
+ code: "invalid_key",
1976
+ origin: "record",
1977
+ issues: keyResult.issues.map((iss) => finalizeIssue(iss, ctx, config())),
1978
+ input: key,
1979
+ path: [key],
1980
+ inst
1981
+ });
1982
+ continue;
1983
+ }
1984
+ const result = def.valueType._zod.run({
1985
+ value: input[key],
1986
+ issues: []
1987
+ }, ctx);
1988
+ if (result instanceof Promise) proms.push(result.then((result) => {
1989
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
1990
+ payload.value[keyResult.value] = result.value;
1991
+ }));
1992
+ else {
1993
+ if (result.issues.length) payload.issues.push(...prefixIssues(key, result.issues));
1994
+ payload.value[keyResult.value] = result.value;
1995
+ }
1996
+ }
1997
+ }
1998
+ if (proms.length) return Promise.all(proms).then(() => payload);
1999
+ return payload;
2000
+ };
2001
+ });
1893
2002
  const $ZodEnum = /*@__PURE__*/ $constructor("$ZodEnum", (inst, def) => {
1894
2003
  $ZodType.init(inst, def);
1895
2004
  const values = getEnumValues(def.entries);
@@ -3180,6 +3289,39 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3180
3289
  const isSimpleIntersection = (val) => "allOf" in val && Object.keys(val).length === 1;
3181
3290
  json.allOf = [...isSimpleIntersection(a) ? a.allOf : [a], ...isSimpleIntersection(b) ? b.allOf : [b]];
3182
3291
  };
3292
+ const recordProcessor = (schema, ctx, _json, params) => {
3293
+ const json = _json;
3294
+ const def = schema._zod.def;
3295
+ json.type = "object";
3296
+ const keyType = def.keyType;
3297
+ const patterns = keyType._zod.bag?.patterns;
3298
+ if (def.mode === "loose" && patterns && patterns.size > 0) {
3299
+ const valueSchema = process(def.valueType, ctx, {
3300
+ ...params,
3301
+ path: [
3302
+ ...params.path,
3303
+ "patternProperties",
3304
+ "*"
3305
+ ]
3306
+ });
3307
+ json.patternProperties = {};
3308
+ for (const pattern of patterns) json.patternProperties[pattern.source] = valueSchema;
3309
+ } else {
3310
+ if (ctx.target === "draft-07" || ctx.target === "draft-2020-12") json.propertyNames = process(def.keyType, ctx, {
3311
+ ...params,
3312
+ path: [...params.path, "propertyNames"]
3313
+ });
3314
+ json.additionalProperties = process(def.valueType, ctx, {
3315
+ ...params,
3316
+ path: [...params.path, "additionalProperties"]
3317
+ });
3318
+ }
3319
+ const keyValues = keyType._zod.values;
3320
+ if (keyValues) {
3321
+ const validKeyValues = [...keyValues].filter((v) => typeof v === "string" || typeof v === "number");
3322
+ if (validKeyValues.length > 0) json.required = validKeyValues;
3323
+ }
3324
+ };
3183
3325
  const nullableProcessor = (schema, ctx, json, params) => {
3184
3326
  const def = schema._zod.def;
3185
3327
  const inner = process(def.innerType, ctx, params);
@@ -3871,6 +4013,27 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3871
4013
  right
3872
4014
  });
3873
4015
  }
4016
+ const ZodRecord = /*@__PURE__*/ $constructor("ZodRecord", (inst, def) => {
4017
+ $ZodRecord.init(inst, def);
4018
+ ZodType.init(inst, def);
4019
+ inst._zod.processJSONSchema = (ctx, json, params) => recordProcessor(inst, ctx, json, params);
4020
+ inst.keyType = def.keyType;
4021
+ inst.valueType = def.valueType;
4022
+ });
4023
+ function record(keyType, valueType, params) {
4024
+ if (!valueType || !valueType._zod) return new ZodRecord({
4025
+ type: "record",
4026
+ keyType: string(),
4027
+ valueType: keyType,
4028
+ ...normalizeParams(valueType)
4029
+ });
4030
+ return new ZodRecord({
4031
+ type: "record",
4032
+ keyType,
4033
+ valueType,
4034
+ ...normalizeParams(params)
4035
+ });
4036
+ }
3874
4037
  const ZodEnum = /*@__PURE__*/ $constructor("ZodEnum", (inst, def) => {
3875
4038
  $ZodEnum.init(inst, def);
3876
4039
  ZodType.init(inst, def);
@@ -4140,7 +4303,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4140
4303
  "denied": array(object({
4141
4304
  "name": string(),
4142
4305
  "detail": string()
4143
- }))
4306
+ })),
4307
+ "stars": record(string(), number())
4144
4308
  });
4145
4309
  const dsh_plugin_shop_shop_installStart_parameter_0$schema = object({
4146
4310
  "name": string(),
@@ -4220,7 +4384,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4220
4384
  },
4221
4385
  sourceLocation: {
4222
4386
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4223
- "line": 177,
4387
+ "line": 180,
4224
4388
  "column": 9
4225
4389
  }
4226
4390
  },
@@ -4248,7 +4412,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4248
4412
  },
4249
4413
  sourceLocation: {
4250
4414
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4251
- "line": 202,
4415
+ "line": 206,
4252
4416
  "column": 9
4253
4417
  }
4254
4418
  },
@@ -4275,7 +4439,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4275
4439
  },
4276
4440
  sourceLocation: {
4277
4441
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4278
- "line": 240,
4442
+ "line": 244,
4279
4443
  "column": 3
4280
4444
  }
4281
4445
  },
@@ -4293,7 +4457,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4293
4457
  },
4294
4458
  sourceLocation: {
4295
4459
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4296
- "line": 248,
4460
+ "line": 252,
4297
4461
  "column": 9
4298
4462
  }
4299
4463
  },
@@ -4320,7 +4484,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4320
4484
  },
4321
4485
  sourceLocation: {
4322
4486
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4323
- "line": 146,
4487
+ "line": 149,
4324
4488
  "column": 3
4325
4489
  }
4326
4490
  }
@@ -4426,6 +4590,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4426
4590
  const segments = name.toLowerCase().split(/[-_.]+/);
4427
4591
  const hasPlugin = segments.includes("plugin");
4428
4592
  if (segments.some((segment) => /plugin(store|market|mall|shop|marketplace)/.test(segment) || /(store|market|mall|shop|marketplace)plugin/.test(segment))) return true;
4593
+ if (segments.some((segment) => segment.startsWith("dsh") && SHOP_KEYWORDS.some((keyword) => segment === `dsh${keyword}`))) return true;
4594
+ if (segments.length >= 2 && (segments[0] === "dsh" || segments[0]?.endsWith("/dsh") === true) && SHOP_KEYWORDS.includes(segments[1]) && (segments.length === 2 || segments[2] !== void 0 && [
4595
+ "plus",
4596
+ "pro",
4597
+ "hub",
4598
+ "center",
4599
+ "centre",
4600
+ "max",
4601
+ "free",
4602
+ "lite"
4603
+ ].includes(segments[2]))) return true;
4429
4604
  if (segments.filter((segment) => SHOP_KEYWORDS.includes(segment)).length === 0) return false;
4430
4605
  return hasPlugin || SHOP_KEYWORDS.includes(segments.at(-1));
4431
4606
  }
@@ -4442,6 +4617,28 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4442
4617
  function categoryKey(entry) {
4443
4618
  return CATEGORY_KEYS[entry.catalog?.category ?? "other"];
4444
4619
  }
4620
+ /** The six categories in display order (map insertion order). */
4621
+ const CATEGORY_ORDER = Object.keys(CATEGORY_KEYS);
4622
+ /** The locale key for one bare category value (the filter buttons). */
4623
+ function categoryLocaleKey(category) {
4624
+ return CATEGORY_KEYS[category];
4625
+ }
4626
+ /** Sort the shelf: stars descending, un-starred entries last, name ascending
4627
+ * (case-insensitive) on ties (spec 2026-08-26-github-stars-design.md D1).
4628
+ * Display-time only — the catalog's own name sort is untouched. */
4629
+ function sortByStars(entries, stars) {
4630
+ const count = (e) => stars[e.name] ?? -1;
4631
+ return [...entries].sort((a, b) => {
4632
+ const byStars = count(b) - count(a);
4633
+ if (byStars !== 0) return byStars;
4634
+ return a.name.localeCompare(b.name, void 0, { sensitivity: "base" });
4635
+ });
4636
+ }
4637
+ /** 999 → "999"; 1000 → "1k"; 1234 → "1.2k"; 1500 → "1.5k"; 99999 → "100k". */
4638
+ function formatStars(n) {
4639
+ if (n < 1e3) return String(n);
4640
+ return `${(n / 1e3).toFixed(1).replace(/\.0$/, "")}k`;
4641
+ }
4445
4642
  //#endregion
4446
4643
  //#region src/client/locales.ts
4447
4644
  /** Copy dictionaries for the shop Settings tab. */
@@ -4452,8 +4649,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4452
4649
  error: "暂时无法读取目录。",
4453
4650
  retry: "重试",
4454
4651
  search: "搜索插件",
4652
+ all: "全部",
4455
4653
  catalog: "插件目录",
4456
4654
  catalogStats: "{count} 个插件 · 构建于 {date}",
4655
+ stars: "{count} 星",
4457
4656
  categoryTool: "工具",
4458
4657
  categoryProvider: "模型服务",
4459
4658
  categoryUi: "界面",
@@ -4503,8 +4702,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4503
4702
  error: "The catalog is temporarily unavailable.",
4504
4703
  retry: "Retry",
4505
4704
  search: "Search plugins",
4705
+ all: "All",
4506
4706
  catalog: "Plugin catalog",
4507
4707
  catalogStats: "{count} packages · built {date}",
4708
+ stars: "{count} stars",
4508
4709
  categoryTool: "Tool",
4509
4710
  categoryProvider: "Provider",
4510
4711
  categoryUi: "UI",
@@ -4600,7 +4801,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4600
4801
  }
4601
4802
  //#endregion
4602
4803
  //#region \0dsh-plugin-shop-css:e3675a89
4603
- 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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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.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.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\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.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.sccc4d1_catalogStats {\n margin: -8px 0 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.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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\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 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.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.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.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.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 font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n";
4804
+ 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 (six fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so six brand opacities read as six shades\n * 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}\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: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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.s30e662_skeletonCover {\n display: block;\n height: 26px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 10%, transparent);\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 12px 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 14px;\n border-radius: 4px;\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.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.s1555aa_staleBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 10px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.sccc4d1_catalogStats {\n margin: -8px 0 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.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.sf657f5_cards {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));\n gap: 12px;\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 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 240px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n.s9e2bec_cardCover {\n display: block;\n padding: 5px 12px 5px 12px;\n border-bottom: 1px solid var(--dsw-alias-border-l1);\n background: color-mix(in srgb, var(--spine-hue, #8B8E96) 12%, transparent);\n}\n\n.s1a7c5b_coverLabel {\n display: block;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n gap: 8px;\n padding: 10px 12px 6px;\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 owns the full header width; badges sit on their own line below. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n overflow-wrap: anywhere;\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.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.s23e3e1_unclaimedBadge {\n padding: 1px 7px;\n border-radius: 999px;\n border: 1px dashed var(--dsw-alias-border-l2);\n font-size: 11px;\n line-height: 1.6;\n color: var(--dsw-alias-label-secondary);\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 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.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.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.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.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 font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n";
4604
4805
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
4605
4806
  const tag = document.createElement("style");
4606
4807
  tag.dataset.pluginCss = "e3675a89";
@@ -4626,6 +4827,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4626
4827
  "searchInput": "s232ec4_searchInput",
4627
4828
  "staleBlock": "s1555aa_staleBlock",
4628
4829
  "staleBadge": "s3946f1_staleBadge",
4830
+ "categoryBar": "sb2c859_categoryBar",
4831
+ "categoryButton": "s26d58e_categoryButton",
4832
+ "categoryButtonOn": "s5f68c4_categoryButtonOn",
4629
4833
  "catalogHeading": "s668774_catalogHeading",
4630
4834
  "catalogStats": "sccc4d1_catalogStats",
4631
4835
  "emptyLine": "s6de1ab_emptyLine",
@@ -4641,11 +4845,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4641
4845
  "badges": "s513026_badges",
4642
4846
  "tierBadge": "s5e82cb_tierBadge",
4643
4847
  "unclaimedBadge": "s23e3e1_unclaimedBadge",
4848
+ "starsBadge": "sff43b4_starsBadge",
4644
4849
  "chevron": "s73f7fb_chevron",
4645
4850
  "chevronOpen": "s61393c_chevronOpen",
4646
4851
  "body": "s820ebf_body",
4647
4852
  "summary": "sb8fbc8_summary",
4648
4853
  "summaryZh": "se507db_summaryZh",
4854
+ "summaryExpanded": "s10baf0_summaryExpanded",
4855
+ "summaryZhExpanded": "sb9dba9_summaryZhExpanded",
4649
4856
  "capabilitiesBlock": "s9d2029_capabilitiesBlock",
4650
4857
  "capabilitiesNote": "s3bdbd5_capabilitiesNote",
4651
4858
  "capabilities": "s441702_capabilities",
@@ -4703,7 +4910,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4703
4910
  * (name, tier, category, unclaimed marker), the plain-text summary in both
4704
4911
  * languages, the self-declared capabilities, the detail section, and the
4705
4912
  * install controls. */
4706
- const EntryCard = (0, react.memo)(function EntryCard({ entry, t, install, installStatus }) {
4913
+ const EntryCard = (0, react.memo)(function EntryCard({ entry, stars, t, install, installStatus }) {
4707
4914
  const [open, setOpen] = (0, react.useState)(false);
4708
4915
  const detailId = (0, react.useId)();
4709
4916
  const summary = entry.catalog?.summary;
@@ -4746,6 +4953,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4746
4953
  className: _dsh_plugin_shop_css_e3675a89_default.unclaimedBadge,
4747
4954
  children: t("unclaimed")
4748
4955
  }),
4956
+ stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
4957
+ className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
4958
+ role: "img",
4959
+ "aria-label": t("stars", { count: stars }),
4960
+ children: ["★ ", formatStars(stars)]
4961
+ }),
4749
4962
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ChevronIcon, { open })
4750
4963
  ]
4751
4964
  })]
@@ -4754,11 +4967,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4754
4967
  className: _dsh_plugin_shop_css_e3675a89_default.body,
4755
4968
  children: [
4756
4969
  summary !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4757
- className: _dsh_plugin_shop_css_e3675a89_default.summary,
4970
+ className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summary} ${_dsh_plugin_shop_css_e3675a89_default.summaryExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summary,
4758
4971
  children: summary.en
4759
4972
  }),
4760
4973
  summary?.zh !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
4761
- className: _dsh_plugin_shop_css_e3675a89_default.summaryZh,
4974
+ className: open ? `${_dsh_plugin_shop_css_e3675a89_default.summaryZh} ${_dsh_plugin_shop_css_e3675a89_default.summaryZhExpanded}` : _dsh_plugin_shop_css_e3675a89_default.summaryZh,
4762
4975
  children: summary.zh
4763
4976
  }),
4764
4977
  entry.catalog !== void 0 && entry.catalog.capabilities.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -4783,7 +4996,12 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4783
4996
  }),
4784
4997
  entry.repository !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4785
4998
  className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
4786
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("repository") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: entry.repository })]
4999
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("dt", { children: t("repository") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dd", { children: /^https?:\/\//.test(entry.repository) ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
5000
+ href: entry.repository,
5001
+ target: "_blank",
5002
+ rel: "noopener noreferrer",
5003
+ children: entry.repository
5004
+ }) : entry.repository })]
4787
5005
  }),
4788
5006
  entry.license !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
4789
5007
  className: _dsh_plugin_shop_css_e3675a89_default.detailRow,
@@ -5044,6 +5262,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5044
5262
  const [outdatedState, setOutdatedState] = (0, react.useState)({ kind: "loading" });
5045
5263
  const [request, setRequest] = (0, react.useState)({ kind: "initial" });
5046
5264
  const [query, setQuery] = (0, react.useState)("");
5265
+ const [category, setCategory] = (0, react.useState)(null);
5047
5266
  const incremental = typeof IntersectionObserver !== "undefined";
5048
5267
  const [visibleCount, setVisibleCount] = (0, react.useState)(48);
5049
5268
  const sentinelRef = (0, react.useRef)(null);
@@ -5090,13 +5309,20 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5090
5309
  const q = query.trim().toLowerCase();
5091
5310
  return catalogState.result.plugins.filter((entry) => {
5092
5311
  if (isShopLike(entry.name)) return false;
5312
+ if (category !== null && categoryKey(entry) !== categoryLocaleKey(category)) return false;
5093
5313
  if (q === "") return true;
5094
5314
  const summaryEn = entry.catalog?.summary.en ?? "";
5095
5315
  const summaryZh = entry.catalog?.summary.zh ?? "";
5096
5316
  return entry.name.toLowerCase().includes(q) || summaryEn.toLowerCase().includes(q) || summaryZh.toLowerCase().includes(q);
5097
5317
  });
5098
- }, [catalogState, query]);
5318
+ }, [
5319
+ catalogState,
5320
+ query,
5321
+ category
5322
+ ]);
5099
5323
  filteredLenRef.current = filtered.length;
5324
+ const stars = catalogState.kind === "ready" ? catalogState.result.stars : {};
5325
+ const sorted = (0, react.useMemo)(() => sortByStars(filtered, stars), [filtered, stars]);
5100
5326
  (0, react.useEffect)(() => {
5101
5327
  if (!incremental) return;
5102
5328
  const node = sentinelRef.current;
@@ -5111,7 +5337,17 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5111
5337
  visibleCount,
5112
5338
  filtered.length
5113
5339
  ]);
5114
- const visible = incremental ? filtered.slice(0, visibleCount) : filtered;
5340
+ const visible = incremental ? sorted.slice(0, visibleCount) : sorted;
5341
+ const categoryCounts = (0, react.useMemo)(() => {
5342
+ const counts = /* @__PURE__ */ new Map();
5343
+ if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) {
5344
+ if (isShopLike(entry.name)) continue;
5345
+ const key = categoryKey(entry);
5346
+ const bare = CATEGORY_ORDER.find((c) => categoryLocaleKey(c) === key);
5347
+ if (bare !== void 0) counts.set(bare, (counts.get(bare) ?? 0) + 1);
5348
+ }
5349
+ return counts;
5350
+ }, [catalogState]);
5115
5351
  const tiers = (0, react.useMemo)(() => {
5116
5352
  const map = /* @__PURE__ */ new Map();
5117
5353
  if (catalogState.kind === "ready") for (const entry of catalogState.result.plugins) map.set(entry.name, entry.tier);
@@ -5182,6 +5418,38 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5182
5418
  })]
5183
5419
  })]
5184
5420
  }),
5421
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
5422
+ className: _dsh_plugin_shop_css_e3675a89_default.categoryBar,
5423
+ role: "group",
5424
+ "aria-label": t("catalog"),
5425
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5426
+ type: "button",
5427
+ className: category === null ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5428
+ "aria-pressed": category === null,
5429
+ onClick: () => {
5430
+ setCategory(null);
5431
+ setVisibleCount(48);
5432
+ },
5433
+ children: [
5434
+ t("all"),
5435
+ " ",
5436
+ [...categoryCounts.values()].reduce((a, b) => a + b, 0)
5437
+ ]
5438
+ }), CATEGORY_ORDER.map((key) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
5439
+ type: "button",
5440
+ className: category === key ? `${_dsh_plugin_shop_css_e3675a89_default.categoryButton} ${_dsh_plugin_shop_css_e3675a89_default.categoryButtonOn}` : _dsh_plugin_shop_css_e3675a89_default.categoryButton,
5441
+ "aria-pressed": category === key,
5442
+ onClick: () => {
5443
+ setCategory(key);
5444
+ setVisibleCount(48);
5445
+ },
5446
+ children: [
5447
+ t(categoryLocaleKey(key)),
5448
+ " ",
5449
+ categoryCounts.get(key) ?? 0
5450
+ ]
5451
+ }, key))]
5452
+ }),
5185
5453
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
5186
5454
  className: _dsh_plugin_shop_css_e3675a89_default.catalogHeading,
5187
5455
  children: t("catalog")
@@ -5203,6 +5471,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5203
5471
  className: _dsh_plugin_shop_css_e3675a89_default.cards,
5204
5472
  children: [visible.map((entry) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("li", { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EntryCard, {
5205
5473
  entry,
5474
+ stars: stars[entry.name],
5206
5475
  t,
5207
5476
  install,
5208
5477
  installStatus
package/lib/index.js CHANGED
@@ -63,7 +63,11 @@ const pointerSchema = z.object({
63
63
  plugins: z.object({
64
64
  url: z.string(),
65
65
  sha256: z.string()
66
- })
66
+ }),
67
+ stars: z.object({
68
+ url: z.string(),
69
+ sha256: z.string()
70
+ }).optional()
67
71
  });
68
72
  const nodeFs = {
69
73
  exists: (path) => existsSync(path),
@@ -85,13 +89,28 @@ function resolveDataUrl(baseUrl, url) {
85
89
  if (resolved.origin !== new URL(baseUrl).origin) throw new Error("catalog data url must be relative to the catalog base");
86
90
  return resolved.href;
87
91
  }
92
+ /** Read and verify a cached/fetched stars sidecar; ANY irregularity degrades
93
+ * to an empty map — stars are advisory (spec §5). */
94
+ function parseStarsText(text) {
95
+ try {
96
+ const parsed = JSON.parse(text);
97
+ if (typeof parsed.stars !== "object" || parsed.stars === null) return {};
98
+ const out = {};
99
+ for (const [key, value] of Object.entries(parsed.stars)) if (typeof value === "number") out[key] = value;
100
+ return out;
101
+ } catch {
102
+ return {};
103
+ }
104
+ }
88
105
  /**
89
106
  * Load the catalog snapshot: fetch the pointer, verify the data file's sha256
90
107
  * against it, cache both on disk, and serve the cached copy with `stale: true`
91
108
  * only when the transport itself failed — the fetch threw or returned a
92
109
  * non-2xx (§10). A schemaVersion higher than this build supports, a malformed
93
110
  * pointer or data file, an absolute data URL, or a sha256 mismatch — fresh or
94
- * cached — throws even when a cache exists; never silently degraded.
111
+ * cached — throws even when a cache exists; never silently degraded. The stars
112
+ * sidecar is the sole exception: any irregularity there, a refused url
113
+ * included, degrades to no stars (spec §5).
95
114
  */
96
115
  async function loadCatalog(options) {
97
116
  const { baseUrl, cacheDir, refresh = false, fetchImpl = fetch, now = () => /* @__PURE__ */ new Date(), fsImpl = nodeFs } = options;
@@ -118,11 +137,17 @@ async function loadCatalog(options) {
118
137
  if (actual !== pointer.plugins.sha256) throw new Error(`cached catalog data failed integrity check: expected ${pointer.plugins.sha256}, got ${actual}`);
119
138
  const data = dataSchema.parse(JSON.parse(dataText));
120
139
  if (data.schemaVersion > 2) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (2)`);
140
+ let stars = {};
141
+ if (pointer.stars !== void 0) try {
142
+ const starsText = fsImpl.read(join(cacheDir, basename(pointer.stars.url)));
143
+ if (createHash("sha256").update(starsText).digest("hex") === pointer.stars.sha256) stars = parseStarsText(starsText);
144
+ } catch {}
121
145
  return {
122
146
  schemaVersion: pointer.schemaVersion,
123
147
  builtAt: pointer.builtAt,
124
148
  entries: data.plugins,
125
- denied: data.denied
149
+ denied: data.denied,
150
+ stars
126
151
  };
127
152
  } catch {
128
153
  return null;
@@ -171,11 +196,23 @@ async function loadCatalog(options) {
171
196
  if (actual !== pointer.plugins.sha256) throw new Error(`catalog data failed integrity check: expected ${pointer.plugins.sha256}, got ${actual}`);
172
197
  const data = dataSchema.parse(JSON.parse(dataText));
173
198
  if (data.schemaVersion > 2) throw new Error(`catalog schemaVersion ${data.schemaVersion} is newer than this build supports (2)`);
199
+ let stars = {};
200
+ if (pointer.stars !== void 0) try {
201
+ const starsResponse = await fetchImpl(resolveDataUrl(baseUrl, pointer.stars.url));
202
+ if (starsResponse.ok) {
203
+ const starsText = await starsResponse.text();
204
+ if (createHash("sha256").update(starsText).digest("hex") === pointer.stars.sha256) {
205
+ stars = parseStarsText(starsText);
206
+ fsImpl.write(join(cacheDir, basename(pointer.stars.url)), starsText);
207
+ }
208
+ }
209
+ } catch {}
174
210
  const snapshot = {
175
211
  schemaVersion: pointer.schemaVersion,
176
212
  builtAt: pointer.builtAt,
177
213
  entries: data.plugins,
178
- denied: data.denied
214
+ denied: data.denied,
215
+ stars
179
216
  };
180
217
  fsImpl.write(indexPath, JSON.stringify(pointer));
181
218
  fsImpl.write(join(cacheDir, basename(pointer.plugins.url)), dataText);
@@ -626,7 +663,8 @@ let ShopGateway = (() => {
626
663
  builtAt: snapshot.builtAt,
627
664
  stale,
628
665
  plugins: snapshot.entries,
629
- denied: snapshot.denied
666
+ denied: snapshot.denied,
667
+ stars: snapshot.stars
630
668
  };
631
669
  }
632
670
  /**
@@ -36,6 +36,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
36
36
  'name': z.string(),
37
37
  'detail': z.string(),
38
38
  })),
39
+ 'stars': z.record(z.string(), z.number()),
39
40
  })
40
41
  const dsh_plugin_shop_shop_installStart_parameter_0$schema = z.object({
41
42
  'name': z.string(),
@@ -104,7 +105,7 @@ export const TYPERT = {
104
105
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
105
106
  schema: dsh_plugin_shop_shop_catalog_result$schema,
106
107
  },
107
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":177,"column":9},
108
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":180,"column":9},
108
109
  },
109
110
  {
110
111
  id: 'dsh-plugin-shop#shop/installStart',
@@ -130,7 +131,7 @@ export const TYPERT = {
130
131
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
131
132
  schema: dsh_plugin_shop_shop_installStart_result$schema,
132
133
  },
133
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":202,"column":9},
134
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":206,"column":9},
134
135
  },
135
136
  {
136
137
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -155,7 +156,7 @@ export const TYPERT = {
155
156
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
156
157
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
157
158
  },
158
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":240,"column":3},
159
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":3},
159
160
  },
160
161
  {
161
162
  id: 'dsh-plugin-shop#shop/outdated',
@@ -170,7 +171,7 @@ export const TYPERT = {
170
171
  typeSymbol: 'dsh-plugin-shop#shop/outdated:result',
171
172
  schema: dsh_plugin_shop_shop_outdated_result$schema,
172
173
  },
173
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":248,"column":9},
174
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":252,"column":9},
174
175
  },
175
176
  {
176
177
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -195,7 +196,7 @@ export const TYPERT = {
195
196
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
196
197
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
197
198
  },
198
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":146,"column":3},
199
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":149,"column":3},
199
200
  },
200
201
  ],
201
202
  model: {
@@ -285,7 +286,7 @@ export const TYPERT = {
285
286
  },
286
287
  {
287
288
  "name": "ShopCatalogResult",
288
- "declaration": "export interface ShopCatalogResult {\n schemaVersion: number;\n builtAt: string;\n stale: boolean;\n plugins: CatalogEntry[];\n denied: DeniedEntry[];\n}"
289
+ "declaration": "export interface ShopCatalogResult {\n schemaVersion: number;\n builtAt: string;\n stale: boolean;\n plugins: CatalogEntry[];\n denied: DeniedEntry[];\n stars: Record<string, number>;\n}"
289
290
  },
290
291
  {
291
292
  "name": "ShopInstallResult",
@@ -36,6 +36,7 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
36
36
  'name': z.string(),
37
37
  'detail': z.string(),
38
38
  })),
39
+ 'stars': z.record(z.string(), z.number()),
39
40
  })
40
41
  const dsh_plugin_shop_shop_installStart_parameter_0$schema = z.object({
41
42
  'name': z.string(),
@@ -101,7 +102,7 @@ export const TYPERT_REMOTE = {
101
102
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
102
103
  schema: dsh_plugin_shop_shop_catalog_result$schema,
103
104
  },
104
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":177,"column":9},
105
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":180,"column":9},
105
106
  },
106
107
  {
107
108
  id: 'dsh-plugin-shop#shop/installStart',
@@ -127,7 +128,7 @@ export const TYPERT_REMOTE = {
127
128
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
128
129
  schema: dsh_plugin_shop_shop_installStart_result$schema,
129
130
  },
130
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":202,"column":9},
131
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":206,"column":9},
131
132
  },
132
133
  {
133
134
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -152,7 +153,7 @@ export const TYPERT_REMOTE = {
152
153
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
153
154
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
154
155
  },
155
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":240,"column":3},
156
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":3},
156
157
  },
157
158
  {
158
159
  id: 'dsh-plugin-shop#shop/outdated',
@@ -167,7 +168,7 @@ export const TYPERT_REMOTE = {
167
168
  typeSymbol: 'dsh-plugin-shop#shop/outdated:result',
168
169
  schema: dsh_plugin_shop_shop_outdated_result$schema,
169
170
  },
170
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":248,"column":9},
171
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":252,"column":9},
171
172
  },
172
173
  {
173
174
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -192,7 +193,7 @@ export const TYPERT_REMOTE = {
192
193
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
193
194
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
194
195
  },
195
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":146,"column":3},
196
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":149,"column":3},
196
197
  },
197
198
  ],
198
199
  }
@@ -6,8 +6,10 @@ export declare const zh: {
6
6
  error: string;
7
7
  retry: string;
8
8
  search: string;
9
+ all: string;
9
10
  catalog: string;
10
11
  catalogStats: string;
12
+ stars: string;
11
13
  categoryTool: string;
12
14
  categoryProvider: string;
13
15
  categoryUi: string;
@@ -59,8 +61,10 @@ export declare const en: {
59
61
  error: string;
60
62
  retry: string;
61
63
  search: string;
64
+ all: string;
62
65
  catalog: string;
63
66
  catalogStats: string;
67
+ stars: string;
64
68
  categoryTool: string;
65
69
  categoryProvider: string;
66
70
  categoryUi: string;
@@ -81,6 +81,28 @@ export declare function reduceInstall(state: InstallView, event: InstallEvent):
81
81
  * it matches other people's package names, not ours.
82
82
  */
83
83
  export declare function isShopLike(name: string): boolean;
84
+ declare const CATEGORY_KEYS: {
85
+ readonly tool: "categoryTool";
86
+ readonly provider: "categoryProvider";
87
+ readonly ui: "categoryUi";
88
+ readonly workflow: "categoryWorkflow";
89
+ readonly integration: "categoryIntegration";
90
+ readonly other: "categoryOther";
91
+ };
84
92
  /** The locale key for one entry's category — derived entries read as `other`
85
93
  * (§6.1: a derived listing has no declared category). */
86
94
  export declare function categoryKey(entry: CatalogEntry): ShopLocaleKey;
95
+ /** The category vocabulary as a type, derived from the map above so the
96
+ * client never repeats the six literals. */
97
+ export type Category = keyof typeof CATEGORY_KEYS;
98
+ /** The six categories in display order (map insertion order). */
99
+ export declare const CATEGORY_ORDER: Category[];
100
+ /** The locale key for one bare category value (the filter buttons). */
101
+ export declare function categoryLocaleKey(category: Category): ShopLocaleKey;
102
+ /** Sort the shelf: stars descending, un-starred entries last, name ascending
103
+ * (case-insensitive) on ties (spec 2026-08-26-github-stars-design.md D1).
104
+ * Display-time only — the catalog's own name sort is untouched. */
105
+ export declare function sortByStars(entries: CatalogEntry[], stars: Record<string, number>): CatalogEntry[];
106
+ /** 999 → "999"; 1000 → "1k"; 1234 → "1.2k"; 1500 → "1.5k"; 99999 → "100k". */
107
+ export declare function formatStars(n: number): string;
108
+ export {};
@@ -7,6 +7,9 @@ export interface CatalogSnapshot {
7
7
  builtAt: string;
8
8
  entries: CatalogEntry[];
9
9
  denied: DeniedEntry[];
10
+ /** GitHub star counts by package name; {} when the pointer names no
11
+ * sidecar or the sidecar could not be fetched/verified (spec §5). */
12
+ stars: Record<string, number>;
10
13
  }
11
14
  export interface CatalogResult {
12
15
  snapshot: CatalogSnapshot;
@@ -32,6 +35,8 @@ export interface LoadCatalogOptions {
32
35
  * only when the transport itself failed — the fetch threw or returned a
33
36
  * non-2xx (§10). A schemaVersion higher than this build supports, a malformed
34
37
  * pointer or data file, an absolute data URL, or a sha256 mismatch — fresh or
35
- * cached — throws even when a cache exists; never silently degraded.
38
+ * cached — throws even when a cache exists; never silently degraded. The stars
39
+ * sidecar is the sole exception: any irregularity there, a refused url
40
+ * included, degrades to no stars (spec §5).
36
41
  */
37
42
  export declare function loadCatalog(options: LoadCatalogOptions): Promise<CatalogResult>;
@@ -62,6 +62,9 @@ export interface ShopCatalogResult {
62
62
  stale: boolean;
63
63
  plugins: CatalogEntry[];
64
64
  denied: DeniedEntry[];
65
+ /** GitHub star counts by package name; {} when the pointer names no sidecar
66
+ * or the sidecar could not be fetched/verified (§5). */
67
+ stars: Record<string, number>;
65
68
  }
66
69
  /** Remote-only service exposing the shop Remote methods of §7.3.
67
70
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.3.1",
3
+ "version": "0.4.1",
4
4
  "description": "The DeepSeek Harness plugin shop: browse, install, enable, and update dsh plugins from a git-auditable catalog.",
5
5
  "repository": {
6
6
  "type": "git",