מדריך מלא: מימוש מצב חשוך מותאם לעיניים
תאריך: ינואר 2025
גרסה: 1.0
סטטוס: מדריך מימוש
📋 תוכן עניינים
- סקירה כללית
- דרישות מקדימות
- שלב 1: הוספת CSS Variables למצב חשוך
- שלב 2: יצירת קובץ CSS למצב חשוך
- שלב 3: עדכון Backend
- שלב 4: עדכון UI - הוספת טוגל
- שלב 5: התאמת CodeMirror
- שלב 6: התאמת Pygments
- שלב 7: JavaScript לניהול מצב
- שלב 8: התאמת קומפוננטים נוספים
- בדיקות ואימות
- טיפים ופתרון בעיות
סקירה כללית
אז, חברים, בואו נצלול לעומק ונבין איך אפשר להפוך את האפליקציה שלנו לידידותית יותר לעיניים עם מצב חשוך אמיתי. המדריך הזה יעזור לכם לממש מצב חשוך מלא, שמותאם אישית לעיניים, תוך ניצול חכם של מערכת ה-theme הקיימת שלכם. זה לא סתם "מצב חשוך", אלא חוויה שלמה שתהפוך את השימוש באפליקציה לנוח ונעים יותר, במיוחד בשעות הערב או בלילה. מה הופך את המצב החשוך שלנו למיוחד? ובכן, הוא משלב כמה אלמנטים חשובים שיבטיחו חוויה אופטימלית:
- ✅ פלטת צבעים מאוזנת (low blue light): הבהירות המוגזמת של מסכים יכולה לגרום לעייפות עיניים. המצב החשוך שלנו משתמש בפלטת צבעים רכה יותר, עם פחות אור כחול, שמפחיתה את העומס על העיניים ומאפשרת שימוש ממושך בנוחות.
- ✅ ניגודיות מותאמת לקריאה ארוכה: ניגודיות היא המפתח לקריאות. במצב חשוך, אנחנו שואפים לניגודיות אופטימלית – מספיק חזקה כדי שהטקסט יהיה ברור וחד, אבל לא כל כך חזקה שתגרום לסנוור או לעייפות. זה אומר בחירת צבעי טקסט ורקע שמשלימים זה את זה בצורה מושלמת.
- ✅ syntax highlighting מותאם למצב חשוך: עבור מפתחים, קריאות הקוד היא קריטית. המצב החשוך שלנו מבטיח שגם ה-syntax highlighting יהיה מותאם – צבעים מובחנים ונעימים לעין שמשפרים את הקריאות וההבנה של הקוד.
- ✅ מעבר חלק בין מצבים (fade transition): אין דבר פחות נעים ממעבר פתאומי בין מצבים. אנחנו נדאג למעברים עדינים וחלקים, שיגרמו לשינוי ה-theme להרגיש טבעי וזורם.
- ✅ התאמה אוטומטית למערכת (prefers-color-scheme): האפליקציה תדע לזהות מתי מערכת ההפעלה שלכם נמצאת במצב חשוך ולהתאים את עצמה אוטומטית. אם אתם אוהבים את מה שהמערכת קבעה, אתם לא צריכים לעשות כלום!
- ✅ 3 רמות: Light / Dim / Dark: אנחנו לא מגבילים אתכם לאופציה אחת. אתם תוכלו לבחור בין מצב בהיר (Light), מצב מעומעם (Dim) שמהווה פשרה נעימה, למצב חשוך מלא (Dark).
- ✅ שמירת העדפה ב-localStorage ו-DB: הבחירה שלכם תישמר, בין אם זה באמצעות ה-localStorage בדפדפן שלכם ובין אם בבסיס הנתונים של המשתמש, כך שההעדפה שלכם תישמר בין הפעלות ובכל המכשירים.
המדריך הזה יפרט צעד אחר צעד את כל התהליך, מהוספת משתני CSS ועד להתאמת ספריות חיצוניות. אז בואו נתחיל להפוך את החוויה שלנו לנעימה ונוחה יותר!
דרישות מקדימות
רגע לפני שאתם צוללים לעומק המדריך, חשוב שנדבר רגע על מה שאתם צריכים כדי שהדברים יזרמו חלק. אין פה שום דבר מסובך מדי, אבל ידיעה מוקדמת תעזור לכם להימנע מתסכולים מיותרים. קודם כל, הבנה בסיסית ב-CSS, JavaScript ו-Flask. אם אתם מכירים את הבסיס של השפות האלה, אתם בכיוון הנכון. אתם לא צריכים להיות מומחים, אבל אתם כן צריכים להרגיש בנוח עם מבנה הקוד, איך משתנים עובדים, איך בונים פונקציות ב-JS, ואיך Flask מנהל את הבקשות והתגובות. זה המנוע שיניע את כל השינויים שאנחנו הולכים לעשות. גישה לקוד ה-Source של WebApp שלכם היא חובה. בלי גישה לקוד, אין באמת מה לעשות, נכון? אתם צריכים להיות מסוגלים לערוך קבצים, להוסיף חדשים, ולבדוק את השינויים שלכם. אז תדאגו שיש לכם את כל ההרשאות הדרושות. לבסוף, ידע במערכת ה-theme הקיימת שלכם, ובפרט איך היא משתמשת ב-data-theme attribute. רוב האפליקציות המודרניות משתמשות במנגנון הזה כדי להחליף theme – זה אומר שה-HTML tag הראשי (בדרך כלל <html> או <body>) מקבל attribute שמשנה את הצבעים והסגנונות. הבנה איך זה עובד אצלכם תקל מאוד על ההתאמה של המצב החשוך, כי אנחנו נשתמש באותו מנגנון בדיוק. אם אתם לא בטוחים איך זה עובד, תסתכלו על הקוד הקיים, חפשו את ה-attribute הזה, ותבינו איך הוא משפיע על הסגנונות. אם אתם מתכננים להשתמש במדריך הזה, ודאו שאתם מכירים את המבנה הזה. זה יבטיח שהמדריך יהיה רלוונטי וקל ליישום עבורכם. אז, עם הבסיס הזה, אתם מוכנים לצאת לדרך!
שלב 1: הוספת CSS Variables למצב חשוך
חברים, אחד הדברים הכי חשובים בבניית theme גמיש, כמו המצב החשוך שלנו, זה להשתמש ב-CSS Variables, שנקראים גם Custom Properties. הם מאפשרים לנו להגדיר ערכים (כמו צבעים, גדלים וכו') במקום אחד, ולהשתמש בהם בכל מקום בקוד ה-CSS שלנו. כשה theme משתנה, אנחנו פשוט משנים את הערכים של המשתנים האלה, וכל מה שמשתמש בהם מתעדכן אוטומטית. זה הופך את התהליך להרבה יותר נקי ויעיל. אז, הדבר הראשון שאנחנו צריכים לעשות זה להגדיר את משתני ה-CSS עבור המצב החשוך שלנו. זה נעשה בקובץ ה-base.html, שזה כמו השלד של כל הדפים שלנו. אנחנו מוסיפים בלוק CSS חדש שמתחיל עם :root[data-theme="dark"]. זה אומר שכל מה שבפנים יחול רק כאשר ה-data-theme attribute על האלמנט הראשי (בדרך כלל <html>) הוא "dark". כאן אנחנו מגדירים את פלטת הצבעים שלנו למצב החשוך:
--bg-primary,--bg-secondary,--bg-tertiary: אלו יהיו צבעי הרקע העיקריים שלנו. במצב חשוך, אנחנו רוצים רקעים כהים, אבל לא שחורים מדי, כדי שלא יהיה קשה מדי להבחין בהבדלים. נבחר בגוונים של אפור כהה.--text-primary,--text-secondary,--text-muted: אלו צבעי הטקסט. במצב חשוך, הטקסט צריך להיות בהיר מספיק כדי לקרוא אותו בנוחות על רקע כהה. נשתמש בגוונים של לבן ואפור בהיר, כש--text-primaryהוא הצבע הראשי והבהיר ביותר.--primary,--primary-dark,--secondary: אלו צבעי האקסנט שלנו – הצבעים שיבלטו וימשכו תשומת לב, כמו כפתורים, לינקים או אלמנטים חשובים אחרים. גם כאן, נבחר גוונים שיתאימו למצב החשוך, כמו כחול או סגול עדינים.--success,--danger,--warning,--info: צבעי מערכת סטנדרטיים, שגם אותם נתאים למצב החשוך כדי שיתאימו לפלטה הכללית.--glass,--glass-border,--glass-hover: אלו משתנים שמגדירים אפקט "glassmorphism" עדין, שנותן תחושה של שקיפות וריחוף. במצב חשוך, נשתמש בצבעים כמעט שקופים של לבן.--card-bg,--card-border: צבעי הרקע והגבול של כרטיסים או "בלוקים" באפליקציה. גם כאן, נרצה משהו שמרגיש כהה ונעים.--code-bg,--code-text,--code-border: צבעים מיוחדים לקטעי קוד, כדי לוודא ש-syntax highlighting יהיה ברור ונעים לקריאה.
בנוסף, אנחנו מגדירים גם :root[data-theme="dim"] עבור המצב המעומעם, שיהיה קצת יותר בהיר מהמצב החשוך המלא. ואז, אנחנו מעדכנים את הרקע של ה-<body> כדי שישתמש בצבעים החדשים שלנו, ומוסיפים אפקט רקע עדין שמתאים למצב החשוך. כל השינויים האלה נעשים בתוך קובץ base.html, ממש אחרי ההגדרות הקיימות, כדי שהם יחליפו את ברירות המחדל.
1.1 עדכון base.html
/* Theme palettes - הוספה אחרי השורות הקיימות */
:root[data-theme="dark"] {
/* צבעי רקע - כהה ונוח לעיניים */
--bg-primary: #1a1a1a;
--bg-secondary: #252525;
--bg-tertiary: #2d2d2d;
/* צבעי טקסט - ניגודיות מאוזנת */
--text-primary: #e0e0e0;
--text-secondary: #b0b0b0;
--text-muted: #808080;
/* צבעי אקסנט - מותאמים למצב חשוך */
--primary: #7c8aff;
--primary-dark: #6b7aff;
--secondary: #9d7aff;
/* צבעי מערכת */
--success: #4ade80;
--danger: #f87171;
--warning: #fbbf24;
--info: #60a5fa;
/* Glass morphism - מותאם למצב חשוך */
--glass: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
--glass-hover: rgba(255, 255, 255, 0.08);
/* צבעי רקע לכרטיסים */
--card-bg: rgba(30, 30, 30, 0.8);
--card-border: rgba(255, 255, 255, 0.1);
/* צבעי קוד */
--code-bg: #1e1e1e;
--code-text: #d4d4d4;
--code-border: rgba(255, 255, 255, 0.1);
}
/* מצב Dim - ביניים */
:root[data-theme="dim"] {
--bg-primary: #2a2a2a;
--bg-secondary: #333333;
--bg-tertiary: #3a3a3a;
--text-primary: #d0d0d0;
--text-secondary: #a0a0a0;
--text-muted: #707070;
--primary: #7c8aff;
--secondary: #9d7aff;
--glass: rgba(255, 255, 255, 0.08);
--glass-border: rgba(255, 255, 255, 0.15);
--card-bg: rgba(40, 40, 40, 0.8);
--code-bg: #2a2a2a;
}
/* עדכון body למצב חשוך */
:root[data-theme="dark"] body,
:root[data-theme="dim"] body {
background: linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%);
color: var(--text-primary);
}
/* עדכון רקע הגל למצב חשוך */
:root[data-theme="dark"] body::before,
:root[data-theme="dim"] body::before {
background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320"><path fill="%23000000" fill-opacity="0.1" d="M0,96L48,112C96,128,192,160,288,160C384,160,480,128,576,122.7C672,117,768,139,864,138.7C960,139,1056,117,1152,101.3C1248,85,1344,75,1392,69.3L1440,64L1440,320L1392,320C1344,320,1248,320,1152,320C1056,320,960,320,864,320C768,320,672,320,576,320C480,320,384,320,288,320C192,320,96,320,48,320L0,320Z"/></svg>') no-repeat bottom center;
}
מיקום: הוסף אחרי שורה 57 ב-base.html (אחרי :root[data-theme="forest"])
שלב 2: יצירת קובץ CSS למצב חשוך
אז אחרי שהגדרנו את משתני ה-CSS הבסיסיים שלנו, הגיע הזמן להגדיר את הסגנונות הספציפיים שישתמשו במשתנים האלה. את זה נעשה בקובץ CSS חדש, שנקרא לו dark-mode.css. הקובץ הזה יכיל את כל הכללים שמשנים את המראה של האלמנטים השונים באפליקציה כאשר data-theme הוא "dark" או "dim". חשוב להבין שהקובץ הזה לא מחליף את ה-CSS הקיים, אלא מרחיב אותו. הוא מוסיף סגנונות שמופעלים רק כאשר theme מסוים פעיל. זה מבטיח שה-CSS הראשי שלנו נשאר נקי וברור, ושהשינויים למצב חשוך מרוכזים במקום אחד.
מה אנחנו הולכים לעצב בקובץ הזה? כמעט הכל!
- Base Elements: נתחיל עם אלמנטים בסיסיים כמו ה-navbar, כרטיסים (
.glass-card), כדי לוודא שהם נראים טוב במצב חשוך. נשתמש באפקט ה-glassmorphism שהגדרנו במשתנים, עם רקע שקוף-כהה וגבול עדין. - Typography: נתאים את צבעי הכותרות (
h1עדh6) והפסקאות (p) כך שיהיו קריאים על הרקע הכהה. - Buttons: כפתורים הם חלק מרכזי בחוויית המשתמש. נגדיר את הצבעים והמראה של כפתורי ה-primary וה-secondary שלנו, ונוסיף אפקטים קטנים כמו
transformו-box-shadowכדי להפוך אותם לדינמיים יותר. - Inputs & Forms: שדות קלט, תיבות טקסט ובחירות צריכים להיראות ולהרגיש נכון. ניתן להם רקע כהה, גבול מתאים, וגם צבע מיוחד כשמתמקדים בהם (
:focus). - Code Blocks: זה קריטי! נדאג שבלוקי קוד (
.source) וגם מספרי שורות (.linenos) ייראו מצוין, עם רקע כהה וטקסט בהיר, תוך שימוש במשתנים שהגדרנו (--code-bg,--code-text,--code-border). - Alerts: הודעות מערכת (הצלחה, שגיאה, מידע) יקבלו עיצוב חדש שמתאים למצב החשוך, תוך שימוש בצבעי המערכת שהגדרנו.
- Badges: תגיות או "באדג'ים" צריכים להיראות ברורים ולא להפריע.
- Links: קישורים ישתנו לצבע ה-primary שלנו, עם שינוי עדין בצבע בעת ריחוף.
- Quick Access Menu, Modals, File Cards: כל האלמנטים המורכבים יותר, כמו תפריטים נפתחים, חלונות "פופ-אפ" (modals), וכרטיסי קבצים, יקבלו את העיצוב החשוך שלהם.
- Search & Filters: שדות חיפוש ובחירות סינון צריכים להיות ברורים וקלים לשימוש.
- Transitions: נוסיף transition גלובלי לכל האלמנטים כדי להבטיח שהשינויים יקרו בצורה חלקה ונעימה לעין.
- Scrollbar: נתאים גם את מראה פס הגלילה, כך שיתמזג עם העיצוב הכללי.
- Selection: הדרך שבה טקסט מסומן צריכה גם היא להתאים.
- Mobile Adjustments: נוסיף התאמות קטנות למסכים קטנים יותר, כמו בטאבלטים וטלפונים.
בסופו של דבר, המטרה היא ליצור חוויה ויזואלית עקבית ונעימה בכל רחבי האפליקציה, בכל אחד מהמצבים שבחרנו.
2.1 יצירת dark-mode.css
/* Dark Mode Styles - Comprehensive Theme Support */
/* ============================================
Base Elements
============================================ */
[data-theme="dark"] .navbar,
[data-theme="dim"] .navbar {
background: var(--glass);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--glass-border);
}
[data-theme="dark"] .glass-card,
[data-theme="dim"] .glass-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--text-primary);
}
[data-theme="dark"] .glass-card:hover,
[data-theme="dim"] .glass-card:hover {
background: var(--glass-hover);
}
/* ============================================
Typography
============================================ */
[data-theme="dark"] h1,
[data-theme="dark"] h2,
[data-theme="dark"] h3,
[data-theme="dark"] h4,
[data-theme="dark"] h5,
[data-theme="dark"] h6,
[data-theme="dim"] h1,
[data-theme="dim"] h2,
[data-theme="dim"] h3,
[data-theme="dim"] h4,
[data-theme="dim"] h5,
[data-theme="dim"] h6 {
color: var(--text-primary);
}
[data-theme="dark"] p,
[data-theme="dim"] p {
color: var(--text-secondary);
}
/* ============================================
Buttons
============================================ */
[data-theme="dark"] .btn-primary,
[data-theme="dim"] .btn-primary {
background: var(--primary);
color: white;
border: 1px solid var(--primary-dark);
}
[data-theme="dark"] .btn-primary:hover,
[data-theme="dim"] .btn-primary:hover {
background: var(--primary-dark);
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(124, 138, 255, 0.4);
}
[data-theme="dark"] .btn-secondary,
[data-theme="dim"] .btn-secondary {
background: var(--glass);
color: var(--text-primary);
border: 1px solid var(--glass-border);
}
[data-theme="dark"] .btn-secondary:hover,
[data-theme="dim"] .btn-secondary:hover {
background: var(--glass-hover);
border-color: var(--primary);
}
/* ============================================
Inputs & Forms
============================================ */
[data-theme="dark"] input,
[data-theme="dark"] textarea,
[data-theme="dark"] select,
[data-theme="dim"] input,
[data-theme="dim"] textarea,
[data-theme="dim"] select {
background: var(--bg-tertiary);
color: var(--text-primary);
border: 1px solid var(--glass-border);
}
[data-theme="dark"] input:focus,
[data-theme="dark"] textarea:focus,
[data-theme="dark"] select:focus,
[data-theme="dim"] input:focus,
[data-theme="dim"] textarea:focus,
[data-theme="dim"] select:focus {
outline: 2px solid var(--primary);
border-color: var(--primary);
}
[data-theme="dark"] input::placeholder,
[data-theme="dark"] textarea::placeholder,
[data-theme="dim"] input::placeholder,
[data-theme="dim"] textarea::placeholder {
color: var(--text-muted);
}
/* ============================================
Code Blocks
============================================ */
[data-theme="dark"] .source,
[data-theme="dim"] .source {
background: var(--code-bg) !important;
color: var(--code-text) !important;
border: 1px solid var(--code-border);
}
[data-theme="dark"] .highlighttable td.linenos,
[data-theme="dim"] .highlighttable td.linenos {
background: var(--bg-tertiary);
color: var(--text-muted);
border-right: 1px solid var(--code-border);
}
/* ============================================
Alerts
============================================ */
[data-theme="dark"] .alert-success,
[data-theme="dim"] .alert-success {
background: rgba(74, 222, 128, 0.15);
border: 1px solid rgba(74, 222, 128, 0.3);
color: var(--success);
}
[data-theme="dark"] .alert-error,
[data-theme="dim"] .alert-error {
background: rgba(248, 113, 113, 0.15);
border: 1px solid rgba(248, 113, 113, 0.3);
color: var(--danger);
}
[data-theme="dark"] .alert-info,
[data-theme="dim"] .alert-info {
background: rgba(96, 165, 250, 0.15);
border: 1px solid rgba(96, 165, 250, 0.3);
color: var(--info);
}
/* ============================================
Badges
============================================ */
[data-theme="dark"] .badge,
[data-theme="dim"] .badge {
background: var(--glass);
border: 1px solid var(--glass-border);
color: var(--text-primary);
}
/* ============================================
Links
============================================ */
[data-theme="dark"] a,
[data-theme="dim"] a {
color: var(--primary);
}
[data-theme="dark"] a:hover,
[data-theme="dim"] a:hover {
color: var(--primary-dark);
opacity: 0.9;
}
/* ============================================
Quick Access Menu
============================================ */
[data-theme="dark"] .quick-access-dropdown,
[data-theme="dim"] .quick-access-dropdown {
background: var(--card-bg);
border: 1px solid var(--card-border);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.5);
}
[data-theme="dark"] .quick-access-item,
[data-theme="dim"] .quick-access-item {
color: var(--text-primary);
border-color: var(--glass-border);
}
[data-theme="dark"] .quick-access-item:hover,
[data-theme="dim"] .quick-access-item:hover {
background: var(--glass-hover);
color: var(--primary);
}
/* ============================================
Modals
============================================ */
[data-theme="dark"] .recent-files-modal .modal-content,
[data-theme="dark"] .community-modal .modal-content,
[data-theme="dim"] .recent-files-modal .modal-content,
[data-theme="dim"] .community-modal .modal-content {
background: var(--card-bg);
color: var(--text-primary);
border: 1px solid var(--card-border);
}
[data-theme="dark"] .recent-files-modal .modal-header,
[data-theme="dark"] .community-modal .modal-header,
[data-theme="dim"] .recent-files-modal .modal-header,
[data-theme="dim"] .community-modal .modal-header {
border-bottom-color: var(--glass-border);
}
/* ============================================
File Cards
============================================ */
[data-theme="dark"] .file-card,
[data-theme="dim"] .file-card {
background: var(--card-bg);
border: 1px solid var(--card-border);
}
[data-theme="dark"] .file-card:hover,
[data-theme="dim"] .file-card:hover {
background: var(--glass-hover);
transform: translateY(-2px);
}
/* ============================================
Search & Filters
============================================ */
[data-theme="dark"] .search-input,
[data-theme="dark"] .filter-select,
[data-theme="dim"] .search-input,
[data-theme="dim"] .filter-select {
background: var(--bg-tertiary);
color: var(--text-primary);
border-color: var(--glass-border);
}
/* ============================================
Transitions - מעבר חלק
============================================ */
[data-theme="dark"] *,
[data-theme="dim"] *,
[data-theme="dark"] *::before,
[data-theme="dim"] *::before,
[data-theme="dark"] *::after,
[data-theme="dim"] *::after {
transition: background-color 0.3s ease,
color 0.3s ease,
border-color 0.3s ease,
box-shadow 0.3s ease;
}
/* ============================================
Scrollbar
============================================ */
[data-theme="dark"] ::-webkit-scrollbar-track,
[data-theme="dim"] ::-webkit-scrollbar-track {
background: var(--bg-primary);
}
[data-theme="dark"] ::-webkit-scrollbar-thumb,
[data-theme="dim"] ::-webkit-scrollbar-thumb {
background: var(--glass-border);
border-radius: 5px;
}
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover,
[data-theme="dim"] ::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* ============================================
Selection
============================================ */
[data-theme="dark"] ::selection,
[data-theme="dim"] ::selection {
background: rgba(124, 138, 255, 0.3);
color: var(--text-primary);
}
/* ============================================
Mobile Adjustments
============================================ */
@media (max-width: 768px) {
[data-theme="dark"] .glass-card,
[data-theme="dim"] .glass-card {
padding: 1rem;
}
}
2.2 הוספת הקובץ ל-base.html
אחרי שהגדרנו את הסגנונות החדשים, אנחנו צריכים לוודא שהם נטענים יחד עם שאר קבצי ה-CSS. נעשה זאת על ידי הוספת שורת <link> חדשה בקובץ base.html, ממש אחרי שאר קבצי ה-CSS. חשוב להוסיף את ה-?v={{ static_version }} בסוף כדי לוודא שהדפדפן טוען את הגרסה העדכנית ביותר של הקובץ (cache busting).
<link rel="stylesheet" href="{{ url_for('static', filename='css/dark-mode.css') }}?v={{ static_version }}">
מיקום: אחרי שורה 874 ב-base.html (אחרי high-contrast.css)
שלב 3: עדכון Backend
עכשיו, כשה-Frontend שלנו מוכן לטפל במצבים החדשים, אנחנו צריכים לוודא שגם ה-Backend שלנו מודע אליהם. זה חשוב מכמה סיבות: קודם כל, אנחנו רוצים שהשרת ידע לזהות ולהבין אילו themes זמינים, ושנית, אנחנו רוצים לאפשר למשתמשים לשמור את ההעדפה שלהם ל-theme גם ב-Database.
השינוי העיקרי נעשה בקובץ app.py. קודם כל, אנחנו צריכים לעדכן את הרשימה של ה-themes המותרים. אם בעבר הגבלנו את הבחירה ל-'classic', 'ocean', 'forest', 'high-contrast', עכשיו נוסיף גם את 'dark' ו-'dim' לרשימה הזו. זה מבטיח שאם משתמש ינסה לשלוח theme לא חוקי, המערכת תחזור לברירת המחדל בצורה תקינה. נעשה את זה בשני מקומות בקוד:
- בדיקת ה-theme שמגיע מה-Session או ה-Cookie: בדרך כלל, ה-theme הנוכחי נשמר בסשן של המשתמש. אנחנו צריכים לוודא שהבדיקה שעושה
if theme not in {...}תכלול גם את ה-themes החדשים. - ב-API endpoint ששומר את ההעדפות: אם יש לכם API endpoint שמאפשר למשתמש לשנות את ה-theme (כמו
/api/ui_prefs), גם שם צריך לעדכן את הרשימה של ה-themes המותרים כדי שהשרת יקבל וישמור את הערכים החדשים.
בנוסף, אם אתם משתמשים בספריית Pygments כדי להציג קוד בצורה מודגשת (syntax highlighting), תצטרכו להתאים גם את הבחירה של ה-style שלה. Pygments מציעה מגוון רחב של styles, חלקם מיועדים למצב חשוך וחלקם למצב בהיר. אנחנו רוצים שהשרת יבחר את ה-style המתאים ביותר ל-theme הנוכחי. לדוגמה, אם המשתמש בחר במצב dark או dim, נרצה להשתמש ב-style כמו 'github-dark' או 'monokai'. אם המצב הוא בהיר (כמו classic או ocean), נשתמש ב-style ברירת מחדל כמו 'github'. אפשר ליצור פונקציה קטנה שתקבל את ה-theme ותחזיר את ה-style המתאים של Pygments. זה מבטיח שגם הקוד המוצג בשרת ייראה נכון במצב החשוך.
חשוב לוודא שהשינויים האלה נעשים בצורה שלא תשבור את הפונקציונליות הקיימת. תמיד כדאי לבדוק שה-themes הישנים עדיין עובדים כמו שצריך, ושה-themes החדשים משתלבים חלק.
3.1 עדכון app.py
# מצא את השורה (בערך 819):
if theme not in {'classic','ocean','forest','high-contrast'}:
theme = 'classic'
# שנה ל:
if theme not in {'classic','ocean','forest','high-contrast','dark','dim'}:
theme = 'classic'
עדכן גם את ה-API endpoint:
# מצא את השורה (בערך 6608):
if theme in {'classic', 'ocean', 'forest', 'high-contrast'}:
# שנה ל:
if theme in {'classic', 'ocean', 'forest', 'high-contrast', 'dark', 'dim'}:
שלב 4: עדכון UI - הוספת טוגל
אז, אחרי שהגדרנו את ה-CSS ואת ה-Backend, הגיע הזמן לדבר על איך המשתמשים שלנו באמת ישתמשו בכל זה. אנחנו רוצים לתת להם שליטה קלה וברורה על בחירת ה-theme. הדרך הכי נפוצה לעשות את זה היא באמצעות תפריט בחירה (select dropdown) שיהיה זמין בהגדרות של המשתמש. זה המקום שבו הם יכולים לשנות את הגדרות ה-UI שלהם, ולכן זה המקום הטבעי להוסיף את האפשרויות החדשות.
אנחנו הולכים לערוך את קובץ settings.html. שם, נחפש את ה-select element הקיים שמשמש לבחירת ה-theme, או נוסיף חדש אם הוא לא קיים. ל-select הזה, נוסיף option חדש עבור כל אחד מהמצבים החדשים שהוספנו: "🌙 חשוך" (dark) ו- "🌆 מעומעם" (dim). חשוב שה-value של כל option יתאים בדיוק ל-theme name שאנחנו מצפים לו ב-Backend וב-CSS (כלומר, 'dark' ו-'dim'). בנוסף, נוסיף תנאי קטן ({% if ui_theme == '...' %}selected{% endif %}) כדי שהאופציה הנוכחית תהיה מסומנת מראש, וגם טקסט שמציין אם זו האופציה הפעילה כרגע. זה עוזר למשתמש לדעת איפה הוא נמצא.
אבל רגע, זה לא הכל! כדי להפוך את החוויה להרבה יותר אינטראקטיבית ומהירה, אנחנו יכולים להוסיף גם טוגל מהיר ישירות ב-Navbar, ליד הלוגו או פרטי המשתמש. זה ייתן למשתמשים אפשרות לעבור בין מצבים בלחיצת כפתור, בלי צורך להיכנס לדף ההגדרות.
איך עושים את זה? נוסיף כפתור (<button>) שיהיה לו ID ייחודי, למשל darkModeToggle. הכפתור הזה יכיל אייקון (כמו ירח למצב חשוך, שמש למצב בהיר) וכיתוב שמסביר את הפעולה. כשלוחצים על הכפתור, JavaScript יפעיל פונקציה שתחליף את ה-theme הנוכחי למצב הבא בתור (למשל, מ-light ל-dark, או מ-dark ל-dim). אנחנו נשתמש גם באייקון ובכיתוב שמשתנה בהתאם למצב הנוכחי, כדי שהמשתמש תמיד ידע מה הוא הולך לעשות. חשוב שהכפתור הזה יופיע רק אם המשתמש מחובר ({% if session.user_id %}), כי זה מאפיין ששמור למשתמש.
השילוב של תפריט הגדרות מפורט וטוגל מהיר ב-Navbar ייתן למשתמשים גמישות מקסימלית ונוחות בשימוש באפליקציה שלנו, בכל מצב ובכל זמן.
4.1 עדכון settings.html
<select id="themeSelect" class="filter-select" aria-label="בחר ערכת נושא" style="padding: 0.5rem 0.75rem; border-radius: 10px; border: 1px solid rgba(255,255,255,0.2); background: rgba(255,255,255,0.1); color: white;">
<option value="classic" {% if ui_theme == 'classic' %}selected{% endif %}>קלאסי{% if ui_theme == 'classic' %} (נוכחית){% endif %}</option>
<option value="ocean" {% if ui_theme == 'ocean' %}selected{% endif %}>אוקיינוס{% if ui_theme == 'ocean' %} (נוכחית){% endif %}</option>
<option value="forest" {% if ui_theme == 'forest' %}selected{% endif %}>יער{% if ui_theme == 'forest' %} (נוכחית){% endif %}</option>
<option value="dark" {% if ui_theme == 'dark' %}selected{% endif %}>🌙 חשוך{% if ui_theme == 'dark' %} (נוכחית){% endif %}</option>
<option value="dim" {% if ui_theme == 'dim' %}selected{% endif %}>🌆 מעומעם{% if ui_theme == 'dim' %} (נוכחית){% endif %}</option>
<option value="high-contrast" {% if ui_theme == 'high-contrast' %}selected{% endif %}>ניגודיות גבוהה{% if ui_theme == 'high-contrast' %} (נוכחית){% endif %}</option>
</select>
4.2 הוספת טוגל מהיר ב-Navbar (אופציונלי)
<!-- הוסף ב-navbar, אחרי הלוגו -->
{% if session.user_id %}
<button id="darkModeToggle" class="btn btn-secondary btn-icon" title="החלף מצב חשוך/בהיר" aria-label="החלף מצב חשוך">
<i class="fas fa-moon" id="darkModeIcon"></i>
<span class="btn-text">חשוך</span>
</button>
{% endif %}
שלב 5: התאמת CodeMirror
אם האפליקציה שלכם כוללת עורך קוד, סביר להניח שאתם משתמשים בספרייה כמו CodeMirror כדי לספק חווית עריכה עשירה עם הדגשת תחביר (syntax highlighting), השלמה אוטומטית ועוד. כמובן, מצב חשוך לא יהיה שלם בלי התאמה גם של עורך הקוד. אנחנו רוצים שהקוד יהיה קריא ונעים לעין גם כשהרקע כהה.
ההתאמה מתחלקת לכמה חלקים:
-
לוגיקת בחירת ה-theme: CodeMirror צריך לדעת איזה theme להשתמש בו. אנחנו נעדכן את הפונקציה שאחראית על טעינת ה-themes (בדרך כלל משהו כמו
getThemeבקובץ ה-bundle של CodeMirror, למשלcodemirror.bundle.entry.mjs). בפונקציה הזו, נוסיף תנאי שיבדוק אם ה-theme המבוקש הוא'dark'או'dim'. אם כן, נטען theme מתאים למצב חשוך (לרוב, theme מובנה כמוoneDarkאו theme מותאם אישית שנצטרך ליצור). חשוב גם להוסיף בדיקה נוספת: אם ה-data-themeattribute של האלמנט הראשי (<html>) הוא'dark'או'dim', אנחנו רוצים להשתמש ב-theme החשוך באופן אוטומטי, גם אם המשתמש לא בחר בו במפורש דרך CodeMirror עצמו. זה מבטיח שההתאמה תהיה עקבית עם שאר האפליקציה. -
אתחול העורך: בקוד שמאתחל את CodeMirror (למשל, ב-
editor-manager.js), אנחנו צריכים לוודא שה-theme הנכון מועבר בעת יצירת העורך. נעשה זאת על ידי קריאת ה-data-themeattribute מה-HTML (document.documentElement.getAttribute('data-theme')) ובדיקה אם הוא'dark'או'dim'. אם כן, נשתמש ב-theme חשוך, אחרת נשתמש ב-theme שהמשתמש בחר. -
עיצוב CSS נוסף: גם אחרי שהלוגיקה פועלת, עדיין צריך להגדיר את המראה של CodeMirror במצב חשוך. ניצור קובץ CSS חדש (או נוסיף לקובץ קיים, כמו
codemirror-custom.css) עם כללים שמתחילים ב-[data-theme="dark"]או[data-theme="dim"]. כאן נגדיר את צבעי הרקע של העורך (.cm-editor), ה-gutters (מספרי שורות), הטקסט, הרקע של השורה הפעילה (.cm-activeLine), וגם את צבע הסימון של הטקסט הנבחר (.cm-selectionBackground). כל אלה צריכים להיות מותאמים לפלטת הצבעים שהגדרנו בשלב 1, עם שימוש במשתנים כמו--code-bg,--code-text,--bg-tertiaryוכו'.
השילוב של הלוגיקה ב-JavaScript והעיצוב ב-CSS יבטיח שעורך הקוד שלנו ייראה ויתפקד בצורה מושלמת, גם במצב החשוך, ויספק למפתחים חוויה נוחה ופרודוקטיבית.
5.1 עדכון codemirror.bundle.entry.mjs
function getTheme(name) {
const themeName = String(name || '').toLowerCase();
// תמיכה במצב חשוך
if (themeName === 'dark' || themeName === 'dim') {
return oneDark || [];
}
// אם ה-HTML element במצב חשוך, השתמש ב-oneDark
if (typeof document !== 'undefined') {
const htmlTheme = document.documentElement.getAttribute('data-theme');
if (htmlTheme === 'dark' || htmlTheme === 'dim') {
return oneDark || [];
}
}
return [];
}
5.2 עדכון editor-manager.js
async initCodeMirror(container, { language, value, theme }) {
// ... קוד קיים ...
// זיהוי theme אוטומטי מה-HTML
const htmlTheme = document.documentElement.getAttribute('data-theme');
const effectiveTheme = (htmlTheme === 'dark' || htmlTheme === 'dim') ? 'dark' : theme;
// ... המשך הקוד עם effectiveTheme ...
}
5.3 עדכון codemirror-custom.css
/* Dark Mode CodeMirror */
[data-theme="dark"] .codemirror-container,
[data-theme="dim"] .codemirror-container {
background: var(--code-bg);
border-color: var(--code-border);
}
[data-theme="dark"] .cm-editor,
[data-theme="dim"] .cm-editor {
background: var(--code-bg);
color: var(--code-text);
}
[data-theme="dark"] .cm-gutters,
[data-theme="dim"] .cm-gutters {
background: var(--bg-tertiary);
border-right-color: var(--code-border);
color: var(--text-muted);
}
[data-theme="dark"] .cm-activeLineGutter,
[data-theme="dim"] .cm-activeLineGutter {
background: var(--glass-hover);
}
[data-theme="dark"] .cm-activeLine,
[data-theme="dim"] .cm-activeLine {
background: var(--glass);
}
[data-theme="dark"] .cm-selectionBackground,
[data-theme="dim"] .cm-selectionBackground {
background-color: rgba(124, 138, 255, 0.3) !important;
}
שלב 6: התאמת Pygments
מעבר ל-CodeMirror, אם אתם מציגים קטעי קוד שנוצרו על ידי השרת, סביר להניח שאתם משתמשים בספריית Pygments בפייתון. Pygments היא כלי חזק להדגשת תחביר, והיא יכולה ליצור פלט HTML עם CSS שמתאים לקוד. גם כאן, אנחנו צריכים לוודא שהיא מתאימה למצב החשוך.
ההתאמה העיקרית נעשית ב-Backend, בקובץ app.py, במקום שבו אתם משתמשים ב-Pygments כדי לעבד קוד. Pygments משתמשת במושג שנקרא "style" כדי לקבוע את הצבעים והמראה של ההדגשה. לכל style יש שם ייחודי (למשל, 'default', 'monokai', 'github-dark').
מה שאנחנו צריכים לעשות זה לוודא שבכל פעם שאתם קוראים לפונקציה של Pygments (כמו highlight), אתם מעבירים לה את ה-style המתאים בהתאם ל-theme הנוכחי של האפליקציה.
הדרך הכי טובה לעשות את זה היא ליצור פונקציה קטנה ב-app.py שמקבלת את שם ה-theme (למשל, 'dark', 'dim', 'classic') ומחזירה את שם ה-style המתאים של Pygments. למשל:
- אם ה-theme הוא
'dark'או'dim', הפונקציה תחזיר'github-dark'(זהו style פופולרי למצב חשוך). - אם ה-theme הוא
'high-contrast', אולי נרצה להשתמש ב-style אחר שמיועד לניגודיות גבוהה. - עבור themes בהירים כמו
'classic','ocean'או'forest', נשתמש ב-style ברירת מחדל כמו'github'.
את הפונקציה הזו אפשר להוסיף בתחילת הקובץ או ליד הפונקציות שמשתמשות ב-Pygments. לאחר מכן, בכל מקום שבו אתם יוצרים HtmlFormatter (החלק של Pygments שאחראי על יצירת ה-HTML וה-CSS), תעבירו לה את ה-style שמוחזר מהפונקציה החדשה שלכם.
לדוגמה, אם בעבר הקוד שלכם נראה כך:
from pygments.formatters import HtmlFormatter
formatter = HtmlFormatter(style='github-dark', linenos=True, cssclass='source')
אז עכשיו הוא יראה כך:
# תחילה, הגדרת הפונקציה:
def get_pygments_style(theme):
if theme in ('dark', 'dim'):
return 'github-dark'
elif theme == 'high-contrast':
return 'monokai'
else:
return 'github'
# ואז בשימוש:
formatter = HtmlFormatter(style=get_pygments_style(theme), linenos=True, cssclass='source')
זה מבטיח שגם התוכן שנוצר בשרת יתאים באופן מושלם ל-theme שהמשתמש בחר, ושהקוד יהיה קריא וברור בכל מצב.
6.1 עדכון app.py
# מצא את השורות שמשתמשות ב-Pygments (בערך 4700, 4811, 6932):
# במקום:
style='github-dark'
# שנה ל:
def get_pygments_style(theme):
"""החזר Pygments style בהתאם ל-theme"""
if theme in ('dark', 'dim'):
return 'github-dark'
elif theme == 'high-contrast':
return 'monokai' # או style אחר עם ניגודיות גבוהה
else:
return 'github' # light theme
# ואז השתמש:
style = get_pygments_style(theme)
דוגמה מלאה:
# בפונקציה שמציגה קוד (view_file route):
def get_pygments_style(theme):
"""החזר Pygments style בהתאם ל-theme"""
theme_map = {
'dark': 'github-dark',
'dim': 'github-dark',
'high-contrast': 'monokai',
'classic': 'github',
'ocean': 'github',
'forest': 'github'
}
return theme_map.get(theme, 'github')
# בשימוש:
formatter = HtmlFormatter(
style=get_pygments_style(theme),
linenos=True,
cssclass='source',
lineanchors='line',
anchorlinenos=True
)
highlighted_code = highlight(code, lexer, formatter)
שלב 7: JavaScript לניהול מצב
עד עכשיו, אנחנו הגדרנו את ה-CSS, עדכנו את ה-Backend, ואפילו טיפלנו בכלים חיצוניים כמו CodeMirror ו-Pygments. אבל כל זה לא שווה הרבה בלי ה-JavaScript שיגרום לכל השינויים האלה לקרות בצורה דינמית וחלקה, וגם ישמור על ההעדפות של המשתמש. כאן נכנס לתמונה קובץ ה-JavaScript שלנו, dark-mode.js, שהוא הלב הפועם של המערכת הזו.
מה הקוד הזה עושה? הוא בעצם מנהל את כל תהליך בחירת ה-theme:
-
טעינת העדפה: כשהדף נטען, הקוד קודם כל בודק אם יש העדפה שמורה ב-
localStorageשל הדפדפן.localStorageהוא מקום שבו דפי אינטרנט יכולים לשמור נתונים באופן קבוע, כך שההעדפה של המשתמש (למשל, "dark") תישאר גם אם הוא סוגר ופותח את הדפדפן. אם אין משהו שמור, הוא ינסה לקבוע את ברירת המחדל. -
זיהוי העדפת מערכת (Auto Mode): אנחנו רוצים לאפשר "מצב אוטומטי". במצב הזה, האפליקציה תזהה אם מערכת ההפעלה של המשתמש מוגדרת למצב חשוך (
prefers-color-scheme: dark) ותתאים את עצמה בהתאם. הקוד שלנו בודק את זה באמצעותwindow.matchMedia. אם המערכת במצב חשוך, ה-theme יהיה'dark', אחרת הוא יהיה'classic'(ברירת מחדל למצב בהיר). -
החלת ה-theme: בהתבסס על ההעדפה מה-
localStorageאו העדפת המערכת, הקוד מוסיף או משנה את ה-data-themeattribute על האלמנט הראשי (<html>). זה מה שגורם לכללי ה-CSS שלנו (במיוחד אלה שמתחילים עם[data-theme="dark"]) להיכנס לפעולה ולשנות את המראה של האפליקציה. -
מעבר חלק (Transition): כדי שהשינוי לא יהיה פתאומי, הקוד מוודא שמתבצע מעבר חלק (fade transition) בין המצבים. זה נעשה ב-CSS, אבל ה-JavaScript אחראי להפעיל את השינוי שגורם למעבר להתרחש.
-
טוגל (Toggle) מהיר: כפי שדיברנו בשלב הקודם, הקוד הזה גם מנהל את הלוגיקה של כפתור הטוגל ב-Navbar. בלחיצה על הכפתור, הוא עובר בין המצבים השונים במחזוריות (auto -> dark -> dim -> light -> auto), מעדכן את ה-
localStorage, משנה את ה-data-themeואת האייקון והטקסט של הכפתור. -
סנכרון עם השרת: כדי שההעדפה תישמר גם ב-Database, הקוד מבצע קריאת API (POST request) לשרת בכל פעם שהמשתמש משנה את ה-theme, ושולח אליו את ההעדפה החדשה. זה מבטיח שההגדרה תישאר גם אם המשתמש נכנס ממכשיר אחר.
-
עדכון ה-Select ב-Settings: הקוד גם מוודא שתפריט הבחירה ב-
settings.htmlיציג את ה-theme הנכון כבחירה הנוכחית, בהתאם למה ששמור ב-localStorage.
כל הלוגיקה הזו רצה מיד כשהדף נטען (או אחרי שה-DOM מוכן), ומבטיחה שחוויית המשתמש תהיה חלקה, אינטואיטיבית, ושההעדפות תמיד יישמרו.
7.1 יצירת dark-mode.js
/**
* Dark Mode Manager
* ניהול מצב חשוך/בהיר עם תמיכה ב-Auto mode
*/
(function() {
'use strict';
const DARK_MODE_KEY = 'dark_mode_preference';
const THEME_ATTRIBUTE = 'data-theme';
/**
* זיהוי העדפת מערכת
*/
function getSystemPreference() {
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}
/**
* טעינת העדפה מ-localStorage
*/
function loadPreference() {
try {
const saved = localStorage.getItem(DARK_MODE_KEY);
// אפשר להחזיר 'auto', 'dark', 'dim', 'light', או 'classic' (אם light ממופה ל-classic)
if (['auto', 'dark', 'dim', 'light'].includes(saved)) {
return saved;
}
} catch (e) {
console.warn('Failed to load dark mode preference:', e);
}
return 'auto'; // ברירת מחדל
}
/**
* שמירת העדפה
*/
function savePreference(mode) {
try {
localStorage.setItem(DARK_MODE_KEY, mode);
} catch (e) {
console.warn('Failed to save dark mode preference:', e);
}
}
/**
* החלת theme על ה-HTML element
*/
function applyTheme(theme) {
const html = document.documentElement;
let themeToApply = theme;
if (theme === 'auto') {
const systemPref = getSystemPreference();
themeToApply = systemPref === 'dark' ? 'dark' : 'classic'; // ברירת מחדל ל-light
} else if (theme === 'light') {
themeToApply = 'classic'; // מיפוי light ל-classic
}
// בדוק אם themeToApply הוא אחד מה-themes התקינים
const validThemes = ['classic', 'ocean', 'forest', 'dark', 'dim', 'high-contrast'];
if (!validThemes.includes(themeToApply)) {
console.warn(`Invalid theme to apply: ${themeToApply}. Falling back to classic.`);
themeToApply = 'classic';
}
html.setAttribute(THEME_ATTRIBUTE, themeToApply);
}
/**
* עדכון theme בהתאם להעדפה
*/
function updateTheme() {
const preference = loadPreference();
applyTheme(preference);
}
/**
* החלפת מצב (toggle)
*/
function toggleDarkMode() {
const currentPreference = loadPreference();
let nextPreference;
// מחזור: auto -> dark -> dim -> light -> auto
switch (currentPreference) {
case 'auto':
nextPreference = 'dark';
break;
case 'dark':
nextPreference = 'dim';
break;
case 'dim':
nextPreference = 'light'; // light ימופה ל-classic
break;
case 'light':
default:
nextPreference = 'auto';
break;
}
savePreference(nextPreference);
applyTheme(nextPreference);
updateToggleButton(nextPreference);
// עדכון לשרת (אופציונלי)
syncToServer(nextPreference);
}
/**
* עדכון כפתור הטוגל
*/
function updateToggleButton(preference) {
const toggleBtn = document.getElementById('darkModeToggle');
const icon = document.getElementById('darkModeIcon');
const text = toggleBtn?.querySelector('.btn-text');
if (!toggleBtn || !icon) return;
let currentThemeForDisplay;
let label;
switch (preference) {
case 'auto':
currentThemeForDisplay = getSystemPreference() === 'dark' ? 'dark' : 'classic';
label = 'אוטומטי';
break;
case 'dark':
currentThemeForDisplay = 'dark';
label = 'חשוך';
break;
case 'dim':
currentThemeForDisplay = 'dim';
label = 'מעומעם';
break;
case 'light':
currentThemeForDisplay = 'classic';
label = 'בהיר';
break;
default:
currentThemeForDisplay = 'classic';
label = 'קלאסי';
break;
}
// עדכון אייקון
icon.className = 'fas'; // איפוס קלאסים
if (currentThemeForDisplay === 'dark') {
icon.classList.add('fa-moon');
} else if (currentThemeForDisplay === 'dim') {
icon.classList.add('fa-cloud-moon');
} else {
icon.classList.add('fa-sun');
}
if (text) text.textContent = label;
toggleBtn.setAttribute('title', `מצב: ${label}`);
}
/**
* סנכרון עם השרת
*/
async function syncToServer(preference) {
try {
let themeName = preference;
if (preference === 'auto') {
themeName = getSystemPreference() === 'dark' ? 'dark' : 'classic';
} else if (preference === 'light') {
themeName = 'classic';
}
// בדוק אם themeName הוא אחד מה-themes התקינים לפני השליחה
const validThemes = ['classic', 'ocean', 'forest', 'dark', 'dim', 'high-contrast'];
if (!validThemes.includes(themeName)) {
console.warn(`Invalid theme name for sync: ${themeName}. Skipping sync.`);
return;
}
await fetch('/api/ui_prefs', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ theme: themeName })
});
} catch (e) {
console.warn('Failed to sync theme to server:', e);
}
}
/**
* אתחול
*/
function init() {
// עדכון theme בהתחלה
updateTheme();
// חיבור לכפתור הטוגל (אם קיים)
const toggleBtn = document.getElementById('darkModeToggle');
if (toggleBtn) {
toggleBtn.addEventListener('click', toggleDarkMode);
const currentPreference = loadPreference();
updateToggleButton(currentPreference);
}
// עדכון theme selector ב-settings
const themeSelect = document.getElementById('themeSelect');
if (themeSelect) {
const preference = loadPreference();
let themeValueToSet = preference;
// המרה אם צריך
if (preference === 'auto') {
themeValueToSet = 'auto'; // אפשרות נפרדת ב-select
} else if (preference === 'light') {
themeValueToSet = 'classic';
} else if (['dark', 'dim', 'classic', 'ocean', 'forest', 'high-contrast'].includes(preference)) {
themeValueToSet = preference;
}
// אם הערך קיים באפשרויות, בחר אותו
const option = themeSelect.querySelector(`option[value="${themeValueToSet}"]`);
if (option) {
themeSelect.value = themeValueToSet;
} else {
// אם האפשרות לא קיימת (למשל, אם ה-preference הוא 'auto' ולא קיימת אופציה כזו), בחר את ברירת המחדל
themeSelect.value = 'classic';
}
}
// האזנה לשינויים בהעדפת המערכת (אם ב-auto mode)
if (window.matchMedia) {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
mediaQuery.addEventListener('change', () => {
if (loadPreference() === 'auto') {
applyTheme('auto');
updateToggleButton('auto'); // עדכן גם את הכפתור
}
});
}
}
// הפעלה בעת טעינת הדף
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// חשיפת API גלובלי (אופציונלי)
window.DarkMode = {
toggle: toggleDarkMode,
set: function(mode) { // mode יכול להיות 'auto', 'light', 'dark', 'dim', 'classic' וכו'
savePreference(mode);
applyTheme(mode);
updateToggleButton(mode);
syncToServer(mode);
},
get: loadPreference,
updateToggles: updateToggleButton // פונקציה לעדכון הכפתור מבחוץ
};
})();
7.2 הוספת הקובץ ל-base.html
כדי שה-JavaScript הזה ירוץ, אנחנו צריכים לטעון אותו. את זה נעשה על ידי הוספת תגית <script> בקובץ base.html, בסוף ה-<body>. השימוש ב-defer מבטיח שהסקריפט ירוץ רק אחרי שה-HTML נטען, וזה חשוב כי הוא צריך לגשת לאלמנטים ב-DOM.
<script src="{{ url_for('static', filename='js/dark-mode.js') }}?v={{ static_version }}" defer></script>
מיקום: אחרי שורה 1522 ב-base.html (אחרי global_search.js)
שלב 8: התאמת קומפוננטים נוספים
מעולה, חברים! עד עכשיו כיסינו את הליבה של המערכת: הגדרות CSS, לוגיקת JavaScript, התאמות ל-Backend ולכלי עריכת קוד. אבל אפליקציה מודרנית מורכבת מהרבה חלקים. אנחנו צריכים לוודא שכל החלקים האלה, או לפחות החלקים הויזואליים החשובים, מתאימים למצב החשוך החדש שלנו.
בואו נסתכל על כמה דוגמאות נפוצות:
-
Markdown Preview: אם יש לכם פיצ'ר שמציג תצוגה מקדימה של Markdown (למשל, כשכותבים קובץ README או פוסט בפורום), צריך להתאים גם אותו. אנחנו נוסיף כללי CSS בתוך קובץ ה-CSS של ה-Markdown preview (או בקובץ ה-
dark-mode.cssהראשי, אם אפשר). הכללים האלה יתחילו עם[data-theme="dark"] #md-contentו-[data-theme="dim"] #md-content. בתוכם, נגדיר את צבעי הרקע והטקסט של התוכן עצמו, וגם את העיצוב של קטעי קוד (code,pre) שמופיעים בתוך ה-Markdown. המטרה היא שהטקסט והקוד יוצגו בצורה ברורה וקריאה, בדיוק כמו שהגדרנו קודם. -
Collections & Bookmarks: אם יש לכם חלקים באפליקציה שמנהלים אוספים, סימניות, או כל סוג של "כרטיסים" (cards) שמרכזים מידע, גם להם צריך לתת מראה חשוך. זה יכול לכלול עדכון של קבצי CSS ייעודיים (כמו
collections.cssאוbookmarks.css), או הוספת הכללים לקובץ המרכזי. נשתמש שוב ב-CSS Variables כדי להגדיר את צבעי הרקע (--card-bg), הגבולות (--card-border) והטקסט (--text-primary) של הכרטיסים האלה, כדי שיתאימו לפלטה החשוכה. -
אלמנטים אינטראקטיביים: כל כפתור, קישור, שדה קלט, תפריט נפתח, חלון פופ-אפ (modal), או כל אלמנט אחר שהמשתמש מקיים איתו אינטראקציה, צריך להיראות תקין במצב חשוך. רוב הדברים האלה כבר אמורים להיות מכוסים על ידי ה-CSS הכללי שיצרנו בשלב 2, אבל תמיד כדאי לעבור ולבדוק. שימו לב במיוחד לאלמנטים שמשתמשים בצבעים סטטיים או שהעיצוב שלהם היה תלוי ברקע בהיר.
-
תמונות ואיקונים: למרות שזה פחות נפוץ, לפעמים צריך לשקול אם תמונות מסוימות או איקונים צריכים גרסה כהה יותר או פחות בולטת במצב חשוך. במקרה של SVG, אפשר להשתמש ב-CSS filters או לשנות את ה-fill color כדי להתאים אותו.
העיקרון המנחה: בכל מקום שאתם רואים אלמנט ויזואלי, שאלו את עצמכם: "האם זה נראה טוב גם על רקע כהה? האם הניגודיות מספיקה? האם הצבעים מתאימים לפלטה הכללית?". אם התשובה היא לא, כנראה שצריך להוסיף כלל CSS שמתחיל ב-[data-theme="dark"] או [data-theme="dim"] כדי לתקן את המראה.
השלב הזה הוא קצת יותר יצירתי ודורש סריקה של כל חלקי האפליקציה, אבל הוא חיוני כדי להבטיח חוויה אחידה ואיכותית לכל המשתמשים, בכל מצב.
8.1 Markdown Preview
/* הוסף ל-md_preview.html */
[data-theme="dark"] #md-content,
[data-theme="dim"] #md-content {
background: var(--bg-primary);
color: var(--text-primary);
}
[data-theme="dark"] #md-content code:not(pre code),
[data-theme="dim"] #md-content code:not(pre code) {
background: var(--bg-tertiary);
color: var(--code-text);
border: 1px solid var(--code-border);
}
[data-theme="dark"] #md-content pre,
[data-theme="dim"] #md-content pre {
background: var(--code-bg);
border: 1px solid var(--code-border);
}
8.2 Collections & Bookmarks
ודא שכל הקומפוננטים משתמשים ב-CSS variables:
/* ב-collections.css, bookmarks.css וכו' */
[data-theme="dark"] .collection-card,
[data-theme="dim"] .collection-card {
background: var(--card-bg);
border-color: var(--card-border);
color: var(--text-primary);
}
בדיקות ואימות
אוקיי, חברים, עברנו את כל השלבים, הוספנו קוד, שינינו סגנונות, וזה נראה מבטיח. אבל רגע לפני שאנחנו מכריזים על ניצחון, מה הדבר הכי חשוב שעושים אחרי שמשקיעים כל כך הרבה מאמץ? בדיקות! אי אפשר פשוט להניח שהכל עובד כמו שצריך. אנחנו צריכים לוודא שכל מה שעשינו אכן משרת את המטרה – חווית משתמש נוחה, יפה, ובעיקר, ידידותית לעיניים. אז בואו נעבור על רשימת הבדיקות שלנו ונהפוך את האפליקציה שלנו למצוחצחת.
9.1 רשימת בדיקות
הנה רשימה של הדברים שחייבים לבדוק בקפידה:
- [ ] מעבר בין מצבים (Light/Dark/Dim/Auto) עובד חלק: לחצו על כפתור הטוגל, בחרו כל אחת מהאפשרויות בתפריט ההגדרות, ובדקו שהמעברים בין המצבים חלקים, בלי קפיצות או "גמגומים" ויזואליים. ודאו שה-
automode מתנהג כמצופה לפי הגדרות המערכת. - [ ] כל הקומפוננטים נראים נכון במצב חשוך: עברו על כל מסך, כל פינה באפליקציה. בדקו כפתורים, טבלאות, טפסים, תפריטים, הודעות, כרטיסים – כל דבר שאתם רואים. האם הכל נראה כמו שצריך? האם הצבעים תואמים? האם יש אלמנטים "שקופים" או כאלה שמתמזגים מדי עם הרקע?
- [ ] CodeMirror מציג syntax highlighting נכון: אם יש לכם עורך קוד, פתחו קובץ קוד (בכמה שפות שונות), וודאו שהדגשת התחביר עובדת מצוין במצב החשוך. הצבעים צריכים להיות ברורים ומובחנים.
- [ ] Pygments מציג קוד נכון: אם אתם מציגים קוד שנוצר בשרת (למשל, במדריכים או בדפי קוד), ודאו שגם הוא מוצג כהלכה, עם ה-style המתאים למצב החשוך.
- [ ] כל הכפתורים והקישורים נראים ופועלים: בדקו שכל הקישורים ניתנים ללחיצה, שהכפתורים ברורים, ושאפקטי הריחוף (hover) עובדים כמו שצריך. ודאו שצבעי הקישורים והכפתורים מובחנים מהטקסט והרקע.
- [ ] Forms ו-inputs נראים נכון: בדקו שדות טקסט, שדות סיסמה, תיבות סימון, כפתורי רדיו, ובחירות נפתחות. ודאו שהם קריאים, שהגבולות ברורים, ושהטקסט שהמשתמש מקליד נראה טוב.
- [ ] Modals ו-dropdowns נראים נכון: חלונות קופצים ותפריטים נפתחים צריכים להיות ברורים, עם רקע וגבולות מתאימים, ושהתוכן שלהם קריא.
- [ ] התאמה אוטומטית ל-
prefers-color-schemeעובדת: שנו את הגדרת ה-theme של מערכת ההפעלה שלכם (מ-dark ל-light ולהיפך) וודאו שהאפליקציה מגיבה בהתאם (אם היא מוגדרת עלauto). - [ ] העדפה נשמרת ב-localStorage ו-DB: אחרי שבחרתם theme, סגרו את הדפדפן, פתחו אותו מחדש (או עברו לדף אחר וחזרו), וודאו שה-theme שבחרתם עדיין פעיל. אם אתם מחוברים, ודאו שההעדפה נשמרה גם ב-Database.
- [ ] מעבר חלק ללא flickering: ודאו שאין "הבהוב" (flickering) של המצב הבהיר לרגע לפני שהמצב החשוך נטען. זה קורה לפעמים אם ה-JavaScript לא רץ מספיק מהר.
9.2 בדיקות ידניות
כדי לבצע את הבדיקות הללו בצורה שיטתית, מומלץ:
- בדיקת טוגל: פתחו את האפליקציה, השתמשו בכפתור הטוגל ב-Navbar. ודאו שהמצב משתנה חלק (dark -> dim -> light -> auto), שהאייקון והטקסט מתעדכנים, ושבכל לחיצה, ה-theme הנבחר נשמר.
- בדיקת Auto Mode: הגדירו את ה-theme שלכם ל-"Auto". לאחר מכן, שנו את הגדרות ה-theme של מערכת ההפעלה (ב-Windows, Mac, או Linux). ודאו שהאפליקציה משנה את המראה שלה בהתאם לשינוי במערכת.
- בדיקת כל הדפים: עברו ידנית על כל המסכים והדפים העיקריים באפליקציה. פתחו קבצים, ערכו אותם, השתמשו בכל הפיצ'רים. ודאו שאין שום דבר שנראה "שבור" או לא מתאים למצב החשוך. שימו לב במיוחד לקצוות, לגבולות, והיכן שטקסט פוגש רקע.
השקעת זמן בבדיקות יסודיות תבטיח שה-dark mode שלכם הוא לא רק "יפה", אלא גם פונקציונלי ושימושי.
טיפים ופתרון בעיות
גם הפרויקטים הכי מתוכננים יכולים להיתקל בקצת קשיים בדרך, וזה לגמרי בסדר, חברים! עולם ה-WebDev הוא דינמי, ויש כל מיני הפתעות. אבל אל דאגה, עם קצת סבלנות וטיפים נכונים, אפשר להתגבר על רוב הבעיות. כאן ריכזנו כמה מהבעיות הנפוצות ביותר שעלולות לצוץ כשמיישמים מצב חשוך, יחד עם פתרונות אפשריים, וגם כמה טיפים שיעזרו לכם לייעל את התהליך.
10.1 בעיות נפוצות
-
בעיה: אלמנטים מסוימים לא משתנים למצב חשוך. הם נשארים עם הצבע המקורי.
- פתרון: הדבר הראשון שצריך לבדוק הוא ה-CSS Selector. האם הוא באמת מכוון לאלמנט שאתם רוצים לשנות? ודאו שהסלקטור שלכם כולל את הפרדיקט
[data-theme="dark"]או[data-theme="dim"]. לדוגמה, אם אתם מנסים לשנות כפתור, הסלקטור צריך להיות משהו כמו[data-theme="dark"] .my-button. אם אתם לא מוצאים את הבעיה, נסו להשתמש בכלי הפיתוח של הדפדפן (Inspect Element) כדי לראות איזה CSS חל על האלמנט, ולמה הסגנונות החדשים שלכם לא מחליפים אותו. - פתרון: ודאו שה-CSS Variables באמת הוגדרו במקום הנכון. אם הגדרתם אותם בתוך
:root[data-theme="dark"]אבל אתם משתמשים בהם במקום אחר בלי שהם יקבלו ערך, זה עלול לגרום לבעיות. בדקו שהמשתנים (--bg-primary,--text-primaryוכו') אכן מוגדרים בכל ה-themes הרלוונטיים.
- פתרון: הדבר הראשון שצריך לבדוק הוא ה-CSS Selector. האם הוא באמת מכוון לאלמנט שאתם רוצים לשנות? ודאו שהסלקטור שלכם כולל את הפרדיקט
-
בעיה: יש "הבהוב" (Flickering) של המצב הבהיר לרגע קט בתחילת טעינת הדף, לפני שהמצב החשוך נכנס לתמונה.
- פתרון: הבעיה הזו קורית כי הדפדפן טוען את ה-CSS הראשי (שבדרך כלל בהיר כברירת מחדל) לפני שה-JavaScript מספיק לרוץ ולהחיל את ה-
data-themeהחשוך. הפתרון הקלאסי הוא להוסיף קטע קטן של JavaScript ממש בתחילת ה-<head>של קובץbase.html. הקוד הזה יקרא מיד את ה-localStorage, יבדוק אם יש העדפה למצב חשוך, ואם כן – יחיל את ה-data-themeהמתאים לפני שכל ה-CSS האחר נטען. זה "מרמה" את הדפדפן לחשוב שה-theme הנכון הוחל מההתחלה. הנה דוגמה לקוד כזה:
- פתרון: הבעיה הזו קורית כי הדפדפן טוען את ה-CSS הראשי (שבדרך כלל בהיר כברירת מחדל) לפני שה-JavaScript מספיק לרוץ ולהחיל את ה-
<script>
(function() {
const saved = localStorage.getItem('dark_mode_preference');
const html = document.documentElement;
if (saved === 'dark' || saved === 'dim') {
html.setAttribute('data-theme', saved);
} else if (saved === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches) {
html.setAttribute('data-theme', 'dark');
}
})();
</script>
-
בעיה: CodeMirror לא משתנה למצב חשוך, או שהוא נראה "מוזר".
- פתרון: ודאו שהלוגיקה ב-
codemirror.bundle.entry.mjs(בפונקציהgetTheme) באמת בודקת אתdata-themeשל האלמנט הראשי (document.documentElement) כאשר אין theme ספציפי מועבר ל-CodeMirror עצמו. כמו כן, בדקו ש-editor-manager.jsאכן מעביר את ה-theme הנכון (שיכול להיות'dark'או'dim') בעת יצירת העורך. לפעמים צריך גם לוודא שה-CSS של CodeMirror עצמו (ה-gutters, השורות הפעילות וכו') מעוצב בהתאם למצב החשוך, כפי שהוסבר בשלב 5.
- פתרון: ודאו שהלוגיקה ב-
-
בעיה: Pygments לא מציג קוד בצבעים הנכונים במצב חשוך.
- פתרון: בדקו שוב את הפונקציה
get_pygments_styleב-app.py. האם היא מקבלת את ה-theme הנכון מה-request או מה-session? האם היא מחזירה את שם ה-style הנכון של Pygments (למשל,'github-dark') עבור themes חשוכים? ודאו שה-style הזה אכן מועבר ל-HtmlFormatterבכל הפעמים שאתם קוראים לפונקציהhighlight.
- פתרון: בדקו שוב את הפונקציה
10.2 אופטימיזציות
- Lazy Loading: אפשר לשפר את מהירות הטעינה הראשונית על ידי טעינה "עצלנית" (lazy loading) של קובץ ה-
dark-mode.css. במקום לטעון אותו תמיד, אפשר לטעון אותו רק אחרי שהדף הראשי נטען, או רק כאשר המשתמש מפעיל את המצב החשוך בפעם הראשונה. זה יכול להיעשות באמצעות JavaScript. - CSS Variables: כפי שהדגשנו שוב ושוב, שימוש ב-CSS Variables הוא המפתח. זה לא רק מקל על התחזוקה, אלא גם מאפשר גמישות מירבית. נסו להשתמש בהם בכל מקום אפשרי.
- Transitions: ודאו שה-transitions חלקים ונעימים. במצב חשוך, transitions ארוכים מדי יכולים להרגיש איטיים, וקצרים מדי יכולים להרגיש פתאומיים. כוונו ל-0.3s או 0.4s כנקודת התחלה טובה.
- Caching: אל תשכחו את הכוח של הקאשינג.
localStorageהוא מצוין לשמירת העדפות בצד הלקוח, וה-DB שומר אותן בצד השרת. קבצי ה-CSS וה-JS עם הגרסאות (cache busting) מבטיחים שהמשתמשים יקבלו את העדכונים האחרונים.
10.3 נגישות
- ודאו ניגודיות מספקת: מצב חשוך לא אומר שהטקסט צריך להיות קשה לקריאה. השתמשו בכלים לבדיקת יחסי ניגודיות (contrast ratio) כדי לוודא שאתם עומדים בתקנים כמו WCAG AA (לפחות 4.5:1 לטקסט רגיל).
- אלמנטים אינטראקטיביים: ודאו שכל כפתורים, קישורים ושדות קלט ברורים וניתנים לזיהוי במצב חשוך. אל תסמכו רק על צבע כדי להעביר מידע.
aria-label: עבור אלמנטים כמו כפתור הטוגל, הוסיפוaria-labelמתאים כדי שגם משתמשים שמשתמשים בקוראי מסך יבינו מה הכפתור עושה.
עם הטיפים האלה, אתם מוכנים להתמודד עם כל אתגר שיצוץ בדרך!
סיכום
אז זהו, חברים יקרים! עברנו מסע מקיף, צעד אחר צעד, כדי להפוך את האפליקציה שלנו לידידותית יותר לעיניים ונעימה יותר לשימוש, באמצעות יישום מצב חשוך מלא. דיברנו על היתרונות, על ההגדרות הטכניות, ועל כל הפיצ'רים הקטנים שהופכים את החוויה למיוחדת. עכשיו, אחרי שיישמתם את כל השלבים במדריך הזה, אתם יכולים להיות גאים – המשתמשים שלכם יוכלו ליהנות ממגוון יתרונות משמעותיים:
- ✅ בחירה מגוונת: הם יוכלו לבחור בין שלושה מצבים עיקריים – בהיר (Light), מעומעם (Dim), וחושך מלא (Dark), ובנוסף, לבחור במצב "אוטומטי" שיסתנכרן עם הגדרות מערכת ההפעלה שלהם. זה נותן להם שליטה מלאה על החוויה הוויזואלית שלהם.
- ✅ מעבר חלק ונעים: שכחו ממעברים פתאומיים וצורמים. המעברים בין המצבים יהיו חלקים וזורמים (fade transitions), מה שהופך את השימוש באפליקציה לאינטואיטיבי ומרגיע יותר.
- ✅ התאמה אוטומטית חכמה: היכולת לזהות את העדפת המערכת (
prefers-color-scheme) פירושה שהאפליקציה תתאים את עצמה באופן דינמי, ותספק את החוויה המתאימה ביותר בהתאם לשעה ביום או להעדפות המשתמש, בלי צורך בהתערבות ידנית. - ✅ קריאות משופרת לקוד: עבור המפתחים, הדגשת התחביר (syntax highlighting) במצב חשוך תהיה מותאמת אישית, עם צבעים ברורים ומובחנים, שתורמים לקריאות נוחה יותר ומפחיתים עייפות עיניים בזמן עבודה ממושכת.
- ✅ שמירה על ההעדפות: הבחירה של המשתמש לא תלך לאיבוד. היא תינשמר בצורה מאובטחת ב-
localStorageשל הדפדפן וב-Database, כך שההעדפה האישית תישאר עקבית בכל הפעלה ובכל מכשיר.
יישום מצב חשוך הוא לא רק עניין של אסתטיקה; הוא נוגע ישירות לחוויית המשתמש, לנגישות, ואפילו לבריאות העיניים. זו השקעה שבהחלט משתלמת!
זמן משוער ליישום: 3-5 ימים (תלוי במורכבות האפליקציה ובזמינות המפתחים).
קובצי מפתח לעריכה:
webapp/templates/base.html- הגדרות CSS Variables, הוספת CSS ו-JS.webapp/static/css/dark-mode.css- קובץ CSS חדש עם כל הסגנונות למצב חשוך.webapp/app.py- עדכון רשימת themes, התאמת Pygments.webapp/templates/settings.html- הוספת אפשרויות בחירה בתפריט.webapp/static/js/dark-mode.js- קובץ JavaScript חדש לניהול הלוגיקה וההעדפות.webapp/static_build/codemirror.bundle.entry.mjs(או קובץ דומה) - עדכון לוגיקת בחירת ה-theme של CodeMirror.webapp/static/css/codemirror-custom.css(או קובץ CSS דומה) - סגנונות CSS ספציפיים ל-CodeMirror במצב חשוך.
נוצר על ידי: Background Agent
תאריך: ינואר 2025
גרסה: 1.0