refactor(pdf): render every resume with semantic css and drop the legacy styler

Now that the API hands out every resume with its legacy style rules
converted, the renderer runs one styling system: the stylesheet mode,
the per-slot legacy rule hooks in the template primitives and the
section style provider are gone, and the Custom Styles editor loses its
legacy draft banner and Activate step. The converter stays (the API
uses it), with its fixtures now checked by rendering the converted
stylesheets.
This commit is contained in:
Amruth Pillai
2026-09-29 15:25:44 +02:00
parent db97381b3d
commit a325232a09
98 changed files with 257 additions and 1310 deletions
-12
View File
@@ -738,10 +738,6 @@ msgstr "Rekening"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktiveer Semantiese CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Omgeskakelde stylbladkonsep"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Jou ou style bly aktief totdat jy hierdie Semantic CSS-konsep aktiveer."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Jou lyne is baie styf gepak."
-12
View File
@@ -738,10 +738,6 @@ msgstr "መለያ"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "የሴማንቲክ CSS ን ያግብሩ"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "የተቀየረ የቅጥ ሉህ ረቂቅ"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "የቀድሞ ቅጦችዎ ይህን የ Semantic CSS ረቂቅ እስኪያነቁ ድረስ በሥራ ላይ ይቆያሉ።"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "መስመሮችዎ በጣም በጥብቅ የታሸጉ ናቸው።"
-12
View File
@@ -738,10 +738,6 @@ msgstr "الحساب"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "تفعيل CSS الدلالي"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "مسودة ورقة الأنماط المحولة"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "تبقى أنماطك القديمة فعّالة حتى تفعّل مسودة Semantic CSS هذه."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "خطوطك معبأة بإحكام شديد."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Hesab"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Semantik CSS-i aktivləşdirin"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Çevirilmiş stil cədvəli qaralaması"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Köhnə üslublarınız siz bu Semantic CSS qaralamasını aktivləşdirənə qədər qüvvədə qalır."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Sətirləriniz çox sıx yığılıb."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Акаунт"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Активиране на семантичен CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Конвертирана чернова на стилов лист"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Старите ви стилове остават активни, докато не активирате тази чернова със Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Вашите линии са опаковани много плътно."
-12
View File
@@ -738,10 +738,6 @@ msgstr "অ্যাকাউন্ট"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "সিমান্টিক CSS সক্রিয় করুন"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "রূপান্তরিত স্টাইলশীট খসড়া"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "আপনি এই Semantic CSS খসড়া সক্রিয় না করা পর্যন্ত আপনার পুরোনো স্টাইলগুলোই চালু থাকবে।"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "আপনার লাইন খুব শক্তভাবে প্যাক করা হয়."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Compte"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activa el CSS semàntic"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Esborrany de full d'estil convertit"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Els teus estils antics continuen actius fins que activis aquest esborrany de CSS semàntic."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Les teves línies estan molt atapeïdes."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Účet"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivace sémantického CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konverze konceptu stylů"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Vaše původní styly zůstávají aktivní, dokud neaktivujete tento koncept Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Vaše linky jsou nabité velmi pevně."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Konto"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivér semantisk CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konverteret stylesheet-kladde"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Dine hidtidige typografier forbliver aktive, indtil du aktiverer dette Semantic CSS-udkast."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Dine linjer er pakket meget tæt."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Konto"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Semantisches CSS aktivieren"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konvertierter Stylesheet-Entwurf"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Ihre bisherigen Stile bleiben aktiv, bis Sie diesen Semantic-CSS-Entwurf aktivieren."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Deine Zeilen sind sehr dicht gepackt."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Λογαριασμός"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Ενεργοποίηση Σημασιολογικού CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Μετατρεπόμενο προσχέδιο φύλλου στυλ"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Τα παλαιά σας στυλ παραμένουν ενεργά μέχρι να ενεργοποιήσετε αυτό το πρόχειρο Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Οι γραμμές σας είναι γεμάτες πολύ σφιχτά."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Account"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activate Semantic CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Converted stylesheet draft"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Your legacy styles stay active until you activate this Semantic CSS draft."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Your lines are packed very tightly."
-12
View File
@@ -733,10 +733,6 @@ msgstr "Account"
msgid "Actions"
msgstr "Actions"
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activate Semantic CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr "Activity"
@@ -1990,10 +1986,6 @@ msgstr "Conversation"
msgid "Conversations"
msgstr "Conversations"
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Converted stylesheet draft"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9044,10 +9036,6 @@ msgstr "Your headline changed on the resume. The letter updated too."
msgid "Your keys, stored encrypted"
msgstr "Your keys, stored encrypted"
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Your legacy styles stay active until you activate this Semantic CSS draft."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Your lines are packed very tightly."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Cuenta"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activar CSS semántico"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Borrador de hoja de estilo convertido"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Tus estilos antiguos siguen activos hasta que actives este borrador de CSS semántico."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Tus líneas están muy apretadas."
-12
View File
@@ -738,10 +738,6 @@ msgstr "حساب کاربری"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "فعال کردن CSS معنایی"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "پیش‌نویس شیوه‌نامه تبدیل‌شده"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "سبک‌های قدیمی شما تا زمانی که این پیش‌نویس Semantic CSS را فعال کنید، فعال می‌مانند."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "خطوط شما بیش از حد فشرده‌اند."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Tili"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivoi semanttinen CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Muunnettu tyylitiedoston luonnos"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Vanhat tyylisi pysyvät voimassa, kunnes otat tämän Semantic CSS -luonnoksen käyttöön."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Sinun rivisi on pakattu erittäin tiukasti."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Compte"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activer le CSS sémantique"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Brouillon de feuille de style converti"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Vos anciens styles restent actifs tant que vous n'activez pas ce brouillon de CSS sémantique."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Vos lignes sont très serrées."
-12
View File
@@ -738,10 +738,6 @@ msgstr "חשבון"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "הפעלת CSS סמנטי"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "טיוטת גיליון סגנונות שהומרה"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "הסגנונות הישנים שלך נשארים פעילים עד שתפעיל את טיוטת ה-Semantic CSS הזו."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "השורות שלך צפופות מאוד."
-12
View File
@@ -738,10 +738,6 @@ msgstr "खाता"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "सिमेंटिक सीएसएस को सक्रिय करें"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "रूपांतरित स्टाइलशीट ड्राफ्ट"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "जब तक आप यह Semantic CSS ड्राफ़्ट सक्रिय नहीं करते, तब तक आपकी पुरानी स्टाइल ही लागू रहेंगी।"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "आपकी पंक्तियाँ बहुत कसकर भरी हुई हैं।"
-12
View File
@@ -738,10 +738,6 @@ msgstr "Fiók"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Szemantikus CSS aktiválása"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Stíluslap-tervezet konvertálása"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "A korábbi stílusai aktívak maradnak, amíg nem aktiválja ezt a Semantic CSS piszkozatot."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "A soraid nagyon szorosan vannak csomagolva."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Akun"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktifkan CSS Semantik"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Draf stylesheet yang telah dikonversi"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Gaya lama Anda tetap aktif sampai Anda mengaktifkan draf Semantic CSS ini."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Garis Anda dikemas sangat rapat."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Account"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Attiva il CSS semantico"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Bozza del foglio di stile convertito"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "I tuoi stili precedenti restano attivi finché non attivi questa bozza di CSS semantico."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Le tue battute sono molto fitte."
-12
View File
@@ -738,10 +738,6 @@ msgstr "アカウント"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "セマンティックCSSを有効にする"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "変換されたスタイルシートのドラフト"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "このセマンティック CSS の下書きを有効にするまで、これまでのスタイルがそのまま適用されます。"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "あなたのラインは非常に密集しています。"
-12
View File
@@ -738,10 +738,6 @@ msgstr "គណនី"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "ធ្វើឱ្យ CSS ន័យវិទ្យាសកម្ម"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "សេចក្តីព្រាងសន្លឹករចនាប័ទ្មដែលបានបម្លែង"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "រចនាប័ទ្មចាស់របស់អ្នកនៅតែដំណើរការ រហូតដល់អ្នកបើកដំណើរការសេចក្ដីព្រាង Semantic CSS នេះ។"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "បន្ទាត់របស់អ្នកត្រូវបានខ្ចប់យ៉ាងតឹងរ៉ឹង។"
-12
View File
@@ -738,10 +738,6 @@ msgstr "ಖಾತೆ"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "ಲಾಕ್ಷಣಿಕ CSS ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಿ"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "ಪರಿವರ್ತಿಸಲಾದ ಸ್ಟೈಲ್‌ಶೀಟ್ ಡ್ರಾಫ್ಟ್"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "ಈ Semantic CSS ಕರಡನ್ನು ನೀವು ಸಕ್ರಿಯಗೊಳಿಸುವವರೆಗೆ ನಿಮ್ಮ ಹಳೆಯ ಶೈಲಿಗಳು ಸಕ್ರಿಯವಾಗಿಯೇ ಇರುತ್ತವೆ."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "ನಿಮ್ಮ ಸಾಲುಗಳನ್ನು ತುಂಬಾ ಬಿಗಿಯಾಗಿ ಪ್ಯಾಕ್ ಮಾಡಲಾಗಿದೆ."
-12
View File
@@ -738,10 +738,6 @@ msgstr "계정"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "시맨틱 CSS 활성화"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "변환된 스타일시트 초안"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "이 시맨틱 CSS 초안을 활성화하기 전까지는 기존 스타일이 그대로 적용됩니다."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "귀하의 라인은 매우 촘촘하게 포장되어 있습니다."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Paskyra"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Semantinio CSS aktyvavimas"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konvertuotas stilių lapo juodraštis"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Seni jūsų stiliai lieka aktyvūs, kol neaktyvuosite šio Semantic CSS juodraščio."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Jūsų eilutės supakuotos labai sandariai."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Konts"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivizēt semantisko CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konvertēts stila lapas melnraksts"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Jūsu vecie stili paliek aktīvi, līdz aktivizēsiet šo Semantic CSS melnrakstu."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Jūsu rindas ir ļoti cieši iesaiņotas."
-12
View File
@@ -738,10 +738,6 @@ msgstr "അക്കൗണ്ട്"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "സെമാന്റിക് CSS സജീവമാക്കുക"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "പരിവർത്തനം ചെയ്ത സ്റ്റൈൽഷീറ്റ് ഡ്രാഫ്റ്റ്"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "ഈ Semantic CSS ഡ്രാഫ്റ്റ് നിങ്ങൾ സജീവമാക്കുന്നതുവരെ പഴയ സ്റ്റൈലുകൾ സജീവമായി തുടരും."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "നിങ്ങളുടെ വരികൾ വളരെ കർശനമായി പായ്ക്ക് ചെയ്തിട്ടുണ്ട്."
-12
View File
@@ -738,10 +738,6 @@ msgstr "खाते"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "सिमँटिक CSS सक्रिय करा"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "रूपांतरित स्टाईलशीटचा मसुदा"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "तुम्ही हा Semantic CSS मसुदा सक्रिय करेपर्यंत तुमच्या जुन्या स्टाइल्स लागू राहतील."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "तुमच्या ओळी खूप घट्ट बांधल्या आहेत."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Akaun"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktifkan CSS Semantik"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Draf helaian gaya yang ditukar"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Gaya lama anda kekal aktif sehingga anda mengaktifkan draf Semantic CSS ini."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Talian anda padat dengan sangat ketat."
-12
View File
@@ -738,10 +738,6 @@ msgstr "खाता"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "सिमेन्टिक CSS सक्रिय गर्नुहोस्"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "रूपान्तरित स्टाइलसिट ड्राफ्ट"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "तपाईंले यो Semantic CSS मस्यौदा सक्रिय नगरेसम्म तपाईंका पुराना शैलीहरू नै लागू रहनेछन्।"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "तपाईंको लाइनहरू धेरै कडा प्याक गरिएका छन्।"
-12
View File
@@ -738,10 +738,6 @@ msgstr "Rekening"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activeer semantische CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Omgezette stylesheet-concept"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Je bestaande stijlen blijven actief totdat je dit Semantic CSS-concept activeert."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Je paginamarges zijn erg smal."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Konto"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktiver semantisk CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konvertert stilarkutkast"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "De gamle stilene dine forblir aktive til du aktiverer dette Semantic CSS-utkastet."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Linjeavstanden din er svært tett."
-12
View File
@@ -738,10 +738,6 @@ msgstr "ଆକାଉଣ୍ଟ"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "ସିମାଣ୍ଟିକ୍ CSS ସକ୍ରିୟ କରନ୍ତୁ"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "ରୂପାନ୍ତରିତ ଷ୍ଟାଇଲସିଟ୍ ଡ୍ରାଫ୍ଟ"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "ଆପଣ ଏହି Semantic CSS ଡ୍ରାଫ୍ଟକୁ ସକ୍ରିୟ ନକରିବା ପର୍ଯ୍ୟନ୍ତ ଆପଣଙ୍କ ପୁରୁଣା ଶୈଳୀଗୁଡିକ ସକ୍ରିୟ ରହିଥାଏ।"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "ଆପଣଙ୍କ ଲାଇନ୍‌ଗୁଡ଼ିକ ଅତ୍ୟଧିକ ଘନ।"
-12
View File
@@ -738,10 +738,6 @@ msgstr "Konto"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktywuj semantyczny CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Przekonwertowany projekt arkusza stylów"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Twoje dotychczasowe style pozostają aktywne, dopóki nie aktywujesz tej wersji roboczej Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Wiersze są bardzo ciasno ułożone."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Conta"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Ativar CSS Semântico"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Rascunho da folha de estilo convertida"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Seus estilos antigos continuam ativos até você ativar este rascunho de CSS semântico."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Suas linhas estão muito compactadas."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Conta"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Ativar CSS Semântico"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Rascunho da folha de estilo convertida"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Os seus estilos antigos continuam ativos até ativar este rascunho de CSS semântico."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Suas falas estão muito compactadas."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Cont"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Activează CSS-ul semantic"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Schiță de foaie de stil convertită"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Stilurile vechi rămân active până când activați această ciornă Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Liniile tale sunt foarte strâns."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Аккаунт"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Активировать семантический CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Преобразованный черновик таблицы стилей"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Ваши прежние стили остаются активными, пока вы не активируете этот черновик Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Ваши строки упакованы очень плотно."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Účet"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivovať sémantický CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konvertovaný koncept štýlového hárka"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Vaše pôvodné štýly zostávajú aktívne, kým neaktivujete tento koncept Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Vaše linky sú nahustené."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Račun"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktiviraj semantični CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Pretvorjen osnutek slogovne predloge"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Vaši stari slogi ostanejo aktivni, dokler ne aktivirate tega osnutka Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Vaše vrstice so zelo natrpane."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Llogaria"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivizo CSS-në Semantike"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Drafti i konvertuar i fletës së stilit"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Stilet tuaja të vjetra mbeten aktive derisa ta aktivizoni këtë draft Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Linjat tuaja janë të mbushura shumë fort."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Налог"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Активирајте семантички CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Конвертована нацртна верзија стилског листа"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Ваши стари стилови остају активни док не активирате овај Semantic CSS нацрт."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Ваше линије су веома збијене."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Konto"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Aktivera semantisk CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Konverterat utkast till stilark"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Dina tidigare stilar förblir aktiva tills du aktiverar det här Semantic CSS-utkastet."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Dina linjer är mycket tätt packade."
-12
View File
@@ -738,10 +738,6 @@ msgstr "கணக்கு"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "செமாண்டிக் CSS-ஐ செயல்படுத்துக"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "மாற்றப்பட்ட ஸ்டைல்ஷீட் வரைவு"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "இந்த Semantic CSS வரைவை நீங்கள் செயல்படுத்தும் வரை உங்கள் பழைய பாணிகள் செயலில் இருக்கும்."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "உங்கள் வரிகள் மிகவும் இறுக்கமாக நிரம்பியுள்ளன."
-12
View File
@@ -738,10 +738,6 @@ msgstr "ఖాతా"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "సెమాంటిక్ CSSని యాక్టివేట్ చేయండి"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "మార్చబడిన స్టైల్‌షీట్ డ్రాఫ్ట్"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "ఈ Semantic CSS ముసాయిదాను మీరు సక్రియం చేసే వరకు మీ పాత శైలులు అమల్లోనే ఉంటాయి."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "మీ లైన్లు చాలా గట్టిగా ప్యాక్ చేయబడ్డాయి."
-12
View File
@@ -738,10 +738,6 @@ msgstr "บัญชี"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "เปิดใช้งาน Semantic CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "แปลงร่างสไตล์ชีตแล้ว"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "สไตล์เดิมของคุณจะยังใช้งานอยู่จนกว่าคุณจะเปิดใช้งานฉบับร่าง Semantic CSS นี้"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "เส้นของคุณแน่นมาก"
-12
View File
@@ -738,10 +738,6 @@ msgstr "Hesap"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Semantik CSS'i Etkinleştir"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Dönüştürülmüş stil sayfası taslağı"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Bu Semantic CSS taslağını etkinleştirene kadar eski stilleriniz etkin kalır."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Hatlarınız çok sıkı paketlenmiş."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Обліковий запис"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Активуйте семантичний CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Конвертований чернетка таблиці стилів"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Ваші попередні стилі залишаються активними, доки ви не активуєте цю чернетку Semantic CSS."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Ваші рядки зібрані дуже щільно."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Hisob"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Semantik CSS-ni faollashtirish"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "O'zgartirilgan uslublar jadvali qoralamasi"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Eski uslublaringiz siz ushbu Semantic CSS qoralamasini faollashtirmaguningizcha amalda qoladi."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Sizning chiziqlaringiz juda qattiq o'ralgan."
-12
View File
@@ -738,10 +738,6 @@ msgstr "Tài khoản"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "Kích hoạt CSS ngữ nghĩa"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "Bản nháp bảng định kiểu đã chuyển đổi"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "Các kiểu cũ của bạn vẫn còn hiệu lực cho đến khi bạn kích hoạt bản nháp Semantic CSS này."
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "Đường dây của bạn được đóng gói rất chặt chẽ."
-12
View File
@@ -738,10 +738,6 @@ msgstr "账户"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "启用语义化 CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "已转换的样式表草稿"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "在你启用这份语义化 CSS 草稿之前,原有的样式会继续生效。"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "你的线路排得很紧。"
-12
View File
@@ -738,10 +738,6 @@ msgstr "帳戶"
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr "啟用語意化 CSS"
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1995,10 +1991,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr "已轉換的樣式表草稿"
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9049,10 +9041,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr "在您啟用這份語意化 CSS 草稿之前,原有的樣式會繼續生效。"
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr "你的線路排得很緊。"
-12
View File
@@ -733,10 +733,6 @@ msgstr ""
msgid "Actions"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Activate Semantic CSS"
msgstr ""
#: src/features/applications/components/detail/activity.tsx
msgid "Activity"
msgstr ""
@@ -1990,10 +1986,6 @@ msgstr ""
msgid "Conversations"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Converted stylesheet draft"
msgstr ""
#: src/features/resume/public/public-resume.tsx
#: src/features/resume/share/link-tab.tsx
#: src/features/settings/ai/api-keys.tsx
@@ -9044,10 +9036,6 @@ msgstr ""
msgid "Your keys, stored encrypted"
msgstr ""
#: src/features/resume/stylesheet/legacy-banner.tsx
msgid "Your legacy styles stay active until you activate this Semantic CSS draft."
msgstr ""
#: src/features/ats-checker/messages.ts
msgid "Your lines are packed very tightly."
msgstr ""
@@ -58,16 +58,6 @@ vi.mock("./worker-client", () => ({
}),
}));
const fatalError: SemanticCssDiagnostic = {
code: "VERSION_MISMATCH",
severity: "error",
message: "Version mismatch",
range: {
start: { line: 2, column: 3, offset: 17 },
end: { line: 2, column: 9, offset: 23 },
},
};
const guideName = /read the applying custom styles guide.*opens in new tab/i;
const expectGuideLink = (root: HTMLElement) => {
@@ -294,7 +284,6 @@ describe("StylesheetEditorShell", () => {
</I18nProvider>,
);
expect(screen.queryByRole("button", { name: /activate semantic css/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /save|apply/i })).not.toBeInTheDocument();
});
@@ -321,7 +310,7 @@ describe("StylesheetEditorShell", () => {
expect(builder.data.metadata.stylesheet.source.text).toBe("name { color: blue; }\n");
});
it("switches a converted legacy draft through the ordinary resume update", async () => {
it("writes a first edit to a resume without a stylesheet as Semantic CSS", () => {
render(
<I18nProvider i18n={i18n}>
<TooltipProvider>
@@ -329,35 +318,17 @@ describe("StylesheetEditorShell", () => {
</TooltipProvider>
</I18nProvider>,
);
const view = EditorView.findFromDOM(screen.getByRole("textbox", { name: "Semantic CSS stylesheet" }));
if (!view) throw new Error("Missing editor view");
const activate = await screen.findByRole("button", { name: "Activate Semantic CSS" });
await waitFor(() => expect(activate).toBeEnabled());
fireEvent.click(activate);
act(() => view.dispatch({ changes: { from: 0, insert: "name { color: blue; }\n" } }));
expect(builder.data?.metadata.stylesheet).toEqual({
mode: "semantic",
source: { languageVersion: 1, text: "" },
source: { languageVersion: 1, text: "name { color: blue; }\n" },
});
});
it("does not block legacy activation on compiler errors", async () => {
compileWorker.diagnostics = [fatalError];
render(
<I18nProvider i18n={i18n}>
<TooltipProvider>
<StylesheetEditorShell />
</TooltipProvider>
</I18nProvider>,
);
const activate = await screen.findByRole("button", { name: "Activate Semantic CSS" });
expect(activate).toBeEnabled();
fireEvent.click(activate);
expect(builder.data?.metadata.stylesheet?.mode).toBe("semantic");
});
it("makes editor mutation controls read-only while the resume is locked", () => {
builder.isLocked = true;
builder.canUndo = true;
@@ -375,7 +346,6 @@ describe("StylesheetEditorShell", () => {
"contenteditable",
"false",
);
expect(screen.getByRole("button", { name: "Activate Semantic CSS" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Undo stylesheet edit" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Redo stylesheet edit" })).toBeDisabled();
expect(screen.getByRole("button", { name: "Format stylesheet" })).toBeDisabled();
@@ -397,7 +367,6 @@ describe("StylesheetEditorShell", () => {
const sheet = await screen.findByRole("dialog");
expect(within(sheet).getByRole("heading", { name: "Semantic CSS stylesheet" })).toBeInTheDocument();
expect(within(sheet).getByRole("button", { name: "Activate Semantic CSS" })).toBeInTheDocument();
expect(within(sheet).getByRole("toolbar", { name: "Stylesheet editor" })).toBeInTheDocument();
expectGuideLink(sheet);
expect(document.querySelectorAll(".cm-editor")).toHaveLength(1);
@@ -20,7 +20,6 @@ import { tags } from "@lezer/highlight";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { convertLegacyStyleRules } from "@reactive-resume/pdf/semantic-legacy";
import {
buildSemanticTree,
getTemplateSemanticManifest,
@@ -43,7 +42,6 @@ import {
} from "./editor-extensions";
import { formatEditorDocument } from "./formatter";
import { matchedNodeKeys } from "./highlight";
import { LegacyStylesheetBanner } from "./legacy-banner";
import { listStyleTargets, styleTargetFor } from "./targets";
import { StylesheetToolbar } from "./toolbar";
import { createCompileWorkerClient } from "./worker-client";
@@ -430,10 +428,10 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps)
return () => client.destroy();
}, []);
const stylesheet = data?.metadata.stylesheet;
const mode = stylesheet?.mode ?? "legacy";
const source = useMemo<StylesheetSource>(
() => stylesheet?.source ?? (data ? convertLegacyStyleRules(data).source : { languageVersion: 1, text: "" }),
[data, stylesheet],
// Resumes reach the builder with legacy rules already converted (the API does it), so there is one stylesheet.
() => stylesheet?.source ?? { languageVersion: 1, text: "" },
[stylesheet],
);
const metadata = useMemo(() => (data ? createEditorMetadata(data) : emptyMetadata), [data]);
const disabled = readOnly || isLocked;
@@ -503,14 +501,7 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps)
const setSourceText = (next: string) => {
if (disabled || next === text) return;
updateResumeData((draft) => {
draft.metadata.stylesheet = { mode, source: { ...source, text: next } };
});
};
const activate = () => {
if (disabled || mode === "semantic") return;
updateResumeData((draft) => {
draft.metadata.stylesheet = { mode: "semantic", source };
draft.metadata.stylesheet = { mode: "semantic", source: { ...source, text: next } };
});
};
@@ -536,8 +527,6 @@ function StylesheetEditorShell({ readOnly = false }: StylesheetEditorShellProps)
);
const editorChrome = (
<div className="space-y-3">
{mode === "legacy" && <LegacyStylesheetBanner disabled={disabled} onActivate={activate} />}
<StylesheetToolbar
source={text}
canUndo={canUndo}
@@ -1,29 +0,0 @@
import { Trans } from "@lingui/react/macro";
import { Alert, AlertDescription, AlertTitle } from "@reactive-resume/ui/components/alert";
import { Button } from "@reactive-resume/ui/components/button";
import { Icon } from "@reactive-resume/ui/components/icon";
export type LegacyStylesheetBannerProps = {
disabled: boolean;
onActivate(): void;
};
export function LegacyStylesheetBanner({ disabled, onActivate }: LegacyStylesheetBannerProps) {
return (
<Alert>
<Icon name="info" size={16} />
<AlertTitle>
<Trans>Converted stylesheet draft</Trans>
</AlertTitle>
<AlertDescription className="space-y-3">
<p>
<Trans>Your legacy styles stay active until you activate this Semantic CSS draft.</Trans>
</p>
<Button type="button" size="sm" disabled={disabled} onClick={onActivate}>
<Trans>Activate Semantic CSS</Trans>
<Icon name="arrow_forward" size={16} />
</Button>
</AlertDescription>
</Alert>
);
}
@@ -6,7 +6,6 @@ import { useStore } from "@tanstack/react-form";
import { AnimatePresence, m } from "motion/react";
import { colorDesignSchema, levelDesignSchema } from "@reactive-resume/schema/resume/data";
import { resolveLevelDisplaySizes } from "@reactive-resume/schema/resume/level-display-sizes";
import { resolveStyleRuleFontSize } from "@reactive-resume/schema/resume/style-rules";
import { FormControl, FormItem, FormLabel, FormMessage } from "@reactive-resume/ui/components/form";
import { Input } from "@reactive-resume/ui/components/input";
import { Separator } from "@reactive-resume/ui/components/separator";
@@ -235,12 +234,8 @@ function LevelSectionForm() {
const previewType = useStore(form.store, (s) => s.values.type);
const previewIcon = useStore(form.store, (s) => s.values.icon);
const iconFontSize = resolveStyleRuleFontSize(resume.data, { slot: "icon" });
const levelFontSize = resolveStyleRuleFontSize(resume.data, { slot: "level" });
const { decorationSize, levelIconExplicitSize } = resolveLevelDisplaySizes({
bodyFontSize: resume.data.metadata.typography.body.fontSize,
iconFontSize,
levelFontSize,
});
return (
+3 -6
View File
@@ -10,7 +10,7 @@ import { Document } from "#react-pdf-renderer";
import { RenderProvider } from "./context";
import { resolvePdfFonts, resumeContentContainsCJK, resumeContentScripts } from "./hooks/use-register-fonts";
import { SemanticRenderProvider } from "./semantic/context";
import { resolveResumeRuntime, resolveStylesheetMode } from "./semantic/resolve";
import { resolveResumeRuntime } from "./semantic/resolve";
import { getTemplatePage } from "./templates";
import { shouldShowResumeHeader } from "./templates/shared/cover-letter";
import { getTemplatePageMinHeightStyle, getTemplatePageSize } from "./templates/shared/page-size";
@@ -63,17 +63,14 @@ export const ResumeDocument = ({
() => (renderOptions ? { ...resumeData, renderOptions } : resumeData),
[resumeData, renderOptions],
);
const stylesheetMode = resolveStylesheetMode(resumeData);
// The tree is built with the render options, so a letter printed with its header keeps it.
const runtime = useMemo(
() => semanticRuntime ?? resolveResumeRuntime({ data: headerResumeData, template, mode: stylesheetMode }),
[headerResumeData, semanticRuntime, stylesheetMode, template],
() => semanticRuntime ?? resolveResumeRuntime({ data: headerResumeData, template }),
[headerResumeData, semanticRuntime, template],
);
const semanticMode = semanticRuntime ? "semantic" : stylesheetMode;
return (
<SemanticRenderProvider
presentation={runtime.presentation}
mode={semanticMode}
sourceTree={runtime.sourceTree}
renderTree={runtime.renderTree}
>
@@ -16,7 +16,6 @@ describe("Semantic CSS all-template presentation", () => {
data,
template,
source: comprehensiveStylesheet,
mode: "semantic",
});
expect(runtime.diagnostics.filter(({ severity }) => severity === "error")).toEqual([]);
@@ -35,7 +34,6 @@ describe("Semantic CSS all-template presentation", () => {
data,
template,
source: comprehensiveStylesheet,
mode: "semantic",
});
const templateParts = new Set<string>();
@@ -36,7 +36,7 @@ const findTexts = (node: HostNode, text: string): HostNode[] => [
...(node.children ?? []).flatMap((child) => findTexts(child, text)),
];
const fixture = (mode: "legacy" | "semantic", section: "experience" | "education", rule = ""): ResumeData => {
const fixture = (section: "experience" | "education", rule = ""): ResumeData => {
const data = structuredClone(defaultResumeData);
data.picture.hidden = true;
data.basics.name = "Ada Lovelace";
@@ -69,10 +69,7 @@ const fixture = (mode: "legacy" | "semantic", section: "experience" | "education
},
];
data.metadata.layout.pages = [{ fullWidth: true, main: [section], sidebar: [] }];
if (mode === "semantic") {
const stylesheet = { languageVersion: 1, text: `@version 1; ${rule}` };
data.metadata.stylesheet = { mode, source: stylesheet };
}
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${rule}` } };
return data;
};
@@ -83,14 +80,6 @@ const renderHost = async (template: Template, data: ResumeData): Promise<HostNod
return instance.container.document as HostNode;
};
const renderPdf = async (template: Template, data: ResumeData): Promise<Uint8Array> => {
const renderer = await vi.importActual<typeof import("../forme/testing")>("../forme/testing");
const element = createElement(ResumeDocument, { data, template }) as unknown as Parameters<
typeof renderer.renderToBuffer
>[0];
return new Uint8Array(await renderer.renderToBuffer(element));
};
const rasterizeFirstPage = async (bytes: Uint8Array): Promise<Buffer> => {
const document = await getDocument({ data: bytes }).promise;
const page = await document.getPage(1);
@@ -197,8 +186,8 @@ describe("combined PDF field bindings", () => {
combined-text { color: #334455; font-size: 14pt; opacity: 0.6; margin-left: 3pt; }
field[name="degree"] { opacity: 0.4; }
`;
const onyxData = fixture("semantic", "education", semanticRule);
const meowthData = fixture("semantic", "education", semanticRule);
const onyxData = fixture("education", semanticRule);
const meowthData = fixture("education", semanticRule);
const onyxTree = buildSemanticTree({
data: onyxData,
template: "onyx",
@@ -274,9 +263,9 @@ describe("combined PDF field bindings", () => {
field[name="grade"] { color: #550000; }
field[name="period"] { color: #660000; }
`;
const experience = await renderHost("meowth", fixture("semantic", "experience", colors));
const inlineEducation = await renderHost("meowth", fixture("semantic", "education", colors));
const splitEducation = await renderHost("onyx", fixture("semantic", "education", colors));
const experience = await renderHost("meowth", fixture("experience", colors));
const inlineEducation = await renderHost("meowth", fixture("education", colors));
const splitEducation = await renderHost("onyx", fixture("education", colors));
expect(nodeText(experience)).toContain("Engineer (London)");
expectColor(experience, "Engineer", "#110000");
@@ -299,7 +288,6 @@ describe("combined PDF field bindings", () => {
it("uses the promoted single field's real semantic key", async () => {
const experienceData = fixture(
"semantic",
"experience",
'field[name="period"] { color: #660000; } field[name="location"] { color: #220000; }',
);
@@ -308,7 +296,6 @@ describe("combined PDF field bindings", () => {
experience.location = "";
const educationData = fixture(
"semantic",
"education",
'field[name="period"] { color: #660000; } field[name="degree"] { color: #440000; }',
);
@@ -325,11 +312,7 @@ describe("combined PDF field bindings", () => {
it("keeps non-inheritable split-field styles on their final field hosts", async () => {
const document = await renderHost(
"meowth",
fixture(
"semantic",
"experience",
'field[name="position"] { opacity: 0.6; } field[name="location"] { margin-left: 3pt; }',
),
fixture("experience", 'field[name="position"] { opacity: 0.6; } field[name="location"] { margin-left: 3pt; }'),
);
const combined = findTexts(document, "Engineer (London)");
const position = findTexts(document, "Engineer");
@@ -342,7 +325,7 @@ describe("combined PDF field bindings", () => {
});
it("publishes one existing Text binding for every split field key", () => {
const data = fixture("semantic", "education");
const data = fixture("education");
const tree = buildSemanticTree({
data,
template: "meowth",
@@ -361,20 +344,6 @@ describe("combined PDF field bindings", () => {
);
});
it.each([
["meowth", "experience"],
["meowth", "education"],
["onyx", "education"],
] as const)(
"keeps the real %s %s section raster-identical between legacy and empty semantic mode",
async (template, section) => {
const legacy = await rasterizeFirstPage(await renderPdf(template, fixture("legacy", section)));
const semantic = await rasterizeFirstPage(await renderPdf(template, fixture("semantic", section)));
expect(semantic.equals(legacy)).toBe(true);
},
);
it.each([
{
name: "Meowth experience position and location",
+7 -13
View File
@@ -1,5 +1,4 @@
import type { SemanticNode } from "@reactive-resume/resume/stylesheet";
import type { StylesheetMode } from "@reactive-resume/schema/resume/stylesheet";
import type { ReactNode } from "react";
import type { ResolvedPdfNodePresentation } from "./adapter";
import { createContext, use, useMemo } from "react";
@@ -9,7 +8,6 @@ export type ResolvedResumePresentation = Readonly<Record<string, ResolvedPdfNode
type SemanticRenderProviderProps = {
presentation: ResolvedResumePresentation;
mode: StylesheetMode;
sourceTree: SemanticNode;
renderTree: SemanticNode;
children: ReactNode;
@@ -18,7 +16,6 @@ type SemanticRenderProviderProps = {
const EMPTY_NODE = Object.freeze({}) satisfies ResolvedPdfNodePresentation;
const SemanticRenderContext = createContext<{
presentation: ResolvedResumePresentation;
mode: StylesheetMode;
renderedNodeKeys: ReadonlySet<string>;
sourceNodeKeys: ReadonlySet<string>;
renderedChildKeys: ReadonlyMap<string, readonly string[]>;
@@ -61,7 +58,6 @@ const indexRenderTree = (root: SemanticNode) => {
export function SemanticRenderProvider({
presentation,
mode,
sourceTree,
renderTree,
children,
@@ -80,8 +76,8 @@ export function SemanticRenderProvider({
[sourceNodes],
);
const value = useMemo(
() => ({ presentation, mode, sourceNodeKeys, sourceChildKeys, sourceNodes, ...treeIndex }),
[mode, presentation, sourceChildKeys, sourceNodeKeys, sourceNodes, treeIndex],
() => ({ presentation, sourceNodeKeys, sourceChildKeys, sourceNodes, ...treeIndex }),
[presentation, sourceChildKeys, sourceNodeKeys, sourceNodes, treeIndex],
);
return <SemanticRenderContext.Provider value={value}>{children}</SemanticRenderContext.Provider>;
}
@@ -92,11 +88,9 @@ export function useResolvedNode(nodeKey: string | undefined): ResolvedPdfNodePre
return context.presentation[nodeKey] ?? EMPTY_NODE;
}
export const useSemanticRenderMode = (): StylesheetMode => use(SemanticRenderContext)?.mode ?? "legacy";
export const useSemanticNodeVisible = (nodeKey: string | undefined): boolean => {
const context = use(SemanticRenderContext);
if (context?.mode !== "semantic" || !nodeKey) return true;
if (!context || !nodeKey) return true;
return context.renderedNodeKeys.has(nodeKey);
};
@@ -107,7 +101,7 @@ export const useSemanticNodeExists = (nodeKey: string | undefined): boolean => {
export const useRenderedChildKeys = (nodeKey: string | undefined): readonly string[] | undefined => {
const context = use(SemanticRenderContext);
if (context?.mode !== "semantic" || !nodeKey) return undefined;
if (!context || !nodeKey) return undefined;
const rendered = context.renderedChildKeys.get(nodeKey) ?? [];
const source = context.sourceChildKeys.get(nodeKey) ?? [];
return rendered.length === source.length && rendered.every((key, index) => key === source[index])
@@ -142,7 +136,7 @@ export const useSemanticSectionNodeKey = (pageNodeKey: string, sectionId: string
export const useRenderedSectionIds = (pageNodeKey: string, authoredIds: readonly string[]): string[] => {
const context = use(SemanticRenderContext);
if (context?.mode !== "semantic") return [...authoredIds];
if (!context) return [...authoredIds];
const authored = new Set(authoredIds);
return context.sourceNodes
@@ -171,11 +165,11 @@ export const useSemanticNodeBindings = () => {
return context.presentation[nodeKey] ?? EMPTY_NODE;
},
isNodeVisible: (nodeKey: string | undefined): boolean => {
if (context?.mode !== "semantic" || !nodeKey) return true;
if (!context || !nodeKey) return true;
return context.renderedNodeKeys.has(nodeKey);
},
renderedChildKeysFor: (nodeKey: string | undefined): readonly string[] | undefined => {
if (context?.mode !== "semantic" || !nodeKey) return undefined;
if (!context || !nodeKey) return undefined;
const rendered = context.renderedChildKeys.get(nodeKey) ?? [];
const source = context.sourceChildKeys.get(nodeKey) ?? [];
return rendered.length === source.length && rendered.every((key, index) => key === source[index])
-1
View File
@@ -6,6 +6,5 @@ export * from "./legacy-converter";
export {
resolveResumePresentation,
resolveResumeRuntime,
resolveStylesheetMode,
} from "./resolve";
export * from "./tree";
@@ -6,7 +6,7 @@ import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { ResumeDocument } from "../document";
import { pdf } from "../forme/testing";
import { semanticNodeKeys } from "./node-keys";
import { resolveResumePresentation, resolveStylesheetMode } from "./resolve";
import { resolveResumePresentation } from "./resolve";
const source = (text: string) => ({ languageVersion: 1, text });
@@ -100,7 +100,6 @@ const resolveIssueFixture = (text: string) => {
data,
template: "onyx",
source: source(text),
mode: "semantic",
});
};
@@ -148,7 +147,6 @@ describe("semantic issue fixtures", () => {
data,
template: "onyx",
source: source(`@version 1;${text}`),
mode: "semantic",
});
expect(resolve("contact-item { color: red; } link { color: blue; }")[contact]?.style?.color).toBe("blue");
@@ -185,26 +183,6 @@ describe("semantic issue fixtures", () => {
expect(presentation[company]?.style?.fontWeight).not.toBe("400");
});
it("never applies legacy and semantic custom styles together", () => {
const semanticData = buildIssueFixture();
semanticData.metadata.stylesheet = {
mode: "semantic",
source: source("@version 1;"),
};
const legacyData = buildIssueFixture();
expect(resolveStylesheetMode(semanticData)).toBe("semantic");
expect(resolveStylesheetMode(legacyData)).toBe("legacy");
expect(
resolveResumePresentation({
data: legacyData,
template: "onyx",
source: source("@version 1; name { color: red; }"),
mode: "legacy",
}),
).toEqual({});
});
it("applies issue-regression styles to the final existing PDF primitives", async () => {
const data = buildIssueFixture();
const stylesheet = source(`
@@ -58,7 +58,6 @@ const renderHeaderBoxes = async (template: Template): Promise<string[]> => {
const semanticRuntime = resolveResumeRuntime({
data,
template,
mode: "semantic",
source: { languageVersion: 1, text: STYLESHEET },
});
const element = createElement(ResumeDocument, { data, template, semanticRuntime }) as unknown as Parameters<
@@ -65,7 +65,7 @@ const buildFixture = (): ResumeData => {
const renderTitleRowStyles = async (template: Template, stylesheet?: string) => {
const data = buildFixture();
const semanticRuntime = stylesheet
? resolveResumeRuntime({ data, template, mode: "semantic", source: { languageVersion: 1, text: stylesheet } })
? resolveResumeRuntime({ data, template, source: { languageVersion: 1, text: stylesheet } })
: undefined;
const element = createElement(ResumeDocument, { data, template, semanticRuntime }) as unknown as Parameters<
typeof pdf
@@ -159,94 +159,60 @@ const render = async (data: ResumeData, template: Template): Promise<Uint8Array>
return new Uint8Array(await renderToBuffer(document));
};
const semanticData = (data: ResumeData): ResumeData => {
// What the API stores on read/write: the legacy rules converted into the resume's Semantic CSS stylesheet.
const adopt = (data: ResumeData): ResumeData => {
const conversion = convertLegacyStyleRules(data);
const semantic = structuredClone(data);
semantic.metadata.styleRules = [...conversion.sanitizedRules];
semantic.metadata.stylesheet = { mode: "semantic", source: conversion.source };
return semantic;
const adopted = structuredClone(data);
adopted.metadata.styleRules = [...conversion.sanitizedRules];
adopted.metadata.stylesheet = { mode: "semantic", source: conversion.source };
return adopted;
};
type RasterComparison = {
pixelDiffRatio: number;
mismatches: readonly string[];
};
const comparePdfRasters = async (legacy: Uint8Array, semantic: Uint8Array): Promise<RasterComparison> => {
const legacyPages = await rasterizePdf(legacy);
const semanticPages = await rasterizePdf(semantic);
const comparePdfRasters = async (before: Uint8Array, after: Uint8Array): Promise<string[]> => {
const beforePages = await rasterizePdf(before);
const afterPages = await rasterizePdf(after);
const mismatches: string[] = [];
if (legacyPages.length !== semanticPages.length) {
mismatches.push(`page count: legacy=${legacyPages.length} semantic=${semanticPages.length}`);
if (beforePages.length !== afterPages.length) {
mismatches.push(`page count: ${beforePages.length} -> ${afterPages.length}`);
}
let changed = 0;
let pixels = 0;
for (const [index, legacyPage] of legacyPages.entries()) {
const semanticPage = semanticPages[index];
if (!semanticPage) continue;
if (semanticPage.width !== legacyPage.width || semanticPage.height !== legacyPage.height) {
mismatches.push(
`page ${index + 1} dimensions: legacy=${legacyPage.width}x${legacyPage.height} semantic=${semanticPage.width}x${semanticPage.height}`,
);
for (const [index, beforePage] of beforePages.entries()) {
const afterPage = afterPages[index];
if (!afterPage) continue;
if (afterPage.width !== beforePage.width || afterPage.height !== beforePage.height) {
mismatches.push(`page ${index + 1} dimensions changed`);
continue;
}
const changedOnPage = pixelmatch(
legacyPage.data,
semanticPage.data,
undefined,
legacyPage.width,
legacyPage.height,
{ threshold: 0 },
);
if (changedOnPage > 0) {
mismatches.push(`page ${index + 1} pixels: changed=${changedOnPage}/${legacyPage.width * legacyPage.height}`);
}
changed += changedOnPage;
pixels += legacyPage.width * legacyPage.height;
const changed = pixelmatch(beforePage.data, afterPage.data, undefined, beforePage.width, beforePage.height, {
threshold: 0,
});
if (changed > 0) mismatches.push(`page ${index + 1} pixels: changed=${changed}`);
}
return { pixelDiffRatio: pixels === 0 ? (mismatches.length > 0 ? 1 : 0) : changed / pixels, mismatches };
return mismatches;
};
describe("legacy activation raster parity", () => {
it.each(fixtureNames)(
"has zero real-PDF pixel drift for %s",
async (fixture) => {
const legacy = buildFixture(readRules(fixture));
const semantic = semanticData(legacy);
// Rules with no visible effect on this fixture: disabled, or restating the award title's existing weight.
const noOpFixtures = new Set<string>(["award-unbold", "disabled-rules"]);
expect(await comparePdfRasters(await render(legacy, "onyx"), await render(semantic, "onyx"))).toEqual({
pixelDiffRatio: 0,
mismatches: [],
});
// Unconverted rules no longer render, so the fixture itself is the unstyled baseline.
const drift = async (data: ResumeData, template: Template) =>
comparePdfRasters(await render(data, template), await render(adopt(data), template));
describe("adopted legacy rules in the rendered PDF", () => {
it.each(fixtureNames)(
"renders the converted %s stylesheet",
async (fixture) => {
const mismatches = await drift(buildFixture(readRules(fixture)), "onyx");
if (noOpFixtures.has(fixture)) expect(mismatches).toEqual([]);
else expect(mismatches).not.toEqual([]);
},
30_000,
);
it.each(templates)(
"smoke-renders %s without activation pixel drift",
"renders the converted smoke stylesheet on %s",
async (template) => {
const legacy = buildFixture(readRules("all-templates-smoke"));
const semantic = semanticData(legacy);
expect(await comparePdfRasters(await render(legacy, template), await render(semantic, template))).toEqual({
pixelDiffRatio: 0,
mismatches: [],
});
},
30_000,
);
it.each(["onyx", "meowth"] as const)(
"has zero combined-separator and box-style pixel drift on %s",
async (template) => {
const legacy = buildFixture(readRules("combined-text-host"));
const semantic = semanticData(legacy);
expect(await comparePdfRasters(await render(legacy, template), await render(semantic, template))).toEqual({
pixelDiffRatio: 0,
mismatches: [],
});
expect(await drift(buildFixture(readRules("all-templates-smoke")), template)).not.toEqual([]);
},
30_000,
);
@@ -15,7 +15,7 @@ type HostNode = {
const emptyStylesheet = { languageVersion: 1, text: "@version 1;" };
const buildFixture = (
mode: "missing" | "legacy" | "semantic",
mode: "missing" | "semantic",
hideLinkUnderline: boolean,
text = emptyStylesheet.text,
): ResumeData => {
@@ -58,8 +58,6 @@ describe("PDF link decoration fidelity", () => {
it.each([
["missing", false, "underline"],
["missing", true, "none"],
["legacy", false, "underline"],
["legacy", true, "none"],
["semantic", false, "underline"],
["semantic", true, "none"],
] as const)("%s stylesheet with hideLinkUnderline=%s resolves to %s", async (mode, hidden, expected) => {
@@ -23,7 +23,7 @@ const semanticSource = (text = "@version 1;\n") => ({
text,
});
const buildFixture = (mode: "legacy" | "semantic"): ResumeData => {
const buildFixture = (withStylesheet: boolean): ResumeData => {
const data = structuredClone(sampleResumeData);
data.picture.hidden = true;
data.basics.name = "Ada Lovelace";
@@ -49,9 +49,9 @@ const buildFixture = (mode: "legacy" | "semantic"): ResumeData => {
sidebar: ["skills", "languages"],
},
];
if (mode === "semantic") {
if (withStylesheet) {
data.metadata.stylesheet = {
mode,
mode: "semantic",
source: semanticSource(),
};
}
@@ -74,9 +74,9 @@ const textValues = (node: HostNode): string[] => [
...(node.children ?? []).flatMap((child) => textValues(child)),
];
describe("semantic PDF bindings do not add layout wrappers", () => {
describe("resumes without a stylesheet render like an empty stylesheet", () => {
it("uses a split-page fixture with chrome, ordinary sections, rich lists, and levels", () => {
const data = buildFixture("legacy");
const data = buildFixture(false);
expect(data.metadata.layout.pages[0]).toMatchObject({
fullWidth: false,
@@ -93,11 +93,11 @@ describe("semantic PDF bindings do not add layout wrappers", () => {
it.each(templateSchema.options)(
"%s preserves primitive type, count, and order for an empty stylesheet",
async (template) => {
const legacy = await renderHostTree(buildFixture("legacy"), template);
const semantic = await renderHostTree(buildFixture("semantic"), template);
const missing = await renderHostTree(buildFixture(false), template);
const empty = await renderHostTree(buildFixture(true), template);
expect(textValues(semantic)).toEqual(textValues(legacy));
expect(primitiveTypes(semantic)).toEqual(primitiveTypes(legacy));
expect(textValues(empty)).toEqual(textValues(missing));
expect(primitiveTypes(empty)).toEqual(primitiveTypes(missing));
},
);
});
@@ -70,7 +70,7 @@ describe("semantic pagination cancellation", () => {
["revert", true, false],
] as const)("maps %s over builder pagination to explicit break=%s and wrap=%s", (value, breakBefore, wrap) => {
const data = buildFixture(value);
const presentation = resolveResumePresentation({ data, template: "onyx", mode: "semantic" });
const presentation = resolveResumePresentation({ data, template: "onyx" });
const sectionKey = semanticNodeKeys.section(semanticNodeKeys.region(semanticNodeKeys.page(1), "main"), "summary");
expect(presentation[sectionKey]).toMatchObject({ break: breakBefore, wrap });
+3 -3
View File
@@ -7,7 +7,7 @@ describe("@reactive-resume/pdf/semantic", () => {
const data = structuredClone(sampleResumeData);
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 2, text: "@version 2;" } };
const inspection = resolveResumeRuntime({ data, template: data.metadata.template, mode: "semantic" });
const inspection = resolveResumeRuntime({ data, template: data.metadata.template });
expect(inspection.diagnostics).toContainEqual(
expect.objectContaining({ code: "UNSUPPORTED_VERSION", severity: "error" }),
@@ -23,7 +23,7 @@ describe("@reactive-resume/pdf/semantic", () => {
source: { languageVersion: 1, text: `@version 1;\n${selectors} { color: red; }` },
};
const inspection = resolveResumeRuntime({ data, template: data.metadata.template, mode: "semantic" });
const inspection = resolveResumeRuntime({ data, template: data.metadata.template });
expect(inspection.diagnostics).toContainEqual(
expect.objectContaining({ code: "RESOURCE_LIMIT", severity: "error" }),
@@ -42,7 +42,7 @@ describe("@reactive-resume/pdf/semantic", () => {
},
};
const inspection = resolveResumeRuntime({ data, template: data.metadata.template, mode: "semantic" });
const inspection = resolveResumeRuntime({ data, template: data.metadata.template });
expect(inspection.presentation["page-1/region-header/header/name"]?.style?.color).toBe("#123456");
expect(inspection.diagnostics).toContainEqual(
+1 -10
View File
@@ -1,6 +1,6 @@
import type { ResolveStylesheetResult, SemanticCssDiagnostic, SemanticNode } from "@reactive-resume/resume/stylesheet";
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import type { StylesheetMode, StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { StylesheetSource } from "@reactive-resume/schema/resume/stylesheet";
import type { Template } from "@reactive-resume/schema/templates";
import type { ResolvedResumePresentation } from "./context";
import { compileStylesheet, isFatalStylesheetDiagnostic, resolveStylesheet } from "@reactive-resume/resume/stylesheet";
@@ -18,7 +18,6 @@ export type ResolveResumePresentationInput = {
data: ResumeData;
template: Template;
source?: StylesheetSource;
mode: StylesheetMode;
};
export type ResolvedResumeRuntime = {
@@ -78,20 +77,12 @@ const toPresentation = (
) as ResolvedResumePresentation;
};
export function resolveStylesheetMode(data: ResumeData): StylesheetMode {
return data.metadata.stylesheet?.mode ?? "legacy";
}
export function resolveResumeRuntime({
data,
template,
source,
mode,
}: ResolveResumePresentationInput): ResolvedResumeRuntime {
const sourceTree = mergeAuthoredPageTrees(data, template);
if (mode !== "semantic") {
return { presentation: EMPTY_PRESENTATION, sourceTree, renderTree: sourceTree, diagnostics: [] };
}
const stylesheetSource = source ??
data.metadata.stylesheet?.source ?? {
@@ -19,7 +19,7 @@ const table = (paragraphs = false) =>
)
.join("")}</tbody></table>`;
const fixture = (html: string, mode: "legacy" | "semantic", css = ""): ResumeData => {
const fixture = (html: string, css = ""): ResumeData => {
const data = structuredClone(defaultResumeData);
data.basics.name = "Table probe";
data.picture.hidden = true;
@@ -27,7 +27,7 @@ const fixture = (html: string, mode: "legacy" | "semantic", css = ""): ResumeDat
data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }];
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: `@version 1; ${css}` } };
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${css}` } };
return data;
};
@@ -95,96 +95,90 @@ const tableCoordinates = (items: Awaited<ReturnType<typeof readPdf>>["items"]) =
describe("imported rich-text tables", () => {
for (const template of ["ditgar", "onyx"] as const) {
for (const mode of ["legacy", "semantic"] as const) {
it(`${template} ${mode} preserves bare cell text and row/column positions`, async () => {
const { items } = await readPdf(fixture(table(), mode), template);
const cell = (text: string) => {
const cell = items.find((item) => item.text === text);
if (!cell) throw new Error(`Missing table cell ${text}`);
return cell;
};
const alpha = cell("Alpha");
const beta = cell("Beta");
const gamma = cell("Gamma");
const delta = cell("Delta");
const epsilon = cell("Epsilon");
const zeta = cell("Zeta");
expect(alpha.y).toBe(beta.y);
expect(alpha.y).toBe(gamma.y);
expect(delta.y).toBe(epsilon.y);
expect(delta.y).toBe(zeta.y);
expect(alpha.y).toBeGreaterThan(delta.y);
expect(alpha.x).toBe(delta.x);
expect(beta.x).toBe(epsilon.x);
expect(gamma.x).toBe(zeta.x);
expect(beta.x).toBeGreaterThan(alpha.x);
expect(gamma.x).toBeGreaterThan(beta.x);
});
}
it(`${template} preserves bare cell text and row/column positions`, async () => {
const { items } = await readPdf(fixture(table()), template);
const cell = (text: string) => {
const cell = items.find((item) => item.text === text);
if (!cell) throw new Error(`Missing table cell ${text}`);
return cell;
};
const alpha = cell("Alpha");
const beta = cell("Beta");
const gamma = cell("Gamma");
const delta = cell("Delta");
const epsilon = cell("Epsilon");
const zeta = cell("Zeta");
expect(alpha.y).toBe(beta.y);
expect(alpha.y).toBe(gamma.y);
expect(delta.y).toBe(epsilon.y);
expect(delta.y).toBe(zeta.y);
expect(alpha.y).toBeGreaterThan(delta.y);
expect(alpha.x).toBe(delta.x);
expect(beta.x).toBe(epsilon.x);
expect(gamma.x).toBe(zeta.x);
expect(beta.x).toBeGreaterThan(alpha.x);
expect(gamma.x).toBeGreaterThan(beta.x);
});
}
it("preserves table cells containing recognized paragraphs", async () => {
const { items } = await readPdf(fixture(table(true), "semantic"), "ditgar");
const { items } = await readPdf(fixture(table(true)), "ditgar");
expect(items.map((item) => item.text)).toEqual(expect.arrayContaining(["Alpha", "Beta", "Gamma", "Delta"]));
});
it("preserves raw text inside an unrecognized block wrapper", async () => {
const { items } = await readPdf(fixture("<div>Wrapper content</div>", "semantic"), "ditgar");
const { items } = await readPdf(fixture("<div>Wrapper content</div>"), "ditgar");
expect(items.map((item) => item.text)).toContain("Wrapper content");
});
it("still honors explicit semantic rich-text hiding", async () => {
const { items } = await readPdf(fixture(table(), "semantic", "rich-text { display: none; }"), "ditgar");
const { items } = await readPdf(fixture(table(), "rich-text { display: none; }"), "ditgar");
expect(items.map((item) => item.text)).toContain("Table probe");
expect(items.map((item) => item.text)).not.toContain("Alpha");
});
it.each(["legacy", "semantic"] as const)(
"keeps six cell coordinates, border operators, and fixed-DPI pixels through %s persistence",
async (mode) => {
const bordered = table().replaceAll("black", "#cc00cc");
const unrelated = fixture(bordered, mode);
unrelated.basics.name = "Border Probe unrelated edit";
const stages = [
{ name: "original", data: fixture(bordered, mode), expectedBeta: "Beta" },
{ name: "unrelated edit", data: unrelated, expectedBeta: "Beta" },
{ name: "table edit", data: fixture(bordered.replace("Beta", "Beta!"), mode), expectedBeta: "Beta!" },
it("keeps six cell coordinates, border operators, and fixed-DPI pixels through persistence", async () => {
const bordered = table().replaceAll("black", "#cc00cc");
const unrelated = fixture(bordered);
unrelated.basics.name = "Border Probe unrelated edit";
const stages = [
{ name: "original", data: fixture(bordered), expectedBeta: "Beta" },
{ name: "unrelated edit", data: unrelated, expectedBeta: "Beta" },
{ name: "table edit", data: fixture(bordered.replace("Beta", "Beta!")), expectedBeta: "Beta!" },
];
// The first render sets the reference: three equal columns over two rows, with the borders drawn. Neither an
// unrelated edit nor an edit inside a cell may move a cell or change a border.
let reference: { cells: Record<string, number[]>; colored: unknown } | undefined;
for (const stage of stages) {
const pdf = await readPdf(stage.data, "ditgar");
const cells = tableCoordinates(pdf.items);
const { colored } = await inspectTableBorders(pdf);
const cell = (text: string) => {
const [x, y] = cells[text] ?? [];
if (x === undefined || y === undefined) throw new Error(`${stage.name}: missing cell ${text}`);
return { x, y };
};
const [alpha, beta, gamma, delta] = ["Alpha", stage.expectedBeta, "Gamma", "Delta"].map(cell) as [
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number },
];
// The first render sets the reference: three equal columns over two rows, with the borders drawn. Neither an
// unrelated edit nor an edit inside a cell may move a cell or change a border.
let reference: { cells: Record<string, number[]>; colored: unknown } | undefined;
for (const stage of stages) {
const pdf = await readPdf(stage.data, "ditgar");
const cells = tableCoordinates(pdf.items);
const { colored } = await inspectTableBorders(pdf);
const cell = (text: string) => {
const [x, y] = cells[text] ?? [];
if (x === undefined || y === undefined) throw new Error(`${stage.name}: missing cell ${text}`);
return { x, y };
};
const [alpha, beta, gamma, delta] = ["Alpha", stage.expectedBeta, "Gamma", "Delta"].map(cell) as [
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number },
{ x: number; y: number },
];
expect(beta.x - alpha.x, stage.name).toBeCloseTo(gamma.x - beta.x, 1);
expect(beta.y, stage.name).toBe(alpha.y);
expect(delta.x, stage.name).toBe(alpha.x);
expect(delta.y, stage.name).toBeLessThan(alpha.y);
expect((colored as { pixels: number }).pixels, stage.name).toBeGreaterThan(0);
const { [stage.expectedBeta]: _beta, ...rest } = cells;
const comparable = { cells: { ...rest, Beta: [beta.x, beta.y] }, colored };
if (reference) expect(comparable, stage.name).toEqual(reference);
else reference = comparable;
}
},
30_000,
);
expect(beta.x - alpha.x, stage.name).toBeCloseTo(gamma.x - beta.x, 1);
expect(beta.y, stage.name).toBe(alpha.y);
expect(delta.x, stage.name).toBe(alpha.x);
expect(delta.y, stage.name).toBeLessThan(alpha.y);
expect((colored as { pixels: number }).pixels, stage.name).toBeGreaterThan(0);
const { [stage.expectedBeta]: _beta, ...rest } = cells;
const comparable = { cells: { ...rest, Beta: [beta.x, beta.y] }, colored };
if (reference) expect(comparable, stage.name).toEqual(reference);
else reference = comparable;
}
}, 30_000);
it.each(["legacy", "semantic"] as const)("keeps borderless tables borderless in %s mode", async (mode) => {
it("keeps borderless tables borderless", async () => {
const borderless = table().replaceAll("border: 1pt solid black; ", "");
const pdf = await readPdf(fixture(borderless, mode), "ditgar");
const pdf = await readPdf(fixture(borderless), "ditgar");
expect((await inspectTableBorders(pdf)).geometry).toEqual({ horizontal: 0, vertical: 0 });
});
});
@@ -57,11 +57,11 @@ const buildFixture = (): ResumeData => {
return data;
};
const buildNodeBudgetFixture = (mode: "legacy" | "semantic"): ResumeData => {
const buildNodeBudgetFixture = (): ResumeData => {
const data = structuredClone(defaultResumeData);
const source = { languageVersion: 1, text: "@version 1;\n" };
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
data.metadata.stylesheet = { mode, source };
data.metadata.stylesheet = { mode: "semantic", source };
data.sections.skills.items = Array.from({ length: 2_000 }, (_, index) => ({
id: `skill-${index}`,
hidden: false,
@@ -128,8 +128,8 @@ describe("browser/server semantic runtime identity", () => {
expect(browserProps).toEqual(serverProps);
}, 15_000);
it("keeps legacy PDF rendering unaffected by the semantic node budget", async () => {
const data = buildNodeBudgetFixture("legacy");
it("still renders a resume beyond the semantic node budget", async () => {
const data = buildNodeBudgetFixture();
const blob = await createResumePdfBlob({ data, template: "onyx" });
const file = await createResumePdfFile({ data, filename: "resume.pdf", template: "onyx" });
@@ -237,7 +237,7 @@ describe("semantic sibling ordering reaches final PDF output", () => {
data.metadata.page.locale = locale;
data.summary.content = "<ul><li>List content</li></ul>";
data.metadata.layout.pages = [{ fullWidth: true, main: ["summary"], sidebar: [] }];
const runtime = resolveResumeRuntime({ data, template: "onyx", mode: "semantic" });
const runtime = resolveResumeRuntime({ data, template: "onyx" });
const renderedItem = findSemanticNode(runtime.renderTree, (node) => node.kind === "list-item");
expect(runtime.diagnostics.filter(({ severity }) => severity === "error")).toEqual([]);
@@ -7,7 +7,6 @@ import { ResumeDocument } from "../../document";
import { renderToBuffer } from "../../forme/testing";
type FixtureOptions = {
mode: "legacy" | "semantic";
columns: number;
gapX: number;
rtl?: boolean;
@@ -19,7 +18,7 @@ type FixtureOptions = {
type TextRun = { text: string; x: number; right: number; y: number };
function fixture({ mode, columns, gapX, rtl, sidebar, longTitle, experience, css = "" }: FixtureOptions): ResumeData {
function fixture({ columns, gapX, rtl, sidebar, longTitle, experience, css = "" }: FixtureOptions): ResumeData {
const data = structuredClone(defaultResumeData);
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
@@ -30,7 +29,7 @@ function fixture({ mode, columns, gapX, rtl, sidebar, longTitle, experience, css
data.metadata.layout.pages = [
{ fullWidth: false, main: sidebar ? [] : [section], sidebar: sidebar ? [section] : [] },
];
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: `@version 1; ${css}` } };
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${css}` } };
data.sections.projects.columns = columns;
data.sections.experience.columns = columns;
for (let index = 0; index < 3; index++) {
@@ -82,29 +81,26 @@ function assertAligned(runs: TextRun[], rtl = false, expectedOffset = 0) {
}
describe("Ditgar item-header alignment (#3068)", () => {
for (const mode of ["legacy", "semantic"] as const) {
for (const columns of [1, 2]) {
it.each([0, 4, 12])(`aligns ${mode} Projects in ${columns} columns at gapX %i`, async (gapX) => {
assertAligned(await renderText({ mode, columns, gapX }));
});
}
it(`aligns wrapped ${mode} project titles with their descriptions and links`, async () => {
assertAligned(await renderText({ mode, columns: 2, gapX: 4, longTitle: true }));
});
it(`aligns the shared ${mode} Experience header`, async () => {
assertAligned(await renderText({ mode, columns: 2, gapX: 4, experience: true }));
});
it(`preserves ${mode} sidebar alignment`, async () => {
assertAligned(await renderText({ mode, columns: 1, gapX: 4, sidebar: true }));
});
it(`preserves ${mode} RTL alignment`, async () => {
assertAligned(await renderText({ mode, columns: 2, gapX: 4, rtl: true }), true);
for (const columns of [1, 2]) {
it.each([0, 4, 12])(`aligns Projects in ${columns} columns at gapX %i`, async (gapX) => {
assertAligned(await renderText({ columns, gapX }));
});
}
it("aligns wrapped project titles with their descriptions and links", async () => {
assertAligned(await renderText({ columns: 2, gapX: 4, longTitle: true }));
});
it("aligns the shared Experience header", async () => {
assertAligned(await renderText({ columns: 2, gapX: 4, experience: true }));
});
it("preserves sidebar alignment", async () => {
assertAligned(await renderText({ columns: 1, gapX: 4, sidebar: true }));
});
it("preserves RTL alignment", async () => {
assertAligned(await renderText({ columns: 2, gapX: 4, rtl: true }), true);
});
it("retains an authored Semantic CSS header inset", async () => {
assertAligned(
await renderText({
mode: "semantic",
columns: 2,
gapX: 4,
css: "item-header { margin-left: 7pt; padding-left: 3pt; border-left: 2pt solid #0000ff; }",
@@ -13,7 +13,6 @@ import { rasterizePdf } from "../../semantic/test/rasterize-pdf";
type SkillLayout = "default" | "inline";
type Placement = "main" | "sidebar";
type LevelDesign = "circle" | "rectangle" | "rectangle-full";
type StylesheetMode = "legacy" | "semantic";
type GengarFixtureOptions = {
keywordLayout?: "inline" | "list";
@@ -25,7 +24,6 @@ type GengarFixtureOptions = {
proficiency?: string;
keywords?: string[];
custom?: boolean;
mode?: StylesheetMode;
count?: number;
overflow?: boolean;
};
@@ -45,7 +43,6 @@ const gengarFixture = ({
proficiency = "Experienced",
keywords = ["Alpha", "Beta"],
custom = false,
mode = "legacy",
count = 1,
overflow = false,
}: GengarFixtureOptions = {}): ResumeData => {
@@ -89,8 +86,6 @@ const gengarFixture = ({
mode: "semantic",
source: { languageVersion: 1, text: "@version 1; page { size: 300pt 220pt; }" },
};
} else if (mode === "semantic") {
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: "@version 1;" } };
}
if (custom) data.customSections = [{ ...section, id: sectionId, type: "skills" }];
else data.sections.skills = section;
@@ -125,11 +120,7 @@ describe("Gengar skill rating placement (#2611)", () => {
{ layout: "default" as const, placement: "main" as const, columns: 2, design: "rectangle-full" as const, level: 5 },
])("orders name, rating, proficiency, keywords in $layout/$placement/$design", (options) => {
const data = gengarFixture(options);
const runtime = resolveResumeRuntime({
data,
template: "gengar",
mode: options.layout === "inline" ? "semantic" : "legacy",
});
const runtime = resolveResumeRuntime({ data, template: "gengar" });
const item = required(getSkillItem(runtime));
const level = required(item.children.find((node) => node.kind === "level"));
const header = required(item.children.find((node) => node.kind === "item-header"));
@@ -143,7 +134,7 @@ describe("Gengar skill rating placement (#2611)", () => {
it.each([0, 3, 5])("keeps level %s semantic decorations", (level) => {
const data = gengarFixture({ level });
const runtime = resolveResumeRuntime({ data, template: "gengar", mode: "legacy" });
const runtime = resolveResumeRuntime({ data, template: "gengar" });
const item = required(getSkillItem(runtime));
const rating = item.children.find((node) => node.kind === "level");
if (level === 0) expect(rating).toBeUndefined();
@@ -153,7 +144,7 @@ describe("Gengar skill rating placement (#2611)", () => {
it("hides rating without leaving a rating node or spacing", async () => {
const data = gengarFixture({ design: "circle", level: 5 });
data.metadata.design.level = { type: "hidden", icon: "star" };
const runtime = resolveResumeRuntime({ data, template: "gengar", mode: "semantic" });
const runtime = resolveResumeRuntime({ data, template: "gengar" });
expect(required(getSkillItem(runtime)).children.map((node) => node.kind)).toEqual([
"item-header",
"field",
@@ -169,7 +160,7 @@ describe("Gengar skill rating placement (#2611)", () => {
it("omits level-zero rating without adding rating spacing and keeps empty proficiency", async () => {
const data = gengarFixture({ level: 0, proficiency: "", keywords: ["Short"] });
const runtime = resolveResumeRuntime({ data, template: "gengar", mode: "legacy" });
const runtime = resolveResumeRuntime({ data, template: "gengar" });
const item = required(getSkillItem(runtime));
expect(item.children.map((node) => node.kind)).toEqual(["item-header", "field"]);
expect(item.children.at(-1)?.attributes.name).toBe("keywords");
@@ -177,9 +168,9 @@ describe("Gengar skill rating placement (#2611)", () => {
expect(result.pages.flat().map((item) => item.str)).toContain("Short");
});
it.each([false, true])("keeps Gengar ordering for %s stylesheet mode", async (semantic) => {
const data = gengarFixture({ mode: semantic ? "semantic" : "legacy", custom: semantic, columns: 1 });
const runtime = resolveResumeRuntime({ data, template: "gengar", mode: semantic ? "semantic" : "legacy" });
it.each([false, true])("keeps Gengar ordering for custom=%s Skills", async (custom) => {
const data = gengarFixture({ custom, columns: 1 });
const runtime = resolveResumeRuntime({ data, template: "gengar" });
const item = required(getSkillItem(runtime));
expect(item.children.map((node) => node.kind)).toEqual(["item-header", "level", "field", "field"]);
const result = await renderGengar(data);
@@ -204,7 +195,7 @@ describe("Gengar skill rating placement (#2611)", () => {
it("keeps long keywords and mixed-height ratings across overflow pages", async () => {
const data = gengarFixture({ count: 12, columns: 2, overflow: true, keywords: ["Long keyword"] });
const runtime = resolveResumeRuntime({ data, template: "gengar", mode: "semantic" });
const runtime = resolveResumeRuntime({ data, template: "gengar" });
expect(required(getSkillItem(runtime)).children.map((node) => node.kind)).toEqual([
"item-header",
"level",
@@ -224,7 +215,7 @@ describe("Gengar skill rating placement (#2611)", () => {
it("does not change Onyx semantic ordering", () => {
const data = gengarFixture();
const runtime = resolveResumeRuntime({ data, template: "onyx", mode: "legacy" });
const runtime = resolveResumeRuntime({ data, template: "onyx" });
const item = required(getSkillItem(runtime));
expect(item.children.map((node) => node.kind)).toEqual(["item-header", "field", "field", "level"]);
});
@@ -1,6 +1,4 @@
import type { StyleSlot } from "@reactive-resume/schema/resume/data";
import type { ReactNode } from "react";
import type { SectionStyleRuleContext } from "./style-rules";
import type { StyleInput, TemplatePlacement } from "./styles";
import type {
SectionTimelineStyleSlots,
@@ -13,9 +11,6 @@ import type {
TemplateStyleSlots,
} from "./types";
import { createContext, use, useMemo } from "react";
import { useRender } from "../../context";
import { useSemanticRenderMode } from "../../semantic/context";
import { resolveStyleRuleSlot } from "./style-rules";
type TemplateContextValue = {
styles: TemplateStyleSlots;
@@ -37,14 +32,8 @@ type TemplatePlacementProviderProps = {
children: ReactNode;
};
type SectionStyleProviderProps = {
context: SectionStyleRuleContext;
children: ReactNode;
};
const TemplateContext = createContext<TemplateContextValue | null>(null);
const TemplatePlacementContext = createContext<TemplatePlacement>("main");
const SectionStyleContext = createContext<SectionStyleRuleContext | null>(null);
const resolveStyleSlot = (slot: TemplateStyleSlot | undefined, context: TemplateStyleContextValue): StyleInput => {
if (!slot) return undefined;
@@ -91,10 +80,6 @@ export const TemplatePlacementProvider = ({ placement, children }: TemplatePlace
return <TemplatePlacementContext.Provider value={placement}>{children}</TemplatePlacementContext.Provider>;
};
export const SectionStyleProvider = ({ context, children }: SectionStyleProviderProps) => {
return <SectionStyleContext.Provider value={context}>{children}</SectionStyleContext.Provider>;
};
const useTemplateContext = () => {
const context = use(TemplateContext);
@@ -127,16 +112,6 @@ export const useTemplateStyle = (slot: keyof TemplateStyleSlots): StyleInput =>
return resolveStyleSlot(styles[slot] as TemplateStyleSlot | undefined, context);
};
export const useSectionStyleRule = (slot: StyleSlot): StyleInput => {
const data = useRender();
const context = use(SectionStyleContext);
const mode = useSemanticRenderMode();
if (!context || mode === "semantic") return undefined;
return resolveStyleRuleSlot(data, { ...context, slot });
};
export const useTemplateFeatureStyle = (
feature: keyof TemplateFeatureStyleSlots,
slot: keyof SectionTimelineStyleSlots,
@@ -612,7 +612,7 @@ describe("date layout characterization (#3155, #2841)", () => {
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
for (const type of ["hidden", "circle", "square", "rectangle", "rectangle-full", "progress-bar", "icon"] as const) {
data.metadata.design.level = { type, icon: "star" };
const runtime = resolveResumeRuntime({ data, template: "chikorita", mode: "legacy" });
const runtime = resolveResumeRuntime({ data, template: "chikorita" });
const level = flatten(runtime.sourceTree).find((node) => node.kind === "level");
if (type === "hidden") expect(level).toBeUndefined();
else expect(level?.children.every((node) => node.attributes.type === type)).toBe(true);
@@ -11,7 +11,7 @@ import {
useSemanticNodeVisible,
} from "../../semantic/context";
import { semanticNodeKeys } from "../../semantic/node-keys";
import { useSectionStyleRule, useTemplateIconSlot, useTemplateStyle } from "./context";
import { useTemplateIconSlot, useTemplateStyle } from "./context";
import { resolveStyleFontSize } from "./icon-size";
import { getTemplateMetrics } from "./metrics";
import { Icon } from "./primitives";
@@ -37,12 +37,9 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
const levelItemStyle = useTemplateStyle("levelItem");
const levelItemActiveStyle = useTemplateStyle("levelItemActive");
const levelItemInactiveStyle = useTemplateStyle("levelItemInactive");
const iconRuleStyle = useSectionStyleRule("icon");
const levelRuleStyle = useSectionStyleRule("level");
const { decorationSize, levelIconExplicitSize } = resolveLevelDisplaySizes({
bodyFontSize: data.metadata.typography.body.fontSize,
iconFontSize: resolveStyleFontSize(iconRuleStyle),
levelFontSize: resolveStyleFontSize(levelRuleStyle, resolved.style),
levelFontSize: resolveStyleFontSize(resolved.style),
});
const color = typeof iconProps.color === "string" ? iconProps.color : "#000000";
@@ -66,7 +63,6 @@ export const LevelDisplay = ({ level }: LevelDisplayProps) => {
style={composeStyles(
{ flexDirection: "row", alignItems: "center", marginTop: 2, columnGap: gap },
levelContainerStyle,
levelRuleStyle,
resolved.style,
)}
>
@@ -11,7 +11,7 @@ import { rasterizePdf } from "../../semantic/test/rasterize-pdf";
const pdfRasterScale = 1.5;
const circleDiameterPt = 8;
async function circlePositions(declaration = "", mode: "semantic" | "legacy" = "semantic") {
async function circlePositions(declaration = "") {
const data = structuredClone(defaultResumeData);
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
@@ -19,7 +19,7 @@ async function circlePositions(declaration = "", mode: "semantic" | "legacy" = "
data.metadata.design.colors.primary = "rgba(255, 0, 0, 1)";
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
data.metadata.stylesheet = {
mode,
mode: "semantic",
source: { languageVersion: 1, text: `@version 1; section[type="skills"] level { ${declaration} }` },
};
data.sections.skills.items = [
@@ -34,7 +34,7 @@ async function circlePositions(declaration = "", mode: "semantic" | "legacy" = "
keywords: [],
},
];
const runtime = resolveResumeRuntime({ data, template: "onyx", mode });
const runtime = resolveResumeRuntime({ data, template: "onyx" });
const bytes = await act(() => renderToBuffer(<ResumeDocument data={data} template="onyx" />));
const [page] = await rasterizePdf(new Uint8Array(bytes));
if (!page) throw new Error("Missing PDF page");
@@ -67,9 +67,6 @@ async function circlePositions(declaration = "", mode: "semantic" | "legacy" = "
}
describe("semantic level gaps (#3040)", () => {
it("preserves default circle geometry in legacy and semantic modes", async () => {
expect(await circlePositions()).toEqual(await circlePositions("", "legacy"));
});
it.each([
["gap: 0;", 0],
["gap: 4pt;", 4],
@@ -35,7 +35,7 @@ async function listPages(
text: `@version 1; page { size: 300pt 300pt; } rich-text { margin-top: ${margin}pt; } ${css}`,
},
};
const runtime = resolveResumeRuntime({ data, template: "onyx", mode: "semantic" });
const runtime = resolveResumeRuntime({ data, template: "onyx" });
expect(runtime.diagnostics).toEqual([]);
const bytes = await act(() => renderToBuffer(<ResumeDocument data={data} template="onyx" />));
const task = getDocument({ data: new Uint8Array(bytes), useSystemFonts: true });
@@ -45,7 +45,7 @@ async function renderList({
// Distinct body weight keeps marker and content separate in PDF.js text runs.
description: `${nested ? "<ul><li>Outer" : ""}<ol>${Array.from({ length: count }, (_, index) => `<li><strong>ITEM${column}_${String(index + 1).padStart(3, "0")}</strong></li>`).join("")}</ol>${nested ? "</li></ul>" : ""}`,
}));
expect(resolveResumeRuntime({ data, template: "rhyhorn", mode: "semantic" }).diagnostics).toEqual([]);
expect(resolveResumeRuntime({ data, template: "rhyhorn" }).diagnostics).toEqual([]);
const bytes = await act(() => renderToBuffer(<ResumeDocument data={data} template="rhyhorn" />));
const loading = getDocument({ data: new Uint8Array(bytes), useSystemFonts: true });
try {
@@ -13,14 +13,13 @@ const renderOverflow = async (
template: Template,
placement: "main" | "sidebar",
locale = "en-US",
mode: "semantic" | "legacy" = "semantic",
explicitPage?: { fullWidth: boolean },
stylesheet = "@version 1;",
fullWidth = false,
) => {
const data = structuredClone(defaultResumeData);
data.basics.name = "Margin Audit";
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: stylesheet } };
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: stylesheet } };
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
data.metadata.page.marginY = 48;
@@ -90,7 +89,6 @@ describe("physical page margins (#3337, #3175)", () => {
"glalie",
"main",
"en-US",
"semantic",
undefined,
`@version 1; template-part[name="sidebar-background"] { background-color: ${color}; }`,
);
@@ -102,7 +100,7 @@ describe("physical page margins (#3337, #3175)", () => {
expect(backgroundAt(overflow, 3, overflow.height - 4)).toEqual(inside);
});
it("preserves the existing Glalie sidebar background on full-width overflow", async () => {
const { rasters } = await renderOverflow("glalie", "main", "en-US", "semantic", undefined, "@version 1;", true);
const { rasters } = await renderOverflow("glalie", "main", "en-US", undefined, "@version 1;", true);
const overflow = rasters[1];
if (!overflow) throw new Error("Missing overflow raster");
expect(backgroundAt(overflow, 3, 100)).toEqual([242, 177, 177]);
@@ -174,20 +172,11 @@ describe("physical page margins (#3337, #3175)", () => {
expect(line.transform[5]).toBeGreaterThanOrEqual(47);
}
});
it.each(templates)("keeps legacy overflow within margins (%s)", async (template) => {
const { pages } = await renderOverflow(template, "sidebar", "en-US", "legacy");
expect(pages.length).toBeGreaterThan(1);
for (const page of pages)
for (const line of page.lines) {
expect(page.height - line.transform[5] - line.height).toBeGreaterThanOrEqual(45.8);
expect(line.transform[5]).toBeGreaterThanOrEqual(47);
}
});
for (const fullWidth of [false, true]) {
it.each(templates)(
`starts explicit headerless pages at the margin (fullWidth: ${fullWidth}, %s)`,
async (template) => {
const { pages } = await renderOverflow(template, "main", "en-US", "semantic", { fullWidth });
const { pages } = await renderOverflow(template, "main", "en-US", { fullWidth });
expect(pages).toHaveLength(2);
const page = pages[1];
if (!page) throw new Error("Missing explicit second page");
@@ -16,7 +16,6 @@ context.fillRect(0, 0, 100, 100);
async function picturePixels(
template: "onyx" | "ditto" | "glalie",
borderWidth: number,
mode: "semantic" | "legacy" = "semantic",
stylesheet = "@version 1;",
shadowWidth = 0,
url = source.toDataURL("image/png"),
@@ -26,7 +25,7 @@ async function picturePixels(
data.basics.name = "Picture";
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: stylesheet } };
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: stylesheet } };
data.metadata.layout.pages = [{ fullWidth: false, main: [], sidebar: [] }];
Object.assign(data.picture, {
url,
@@ -38,7 +37,7 @@ async function picturePixels(
shadowWidth,
shadowColor: "rgba(0, 0, 255, 1)",
});
let runtime = resolveResumeRuntime({ data, template, mode });
let runtime = resolveResumeRuntime({ data, template });
if (styleOverride) {
const entry = Object.entries(runtime.presentation).find(([key]) => key.endsWith("/picture"));
if (!entry) throw new Error("Missing picture presentation");
@@ -115,13 +114,12 @@ async function picturePixels(
describe("picture border visibility (#3017)", () => {
it("keeps the shadow centered on the outer border box when borders and padding change", async () => {
const plain = await picturePixels("onyx", 0, "semantic", "@version 1;", 10);
const bordered = await picturePixels("onyx", 10, "semantic", "@version 1; picture { padding: 5pt; }", 10);
const plain = await picturePixels("onyx", 0, "@version 1;", 10);
const bordered = await picturePixels("onyx", 10, "@version 1; picture { padding: 5pt; }", 10);
expect(bordered.shadowBounds).toEqual(plain.shadowBounds);
const asymmetric = await picturePixels(
"onyx",
0,
"semantic",
"@version 1; picture { border-top: 12pt solid #ff00ff; border-right: 3pt solid #ff00ff; border-bottom: 8pt solid #ff00ff; border-left: 5pt solid #ff00ff; padding: 3pt 4pt 5pt 6pt; }",
10,
);
@@ -130,9 +128,9 @@ describe("picture border visibility (#3017)", () => {
it("preserves percentage dimensions, padding, rounded borders, rotation and opacity", async () => {
const css =
"@version 1; picture { width: 40%; height: 100pt; padding: 1%; border-radius: 25%; transform: rotate(15deg); }";
const opaque = await picturePixels("onyx", 10, "semantic", css);
const plain = await picturePixels("onyx", 10, "semantic", css, 0, undefined, { opacity: 0.5 });
const shadow = await picturePixels("onyx", 10, "semantic", css, 10, undefined, { opacity: 0.5 });
const opaque = await picturePixels("onyx", 10, css);
const plain = await picturePixels("onyx", 10, css, 0, undefined, { opacity: 0.5 });
const shadow = await picturePixels("onyx", 10, css, 10, undefined, { opacity: 0.5 });
expect(opaque.borderPixels).toBeGreaterThan(1000);
expect(plain.imagePixels).toBeGreaterThan(1000);
expect(shadow).toEqual(plain); // Unresolved percentage dimensions omit the shadow.
@@ -141,15 +139,15 @@ describe("picture border visibility (#3017)", () => {
});
// Forme 0.25 takes percentages only for box sizes, so percentage padding is left out; this passes once it isn't.
it.fails.each([0, 10])("keeps percentage picture padding outside the border inset (%s)", async (shadowWidth) => {
const plain = await picturePixels("onyx", 10, "semantic", "@version 1;", shadowWidth);
const padded = await picturePixels("onyx", 10, "semantic", "@version 1; picture { padding: 1%; }", shadowWidth);
const plain = await picturePixels("onyx", 10, "@version 1;", shadowWidth);
const padded = await picturePixels("onyx", 10, "@version 1; picture { padding: 1%; }", shadowWidth);
expect(padded.borderPixels).toBe(plain.borderPixels);
expect(padded.imagePixels).toBeLessThan(plain.imagePixels);
});
it.each([0, 10])("preserves border and authored padding with shadow width %s", async (shadowWidth) => {
const plain = await picturePixels("onyx", 10, "semantic", "@version 1;", shadowWidth);
const zero = await picturePixels("onyx", 10, "semantic", "@version 1; picture { padding: 0; }", shadowWidth);
const padded = await picturePixels("onyx", 10, "semantic", "@version 1; picture { padding: 5pt; }", shadowWidth);
const plain = await picturePixels("onyx", 10, "@version 1;", shadowWidth);
const zero = await picturePixels("onyx", 10, "@version 1; picture { padding: 0; }", shadowWidth);
const padded = await picturePixels("onyx", 10, "@version 1; picture { padding: 5pt; }", shadowWidth);
expect(zero).toEqual(plain);
expect(padded.borderPixels).toBe(plain.borderPixels);
expect(padded.imagePixels).toBeLessThan(plain.imagePixels);
@@ -167,23 +165,14 @@ describe("picture border visibility (#3017)", () => {
});
it("honors semantic border width overrides", async () => {
const metadataBorder = await picturePixels("onyx", 10);
const overridden = await picturePixels(
"onyx",
0,
"semantic",
"@version 1; picture { border: 10pt solid #ff00ff; }",
);
const overridden = await picturePixels("onyx", 0, "@version 1; picture { border: 10pt solid #ff00ff; }");
expect(overridden).toEqual(metadataBorder);
});
it("keeps legacy picture borders visible", async () => {
const bordered = await picturePixels("onyx", 10, "legacy");
expect(bordered.borderPixels).toBeGreaterThan(1000);
});
it.each(["onyx", "ditto", "glalie"] as const)(
"draws a soft centered shadow without moving the photo (%s)",
async (template) => {
const plain = await picturePixels(template, 0);
const shadow = await picturePixels(template, 0, "semantic", "@version 1;", 10);
const shadow = await picturePixels(template, 0, "@version 1;", 10);
expect(plain.shadowPixels).toBe(0);
expect(shadow.shadowPixels).toBeGreaterThan(100);
expect(shadow.bounds).toEqual(plain.bounds);
@@ -194,7 +183,6 @@ describe("picture border visibility (#3017)", () => {
const shadow = await picturePixels(
"onyx",
0,
"semantic",
"@version 1; picture { -resume-shadow-width: 10pt; -resume-shadow-color: rgba(0, 0, 255, 0.5); border-radius: 50pt; }",
);
expect(shadow.shadowPixels).toBeGreaterThan(100);
@@ -206,25 +194,25 @@ describe("picture border visibility (#3017)", () => {
context.fillStyle = "#00aa00";
context.fillRect(0, 0, 100, 100);
context.clearRect(30, 30, 40, 40);
const shadow = await picturePixels("onyx", 0, "semantic", "@version 1;", 10, transparent.toDataURL("image/png"));
const shadow = await picturePixels("onyx", 0, "@version 1;", 10, transparent.toDataURL("image/png"));
expect(shadow.shadowPixels).toBeGreaterThan(100);
expect(shadow.center).toEqual([255, 255, 255]);
});
it("preserves rotated photo geometry and opacity", async () => {
const css = "@version 1; picture { transform: rotate(25deg); }";
const plain = await picturePixels("onyx", 0, "semantic", css, 0, undefined, { opacity: 0.5 });
const shadow = await picturePixels("onyx", 0, "semantic", css, 10, undefined, { opacity: 0.5 });
const plain = await picturePixels("onyx", 0, css, 0, undefined, { opacity: 0.5 });
const shadow = await picturePixels("onyx", 0, css, 10, undefined, { opacity: 0.5 });
expect(shadow.shadowPixels).toBeGreaterThan(100);
expect(shadow.bounds).toEqual(plain.bounds);
expect(shadow.center).toEqual(plain.center);
});
it("keeps the outer shadow visible when the picture clips its contents", async () => {
const shadow = await picturePixels("onyx", 0, "semantic", "@version 1;", 10, undefined, { overflow: "hidden" });
const shadow = await picturePixels("onyx", 0, "@version 1;", 10, undefined, { overflow: "hidden" });
expect(shadow.shadowPixels).toBeGreaterThan(100);
});
it("matches percentage and absolute picture corner radii", async () => {
const absolute = await picturePixels("onyx", 0, "semantic", "@version 1; picture { border-radius: 50pt; }", 10);
const percent = await picturePixels("onyx", 0, "semantic", "@version 1; picture { border-radius: 50%; }", 10);
const absolute = await picturePixels("onyx", 0, "@version 1; picture { border-radius: 50pt; }", 10);
const percent = await picturePixels("onyx", 0, "@version 1; picture { border-radius: 50%; }", 10);
expect(percent).toEqual(absolute);
});
});
@@ -17,7 +17,7 @@ import {
} from "../../semantic/context";
import { semanticNodeKeys } from "../../semantic/node-keys";
import { Link as PdfLink, Text as PdfText } from "../../text";
import { useSectionStyleRule, useTemplateIconSlot, useTemplatePageNodeKey, useTemplateStyle } from "./context";
import { useTemplateIconSlot, useTemplatePageNodeKey, useTemplateStyle } from "./context";
import { resolveIconSize } from "./icon-size";
import { getPictureShadow } from "./picture-shadow";
import { safeTextStyle } from "./safe-text-style";
@@ -135,7 +135,6 @@ export const Text = ({
...props
}: ComponentProps<typeof PdfText> & SemanticProps) => {
const textStyle = useTemplateStyle("text");
const textRuleStyle = useSectionStyleRule("text");
const resolvedNodeKey = usePrimitiveNodeKey({
nodeKey,
semanticField,
@@ -150,7 +149,7 @@ export const Text = ({
<PdfText
{...props}
{...resolvedPdfTextProps(resolved)}
style={composeStyles(textStyle, textRuleStyle, asStyleInput(style), resolved.style, safeTextStyle)}
style={composeStyles(textStyle, asStyleInput(style), resolved.style, safeTextStyle)}
/>
);
};
@@ -164,7 +163,6 @@ export const Heading = ({
...props
}: ComponentProps<typeof PdfText> & SemanticProps) => {
const headingStyle = useTemplateStyle("heading");
const headingRuleStyle = useSectionStyleRule("heading");
const resolvedNodeKey = usePrimitiveNodeKey({
nodeKey,
semanticField,
@@ -180,7 +178,7 @@ export const Heading = ({
<PdfText
{...props}
{...resolvedPdfTextProps(resolved)}
style={composeStyles(headingStyle, headingRuleStyle, asStyleInput(style), resolved.style, safeTextStyle)}
style={composeStyles(headingStyle, asStyleInput(style), resolved.style, safeTextStyle)}
/>
);
};
@@ -196,7 +194,6 @@ export const Link = ({
}: ComponentProps<typeof PdfLink> & SemanticLinkProps) => {
const { metadata } = useRender();
const linkStyle = useTemplateStyle("link");
const linkRuleStyle = useSectionStyleRule("link");
const parentKey = useSemanticNodeKey();
const resolvedNodeKey =
nodeKey ??
@@ -211,12 +208,7 @@ export const Link = ({
{...props}
{...resolvedPdfTextProps(resolved)}
style={composeStyles(
composeLinkStyles(
{ hideUnderline: metadata.page.hideLinkUnderline },
linkStyle,
linkRuleStyle,
asStyleInput(style),
),
composeLinkStyles({ hideUnderline: metadata.page.hideLinkUnderline }, linkStyle, asStyleInput(style)),
resolved.style,
safeTextStyle,
)}
@@ -234,7 +226,6 @@ export const Small = ({
}: ComponentProps<typeof PdfText> & SemanticProps) => {
const textStyle = useTemplateStyle("text");
const smallStyle = useTemplateStyle("small");
const secondaryTextRuleStyle = useSectionStyleRule("secondaryText");
const resolvedNodeKey = usePrimitiveNodeKey({
nodeKey,
semanticField,
@@ -249,14 +240,7 @@ export const Small = ({
<PdfText
{...props}
{...resolvedPdfTextProps(resolved)}
style={composeStyles(
textStyle,
smallStyle,
secondaryTextRuleStyle,
asStyleInput(style),
resolved.style,
safeTextStyle,
)}
style={composeStyles(textStyle, smallStyle, asStyleInput(style), resolved.style, safeTextStyle)}
/>
);
};
@@ -271,7 +255,6 @@ export const Bold = ({
}: ComponentProps<typeof PdfText> & SemanticProps) => {
const textStyle = useTemplateStyle("text");
const boldStyle = useTemplateStyle("bold");
const textRuleStyle = useSectionStyleRule("text");
const resolvedNodeKey = usePrimitiveNodeKey({
nodeKey,
semanticField,
@@ -286,7 +269,7 @@ export const Bold = ({
<PdfText
{...props}
{...resolvedPdfTextProps(resolved)}
style={composeStyles(textStyle, textRuleStyle, boldStyle, asStyleInput(style), resolved.style, safeTextStyle)}
style={composeStyles(textStyle, boldStyle, asStyleInput(style), resolved.style, safeTextStyle)}
/>
);
};
@@ -298,8 +281,7 @@ export const Icon = ({
...props
}: ComponentProps<typeof PhosphorIcon> & { nodeKey?: string | undefined }) => {
const { style: iconStyle, size: templateSize, ...iconProps } = useTemplateIconSlot("icon");
const iconRuleStyle = useSectionStyleRule("icon");
const composedStyle = composeStyles(asStyleInput(iconStyle), iconRuleStyle, asStyleInput(style));
const composedStyle = composeStyles(asStyleInput(iconStyle), asStyleInput(style));
const templateIconSize =
typeof templateSize === "number" || typeof templateSize === "string" ? templateSize : undefined;
const parentKey = useSemanticNodeKey();
@@ -312,7 +294,7 @@ export const Icon = ({
const resolvedSize =
resolveIconSize({
size: sizeProp,
styles: [iconRuleStyle, asStyleInput(style), resolved.style],
styles: [asStyleInput(style), resolved.style],
}) ?? templateIconSize;
if (iconProps.display === "none" || !visible) return null;
@@ -14,7 +14,7 @@ import {
import { semanticNodeKeys } from "../../semantic/node-keys";
import { getRichTextSemanticNodeKey } from "../../semantic/rich-text-keys";
import { Html, Link as PdfLink, Text as PdfText } from "../../text";
import { useSectionStyleRule, useTemplateStyle } from "./context";
import { useTemplateStyle } from "./context";
import {
normalizeRichTextHtml,
projectNormalizedRichTextHtml,
@@ -93,19 +93,7 @@ export const RichText = ({ children, semanticField }: RichTextProps) => {
const richListItemRowStyle = useTemplateStyle("richListItemRow");
const richListItemMarkerStyle = useTemplateStyle("richListItemMarker");
const richListItemContentStyle = useTemplateStyle("richListItemContent");
const richParagraphRuleStyle = useSectionStyleRule("richParagraph");
const richListRuleStyle = useSectionStyleRule("richList");
const richListItemRowRuleStyle = useSectionStyleRule("richListItemRow");
const richListItemContentRuleStyle = useSectionStyleRule("richListItemContent");
const richLinkRuleStyle = useSectionStyleRule("richLink");
const richBoldRuleStyle = useSectionStyleRule("richBold");
const richMarkRuleStyle = useSectionStyleRule("richMark");
const bodyLineHeight = resolveRichTextBodyLineHeight(
richParagraphStyle,
richParagraphRuleStyle,
richListItemContentStyle,
richListItemContentRuleStyle,
);
const bodyLineHeight = resolveRichTextBodyLineHeight(richParagraphStyle, richListItemContentStyle);
const proseSpacing = createRichTextProseSpacing(bodyLineHeight);
const normalizedHtml = normalizeRichTextHtml(children, {
@@ -317,7 +305,6 @@ export const RichText = ({ children, semanticField }: RichTextProps) => {
{...resolvedPdfTextProps(contentResolved)}
style={composeStyles(
richListItemContentStyle,
richListItemContentRuleStyle,
contentItemStyles,
contentResolved.style,
safeTextStyle,
@@ -333,7 +320,6 @@ export const RichText = ({ children, semanticField }: RichTextProps) => {
{...resolvedPdfFlowProps(contentResolved)}
style={composeStyles(
richListItemContentStyle,
richListItemContentRuleStyle,
contentItemStyles,
contentResolved.style,
richListItemContentStackStyle,
@@ -365,7 +351,6 @@ export const RichText = ({ children, semanticField }: RichTextProps) => {
{...resolvedPdfFlowProps(itemResolved)}
style={composeStyles(
richListItemRowStyle,
richListItemRowRuleStyle,
itemStyles,
getRichTextEdgeTrimStyle(element),
itemResolved.style,
@@ -385,11 +370,6 @@ export const RichText = ({ children, semanticField }: RichTextProps) => {
hideLinkUnderline: metadata.page.hideLinkUnderline,
linkStyle,
richParagraphStyle,
richParagraphRuleStyle,
richListRuleStyle,
richBoldRuleStyle,
richLinkRuleStyle,
richMarkRuleStyle,
proseSpacing,
})}
>
@@ -75,15 +75,12 @@ const expectTrailingAlignment = (items: TextItem[], rtl = false) => {
};
describe("optional experience and education header fields (#3338)", () => {
it.each(["legacy", "semantic"] as const)(
"keeps dates at the trailing edge with empty leading fields in %s mode",
async (mode) => {
const data = fixture();
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: "@version 1;" } };
const items = await renderText(data);
expectTrailingAlignment(items);
},
);
it("keeps dates at the trailing edge with empty leading fields", async () => {
const data = fixture();
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: "@version 1;" } };
const items = await renderText(data);
expectTrailingAlignment(items);
});
it("preserves RTL row alignment when leading fields are empty", async () => {
const data = fixture();
@@ -32,12 +32,10 @@ describe("SectionShell", () => {
expect(getSectionHeadingTextStyle({ paddingLeft: 6 })).toEqual([{ paddingLeft: 1 }]);
});
it("keeps section and heading style rules when section heading icons are hidden", () => {
expect(source).toContain(
"const resolvedSectionStyle = composeStyles(sectionStyle, sectionRuleStyle, resolved.style)",
);
it("keeps section and heading styles when section heading icons are hidden", () => {
expect(source).toContain("const resolvedSectionStyle = composeStyles(sectionStyle, resolved.style)");
expect(source).toContain("<View style={resolvedSectionStyle} {...flowProps} data-resume-node={sectionNodeKey}>");
expect(source).toContain("<Heading style={composeStyles(sectionHeadingStyle, sectionHeadingRuleStyle)}>");
expect(source).toContain("<Heading style={composeStyles(sectionHeadingStyle)}>");
});
it("wires the section heading container style slot into the icon row", () => {
@@ -51,7 +49,7 @@ describe("SectionShell", () => {
);
expect(headingContainerBlock?.groups?.body).toContain('alignItems: "flex-start"');
expect(source).toMatch(/getSectionHeadingTextStyle\(\s*sectionHeadingStyle,\s*sectionHeadingRuleStyle(?:,|\))/);
expect(source).toMatch(/getSectionHeadingTextStyle\(\s*sectionHeadingStyle(?:,|\))/);
expect(source).toContain("width: _width");
expect(source).not.toContain('width: "auto"');
});
+5 -15
View File
@@ -46,9 +46,7 @@ import { ITEM_HEADER_ROW_PART_KEYS } from "../../semantic/shared-parts";
import { getSectionItemRows, getSectionItemsLayout, shouldUseSectionTimeline } from "./columns";
import { getWebsiteDisplayText } from "./contact";
import {
SectionStyleProvider,
TemplatePlacementProvider,
useSectionStyleRule,
useTemplateFeature,
useTemplateFeatureStyle,
useTemplatePageNodeKey,
@@ -74,7 +72,6 @@ import { RichText } from "./rich-text";
import { createRtlStyleHelpers } from "./rtl";
import { getInlineItemWebsiteUrl, shouldRenderSeparateItemWebsite } from "./section-links";
import { hasSplitRowText } from "./split-row";
import { getSectionStyleRuleContext } from "./style-rules";
import { composeStyles, mergeStyles } from "./styles";
type SectionItemsContextValue = {
@@ -321,10 +318,8 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio
const resolved = useResolvedNode(sectionNodeKey);
const visible = useSemanticNodeVisible(sectionNodeKey);
const sectionStyle = useTemplateStyle("section");
const sectionRuleStyle = useSectionStyleRule("section");
const sectionHeadingStyle = useTemplateStyle("sectionHeading");
const sectionHeadingContainerStyle = useTemplateStyle("sectionHeadingContainer");
const sectionHeadingRuleStyle = useSectionStyleRule("heading");
const sectionTitle = getResumeSectionTitle(data, sectionId, title);
const sectionHeadingEnabled = (() => {
if (sectionId === "summary") return data.summary.showHeading !== false;
@@ -346,7 +341,7 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio
if (keepTogether) breakProps.wrap = false;
if (startOnNewPage) breakProps.break = true;
const flowProps = { ...breakProps, ...resolvedPdfFlowProps(resolved) };
const resolvedSectionStyle = composeStyles(sectionStyle, sectionRuleStyle, resolved.style);
const resolvedSectionStyle = composeStyles(sectionStyle, resolved.style);
if (!visible) return null;
if (!showIcon) {
@@ -355,7 +350,7 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio
<SemanticNodeKeyProvider nodeKey={sectionNodeKey}>
<View style={resolvedSectionStyle} {...flowProps} data-resume-node={sectionNodeKey}>
{showHeading && sectionHeadingEnabled && (
<Heading style={composeStyles(sectionHeadingStyle, sectionHeadingRuleStyle)}>{sectionTitle}</Heading>
<Heading style={composeStyles(sectionHeadingStyle)}>{sectionTitle}</Heading>
)}
{children}
</View>
@@ -374,7 +369,6 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio
sectionHeadingStyle,
defaultSectionHeadingContainerStyle,
sectionHeadingContainerStyle,
sectionHeadingRuleStyle,
sectionHeadingResolved.style,
)}
>
@@ -386,7 +380,6 @@ const SectionShell = ({ sectionId, title, showHeading = true, children }: Sectio
bindSemanticNode={false}
style={getSectionHeadingTextStyle(
sectionHeadingStyle,
sectionHeadingRuleStyle,
sectionHeadingResolved.style?.color === undefined
? undefined
: { color: sectionHeadingResolved.style.color },
@@ -506,7 +499,6 @@ const SectionItem = ({ itemId, children, style }: SectionItemProps) => {
const visible = useSemanticNodeVisible(itemNodeKey);
const { itemStyle: sectionItemStyle, useTimeline } = useSectionItemsContext();
const itemStyle = useTemplateStyle("item");
const itemRuleStyle = useSectionStyleRule("item");
const timelineItemStyle = useTemplateFeatureStyle("sectionTimeline", "item");
const timelineMarkerStyle = useTemplateFeatureStyle("sectionTimeline", "marker");
const timelineDotStyle = useTemplateFeatureStyle("sectionTimeline", "dot");
@@ -518,7 +510,7 @@ const SectionItem = ({ itemId, children, style }: SectionItemProps) => {
if (!useTimeline) {
return (
<SemanticNodeKeyProvider nodeKey={itemNodeKey}>
<Div nodeKey={itemNodeKey} style={composeStyles(itemStyle, itemRuleStyle, sectionItemStyle, style)}>
<Div nodeKey={itemNodeKey} style={composeStyles(itemStyle, sectionItemStyle, style)}>
{children}
</Div>
</SemanticNodeKeyProvider>
@@ -542,7 +534,7 @@ const SectionItem = ({ itemId, children, style }: SectionItemProps) => {
<Div
nodeKey={itemNodeKey}
{...resolvedPdfFlowProps(timelineContentResolved)}
style={composeStyles(itemStyle, itemRuleStyle, timelineContentStyle, style, timelineContentResolved.style)}
style={composeStyles(itemStyle, timelineContentStyle, style, timelineContentResolved.style)}
>
{children}
</Div>
@@ -1597,9 +1589,7 @@ export const Section = ({ section, placement, showHeading = true }: SectionProps
return (
<TemplatePlacementProvider placement={placement}>
<SectionStyleProvider context={getSectionStyleRuleContext(data, section)}>
{render ? render() : <CustomSection sectionId={section} showHeading={showHeading} />}
</SectionStyleProvider>
{render ? render() : <CustomSection sectionId={section} showHeading={showHeading} />}
</TemplatePlacementProvider>
);
};
@@ -110,7 +110,7 @@ describe("skill keyword presentation", () => {
text: '@version 1; section[type="skills"] field[name="keywords"] { font-size: 18pt; } section[type="skills"] item[id="css-hidden"] { display: none; }',
},
};
const runtime = resolveResumeRuntime({ data, template: "leafish", mode: "semantic" });
const runtime = resolveResumeRuntime({ data, template: "leafish" });
expect(runtime.diagnostics).toEqual([]);
const { text, pages } = await renderKeywords(data);
expect(text).toContain("Engineering");
@@ -11,7 +11,6 @@ import { rasterizePdf } from "../../semantic/test/rasterize-pdf";
type FixtureOptions = {
columns: number;
count: number;
mode?: "semantic" | "legacy";
configure?: (data: ResumeData) => void;
};
@@ -19,14 +18,14 @@ function setCss(data: ResumeData, css: string) {
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: `@version 1; ${css}` } };
}
async function renderRatings({ columns, count, mode = "semantic", configure }: FixtureOptions) {
async function renderRatings({ columns, count, configure }: FixtureOptions) {
const data = structuredClone(defaultResumeData);
data.metadata.typography.body.fontFamily = "Helvetica";
data.metadata.typography.heading.fontFamily = "Helvetica";
data.metadata.page.hideIcons = true;
data.metadata.design.colors.primary = "rgba(255, 0, 0, 1)";
data.metadata.layout.pages = [{ fullWidth: true, main: ["skills"], sidebar: [] }];
data.metadata.stylesheet = { mode, source: { languageVersion: 1, text: "@version 1;" } };
data.metadata.stylesheet = { mode: "semantic", source: { languageVersion: 1, text: "@version 1;" } };
data.sections.skills.columns = columns;
data.sections.skills.items = Array.from({ length: count }, (_, index) => ({
id: `skill-${index}`,
@@ -39,7 +38,7 @@ async function renderRatings({ columns, count, mode = "semantic", configure }: F
keywords: index === 1 ? Array.from({ length: 12 }, (_, word) => `Keyword${word}`) : ["Short"],
}));
configure?.(data);
expect(resolveResumeRuntime({ data, template: "onyx", mode }).diagnostics).toEqual([]);
expect(resolveResumeRuntime({ data, template: "onyx" }).diagnostics).toEqual([]);
const bytes = await act(() => renderToBuffer(<ResumeDocument data={data} template="onyx" />));
const pages = await rasterizePdf(new Uint8Array(bytes));
const loading = getDocument({ data: new Uint8Array(bytes) });
@@ -82,21 +81,18 @@ async function renderRatings({ columns, count, mode = "semantic", configure }: F
}
describe("skill rating alignment (#3343)", () => {
it.each(["semantic", "legacy"] as const)(
"aligns mixed-height skills and preserves an incomplete row in %s mode",
async (mode) => {
const {
rows: [rows],
text,
} = await renderRatings({ columns: 2, count: 3, mode });
expect(rows).toHaveLength(2);
if (!rows?.[0] || !rows[1]) throw new Error("Missing rating rows");
// Ten circles share the first row; the remaining skill has five.
expect(rows.map((row) => row.circles)).toEqual([10, 5]);
expect(text).toContain("Keyword11");
expect(text).toContain("Skill 2");
},
);
it("aligns mixed-height skills and preserves an incomplete row", async () => {
const {
rows: [rows],
text,
} = await renderRatings({ columns: 2, count: 3 });
expect(rows).toHaveLength(2);
if (!rows?.[0] || !rows[1]) throw new Error("Missing rating rows");
// Ten circles share the first row; the remaining skill has five.
expect(rows.map((row) => row.circles)).toEqual([10, 5]);
expect(text).toContain("Keyword11");
expect(text).toContain("Skill 2");
});
it("aligns three columns and preserves an incomplete row", async () => {
const {
rows: [rows],
@@ -1,7 +1,8 @@
import type { ResumeData } from "@reactive-resume/schema/resume/data";
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "@reactive-resume/schema/resume/default";
import { getSectionStyleRuleContext, resolveStyleRuleSlot } from "./style-rules";
import { getSectionStyleRuleContext } from "@reactive-resume/schema/resume/style-rules";
import { resolveStyleRuleSlot } from "./style-rules";
const createResumeData = (styleRules: ResumeData["metadata"]["styleRules"]): ResumeData => ({
...defaultResumeData,
@@ -1,17 +1,13 @@
import type { ResumeData, StyleIntent, StyleSlot } from "@reactive-resume/schema/resume/data";
import type { SectionStyleRuleContext } from "@reactive-resume/schema/resume/style-rules";
import type { Style } from "../../forme/style-types";
import { getSectionStyleRuleContext, resolveStyleIntentForSlot } from "@reactive-resume/schema/resume/style-rules";
import { resolveStyleIntentForSlot } from "@reactive-resume/schema/resume/style-rules";
import { rgbaStringToHex } from "@reactive-resume/utils/color";
export type { SectionStyleRuleContext };
export type ResolveStyleRuleSlotOptions = SectionStyleRuleContext & {
slot: StyleSlot;
};
export { getSectionStyleRuleContext };
const spacingProperties = [
"padding",
"paddingTop",
@@ -1,45 +0,0 @@
import { describe, expect, it } from "vitest";
import { defaultResumeData } from "./default";
import { resolveStyleRuleFontSize } from "./style-rules";
describe("resolveStyleRuleFontSize", () => {
it("returns global icon font sizes without section context", () => {
const data = {
...defaultResumeData,
metadata: {
...defaultResumeData.metadata,
styleRules: [
{
id: "icon-global",
label: "",
enabled: true,
target: { scope: "global" as const },
slots: { icon: { fontSize: 22 } },
},
],
},
};
expect(resolveStyleRuleFontSize(data, { slot: "icon" })).toBe(22);
});
it("does not apply level font sizes to the icon slot", () => {
const data = {
...defaultResumeData,
metadata: {
...defaultResumeData.metadata,
styleRules: [
{
id: "level-global",
label: "",
enabled: true,
target: { scope: "global" as const },
slots: { level: { fontSize: 18 } },
},
],
},
};
expect(resolveStyleRuleFontSize(data, { slot: "icon" })).toBeUndefined();
});
});
-12
View File
@@ -26,8 +26,6 @@ const builtInSectionTypes = new Set<SectionType>([
"references",
]);
const clamp = (value: number, min: number, max: number) => Math.min(max, Math.max(min, value));
export const getSectionStyleRuleContext = (data: ResumeData, sectionId: string): SectionStyleRuleContext => {
if (sectionId === "summary") return { sectionId, sectionType: "summary" };
if (builtInSectionTypes.has(sectionId as SectionType)) {
@@ -62,13 +60,3 @@ export const resolveStyleIntentForSlot = (data: ResumeData, options: ResolveStyl
return Object.assign({}, ...bySpecificity.map((rule) => rule.slots[options.slot]));
};
export const resolveStyleRuleFontSize = (
data: ResumeData,
options: ResolveStyleRuleSlotOptions,
): number | undefined => {
const fontSize = resolveStyleIntentForSlot(data, options).fontSize;
if (fontSize === undefined) return undefined;
return clamp(fontSize, 6, 48);
};
+1 -16
View File
@@ -1,15 +1,6 @@
import type { E2EAccount } from "./data";
import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { Pool } from "pg";
const legacyParityRules = JSON.parse(
readFileSync(
resolve(process.cwd(), "packages/pdf/src/semantic/__fixtures__/legacy/custom-section-type.json"),
"utf8",
),
) as unknown[];
function getDatabaseUrl() {
const databaseUrl = process.env.DATABASE_URL;
if (!databaseUrl) throw new Error("DATABASE_URL is required for E2E cleanup.");
@@ -28,7 +19,7 @@ export async function deleteE2EUser(account: E2EAccount) {
}
type SemanticStylesheetSeed = {
mode: "legacy" | "semantic";
mode: "semantic";
source: { languageVersion: number; text: string };
};
@@ -38,7 +29,6 @@ async function _updateSemanticCssFixture(
stylesheet?: SemanticStylesheetSeed;
portableLayout?: "balanced" | "pagination-stress";
experienceItemId?: string;
legacyStyleRule?: boolean;
hidePicture?: boolean;
basicsName?: string;
},
@@ -116,11 +106,6 @@ async function _updateSemanticCssFixture(
},
];
}
if (update.legacyStyleRule) {
const metadata = data.metadata as Record<string, unknown>;
delete metadata.stylesheet;
metadata.styleRules = structuredClone(legacyParityRules);
}
if (update.hidePicture) {
const picture = data.picture as Record<string, unknown>;
picture.hidden = true;