:summary: הפתק כווידג'ט בדפדפן: flush לפני פעולה הרסנית, המשטח בדפדפן הריפו, הגודל שנקבע מול הגודל שמוצג, פתק חדש והעתקת המזהה, תפריט ⋮, גודל הטקסט וכתב היד, החיפוש בתוך פתק, הממשק של השרשורים, הארכיון בדפדפן, וצבעים שנמדדים בפיקסל. עקרונות להוספת פיצ'ר לסטיקי-נוטס — הממשק בדפדפן: גאומטריה, אינטראקציה וניגודיות =============================================================================== העמוד הזה הוא חלק מ-:doc:`עקרונות להוספת פיצ'ר לסטיקי-נוטס `, ומחזיק את הפתק כווידג'ט בדפדפן: מיקום וגודל, אינטראקציה, וצבעים שנמדדים על הנייר. מה שנשמר במסד ועובר בין השירותים — ב-:doc:`עמוד השרת `, ומנוע הרינדור של הפתק — ב-:doc:`עמוד מנוע המארקדאון `. עיגון שורה בפתקי ריפו: למה ``anchored`` אינו זמין -------------------------------------------------- ``anchored`` מחייב עוגן שורה יציב. דפדפן הריפו משתמש ב-CodeMirror, שמרנדר רק את השורות שבתחום הנראה; לכן אין אלמנט DOM יציב שאפשר להיקשר אליו עבור שורה מחוץ למסך. פתקי ריפו תומכים רק ב-``surface`` וב-``screen``. תיאור ההתנהגות למשתמש נמצא ב-:doc:`/user/sticky_notes`. פתקי ריפו: המשטח הוא התוכן של הגולל, לא הקונטיינר ------------------------------------------------- בלוח, המשטח והקונטיינר הם אותו אלמנט: ``#boardSurface`` נגלל עם העמוד וגדל עם הפתקים שעליו. בדפדפן הריפו הם שניים. ``#code-viewer-container`` הוא מסגרת בגובה קבוע — עמודת flex עם ``min-height: 0`` ו-``overflow: hidden`` ב-``repo-browser.css`` — והתוכן נגלל בתוך הגולל הפנימי: ``.cm-scroller`` של CodeMirror, או תצוגת ה-Markdown. ``repo-notes.js`` מזריק את הגולל כ-``scroller``, ו-``_positionOrigin`` ממיר דרך ``_surfaceScrollShift`` בין מרחב התוכן, שבו המיקום נשמר, למרחב הקונטיינר, שבו הוא מרונדר. שלוש החלטות גאומטריה שאלו את הקונטיינר גם כאן, וכל אחת מהן הגיעה למשתמש: - **איפה פתק חדש נולד.** ``createNote`` שאל כמה מהקונטיינר נגלל מעל החלון — בדפדפן הריפו תמיד אפס — ופתק חדש נשמר בראש הקובץ, מחוץ לתחום הנראה. היום ``_newNotePosition`` מודד את הפינה של מה שרואים (``_visibleTopLeft``: החיתוך של החלון, הקונטיינר והגולל) וממיר אותה עם ``_positionOrigin`` — ההמרה ש-``_notePayloadFromEl`` עושה בכיוון ההפוך. כך פתק חדש עובר באותה המרה כמו גרירה ונעיצה, ולא בנוסחה משלו, ולכן מה שנשמר לו הוא מה שמוצג. - **כמה המשטח גדל.** ``_applySurfaceExtent`` כתב ``min-height`` על הקונטיינר, כמו בלוח. הסגנון האינליין גבר על ``min-height: 0``, הקונטיינר גדל מעבר ל-``.repo-content``, העורך נמתח איתו, וטווח הגלילה שלו התקצר בדיוק בכמה שנחתך — סוף הקובץ לא היה נגיש בשום גלילה. והערך עצמו חושב מ-``style.top`` של הפתקים, שבדפדפן הריפו כולל את היסט הגלילה; לכן אותו פתק נעל או שחרר לפי המקום שבו המשתמש עמד כשהחישוב רץ, וגם גרירה של פתק ``screen`` הפעילה אותו. בדפדפן הריפו המשטח אינו גדל — את אורך התוכן קובע CodeMirror — והפונקציה אינה כותבת שם דבר. - **עד איפה מותר לפתק לשבת לרוחב.** ``_clampToSurface`` הצמיד גם בדפדפן הריפו לרוחב הקונטיינר, בזמן ש-``x`` השמור נמדד בתוכן — ושורות ארוכות נגללות הצידה. פתק שנגרר בזמן גלילה הצידה קפץ בגלילה הבאה, ופתק חדש בשורות ארוכות נחסם אל מחוץ לתחום. היום ``_applyPositionMode`` מעביר ל-``_clampToSurface`` את ``_surfaceReach`` — המסגרת שהגרירה מצמידה אליה, על כל מיקומי הגלילה, במרחב התוכן — ורוחב הקונטיינר נשאר ברירת המחדל: לגרירה, שמודדת במרחב הרינדור מול המסגרת שרואים, וללוח. הטווח נגזר מהמסגרת ולא מהקופסה של הגולל עצמו בכוונה: בתצוגת ה-Markdown הגולל צר מהמסגרת (ה-``margin`` וה-``border`` של ``.markdown-preview-container`` ב-``markdown-preview.css``), וטווח שנגזר ממנו הזיז בגלילה הבאה פתק שנגרר אל הקצה הימני. .. important:: **החלטת גאומטריה חדשה על פתק ``surface`` שואלת את המשטח, לא את הקונטיינר.** בלוח שני אלה אותו אלמנט, ולכן בדיקה על הלוח לא תתפוס את ההבדל; בדפדפן הריפו הוא שובר את הגלילה. ההמרה בין המרחבים היא ``_positionOrigin`` ו-``_surfaceScrollShift``, והטווח הוא ``_surfaceReach`` — לא נוסחה חדשה במקום שלישי. .. note:: **הכיוון של הגולל קובע לאיזה צד הטווח מתרחב.** בגולל ``rtl`` — תצוגת ה-Markdown — ה-``scrollLeft`` הוא 0 בקצה הימני ושלילי בגלילה שמאלה (MDN, ‏Element.scrollLeft), כלומר התוכן שנחשף בגלילה שמאלה יושב במיקומים שליליים. השרת מצמיד מיקום שלילי ל-0 (``_coerce_int`` ב-``webapp/sticky_notes_api.py``), ולכן שם הטווח אינו מתרחב: מה שמוצג הוא מה שנשמר, אבל פתק אינו יכול לשבת מעל התוכן שמשמאל לנקודת ההתחלה. זה מקרה נדיר — תצוגת Markdown שתוכנה רחב מהחלונית, כמו מילה ארוכה מאוד שאינה נשברת — והפתרון שלו הוא מיקום שמור שאינו נמדד מהקצה השמאלי, כלומר שינוי במודל ולא בהצמדה. ומאותה סיבה, פתק שנגרר אל השוליים השמאליים של תצוגת ה-Markdown (ה-``margin`` של ``.markdown-preview-container``) זז מהם בגלילה הבאה: הגרירה מצמידה למסגרת ואינה מכירה את הגבול של השרת. כך היה גם לפני ``_surfaceReach`` — נמדד באוקטובר 2026, בשני הכיוונים של הטאבלט. המדידות, בדף ``/repo/`` האמיתי ובמגע של טאבלט, נמצאות ב-``tests/test_repo_notes_browser.py``; ההיגיון עצמו ננעל גם ב-node, בבדיקות שנפתחות ב-"ריפו:" ב-``tests/sticky-notes-target.test.js``. סדר: ``flush`` לפני כל פעולה הרסנית ----------------------------------- תור השמירה עובד עם ``debounce``, ולכן עריכה שנעשתה זה עתה עדיין ממתינה בו. כל פעולה שעלולה למחוק את התור — פירוק המנהל בהחלפת יעד, סימון צ'קבוקס שמפעיל כתיבה משלו — חייבת **לרוקן את התור תחילה**. אחרת העריכה האחרונה נעלמת בלי שום סימן. זו תקלת סדר שכבר נתפסה כאן פעמיים (``_flushFor`` לפני ה-toggle; ``destroy`` שמרוקן לפני שהוא מפרק). גודל: מה שנקבע לעומת מה שנכנס ------------------------------ מיקום וגודל נשמרים בפיקסלים מוחלטים, ולכן פתק שנוצר במסך אחד מוצג במסך אחר בערכים שאינם שלו. המיקום נפתר בהצמדה — היא אינה מאבדת מידע. **הגודל אינו:** צמצום לגבול משנה את מה שיוצג, וכל עוד אין מקום שני שזוכר את מה שנקבע, ההקטנה היא דריסה. לכן הגודל מוחזק בשני ערכים: **הכוונה**, שנקבעת ברינדור מהערך שב-DB ומתעדכנת אך ורק ב-``_enableResize``; ו**המוצג**, שנגזר ממנה דרך ``_fitSizeToBounds``. המיקום נשאר ערך אחד. **תשובה אחת לשאלה "מה הרוחב של הפתק הזה".** ארבעה קוראים שואלים אותה — שלושת הענפים ב-``_applyPositionMode``, ו-``_clampToSurface`` (``_reflowWithinViewport`` מגיע אליה דרך ``_fitAndClampToViewport``) — וכולם עוברים דרך ``_resolveDisplaySize``, בסדר אחד: **הכוונה שעל האלמנט ←** ``note.size`` **←** ``el.style`` **← המלבן ← ברירת המחדל**. הכוונה קודמת כי היא ה-DOM המקומי, ו-``_enableResize`` מעדכן אותה לפני שהשמירה יוצאת לדרך. סדר שונה באותה שאלה אינו כפילות סגנונית: ההצמדה מחשבת כמה מקום נשאר לפי רוחב אחד בזמן שהתצוגה כותבת רוחב אחר, והפתק נצמד למקום שנכון לגודל שאינו הגודל שיוצג. **לא כל הצמדה מצמצמת.** ``_clampToViewport`` אינו נוגע בגודל כלל — הוא קורא את המלבן ומצמיד x/y בלבד, בשונה מ-``_fitAndClampToViewport`` שמצמצם קודם. הקורא היחיד שלו הוא לולאת הגרירה של פתק צף, ושם הגודל כבר נקבע קודם לכן. ההבחנה חשובה למי שיוסיף שם צמצום: הוא יצמצם לפי המלבן, כלומר לפי המוצג ולא לפי הכוונה, ובכך יהפוך כל גרירה במסך צר לדריסה של הגודל — בדיוק הבאג שהמבנה הזה מונע. **שינוי גודל ידני מסתיים בהצמדה.** ``_enableResize.onMove`` חוסם את הרוחב ל-120..1200 בלי שום מודעות לגבולות — במכוון, כי שם נקבעת הכוונה, וחסימתה לגבולות הייתה הופכת מסך צר לתקרה קבועה על מה שהמשתמש רשאי לבקש. לכן ``onUp`` כותב את הכוונה **ואז** קורא ל-``_applyPositionMode``, שמצמיד את המוצג. הסדר הוא כל העניין: הכוונה לפני, ולכן מה שנבחר נשמר; ``_notePayloadFromEl`` אחרי, ולכן מה שנשמר כמיקום הוא המוצמד. בלי הקריאה הזו פתק שהוגדל ליד קצה הלוח גלש מעבר לו ואף אחד לא הצמיד עד אירוע ``resize`` או גלילה — שבלוח אין. נמדד בכרומיום: גלישה של 54 פיקסלים, עם ידית שינוי הגודל עצמה מחוץ לחלון — כלומר אי אפשר לכווץ בחזרה. .. important:: **כל השמירות עוברות דרך משפך אחד — ``_notePayloadFromEl``** — וחמש נקודות קוראות לו: גרירה, נעיצה, ביטול עיגון, החלפת מצב, ושינוי גודל ידני. הוא קורא את **המיקום מהמלבן ואת הגודל מהכוונה**, ובכך כל חמשתן מוגנות בלי הגנה נפרדת בכל אתר. קריאת הגודל מהמלבן היא הבאג שהמבנה הזה מונע, והוא היה קיים עוד לפני ההקטנה: פתק ממוזער מקבל ``height: auto !important`` ומתכווץ לגובה הכותרת, ולכן מיזעור ואז גרירה שמרו את גובה הכותרת כגובה הפתק. הצמצום קודם להצמדה, לא להפך: ``maxLeft`` נגזר מהרוחב, וחישובו לפי רוחב שכבר לא יוצג מצמיד את הפתק למקום שנכון לגודל אחר. זה בדיוק המצב שבו פתק "נדחף לקצה וממשיך לגלוש". שלושת מצבי המיקום נבדלים במסגרת הייחוס שלהם, ולכן גם במה שמצטמצם ובאיזה ציר מוצמד: .. list-table:: :header-rows: 1 :widths: 14 22 22 42 * - מצב - מה מצטמצם - איזה ציר מוצמד - למה * - ``surface`` - רוחב, לרוחב המשטח - X בלבד - המשטח גליל וגדל כלפי מטה, ולכן פתק גבוה נשאר נגיש בעוד שפתק רחב גולש ואין דרך להגיע לידית שלו. * - ``anchored`` - רוחב, לרוחב אזור התצוגה - X בלבד - הפתק הוא ``absolute`` במרחב המסמך. ה-``top`` נגזר מהעוגן ב-``_updateAnchoredNotePosition``, והצמדתו הייתה מנתקת אותו מהשורה שהוא מצביע עליה. * - ``screen`` - רוחב וגובה, לאזור התצוגה - שניהם - הפתק הוא ``fixed`` מול אזור התצוגה, ולכן חריגה בכל ציר מוציאה אותו מהמסך לצמיתות. המרווחים בענפי ה-X המוצמדים (``screen`` ו-``anchored``) באים מ-``NOTE_EDGE_GAP``; בענף המשטח אין מרווח קצה — הוא מצמיד את X ל-0 ומתאים את הרוחב ל-``clientWidth`` המלא, בלי קבועים בכל אתר. בדפדפן הריפו הגבול הימני של ההצמדה אינו רוחב הקונטיינר אלא ``_surfaceReach``, כי שם המשטח נגלל לרוחב — ראו "פתקי ריפו: המשטח הוא התוכן של הגולל, לא הקונטיינר". .. note:: **‏X של פתק מעוגן נשמר במרחב המסמך, וההצמדה מודדת מול אזור התצוגה** — שני מרחבים שונים, שמתלכדים כאן כי ``scroll.x`` הוא תמיד אפס. נמדד בכרומיום על ``/md/``: ``documentElement.scrollWidth`` שווה ל-``clientWidth`` גם עם בלוק קוד רחב, טבלה ברוחב 2864 פיקסלים ומילה בת 300 תווים — כולם נבלמים ב-``overflow-x: auto`` פר-אלמנט — וטווח הגלילה האופקית הוא 0 לשני הכיוונים. **מי שיוסיף גלילה אופקית לעמוד חייב להוסיף להצמדה את ההיסט.** ומדוע ההצמדה נחוצה בכלל, אם למסמך אין קצה ימני קשיח כמו למשטח: פתק ב-``left: 1500px`` על מסך 420 יושב מחוץ למסך **ואינו מרחיב את שטח הגלילה** — נמדד. כלומר אין שום דרך להגיע אליו, בדיוק כמו פתק שגלש מהמשטח. .. warning:: ``is-pinned`` מסמן **גם** ``surface`` וגם ``anchored``, ולכן ``_reflowWithinViewport`` מדלג על שניהם. מי שיוסיף מצב מיקום חדש ויסמן אותו ב-``is-pinned`` יקבל בירושה גם את הדילוג הזה: הצמצום שלו חייב לחיות בענף שלו ב-``_applyPositionMode``, שאליו ``_reapplyPinnedLayout`` מגיע בשינוי גבולות. כך זה נשבר בפעם הקודמת: המצב ``anchored`` נוסף בלי מדיניות גודל כלל, ופתק ברוחב 600 גלש במסך 320 כבר בטעינה — לא רק אחרי סיבוב. **היום יש לו מדיניות, כמתואר בטבלה שלמעלה.** **שינוי גבולות חייב מסלול משלו לפתקים הנעוצים.** ``_reflowWithinViewport`` מדלג על ``is-pinned`` בכוונה: המיקום של פתק משטח נמדד במרחב המשטח ולא במסך, והצמדה לפי ה-viewport הייתה מזיזה אותו למקום שגוי. לכן ``resize`` קורא גם ל-``_reapplyPinnedLayout``, שמריץ ``_applyPositionMode`` על הפתקים הנעוצים — הפונקציה היחידה שמצמצמת פתק משטח לרוחב המשטח. בלעדיה לוח שנטען במסך רחב ואז סובב נשאר עם פתקים ברוחב הישן, גולשים מעבר לקצה עם הידית מחוץ למסך. **גם המאזין של** ``visualViewport`` **קורא לה**, ומאותה סיבה בדיוק. פעם זה לא היה נחוץ: פתק משטח גוזר את רוחבו מ-``parent.clientWidth``, שפינץ'-זום ופתיחת מקלדת אינם משנים. הפתק המעוגן גוזר אותו מ-``_viewportBox()``, שקורא את ``visualViewport`` עצמו — ולכן בלי הקריאה הזו הוא נשאר ברוחב שנגזר מהזום הקודם. במשטח זו עבודה מיותרת אך לא מזיקה, וזה המחיר הנכון לעומת חיווט נפרד לכל מצב. .. _sticky-notes-new-note-size: גודל של פתק חדש, והגיבוי של פתק בלי גודל ----------------------------------------- עד אוקטובר 2026 שתי שאלות נראו כמו שאלה אחת, כי לשתיהן הייתה אותה תשובה, 260×200: **באיזה גודל נפתח פתק חדש**, ו**מה מקבל פתק שאין לו גודל בכלל**. ההתנהגות הנראית מתוארת ב-:doc:`/user/sticky_notes`; כאן מה שמסביבה. היום אלה שני זוגות של קבועים ב-``sticky-notes.js``: - ``NEW_NOTE_W``/``NEW_NOTE_H`` ו-``NEW_NOTE_PHONE_W``/``NEW_NOTE_PHONE_H`` — מה ש-``createNote`` שולח לשרת, ומשם הכוונה של הפתק (הסעיף הקודם). ``NEW_NOTE_PHONE_MAX_VW`` מכריע בין השניים: דרך ``matchMedia`` עם ``max-width``, ודרך ``innerWidth`` כשאין ``matchMedia``. עד אז היצירה לקחה מספרים מוקלדים, והקבוע שנקרא ``NOTE_DEFAULT_W`` לא היה הגודל שלה כלל. - ``NOTE_FALLBACK_W``/``NOTE_FALLBACK_H`` (עד אוקטובר 2026 ``NOTE_DEFAULT_*``) — הגיבוי בלקוח, ב-``_renderNote``, ב-``_sizeIntentFallback`` וב-``_clampToViewport``. הוא נשאר 260×200 **בהחלטה**: פתק בלי גודל שמור אינו גדל עם ברירת המחדל החדשה. **במסך רחב — הגודל שנבחר בעין, ביחס של הגודל הקודם.** המשתמש גרר פתק לגודל שנראה לו נכון, והגודל השמור שלו נקרא מהמסד ב-16.10.2026: 503×373. ``NEW_NOTE_W``/``NEW_NOTE_H`` שומרים על השטח הזה ועל היחס 13:10 של 260×200 — כלומר בדיוק פי 1.9 מהגודל הקודם. **בטלפון הגודל לא השתנה**, בהחלטה של המשתמש. .. important:: **על פתק בלי גודל מכריע השרת, ולא הגיבוי בלקוח.** ``_as_note_response`` ב-``webapp/sticky_notes_api.py`` שולח ``size`` עם כל פתק, ולמסמך בלי ``width``/``height`` הוא נותן ערכים משלו. לכן ``NOTE_FALLBACK_*`` חל רק על אובייקט פתק שהגיע בלי ``size`` בכלל — קאש ישן ב-``localStorage`` — ועל המדידות שנופלות אליו כשאין מלבן. שני הגיבויים לא השתנו, ולכן פתק בלי גודל שמור מוצג כמו קודם. ``test_notes_that_exist_keep_their_size`` ב-``tests/test_sticky_note_copy_id_browser.py`` בודק את הגיבוי בלקוח; בזה שבשרת השינוי לא נגע. .. note:: **הכותב השני — ה-MCP — יוצר פתקים בגודל משלו**, ב-``_new_note_doc`` וב-``create_note`` של ``mcp_server/backend.py``, בלי קשר למסך שבו הפתק יוצג. השינוי כאן הוא בוובאפ בלבד, ולכן פתק שסוכן יוצר עדיין קטן מפתק שנוצר בדפדפן במסך רחב. ההערה שליד הערכים שם אמרה עד היום שהם ברירות המחדל של הוובאפ, ותוקנה. **שורת הפעולות נכנסת כולה בגודל החדש.** נמדד בכרומיום 153 ב-24.10.2026, בצעדים של פיקסל ובנוסחה של ``ROW_JS`` ב-``tests/test_sticky_note_copy_id_browser.py``: הכפתורים בשורה נכנסים בפתק מרוחב 268 — זה הרוחב שבו :ref:`⋮ `, האחרון בשורה, כבר אינו חתוך. עד שנוסף ⋮ ו-🔔 עבר לתפריט שלו, הרוחב הזה היה 272 והאחרון בשורה היה ×, ושם הוא היה הנחתך (נמדד בכרומיום 141 ב-16.10.2026, ושוב ב-24.10.2026 על הקוד שלפני השינוי); הסעיף על השרשורים מתאר את השורה ההיא במדידה בצעדים של 20 — ופתק חדש במסך רחב רחב ממנו בהרבה. ``test_a_new_note_on_a_wide_screen_is_bigger_and_its_whole_row_fits`` יוצר פתק דרך ``createNote``, ודורש שאף כפתור בשורה לא ייחתך ושהרוחב שהיא צריכה (``needed``) יהיה קטן מרוחב הפתק. בטלפון השורה רחבה מהפתק כמו קודם (`#3478 `__). .. _sticky-notes-copy-id: העתקת מזהה הפתק: בשורת השם, והמזהה נקרא ברגע הלחיצה ------------------------------------------------------- ההתנהגות הנראית מתוארת ב-:ref:`העתקת המזהה `; כאן הנימוק. **בשורת השם, ליד החיפוש — ולא בשורת הפעולות.** זו הכרעה של המשתמש, ומאותה סיבה שכפתור החיפוש יושב שם (הסעיף "התיבה: ילד של הפתק"): שורת הפעולות אינה נשברת. באותה מדידה, עם כפתור המזהה בתוכה השורה הייתה צריכה 308 במקום 272 — כלומר בכל פתק צר מ-308 היה נחתך עוד כפתור: בפתק ברוחב 260, ברירת המחדל במחשב עד אז, גם המזעור ולא רק המחיקה, ובפתק חדש בטלפון ארבעה כפתורים במקום שלושה. בדיקה במסד באותו יום מצאה כשליש מהפתקים צרים מ-308. המספרים האלה נמדדו עם האייקון הראשון של הכפתור, ``🆔``. היום האייקון הוא ``#``, תו טקסט שמקבל את צבע הכפתור כמו ``✎``, ולא אימוג'י בצבעים משלו; איתו השורה הייתה צריכה 304, ומספר הכפתורים שנחתכים זהה — שניים בפתק ברוחב 260 וארבעה ברוחב 200 (נמדד בכרומיום 141 ב-16.10.2026, בנוסחה של ``ROW_JS`` ב-``tests/test_sticky_note_copy_id_browser.py``). כלומר ההחלפה לא שינתה את ההכרעה. ובזמן חיפוש, כשהפתק אינו חותך (``.is-unclipped``), השורה הארוכה יצאה בפתק צר מהקצה השמאלי של החלון ופתחה לעמוד גלילה לרוחב — ``test_nothing_the_note_clipped_leaks_out_while_searching`` נפל בדיוק על זה. בשורת השם הכפתור גלוי בכל רוחב, כי בפתק צר השורה הזו יורדת לשורה משלה ומקבלת את כל הרוחב. **המחיר, שנמדד גם הוא:** בפתק צר מ-136 בערך שדה השם נדחק לרוחב המינימלי שלו ונחתך מצד שמאל — עד 16 פיקסלים ברוחב המינימלי של פתק (120). במסד כמעט אין פתקים כאלה (נבדק ב-16.10.2026). **המזהה נקרא ברגע הלחיצה, מהאלמנט, ולא נלכד בכפתור.** ``_copyNoteId`` קורא את ``el.dataset.noteId`` — הזהות שכל המנהל שואל עליה (``_getEntry``) — ולא ``note.id`` שהיה נלכד בסגור של המאזין. מה שמועתק הוא מה שהפתק נושא ברגע הלחיצה, והבדיקה "מזהה: נקרא ברגע הלחיצה, ולא כשהכפתור נבנה" נופלת על לכידה בבנייה. **מזהה שהשרת לא נתן אינו מועתק.** ``isSavedNoteId`` בודק את הצורה ש-``codekeeper_get_note`` מקבל: הוא מפענח ב-``ObjectId(str(note_id))``, שמקבל מחרוזת רק באורך 24 שעוברת ``bytes.fromhex`` (``ObjectId.__validate`` ב-``bson/objectid.py``, pymongo 4.15.3) — וזו הצורה שהשרת שולח, ``str`` של ה-``ObjectId``. על כל דבר אחר מוצג חיווי הכשל של ההעתקה, עם ההסבר "הפתק עוד לא נשמר". אין כפתור מושבת ואין מסלול שמדליק אותו אחרי השמירה: הבדיקה רצה מחדש בכל לחיצה. אותו כלל, למזהה של קובץ, יושב ב-``OBJECT_ID_RE`` של ``card-preview.js``, ובדיקה ב-``tests/sticky-notes-target.test.js`` דורשת ששני הביטויים יישארו זהים. והחוזה מול השרת נבדק בפייתון: ``TestSavedNoteIdRule`` ב-``tests/test_sticky_notes_api.py`` קורא את הביטוי מקובץ ה-JS, ובודק שכל מזהה ש-``_as_note_response`` שולח עובר אותו, ושכל מה שהוא מקבל ``ObjectId`` מפענח לאותו מזהה בדיוק. "לאותו מזהה" ולא רק "בלי חריגה", כי ``bytes.fromhex`` מדלג על רווחים בין זוגות ספרות: מחרוזת של 24 תווים עם שני רווחים בתוכה עוברת את ``ObjectId``, ומפוענחת למזהה אחר של 22 ספרות. לכן הכלל בלקוח צר ממה ש-``ObjectId`` מקבל, ובכוונה. .. note:: **היום הענף הזה אינו נפתח מהממשק, וזה נאמר במפורש.** פתק מוצג רק אחרי שהשרת החזיר לו מזהה: ``createNote`` מרנדר אחרי התשובה, ו-``loadNotes`` והקאש מרנדרים את מה שהשרת שלח. מזהה כזה מגיע רק מתשובת יצירה בלי ``id``, או מקאש פגום ב-``localStorage``. מי שיוסיף יצירה אופטימית, עם מזהה זמני עד שהשרת עונה, מקבל את ההתנהגות הנכונה בלי לגעת כאן — בתנאי שהמזהה הקבוע נכתב ל-``el.dataset.noteId`` כשהוא מגיע. ``test_a_note_the_server_gave_no_id_says_not_saved_and_copies_nothing`` יוצר בדיוק את המצב הזה: ``createNote`` מול שרת שעונה ``{"ok": true}`` בלי מזהה. **מנגנון אחד.** ההעתקה עוברת ב-``_copyText`` והחיווי ב-``_flashCopyResult``, כמו ``⧉`` ובלוק הקוד (:ref:`הסעיף "ההעתקה: מהמקור, לא מה-DOM" `). ``_flashCopyResult`` קיבל ``failTitle`` — ההסבר לכשל שהדפדפן אינו הסיבה שלו — ולא נוסף חיווי שני. כלל ה-CSS של החיווי חל על שני הכפתורים. .. danger:: **ובאותו מנגנון היה באג, והכפתור החדש היה הופך אותו לנגיש בכל לחיצה כפולה.** כל לחיצה לכדה את ה-``title`` של אותו רגע, כדי לשחזר אותו כשהחיווי יורד — ואחרי כשל, ה-``title`` של אותו רגע הוא הודעת הכשל עצמה. שתי העתקות שנכשלו בהפרש קצר השאירו את הכפתור עם ההודעה לתמיד; נמדד בכרומיום לפני התיקון, על ``⧉`` עם קליפבורד חסום. בכפתור המזהה, על פתק בלי מזהה, כל לחיצה נכשלת. היום ``_copyFlashes`` מחזיק לכל כפתור את החיווי שמוצג עליו: לחיצה בזמן חיווי מבטלת את הטיימר שלו, וה-``title`` שישוחזר הוא זה שנלכד לפני הראשונה. וה-``catch`` שעטף את הפונקציה כבר אינו בולע בשקט — כשל בה נרשם בקונסול. **מה לבדוק, ובמה.** ההיגיון — מה נשלח להעתקה ומתי, איפה הכפתור יושב, הכלל המשותף עם ``card-preview.js``, החיווי בלחיצה כפולה, והגודל שנשלח בפתק חדש — בבדיקות שבסוף ``tests/sticky-notes-target.test.js``, ב-node. רץ ב-CI. החוזה של המזהה מול השרת — ב-``TestSavedNoteIdRule`` (``tests/test_sticky_notes_api.py``), בפייתון, וגם הוא רץ ב-CI. הקליפבורד, החיווי בדפדפן, הגודל שמוצג, שורת הפעולות, והכפתור הגלוי בטלפון — ב-``tests/test_sticky_note_copy_id_browser.py``; ופתק ריפו בעמוד ``/repo/`` האמיתי — ב-``test_the_id_button_copies_the_id_of_a_repo_note`` (``tests/test_repo_notes_browser.py``). בדיקות הדפדפן אינן רצות ב-CI. .. _sticky-notes-more-menu: תפריט ⋮: הפעולות הנדירות, ומה קובע לאן הוא נפתח ------------------------------------------------- ההתנהגות הנראית מתוארת ב-:ref:`sticky-note-more-menu`; כאן הנימוק. **בסוף שורת הפעולות — הכרעה של המשתמש, ולכן הוא הראשון שנחתך.** שורת הפעולות אינה נשברת (`#3478 `__), ובפתק צר הקצה שלה נחתך. ⋮ יושב שם בכוונה: לתוכו נכנסות הפעולות שהכי פחות שכיחות, ומה שבתוכו הוא בדיוק מה שאפשר לוותר עליו. 🔔 עבר מהשורה לתפריט, ולכן מספר הכפתורים בשורה לא השתנה, והשורה אף התקצרה — המספרים ב-:ref:`sticky-notes-new-note-size`. ``test_in_a_narrow_note_the_more_button_is_cut_first`` ב-``tests/test_sticky_note_menu_browser.py`` דורש שמה שנחתך יהיה תמיד סוף השורה, ו-⋮ בתוכו. **רשימה אחת, והתפריט נבנה ממנה** — ``NOTE_MENU_ITEMS`` ב-``sticky-notes.js``. לכל פריט אייקון, שם נגיש (``label``, שהוא גם ה-``title``), ופעולה. הפעולה היא פונקציה שמקבלת את המנהל ואת הפתק, ולא שם של מתודה במחרוזת: כך הקריאה למתודה כתובה ברשימה כמו כל קריאה אחרת בקוד, וחיפוש של שם המתודה, או שינוי שם שלה, מוצא גם אותה. פריט חדש נכנס כשורה ברשימה, בלי מנגנון נוסף. בחירה בפריט סוגרת את התפריט **לפני** שהפעולה רצה, ובלי להחזיר את הפוקוס ל-⋮ — הפעולה עשויה לפתוח משטח משלה, כמו חלון התזכורת, והפוקוס שייך לשם. חלון התזכורת אכן לוקח אותו: ``_openReminderModal`` מעביר את המיקוד לכפתור הקיצור הראשון, גם כשהחלון כבר היה פתוח, ובסגירה מחזיר אותו ל-⋮ של הפתק — הפריט שנלחץ ירד מה-DOM יחד עם התפריט, ובלי זה המיקוד היה נופל ל-``body``. **ילד של הפתק, כמו רשימת "+N" של השרשורים.** התפריט זז עם הפתק ונמחק איתו, וממוקם ``absolute`` — ולכן אינו משנה את המלבן ש-``_notePayloadFromEl`` קורא ממנו את המיקום (אותו תנאי כמו בתיבת החיפוש, :ref:`sticky-notes-in-note-search`). הוא נושא ``sticky-note-outside`` ומדליק את ``.is-unclipped`` דרך ``_syncNoteClipping``, כי בפתק ממוזער הוא יוצא מתחת לכותרת; ופתיחתו מעלה את הפתק לקדמת הערימה (``_raiseToFront``), כי ה-``z-index`` שלו כלוא בתוך הפתק, ופתק שכן שמצויר אחריו היה מכסה אותו. תפריט אחד לכל המנהל: פתיחה סוגרת תפריט פתוח בפתק אחר. .. important:: **צירוף חדש: פתק ממוזער שאינו חותך.** החיפוש והשרשורים מושבתים בפתק ממוזער, ולכן עד התפריט פתק ממוזער לא הפסיק לחתוך אף פעם — והכלל שמעגל את הפינות של הכותרת (``.is-unclipped:not(.is-searching) .sticky-note-header``) מעגל רק את העליונות, כי מתחת לכותרת יש תמיד גוף. בפתק ממוזער הכותרת היא כל הפתק, ובהשוואת פיקסלים בכרומיום רקע הכותרת יצא מהקשת בפינות התחתונות ברגע שהתפריט נפתח. הכלל ``.is-minimized.is-unclipped`` מעגל את ארבע הפינות, ו-``test_a_minimized_note_reveals_nothing_while_its_menu_is_open`` נופל בלעדיו. מי שיאפשר בעתיד מצב אחר של ``.is-unclipped`` בפתק ממוזער מקבל את הכלל הזה בירושה. **המיקום — כלפי מטה, ולמעלה רק כשאין מקום (הכרעה של המשתמש), ומצמצמים לפני שמזיזים (H7).** ``_layoutNoteMenu`` מגביל קודם את הרוחב למלבן שמותר פחות השוליים (``NOTE_EDGE_GAP``); אחר כך בוחר כיוון — מטה, ומעלה רק כשהתפריט אינו נכנס מתחת ל-⋮ ומעליו יש יותר מקום; אחר כך מגביל את הגובה למקום בכיוון שנבחר; ורק אז ממרכז מתחת ל-⋮ ומצמיד אל תוך המלבן. החשבון הוא הפרש בין מלבנים שנמדדו, ולכן נכון ב-RTL וב-LTR כאחד, ו-``left`` נכתב פיזי בכוונה — ההערה ב-CSS מסבירה למה לא ``inset-inline-start``. נמדד בשתי הפריסות: ``test_the_menu_is_centered_under_the_button_in_both_layouts`` ו-``test_near_the_edge_the_menu_is_pulled_back_onto_the_screen``. התפריט ממוקם מחדש בכל גלילה ובשינוי גודל החלון, באותו פריים שבו ממוקמות חלונית השרשור ורשימת "+N". **מה חותך את הפתק — לפי הכלל של CSS, ולא לפי הנחה על הקונטיינר** (``_noteMenuBounds``). ``overflow`` שאינו ``visible`` חותך את הצאצאים, חוץ מאלה שה-containing block שלהם הוא אב שלו או אזור התצוגה (CSS 2.1, סעיף 11.1.1). לכן פתק ``fixed`` — מצב ``screen`` — נחתך רק במסך, ופתק ``absolute`` נחתך בידי ה-containing block שלו, שאותו מחזיר ``offsetParent`` (CSSOM View), ובידי כל אב שלו שחותך. הגרסה הראשונה חתכה תמיד עם הקונטיינר, וזה נמדד כשגוי: על משטח שאינו חותך, תפריט של פתק ממוזער התהפך כלפי מעלה למרות שמתחתיו היה מקום במסך. בעמודים האמיתיים הקונטיינר כן חותך — ``.board-surface`` הוא ``overflow: auto``, ו-``#code-viewer-container`` הוא ``overflow: hidden`` — וזה נגזר כאן מה-CSS ולא נקבע מראש. בדפדפן הריפו המסגרת נמשכת מתחת לתחתית המסך (נמדד בכרומיום 153 ב-24.10.2026: ``bottom`` ‏844 בטאבלט של 1280×800), ולכן שם המסך הוא שקובע בתחתית. .. note:: **חישוב, ולא חיישן — בשונה מתיבת החיפוש ומהבועות** (:ref:`sticky-notes-in-note-search`). שם ``IntersectionObserver`` מכריע, כי הוא סופר חיתוך של כל אב. אבל ההודעה שלו מגיעה כמשימה שנכנסת לתור אחרי עדכון הרינדור (W3C Intersection Observer, 3.2.4, "Queue an Intersection Observer Task"), כלומר אחרי שהתפריט כבר צויר במקום הראשון — והוא היה נפתח כלפי מטה ואז קופץ למעלה. התפריט נולד בלחיצה, וההכרעה צריכה להיות מוכנה לפני הציור הראשון שלו. החישוב כאן הוא אותו כלל שהדפדפן מחיל, ו-``test_inside_a_clipping_ancestor_the_menu_flips_before_the_screen_edge`` בודק אותו מול אב שחותך מעל ה-containing block. **מקלדת ואירועים.** ``Enter`` על ⋮ פותח ומפקס את הפריט הראשון; החיצים, ``Home`` ו-``End`` עוברים בין הפריטים; ``Escape`` סוגר ומחזיר את הפוקוס ל-⋮; ``Tab`` סוגר ומחזיר את הפוקוס ל-⋮ **בלי** לבטל את ברירת המחדל, כך שהמעבר ממשיך מ-⋮ ולא מאלמנט שכבר ירד מה-DOM. מקשים בתפריט אינם מבעבעים, מאותה סיבה כמו בתיבת החיפוש: בדפדפן הריפו ``Escape`` ברמת העמוד סוגר חלוניות. לחיצה מחוץ לתפריט ומחוץ ל-⋮ סוגרת אותו (``pointerdown`` על ``document``, בשלב הלכידה, כמו רשימת "+N"). .. _sticky-notes-font-token: גודל הטקסט: טוקן אחד, ולמה דווקא בפיקסלים ----------------------------------------- הצד של הנושא הזה בשרת: :ref:`sticky-notes-font-device-scope` ו-:ref:`sticky-notes-font-settings`. ``font-size: 14px`` היה מוצהר **פעמיים** — על תיבת העריכה ועל התצוגה שמחליפה אותה — ולידו הערה שהן "אותו ארגז בדיוק". שני מספרים שחייבים להישאר שווים ואיש לא אכף. הבורר החדש לא הוסיף שלישי אלא איחד את השניים ל-``--sticky-note-font``. **וגם ברירת המחדל של הטוקן מוצהרת פעם אחת — על ``:root`` ב-``sticky-notes.css``.** עד אוקטובר 2026 היא ישבה כ-fallback בתוך ה-``var()`` של שני הצרכנים, כלומר אותו מספר בשני מקומות, ושינוי של "רגיל" הוא שחשף את זה. מחלקות הגודל על ```` גוברות על ההצהרה הזו בספציפיות ולא בסדר הכללים: ``:root`` הוא 0-1-0 ו-``:root.sticky-font-*`` הוא 0-2-0 (MDN, ``:root``). הצרכנים קוראים ``var()`` בלי fallback, כי עותק שם היה נראה כמו ברירת המחדל בלי להיות היא, ובדיקה ב-``tests/board-handwriting.test.js`` מוודאת שיש הצהרה אחת בדיוק ושאין עותק. .. important:: **הערכים מוחלטים, וזו החלטה שכבר נכשלה כאן פעם.** ההערה על כתב היד בסוף ``sticky-notes.css`` מתעדת ניסיון קודם להגדיל גופן ב-``1.08em``: ``em`` ב-``font-size`` נפתר מול **ההורה** ולא מול הבסיס של האלמנט, ולכן הוא החליף 14px ב-``16px × 1.08 = 17.28px`` — 23% במקום 8%. המדידה הפעם מצאה משהו **חמור יותר** ממה שתועד שם. בריצת בקרה עם ``--sticky-note-font: 1.14em``, שני הצרכנים קיבלו ערכים **שונים**: התצוגה 18.24px ותיבת העריכה 14.82px. כלומר ``em`` אינו רק נותן מספר שגוי — הוא שובר את ההבטחה ששני הארגזים זהים, וזו בדיוק ההבטחה שהטוקן נועד לאכוף. **כל השאר בפתק נגזר מהבסיס ב-``em``, ולכן גדל איתו בחינם.** נמדד בכרומיום בספטמבר 2026, על הסולם שהיה אז, בשלושת המצבים: תשעת הרכיבים שנבדקו — תצוגה, תיבת עריכה, כותרת, קוד בשורה, בלוק קוד, תווית שפה, טבלה, גוף אלרט והזחת הקינון — יצאו ביחס **זהה בדיוק** לבסיס. אף רכיב לא נשאר מאחור, ולא נדרש אף כלל נוסף. הסולם של אוקטובר 2026 לא נמדד מחדש: הוא שינה רק את ערכי הטוקן, ואף כלל שנגזר ב-``em`` לא השתנה. .. note:: הזחת הקינון הייתה הרכיב שהכי קל היה לטעות לגביו: ``--sticky-li-indent: 1.1em`` מוגדר על ``.sticky-note``, שאין לו ``font-size`` משלו. אבל ``em`` בערך של תכונה מותאמת נפתר **במקום השימוש** ולא במקום ההגדרה, ולכן הוא נגזר מ-``.sticky-task-line`` וגדל עם הבסיס. נמדד, לא הונח. **המפה אומרת אילו גדלים קיימים; רשימה שנייה אומרת מי מציע מה.** ``STICKY_NOTE_FONT_SIZES`` ב-``_note_fonts_head.html`` מחזיקה את הטוקנים ואת שמות המחלקות, ומתוכה נגזרים הוולידציה של הערך השמור ושם המחלקה שמוחלת. לצידה ``STICKY_NOTE_FONT_MENUS`` מחזיקה את מה שכל תפריט מציע. כל עוד היה תפריט אחד, "אילו גדלים קיימים" ו"מה מוצע" היו אותה שאלה, והבדיקה השוותה את הבורר ישירות למפה. משהצטרף הבורר שבעמוד ההגדרות הן נפרדו, כי כל אחד מהשניים הציע גדלים אחרים, והמפה הייתה גדולה משניהם. מקור אחד לשתי השאלות היה מכריח כל תפריט להציע את מה ששייך לשני. **מאוקטובר 2026 שני התפריטים מציעים בדיוק אותו דבר, וההפרדה נשארה בכוונה.** אלה עדיין שתי שאלות, ותפריט שיחזור להציע גודל משלו לא יחייב לפצל מחדש. המחיר: מי שמשנה את מה שמוצע בשני המקומות משנה את שתי הרשימות. שום בדיקה אינה דורשת ששתיהן יהיו זהות — הזהות היא החלטת מוצר של היום, לא חוזה. .. warning:: **תפריט שגדל מרחיב גם את מה שהמשטח השני צריך לדחות.** הלוח מציב את הערך השמור על ה-``