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
16 changes: 16 additions & 0 deletions mydictionary/miniapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,22 @@ def miniapp_text_direction(locale: str | None) -> str:
"custom_words_empty", "custom_words_no_transcription",
), _values))

_WORD_LIBRARY_COPY = {
"en": ("Word library", "Previous", "Next", "Page {current} of {total}"),
"fr": ("Bibliothèque de mots", "Précédent", "Suivant", "Page {current} sur {total}"),
"de": ("Wortbibliothek", "Zurück", "Weiter", "Seite {current} von {total}"),
"ja": ("単語ライブラリ", "前へ", "次へ", "{current} / {total} ページ"),
"ar": ("مكتبة الكلمات", "السابق", "التالي", "الصفحة {current} من {total}"),
"zh": ("单词库", "上一页", "下一页", "第 {current} 页,共 {total} 页"),
"ru": ("Библиотека слов", "Назад", "Дальше", "Страница {current} из {total}"),
"es": ("Biblioteca de palabras", "Anterior", "Siguiente", "Página {current} de {total}"),
}
for _locale, _values in _WORD_LIBRARY_COPY.items():
MINIAPP_COPY[_locale].update(zip((
"word_library_title", "word_library_previous",
"word_library_next", "word_library_page",
), _values))

_LANGUAGE_SWITCH_COPY = {
"en": ("Switching dictionary…", "Could not switch dictionary.", "Try again"),
"fr": ("Changement de dictionnaire…", "Impossible de changer de dictionnaire.", "Réessayer"),
Expand Down
74 changes: 69 additions & 5 deletions mydictionary/static/miniapp.css
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,6 @@ button:not(:disabled):active { transform: translateY(1px) scale(.985); }
.dictionary-shortcut small { color:var(--dashboard-muted); font-size:.8rem; font-weight:400; margin-top:3px; }
.dictionary-entry { display:flex; flex-wrap:wrap; gap:8px; margin-block:14px 22px; }
.dictionary-entry p { flex-basis:100%; color:var(--dashboard-muted); font-size:.84rem; margin:2px 0 0; }
.tracked-heading { font-size:1rem; margin:16px 0 10px; }

/* Shared dashboard vocabulary. */
.dashboard { display: grid; gap: var(--dashboard-gap); }
Expand Down Expand Up @@ -473,10 +472,75 @@ button:not(:disabled):active { transform: translateY(1px) scale(.985); }
.custom-vocabulary-add { color: #241405; border-color: #ffb36a; background: linear-gradient(145deg, #ffd166, #ff914d); }
.custom-vocabulary-practice { color: #041d1a; border-color: #69dcc8; background: linear-gradient(145deg, #83ead8, #46c4b3); }
.custom-vocabulary-controls button > span:first-child { font-size: 1rem; }
.custom-vocabulary-library { margin-bottom: 14px; }
.custom-word-list-heading { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin: 0 2px 7px; }
.custom-word-list-heading h2 { margin: 0; font-size: .82rem; }
.custom-word-list-heading strong { display: grid; place-items: center; min-width: 28px; min-height: 28px; border-radius: 999px; color: var(--dashboard-warning); background: color-mix(in srgb, var(--dashboard-warning) 14%, var(--dashboard-surface)); font-size: .72rem; }
.word-library { margin-bottom: 14px; }
.word-library-tabs {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 4px;
padding: 4px;
border: 1px solid var(--dashboard-line);
border-radius: 14px;
background: color-mix(in srgb, var(--dashboard-line) 30%, var(--dashboard-bg));
}
.word-library-tab {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 7px;
align-items: center;
min-height: 48px;
padding: 8px 10px;
border: 0;
border-radius: 10px;
color: var(--dashboard-muted);
background: transparent;
font: inherit;
font-size: .72rem;
font-weight: 850;
line-height: 1.2;
text-align: start;
}
.word-library-tab[aria-selected="true"] {
color: var(--dashboard-text);
background: var(--dashboard-surface);
box-shadow: 0 1px 4px color-mix(in srgb, var(--dashboard-text) 12%, transparent);
}
.word-library-tab strong {
display: grid;
place-items: center;
min-width: 26px;
min-height: 26px;
padding-inline: 5px;
border-radius: 999px;
color: var(--dashboard-warning);
background: color-mix(in srgb, var(--dashboard-warning) 14%, var(--dashboard-surface));
font-size: .68rem;
font-variant-numeric: tabular-nums;
}
.word-library-panel { margin-top: 10px; outline: none; }
.word-library-panel:focus-visible {
border-radius: var(--dashboard-radius);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--dashboard-accent) 34%, transparent);
}
.word-library-pager {
display: grid;
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
gap: 8px;
align-items: center;
margin-top: 8px;
}
.word-library-pager button {
min-height: 44px;
padding: 8px 10px;
font-size: .7rem;
}
.word-library-pager button:last-child { justify-self: stretch; }
.word-library-pager span {
min-width: 6ch;
color: var(--dashboard-muted);
font-size: .68rem;
font-variant-numeric: tabular-nums;
text-align: center;
}
.custom-word-card { border-inline-start: 3px solid var(--lexi-orange); }
.custom-word-transcription { display: block; margin-top: 4px; color: var(--dashboard-accent); font-family: "Noto Sans", "DejaVu Sans", "Segoe UI", "Helvetica Neue", Arial, sans-serif; font-size: .8rem; line-height: 1.35; }
.custom-words-empty { margin: 0; padding: 12px; border: 1px dashed var(--dashboard-line); border-radius: 12px; color: var(--dashboard-muted); background: var(--dashboard-surface); font-size: .72rem; line-height: 1.4; }
Expand Down
103 changes: 92 additions & 11 deletions mydictionary/static/miniapp.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,13 @@
const PROFILE_REORDER_HOLD_MS = 420;
const PROFILE_REORDER_MOVE_TOLERANCE_PX = 10;
const PROFILE_REORDER_NATIVE_CONTROL_SELECTOR = "button, a, input, select, textarea, summary";
const WORD_LIBRARY_PAGE_SIZE = 6;
let profileReorderState = null;
let profileReorderSuppressClick = false;
let wordLibraryInitialized = false;
let wordLibraryTab = "custom";
const wordLibraryPage = {custom: 0, tracked: 0};
const wordLibraryItems = {custom: [], tracked: []};

const node = (id) => document.getElementById(id);
const text = (element, value) => { element.textContent = String(value ?? ""); };
Expand Down Expand Up @@ -550,7 +555,7 @@
}
}

function addWord(word, copy) {
function addWord(word, copy, container = node("word-list")) {
const card = document.createElement("article");
card.className = "word-card";
card.classList.add("dashboard-row");
Expand All @@ -573,10 +578,10 @@
text(attempts, `${copy.attempts_correct}: ${word.correct} · ${copy.attempts_wrong}: ${word.wrong}`);
main.append(header, meaning);
card.append(main, attempts);
node("word-list").append(card);
container.append(card);
}

function addCustomWord(word, copy) {
function addCustomWord(word, copy, container = node("custom-word-list")) {
const card = document.createElement("article");
card.className = "word-card custom-word-card dashboard-row";
const main = document.createElement("div");
Expand All @@ -598,7 +603,59 @@
text(meaning, word.meaning);
main.append(header, transcription, meaning);
card.append(main);
node("custom-word-list").append(card);
container.append(card);
}

function renderWordLibraryPage(kind, copy) {
const isCustom = kind === "custom";
const items = wordLibraryItems[kind];
const list = node(isCustom ? "custom-word-list" : "word-list");
const empty = node(isCustom ? "empty-custom-words" : "empty-words");
const pager = node(isCustom ? "custom-word-pager" : "tracked-word-pager");
const previous = node(isCustom ? "custom-word-page-previous" : "tracked-word-page-previous");
const next = node(isCustom ? "custom-word-page-next" : "tracked-word-page-next");
const status = node(isCustom ? "custom-word-page-status" : "tracked-word-page-status");
const totalPages = Math.max(1, Math.ceil(items.length / WORD_LIBRARY_PAGE_SIZE));
const currentPage = Math.min(wordLibraryPage[kind], totalPages - 1);
const start = currentPage * WORD_LIBRARY_PAGE_SIZE;

wordLibraryPage[kind] = currentPage;
list.replaceChildren();
items.slice(start, start + WORD_LIBRARY_PAGE_SIZE).forEach((word) => {
if (isCustom) addCustomWord(word, copy, list);
else addWord(word, copy, list);
});
empty.hidden = items.length !== 0;
pager.hidden = items.length <= WORD_LIBRARY_PAGE_SIZE;
previous.disabled = currentPage === 0;
next.disabled = currentPage >= totalPages - 1;
text(
status,
copy.word_library_page
.replace("{current}", String(currentPage + 1))
.replace("{total}", String(totalPages))
);
}

function setWordLibraryTab(kind, focus = false) {
if (!Object.prototype.hasOwnProperty.call(wordLibraryItems, kind)) return;
wordLibraryTab = kind;
document.querySelectorAll("[data-word-library-tab]").forEach((tab) => {
const selected = tab.dataset.wordLibraryTab === kind;
tab.setAttribute("aria-selected", String(selected));
tab.tabIndex = selected ? 0 : -1;
if (selected && focus) tab.focus();
});
document.querySelectorAll("[data-word-library-panel]").forEach((panel) => {
panel.hidden = panel.dataset.wordLibraryPanel !== kind;
});
}

function moveWordLibraryPage(kind, offset) {
if (!payload) return;
const totalPages = Math.max(1, Math.ceil(wordLibraryItems[kind].length / WORD_LIBRARY_PAGE_SIZE));
wordLibraryPage[kind] = Math.max(0, Math.min(totalPages - 1, wordLibraryPage[kind] + offset));
renderWordLibraryPage(kind, payload.copy);
}

function addInterfaceLocaleSetting(container, data, copy) {
Expand Down Expand Up @@ -946,20 +1003,23 @@
metric(copy.metric_tracked_words, progress.tracked_words)
);

node("word-list").replaceChildren();
node("word-summary").replaceChildren(
summaryStat(copy.metric_tracked_words, progress.tracked_words, "tracked"),
summaryStat(copy.metric_learned_words, data.words.filter((word) => word.learned).length, "learned"),
summaryStat(copy.word_review, data.words.filter((word) => word.due).length, "due")
);
data.words.forEach((word) => addWord(word, copy));
node("empty-words").hidden = data.words.length !== 0;

node("custom-word-list").replaceChildren();
const customWords = Array.isArray(data.custom_words) ? data.custom_words : [];
wordLibraryItems.custom = customWords;
wordLibraryItems.tracked = data.words;
text(node("custom-word-count"), customWords.length);
customWords.forEach((word) => addCustomWord(word, copy));
node("empty-custom-words").hidden = customWords.length !== 0;
text(node("tracked-word-count"), data.words.length);
renderWordLibraryPage("custom", copy);
renderWordLibraryPage("tracked", copy);
if (!wordLibraryInitialized) {
wordLibraryTab = customWords.length || data.words.length === 0 ? "custom" : "tracked";
wordLibraryInitialized = true;
}
setWordLibraryTab(wordLibraryTab);

text(node("wallet-available"), data.credits.available);
node("credit-summary").replaceChildren(
Expand Down Expand Up @@ -1244,6 +1304,27 @@
node("retry-button").addEventListener("click", load);
node("calendar-previous").addEventListener("click", () => moveCalendar(-1));
node("calendar-next").addEventListener("click", () => moveCalendar(1));
document.querySelectorAll("[data-word-library-tab]").forEach((tab) => {
tab.addEventListener("click", () => setWordLibraryTab(tab.dataset.wordLibraryTab));
tab.addEventListener("keydown", (event) => {
const tabs = Array.from(document.querySelectorAll("[data-word-library-tab]"));
const current = tabs.indexOf(tab);
let target = -1;
if (event.key === "ArrowLeft" || event.key === "ArrowUp") target = (current - 1 + tabs.length) % tabs.length;
if (event.key === "ArrowRight" || event.key === "ArrowDown") target = (current + 1) % tabs.length;
if (event.key === "Home") target = 0;
if (event.key === "End") target = tabs.length - 1;
if (target < 0) return;
event.preventDefault();
setWordLibraryTab(tabs[target].dataset.wordLibraryTab, true);
});
});
document.querySelectorAll("[data-word-library-page]").forEach((button) => {
button.addEventListener("click", () => {
const offset = button.dataset.pageDirection === "previous" ? -1 : 1;
moveWordLibraryPage(button.dataset.wordLibraryPage, offset);
});
});
const profileLayout = node("profile-layout");
profileLayout.addEventListener("pointerdown", handleProfilePointerDown);
profileLayout.addEventListener("pointermove", handleProfilePointerMove);
Expand Down
40 changes: 31 additions & 9 deletions mydictionary/templates/miniapp.html
Original file line number Diff line number Diff line change
Expand Up @@ -177,21 +177,43 @@ <h2 id="custom-vocabulary-title" data-i18n="custom_words_title"></h2>
<button class="custom-vocabulary-practice dashboard-action" type="button" data-action="practice_custom"><span aria-hidden="true">▶</span><span data-i18n="custom_words_practice"></span></button>
</div>
</section>
<section class="custom-vocabulary-library" aria-labelledby="custom-word-list-title">
<div class="custom-word-list-heading"><h2 id="custom-word-list-title" data-i18n="custom_words_saved"></h2><strong id="custom-word-count">0</strong></div>
<div id="custom-word-list" class="word-list dashboard-list"></div>
<p id="empty-custom-words" class="custom-words-empty" data-i18n="custom_words_empty"></p>
</section>
<div class="dictionary-entry">
<p class="device-dictionary-note" data-i18n="device_dictionary_note"></p>
<button class="primary-action dashboard-action" type="button" data-open-dictionary data-i18n="dictionary_open"></button>
<button class="dashboard-action" type="button" data-download-dictionary data-i18n="dictionary_download"></button>
<p data-i18n="dictionary_hint"></p>
</div>
<h2 class="tracked-heading" data-i18n="dictionary_tracked"></h2>
<div id="word-summary" class="word-overview" role="list"></div>
<div id="word-list" class="word-list dashboard-list"></div>
<div id="empty-words" class="state-card dashboard-state empty" data-ui-state="empty" role="status" hidden><p data-i18n="empty_words"></p><button class="dashboard-action" type="button" data-action="continue" data-i18n="start_lesson"></button></div>
<section class="word-library" data-i18n-aria-label="word_library_title">
<div class="word-library-tabs" role="tablist" data-i18n-aria-label="word_library_title">
<button id="word-library-custom-tab" class="word-library-tab" type="button" role="tab" aria-selected="true" aria-controls="word-library-custom-panel" tabindex="0" data-word-library-tab="custom">

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Scope primary-tab listeners away from library tabs

When either new library tab is clicked, it also matches the existing global document.querySelectorAll('[role="tab"]') in miniapp.js and receives the primary activateTab handler. Because these buttons have no data-tab, that handler hides every [data-panel], including the containing Words panel; the later library-specific handler cannot reveal it. Normal bottom-nav activation also clears the nested tabs' selected state. Restrict the primary selector/listeners to the five data-tab buttons so the two tab groups operate independently.

Useful? React with 👍 / 👎.

<span data-i18n="custom_words_saved"></span>
<strong id="custom-word-count">0</strong>
</button>
<button id="word-library-tracked-tab" class="word-library-tab" type="button" role="tab" aria-selected="false" aria-controls="word-library-tracked-panel" tabindex="-1" data-word-library-tab="tracked">
<span data-i18n="dictionary_tracked"></span>
<strong id="tracked-word-count">0</strong>
</button>
</div>
<section id="word-library-custom-panel" class="word-library-panel" role="tabpanel" aria-labelledby="word-library-custom-tab" tabindex="0" data-word-library-panel="custom">
<div id="custom-word-list" class="word-list dashboard-list"></div>
<p id="empty-custom-words" class="custom-words-empty" data-i18n="custom_words_empty"></p>
<nav id="custom-word-pager" class="word-library-pager" data-word-library-pager="custom" data-i18n-aria-label="word_library_title" hidden>
<button id="custom-word-page-previous" class="dashboard-action" type="button" data-word-library-page="custom" data-page-direction="previous" data-i18n="word_library_previous"></button>
<span id="custom-word-page-status" role="status" aria-live="polite"></span>
<button id="custom-word-page-next" class="dashboard-action" type="button" data-word-library-page="custom" data-page-direction="next" data-i18n="word_library_next"></button>
</nav>
</section>
<section id="word-library-tracked-panel" class="word-library-panel" role="tabpanel" aria-labelledby="word-library-tracked-tab" tabindex="0" data-word-library-panel="tracked" hidden>
<div id="word-summary" class="word-overview" role="list"></div>
<div id="word-list" class="word-list dashboard-list"></div>
<div id="empty-words" class="state-card dashboard-state empty" data-ui-state="empty" role="status" hidden><p data-i18n="empty_words"></p><button class="dashboard-action" type="button" data-action="continue" data-i18n="start_lesson"></button></div>
<nav id="tracked-word-pager" class="word-library-pager" data-word-library-pager="tracked" data-i18n-aria-label="word_library_title" hidden>
<button id="tracked-word-page-previous" class="dashboard-action" type="button" data-word-library-page="tracked" data-page-direction="previous" data-i18n="word_library_previous"></button>
<span id="tracked-word-page-status" role="status" aria-live="polite"></span>
<button id="tracked-word-page-next" class="dashboard-action" type="button" data-word-library-page="tracked" data-page-direction="next" data-i18n="word_library_next"></button>
</nav>
</section>
</section>
</section>

<section id="panel-credits" class="panel dashboard-panel" role="tabpanel" data-panel="credits" aria-labelledby="tab-credits" hidden>
Expand Down
3 changes: 2 additions & 1 deletion tests/test_custom_vocabulary_v1.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,8 @@ def test_miniapp_has_two_primary_custom_word_actions_and_rendering(self):
self.assertIn('data-action="add_words"', HTML)
self.assertIn('data-action="practice_custom"', HTML)
self.assertIn('id="custom-word-list"', HTML)
self.assertIn('customWords.forEach((word) => addCustomWord', JS)
self.assertIn('wordLibraryItems.custom = customWords', JS)
self.assertIn('addCustomWord(word, copy, list)', JS)
self.assertIn('"add_words": "miniapp_add_words"', MINIAPP)
self.assertIn('"practice_custom": "miniapp_practice_custom"', MINIAPP)
self.assertIn(".custom-vocabulary-actions", CSS)
Expand Down
Loading
Loading