diff --git a/.jules/palette.md b/.jules/palette.md index ea004e2d..f38da406 100644 --- a/.jules/palette.md +++ b/.jules/palette.md @@ -81,3 +81,7 @@ ## 2026-08-12 - Skip to Content Accessibility **Learning:** Screen reader and keyboard-only users experience significant friction when forced to navigate through repetitive header controls on every page load. **Action:** Keep a visible-on-focus skip link as the first interactive element, target a programmatically focusable main container, and give the focused link a high-contrast outline. + +## 2024-09-01 - Redundant UI Events During Async Loading +**Learning:** During asynchronous loading (e.g. `aria-busy="true"` on a clicked element), user clicks or keyboard interaction can repeatedly trigger event handlers unless explicitly prevented. CSS `pointer-events: none` only guards against mouse events, but keyboard (Enter, Space) or programmatic clicks still fire. +**Action:** When an element is put into a loading/busy state, not only apply visual styles but also explicitly add a guard in all interaction listeners (e.g., `if (el.getAttribute("aria-busy") === "true") return;`) to ensure the request is not duplicated. diff --git a/plan.md b/plan.md new file mode 100644 index 00000000..4e8f6f85 --- /dev/null +++ b/plan.md @@ -0,0 +1,10 @@ +1. **`scanner/dashboard/console.html` 수정 (로딩 상태 의미론적 스타일링 적용 및 이벤트 가드 추가)** + - `button:disabled` 및 `[aria-busy="true"]` 상태일 때 사용자가 시각적으로 비활성화되었음을 알 수 있도록 CSS에 `opacity: 0.7; pointer-events: none;` 속성을 추가합니다. + - `tr.scan` 요소에 대해 클릭 및 키보드(엔터/스페이스) 이벤트 리스너에서 `aria-busy` 속성을 확인하여, `aria-busy="true"`일 경우 추가적인 `detail()` 호출을 방지하는(즉, 중복 요청 방지) 가드 로직을 추가합니다. +2. **테스트 및 검증 실행** + - 코드를 수정한 후 의도대로 변경이 되었는지 브라우저나 도구를 이용해 시각적으로 또는 스크립트로 검증합니다. + - 프로젝트의 테스트 명령어가 있다면(예: `pnpm test` 등) 이를 실행하여 변경 사항이 다른 기능을 망가뜨리지 않았는지 확인합니다. +3. **Pre-commit 점검 단계 수행** + - `pre_commit_instructions` 도구를 호출하여 올바른 검증, 린트, 포매팅 등의 작업이 모두 완료되었는지 최종 점검합니다. +4. **변경 사항 제출(PR 생성)** + - 코드 리뷰를 통과하면 변경 사항을 제출합니다. 커밋 제목은 "🎨 Palette: Add semantic disabled styles and event guards for async loading states" 와 같은 형태를 따르며, 설명에는 '무엇을', '왜', '접근성(Accessibility)'에 대한 내용을 한국어로 작성합니다. diff --git a/scanner/dashboard/console.html b/scanner/dashboard/console.html index 7ec262af..c016b16c 100644 --- a/scanner/dashboard/console.html +++ b/scanner/dashboard/console.html @@ -45,6 +45,7 @@ .muted{color:var(--muted)} .err{color:var(--crit);font-weight:600} code{background:var(--bg);padding:1px 5px;border-radius:4px} + button:disabled, [aria-busy="true"] { opacity: 0.7; pointer-events: none; } .hidden{display:none} @@ -135,8 +136,8 @@

AppGuardrail Console

${esc(s.created_at)}${esc(s.repo||"—")}${esc((s.commit||"—").slice(0,10))} ${s.total}${pill(s.deploy_blocking,"var(--crit)")}${pill(s.new_blocking,"var(--high)")}`).join("")||'No scans. POST to /api/v1/scans from CI.'; document.querySelectorAll("tr.scan").forEach(tr=>{ - tr.onclick=()=>detail(tr.dataset.id,tr); - tr.addEventListener('keydown', e => { if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); detail(tr.dataset.id,tr); } }); + tr.onclick=()=>{ if(tr.getAttribute("aria-busy") === "true") return; detail(tr.dataset.id,tr); }; + tr.addEventListener('keydown', e => { if(tr.getAttribute('aria-busy') === 'true') return; if(e.key === 'Enter' || e.key === ' ') { e.preventDefault(); detail(tr.dataset.id,tr); } }); }); }catch(e){ $("#msg").classList.remove("hidden");$("#app").classList.add("hidden"); $("#msg").innerHTML=`${esc(e.message)}`; }