Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .tdd/spec-miniapp-card-mode-simplification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Mini App card-mode simplification

Status: locked 2026-09-17

## Acceptance criteria

- AC-1: The Words tab presents the card trainer before custom-word, device-dictionary, and word-library utilities. All three card modes (mix, review, new) remain visible together.
- AC-2: The trainer exposes one primary start action. Its label and supporting explanation reflect the selected mode, while the mode buttons expose available counts after status loads.
- AC-3: Custom-word actions, the device dictionary, and the word library are progressive disclosures closed by default. Their existing actions, IDs, and deep links remain available after expansion.
- AC-4: The Words tab keeps one orange Lexi selection/action system, uses native accessible controls, and connects the mode group to a visible label and explanation.

## Edge cases

- EC-1: A mode with zero available words remains visible and selectable; starting it keeps the existing explicit empty-state feedback instead of silently switching modes.
- EC-2: The word library remains paginated at six items per page after expansion.
- EC-3: The disclosure layout remains usable at the 320 px project minimum and in RTL locales.

## Constraints

- Preserve the deterministic server-owned queue, rating, resume, undo, and completion behavior.
- Preserve all eight interface locales and Telegram authentication boundaries.
- Do not add dependencies or change database schema.

## Out of scope

- New learning algorithms, new vocabulary sources, or changes to the Telegram bot menu.
- Redesigning the active-card study state.
15 changes: 14 additions & 1 deletion mydictionary/static/miniapp-swipe.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,17 @@
ar: ["تابع البطاقات المتبقية", "توقف مؤقت · الإجابات محفوظة", "الصوت غير متاح. حاول الاستماع مجددًا.", "مزيج", "ابدأ التدريب"]
};
Object.entries(additional).forEach(([locale, values]) => Object.assign(copies[locale], Object.fromEntries(["resume", "pause", "audio_error", "mix", "start"].map((key, index) => [key, values[index]]))));
const modeUx = {
en: {mode_prompt: "Choose a mode", mix_hint: "Recommended: due reviews first, then new words.", forgotten_hint: "Only words that are due for review.", new_hint: "Only words you have not studied yet.", mix_start: "Start smart mix", forgotten_start: "Start review", new_start: "Learn new words"},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Describe review mode as including mistaken words

For learners who recently answered a word incorrectly, this explanation is false: _pools() in mydictionary/swipe.py adds words with wrong_count > 0 and correct_count < 3 to the forgotten pool even when next_review is still in the future. Selecting Review can therefore show non-due mistake words despite promising “Only words that are due for review”; update this hint and its localized equivalents to describe the actual pool.

Useful? React with 👍 / 👎.

ru: {mode_prompt: "Выбери режим", mix_hint: "Рекомендуем: сначала повторение, затем новые слова.", forgotten_hint: "Только слова, которые пора повторить.", new_hint: "Только слова, которые ты ещё не изучал.", mix_start: "Начать умный микс", forgotten_start: "Начать повторение", new_start: "Учить новые слова"},
fr: {mode_prompt: "Choisis un mode", mix_hint: "Recommandé : révisions dues, puis nouveaux mots.", forgotten_hint: "Uniquement les mots à réviser maintenant.", new_hint: "Uniquement les mots pas encore étudiés.", mix_start: "Lancer le mix malin", forgotten_start: "Commencer la révision", new_start: "Apprendre de nouveaux mots"},
de: {mode_prompt: "Modus wählen", mix_hint: "Empfohlen: zuerst fällige Wiederholungen, dann neue Wörter.", forgotten_hint: "Nur Wörter, die jetzt wiederholt werden sollen.", new_hint: "Nur Wörter, die du noch nicht gelernt hast.", mix_start: "Smarten Mix starten", forgotten_start: "Wiederholung starten", new_start: "Neue Wörter lernen"},
es: {mode_prompt: "Elige un modo", mix_hint: "Recomendado: primero repasos pendientes y luego palabras nuevas.", forgotten_hint: "Solo palabras que ya toca repasar.", new_hint: "Solo palabras que aún no has estudiado.", mix_start: "Empezar mezcla inteligente", forgotten_start: "Empezar repaso", new_start: "Aprender palabras nuevas"},
ja: {mode_prompt: "モードを選ぶ", mix_hint: "おすすめ:復習する単語のあとに新しい単語を学びます。", forgotten_hint: "今が復習のタイミングの単語だけ。", new_hint: "まだ学習していない単語だけ。", mix_start: "スマートミックスを始める", forgotten_start: "復習を始める", new_start: "新しい単語を学ぶ"},
zh: {mode_prompt: "选择模式", mix_hint: "推荐:先复习到期单词,再学习新单词。", forgotten_hint: "只练习现在需要复习的单词。", new_hint: "只学习尚未学过的单词。", mix_start: "开始智能混合", forgotten_start: "开始复习", new_start: "学习新单词"},
ar: {mode_prompt: "اختر وضعًا", mix_hint: "موصى به: راجع الكلمات المستحقة ثم تعلّم كلمات جديدة.", forgotten_hint: "الكلمات المستحقة للمراجعة الآن فقط.", new_hint: "الكلمات التي لم تدرسها بعد فقط.", mix_start: "ابدأ المزيج الذكي", forgotten_start: "ابدأ المراجعة", new_start: "تعلّم كلمات جديدة"}
};
Object.entries(modeUx).forEach(([locale, values]) => Object.assign(copies[locale], values));
const plurals = {en: "plural", ru: "мн. ч.", fr: "pluriel", de: "Plural", es: "plural", ja: "複数", zh: "复数", ar: "الجمع"};
Object.entries(plurals).forEach(([locale, label]) => { copies[locale].plural = label; });
const root = document.getElementById("swipe-trainer");
Expand Down Expand Up @@ -70,7 +81,8 @@
setText("resume", copy.resume);
}
if (el("pause")) { el("pause").hidden = !card; el("pause").disabled = blocked; setText("pause", copy.pause); }
setText("start", completed ? copy.restart : copy.start);
setText("mode-help", copy[`${mode}_hint`]);
setText("start", completed ? copy.restart : copy[`${mode}_start`] || copy.start);
setText("reveal", revealed ? copy.hide : copy.reveal);
el("reveal").setAttribute("aria-expanded", String(revealed));
el("meaning").hidden = !revealed;
Expand Down Expand Up @@ -303,6 +315,7 @@
copy = copies[data.locale] || copies.en;
fatal = !authenticated() || data.privacy?.access_erased === true;
["title", "hint", "again", "know", "undo", "retry", "speak"].forEach(id => setText(id, copy[id]));
setText("mode-label", copy.mode_prompt);
setText("keyboard-hint", copy.keyboard);
setText("language", current ? `${current.label} ›` : copy.language);
if (el("language")) el("language").setAttribute("aria-label", copy.language);
Expand Down
75 changes: 59 additions & 16 deletions mydictionary/static/miniapp.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,20 @@
--words-accent-line: color-mix(in srgb, var(--words-accent) 38%, var(--dashboard-line));
--words-surface-raised: color-mix(in srgb, var(--words-accent) 5%, var(--dashboard-surface));
}
#panel-words > header[class~="section-hero"] { display: none; }
.swipe-trainer { --surface: var(--dashboard-surface); --radius: var(--dashboard-radius); --danger: var(--dashboard-danger); --muted: var(--dashboard-muted); --line: var(--dashboard-line); min-width: 0; padding: 16px 0 22px; border-bottom: 1px solid var(--line); }
#panel-words:has(.swipe-trainer[data-active="true"]) > header[class~="section-hero"] { display: none; }
.swipe-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
.swipe-heading > div { min-width: 0; }
.swipe-heading h2 { margin: 5px 0 0; font-size: 1.4rem; letter-spacing: -.025em; }
.swipe-kicker { color: var(--words-accent); font-size: .68rem; font-weight: 800; letter-spacing: .08em; }
.swipe-language { flex-shrink: 0; max-width: 45%; border: 0; background: transparent; color: var(--words-accent); font-size: .8rem; padding: 10px 0 10px 8px; overflow-wrap: anywhere; }
.swipe-hint, .swipe-keyboard-hint { color: var(--muted); font-size: .85rem; line-height: 1.5; }
.swipe-hint { margin: 10px 0 16px; }
.swipe-mode-label { margin: 0 0 7px; color: var(--dashboard-text); font-size: .82rem; font-weight: 850; }
.swipe-modes { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 4px; background: var(--surface); padding: 4px; border: 1px solid var(--line); border-radius: 12px; }
.swipe-modes button { min-width: 0; min-height: 44px; padding: 8px 5px; border: 0; border-radius: 8px; background: transparent; color: var(--muted); font-size: .8rem; font-weight: 750; overflow-wrap: anywhere; }
.swipe-modes button[aria-pressed="true"] { background: var(--words-accent); color: var(--words-accent-ink); }
.swipe-mode-help { min-height: 2.9em; margin: 9px 0 0; color: var(--dashboard-muted); font-size: .78rem; line-height: 1.45; text-wrap: pretty; }
.swipe-session-line { display: flex; align-items: center; gap: 12px; margin: 18px 0 12px; color: var(--muted); font-size: .8rem; font-variant-numeric: tabular-nums; }
.swipe-session-line progress { flex: 1; min-width: 0; height: 5px; border: 0; border-radius: 5px; overflow: hidden; background: var(--line); accent-color: var(--words-accent); }
.swipe-session-line progress::-webkit-progress-bar { background: var(--line); }
Expand Down Expand Up @@ -194,8 +196,9 @@ button:not(:disabled):active { transform: translateY(1px) scale(.985); }
.dictionary-shortcut > span:nth-child(2) { flex:1; min-width:0; }
.dictionary-shortcut strong, .dictionary-shortcut small { display:block; }
.dictionary-shortcut small { color:var(--dashboard-muted); font-size:.8rem; font-weight:400; margin-top:3px; }
.dictionary-entry { display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px; margin-block:4px 22px; padding-top:16px; border-top:1px solid var(--dashboard-line); }
.dictionary-entry p { grid-column:1 / -1; color:var(--dashboard-muted); font-size:.78rem; line-height:1.45; margin:2px 0 0; }
.dictionary-entry-body { display: grid; gap: 10px; }
.dictionary-entry-body p { color: var(--dashboard-muted); font-size: .78rem; line-height: 1.45; margin: 0; }
.dictionary-entry-actions { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }

/* Shared dashboard vocabulary. */
.dashboard { display: grid; gap: var(--dashboard-gap); }
Expand Down Expand Up @@ -469,19 +472,56 @@ button:not(:disabled):active { transform: translateY(1px) scale(.985); }
}

/* My words. */
.custom-vocabulary-actions {
display: grid;
gap: 16px;
margin-bottom: 16px;
padding: 16px;
.words-disclosure,
[data-words-disclosure] {
margin: 0 0 8px;
border: 1px solid var(--words-accent-line);
border-radius: var(--dashboard-radius);
border-radius: 12px;
background: var(--words-surface-raised);
}
.words-disclosure > summary,
[data-words-disclosure] > summary {
display: grid;
grid-template-columns: 36px minmax(0, 1fr) 24px;
gap: 10px;
align-items: center;
min-height: 60px;
padding: 9px 10px;
color: var(--dashboard-text);
cursor: pointer;
list-style: none;
}
.words-disclosure > summary::-webkit-details-marker,
[data-words-disclosure] > summary::-webkit-details-marker { display: none; }
.words-disclosure > summary:focus-visible,
[data-words-disclosure] > summary:focus-visible { outline: 3px solid var(--words-accent); outline-offset: 2px; border-radius: 10px; }
.words-disclosure-icon {
display: grid;
place-items: center;
width: 36px;
height: 36px;
border-radius: 10px;
color: var(--words-accent-ink);
background: var(--words-accent);
font-size: 1rem;
font-weight: 900;
}
.words-disclosure-label { display: grid; min-width: 0; gap: 2px; text-align: start; }
.words-disclosure-label strong { font-size: .86rem; line-height: 1.2; }
.words-disclosure-label small { color: var(--dashboard-muted); font-size: .68rem; font-weight: 600; line-height: 1.35; overflow-wrap: anywhere; }
.words-disclosure-label b { color: var(--words-accent); font-variant-numeric: tabular-nums; }
.words-disclosure-chevron { color: var(--words-accent); font-size: 1.45rem; line-height: 1; text-align: center; transition: transform 180ms var(--control-ease); }
.words-disclosure[open] > summary,
[data-words-disclosure][open] > summary { border-bottom: 1px solid var(--words-accent-line); }
.words-disclosure[open] > summary .words-disclosure-chevron,
[data-words-disclosure][open] > summary .words-disclosure-chevron { transform: rotate(90deg); }
.words-disclosure-body { padding: 12px; }
[dir="rtl"] .words-disclosure-chevron { transform: scaleX(-1); }
[dir="rtl"] .words-disclosure[open] > summary .words-disclosure-chevron,
[dir="rtl"] [data-words-disclosure][open] > summary .words-disclosure-chevron { transform: scaleX(-1) rotate(90deg); }
.custom-vocabulary-actions { display: block; }
.custom-vocabulary-copy { display: grid; gap: 4px; }
.custom-vocabulary-kicker { color: var(--words-accent); font-size: .68rem; font-weight: 850; }
.custom-vocabulary-copy h2 { margin: 0; color: var(--dashboard-text); font-size: 1.02rem; }
.custom-vocabulary-copy p { margin: 0; color: var(--dashboard-muted); font-size: .74rem; line-height: 1.4; }
.custom-vocabulary-copy h2 { margin: 0 0 10px; color: var(--dashboard-text); font-size: .9rem; }
.custom-vocabulary-controls { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.custom-vocabulary-controls button { display: flex; align-items: center; justify-content: center; gap: 6px; min-height: 52px; padding: 9px 10px; font-size: .72rem; font-weight: 900; }
.custom-vocabulary-practice { color: var(--words-accent-ink); border-color: var(--words-accent); background: var(--words-accent); }
Expand Down Expand Up @@ -853,7 +893,7 @@ html[dir="rtl"] .privacy-state dd { text-align: start; }
#panel-profile[data-first-lesson="true"] [data-profile-section="hero"],
#panel-profile[data-first-lesson="true"] [data-profile-section="calendar"],
#panel-profile[data-first-lesson="true"] #daily-quest { display: none; }
.practice-source, .device-dictionary-note { font-size: .875rem; line-height: 1.5; color: var(--dashboard-muted); }
.device-dictionary-note { font-size: .875rem; line-height: 1.5; color: var(--dashboard-muted); }
.device-dictionary-note { margin: 0 0 12px; }
#swipe-ipa summary { min-height: 44px; padding: 10px 4px; font-size: .875rem; cursor: pointer; color: var(--dashboard-muted); }
#swipe-ipa p { max-width: 32ch; font-size: .875rem; line-height: 1.5; }
Expand All @@ -865,7 +905,8 @@ html[dir="rtl"] .privacy-state dd { text-align: start; }
.swipe-trainer[data-active="true"] { padding-top: 0; border-bottom: 0; }
.swipe-trainer[data-active="true"] .swipe-heading,
.swipe-trainer[data-active="true"] .swipe-hint,
.swipe-trainer[data-active="true"] .practice-source,
.swipe-trainer[data-active="true"] .swipe-mode-label,
.swipe-trainer[data-active="true"] .swipe-mode-help,
.swipe-trainer[data-active="true"] .swipe-modes,
.swipe-trainer[data-active="true"] .swipe-keyboard-hint { display: none; }
.swipe-trainer[data-active="true"] .swipe-card { min-height: 210px; gap: 12px; padding: 14px; cursor: default; }
Expand Down Expand Up @@ -1116,8 +1157,10 @@ html[dir="rtl"] .privacy-state dd { text-align: start; }
.detail-back { justify-self: start; }
.detail-actions { grid-template-columns: 1fr; }
.custom-vocabulary-controls { grid-template-columns: 1fr; }
.dictionary-entry { grid-template-columns: 1fr; }
.dictionary-entry p { grid-column: 1; }
.dictionary-entry-actions { grid-template-columns: 1fr; }
.words-disclosure > summary,
[data-words-disclosure] > summary { grid-template-columns: 34px minmax(0, 1fr) 20px; gap: 8px; padding-inline: 8px; }
.words-disclosure-icon { width: 34px; height: 34px; }
.app-shell { padding-inline: max(10px, env(safe-area-inset-left)) max(10px, env(safe-area-inset-right)); }
.bottom-nav button { font-size: .75rem; }
.nav-icon { width: 30px; height: 30px; }
Expand Down
2 changes: 2 additions & 0 deletions mydictionary/static/miniapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -1031,6 +1031,8 @@
wordLibraryItems.tracked = data.words;
text(node("custom-word-count"), customWords.length);
text(node("tracked-word-count"), data.words.length);
text(node("custom-word-summary-count"), customWords.length);
text(node("tracked-word-summary-count"), data.words.length);
renderWordLibraryPage("custom", copy);
renderWordLibraryPage("tracked", copy);
if (!wordLibraryInitialized) {
Expand Down
Loading
Loading