diff --git a/.c8rc.json b/.c8rc.json index 35989568..d1c4c60e 100644 --- a/.c8rc.json +++ b/.c8rc.json @@ -1,3 +1,8 @@ { - "all": true + "all": true, + "exclude": [ + "eslint.config.mjs", + "**/fixtures", + "src/generators/legacy-html/assets" + ] } diff --git a/codecov.yml b/codecov.yml index 9c52ddfb..f20b1445 100644 --- a/codecov.yml +++ b/codecov.yml @@ -9,10 +9,6 @@ coverage: target: 80% project: default: - # TODO(@avivkeller): Once our coverage > 70%, - # increase this to 70%, and increase on increments - target: 60% - -ignore: - - 'eslint.config.mjs' - - '**/fixtures/' + # TODO(@avivkeller): Once our coverage > 80%, + # increase this to 80%, and increase on increments + target: 70% diff --git a/eslint.config.mjs b/eslint.config.mjs index a46a1697..d5bfa243 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -76,6 +76,6 @@ export default [ }, { files: ['src/generators/legacy-html/assets/*.js'], - languageOptions: { globals: { ...globals.browser } }, + languageOptions: { globals: { ...globals.browser }, ecmaVersion: 'latest' }, }, ]; diff --git a/src/generators/legacy-html-all/index.mjs b/src/generators/legacy-html-all/index.mjs index 407416e5..28ecf306 100644 --- a/src/generators/legacy-html-all/index.mjs +++ b/src/generators/legacy-html-all/index.mjs @@ -97,8 +97,6 @@ export default { // We minify the html result to reduce the file size and keep it "clean" const minified = await minify(generatedAllTemplate, { collapseWhitespace: true, - minifyJS: true, - minifyCSS: true, }); if (output) { diff --git a/src/generators/legacy-html/assets/api.js b/src/generators/legacy-html/assets/api.js index 611fa8c8..62ba7fa1 100644 --- a/src/generators/legacy-html/assets/api.js +++ b/src/generators/legacy-html/assets/api.js @@ -1,218 +1,204 @@ 'use strict'; -{ - function setupTheme() { - const storedTheme = localStorage.getItem('theme'); - const themeToggleButton = document.getElementById('theme-toggle-btn'); - - // Follow operating system theme preference - if (storedTheme === null && window.matchMedia) { - const mq = window.matchMedia('(prefers-color-scheme: dark)'); - - if ('onchange' in mq) { - function mqChangeListener(e) { - document.documentElement.classList.toggle('dark-mode', e.matches); - } - mq.addEventListener('change', mqChangeListener); - if (themeToggleButton) { - themeToggleButton.addEventListener( - 'click', - function () { - mq.removeEventListener('change', mqChangeListener); - }, - { once: true } - ); - } - } - } - - if (themeToggleButton) { - themeToggleButton.hidden = false; - themeToggleButton.addEventListener('click', function () { - localStorage.setItem( - 'theme', - document.documentElement.classList.toggle('dark-mode') - ? 'dark' - : 'light' - ); - }); - } +/** + * Initialize all UI features + */ +const initFeatures = () => { + // Add JavaScript support indicator + document.documentElement.classList.add('has-js'); + + setupTheme(); + setupPickers(); + setupStickyHeaders(); + setupAltDocsLink(); + setupFlavorToggles(); + setupCopyButton(); +}; + +// Initialize either on DOMContentLoaded or immediately if already loaded +document.addEventListener('DOMContentLoaded', initFeatures); +if (document.readyState !== 'loading') initFeatures(); + +/** + * Sets up theme toggling functionality + */ +const setupTheme = () => { + const storedTheme = localStorage.getItem('theme'); + const themeToggleButton = document.getElementById('theme-toggle-btn'); + const prefersDark = window.matchMedia?.('(prefers-color-scheme: dark)'); + + // Apply theme based on storage or system preference + if ( + storedTheme === 'dark' || + (storedTheme === null && prefersDark?.matches) + ) { + document.documentElement.classList.add('dark-mode'); } - function setupPickers() { - function closeAllPickers() { - for (const picker of pickers) { - picker.parentNode.classList.remove('expanded'); - picker.ariaExpanded = false; - } - - window.removeEventListener('click', closeAllPickers); - window.removeEventListener('keydown', onKeyDown); - } - - function onKeyDown(e) { - if (e.key === 'Escape') { - closeAllPickers(); - } - } - - const pickers = document.querySelectorAll('.picker-header > a'); - - for (const picker of pickers) { - const parentNode = picker.parentNode; - - picker.ariaExpanded = parentNode.classList.contains('expanded'); - picker.addEventListener('click', function (e) { - e.preventDefault(); - - /* - closeAllPickers as window event trigger already closed all the pickers, - if it already closed there is nothing else to do here - */ - if (picker.ariaExpanded === 'true') { - return; - } - - /* - In the next frame reopen the picker if needed and also setup events - to close pickers if needed. - */ - - requestAnimationFrame(function () { - picker.ariaExpanded = true; - parentNode.classList.add('expanded'); - window.addEventListener('click', closeAllPickers); - window.addEventListener('keydown', onKeyDown); - parentNode.querySelector('.picker a').focus(); - }); - }); - } - } - - function setupStickyHeaders() { - const header = document.querySelector('.header'); - let ignoreNextIntersection = false; - - new IntersectionObserver( - function (e) { - const currentStatus = header.classList.contains('is-pinned'); - const newStatus = e[0].intersectionRatio < 1; - - // Same status, do nothing - if (currentStatus === newStatus) { - return; - } else if (ignoreNextIntersection) { - ignoreNextIntersection = false; - return; - } - - /* - To avoid flickering, ignore the next changes event that is triggered - as the visible elements in the header change once we pin it. - - The timer is reset anyway after few milliseconds. - */ - ignoreNextIntersection = true; - setTimeout(function () { - ignoreNextIntersection = false; - }, 50); - - header.classList.toggle('is-pinned', newStatus); + if (!themeToggleButton) return; + + themeToggleButton.hidden = false; + + // Setup system preference change listener + if ( + storedTheme === null && + prefersDark && + 'addEventListener' in prefersDark + ) { + const mqListener = e => + document.documentElement.classList.toggle('dark-mode', e.matches); + prefersDark.addEventListener('change', mqListener); + + // Remove system preference listener on first manual toggle + themeToggleButton.addEventListener( + 'click', + () => { + prefersDark.removeEventListener('change', mqListener); }, - { threshold: [1] } - ).observe(header); + { once: true } + ); } - function setupAltDocsLink() { - const linkWrapper = document.getElementById('alt-docs'); - - function updateHashes() { - for (const link of linkWrapper.querySelectorAll('a')) { - link.hash = location.hash; - } - } - - addEventListener('hashchange', updateHashes); - updateHashes(); - } - - function setupFlavorToggles() { - const kFlavorPreference = 'customFlavor'; - const flavorSetting = localStorage.getItem(kFlavorPreference) === 'true'; - const flavorToggles = document.querySelectorAll('.js-flavor-toggle'); - - flavorToggles.forEach(toggleElement => { - toggleElement.checked = flavorSetting; - toggleElement.addEventListener('change', e => { - const checked = e.target.checked; - - if (checked) { - localStorage.setItem(kFlavorPreference, true); - } else { - localStorage.removeItem(kFlavorPreference); - } - - flavorToggles.forEach(el => { - el.checked = checked; - }); - }); + // Handle theme toggle clicks + themeToggleButton.addEventListener('click', () => { + const isDark = document.documentElement.classList.toggle('dark-mode'); + localStorage.setItem('theme', isDark ? 'dark' : 'light'); + }); +}; + +/** + * Sets up dropdown picker functionality + */ +const setupPickers = () => { + const pickers = document.querySelectorAll('.picker-header > a'); + if (!pickers.length) return; + + const closeAllPickers = () => { + pickers.forEach(picker => { + picker.parentNode.classList.remove('expanded'); + picker.ariaExpanded = false; }); - } - - function setupCopyButton() { - const buttons = document.querySelectorAll('.copy-button'); - buttons.forEach(button => { - button.addEventListener('click', el => { - const parentNode = el.target.parentNode; - const flavorToggle = parentNode.querySelector('.js-flavor-toggle'); + window.removeEventListener('click', closeAllPickers); + window.removeEventListener('keydown', handleEscKey); + }; - let code = ''; + const handleEscKey = e => { + if (e.key === 'Escape') closeAllPickers(); + }; - if (flavorToggle) { - if (flavorToggle.checked) { - code = parentNode.querySelector('.mjs').textContent; - } else { - code = parentNode.querySelector('.cjs').textContent; - } - } else { - code = parentNode.querySelector('code').textContent; - } + pickers.forEach(picker => { + const parentNode = picker.parentNode; + picker.ariaExpanded = parentNode.classList.contains('expanded'); - button.textContent = 'Copied'; - navigator.clipboard.writeText(code); + picker.addEventListener('click', e => { + e.preventDefault(); + if (picker.ariaExpanded === 'true') return; - setTimeout(() => { - button.textContent = 'Copy'; - }, 2500); + requestAnimationFrame(() => { + picker.ariaExpanded = true; + parentNode.classList.add('expanded'); + window.addEventListener('click', closeAllPickers); + window.addEventListener('keydown', handleEscKey); + parentNode.querySelector('.picker a').focus(); }); }); - } - - function bootstrap() { - // Check if we have JavaScript support. - document.documentElement.classList.add('has-js'); - - // Restore user mode preferences. - setupTheme(); - - // Handle pickers with click/taps rather than hovers. - setupPickers(); - - // Track when the header is in sticky position. - setupStickyHeaders(); + }); +}; + +/** + * Sets up sticky header behavior + */ +const setupStickyHeaders = () => { + const header = document.querySelector('.header'); + if (!header) return; + + let ignoreNextIntersection = false; + + new IntersectionObserver( + entries => { + const currentPinned = header.classList.contains('is-pinned'); + const shouldPin = entries[0].intersectionRatio < 1; + + if (currentPinned === shouldPin) return; + if (ignoreNextIntersection) { + ignoreNextIntersection = false; + return; + } - // Make link to other versions of the doc open to the same hash target (if it exists). - setupAltDocsLink(); + ignoreNextIntersection = true; + setTimeout(() => (ignoreNextIntersection = false), 50); + header.classList.toggle('is-pinned', shouldPin); + }, + { threshold: [1] } + ).observe(header); +}; + +/** + * Sets up alternative docs link with hash synchronization + */ +const setupAltDocsLink = () => { + const linkWrapper = document.getElementById('alt-docs'); + if (!linkWrapper) return; + + const updateHashes = () => { + linkWrapper + .querySelectorAll('a') + .forEach(link => (link.hash = location.hash)); + }; + + window.addEventListener('hashchange', updateHashes); + updateHashes(); +}; + +/** + * Sets up flavor toggle functionality + */ +const setupFlavorToggles = () => { + const toggles = document.querySelectorAll('.js-flavor-toggle'); + if (!toggles.length) return; + + const isCustomFlavorEnabled = localStorage.getItem('customFlavor') === 'true'; + + toggles.forEach(toggle => { + toggle.checked = isCustomFlavorEnabled; + + toggle.addEventListener('change', e => { + const checked = e.target.checked; + + if (checked) { + localStorage.setItem('customFlavor', 'true'); + } else { + localStorage.removeItem('customFlavor'); + } - setupFlavorToggles(); + toggles.forEach(el => (el.checked = checked)); + }); + }); +}; + +/** + * Sets up code copy button functionality + */ +const setupCopyButton = () => { + document.querySelectorAll('.copy-button').forEach(button => { + button.addEventListener('click', e => { + const parent = e.target.parentNode; + const flavorToggle = parent.querySelector('.js-flavor-toggle'); + + let code; + if (flavorToggle) { + code = parent.querySelector( + flavorToggle.checked ? '.mjs' : '.cjs' + ).textContent; + } else { + code = parent.querySelector('code').textContent; + } - setupCopyButton(); - } + navigator.clipboard.writeText(code); - if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', bootstrap, { once: true }); - } else { - bootstrap(); - } -} + button.textContent = 'Copied'; + setTimeout(() => (button.textContent = 'Copy'), 2500); + }); + }); +}; diff --git a/src/generators/legacy-html/assets/style.css b/src/generators/legacy-html/assets/style.css index f2fcb3a5..bf46623f 100644 --- a/src/generators/legacy-html/assets/style.css +++ b/src/generators/legacy-html/assets/style.css @@ -1139,3 +1139,13 @@ kbd { background-image: url('./js-flavor-esm.svg'); } } + +html.dark-mode .shiki, +html.dark-mode .shiki span { + color: var(--shiki-dark) !important; + background-color: var(--shiki-dark-bg) !important; + /* Optional, if you also want font styles */ + font-style: var(--shiki-dark-font-style) !important; + font-weight: var(--shiki-dark-font-weight) !important; + text-decoration: var(--shiki-dark-text-decoration) !important; +} diff --git a/src/generators/legacy-html/index.mjs b/src/generators/legacy-html/index.mjs index 94bb3fc5..38f8baf5 100644 --- a/src/generators/legacy-html/index.mjs +++ b/src/generators/legacy-html/index.mjs @@ -162,11 +162,7 @@ export default { if (output) { // We minify the html result to reduce the file size and keep it "clean" - const minified = await minify(result, { - collapseWhitespace: true, - minifyJS: true, - minifyCSS: true, - }); + const minified = await minify(result, { collapseWhitespace: true }); await writeFile(join(output, `${node.api}.html`), minified); } diff --git a/src/generators/legacy-html/template.html b/src/generators/legacy-html/template.html index 400fe835..210506c3 100644 --- a/src/generators/legacy-html/template.html +++ b/src/generators/legacy-html/template.html @@ -26,17 +26,6 @@ document.documentElement.classList.add('dark-mode'); } -
Skip to content