From 771516668cbf3f3ce1a1380e7e1d425eab851309 Mon Sep 17 00:00:00 2001 From: Max Date: Mon, 21 Sep 2026 11:48:44 +0400 Subject: [PATCH] Polish Telegram Stars and RTL language rows --- mydictionary/static/miniapp.css | 46 +++++++++++++++++-- mydictionary/static/miniapp.js | 41 ++++++++++++++--- mydictionary/templates/miniapp.html | 4 +- tests/test_miniapp_brand_controls_v1.py | 2 +- tests/test_miniapp_secondary_tabs_v2.py | 22 +++++++-- ...test_miniapp_unified_section_headers_v1.py | 2 +- tests/test_telegram_miniapp_v1.py | 2 +- 7 files changed, 102 insertions(+), 17 deletions(-) diff --git a/mydictionary/static/miniapp.css b/mydictionary/static/miniapp.css index beef295..6e4cf8b 100644 --- a/mydictionary/static/miniapp.css +++ b/mydictionary/static/miniapp.css @@ -650,7 +650,44 @@ button:not(:disabled):active { transform: translateY(1px) scale(.985); } .product-card-copy { display: grid; min-width: 0; } .product-card-copy strong { overflow-wrap: anywhere; font-size: .84rem; } .product-card-copy small { margin-top: 2px; color: var(--dashboard-muted); font-size: .7rem; } -.product-card-price { flex: 0 0 auto; margin-inline-start: 10px; border-radius: 999px; padding: 5px 8px; color: var(--dashboard-warning); background: color-mix(in srgb, var(--dashboard-warning) 13%, var(--dashboard-surface)); font-size: .7rem; font-weight: 850; } +.product-card-price { + display: inline-flex; + flex: 0 0 auto; + gap: 6px; + align-items: center; + margin-inline-start: 10px; + border: 1px solid color-mix(in srgb, var(--dashboard-warning) 34%, var(--dashboard-line)); + border-radius: 999px; + padding: 5px 9px 5px 6px; + color: var(--dashboard-text); + background: color-mix(in srgb, var(--dashboard-warning) 10%, var(--dashboard-surface)); + font-size: .74rem; + font-weight: 900; + font-variant-numeric: tabular-nums; +} +.telegram-star-icon { + position: relative; + display: inline-grid; + place-items: center; + width: 20px; + height: 20px; + color: #ffc83d; + font-family: Arial, sans-serif; + font-size: 19px; + line-height: 1; + filter: drop-shadow(0 1px 1px rgb(117 70 0 / 35%)); + transform: rotate(-4deg); +} +.telegram-star-icon::after { + position: absolute; + inset: 4px auto auto 6px; + width: 4px; + height: 4px; + border-radius: 50%; + background: rgb(255 255 255 / 68%); + content: ""; +} +.telegram-star-value { letter-spacing: .01em; } .checkout-state { margin: 0; font-size: .74rem; } .referral-card { position: relative; @@ -691,15 +728,18 @@ html[dir="rtl"] .referral-status { text-align: right; } .language-spotlight .language-card { min-height: 62px; border: 0; background: transparent; } .language-spotlight .language-switch[aria-checked="true"] { border-radius: inherit; background: transparent; } .choice-list { margin-bottom: 9px; } -.language-card { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 9px; align-items: center; min-height: var(--dashboard-row-height); padding: 9px 12px; } +.language-card { display: grid; direction: ltr; grid-template-columns: minmax(0, 1fr) auto auto; gap: 9px; align-items: center; min-height: var(--dashboard-row-height); padding: 9px 12px; } .language-switch { width: 100%; border: 0; border-radius: 0; text-align: start; transition: background-color .16s ease, opacity .16s ease; } .language-switch:focus-visible { position: relative; z-index: 1; outline-offset: -3px; } .language-switch[aria-checked="true"] { opacity: 1; color: var(--dashboard-text); background: var(--dashboard-soft-accent); cursor: default; } .language-switch[aria-busy="true"] { opacity: .66; cursor: progress; } -.language-switch[dir="rtl"] { text-align: start; } .language-switch-error { display: flex; gap: 8px; align-items: center; justify-content: space-between; min-height: 44px; border-width: 1px 0 0; border-radius: 0; padding: 7px 11px; font-size: .7rem; } .language-switch-error button { flex: 0 0 auto; min-height: 44px; padding: 6px 9px; color: var(--dashboard-accent-text); background: var(--dashboard-accent); } .language-card strong { min-width: 0; overflow-wrap: anywhere; font-size: .88rem; } +.language-label { display: flex; gap: 8px; align-items: center; } +.language-flag { flex: 0 0 auto; } +.language-name { min-width: 0; unicode-bidi: isolate; } +.language-name[dir="rtl"] { text-align: right; } .language-count { color: var(--dashboard-muted); font-size: .72rem; font-variant-numeric: tabular-nums; } .language-action { width: 100%; } diff --git a/mydictionary/static/miniapp.js b/mydictionary/static/miniapp.js index 3b1d719..a951fda 100644 --- a/mydictionary/static/miniapp.js +++ b/mydictionary/static/miniapp.js @@ -767,6 +767,12 @@ return String(language.label || "").trim().replace(/\s*·\s*\d+\s*$/u, ""); } + function languageDisplayName(language) { + const label = languageDisplayLabel(language); + const flag = String(language.flag || "").trim(); + return flag && label.startsWith(flag) ? label.slice(flag.length).trim() : label; + } + function languageCard(language, copy) { const template = document.createElement("template"); template.innerHTML = ''; @@ -774,13 +780,23 @@ const card = control.switch; card.className = "language-card language-switch"; card.classList.add("dashboard-row"); - card.dir = language.direction; + // Keep the list grid stable: only the language name is RTL, never the count. + card.dir = "ltr"; card.setAttribute("aria-checked", String(language.current)); card.dataset.packId = language.switch_value; control.switch.disabled = language.current; const label = document.createElement("strong"); - label.dir = language.direction; - text(label, languageDisplayLabel(language)); + label.className = "language-label"; + label.dir = "ltr"; + const flag = document.createElement("span"); + flag.className = "language-flag"; + flag.dir = "ltr"; + text(flag, language.flag); + const name = document.createElement("span"); + name.className = "language-name"; + name.dir = language.direction; + text(name, languageDisplayName(language)); + label.append(flag, name); const count = document.createElement("span"); count.className = "language-count"; text(count, language.word_count); @@ -795,6 +811,21 @@ return card; } + function telegramStarsPrice(amount) { + const price = document.createElement("span"); + price.className = "product-card-price"; + price.setAttribute("aria-label", `${amount} Telegram Stars`); + const icon = document.createElement("span"); + icon.className = "telegram-star-icon"; + icon.setAttribute("aria-hidden", "true"); + text(icon, "★"); + const value = document.createElement("span"); + value.className = "telegram-star-value"; + text(value, amount); + price.append(icon, value); + return price; + } + function languageSwitchStatus(control, message, retryLabel, retry) { const existing = control.switch.parentElement ? control.switch.parentElement.querySelector(".language-switch-error") @@ -1076,9 +1107,7 @@ text(title, product.title); text(credits, `${product.credits} ✦`); productCopy.append(title, credits); - const price = document.createElement("span"); - price.className = "product-card-price"; - text(price, `${product.price_xtr} XTR`); + const price = telegramStarsPrice(product.price_xtr); button.append(productCopy, price); button.addEventListener("click", () => openAction(product.deep_link_action)); products.append(button); diff --git a/mydictionary/templates/miniapp.html b/mydictionary/templates/miniapp.html index d063814..9905e33 100644 --- a/mydictionary/templates/miniapp.html +++ b/mydictionary/templates/miniapp.html @@ -6,13 +6,13 @@ Lexi — языковой тренер - + {% for section in ('profile', 'words', 'credits', 'languages', 'settings') %} {% endfor %} - +
diff --git a/tests/test_miniapp_brand_controls_v1.py b/tests/test_miniapp_brand_controls_v1.py index b3abc02..8e3adea 100644 --- a/tests/test_miniapp_brand_controls_v1.py +++ b/tests/test_miniapp_brand_controls_v1.py @@ -48,7 +48,7 @@ def test_dictionary_shortcut_and_primary_actions_have_tactile_states(self) -> No self.assertIn("@media (prefers-reduced-motion: reduce)", CSS) def test_stylesheet_url_is_versioned_for_telegram_webview_cache(self) -> None: - self.assertIn("miniapp.css') }}?v=20260921-words-banner-v1", HTML) + self.assertIn("miniapp.css') }}?v=20260921-native-stars-rtl-v1", HTML) if __name__ == "__main__": diff --git a/tests/test_miniapp_secondary_tabs_v2.py b/tests/test_miniapp_secondary_tabs_v2.py index f3b066c..c15f964 100644 --- a/tests/test_miniapp_secondary_tabs_v2.py +++ b/tests/test_miniapp_secondary_tabs_v2.py @@ -52,20 +52,36 @@ def test_ac2_ac3_credits_use_wallet_hierarchy_and_honest_packages(self): self.assertIn('const allowedDetailViews = new Set(["help", "privacy"]);', self.js) self.assertIn("allowedDetailViews.has(requestedView)", self.js) - def test_ac4_current_language_is_spotlighted_without_losing_direction_or_count(self): + def test_ac2_prices_use_the_native_telegram_stars_mark_instead_of_xtr_copy(self): + for token in ( + "telegramStarsPrice(product.price_xtr)", + 'icon.className = "telegram-star-icon"', + 'value.className = "telegram-star-value"', + 'price.setAttribute("aria-label", `${amount} Telegram Stars`)', + ".telegram-star-icon", + ): + with self.subTest(token=token): + self.assertIn(token, f"{self.css}\n{self.js}") + self.assertNotIn('`${product.price_xtr} XTR`', self.js) + + def test_ac4_current_language_is_spotlighted_without_reversing_the_row_grid(self): for token in ( 'id="language-current"', 'class="language-spotlight"', 'data.languages.find((language) => language.current)', 'data.languages.filter((language) => !language.current)', - 'card.dir = language.direction', - 'languageDisplayLabel(language)', + 'card.dir = "ltr"', + 'flag.className = "language-flag"', + 'name.className = "language-name"', + 'name.dir = language.direction', + 'languageDisplayName(language)', r'.replace(/\s*·\s*\d+\s*$/u, "")', 'language.word_count', 'copy.language_current', ): with self.subTest(token=token): self.assertIn(token, f"{self.html}\n{self.js}") + self.assertNotIn("card.dir = language.direction", self.js) def test_ac5_settings_are_grouped_and_group_copy_is_complete(self): groups = ( diff --git a/tests/test_miniapp_unified_section_headers_v1.py b/tests/test_miniapp_unified_section_headers_v1.py index c2faeab..a5a30e4 100644 --- a/tests/test_miniapp_unified_section_headers_v1.py +++ b/tests/test_miniapp_unified_section_headers_v1.py @@ -71,7 +71,7 @@ def test_shared_hero_is_compact_and_uses_a_fresh_stylesheet_version(self) -> Non self.assertIn("min-height: 148px", header) self.assertIn("height: clamp(148px, 38vw, 208px)", header) - self.assertIn("miniapp.css') }}?v=20260921-words-banner-v1", HTML) + self.assertIn("miniapp.css') }}?v=20260921-native-stars-rtl-v1", HTML) if __name__ == "__main__": diff --git a/tests/test_telegram_miniapp_v1.py b/tests/test_telegram_miniapp_v1.py index b757a7b..c9b6b1f 100644 --- a/tests/test_telegram_miniapp_v1.py +++ b/tests/test_telegram_miniapp_v1.py @@ -1524,7 +1524,7 @@ def test_ac7_languages_use_one_flag_and_a_dedicated_localized_current_label(self violations.append("double-flag-render") if ( re.search( - r"text\(label,\s*(?:language\.label|languageDisplayLabel\(language\))\)", + r"text\((?:label|name),\s*(?:language\.label|languageDisplayLabel\(language\)|languageDisplayName\(language\))\)", js, ) is None