fix(b09): 묶음 탭 이동 빨강 = 설계내역서 규칙 — 빨강 하나 · 줄 · 카드 누름 · 방향키 · Esc = 빨강 풂 · 누른 줄만 고름 (248-1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SE9MxeXTu2uaDZ55BJowAY
This commit is contained in:
2026-10-10 23:28:49 +09:00
co-authored by Claude Opus 5.5
parent d449c38598
commit 1cd8668d8a
6 changed files with 150 additions and 11 deletions
@@ -6,6 +6,8 @@
* 카드 = 탭 표 줄(`TabTable.rows`)을 호표(줄 키 앞머리)마다 묶음 — 값 · 풍선 · 누름 이동은 표와 같음
* 머리 줄 = 묶음 첫 줄 · 합계 줄 = 키 끝 `|합계` · `|총계` · 그 밖 = 몸 줄(표 줄 번호 그대로)
* 목록표에서 온 호표(181-2) = 박스 붉은 외곽선 · 표 보기 = 그 호표 줄 구간 전체 붉은 외곽선
* 248-1 빨강 하나(공용 고름 강조 안 붙임 · 스크롤만) · 줄 · 카드 누름 · 방향키 · Esc = 빨강 풂 ·
* 누른 줄만 고름(is-focus) · Esc = 고름도 풂
* 보기 · 가로 열 수 기억 = `b_page_state`(`b09-group-view` · `b09-group-cols` · 탭마다) · 못 읽으면 카드 · 2
* 보기 단추 · 가로 열 수(202-1) = 페이지 보조 탭 줄 오른쪽(`setTools`) · 안내 글 = 표 · 카드 판 맨 아래
* 안내 글 길면 한 줄 줄임표 + 공용 호버로 전문(200-1)
@@ -17,6 +19,7 @@ import { attachHoverInfo } from "@ui/ui_template_hover_info";
import {
clampColumns,
createGroupCards,
unpickCards,
type GroupCard,
type GroupFoot,
type GroupLine,
@@ -185,6 +188,21 @@ export function lineHint(hint: HTMLElement | null): HTMLElement | null {
return hint;
}
/** 248-1 지금 그린 묶음 판 빨강 풀기(esc = 고름도) — 방향키 · Esc 가 부름 */
let live: { root: HTMLElement; unpick: (esc: boolean) => void } | null = null;
let keysOn = false;
const UNPICK_KEYS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Escape"]);
function listenKeys(): void {
if (keysOn) return;
keysOn = true;
document.addEventListener("keydown", (event) => {
if (!live?.root.isConnected || !UNPICK_KEYS.has(event.key)) return;
if ((event.target as Element | null)?.closest?.("input, textarea, select")) return;
live.unpick(event.key === "Escape");
});
}
/** 이 화면 안 보기(기억이 안 돼도 바꾼 보기 유지) */
const chosen = new Map<TabId, GroupView>();
@@ -227,11 +245,28 @@ export function renderGroupPanel(o: {
placeTools: o.setTools, // 202-1 가로 열 수 · 보기 단추 = 보조 탭 줄 오른쪽
});
cards.dataset.b09Table = o.tab;
live = { root: cards, unpick: (esc) => unpickCards(cards, esc) };
listenKeys();
return [cards, ...(lead ? [lead] : [])];
}
o.setTools([toggle]);
const range = pickRange(o.spec.rows, o.focus);
const table = o.makeTable((i) => pickClass(range, i));
if (at >= 0) requestAnimationFrame(() => table.focusRow(at));
let range = pickRange(o.spec.rows, o.focus);
let chose = -1; // 누른 줄(다시 그려도 유지)
const table = o.makeTable((i) => (i === chose ? "is-focus" : pickClass(range, i)));
// 줄 class 만 고침(다시 그리지 않음 — 더블클릭 입력 그대로)
const mark = (row: number): void => {
range = null;
chose = row;
for (const tr of table.root.querySelectorAll("tr.is-pick, tr.is-focus"))
tr.classList.remove("is-pick", "is-pick-top", "is-pick-end", "is-focus");
table.root.querySelector(`tr[data-i="${row}"]`)?.classList.add("is-focus");
};
table.root.addEventListener("click", (event) => {
const tr = (event.target as Element).closest<HTMLElement>("tr[data-i]");
mark(tr ? Number(tr.dataset.i) : chose);
});
live = { root: table.root, unpick: (esc) => mark(esc ? -1 : chose) };
listenKeys();
if (at >= 0) requestAnimationFrame(() => table.focusRow(at, false)); // 스크롤만(표시 = 빨강)
return [table.root, ...(lead ? [lead] : [])];
}