Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions FEATURE_SUGGESTIONS/theme_matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
| ``--btn-primary-*`` | `color-mix` על סמך ``--bg-tertiary`` | טקסט כהה על לבן | טקסט כחול כהה על לבן | טקסט ירוק כהה על לבן | `color-mix` עם ורוד | שחור/לבן/צהוב בלבד |
| ``--md-surface`` / ``--md-text`` | כהה אחיד | כהה מותאם (`#15141f`) | כהה כחול (`#1a365d`) | כהה ירוק (`#1a4731`) | נשאר כהה כדי להגן על Highlight | שחור/לבן בהתאם לחוקים |
| ``--split-preview-*`` | מוגדר בערכי ברירת מחדל ב-`split-view.css` | ✔️ | ✔️ (rgba בגוון כחול) | ✔️ (rgba ירוק) | ✔️ (rgba סגלגל) | ✔️ (שחור/לבן/צהוב) |
| ``--ck-toast-*`` | Override כהה ב-`toast.css` | ברירת המחדל הבהירה | ברירת המחדל הבהירה | ברירת המחדל הבהירה | ברירת המחדל הבהירה | שחור/לבן ייעודי |
| ``--search-*`` | משתמש בערכי ברירת מחדל כהים | צל ייחודי `rgba(7,7,31,0.35)` | צל כחול + הדגשת info | צל ירוק | מדגיש צבעי עץ | שחור/לבן עם צהוב (WCAG) |

## בדיקות רגרסיה
Expand Down
4 changes: 3 additions & 1 deletion docs/webapp/theming_and_css.rst
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,8 @@ Component Tokens ו‑Theme Builder
- Collections (`webapp/static/css/collections.css`) עדיין מכיל צבעים קשיחים ישנים – כל שינוי חייב להמיר ל‑`var()` לפי טבלת הטוקנים.
- Split View ו‑Markdown Enhanced משתמשים ב‑``--split-*`` ו‑``--md-*`` בהתאמה – הוסיפו טוקן לפני שמוסיפים Class חדש.
- Sticky Notes, Reader Modes (`md_preview.html`) וה‑``theme_preview.html`` הם חריגים שנשארים Hardcoded כדי לשמור על תצוגת Preset.
- **מלכודת ספציפיות ב-**\ ``transition``\ **, שכל רכיב חדש נתקל בה.** ``dark-mode.css`` מגדיר ``transition`` על ``[data-theme="dark"] *`` (וכן ``dim``, ``nebula``, ``custom`` ו-``shared:``). הספציפיות של ``[attr] *`` שווה לזו של מחלקה יחידה, והקובץ נטען אחרי בלוק ``extra_css`` שבתבנית — ולכן כלל ``transition`` שנכתב על מחלקה אחת בקובץ רכיב **נדרס בשקט בכל הערכות הכהות**, וההנפשה פשוט לא רצה. הכשל שקט לחלוטין: אין שגיאה, ובדיקה בערכה בהירה עוברת. הפתרון שבשימוש ב-``toast.css`` הוא חזרה על שם המחלקה (``.ck-toast.ck-toast``), שמעלה את הספציפיות בלי ``!important``. אותו טיפול נדרש גם לכלל ה-``prefers-reduced-motion`` של הרכיב, שאחרת נדרס באותה דרך.
- ``toast.css`` מגדיר את משפחת ``--ck-toast-*`` (Level 3) ונטען כיום רק מ-``settings.html``. מרחב השמות שלו נפרד מ-``.notification`` בכוונה: ``bookmarks.css`` ו-``multi-select.css`` מגדירים את הסלקטור ההוא בשניהם, עם ערכים סותרים. את ערכי הרקע והטקסט הוא מעתיק מ-``bookmarks.css`` במקום לגזור אותם מ-``--bg-secondary``; הגזירה נבדקה ונפסלה כי ב-Classic היא יורדת מתחת ליחס הניגודיות הנדרש.
- `global_search.css` הינו דוגמה מצוינת לרכיב Component Tokens – כאשר מוסיפים תכונה חדשה (למשל badge נוסף) המשיכו את התבנית שם.
- Collections / Split View / Markdown Enhanced מוזכרים בדף זה כדי שמפתחים ידעו להצליב בין הרכיבים ולזהות אילו טוקנים משותפים.

Expand All @@ -336,7 +338,7 @@ Component Tokens ו‑Theme Builder
- ``webapp/templates/base.html`` – בלוק ה‑``<style>``: מקור כל ה‑Primitives ו‑``:root[data-theme]``, קביעת ``data-theme`` מוקדמת, וה‑Theme Wizard.
- ``webapp/static/css/dark-mode.css`` – שימוש בטוקנים עבור רכיבי Dark/Dim/Nebula.
- ``webapp/static/css/high-contrast.css`` – טוקני הבסיס של High Contrast, ובנוסף התאמות פוקוס/Outline. שאר הטוקנים שלה מושלמים ברמת הרכיב.
- ``webapp/static/css/global_search.css``, ``split-view.css``, ``bookmarks.css``, ``collections.css`` – דוגמאות מעשיות לטוקנים.
- ``webapp/static/css/global_search.css``, ``split-view.css``, ``bookmarks.css``, ``collections.css``, ``toast.css`` – דוגמאות מעשיות לטוקנים.
- Issue #2097 – מפרט Theme Builder (טוקנים במיקוד, UI/Backend/API, נגישות ו‑Reset flow).

לשאלות תיעוד/Testing יש לפנות לערוץ Frontend או לפתוח Issue חדש עם קישור לדף זה. הקפידו לעיין גם ב‑`FEATURE_SUGGESTIONS/css_refactor_plan.md` לפני שינויים רוחביים בקוד.
174 changes: 167 additions & 7 deletions tests/settings-note-fonts.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,12 @@ function mkSelect(surface, value) {
/**
* ``fetchImpl`` מקבל את גוף הבקשה המפורסר ומחזיר ``{ok, body}``.
* ברירת המחדל: הצלחה.
*
* ``opts.toast`` — ``undefined``: טוסט שמצליח (המסלול הרגיל);
* ``false``: טוסט שאינו מצליח להציג; ``null``: אין ``ckToast`` כלל.
* שני האחרונים חייבים ליפול לשורת ההודעה, אחרת הודעת כשל שמירה נעלמת.
*/
function load(fetchImpl) {
function load(fetchImpl, opts = {}) {
const sent = [];
const msg = { textContent: '', style: { display: 'none', color: '' } };
const selects = [
Expand All @@ -68,8 +72,14 @@ function load(fetchImpl) {
];
const scopeSel = mkSelect('__scope', 'global');

// שעון אירועים בדיד לשורת הגיבוי, שמסתירה את עצמה בטיימר.
let now = 0, seq = 0;
const timers = new Map();

const sandbox = {
console: { error: () => {} },
setTimeout: (fn, ms) => { const id = ++seq; timers.set(id, { at: now + ms, fn }); return id; },
clearTimeout: (id) => { timers.delete(id); },
document: {
getElementById: (id) => (id === 'noteFontsScopeSelect' ? scopeSel
: id === 'noteFontsMsg' ? msg : null),
Expand All @@ -87,10 +97,38 @@ function load(fetchImpl) {
};
},
};
const toasts = [];
// ``ckToast`` מותקן על ה-sandbox ולא נלכד במשתנה, כי יש תרחיש שבו הוא
// **מופיע באמצע**: ``toast.js`` נטען ב-``defer`` ולכן רץ אחרי הפרסור,
// בעוד שהמאזינים נרשמים בסקריפט מוטבע תוך כדי הפרסור.
function installToast(ok = true) {
sandbox.ckToast = (message, type, o) => {
toasts.push({ message, type, key: o && o.key });
return ok;
};
}
if (opts.toast !== null) installToast(opts.toast !== false);

vm.createContext(sandbox);
sandbox.window = sandbox;
vm.runInContext(fontsScript(), sandbox);
return { sent, msg, selects, scopeSel };

function advance(ms) {
const target = now + ms;
for (;;) {
let nextId = null, next = null;
for (const [id, t] of timers) {
if (t.at <= target && (next === null || t.at < next.at)) { nextId = id; next = t; }
}
if (next === null) { now = target; return; }
now = next.at;
timers.delete(nextId);
next.fn();
}
}

return { sent, msg, selects, scopeSel, toasts, advance, installToast,
pending: () => timers.size };
}

// ─── גוף הבקשה ──────────────────────────────────────────────────────────
Expand Down Expand Up @@ -131,22 +169,29 @@ await acheck('שינוי התחולה שולח גם את הערכים', async ()

// ─── כשל ────────────────────────────────────────────────────────────────

await acheck('כשל מחזיר את הבורר ומציג הודעה', async () => {
await acheck('כשל מחזיר את הבורר ומציג חיווי שגיאה', async () => {
const h = load(async () => ({ ok: false, status: 500, body: null }));
h.selects[1].value = '1';
await h.selects[1].__fire('change');

eq(h.selects[1].value, '0', 'הבורר הוחזר');
eq(h.msg.style.display, 'block', 'ההודעה מוצגת');
eq(h.msg.textContent.includes('נכשלה'), true, 'נוסח ההודעה');
eq(h.toasts.length, 1, 'מספר החיוויים');
eq(h.toasts[0].type, 'error', 'סוג החיווי');
eq(h.toasts[0].message.includes('נכשלה'), true, 'נוסח ההודעה');
eq(h.msg.style.display, 'none', 'שורת הגיבוי אינה מוצגת');
});

await acheck('הצלחה מציגה אישור ואינה מחזירה כלום', async () => {
const h = load();
h.selects[0].value = '1';
await h.selects[0].__fire('change');
eq(h.selects[0].value, '1', 'הבורר נשאר');
eq(h.msg.textContent, 'נשמר', 'ההודעה');
eq(h.toasts.length, 1, 'מספר החיוויים');
eq(h.toasts[0].message, 'נשמר', 'ההודעה');
eq(h.toasts[0].type, 'success', 'סוג החיווי');
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
// שני ערוצים, ורק אחד פעיל. בלי ההטענה הזו, ``notify`` שיאבד את
// ה-``return`` המוקדם היה מציג גם טוסט וגם שורה, וכל הבדיקות עוברות.
eq(h.msg.style.display, 'none', 'שורת הגיבוי אינה מוצגת');
});

await acheck('כשל מחזיר למצב האחרון שנשמר, לא לערך הקודם', async () => {
Expand Down Expand Up @@ -235,7 +280,7 @@ await acheck('הצלחה ואז כשל באותו תור — ההחזרה תוא
eq(h.sent.length, 2, 'שתי בקשות');
eq(h.selects[0].value, '1', 'מה שהשרת קיבל נשאר על המסך');
eq(h.selects[1].value, '0', 'מה שנכשל הוחזר');
eq(h.msg.textContent.includes('נכשלה'), true, 'ההודעה');
eq(h.toasts[h.toasts.length - 1].type, 'error', 'החיווי האחרון');
});

await acheck('הגוף שנשלח תואם ל-snapshot של אותו רגע', async () => {
Expand All @@ -262,5 +307,120 @@ await acheck('הגוף שנשלח תואם ל-snapshot של אותו רגע', as
JSON.stringify({ repo: true, md: false, board: true }), 'השנייה');
});

// ─── החיווי ─────────────────────────────────────────────────────────────

await acheck('שתי שמירות מוצלחות מפיקות שני חיוויים', async () => {
// **זה הבאג שדווח.** בגרסה הקודמת החיווי היה שורת סטטוס קבועה, ושמירה
// שנייה כתבה בה את אותה מחרוזת בדיוק — אפס שינוי ב-DOM, ולכן אפס חיווי.
// בדיקה על תוכן השורה הייתה עוברת גם על הקוד השבור; רק ספירת אירועים
// מבחינה ביניהם.
const h = load();
h.selects[0].value = '1';
await h.selects[0].__fire('change');
h.selects[1].value = '1';
await h.selects[1].__fire('change');

eq(h.sent.length, 2, 'שתי בקשות');
eq(h.toasts.length, 2, 'שני חיוויים — לא אחד');
eq(h.toasts[1].message, 'נשמר', 'גם השני');
});

await acheck('כל החיוויים נושאים מפתח אחיד', async () => {
// המפתח הוא מה שגורם לחיווי חדש להחליף את הקודם במקום להיערם לצידו.
const h = load();
h.selects[0].value = '1';
await h.selects[0].__fire('change');
h.scopeSel.value = 'device';
await h.scopeSel.__fire('change');

eq(h.toasts.length, 2, 'שני חיוויים');
eq(h.toasts[0].key, 'note-fonts', 'מפתח ראשון');
eq(h.toasts[1].key, 'note-fonts', 'מפתח שני');
});

await acheck('בלי ckToast — ההודעה נופלת לשורת הגיבוי', async () => {
// ``toast.js`` נטען ב-``defer``. אם הוא לא הגיע, הודעת כשל שמירה
// חייבת עדיין להגיע למשתמש.
const h = load(async () => ({ ok: false, status: 500, body: null }), { toast: null });
h.selects[1].value = '1';
await h.selects[1].__fire('change');

eq(h.msg.style.display, 'block', 'השורה מוצגת');
eq(h.msg.textContent.includes('נכשלה'), true, 'נוסח ההודעה');
});

await acheck('ckToast שהחזירה false — נופלת לשורת הגיבוי', async () => {
// ``ckToast`` מדווחת כשל בערך ההחזרה ולא בזריקה. קורא שמתעלם ממנו
// מציג "הכול תקין" על חיווי שלא הוצג — K11 בדפוסי הבאגים.
const h = load(async () => ({ ok: false, status: 500, body: null }), { toast: false });
h.selects[1].value = '1';
await h.selects[1].__fire('change');

eq(h.toasts.length, 1, 'הטוסט נוסה');
eq(h.msg.style.display, 'block', 'ובכל זאת השורה הוצגה');
eq(h.msg.textContent.includes('נכשלה'), true, 'נוסח ההודעה');
});

await acheck('שורת הגיבוי מסתתרת מעצמה', async () => {
// שורת מצב שלא מסתירה את עצמה היא בדיוק התסמין שתוקן כאן. הגיבוי
// מקבל את אותו טיפול.
const h = load(async () => ({ ok: false, status: 500, body: null }), { toast: null });
h.selects[1].value = '1';
await h.selects[1].__fire('change');

eq(h.msg.style.display, 'block', 'מוצגת');
h.advance(3999);
eq(h.msg.style.display, 'block', 'לפני הזמן');
h.advance(1);
eq(h.msg.style.display, 'none', 'נעלמה');
eq(h.pending(), 0, 'לא נשארו טיימרים');
});

await acheck('שמירה נוספת מאפסת את טיימר הגיבוי ואינה עורמת', async () => {
// בלי ``clearTimeout``, טיימר של הודעה קודמת היה מסתיר הודעה חדשה
// מוקדם מדי — אותו CORE U1 שכבר טופל בטוסט.
const h = load(async () => ({ ok: false, status: 500, body: null }), { toast: null });
h.selects[0].value = '1';
await h.selects[0].__fire('change');
h.advance(3900);
h.selects[1].value = '1';
await h.selects[1].__fire('change');

eq(h.pending(), 1, 'טיימר אחד, לא שניים');
h.advance(200); // הזמן שבו הראשון היה מסתיר
eq(h.msg.style.display, 'block', 'עדיין מוצגת');
h.advance(3800);
eq(h.msg.style.display, 'none', 'נעלמה לפי הטיימר החדש');
});

await acheck('מעבר מהגיבוי לטוסט מכבה את שורת הגיבוי', async () => {
// **התרחיש אמיתי ולא תיאורטי.** ``toast.js`` נטען ב-``defer``
// (``settings.html:377``) ולכן רץ רק אחרי סיום הפרסור, בעוד שהמאזינים
// ל-``change`` נרשמים בסקריפט מוטבע (``settings.html:1936``) תוך כדי
// הפרסור. בחיבור איטי יש חלון שבו העמוד אינטראקטיבי ו-``ckToast``
// עדיין אינו קיים.
//
// שמירה שנכשלת בחלון הזה נופלת לשורת הגיבוי ודורכת טיימר של 4 שניות.
// ניסיון חוזר אחרי שהסקריפט נטען עובר לטוסט — ובלי כיבוי מפורש,
// השגיאה המיושנת נשארת גלויה לצד טוסט ההצלחה.
let fail = true;
const h = load(async () => (fail ? { ok: false, status: 500, body: null }
: { ok: true, body: { ok: true } }), { toast: null });

h.selects[1].value = '1';
await h.selects[1].__fire('change');
eq(h.msg.style.display, 'block', 'שורת הכשל מוצגת');
eq(h.pending(), 1, 'הטיימר דרוך');

h.installToast(); // ``toast.js`` הגיע
fail = false;
h.selects[0].value = '1';
await h.selects[0].__fire('change');

eq(h.toasts.length, 1, 'ההצלחה עברה בטוסט');
eq(h.msg.style.display, 'none', 'ושורת הגיבוי כובתה');
eq(h.pending(), 0, 'והטיימר שלה בוטל');
});

console.log(`${passed} עברו, ${failed} נכשלו`);
process.exit(failed === 0 ? 0 : 1);
Loading
Loading