אייקוני שפות התכנות

summary:

כל קובץ ב-Web App מוצג עם אייקון שמייצג את שפת התכנות שלו. עד אוגוסט 2026 אלה היו אמוג’ים (🐍 לפייתון, 📜 ל-JavaScript); היום אלה אייקונים מצוירים בסגנון אחיד — אריח ריבועי עם גרדיאנט וסימן לבן.

המסמך מתאר איך המערכת בנויה, איך מוסיפים אייקון, ובאילו מלכודות כבר נתקלנו.

למה זה נבנה כמערכת ולא כהחלפה נקודתית

לפני השינוי, אייקוני השפה יוצרו בחמישה מקומות נפרדים עם כיסוי שונה:

המקום

כמה שפות כיסה

get_language_icon ב-webapp/app.py

28

getFileIcon ב-base.html

8

מפה מקומית ב-global_search.js

חלקי

מפה מקומית ב-snippets.js

חלקי

מפות ב-collections.js

חלקי

התוצאה הייתה שאותו קובץ קיבל אייקון אחד בדף הקבצים ואייקון אחר בחיפוש הגלובלי. החלפה נקודתית בכל אחד מהחמישה הייתה משכפלת את הבעיה במקום לפתור אותה, ולכן המערכת נבנתה סביב מקור אמת אחד.

המבנה

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:

{{ 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:

שם

גודל

היכן

file_row

44px

דף הקבצים, כרטיסים נעוצים, עמוד הקובץ

list

32px

דשבורד, קבצים משותפים, סל מיחזור, מודאלים

timeline

28px

אירועי קבצים בטיימליין הפעילות

search

28px

תוצאות החיפוש הגלובלי

compact

22px

ספריית הסניפטים

חשוב

אין להשתמש במספר קשיח בקריאה ל-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.

הוספה או שינוי של אייקון

# 1. ערכו או הוסיפו קובץ תחת FEATURE_SUGGESTIONS/New_Icons/<slug>.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 הם הגוון הבהיר והכהה של האריח — כל אייקון בוחר זוג משלו:

<defs>
  <linearGradient id="g-SLUG" x1="0" y1="0" x2=".35" y2="1">
    <stop offset="0" stop-color="#8fa6c9"/><stop offset="1" stop-color="#3b5378"/>
  </linearGradient>
  <g id="m-SLUG"> <!-- המוטיב, ב-currentColor --> </g>
</defs>
<rect x="2" y="2" width="60" height="60" rx="15" fill="url(#g-SLUG)"/>
<rect x="3" y="3" width="58" height="58" rx="14" fill="none"
      stroke="#fff" stroke-opacity=".28" stroke-width="1.6"/>
<use href="#m-SLUG" transform="translate(0 1.8)" color="#141010" opacity=".22"/>
<use href="#m-SLUG" color="#ffffff"/>

אזהרה

ה-IDs חייבים לכלול את שם השפה (g-python, לא g). בגרסה הראשונה כל 24 הקבצים השתמשו ב-id="g" ו-id="m", ולכן הטמעת שניים מהם inline באותו דף גרמה לשני האייקונים לקבל את הגרדיאנט של הראשון.

שמות נרדפים

LANG_ICON_ALIASES ממפה שפות שחולקות אייקון: shell/sh/zsh ל-bash, scss/sass/less ל-css, node/nodejs ל-javascript ועוד. זה חוסך ציור אייקון כפול.

הערה

ב-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 הדפדפן אינו מרנדר את הגרדיאנטים שב-<defs>, וכל האריחים יוצאים שקופים לגמרי — האייקון מופיע בלי צבע. יש בדיקה שמונעת חזרה לאחור.

הטמעה inline ולא כקובץ חיצוני

<use href="/static/sprite.svg#lang-python"> — הפניה לקובץ 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 — הבדיקות המרכזיות:

  • כל שפה ברשימה קיימת כ-<symbol> בספרייט, וכל <symbol> רשום ברשימה.

  • הספרייט מסונכרן עם קבצי המקור (מריצה את הסקריפט ב---check).

  • השרת והלקוח מייצרים HTML זהה — הבדיקה מריצה את קוד ה-JS ב-Node ומשווה מול lang_icon על 25 קלטים, רובם מקרי קצה של גודל.

  • שני מסלולי הטיימליין מעבירים icon_lang ושולפים את הגודל מאותו מקום.

  • אין גדלים קשיחים בקריאות lang_icon בתבניות.