diff --git a/css/common.css b/css/common.css
index eb2c832..99a4a8a 100644
--- a/css/common.css
+++ b/css/common.css
@@ -11,6 +11,7 @@ Version: 0.1: added line-height 1.5 for readability and spacing between labels a
--p-sm: 0.5rem;
--p-md: 1rem;
--sm: 600px;
+ --max-content-width: 75rem;
}
body {
@@ -26,6 +27,19 @@ body {
color 0.5s;
}
+main {
+ max-width: var(--max-content-width);
+ margin: 0 auto;
+ width: 100%;
+ padding: var(--p-md);
+}
+
+main.full-width {
+ max-width: none;
+ margin: 0;
+ padding: 0 !important;
+}
+
footer {
bottom: 0;
width: 100%;
diff --git a/index.html b/index.html
index a39a460..d9d41dc 100644
--- a/index.html
+++ b/index.html
@@ -30,7 +30,7 @@
-->
-
+
diff --git a/js/components/code-viewer.js b/js/components/code-viewer.js
new file mode 100644
index 0000000..7a7f676
--- /dev/null
+++ b/js/components/code-viewer.js
@@ -0,0 +1,58 @@
+// File: js/components/code-viewer.js
+// Component for displaying code snippets in multiple languages with a selector
+
+export default (hostComponent) => {
+ const pres = Array.from(hostComponent.querySelectorAll('pre[data-lang]'));
+ if (pres.length === 0) return;
+
+ const selector = document.createElement('select');
+ pres.forEach((pre, index) => {
+ const { lang } = pre.dataset;
+ const code = pre.querySelector('code');
+ if (code) {
+ code.classList.add(`language-${lang}`);
+ }
+ const option = document.createElement('option');
+ option.value = lang;
+ option.textContent = lang;
+ selector.appendChild(option);
+ pre.style.display = index === 0 ? '' : 'none';
+ });
+
+ selector.addEventListener('change', (event) => {
+ const lang = event.target.value;
+ pres.forEach((pre) => {
+ pre.style.display = pre.dataset.lang === lang ? '' : 'none';
+ });
+ });
+
+ hostComponent.insertBefore(selector, hostComponent.firstChild);
+
+ const ensureHighlight = () =>
+ new Promise((resolve) => {
+ if (window.hljs) {
+ resolve();
+ return;
+ }
+ const link = document.createElement('link');
+ link.rel = 'stylesheet';
+ link.href =
+ 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/default.min.css';
+ document.head.appendChild(link);
+ const script = document.createElement('script');
+ script.src =
+ 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
+ script.onload = resolve;
+ document.head.appendChild(script);
+ });
+
+ ensureHighlight().then(() => {
+ pres.forEach((pre) => {
+ const code = pre.querySelector('code');
+ if (code && window.hljs?.highlightElement) {
+ window.hljs.highlightElement(code);
+ }
+ });
+ });
+};
+
diff --git a/js/components/code-viewer.test.js b/js/components/code-viewer.test.js
new file mode 100644
index 0000000..123ea4b
--- /dev/null
+++ b/js/components/code-viewer.test.js
@@ -0,0 +1,42 @@
+import { describe, it, expect, beforeEach, vi } from 'vitest';
+import codeViewer from './code-viewer.js';
+
+beforeEach(() => {
+ // Mock hljs to avoid loading external scripts during tests
+ vi.stubGlobal('hljs', {
+ highlightElement: vi.fn(),
+ });
+});
+
+describe('code-viewer component', () => {
+ it('renders selector and shows only first snippet', () => {
+ const host = document.createElement('div');
+ host.innerHTML = `
+ console.log('hi')
+ print('hi')
+ `;
+ codeViewer(host);
+ const select = host.querySelector('select');
+ expect(select).toBeTruthy();
+ const pres = host.querySelectorAll('pre[data-lang]');
+ expect(pres[0].style.display).toBe('');
+ expect(pres[1].style.display).toBe('none');
+ });
+
+ it('switches visible snippet on selector change', () => {
+ const host = document.createElement('div');
+ host.innerHTML = `
+ console.log('hi')
+ print('hi')
+ `;
+ codeViewer(host);
+ const select = host.querySelector('select');
+ select.value = 'python';
+ select.dispatchEvent(new Event('change'));
+ const jsPre = host.querySelector('pre[data-lang="js"]');
+ const pyPre = host.querySelector('pre[data-lang="python"]');
+ expect(jsPre.style.display).toBe('none');
+ expect(pyPre.style.display).toBe('');
+ });
+});
+
diff --git a/js/components/nav.js b/js/components/nav.js
index 3d537c0..278ca37 100644
--- a/js/components/nav.js
+++ b/js/components/nav.js
@@ -44,6 +44,9 @@ export default (hostComponent) => {
flex-direction: column;
gap: 0.4rem;
padding: 10px 10px;
+ max-width: var(--max-content-width);
+ margin: 0 auto;
+ width: 100%;
background-color: var(--nav-background-color);
z-index: 10;
a {
@@ -79,6 +82,12 @@ export default (hostComponent) => {
}
}
}
+ nav.overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ }
.burger-button {
position: absolute;
right: 0;
@@ -191,6 +200,10 @@ export default (hostComponent) => {
🧮
Web GPU tutorial
+
+ 💻
+ Developers
+