@noob-stupid/dsh-plugin-console 0.5.92 → 0.5.94

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
@@ -1,4 +1,4 @@
1
- window.__ModuleLoader__.load({
1
+ window.__ModuleLoader__.load({
2
2
  id: "@noob-stupid/dsh-plugin-console",
3
3
  factory: (require) => {
4
4
  var module = { exports: {} };
@@ -6,7 +6,7 @@ window.__ModuleLoader__.load({
6
6
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
7
7
  let react = require("react");
8
8
  //#region styles
9
- const css = ".pc_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.pc_section h3{margin:0;font-size:13px;font-weight:600;line-height:20px}.pc_message{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;margin:0}.pc_messageRow{display:flex;align-items:center;gap:10px}.pc_spinner{width:14px;height:14px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}@keyframes pcspin{to{transform:rotate(360deg)}}.pc_message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.pc_message[data-warn=true]{color:var(--dsw-alias-state-warning-primary)}.pc_list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none;display:grid}.pc_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_rowTop{align-items:center;gap:8px;display:flex}.pc_name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pc_tag{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px;flex:none}.pc_tag[data-enabled=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-user=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_tag[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_status[data-pending=true]{color:var(--dsw-alias-state-warning-primary)}.pc_toggle[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-skill=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-system=true]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-disabled=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tag[data-suite=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tabGroup{display:inline-flex;gap:4px;align-items:center}.pc_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;gap:6px;display:flex;align-items:center}.pc_phase[data-phase=failed]{color:var(--dsw-alias-state-error-primary)}.pc_phase[data-phase=active]{color:var(--dsw-alias-state-success-primary)}.pc_toggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_toggle:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_toggle:disabled{opacity:.5;cursor:default}.pc_search{gap:8px;display:flex}.pc_search input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;flex:1;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 12px;font-size:13px}.pc_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.pc_search button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:0 14px;font-size:13px}.pc_search button:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_market{flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;display:flex}.pc_item{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_itemTop{align-items:center;gap:8px;display:flex}.pc_itemTop a{color:var(--dsw-alias-state-business-primary);font-size:13px;font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pc_stars{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}.pc_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_item button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;align-self:flex-start;font-size:12px}.pc_item button:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_item button:disabled{opacity:.5;cursor:default}.pc_detail{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}.pc_note[data-truncated=true]{color:var(--dsw-alias-state-warning-primary)}.pc_marketHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.pc_descWrap{flex-direction:column;display:flex}.pc_installedHead{display:flex;align-items:center;gap:10px}.pc_backTopWrap{position:fixed;top:56px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px}.pc_backTop{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(22,27,34,.72);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;backdrop-filter:blur(4px);opacity:.7}.pc_backTop:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_descTopbar{display:flex;justify-content:flex-end;margin:2px 0}.pc_ghpill{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;line-height:18px}.pc_ghpill[data-on=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_extraBtn{opacity:.75;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 10px;font-size:12px;line-height:16px;flex:none;transition:opacity .15s ease,border-color 1.5s ease,color 1.5s ease}.pc_extraBtn:hover{opacity:1}.pc_extraBtn[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_extraBtn[data-flash=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_view{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:2px 10px;font-size:12px;line-height:18px;flex:none}.pc_floatPanel{position:fixed;left:20px;top:50%;transform:translateY(-50%);width:340px;max-height:75vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:50;flex-direction:column;gap:8px;display:flex}.pc_refresh{position:sticky;bottom:14px;align-self:flex-end;margin-top:8px;z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_refresh:hover{opacity:.9}.pc_date{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;flex:none}.pc_searchInner{position:relative;flex:1;display:flex;align-items:center;min-width:0}.pc_searchInner input{padding-right:32px;width:100%}.pc_starBtn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;opacity:.75;transition:opacity .15s ease;flex:none}.pc_starBtn:hover{opacity:1}.pc_starBtn[data-active=true]{opacity:1;color:var(--dsw-alias-state-business-primary)}.pc_restartBtn{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_restartBtn:hover{opacity:.9}.pc_relaunchFloat{position:fixed;left:20px;top:50%;transform:translateY(-50%);z-index:70;opacity:.85;border:1px solid var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_relaunchFloat:hover{opacity:1}.pc_aiToggle{position:fixed;right:20px;top:20px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiToggle:hover{opacity:1}.pc_aiToggle[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_sourcesFloat{position:fixed;right:20px;top:104px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_sourcesFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat{position:fixed;right:20px;top:204px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_modeFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_trashBtn{border:0;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:16px;padding:2px 6px;flex:none;display:flex;align-items:center;border-radius:6px;transition:color .15s ease}.pc_trashBtn:hover{color:var(--dsw-alias-state-error-primary)}.pc_consentRemember{display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}.pc_modalBackdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center}.pc_modalCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:16px 18px;width:min(760px,calc(100vw - 64px));max-height:calc(100vh - 96px);overflow-y:auto;overscroll-behavior:contain;flex-direction:column;gap:10px;display:flex;box-shadow:0 12px 32px rgba(0,0,0,.25)}.pc_modalCard::-webkit-scrollbar{width:8px}.pc_modalCard::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:4px}.pc_srcUrl{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}.pc_accWrap{display:flex;flex-direction:column;gap:8px}.pc_acc{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}.pc_accHead{width:100%;text-align:left;background:transparent;border:0;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;font-weight:600;line-height:20px;padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:6px}.pc_accHead:hover{background:var(--dsw-alias-bg-layer-3)}.pc_accHead[data-open=true]{border-bottom:1px solid var(--dsw-alias-border-l2)}.pc_accBody{padding:10px 12px;display:flex;flex-direction:column;gap:8px}.pc_ghpill{background:0 0;cursor:pointer}.pc_ghwrap{position:relative;display:inline-flex}.pc_sourceMenu{position:absolute;right:0;top:calc(100% + 6px);z-index:90;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:4px;flex-direction:column;display:flex;min-width:120px;box-shadow:0 8px 24px rgba(0,0,0,.18)}.pc_sourceOpt{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px;text-align:left;font-size:12px;line-height:16px}.pc_sourceOpt[data-active=true]{color:var(--dsw-alias-state-business-primary);font-weight:600}.pc_sourceOpt:hover{background:var(--dsw-alias-bg-layer-3)}.pc_aiEmpower{position:fixed;right:20px;top:48px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiEmpower:hover{opacity:.9}.pc_aiEmpower[data-busy=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_aiEmpower[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_compCard{position:fixed;left:8px;top:20px;width:320px;max-height:70vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:45;flex-direction:column;gap:8px;display:flex}.pc_compCard .pc_rowTop{flex-wrap:wrap}.pc_compCard .pc_toggle{padding:3px 8px;font-size:11px}.pc_compCard[data-hidden=true]{display:none}.pc_compDrop{background:rgba(24,28,34,.92);backdrop-filter:blur(6px);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.35);flex-direction:column;display:flex;gap:2px}.pc_compDropRow{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 8px;text-align:left;font-size:12px;line-height:16px;flex-direction:column;display:flex;gap:2px}.pc_compDropRow:hover{background:var(--dsw-alias-bg-layer-2)}.pc_compDropUrl{color:var(--dsw-alias-label-tertiary);font-size:11px;word-break:break-all}.pc_compToggle{position:fixed;left:336px;top:20px;z-index:46;opacity:.55;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:rgba(22,27,34,.72);font:inherit;cursor:pointer;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1;backdrop-filter:blur(4px);transition:opacity .15s ease}.pc_compToggle:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain{position:fixed;right:70px;top:max(88px,21vh);z-index:71;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:border-color .15s ease}.pc_toolbarMain:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain[data-active=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolbarGroup{position:fixed;right:70px;top:calc(max(88px,21vh) + 36px);z-index:70;flex-direction:column;gap:8px;display:flex;transform-origin:top right;animation:pcDrawerIn .3s cubic-bezier(.34,1.56,.64,1)}@media (max-width:1160px){.pc_toolbarMain{top:max(150px,30vh)}.pc_toolbarGroup{top:calc(max(150px,30vh) + 36px)}.pc_aiToggle{top:104px}.pc_aiEmpower{top:132px}.pc_sourcesFloat{top:188px}.pc_modeFloat{top:288px}}@keyframes pcDrawerIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:translateX(0) scale(1)}}.pc_toolItem{opacity:.85;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_toolItem:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_switch{width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;flex:none;transition:background .15s ease,border-color .15s ease}.pc_switch:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_switch[data-on=true]{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.pc_switchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;transition:left .15s ease}.pc_switch[data-on=true] .pc_switchKnob{left:18px}.pc_aiSpinner{width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}.pc_aiSpinBtn{position:fixed;right:152px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px}.pc_aiSpinBtn:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_tag[data-bad=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_progress{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:0;font-variant-numeric:tabular-nums}.pc_consentCard{border:2px solid var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px 14px;flex-direction:column;gap:8px;display:flex;box-shadow:0 0 0 3px rgba(255,176,32,.10)}.pc_consentCountdown{margin-left:auto;flex:none;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--dsw-alias-state-warning-primary)}.pc_consentApprove{border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:0 0;font:inherit;font-weight:600;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_consentApprove:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_starFoot{margin-top:auto;position:sticky;bottom:0;justify-content:flex-end;display:flex;padding:6px 0 2px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 16px 0 var(--dsw-alias-bg-layer-1)}.pc_starLink{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:16px;text-decoration:none;display:inline-flex;flex:none;border-radius:4px}.pc_starLink svg{flex:none;display:block}.pc_starLink:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline;text-underline-offset:3px}.pc_starLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}";
9
+ const css = ".pc_section{width:100%;max-width:760px;color:var(--dsw-alias-label-primary);flex-direction:column;gap:14px;display:flex}.pc_section h3{margin:0;font-size:13px;font-weight:600;line-height:20px}.pc_message{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:20px;margin:0}.pc_messageRow{display:flex;align-items:center;gap:10px}.pc_spinner{width:14px;height:14px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}@keyframes pcspin{to{transform:rotate(360deg)}}.pc_message[data-error=true]{color:var(--dsw-alias-state-error-primary)}.pc_message[data-warn=true]{color:var(--dsw-alias-state-warning-primary)}.pc_list{grid-template-columns:repeat(2,minmax(0,1fr));gap:10px;margin:0;padding:0;list-style:none;display:grid}.pc_row{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_rowTop{align-items:center;gap:8px;display:flex}.pc_name{font-size:13px;font-weight:600;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pc_tag{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 8px;font-size:11px;line-height:16px;flex:none}.pc_tag[data-enabled=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-user=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_tag[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_status[data-pending=true]{color:var(--dsw-alias-state-warning-primary)}.pc_toggle[data-pending=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-skill=true]{border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_tag[data-system=true]{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-disabled=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tag[data-suite=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_tag[data-error=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_tabGroup{display:inline-flex;gap:4px;align-items:center}.pc_meta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;gap:6px;display:flex;align-items:center}.pc_phase[data-phase=failed]{color:var(--dsw-alias-state-error-primary)}.pc_phase[data-phase=active]{color:var(--dsw-alias-state-success-primary)}.pc_toggle{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_toggle:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_toggle:disabled{opacity:.5;cursor:default}.pc_search{gap:8px;display:flex}.pc_search input{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);height:34px;flex:1;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 12px;font-size:13px}.pc_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary)}.pc_search button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:0 14px;font-size:13px}.pc_search button:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_market{flex-direction:column;gap:10px;margin:0;padding:0;list-style:none;display:flex}.pc_item{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;min-width:0;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_itemTop{align-items:center;gap:8px;display:flex}.pc_itemTop a{color:var(--dsw-alias-state-business-primary);font-size:13px;font-weight:600;text-decoration:none;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pc_stars{color:var(--dsw-alias-label-tertiary);font-size:11px;flex:none}.pc_desc{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_item button{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:3px 10px;align-self:flex-start;font-size:12px}.pc_item button:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_item button:disabled{opacity:.5;cursor:default}.pc_detail{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:10px 12px;flex-direction:column;gap:6px;display:flex}.pc_status{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px;margin:0}.pc_note{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;margin:0}.pc_note[data-truncated=true]{color:var(--dsw-alias-state-warning-primary)}.pc_marketHead{display:flex;align-items:center;justify-content:space-between;gap:10px}.pc_descWrap{flex-direction:column;display:flex}.pc_installedHead{display:flex;align-items:center;gap:10px}.pc_iconBtn{width:26px;height:26px;padding:0;display:flex;align-items:center;justify-content:center;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:0 0;color:var(--dsw-alias-label-primary);cursor:pointer;flex:none}.pc_iconBtn:hover:not(:disabled){border-color:var(--dsw-alias-state-business-primary)}.pc_iconBtn:disabled{opacity:.5;cursor:default}.pc_iconBtn[data-state=done]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_iconBtn[data-state=active]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-2)}.pc_badgeRow{display:flex;align-items:center;gap:4px;min-width:0;overflow:hidden;flex-wrap:nowrap;flex:0 1 auto;max-width:60%;height:20px;white-space:nowrap}.pc_badgeAll{display:flex;flex-wrap:wrap;gap:4px;margin-top:2px}.pc_tag[data-user-badge=true]{border-style:dashed;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-2)}.pc_tag[data-more=true]{border-style:dotted;cursor:pointer;font:inherit;background:0 0}.pc_userNote{border-left:2px solid var(--dsw-alias-state-business-primary);padding-left:8px;display:flex;flex-direction:column;gap:4px;margin-top:2px}.pc_noteInput{width:100%;font:inherit;padding:4px 6px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:var(--dsw-alias-bg-layer-3);color:var(--dsw-alias-label-primary);resize:vertical}.pc_annotRow{display:flex;align-items:center;gap:6px;flex-wrap:wrap}.pc_backTopWrap{position:fixed;top:56px;right:20px;z-index:100;display:flex;flex-direction:column;gap:8px}.pc_backTop{width:34px;height:34px;border-radius:50%;display:inline-flex;align-items:center;justify-content:center;background:rgba(22,27,34,.72);border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);cursor:pointer;backdrop-filter:blur(4px);opacity:.7}.pc_backTop:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_descTopbar{display:flex;justify-content:flex-end;margin:2px 0}.pc_ghpill{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-tertiary);border-radius:999px;padding:1px 10px;font-size:11px;line-height:18px}.pc_ghpill[data-on=true]{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.pc_extraBtn{opacity:.75;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:4px 10px;font-size:12px;line-height:16px;flex:none;transition:opacity .15s ease,border-color 1.5s ease,color 1.5s ease}.pc_extraBtn:hover{opacity:1}.pc_extraBtn[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_extraBtn[data-flash=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_view{border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:0 0;font:inherit;cursor:pointer;border-radius:6px;padding:2px 10px;font-size:12px;line-height:18px;flex:none}.pc_floatPanel{position:fixed;left:20px;top:50%;transform:translateY(-50%);width:340px;max-height:75vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:50;flex-direction:column;gap:8px;display:flex}.pc_refresh{position:sticky;bottom:14px;align-self:flex-end;margin-top:8px;z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_refresh:hover{opacity:.9}.pc_date{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px;flex:none}.pc_searchInner{position:relative;flex:1;display:flex;align-items:center;min-width:0}.pc_searchInner input{padding-right:32px;width:100%}.pc_starBtn{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:13px;line-height:1;padding:4px 8px;border-radius:6px;opacity:.75;transition:opacity .15s ease;flex:none}.pc_starBtn:hover{opacity:1}.pc_starBtn[data-active=true]{opacity:1;color:var(--dsw-alias-state-business-primary)}.pc_restartBtn{position:fixed;right:20px;top:50%;transform:translateY(-50%);z-index:60;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:transparent;font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_restartBtn:hover{opacity:.9}.pc_relaunchFloat{position:fixed;left:20px;top:50%;transform:translateY(-50%);z-index:70;opacity:.85;border:1px solid var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_relaunchFloat:hover{opacity:1}.pc_aiToggle{position:fixed;right:20px;top:20px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiToggle:hover{opacity:1}.pc_aiToggle[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_sourcesFloat{position:fixed;right:20px;top:104px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_sourcesFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat{position:fixed;right:20px;top:204px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_modeFloat:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_modeFloat[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_trashBtn{border:0;background:0 0;color:var(--dsw-alias-label-tertiary);cursor:pointer;font-size:12px;line-height:16px;padding:2px 6px;flex:none;display:flex;align-items:center;border-radius:6px;transition:color .15s ease}.pc_trashBtn:hover{color:var(--dsw-alias-state-error-primary)}.pc_consentRemember{display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;color:var(--dsw-alias-label-secondary);cursor:pointer}.pc_modalBackdrop{position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:1000;display:flex;align-items:center;justify-content:center}.pc_modalCard{border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:12px;padding:16px 18px;width:min(760px,calc(100vw - 64px));max-height:calc(100vh - 96px);overflow-y:auto;overscroll-behavior:contain;flex-direction:column;gap:10px;display:flex;box-shadow:0 12px 32px rgba(0,0,0,.25)}.pc_modalCard::-webkit-scrollbar{width:8px}.pc_modalCard::-webkit-scrollbar-thumb{background:var(--dsw-alias-border-l2);border-radius:4px}.pc_srcUrl{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;color:var(--dsw-alias-label-tertiary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;max-width:100%}.pc_accWrap{display:flex;flex-direction:column;gap:8px}.pc_acc{border:1px solid var(--dsw-alias-border-l2);border-radius:10px;overflow:hidden;background:var(--dsw-alias-bg-layer-2)}.pc_accHead{width:100%;text-align:left;background:transparent;border:0;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;font-weight:600;line-height:20px;padding:8px 12px;cursor:pointer;display:flex;align-items:center;gap:6px}.pc_accHead:hover{background:var(--dsw-alias-bg-layer-3)}.pc_accHead[data-open=true]{border-bottom:1px solid var(--dsw-alias-border-l2)}.pc_accBody{padding:10px 12px;display:flex;flex-direction:column;gap:8px}.pc_ghpill{background:0 0;cursor:pointer}.pc_ghwrap{position:relative;display:inline-flex}.pc_sourceMenu{position:absolute;right:0;top:calc(100% + 6px);z-index:90;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1);border-radius:8px;padding:4px;flex-direction:column;display:flex;min-width:120px;box-shadow:0 8px 24px rgba(0,0,0,.18)}.pc_sourceOpt{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 10px;text-align:left;font-size:12px;line-height:16px}.pc_sourceOpt[data-active=true]{color:var(--dsw-alias-state-business-primary);font-weight:600}.pc_sourceOpt:hover{background:var(--dsw-alias-bg-layer-3)}.pc_aiEmpower{position:fixed;right:20px;top:48px;z-index:70;opacity:1;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_aiEmpower:hover{opacity:.9}.pc_aiEmpower[data-busy=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_aiEmpower[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_compCard{position:fixed;left:8px;top:20px;width:320px;max-height:70vh;overflow-y:auto;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-3);border-radius:10px;padding:12px;box-shadow:0 8px 24px rgba(0,0,0,.18);z-index:45;flex-direction:column;gap:8px;display:flex}.pc_compCard .pc_rowTop{flex-wrap:wrap}.pc_compCard .pc_toggle{padding:3px 8px;font-size:11px}.pc_compCard[data-hidden=true]{display:none}.pc_compDrop{background:rgba(24,28,34,.92);backdrop-filter:blur(6px);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:4px;box-shadow:0 8px 24px rgba(0,0,0,.35);flex-direction:column;display:flex;gap:2px}.pc_compDropRow{border:0;background:0 0;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;border-radius:6px;padding:6px 8px;text-align:left;font-size:12px;line-height:16px;flex-direction:column;display:flex;gap:2px}.pc_compDropRow:hover{background:var(--dsw-alias-bg-layer-2)}.pc_compDropUrl{color:var(--dsw-alias-label-tertiary);font-size:11px;word-break:break-all}.pc_compToggle{position:fixed;left:336px;top:20px;z-index:46;opacity:.55;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:rgba(22,27,34,.72);font:inherit;cursor:pointer;width:20px;height:20px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;line-height:1;backdrop-filter:blur(4px);transition:opacity .15s ease}.pc_compToggle:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain{position:fixed;right:70px;top:max(88px,21vh);z-index:71;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-2);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:border-color .15s ease}.pc_toolbarMain:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_toolbarMain[data-active=true]{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolbarGroup{position:fixed;right:70px;top:calc(max(88px,21vh) + 36px);z-index:70;flex-direction:column;gap:8px;display:flex;transform-origin:top right;animation:pcDrawerIn .3s cubic-bezier(.34,1.56,.64,1)}@media (max-width:1160px){.pc_toolbarMain{top:max(150px,30vh)}.pc_toolbarGroup{top:calc(max(150px,30vh) + 36px)}.pc_aiToggle{top:104px}.pc_aiEmpower{top:132px}.pc_sourcesFloat{top:188px}.pc_modeFloat{top:288px}}@keyframes pcDrawerIn{from{opacity:0;transform:translateX(28px) scale(.94)}to{opacity:1;transform:translateX(0) scale(1)}}.pc_toolItem{opacity:.85;border:1px solid var(--dsw-alias-border-l2);color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-3);font:inherit;cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px;transition:opacity .15s ease}.pc_toolItem:hover{opacity:1;border-color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-active=true]{opacity:1;border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_toolItem[data-mode=true]{opacity:1;border-color:var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary)}.pc_switch{width:34px;height:18px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);position:relative;cursor:pointer;flex:none;transition:background .15s ease,border-color .15s ease}.pc_switch:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_switch[data-on=true]{background:var(--dsw-alias-state-success-primary);border-color:var(--dsw-alias-state-success-primary)}.pc_switchKnob{position:absolute;top:2px;left:2px;width:12px;height:12px;border-radius:50%;background:#fff;transition:left .15s ease}.pc_switch[data-on=true] .pc_switchKnob{left:18px}.pc_aiSpinner{width:12px;height:12px;border:2px solid var(--dsw-alias-border-l2);border-top-color:var(--dsw-alias-state-business-primary);border-radius:50%;animation:pcspin 1s linear infinite;flex:none}.pc_aiSpinBtn{position:fixed;right:152px;top:20px;z-index:71;border:1px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-2);cursor:pointer;border-radius:8px;padding:6px 10px;display:flex;align-items:center;gap:6px;font-size:12px;line-height:16px}.pc_aiSpinBtn:hover{border-color:var(--dsw-alias-state-business-primary)}.pc_tag[data-bad=true]{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.pc_progress{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:18px;margin:0;font-variant-numeric:tabular-nums}.pc_consentCard{border:2px solid var(--dsw-alias-state-warning-primary);background:var(--dsw-alias-bg-layer-2);border-radius:10px;padding:12px 14px;flex-direction:column;gap:8px;display:flex;box-shadow:0 0 0 3px rgba(255,176,32,.10)}.pc_consentCountdown{margin-left:auto;flex:none;font-size:14px;font-weight:600;font-variant-numeric:tabular-nums;color:var(--dsw-alias-state-warning-primary)}.pc_consentApprove{border:1px solid var(--dsw-alias-state-warning-primary);color:var(--dsw-alias-state-warning-primary);background:0 0;font:inherit;font-weight:600;cursor:pointer;border-radius:6px;padding:4px 12px;align-self:flex-start}.pc_consentApprove:hover{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.pc_starFoot{margin-top:auto;position:sticky;bottom:0;justify-content:flex-end;display:flex;padding:6px 0 2px;background:var(--dsw-alias-bg-layer-1);box-shadow:0 16px 0 var(--dsw-alias-bg-layer-1)}.pc_starLink{color:var(--dsw-alias-label-secondary);align-items:center;gap:6px;font-size:12px;line-height:16px;text-decoration:none;display:inline-flex;flex:none;border-radius:4px}.pc_starLink svg{flex:none;display:block}.pc_starLink:hover{color:var(--dsw-alias-state-business-primary);text-decoration:underline;text-underline-offset:3px}.pc_starLink:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}.pc_groupNew{animation:pcGroupIn 260ms cubic-bezier(.2,.9,.3,1.25) both}@keyframes pcGroupIn{0%{opacity:0;transform:translateY(-10px) scale(.94)}55%{opacity:1;transform:translateY(4px) scale(1.03)}76%{transform:translateY(-2px) scale(.995)}100%{opacity:1;transform:none}}@media (prefers-reduced-motion:reduce){.pc_groupNew{animation:none}}";
10
10
  const tagId = "@noob-stupid/dsh-plugin-console/PluginConsoleTab.css";
11
11
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
12
12
  const tag = document.createElement("style");
@@ -87,6 +87,13 @@ window.__ModuleLoader__.load({
87
87
  detail: "pc_detail",
88
88
  status: "pc_status",
89
89
  note: "pc_note",
90
+ iconBtn: "pc_iconBtn",
91
+ badgeRow: "pc_badgeRow",
92
+ badgeAll: "pc_badgeAll",
93
+ userNote: "pc_userNote",
94
+ noteInput: "pc_noteInput",
95
+ annotRow: "pc_annotRow",
96
+ groupNew: "pc_groupNew",
90
97
  };
91
98
  //#endregion
92
99
  //#region locales
@@ -268,7 +275,87 @@ window.__ModuleLoader__.load({
268
275
  familyCheckUpdate: "批量检测更新",
269
276
  familyUpdateBtn: "更新",
270
277
  familyCheckUpdateTip: "检测到新版本后此处出现「更新」按钮,点击开始整包更新(含子包版本对齐与适配校验)",
278
+ // issue #22 ①:已安装列表的「一键检测全部更新」(只有图标 ↑↑↑ 的圆按钮 + 结果/筛选态)
279
+ checkAllUpdates: "检测全部更新",
280
+ checkAllUpdatesHint: "一键检测全部已装插件是否有新版(本次要查 {n} 个)",
281
+ checkingAllUpdates: "正在检测全部更新({n} 个,并发受限,可能要几秒)",
282
+ checkAllUpdatesDone: "{n} 个可更新",
283
+ checkAllUpdateNone: "没有可更新的插件",
284
+ checkAllUpdateUnknownNote: "其中 {n} 个未检测到(网络或 registry 没答上来,可单独点卡片上的「检测更新」重试)",
285
+ checkAllUpdateFailed: "批量检测失败",
286
+ checkAllUpdateEmptyList: "已安装列表里没有可检测的插件",
287
+ batchUpdateOnly: "仅看可更新",
288
+ batchUpdateOnlyTip: "只显示本次检测到有更新的插件(再点一次取消)",
289
+ batchUpdateFilterEmpty: "本次检测没有可更新的插件(取消「仅看可更新」可看全部)",
290
+ batchUpdateAvailable: "可更新 → v{v}",
291
+ batchUpdateAvailableTip: "本次批量检测到新版本;点「更新」按这个版本更新(与单卡「检测更新」同一判据)",
292
+ batchUpToDate: "已最新",
293
+ batchUpdateUnknown: "未检测到",
294
+ batchUpdateUnknownTip: "registry 与 GitHub 都没答上来 ⇒ 不知道有没有新版(不是\"已是最新\");可单独点「检测更新」重试",
295
+ batchUpdateResultTip: "本次检测的结论:{n} 个可更新、{m} 个已最新、{k} 个未检测到({at})",
271
296
  adaptUnlockBtn: "已适配,立即解锁",
297
+ // issue #22 ②③:用户注解(备注 / 用户徽章 / 用户自建分组)—— 都是**用户自己的数据**
298
+ annotationsBtn: "备注插件",
299
+ annotationsPanelTitle: "备注 / 用户徽章 / 用户分组:这些都是你自己的数据,存在 <DSH_HOME>/plugin-console/plugin-annotations.json,不写进插件、不改 profile;文件坏了也不影响上面的插件列表",
300
+ annotationsFailed: "注解操作失败",
301
+ annotationsCorrupt: "注解文件读不出来({err})—— 插件列表不受影响;修好或删掉该文件后再试写",
302
+ annotationsRepaired: "注解文件被手工改过,已按规则忽略 {n} 处不合规内容",
303
+ annotationsEmpty: "还没有任何备注 / 徽章 / 分组:在下面任一插件行里写第一条",
304
+ annotationsStats: "{p} 个插件有注解 · {b} 个用户徽章 · {g} 个分组",
305
+ annotationsListShow: "插件列表({n})· 点此展开",
306
+ annotationsListHide: "插件列表({n})· 点此收起",
307
+ annotationsSearchPlaceholder: "搜索:包名 / rowId / 备注内容(边打边筛)",
308
+ annotationsNoMatch: "没有匹配的插件",
309
+ noteLabel: "备注",
310
+ noteEdit: "改备注",
311
+ noteEmpty: "+ 加备注",
312
+ notePlaceholder: "这是干什么的、为什么装它",
313
+ noteSaveHint: "回车保存 · Shift+回车换行 · Esc 取消(存空 = 清空备注)",
314
+ noteSaved: "备注已保存",
315
+ noteCleared: "备注已清空",
316
+ badgeLabel: "用户徽章",
317
+ badgeAddPlaceholder: "徽章文字(回车添加)",
318
+ badgeAdded: "已加徽章「{b}」",
319
+ badgeRemove: "移除这个徽章",
320
+ badgeRemoved: "已移除徽章「{b}」",
321
+ badgeUndo: "撤销",
322
+ badgeUndoDone: "已恢复徽章",
323
+ badgeMax: "用户徽章最多 {n} 个",
324
+ badgesAll: "全部徽章({n}):系统徽章 + 用户徽章都在这",
325
+ groupLabel: "分组",
326
+ groupNone: "(不分组)",
327
+ groupCreate: "新建分组",
328
+ groupCreatePlaceholder: "分组名,如:记忆类(回车创建)",
329
+ groupCreated: "已创建分组「{name}」",
330
+ groupNewInlineHint: "在详情页直接建一个分组,并把当前插件放进去",
331
+ groupNewInlinePlaceholder: "分组名(回车创建并拉入)",
332
+ groupNewInline: "新建分组",
333
+ groupRename: "重命名",
334
+ groupRenamed: "分组已重命名",
335
+ groupRemove: "删除这个分组(只解除归属,不删插件)",
336
+ groupDeleteConfirm: "确认删除分组「{name}」?只解除归属,插件本身不会删",
337
+ groupDeleted: "已删除分组「{name}」(解除 {n} 个归属,插件本身没删)",
338
+ groupRestore: "撤销删除",
339
+ groupRestored: "分组「{name}」已恢复",
340
+ groupAssigned: "已加入分组「{name}」",
341
+ groupUnassigned: "已从分组移除",
342
+ groupMembers: "{n} 个插件",
343
+ groupNoMembers: "这个分组里还没有插件:从右边下拉里选一个加进来",
344
+ groupMemberPick: "+ 选一个插件加进这个分组",
345
+ groupUpdateAll: "一键更新本组",
346
+ groupUpdateAllTip: "按组内每个成员各自的目标版本逐个更新(串行,不并发;不适配的仍会被门控挡住)",
347
+ groupUpdateAllProgress: "正在更新第 {i}/{n} 个:",
348
+ groupUpdateAllDone: "本组更新完成:{n} 个成员,{hit} 个有新版并已发起更新",
349
+ groupLoadMore: "加载更多(还有 {n} 个)",
350
+ groupOpenHint: "点分组名可以在这里展开,搜索并添加插件",
351
+ groupAddSearchPlaceholder: "搜索插件名,点「加入」放进这个分组",
352
+ groupAddOne: "加入",
353
+ groupAddNoMatch: "没有匹配的插件(或都已在分组里)",
354
+ groupInFamilyNote: "成员都在官方全家桶区块里(同一插件不会重复显示)",
355
+ groupMemberNone: "插件都已经在这个分组里了",
356
+ groupMemberRemove: "从这个分组移出(只解除归属,不删插件)",
357
+ groupEmptyHint: "还没有分组:先在上面建一个(如「记忆类」),再到插件行里选它",
358
+ userGroupTag: "我的分组",
272
359
  adaptUnlockHint: "已安装版本经源码扫描确认适配当前框架:立即移除禁用块并解锁启用",
273
360
  adaptUnlockDone: "已适配解锁:刷新后该行可正常启用",
274
361
  riskyEnableTitle: "强行启用不适配插件?",
@@ -441,7 +528,7 @@ window.__ModuleLoader__.load({
441
528
  cfDone: "已改名 {renamed} / 重建链接 {linked} / 删备份 {removed} 条(释放约 {mb} MB)",
442
529
  cfFailed: "{n} 项失败(详见 fw/console 日志)",
443
530
  compatNoticeDismiss: "我知道了(此版本不再提示)",
444
- searchInstalledPlaceholder: "搜索已安装插件(名称或 id)",
531
+ searchInstalledPlaceholder: "搜索已安装插件(名称 / id / 备注)",
445
532
  extraFilter: "已装",
446
533
  extraFilterAll: "全部",
447
534
  extraTag: "第三方",
@@ -927,7 +1014,83 @@ window.__ModuleLoader__.load({
927
1014
  familyCheckUpdate: "Check all",
928
1015
  familyUpdateBtn: "Update",
929
1016
  familyCheckUpdateTip: "When a newer version is detected this Update button appears; click to start the full-family update (subpackage alignment + adapt check)",
1017
+ // issue #22 ①: bulk update check for the installed list (icon-only ↑↑↑ button + result/filter states)
1018
+ checkAllUpdates: "Check all updates",
1019
+ checkAllUpdatesHint: "Check every installed plugin for a newer version ({n} to check)",
1020
+ checkingAllUpdates: "Checking all updates ({n} plugins, concurrency capped - may take a few seconds)",
1021
+ checkAllUpdatesDone: "{n} updatable",
1022
+ checkAllUpdateNone: "No updatable plugin",
1023
+ checkAllUpdateUnknownNote: "{n} could not be checked (network or registry did not answer; use the per-card \"Check update\" to retry)",
1024
+ checkAllUpdateFailed: "Bulk update check failed",
1025
+ checkAllUpdateEmptyList: "No checkable plugin in the installed list",
1026
+ batchUpdateOnly: "Updatable only",
1027
+ batchUpdateOnlyTip: "Show only plugins with an update detected in this check (click again to clear)",
1028
+ batchUpdateFilterEmpty: "No updatable plugin in this check (clear \"Updatable only\" to see all)",
1029
+ batchUpdateAvailable: "Update → v{v}",
1030
+ batchUpdateAvailableTip: "A newer version was detected by the bulk check; Update installs exactly this version (same criterion as the per-card Check update)",
1031
+ batchUpToDate: "Up to date",
1032
+ batchUpdateUnknown: "Not detected",
1033
+ batchUpdateUnknownTip: "Neither the registry nor GitHub answered, so \"no update\" is unknown - not \"up to date\". Retry per card with Check update",
1034
+ batchUpdateResultTip: "This check: {n} updatable, {m} up to date, {k} not detected ({at})",
930
1035
  adaptUnlockBtn: "Adapted - unlock now",
1036
+ // issue #22 ②③: user annotations (notes / user badges / user groups) - the user's own data
1037
+ annotationsBtn: "Annotate",
1038
+ annotationsPanelTitle: "Notes / user badges / user groups: your own data, stored in <DSH_HOME>/plugin-console/plugin-annotations.json. Nothing is written into plugins or the profile, and a broken file never affects the plugin list above.",
1039
+ annotationsFailed: "Annotation action failed",
1040
+ annotationsCorrupt: "The annotation file cannot be read ({err}) - the plugin list is unaffected; fix or delete that file, then retry",
1041
+ annotationsRepaired: "The annotation file was edited by hand; {n} invalid entries were ignored",
1042
+ annotationsEmpty: "No notes / badges / groups yet: write the first one in any plugin row below",
1043
+ annotationsStats: "{p} annotated plugins · {b} user badges · {g} groups",
1044
+ annotationsListShow: "Plugin list ({n}) · click to expand",
1045
+ annotationsListHide: "Plugin list ({n}) · click to collapse",
1046
+ annotationsSearchPlaceholder: "Search: package name / rowId / note text (filters as you type)",
1047
+ annotationsNoMatch: "No matching plugins",
1048
+ noteLabel: "Note",
1049
+ noteEdit: "Edit note",
1050
+ noteEmpty: "+ Add note",
1051
+ notePlaceholder: "What is this plugin, and why did you install it",
1052
+ noteSaveHint: "Enter saves · Shift+Enter newline · Esc cancels (empty = clear the note)",
1053
+ noteSaved: "Note saved",
1054
+ noteCleared: "Note cleared",
1055
+ badgeLabel: "User badge",
1056
+ badgeAddPlaceholder: "Badge text (Enter to add)",
1057
+ badgeAdded: "Badge \"{b}\" added",
1058
+ badgeRemove: "Remove this badge",
1059
+ badgeRemoved: "Badge \"{b}\" removed",
1060
+ badgeUndo: "Undo",
1061
+ badgeUndoDone: "Badges restored",
1062
+ badgeMax: "At most {n} user badges",
1063
+ badgesAll: "All badges ({n}): system badges and user badges",
1064
+ groupLabel: "Group",
1065
+ groupNone: "(no group)",
1066
+ groupCreate: "New group",
1067
+ groupCreatePlaceholder: "Group name, e.g. Memory (Enter to create)",
1068
+ groupCreated: "Group \"{name}\" created",
1069
+ groupNewInlineHint: "Create a group here and put this plugin in it",
1070
+ groupNewInlinePlaceholder: "Group name (Enter creates and adds)",
1071
+ groupNewInline: "New group",
1072
+ groupRename: "Rename",
1073
+ groupRenamed: "Group renamed",
1074
+ groupRemove: "Delete this group (membership only, plugins are kept)",
1075
+ groupDeleteConfirm: "Delete group \"{name}\"? Only the membership is removed - plugins stay",
1076
+ groupDeleted: "Group \"{name}\" deleted ({n} memberships removed, plugins kept)",
1077
+ groupRestore: "Undo delete",
1078
+ groupRestored: "Group \"{name}\" restored",
1079
+ groupAssigned: "Added to group \"{name}\"",
1080
+ groupUnassigned: "Removed from the group",
1081
+ groupMembers: "{n} plugins",
1082
+ groupNoMembers: "No plugins in this group yet: pick one from the dropdown on the right",
1083
+ groupMemberPick: "+ Pick a plugin to add to this group",
1084
+ groupUpdateAll: "Update this group",
1085
+ groupUpdateAllTip: "Updates each member sequentially, one at a time, at its own target version; the compatibility gate still applies",
1086
+ groupUpdateAllProgress: "Updating {i}/{n}:",
1087
+ groupUpdateAllDone: "Group update finished: {n} members, {hit} had a newer version and were started",
1088
+ groupLoadMore: "Load more ({n} left)",
1089
+ groupInFamilyNote: "Members live inside the official family block, not duplicated here",
1090
+ groupMemberNone: "Every plugin is already in this group",
1091
+ groupMemberRemove: "Remove from this group (membership only, the plugin is kept)",
1092
+ groupEmptyHint: "No groups yet: create one above (e.g. Memory), then pick it in a plugin row",
1093
+ userGroupTag: "My group",
931
1094
  adaptUnlockHint: "Installed version passed the source scan for the current framework: unlock immediately",
932
1095
  adaptUnlockDone: "Unlocked: refresh and the row is enabled",
933
1096
  riskyEnableTitle: "Force-enable an incompatible plugin?",
@@ -1088,7 +1251,7 @@ window.__ModuleLoader__.load({
1088
1251
  githubLoginBtn: "Sign in",
1089
1252
  githubLoginOk: "Signed in to GitHub: {login}",
1090
1253
  githubLoginFail: "Sign-in failed",
1091
- searchInstalledPlaceholder: "Search installed plugins (name or id)",
1254
+ searchInstalledPlaceholder: "Search installed plugins (name / id / note)",
1092
1255
  extraFilter: "Installed",
1093
1256
  extraFilterAll: "All",
1094
1257
  extraTag: "Third-party",
@@ -1673,6 +1836,10 @@ window.__ModuleLoader__.load({
1673
1836
  }
1674
1837
  // <<<update-pick:end>>>
1675
1838
  const el = react.createElement;
1839
+ // ★ 0.5.93(issue #22 ② 收口):徽章"放得下几个"的估算函数(BADGE_AREA_FALLBACK_PX / badgeChipWidthOf /
1840
+ // fitBadgeChunks / fitBadgeList)**整段删除**。它们配合 ResizeObserver 撑起了"卡片上收成 +N"那套机制,
1841
+ // 而用户定稿的行为是:卡片上放不下就**不显示**(纯 CSS 截断、无任何提示),全部徽章去详情页看。
1842
+ // 留着这套估算只会有人再把它接回去(抖动正是这么来的)。
1676
1843
  // ★ 0.5.60:被跳过 bundle 的签名(包名+版本)。关掉提示时记下它;清单变了就重新出现。
1677
1844
  // 模块级 —— 上一版把辅助函数放子组件里、主面板误用,导致整块面板空白。
1678
1845
  function skippedSignature(skipped) {
@@ -2390,6 +2557,42 @@ const RESTART_FAILED_NOTE = "重启请求失败";
2390
2557
  }, []);
2391
2558
  // 自动版本比对结果:{ fullName: latest }(已安装且有新版)
2392
2559
  const [updateMap, setUpdateMap] = react.useState({});
2560
+ // ★ issue #22 ①:已安装列表的「一键检测全部更新」(批量)。
2561
+ // 状态机:null(还没查过)→ checking(查中,带本次要查的包数)→ ready(有结论)/ error。
2562
+ // 界面只**展示**结论,不当判据:哪个版本更高由服务端唯一判据(`<<<update-pick>>>` 那块)给出,
2563
+ // 这里把 target 原样交给 startJob —— 与单卡「检测更新」同一个参数形状。
2564
+ const [batchCheck, setBatchCheck] = react.useState(null);
2565
+ // 「仅看可更新」筛选:只在 ready 且真的有可更新项时才渲染成按钮(没有可点的空状态)
2566
+ const [onlyUpdatable, setOnlyUpdatable] = react.useState(false);
2567
+ // ★ issue #22 ②③:用户注解(备注 / 用户徽章 / 用户自建分组)—— 独立文件、独立通道;
2568
+ // 读不到或写失败只影响这块,插件列表照常(服务端边界见 domain/plugin-annotations.js 顶部)。
2569
+ const [annotations, setAnnotations] = react.useState(null);
2570
+ const [annotationsNote, setAnnotationsNote] = react.useState(null);
2571
+ const [annotationBusy, setAnnotationBusy] = react.useState(null);
2572
+ const [annotationsPanelOpen, setAnnotationsPanelOpen] = react.useState(false);
2573
+ // ② 收口(0.5.93,用户定稿):面板一打开列表**收起**(只留一行摘要「插件列表(N)· 点此展开」);
2574
+ // 展开后可按 包名 / rowId / 已有备注内容 **边打边筛**(不需要点按钮)。
2575
+ // 查询词是受控 state —— 过滤过程绝不回写、更不清空用户正在输入的内容。
2576
+ const [annotationsListOpen, setAnnotationsListOpen] = react.useState(false);
2577
+ const [annotationsQuery, setAnnotationsQuery] = react.useState("");
2578
+ const [noteEdit, setNoteEdit] = react.useState(null);
2579
+ const [badgeDraft, setBadgeDraft] = react.useState({});
2580
+ const [badgeUndo, setBadgeUndo] = react.useState({});
2581
+ const [groupDraftName, setGroupDraftName] = react.useState("");
2582
+ // ③ 收口(0.5.93):**只**记住"刚刚建出来的那个分组 id"用来播一次入场动画;
2583
+ // 分组本身的真相永远来自服务端返回的全量 groups(annotationWrite 直接覆盖),
2584
+ // 这里不维护第二份客户端状态。
2585
+ const [groupJustCreated, setGroupJustCreated] = react.useState(null);
2586
+ // #5:详情页里就地新建分组 —— **按卡片 key 区分**({ key, draft } | null)。
2587
+ // 用一个全局值会让所有卡片同时展开输入框(用户实测到的问题)。
2588
+ const [detailGroupDraft, setDetailGroupDraft] = react.useState(null);
2589
+ // 面板里"点开某个分组"(只允许一个展开)+ 该分组内的搜索词
2590
+ const [panelGroupOpen, setPanelGroupOpen] = react.useState(null);
2591
+ const [panelGroupQuery, setPanelGroupQuery] = react.useState("");
2592
+ const [panelGroupShow, setPanelGroupShow] = react.useState(8); // 候选默认 8 个,可加载更多
2593
+ const [groupRenameEdit, setGroupRenameEdit] = react.useState(null);
2594
+ const [groupDeleteArm, setGroupDeleteArm] = react.useState(null);
2595
+ const [groupUndo, setGroupUndo] = react.useState(null);
2393
2596
  const [expandedDescs, setExpandedDescs] = react.useState({});
2394
2597
  const [installedQuery, setInstalledQuery] = react.useState(() => {
2395
2598
  try { return localStorage.getItem("pc-market-installed-q") ?? ""; } catch { return ""; }
@@ -3216,6 +3419,68 @@ const RESTART_FAILED_NOTE = "重启请求失败";
3216
3419
  (error) => setUpdateCheck({ status: "ready", latest: null, source: "npm", error: friendlyGithubError(error).message }),
3217
3420
  );
3218
3421
  };
3422
+ // ★ issue #22 ①:一键检测全部更新 —— **一次**请求查完已装列表(服务端并发上限 4,见 domain/update-batch.js)。
3423
+ // 为什么不是逐个包各发一个请求:那样并发由客户端决定,几十个包同时出去就是 0.5.84
3424
+ // 市场富化把网络打满、安装作业被拖死那次的形状(教训写在 domain/enrich-throttle.js 顶部)。
3425
+ // 为什么没有 x/y 进度:单个请求拿不到逐包进度;编一个假百分比比转圈更糟。要真进度只能分批请求
3426
+ // (每批一次请求、累加 x/y)——已写进 UI 方案等用户定,本版先给转圈 + 待查包数。
3427
+ const checkAllUpdates = () => {
3428
+ if (batchCheck !== null && batchCheck.status === "checking") return;
3429
+ const names = [];
3430
+ const seen = new Set();
3431
+ for (const entry of (state.status === "ready" ? state.data.entries : [])) {
3432
+ const mn = String(entry.moduleName ?? "");
3433
+ if (mn === "" || mn.startsWith("cordis:") || seen.has(mn)) continue;
3434
+ seen.add(mn);
3435
+ names.push(mn);
3436
+ }
3437
+ if (names.length === 0) {
3438
+ // 没有可查的东西也必须**有反馈**(空列表不是"点了没反应")
3439
+ setMessage(t("checkAllUpdateEmptyList"));
3440
+ return;
3441
+ }
3442
+ setBatchCheck({ status: "checking", total: names.length });
3443
+ setMessage(t("checkingAllUpdates").replace("{n}", String(names.length)));
3444
+ // ★ 2026-10-27:**分批**跑(每批 40,逐批回报进度)。整批一次性丢给服务端时,
3445
+ // 界面在几分钟里只有一个转圈,用户以为"卡在限制上"。分批后可看到"已查 40/238"逐步推进。
3446
+ const CHUNK = 40;
3447
+ const merged = { items: [], checked: 0, updatable: 0, unknown: 0 };
3448
+ const runChunk = (offset) => {
3449
+ const slice = names.slice(offset, offset + CHUNK);
3450
+ if (slice.length === 0) return Promise.resolve(merged);
3451
+ setMessage(t("checkingAllUpdates").replace("{n}", String(names.length)) + "(" + offset + "/" + names.length + ")");
3452
+ return call("/plugin-console/check-updates", { packageNames: slice }).then((data) => {
3453
+ if (data !== null && Array.isArray(data.items)) merged.items.push(...data.items);
3454
+ merged.checked += typeof data?.checked === "number" ? data.checked : slice.length;
3455
+ merged.updatable += typeof data?.updatable === "number" ? data.updatable : 0;
3456
+ merged.unknown += typeof data?.unknown === "number" ? data.unknown : 0;
3457
+ setBatchCheck({ status: "checking", total: names.length, done: merged.checked });
3458
+ return runChunk(offset + CHUNK);
3459
+ }, () => runChunk(offset + CHUNK));
3460
+ };
3461
+ runChunk(0).then(
3462
+ (data) => {
3463
+ const items = {};
3464
+ for (const item of (data !== null && Array.isArray(data.items) ? data.items : [])) {
3465
+ if (item !== null && typeof item === "object" && typeof item.packageName === "string") items[item.packageName] = item;
3466
+ }
3467
+ const updatable = typeof data?.updatable === "number" ? data.updatable : 0;
3468
+ const unknown = typeof data?.unknown === "number" ? data.unknown : 0;
3469
+ const checked = typeof data?.checked === "number" ? data.checked : names.length;
3470
+ setBatchCheck({ status: "ready", at: Date.now(), checked, updatable, unknown, items });
3471
+ // 结论必须看得见:没有可更新项时**不动**"仅看可更新"筛选(否则列表突然变空)
3472
+ if (updatable === 0) setOnlyUpdatable(false);
3473
+ setMessage(
3474
+ (updatable > 0 ? t("checkAllUpdatesDone").replace("{n}", String(updatable)) : t("checkAllUpdateNone"))
3475
+ + (unknown > 0 ? ";" + t("checkAllUpdateUnknownNote").replace("{n}", String(unknown)) : ""),
3476
+ );
3477
+ },
3478
+ (error) => {
3479
+ setBatchCheck({ status: "error", error: friendlyGithubError(error).message });
3480
+ setMessage(t("checkAllUpdateFailed") + ":" + friendlyGithubError(error).message);
3481
+ },
3482
+ );
3483
+ };
3219
3484
  const toggle = (entry, enabled, confirmRisky = false) => {
3220
3485
  setBusy(entry.entryId);
3221
3486
  setMessage(null);
@@ -4275,14 +4540,297 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4275
4540
  const label = primary?.name ?? t("repoLandSourceUnknown");
4276
4541
  return t("repoLandTitle").replace("{source}", label);
4277
4542
  };
4543
+ // issue #22 ①:批量检测结论查询(entry → 该项结论)。没查过 / 查中 / 出错一律 null =「这次没有结论」,
4544
+ // 于是卡片既不显示"可更新"也不显示"已最新"——不拿"没查过"冒充"已是最新"。
4545
+ const batchItemOf = (entry) => (batchCheck !== null && batchCheck.status === "ready"
4546
+ ? batchCheck.items[String(entry.moduleName ?? "")] ?? null
4547
+ : null);
4548
+
4549
+ // ★ 0.5.93 改错(issue #22 ② 收口):徽章区**不再做任何宽度测量**。
4550
+ // 上一版用 ResizeObserver 读第一张卡片的徽章行宽度、再按估算宽度决定"显示几个 / 收成 +N"——
4551
+ // 测量值随卡片宽度抖动 ⇒ 徽章时不时多一个少一个(用户原话「徽章有挤压变化」)。
4552
+ // 现在:卡片上把徽章**全渲染**,靠 `.pc_badgeRow` 的 overflow:hidden + white-space:nowrap
4553
+ // 在纯 CSS 层截断(单行、固定高度、放不下的直接看不见,不给"还有 N 个"的提示);
4554
+ // 全部徽章在**详情页**一次看全。于是没有任何"测量 → 重算 → 重渲染"的反馈环。
4555
+ // ── issue #22 ②③:用户注解的读写与编辑块(备注 / 用户徽章 / 用户自建分组)───────────
4556
+ // 数据在**独立文件**里、走独立接口;这块出问题绝不影响插件列表(那是另一条通道)。
4557
+ const annotationKeyOf = (entry) => {
4558
+ const name = String(entry?.moduleName ?? "");
4559
+ return name !== "" ? name : String(entry?.rowId ?? "");
4560
+ };
4561
+ const annotationOf = (entry) => {
4562
+ const key = annotationKeyOf(entry);
4563
+ const record = key === "" ? null : annotations?.plugins?.[key];
4564
+ return record !== null && typeof record === "object" ? record : null;
4565
+ };
4566
+ const annotationsLoad = () => {
4567
+ call("/plugin-console/annotations").then(
4568
+ (data) => {
4569
+ setAnnotations(data !== null && typeof data === "object" ? data : null);
4570
+ // 手工改过文件 → 服务端规范化时丢弃了不合规内容,如实说(不静默)
4571
+ if (data !== null && Array.isArray(data.repaired) && data.repaired.length > 0) {
4572
+ setAnnotationsNote(t("annotationsRepaired").replace("{n}", String(data.repaired.length)));
4573
+ }
4574
+ },
4575
+ (error) => {
4576
+ setAnnotations(null);
4577
+ setAnnotationsNote(t("annotationsCorrupt").replace("{err}", friendlyGithubError(error).message));
4578
+ },
4579
+ );
4580
+ };
4581
+ /** 一次写动作:成功就用响应里的**整份最新状态**刷新(服务端保证回了全量),失败如实提示。 */
4582
+ const annotationWrite = (body, okText) => {
4583
+ const busyKey = String(body?.packageName ?? body?.id ?? body?.action ?? "");
4584
+ setAnnotationBusy(busyKey);
4585
+ return call("/plugin-console/annotations", body).then(
4586
+ (data) => {
4587
+ if (data !== null && data.plugins !== null && typeof data.plugins === "object") {
4588
+ setAnnotations({
4589
+ ok: true, file: data.file ?? null, exists: true, emptyFile: false, version: 1, updatedAt: 0,
4590
+ plugins: data.plugins, groups: Array.isArray(data.groups) ? data.groups : [],
4591
+ stats: data.stats ?? null, repaired: [], error: null,
4592
+ });
4593
+ }
4594
+ setAnnotationsNote(okText === undefined || okText === null ? null : okText);
4595
+ return data;
4596
+ },
4597
+ (error) => { setAnnotationsNote(t("annotationsFailed") + ":" + friendlyGithubError(error).message); return null; },
4598
+ ).finally(() => setAnnotationBusy(null));
4599
+ };
4600
+ const userGroups = Array.isArray(annotations?.groups) ? annotations.groups : [];
4601
+ /** 某个包属于哪个用户分组(单值:取第一个命中的)。渲染请用 groupsOfPackage —— 分组允许重叠。 */
4602
+ const groupOfPackage = (moduleName) => {
4603
+ const name = String(moduleName ?? "");
4604
+ if (name === "") return null;
4605
+ for (const group of userGroups) {
4606
+ if (Array.isArray(group.packages) && group.packages.includes(name)) return group;
4607
+ }
4608
+ return null;
4609
+ };
4610
+ /**
4611
+ * 某个包属于**哪些**用户分组(分组允许重叠 ⇒ 可能多个)。
4612
+ * 与 groupOfPackage 的区别:那个用于"当前选中哪个组"(单值),这个用于渲染(多值)。
4613
+ */
4614
+ const groupsOfPackage = (moduleName) => {
4615
+ const name = String(moduleName ?? "");
4616
+ if (name === "") return [];
4617
+ return userGroups.filter((group) => Array.isArray(group.packages) && group.packages.includes(name));
4618
+ };
4619
+ const createUserGroup = () => {
4620
+ const name = groupDraftName.trim();
4621
+ if (name === "") return;
4622
+ // ③:新建只走**回车/点按钮**这一条明确动作(绝不边打边建);成功后用服务端返回的
4623
+ // group.id 标一下"刚建好"(只为了在主列表的横框上播一次入场动画,数据仍是服务端那份)。
4624
+ setGroupDraftName("");
4625
+ annotationWrite({ action: "group-create", name }, t("groupCreated").replace("{name}", name)).then((data) => {
4626
+ const id = data !== null && data !== undefined && data.group !== null && data.group !== undefined
4627
+ ? String(data.group.id ?? "")
4628
+ : "";
4629
+ if (id === "") return;
4630
+ setGroupJustCreated(id);
4631
+ try {
4632
+ // 动画只有 220ms;1.2s 后把这个标记撤掉,避免以后重渲染时又播一次
4633
+ window.setTimeout(() => setGroupJustCreated((prev) => (prev === id ? null : prev)), 1200);
4634
+ } catch {}
4635
+ });
4636
+ };
4637
+ /**
4638
+ * 打开「备注插件」面板(用户定稿:面板打开时插件列表**收起**,只留一行摘要)。
4639
+ * 每次打开都回到收起 + 空查询 —— 不给"上次展开到一半"的残留状态。
4640
+ */
4641
+ const openAnnotationsPanel = () => {
4642
+ setAnnotationsListOpen(false);
4643
+ setAnnotationsQuery("");
4644
+ setAnnotationsPanelOpen(true);
4645
+ annotationsLoad();
4646
+ };
4647
+ /** 工具栏那个按钮是**开关**:开着再点就关掉("点了没反应"算缺陷)。 */
4648
+ const toggleAnnotationsPanel = () => {
4649
+ if (annotationsPanelOpen) { setAnnotationsPanelOpen(false); return; }
4650
+ openAnnotationsPanel();
4651
+ };
4652
+ // issue #22 ②:注解面板里的插件行 —— 默认收起;展开后按 包名 / rowId / 已有备注内容 即时过滤。
4653
+ // 查询词只读不写 ⇒ 边打边筛的过程中用户正在输入的内容**不会被清掉**。
4654
+ const annotationListEntries = state.status === "ready" ? state.data.entries : [];
4655
+ const annotationsQueryNorm = annotationsQuery.trim().toLowerCase();
4656
+ const annotationListFiltered = annotationsQueryNorm === ""
4657
+ ? annotationListEntries
4658
+ : annotationListEntries.filter((entry) => [entry.moduleName, entry.rowId, annotationOf(entry)?.note]
4659
+ .some((value) => String(value ?? "").toLowerCase().includes(annotationsQueryNorm)));
4660
+ /**
4661
+ * 用户注解编辑块 —— 详情页与「备注插件」面板**共用同一份**(两处交互必须一模一样)。
4662
+ * · 备注:点一下就地编辑,回车保存、Esc 取消;存空 = 清空(清空后**不占位**)。
4663
+ * · 徽章:∪ 追加(系统徽章在卡片上是另一组,**绝不替换、绝不合并**);× 删除后可撤销一次。
4664
+ * · 没有备注也没有徽章时:只显示一个「+ 加备注」入口(用户定稿的那一种,不显示空壳)。
4665
+ */
4666
+ const annotationBlock = (entry) => {
4667
+ const key = annotationKeyOf(entry);
4668
+ const packageName = String(entry?.moduleName ?? key);
4669
+ const record = annotationOf(entry);
4670
+ const note = typeof record?.note === "string" ? record.note : "";
4671
+ const badges = Array.isArray(record?.badges) ? record.badges : [];
4672
+ const group = groupOfPackage(packageName);
4673
+ const editing = noteEdit !== null && noteEdit.key === key;
4674
+ const busy = annotationBusy === key || annotationBusy === packageName;
4675
+ const draft = badgeDraft[key] ?? "";
4676
+ const undo = badgeUndo[key];
4677
+ const writeNote = (text) => annotationWrite({ action: "note", packageName, rowId: entry?.rowId ?? null, note: text }, text.trim() === "" ? t("noteCleared") : t("noteSaved"));
4678
+ const writeBadges = (list, okText) => annotationWrite({ action: "badges", packageName, rowId: entry?.rowId ?? null, badges: list }, okText);
4679
+ const addBadge = () => {
4680
+ const text = draft.trim();
4681
+ if (text === "") return;
4682
+ setBadgeDraft((prev) => ({ ...prev, [key]: "" }));
4683
+ if (badges.includes(text)) return;
4684
+ if (badges.length >= 12) { setAnnotationsNote(t("badgeMax").replace("{n}", "12")); return; }
4685
+ writeBadges([...badges, text], t("badgeAdded").replace("{b}", text));
4686
+ };
4687
+ return el("div", { className: styles.userNote, "data-annot": "true" },
4688
+ editing
4689
+ ? el("div", null,
4690
+ el("textarea", {
4691
+ className: styles.noteInput,
4692
+ rows: 2,
4693
+ value: noteEdit.draft,
4694
+ placeholder: t("notePlaceholder"),
4695
+ "aria-label": t("noteLabel"),
4696
+ onChange: (event) => setNoteEdit({ key, draft: String(event.target.value ?? "") }),
4697
+ onKeyDown: (event) => {
4698
+ if (event.key === "Escape") { event.preventDefault(); setNoteEdit(null); return; }
4699
+ if (event.key === "Enter" && event.shiftKey !== true) {
4700
+ event.preventDefault();
4701
+ const text = noteEdit.draft;
4702
+ setNoteEdit(null);
4703
+ writeNote(text);
4704
+ }
4705
+ },
4706
+ }),
4707
+ el("p", { className: styles.note, style: { margin: 0 } }, t("noteSaveHint")))
4708
+ : el("div", { className: styles.annotRow },
4709
+ note !== "" ? el("p", { className: styles.desc, style: { margin: 0, whiteSpace: "pre-wrap", flex: "1 1 200px" } }, note) : null,
4710
+ el("button", {
4711
+ type: "button",
4712
+ className: styles.toggle,
4713
+ disabled: busy,
4714
+ title: t("notePlaceholder"),
4715
+ onClick: () => setNoteEdit({ key, draft: note }),
4716
+ }, note !== "" ? t("noteEdit") : t("noteEmpty"))),
4717
+ el("div", { className: styles.annotRow },
4718
+ el("span", { className: styles.status }, t("badgeLabel") + ":"),
4719
+ badges.map((badge) => el("span", { key: "ub-" + badge, className: styles.tag, "data-user-badge": "true" },
4720
+ badge,
4721
+ el("button", {
4722
+ type: "button",
4723
+ className: styles.tag,
4724
+ "data-more": "true",
4725
+ title: t("badgeRemove"),
4726
+ "aria-label": t("badgeRemove"),
4727
+ disabled: busy,
4728
+ onClick: () => {
4729
+ setBadgeUndo((prev) => ({ ...prev, [key]: badges }));
4730
+ writeBadges(badges.filter((item) => item !== badge), t("badgeRemoved").replace("{b}", badge));
4731
+ },
4732
+ }, "×"))),
4733
+ el("input", {
4734
+ type: "text",
4735
+ value: draft,
4736
+ placeholder: t("badgeAddPlaceholder"),
4737
+ "aria-label": t("badgeAddPlaceholder"),
4738
+ disabled: busy,
4739
+ style: { width: 130, font: "inherit", padding: "2px 6px" },
4740
+ onChange: (event) => setBadgeDraft((prev) => ({ ...prev, [key]: String(event.target.value ?? "") })),
4741
+ onKeyDown: (event) => { if (event.key === "Enter") { event.preventDefault(); addBadge(); } },
4742
+ }),
4743
+ undo !== undefined
4744
+ ? el("button", {
4745
+ type: "button",
4746
+ className: styles.toggle,
4747
+ disabled: busy,
4748
+ onClick: () => {
4749
+ writeBadges(undo, t("badgeUndoDone"));
4750
+ setBadgeUndo((prev) => { const next = { ...prev }; delete next[key]; return next; });
4751
+ },
4752
+ }, t("badgeUndo"))
4753
+ : null),
4754
+ el("div", { className: styles.annotRow },
4755
+ el("span", { className: styles.status }, t("groupLabel") + ":"),
4756
+ // #5:就在这里新建分组,并把当前插件直接拉进去(回车创建)
4757
+ detailGroupDraft === null || detailGroupDraft.key !== key
4758
+ ? el("button", {
4759
+ type: "button",
4760
+ className: styles.toggle,
4761
+ "data-newgroup": "true",
4762
+ title: t("groupNewInlineHint"),
4763
+ disabled: busy,
4764
+ onClick: () => setDetailGroupDraft({ key, draft: "" }),
4765
+ }, "+ " + t("groupNewInline"))
4766
+ : el("input", {
4767
+ type: "text",
4768
+ autoFocus: true,
4769
+ value: detailGroupDraft.draft,
4770
+ placeholder: t("groupNewInlinePlaceholder"),
4771
+ "aria-label": t("groupNewInline"),
4772
+ style: { font: "inherit", padding: "2px 6px", width: 160 },
4773
+ onChange: (event) => setDetailGroupDraft({ key, draft: String(event.target.value ?? "") }),
4774
+ onKeyDown: (event) => {
4775
+ if (event.key === "Escape") { event.preventDefault(); setDetailGroupDraft(null); return; }
4776
+ if (event.key !== "Enter") return;
4777
+ event.preventDefault();
4778
+ const name = String(detailGroupDraft.draft ?? "").trim();
4779
+ if (name === "") { setDetailGroupDraft(null); return; }
4780
+ setDetailGroupDraft(null);
4781
+ // 建组 → 用返回的全量 groups 找到它 → 把当前插件拉进去
4782
+ annotationWrite({ action: "group-create", name }, t("groupCreated").replace("{name}", name))
4783
+ .then((data) => {
4784
+ if (data === null) return;
4785
+ const created = (Array.isArray(data.groups) ? data.groups : [])
4786
+ .find((item) => String(item.name) === name);
4787
+ if (created !== undefined && created !== null) {
4788
+ annotationWrite({ action: "group-assign", id: created.id, packageName }, t("groupAssigned").replace("{name}", name));
4789
+ }
4790
+ });
4791
+ },
4792
+ }),
4793
+ el("select", {
4794
+ value: group?.id ?? "",
4795
+ "aria-label": t("groupLabel"),
4796
+ disabled: busy,
4797
+ style: { font: "inherit", padding: "2px 6px" },
4798
+ onChange: (event) => {
4799
+ const picked = String(event.target.value ?? "");
4800
+ if (picked === "") {
4801
+ if (group === null) return;
4802
+ annotationWrite({ action: "group-assign", id: group.id, packageName, member: false }, t("groupUnassigned"));
4803
+ return;
4804
+ }
4805
+ const pickedName = String(userGroups.find((item) => item.id === picked)?.name ?? "");
4806
+ annotationWrite({ action: "group-assign", id: picked, packageName }, t("groupAssigned").replace("{name}", pickedName));
4807
+ },
4808
+ },
4809
+ el("option", { value: "" }, t("groupNone")),
4810
+ userGroups.map((item) => el("option", { key: "gs-" + item.id, value: item.id }, String(item.name))))));
4811
+ };
4278
4812
  const extraIds = new Set((state.status === "ready" ? state.data.patch?.inserts ?? [] : []));
4279
4813
  const extraCount = state.status === "ready" ? state.data.entries.filter((entry) => entry.extra === true || extraIds.has(entry.rowId)).length : 0;
4814
+ // issue #22 ②③:打开面板即读一次注解(读失败只提示,插件列表照常)
4815
+ react.useEffect(() => { annotationsLoad(); }, []);
4280
4816
  const pkgRootOf = (m) => { const mm = String(m ?? ""); const x = mm.match(/^(@[^/]+\/[^/]+)(?:\/.*)?$/u); return x ? x[1] : mm; };
4281
4817
  const filteredEntries = state.status === "ready"
4282
4818
  ? state.data.entries.filter((entry) => {
4283
4819
  if (extraOnly && !(entry.extra === true || extraIds.has(entry.rowId))) return false;
4820
+ // issue #22 ①:「仅看可更新」只筛**本次批量检测确实查到新版**的行。
4821
+ // 没查到结论的行(未检测到 / 没查过)**不算**可更新 —— 与"没查到 ≠ 已最新"同一条纪律。
4822
+ if (onlyUpdatable && batchItemOf(entry)?.status !== "update") return false;
4284
4823
  if (installedQueryNorm === "") return true;
4285
- return [entry.moduleName, entry.entryId, entry.rowId]
4824
+ // ★ 用户 2026-10-27:搜索也要命中**备注**(备注是可用来命名的,只按包名搜会漏)
4825
+ const noteText = (() => {
4826
+ const key = annotationKeyOf(entry);
4827
+ if (key === "") return "";
4828
+ const rec = annotations !== null && annotations.plugins !== null && annotations.plugins !== undefined
4829
+ ? annotations.plugins[key] ?? null
4830
+ : null;
4831
+ return typeof rec?.note === "string" ? rec.note : "";
4832
+ })();
4833
+ return [entry.moduleName, entry.entryId, entry.rowId, noteText]
4286
4834
  .some((value) => String(value ?? "").toLowerCase().includes(installedQueryNorm));
4287
4835
  })
4288
4836
  : [];
@@ -4302,10 +4850,33 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4302
4850
  familyCounts.set(root, set);
4303
4851
  }
4304
4852
  const familySize = (root) => (familyCounts.get(root)?.size ?? 0);
4305
- const familyCards = filteredEntries.map((entry) => {
4853
+ const familyCards = filteredEntries.map((entry, cardIndex) => {
4306
4854
  const familyRoot = String(entry.moduleName ?? "").startsWith("@deepseek-ai/") ? null : pkgRootOf(entry.moduleName);
4307
4855
  void familyRoot;
4308
4856
  const open = details !== null && details.entryId === entry.entryId;
4857
+ // issue #22 ①:这张卡本次的批量检测结论(null = 没查过/查中/查失败 ⇒ 不加任何"已最新"标签)
4858
+ const batchItem = batchItemOf(entry);
4859
+ // issue #22 ②:卡片徽章 = **系统徽章(现有顺序,一条不少)** + 用户徽章(追加在最后)。
4860
+ // 系统徽章用现有实心/描边样式(props 原样搬过来),用户徽章带 data-user-badge(虚线+浅底)。
4861
+ const systemBadges = [
4862
+ { key: "enabled", label: t(entry.enabled ? "enabledTag" : "disabledTag"), props: { "data-enabled": entry.enabled ? "true" : "false" } },
4863
+ entry.extra === true ? { key: "extra", label: t("extraTag"), props: { "data-user": "true" } } : null,
4864
+ entry.protected ? { key: "protected", label: t("protectedTag"), props: { "data-user": "true" } } : null,
4865
+ entry.userDisabled ? { key: "userDisabled", label: t("userDisabledTag"), props: { "data-user": "true" } } : null,
4866
+ entry.userForced ? { key: "userForced", label: t("userForcedTag"), props: { "data-user": "true" } } : null,
4867
+ batchItem?.status === "update"
4868
+ ? { key: "batch-update", label: t("batchUpdateAvailable").replace("{v}", String(batchItem.target ?? "?")), props: { "data-pending": "true", title: t("batchUpdateAvailableTip") } }
4869
+ : batchItem?.status === "current"
4870
+ ? { key: "batch-current", label: t("batchUpToDate"), props: { "data-enabled": "true" } }
4871
+ : batchItem?.status === "unknown"
4872
+ ? { key: "batch-unknown", label: t("batchUpdateUnknown"), props: { title: t("batchUpdateUnknownTip") + (batchItem.error ? "(" + String(batchItem.error) + ")" : "") } }
4873
+ : null,
4874
+ entry.disableSource === "bundle" ? { key: "bundle", label: t("disabledByBundleTag"), props: { "data-user": "true", "data-source": "bundle" } } : null,
4875
+ entry.forcedNotLoaded === true ? { key: "ineffective", label: t("forcedNotLoadedTag"), props: { "data-user": "true", "data-source": "ineffective" } } : null,
4876
+ ].filter((badge) => badge !== null);
4877
+ const userBadges = (Array.isArray(annotationOf(entry)?.badges) ? annotationOf(entry).badges : [])
4878
+ .map((label) => ({ key: "user-" + label, label: String(label), props: { "data-user-badge": "true", title: t("badgeLabel") } }));
4879
+ const allBadges = [...systemBadges, ...userBadges];
4309
4880
  let detailPanel = null;
4310
4881
  if (open) {
4311
4882
  if (details.status === "loading") {
@@ -4320,20 +4891,10 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4320
4891
  const compatInfo = state.status === "ready" && state.data.compatPending
4321
4892
  ? (state.data.compatPending.pending ?? []).find((p) => p.rowId === entry.rowId) ?? null
4322
4893
  : null;
4323
- detailPanel = el("div", { className: styles.detail },
4324
- meta !== null ? el("p", { className: styles.desc }, meta.description ?? t("noDetail")) : null,
4325
- entry.pendingCompat === true
4326
- ? el("p", { className: styles.status, "data-pending": entry.adoptable ? "false" : "true" },
4327
- entry.adoptable
4328
- ? (entry.adoptable.basis === "verdict-rescinded"
4329
- // 0.5.34:旧判据的误报已被当前判据撤回(真机 dshmarket 现场)——如实说是"判据更新",
4330
- // 不能说成"插件已适配"(那会让人以为作者发过新版)
4331
- ? t("adoptableRescinded").replace("{version}", String(entry.adoptable.version ?? "?"))
4332
- : t("adoptableDetected").replace("{version}", String(entry.adoptable.version ?? "?")).replace("{prev}", String(compatInfo?.version ?? "?")))
4333
- : t("pendingCompatHint") + (compatInfo && compatInfo.checkNote ? ";已知校验:" + compatInfo.checkNote : ""))
4334
- : null,
4335
- meta !== null && meta.version ? el("div", { className: styles.rowTop },
4336
- el("p", { className: styles.status }, t("versionLabel") + ":" + meta.version),
4894
+ // ★ issue #22 ②(用户定稿):动作按钮排在详情页**最后**(原来挂在"版本"那一行,夹在中间)。
4895
+ // 判据/禁用条件/文案逐字搬过来,一个字都没改,只换了位置。
4896
+ const detailActionRow = meta !== null && meta.version
4897
+ ? el("div", { className: styles.rowTop },
4337
4898
  el("button", {
4338
4899
  type: "button",
4339
4900
  className: styles.toggle,
@@ -4365,7 +4926,29 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4365
4926
  );
4366
4927
  },
4367
4928
  }, adaptUnlocking === entry.entryId ? t("loadingPhase") : t("adaptUnlockBtn"))
4368
- : null) : null,
4929
+ : null)
4930
+ : null;
4931
+ detailPanel = el("div", { className: styles.detail },
4932
+ // ★ 0.5.93(issue #22 ② 收口):卡片上放不下的徽章**不提示、不展开**,
4933
+ // 全部徽章(系统徽章 + 用户徽章,顺序与卡片一致)在详情页里一次看全 ——
4934
+ // 这也是"卡片不再出现 +N"之后"看不到的徽章去哪了"的唯一答案。
4935
+ el("div", { className: styles.badgeAll, "data-allbadges": "true", title: t("badgesAll").replace("{n}", String(allBadges.length)) },
4936
+ allBadges.map((badge) => el("span", { key: "detail-" + badge.key, className: styles.tag, ...badge.props }, badge.label))),
4937
+ // ★ issue #22 ②(用户定稿的详情页顺序):用户备注/用户徽章放**最前面**,
4938
+ // 系统诊断 → 版本/仓库/说明摘要 → 动作按钮依次在其后。
4939
+ annotationBlock(entry),
4940
+ meta !== null ? el("p", { className: styles.desc }, meta.description ?? t("noDetail")) : null,
4941
+ entry.pendingCompat === true
4942
+ ? el("p", { className: styles.status, "data-pending": entry.adoptable ? "false" : "true" },
4943
+ entry.adoptable
4944
+ ? (entry.adoptable.basis === "verdict-rescinded"
4945
+ // 0.5.34:旧判据的误报已被当前判据撤回(真机 dshmarket 现场)——如实说是"判据更新",
4946
+ // 不能说成"插件已适配"(那会让人以为作者发过新版)
4947
+ ? t("adoptableRescinded").replace("{version}", String(entry.adoptable.version ?? "?"))
4948
+ : t("adoptableDetected").replace("{version}", String(entry.adoptable.version ?? "?")).replace("{prev}", String(compatInfo?.version ?? "?")))
4949
+ : t("pendingCompatHint") + (compatInfo && compatInfo.checkNote ? ";已知校验:" + compatInfo.checkNote : ""))
4950
+ : null,
4951
+ meta !== null && meta.version ? el("p", { className: styles.status }, t("versionLabel") + ":" + meta.version) : null,
4369
4952
  updateCheck !== null && updateCheck.status === "ready" && updateCheck.migrate && updateCheck.migrate.compatible
4370
4953
  ? el("div", null,
4371
4954
  el("div", { className: styles.rowTop },
@@ -4418,28 +5001,34 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4418
5001
  readme.summary ? el("p", { className: styles.desc }, readme.summary) : null,
4419
5002
  )
4420
5003
  : meta === null ? el("p", { className: styles.status }, t("noDetail")) : null,
5004
+ detailActionRow,
4421
5005
  );
4422
5006
  }
4423
5007
  }
4424
5008
  return el("li", { key: entry.entryId, className: styles.row },
4425
- el("div", { className: styles.rowTop },
4426
- el("strong", { className: styles.name, title: entry.moduleName }, (() => {
5009
+ el("div", { className: styles.rowTop, style: { minWidth: 0 } },
5010
+ el("strong", { className: styles.name, title: entry.moduleName, style: { minWidth: 0 } }, (() => {
4427
5011
  const famRoot = famRootOf(entry);
4428
5012
  const mn = String(entry.moduleName ?? "");
4429
- return famRoot !== null && familySize(famRoot) > 1 && mn.startsWith(famRoot + "/")
5013
+ const shortName = famRoot !== null && familySize(famRoot) > 1 && mn.startsWith(famRoot + "/")
4430
5014
  ? mn.slice(famRoot.length + 1)
4431
5015
  : moduleShortName(entry.moduleName);
5016
+ // ★ 用户 2026-10-26:备注可以**覆盖显示名**。有备注时标题用备注(强调色 + ✎ 标记),
5017
+ // 原短名降到旁边的小字 —— 一眼能看出"这个插件被我备注过",与没备注的区分开。
5018
+ const rec = annotationOf(entry);
5019
+ const custom = typeof rec?.note === "string" ? rec.note.trim() : "";
5020
+ if (custom === "") return shortName;
5021
+ return el("span", { "data-note-name": "true", style: { display: "inline-flex", alignItems: "baseline", gap: 6, minWidth: 0 } },
5022
+ el("span", { className: styles.noteName ?? undefined, style: { color: "var(--dsw-alias-state-business-primary)", fontWeight: 500, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, custom),
5023
+ el("span", { style: { opacity: 0.55, fontWeight: 400, fontSize: ".85em", whiteSpace: "nowrap" } }, shortName));
4432
5024
  })()),
4433
- el("span", { className: styles.tag, "data-enabled": entry.enabled ? "true" : "false" },
4434
- t(entry.enabled ? "enabledTag" : "disabledTag")),
4435
- entry.extra === true ? el("span", { className: styles.tag, "data-user": "true" }, t("extraTag")) : null,
4436
- entry.protected ? el("span", { className: styles.tag, "data-user": "true" }, t("protectedTag")) : null,
4437
- entry.userDisabled ? el("span", { className: styles.tag, "data-user": "true" }, t("userDisabledTag")) : null,
4438
- entry.userForced ? el("span", { className: styles.tag, "data-user": "true" }, t("userForcedTag")) : null,
4439
- // ★ 0.5.76:被**别处**(插件自身补丁/框架默认)禁用的行 —— 这就是"补丁强制启用"的来由
4440
- entry.disableSource === "bundle" ? el("span", { className: styles.tag, "data-user": "true", "data-source": "bundle" }, t("disabledByBundleTag")) : null,
4441
- // ★ 0.5.76:补丁写了强制开启、运行时却仍没起来 ⇒ 这条覆盖是**无效的**,必须说出来
4442
- entry.forcedNotLoaded === true ? el("span", { className: styles.tag, "data-user": "true", "data-source": "ineffective" }, t("forcedNotLoadedTag")) : null,
5025
+ // ★ 0.5.93(issue #22 ② 收口):徽章区**单行不换行**,放不下的直接看不见
5026
+ // (纯 CSS:`.pc_badgeRow` 固定高度 + overflow:hidden + white-space:nowrap),
5027
+ // 卡片上**不给** "+N"、不给"还有几个"的任何提示,也不做宽度测量 ——
5028
+ // 行为与这个功能上线前一致;全部徽章去详情页看。
5029
+ // 顺序(用户定稿):**先系统徽章(现有顺序一个字没改)→ 再用户徽章(描边样式)**。
5030
+ el("div", { className: styles.badgeRow },
5031
+ allBadges.map((badge) => el("span", { key: badge.key, className: styles.tag, ...badge.props }, badge.label))),
4443
5032
  ),
4444
5033
  el("div", { className: styles.meta },
4445
5034
  el("span", { className: styles.phase, "data-phase": entry.fiberPhase ?? "unobserved" },
@@ -4475,6 +5064,20 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4475
5064
  title: entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedHint") : t("pendingCompatHint")) : undefined,
4476
5065
  onClick: () => toggle(entry, !entry.enabled),
4477
5066
  }, busy === entry.entryId ? t("loadingPhase") : (entry.pendingCompat === true ? (entry.compatRescinded === true ? t("compatRescindedTag") : t("pendingCompatTag")) : t(entry.enabled ? "off" : "on"))),
5067
+ // ★ issue #22 ①:批量检测到新版的卡片直接给「更新」按钮 —— 与详情卡片里那个按钮
5068
+ // **完全同一条路**(startJob(-, moduleName, github, plugin, update=true, -, target)),
5069
+ // 目标是服务端唯一判据算出的 target;点下去走的是既有安装/更新作业,不新增任何写盘路径。
5070
+ batchItem !== null && batchItem.status === "update"
5071
+ ? el("button", {
5072
+ type: "button",
5073
+ className: styles.toggle,
5074
+ title: t("batchUpdateAvailableTip"),
5075
+ disabled: installing === entry.moduleName || inFlight.has(entry.moduleName),
5076
+ onClick: () => startJob("", entry.moduleName, "github", "plugin", true, null, batchItem.target ?? null),
5077
+ }, installing === entry.moduleName
5078
+ ? t("installing")
5079
+ : (entry.pendingCompat === true ? t("updateAndAdapt") : t("update") + " → v" + String(batchItem.target ?? "?")))
5080
+ : null,
4478
5081
  // ★ 0.5.76:补丁写了强制开启但运行时没起来 ⇒ 给一个撤销覆盖的入口
4479
5082
  entry.userForced === true ? el("button", {
4480
5083
  type: "button",
@@ -4602,11 +5205,29 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4602
5205
  // 组头放到最底部:普通条目在前,全家桶(组头+子包卡片)整体在后
4603
5206
  const nonFamilyCards = [];
4604
5207
  const familySections = new Map();
5208
+ // issue #22 ③:用户自建分组 → 折叠区块。**官方 family 优先**(用户拍定的规则):
5209
+ // 属于官方 family 的插件即使被归进用户分组,也**留在官方 family 区块里**,不进用户分组区块 ——
5210
+ // 官方 family 的渲染因此一个字都没变,两者共存且不会出现同一插件两处重复。
5211
+ const userGroupBuckets = new Map();
4605
5212
  familyCards.forEach((card, i) => {
4606
5213
  const e = filteredEntries[i];
4607
5214
  if (e === undefined) { nonFamilyCards.push(card); return; }
4608
5215
  const root = famRootOf(e);
4609
- if (root === null || familySize(root) <= 1) { nonFamilyCards.push(card); return; }
5216
+ // ★ 用户 2026-10-26:**用户分组优先** —— 被归进自己分组的插件,卡片直接住进那个分组区块
5217
+ // (像全家桶一样能展开看到卡片),官方 family 区块里不再重复渲染它。
5218
+ // ★ 分组允许重叠:一个插件可以同时出现在多个分组区块里(用户 2026-10-27 定稿的规则)
5219
+ const userGroupsOf = groupsOfPackage(e.moduleName);
5220
+ if (userGroupsOf.length > 0) {
5221
+ for (const g of userGroupsOf) {
5222
+ if (!userGroupBuckets.has(g.id)) userGroupBuckets.set(g.id, []);
5223
+ userGroupBuckets.get(g.id).push(card);
5224
+ }
5225
+ return;
5226
+ }
5227
+ if (root === null || familySize(root) <= 1) {
5228
+ nonFamilyCards.push(card);
5229
+ return;
5230
+ }
4610
5231
  if (!familySections.has(root)) familySections.set(root, { head: familyHeaders.find((h) => h && h.key === "family-" + root) ?? null, cards: [] });
4611
5232
  familySections.get(root).cards.push(card);
4612
5233
  });
@@ -4671,7 +5292,131 @@ const RESTART_FAILED_NOTE = "重启请求失败";
4671
5292
  ),
4672
5293
  ),
4673
5294
  ));
4674
- const rows = state.status === "ready" ? [...nonFamilyCards, ...familySectionEls, ...pendingCards] : [];
5295
+ // issue #22 ③(0.5.93 收口,用户定稿):用户自建分组在主列表里是**横框**,外观与官方 family
5296
+ // 区块**同一套视觉语言**(标题行 + 折叠箭头 + 容器 + 成员数徽章)。三条行为:
5297
+ // · 空分组(刚建出来、还没放插件)**也渲染** —— 否则"新建分组"在列表里根本看不到,
5298
+ // 等于点了没反应;
5299
+ // · 框里可以直接**加入 / 移出**插件(走 group-assign;服务端保证一个包只属于一个分组,
5300
+ // 移出只解除归属,不删插件、不动备注与徽章);
5301
+ // · **只含官方 family 成员**的分组仍不画(官方 family 优先:那些成员留在 family 区块里,
5302
+ // 绝不出现同一插件两处重复);
5303
+ // · 刚建出来的那个横框多挂一个 pc_groupNew(220ms 入场动画;prefers-reduced-motion 下不播)。
5304
+ // 数据来源只有服务端那一份 groups(annotationWrite 用响应里的全量状态覆盖 annotations),
5305
+ // 客户端额外只记一个"刚建好的 id"用于播动画,不维护第二份状态。
5306
+ const userGroupFrame = (group, bucket) => {
5307
+ const members = (Array.isArray(group.packages) ? group.packages : []).map((item) => String(item));
5308
+ const inGroup = new Set(members);
5309
+ const candidates = [];
5310
+ for (const entry of (state.status === "ready" ? state.data.entries : [])) {
5311
+ const name = String(entry.moduleName ?? "");
5312
+ if (name === "" || inGroup.has(name) || candidates.includes(name)) continue;
5313
+ candidates.push(name);
5314
+ }
5315
+ const fresh = groupJustCreated !== null && String(group.id) === String(groupJustCreated);
5316
+ const header = el("div", { className: styles.rowTop, style: { alignItems: "center" } },
5317
+ el("strong", { className: styles.name, style: { flex: "0 0 auto", whiteSpace: "nowrap" }, title: String(group.name) }, String(group.name)),
5318
+ el("span", { className: styles.tag, "data-user-badge": "true" }, t("userGroupTag") + " · " + members.length + " " + t("familySubs")),
5319
+ el("button", {
5320
+ type: "button",
5321
+ className: styles.toggle,
5322
+ style: { whiteSpace: "nowrap", flexShrink: 0 },
5323
+ disabled: annotationBusy === group.id,
5324
+ onClick: () => annotationWrite({ action: "group-collapse", id: group.id, collapsed: group.collapsed !== true }, null),
5325
+ }, group.collapsed === true ? "▸ " + t("familyExpand") : "▾ " + t("familyCollapse")),
5326
+ // ★ 用户 2026-10-27:分组框也要有「批量检测更新 / 一键启用已适配 / 删除分组」
5327
+ el("button", {
5328
+ type: "button",
5329
+ className: styles.toggle,
5330
+ style: { whiteSpace: "nowrap", flexShrink: 0 },
5331
+ disabled: annotationBusy === group.id,
5332
+ onClick: () => {
5333
+ // 批检:组内每个成员各查一次(单包接口复用,判据不分叉)
5334
+ setMessage(t("checkingUpdate"));
5335
+ const names = members.slice();
5336
+ Promise.all(names.map((nm) => call("/plugin-console/check-update", { packageName: nm }).catch(() => null)))
5337
+ .then((list) => {
5338
+ const hits = list.filter((d) => d !== null && typeof d?.target === "string" && d.target !== "")
5339
+ setMessage(hits.length > 0 ? t("familyUpdate") + ":" + hits.length + " / " + names.length : t("upToDate"));
5340
+ });
5341
+ },
5342
+ }, t("familyCheckUpdate")),
5343
+ el("button", {
5344
+ type: "button",
5345
+ className: styles.toggle,
5346
+ style: { whiteSpace: "nowrap", flexShrink: 0 },
5347
+ disabled: annotationBusy === group.id,
5348
+ onClick: () => {
5349
+ // 一键启用已适配:只对组内有 adoptable 的成员解锁
5350
+ const ready = bucket
5351
+ .map((card) => card?.key ?? null)
5352
+ .filter((x) => x !== null);
5353
+ const targets = (state.status === "ready" ? state.data.entries : [])
5354
+ .filter((e) => members.includes(String(e.moduleName ?? "")) && e.adoptable !== null && e.adoptable !== undefined);
5355
+ if (targets.length === 0) { setMessage(t("gateNoAdoptable")); return; }
5356
+ Promise.all(targets.map((e) => call("/plugin-console/adapt-unlock", { entryId: e.entryId }).catch(() => null)))
5357
+ .then(() => { setMessage(t("familyUnlockAll") + ":" + targets.length); refresh(); });
5358
+ },
5359
+ }, t("familyUnlockAll")),
5360
+ el("button", {
5361
+ type: "button",
5362
+ className: styles.toggle,
5363
+ style: { display: "none" },
5364
+ title: t("annotationsPanelTitle"),
5365
+ onClick: () => { openAnnotationsPanel(); },
5366
+ }, t("annotationsBtn")));
5367
+ const memberChips = members.length === 0
5368
+ ? el("span", { className: styles.status }, t("groupNoMembers"))
5369
+ : members.map((name) => el("span", { key: "ugm-" + name, className: styles.tag, "data-usergroup-member": "true", title: name }, name,
5370
+ el("button", {
5371
+ type: "button",
5372
+ className: styles.tag,
5373
+ "data-more": "true",
5374
+ title: t("groupMemberRemove"),
5375
+ "aria-label": t("groupMemberRemove"),
5376
+ disabled: annotationBusy === group.id,
5377
+ onClick: () => annotationWrite({ action: "group-assign", id: group.id, packageName: name, member: false }, t("groupUnassigned")),
5378
+ }, "×")));
5379
+ const addSelect = el("select", {
5380
+ value: "",
5381
+ "aria-label": "groupMemberAdd",
5382
+ disabled: annotationBusy === group.id || candidates.length === 0,
5383
+ style: { font: "inherit", padding: "2px 6px", maxWidth: 320 },
5384
+ onChange: (event) => {
5385
+ const picked = String(event.target.value ?? "");
5386
+ if (picked === "") return;
5387
+ annotationWrite({ action: "group-assign", id: group.id, packageName: picked }, t("groupAssigned").replace("{name}", String(group.name)));
5388
+ },
5389
+ },
5390
+ el("option", { value: "" }, candidates.length === 0 ? t("groupMemberNone") : t("groupMemberPick")),
5391
+ candidates.map((name) => el("option", { key: "ugc-" + name, value: name }, name)));
5392
+ const body = el("div", { style: { flexDirection: "column", gap: 6, display: "flex", marginTop: 2 } },
5393
+ el("div", { className: styles.annotRow }, memberChips, addSelect),
5394
+ bucket.length > 0
5395
+ ? el("ul", { className: styles.list }, bucket)
5396
+ : members.length > 0
5397
+ ? el("p", { className: styles.status, "data-in-family": "true" }, t("groupInFamilyNote"))
5398
+ : null);
5399
+ return el("div", {
5400
+ key: "usergroup-" + group.id,
5401
+ className: styles.detail + (fresh ? " " + styles.groupNew : ""),
5402
+ "data-usergroup": "true",
5403
+ "data-group-id": String(group.id),
5404
+ "data-new-group": fresh ? "true" : undefined,
5405
+ style: { border: "1px solid var(--dsw-alias-state-business-primary)", borderRadius: 10, padding: "8px 12px", gridColumn: "1 / -1" },
5406
+ },
5407
+ header,
5408
+ group.collapsed === true ? null : body);
5409
+ };
5410
+ const userGroupSectionEls = [];
5411
+ for (const group of userGroups) {
5412
+ const bucket = userGroupBuckets.get(group.id) ?? [];
5413
+ const members = Array.isArray(group.packages) ? group.packages : [];
5414
+ // ★ 用户 2026-10-26:分组框**永远画出来**。此前"成员全在官方 family 里就跳过"会让用户
5415
+ // 建完分组后在主列表什么都看不到(既没框也没入场动画),观感等于"点了没反应"。
5416
+ // 同一插件仍不重复渲染:那些成员留在官方 family 区块里,这里只给一句如实说明。
5417
+ userGroupSectionEls.push(userGroupFrame(group, bucket));
5418
+ }
5419
+ const rows = state.status === "ready" ? [...nonFamilyCards, ...familySectionEls, ...userGroupSectionEls, ...pendingCards] : [];
4675
5420
  const marketShown = market === null || market.status !== "ready"
4676
5421
  ? []
4677
5422
  : officialOnly
@@ -5225,7 +5970,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5225
5970
  "aria-label": t("linksDevRootLabel"),
5226
5971
  value: "",
5227
5972
  disabled: linksPinBusy,
5228
- onChange: (event) => { const picked = String(event.currentTarget.value ?? ""); if (picked !== "") setLinksDir(devRootDraft(picked)); },
5973
+ onChange: (event) => { const picked = String(event.target.value ?? ""); if (picked !== "") setLinksDir(devRootDraft(picked)); },
5229
5974
  style: { flex: "0 1 300px", minWidth: 180, font: "inherit", padding: "4px 6px" },
5230
5975
  },
5231
5976
  el("option", { value: "" }, t("linksDevRootPick")),
@@ -5237,7 +5982,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5237
5982
  value: linksDir,
5238
5983
  disabled: linksPinBusy,
5239
5984
  "aria-label": t("linksDirPlaceholder"),
5240
- onChange: (event) => setLinksDir(String(event.currentTarget.value ?? "")),
5985
+ onChange: (event) => setLinksDir(String(event.target.value ?? "")),
5241
5986
  style: { flex: 1, minWidth: 220, font: "inherit", padding: "4px 8px" },
5242
5987
  }),
5243
5988
  // ★ 只在宿主**确实**有系统目录对话框时才渲染(web 端没有 ⇒ 不显示,不是灰掉)
@@ -5318,6 +6063,231 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5318
6063
  el("div", { className: styles.rowTop, style: { justifyContent: "flex-end", gap: 8, marginTop: 10 } },
5319
6064
  el("button", { type: "button", className: styles.toolItem, "data-busy": linksPanelBusy ? "true" : "false", onClick: () => { linksPanelLoad(); linksDevRootsLoad(); } }, linksPanelBusy ? t("linksPanelLoading") : t("linksPanelReload")))))
5320
6065
  : null,
6066
+ annotationsPanelOpen
6067
+ ? el("div", { className: styles.modalBackdrop, onClick: (event) => { if (event.target === event.currentTarget) setAnnotationsPanelOpen(false); } },
6068
+ el("div", { className: styles.modalCard, style: { maxWidth: 760, maxHeight: "82vh", overflow: "auto" } },
6069
+ el("div", { className: styles.rowTop, style: { justifyContent: "space-between" } },
6070
+ el("strong", { className: styles.name }, t("annotationsBtn")),
6071
+ el("button", { type: "button", className: styles.trashBtn, style: { fontSize: 14, padding: "4px 8px" }, title: t("closeModal"), "aria-label": t("closeModal"), onClick: () => setAnnotationsPanelOpen(false) }, "✕")),
6072
+ el("p", { className: styles.message }, t("annotationsPanelTitle")),
6073
+ annotations !== null && annotations.file !== null && annotations.file !== undefined
6074
+ ? el("p", { className: styles.status }, String(annotations.file))
6075
+ : null,
6076
+ // 失败/降级一律有文字(绝不"点了一下没反应")
6077
+ annotationsNote !== null
6078
+ ? el("p", { className: styles.message, "data-error": "true" }, annotationsNote)
6079
+ : null,
6080
+ // ── 分组管理:新建 / 重命名 / 删除(两段式)/ 撤销删除 ─────────────────
6081
+ el("div", { style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 6, paddingTop: 8, borderTop: "1px solid var(--dsw-alias-border-secondary, rgba(127,127,127,.18))" } },
6082
+ el("div", { className: styles.annotRow },
6083
+ el("input", {
6084
+ type: "text",
6085
+ value: groupDraftName,
6086
+ placeholder: t("groupCreatePlaceholder"),
6087
+ "aria-label": t("groupCreatePlaceholder"),
6088
+ disabled: annotationBusy !== null,
6089
+ style: { flex: 1, minWidth: 200, font: "inherit", padding: "4px 8px" },
6090
+ onChange: (event) => setGroupDraftName(String(event.target.value ?? "")),
6091
+ onKeyDown: (event) => { if (event.key === "Enter") { event.preventDefault(); createUserGroup(); } },
6092
+ }),
6093
+ el("button", { type: "button", className: styles.toggle, disabled: groupDraftName.trim() === "" || annotationBusy !== null, onClick: createUserGroup }, t("groupCreate"))),
6094
+ groupUndo !== null
6095
+ ? el("div", { className: styles.annotRow },
6096
+ el("span", { className: styles.status, "data-pending": "true" }, t("groupDeleted").replace("{name}", String(groupUndo.group?.name ?? "")).replace("{n}", String(Array.isArray(groupUndo.group?.packages) ? groupUndo.group.packages.length : 0))),
6097
+ el("button", {
6098
+ type: "button",
6099
+ className: styles.toggle,
6100
+ disabled: annotationBusy !== null,
6101
+ onClick: () => {
6102
+ const record = groupUndo;
6103
+ setGroupUndo(null);
6104
+ annotationWrite({ action: "group-restore", group: record.group, index: record.index }, t("groupRestored").replace("{name}", String(record.group?.name ?? "")));
6105
+ },
6106
+ }, t("groupRestore")))
6107
+ : null,
6108
+ userGroups.length === 0
6109
+ ? el("p", { className: styles.status }, t("groupEmptyHint"))
6110
+ : el("ul", { className: styles.list }, userGroups.map((group) => el("li", { key: "ug-" + group.id, className: styles.row },
6111
+ el("div", { className: styles.rowTop, style: { justifyContent: "space-between" } },
6112
+ el("strong", { className: styles.name, "data-group-name": String(group.id), style: { cursor: "pointer", borderBottom: "1px dotted currentColor" }, title: t("groupOpenHint"), onClick: () => { setPanelGroupOpen((prev) => (prev === group.id ? null : group.id)); setPanelGroupQuery(""); } }, String(group.name)),
6113
+ el("span", { className: styles.tag, "data-user-badge": "true" }, t("groupMembers").replace("{n}", String(Array.isArray(group.packages) ? group.packages.length : 0)))),
6114
+ el("div", { className: styles.annotRow },
6115
+ groupRenameEdit !== null && groupRenameEdit.id === group.id
6116
+ ? el("div", { className: styles.annotRow },
6117
+ el("input", {
6118
+ type: "text",
6119
+ value: groupRenameEdit.draft,
6120
+ "aria-label": t("groupRename"),
6121
+ style: { font: "inherit", padding: "2px 6px" },
6122
+ onChange: (event) => setGroupRenameEdit({ id: group.id, draft: String(event.target.value ?? "") }),
6123
+ onKeyDown: (event) => {
6124
+ if (event.key === "Escape") { event.preventDefault(); setGroupRenameEdit(null); return; }
6125
+ if (event.key === "Enter") {
6126
+ event.preventDefault();
6127
+ const draft = groupRenameEdit.draft;
6128
+ setGroupRenameEdit(null);
6129
+ annotationWrite({ action: "group-rename", id: group.id, name: draft }, t("groupRenamed"));
6130
+ }
6131
+ },
6132
+ }),
6133
+ el("button", { type: "button", className: styles.toggle, onClick: () => setGroupRenameEdit(null) }, t("linksCancel")))
6134
+ : el("button", { type: "button", className: styles.toggle, disabled: annotationBusy === group.id, onClick: () => setGroupRenameEdit({ id: group.id, draft: String(group.name) }) }, t("groupRename")),
6135
+ groupDeleteArm === group.id
6136
+ ? el("div", { className: styles.annotRow },
6137
+ el("span", { className: styles.status, "data-pending": "true" }, t("groupDeleteConfirm").replace("{name}", String(group.name))),
6138
+ el("button", { type: "button", className: styles.toggle, onClick: () => setGroupDeleteArm(null) }, t("linksCancel")),
6139
+ el("button", {
6140
+ type: "button",
6141
+ className: styles.toggle,
6142
+ disabled: annotationBusy === group.id,
6143
+ onClick: () => {
6144
+ setGroupDeleteArm(null);
6145
+ const memberCount = Array.isArray(group.packages) ? group.packages.length : 0;
6146
+ // 两段式:第一次点只是"上膛",这里才是真删(且**只解除归属**)
6147
+ annotationWrite({ action: "group-delete", id: group.id }, t("groupDeleted").replace("{name}", String(group.name)).replace("{n}", String(memberCount))).then((data) => {
6148
+ const removed = data?.deleted;
6149
+ if (removed !== null && removed !== undefined && removed.group !== null && removed.group !== undefined) setGroupUndo({ group: removed.group, index: Number.isFinite(removed.index) ? removed.index : null });
6150
+ });
6151
+ },
6152
+ }, t("confirmDelete")))
6153
+ : el("button", { type: "button", className: styles.trashBtn, title: t("groupRemove"), "aria-label": t("groupRemove"), disabled: annotationBusy === group.id, onClick: () => setGroupDeleteArm(group.id) }, "✕")))))),
6154
+ // ★ 2026-10-27:点分组名 → 展开「成员 + 一键更新 + 搜索添加(含备注)」
6155
+ (() => {
6156
+ const open = userGroups.find((g) => g.id === panelGroupOpen) ?? null;
6157
+ if (open === null) {
6158
+ return el("p", { className: styles.status, "data-panel-groupadd-hint": "true" }, t("groupOpenHint"));
6159
+ }
6160
+ const inGroup = new Set((Array.isArray(open.packages) ? open.packages : []).map((x) => String(x)));
6161
+ const noteOf = (nm) => {
6162
+ const rec = annotations !== null && annotations.plugins !== null && annotations.plugins !== undefined
6163
+ ? annotations.plugins[nm] ?? null
6164
+ : null;
6165
+ return typeof rec?.note === "string" ? rec.note : "";
6166
+ };
6167
+ const q = panelGroupQuery.trim().toLowerCase();
6168
+ const pool = (state.status === "ready" ? state.data.entries : [])
6169
+ .map((e) => String(e.moduleName ?? ""))
6170
+ .filter((nm) => nm !== "" && !nm.startsWith("cordis:") && !inGroup.has(nm))
6171
+ // ★ 用户 2026-10-27:搜索要能命中**备注**(不只是包名)
6172
+ .filter((nm) => q === "" || nm.toLowerCase().includes(q) || noteOf(nm).toLowerCase().includes(q));
6173
+ const shown = pool.slice(0, panelGroupShow);
6174
+ const hidden = Math.max(0, pool.length - shown.length);
6175
+ const chips = [...inGroup].map((nm) => el("span", { key: "pgm-" + nm, className: styles.tag, "data-usergroup-member": "true" },
6176
+ nm,
6177
+ el("button", {
6178
+ type: "button",
6179
+ className: styles.tag,
6180
+ "data-more": "true",
6181
+ title: t("groupMemberRemove"),
6182
+ onClick: () => annotationWrite({ action: "group-assign", id: open.id, packageName: nm, member: false }, t("groupUnassigned")),
6183
+ }, "×")))
6184
+ const headerRow = el("div", { className: styles.annotRow },
6185
+ el("strong", { className: styles.name }, String(open.name)),
6186
+ el("span", { className: styles.status }, t("groupMembers").replace("{n}", String(inGroup.size))),
6187
+ el("button", {
6188
+ type: "button",
6189
+ className: styles.toggle,
6190
+ "data-group-update-all": String(open.id),
6191
+ title: t("groupUpdateAllTip"),
6192
+ onClick: () => {
6193
+ const members2 = [...inGroup];
6194
+ if (members2.length === 0) { setMessage(t("groupNoMembers")); return; }
6195
+ // 逐个串行:先查目标版本,再按那个版本更新(与单卡同一判据),门控照旧生效
6196
+ const step = (i, hit) => {
6197
+ if (i >= members2.length) {
6198
+ setMessage(t("groupUpdateAllDone").replace("{hit}", String(hit)).replace("{n}", String(members2.length)));
6199
+ return;
6200
+ }
6201
+ const nm = members2[i];
6202
+ setMessage(t("groupUpdateAllProgress").replace("{i}", String(i + 1)).replace("{n}", String(members2.length)) + " — " + nm);
6203
+ call("/plugin-console/check-update", { packageName: nm }).then((d) => {
6204
+ const target = typeof d?.target === "string" && d.target !== "" ? d.target : (typeof d?.latest === "string" ? d.latest : null);
6205
+ if (target !== null) startJob("", nm, "github", "plugin", true, null, target);
6206
+ step(i + 1, target !== null ? hit + 1 : hit);
6207
+ }, () => step(i + 1, hit));
6208
+ };
6209
+ step(0, 0);
6210
+ },
6211
+ }, t("groupUpdateAll")))
6212
+ const searchInput = el("input", {
6213
+ type: "search",
6214
+ value: panelGroupQuery,
6215
+ placeholder: t("groupAddSearchPlaceholder"),
6216
+ "aria-label": t("groupAddSearchPlaceholder"),
6217
+ style: { font: "inherit", padding: "4px 8px", width: "100%" },
6218
+ onChange: (event) => setPanelGroupQuery(String(event.target.value ?? "")),
6219
+ })
6220
+ const candRows = shown.map((nm) => {
6221
+ const note = noteOf(nm);
6222
+ return el("li", { key: "pgc-" + nm, className: styles.row },
6223
+ el("div", { className: styles.rowTop },
6224
+ el("strong", { className: styles.name, title: nm, style: { minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, nm),
6225
+ note !== ""
6226
+ ? el("span", { style: { color: "var(--dsw-alias-state-business-primary)", fontWeight: 500, fontSize: ".9em", whiteSpace: "nowrap", maxWidth: "45%", overflow: "hidden", textOverflow: "ellipsis" } }, note)
6227
+ : null,
6228
+ el("button", {
6229
+ type: "button",
6230
+ className: styles.toggle,
6231
+ "data-group-add": nm,
6232
+ disabled: annotationBusy !== null,
6233
+ onClick: () => annotationWrite({ action: "group-assign", id: open.id, packageName: nm }, t("groupAssigned").replace("{name}", String(open.name))),
6234
+ }, "+ " + t("groupAddOne"))));
6235
+ })
6236
+ const candList = el("ul", { className: styles.list, "data-group-candidates": "true" },
6237
+ pool.length === 0
6238
+ ? el("li", { className: styles.row }, el("span", { className: styles.status }, t("groupAddNoMatch")))
6239
+ : candRows)
6240
+ const moreBtn = hidden > 0
6241
+ ? el("button", { type: "button", className: styles.toggle, "data-group-loadmore": "true", onClick: () => setPanelGroupShow((prev) => prev + 20) }, t("groupLoadMore").replace("{n}", String(hidden)))
6242
+ : null
6243
+ return el("div", { "data-panel-groupadd": String(open.id), style: { display: "flex", flexDirection: "column", gap: 6, marginTop: 6 } },
6244
+ headerRow,
6245
+ el("div", { className: styles.annotRow }, chips.length > 0 ? chips : el("span", { className: styles.status }, t("groupNoMembers"))),
6246
+ searchInput,
6247
+ candList,
6248
+ moreBtn)
6249
+ })(),
6250
+ // ── 插件行:备注 / 徽章 / 分组(每行一套,与详情页同一个编辑块)───────────
6251
+ el("p", { className: styles.message, style: { marginTop: 8 } },
6252
+ annotations === null
6253
+ ? t("annotationsEmpty")
6254
+ : t("annotationsStats")
6255
+ .replace("{p}", String(annotations.stats?.packages ?? 0))
6256
+ .replace("{b}", String(annotations.stats?.badges ?? 0))
6257
+ .replace("{g}", String(annotations.stats?.groups ?? 0))),
6258
+ // ★ 0.5.93(issue #22 ②,用户定稿):列表**默认收起**,只留一行摘要「插件列表(N)· 点此展开」;
6259
+ // 点开才渲染搜索框与插件行。查询词是受控 state ⇒ 边打边筛,且过滤不会清掉输入框里的字。
6260
+ el("button", {
6261
+ type: "button",
6262
+ className: styles.toggle,
6263
+ "aria-label": "annotationsListToggle",
6264
+ "aria-expanded": annotationsListOpen ? "true" : "false",
6265
+ "data-open": annotationsListOpen ? "true" : "false",
6266
+ onClick: () => setAnnotationsListOpen((prev) => !prev),
6267
+ }, (annotationsListOpen ? t("annotationsListHide") : t("annotationsListShow"))
6268
+ .replace("{n}", String(annotationListEntries.length))),
6269
+ annotationsListOpen === false
6270
+ ? null
6271
+ : el("div", { style: { flexDirection: "column", gap: 8, display: "flex", marginTop: 6 } },
6272
+ el("input", {
6273
+ type: "text",
6274
+ value: annotationsQuery,
6275
+ placeholder: t("annotationsSearchPlaceholder"),
6276
+ "aria-label": "annotationsSearch",
6277
+ style: { font: "inherit", padding: "4px 8px", borderRadius: 6 },
6278
+ onChange: (event) => setAnnotationsQuery(String(event.target.value ?? "")),
6279
+ }),
6280
+ annotationListEntries.length === 0
6281
+ ? el("p", { className: styles.status }, t("empty"))
6282
+ : annotationListFiltered.length === 0
6283
+ ? el("p", { className: styles.status, "data-nomatch": "true" }, t("annotationsNoMatch"))
6284
+ : el("ul", { className: styles.list, "data-annotlist": "true" }, annotationListFiltered.map((entry) => el("li", { key: "an-" + entry.entryId, className: styles.row },
6285
+ el("div", { className: styles.rowTop },
6286
+ el("strong", { className: styles.name, title: entry.moduleName }, moduleShortName(entry.moduleName)),
6287
+ entry.extra === true ? el("span", { className: styles.tag, "data-user": "true" }, t("extraTag")) : null),
6288
+ el("div", { className: styles.meta }, el("code", null, String(entry.moduleName ?? ""))),
6289
+ annotationBlock(entry)))))))
6290
+ : null,
5321
6291
  gateOpen
5322
6292
  ? el("div", { className: styles.modalBackdrop, onClick: (event) => { if (event.target === event.currentTarget) setGateOpen(false); } },
5323
6293
  el("div", { className: styles.modalCard, style: { maxWidth: 520 } },
@@ -5424,7 +6394,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5424
6394
  aiJob === null || (aiJob.status === "running" && aiJob.stage === "planning")
5425
6395
  ? el("div", { style: { flexDirection: "column", gap: "10px", display: "flex" } },
5426
6396
  el("section", null,
5427
- el("input", { type: "text", placeholder: t("aiEmpowerSrcTitle"), value: aiSource, disabled: aiRunning, onChange: (event) => setAiSource(event.currentTarget.value), onKeyDown: (event) => { if (event.key === "Enter" && !aiRunning) aiStartPlan(); }, style: { width: "100%", boxSizing: "border-box", font: "inherit", padding: "6px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" } }),
6397
+ el("input", { type: "text", placeholder: t("aiEmpowerSrcTitle"), value: aiSource, disabled: aiRunning, onChange: (event) => setAiSource(event.target.value), onKeyDown: (event) => { if (event.key === "Enter" && !aiRunning) aiStartPlan(); }, style: { width: "100%", boxSizing: "border-box", font: "inherit", padding: "6px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" } }),
5428
6398
  el("div", { className: styles.rowTop },
5429
6399
  el("button", { type: "button", className: styles.toggle, disabled: aiRunning || aiSource.trim() === "", onClick: aiStartPlan }, t("aiEmpowerLabel")),
5430
6400
  aiRunning ? el("span", { className: styles.message }, t("aiEmpowerWaiting") + (aiJob?.source ? "(" + aiJob.source + ")" : "")) : null,
@@ -5447,7 +6417,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5447
6417
  el("div", { style: { flexDirection: "column", gap: "6px", display: "flex", maxHeight: 220, overflowY: "auto" } },
5448
6418
  (aiJob.steps ?? []).map((s) =>
5449
6419
  el("label", { key: s.index, className: styles.consentRemember, style: { alignItems: "flex-start" } },
5450
- el("input", { type: "checkbox", checked: aiSelected[s.index] === true, onChange: (event) => setAiSelected((prev) => ({ ...prev, [s.index]: event.currentTarget.checked })) }),
6420
+ el("input", { type: "checkbox", checked: aiSelected[s.index] === true, onChange: (event) => setAiSelected((prev) => ({ ...prev, [s.index]: event.target.checked })) }),
5451
6421
  el("span", null, (s.index + 1) + ". [" + s.action + "] " + (s.description ?? "") + (s.path ? " → " + s.path : "")))),
5452
6422
  el("div", { className: styles.rowTop },
5453
6423
  el("button", { type: "button", className: styles.toggle, onClick: aiRun }, t("aiEmpowerRun")),
@@ -5468,10 +6438,10 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5468
6438
  el("p", { className: styles.message }, repoLandTitleText()),
5469
6439
  el("div", { className: styles.rowTop, style: { flexWrap: "wrap" } },
5470
6440
  el("span", { className: styles.message }, t("repoLandDirLabel") + ":" + (repoLandDir || "—")),
5471
- el("input", { type: "text", value: repoLandDirInput, onChange: (event) => setRepoLandDirInput(event.currentTarget.value), style: { flex: 1, minWidth: 120, font: "inherit", padding: "4px 8px", borderRadius: 6, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" } }),
6441
+ el("input", { type: "text", value: repoLandDirInput, onChange: (event) => setRepoLandDirInput(event.target.value), style: { flex: 1, minWidth: 120, font: "inherit", padding: "4px 8px", borderRadius: 6, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" } }),
5472
6442
  el("button", { type: "button", className: styles.toggle, onClick: repoLandSaveDir }, t("repoLandSaveDir"))),
5473
6443
  el("section", null,
5474
- el("input", { type: "text", placeholder: t("repoLandPlaceholder"), value: repoLandInput, disabled: repoLandBusy, onChange: (event) => setRepoLandInput(event.currentTarget.value), onKeyDown: (event) => { if (event.key === "Enter" && !repoLandBusy) repoLandStart(); }, style: { width: "100%", boxSizing: "border-box", font: "inherit", padding: "6px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" } }),
6444
+ el("input", { type: "text", placeholder: t("repoLandPlaceholder"), value: repoLandInput, disabled: repoLandBusy, onChange: (event) => setRepoLandInput(event.target.value), onKeyDown: (event) => { if (event.key === "Enter" && !repoLandBusy) repoLandStart(); }, style: { width: "100%", boxSizing: "border-box", font: "inherit", padding: "6px 10px", borderRadius: 8, border: "1px solid var(--dsw-alias-border-l2)", background: "var(--dsw-alias-bg-layer-1)", color: "var(--dsw-alias-label-primary)" } }),
5475
6445
  el("div", { className: styles.rowTop },
5476
6446
  el("button", { type: "button", className: styles.toggle, disabled: repoLandBusy || repoLandInput.trim() === "", onClick: repoLandStart }, repoLandBusy ? t("repoLandBusy") : t("repoLandLabel"))),
5477
6447
  repoLandResult !== null
@@ -5728,7 +6698,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5728
6698
  type: "password",
5729
6699
  placeholder: t("githubTokenPlaceholder"),
5730
6700
  value: ghToken,
5731
- onChange: (event) => setGhToken(event.currentTarget.value),
6701
+ onChange: (event) => setGhToken(event.target.value),
5732
6702
  style: { width: "100%", boxSizing: "border-box" },
5733
6703
  }),
5734
6704
  el("button", { type: "button", className: styles.toggle, disabled: ghLoginBusy || ghToken.trim() === "", onClick: githubLogin }, t("githubLoginBtn")))
@@ -5811,6 +6781,16 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5811
6781
  toolbarOpen
5812
6782
  ? el("div", { className: styles.toolbarGroup, style: (dragPos ?? toolbarPos) !== null ? { left: (dragPos ?? toolbarPos).left, top: (dragPos ?? toolbarPos).top + 36, right: "auto" } : undefined },
5813
6783
 
6784
+ el("button", {
6785
+ type: "button",
6786
+ className: styles.toolItem,
6787
+ "data-active": annotationsPanelOpen ? "true" : "false",
6788
+ title: t("annotationsPanelTitle"),
6789
+ "aria-label": t("annotationsBtn"),
6790
+ // issue #22 ②③:打开即读一次(注解是独立文件,别处改了也能看到最新的);
6791
+ // ★ 0.5.93:打开时列表回到**收起**状态(见 openAnnotationsPanel)
6792
+ onClick: () => toggleAnnotationsPanel(),
6793
+ }, t("annotationsBtn") + (annotations !== null && annotations.stats !== null && annotations.stats !== undefined && annotations.stats.packages > 0 ? " " + annotations.stats.packages : "")),
5814
6794
  hostedByShell ? null : el("button", { // ★ 0.5.84:桌面端(shell 托管)不显示;web 端保留且可用
5815
6795
  type: "button",
5816
6796
  className: styles.toolItem,
@@ -5965,7 +6945,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
5965
6945
  : t("searchPlaceholderOther").replace("{source}", sourceDisplayName(searchSource))),
5966
6946
  "aria-label": t("search"),
5967
6947
  onChange: (event) => {
5968
- const v = event.currentTarget.value;
6948
+ const v = event.target.value;
5969
6949
  setQuery(v);
5970
6950
  try { localStorage.setItem("pc-market-query", v); } catch {}
5971
6951
  },
@@ -6183,6 +7163,49 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6183
7163
  el("h3", null, mode === "skills" ? t("installedSkillsTitle") : t("installedTitle")),
6184
7164
  mode === "plugins"
6185
7165
  ? el("button", { type: "button", className: styles.toggle, onClick: () => setInstalledSearchOpen((v) => !v) }, t("search"))
7166
+ : null,
7167
+ // ★ issue #22 ①:一键检测全部更新(**只有图标**的圆按钮 ↑↑↑,与「搜索」同高)。
7168
+ // 三态都在按钮上:idle(可点)/ checking(转圈 + 禁用)/ done(描边高亮,再点是重查)。
7169
+ // 判据全在服务端(唯一判据 + 并发上限),这里只负责"问一句、如实显示"。
7170
+ mode === "plugins"
7171
+ ? el("button", {
7172
+ type: "button",
7173
+ className: styles.iconBtn,
7174
+ title: batchCheck !== null && batchCheck.status === "checking"
7175
+ ? t("checkingAllUpdates").replace("{n}", String(batchCheck.total ?? 0))
7176
+ : t("checkAllUpdates") + ":" + t("checkAllUpdatesHint").replace("{n}", String(state.status === "ready" ? state.data.entries.length : 0)),
7177
+ "aria-label": t("checkAllUpdates"),
7178
+ "data-state": batchCheck !== null && batchCheck.status === "checking" ? "checking" : (batchCheck !== null && batchCheck.status === "ready" ? "done" : "idle"),
7179
+ disabled: (batchCheck !== null && batchCheck.status === "checking") || state.status !== "ready",
7180
+ onClick: checkAllUpdates,
7181
+ }, batchCheck !== null && batchCheck.status === "checking"
7182
+ ? el("span", { className: styles.spinner })
7183
+ : el("svg", { width: 13, height: 13, viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: 2.2, strokeLinecap: "round", strokeLinejoin: "round" },
7184
+ el("path", { d: "M5 20V7" }), el("polyline", { points: "3 9 5 7 7 9" }),
7185
+ el("path", { d: "M12 20V7" }), el("polyline", { points: "10 9 12 7 14 9" }),
7186
+ el("path", { d: "M19 20V7" }), el("polyline", { points: "17 9 19 7 21 9" })))
7187
+ : null,
7188
+ // 结果态:可更新 > 0 ⇒ 可点的筛选按钮(同一个筛选的开关);可更新 = 0 ⇒ **只读**小结,
7189
+ // 不做成按钮(否则就是"点了一下列表变空"的死按钮)。
7190
+ mode === "plugins" && batchCheck !== null && batchCheck.status === "ready" && batchCheck.updatable > 0
7191
+ ? el("button", {
7192
+ type: "button",
7193
+ className: styles.extraBtn,
7194
+ title: t("batchUpdateResultTip")
7195
+ .replace("{n}", String(batchCheck.updatable))
7196
+ .replace("{m}", String(Math.max(0, batchCheck.checked - batchCheck.updatable - batchCheck.unknown)))
7197
+ .replace("{k}", String(batchCheck.unknown))
7198
+ .replace("{at}", new Date(batchCheck.at).toLocaleTimeString()),
7199
+ "aria-pressed": onlyUpdatable ? "true" : "false",
7200
+ onClick: () => setOnlyUpdatable((v) => !v),
7201
+ }, t("batchUpdateOnly") + " · " + batchCheck.updatable + (onlyUpdatable ? " ✓" : ""))
7202
+ : null,
7203
+ mode === "plugins" && batchCheck !== null && batchCheck.status === "ready" && batchCheck.updatable === 0
7204
+ ? el("span", { className: styles.tag, "data-enabled": "true", title: t("batchUpdateResultTip").replace("{n}", "0").replace("{m}", String(Math.max(0, batchCheck.checked - batchCheck.unknown))).replace("{k}", String(batchCheck.unknown)).replace("{at}", new Date(batchCheck.at).toLocaleTimeString()) },
7205
+ t("checkAllUpdateNone") + (batchCheck.unknown > 0 ? "(" + t("batchUpdateUnknown") + " " + batchCheck.unknown + ")" : ""))
7206
+ : null,
7207
+ mode === "plugins" && batchCheck !== null && batchCheck.status === "error"
7208
+ ? el("span", { className: styles.tag, "data-pending": "true", title: String(batchCheck.error ?? "") }, t("checkAllUpdateFailed"))
6186
7209
  : null),
6187
7210
  mode === "plugins" && installedSearchOpen
6188
7211
  ? el("div", { className: styles.search },
@@ -6193,7 +7216,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6193
7216
  placeholder: t("searchInstalledPlaceholder"),
6194
7217
  "aria-label": t("searchInstalledPlaceholder"),
6195
7218
  onChange: (event) => {
6196
- const v = event.currentTarget.value;
7219
+ const v = event.target.value;
6197
7220
  setInstalledQuery(v);
6198
7221
  try { localStorage.setItem("pc-market-installed-q", v); } catch {}
6199
7222
  },
@@ -6273,9 +7296,13 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6273
7296
  ? el("p", { className: styles.status }, t("empty"))
6274
7297
  : installedQueryNorm !== "" && rows.length === 0
6275
7298
  ? el("p", { className: styles.status }, t("noMatch"))
6276
- : extraOnly && rows.length === 0
6277
- ? el("p", { className: styles.status }, t("extraEmpty"))
6278
- : el("ul", { className: styles.list }, rows)),
7299
+ // issue #22 ①:把「仅看可更新」筛空的话必须说清楚**是筛空的**,
7300
+ // 否则用户看到空列表会以为插件没了(并给出取消筛选的办法)。
7301
+ : onlyUpdatable && rows.length === 0
7302
+ ? el("p", { className: styles.status }, t("batchUpdateFilterEmpty"))
7303
+ : extraOnly && rows.length === 0
7304
+ ? el("p", { className: styles.status }, t("extraEmpty"))
7305
+ : el("ul", { className: styles.list }, rows)),
6279
7306
  showBackTop
6280
7307
  ? el("button", {
6281
7308
  type: "button",
@@ -6323,7 +7350,7 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6323
7350
  type: "checkbox",
6324
7351
  checked: aiRemember,
6325
7352
  onChange: (event) => {
6326
- const checked = event.currentTarget.checked;
7353
+ const checked = event.target.checked;
6327
7354
  setAiRemember(checked);
6328
7355
  try { localStorage.setItem("pc-ai-remember", checked ? "1" : "0"); } catch {}
6329
7356
  },
@@ -6351,8 +7378,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6351
7378
  ...sourcesData.registries.map((src) =>
6352
7379
  editReg !== null && editReg.id === src.id
6353
7380
  ? el("div", { key: src.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
6354
- el("input", { type: "text", value: editReg.name, onChange: (event) => setEditReg({ ...editReg, name: event.currentTarget.value }), style: { flex: 1, minWidth: 80 } }),
6355
- el("input", { type: "text", value: editReg.url, onChange: (event) => setEditReg({ ...editReg, url: event.currentTarget.value }), style: { flex: 2, minWidth: 120 } }),
7381
+ el("input", { type: "text", value: editReg.name, onChange: (event) => setEditReg({ ...editReg, name: event.target.value }), style: { flex: 1, minWidth: 80 } }),
7382
+ el("input", { type: "text", value: editReg.url, onChange: (event) => setEditReg({ ...editReg, url: event.target.value }), style: { flex: 2, minWidth: 120 } }),
6356
7383
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: saveEditReg }, t("saveSource")),
6357
7384
  el("button", { type: "button", className: styles.toggle, onClick: () => setEditReg(null) }, t("cancelEdit")))
6358
7385
  : el("div", { key: src.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
@@ -6378,8 +7405,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6378
7405
  src.primary ? null : el("button", { type: "button", className: styles.toggle, title: t("setPrimaryHint"), disabled: sourcesBusy, onClick: () => sourcesAction({ action: "set-primary", id: src.id }) }, t("setPrimary")),
6379
7406
  el("button", { type: "button", className: styles.toggle, title: t("editSourceHint"), disabled: sourcesBusy, onClick: () => setEditReg({ id: src.id, name: src.name, url: src.url }) }, t("editSource"))))),
6380
7407
  el("div", { className: styles.rowTop },
6381
- el("input", { type: "text", placeholder: t("sourceName"), value: sourceName, onChange: (event) => setSourceName(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
6382
- el("input", { type: "text", placeholder: t("sourceUrl"), value: sourceUrl, onChange: (event) => setSourceUrl(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
7408
+ el("input", { type: "text", placeholder: t("sourceName"), value: sourceName, onChange: (event) => setSourceName(event.target.value), style: { flex: 1, minWidth: 0 } }),
7409
+ el("input", { type: "text", placeholder: t("sourceUrl"), value: sourceUrl, onChange: (event) => setSourceUrl(event.target.value), style: { flex: 2, minWidth: 0 } }),
6383
7410
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: addSource }, t("addSource"))),
6384
7411
  el("div", { className: styles.rowTop },
6385
7412
  el("button", {
@@ -6401,8 +7428,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6401
7428
  (sourcesData.indexSources ?? []).map((s) =>
6402
7429
  editIndex !== null && editIndex.id === s.id
6403
7430
  ? el("div", { key: s.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
6404
- el("input", { type: "text", value: editIndex.name, onChange: (event) => setEditIndex({ ...editIndex, name: event.currentTarget.value }), style: { flex: 1, minWidth: 80 } }),
6405
- el("input", { type: "text", value: editIndex.url, onChange: (event) => setEditIndex({ ...editIndex, url: event.currentTarget.value }), style: { flex: 2, minWidth: 120 } }),
7431
+ el("input", { type: "text", value: editIndex.name, onChange: (event) => setEditIndex({ ...editIndex, name: event.target.value }), style: { flex: 1, minWidth: 80 } }),
7432
+ el("input", { type: "text", value: editIndex.url, onChange: (event) => setEditIndex({ ...editIndex, url: event.target.value }), style: { flex: 2, minWidth: 120 } }),
6406
7433
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: saveEditIndex }, t("saveSource")),
6407
7434
  el("button", { type: "button", className: styles.toggle, onClick: () => setEditIndex(null) }, t("cancelEdit")))
6408
7435
  : el("div", { key: s.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
@@ -6414,8 +7441,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6414
7441
  el("button", { type: "button", className: styles.toggle, title: t("editSourceHint"), disabled: sourcesBusy, onClick: () => setEditIndex({ id: s.id, name: s.name, url: s.url }) }, t("editSource")),
6415
7442
  el("button", { type: "button", className: styles.toggle, title: t("removeSourceHint"), disabled: sourcesBusy, onClick: () => sourcesAction({ action: "remove-index", id: s.id }) }, t("removeSource"))))),
6416
7443
  el("div", { className: styles.rowTop },
6417
- el("input", { type: "text", placeholder: t("sourceName"), value: indexSourceName, onChange: (event) => setIndexSourceName(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
6418
- el("input", { type: "text", placeholder: t("indexUrlPlaceholder"), value: indexSourceUrl, onChange: (event) => setIndexSourceUrl(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
7444
+ el("input", { type: "text", placeholder: t("sourceName"), value: indexSourceName, onChange: (event) => setIndexSourceName(event.target.value), style: { flex: 1, minWidth: 0 } }),
7445
+ el("input", { type: "text", placeholder: t("indexUrlPlaceholder"), value: indexSourceUrl, onChange: (event) => setIndexSourceUrl(event.target.value), style: { flex: 2, minWidth: 0 } }),
6419
7446
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: addIndexSource }, t("addIndexSource"))),
6420
7447
  el("div", { className: styles.rowTop },
6421
7448
  el("button", {
@@ -6433,8 +7460,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6433
7460
  (sourcesData.gitSources ?? []).map((s) =>
6434
7461
  editGit !== null && editGit.id === s.id
6435
7462
  ? el("div", { key: s.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
6436
- el("input", { type: "text", value: editGit.name, onChange: (event) => setEditGit({ ...editGit, name: event.currentTarget.value }), style: { flex: 1, minWidth: 80 } }),
6437
- el("input", { type: "text", value: editGit.urlTemplate, onChange: (event) => setEditGit({ ...editGit, urlTemplate: event.currentTarget.value }), style: { flex: 2, minWidth: 120 } }),
7463
+ el("input", { type: "text", value: editGit.name, onChange: (event) => setEditGit({ ...editGit, name: event.target.value }), style: { flex: 1, minWidth: 80 } }),
7464
+ el("input", { type: "text", value: editGit.urlTemplate, onChange: (event) => setEditGit({ ...editGit, urlTemplate: event.target.value }), style: { flex: 2, minWidth: 120 } }),
6438
7465
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: saveEditGit }, t("saveSource")),
6439
7466
  el("button", { type: "button", className: styles.toggle, onClick: () => setEditGit(null) }, t("cancelEdit")))
6440
7467
  : el("div", { key: s.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
@@ -6446,8 +7473,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6446
7473
  el("button", { type: "button", className: styles.toggle, title: t("editSourceHint"), disabled: sourcesBusy, onClick: () => setEditGit({ id: s.id, name: s.name, urlTemplate: s.urlTemplate }) }, t("editSource")),
6447
7474
  el("button", { type: "button", className: styles.toggle, title: t("removeSourceHint"), disabled: sourcesBusy, onClick: () => sourcesAction({ action: "remove-git", id: s.id }) }, t("removeSource"))))),
6448
7475
  el("div", { className: styles.rowTop },
6449
- el("input", { type: "text", placeholder: t("sourceName"), value: gitSourceName, onChange: (event) => setGitSourceName(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
6450
- el("input", { type: "text", placeholder: t("gitUrlPlaceholder"), value: gitSourceUrl, onChange: (event) => setGitSourceUrl(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
7476
+ el("input", { type: "text", placeholder: t("sourceName"), value: gitSourceName, onChange: (event) => setGitSourceName(event.target.value), style: { flex: 1, minWidth: 0 } }),
7477
+ el("input", { type: "text", placeholder: t("gitUrlPlaceholder"), value: gitSourceUrl, onChange: (event) => setGitSourceUrl(event.target.value), style: { flex: 2, minWidth: 0 } }),
6451
7478
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: addGitSource }, t("addGitSource"))),
6452
7479
  ]),
6453
7480
  accSection("archive", t("archiveSourcesTitle"), [
@@ -6455,8 +7482,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6455
7482
  (sourcesData.archiveSources ?? []).map((s) =>
6456
7483
  editArchive !== null && editArchive.id === s.id
6457
7484
  ? el("div", { key: s.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
6458
- el("input", { type: "text", value: editArchive.name, onChange: (event) => setEditArchive({ ...editArchive, name: event.currentTarget.value }), style: { flex: 1, minWidth: 80 } }),
6459
- el("input", { type: "text", value: editArchive.urlTemplate, onChange: (event) => setEditArchive({ ...editArchive, urlTemplate: event.currentTarget.value }), style: { flex: 2, minWidth: 120 } }),
7485
+ el("input", { type: "text", value: editArchive.name, onChange: (event) => setEditArchive({ ...editArchive, name: event.target.value }), style: { flex: 1, minWidth: 80 } }),
7486
+ el("input", { type: "text", value: editArchive.urlTemplate, onChange: (event) => setEditArchive({ ...editArchive, urlTemplate: event.target.value }), style: { flex: 2, minWidth: 120 } }),
6460
7487
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: saveEditArchive }, t("saveSource")),
6461
7488
  el("button", { type: "button", className: styles.toggle, onClick: () => setEditArchive(null) }, t("cancelEdit")))
6462
7489
  : el("div", { key: s.id, className: styles.rowTop, style: { justifyContent: "space-between" } },
@@ -6468,8 +7495,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6468
7495
  el("button", { type: "button", className: styles.toggle, title: t("editSourceHint"), disabled: sourcesBusy, onClick: () => setEditArchive({ id: s.id, name: s.name, urlTemplate: s.urlTemplate }) }, t("editSource")),
6469
7496
  el("button", { type: "button", className: styles.toggle, title: t("removeSourceHint"), disabled: sourcesBusy, onClick: () => sourcesAction({ action: "remove-archive", id: s.id }) }, t("removeSource"))))),
6470
7497
  el("div", { className: styles.rowTop },
6471
- el("input", { type: "text", placeholder: t("sourceName"), value: archiveSourceName, onChange: (event) => setArchiveSourceName(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
6472
- el("input", { type: "text", placeholder: t("archiveUrlPlaceholder"), value: archiveSourceUrl, onChange: (event) => setArchiveSourceUrl(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
7498
+ el("input", { type: "text", placeholder: t("sourceName"), value: archiveSourceName, onChange: (event) => setArchiveSourceName(event.target.value), style: { flex: 1, minWidth: 0 } }),
7499
+ el("input", { type: "text", placeholder: t("archiveUrlPlaceholder"), value: archiveSourceUrl, onChange: (event) => setArchiveSourceUrl(event.target.value), style: { flex: 2, minWidth: 0 } }),
6473
7500
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: addArchiveSource }, t("addArchiveSource"))),
6474
7501
  ]),
6475
7502
  accSection("search", t("searchSourcesTitle"), [
@@ -6486,14 +7513,14 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6486
7513
  ? el("button", { type: "button", className: styles.toggle, title: t("removeSourceHint"), disabled: sourcesBusy, onClick: () => sourcesAction({ action: "remove-search", id: s.id }) }, t("removeSource"))
6487
7514
  : null)),
6488
7515
  el("div", { className: styles.rowTop },
6489
- el("input", { type: "text", placeholder: t("sourceName"), value: searchSourceName, onChange: (event) => setSearchSourceName(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
6490
- el("input", { type: "text", placeholder: t("searchUrlPlaceholder"), value: searchSourceUrl, onChange: (event) => setSearchSourceUrl(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
7516
+ el("input", { type: "text", placeholder: t("sourceName"), value: searchSourceName, onChange: (event) => setSearchSourceName(event.target.value), style: { flex: 1, minWidth: 0 } }),
7517
+ el("input", { type: "text", placeholder: t("searchUrlPlaceholder"), value: searchSourceUrl, onChange: (event) => setSearchSourceUrl(event.target.value), style: { flex: 2, minWidth: 0 } }),
6491
7518
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy, onClick: addSearchSource }, t("addSearchSource"))),
6492
7519
  el("textarea", {
6493
7520
  rows: 2,
6494
7521
  placeholder: t("headersPlaceholder"),
6495
7522
  value: searchSourceHeaders,
6496
- onChange: (event) => setSearchSourceHeaders(event.currentTarget.value),
7523
+ onChange: (event) => setSearchSourceHeaders(event.target.value),
6497
7524
  style: { width: "100%", boxSizing: "border-box", font: "inherit", resize: "vertical" },
6498
7525
  }),
6499
7526
  ]),
@@ -6505,8 +7532,8 @@ const RESTART_FAILED_NOTE = "重启请求失败";
6505
7532
  ? t("giteeLoggedIn") + (giteeStatus.login || "—")
6506
7533
  : (giteeStatus !== null && giteeStatus.clientConfigured ? t("giteeSetupHint") : t("giteeLoginPrompt")))),
6507
7534
  el("div", { className: styles.rowTop },
6508
- el("input", { type: "text", placeholder: t("giteeClientId"), value: giteeClientId, onChange: (event) => setGiteeClientId(event.currentTarget.value), style: { flex: 1, minWidth: 0 } }),
6509
- el("input", { type: "password", placeholder: t("giteeClientSecret"), value: giteeClientSecret, onChange: (event) => setGiteeClientSecret(event.currentTarget.value), style: { flex: 2, minWidth: 0 } }),
7535
+ el("input", { type: "text", placeholder: t("giteeClientId"), value: giteeClientId, onChange: (event) => setGiteeClientId(event.target.value), style: { flex: 1, minWidth: 0 } }),
7536
+ el("input", { type: "password", placeholder: t("giteeClientSecret"), value: giteeClientSecret, onChange: (event) => setGiteeClientSecret(event.target.value), style: { flex: 2, minWidth: 0 } }),
6510
7537
  el("button", { type: "button", className: styles.toggle, disabled: sourcesBusy || !giteeClientId.trim() || !giteeClientSecret.trim(), onClick: saveGiteeSetup }, t("giteeSave"))),
6511
7538
  el("div", { className: styles.rowTop },
6512
7539
  el("button", { type: "button", className: styles.toggle, disabled: giteeStatus !== null && !giteeStatus.clientConfigured, onClick: startGiteeOauth }, t("giteeLoginBtn")),