Skip to content

תיקונים ושיפורים - #6

Merged
Y-PLONI merged 19 commits into
Y-PLONI:mainfrom
Nathaniel-260:fix/word-double-click-selection
Aug 28, 2026
Merged

תיקונים ושיפורים#6
Y-PLONI merged 19 commits into
Y-PLONI:mainfrom
Nathaniel-260:fix/word-double-click-selection

Conversation

@Nathaniel-260

@Nathaniel-260 Nathaniel-260 commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator
  • מסך טעינה שמאמץ את ערכת הנושא של אוצריא
  • תיקון הדבקה: תוכן נכנס במקום הסמן, לא בסוף המסמך
  • תיקון הבהוב בחיווי העיצוב ברצועה בזמן הקלדה
  • טולטיפ מעוצב במקום המלבן האפור של מערכת ההפעלה
  • gitignore מתעלם רק מהתיקייה node_modules, לא מקובץ באותו שם
  • שורת הלשוניות ברצועה נמוכה יותר, עם גופן וריפוד מותאמים

המנוע מתייחס לניקוד ולטעמים כאל מפרידי מילה, ולכן לחיצה כפולה על
„בְּרֵאשִׁ֖ית" בחרה רסיס של אות אחת. נמדד ב-Chrome אמיתי, גם על המנוע לבדו
וגם על ה-dist הארוז מ-`file://`; הטבלה נמצאת ב-docs/engine-gaps.md.

התיקון הוא שכבה חיצונית בלבד — אין נגיעה במנוע. אחרי הלחיצה קוראים את
הבחירה ב-`doc.selection.current()`, פותחים חלון טקסט של 90 תווים לכל צד
ב-`doc.ranges.resolve()`, מרחיבים את גבולות המילה על התווים שהמנוע חתך
(ניקוד, טעמים, גרש וגרשיים) ומחילים את התוצאה ב-`ui.selection.apply()`.
הכול דרך ה-API הציבורי של SuperDoc.

ספירת הלחיצות היא שלנו ולא של הדפדפן: באוצריא ה-WebView מוזן דרך
`SendMouseInput` של WebView2, שמקבל DOWN/UP/MOVE בלבד — בלי
`LEFT_BUTTON_DOUBLE_CLICK` — ולכן `clickCount` נגזר מהיוריסטיקה של
Chromium ואינו זהה בין הסביבות. אנחנו סופרים על `mousedown`/`click` לפי
ספי Windows (500ms, 5px), מתעלמים מגרירה, ודוחים זרע ארוך מ-90 תווים
שהוא בחירה קודמת שלא התחלפה.

שלוש לחיצות כבר עבדו במנוע, ובכל זאת נשלחות גם מאיתנו — כדי שהתנהגות
הבחירה לא תהיה תלויה בספירה של המארח.
הסעיף תיאר עד עכשיו תסמין בלבד. מדידה שמשווה באותה נקודה את
`elementFromPoint`, את `caretRangeFromPoint`, את `getSelection()` ואת
`doc.selection.current()` מראה שהקריסה מתרחשת בדיוק בצעד שבו הסמן חדל
להיות מעל תיבת שורה ונכנס לרצועה שבין השורה האחרונה לתחתית העמוד.

`caretRangeFromPoint` באותן קואורדינטות ממשיך להחזיר את המיקום המהודק
הנכון, ו-`getSelection()` ריק לאורך כל הגרירה — כלומר המנוע מחזיק את
המיקום בעצמו ואינו יורש את ההידוק של הדפדפן.

בקרה: פסקה קצרה שנוספה אחרי הארוכה מזיזה את הקריסה מטה יחד עם הטקסט. וזה
גם מסביר את „91" מהמדידה הראשונה — שורה בקובץ ההוא היא כ-90 תווים.
בנתיב `node_modules` ישב קישור סימבולי מנוטר בגיט (mode 120000) אל
`/Users/david/Documents/otzaria-others/otzaria-word-editor/node_modules` —
נתיב ממכונת macOS אחרת, שנכנס לקומיט 37de9f7. ב-checkout בווינדוס הוא
מתממש כקובץ רגיל בן 70 בתים ובו הנתיב עצמו.

npm יוצר בנתיב הזה **תיקייה**, ושני אלה אינם יכולים לדור באותו מקום:
`npm install` נכשל בכל clone חדש, ואיתו כל בנייה וכל שער.

`.gitignore` לא עצר את זה, וזה גם מה שמונע חזרה: `node_modules/` עם לוכסן
תופס תיקיות בלבד, ולכן ערך אחר באותו שם עבר דרכו בשקט. בלי הלוכסן הדפוס
תופס את שניהם.
מיכל העמודים של המנוע הוא flex column עם `gap: 24px`, כלומר **בין** עמודים
כבר יש מרווח — אבל מעל הראשון ומתחת לאחרון היה `0px`. נמדד ב-CDP על ה-dist
הארוז בארבעה אחוזי זום: בין העמודים 24/14/36/72 (100%/60%/150%/300%),
ובשני הקצוות אפס בכולם. על המסך ראש העמוד נושק לרצועה ותחתיתו לשורת המצב,
והמסמך נראה חתוך במקום מונח על משטח.

`padding-block: 24px` על אותו מיכל. 24 ולא ערך משלנו — זה בדיוק ה-`gap`
שהמנוע כותב, ולכן שלושת המרווחים יוצאים זהים וגדלים יחד עם הזום, כמו ב-Word.
`padding` על המיכל ולא `margin` על העמודים: המרווח האמצעי כבר מגיע מה-`gap`,
ושתי מערכות מרווח על אותה ערימה הן מרווח כפול באמצע. `padding-block` ולא
`padding` — הציר האופקי שייך לנוסחת המרכוז בזום, ו-`50%` שבה נפתר מול תיבת
התוכן של המיכל הזה.

`!important` מפני שהמנוע כותב `padding: 0px` inline על המיכל בכל ציור. זה
CSS ולא JavaScript, ולכן הכלל חל כבר בציור הראשון — בלי הבהוב ובלי „מי כתב
אחרון”, אותו שיקול שמתועד ב-styles/engine-chrome.css.

מה שנבדק לפני שזה נכתב: למנוע יש `PageGeometryHelper` שמחשב „מרחק מראש
המיכל אל ראש עמוד N”, ובמרחב הזה `getPageTop(0)` הוא אפס — כלומר ריפוד היה
יכול להזיז כל שכבה שנשענת על החישוב במקום על ה-DOM. נמדד: קליק על שורה
בעמוד השני מציב את הסמן בדיוק על ראש השורה, סטייה 0px, עם הריפוד ובלעדיו.

ההדפסה מאפסת את הריפוד (print.css). זה לא קישוט: מסמך של שלושה עמודים יצא
שלושה גיליונות עם האיפוס, וחמישה בבקרה בלעדיו — `@page { margin: 0 }` הופך
דחיפה של 24px לגלישה.

scripts/page-gutter-probe.mjs (`npm run check:gutter`, ומחובר ל-verify) מודד
שהמרווח מעל, בין ומתחת זהים בכל אחוז זום, ושהוא מאופס במדיית print. כלומר
היום שבו המנוע ישנה את ה-`gap` שלו ייפול שם, ולא אצל המשתמש.
הבאנדל הארוז הוא 16MB, וב-file:// הפריסה וההרצה שלו לוקחות כשתי שניות.
שתי תגיות הסקריפט ישבו ב-<head> בלי defer, ולכן ה-<body> לא נפרס באותו
זמן — מסך לבן בלי שום סימן שמשהו קורה. נמדד: צביעה ראשונה ב-2135ms.

התגיות מוזרקות עכשיו מטוען inline אחרי הצביעה הראשונה, ו-index.html
נושא מסך טעינה (HTML+CSS+JS) שמדווח שלבים ומאמץ את ערכת הנושא של אוצריא.
הפלטה זהה ל-tokens.css של התוסף.

התגלה תוך כדי: הקריאה הראשונה לערכת הנושא הייתה חד-פעמית מה-latch, אבל
plugin.boot מגיע אחרי שהמסך כבר צויר — כך שהוא תמיד פספס אותה, ופס
ההתקדמות נשאר כחול במקום לאמץ את הצבע האמיתי. עכשיו יש גם מאזין.

scripts/startup-probe.mjs (npm run check:startup) משגר boot באיחור
מכוון ומאמת גם את הצביעה וגם את אימוץ הצבע בדפדפן אמיתי.
נמדד מול המנוע האמיתי: doc.clipboard.insert בלי target מכניס את התוכן
אחרי הפסקה האחרונה גם כשיש בחירה חיה בתחילת המסמך, למרות שהחוזה מתאר
"the current model selection" — ההנחה הזו הייתה שגויה. עם target
מהבחירה החיה, ההדבקה נכנסת בדיוק בסמן. אומת מקצה לקצה: לחיצה על כפתור
״הדבק״ האמיתי, עם הרשאת לוח ובחירה בתחילת המסמך, מכניסה את התוכן
לפסקה הראשונה ולא בסוף.

זה גם ההסבר לבחירת פריט אחר מהיסטוריית Win+V שלא נדבק: הלוח הפנימי
היה נקרא בכל פעם שלוח המערכת חסום לקריאה, גם כשהוא רק חסום לקריאה
ולא נכתב אליו — ומחזיר תוכן ישן שהועתק בתוך התוסף עצמו במקום מה
שהמשתמש בפועל בחר. עכשיו הלוח הפנימי מסומן mirrored כשהתוכן גם הגיע
ללוח המערכת, וההדבקה נופלת אליו רק כשהוא העותק היחיד שקיים.
נמדד ב-Chrome אמיתי: כפתור "יישור לימין" הבהב 34 פעמים ב-40 שניות
הקלדה. המנוע מקפל "מעורב" ו"עוד לא נפתר" לאותו undefined בכל קריאה
אסינכרונית שמתאפסת עם כל תו, והרצועה הציגה זאת כ"אין עיצוב". הפתרון
מחזיק את הערך האחרון הידוע רק כשידוע ש-undefined אינו יכול להיות
האמת (סמן מכווץ, או קריאה שטרם התיישבה) — ולא כשבחירת טווח באמת
מעורבת, ששם undefined הוא התשובה הנכונה.

npm run check:readout מודד את זה בדפדפן אמיתי ונוסף ל-verify.
…דה, והסבר מתחתיה

כרטיס אחד לכל התוכנה (TooltipLayer.vue), שמאזין במסירה ומכבה את ה-title
המולד בזמן שהוא מוצג. פקד שכבר יש לו title — כולל כפתור מנוטרל, שאירועי
עכבר אינם מגיעים אליו — מקבל את העיצוב החדש בלי חיווט. RibbonButton
מוסיף שדה description חדש, ו-HomeTab מוזן בהסברים לכל כפתוריו.
@Nathaniel-260 Nathaniel-260 changed the title לחיצה כפולה בוחרת מילה שלמה גם בטקסט מנוקד, ושלוש לחיצות בוחרות קטע תיקונים ושיפורים Aug 27, 2026
Nathaniel-260 and others added 9 commits August 27, 2026 18:45
…פך אותו לעוגן

כיבוי הטולטיפ של מערכת ההפעלה מסיר את `title` מהעוגן הפעיל. בפקד שאין לו
`data-tip-title` משלו — הפס העליון, שורת המצב, לוח הצבעים, בוררי הגופן,
כלומר בדיוק „הכיסוי המלא ללא חיווט” שהקובץ מבטיח — התכונה הזאת היא מה
שהתאים אותו ל-TIP_ANCHOR_SELECTOR, ועם הסרתה הוא חדל להיות עוגן.

נמדד ב-Chrome על ה-dist הארוז, על `.word-app-badge` שבפס העליון:

  after hover:     {"tip":true,"text":"וורד לאוצריא"}   ← title הוסר
  after tiny move: {"tip":false}                        ← תזוזה של פיקסל

`anchorAt` מחזיר null בשני המסלולים (גם `elementFromPoint` אינו עוזר — הוא
מחזיר את אותו אלמנט חסר-התכונה), `scheduleHide` רץ, `restoreNative` מחזיר
את ה-title, וכעבור 400ms הכול חוזר חלילה.

התיקון אינו מוחק את התוכן אלא **מעביר** אותו ל-`data-tip-title` לאורך
ההשהיה, ומחזיר ביציאה. האלמנט נשאר עוגן, `readTip` מחזיר את אותו תוכן
בדיוק — הוא קורא את שתי התכונות באותה נפילה — ומערכת ההפעלה אינה מציירת
דבר. פקד שכבר מחווט אינו מושאל ואינו נדרס.

שני שומרים: tests/component/tooltip-layer.test.ts (נופל על הקוד שלפני
התיקון — נמדד) ותזוזה שנייה על פקד ללא חיווט ב-scripts/tooltip-probe.mjs.
הבאנדל הוא IIFE, ו-main.ts מרכיב את Vue במיקרו-טסק בתוך אותה הרצה — כלומר
התחנה 68 („מכין את סביבת העריכה…”) מדווחת **לפני** שאירוע ה-load של
app.js יורה את 55. מסך הטעינה בולע יעד נמוך מהנוכחי, ובולע יחד איתו גם את
הטקסט. נמדד ברצף שהוצג בפועל על ה-dist הארוז:

  מתחיל · טוען את מנוע המסמכים… · מכין את סביבת העריכה… · פותח את המסמך… · מוכן

התחנה שבאמצע פשוט אינה קיימת. מכאן שהטוען מדווח מעתה על השלב ש**מתחיל**
ולא על זה שנגמר: ההזרקה עצמה מדווחת 22 („טוען את מנוע המסמכים…”, כי אז
הבאנדל של המנוע מתחיל לרדת), וסיום ה-workers מדווח 55 („מרכיב את
הממשק…”, כי אז app.js מתחיל להיפרס). אחרי התיקון כל חמש התחנות מופיעות.

השער: הבדיקה הסטטית ב-tests/unit/splash.test.ts מודדת סדר מספרים ואינה
יכולה לראות סדר בזמן, ולכן check:startup רושם מעתה את רצף הטקסטים שהוצגו
בפועל ונכשל על כל תחנה שהטוען מצהיר עליה ולא הגיעה למסך. אומת: על הקוד
שלפני התיקון השער נופל בדיוק על „מרכיב את הממשק…”.
v2.0.0 כבר קיים כ-Release, ו-release.yml מדלג על יצירת Release ועל הפרסום
לחנות כשהתג קיים ("Check if version release already exists"). כלומר מיזוג
בלי העלאת גרסה מעדכן רק את ערוץ v2-dev, והחנות דוחה ממילא מספר גרסה חוזר.

מקור האמת הוא public/manifest.json, כמתועד ב-README; package.json אינו
נארז ואינו נבדק על ידי הוולידטור, ולכן הוא נשאר כפי שהוא.
…istInstalled

`src/types/otzaria_plugin.d.ts` הוא עותק מדויק של מה שאוצריא מפרסמת
ב-docs/plugin-sdk, ו-check:sdk נכשל מולו (הוא מדלג ב-CI, כי המקור אינו שם,
ולכן הסחיפה לא נעצרה בשום שער אוטומטי). הועתק מ-c8b8023d8 — אותו קומיט
בדיוק כמו origin/dev.

מה נכנס:

- `typography.uiFontFamily` — גופן הממשק, נפרד מגופן הקריאה, מ-0.9.97.
  `host/theme.ts` מחיל היום `fontSize` ו-`lineHeight` בלבד ומדלג במפורש
  על `fontFamily` (גופן הקריאה אינו של הממשק). `uiFontFamily` הוא דווקא כן
  של הממשק, ולכן ההחלה שלו היא שינוי התנהגות — נשאר לדיון נפרד ולא נגרר
  לכאן.
- `RawBookLink` / `GetRawLinksResult` ו-`library.getRawLinks`.
- `InstalledPlugin` ו-`plugin.listInstalled`, שהועלה מ-`@internal` לחוזה
  מתועד.
- ניסוח: „FluentUI icon” → „icon (see ICONS.md)”.

אף אחד מהם אינו נצרך היום בתוסף, ולכן זהו סנכרון בלבד: typecheck ו-1935
הבדיקות עוברים בלי שינוי נוסף, ו-check:sdk עובר מול המקור.
@Y-PLONI
Y-PLONI force-pushed the fix/word-double-click-selection branch from bbf6127 to 342cfc4 Compare August 28, 2026 12:21
@Y-PLONI
Y-PLONI merged commit 305fa99 into Y-PLONI:main Aug 28, 2026
1 check passed
Y-PLONI added a commit that referenced this pull request Aug 29, 2026
באג #6 בסקר הפקדים: לחיצה על "מברשת עיצוב" מפעילה את הפקודה copy-format
(הכפתור נדלק, cmd(copy-format).active עובר ל-true), אבל סימון היעד לא
מחיל דבר — ה-rPr של הטקסט ביעד נשאר ריק בכל מסלול.

הסיבה: ui.formatPainter הוא "DOM listener coordination" בלבד — הוא מחיל
רק כשמישהו קורא ל-notifyPointerUp/notifyKeyUp שלו. במנוע הרגיל זה
SuperToolbar עושה, אבל האפליקציה בונה את המנוע עם ui: false
(create-editor.ts), ולכן SuperToolbar אף פעם לא קם ואיש לא קורא לפעולות
האלה. חימוש בלי מי שיפעיל את ההחלה.

התיקון: מודול חדש src/engine/format-painter.ts שמחווט מאזינים על
ה-container של המנוע (כמו word-selection.ts), ומשתחרר ב-onDispose
(create-editor.ts).

שני עידונים מעבר לחיקוי הפשוט של SuperToolbar, שנמצאו ע"י קריאת המימוש
הפנימי של maybeApply/applyFormatPainter ואומתו בדפדפן חי:

1. לחיצה או ניווט מקלדת שרק ממקמים סמן (בלי גרירה/Shift) מפעילים במנוע
   מסלול "צביעת פסקה על הסמן" שמכבה את המצב החמוש גם כשאין שינוי לצייר —
   ולכן notifyPointerUp/notifyKeyUp נקראים רק על גרירה אמיתית או Shift.

2. הרחבת בחירה בכמה לחיצות מקש נפרדות (לא החזקה רציפה אחת) מייצרת כמה
   keyup נפרדים; קריאה מיידית על הראשון מחילה על תו אחד בלבד ומסיימת את
   המצב החמוש לפני שההרחבה הושלמה. נפתר בדבאונס קצר (250ms) על הקריאה
   בפועל.

בדיקות חדשות ב-tests/unit/format-painter.test.ts: רישום/שחרור המאזינים,
הסייג (קליק תמים לא מחיל, גרירה/Shift כן), והדבאונס.

אומת בדפדפן חי (scripts/qa/home-font-qa.mjs, שלב 7 מבודד): אחרי סימון
היעד, ה-rPr שלו מכיל בפועל <w:sz w:val="24"/><w:rFonts w:ascii="Arial"
w:hAnsi="Arial"/><w:b/> — בדיוק העיצוב שהועתק מהמקור. שאר השלבים (הדגשה,
נטוי, קו תחתון/חוצה, כתב עילי/תחתי, בוררי גופן/גודל, צבעים) נבדקו יחד
בריצה מלאה וממשיכים לעבוד; שלב 5 (דיאלוג גופן מתקדם) נתקע בתקלת קלט
ידועה ובלתי קשורה (CDP, מתועדת בסקריפט עצמו) שקדמה לשינוי הזה.
Nathaniel-260 added a commit to Nathaniel-260/otzaria-word-editor that referenced this pull request Sep 2, 2026
ב-`main` יש רשומה במצב `120000` בשם `node_modules`, שמצביעה לנתיב מוחלט
במכונה של מי שיצר אותה (`/Users/david/…`). זה הקישור הסמלי היחיד במאגר בכל
ההיסטוריה.

מה שזה עושה, נמדד: `git checkout` של ה-main הזה בעץ עבודה שיש בו התקנה
**מחליף את התיקייה `node_modules` בקובץ של 70 בייט** — כלומר מוחק את ההתקנה.
זה קרה כאן על עץ עבודה שרק דקות לפני כן הושלמה בו התקנה מלאה.

**ולמה בשקט:** `node_modules/` גורם לגיט להתעלם מהתיקייה, ולכן השומר „המחיקה
הזאת תדרוס קבצים לא-מנוטרים” אינו חל. `checkout` יוצא 0 ואינו מדפיס דבר. זה
הצירוף שקובע — הרשומה מצד אחד, ותבנית ההתעלמות מצד שני.

ב-Windows הרשומה מתממשת כקובץ טקסט שתוכנו הנתיב, מפני ש-`core.symlinks=false`
בקונפיג המערכתי של Git for Windows. זו הגדרה ולא מגבלת מערכת הפעלה: המכונה כן
יוצרת קישורים סמליים וגיט מנטר אותם כ-`120000`. ב-Unix הרשומה מתממשת כקישור
תלוי שאינו מצביע לשום מקום.

מה שזה **אינו** עושה: להפיל את ה-CI. `npm ci`/`npm install` מדווח
`npm warn reify Removing non-directory …` ומתאושש — ב-`@npmcli/arborist`
הבדיקה היא `lstat`, ולכן גם קישור תלוי ב-Linux אינו נראה כתיקייה ונמחק באותו
מסלול. הלוג של `Release` על `8c776e9` מדפיס את האזהרה הזאת בעצמו. לכן הכשל
נופל על מי שמשך את main, ולא על ה-CI.

## וזה כבר תוקן פעם אחת, ובוטל

הרשומה נוספה ב-37de9f7 ונכנסה ל-main במיזוג של Y-PLONI#5. אחר כך 8f0bbd8 הסיר אותה
**והוריד את הלוכסן**, ו-a710b84 — אותו ענף, אותו יום — **החזיר את הלוכסן**;
שניהם מוזגו ב-Y-PLONI#6, ונטו: הרשומה ירדה והמלכודת חזרה. שישה ימים אחר כך 2763bcb
הכניס את הרשומה בחזרה (`node_modules | 1 +` יושב ב-stat שלו), ומשם ל-main
ב-Y-PLONI#29.

שני דברים שראוי לדעת על הסבב ההוא. **ההצדקה שם הייתה שגויה:** 8f0bbd8 נקרא
„קובץ בשם node_modules חסם כל npm install”, וזה לא נכון — npm מתאושש, כפי
שנמדד למעלה. **ול-a710b84 אין הסבר:** כותרתו היא „gitignore מתעלם רק
מהתיקייה node_modules, לא מקובץ באותו שם”, כלומר תיאור הבעיה שהלוכסן יוצר —
בקומיט שמחזיר את הלוכסן.

ומאיפה זה חוזר: הקישור יושב בעץ העבודה של מי שיצר אותו, ו-`git add -A` שם
מעלה אותו שוב. הלוכסן הוא מה שמאפשר לזה לקרות בשקט.

## למה `.gitignore` לא עצר את זה

`node_modules/` — עם לוכסן — הוא **תבנית לתיקיות בלבד**. קישור סמלי אינו
תיקייה, ולכן הוא לא הותאם ונוסף כמו כל קובץ אחר. נמדד עם קישור סמלי אמיתי
בתוך clone של המאגר, בשני הכיוונים:

    עם לוכסן:   ln -s … node_modules ; git add -A  →  M node_modules (120000)
    בלי לוכסן:  ln -s … node_modules ; git add -A  →  שום דבר לא נבמה

הלוכסן הוסר משלוש השורות ולא רק מזו שנשברה: `dist/` ו-`tmp/` הן אותה תבנית
בדיוק, ולתקן אחת ולהשאיר שתיים זהות פירושו להשאיר את אותה מלכודת. ההרחבה
נבדקה ולא הונחה — הפרש הקבוצות „מותאם בישן ולא בחדש” יצא ריק בשלוש התבניות,
אין הרחבה לפי תת-מחרוזת (`distfile`, `my-dist`, `dist.js` אינם מותאמים לא
לפני ולא אחרי), וכל 432 הקבצים המנוטרים נבדקו מול ה-`.gitignore` החדש ואף
אחד אינו מותאם.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants