Commit ·
e58b76f
1
Parent(s): bdf0b17
feat(extension): update icons, fix logo in ui, and improve content script positioning/observers
Browse files- extension/_locales/ar/messages.json +4 -4
- extension/assets/icons/icon128.png +3 -0
- extension/assets/icons/icon16.png +3 -0
- extension/assets/icons/icon32.png +3 -0
- extension/assets/icons/icon48.png +3 -0
- extension/background.js +4 -4
- extension/content-inline.css +14 -6
- extension/content-inline.js +116 -29
- extension/manifest.json +2 -0
- extension/popup.html +2 -12
- extension/sidepanel/sidepanel.html +2 -12
- src/index.html +8 -3
- src/js/editor.js +2 -2
- tests/phase10/reports/phase10_results.json +0 -0
extension/_locales/ar/messages.json
CHANGED
|
@@ -8,19 +8,19 @@
|
|
| 8 |
"description": "Extension description shown in Chrome Web Store"
|
| 9 |
},
|
| 10 |
"contextMenuCorrect": {
|
| 11 |
-
"message": "تصحيح مع بيان",
|
| 12 |
"description": "Context menu item for correcting selected text"
|
| 13 |
},
|
| 14 |
"contextMenuSummarize": {
|
| 15 |
-
"message": "تلخيص مع بيان",
|
| 16 |
"description": "Context menu item for summarizing selected text"
|
| 17 |
},
|
| 18 |
"contextMenuDialect": {
|
| 19 |
-
"message": "تحويل اللهجة إلى الفصحى مع بيان",
|
| 20 |
"description": "Context menu: dialect→MSA"
|
| 21 |
},
|
| 22 |
"contextMenuQuran": {
|
| 23 |
-
"message": "تدقيق الآي
|
| 24 |
"description": "Context menu: Quran verify"
|
| 25 |
}
|
| 26 |
}
|
|
|
|
| 8 |
"description": "Extension description shown in Chrome Web Store"
|
| 9 |
},
|
| 10 |
"contextMenuCorrect": {
|
| 11 |
+
"message": "تصحيح النص مع بيان",
|
| 12 |
"description": "Context menu item for correcting selected text"
|
| 13 |
},
|
| 14 |
"contextMenuSummarize": {
|
| 15 |
+
"message": "تلخيص النص مع بيان",
|
| 16 |
"description": "Context menu item for summarizing selected text"
|
| 17 |
},
|
| 18 |
"contextMenuDialect": {
|
| 19 |
+
"message": "تحويل اللهجة إلى العربية الفصحى مع بيان",
|
| 20 |
"description": "Context menu: dialect→MSA"
|
| 21 |
},
|
| 22 |
"contextMenuQuran": {
|
| 23 |
+
"message": "تدقيق النص القرآني مع بيان",
|
| 24 |
"description": "Context menu: Quran verify"
|
| 25 |
}
|
| 26 |
}
|
extension/assets/icons/icon128.png
ADDED
|
|
Git LFS Details
|
extension/assets/icons/icon16.png
ADDED
|
|
Git LFS Details
|
extension/assets/icons/icon32.png
ADDED
|
|
Git LFS Details
|
extension/assets/icons/icon48.png
ADDED
|
|
Git LFS Details
|
extension/background.js
CHANGED
|
@@ -51,22 +51,22 @@ function getStorage() {
|
|
| 51 |
chrome.runtime.onInstalled.addListener(() => {
|
| 52 |
chrome.contextMenus.create({
|
| 53 |
id: 'bayan-correct',
|
| 54 |
-
title: chrome.i18n.getMessage('contextMenuCorrect') || 'تصحيح مع بيان',
|
| 55 |
contexts: ['selection'],
|
| 56 |
});
|
| 57 |
chrome.contextMenus.create({
|
| 58 |
id: 'bayan-summarize',
|
| 59 |
-
title: chrome.i18n.getMessage('contextMenuSummarize') || 'تلخيص مع بيان',
|
| 60 |
contexts: ['selection'],
|
| 61 |
});
|
| 62 |
chrome.contextMenus.create({
|
| 63 |
id: 'bayan-dialect',
|
| 64 |
-
title: chrome.i18n.getMessage('contextMenuDialect') || 'تحويل اللهجة إلى الفصحى مع بيان',
|
| 65 |
contexts: ['selection'],
|
| 66 |
});
|
| 67 |
chrome.contextMenus.create({
|
| 68 |
id: 'bayan-quran',
|
| 69 |
-
title: chrome.i18n.getMessage('contextMenuQuran') || 'تدقيق الآي
|
| 70 |
contexts: ['selection'],
|
| 71 |
});
|
| 72 |
});
|
|
|
|
| 51 |
chrome.runtime.onInstalled.addListener(() => {
|
| 52 |
chrome.contextMenus.create({
|
| 53 |
id: 'bayan-correct',
|
| 54 |
+
title: chrome.i18n.getMessage('contextMenuCorrect') || 'تصحيح النص مع بيان',
|
| 55 |
contexts: ['selection'],
|
| 56 |
});
|
| 57 |
chrome.contextMenus.create({
|
| 58 |
id: 'bayan-summarize',
|
| 59 |
+
title: chrome.i18n.getMessage('contextMenuSummarize') || 'تلخيص النص مع بيان',
|
| 60 |
contexts: ['selection'],
|
| 61 |
});
|
| 62 |
chrome.contextMenus.create({
|
| 63 |
id: 'bayan-dialect',
|
| 64 |
+
title: chrome.i18n.getMessage('contextMenuDialect') || 'تحويل اللهجة إلى العربية الفصحى مع بيان',
|
| 65 |
contexts: ['selection'],
|
| 66 |
});
|
| 67 |
chrome.contextMenus.create({
|
| 68 |
id: 'bayan-quran',
|
| 69 |
+
title: chrome.i18n.getMessage('contextMenuQuran') || 'تدقيق النص القرآني مع بيان',
|
| 70 |
contexts: ['selection'],
|
| 71 |
});
|
| 72 |
});
|
extension/content-inline.css
CHANGED
|
@@ -29,20 +29,20 @@
|
|
| 29 |
}
|
| 30 |
|
| 31 |
.bayan-il-punctuation {
|
| 32 |
-
background: rgba(
|
| 33 |
-
border-bottom: 2px solid #
|
| 34 |
border-radius: 2px !important;
|
| 35 |
cursor: pointer !important;
|
| 36 |
transition: background 150ms ease !important;
|
| 37 |
}
|
| 38 |
.bayan-il-punctuation:hover {
|
| 39 |
-
background: rgba(
|
| 40 |
}
|
| 41 |
|
| 42 |
/* ── Floating Action Button (FAB) ── */
|
| 43 |
|
| 44 |
.bayan-il-fab {
|
| 45 |
-
position:
|
| 46 |
z-index: 2147483646 !important;
|
| 47 |
width: 34px;
|
| 48 |
height: 34px;
|
|
@@ -176,8 +176,8 @@
|
|
| 176 |
color: #f59e0b !important;
|
| 177 |
}
|
| 178 |
.bayan-il-badge-punctuation {
|
| 179 |
-
background: rgba(
|
| 180 |
-
color: #
|
| 181 |
}
|
| 182 |
|
| 183 |
.bayan-il-tooltip-close {
|
|
@@ -278,3 +278,11 @@
|
|
| 278 |
color: rgba(120, 120, 130, 0.75) !important;
|
| 279 |
opacity: 0.9;
|
| 280 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
}
|
| 30 |
|
| 31 |
.bayan-il-punctuation {
|
| 32 |
+
background: rgba(107, 201, 138, 0.14) !important;
|
| 33 |
+
border-bottom: 2px solid #6BC98A !important;
|
| 34 |
border-radius: 2px !important;
|
| 35 |
cursor: pointer !important;
|
| 36 |
transition: background 150ms ease !important;
|
| 37 |
}
|
| 38 |
.bayan-il-punctuation:hover {
|
| 39 |
+
background: rgba(107, 201, 138, 0.28) !important;
|
| 40 |
}
|
| 41 |
|
| 42 |
/* ── Floating Action Button (FAB) ── */
|
| 43 |
|
| 44 |
.bayan-il-fab {
|
| 45 |
+
position: fixed;
|
| 46 |
z-index: 2147483646 !important;
|
| 47 |
width: 34px;
|
| 48 |
height: 34px;
|
|
|
|
| 176 |
color: #f59e0b !important;
|
| 177 |
}
|
| 178 |
.bayan-il-badge-punctuation {
|
| 179 |
+
background: rgba(107, 201, 138, 0.15) !important;
|
| 180 |
+
color: #6BC98A !important;
|
| 181 |
}
|
| 182 |
|
| 183 |
.bayan-il-tooltip-close {
|
|
|
|
| 278 |
color: rgba(120, 120, 130, 0.75) !important;
|
| 279 |
opacity: 0.9;
|
| 280 |
}
|
| 281 |
+
|
| 282 |
+
/* ── FAB dragging state ── */
|
| 283 |
+
|
| 284 |
+
.bayan-il-fab--dragging {
|
| 285 |
+
cursor: grabbing !important;
|
| 286 |
+
opacity: 0.85 !important;
|
| 287 |
+
transition: none !important;
|
| 288 |
+
}
|
extension/content-inline.js
CHANGED
|
@@ -76,7 +76,7 @@
|
|
| 76 |
const AC_MIN_CONTEXT = 3;
|
| 77 |
|
| 78 |
// ══════════════════════════════════════════════════════════
|
| 79 |
-
// 1. DETECTION
|
| 80 |
// ══════════════════════════════════════════════════════════
|
| 81 |
|
| 82 |
function isEditableField(el) {
|
|
@@ -87,8 +87,8 @@
|
|
| 87 |
const type = (el.type || '').toLowerCase();
|
| 88 |
return ['text', 'search', 'url', ''].includes(type);
|
| 89 |
}
|
| 90 |
-
// Contenteditable: allow on non-protected sites
|
| 91 |
if (el.isContentEditable && !IS_PROTECTED) return true;
|
|
|
|
| 92 |
return false;
|
| 93 |
}
|
| 94 |
|
|
@@ -106,30 +106,29 @@
|
|
| 106 |
function onFieldInput(e) {
|
| 107 |
if (paused || !activeField) return;
|
| 108 |
|
| 109 |
-
// Only a genuine user keystroke (a trusted `input` event) clears
|
| 110 |
-
// suppression and re-enables analysis. Our own write-back dispatches an
|
| 111 |
-
// untrusted event (isTrusted === false), and internal re-runs pass no
|
| 112 |
-
// event at all — neither should count as a manual edit (Change 3).
|
| 113 |
const isUserKeystroke = !!e && e.isTrusted === true;
|
| 114 |
if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
|
| 115 |
-
// A real edit invalidates any selection captured at right-click time.
|
| 116 |
if (isUserKeystroke) pendingSelection = null;
|
| 117 |
|
| 118 |
const text = getFieldText(activeField);
|
| 119 |
|
| 120 |
-
// Ghost-text autocomplete (textarea/input only) runs independently of
|
| 121 |
-
// analysis. Kept active even when suppressed (separate, opt-in feature).
|
| 122 |
scheduleGhost();
|
| 123 |
|
| 124 |
if (analysisSuppressed) {
|
| 125 |
-
// Model output (summarize/dialect/quran) was just written — skip the
|
| 126 |
-
// correction pipeline and show the clean badge, not the spinner.
|
| 127 |
clearHighlights();
|
| 128 |
updateBadge(0);
|
| 129 |
return;
|
| 130 |
}
|
| 131 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 132 |
if (!BayanController.hasArabic(text)) {
|
|
|
|
|
|
|
| 133 |
clearHighlights();
|
| 134 |
updateBadge(0);
|
| 135 |
return;
|
|
@@ -241,8 +240,8 @@
|
|
| 241 |
|
| 242 |
ghostEl = document.createElement('div');
|
| 243 |
ghostEl.className = 'bayan-il-ghost';
|
| 244 |
-
ghostEl.style.cssText = `position:
|
| 245 |
-
+ `left:${rect.left
|
| 246 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 247 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 248 |
+ `direction:${cs.direction};text-align:${cs.textAlign};overflow:hidden;`
|
|
@@ -329,14 +328,17 @@
|
|
| 329 |
clearHighlights();
|
| 330 |
if (!field || !suggs?.length) return;
|
| 331 |
|
|
|
|
|
|
|
|
|
|
| 332 |
try {
|
| 333 |
const rect = field.getBoundingClientRect();
|
| 334 |
const cs = window.getComputedStyle(field);
|
| 335 |
|
| 336 |
overlayContainer = document.createElement('div');
|
| 337 |
overlayContainer.className = 'bayan-il-overlay';
|
| 338 |
-
overlayContainer.style.cssText = `position:
|
| 339 |
-
+ `left:${rect.left
|
| 340 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 341 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 342 |
+ `direction:${cs.direction};text-align:${cs.textAlign};overflow:hidden;`
|
|
@@ -533,9 +535,32 @@
|
|
| 533 |
// UI: FAB + Badge
|
| 534 |
// ══════════════════════════════════════════════════════════
|
| 535 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 536 |
function createFloatingBtn() {
|
| 537 |
if (floatingBtn) return;
|
| 538 |
|
|
|
|
|
|
|
| 539 |
floatingBtn = document.createElement('div');
|
| 540 |
floatingBtn.className = 'bayan-il-fab';
|
| 541 |
safeHTML(floatingBtn, `
|
|
@@ -556,7 +581,47 @@
|
|
| 556 |
document.body.appendChild(floatingBtn);
|
| 557 |
badgeCount = floatingBtn.querySelector('.bayan-il-badge');
|
| 558 |
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
e.stopPropagation();
|
| 561 |
if (paused) {
|
| 562 |
paused = false;
|
|
@@ -566,11 +631,7 @@
|
|
| 566 |
}
|
| 567 |
if (suggestions.length > 0) {
|
| 568 |
try {
|
| 569 |
-
// FAB = whole-field intent. Drop any selection captured by an earlier
|
| 570 |
-
// right-click so write-back replaces the entire field, not a range.
|
| 571 |
pendingSelection = null;
|
| 572 |
-
// Tag the source field so write-back can re-find it after focus
|
| 573 |
-
// moves to the side panel (Change 1).
|
| 574 |
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
| 575 |
chrome.runtime.sendMessage({ type: 'OPEN_SIDEPANEL', text: lastAnalyzedText });
|
| 576 |
} catch {}
|
|
@@ -580,9 +641,16 @@
|
|
| 580 |
|
| 581 |
function positionFab(field) {
|
| 582 |
if (!floatingBtn || !field) return;
|
| 583 |
-
|
| 584 |
-
|
| 585 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 586 |
floatingBtn.classList.add('bayan-il-fab--visible');
|
| 587 |
}
|
| 588 |
|
|
@@ -933,13 +1001,13 @@
|
|
| 933 |
if (activeField && floatingBtn) positionFab(activeField);
|
| 934 |
if (overlayContainer && activeField) {
|
| 935 |
const rect = activeField.getBoundingClientRect();
|
| 936 |
-
overlayContainer.style.top = `${rect.top
|
| 937 |
-
overlayContainer.style.left = `${rect.left
|
| 938 |
}
|
| 939 |
if (ghostEl && activeField) {
|
| 940 |
const rect = activeField.getBoundingClientRect();
|
| 941 |
-
ghostEl.style.top = `${rect.top
|
| 942 |
-
ghostEl.style.left = `${rect.left
|
| 943 |
}
|
| 944 |
}, { passive: true });
|
| 945 |
|
|
@@ -953,9 +1021,28 @@
|
|
| 953 |
}
|
| 954 |
}, { passive: true });
|
| 955 |
|
| 956 |
-
// ── MutationObserver ──
|
| 957 |
-
observer = new MutationObserver(() => {
|
| 958 |
if (activeField && !document.body.contains(activeField)) detachField();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 959 |
});
|
| 960 |
observer.observe(document.body, { childList: true, subtree: true });
|
| 961 |
|
|
|
|
| 76 |
const AC_MIN_CONTEXT = 3;
|
| 77 |
|
| 78 |
// ══════════════════════════════════════════════════════════
|
| 79 |
+
// 1. DETECTION — universal editable field finder
|
| 80 |
// ══════════════════════════════════════════════════════════
|
| 81 |
|
| 82 |
function isEditableField(el) {
|
|
|
|
| 87 |
const type = (el.type || '').toLowerCase();
|
| 88 |
return ['text', 'search', 'url', ''].includes(type);
|
| 89 |
}
|
|
|
|
| 90 |
if (el.isContentEditable && !IS_PROTECTED) return true;
|
| 91 |
+
if (!IS_PROTECTED && (el.getAttribute('role') === 'textbox' || el.getAttribute('aria-multiline') === 'true')) return true;
|
| 92 |
return false;
|
| 93 |
}
|
| 94 |
|
|
|
|
| 106 |
function onFieldInput(e) {
|
| 107 |
if (paused || !activeField) return;
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
const isUserKeystroke = !!e && e.isTrusted === true;
|
| 110 |
if (isUserKeystroke && analysisSuppressed) analysisSuppressed = false;
|
|
|
|
| 111 |
if (isUserKeystroke) pendingSelection = null;
|
| 112 |
|
| 113 |
const text = getFieldText(activeField);
|
| 114 |
|
|
|
|
|
|
|
| 115 |
scheduleGhost();
|
| 116 |
|
| 117 |
if (analysisSuppressed) {
|
|
|
|
|
|
|
| 118 |
clearHighlights();
|
| 119 |
updateBadge(0);
|
| 120 |
return;
|
| 121 |
}
|
| 122 |
|
| 123 |
+
// Immediately clear stale highlights when text changes — the old overlay
|
| 124 |
+
// was rendered for a different string and its offsets are invalid now.
|
| 125 |
+
if (text !== lastAnalyzedText && overlayContainer) {
|
| 126 |
+
clearHighlights();
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
if (!BayanController.hasArabic(text)) {
|
| 130 |
+
suggestions = [];
|
| 131 |
+
lastAnalyzedText = '';
|
| 132 |
clearHighlights();
|
| 133 |
updateBadge(0);
|
| 134 |
return;
|
|
|
|
| 240 |
|
| 241 |
ghostEl = document.createElement('div');
|
| 242 |
ghostEl.className = 'bayan-il-ghost';
|
| 243 |
+
ghostEl.style.cssText = `position:fixed;top:${rect.top}px;`
|
| 244 |
+
+ `left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;`
|
| 245 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 246 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 247 |
+ `direction:${cs.direction};text-align:${cs.textAlign};overflow:hidden;`
|
|
|
|
| 328 |
clearHighlights();
|
| 329 |
if (!field || !suggs?.length) return;
|
| 330 |
|
| 331 |
+
const currentText = getFieldText(field);
|
| 332 |
+
if (currentText !== originalText) return;
|
| 333 |
+
|
| 334 |
try {
|
| 335 |
const rect = field.getBoundingClientRect();
|
| 336 |
const cs = window.getComputedStyle(field);
|
| 337 |
|
| 338 |
overlayContainer = document.createElement('div');
|
| 339 |
overlayContainer.className = 'bayan-il-overlay';
|
| 340 |
+
overlayContainer.style.cssText = `position:fixed;top:${rect.top}px;`
|
| 341 |
+
+ `left:${rect.left}px;width:${rect.width}px;height:${rect.height}px;`
|
| 342 |
+ `font-family:${cs.fontFamily};font-size:${cs.fontSize};line-height:${cs.lineHeight};`
|
| 343 |
+ `padding:${cs.padding};border:${cs.border};border-color:transparent;`
|
| 344 |
+ `direction:${cs.direction};text-align:${cs.textAlign};overflow:hidden;`
|
|
|
|
| 535 |
// UI: FAB + Badge
|
| 536 |
// ══════════════════════════════════════════════════════════
|
| 537 |
|
| 538 |
+
// ── Draggable FAB state ──
|
| 539 |
+
let fabDragging = false;
|
| 540 |
+
let fabDragStartX = 0;
|
| 541 |
+
let fabDragStartY = 0;
|
| 542 |
+
let fabDragOffsetX = 0;
|
| 543 |
+
let fabDragOffsetY = 0;
|
| 544 |
+
let fabCustomPos = null; // {x, y} when user has dragged to a custom position
|
| 545 |
+
|
| 546 |
+
function loadFabPosition() {
|
| 547 |
+
try {
|
| 548 |
+
const saved = localStorage.getItem('bayan_fab_pos');
|
| 549 |
+
if (saved) fabCustomPos = JSON.parse(saved);
|
| 550 |
+
} catch {}
|
| 551 |
+
}
|
| 552 |
+
|
| 553 |
+
function saveFabPosition() {
|
| 554 |
+
try {
|
| 555 |
+
if (fabCustomPos) localStorage.setItem('bayan_fab_pos', JSON.stringify(fabCustomPos));
|
| 556 |
+
} catch {}
|
| 557 |
+
}
|
| 558 |
+
|
| 559 |
function createFloatingBtn() {
|
| 560 |
if (floatingBtn) return;
|
| 561 |
|
| 562 |
+
loadFabPosition();
|
| 563 |
+
|
| 564 |
floatingBtn = document.createElement('div');
|
| 565 |
floatingBtn.className = 'bayan-il-fab';
|
| 566 |
safeHTML(floatingBtn, `
|
|
|
|
| 581 |
document.body.appendChild(floatingBtn);
|
| 582 |
badgeCount = floatingBtn.querySelector('.bayan-il-badge');
|
| 583 |
|
| 584 |
+
// ── Drag handling ──
|
| 585 |
+
floatingBtn.addEventListener('pointerdown', (e) => {
|
| 586 |
+
fabDragging = false;
|
| 587 |
+
fabDragStartX = e.clientX;
|
| 588 |
+
fabDragStartY = e.clientY;
|
| 589 |
+
const rect = floatingBtn.getBoundingClientRect();
|
| 590 |
+
fabDragOffsetX = e.clientX - rect.left;
|
| 591 |
+
fabDragOffsetY = e.clientY - rect.top;
|
| 592 |
+
floatingBtn.setPointerCapture(e.pointerId);
|
| 593 |
+
});
|
| 594 |
+
|
| 595 |
+
floatingBtn.addEventListener('pointermove', (e) => {
|
| 596 |
+
if (!floatingBtn.hasPointerCapture(e.pointerId)) return;
|
| 597 |
+
const dx = e.clientX - fabDragStartX;
|
| 598 |
+
const dy = e.clientY - fabDragStartY;
|
| 599 |
+
if (!fabDragging && (Math.abs(dx) > 4 || Math.abs(dy) > 4)) {
|
| 600 |
+
fabDragging = true;
|
| 601 |
+
floatingBtn.classList.add('bayan-il-fab--dragging');
|
| 602 |
+
}
|
| 603 |
+
if (fabDragging) {
|
| 604 |
+
const x = Math.max(0, Math.min(window.innerWidth - 40, e.clientX - fabDragOffsetX));
|
| 605 |
+
const y = Math.max(0, Math.min(window.innerHeight - 40, e.clientY - fabDragOffsetY));
|
| 606 |
+
floatingBtn.style.position = 'fixed';
|
| 607 |
+
floatingBtn.style.left = `${x}px`;
|
| 608 |
+
floatingBtn.style.top = `${y}px`;
|
| 609 |
+
}
|
| 610 |
+
});
|
| 611 |
+
|
| 612 |
+
floatingBtn.addEventListener('pointerup', (e) => {
|
| 613 |
+
floatingBtn.releasePointerCapture(e.pointerId);
|
| 614 |
+
if (fabDragging) {
|
| 615 |
+
fabDragging = false;
|
| 616 |
+
floatingBtn.classList.remove('bayan-il-fab--dragging');
|
| 617 |
+
fabCustomPos = {
|
| 618 |
+
x: parseInt(floatingBtn.style.left, 10),
|
| 619 |
+
y: parseInt(floatingBtn.style.top, 10),
|
| 620 |
+
};
|
| 621 |
+
saveFabPosition();
|
| 622 |
+
return;
|
| 623 |
+
}
|
| 624 |
+
// Click (no drag)
|
| 625 |
e.stopPropagation();
|
| 626 |
if (paused) {
|
| 627 |
paused = false;
|
|
|
|
| 631 |
}
|
| 632 |
if (suggestions.length > 0) {
|
| 633 |
try {
|
|
|
|
|
|
|
| 634 |
pendingSelection = null;
|
|
|
|
|
|
|
| 635 |
if (lastInteractedField) lastInteractedField.dataset.bayanSource = '1';
|
| 636 |
chrome.runtime.sendMessage({ type: 'OPEN_SIDEPANEL', text: lastAnalyzedText });
|
| 637 |
} catch {}
|
|
|
|
| 641 |
|
| 642 |
function positionFab(field) {
|
| 643 |
if (!floatingBtn || !field) return;
|
| 644 |
+
if (fabCustomPos) {
|
| 645 |
+
floatingBtn.style.position = 'fixed';
|
| 646 |
+
floatingBtn.style.left = `${fabCustomPos.x}px`;
|
| 647 |
+
floatingBtn.style.top = `${fabCustomPos.y}px`;
|
| 648 |
+
} else {
|
| 649 |
+
const rect = field.getBoundingClientRect();
|
| 650 |
+
floatingBtn.style.position = 'fixed';
|
| 651 |
+
floatingBtn.style.top = `${Math.max(4, rect.top + 6)}px`;
|
| 652 |
+
floatingBtn.style.left = `${Math.max(4, rect.left + 6)}px`;
|
| 653 |
+
}
|
| 654 |
floatingBtn.classList.add('bayan-il-fab--visible');
|
| 655 |
}
|
| 656 |
|
|
|
|
| 1001 |
if (activeField && floatingBtn) positionFab(activeField);
|
| 1002 |
if (overlayContainer && activeField) {
|
| 1003 |
const rect = activeField.getBoundingClientRect();
|
| 1004 |
+
overlayContainer.style.top = `${rect.top}px`;
|
| 1005 |
+
overlayContainer.style.left = `${rect.left}px`;
|
| 1006 |
}
|
| 1007 |
if (ghostEl && activeField) {
|
| 1008 |
const rect = activeField.getBoundingClientRect();
|
| 1009 |
+
ghostEl.style.top = `${rect.top}px`;
|
| 1010 |
+
ghostEl.style.left = `${rect.left}px`;
|
| 1011 |
}
|
| 1012 |
}, { passive: true });
|
| 1013 |
|
|
|
|
| 1021 |
}
|
| 1022 |
}, { passive: true });
|
| 1023 |
|
| 1024 |
+
// ── MutationObserver — detect removed fields AND newly added editable areas ──
|
| 1025 |
+
observer = new MutationObserver((mutations) => {
|
| 1026 |
if (activeField && !document.body.contains(activeField)) detachField();
|
| 1027 |
+
for (const m of mutations) {
|
| 1028 |
+
if (m.type !== 'childList') continue;
|
| 1029 |
+
for (const node of m.addedNodes) {
|
| 1030 |
+
if (node.nodeType !== 1) continue;
|
| 1031 |
+
if (isEditableField(node) && document.activeElement === node) {
|
| 1032 |
+
attachField(node);
|
| 1033 |
+
return;
|
| 1034 |
+
}
|
| 1035 |
+
const nested = node.querySelectorAll?.('textarea, input, [contenteditable="true"], [role="textbox"]');
|
| 1036 |
+
if (nested) {
|
| 1037 |
+
for (const el of nested) {
|
| 1038 |
+
if (isEditableField(el) && document.activeElement === el) {
|
| 1039 |
+
attachField(el);
|
| 1040 |
+
return;
|
| 1041 |
+
}
|
| 1042 |
+
}
|
| 1043 |
+
}
|
| 1044 |
+
}
|
| 1045 |
+
}
|
| 1046 |
});
|
| 1047 |
observer.observe(document.body, { childList: true, subtree: true });
|
| 1048 |
|
extension/manifest.json
CHANGED
|
@@ -8,6 +8,7 @@
|
|
| 8 |
|
| 9 |
"icons": {
|
| 10 |
"16": "assets/icons/icon16.png",
|
|
|
|
| 11 |
"48": "assets/icons/icon48.png",
|
| 12 |
"128": "assets/icons/icon128.png"
|
| 13 |
},
|
|
@@ -16,6 +17,7 @@
|
|
| 16 |
"default_popup": "popup.html",
|
| 17 |
"default_icon": {
|
| 18 |
"16": "assets/icons/icon16.png",
|
|
|
|
| 19 |
"48": "assets/icons/icon48.png"
|
| 20 |
},
|
| 21 |
"default_title": "__MSG_extName__"
|
|
|
|
| 8 |
|
| 9 |
"icons": {
|
| 10 |
"16": "assets/icons/icon16.png",
|
| 11 |
+
"32": "assets/icons/icon32.png",
|
| 12 |
"48": "assets/icons/icon48.png",
|
| 13 |
"128": "assets/icons/icon128.png"
|
| 14 |
},
|
|
|
|
| 17 |
"default_popup": "popup.html",
|
| 18 |
"default_icon": {
|
| 19 |
"16": "assets/icons/icon16.png",
|
| 20 |
+
"32": "assets/icons/icon32.png",
|
| 21 |
"48": "assets/icons/icon48.png"
|
| 22 |
},
|
| 23 |
"default_title": "__MSG_extName__"
|
extension/popup.html
CHANGED
|
@@ -12,17 +12,7 @@
|
|
| 12 |
<!-- ══════════════════════════════════════════════ -->
|
| 13 |
<header class="bayan-header">
|
| 14 |
<div class="bayan-header-brand">
|
| 15 |
-
<
|
| 16 |
-
<circle cx="50" cy="50" r="46" fill="url(#logoGrad)" />
|
| 17 |
-
<path d="M30 55 Q35 35, 50 30 Q65 35, 70 55 Q65 65, 50 70 Q35 65, 30 55Z" fill="rgba(255,255,255,0.9)" />
|
| 18 |
-
<circle cx="50" cy="42" r="4" fill="url(#logoGrad)" />
|
| 19 |
-
<defs>
|
| 20 |
-
<linearGradient id="logoGrad" x1="0" y1="0" x2="100" y2="100">
|
| 21 |
-
<stop offset="0%" stop-color="#6366f1"/>
|
| 22 |
-
<stop offset="100%" stop-color="#8b5cf6"/>
|
| 23 |
-
</linearGradient>
|
| 24 |
-
</defs>
|
| 25 |
-
</svg>
|
| 26 |
<span class="bayan-header-title">بيان</span>
|
| 27 |
</div>
|
| 28 |
<div class="bayan-header-status" id="status-indicator">
|
|
@@ -49,7 +39,7 @@
|
|
| 49 |
</button>
|
| 50 |
<button class="bayan-tab" role="tab" aria-selected="false" data-tab="quran" id="tab-quran">
|
| 51 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 52 |
-
قرآن
|
| 53 |
</button>
|
| 54 |
<button class="bayan-tab" role="tab" aria-selected="false" data-tab="autocomplete" id="tab-autocomplete">
|
| 55 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
|
|
| 12 |
<!-- ══════════════════════════════════════════════ -->
|
| 13 |
<header class="bayan-header">
|
| 14 |
<div class="bayan-header-brand">
|
| 15 |
+
<img src="assets/icons/icon48.png" alt="بيان" width="28" height="28" style="border-radius:6px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<span class="bayan-header-title">بيان</span>
|
| 17 |
</div>
|
| 18 |
<div class="bayan-header-status" id="status-indicator">
|
|
|
|
| 39 |
</button>
|
| 40 |
<button class="bayan-tab" role="tab" aria-selected="false" data-tab="quran" id="tab-quran">
|
| 41 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 42 |
+
تدقيق النص القرآني
|
| 43 |
</button>
|
| 44 |
<button class="bayan-tab" role="tab" aria-selected="false" data-tab="autocomplete" id="tab-autocomplete">
|
| 45 |
<svg width="16" height="16" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
extension/sidepanel/sidepanel.html
CHANGED
|
@@ -12,17 +12,7 @@
|
|
| 12 |
<!-- ══════════════════════════════════════════════ -->
|
| 13 |
<header class="sp-header">
|
| 14 |
<div class="sp-header-brand">
|
| 15 |
-
<
|
| 16 |
-
<circle cx="50" cy="50" r="46" fill="url(#spGrad)" />
|
| 17 |
-
<path d="M30 55 Q35 35, 50 30 Q65 35, 70 55 Q65 65, 50 70 Q35 65, 30 55Z" fill="rgba(255,255,255,0.9)" />
|
| 18 |
-
<circle cx="50" cy="42" r="4" fill="url(#spGrad)" />
|
| 19 |
-
<defs>
|
| 20 |
-
<linearGradient id="spGrad" x1="0" y1="0" x2="100" y2="100">
|
| 21 |
-
<stop offset="0%" stop-color="#6366f1"/>
|
| 22 |
-
<stop offset="100%" stop-color="#8b5cf6"/>
|
| 23 |
-
</linearGradient>
|
| 24 |
-
</defs>
|
| 25 |
-
</svg>
|
| 26 |
<span class="sp-header-title">بيان</span>
|
| 27 |
</div>
|
| 28 |
<div class="sp-header-status" id="status-indicator">
|
|
@@ -49,7 +39,7 @@
|
|
| 49 |
</button>
|
| 50 |
<button class="sp-tab" role="tab" aria-selected="false" data-tab="quran" id="tab-quran">
|
| 51 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 52 |
-
قرآن
|
| 53 |
</button>
|
| 54 |
<button class="sp-tab" role="tab" aria-selected="false" data-tab="autocomplete" id="tab-autocomplete">
|
| 55 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
|
|
|
| 12 |
<!-- ══════════════════════════════════════════════ -->
|
| 13 |
<header class="sp-header">
|
| 14 |
<div class="sp-header-brand">
|
| 15 |
+
<img src="../assets/icons/icon48.png" alt="بيان" width="24" height="24" style="border-radius:5px;">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
<span class="sp-header-title">بيان</span>
|
| 17 |
</div>
|
| 18 |
<div class="sp-header-status" id="status-indicator">
|
|
|
|
| 39 |
</button>
|
| 40 |
<button class="sp-tab" role="tab" aria-selected="false" data-tab="quran" id="tab-quran">
|
| 41 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253"/></svg>
|
| 42 |
+
تدقيق النص القرآني
|
| 43 |
</button>
|
| 44 |
<button class="sp-tab" role="tab" aria-selected="false" data-tab="autocomplete" id="tab-autocomplete">
|
| 45 |
<svg width="14" height="14" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/></svg>
|
src/index.html
CHANGED
|
@@ -7,7 +7,12 @@
|
|
| 7 |
<meta name="supabase-anon-key" content="">
|
| 8 |
<title>بيان - مساعد الكتابة العربية الذكي</title>
|
| 9 |
<meta name="description" content="بيان — منصة ذكاء اصطناعي متكاملة لتصحيح الإملاء والنحو والترقيم وتلخيص النصوص والإكمال التلقائي وتدقيق القرآن الكريم وتحويل اللهجات — مصمّمة خصيصًا للغة العربية. جرّبه مجاناً!">
|
| 10 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 12 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 13 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
@@ -59,7 +64,7 @@
|
|
| 59 |
<button id="mobile-menu-btn" class="mobile-menu-btn md:hidden" aria-label="فتح القائمة" aria-expanded="false" aria-controls="mobile-drawer">
|
| 60 |
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
| 61 |
</button>
|
| 62 |
-
<button onclick="showPage('home')" class="flex items-center gap-3" style="background:none;border:none;cursor:pointer;" aria-label="الرئيسية"><
|
| 63 |
</div>
|
| 64 |
<div class="hidden md:flex items-center gap-8">
|
| 65 |
<button onclick="showPage('home')" class="nav-link active text-base font-medium" data-page="home">الرئيسية</button>
|
|
@@ -1154,7 +1159,7 @@
|
|
| 1154 |
<div class="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12" dir="rtl">
|
| 1155 |
<div>
|
| 1156 |
<div class="flex items-center gap-3 mb-4">
|
| 1157 |
-
<
|
| 1158 |
</div>
|
| 1159 |
<p class="text-sm leading-relaxed" style="color: var(--text-secondary); line-height: 1.8;">منصة ذكاء اصطناعي للكتابة العربية — تدقيق إملائي ونحوي، ترقيم، تلخيص، إكمال تلقائي، تدقيق القرآن، وتحويل اللهجات.</p>
|
| 1160 |
</div>
|
|
|
|
| 7 |
<meta name="supabase-anon-key" content="">
|
| 8 |
<title>بيان - مساعد الكتابة العربية الذكي</title>
|
| 9 |
<meta name="description" content="بيان — منصة ذكاء اصطناعي متكاملة لتصحيح الإملاء والنحو والترقيم وتلخيص النصوص والإكمال التلقائي وتدقيق القرآن الكريم وتحويل اللهجات — مصمّمة خصيصًا للغة العربية. جرّبه مجاناً!">
|
| 10 |
+
<meta property="og:image" content="/LOGOS/3.png">
|
| 11 |
+
<meta property="og:title" content="بيان — مساعدك الذكي للكتابة بالعربية">
|
| 12 |
+
<meta property="og:description" content="تدقيق إملائي ونحوي وترقيم، تلخيص ذكي، إكمال تلقائي، تدقيق القرآن الكريم، وتحويل اللهجات">
|
| 13 |
+
<meta name="twitter:card" content="summary_large_image">
|
| 14 |
+
<meta name="twitter:image" content="/LOGOS/3.png">
|
| 15 |
+
<link rel="icon" type="image/png" href="/LOGOS/1.png">
|
| 16 |
<script src="https://cdn.tailwindcss.com"></script>
|
| 17 |
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 18 |
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
|
|
| 64 |
<button id="mobile-menu-btn" class="mobile-menu-btn md:hidden" aria-label="فتح القائمة" aria-expanded="false" aria-controls="mobile-drawer">
|
| 65 |
<svg width="22" height="22" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg>
|
| 66 |
</button>
|
| 67 |
+
<button onclick="showPage('home')" class="flex items-center gap-3" style="background:none;border:none;cursor:pointer;" aria-label="الرئيسية"><img src="/LOGOS/2.png" alt="بيان" width="28" height="28" style="flex-shrink:0;"><span id="nav-brand" class="text-xl md:text-2xl font-bold text-gradient">بيان</span></button>
|
| 68 |
</div>
|
| 69 |
<div class="hidden md:flex items-center gap-8">
|
| 70 |
<button onclick="showPage('home')" class="nav-link active text-base font-medium" data-page="home">الرئيسية</button>
|
|
|
|
| 1159 |
<div class="grid grid-cols-1 md:grid-cols-3 gap-12 mb-12" dir="rtl">
|
| 1160 |
<div>
|
| 1161 |
<div class="flex items-center gap-3 mb-4">
|
| 1162 |
+
<img src="/LOGOS/2.png" alt="بيان" width="28" height="28" style="flex-shrink:0;"><span id="footer-brand" class="text-2xl font-bold text-gradient">بيان</span>
|
| 1163 |
</div>
|
| 1164 |
<p class="text-sm leading-relaxed" style="color: var(--text-secondary); line-height: 1.8;">منصة ذكاء اصطناعي للكتابة العربية — تدقيق إملائي ونحوي، ترقيم، تلخيص، إكمال تلقائي، تدقيق القرآن، وتحويل اللهجات.</p>
|
| 1165 |
</div>
|
src/js/editor.js
CHANGED
|
@@ -82,13 +82,13 @@ function initEditor() {
|
|
| 82 |
// Debounced undo push — saves state after 500ms of no typing
|
| 83 |
let _undoInputTimer = null;
|
| 84 |
editor.addEventListener('input', () => {
|
| 85 |
-
// Pipeline Hardening v3.3: Skip re-analysis when programmatically applying suggestions
|
| 86 |
if (_isApplyingSuggestion) return;
|
| 87 |
_lastInputTime = Date.now();
|
|
|
|
|
|
|
| 88 |
updateEditorStats();
|
| 89 |
updatePlaceholder();
|
| 90 |
analyzeTextDelayed();
|
| 91 |
-
// Push undo state after typing pauses
|
| 92 |
clearTimeout(_undoInputTimer);
|
| 93 |
_undoInputTimer = setTimeout(pushUndoState, 500);
|
| 94 |
try {
|
|
|
|
| 82 |
// Debounced undo push — saves state after 500ms of no typing
|
| 83 |
let _undoInputTimer = null;
|
| 84 |
editor.addEventListener('input', () => {
|
|
|
|
| 85 |
if (_isApplyingSuggestion) return;
|
| 86 |
_lastInputTime = Date.now();
|
| 87 |
+
clearOverlays(editor);
|
| 88 |
+
window.currentSuggestions = [];
|
| 89 |
updateEditorStats();
|
| 90 |
updatePlaceholder();
|
| 91 |
analyzeTextDelayed();
|
|
|
|
| 92 |
clearTimeout(_undoInputTimer);
|
| 93 |
_undoInputTimer = setTimeout(pushUndoState, 500);
|
| 94 |
try {
|
tests/phase10/reports/phase10_results.json
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|