Files
Antigravity 96e7902f01
Some checks failed
CI / Lint, Unit Tests & Build (push) Failing after 1m22s
CI / Deploy production (on server) (push) Has been skipped
feat: publication IA (magazine/brief/essay) + fixes critique
Publication IA:
- 4 templates (magazine, brief, essay, simple) avec CSS riche
- Rewrite IA (article/exercises/tutorial/reference/mixed)
- Modération avec timeout 12s + fallback safe
- Quotas publish_enhance par tier (basic=2, pro=15, business=100)
- Détection contenu stale (hash)
- Migration DB publishedContent/publishedTemplate/publishedSourceHash

Fixes:
- cheerio v1.2: Element -> AnyNode (domhandler), decodeEntities cast
- _isShared ajouté au type Note (champ virtuel serveur)
- callout colors PDF export: extraction fonction pure testable
- admin/published: guard note.userId null
- Cmd+S fonctionne en mode dialog (pas seulement fullPage)

i18n:
- 23 clés publish* traduites dans les 15 locales
- Extension Web Clipper: 13 locales mise à jour

Tests:
- callout-colors.test.ts (6 tests)
- note-visible-in-view.test.ts (5 tests)
- entitlements.test.ts + byok-entitlements.test.ts: mock usageLog + unstubAllEnvs
- 199/199 tests passent

Tracker: user-stories.md sync avec sprint-status.yaml
2026-06-28 07:32:57 +00:00

212 lines
6.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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.
*/
;(function initMementoClipperContent() {
if (globalThis.__mementoClipperContent) return
globalThis.__mementoClipperContent = true
const HIGHLIGHT_ID = 'memento-clipper-highlight-root'
const BANNER_ID = 'memento-clipper-banner-root'
const STYLE_ID = 'memento-clipper-styles'
let pickMode = false
let debounceTimer = null
function getSelectionText() {
return window.getSelection()?.toString().trim() || ''
}
function getPageMeta() {
const dir =
document.documentElement.getAttribute('dir') ||
document.body?.getAttribute('dir') ||
''
const lang = (
document.documentElement.getAttribute('lang') ||
document.body?.getAttribute('lang') ||
''
).split('-')[0]
return {
text: getSelectionText(),
dir,
lang,
url: location.href,
title: document.title,
}
}
function broadcastSelection() {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
const payload = { type: 'SELECTION_CHANGED', ...getPageMeta() }
try {
chrome.runtime.sendMessage(payload).catch(() => {})
} catch {
/* ignore */
}
if (pickMode) paintHighlight()
}, 80)
}
function removeHighlight() {
document.getElementById(HIGHLIGHT_ID)?.remove()
}
function paintHighlight() {
removeHighlight()
const sel = window.getSelection()
if (!sel || sel.isCollapsed || !sel.rangeCount) return
let range
try {
range = sel.getRangeAt(0)
} catch {
return
}
const host = document.createElement('div')
host.id = HIGHLIGHT_ID
host.setAttribute('aria-hidden', 'true')
host.style.cssText =
'position:fixed;inset:0;pointer-events:none;z-index:2147483644;overflow:hidden;'
for (const rect of range.getClientRects()) {
if (rect.width < 2 || rect.height < 2) continue
const box = document.createElement('div')
box.style.cssText = [
'position:fixed',
`left:${rect.left - 2}px`,
`top:${rect.top - 1}px`,
`width:${rect.width + 4}px`,
`height:${rect.height + 2}px`,
'background:rgba(164,113,72,0.28)',
'border-radius:3px',
'box-shadow:0 0 0 1px rgba(164,113,72,0.35)',
'transition:opacity 0.15s ease',
].join(';')
host.appendChild(box)
}
if (host.childNodes.length) document.documentElement.appendChild(host)
}
function ensureStyles() {
if (document.getElementById(STYLE_ID)) return
const style = document.createElement('style')
style.id = STYLE_ID
style.textContent = `
html.memento-clipper-pick ::selection {
background: rgba(164, 113, 72, 0.45) !important;
color: inherit !important;
}
html.memento-clipper-pick {
scroll-behavior: auto;
}
`
document.documentElement.appendChild(style)
}
function removeBanner() {
document.getElementById(BANNER_ID)?.remove()
}
function ensureBanner() {
if (document.getElementById(BANNER_ID)) return
const bannerText =
(typeof chrome !== 'undefined' && chrome.i18n?.getMessage?.('bannerPickText')) ||
'Highlight the text to clip'
const host = document.createElement('div')
host.id = BANNER_ID
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;'
const shadow = host.attachShadow({ mode: 'open' })
shadow.innerHTML = `
<style>
.pill {
display: flex; align-items: center; gap: 10px;
padding: 10px 18px; border-radius: 999px;
background: #1c1c1c; color: #faf9f5;
box-shadow: 0 12px 32px rgba(0,0,0,0.22);
font-size: 12px; font-weight: 600;
letter-spacing: 0.02em;
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;
}
.dot {
width: 8px; height: 8px; border-radius: 50%;
background: #a47148; animation: pulse 1.2s ease infinite;
}
@keyframes slideIn {
from { opacity: 0; transform: translateY(-8px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes pulse {
0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity: 0.5; transform: scale(0.85); }
}
</style>
<div class="pill">
<span class="logo">M</span>
<span class="dot"></span>
<span>${bannerText.replace(/</g, '&lt;')}</span>
</div>
`
document.documentElement.appendChild(host)
}
function setPickMode(enabled) {
pickMode = !!enabled
ensureStyles()
if (pickMode) {
document.documentElement.classList.add('memento-clipper-pick')
ensureBanner()
paintHighlight()
} else {
document.documentElement.classList.remove('memento-clipper-pick')
removeBanner()
removeHighlight()
}
}
function onScrollOrResize() {
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 })
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message?.type === 'PING') {
sendResponse({ ok: true })
return true
}
if (message?.type === 'GET_CONTEXT') {
sendResponse({
html: document.documentElement.outerHTML,
...getPageMeta(),
})
return true
}
if (message?.type === 'SET_PICK_MODE') {
setPickMode(!!message.enabled)
sendResponse({ ok: true, pickMode })
return true
}
return false
})
broadcastSelection()
})()