diff --git a/app.css b/app.css index a29263c..4471d8a 100644 --- a/app.css +++ b/app.css @@ -186,9 +186,10 @@ button.row:active, .deck-row:active { background: var(--c); } .deck-row:active { transform: scale(.98); } .deck-row .name { flex: 1; font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .deck-row .ic:first-child { color: var(--on-variant); } -.counts { display: flex; gap: 8px; font-size: 12px; line-height: 16px; font-weight: 700; font-variant-numeric: tabular-nums; } +.counts { display: flex; gap: 8px; font-size: 11px; line-height: 16px; font-weight: 700; font-variant-numeric: tabular-nums; } .counts .n { color: var(--new); } .counts .l { color: var(--learn); } .counts .d { color: var(--due); } .counts .zero { color: var(--outline); font-weight: 400; } +.count-lbl { font-size: 10px; font-weight: 500; opacity: 0.8; margin-right: 2px; } .deck-row .ic.done { color: var(--due); } .banner { @@ -264,7 +265,24 @@ button.row:active, .deck-row:active { background: var(--c); } .dv-counts .n b { color: var(--new); } .dv-counts .l b { color: var(--learn); } .dv-counts .d b { color: var(--due); } .card-row { display: flex; flex-direction: column; align-items: flex-start; gap: 2px; } .card-row .q { font-weight: 600; } -.card-row .a { font-size: 13px; color: var(--on-variant); } +.card-row .a { + font-size: 13px; + color: var(--on-variant); + filter: blur(5.5px); + user-select: none; + cursor: pointer; + transition: filter 0.2s, background 0.2s; + background: color-mix(in srgb, var(--on-variant) 12%, transparent); + border-radius: var(--r-sm); + padding: 0 6px; + display: inline-block; +} +.card-row .a.revealed { + filter: none; + background: none; + padding: 0; + user-select: auto; +} .card-row .meta { font-size: 12px; color: var(--outline); } .card-row .q, .card-row .a { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 100%; } @@ -300,8 +318,27 @@ button.row:active, .deck-row:active { background: var(--c); } .face.back .card-text.dim { font-size: 15px; line-height: 20px; color: var(--on-variant); font-weight: 600; } .face hr { width: 64px; border: 0; border-top: .5px solid var(--outline-variant); } .hint { text-align: center; color: var(--outline); font-size: 13px; padding-bottom: 10px; } -.face-tools { display: flex; justify-content: space-between; border-top: .5px solid var(--outline-variant); - padding-top: 6px; margin: 0 -4px -6px; color: var(--on-variant); } +.face-tools { + position: absolute; + top: 12px; + right: 12px; + display: flex; + gap: 8px; + color: var(--on-variant); +} +.face-tools .icon-btn { + width: 36px; + height: 36px; + border-radius: var(--r); + background: var(--c-low); + border: .5px solid var(--outline-variant); + display: grid; + place-items: center; +} +.face-tools .icon-btn .ic { + width: 18px; + height: 18px; +} .rating { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--stack); padding: 0 var(--m-page) calc(env(safe-area-inset-bottom) + 16px); } @@ -322,11 +359,38 @@ button.row:active, .deck-row:active { background: var(--c); } flex-direction: column; align-items: center; justify-content: center; gap: 12px; padding: var(--m-page) 32px calc(var(--m-page) + 40px); text-align: center; animation: fade .3s var(--ease); } .done-panel[hidden] { display: none; } -.done-check { width: 56px; height: 56px; color: var(--due); stroke-width: 1.6; } +.done-check { width: 56px; height: 56px; color: var(--due); stroke-width: 1.6; animation: pop-check 0.5s var(--ease); } +@keyframes pop-check { + 0% { transform: scale(0); } + 90% { transform: scale(1.1); } + 100% { transform: scale(1); } +} .done-panel h3 { font-size: 24px; line-height: 30px; } .done-panel p { color: var(--on-variant); } .done-panel .primary-btn { max-width: 280px; margin-top: 12px; } +.streak-badge { + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + background: color-mix(in srgb, var(--tertiary) 10%, var(--c-lowest)); + border: 1px solid var(--tertiary); + border-radius: var(--r-xl); + padding: 10px 24px; + font-size: 16px; + font-weight: 700; + color: var(--tertiary); + margin: 12px 0; + animation: bounce-in 0.6s var(--ease); +} +@keyframes bounce-in { + 0% { transform: scale(0.3); opacity: 0; } + 50% { transform: scale(1.08); } + 75% { transform: scale(0.95); } + 100% { transform: scale(1); opacity: 1; } +} + /* ---------- sheet ---------- */ .scrim { position: fixed; inset: 0; background: rgba(0,0,0,.4); z-index: 30; animation: fadein .25s; } .scrim[hidden] { display: none; } diff --git a/app.js b/app.js index 130dfda..8da9e87 100644 --- a/app.js +++ b/app.js @@ -63,9 +63,9 @@ function renderHome() { ${icon('folder')} ${esc(d.name)} ${clear && total ? `${icon('check', 'ic done')}` : ` - ${c.n} - ${c.l} - ${c.d}`} + 新規${c.n} + 学習中${c.l} + 復習${c.d}`} ${icon('chevron', 'ic chev')} `; }).join(''); @@ -246,6 +246,19 @@ function grade(g) { function finishReview() { const mins = Math.max(1, Math.round((Date.now() - sessionStart) / 60000)); $('#rv-done-sub').textContent = `${answered}枚を復習しました(約${mins}分)`; + + const days = new Set(db.log.map(l => dayKey(l.t))); + let streak = 0, cur = startOfDay(Date.now()); + if (!days.has(dayKey(cur))) cur -= DAY; + while (days.has(dayKey(cur))) { streak++; cur -= DAY; } + + if (streak > 0) { + $('#rv-streak-count').textContent = streak; + $('#rv-done-streak').hidden = false; + } else { + $('#rv-done-streak').hidden = true; + } + $('#rv-done').hidden = false; $('#rating').classList.add('hidden'); } @@ -346,6 +359,12 @@ function go(tab) { /* ---------------- events ---------------- */ document.addEventListener('click', e => { const t = e.target; + if (t.classList.contains('a') && t.closest('.card-row')) { + e.stopPropagation(); + e.preventDefault(); + t.classList.toggle('revealed'); + return; + } const tab = t.closest('[data-goto]'); if (tab) return go(tab.dataset.goto); const deck = t.closest('[data-deck]'); if (deck) return openDeckView(deck.dataset.deck); const card = t.closest('[data-card]'); if (card) return cardSheet(db.cards.find(c => c.id === card.dataset.card)); @@ -376,7 +395,54 @@ $('#set-theme').onchange = e => { db.settings.theme = e.target.value; save(); ap $('#opt-' + k).onchange = e => { deckOf(openDeck)[k] = e.target.checked; save(); renderDeck(); }; }); $('#btn-import-json').onclick = () => pickFile('json'); -$('#btn-import-csv').onclick = () => pickFile('csv'); +function showCSVImportModal() { + const bodyHTML = ` +
取り込むファイルのフォーマットが以下のように構成されていることを確認してください。
+| 1列目 | +2列目 | +
|---|---|
| 表面(問題) | +裏面(答え) | +
+ ※カンマ区切り(.csv)およびタブ区切り(.tsv/.txt)に対応しています。ファイル名がそのまま新規作成されるデッキの名前になります。 +
+