אייקוני שפות התכנות ===================== :summary: כל קובץ ב-Web App מוצג עם אייקון שמייצג את שפת התכנות שלו. עד אוגוסט 2026 אלה היו אמוג'ים (🐍 לפייתון, 📜 ל-JavaScript); היום אלה אייקונים מצוירים בסגנון אחיד — אריח ריבועי עם גרדיאנט וסימן לבן. המסמך מתאר איך המערכת בנויה, איך מוסיפים אייקון, ובאילו מלכודות כבר נתקלנו. .. contents:: :local: :depth: 2 למה זה נבנה כמערכת ולא כהחלפה נקודתית --------------------------------------- לפני השינוי, אייקוני השפה יוצרו בחמישה מקומות נפרדים עם כיסוי שונה: .. list-table:: :header-rows: 1 * - המקום - כמה שפות כיסה * - ``get_language_icon`` ב-``webapp/app.py`` - 28 * - ``getFileIcon`` ב-``base.html`` - 8 * - מפה מקומית ב-``global_search.js`` - חלקי * - מפה מקומית ב-``snippets.js`` - חלקי * - מפות ב-``collections.js`` - חלקי התוצאה הייתה שאותו קובץ קיבל אייקון אחד בדף הקבצים ואייקון אחר בחיפוש הגלובלי. החלפה נקודתית בכל אחד מהחמישה הייתה משכפלת את הבעיה במקום לפתור אותה, ולכן המערכת נבנתה סביב **מקור אמת אחד**. המבנה ------ .. code-block:: text FEATURE_SUGGESTIONS/New_Icons/*.svg 28 קבצי מקור, אחד לכל שפה │ │ scripts/build_lang_sprite.py ▼ webapp/templates/components/lang_sprite.html ספרייט מוזרק ב-base.html │ ├──► lang_icon() ב-webapp/app.py — צד השרת (Jinja) └──► window.langIcon() ב-base.html — צד הלקוח שני הצדדים ניזונים מאותם נתונים: השרת מזריק אותם ל-``window.LANG_ICON_DATA``, ולכן אין מפת שפות משוכפלת בקוד ה-JS. צד השרת ~~~~~~~~ ``lang_icon()`` רשומה כגלובל של Jinja וזמינה בכל תבנית בלי import: .. code-block:: jinja {{ lang_icon(file.language, LANG_ICON_SIZES.file_row) }} חתימה: ``lang_icon(language, size=32, css_class='', decorative=True)``. סדר העדיפויות בבחירת האייקון: 1. יש אייקון מצויר לשפה — מחזירים אותו. 2. אין אייקון אבל יש לשפה סמל ייחודי במפת האמוג'י (למשל 🔒 לקובץ בינארי) — שומרים עליו במקום לאבד מידע. 3. שפה שאינה מוכרת בכלל — אייקון ה-``text``, כדי שלא תתקבל תערובת של אמוג'י ואייקונים באותה רשימה. צד הלקוח ~~~~~~~~~ שתי פונקציות, שתיהן ב-``base.html``: - ``window.langIcon(language, size, decorative)`` — מחזירה מחרוזת HTML, לקוד שמרכיב תבנית טקסטואלית. - ``window.langIconEl(language, size, decorative)`` — מחזירה אלמנט DOM בנוי ב-``createElementNS``. **עדיפה** בכל מקום שממילא בונה DOM, כי אז אין צורך ב-``innerHTML`` בכלל. שתיהן נגזרות מאותה החלטה פנימית, כדי שלא יתפצלו. גדלים ------ הגדלים מרוכזים ב-``LANG_ICON_SIZES`` ב-``webapp/app.py``, ונחשפים גם ל-Jinja וגם ללקוח דרך ``window.LANG_ICON_DATA.sizes``: .. list-table:: :header-rows: 1 * - שם - גודל - היכן * - ``file_row`` - 44px - דף הקבצים, כרטיסים נעוצים, עמוד הקובץ * - ``list`` - 32px - דשבורד, קבצים משותפים, סל מיחזור, מודאלים * - ``timeline`` - 28px - אירועי קבצים בטיימליין הפעילות * - ``search`` - 28px - תוצאות החיפוש הגלובלי * - ``compact`` - 22px - ספריית הסניפטים .. important:: אין להשתמש במספר קשיח בקריאה ל-``lang_icon`` בתבנית. יש בדיקה שנופלת על כך (``test_templates_use_named_sizes_not_numbers``). הסיבה אינה אסתטית: כשהמספרים ישבו בנפרד, אותו מסך קיבל שני גדלים שונים בשני מסלולי רינדור. למה 44px ולא 32px ~~~~~~~~~~~~~~~~~~ האייקונים הוחלפו בתחילה באותו גודל שהיה לאמוג'י, והתוצאה נראתה קטנה יותר. מדידה בדפדפן הראתה שתי סיבות שמצטברות: - אמוג'י ב-``font-size: 32px`` מצויר בפועל ב-**35.5px** — הגליף חורג מהמסגרת שהוגדרה לו. אייקון SVG ב-32px מצויר ב-**30px** בדיוק, כי ל-``viewBox`` יש שוליים פנימיים. - והעיקר: הסימן הלבן בתוך אריח של 32px נמדד **15×15 פיקסלים**, מול 35.5px של אמוג'י שהצורה שלו ממלאת את כל שטחו. הסימן שהעין מזהה קטן פי 2.4. השני הוא תכונה של האריח ולא באג — יש לו נשימה מכוונת סביב הסימן. המסקנה המעשית: **החלפת אמוג'י באריח דורשת הגדלה**, ולא החלפה 1:1. הוספה או שינוי של אייקון -------------------------- .. code-block:: bash # 1. ערכו או הוסיפו קובץ תחת FEATURE_SUGGESTIONS/New_Icons/.svg # 2. בנו מחדש את הספרייט python scripts/build_lang_sprite.py # 3. אם נוספה שפה חדשה — הוסיפו את ה-slug ל-LANG_ICON_SLUGS ב-webapp/app.py # אימות שהספרייט מסונכרן, בלי לכתוב: python scripts/build_lang_sprite.py --check הספרייט נגזר מקבצי המקור ולא נערך ידנית. הסיבה קונקרטית: כשארבעת האייקונים האחרונים נוספו לתיקייה, הספרייט נשאר עם 24 ואף אחד לא שם לב עד שנבדק. מבנה קובץ אייקון ~~~~~~~~~~~~~~~~~ כל אייקון הוא ``viewBox="0 0 64 64"`` ובנוי מארבע שכבות. הדוגמה למטה היא אייקון ה-markdown בפועל, פרט ל-``SLUG`` שיש להחליף בשם השפה. שני ערכי ה-``stop-color`` הם הגוון הבהיר והכהה של האריח — כל אייקון בוחר זוג משלו: .. code-block:: xml .. warning:: ה-IDs חייבים לכלול את שם השפה (``g-python``, לא ``g``). בגרסה הראשונה כל 24 הקבצים השתמשו ב-``id="g"`` ו-``id="m"``, ולכן הטמעת שניים מהם inline באותו דף גרמה לשני האייקונים לקבל את הגרדיאנט של הראשון. שמות נרדפים ~~~~~~~~~~~~ ``LANG_ICON_ALIASES`` ממפה שפות שחולקות אייקון: ``shell``/``sh``/``zsh`` ל-``bash``, ``scss``/``sass``/``less`` ל-``css``, ``node``/``nodejs`` ל-``javascript`` ועוד. זה חוסך ציור אייקון כפול. .. note:: ב-``snippets.js`` יש נרמול שפות **נפרד**, לצורכי הדגשת תחביר. שתי המפות אינן זהות בכוונה: ``scss`` חולק אייקון עם ``css``, אבל חייב להישאר ``scss`` כדי ש-highlight.js יצבע נכון. בדיקה מוודאת שהן לא סותרות זו את זו. נגישות ------- האייקון מוסתר מקוראי מסך כברירת מחדל (``aria-hidden="true"``). בכל מקום שהוא מוצג בו מופיע לצדו שם הקובץ עם הסיומת או תגית השפה, כך שהקראה נוספת רק מכפילה את המידע — ובשורות השפות בדשבורד היא גרמה להקראת השם פעמיים. כשהאייקון הוא הסימן היחיד לשפה, העבירו ``decorative=False`` כדי לקבל ``role="img"`` ו-``aria-label``. מלכודות שכבר נתקלנו בהן ------------------------- הסתרת הספרייט ~~~~~~~~~~~~~~ הספרייט חייב להיות מוסתר עם ``position:absolute;width:0;height:0;overflow:hidden`` ולא עם ``display:none``. עם ``display:none`` הדפדפן אינו מרנדר את הגרדיאנטים שב-````, וכל האריחים יוצאים **שקופים לגמרי** — האייקון מופיע בלי צבע. יש בדיקה שמונעת חזרה לאחור. הטמעה inline ולא כקובץ חיצוני ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ```` — הפניה לקובץ SVG חיצוני — אינה נתמכת ב-Safari/iOS בלי polyfill. לכן הספרייט מוזרק inline ב-``base.html``. העלות: כ-22KB לכל דף, כ-5KB אחרי gzip, בתמורה לאפס בקשות רשת. שני מסלולי רינדור לאותו מסך ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ הטיימליין בדשבורד נבנה בשני מסלולים: רינדור ראשוני ב-``app.py``, ו"טען עוד" דרך ``/api/dashboard/activity/files``. כשרק הראשון עבר לאייקון מצויר, לחיצה על "טען עוד" הכניסה אמוג'ים לרשימה שכבר הכילה אייקונים. הפתרון אינו העתקת הקריאה לשני המקומות. האירוע נושא את שם השפה בשדה ``icon_lang``, וצד הלקוח בונה ממנו את האייקון עם ``langIconEl`` — אותה פונקציה שבונה כל אייקון אחר. כך אין HTML ב-JSON, ואין שני מימושים שיכולים להיפרד. הנחיות לסוכני AI ------------------ - **אל תסתמכו על האמוג'י כדי לזהות שפה.** השדה הקובע הוא ``language`` (או ``programming_language`` במסד). ה-API מחזיר אותו לצד ``icon``. - ``get_language_icon`` עדיין קיימת ומחזירה אמוג'י. היא נשארה לתאימות ולשימוש כ-fallback, ואינה מה שמוצג למשתמש ברשימות הקבצים. - כשמוסיפים מקום תצוגה חדש, השתמשו ב-``lang_icon`` עם גודל מ-``LANG_ICON_SIZES`` ולא במספר קשיח, ובצד הלקוח ב-``langIconEl`` ולא ב-``innerHTML``. בדיקות ------- ``tests/test_lang_icons.py`` — הבדיקות המרכזיות: - כל שפה ברשימה קיימת כ-```` בספרייט, וכל ```` רשום ברשימה. - הספרייט מסונכרן עם קבצי המקור (מריצה את הסקריפט ב-``--check``). - השרת והלקוח מייצרים HTML **זהה** — הבדיקה מריצה את קוד ה-JS ב-Node ומשווה מול ``lang_icon`` על 25 קלטים, רובם מקרי קצה של גודל. - שני מסלולי הטיימליין מעבירים ``icon_lang`` ושולפים את הגודל מאותו מקום. - אין גדלים קשיחים בקריאות ``lang_icon`` בתבניות.