dsh-session-manager 0.4.7 → 0.4.10

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
@@ -192,6 +192,7 @@ window.__ModuleLoader__.load({
192
192
  ".sm-moveItemMeta{color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:16px}.sm-panelModal{width:760px;max-width:92vw}.sm-panelModal .sm-rowActions{flex-wrap:wrap;justify-content:flex-end}.sm-headerMenu{position:absolute;right:0;top:100%;z-index:50;display:flex;flex-direction:column;min-width:160px;padding:4px;background:var(--dsw-alias-surface-l1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;box-shadow:0 4px 16px rgba(0,0,0,.12)}.sm-headerMenuItem{display:flex;align-items:center;width:100%;text-align:left;background:0 0;border:0;border-radius:6px;color:var(--dsw-alias-label-primary);cursor:pointer;padding:6px 10px;font-size:13px;line-height:18px}.sm-headerMenuItem:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-headerMenuItemDanger{color:var(--dsw-alias-state-error-primary)}.sm-headerBtn{display:inline-flex;align-items:center;justify-content:center;gap:6px;min-height:32px;height:auto;color:var(--dsw-alias-label-secondary);background:var(--dsw-alias-fill-l2);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;padding:4px 12px;font-size:13px;line-height:18px;white-space:nowrap;transition:background .15s,color .15s,border-color .15s}.sm-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtn:focus-visible{outline:2px solid var(--dsw-alias-accent-primary);outline-offset:1px}.sm-headerBtn svg{width:16px;height:16px}.sm-migratePanel{display:flex;flex-direction:column;gap:14px;min-width:480px}.sm-migrateRow{display:flex;flex-direction:column;gap:8px}.sm-migrateRow>label{color:var(--dsw-alias-label-secondary);font-size:12px;line-height:16px;font-weight:500}.sm-migrateChips{display:flex;flex-wrap:wrap;gap:6px}.sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2);background:transparent;cursor:pointer;font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary);white-space:nowrap}.sm-migrateChip:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-migrateChip:disabled{opacity:.5;cursor:not-allowed}.sm-migrateChipOn{background:var(--dsw-alias-accent-primary);color:#fff;border-color:var(--dsw-alias-accent-primary)}.sm-migrateEmpty{color:var(--dsw-alias-label-tertiary);font-size:12px;line-height:18px}.sm-migratePreview{padding:10px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary);font-size:13px;line-height:18px}.sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background:rgba(217,119,6,.08);color:var(--dsw-alias-label-secondary);font-size:13px;line-height:18px}.sm-migrateList{max-height:200px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:8px}.sm-headerBtn:hover{background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l1)}.sm-headerBtnDanger{background:var(--dsw-alias-state-error-tertiary);color:var(--dsw-alias-state-error-primary);border-color:var(--dsw-alias-state-error-primary);font-weight:500}.sm-headerBtnDanger:hover:not(:disabled){background:#d92d20!important;color:#fff!important;border-color:#d92d20!important}.sm-header{display:inline-flex;align-items:center;gap:6px}.sm-headerBtnActive{background:var(--dsw-alias-fill-l3);color:var(--dsw-alias-label-primary)}",
193
193
  ".sm-panelDialog{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:560px;max-width:calc(100vw - 32px);max-height:540px;height:auto;box-sizing:border-box;display:flex;flex-direction:column;gap:0;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);overflow:hidden;z-index:9999;opacity:1}.sm-panelDialog .sm-nativeDialogHeader{padding:10px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none}.sm-panelDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-panelDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;gap:6px;margin-top:0}.sm-panelDialog .sm-panel{display:flex;flex-direction:column;gap:6px;min-height:0;flex:1 1 auto;overflow:auto;padding:8px 12px}.sm-panelDialog .sm-list{display:flex;flex-direction:column;gap:2px;flex:1 1 auto;min-height:0;overflow:auto}.sm-panelDialog .sm-row{display:flex;align-items:center;gap:8px;padding:0 10px;height:36px;min-height:36px;border:1px solid transparent;border-radius:8px;background:transparent}.sm-panelDialog .sm-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.sm-panelDialog .sm-rowCurrent{background:var(--dsw-alias-fill-l2);border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-rowMain{display:flex;align-items:stretch!important;gap:8px;min-width:0;flex:1 1 auto;overflow:hidden;text-align:left!important}.sm-panelDialog .sm-rowTitle{min-width:0;flex:1 1 0%;width:100%;align-self:stretch;display:flex;align-items:center;justify-content:flex-start;gap:6px;font-size:12px;line-height:16px;overflow:hidden;text-align:left!important;direction:ltr!important}.sm-confirmDialog{z-index:10000}.sm-confirmDialog.sm-nativeDialogLayer{position:fixed;left:calc(50vw + 184px);top:calc(50vh - 90px);width:300px;max-width:calc(100vw - 32px);max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-confirmDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;box-sizing:border-box;background:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.25);padding:0;overflow:hidden;animation:sm-confirmPop .14s ease-out}.sm-confirmDialog .sm-nativeDialogHeader{padding:12px 16px;border-bottom:1px solid var(--dsw-alias-border-l2,#eee)}.sm-confirmDialog .sm-nativeDialogTitle{margin:0;font-size:14px;line-height:20px;font-weight:600}.sm-confirmDialog .sm-nativeDialogBody{padding:10px 16px;display:flex;flex-direction:column;gap:8px;min-height:0}.sm-confirmDialog .sm-nativeDialogDescription{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary);white-space:pre-wrap}.sm-confirmDialog .sm-warn{padding:6px 10px;font-size:12px;line-height:16px;margin-top:0}.sm-confirmDialog .sm-nativeDialogFooter{padding:10px 16px;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:0}.sm-confirmDialog .sm-nativeDialogButton{min-height:28px;padding:3px 14px;font-size:12px;line-height:20px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;cursor:pointer;background:transparent;color:var(--dsw-alias-label-primary)}.sm-confirmDialog .sm-nativeDialogCancel:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.sm-confirmDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-state-error-primary);color:#fff;border-color:var(--dsw-alias-state-error-primary)}.sm-confirmDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}@keyframes sm-confirmPop{from{opacity:0;transform:translateY(-4px)}to{opacity:1;transform:translateY(0)}}.sm-confirmDialog.sm-nativeDialog{padding:0;width:auto;max-width:none}.sm-panelDialog .sm-rowTitle>span:first-child{display:block;min-width:0;text-overflow:ellipsis;white-space:nowrap;overflow:hidden;flex:1 1 auto;width:auto;text-align:left!important;direction:ltr!important;unicode-bidi:plaintext}.sm-panelDialog .sm-rowMeta{display:none}.sm-panelDialog .sm-rowActions{display:flex;gap:2px;flex:none;justify-content:flex-end}.sm-panelDialog .sm-rowActions .sm-rowBtn{min-height:22px;height:22px;padding:0 8px;font-size:11px;line-height:14px;border-radius:6px;border:1px solid transparent;background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-rowActions .sm-rowBtn:hover{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-rowActions .sm-rowBtnDanger{color:var(--dsw-alias-state-error-primary)}.sm-panelDialog .sm-filter{flex:none;padding:6px 12px 4px;gap:4px;display:flex;flex-wrap:wrap}.sm-panelDialog .sm-filterBtn{min-height:24px;padding:2px 10px;font-size:11px;line-height:16px;border-radius:4px;border:1px solid var(--dsw-alias-border-l2);background:transparent;color:var(--dsw-alias-label-secondary);cursor:pointer}.sm-panelDialog .sm-filterBtn.sm-on{background:var(--dsw-alias-fill-l2);color:var(--dsw-alias-label-primary)}.sm-panelDialog .sm-badge{font-size:10px;line-height:14px;padding:0 5px;border-radius:6px;flex:none}.sm-panelDialog .sm-error{padding:6px 10px;margin:0;font-size:12px;line-height:16px}.sm-panelDialog .sm-nativeDialogButton{min-height:28px;padding:3px 12px;font-size:12px;line-height:18px;border:1px solid var(--dsw-alias-border-l2);border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);cursor:pointer}.sm-panelDialog .sm-nativeDialogCancel{background:transparent}.sm-panelDialog .sm-nativeDialogGhost{background:transparent;border-color:var(--dsw-alias-border-l2)}.sm-panelDialog .sm-nativeDialogConfirm{background:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-panelDialog .sm-empty{padding:18px 0;font-size:12px;color:var(--dsw-alias-label-tertiary);text-align:center}.sm-panelDialog .sm-footError{font-size:11px;color:var(--dsw-alias-state-error-primary);margin-right:auto;line-height:20px}.sm-panelDialog .sm-moveLabel{font-size:12px;line-height:18px;color:var(--dsw-alias-label-secondary)}.sm-migrateDialog .sm-nativeDialogFooter{flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-top:14px}.sm-migrateDialog .sm-migrateChip{cursor:pointer;pointer-events:auto;position:relative;z-index:1}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog.sm-nativeDialogLayer{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:none;display:block;align-items:initial;justify-content:initial;box-sizing:border-box;background:transparent;border:0;box-shadow:none;padding:0;inset:auto;z-index:10001}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialogBackdrop{display:none}.sm-migrateDialog.sm-nativeDialogLayer .sm-nativeDialog{position:relative;left:auto;top:auto;transform:none;width:auto;max-height:min(70vh,520px);box-sizing:border-box;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;padding:0;display:flex;flex-direction:column;gap:0;opacity:1}.sm-migrateDialog{position:fixed;left:50vw;top:auto;bottom:48px;transform:translateX(-50%);width:min(560px,calc(100vw - 32px));max-height:min(70vh,520px);box-sizing:border-box;display:flex;flex-direction:column;gap:0;background-color:#ffffff;background-color:var(--dsw-alias-surface-l1,#fff);color:var(--dsw-alias-label-primary,#111);border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:12px;box-shadow:0 16px 48px rgba(0,0,0,.45);overflow:hidden;opacity:1;z-index:10001;backdrop-filter:none;-webkit-backdrop-filter:none}.sm-migrateDialog .sm-nativeDialogHeader{padding:12px 18px;border-bottom:1px solid #eee;border-bottom:1px solid var(--dsw-alias-border-l2,#eee);flex:none;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogTitle{margin:0;font-size:15px;line-height:22px;font-weight:600;color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-migratePanel{padding:12px 18px;display:flex;flex-direction:column;gap:14px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogBody{padding:0;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-nativeDialogFooter{padding:12px 18px;border-top:1px solid #eee;border-top:1px solid var(--dsw-alias-border-l2,#eee);flex:none;flex-wrap:wrap;justify-content:flex-end;gap:8px;background-color:#fff;background-color:var(--dsw-alias-surface-l1,#fff)}.sm-migrateDialog .sm-migrateRow>label{color:#444;color:var(--dsw-alias-label-secondary,#444)}.sm-migrateDialog .sm-migrateChip{padding:6px 12px;border-radius:999px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px;cursor:pointer;pointer-events:auto;position:relative;z-index:1;white-space:nowrap}.sm-migrateDialog .sm-migrateChip:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-migrateChipOn{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-migrateChip.sm-migrateChipOn:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-hover,#4576f0);filter:none}.sm-migrateDialog .sm-migrateChipOn:hover:not(:disabled){filter:brightness(.92)}.sm-migrateDialog .sm-migrateChip[disabled]{opacity:.5;cursor:not-allowed}.sm-migrateDialog .sm-migrateEmpty{color:#888;color:var(--dsw-alias-label-tertiary,#888)}.sm-migrateDialog .sm-migratePreview{padding:10px 12px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background-color:#f5f5f5;background-color:var(--dsw-alias-fill-l2,#f5f5f5);color:#111;color:var(--dsw-alias-label-primary,#111);font-size:13px;line-height:18px}.sm-migrateDialog .sm-migrateWarn{padding:10px 12px;border:1px solid rgba(217,119,6,.25);border-radius:8px;background-color:rgba(217,119,6,.08);color:#444;color:var(--dsw-alias-label-secondary,#444);font-size:13px;line-height:18px}.sm-migrateDialog .sm-nativeDialogButton{min-height:32px;padding:4px 14px;font-size:13px;line-height:20px;border:1px solid #d4d4d8;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;cursor:pointer;background-color:#fff;background-color:var(--dsw-alias-fill-l2,#fff);color:#111;color:var(--dsw-alias-label-primary,#111)}.sm-migrateDialog .sm-nativeDialogButton:hover:not(:disabled){background-color:#e8e8e8;background-color:var(--dsw-alias-interactive-bg-hover,#e8e8e8)}.sm-migrateDialog .sm-nativeDialogConfirm{background-color:#356ae6;background-color:var(--dsw-alias-accent-primary,#356ae6);color:#fff;border-color:#356ae6;border-color:var(--dsw-alias-accent-primary,#356ae6)}.sm-migrateDialog .sm-nativeDialogConfirm:hover:not(:disabled){filter:brightness(.94)}",
194
194
  ".sm-panelDialog .sm-nativeDialogHeader{display:flex;align-items:center;gap:8px}.sm-panelHeaderClose{min-height:26px;padding:2px 9px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:7px;background:transparent;color:var(--dsw-alias-label-secondary,#555);cursor:pointer;font-size:12px;line-height:18px}.sm-panelHeaderClose:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover,#eee);color:var(--dsw-alias-label-primary,#111)}.sm-panelHeaderClose:disabled{opacity:.5;cursor:not-allowed}.sm-migrateCurrent{padding:8px 10px;border:1px solid var(--dsw-alias-border-l2,#ddd);border-radius:8px;background:var(--dsw-alias-fill-l2,#f5f5f5);font-size:13px;line-height:18px;color:var(--dsw-alias-label-primary,#111)}.sm-migrateConfirmLayer.sm-confirmDialog.sm-nativeDialogLayer{width:300px;inset:calc(50vh - 90px) auto auto calc(50vw + 308px)}.sm-migrateConfirmLayer.sm-confirmDialog.sm-nativeDialogLayer .sm-migrateDialog{position:relative;left:auto;top:auto;bottom:auto;transform:none;width:100%;max-width:none;max-height:min(70vh,520px);z-index:auto}.sm-migrateConfirmLayer .sm-migratePanel{min-width:0}.sm-migrateConfirmLayer .sm-nativeDialogButton.sm-nativeDialogConfirm:hover:not(:disabled){background-color:#4576f0;background-color:var(--dsw-alias-accent-primary-hover,#4576f0);color:#fff;border-color:#4576f0;border-color:var(--dsw-alias-accent-primary-hover,#4576f0);filter:none}",
195
+
195
196
  // Theme colors are scoped to the plugin dialogs. DSH exposes its
196
197
  // actual theme through `body[data-ds-dark-theme]` and color-scheme;
197
198
  // keeping these values local avoids changing DSH's global variables.
@@ -354,6 +355,33 @@ window.__ModuleLoader__.load({
354
355
  const warning = props.warning || "";
355
356
  const confirmLabel = props.confirmLabel || "确认";
356
357
  const cancelLabel = props.cancelLabel || "取消";
358
+ // Esc is handled at window level (issue #7): the layer-bound
359
+ // onKeyDown was unreachable when the row button that opened
360
+ // the dialog stayed focused. ConfirmDialog's portal to body
361
+ // makes it a separate subtree from the panel section, so
362
+ // keydown never bubbles through it. Enter was previously on
363
+ // the layer too, but that races with the focused Cancel /
364
+ // Confirm button's native Enter handler -- removing the layer
365
+ // Enter leaves Enter handling purely to the focused button.
366
+ const escStateRef = useRef({});
367
+ escStateRef.current = { busy, onCancel };
368
+ useEffect(() => {
369
+ if (!open) return;
370
+ const handler = (e) => {
371
+ if (e.key !== "Escape") return;
372
+ if (e.defaultPrevented) return;
373
+ const s = escStateRef.current;
374
+ if (s.busy) return;
375
+ e.preventDefault();
376
+ e.stopPropagation();
377
+ s.onCancel();
378
+ // See SessionManagerPanel for the rationale.
379
+ const ae = document.activeElement;
380
+ if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
381
+ };
382
+ window.addEventListener("keydown", handler);
383
+ return () => window.removeEventListener("keydown", handler);
384
+ }, [open]);
357
385
  const R = (typeof window !== "undefined") ? window.__smReact : null;
358
386
  const createPortal = (R && typeof R.createPortal === "function") ? R.createPortal : null;
359
387
  const body = (typeof document === "undefined") ? null : document.body;
@@ -364,23 +392,14 @@ window.__ModuleLoader__.load({
364
392
  style: props.anchor && typeof props.anchor.top === "number"
365
393
  ? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
366
394
  : null,
367
- role: "presentation",
368
- onKeyDown: (e) => {
369
- if (e.key === "Escape" && !busy) { e.preventDefault(); onCancel(); }
370
- else if (e.key === "Enter" && !busy) { e.preventDefault(); onConfirm(); }
371
- },
372
- tabIndex: -1
395
+ role: "presentation"
373
396
  },
374
- h("div", {
375
- className: "sm-nativeDialogBackdrop",
376
- onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); }
377
- }),
397
+
378
398
  h("section", {
379
399
  className: "sm-nativeDialog",
380
400
  role: "dialog",
381
401
  "aria-modal": "true",
382
- "aria-labelledby": "sm-confirm-dialog-title",
383
- ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
402
+ "aria-labelledby": "sm-confirm-dialog-title"
384
403
  },
385
404
  h("div", { className: "sm-nativeDialogHeader" },
386
405
  h("h2", { id: "sm-confirm-dialog-title", className: "sm-nativeDialogTitle" }, title)
@@ -428,6 +447,33 @@ window.__ModuleLoader__.load({
428
447
  const cancelLabel = props.cancelLabel || t("confirm.cancel");
429
448
  const error = props.error || "";
430
449
  const [selectedWorkspaceId, setSelectedWorkspaceId] = useState("");
450
+ // Esc handled at window level (issue #7) -- Enter stays on the
451
+ // layer because (a) it gates on selectedWorkspaceId which is set
452
+ // only by clicking a listbox item, and (b) racing Enter on the
453
+ // Cancel / Confirm buttons is benign since the layer Enter
454
+ // handler only fires when focus is inside the dialog subtree,
455
+ // which is exactly when the focused button's native Enter has
456
+ // already triggered its own click -- and in MoveDialog that
457
+ // click just toggles selection, never cancels or confirms.
458
+ const escStateRef = useRef({});
459
+ escStateRef.current = { busy, onCancel };
460
+ useEffect(() => {
461
+ if (!open) return;
462
+ const handler = (e) => {
463
+ if (e.key !== "Escape") return;
464
+ if (e.defaultPrevented) return;
465
+ const s = escStateRef.current;
466
+ if (s.busy) return;
467
+ e.preventDefault();
468
+ e.stopPropagation();
469
+ s.onCancel();
470
+ // See SessionManagerPanel for the rationale.
471
+ const ae = document.activeElement;
472
+ if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
473
+ };
474
+ window.addEventListener("keydown", handler);
475
+ return () => window.removeEventListener("keydown", handler);
476
+ }, [open]);
431
477
  if (!open) return null;
432
478
 
433
479
  const list = (Array.isArray(props.workspaces) ? props.workspaces : [])
@@ -458,10 +504,8 @@ window.__ModuleLoader__.load({
458
504
  // Single cancel button (footer) + ESC handler. No "x" close button so the
459
505
  // user only sees one cancel entry point.
460
506
  const onKeyDown = (e) => {
461
- if (e.key === "Escape" && !busy) {
462
- e.preventDefault();
463
- onCancel();
464
- } else if (e.key === "Enter" && !busy && selectedWorkspaceId && list.length > 0) {
507
+ // Enter only -- Esc moved to a window-level listener (see above).
508
+ if (e.key === "Enter" && !busy && selectedWorkspaceId && list.length > 0) {
465
509
  e.preventDefault();
466
510
  onConfirm(selectedWorkspaceId);
467
511
  }
@@ -472,19 +516,14 @@ window.__ModuleLoader__.load({
472
516
  ? { position: "fixed", top: props.anchor.top + "px", left: props.anchor.left + "px", zIndex: 10001 }
473
517
  : null,
474
518
  role: "presentation",
475
- onKeyDown,
476
- tabIndex: -1
519
+ onKeyDown
477
520
  },
478
- h("div", {
479
- className: "sm-nativeDialogBackdrop",
480
- onMouseDown: (e) => { if (e.target === e.currentTarget && !busy) onCancel(); }
481
- }),
521
+
482
522
  h("section", {
483
523
  className: "sm-nativeDialog",
484
524
  role: "dialog",
485
525
  "aria-modal": "true",
486
- "aria-labelledby": "sm-move-dialog-title",
487
- ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
526
+ "aria-labelledby": "sm-move-dialog-title"
488
527
  },
489
528
  h("div", { className: "sm-nativeDialogHeader" },
490
529
  h("h2", { id: "sm-move-dialog-title", className: "sm-nativeDialogTitle" }, title)
@@ -543,6 +582,30 @@ window.__ModuleLoader__.load({
543
582
  });
544
583
  return () => { alive = false; };
545
584
  }, [open, sessionId]);
585
+ // Esc handled at window level (issue #7). MigratePresetDialog is
586
+ // a sibling of the panel layer in the panel's render tree, so
587
+ // the layer-bound onKeyDown (which the dialog relied on for
588
+ // Esc) was dead code -- focus never reached the dialog's own
589
+ // subtree because the row Migrate button stayed focused.
590
+ const escStateRef = useRef({});
591
+ escStateRef.current = { busy, onCancel };
592
+ useEffect(() => {
593
+ if (!open) return;
594
+ const handler = (e) => {
595
+ if (e.key !== "Escape") return;
596
+ if (e.defaultPrevented) return;
597
+ const s = escStateRef.current;
598
+ if (s.busy) return;
599
+ e.preventDefault();
600
+ e.stopPropagation();
601
+ s.onCancel();
602
+ // See SessionManagerPanel for the rationale.
603
+ const ae = document.activeElement;
604
+ if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
605
+ };
606
+ window.addEventListener("keydown", handler);
607
+ return () => window.removeEventListener("keydown", handler);
608
+ }, [open]);
546
609
  if (!open) return null;
547
610
 
548
611
  const currentPreset = (scan && scan.ok && scan.row && (scan.row.finalPreset || scan.row.headerPreset)) || fallbackPreset || "—";
@@ -578,13 +641,9 @@ window.__ModuleLoader__.load({
578
641
  );
579
642
  return h("div", {
580
643
  className: "sm-nativeDialogLayer sm-confirmDialog sm-migrateConfirmLayer",
581
- role: "presentation",
582
- onKeyDown: (event) => { if (event.key === "Escape" && !busy) { event.preventDefault(); onCancel(); } }
644
+ role: "presentation"
583
645
  },
584
- h("div", {
585
- className: "sm-nativeDialogBackdrop",
586
- onMouseDown: (event) => { if (event.target === event.currentTarget && !busy) onCancel(); }
587
- }),
646
+
588
647
  h("section", {
589
648
  className: "sm-nativeDialog sm-migrateDialog",
590
649
  role: "dialog",
@@ -746,30 +805,48 @@ window.__ModuleLoader__.load({
746
805
  setMigrateFor(null);
747
806
  onClose();
748
807
  };
749
- // Single cancel button in the panel footer (no native "x").
750
- const onPanelKey = (e) => {
751
- if (e.key === "Escape" && busyId === null) {
752
- if (confirmFor || moveFor || migrateFor) return;
808
+ // Esc is handled at window level (issue #7): the original layer-
809
+ // bound onKeyDown was unreachable when focus stayed on the sidebar
810
+ // toggle button that opened the panel -- that button is a sibling
811
+ // of the panel layer, so keydown never bubbles into the panel
812
+ // subtree. The state ref lets the listener see the latest values
813
+ // without re-subscribing on every render. The panel listener
814
+ // returns early when any inner dialog is open so the child
815
+ // dialog's own window listener gets the first shot at Esc.
816
+ const escStateRef = useRef({});
817
+ escStateRef.current = { busyId, confirmFor, moveFor, migrateFor, onPanelClose };
818
+ useEffect(() => {
819
+ if (!open) return;
820
+ const handler = (e) => {
821
+ if (e.key !== "Escape") return;
822
+ if (e.defaultPrevented) return;
823
+ const s = escStateRef.current;
824
+ if (s.busyId !== null) return;
825
+ if (s.confirmFor || s.moveFor || s.migrateFor) return;
753
826
  e.preventDefault();
754
- onPanelClose();
755
- }
756
- };
827
+ e.stopPropagation();
828
+ s.onPanelClose();
829
+ // Esc after a keyboard-driven close leaves the originally-
830
+ // focused trigger button (sidebar toggle / row button) with
831
+ // :focus-visible still active -- blur it so no focus ring
832
+ // lingers after the panel closes. Mouse-driven closes are
833
+ // not affected: :focus-visible only activates for keyboard
834
+ // focus, so the ring never shows in that path.
835
+ const ae = document.activeElement;
836
+ if (ae && typeof ae.blur === "function" && ae !== document.body) ae.blur();
837
+ };
838
+ window.addEventListener("keydown", handler);
839
+ return () => window.removeEventListener("keydown", handler);
840
+ }, [open]);
757
841
  const panelModal = h("div", {
758
842
  className: "sm-nativeDialogLayer",
759
- role: "presentation",
760
- onKeyDown: onPanelKey,
761
- tabIndex: -1
843
+ role: "presentation"
762
844
  },
763
- h("div", {
764
- className: "sm-nativeDialogBackdrop",
765
- onMouseDown: (e) => { if (e.target === e.currentTarget && busyId === null) onPanelClose(); }
766
- }),
767
845
  h("section", {
768
846
  className: "sm-nativeDialog sm-panelDialog",
769
847
  role: "dialog",
770
848
  "aria-modal": "true",
771
- "aria-labelledby": "sm-panel-dialog-title",
772
- ref: (el) => { if (el && typeof el.focus === "function") el.focus(); }
849
+ "aria-labelledby": "sm-panel-dialog-title"
773
850
  },
774
851
  h("div", { className: "sm-nativeDialogHeader" },
775
852
  h("h2", { id: "sm-panel-dialog-title", className: "sm-nativeDialogTitle" }, t("panel.title")),