dsh-plugin-shop 0.7.4 → 0.7.5-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -4437,7 +4437,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4437
4437
  },
4438
4438
  sourceLocation: {
4439
4439
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4440
- "line": 577,
4440
+ "line": 623,
4441
4441
  "column": 9
4442
4442
  }
4443
4443
  },
@@ -4455,7 +4455,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4455
4455
  },
4456
4456
  sourceLocation: {
4457
4457
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4458
- "line": 763,
4458
+ "line": 809,
4459
4459
  "column": 9
4460
4460
  }
4461
4461
  },
@@ -4483,7 +4483,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4483
4483
  },
4484
4484
  sourceLocation: {
4485
4485
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4486
- "line": 628,
4486
+ "line": 674,
4487
4487
  "column": 9
4488
4488
  }
4489
4489
  },
@@ -4510,7 +4510,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4510
4510
  },
4511
4511
  sourceLocation: {
4512
4512
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4513
- "line": 752,
4513
+ "line": 798,
4514
4514
  "column": 3
4515
4515
  }
4516
4516
  },
@@ -4528,7 +4528,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4528
4528
  },
4529
4529
  sourceLocation: {
4530
4530
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4531
- "line": 913,
4531
+ "line": 959,
4532
4532
  "column": 9
4533
4533
  }
4534
4534
  },
@@ -4555,7 +4555,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4555
4555
  },
4556
4556
  sourceLocation: {
4557
4557
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4558
- "line": 469,
4558
+ "line": 515,
4559
4559
  "column": 9
4560
4560
  }
4561
4561
  },
@@ -4583,7 +4583,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4583
4583
  },
4584
4584
  sourceLocation: {
4585
4585
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4586
- "line": 855,
4586
+ "line": 901,
4587
4587
  "column": 9
4588
4588
  }
4589
4589
  },
@@ -4610,7 +4610,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4610
4610
  },
4611
4611
  sourceLocation: {
4612
4612
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4613
- "line": 981,
4613
+ "line": 1027,
4614
4614
  "column": 9
4615
4615
  }
4616
4616
  },
@@ -4628,7 +4628,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4628
4628
  },
4629
4629
  sourceLocation: {
4630
4630
  "file": "packages/dsh-plugin-shop/src/host/index.ts",
4631
- "line": 964,
4631
+ "line": 1010,
4632
4632
  "column": 9
4633
4633
  }
4634
4634
  }
@@ -5300,7 +5300,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5300
5300
  }
5301
5301
  //#endregion
5302
5302
  //#region \0dsh-plugin-shop-css:e3675a89
5303
- const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s73242a_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. */\n.s64a5ff_reloadButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.s64a5ff_reloadButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s64a5ff_reloadButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s4453f3_reloadFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='theme'] { --spine-hue: #F472B6; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--spine-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The publishing account, beside the npm link. Secondary label colour and a\n * middot separator carry the distinction — never a background fill, which is\n * invisible in the light theme where every bg-layer token is the same white. */\n/* Who published the entry, at the right edge of the action row. `margin-left:\n * auto` is what pushes it there; the row is a wrapping flex, so an active\n * install flow (which claims the full width) drops this to the next line\n * rather than squeezing it. */\n.s1ff66f_author {\n margin-left: auto;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
5303
+ const css = "/* Shop tab styles — a market shelf inside the settings surface. The visual\n * language is the dsh theme's alias tokens ONLY (the four tokens the previous\n * styles referenced that do not exist — bg-layer-3, interactive-bg-hover,\n * state-error, state-success — are corrected to bg-layer-2 and the\n * state-*-primary set). The signature is the category spine: one HUE per\n * category (seven fixed hues, not the brand token — the brand resolves to\n * near-black in the light theme, so seven brand opacities read as seven\n * shades of gray), with the category name as the cover label in the same hue. Class names are mapped to content-derived names at bundle time\n * (tsdown.s8583e3_client.s942a79_config.s5f67aa_ts) and stubbed in tests; only the keys are relied\n * on.\n *\n * One token rule the theme forces: the background LAYERS do not carry\n * contrast. In the light theme bg-base, bg-layer-1, bg-layer-2 and\n * bg-layer-3 all resolve to the same white; only the dark theme spreads them\n * (950/875/850/800). So a layer-2 fill on a layer-1 ground is invisible for\n * half of all users, and anything whose fill is its ONLY affordance — the\n * loading skeleton, the borderless capability chips — derives its fill from\n * label-primary instead, which inverts with the theme by construction.\n * Elements that also carry a border or text may keep a layer fill.\n * `css-tokens.s8583e3_client.sfd9d8a_spec.s5f67aa_ts` pins this. */\n\n.s255e5a_panel {\n display: flex;\n flex-direction: column;\n gap: 14px;\n /* The shelf never scrolls sideways: a card too wide for its track wraps\n * its badges rather than widening the panel.\n *\n * Clipped on BOTH axes, with a margin, because `.s232ec4_searchInput` sits at\n * exactly (0, 0) of this box — it is the first child of `.s646509_toolbar`, itself\n * the first child here, and this rule carries no padding — so its focus\n * ring paints ENTIRELY outside the panel.\n *\n * Both axes is the load-bearing part: `overflow-clip-margin` is inert\n * unless the element clips on both. Clipping x alone left the margin doing\n * nothing — measured against the live app on 0.7.1, the ring reached 0 of\n * its 3px on the left at every device pixel ratio (1, 1.25, 1.5, 2), while\n * keeping the full 3px on the right, where the 280px input stops short of\n * the panel and never meets the clip edge. Raising the margin to 6px\n * changed nothing; clipping both axes with the SAME 3px restored the whole\n * ring. A test that reads declarations cannot see that difference, which is\n * why the first attempt at this shipped broken.\n *\n * `clip` is not a scroll container, so this does not bring back the\n * accidental scrolling `hidden` caused by coercing the other axis to\n * `auto`. Nothing is lost vertically: the panel's scrollHeight equals its\n * height and no descendant paints outside the clip rect (measured).\n * `overflow-x: hidden` stays as the fallback for engines without `clip`,\n * where the ring is clipped as it was before, never worse. */\n overflow-x: hidden;\n overflow: clip;\n overflow-clip-margin: 3px;\n}\n\n.s44a3ec_stateLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* Visually hidden but screen-reader readable (the loading copy while the\n * skeleton stands in visually). */\n.s3a0ece_srOnly {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n border: 0;\n clip: rect(0 0 0 0);\n clip-path: inset(50%);\n overflow: hidden;\n white-space: nowrap;\n}\n\n/* The loading shelf: ghost cards with the same geometry as the real grid, so\n * the swap to content is a same-shape handoff instead of a jump. */\n.se0c3b4_skeletonGrid {\n display: grid;\n grid-template-columns: 1fr;\n gap: 8px;\n}\n\n.s520416_skeletonCard {\n position: relative;\n display: flex;\n flex-direction: column;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n}\n\n/* The shimmer sweep: one gradient band travels across each ghost card while\n * the bars breathe, so the loading state reads as \"the shelf is scanning\". */\n.s520416_skeletonCard::after {\n content: '';\n position: absolute;\n inset: 0;\n background: linear-gradient(\n 100deg,\n transparent 25%,\n color-mix(in srgb, var(--dsw-alias-label-primary) 14%, transparent) 50%,\n transparent 75%\n );\n transform: translateX(-100%);\n animation: dshShopSkeletonSweep 1.4s ease-in-out infinite;\n pointer-events: none;\n}\n\n@keyframes dshShopSkeletonSweep {\n to { transform: translateX(100%); }\n}\n\n.s8c7d6f_skeletonName {\n width: 55%;\n height: 14px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sf5179e_skeletonSummary {\n width: 92%;\n height: 12px;\n margin: 10px 12px 0;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.sbb0d25_skeletonSummaryShort {\n width: 68%;\n height: 12px;\n margin: 6px 12px 10px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n/* The ghost of the action line, right where the real buttons land. */\n.s96e910_skeletonActions {\n display: block;\n width: 84px;\n height: 24px;\n margin: 0 12px 10px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 8%, transparent);\n}\n\n.s520416_skeletonCard span {\n animation: dshShopSkeletonPulse 1.6s ease-in-out infinite;\n}\n\n@keyframes dshShopSkeletonPulse {\n 0%, 100% { opacity: 0.5; }\n 50% { opacity: 1; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s520416_skeletonCard span { animation: none; opacity: 0.75; }\n .s520416_skeletonCard::after { content: none; }\n}\n\n.s73242a_actionButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s73242a_actionButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s73242a_actionButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s646509_toolbar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n.s745e3c_toolbarRight {\n display: flex;\n align-items: center;\n gap: 12px;\n flex-wrap: wrap;\n}\n\n/* The shop's own version, right of the search box: `v0.4.4`, plus the\n * update button when the version check found a newer release. */\n.s4fe61f_versionBlock {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.sb9b924_versionText {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa04455_updateSelfButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sa04455_updateSelfButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 20%, transparent);\n}\n\n.sa04455_updateSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The on-demand re-check next to the version number: a quiet pill; the\n * border and text carry it in the light theme where bg layers are white. */\n.s614a38_checkUpdateButton {\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s614a38_checkUpdateButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n color: var(--dsw-alias-label-primary);\n}\n\n.s614a38_checkUpdateButton:disabled {\n opacity: 0.6;\n cursor: default;\n}\n\n.s614a38_checkUpdateButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The project's GitHub mark, right of the version row: a quiet icon link.\n * The octocat carries itself — color shift on hover, no text, no border. */\n.sa987f7_githubLink {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 3px;\n border-radius: 6px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa987f7_githubLink:hover {\n color: var(--dsw-alias-label-primary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n.sa987f7_githubLink:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The self-update's expanded states (running log / done + restart /\n * failure detail) sit under the toolbar, above the category bar. */\n.sb81ba5_selfUpdatePanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 8px;\n}\n\n.s232ec4_searchInput {\n width: min(280px, 100%);\n padding: 6px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n.s232ec4_searchInput:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s3946f1_staleBadge {\n padding: 2px 8px;\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n color: var(--dsw-alias-state-warn-primary);\n font-size: 12px;\n}\n\n.sb2c859_categoryBar {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n margin-top: 6px;\n}\n\n.s26d58e_categoryButton {\n padding: 3px 10px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-1);\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n cursor: pointer;\n}\n\n.s26d58e_categoryButton:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s5f68c4_categoryButtonOn {\n border-color: var(--dsw-alias-brand-primary);\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-weight: 600;\n}\n\n.s26d58e_categoryButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s668774_catalogHeading {\n margin: 2px 0 0;\n font-size: 16px;\n font-weight: 600;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-primary);\n}\n\n.seb0a1e_catalogStatsRow {\n display: flex;\n align-items: center;\n gap: 10px;\n margin: 6px 0 0;\n}\n\n.sccc4d1_catalogStats {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* Sits beside the 12px stats caption, so it is quieter than `.s73242a_actionButton`:\n * this is an affordance, not a call to action — the catalog reloads on its\n * own at mount and the shelf is already usable without it. */\n.s64a5ff_reloadButton {\n padding: 1px 9px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: transparent;\n color: var(--dsw-alias-label-secondary);\n font-size: 11px;\n line-height: 17px;\n cursor: pointer;\n}\n\n.s64a5ff_reloadButton:hover:not(:disabled) {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l2));\n color: var(--dsw-alias-brand-primary);\n}\n\n.s64a5ff_reloadButton:disabled {\n cursor: default;\n opacity: 0.6;\n}\n\n.s64a5ff_reloadButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s4453f3_reloadFailed {\n font-size: 11px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s6de1ab_emptyLine {\n margin: 0;\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n}\n\n/* The shelf: a responsive grid of cards. */\n/* Single-line rows: one full-width column, one plugin per line. */\n.sf657f5_cards {\n display: grid;\n grid-template-columns: 1fr;\n max-width: 100%;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n animation: dshShopCardsIn 220ms ease;\n}\n\n@keyframes dshShopCardsIn {\n from { opacity: 0; transform: translateY(3px); }\n to { opacity: 1; transform: none; }\n}\n\n@media (prefers-reduced-motion: reduce) {\n .sf657f5_cards { animation: none; }\n}\n\n.s65f383_card {\n position: relative;\n display: flex;\n flex-direction: column;\n /* A grid item's min-width is auto by default: its content would widen the\n * 1fr track past the panel and overflow horizontally. */\n min-width: 0;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 10px;\n background: var(--dsw-alias-bg-layer-1);\n overflow: hidden;\n transition: border-color 120ms ease, transform 120ms ease, box-shadow 120ms ease;\n /* Off-screen cards skip layout and paint; the placeholder keeps scrollbar\n * estimation sane before the browser has measured the real height. */\n content-visibility: auto;\n contain-intrinsic-size: auto 140px;\n}\n\n/* The sentinel that triggers the next shelf batch: zero visual footprint. */\n.sd7ee01_cardsSentry {\n height: 1px;\n margin: 0;\n padding: 0;\n}\n\n.s705cc6_showingLine {\n margin: 0;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s65f383_card:hover {\n border-color: color-mix(in srgb, var(--dsw-alias-brand-primary) 45%, var(--dsw-alias-border-l1));\n transform: translateY(-1px);\n box-shadow: 0 2px 8px color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s65f383_card { transition: none; }\n .s65f383_card:hover { transform: none; }\n}\n\n/* The signature: a spine and cover in the category's own hue. The hues are\n * fixed mid-bright colors that read on both themes; `other` deliberately\n * stays a neutral gray so the fallback category does not compete. */\n.s4ce25d_cardSpine {\n position: absolute;\n top: 0;\n bottom: 0;\n left: 0;\n width: 3px;\n background: var(--spine-hue, #8B8E96);\n}\n\n.s65f383_card[data-category='tool'] { --spine-hue: #4C8DFF; }\n.s65f383_card[data-category='provider'] { --spine-hue: #A78BFA; }\n.s65f383_card[data-category='ui'] { --spine-hue: #2DD4BF; }\n.s65f383_card[data-category='workflow'] { --spine-hue: #F59E0B; }\n.s65f383_card[data-category='integration'] { --spine-hue: #34D399; }\n.s65f383_card[data-category='theme'] { --spine-hue: #F472B6; }\n.s65f383_card[data-category='other'] { --spine-hue: #8B8E96; }\n\n/* The category sits in the badge row as a quiet chip in the category's own\n * hue — the border carries it where the light theme's white layers cannot\n * (the same lesson as the check button). */\n.s9c8eaa_categoryBadge {\n padding: 1px 7px;\n border: 1px solid color-mix(in srgb, var(--spine-hue, #8B8E96) 45%, transparent);\n border-radius: 999px;\n font-size: 11px;\n font-weight: 600;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--spine-hue, #8B8E96);\n white-space: nowrap;\n}\n\n.sa97234_entryHeader {\n display: flex;\n flex-direction: row;\n align-items: center;\n gap: 12px;\n min-width: 0;\n padding: 8px 12px 2px;\n border: none;\n background: transparent;\n color: inherit;\n font: inherit;\n text-align: left;\n cursor: pointer;\n}\n\n.sa97234_entryHeader:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: -2px;\n}\n\n/* The name fills the row and truncates rather than wraps; the badges stay\n * intact beside it. */\n.sc3f149_name {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n font-weight: 600;\n flex: 1 1 auto;\n min-width: 0;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--dsw-alias-label-primary);\n}\n\n.s513026_badges {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n/* The action line: buttons side by side; an active install/uninstall flow\n * (gate, log, notices) takes the full width below them. */\n.s9b5048_cardActions {\n display: flex;\n flex-direction: row;\n flex-wrap: wrap;\n align-items: center;\n gap: 8px;\n padding: 8px 12px 10px;\n}\n\n.s9b5048_cardActions > .sc4f2ac_gate,\n.s9b5048_cardActions > .s4d374c_installPanel,\n.s9b5048_cardActions > .sf53ca0_installedActions {\n flex-basis: 100%;\n}\n\n/* The entry's version, always visible on the card (the expanded detail\n * repeats it in the version row) — the same quiet mono as the toolbar. */\n.sc6ec6e_cardVersion {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 12px;\n letter-spacing: 0.02em;\n color: var(--dsw-alias-label-secondary);\n}\n\n/* The octocat marking a github-sourced entry, beside its short commit. */\n.s71f10f_sourceBadge {\n display: inline-flex;\n align-items: center;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s5e82cb_tierBadge {\n padding: 1px 7px;\n border-radius: 999px;\n font-size: 11px;\n line-height: 1.6;\n}\n\n.s5e82cb_tierBadge[data-tier='verified'] {\n color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-success-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='verified-stale'] {\n color: var(--dsw-alias-state-warn-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary) 40%, transparent);\n}\n\n.s5e82cb_tierBadge[data-tier='community'] {\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n border: 1px solid var(--dsw-alias-border-l2);\n}\n\n.s4bbe02_incompatibleBadge {\n flex-shrink: 0;\n padding: 3px 9px;\n border-radius: 999px;\n font-size: 12px;\n white-space: nowrap;\n color: var(--dsw-alias-state-error-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 14%, transparent);\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 40%, transparent);\n}\n\n.sff43b4_starsBadge {\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s73f7fb_chevron, .s61393c_chevronOpen {\n color: var(--dsw-alias-label-secondary);\n transition: transform 120ms ease;\n}\n\n.s61393c_chevronOpen { transform: rotate(180deg); }\n\n@media (prefers-reduced-motion: reduce) {\n .s73f7fb_chevron, .s61393c_chevronOpen { transition: none; }\n}\n\n.s820ebf_body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 0 12px;\n}\n\n.sb8fbc8_summary {\n margin: 0;\n font-size: 13px;\n line-height: 1.5;\n color: var(--dsw-alias-label-primary);\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n.se507db_summaryZh {\n margin: -4px 0 0;\n font-size: 12px;\n line-height: 1.5;\n color: var(--dsw-alias-label-secondary);\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n overflow: hidden;\n}\n\n/* Expanded cards show the full summary: the clamp is lifted, never raised —\n * a higher number would still truncate the author's text, just later. */\n.s10baf0_summaryExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n.sb9dba9_summaryZhExpanded {\n -webkit-line-clamp: unset;\n display: block;\n}\n\n/* `white-space: pre-line` so the newline in the copy is the copy's own: the\n dictionary decides where the line breaks, and a translator changes it\n without touching the component. Collapses runs of spaces like `normal`, so\n only the explicit \\n survives. */\n.s6eca19_incompatibleDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.5;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s9d2029_capabilitiesBlock {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.s3bdbd5_capabilitiesNote {\n margin: 0;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s441702_capabilities {\n display: flex;\n flex-wrap: wrap;\n gap: 4px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s441702_capabilities li {\n padding: 1px 6px;\n border-radius: 4px;\n background: color-mix(in srgb, var(--dsw-alias-label-primary) 6%, transparent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sde3f0c_detail {\n border-top: 1px solid var(--dsw-alias-border-l1);\n padding-top: 8px;\n}\n\n.se98179_detailRows {\n display: flex;\n flex-direction: column;\n gap: 4px;\n margin: 0;\n}\n\n.sfb4d2f_detailRow {\n display: flex;\n justify-content: space-between;\n gap: 12px;\n font-size: 12px;\n}\n\n.sfb4d2f_detailRow dt {\n color: var(--dsw-alias-label-secondary);\n}\n\n.sfb4d2f_detailRow dd {\n margin: 0;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n overflow-wrap: anywhere;\n text-align: right;\n color: var(--dsw-alias-label-primary);\n}\n\n.sfb4d2f_detailRow a {\n color: var(--dsw-alias-brand-primary);\n text-decoration: none;\n}\n\n.sfb4d2f_detailRow a:hover {\n text-decoration: underline;\n}\n\n.sfb4d2f_detailRow a:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The publishing account, beside the npm link. Secondary label colour and a\n * middot separator carry the distinction — never a background fill, which is\n * invisible in the light theme where every bg-layer token is the same white. */\n/* Who published the entry, at the right edge of the action row. `margin-left:\n * auto` is what pushes it there; the row is a wrapping flex, so an active\n * install flow (which claims the full width) drops this to the next line\n * rather than squeezing it. */\n.s1ff66f_author {\n margin-left: auto;\n color: var(--dsw-alias-label-secondary);\n font-size: 12px;\n}\n\n.sd58c6d_reviewedLine {\n margin: 8px 0 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 12%, transparent);\n font-size: 12px;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n/* Install flow. */\n.s4d374c_installPanel {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 2px;\n}\n\n.se7aaed_installButton, .sb43a49_confirmButton, .s6e45d1_cancelButton {\n align-self: flex-start;\n padding: 5px 14px;\n border-radius: 6px;\n font-size: 13px;\n cursor: pointer;\n}\n\n.se7aaed_installButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.se7aaed_installButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.sb43a49_confirmButton {\n border: 1px solid var(--dsw-alias-brand-primary);\n background: var(--dsw-alias-brand-primary);\n color: var(--dsw-alias-bg-base);\n}\n\n.sb43a49_confirmButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 88%, var(--dsw-alias-label-primary));\n}\n\n.s6e45d1_cancelButton {\n border: 1px solid var(--dsw-alias-border-l2);\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n}\n\n.s6e45d1_cancelButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.se7aaed_installButton:focus-visible, .sb43a49_confirmButton:focus-visible, .s6e45d1_cancelButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n/* The shelf card of a plugin that is installed and current carries this\n * non-interactive label instead of an install button. Border + text do the\n * work in the light theme, where every bg-layer token resolves to white. */\n.sb6fc74_installedLabel {\n align-self: flex-start;\n margin: 0;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n font-size: 13px;\n color: var(--dsw-alias-label-secondary);\n background: var(--dsw-alias-bg-layer-2);\n}\n\n/* An installed card's controls: the installed label or the update button,\n * plus the uninstall button. The install panel's running/failed states are\n * column layouts and sit as one item of this row. */\n.sf53ca0_installedActions {\n display: flex;\n align-items: flex-start;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n/* Uninstall revokes privilege; the quiet outline keeps it from competing\n * with the primary action, and the error tint says \"removes\". */\n.s96967f_uninstallButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 55%, transparent);\n border-radius: 6px;\n background: transparent;\n color: var(--dsw-alias-state-error-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s96967f_uninstallButton:hover {\n background: color-mix(in srgb, var(--dsw-alias-state-error-primary) 10%, transparent);\n}\n\n.s96967f_uninstallButton:focus-visible {\n outline: 2px solid var(--dsw-alias-state-error-primary);\n outline-offset: 1px;\n}\n\n/* The restart dsh button on a done install: quiet outline, same rank as\n * the other secondary actions. */\n.sb00c36_restartSelfButton {\n /* The version row's third state. Geometry matches .s614a38_checkUpdateButton and\n * .sa04455_updateSelfButton exactly — the row is one evolving control and a\n * different pill would read as a different kind of thing. It takes the\n * emphasised fill rather than the neutral one because at that moment the\n * restart is the only thing left to do, the same reason Update has it. */\n padding: 2px 10px;\n border: 1px solid var(--dsw-alias-brand-primary);\n border-radius: 999px;\n background: color-mix(in srgb, var(--dsw-alias-brand-primary) 12%, transparent);\n color: var(--dsw-alias-brand-primary);\n font-size: 12px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.sb00c36_restartSelfButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.s6ac237_restartButton {\n align-self: flex-start;\n padding: 5px 14px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 6px;\n background: var(--dsw-alias-bg-layer-2);\n color: var(--dsw-alias-label-primary);\n font-size: 13px;\n cursor: pointer;\n}\n\n.s6ac237_restartButton:hover {\n border-color: var(--dsw-alias-label-secondary);\n}\n\n.s6ac237_restartButton:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n.sc4f2ac_gate {\n display: flex;\n flex-direction: column;\n gap: 8px;\n padding: 10px;\n border: 1px solid var(--dsw-alias-state-warn-primary);\n border-radius: 8px;\n background: color-mix(in srgb, var(--dsw-alias-state-warn-primary) 8%, transparent);\n}\n\n.s559c38_gateTitle {\n margin: 0;\n font-size: 13px;\n font-weight: 600;\n color: var(--dsw-alias-state-warn-primary);\n}\n\n.s3c0ace_gateBody {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.sc323e5_gateWarning {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n white-space: pre-line;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.s4b2373_gateActions {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n}\n\n.s52b5a5_installing {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.s64ab71_log {\n max-height: 160px;\n overflow: auto;\n margin: 0;\n padding: 8px;\n border-radius: 6px;\n background: var(--dsw-alias-bg-base);\n border: 1px solid var(--dsw-alias-border-l1);\n list-style: none;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n}\n\n.s1531a8_logLine {\n white-space: pre-wrap;\n word-break: break-all;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sf20dda_notice {\n margin: 0;\n font-size: 12px;\n color: var(--dsw-alias-state-success-primary);\n}\n\n.seb9b08_failedHeading {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.sfd3021_failedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n.s50ba35_rejectedCode {\n margin: 0;\n font-size: 12px;\n font-weight: 600;\n color: var(--dsw-alias-state-error-primary);\n}\n\n.se75489_rejectedDetail {\n margin: 0;\n font-size: 12px;\n line-height: 1.6;\n color: var(--dsw-alias-label-primary);\n}\n\n/* Installed section. */\n.sa276cf_outdatedSection {\n display: flex;\n flex-direction: column;\n gap: 8px;\n margin-top: 6px;\n}\n\n.sceae6e_outdatedList {\n display: flex;\n flex-direction: column;\n gap: 8px;\n list-style: none;\n margin: 0;\n padding: 0;\n}\n\n.s8d2072_outdatedRow {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n flex-wrap: wrap;\n padding: 10px 12px;\n border: 1px solid var(--dsw-alias-border-l1);\n border-radius: 8px;\n background: var(--dsw-alias-bg-layer-1);\n}\n\n.s5c2545_outdatedInfo {\n display: flex;\n flex-direction: column;\n gap: 2px;\n min-width: 0;\n}\n\n.s5c2545_outdatedInfo strong {\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 13px;\n word-break: break-all;\n color: var(--dsw-alias-label-primary);\n}\n\n.s877c68_outdatedVersions {\n /* Two inline labels with nothing between them in the markup ran together as\n * `installed v1.0.0latest v2.0.0`: JSX drops the whitespace-only line, and\n * the per-label class the markup asked for was never defined (singular\n * against this rule's plural). The container owns the gap, the same idiom\n * as .s513026_badges — the labels need no class of their own. */\n display: inline-flex;\n align-items: center;\n gap: 8px;\n font-size: 12px;\n color: var(--dsw-alias-label-secondary);\n}\n\n.sa1f556_outdatedActions {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n/* The enable/disable switch. */\n.s11c019_switch {\n position: relative;\n width: 32px;\n height: 18px;\n border: 1px solid var(--dsw-alias-border-l2);\n border-radius: 999px;\n background: var(--dsw-alias-bg-layer-2);\n cursor: pointer;\n transition: background 120ms ease, border-color 120ms ease;\n}\n\n.s7580e8_switchOn {\n border-color: var(--dsw-alias-state-success-primary);\n background: color-mix(in srgb, var(--dsw-alias-state-success-primary) 55%, transparent);\n}\n\n.saab2b3_switchKnob {\n position: absolute;\n top: 2px;\n left: 2px;\n width: 12px;\n height: 12px;\n border-radius: 999px;\n background: var(--dsw-alias-label-primary);\n transition: transform 120ms ease, background 120ms ease;\n}\n\n.s7580e8_switchOn .saab2b3_switchKnob {\n transform: translateX(14px);\n background: var(--dsw-alias-bg-base);\n}\n\n.s11c019_switch:focus-visible {\n outline: 2px solid var(--dsw-alias-brand-primary);\n outline-offset: 1px;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .s11c019_switch, .saab2b3_switchKnob { transition: none; }\n}\n\n/* The hot enable/disable switch on installed rows: the knob button plus the\n * §8 hot-apply / failure notices rendered beside it. */\n.sd9f9e4_switchWrap {\n display: inline-flex;\n align-items: center;\n gap: 8px;\n}\n";
5304
5304
  if (typeof document !== "undefined" && !document.querySelector("style[data-plugin-css=\"e3675a89\"]")) {
5305
5305
  const tag = document.createElement("style");
5306
5306
  tag.dataset.pluginCss = "e3675a89";
@@ -5506,12 +5506,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5506
5506
  "data-tier": entry.tier,
5507
5507
  children: t(tierKey(entry.tier))
5508
5508
  }),
5509
- missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5510
- className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
5511
- "data-shop-incompatible": true,
5512
- title: t("incompatibleDetail", { modules: missing.join(", ") }),
5513
- children: t("incompatibleBadge")
5514
- }),
5515
5509
  stars !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
5516
5510
  className: _dsh_plugin_shop_css_e3675a89_default.starsBadge,
5517
5511
  role: "img",
@@ -5618,11 +5612,14 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5618
5612
  installStatus,
5619
5613
  restart,
5620
5614
  restartSupported
5621
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5615
+ }) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
5622
5616
  className: _dsh_plugin_shop_css_e3675a89_default.installedLabel,
5623
5617
  "data-shop-installed": true,
5624
5618
  children: t("installed")
5625
- }),
5619
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IncompatibleBadge, {
5620
+ missing,
5621
+ t
5622
+ })] }),
5626
5623
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)(EnabledSwitch, {
5627
5624
  row: installed,
5628
5625
  t,
@@ -5645,6 +5642,24 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5645
5642
  ]
5646
5643
  });
5647
5644
  });
5645
+ /** The harness-compatibility verdict, rendered beside the control it
5646
+ * qualifies. It answers "what happens if I press this", not "what is this",
5647
+ * so it belongs to the action row and not among the identity badges in the
5648
+ * header — where it also sat inside a <button>, competing with that button's
5649
+ * own hit area for the tooltip. Renders nothing when the harness provides
5650
+ * everything. */
5651
+ function IncompatibleBadge({ missing, t }) {
5652
+ if (missing.length === 0) return null;
5653
+ const detail = t("incompatibleDetail", { modules: missing.join(", ") });
5654
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
5655
+ className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
5656
+ "data-shop-incompatible": true,
5657
+ role: "img",
5658
+ "aria-label": detail,
5659
+ title: detail,
5660
+ children: t("incompatibleBadge")
5661
+ });
5662
+ }
5648
5663
  /** One entry's install flow: the button, the §9.3 acknowledgement gate for
5649
5664
  * community-tier entries, and the live view — running log, restart notice,
5650
5665
  * failure detail, rejection detail — driven by `useInstall`. Shared by the
@@ -5756,7 +5771,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5756
5771
  ]
5757
5772
  });
5758
5773
  const update = variant === "update";
5759
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5774
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
5760
5775
  type: "button",
5761
5776
  className: _dsh_plugin_shop_css_e3675a89_default.installButton,
5762
5777
  ...update ? { "data-shop-update": true } : { "data-shop-install": true },
@@ -5769,7 +5784,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5769
5784
  else setGateOpen(true);
5770
5785
  },
5771
5786
  children: t(update ? "update" : "install")
5772
- });
5787
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(IncompatibleBadge, {
5788
+ missing,
5789
+ t
5790
+ })] });
5773
5791
  }
5774
5792
  /** One installed entry's uninstall flow: remove from the profile through the
5775
5793
  * same executor records and poll loop as installs. Uninstalling revokes
@@ -6012,17 +6030,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6012
6030
  "data-shop-outdated-entry": row.name,
6013
6031
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
6014
6032
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedInfo,
6015
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6016
- className: _dsh_plugin_shop_css_e3675a89_default.badges,
6017
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6018
- className: _dsh_plugin_shop_css_e3675a89_default.name,
6019
- children: row.name
6020
- }), missing.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6021
- className: _dsh_plugin_shop_css_e3675a89_default.incompatibleBadge,
6022
- "data-shop-incompatible": true,
6023
- title: t("incompatibleDetail", { modules: missing.join(", ") }),
6024
- children: t("incompatibleBadge")
6025
- })]
6033
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
6034
+ className: _dsh_plugin_shop_css_e3675a89_default.name,
6035
+ children: row.name
6026
6036
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
6027
6037
  className: _dsh_plugin_shop_css_e3675a89_default.outdatedVersions,
6028
6038
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("installedVersion", { version: source === "github" ? row.installed.slice(0, 7) : row.installed }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("latestVersion", { version: row.latest }) })]
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { createRequire } from "node:module";
2
2
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
3
3
  import { loadOptionalPatches, readProfileManifest, resolveProfileDir } from "@deepseek-ai/dsh-app-boot";
4
- import { lt, minVersion, valid } from "semver";
4
+ import { lt, minVersion, satisfies, valid, validRange } from "semver";
5
5
  import { createHash, randomUUID } from "node:crypto";
6
6
  import { closeSync, existsSync, mkdirSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { fileURLToPath, pathToFileURL } from "node:url";
@@ -18,8 +18,27 @@ import { JSON_SCHEMA, Type, dump, load } from "js-yaml";
18
18
  * both the source tree (tests) and the bundled `lib/index.js` sit exactly
19
19
  * one level below the package root, so the same relative URL resolves in
20
20
  * both. */
21
+ function ownManifest() {
22
+ return JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8"));
23
+ }
21
24
  function ownVersion() {
22
- return JSON.parse(readFileSync(fileURLToPath(new URL("../package.json", import.meta.url)), "utf8")).version;
25
+ return ownManifest().version;
26
+ }
27
+ /**
28
+ * The peer ranges this build declares — the input to the load-time harness
29
+ * self-check. Read from the same shipped manifest as `ownVersion`, so the
30
+ * ranges checked are the ones this build was actually published with, never
31
+ * a second copy that can drift from them.
32
+ *
33
+ * A non-string range is dropped rather than repaired: only what we can
34
+ * compare against is worth a verdict.
35
+ */
36
+ function ownPeerRanges() {
37
+ const declared = ownManifest().peerDependencies;
38
+ if (declared === null || typeof declared !== "object") return {};
39
+ const ranges = {};
40
+ for (const [spec, range] of Object.entries(declared)) if (typeof range === "string") ranges[spec] = range;
41
+ return ranges;
23
42
  }
24
43
  //#endregion
25
44
  //#region src/host/origin.ts
@@ -1618,7 +1637,9 @@ function ownsEntryId(owned, entryId) {
1618
1637
  //#endregion
1619
1638
  //#region src/host/peers.ts
1620
1639
  /** Harness compatibility: which declared peers the running installation does
1621
- * not provide (design 2026-09-01-harness-compatibility). */
1640
+ * not provide (design 2026-09-01-harness-compatibility), and — for the
1641
+ * shop's OWN declared peers — which ones it provides at a version outside
1642
+ * the declared range. */
1622
1643
  /**
1623
1644
  * The production resolver: the same question the harness's own
1624
1645
  * ClientModuleRegistry asks, through a require anchored at the profile. Asking
@@ -1673,6 +1694,101 @@ function incompatibilityMap(entries, resolve) {
1673
1694
  }
1674
1695
  return out;
1675
1696
  }
1697
+ /**
1698
+ * The production resolver: `nodeResolver`'s own resolution, kept this time
1699
+ * instead of collapsed to a boolean. Anything that leaves no version to read
1700
+ * answers null — a peer that is absent, one that restricts `./package.json`
1701
+ * in its exports, an unreadable or malformed manifest, a manifest with no
1702
+ * `version`. Absence is deliberately NOT reported as a version violation:
1703
+ * `incompatibilityMap` is what covers a missing peer.
1704
+ */
1705
+ function nodeVersionResolver(baseUrl) {
1706
+ const require = createRequire(baseUrl);
1707
+ return (spec) => {
1708
+ let manifestPath;
1709
+ try {
1710
+ manifestPath = require.resolve(`${spec}/package.json`);
1711
+ } catch {
1712
+ return null;
1713
+ }
1714
+ try {
1715
+ const version = JSON.parse(readFileSync(manifestPath, "utf8")).version;
1716
+ return typeof version === "string" ? version : null;
1717
+ } catch {
1718
+ return null;
1719
+ }
1720
+ };
1721
+ }
1722
+ /**
1723
+ * The declared peers whose provided version is outside the declared range,
1724
+ * ordered by peer name. Pure: the module graph arrives through `resolve`.
1725
+ *
1726
+ * `includePrerelease` is load-bearing, not a convenience. The harness ships
1727
+ * nothing but `-rc` versions, so under strict semver `^0.1.1-rc.2` excludes
1728
+ * `0.1.2-rc.1` — the version that is installed and works — and every future
1729
+ * rc bump would raise a false alarm. With it on, the range still excludes an
1730
+ * older prerelease (`0.1.1-rc.1`) and a minor- or major-line move
1731
+ * (`0.2.0-rc.1`, `1.0.0`), which are the moves that actually break a plugin
1732
+ * path. Discrimination on both sides is the whole point: one false warning
1733
+ * teaches a reader to ignore every warning.
1734
+ *
1735
+ * Anything unreadable yields NO verdict for that peer and never a violation:
1736
+ * a resolver that answers null or throws, a found version that is not
1737
+ * semver, a declared range semver cannot parse. The other peers are still
1738
+ * judged.
1739
+ */
1740
+ function peerVersionMismatches(ranges, resolve) {
1741
+ const mismatches = [];
1742
+ for (const spec of Object.keys(ranges).sort()) {
1743
+ const range = ranges[spec];
1744
+ if (range === void 0 || validRange(range) === null) continue;
1745
+ let found;
1746
+ try {
1747
+ found = resolve(spec);
1748
+ } catch {
1749
+ continue;
1750
+ }
1751
+ if (found === null || valid(found) === null) continue;
1752
+ if (satisfies(found, range, { includePrerelease: true })) continue;
1753
+ mismatches.push({
1754
+ spec,
1755
+ range,
1756
+ found
1757
+ });
1758
+ }
1759
+ return mismatches;
1760
+ }
1761
+ /**
1762
+ * The load-time message for a set of mismatches, or null when there is
1763
+ * nothing to say. Every mismatch is named with its declared range and the
1764
+ * version found, in ONE message: the reader needs the whole picture in the
1765
+ * line they will scroll past, and the message says the shop still loads
1766
+ * because refusing to load would cost them the shop over a diagnostic.
1767
+ */
1768
+ function peerVersionWarning(mismatches) {
1769
+ if (mismatches.length === 0) return null;
1770
+ return `dsh-plugin-shop: the harness does not provide the peer versions this shop declares — ${mismatches.map(({ spec, range, found }) => `${spec} ${range}, found ${found}`).join("; ")}. The shop still loads; if a path misbehaves, check this first.`;
1771
+ }
1772
+ /**
1773
+ * A self-check that warns AT MOST ONCE, however many times it is called.
1774
+ * The guard is what keeps a diagnostic a diagnostic: a message repeated per
1775
+ * call is noise a reader learns to skip, and the shop's load path is not the
1776
+ * only place that could reasonably ask this question.
1777
+ *
1778
+ * It never throws. A check that could fail a load would be worse than the
1779
+ * mismatch it reports — the user would lose the whole shop over a warning.
1780
+ */
1781
+ function createPeerVersionCheck(deps) {
1782
+ let spoken = false;
1783
+ return () => {
1784
+ if (spoken) return;
1785
+ spoken = true;
1786
+ try {
1787
+ const message = peerVersionWarning(peerVersionMismatches(deps.ranges, deps.resolve));
1788
+ if (message !== null) deps.warn(message);
1789
+ } catch {}
1790
+ };
1791
+ }
1676
1792
  //#endregion
1677
1793
  //#region src/host/index.ts
1678
1794
  /** ShopGateway: the Host half of dsh-plugin-shop (§5.1). */
@@ -1981,6 +2097,26 @@ let ShopGateway = (() => {
1981
2097
  try {
1982
2098
  cleanHotDir(this.profileDirResolved());
1983
2099
  } catch {}
2100
+ this.checkPeerVersions();
2101
+ }
2102
+ /**
2103
+ * Warn once, at load, about any declared peer the harness provides outside
2104
+ * its declared range. Advisory by construction: it cannot throw, and it
2105
+ * never refuses the load — losing the whole shop is a worse outcome than
2106
+ * running against a harness that moved.
2107
+ */
2108
+ checkPeerVersions() {
2109
+ try {
2110
+ createPeerVersionCheck({
2111
+ ranges: this.options.peerRanges ?? ownPeerRanges(),
2112
+ resolve: this.options.resolvePeerVersion ?? nodeVersionResolver(pathToFileURL(join(this.profileDirResolved(), "cordis.yml")).href),
2113
+ warn: (message) => {
2114
+ const logger = this.ctx.logger;
2115
+ if (logger === void 0) console.warn(message);
2116
+ else logger.warn(message);
2117
+ }
2118
+ })();
2119
+ } catch {}
1984
2120
  }
1985
2121
  /** The pins file lives in the shop's own cache, next to the catalog cache. */
1986
2122
  pinsPath() {
@@ -155,7 +155,7 @@ export const TYPERT = {
155
155
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
156
156
  schema: dsh_plugin_shop_shop_catalog_result$schema,
157
157
  },
158
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":577,"column":9},
158
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":623,"column":9},
159
159
  },
160
160
  {
161
161
  id: 'dsh-plugin-shop#shop/installed',
@@ -170,7 +170,7 @@ export const TYPERT = {
170
170
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
171
171
  schema: dsh_plugin_shop_shop_installed_result$schema,
172
172
  },
173
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":763,"column":9},
173
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":809,"column":9},
174
174
  },
175
175
  {
176
176
  id: 'dsh-plugin-shop#shop/installStart',
@@ -196,7 +196,7 @@ export const TYPERT = {
196
196
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
197
197
  schema: dsh_plugin_shop_shop_installStart_result$schema,
198
198
  },
199
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":628,"column":9},
199
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":674,"column":9},
200
200
  },
201
201
  {
202
202
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -221,7 +221,7 @@ export const TYPERT = {
221
221
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
222
222
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
223
223
  },
224
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":752,"column":3},
224
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":798,"column":3},
225
225
  },
226
226
  {
227
227
  id: 'dsh-plugin-shop#shop/restart',
@@ -236,7 +236,7 @@ export const TYPERT = {
236
236
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
237
237
  schema: dsh_plugin_shop_shop_restart_result$schema,
238
238
  },
239
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":913,"column":9},
239
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":959,"column":9},
240
240
  },
241
241
  {
242
242
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -261,7 +261,7 @@ export const TYPERT = {
261
261
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
262
262
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
263
263
  },
264
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":469,"column":9},
264
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":515,"column":9},
265
265
  },
266
266
  {
267
267
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -287,7 +287,7 @@ export const TYPERT = {
287
287
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
288
288
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
289
289
  },
290
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":855,"column":9},
290
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":901,"column":9},
291
291
  },
292
292
  {
293
293
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -312,7 +312,7 @@ export const TYPERT = {
312
312
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
313
313
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
314
314
  },
315
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":981,"column":9},
315
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1027,"column":9},
316
316
  },
317
317
  {
318
318
  id: 'dsh-plugin-shop#shop/version',
@@ -327,7 +327,7 @@ export const TYPERT = {
327
327
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
328
328
  schema: dsh_plugin_shop_shop_version_result$schema,
329
329
  },
330
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":964,"column":9},
330
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1010,"column":9},
331
331
  },
332
332
  ],
333
333
  model: {
@@ -152,7 +152,7 @@ export const TYPERT_REMOTE = {
152
152
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
153
153
  schema: dsh_plugin_shop_shop_catalog_result$schema,
154
154
  },
155
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":577,"column":9},
155
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":623,"column":9},
156
156
  },
157
157
  {
158
158
  id: 'dsh-plugin-shop#shop/installed',
@@ -167,7 +167,7 @@ export const TYPERT_REMOTE = {
167
167
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
168
168
  schema: dsh_plugin_shop_shop_installed_result$schema,
169
169
  },
170
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":763,"column":9},
170
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":809,"column":9},
171
171
  },
172
172
  {
173
173
  id: 'dsh-plugin-shop#shop/installStart',
@@ -193,7 +193,7 @@ export const TYPERT_REMOTE = {
193
193
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
194
194
  schema: dsh_plugin_shop_shop_installStart_result$schema,
195
195
  },
196
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":628,"column":9},
196
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":674,"column":9},
197
197
  },
198
198
  {
199
199
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -218,7 +218,7 @@ export const TYPERT_REMOTE = {
218
218
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
219
219
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
220
220
  },
221
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":752,"column":3},
221
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":798,"column":3},
222
222
  },
223
223
  {
224
224
  id: 'dsh-plugin-shop#shop/restart',
@@ -233,7 +233,7 @@ export const TYPERT_REMOTE = {
233
233
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
234
234
  schema: dsh_plugin_shop_shop_restart_result$schema,
235
235
  },
236
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":913,"column":9},
236
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":959,"column":9},
237
237
  },
238
238
  {
239
239
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -258,7 +258,7 @@ export const TYPERT_REMOTE = {
258
258
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
259
259
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
260
260
  },
261
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":469,"column":9},
261
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":515,"column":9},
262
262
  },
263
263
  {
264
264
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -284,7 +284,7 @@ export const TYPERT_REMOTE = {
284
284
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
285
285
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
286
286
  },
287
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":855,"column":9},
287
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":901,"column":9},
288
288
  },
289
289
  {
290
290
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -309,7 +309,7 @@ export const TYPERT_REMOTE = {
309
309
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
310
310
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
311
311
  },
312
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":981,"column":9},
312
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1027,"column":9},
313
313
  },
314
314
  {
315
315
  id: 'dsh-plugin-shop#shop/version',
@@ -324,7 +324,7 @@ export const TYPERT_REMOTE = {
324
324
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
325
325
  schema: dsh_plugin_shop_shop_version_result$schema,
326
326
  },
327
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":964,"column":9},
327
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1010,"column":9},
328
328
  },
329
329
  ],
330
330
  }
@@ -8,7 +8,7 @@ import { type InstallStatus } from './executor.ts';
8
8
  import { hotMount, hotUnmount } from './hot.ts';
9
9
  import { type RestartOutcome } from './restart.ts';
10
10
  import { type RepoPinFs } from './repo-pins.ts';
11
- import { type PeerResolver } from './peers.ts';
11
+ import { type PeerResolver, type PeerVersionResolver } from './peers.ts';
12
12
  export type { InstallArgs, InstallRejectionCode } from './install.ts';
13
13
  export type { HotRestartReason } from './hot.ts';
14
14
  export type { CatalogEntry } from './types.ts';
@@ -97,6 +97,12 @@ export interface ShopGatewayOptions {
97
97
  /** Test-only injection: answers whether a peer resolves. Production builds
98
98
  * one from the profile anchor. */
99
99
  resolvePeer?: PeerResolver;
100
+ /** Test-only injection: answers which version a peer resolves at, for the
101
+ * load-time self-check. Production builds one from the profile anchor. */
102
+ resolvePeerVersion?: PeerVersionResolver;
103
+ /** Test-only injection: the peer ranges the self-check judges against;
104
+ * production reads them from the shipped package.json. */
105
+ peerRanges?: Record<string, string>;
100
106
  }
101
107
  /** `shop/installStart` result (§7.3): rejections are typed wire values with an
102
108
  * author-readable `detail`, not thrown RPC errors. */
@@ -260,6 +266,13 @@ export declare class ShopGateway extends TypertRemoteService {
260
266
  /** Every install id in insertion order, oldest first; finished-record eviction walks this from the front. */
261
267
  private readonly installOrder;
262
268
  constructor(ctx: Context, options?: ShopGatewayOptions);
269
+ /**
270
+ * Warn once, at load, about any declared peer the harness provides outside
271
+ * its declared range. Advisory by construction: it cannot throw, and it
272
+ * never refuses the load — losing the whole shop is a worse outcome than
273
+ * running against a harness that moved.
274
+ */
275
+ private checkPeerVersions;
263
276
  /** The pins file lives in the shop's own cache, next to the catalog cache. */
264
277
  private pinsPath;
265
278
  /** The boot's Loader root directory (the active profile's `cordis.yml`
@@ -1,5 +1,7 @@
1
1
  /** Harness compatibility: which declared peers the running installation does
2
- * not provide (design 2026-09-01-harness-compatibility). */
2
+ * not provide (design 2026-09-01-harness-compatibility), and — for the
3
+ * shop's OWN declared peers — which ones it provides at a version outside
4
+ * the declared range. */
3
5
  /** Answers "can this installation provide `spec`?" — injected so fixtures
4
6
  * drive every verdict and exactly one call site touches the filesystem. */
5
7
  export type PeerResolver = (spec: string) => boolean;
@@ -23,3 +25,72 @@ export declare function incompatibilityMap(entries: readonly {
23
25
  name: string;
24
26
  peers?: string[];
25
27
  }[], resolve: PeerResolver): Record<string, string[]>;
28
+ /**
29
+ * The same question one step further on: which VERSION does this
30
+ * installation provide for `spec`? `null` is the no-verdict signal — the
31
+ * peer is absent, or its manifest could not be read. Injected exactly like
32
+ * `PeerResolver`, so fixtures drive every verdict and only one call site
33
+ * touches the filesystem.
34
+ */
35
+ export type PeerVersionResolver = (spec: string) => string | null;
36
+ /** One declared peer whose provided version is outside its declared range. */
37
+ export interface PeerVersionMismatch {
38
+ spec: string;
39
+ /** The range this build declares in `peerDependencies`. */
40
+ range: string;
41
+ /** The version the installation actually provides. */
42
+ found: string;
43
+ }
44
+ /**
45
+ * The production resolver: `nodeResolver`'s own resolution, kept this time
46
+ * instead of collapsed to a boolean. Anything that leaves no version to read
47
+ * answers null — a peer that is absent, one that restricts `./package.json`
48
+ * in its exports, an unreadable or malformed manifest, a manifest with no
49
+ * `version`. Absence is deliberately NOT reported as a version violation:
50
+ * `incompatibilityMap` is what covers a missing peer.
51
+ */
52
+ export declare function nodeVersionResolver(baseUrl: string): PeerVersionResolver;
53
+ /**
54
+ * The declared peers whose provided version is outside the declared range,
55
+ * ordered by peer name. Pure: the module graph arrives through `resolve`.
56
+ *
57
+ * `includePrerelease` is load-bearing, not a convenience. The harness ships
58
+ * nothing but `-rc` versions, so under strict semver `^0.1.1-rc.2` excludes
59
+ * `0.1.2-rc.1` — the version that is installed and works — and every future
60
+ * rc bump would raise a false alarm. With it on, the range still excludes an
61
+ * older prerelease (`0.1.1-rc.1`) and a minor- or major-line move
62
+ * (`0.2.0-rc.1`, `1.0.0`), which are the moves that actually break a plugin
63
+ * path. Discrimination on both sides is the whole point: one false warning
64
+ * teaches a reader to ignore every warning.
65
+ *
66
+ * Anything unreadable yields NO verdict for that peer and never a violation:
67
+ * a resolver that answers null or throws, a found version that is not
68
+ * semver, a declared range semver cannot parse. The other peers are still
69
+ * judged.
70
+ */
71
+ export declare function peerVersionMismatches(ranges: Readonly<Record<string, string>>, resolve: PeerVersionResolver): PeerVersionMismatch[];
72
+ /**
73
+ * The load-time message for a set of mismatches, or null when there is
74
+ * nothing to say. Every mismatch is named with its declared range and the
75
+ * version found, in ONE message: the reader needs the whole picture in the
76
+ * line they will scroll past, and the message says the shop still loads
77
+ * because refusing to load would cost them the shop over a diagnostic.
78
+ */
79
+ export declare function peerVersionWarning(mismatches: readonly PeerVersionMismatch[]): string | null;
80
+ /** What a load-time check needs: the ranges this build declares, a way to
81
+ * read what the installation provides, and somewhere to say it once. */
82
+ export interface PeerVersionCheckDeps {
83
+ ranges: Readonly<Record<string, string>>;
84
+ resolve: PeerVersionResolver;
85
+ warn: (message: string) => void;
86
+ }
87
+ /**
88
+ * A self-check that warns AT MOST ONCE, however many times it is called.
89
+ * The guard is what keeps a diagnostic a diagnostic: a message repeated per
90
+ * call is noise a reader learns to skip, and the shop's load path is not the
91
+ * only place that could reasonably ask this question.
92
+ *
93
+ * It never throws. A check that could fail a load would be worse than the
94
+ * mismatch it reports — the user would lose the whole shop over a warning.
95
+ */
96
+ export declare function createPeerVersionCheck(deps: PeerVersionCheckDeps): () => void;
@@ -5,3 +5,13 @@
5
5
  * one level below the package root, so the same relative URL resolves in
6
6
  * both. */
7
7
  export declare function ownVersion(): string;
8
+ /**
9
+ * The peer ranges this build declares — the input to the load-time harness
10
+ * self-check. Read from the same shipped manifest as `ownVersion`, so the
11
+ * ranges checked are the ones this build was actually published with, never
12
+ * a second copy that can drift from them.
13
+ *
14
+ * A non-string range is dropped rather than repaired: only what we can
15
+ * compare against is worth a verdict.
16
+ */
17
+ export declare function ownPeerRanges(): Record<string, string>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.7.4",
3
+ "version": "0.7.5-beta.0",
4
4
  "description": "The DeepSeek Harness plugin shop: browse, install, enable, and update dsh plugins from a git-auditable catalog.",
5
5
  "keywords": [
6
6
  "dsh-plugin",