Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"content_scripts": [
{
"matches": ["https://*.skilljar.com/*"],
"js": ["src/lib/browser-polyfill.js", "src/lib/selectors.js", "src/lib/constants.js", "src/lib/translator.js", "src/lib/youtube-subtitles.js", "src/lib/protected-terms.js", "src/lib/gemini-block.js", "src/content/content.js", "src/content/code-comments.js", "src/content/header-controls.js", "src/content/text-selection.js", "src/content/sidebar-chat.js", "src/content/keyboard-shortcuts.js"],
"js": ["src/lib/browser-polyfill.js", "src/lib/selectors.js", "src/lib/constants.js", "src/lib/translator.js", "src/lib/youtube-subtitles.js", "src/lib/protected-terms.js", "src/lib/gemini-block.js", "src/content/content.js", "src/content/banners.js", "src/content/code-comments.js", "src/content/header-controls.js", "src/content/text-selection.js", "src/content/sidebar-chat.js", "src/content/keyboard-shortcuts.js"],
"css": ["src/content/content.css"],
"run_at": "document_idle"
}
Expand Down
152 changes: 152 additions & 0 deletions src/content/banners.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
/**
* SkillBridge — Banner UI
*
* Pure DOM banner registry split out of content.js. Loaded after content.js
* so it can read live language and helper state via `window._sb.t`.
* Functions attach back onto `window._sb` so call sites only change shape,
* not semantics.
*
* Term-preview stays in content.js because it needs translator state and
* FLASHCARD_COURSE_SLUGS_SORTED resolution.
*/

(function () {
const sb = window._sb;
if (!sb) {
console.warn('[SkillBridge] banners.js loaded before content.js — _sb namespace missing');
return;
}

// Guard: extension auto-update / dev reload re-runs content scripts.
// Without this marker we'd attach a second listener for each event each
// time, causing N banners per fire after N reloads (mirrors the
// history.pushState __sb_wrapped__ guard in content.js).
if (sb.__bannersLoaded) return;
sb.__bannersLoaded = true;

// Build a transient banner element and animate it in. Used for the
// five "small toast" cases below; translation progress is its own
// shape (two coordinated elements, dynamic content) and doesn't fit.
function showSimpleBanner({ id, className, role, ariaLive, labels, autoDismissMs }) {
if (document.getElementById(id)) return;
const banner = document.createElement('div');
banner.id = id;
banner.className = className;
banner.setAttribute('role', role);
if (ariaLive) banner.setAttribute('aria-live', ariaLive);
banner.textContent = sb.t(labels);
document.body.appendChild(banner);
requestAnimationFrame(() => banner.classList.add('visible'));
if (autoDismissMs) {
setTimeout(() => {
banner.classList.remove('visible');
setTimeout(() => banner.remove(), 300);
}, autoDismissMs);
}
}

function showOfflineBanner() {
showSimpleBanner({
id: 'si18n-offline-banner',
className: 'si18n-offline-banner',
role: 'status',
ariaLive: 'polite',
labels: OFFLINE_LABELS,
});
}

function hideOfflineBanner() {
const banner = document.getElementById('si18n-offline-banner');
if (!banner) return;
banner.classList.remove('visible');
setTimeout(() => banner.remove(), 300);
}

// No auto-dismiss: refresh is the only recovery, so keep the alert visible.
window.addEventListener('skillbridge:bridgeunavailable', () => {
showSimpleBanner({
id: 'si18n-bridge-banner',
className: 'si18n-offline-banner si18n-storage-warn',
role: 'alert',
ariaLive: 'assertive',
labels: BRIDGE_UNAVAILABLE_LABELS,
});
});

document.addEventListener('skillbridge:storagequota', () => {
showSimpleBanner({
id: 'si18n-storage-banner',
className: 'si18n-offline-banner si18n-storage-warn',
role: 'status',
ariaLive: 'polite',
labels: STORAGE_WARNING_LABELS,
autoDismissMs: 8000,
});
});

function showExamBanner() {
showSimpleBanner({
id: 'si18n-exam-banner',
className: 'si18n-exam-banner',
role: 'alert',
labels: EXAM_BANNER_LABELS,
});
}

function showTranslationProgress() {
let bar = document.getElementById('si18n-progress-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'si18n-progress-bar';
bar.innerHTML = '<div class="si18n-progress-fill" style="width: 15%"></div>';
document.body.appendChild(bar);
} else {
const fill = bar.querySelector('.si18n-progress-fill');
if (fill) fill.style.width = '15%';
}
let toast = document.getElementById('si18n-progress-toast');
const label = sb.t(PROGRESS_LABELS);
if (!toast) {
toast = document.createElement('div');
toast.id = 'si18n-progress-toast';
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.innerHTML = `<div class="si18n-progress-spinner"></div><span>${label}</span>`;
document.body.appendChild(toast);
} else {
const span = toast.querySelector('span');
if (span) span.textContent = label;
}
requestAnimationFrame(() => {
bar.classList.add('active');
toast.classList.add('active');
});
}

function updateTranslationProgress(pct) {
const fill = document.querySelector('#si18n-progress-bar .si18n-progress-fill');
if (fill) fill.style.width = `${Math.min(pct, 95)}%`;
}

function hideTranslationProgress() {
const fill = document.querySelector('#si18n-progress-bar .si18n-progress-fill');
if (fill) fill.style.width = '100%';
setTimeout(() => {
const bar = document.getElementById('si18n-progress-bar');
const toast = document.getElementById('si18n-progress-toast');
bar?.classList.remove('active');
toast?.classList.remove('active');
setTimeout(() => {
bar?.remove();
toast?.remove();
}, SKILLBRIDGE_DELAYS.PROGRESS_REMOVE);
}, SKILLBRIDGE_DELAYS.PROGRESS_HIDE);
}

sb.showOfflineBanner = showOfflineBanner;
sb.hideOfflineBanner = hideOfflineBanner;
sb.showExamBanner = showExamBanner;
sb.showTranslationProgress = showTranslationProgress;
sb.updateTranslationProgress = updateTranslationProgress;
sb.hideTranslationProgress = hideTranslationProgress;
})();
135 changes: 9 additions & 126 deletions src/content/content.js
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@

window.addEventListener('online', () => {
isOffline = false;
hideOfflineBanner();
window._sb.hideOfflineBanner?.();
// Retry deferred offline items first, then re-apply if needed
if (currentLang !== 'en' && translator && isReady) {
if (_offlinePendingItems.length > 0) {
Expand All @@ -119,59 +119,7 @@

window.addEventListener('offline', () => {
isOffline = true;
if (currentLang !== 'en') showOfflineBanner();
});

function showOfflineBanner() {
if (document.getElementById('si18n-offline-banner')) return;
const banner = document.createElement('div');
banner.id = 'si18n-offline-banner';
banner.className = 'si18n-offline-banner';
banner.setAttribute('role', 'status');
banner.setAttribute('aria-live', 'polite');
banner.textContent = t(OFFLINE_LABELS);
document.body.appendChild(banner);
requestAnimationFrame(() => banner.classList.add('visible'));
}

function hideOfflineBanner() {
const banner = document.getElementById('si18n-offline-banner');
if (banner) {
banner.classList.remove('visible');
setTimeout(() => banner.remove(), 300);
}
}

// Bridge unavailable — Puter.js script never confirmed BRIDGE_READY.
// Persistent banner (no auto-dismiss) so users know AI features are off
// until they refresh the page.
window.addEventListener('skillbridge:bridgeunavailable', () => {
if (document.getElementById('si18n-bridge-banner')) return;
const banner = document.createElement('div');
banner.id = 'si18n-bridge-banner';
banner.className = 'si18n-offline-banner si18n-storage-warn';
banner.setAttribute('role', 'alert');
banner.setAttribute('aria-live', 'assertive');
banner.textContent = t(BRIDGE_UNAVAILABLE_LABELS);
document.body.appendChild(banner);
requestAnimationFrame(() => banner.classList.add('visible'));
});

// Storage quota warning — auto-dismiss after 8 seconds
document.addEventListener('skillbridge:storagequota', () => {
if (document.getElementById('si18n-storage-banner')) return;
const banner = document.createElement('div');
banner.id = 'si18n-storage-banner';
banner.className = 'si18n-offline-banner si18n-storage-warn';
banner.setAttribute('role', 'status');
banner.setAttribute('aria-live', 'polite');
banner.textContent = t(STORAGE_WARNING_LABELS);
document.body.appendChild(banner);
requestAnimationFrame(() => banner.classList.add('visible'));
setTimeout(() => {
banner.classList.remove('visible');
setTimeout(() => banner.remove(), 300);
}, 8000);
if (currentLang !== 'en') window._sb.showOfflineBanner?.();
});

// Lookup helper: returns map entry for given lang, falling back to 'en'
Expand Down Expand Up @@ -200,17 +148,6 @@
return false;
}

function showExamBanner() {
if (document.getElementById('si18n-exam-banner')) return;
const banner = document.createElement('div');
banner.id = 'si18n-exam-banner';
banner.className = 'si18n-exam-banner';
banner.setAttribute('role', 'alert');
banner.textContent = t(EXAM_BANNER_LABELS);
document.body.appendChild(banner);
requestAnimationFrame(() => banner.classList.add('visible'));
}

window._sb = {
get currentLang() {
return currentLang;
Expand Down Expand Up @@ -264,60 +201,6 @@
translateCodeComments: null,
};

// ============================================================
// TRANSLATION PROGRESS INDICATOR
// ============================================================

function showTranslationProgress() {
let bar = document.getElementById('si18n-progress-bar');
if (!bar) {
bar = document.createElement('div');
bar.id = 'si18n-progress-bar';
bar.innerHTML = '<div class="si18n-progress-fill" style="width: 15%"></div>';
document.body.appendChild(bar);
} else {
const fill = bar.querySelector('.si18n-progress-fill');
if (fill) fill.style.width = '15%';
}
let toast = document.getElementById('si18n-progress-toast');
const label = t(PROGRESS_LABELS);
if (!toast) {
toast = document.createElement('div');
toast.id = 'si18n-progress-toast';
toast.setAttribute('role', 'status');
toast.setAttribute('aria-live', 'polite');
toast.innerHTML = `<div class="si18n-progress-spinner"></div><span>${label}</span>`;
document.body.appendChild(toast);
} else {
const span = toast.querySelector('span');
if (span) span.textContent = label;
}
requestAnimationFrame(() => {
bar.classList.add('active');
toast.classList.add('active');
});
}

function updateTranslationProgress(pct) {
const fill = document.querySelector('#si18n-progress-bar .si18n-progress-fill');
if (fill) fill.style.width = `${Math.min(pct, 95)}%`;
}

function hideTranslationProgress() {
const fill = document.querySelector('#si18n-progress-bar .si18n-progress-fill');
if (fill) fill.style.width = '100%';
setTimeout(() => {
const bar = document.getElementById('si18n-progress-bar');
const toast = document.getElementById('si18n-progress-toast');
bar?.classList.remove('active');
toast?.classList.remove('active');
setTimeout(() => {
bar?.remove();
toast?.remove();
}, SKILLBRIDGE_DELAYS.PROGRESS_REMOVE);
}, SKILLBRIDGE_DELAYS.PROGRESS_HIDE);
}

// ============================================================
// PER-LESSON TERM PREVIEW
// ============================================================
Expand Down Expand Up @@ -669,7 +552,7 @@
updateLangClass(targetLang);
// Re-detect exam page (DOM may have loaded since init)
if (!isExamPage) isExamPage = detectExamPage();
if (isExamPage && targetLang !== 'en') showExamBanner();
if (isExamPage && targetLang !== 'en') window._sb.showExamBanner?.();

const elements = getTranslatableElements();
if (elements.length === 0) return;
Expand All @@ -693,8 +576,8 @@

// Start GT for visible elements right away (skip redundant viewport check)
if (gtCandidates.length > 0 && targetLang !== 'en') {
showTranslationProgress();
updateTranslationProgress(
window._sb.showTranslationProgress?.();
window._sb.updateTranslationProgress?.(
Math.round((staticCount / (staticCount + gtCandidates.length + offscreen.length)) * 80),
);
queueForGoogleTranslate(gtCandidates, targetLang, true);
Expand Down Expand Up @@ -746,7 +629,7 @@
} else if (gtGeneration === myGeneration) {
// All offscreen elements processed — queue GT candidates
if (gtCandidates.length > 0 && targetLang !== 'en') {
if (prevGt === 0) showTranslationProgress();
if (prevGt === 0) window._sb.showTranslationProgress?.();
queueForGoogleTranslate(gtCandidates, targetLang);
}
}
Expand Down Expand Up @@ -888,15 +771,15 @@
}

processedItems += batch.length;
updateTranslationProgress(80 + Math.round((processedItems / totalItems) * 15));
window._sb.updateTranslationProgress?.(80 + Math.round((processedItems / totalItems) * 15));

if (gtTranslateQueue.length > 0) {
await new Promise((r) => setTimeout(r, SKILLBRIDGE_DELAYS.GT_BATCH));
}
}
} finally {
gtProcessing = false;
hideTranslationProgress();
window._sb.hideTranslationProgress?.();
pruneDetachedEntries();

// Term-preview only on full completion; on cancellation, the new
Expand Down Expand Up @@ -1004,7 +887,7 @@
currentLang = 'en';
window._protectedTerms.resetProtectedTerms();
updateLangClass('en');
hideTranslationProgress();
window._sb.hideTranslationProgress?.();
originalComments.forEach((html, el) => {
if (el && el.parentNode) el.innerHTML = html;
});
Expand Down
Loading