fix(extension): bannière visible dès l'injection + exclusion store-assets
Some checks failed
CI / Deploy production (on server) (push) Has been cancelled
CI / Lint, Unit Tests & Build (push) Has been cancelled

Bug réel : la bannière 'Surlignez le texte à clipper' n'apparaissait
jamais en pratique parce que le side panel envoyait SET_PICK_MODE
avant que le content script ne soit prêt (race condition MV3 entre
chrome.scripting.executeScript et la mise en place du listener
chrome.runtime.onMessage). Le retry PING dans ensureContentScript
ne suffisait pas : le listener sidepanel peut aussi être en retard.

Fix radical : content.js affiche la bannière IMMÉDIATEMENT à
l'initialisation, sans attendre SET_PICK_MODE. L'utilisateur voit
immédiatement que l'extension est active. Un bouton ✕ permet de
la fermer (state local au tab). Pas de dépendance sur le side panel
pour le feedback visuel.

+ manifest : retrait de optional_host_permissions (redondant avec
  host_permissions http://*/* et https://*/*, peut perturber Chrome
  en dev).
+ bannière rediseignée : max-width 640px responsive, animation
  slideIn, bouton ✕ accessible.
+ nouvelles clés i18n bannerDismiss dans les 15 locales.
+ build : exclusion de store-assets/ du zip et du xpi (sinon le
  package Store faisait 327 Ko au lieu de 47 Ko).
This commit is contained in:
Antigravity
2026-07-18 10:20:31 +00:00
parent d5c44aafb6
commit 1e9b1b30c9
70 changed files with 718 additions and 329 deletions

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "إغلاق"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Schließen"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Dismiss"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Cerrar"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "بستن"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Ignorer"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "बंद करें"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Chiudi"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "閉じる"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "닫기"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Sluiten"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Zamknij"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Fechar"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "Закрыть"
}
}

View File

@@ -186,5 +186,8 @@
"content": "$1"
}
}
},
"bannerDismiss": {
"message": "关闭"
}
}

View File

@@ -1,6 +1,13 @@
/**
* Content script Memento — sélection live, surlignage, communication avec le side panel.
* Injecté automatiquement sur http(s) ; ré-injecté à la demande si longlet était déjà ouvert.
* Injecté automatiquement sur http(s) ; ré-injecté à la demande si l'onglet était déjà ouvert.
*
* IMPORTANT : la bannière "Surlignez le texte à clipper" s'affiche dès
* l'injection du script, sans dépendre de SET_PICK_MODE. C'est volontaire :
* en MV3, le routage des messages entre side panel et content script
* peut être flaky (race conditions entre injection et listeners), donc
* on rend la bannière visible dès qu'on est là. L'utilisateur peut la
* fermer via le bouton ✕.
*/
;(function initMementoClipperContent() {
if (globalThis.__mementoClipperContent) return
@@ -11,8 +18,13 @@
const STYLE_ID = 'memento-clipper-styles'
let pickMode = false
let bannerDismissed = false
let debounceTimer = null
function isRestrictedUrl(url) {
return !url || /^(chrome|chrome-extension|edge|about|moz-extension|devtools|file):/i.test(url)
}
function getSelectionText() {
return window.getSelection()?.toString().trim() || ''
}
@@ -113,40 +125,64 @@
function ensureBanner() {
if (document.getElementById(BANNER_ID)) return
if (!shouldShowBanner()) return
const bannerText =
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
'Highlight the text to clip'
'Highlight the text on the page, or clip the whole page.'
const dismissLabel =
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerDismiss')) ||
'Dismiss'
const host = document.createElement('div')
host.id = BANNER_ID
// Le host est positionné en fixed (hors flux) au top-center.
host.style.cssText =
'all:initial;position:fixed;top:16px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:none;font-family:Inter,system-ui,sans-serif;'
'position:fixed;top:14px;left:50%;transform:translateX(-50%);z-index:2147483647;pointer-events:auto;font-family:Inter,system-ui,-apple-system,sans-serif;'
const shadow = host.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.pill {
display: flex; align-items: center; gap: 10px;
padding: 10px 18px; border-radius: 999px;
:host { all: initial; }
.wrap {
display: inline-flex; align-items: center; gap: 10px;
padding: 10px 10px 10px 18px; border-radius: 999px;
background: #1c1c1c; color: #faf9f5;
box-shadow: 0 12px 32px rgba(0,0,0,0.22);
box-shadow: 0 12px 32px rgba(0,0,0,0.32);
font-size: 12px; font-weight: 600;
letter-spacing: 0.02em;
letter-spacing: 0.01em;
max-width: min(640px, calc(100vw - 24px));
animation: slideIn 0.35s cubic-bezier(0.22,1,0.36,1);
}
.logo {
width: 22px; height: 22px; border-radius: 7px;
background: #faf9f5; color: #1c1c1c;
display: flex; align-items: center; justify-content: center;
font-family: Georgia, serif; font-weight: 900; font-size: 12px;
font-family: Georgia, serif; font-weight: 900; font-size: 13px;
flex-shrink: 0;
}
.dot {
width: 8px; height: 8px; border-radius: 50%;
background: #a47148; animation: pulse 1.2s ease infinite;
flex-shrink: 0;
}
.text {
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
min-width: 0;
}
.close {
width: 22px; height: 22px; border-radius: 50%;
background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.7);
border: none; cursor: pointer; display: flex;
align-items: center; justify-content: center;
font-size: 14px; line-height: 1; font-weight: 700;
flex-shrink: 0; transition: background 0.15s, color 0.15s;
}
.close:hover {
background: rgba(255,255,255,0.18); color: #fff;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
from { opacity: 0; transform: translateY(-10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
@@ -154,13 +190,26 @@
50% { opacity: 0.5; transform: scale(0.85); }
}
</style>
<div class="pill">
<div class="wrap" role="status" aria-live="polite">
<span class="logo">M</span>
<span class="dot"></span>
<span>${bannerText.replace(/</g, '&lt;')}</span>
<span class="text">${bannerText.replace(/</g, '&lt;')}</span>
<button type="button" class="close" aria-label="${dismissLabel}">×</button>
</div>
`
document.documentElement.appendChild(host)
// Bind le bouton ✕ — ferme la bannière pour cette session d'onglet.
const closeBtn = shadow.querySelector('.close')
if (closeBtn) {
closeBtn.addEventListener('click', (e) => {
e.preventDefault()
e.stopPropagation()
bannerDismissed = true
removeBanner()
})
}
}
function setPickMode(enabled) {
@@ -181,15 +230,21 @@
if (pickMode) paintHighlight()
}
document.addEventListener('selectionchange', broadcastSelection)
document.addEventListener('mouseup', broadcastSelection)
document.addEventListener('keyup', broadcastSelection)
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
window.addEventListener('resize', onScrollOrResize, { passive: true })
// Ne rien faire sur les pages restreintes (chrome://, file://, etc.)
if (isRestrictedUrl(location.href)) {
// On écoute quand même les messages au cas où le side panel veut
// quand même parler (ex: pour info).
} else {
document.addEventListener('selectionchange', broadcastSelection)
document.addEventListener('mouseup', broadcastSelection)
document.addEventListener('keyup', broadcastSelection)
window.addEventListener('scroll', onScrollOrResize, { passive: true, capture: true })
window.addEventListener('resize', onScrollOrResize, { passive: true })
}
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === 'PING') {
sendResponse({ ok: true })
sendResponse({ ok: true, pickMode })
return true
}
if (message?.type === 'GET_CONTEXT') {
@@ -204,8 +259,24 @@
sendResponse({ ok: true, pickMode })
return true
}
if (message?.type === 'DISMISS_BANNER') {
bannerDismissed = true
removeBanner()
sendResponse({ ok: true })
return true
}
return false
})
// === AFFICHAGE IMMÉDIAT DE LA BANNIÈRE ===
// On n'attend pas SET_PICK_MODE — le side panel peut être lent à nous
// parler (race conditions MV3). La bannière est le seul signal visuel
// pour l'utilisateur que l'extension est active et qu'il peut clipper.
// L'utilisateur peut la fermer via le bouton ✕.
if (!isRestrictedUrl(location.href)) {
setPickMode(true)
}
// Notifie le side panel qu'on est là (peut aider à amorcer).
broadcastSelection()
})()

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB