fix: address review findings — icons, a11y, quiz state, SW, scores

- C-1: move saveScore() out of resultScore getter into _onAllAnswered
       to prevent repeated localStorage writes on every reactive access
- C-2: $watch('search') → createIcons() so sidebar Lucide icons are
       re-converted after x-for re-render on search filter/clear
- T-8: replace x-show Lucide <i> chevrons with inline SVG in
       .concept-chevron-wrap; Alpine x-show was orphaned after
       createIcons() replaced <i> with <svg>, causing stale display
- H-1: remove quizState={} from stepGoBack() Step3 branch so going
       back no longer silently destroys quiz answers
- H-2: add role=button tabindex=0 and Enter/Space keydown handlers to
       sidebar-item divs for keyboard navigation
- M-3: move skipWaiting() inside Promise.all in install waitUntil
- M-4: return 503 Response in SW fetch catch when cache also unavailable
- M-6: call saveScore() in nextWeakDrillUnit() so weak-drill results
       persist to localStorage the same as regular quiz
- UI:  fix .unit-cat-badge vertical misalignment in flex badge-row
       (was display:inline-block margin-bottom:6px, now inline-flex)
- SW:  bump cache version to v19 to retire old worker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
posimai 2026-04-19 21:36:27 +09:00
parent 7647d8137c
commit 2255ec55c8
3 changed files with 25 additions and 12 deletions

View File

@ -153,7 +153,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
/* Unit header */
.unit-header{margin-bottom:18px}
.unit-meta{min-width:0}
.unit-cat-badge{font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--accent);background:var(--accent-dim);border:1px solid var(--accent-border);border-radius:20px;padding:3px 9px;display:inline-block;margin-bottom:6px}
.unit-cat-badge{font-size:10px;font-weight:600;letter-spacing:.07em;text-transform:uppercase;color:var(--accent);background:var(--accent-dim);border:1px solid var(--accent-border);border-radius:20px;padding:3px 9px;display:inline-flex;align-items:center}
.unit-title{font-size:19px;font-weight:600;letter-spacing:-.01em;line-height:1.3}
/* Concept text */
@ -369,6 +369,7 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
/* Concept expand */
.concept-expand-btn{display:inline-flex;align-items:center;gap:5px;font-size:11px;color:var(--accent);background:none;border:none;cursor:pointer;padding:4px 0 0;font-family:'Geist',sans-serif;font-weight:500}
.concept-expand-btn:hover{opacity:.75}
.concept-chevron-wrap{display:inline-flex;min-width:11px;height:11px;align-items:center;justify-content:center;flex-shrink:0}
</style>
</head>
<body>
@ -425,8 +426,11 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div>
<template x-for="u in cat.units" :key="u.id">
<div class="sidebar-item"
role="button" tabindex="0"
:class="{active: currentUnit && currentUnit.id===u.id, done: isDone(u.id)}"
@click="openUnit(u); sidebarOpen=false">
@click="openUnit(u); sidebarOpen=false"
@keydown.enter.prevent="openUnit(u); sidebarOpen=false"
@keydown.space.prevent="openUnit(u); sidebarOpen=false">
<span class="item-num" x-text="u.num"></span>
<span class="item-title" x-text="u.title"></span>
<span class="score-chip" x-show="bestScore(u.id) && !hasWrong(u.id)" :class="scoreChipCls(u.id)" x-text="bestScore(u.id)"></span>
@ -607,9 +611,13 @@ header{display:flex;align-items:center;justify-content:space-between;padding:0 1
</div>
<div class="concept-text" x-html="safeHtml(conceptPreview)"></div>
<div class="concept-text" x-show="conceptExpanded" x-html="safeHtml(conceptRest)"></div>
<button class="concept-expand-btn" x-show="conceptRest"
<button type="button" class="concept-expand-btn" x-show="conceptRest"
:aria-expanded="conceptExpanded ? 'true' : 'false'"
@click="conceptExpanded = !conceptExpanded">
<i :data-lucide="conceptExpanded ? 'chevron-up' : 'chevron-down'" style="width:11px;height:11px"></i>
<span class="concept-chevron-wrap" aria-hidden="true">
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x-show="!conceptExpanded"><polyline points="6 9 12 15 18 9"></polyline></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" x-show="conceptExpanded"><polyline points="18 15 12 9 6 15"></polyline></svg>
</span>
<span x-text="conceptExpanded ? '閉じる' : 'もっと詳しく'"></span>
</button>
</div>

View File

@ -82,6 +82,9 @@ document.addEventListener('alpine:init', () => {
}
this.syncUnitToUrl();
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
this.$watch('search', ()=>{
this.$nextTick(()=>{ if(window.lucide) lucide.createIcons(); });
});
if('serviceWorker' in navigator){
navigator.serviceWorker.register('/sw.js').catch(()=>{});
}
@ -202,6 +205,7 @@ document.addEventListener('alpine:init', () => {
const uid=this.currentUnit.id;
const total=this.currentUnit.quiz?.length||0;
const correct=Object.values(this.quizState).filter(s=>s?.correct).length;
this.saveScore(uid,correct,total);
if(correct>=total){
delete this.wrongUnits[uid];
} else {
@ -272,7 +276,6 @@ document.addEventListener('alpine:init', () => {
get resultScore(){
const correct=Object.values(this.quizState).filter(s=>s?.correct).length;
const total=this.currentUnit?.quiz?.length||0;
if(this.currentUnit) this.saveScore(this.currentUnit.id,correct,total);
return correct+' / '+total;
},
get resultMsg(){
@ -379,6 +382,7 @@ document.addEventListener('alpine:init', () => {
if(!unit) return;
const correct=Object.values(this.weakDrillQuizState).filter(s=>s?.correct).length;
const total=unit.quiz?.length||0;
this.saveScore(unit.id,correct,total);
this.weakDrillResults.push({unitId:unit.id,num:unit.num,unitTitle:unit.title,correct,total});
// Update wrong tracking
if(correct>=total){
@ -460,7 +464,6 @@ document.addEventListener('alpine:init', () => {
this.resetFlashRevealState();
}
} else if(this.stepStep===3){
this.quizState={};
const drills=this.unitDrills;
if(drills.length>0){
this.stepStep=2;

14
sw.js
View File

@ -1,14 +1,16 @@
// posimai-boki SW — stale-while-revalidate + skipWaiting
const CACHE = 'posimai-boki-v18';
const CACHE = 'posimai-boki-v19';
const STATIC = ['/', '/index.html', '/manifest.json', '/logo.png', '/js/app.js', '/js/data/drills.js', '/js/data/categories.js'];
self.addEventListener('install', e => {
e.waitUntil(
caches.open(CACHE).then(c => c.addAll(STATIC.filter(u => {
try { new URL(u, self.location.origin); return true; } catch { return false; }
})))
Promise.all([
caches.open(CACHE).then(c => c.addAll(STATIC.filter(u => {
try { new URL(u, self.location.origin); return true; } catch { return false; }
}))),
self.skipWaiting()
])
);
self.skipWaiting();
});
self.addEventListener('activate', e => {
@ -29,7 +31,7 @@ self.addEventListener('fetch', e => {
const network = fetch(e.request).then(res => {
if (res.ok && res.type === 'basic') cache.put(e.request, res.clone());
return res;
}).catch(() => cached);
}).catch(() => cached || new Response('Offline', { status: 503, statusText: 'Service Unavailable' }));
return cached || network;
})
)