diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 79f7c3ec..e584b0b1 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -983,8 +983,8 @@ { "name": "modern-web-guidance", "source": { - "source": "url", - "url": "https://github.com/GoogleChrome/modern-web-guidance.git" + "source": "local", + "path": "./plugins/modern-web-guidance" }, "policy": { "installation": "AVAILABLE", diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 4cc58d51..b7f1c670 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1369,10 +1369,7 @@ "category": "development", "keywords": ["web", "best-practices", "chrome", "baseline", "css", "performance"], "tags": ["skill", "web"], - "source": { - "source": "github", - "repo": "GoogleChrome/modern-web-guidance" - }, + "source": "./plugins/modern-web-guidance", "homepage": "https://github.com/GoogleChrome/modern-web-guidance" }, { diff --git a/.cursor-plugin/marketplace.json b/.cursor-plugin/marketplace.json index e4ef0ac8..a6a0239f 100644 --- a/.cursor-plugin/marketplace.json +++ b/.cursor-plugin/marketplace.json @@ -324,6 +324,11 @@ "source": "./plugins/genkit", "description": "Official Genkit skills - build AI-powered applications with Genkit in JavaScript/TypeScript, Go, Dart/Flutter, and Python: flows, generation, prompts, tool calling, agents, and model providers" }, + { + "name": "modern-web-guidance", + "source": "./plugins/modern-web-guidance", + "description": "Keep your coding agent up to date with the latest web best practices from the Google Chrome team" + }, { "name": "java-development", "source": "./plugins/java-development", diff --git a/.release-please-manifest.json b/.release-please-manifest.json index ba8b9e65..d1e0441a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -20,6 +20,7 @@ "plugins/vue": "1.3.3", "plugins/vueuse": "1.3.2", "plugins/web-design": "1.4.3", + "plugins/modern-web-guidance": "0.0.196", "plugins/mastra": "1.7.4", "plugins/nuxt-ui": "1.6.5", "plugins/firebase": "1.1.3", diff --git a/README.md b/README.md index d23cf32c..ccd2cb4e 100644 --- a/README.md +++ b/README.md @@ -492,6 +492,12 @@ End-to-end typesafe APIs with oRPC v2 — the core `os` builder, routers, middle **Install:** `/plugin install orpc@pleaseai` | **Source:** [plugins/orpc](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/orpc) +#### Modern Web Guidance + +Keep your coding agent up to date with the latest web best practices from the Google Chrome team — modern HTML/CSS/JS patterns (`modern-web-guidance`) and Manifest V3 Chrome extensions (`chrome-extensions`). Vendored from [GoogleChrome/modern-web-guidance](https://github.com/GoogleChrome/modern-web-guidance) (Apache-2.0) and refreshed weekly by `update-skills.yml`. + +**Install:** `/plugin install modern-web-guidance@pleaseai` | **Source:** [plugins/modern-web-guidance](https://github.com/pleaseai/claude-code-plugins/tree/main/plugins/modern-web-guidance) + #### evlog Wide-event logging for TypeScript with [evlog](https://www.evlog.dev): review logging patterns and guide evlog adoption (`review-logging-patterns`), design tamper-aware audit trails (`build-audit-logs`), and debug from `.evlog/logs/` output (`analyze-logs`). Bundles the read-only evlog docs MCP server (`https://www.evlog.dev/mcp`) for current docs pages and log-cost estimates. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/SKILL.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/SKILL.md new file mode 100644 index 00000000..91a915cd --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/SKILL.md @@ -0,0 +1,530 @@ +--- +name: chrome-extensions +description: > + Build and publish Chrome Extensions using Manifest V3 best practices. Use this skill + whenever the user asks to create, modify, debug, or understand Chrome browser extensions, + add-ons, or anything involving the Chrome Extensions API. Trigger on mentions of: 'Chrome + extension', 'browser extension', 'manifest.json', 'content script', 'service worker' (in + browser context), 'popup' (in browser extension context), 'side panel', 'chrome.* API', + 'declarativeNetRequest', 'omnibox', 'context menu' (in extension context), 'userScripts', + 'user script', 'script manager', or any request to build functionality that integrates with + the Chrome browser UI. Also trigger for publishing to the Chrome Web Store: 'publish + extension', preparing an extension for publishing, responding to a review rejection, writing + permission justifications, or drafting a privacy policy. +--- + +# Chrome Extensions + +Build production-quality Chrome extensions using Manifest V3 and publish them to the Chrome Web Store. + +## Part 1 — Building Extensions + +### Mandatory Rules + +These address the most common causes of broken extensions. Violating any produces a non-functional build. + +#### 1. Icons: only reference files you create — or omit icons entirely + +``` +❌ BROKEN — referencing files that don't exist or reusing one file for all sizes: + "icons": { "16": "icon.png", "48": "icon.png", "128": "icon.png" } + +✅ CORRECT — each size is a separate file at the correct pixel dimensions: + "icons": { "16": "icons/icon-16.png", "48": "icons/icon-48.png", "128": "icons/icon-128.png" } + (where icon-16.png is 16×16px, icon-48.png is 48×48px, icon-128.png is 128×128px) + +✅ ALSO CORRECT — omit icons from manifest if you cannot generate real PNG files: + (just remove the "icons" and "default_icon" fields — Chrome uses a default icon) +``` + +**If you include icon references, you MUST create the actual image files.** Generate them with a script (see `references/extensions/icons.md`) or leave them out. Never reference non-existent files. + +#### 2. Side panel: you MUST provide a way to open it + +Defining `"side_panel": {"default_path": "..."}` does NOT make it openable. Add a trigger: + +```js +// In service-worker.js — open side panel on extension icon click +// IMPORTANT: chrome.action.onClicked ONLY fires when there is NO default_popup +chrome.action.onClicked.addListener(async (tab) => { + await chrome.sidePanel.open({ windowId: tab.windowId }); +}); +``` + +If the extension has both a popup AND side panel, add a button in the popup that calls `chrome.sidePanel.open()`. Alternatively, use `chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true })` — but the property is `openPanelOnActionClick`, NOT `openPanelOnActionIconClick`; the "Icon" variant causes a synchronous TypeError that silently aborts the service worker. Do NOT also define `default_popup` when using `setPanelBehavior`. See `references/extensions/side-panel.md`. + +#### 3. Code execution: sandboxed iframes ONLY + +Extension CSP blocks `eval()`, `new Function()`, inline ` + + + + + + +``` + +In `popup.js`: +```js +document.getElementById('btn').addEventListener('click', () => { + // Handle click +}); +``` + +## Executing User Code (Code Playground Pattern) + +If you need to execute arbitrary code (e.g., a CodePen-like playground), you MUST use one of these +approaches. **Extension CSP completely blocks `eval()`, `new Function()`, and inline scripts in +normal extension pages.** There is no way around this — you need sandboxing. + +### Option 1: Sandboxed Page in Manifest (Recommended) + +Declare a sandboxed page in manifest.json. Sandboxed pages have a relaxed CSP that allows +`eval()` and inline scripts, but they cannot access chrome.* APIs. + +```json +{ + "sandbox": { + "pages": ["sandbox.html"] + } +} +``` + +Use an iframe in your extension page to embed the sandbox: + +```html + + +``` + +**CRITICAL:** Communication between the extension page and the sandboxed iframe MUST use +`postMessage`. You CANNOT access `iframe.contentDocument` or `iframe.contentWindow.document` +directly — this will throw: + +``` +SecurityError: Blocked a frame with origin "chrome-extension://..." from accessing a cross-origin frame. +``` + +Correct pattern: + +```js +// playground.js — send code to sandbox +const iframe = document.getElementById('preview'); +iframe.contentWindow.postMessage({ + html: htmlCode, + css: cssCode, + js: jsCode +}, '*'); + +// sandbox.js — receive and execute +window.addEventListener('message', (event) => { + const { html, css, js } = event.data; + // Clear previous content + document.body.innerHTML = ''; + document.head.querySelectorAll('style.user-style').forEach(s => s.remove()); + + // Apply HTML + const container = document.createElement('div'); + container.innerHTML = html; + document.body.appendChild(container); + + // Apply CSS + const style = document.createElement('style'); + style.className = 'user-style'; + style.textContent = css; + document.head.appendChild(style); + + // Execute JS (eval is allowed in sandbox!) + try { + eval(js); + } catch (e) { + const errEl = document.createElement('pre'); + errEl.style.color = 'red'; + errEl.textContent = e.message; + document.body.appendChild(errEl); + } +}); +``` + +### Option 2: Blob URL in iframe + +Create a self-contained HTML document via blob URL: + +```js +function updatePreview(htmlCode, cssCode, jsCode) { + const html = ` + + + + + ${htmlCode} + + + +``` + +`devtools/devtools.js`: +```js +chrome.devtools.panels.create( + 'My Panel', // Title shown in DevTools tab + 'icons/icon-16.png', // Icon (optional, can be empty string) + 'devtools/panel/panel.html', // Panel content page — RELATIVE TO EXTENSION ROOT + (panel) => { + // panel.onShown.addListener((window) => { ... }); + // panel.onHidden.addListener(() => { ... }); + } +); +``` + +**CRITICAL: The panel path is relative to the extension root**, NOT relative to the devtools.js +file. This is the most common DevTools extension bug. + +```js +// ❌ WRONG — resolves to /panel/panel.html (file not found) +chrome.devtools.panels.create("My Panel", "", "panel/panel.html"); + +// ✅ CORRECT — resolves to /devtools/panel/panel.html +chrome.devtools.panels.create("My Panel", "", "devtools/panel/panel.html"); +``` + +## Panel Content + +`devtools/panel/panel.html` is a regular extension page with full chrome.* API access. + +## Accessing DevTools APIs + +Only available in the devtools page and panels: + +```js +// Get inspected window's tab ID +const tabId = chrome.devtools.inspectedWindow.tabId; + +// Evaluate JS in the inspected page +chrome.devtools.inspectedWindow.eval('document.title', (result, isException) => { + console.log('Page title:', result); +}); + +// Monitor network requests +chrome.devtools.network.onRequestFinished.addListener((request) => { + // request.request.url, request.response.status, etc. + // HAR entry format +}); + +// Get all captured requests +chrome.devtools.network.getHAR((harLog) => { + harLog.entries.forEach((entry) => { /* process */ }); +}); +``` + +## Communication Architecture + +DevTools pages/panels CANNOT directly talk to the service worker via `chrome.runtime.sendMessage` +in all cases. Use a connection pattern: + +```js +// In panel JS — connect to service worker +const port = chrome.runtime.connect({ name: 'devtools-panel' }); +port.postMessage({ type: 'INIT', tabId: chrome.devtools.inspectedWindow.tabId }); +port.onMessage.addListener((msg) => { /* handle */ }); + +// In service worker +chrome.runtime.onConnect.addListener((port) => { + if (port.name === 'devtools-panel') { + port.onMessage.addListener((msg) => { /* handle */ }); + } +}); +``` + +## Important Notes + +- DevTools pages exist per-DevTools-window (one per inspected tab) +- They are destroyed when DevTools closes +- `chrome.devtools.*` APIs are ONLY available in the devtools page context, not in the service worker +- Panels can inject scripts into the inspected page via `chrome.devtools.inspectedWindow.eval()` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/icons.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/icons.md new file mode 100644 index 00000000..a2340867 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/icons.md @@ -0,0 +1,101 @@ +# Generating Extension Icons + +## Quick: Omit icons + +If generating real icon files is impractical, **omit `icons` and `default_icon` from manifest.json entirely**. Chrome uses a default puzzle-piece icon. This is always better than referencing files that don't exist. + +## Generate with Python (Pillow) + +```python +# generate_icons.py +from PIL import Image, ImageDraw +import os + +os.makedirs('icons', exist_ok=True) + +for size in [16, 48, 128]: + img = Image.new('RGBA', (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + margin = max(1, size // 16) + draw.rounded_rectangle( + [margin, margin, size - margin, size - margin], + radius=size // 4, + fill='#4688F1' + ) + # Add a letter + font_size = size // 2 + draw.text((size // 2, size // 2), 'E', fill='white', anchor='mm') + img.save(f'icons/icon-{size}.png') + print(f'Created icons/icon-{size}.png ({size}x{size})') +``` + +## Generate with Node.js (canvas) + +```js +const { createCanvas } = require('canvas'); +const fs = require('fs'); +const path = require('path'); + +fs.mkdirSync('icons', { recursive: true }); + +for (const size of [16, 48, 128]) { + const canvas = createCanvas(size, size); + const ctx = canvas.getContext('2d'); + const r = size / 4; + + // Rounded rectangle + ctx.beginPath(); + ctx.roundRect(1, 1, size - 2, size - 2, r); + ctx.fillStyle = '#4688F1'; + ctx.fill(); + + // Letter + ctx.fillStyle = 'white'; + ctx.font = `bold ${size / 2}px sans-serif`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('E', size / 2, size / 2); + + fs.writeFileSync(`icons/icon-${size}.png`, canvas.toBuffer('image/png')); + console.log(`Created icons/icon-${size}.png (${size}x${size})`); +} +``` + +## Generate with pure SVG (no dependencies) + +Create SVGs and use them directly (Chrome supports SVG icons in some contexts) or convert: + +```bash +for SIZE in 16 48 128; do + cat > "icons/icon-${SIZE}.svg" << EOF + + + E + +EOF +done +``` + +Note: For Chrome Web Store submission, PNG is required. SVG works for development. + +## Manifest reference + +```json +{ + "icons": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "action": { + "default_icon": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + } + } +} +``` + +Each file MUST match its declared size: icon-16.png = 16×16 pixels, etc. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/media-capture.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/media-capture.md new file mode 100644 index 00000000..21333ebf --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/media-capture.md @@ -0,0 +1,140 @@ +# Media Capture (Tab & Desktop) + +## Choosing the Right API + +| Need | API | +|------|-----| +| Record the active tab's audio/video | `chrome.tabCapture.getMediaStreamId()` | +| Let the user choose a screen, window, or tab | `chrome.desktopCapture.chooseDesktopMedia()` | + +Prefer `tabCapture` when you only need the current tab — it requires no user chooser dialog and +no `"tabs"` permission. Use `desktopCapture` only when the user must select what to capture. + +## Tab Capture + +### Permissions + +```json +{ "permissions": ["tabCapture"] } +``` + +### Pattern + +`chrome.tabCapture.getMediaStreamId()` runs in the **service worker** and returns a stream ID. +The actual `getUserMedia()` call must happen in an **offscreen document** (the SW cannot access +media streams directly). + +```js +// service-worker.js +chrome.action.onClicked.addListener(async (tab) => { + const streamId = await chrome.tabCapture.getMediaStreamId({ targetTabId: tab.id }); + // Pass the ID to the offscreen document to call getUserMedia + await chrome.runtime.sendMessage({ type: 'START_CAPTURE', streamId }); +}); + +// offscreen.js +chrome.runtime.onMessage.addListener((msg) => { + if (msg.type !== 'START_CAPTURE') return; + (async () => { + const stream = await navigator.mediaDevices.getUserMedia({ + audio: { mandatory: { chromeMediaSource: 'tab', chromeMediaSourceId: msg.streamId } }, + video: { mandatory: { chromeMediaSource: 'tab', chromeMediaSourceId: msg.streamId } } + }); + const recorder = new MediaRecorder(stream, { mimeType: 'video/webm' }); + // ... handle recorder events + })(); +}); +``` + +## Desktop Capture + +### Permissions + +```json +{ "permissions": ["tabs", "desktopCapture"] } +``` + +`"tabs"` is **required** — `chooseDesktopMedia` needs a `targetTab` with its `url` field +populated, which requires the `"tabs"` permission. + +### Pattern + +```js +// service-worker.js +chrome.action.onClicked.addListener(async (tab) => { + // ❌ BROKEN — no targetTab + // chrome.desktopCapture.chooseDesktopMedia(['screen', 'window'], cb); + + // ✅ CORRECT — pass the active tab + chrome.desktopCapture.chooseDesktopMedia(['screen', 'window', 'tab'], tab, (streamId) => { + if (!streamId) return; // User cancelled + // Send streamId to offscreen document for getUserMedia + chrome.runtime.sendMessage({ type: 'START_DESKTOP_CAPTURE', streamId }); + }); +}); +``` + +## State Locking — Prevent Double-Start Errors + +Both APIs fail if called while a previous capture is still active: +- `tabCapture`: `"Cannot capture a tab with an active stream"` +- `desktopCapture`: opens a second chooser dialog on top of the first + +Use a state machine stored in `chrome.storage.session` (survives service worker restarts, +cleared on browser close): + +```js +// State: 'idle' → 'starting' → 'recording' → 'stopping' → 'idle' +chrome.action.onClicked.addListener(async (tab) => { + const { recordingState = 'idle' } = await chrome.storage.session.get('recordingState'); + + // Ignore clicks during transitions + if (recordingState === 'starting' || recordingState === 'stopping') return; + + if (recordingState === 'idle') { + await chrome.storage.session.set({ recordingState: 'starting' }); + try { + await startRecording(tab); + await chrome.storage.session.set({ recordingState: 'recording' }); + await chrome.action.setBadgeText({ text: 'REC' }); + await chrome.action.setBadgeBackgroundColor({ color: '#FF0000' }); + } catch (err) { + console.error('Failed to start recording:', err); + await chrome.storage.session.set({ recordingState: 'idle' }); + } + } else if (recordingState === 'recording') { + await chrome.storage.session.set({ recordingState: 'stopping' }); + try { await stopRecording(); } + finally { + await chrome.storage.session.set({ recordingState: 'idle' }); + await chrome.action.setBadgeText({ text: '' }); + } + } +}); +``` + +This same pattern applies to `chrome.offscreen.createDocument` (only one offscreen document +is allowed at a time) and any other API that manages an exclusive resource. + +## Saving Recordings + +Offscreen documents cannot call `chrome.downloads` — send the blob back to the service worker: + +```js +// offscreen.js — when recording stops +recorder.ondataavailable = (e) => chunks.push(e.data); +recorder.onstop = async () => { + const blob = new Blob(chunks, { type: 'video/webm' }); + const url = URL.createObjectURL(blob); + // Service worker handles the download + await chrome.runtime.sendMessage({ type: 'SAVE_RECORDING', url }); +}; + +// service-worker.js +chrome.runtime.onMessage.addListener((msg) => { + if (msg.type !== 'SAVE_RECORDING') return; + chrome.downloads.download({ url: msg.url, filename: 'recording.webm' }); +}); +``` + +See `references/extensions/message-passing.md` for the full offscreen document messaging pattern. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/message-passing.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/message-passing.md new file mode 100644 index 00000000..7846ac49 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/message-passing.md @@ -0,0 +1,136 @@ +# Message Passing + +## Basic patterns + +### One-way message (fire and forget) + +```js +// sender (popup, content script, etc.) +chrome.runtime.sendMessage({ type: 'LOG', data: 'hello' }); + +// receiver (service worker) +chrome.runtime.onMessage.addListener((message, sender) => { + if (message.type === 'LOG') console.log(message.data); +}); +``` + +### Request/response — IIFE + return true (most compatible) + +```js +// sender +const response = await chrome.runtime.sendMessage({ type: 'GET_DATA' }); +console.log(response.data); + +// receiver — IIFE keeps the channel open until sendResponse is called +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + if (message.type === 'GET_DATA') { + (async () => { + const data = await chrome.storage.local.get('key'); + sendResponse({ data }); + })(); + return true; // REQUIRED — tells Chrome to keep the channel open + } +}); +``` + +### Request/response — return a Promise (Chrome 99+) + +Returning a Promise directly from the listener is now supported and cleaner than the IIFE pattern: + +```js +chrome.runtime.onMessage.addListener((message, sender) => { + if (message.type === 'GET_DATA') { + return chrome.storage.local.get('key'); // returned promise resolves the response + } + // Return nothing (or undefined) for messages this listener doesn't handle +}); +``` + +**Note:** Requires Chrome 99+, only use when minimum Chrome version is set to 99. +**Note:** Do NOT mix the two styles. If you return a Promise, do NOT also call `sendResponse` or `return true`. + +## Content script ↔ service worker + +```js +// content script → service worker +const result = await chrome.runtime.sendMessage({ type: 'FETCH_DATA', url: location.href }); + +// service worker → specific tab's content script +await chrome.tabs.sendMessage(tabId, { type: 'HIGHLIGHT', selector: '.important' }); +``` + +## Service worker → content script (targeted) + +Always check that the tab exists and the content script is injected: + +```js +async function sendToContentScript(tabId, message) { + try { + return await chrome.tabs.sendMessage(tabId, message); + } catch (err) { + // Content script not injected yet, or tab navigated away + console.warn('Could not reach content script:', err.message); + return null; + } +} +``` + +## Long-lived connections (ports) + +Use ports when you need a persistent channel (e.g., streaming data, DevTools panel): + +```js +// opener (popup or content script) +const port = chrome.runtime.connect({ name: 'my-channel' }); +port.postMessage({ type: 'START' }); +port.onMessage.addListener((msg) => console.log('received:', msg)); +port.onDisconnect.addListener(() => console.log('disconnected')); + +// receiver (service worker) +chrome.runtime.onConnect.addListener((port) => { + if (port.name !== 'my-channel') return; + port.onMessage.addListener((msg) => { + if (msg.type === 'START') { + port.postMessage({ status: 'ok' }); + } + }); +}); +``` + +## Common mistakes + +### Missing `return true` causes response to never arrive + +```js +// ❌ BROKEN — async work completes but channel is already closed +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + fetchSomething().then(data => sendResponse(data)); // too late + // missing: return true +}); + +// ✅ CORRECT +chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { + fetchSomething().then(data => sendResponse(data)); + return true; +}); +``` + +### Sending to a tab before the content script is ready + +Content scripts are injected after the page loads. If the service worker sends a message immediately on `tabs.onUpdated`, the content script may not be listening yet. Use a handshake or retry: + +```js +// content script — announce it's ready +chrome.runtime.sendMessage({ type: 'CONTENT_READY' }); + +// service worker — wait for CONTENT_READY before sending +chrome.runtime.onMessage.addListener((msg, sender) => { + if (msg.type === 'CONTENT_READY' && sender.tab) { + chrome.tabs.sendMessage(sender.tab.id, { type: 'INIT_DATA', ... }); + } +}); +``` + +### Multiple listeners responding + +Only one listener should respond to a given message type. If multiple listeners call `sendResponse`, only the first one wins and the rest are silently ignored. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/omnibox.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/omnibox.md new file mode 100644 index 00000000..0d9816e3 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/omnibox.md @@ -0,0 +1,83 @@ +# Omnibox Integration + +## Setup + +```json +{ + "omnibox": { "keyword": "wiki" } +} +``` + +User types `wiki` + Space in the address bar to activate. + +## Providing Suggestions + +```js +chrome.omnibox.onInputChanged.addListener(async (text, suggest) => { + if (text.length < 2) return; + + try { + const response = await fetch( + `https://en.wikipedia.org/w/api.php?action=opensearch&search=${encodeURIComponent(text)}&limit=5&format=json` + ); + const [, titles, , urls] = await response.json(); + + const suggestions = titles.map((title, i) => ({ + content: urls[i], + description: `${title} - ${urls[i]}` + })); + + suggest(suggestions); + } catch (err) { + console.error('Search failed:', err); + } +}); +``` + +## Handling Selection + +```js +chrome.omnibox.onInputEntered.addListener((text, disposition) => { + const url = text.startsWith('http') ? text + : `https://en.wikipedia.org/wiki/${encodeURIComponent(text)}`; + + switch (disposition) { + case 'currentTab': + chrome.tabs.update({ url }); + break; + case 'newForegroundTab': + chrome.tabs.create({ url }); + break; + case 'newBackgroundTab': + chrome.tabs.create({ url, active: false }); + break; + } +}); +``` + +## Description Formatting + +Suggestions support XML-like formatting: +- `text` — renders as URL style +- `text` — bold match highlighting +- `text` — dimmed/secondary text + +## Default Suggestion + +```js +chrome.omnibox.onInputChanged.addListener((text, suggest) => { + chrome.omnibox.setDefaultSuggestion({ + description: `Search Wikipedia for "${text}"` + }); + // ... fetch and suggest +}); +``` + +## Required host_permissions + +If fetching suggestions from an API, declare: +```json +{ + "host_permissions": ["https://en.wikipedia.org/*"] +} +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/permissions.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/permissions.md new file mode 100644 index 00000000..c104bef7 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/permissions.md @@ -0,0 +1,113 @@ +# Permissions + +## `permissions` vs `host_permissions` + +These are separate manifest keys — don't conflate them: + +```json +{ + "permissions": ["tabs", "scripting", "storage"], + "host_permissions": ["https://api.example.com/*"] +} +``` + +- `permissions` grants access to chrome.* APIs (`tabs`, `storage`, `scripting`, `desktopCapture`, etc.) +- `host_permissions` grants access to specific origins for `fetch`, `chrome.scripting.executeScript`, and reading tab URLs cross-origin + +Scope `host_permissions` to specific domains rather than `` unless the extension genuinely needs to run on every site — broad host permissions draw extra Chrome Web Store review scrutiny. + +## `tab.url` requires the `tabs` permission + +Without it, `tab.url` and `tab.title` silently return `undefined` — no error thrown. + +```js +// manifest.json — REQUIRED if you read tab.url or tab.title anywhere: +{ "permissions": ["tabs"] } +``` + +See `references/extensions/tab-management.md` for the full tabs/windows API. + +## `activeTab` only works on direct user gestures — not from side panels + +`activeTab` grants temporary access to the current tab ONLY when triggered by: +- Clicking the extension action icon +- A context menu item (including the `"tab"` context) +- A keyboard shortcut from the `commands` API +- Accepting an omnibox suggestion + +It does **NOT** grant access when clicking a button in a side panel, a popup button that opens +later, or any programmatic trigger. + +```js +// ❌ BROKEN — activeTab does NOT work from a side panel button click +document.getElementById('summarize').addEventListener('click', async () => { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: () => document.body.innerText }); +}); + +// ✅ FIX — use "tabs" permission + specific host_permissions instead +// manifest.json: { "permissions": ["tabs", "scripting"], "host_permissions": [""] } +``` + +See `references/extensions/side-panel.md` for the side-panel-specific writeup. + +## `chrome.permissions.request()` needs a user gesture — the gesture survives one `sendMessage` hop, but not an `await` after that + +`chrome.permissions.request()` (for optional/dynamic permissions) only works within a user +gesture (a click, keypress, etc.). A gesture from a UI context (side panel, popup) **does** +propagate across `chrome.runtime.sendMessage` to the service worker's `onMessage` listener — but +it's only good for that one synchronous turn. If the listener `await`s anything (a timer, a +storage read, another message round-trip) before calling `chrome.permissions.request()`, the +gesture is gone and the call throws `"This function must be called during a user gesture, such +as an onclick handler"`. + +```js +// side-panel.js +button.addEventListener('click', () => { + chrome.runtime.sendMessage({ type: 'REQUEST_PERMISSION' }, (granted) => { ... }); +}); + +// service-worker.js + +// ❌ BROKEN — an await before chrome.permissions.request() burns the gesture window +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.type === 'REQUEST_PERMISSION') { + (async () => { + await chrome.storage.local.get('someSetting'); // gesture is gone after this + const granted = await chrome.permissions.request({ permissions: ['downloads'] }); + sendResponse(granted); + })(); + return true; + } +}); + +// ✅ CORRECT — call chrome.permissions.request() as the first thing in the listener, +// nothing awaited before it +chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => { + if (msg.type === 'REQUEST_PERMISSION') { + chrome.permissions.request({ permissions: ['downloads'] }).then(sendResponse); + return true; // fine to await the *result* — the call itself already happened synchronously + } +}); +``` + +**Rule of thumb:** call `chrome.permissions.request()` (or anything else gated on a user +gesture, like `activeTab`) as the very first statement of the message listener that receives the +click-triggered message — before any other `await`. Awaiting the *returned* promise afterward is +fine; awaiting anything *before* the call is what breaks it. + +## Checking and removing permissions + +```js +// Check whether an optional permission is currently granted +const hasIt = await chrome.permissions.contains({ permissions: ['downloads'] }); + +// Remove a previously granted optional permission +await chrome.permissions.remove({ permissions: ['downloads'] }); +``` + +Declare optional permissions in the manifest so they're eligible to be requested at runtime: + +```json +{ "optional_permissions": ["downloads"] } +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/popup-ui.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/popup-ui.md new file mode 100644 index 00000000..8e152843 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/popup-ui.md @@ -0,0 +1,94 @@ +# Popup UI + +## Setup + +```json +{ + "action": { + "default_popup": "popup/popup.html", + "default_icon": { + "16": "icons/icon-16.png", + "48": "icons/icon-48.png", + "128": "icons/icon-128.png" + }, + "default_title": "My Extension" + } +} +``` + +## Key Constraints + +- Popup closes when the user clicks outside it — don't rely on it staying open +- Default max size: 800x600 px. Set size via CSS on body/html +- All scripts must be external files (CSP — no inline scripts) +- All event listeners must use `addEventListener` (no inline handlers) + +## Popup HTML Template + +```html + + + + + + + +

My Extension

+
+ + + +``` + +## Persistence + +Popup state is lost when closed. Use `chrome.storage` for persistence: + +```js +// Save on change +document.getElementById('input').addEventListener('input', (e) => { + chrome.storage.local.set({ savedInput: e.target.value }); +}); + +// Restore on open +document.addEventListener('DOMContentLoaded', async () => { + const { savedInput = '' } = await chrome.storage.local.get('savedInput'); + document.getElementById('input').value = savedInput; +}); +``` + +Note: `localStorage` technically works in popups (they have a persistent origin), but +`chrome.storage` is strongly preferred because it works across all extension contexts +and supports sync. + +## Communicating with Service Worker + +```js +// From popup +const response = await chrome.runtime.sendMessage({ type: 'GET_STATUS' }); + +// Long-lived connection +const port = chrome.runtime.connect({ name: 'popup' }); +port.postMessage({ type: 'INIT' }); +port.onMessage.addListener((msg) => { /* handle */ }); +``` + +## Dynamic Popup vs No Popup + +If you want the action click to do something instead of showing a popup, remove +`default_popup` and use `chrome.action.onClicked`: + +```js +// In service worker — only fires if NO popup is set +chrome.action.onClicked.addListener((tab) => { + // Open side panel, inject script, etc. +}); +``` + +You can toggle between popup and no-popup dynamically: +```js +chrome.action.setPopup({ popup: 'popup/popup.html' }); // Enable popup +chrome.action.setPopup({ popup: '' }); // Disable popup (enables onClicked) +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/prompt-api.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/prompt-api.md new file mode 100644 index 00000000..bda04cd0 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/prompt-api.md @@ -0,0 +1,117 @@ +# Chrome Prompt API (LanguageModel) — Extension-Specific Notes + +The `LanguageModel` API (Prompt API) works in all extension contexts — service worker, popup, +side panel, and other extension pages — with no additional manifest permissions required. + +For general Prompt API usage (availability checks, session creation, streaming, session +management), use the `modern-web-guidance` skill. + +## Deprecated namespace + +Extensions that used the origin trial may still have the old API surface. Remove it: + +```js +// ❌ OLD — deprecated +const session = await self.ai.languageModel.create(); + +// ✅ CURRENT (Chrome 138+) +const session = await LanguageModel.create({ ... }); +``` + +Also remove the expired permission from manifest.json: +```json +"permissions": ["aiLanguageModelOriginTrial"] // ❌ remove this +``` + +## Extension-only: `LanguageModel.params()` + +Extensions have access to `LanguageModel.params()`, which returns model constraints not +available on the web: + +```js +const params = await LanguageModel.params(); +// { defaultTopK: 3, maxTopK: 128, defaultTemperature: 1, maxTemperature: 2 } + +const session = await LanguageModel.create({ + temperature: 0.7, + topK: 5 +}); +``` + +## Complete extension example: page summarizer + +A full wiring example showing manifest + service worker + side panel together. +Note the use of `tabs` + `host_permissions` instead of `activeTab` — side panel button +clicks do NOT activate `activeTab` (see Rule 12). + +### manifest.json +```json +{ + "manifest_version": 3, + "name": "AI Page Summarizer", + "version": "1.0", + "permissions": ["sidePanel", "tabs", "scripting"], + "host_permissions": [""], + "background": { "service_worker": "service-worker.js" }, + "side_panel": { "default_path": "sidepanel/sidepanel.html" }, + "action": { "default_title": "Summarize Page" } +} +``` + +### service-worker.js +```js +chrome.action.onClicked.addListener(async (tab) => { + await chrome.sidePanel.open({ windowId: tab.windowId }); +}); +``` + +### sidepanel/sidepanel.js +```js +const statusEl = document.getElementById('status'); +const summaryEl = document.getElementById('summary'); + +document.getElementById('summarize').addEventListener('click', async () => { + if (!globalThis.LanguageModel) { + statusEl.textContent = 'Prompt API not available in this browser.'; + return; + } + + const availability = await LanguageModel.availability({ + expectedInputs: [{ type: "text", languages: ["en"] }], + expectedOutputs: [{ type: "text", languages: ["en"] }] + }); + if (availability === 'unavailable') { + statusEl.textContent = 'AI model not available on this device.'; + return; + } + + // Requires "tabs" + "host_permissions" — activeTab does NOT work from a side panel button + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + const [{ result: pageText }] = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: () => { + const body = document.body.cloneNode(true); + body.querySelectorAll('script, style, nav, footer, header').forEach(el => el.remove()); + return body.innerText.substring(0, 4000); + } + }); + + const session = await LanguageModel.create({ + expectedInputs: [{ type: "text", languages: ["en"] }], + expectedOutputs: [{ type: "text", languages: ["en"] }], + initialPrompts: [{ role: 'system', content: 'Summarize web page content in 3-5 bullet points.' }], + monitor(m) { + m.addEventListener('downloadprogress', (e) => { + const pct = e.total ? Math.floor((e.loaded / e.total) * 100) : 0; + statusEl.textContent = `Downloading model: ${pct}%`; + }); + } + }); + + summaryEl.textContent = ''; + for await (const chunk of session.promptStreaming(`Summarize:\n\n${pageText}`)) { + summaryEl.textContent += chunk; // APPEND — do not replace + } + session.destroy(); +}); +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/service-worker.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/service-worker.md new file mode 100644 index 00000000..20b88c69 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/service-worker.md @@ -0,0 +1,148 @@ +# Service Worker Lifetime & State Management + +## The Core Problem + +Chrome terminates extension service workers after ~30 seconds of inactivity. Unlike Manifest V2 +persistent background pages, you CANNOT rely on in-memory state. + +## Rules + +1. **Never store state in global variables** — treat every event handler as if the SW just started +2. **Use chrome.storage for all persistent state** — read on demand, write after changes +3. **Use chrome.alarms for timers** — not setTimeout/setInterval (these die with the SW) +4. **Use chrome.storage.session for ephemeral session state** — survives SW restart but not browser restart + +## Storage Tier Selection + +| Need | Use | +|------|-----| +| Survives browser restart, syncs across devices | `chrome.storage.sync` (8KB/item, 100KB total) | +| Survives browser restart, local only | `chrome.storage.local` (10MB default) | +| Survives SW restart only | `chrome.storage.session` (10MB default) | +| Never persisted (avoid) | Global variables ❌ | + +## Pattern: State Read-on-Demand + +```js +// ❌ BAD: State in memory +let count = 0; +chrome.webNavigation.onCompleted.addListener(() => { + count++; + chrome.action.setBadgeText({ text: String(count) }); +}); + +// ✅ GOOD: State in storage +chrome.webNavigation.onCompleted.addListener(async (details) => { + if (details.frameId !== 0) return; // Main frame only + const data = await chrome.storage.local.get({ visitCount: 0 }); + data.visitCount++; + await chrome.storage.local.set(data); + chrome.action.setBadgeText({ text: String(data.visitCount) }); +}); +``` + +## Pattern: Alarms Instead of Timers + +```js +// ❌ BAD: Timer dies when SW terminates +setInterval(() => checkForUpdates(), 60000); + +// ✅ GOOD: Alarm persists +chrome.alarms.create('check-updates', { periodInMinutes: 1 }); +chrome.alarms.onAlarm.addListener((alarm) => { + if (alarm.name === 'check-updates') { + checkForUpdates(); + } +}); +``` + +Minimum alarm interval: 0.5 minutes. + +## Pattern: One-Time Initialization + +```js +// Set up defaults and context menus on install +chrome.runtime.onInstalled.addListener(async (details) => { + if (details.reason === 'install') { + await chrome.storage.local.set({ settings: defaultSettings }); + } + // Context menus must be re-created (they persist, but re-creating is idempotent) + chrome.contextMenus.create({ + id: 'myItem', + title: 'My Context Menu Item', + contexts: ['selection'] + }); +}); +``` + +## Pattern: Keeping the SW Alive (When Necessary) + +Occasionally you need the SW alive for a long-running operation. Use one of: + +1. **chrome.offscreen** — create an offscreen document for long tasks +2. **Periodic storage writes** — each chrome.storage call resets the idle timer +3. **Active port connections** — an open port keeps the SW alive + +```js +// Port-based keepalive from popup/side panel +const port = chrome.runtime.connect({ name: 'keepalive' }); +// The SW stays alive as long as this port is open +``` + +⚠️ Do NOT abuse keepalive patterns. Chrome may enforce stricter limits in future versions. + +## Pattern: Event Registration + +All event listeners MUST be registered synchronously at the top level of the service worker. +Chrome replays events to a restarted SW, but only for listeners that were registered synchronously. + +```js +// ✅ GOOD: Top-level registration +chrome.runtime.onMessage.addListener(handleMessage); +chrome.tabs.onUpdated.addListener(handleTabUpdate); +chrome.webNavigation.onCompleted.addListener(handleNavigation); + +// ❌ BAD: Conditional or async registration +async function setup() { + const { enabled } = await chrome.storage.local.get('enabled'); + if (enabled) { + chrome.tabs.onUpdated.addListener(handleTabUpdate); // Too late! + } +} +setup(); +``` + +Instead, register all listeners and check conditions inside them: + +```js +chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + const { enabled } = await chrome.storage.local.get('enabled'); + if (!enabled) return; + // Process... +}); +``` + +## Date-Based Resets + +For daily counters, store the date alongside the count: + +```js +function getToday() { + return new Date().toISOString().split('T')[0]; // "2025-01-15" +} + +async function incrementDailyCount() { + const { dailyCount = 0, countDate = '' } = await chrome.storage.local.get(['dailyCount', 'countDate']); + const today = getToday(); + + if (countDate !== today) { + // New day — reset + await chrome.storage.local.set({ dailyCount: 1, countDate: today }); + return 1; + } else { + const newCount = dailyCount + 1; + await chrome.storage.local.set({ dailyCount: newCount }); + return newCount; + } +} +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/side-panel.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/side-panel.md new file mode 100644 index 00000000..5a1e5b76 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/side-panel.md @@ -0,0 +1,147 @@ +# Side Panel + +## Setup + +Add to manifest.json: +```json +{ + "permissions": ["sidePanel"], + "side_panel": { + "default_path": "sidepanel/sidepanel.html" + } +} +``` + +## Opening the Side Panel — REQUIRED + +**A side panel definition alone does NOT make it openable.** You MUST provide an explicit +trigger to open it. Without one of these, users have no way to access the panel: + +### Most common: Open on action icon click + +If the extension's primary function is the side panel, remove `default_popup` from the action +and use `chrome.action.onClicked` to open the side panel: + +```js +// service-worker.js +chrome.action.onClicked.addListener(async (tab) => { + await chrome.sidePanel.open({ windowId: tab.windowId }); +}); +``` + +⚠️ `chrome.action.onClicked` only fires when there is NO `default_popup` set. If you have both +a popup and a side panel, open the side panel from the popup via a button, or use a different +trigger. + +### Alternative triggers + +```js +// Open from a context menu item +chrome.contextMenus.onClicked.addListener(async (info, tab) => { + if (info.menuItemId === 'open-panel') { + await chrome.sidePanel.open({ windowId: tab.windowId }); + } +}); + +// Open from a keyboard shortcut (defined in manifest commands) +chrome.commands.onCommand.addListener(async (command) => { + if (command === 'open-side-panel') { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + await chrome.sidePanel.open({ windowId: tab.windowId }); + } +}); +``` + +You can also open it for a specific tab: +```js +await chrome.sidePanel.open({ tabId: tab.id }); +``` + +### Simplest: Auto-open via setPanelBehavior + +If the side panel should open whenever the user clicks the extension icon, use `setPanelBehavior` +as a one-liner instead of an `onClicked` listener: + +```js +// service-worker.js +chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }); +``` + +⚠️ **The property is `openPanelOnActionClick` — NOT `openPanelOnActionIconClick`.** +Using the wrong name causes a synchronous TypeError that silently aborts the service worker. + +When using `setPanelBehavior`, do NOT also define `default_popup` — the popup takes priority. + +## Setting Panel Per-Tab + +```js +// Different side panel content for different tabs +chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, tab) => { + if (tab.url?.includes('github.com')) { + await chrome.sidePanel.setOptions({ + tabId, + path: 'sidepanel/github-panel.html', + enabled: true + }); + } +}); +``` + +## Communication with Side Panel + +The side panel is an extension page, so it can use all chrome.* APIs directly and communicate +with the service worker via `chrome.runtime.sendMessage` / `chrome.runtime.onMessage`. + +To get data from the active tab's content script: + +```js +// In side panel JS +async function getPageContent() { + const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }); + const response = await chrome.tabs.sendMessage(tab.id, { type: 'GET_CONTENT' }); + return response; +} +``` + +Or use `chrome.scripting.executeScript` from the side panel (requires `scripting` and `activeTab` permissions): + +```js +const [{ result }] = await chrome.scripting.executeScript({ + target: { tabId: tab.id }, + func: () => document.body.innerText +}); +``` + +## Side Panel vs Popup + +| Feature | Side Panel | Popup | +|---------|-----------|-------| +| Stays open | Yes | Closes when clicking away | +| Resizable | Yes (by user) | Fixed size | +| Coexists with page | Yes (side by side) | Overlays page | +| Use when | Extended interaction, reading | Quick actions, settings | + +## Important Notes + +- The side panel shares a single instance per window — opening it replaces existing content +- Use `chrome.sidePanel.setOptions({ enabled: false })` to disable for specific tabs +- Side panel HTML files have full access to chrome.* APIs +- The side panel persists across tab switches (per-window) + +### ⚠️ `activeTab` does NOT work from side panel interactions + +`activeTab` only grants tab access on direct user gestures: clicking the extension icon, context +menu items, keyboard shortcuts, or omnibox suggestions. **Clicking a button inside a side panel +does NOT activate `activeTab`.** + +If your side panel needs to read or modify page content (e.g., a "Summarize" button), use +`tabs` + `host_permissions` instead: + +```json +{ + "permissions": ["tabs", "scripting", "sidePanel"], + "host_permissions": [""] +} +``` + +Do NOT rely on `activeTab` for side panel functionality. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/storage.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/storage.md new file mode 100644 index 00000000..0909440b --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/storage.md @@ -0,0 +1,65 @@ +# Chrome Storage API + +## Storage Areas + +| Area | Persists | Syncs | Quota | Use For | +|------|----------|-------|-------|---------| +| `chrome.storage.local` | Yes | No | 10 MB | Most extension data | +| `chrome.storage.sync` | Yes | Yes (across devices) | 100 KB total, 8 KB/item | User preferences, small data | +| `chrome.storage.session` | Until browser close | No | 10 MB | Ephemeral state, survives SW restart | + +Permission required: `"storage"` + +## Basic Operations + +```js +// Set +await chrome.storage.local.set({ key: 'value', count: 42, items: [1,2,3] }); + +// Get (with defaults) +const { key = 'default', count = 0 } = await chrome.storage.local.get(['key', 'count']); + +// Get all +const allData = await chrome.storage.local.get(null); + +// Remove +await chrome.storage.local.remove('key'); +await chrome.storage.local.remove(['key1', 'key2']); + +// Clear all +await chrome.storage.local.clear(); +``` + +## Change Listener (Works Across All Contexts) + +```js +chrome.storage.onChanged.addListener((changes, areaName) => { + for (const [key, { oldValue, newValue }] of Object.entries(changes)) { + console.log(`${areaName}.${key}: ${oldValue} → ${newValue}`); + } +}); +``` + +## storage.sync Quotas + +Be aware of limits when using sync: +- `QUOTA_BYTES_PER_ITEM`: 8,192 bytes per key-value pair +- `MAX_ITEMS`: 512 items +- `QUOTA_BYTES`: 102,400 bytes total +- `MAX_WRITE_OPERATIONS_PER_HOUR`: 1,800 +- `MAX_WRITE_OPERATIONS_PER_MINUTE`: 120 + +For large data, split across multiple keys or use `chrome.storage.local`. + +## storage.session Notes + +- Only available in MV3 +- Cleared when the browser closes (not just when SW terminates) +- Accessible from service worker, popup, side panel, etc. +- Good for: auth tokens, temporary caches, in-progress operations + +## Why Not localStorage? + +`localStorage` works in popup and extension pages but NOT in service workers. +`chrome.storage` works everywhere and supports the cross-context change listener. +Always prefer `chrome.storage`. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/tab-management.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/tab-management.md new file mode 100644 index 00000000..c86855a3 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/tab-management.md @@ -0,0 +1,132 @@ +# Tab Management & Groups + +## Permissions + +```json +{ + "permissions": ["tabs", "tabGroups"] +} +``` + +Note: `tabs` permission gives access to `url`, `title`, `favIconUrl` on Tab objects. +Without it, you can still use `chrome.tabs` but won't see sensitive tab properties. + +## Querying Tabs + +```js +// All tabs +const allTabs = await chrome.tabs.query({}); + +// Active tab in current window +const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true }); + +// Tabs matching a URL pattern +const gmailTabs = await chrome.tabs.query({ url: '*://mail.google.com/*' }); +``` + +## Tab Operations + +```js +// Create +const tab = await chrome.tabs.create({ url: 'https://example.com', active: true }); + +// Update +await chrome.tabs.update(tabId, { url: 'https://new-url.com', pinned: true }); + +// Close +await chrome.tabs.remove(tabId); +await chrome.tabs.remove([tabId1, tabId2]); // Multiple + +// Move +await chrome.tabs.move(tabId, { index: 0 }); // Move to first position + +// Reload +await chrome.tabs.reload(tabId); +``` + +## Tab Groups + +```js +// Create a group from tabs +const groupId = await chrome.tabs.group({ tabIds: [tabId1, tabId2] }); + +// Customize the group +await chrome.tabGroups.update(groupId, { + title: 'Work', + color: 'blue', // grey, blue, red, yellow, green, pink, purple, cyan, orange + collapsed: false +}); + +// Move tab into existing group +await chrome.tabs.group({ tabIds: [newTabId], groupId: existingGroupId }); + +// Ungroup +await chrome.tabs.ungroup(tabId); +``` + +## Grouping by Domain + +```js +async function groupByDomain() { + const tabs = await chrome.tabs.query({ currentWindow: true }); + const byDomain = {}; + + for (const tab of tabs) { + try { + const domain = new URL(tab.url).hostname; + (byDomain[domain] ??= []).push(tab.id); + } catch { /* ignore tabs without URLs */ } + } + + for (const [domain, tabIds] of Object.entries(byDomain)) { + if (tabIds.length > 1) { + const groupId = await chrome.tabs.group({ tabIds }); + await chrome.tabGroups.update(groupId, { + title: domain.replace('www.', ''), + color: 'blue' + }); + } + } +} +``` + +## Events + +```js +chrome.tabs.onCreated.addListener((tab) => { /* new tab */ }); +chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => { /* tab changed */ }); +chrome.tabs.onRemoved.addListener((tabId, removeInfo) => { /* tab closed */ }); +chrome.tabs.onActivated.addListener(({ tabId, windowId }) => { /* tab focused */ }); +chrome.tabGroups.onUpdated.addListener((group) => { /* group changed */ }); +``` + +## Windows + +⚠️ **`chrome.windows` has NO `.query()` method.** Unlike `chrome.tabs.query()`, there is no +`chrome.windows.query()`. Use the correct method for your need: + +```js +// ❌ BROKEN +const windows = await chrome.windows.query({ focused: true }); +// TypeError: chrome.windows.query is not a function + +// ✅ CORRECT +const focused = await chrome.windows.getLastFocused({ populate: true }); // includes tabs array +const current = await chrome.windows.getCurrent({ populate: true }); +const all = await chrome.windows.getAll({ populate: true }); +const single = await chrome.windows.get(windowId, { populate: true }); +``` + +Full API: `getAll`, `getLastFocused`, `getCurrent`, `get(windowId)`, `create`, `update`, `remove`. +Pass `{ populate: true }` to include the `tabs` array on the returned window object. + +```js +// Create a new window with specific tabs +const win = await chrome.windows.create({ url: 'https://example.com', focused: true }); + +// Move current window to a specific position/size +await chrome.windows.update(windowId, { left: 0, top: 0, width: 800, height: 600 }); + +// Minimise / maximise +await chrome.windows.update(windowId, { state: 'minimized' }); // 'normal' | 'minimized' | 'maximized' | 'fullscreen' +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/user-scripts.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/user-scripts.md new file mode 100644 index 00000000..ec9dff89 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/extensions/user-scripts.md @@ -0,0 +1,273 @@ +# User Scripts API + +The `chrome.userScripts` API lets extensions run **arbitrary code provided by the user** at +runtime — code that cannot be shipped as part of the extension package. This is fundamentally +different from content scripts (which are bundled with the extension) and `chrome.scripting` +(which executes extension-owned code programmatically). + +**Use userScripts when:** you are building a script manager, custom automation tool, or any +feature where the user supplies JavaScript that should run on web pages. +**Use content scripts when:** the injected code is written by you and ships with the extension. +**Use `chrome.scripting.executeScript` when:** you need one-off execution of known, extension-owned code. + +## Manifest + +```json +{ + "manifest_version": 3, + "minimum_chrome_version": "120", + "permissions": ["userScripts"], + "host_permissions": ["https://example.com/*"] +} +``` + +- `"userScripts"` permission is required — without it `chrome.userScripts` is undefined. +- `host_permissions` must cover the sites where scripts will be injected. + +## User Enablement — CRITICAL + +**The `chrome.userScripts` API requires explicit user opt-in. Without it, the API throws on +property access.** Behavior differs by Chrome version: + +| Chrome version | Requirement | +|----------------|-------------| +| < 138 | User must enable **Developer mode** at `chrome://extensions` | +| ≥ 138 | User must toggle **"Allow User Scripts"** on the extension's details page | + +```js +// ❌ BROKEN — crashes if user hasn't enabled the API +await chrome.userScripts.register([{ id: 'foo', matches: [...], js: [...] }]); +// TypeError: Cannot read properties of undefined + +// ✅ CORRECT — always guard before any chrome.userScripts.* call +function isUserScriptsAvailable() { + try { + chrome.userScripts; // throws if not enabled + return true; + } catch { + return false; + } +} + +if (!isUserScriptsAvailable()) { + document.getElementById('warning').style.display = 'block'; + document.getElementById('main-ui').style.display = 'none'; + return; +} +``` + +## Execution Worlds + +| World | Constant | Behavior | +|-------|----------|----------| +| **USER_SCRIPT** (default) | `ExecutionWorld.USER_SCRIPT` | Isolated from host page JS; exempt from page CSP | +| **MAIN** | `ExecutionWorld.MAIN` | Shares JS context with the host page; can access page variables | + +Use `USER_SCRIPT` (the default) for safety. Use `MAIN` only when the user's script explicitly +needs to interact with the host page's JavaScript environment. + +Chrome 133+ supports multiple isolated worlds via `worldId`, letting different user scripts +run in separate execution environments without interfering with each other. + +## Registering Scripts (Persistent) + +`register()` / `update()` / `getScripts()` / `unregister()` manage scripts that persist across +page loads and browser sessions. Registered scripts survive the service worker being terminated. + +```js +// ❌ BROKEN — both code and file specified (ScriptSource must have exactly one) +await chrome.userScripts.register([{ + id: 'my-script', + matches: ['https://example.com/*'], + js: [{ code: 'alert(1)', file: 'user-script.js' }] // Error: specify code OR file, not both +}]); + +// ❌ BROKEN — id starts with underscore (reserved prefix) +await chrome.userScripts.register([{ id: '_my-script', ... }]); +// Error: User script IDs must not start with '_' + +// ✅ CORRECT — register from a file bundled with the extension +await chrome.userScripts.register([{ + id: 'my-user-script', + matches: ['https://example.com/*'], + js: [{ file: 'user-script.js' }], + runAt: 'document_idle' // optional, this is the default +}]); + +// ✅ CORRECT — register inline code supplied by the user +await chrome.userScripts.register([{ + id: 'my-user-script', + matches: ['https://example.com/*'], + js: [{ code: userProvidedCode }] +}]); +``` + +Always check before calling `register()` vs `update()` — registering an already-existing ID +throws an error: + +```js +const existing = await chrome.userScripts.getScripts({ ids: ['my-user-script'] }); +if (existing.length > 0) { + await chrome.userScripts.update([{ id: 'my-user-script', js: [{ code: updatedCode }] }]); +} else { + await chrome.userScripts.register([{ + id: 'my-user-script', + matches: ['https://example.com/*'], + js: [{ code: userProvidedCode }] + }]); +} + +// Remove a specific script +await chrome.userScripts.unregister({ ids: ['my-user-script'] }); + +// Remove all registered user scripts +await chrome.userScripts.unregister(); +``` + +## Persistence: Restore on Extension Update + +**Registered user scripts are cleared when the extension updates.** + +```js +// ❌ BROKEN — scripts registered once at install time are lost on every update +chrome.runtime.onInstalled.addListener(({ reason }) => { + if (reason === 'install') { + chrome.userScripts.register([{ id: 'foo', matches: [...], js: [...] }]); + // After the next extension update, 'foo' is gone — no re-registration + } +}); + +// ✅ CORRECT — persist configs in storage, restore on both install and update +chrome.runtime.onInstalled.addListener(async ({ reason }) => { + if (reason === chrome.runtime.OnInstalledReason.UPDATE) { + const { scripts = {} } = await chrome.storage.local.get('scripts'); + for (const s of Object.values(scripts)) { + await chrome.userScripts.register([s]).catch(() => {}); + } + } +}); +``` + +Save the full script config (id, matches, js) to `chrome.storage` whenever the user saves +a script, so it can be restored after an update. + +## One-Off Injection (Chrome 135+) + +`execute()` injects a script immediately into a specific tab/frame without registering it. It +does not persist across page loads. + +```js +// Requires Chrome 135+ +const results = await chrome.userScripts.execute({ + target: { tabId: tabId }, + js: [{ code: userProvidedCode }], + world: 'USER_SCRIPT' // optional, default +}); + +for (const result of results) { + if (result.error) { + console.error('Injection failed in frame', result.frameId, result.error); + } else { + console.log('Result from frame', result.frameId, result.result); + } +} +``` + +Guard for Chrome 135+ availability: + +```js +if (typeof chrome.userScripts.execute === 'function') { + await chrome.userScripts.execute({ ... }); +} else { + // Fall back to register-based approach +} +``` + +## Messaging from User Scripts + +User scripts run in the `USER_SCRIPT` world which has no access to `chrome.runtime` by +default. Messaging requires an explicit opt-in. + +```js +// ❌ BROKEN — messaging not enabled; chrome.runtime is undefined in the user script +// sw.js +chrome.runtime.onMessage.addListener((msg) => { ... }); // Never fires from user scripts + +// user script code (running in the page) +chrome.runtime.sendMessage({ type: 'hello' }); // TypeError: chrome is not defined + +// ✅ CORRECT — opt in first, then use the dedicated listener +// sw.js (run once, e.g. on install) +await chrome.userScripts.configureWorld({ messaging: true }); + +// sw.js — listen on the dedicated handler, not onMessage +chrome.runtime.onUserScriptMessage.addListener((message, sender, sendResponse) => { + sendResponse({ ok: true }); + return true; // keep channel open for async responses +}); + +// user script code — chrome.runtime is now available +chrome.runtime.sendMessage({ type: 'FROM_USER_SCRIPT', data: 42 }); +``` + +For long-lived connections use `chrome.runtime.onUserScriptConnect` (analogous to +`runtime.onConnect` for content scripts). + +Chrome 133+ supports per-world messaging with `worldId`: + +```js +await chrome.userScripts.configureWorld({ worldId: 'my-world', messaging: true }); +``` + +## `configureWorld()` — CSP and Messaging + +```js +await chrome.userScripts.configureWorld({ + csp: "script-src 'self'", // custom CSP for the world + messaging: true // enable chrome.runtime messaging +}); + +// Chrome 133+: configure a named world +await chrome.userScripts.configureWorld({ + worldId: 'my-world', + messaging: true +}); + +// Chrome 133+: reset a world's configuration +await chrome.userScripts.resetWorldConfiguration('my-world'); + +// Chrome 133+: list all world configurations +const configs = await chrome.userScripts.getWorldConfigurations(); +``` + +## Complete Script Manager Pattern + +```js +// options.js — save user's script and (re)register it +async function saveScript(id, matches, code) { + // Persist the config so we can restore it after extension updates + const { scripts = {} } = await chrome.storage.local.get('scripts'); + scripts[id] = { id, matches, js: [{ code }] }; + await chrome.storage.local.set({ scripts }); + + // Register or update the live script + const existing = await chrome.userScripts.getScripts({ ids: [id] }); + if (existing.length > 0) { + await chrome.userScripts.update([{ id, matches, js: [{ code }] }]); + } else { + await chrome.userScripts.register([{ id, matches, js: [{ code }] }]); + } +} +``` + +## Key Differences from Content Scripts + +| | Content Scripts | userScripts | +|--|-----------------|-------------| +| Code source | Bundled with extension | Provided by user at runtime | +| Persistence | Automatic (manifest) | Manual (register + restore on update) | +| Arbitrary code | No | Yes | +| User opt-in | No | Yes (Developer Mode / Allow User Scripts) | +| Messaging | `runtime.sendMessage` | `runtime.onUserScriptMessage` (after `configureWorld`) | +| CSP exemption | Yes | Yes (USER_SCRIPT world) | +| Multiple worlds | No | Yes (Chrome 133+ with `worldId`) | diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/chromewebstore-template.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/chromewebstore-template.md new file mode 100644 index 00000000..976e9a57 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/chromewebstore-template.md @@ -0,0 +1,178 @@ +# CHROMEWEBSTORE.md Template + +Copy this template into the project root as `CHROMEWEBSTORE.md` and fill in each section. +Fields marked `[REQUIRED]` must be completed before submission. Fields marked `[RECOMMENDED]` +are optional but improve listing quality and approval odds. + +--- + +```markdown +# Chrome Web Store Listing — [Extension Name] + +> Last Updated: YYYY-MM-DD + +## Store Listing + +**Extension Name** [REQUIRED] + + + +**Short Description** [REQUIRED] + + + +**Detailed Description** [REQUIRED] + + + +**Category** [REQUIRED] + + + +**Single Purpose** [REQUIRED] + + + +**Primary Language** [REQUIRED] + + + +## Graphics & Assets + +| Asset | Dimensions | Status | Filename | +|-------|-----------|--------|----------| +| Store Icon [REQUIRED] | 128×128 PNG | ⬜ Not created | | +| Screenshot 1 [REQUIRED] | 1280×800 or 640×400 | ⬜ Not created | | +| Screenshot 2 [RECOMMENDED] | 1280×800 or 640×400 | ⬜ Not created | | +| Screenshot 3 [RECOMMENDED] | 1280×800 or 640×400 | ⬜ Not created | | +| Screenshot 4 | 1280×800 or 640×400 | ⬜ Not created | | +| Screenshot 5 | 1280×800 or 640×400 | ⬜ Not created | | +| Small Promo Tile [RECOMMENDED] | 440×280 | ⬜ Not created | | +| Marquee Promo Tile | 1400×560 | ⬜ Not created | | + + + +### Screenshot Notes + + + +## Permissions Justification + + + +| Permission | Type | Justification | +|------------|------|---------------| +| | permissions | | +| | host_permissions | | + + + + +## Privacy & Data Use + + + +### Data Collection + +**Does the extension collect user data?** Yes / No + + + +| Data Type | Collected? | Transmitted Off-Device? | Purpose | Shared with Third Parties? | +|-----------|-----------|------------------------|---------|---------------------------| +| Personally identifiable info | | | | | +| Health info | | | | | +| Financial info | | | | | +| Authentication info | | | | | +| Personal communications | | | | | +| Location | | | | | +| Web history | | | | | +| User activity | | | | | +| Website content | | | | | + +### Data Use Certification + +- [ ] Data is NOT sold to third parties +- [ ] Data is NOT used for purposes unrelated to the extension's core functionality +- [ ] Data is NOT used for creditworthiness or lending purposes + + +## Privacy Policy + +**Privacy Policy URL** [REQUIRED] + + + + +## Distribution + +**Visibility**: Public / Unlisted / Private +**Regions**: All regions / [List specific regions] + +## Developer Info + +**Publisher Name** [REQUIRED] + +**Contact Email** [REQUIRED] + + +**Support URL / Email** [RECOMMENDED] + + +**Homepage URL** [RECOMMENDED] + + +## Version History + + + +| Version | Date | Changes | Status | +|---------|------|---------|--------| +| | | | Draft | + + + + +## Review Notes + + + +### Known Issues / Limitations + + + +### Rejection History + + +``` diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/privacy-policy.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/privacy-policy.md new file mode 100644 index 00000000..77153760 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/privacy-policy.md @@ -0,0 +1,128 @@ +# Privacy Policy Guidance + +## When is a Privacy Policy Required? + +A privacy policy URL is **required** if your extension: +- Handles personal or sensitive user data (as defined by CWS policies) +- Uses any of these permissions: `identity`, `cookies`, `webRequest`, `browsingData`, + `history`, `bookmarks`, `topSites`, `` host permission +- Collects any form of analytics or telemetry +- Transmits any data off the user's device + +A privacy policy is **recommended** for all extensions, even if no data is collected. +It demonstrates professionalism and can prevent delays if a reviewer flags your extension. + +## Where to Host It + +The privacy policy must be at a publicly accessible URL. Options: +- **GitHub Pages**: Free, version-controlled. Create a `privacy.md` in a `docs/` branch. +- **GitHub Gist**: Quick and dirty. Create a public gist and link to the raw URL. +- **Project website**: If you have one, add a `/privacy` page. +- **Notion / Google Sites**: Free hosted pages. Stable URLs. + +Avoid hosting on a URL that might go down or change. The CWS review team checks the link. + +## What to Include + +### Minimal Policy (No Data Collection) + +If your extension genuinely collects no data, the policy can be short: + +``` +Privacy Policy for [Extension Name] + +Last updated: [Date] + +[Extension Name] does not collect, store, or transmit any personal data or +browsing information. All data stays on your device. + +This extension does not use cookies, analytics, or third-party services. + +If you have questions, contact [email]. +``` + +### Standard Policy (Some Data Collection) + +If your extension stores or transmits data, cover these topics: + +1. **What data is collected** — Be specific. "User preferences" is not enough. + Say "Your selected theme preference (light/dark) and saved highlight colors." + +2. **How data is stored** — Local storage only? Synced via chrome.storage.sync? + Sent to a server? + +3. **Why data is collected** — Tie each data type to a specific feature. + +4. **Third-party services** — If you use any APIs (analytics, auth, etc.), name them + and link to their privacy policies. + +5. **Data sharing** — State whether data is shared with third parties. If yes, with + whom and why. If no, say so explicitly. + +6. **Data retention** — How long is data kept? Can the user delete it? + +7. **User controls** — How can users access, export, or delete their data? + If the extension has a "clear data" button, mention it. + +8. **Changes to the policy** — State that you'll update the policy if practices change + and how users will be notified. + +9. **Contact** — Email or URL for privacy questions. + +### Template + +``` +Privacy Policy for [Extension Name] + +Last updated: [Date] + +## What Data We Collect + +[Describe each type of data collected and the feature that requires it.] + +## How Data Is Stored + +[Describe storage mechanism — local only, synced, or server-side.] + +## How Data Is Used + +[Describe each use case. Tie to specific features.] + +## Third-Party Services + +[List any third-party services used. Link to their privacy policies. +If none, state "This extension does not use any third-party services."] + +## Data Sharing + +[State whether data is shared. If yes, with whom and why.] + +## Data Retention and Deletion + +[How long data is kept. How users can delete it.] + +## Changes to This Policy + +[How and when the policy may be updated. How users will be notified.] + +## Contact + +[Email or support URL for privacy inquiries.] +``` + +## Common Mistakes + +- **Policy doesn't match the data disclosure form**: The CWS data disclosure form and your + privacy policy must be consistent. If the form says "no data collected" but the policy + mentions analytics, you'll be rejected. + +- **Policy is too vague**: "We may collect some data" is not acceptable. Be specific. + +- **Dead link**: If your privacy policy URL returns a 404, the submission is auto-rejected. + Verify the link before submitting. + +- **Missing data types**: If your extension uses `chrome.storage.sync`, that data goes to + Google's servers — disclose this. If you make any `fetch()` calls, disclose what's sent. + +- **No contact information**: The CWS requires a way for users to reach you about privacy + concerns. Include an email address at minimum. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/review-checklist.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/review-checklist.md new file mode 100644 index 00000000..0ad374a3 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/review-checklist.md @@ -0,0 +1,145 @@ +# Pre-Publish Review Checklist + +Run through this checklist before every submission to the Chrome Web Store. Each item +corresponds to a common rejection reason or publishing failure. + +## Manifest & Package + +- [ ] **manifest_version is 3** — Manifest V2 is no longer accepted for new submissions. +- [ ] **Version bumped** — CWS rejects uploads with a version ≤ the currently published + version. Use semver: bump patch for fixes, minor for features, major for breaking + changes. +- [ ] **Name matches CHROMEWEBSTORE.md** — The `name` field in manifest.json must exactly + match what you put in the store listing. +- [ ] **Description in manifest ≤ 132 chars** — This is the short description shown in + chrome://extensions. It should match or be close to your CWS short description. +- [ ] **No unnecessary files in ZIP** — Exclude: `.git/`, `node_modules/`, `.env`, + `*.map`, test files, build configs, `CHROMEWEBSTORE.md` itself, `README.md`, + `.DS_Store`, `thumbs.db`. Use a build script or `.cws-ignore`-style exclusion. +- [ ] **ZIP under 2GB** — Maximum package size. Most extensions should be under 10MB. +- [ ] **No absolute file paths** — All paths in manifest.json must be relative. + +## Permissions + +- [ ] **Minimum permissions** — Only request what you need. `` is a red flag. + Use specific host_permissions like `*://*.example.com/*` when possible. +- [ ] **Every permission justified** — Check the Permissions Justification section in + CHROMEWEBSTORE.md. The CWS dashboard has a field for each permission — you'll need + to fill these in during submission. +- [ ] **activeTab preferred over tabs + ** — If you only need access to the + current tab when the user clicks your icon, `activeTab` is the right permission. +- [ ] **No unused permissions** — If you removed a feature that used a permission, remove + the permission from manifest.json too. Leftover permissions cause rejection. +- [ ] **host_permissions justified** — Explain which features need access to which domains + and why. + +## Store Listing Content + +- [ ] **Detailed description is specific** — Describes exactly what the extension does. + No vague marketing language. The review team reads this. +- [ ] **Single purpose is narrow** — One sentence that clearly states the primary function. + "Manages bookmarks into categorized folders" not "Productivity enhancement tool." +- [ ] **No misleading claims** — Don't claim features you don't have. Don't exaggerate + performance claims. +- [ ] **No keyword stuffing** — Don't repeat keywords unnaturally in the description. +- [ ] **No trademark violations** — Don't use other companies' names, logos, or trademarks + in your extension name, description, or screenshots unless you have authorization. +- [ ] **Contact email is valid** — The email shown on the listing must be monitored. Google + sends important notifications (takedowns, policy changes) to this address. + +## Graphics + +- [ ] **Store icon**: 128×128 PNG, no transparency issues, readable at small sizes. +- [ ] **At least 1 screenshot**: 1280×800 or 640×400 pixels. Shows the extension in action. +- [ ] **Screenshots are current** — Match the current version of the extension UI. Outdated + screenshots can trigger rejection. +- [ ] **No misleading screenshots** — Screenshots must accurately represent the extension. +- [ ] **No phone/tablet mockups** — Unless the extension actually works on those devices. +- [ ] **Small promo tile** (recommended): 440×280 PNG or JPEG. Used for featured placements. + +## Privacy & Compliance + +- [ ] **Data disclosure form matches reality** — The CWS data use disclosure checkboxes + must accurately reflect what the extension code actually does. Mismatch = rejection. +- [ ] **Privacy policy URL is live** — Visit the URL yourself. Confirm it loads and + contains an actual privacy policy, not a 404 or placeholder. +- [ ] **Privacy policy matches disclosure** — The text of the policy must be consistent + with what you declared in the disclosure form. +- [ ] **chrome.storage.sync disclosed** — If you use `chrome.storage.sync`, data is + transmitted to Google's servers. This counts as off-device transmission. +- [ ] **Remote code prohibition** — Extensions cannot execute remotely hosted code. + All JS must be bundled in the extension package. No loading scripts from CDNs + at runtime (Manifest V3 enforces this, but verify). +- [ ] **No obfuscated code** — Minification is fine. Obfuscation (intentionally making + code unreadable) is prohibited and will cause rejection. + +## Functionality + +- [ ] **Extension works** — Load it unpacked in Chrome, test all features. Check the + console for errors. +- [ ] **No crashes or blank popups** — Test popup, side panel, options page, content + scripts. All should load without errors. +- [ ] **Works on intended sites** — If the extension targets specific websites, verify + it works on current versions of those sites. +- [ ] **Graceful degradation** — The extension should handle edge cases (no internet, + empty data, restricted pages like chrome:// URLs) without crashing. +- [ ] **Uninstall is clean** — No persistent side effects after the extension is removed. +- [ ] **No excessive resource use** — The extension shouldn't noticeably slow down + browsing. Content scripts in particular should be lightweight. + +## Updates (for existing extensions) + +- [ ] **CHROMEWEBSTORE.md version history updated** — New entry with version, date, and + summary of changes. +- [ ] **Last Updated date bumped** — If any user-facing changes were made. +- [ ] **Feature list in descriptions updated** — If new features were added. +- [ ] **Permissions justification updated** — If manifest.json permissions changed. +- [ ] **Screenshots refreshed** — If the UI changed significantly. +- [ ] **Privacy disclosures updated** — If data practices changed. + +## Packaging Script + +To create a clean ZIP for submission, use a script like: + +```bash +#!/bin/bash +# package-extension.sh — Creates a clean ZIP for Chrome Web Store submission + +EXTENSION_NAME="my-extension" +VERSION=$(node -p "require('./manifest.json').version") +OUTPUT="${EXTENSION_NAME}-v${VERSION}.zip" + +# Remove old package +rm -f "$OUTPUT" + +# Create ZIP excluding dev files +zip -r "$OUTPUT" . \ + -x ".git/*" \ + -x "node_modules/*" \ + -x ".env" \ + -x "*.map" \ + -x "tests/*" \ + -x "__tests__/*" \ + -x "*.test.*" \ + -x "*.spec.*" \ + -x ".eslintrc*" \ + -x ".prettierrc*" \ + -x "tsconfig.json" \ + -x "package.json" \ + -x "package-lock.json" \ + -x "webpack.config.*" \ + -x "vite.config.*" \ + -x "rollup.config.*" \ + -x "CHROMEWEBSTORE.md" \ + -x "README.md" \ + -x "CHANGELOG.md" \ + -x ".DS_Store" \ + -x "Thumbs.db" \ + -x "*.sh" \ + -x "store-assets/*" + +echo "Packaged: $OUTPUT ($(du -h "$OUTPUT" | cut -f1))" +``` + +Customize the exclusion list for your project. The key principle: ship only what Chrome +needs to run the extension. diff --git a/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/store-listing.md b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/store-listing.md new file mode 100644 index 00000000..46c75037 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/chrome-extensions/references/webstore/store-listing.md @@ -0,0 +1,201 @@ +# Store Listing Tips & Common Rejections + +## Writing Effective Descriptions + +### Short Description (132 chars max) + +This appears in search results and category pages. It's your elevator pitch. Rules: + +- Start with a verb or the extension's function: "Blocks ads on all websites" not "Ad blocker" +- Be specific: "Translates selected text into 50+ languages" not "Translation tool" +- Include the primary keyword naturally +- Don't waste characters on "Chrome extension" — the user already knows + +**Good examples:** +- "Save articles to read later with one click. Works offline." +- "Replace new tab with a minimal dashboard showing weather and tasks" +- "Highlight and annotate text on any webpage. Export notes as Markdown." + +**Bad examples:** +- "The best productivity tool for Chrome!" (vague, marketing-speak) +- "Extension for helping you do things better" (says nothing) +- "NEW! Amazing tab manager extension tool app for Chrome browser" (keyword stuffing) + +### Detailed Description (16,000 chars max) + +The CWS strips all markdown formatting. Use plain text with line breaks. Structure: + +``` +[One sentence: what does this extension do?] + +FEATURES +• Feature 1 — brief explanation +• Feature 2 — brief explanation +• Feature 3 — brief explanation + +HOW TO USE +1. Click the extension icon in the toolbar +2. [Next step] +3. [Next step] + +PRIVACY +This extension does not collect any personal data. Your [data type] is stored +locally on your device and never transmitted to any server. + +PERMISSIONS +• "Read and change data on sites you visit" — needed to [specific feature]. + The extension only activates when you [trigger action]. + +SUPPORT +Found a bug? Have a suggestion? Email [email] or open an issue at [URL]. + +Version [X.Y.Z] — [Brief changelog for latest version] +``` + +### The Implementation-Detail Rule + +**Never describe how the extension is built.** Potential users are not developers evaluating your stack — they want to know what the extension does for them. + +Strip all of the following from every piece of copy: + +- Web API names: `MutationObserver`, `IntersectionObserver`, `Service Worker`, `Shadow DOM`, `IndexedDB`, `WebSockets` +- Chrome API names: `chrome.storage`, `declarativeNetRequest`, `chrome.scripting`, `offscreen document` +- Framework/library names: React, custom elements, Lit, Webpack, TypeScript +- Architecture descriptions: "background processing", "event-driven", "declarative" + +**Transform every implementation sentence into a user benefit:** + +| Before (implementation) | After (user benefit) | +|-------------------------|----------------------| +| "Uses a MutationObserver to detect page changes" | "Automatically detects new content as you browse" | +| "Built with custom elements and Shadow DOM" | "Works seamlessly without affecting page styles" | +| "Powered by a service worker" | "Runs quietly in the background" | +| "Your settings are synced via chrome.storage.sync" | "Your settings sync across all your devices" | +| "Implements declarativeNetRequest for filtering" | "Blocks ads and trackers without reading your page content" | + +### Why This Structure Works + +1. **One-sentence opener** — The reviewer and users both scan the first line. Make it count. +2. **Features list** — Users scan for capabilities. Plain-text bullets (•) render well. +3. **How to use** — Reduces support requests and proves the extension actually works. +4. **Privacy section** — Pre-empts user concerns about permissions. Builds trust. +5. **Permissions explanation** — Users see permission warnings during install. If you + explain them in the description, they're less likely to abort installation. +6. **Support info** — Required by CWS policy ("meaningful customer support"). +7. **Latest version note** — Shows the extension is actively maintained. + +### Single Purpose Statement + +This is filled in the developer dashboard, not shown to users. The review team reads it +carefully. It must be a single sentence that describes the extension's narrow purpose. + +**Approved examples:** +- "Saves highlighted text from web pages to a local reading list" +- "Replaces the new tab page with a customizable dashboard" +- "Blocks cookie consent banners on websites" + +**Rejected examples:** +- "Improves your browsing experience" (too vague) +- "Productivity and organization tool" (too broad) +- "Highlights text, saves bookmarks, manages tabs, and blocks ads" (not single purpose) + +If your extension does multiple things, focus on the primary function. The detailed +description can cover secondary features. + +## Common Rejection Reasons + +### 1. Excessive Permissions + +**Symptom:** "Your extension requests more permissions than it needs." + +**Fix:** +- Replace `` with specific host patterns +- Replace `tabs` with `activeTab` if you only need the current tab on click +- Remove permissions you're not using +- Ensure every permission has a clear justification + +### 2. Missing or Inadequate Single Purpose + +**Symptom:** "Your item does not have a single, clear purpose." + +**Fix:** +- Rewrite the single purpose field to be narrow and specific +- If the extension truly does too many unrelated things, consider splitting it + +### 3. Misleading Description or Functionality + +**Symptom:** "Your extension does not provide the functionality described." + +**Fix:** +- Ensure every feature listed in the description actually works +- Remove claims about features you haven't built yet +- Don't use superlatives ("the best", "the fastest") unless verifiable + +### 4. Privacy Policy Issues + +**Symptom:** "Your extension requires a privacy policy." or "Your privacy policy URL +is not accessible." + +**Fix:** +- Host the privacy policy at a stable, public URL +- Ensure it's not behind a login wall +- Make sure it covers all data the extension actually collects +- Match the privacy policy with the data disclosure form + +### 5. Trademark Violation + +**Symptom:** "Your extension uses trademarked content without authorization." + +**Fix:** +- Don't use other companies' names in your extension name (e.g., "YouTube Downloader") +- Don't use logos or brand colors that imply affiliation +- Use generic terms: "Video Downloader for [site]" might be fine, but check the site's terms + +### 6. Code Readability + +**Symptom:** "Your extension contains obfuscated code." + +**Fix:** +- Minification is allowed; obfuscation is not +- If using a bundler (webpack, rollup, vite), ensure source maps are NOT included but + the output is minified, not obfuscated +- Don't use string encoding tricks to hide code intent + +### 7. Remote Code Execution + +**Symptom:** "Your extension executes remotely hosted code." + +**Fix:** +- Bundle all JavaScript in the extension package +- Don't load scripts from CDNs at runtime +- Don't use `eval()` or `new Function()` with remote content +- Fetching JSON data from APIs is fine; fetching and executing JS is not + +### 8. User Data Disclosure Mismatch + +**Symptom:** "Your extension's data usage does not match your disclosure." + +**Fix:** +- Audit every `fetch()`, `XMLHttpRequest`, and `chrome.storage.sync` call +- Remember that `chrome.storage.sync` transmits data to Google's servers +- If you use any analytics library (even self-hosted), declare it +- If you log errors to an external service, declare it + +## After Rejection + +When an extension is rejected: + +1. Read the rejection email carefully — it specifies which policy was violated +2. Update CHROMEWEBSTORE.md with the rejection reason and fix +3. Make the required changes to the extension code or listing +4. Re-verify against the pre-publish checklist +5. Resubmit through the developer dashboard +6. Note: Repeated policy violations can result in account suspension + +## Review Timeline + +- First submission: typically 1–3 business days, can be longer +- Updates to existing extensions: usually faster, often within 24 hours +- Expedited review: not officially available; maintaining a clean track record helps +- Deferred publishing: you can choose to publish manually after review passes, + giving you control over timing. Must publish within 30 days of approval. diff --git a/plugins/modern-web-guidance/.agents/skills/modern-web-guidance/SKILL.md b/plugins/modern-web-guidance/.agents/skills/modern-web-guidance/SKILL.md new file mode 100644 index 00000000..f4e0dbce --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/modern-web-guidance/SKILL.md @@ -0,0 +1,136 @@ +--- +name: modern-web-guidance +description: | + Search tool for modern web development best practices. MANDATORY: Execute FIRST for all HTML/CSS and clientside JS tasks. Do NOT skip — web APIs evolve rapidly and training weights contain obsolete patterns. + + Trigger immediately for: + - UI/Layout: Modals/dialogs/popovers, tables, dark mode, anchor positioning, container queries, `:has()`, etc. + - Scroll/Motion: View Transitions, Scroll-driven animations, parallax/reveals, etc. + - Performance: CWV (LCP, INP), content-visibility, Fetch Priority, image optimization, etc. + - System/APIs: Security, passkeys, HTML sanitization, Web Components, dates/timezones, Built-in AI, WebAssembly, etc. + - Frameworks: Standard HTML/CSS/DOM in React, Vue, Angular, etc. + - General Frontend: Forms, validation (`:user-valid`), autofill, accessibility, custom selects, scrollbars, etc. + + DO NOT trigger for: + - Backend: SQL, ORMs, API routes, etc. + - Infra/Scripts: CI/CD, Docker, Actions, Python/Go, Git, etc. + - Tooling/Trivial: Vite, tsconfig, ESLint, tests, copy/renames, 3P docs, etc. +--- + +# Modern Web Guidance + +A skill to search for specific web development use cases and retrieve their corresponding best practice guides. + +## When to use + +Must use this skill: +- At the **start** of implementing any web UI, CSS layout/animation, or client-side JS logic (dates/timezones, sanitization, performance, accessibility)—including when fixing UX symptoms (e.g., janky interactions, layout shifts). +- Before creating or updating a component, to check if a standardized modern web pattern already exists. +- To avoid implementing ad-hoc solutions or loading large dependencies unnecessarily. + +## Usage Instructions + +### Step 1. Search Use Cases + +Run `modern-web-guidance` directly with `npx` using a **2–5 word action-oriented query** for a **single web platform capability or UI pattern**: +- **Keep queries focused and generic**: Describe the user-visible capability (` + `, optionally paired with the target HTML/CSS/JS feature name if known). Omit framework/library names (`React`, `Vue`, `Tailwind`, etc.) and app/domain nouns (product, route, or component/file names). Split multi-feature tasks into separate `search` calls. +- **Example**: Search `"light dismiss dialog"` or `"responsive data table"`, **not** `"dialog"` (too terse), `"responsive table dark mode"` (compound — split into two searches), or `"close React settings modal on outside click"` (framework/app noise). + +```sh +npx -y modern-web-guidance@latest search "" --skill-version 2026_10_16-6654652b +``` + +**Example Output**: +```json +[ + { + "id": "optimize-image-priority", + "description": "Optimize the loading priority of Largest Contentful Paint (LCP) candidate images.", + "category": "performance", + "featuresUsed": [ "Fetch priority" ], + "tokenCount": 985, + "similarity": 0.7289 + }, + { + "id": "defer-rendering-heavy-content", + "description": "Reduce rendering times in content-heavy web pages by deferring rendering for offscreen content.", + "category": "performance", + "featuresUsed": [ "content-visibility", "hidden=\"until-found\"" ], + "tokenCount": 1250, + "similarity": 0.6961 + } +] +``` + +> **Note**: Judge results by their `description` fields (not raw `similarity` scores): +> - **Match found**: Call `retrieve` directly—do not also run `list`. +> - **No match on a web UI/CSS/DOM behavior (or broad UI audit)**: If `search` returns `[]` or unrelated descriptions because your phrasing differed from the catalog (or your task spans multiple categories), run `npx -y modern-web-guidance@latest list` once to check if the pattern is covered under more general terminology. +> - **Topic not covered**: If no guide `description` directly matches your task, proceed with your implementation without retry loops or retrieving unrelated guides. + +--- + +### Step 2. Retrieve Best Practices + +Once you have relevant `id`(s) from the search results, call `retrieve` to get the full guide(s)—including both specific use-case guides that match your task and any broader discipline guide (e.g., `html`, `css`, `forms`, `accessibility`, `color`, `performance`, `security`) that applies. You can pass multiple IDs separated by commas. + +```sh +npx -y modern-web-guidance@latest retrieve "" +``` + +If the output is truncated, you must repeat the command but redirect to a file and read that file. + +**Example Output**: +`The markdown content of the guide describing implementation steps...` + +--- + +### Step 3. Verify Guidance Compliance + +When generating or modifying code, cross-check the implementation against the retrieved guide before concluding: +- **Applicable Guidance & Fallbacks**: Ensure the relevant modern patterns and necessary fallback strategies from the guide are correctly applied, without forcing unrequested features. +- **Task Fulfillment**: Confirm that the implementation fully satisfies the user's request. + +### Step 4. Disclose Skill Usage + +When you consult `modern-web-guidance` for a task, end the response where you deliver those changes with a `### Modern Web Guidance` section: +- **When retrieved guides shaped your code**: Write **one** bullet (1–2 sentences, ≤75 words) per newly applied `` in first-person voice explaining the non-obvious implementation choices, CSS/API overrides, or fallbacks `` required and the pitfall it prevented, anchored by a short verbatim quote of that specific rule or gotcha (never the guide's intro overview): + `- **\`\`**: ("").` + - **Never** guess ``s or quotes from memory after context compaction (re-run `search`/`retrieve` if needed), and credit **only** guide-backed changes from the current turn. +- **When searched/listed in this turn but zero guides applied**: Write one first-person sentence naming which topics or guides you checked in `modern-web-guidance` and why none applied (omit if any guide applied). + +## Using npx / pnpx + +- Prefer `pnpx` over `npx` if `pnpm` is available (note: `pnpx` does not use the `-y` flag). +- When requesting tool permissions, allowlist `npx -y modern-web-guidance@latest *` specifically (or `pnpx modern-web-guidance@latest *`), never bare `npx *` or `pnpx *`. +- IMPORTANT: on Windows, using `npx` may fail. Use `npx.cmd ...` instead. +- Fetching and running `modern-web-guidance` requires outbound network access. If running in a sandboxed, permission-gated, or approval-based environment (e.g., Codex, Claude Code), **proactively request approval/allowlisting for the command with network access BEFORE executing it the first time**, avoiding sandbox network timeouts. +- In sandboxed environments where `~/.npm` is read-only or restricted, set `NPM_CONFIG_CACHE=/tmp/npm-cache`. +- If the command hangs due to being offline, try running again in offline mode: `npx --offline …`. +- The `--skill-version` flag is used to determine if this SKILL.md is out of date. If it is, a warning message is logged to stderr. + +## Guidelines + +- Always search **first** to find the most relevant guides. +- These guides use vanilla HTML/CSS/JS; adapt them to your framework without dropping the modern feature: + - **Types**: Spread/cast newly shipped HTML attributes (`{...({ popover: 'hint', interestfor: id, commandfor: id, switch: '' } as any)}`, `[attr.*]`) rather than removing them or merging `@types/react`. + - **SSR & Bundlers**: Guard `window`/`HTMLElement` in SSR and load polyfills via bare `import('')` in client effects (or set Vite `build.target: 'esnext'` for top-level `await`)—never copy files from `node_modules`. + - **DOM, IDs & CSS**: Call `showModal()`/`moveBefore()` via refs on non-VDOM-managed nodes, generate per-instance IDs/`anchor-name`s (`useId()`), and keep `:root` and `@starting-style` top-level/unscoped. +- Do not hallucinate guides or ignore them; they represent the preferred local standard for the user's project. + +## Interpreting Browser Support & Fallbacks + +* **Default Behavior**: All guides assume **Baseline Widely available** features are safe to use without fallbacks. For features that are not Baseline widely available, you **MUST** follow the fallback recommendations in the guide, unless the user has specified a custom browser support policy. +* **Custom Policies**: If the user has already defined explicit browser support requirements, use the browser compatibility data in the guide to determine if a fallback can be safely ignored. + - For Baseline YYYY targets, a feature satisfies this target if its "Baseline since" date is <= YYYY. + - **Policy Examples**: + - _"Do not implement feature fallbacks."_ (for exploratory prototypes of the cutting-edge web) + - _"Safari 17.4+"_ (for internal tools targeting macOS or Tauri-based desktop apps) + - _"Never recommend or implement polyfills; if a Baseline Newly Available feature is required for core functionality, provide a lightweight custom fallback or redesign the approach."_ (to minimize bundle size and avoid technical debt) + - _"Assume a modern execution environment where Baseline Newly Available features can be used natively, provided they are strictly feature-detected and degrade gracefully."_ (for progressive enhancement strategies) +* **Reactive Policy Discovery**: Watch for environmental cues to suggest documenting a policy in CLAUDE.md or AGENTS.md. Suggest this if the developer: + - Mentions building for a restricted runtime (e.g., Electron or Tauri). + - Explicitly excludes specific targets (e.g., "we don't support Desktop Chrome"). + - Expresses hesitation about polyfill complexity, bundle size, or performance cost. + - Questions if a feature is safe to use without fallbacks. + + No defined policy format. This is an example: `**Browser Support:** Allow Newly Available features, but only adopt custom fallback code that adds <= 20 lines and does not require external dependencies.` diff --git a/plugins/modern-web-guidance/.agents/skills/modern-web-guidance/guides/accessibility/accessibility.md b/plugins/modern-web-guidance/.agents/skills/modern-web-guidance/guides/accessibility/accessibility.md new file mode 100644 index 00000000..00cb44a2 --- /dev/null +++ b/plugins/modern-web-guidance/.agents/skills/modern-web-guidance/guides/accessibility/accessibility.md @@ -0,0 +1,585 @@ +# Accessibility Coding Guidelines + +This guide provides actionable DOs and DON'Ts for AI coding agents to ensure web applications are accessible to all users, including those using assistive technologies. + +Keep these principles in mind throughout: + +- **Accessibility is the minimum, not the ceiling.** Conformance to standards is the floor; aim for genuine usability. +- **Patterns are use-case specific.** No checklist replaces real testing — including testing with disabled users — to confirm a given implementation is actually accessible in context. + +Contents: + +- [1. Content Navigability and Structure](#1-content-navigability-and-structure) + - [Landmarks](#landmarks) + - [Headings](#headings) + - [Lists](#lists) + - [Tables](#tables) + - [Skip links](#skip-links) + - [Code example: good use of landmarks, headings, lists, tables, and a skip link](#code-example-good-use-of-landmarks-headings-lists-tables-and-a-skip-link) +- [2. Semantic HTML and ARIA](#2-semantic-html-and-aria) + - [Prefer native HTML](#prefer-native-html) + - [ARIA roles and states](#aria-roles-and-states) +- [3. Accessible Names and Descriptions](#3-accessible-names-and-descriptions) + - [Naming mechanisms](#naming-mechanisms) + - [Choosing the name](#choosing-the-name) + - [Descriptions](#descriptions) + - [Code Example: Visually Hidden Utility](#code-example-visually-hidden-utility) +- [4. Document Metadata and Language](#4-document-metadata-and-language) + - [Language](#language) + - [Titles](#titles) + - [Iframes](#iframes) + - [Code Example: using the `` and `<iframe>` elements](#code-example-using-the-title-and-iframe-elements) +- [5. Keyboard and Focus Management](#5-keyboard-and-focus-management) + - [Tab order and focusability](#tab-order-and-focusability) + - [Keyboard handlers](#keyboard-handlers) + - [Focus indicators](#focus-indicators) + - [Target sizes](#target-sizes) +- [6. Alternate Text and Media](#6-alternate-text-and-media) + - [Images](#images) + - [Inline SVGs](#inline-svgs) + - [Complex images and charts](#complex-images-and-charts) + - [Audio and video](#audio-and-video) + - [CSS generated content](#css-generated-content) + - [Content Visibility Decision Matrix](#content-visibility-decision-matrix) +- [7. Forms and Input Controls](#7-forms-and-input-controls) + - [Labels and hints](#labels-and-hints) + - [Input assistance and validation](#input-assistance-and-validation) +- [8. Live Regions](#8-live-regions) + - [Live Region Urgency Table](#live-region-urgency-table) + - [When to announce](#when-to-announce-live-regions) + - [Managing announcements](#managing-announcements) +- [9. Color](#9-color) +- [10. Typography](#10-typography) +- [11. Motion and Preferences](#11-motion-and-preferences) +- [12. Modals and Native Dialogs](#12-modals-and-native-dialogs) + - [Native `<dialog>`](#native-dialog) + - [Custom overlays](#custom-overlays) +- [13. Testing Validations](#13-testing-validations) + - [Automated checks](#automated-checks) + - [Manual testing](#manual-testing) + +## 1. Content Navigability and Structure + +### Landmarks + +- **Place all content within landmarks:** Wrap each relevant part of the page in `<header>`, `<nav>`, `<main>`, `<aside>`, and `<footer>` elements so assistive-tech users can jump between regions. (1.3.6 Identify Purpose (Level AAA)) +- **Use the correct landmark:** Use the `<header>` element as a container for introductory content, the `<nav>` element as a container for site-wide or in-page navigation links, the `<main>` element as a container for the main content of the page, the `<aside>` element as a container for content that is tangentially related to the main content, and the `<footer>` element as a container for information about the related content, such as the author, copyright information, and links to related documents. (1.3.1 Info and Relationships (Level A)) +- **Use one `<main>` landmark per page:** Avoid using multiple `<main>` elements on a single page. (1.3.1 Info and Relationships (Level A)) +- **Provide distinguishable accessible names for navigation regions:** If a page contains multiple `<nav>` elements, apply the `aria-label` attribute to each element and set its value to a distinguishable accessible name for the region. For example: `<nav aria-label="Primary">`, `<nav aria-label="Secondary">`, and so on. (Best practice) +- **Don't overuse landmarks:** Too many landmarks dilute their value. In particular, avoid labeling a `<section>` (which turns it into a `region` landmark) — `region` should be a last resort when no other landmark fits. (Best practice) + +### Headings + +- **Structure main content with headings:** Use `<h1>`–`<h6>` elements sequentially so screen reader users get a navigable outline. (1.3.1 Info and Relationships (Level A)) +- **Mandatory - Don't use "fake" headings:** Never use `<div>` or `<span>` elements to look like headings without standard `<h1>`–`<h6>` elements. (1.3.1 Info and Relationships (Level A)) +- **Avoid nesting headings incorrectly:** Make sure that, if a sub-heading is visually less important than its parent heading, its heading level is lower in the heading hierarchy. (1.3.1 Info and Relationships (Level A)) +- **Use one `<h1>` element per page:** Use the `<h1>` element for the page's main heading. Having more than one `<h1>` element on a page makes it difficult for screen reader users to identify the actual main heading. (Best Practice) + - **Caveat:** If a page includes a modal dialog that is marked up using the `<dialog>` element, `role="dialog"`, or `role="alertdialog"`, it is fine to mark up its main heading using the `<h1>` element because the dialog is a new window that is separate from the page. +- **Avoid "skipped" heading levels:** Avoid jumping directly from (for example) `<h1>` → `<h4>` to avoid breaking the logical hierarchy. (Best Practice) +- **Avoid placing headings inside the `<summary>` element:** Some browsers still expose this element as a button, which removes all roles from its children including headings. If a heading must act as a disclosure trigger, use an accordion or a disclosure implemented with ARIA where the heading element wraps the button, rather than use the `<details>` and `<summary>` elements. (Best Practice) + +### Lists + +- **Use lists for repeated, contiguous content:** `<ul>`/`<ol>` give assistive tech a count of listitems up front and let users skip the entire list. (1.3.1 Info and Relationships (Level A)) +- **Use the correct list element:** Use the `<ul>` element for unordered lists where the order of the items is **not** important (such as a list of related products), and use the `<ol>` element for ordered lists where the order of the content **is** important (such as a list of instructions). (1.3.1 Info and Relationships (Level A)) +- **Don't use lists for non-list content:** Do not use list markup to mark up separate paragraphs of text, as screen readers will announce list information when they encounter lists, therefore leading to a screen reader hearing potentially confusing and redundant information about the related content. (1.3.1 Info and Relationships (Level A)) +- **Don't use lists for checkboxes and radio buttons:** When marked up correctly, screen readers will already announce the number of checkboxes or radio buttons, as well as the number of the currently focused checkbox / radio button in the set, even though there is no list markup. Additional list markup will lead to duplication. (1.3.1 Info and Relationships (Level A)) + +### Tables + +- **Do use the correct table elements:** Use the `<table>` element to identify the table, the `<caption>` element to provide a title and accessible name for the description, the `<tr>` element to identify individual rows in the table, and the `<th>` and `<td>` elements to identify header and data cells respectively. (1.3.1 Info and Relationships (Level A)) +- **Do use the `scope` attribute on `<th>` elements:** The `scope` attribute indicates whether the `<th>` element represents a row header (`<th scope="row">`) or a column header (`<th scope="col">`). (1.3.1 Info and Relationships (Level A)) +- **Mandatory - Don't use tables for layout:** Screen readers will announce redundant information about the layout, which may be confusing for screen reader users, while the use of tabular markup has an impact on the reading and keyboard focus order of content. Use CSS Grid/Flexbox for visual layouts instead. (1.3.1 Info and Relationships (Level A)) +- **Avoid overly-complex tables:** Spanned rows, spanned columns, nested tables, and expandable / collapsible rows can make sense visually and meet the related WCAG success criterion, but the overall structure can be disorienting and difficult to navigate for screen reader users. In such cases, break tables up into smaller tables that are easier and more efficient to navigate. (Best Practice) + +### Skip links + +- **Provide skip links** prior to repeated content like site headers with navigation or long/infinite lists, so that keyboard users can easily bypass them. (2.4.1 Bypass Blocks (Level A)) + - **Caveat:** Sometimes screen magnifiers do not follow the focus when a skip link’s target is not normally focusable. + The recommended workaround is to make the target programmatically focusable, but not a part of the tab order, using `tabindex="-1"`. + If the target contains focusable elements, this creates another usability issue as clicking within the target will always set the **sequential focus navigation start point** to the target rather than at a point between focusable elements. + A solution that satisfies both user expectations is to make the first meaningful element within the intended target the actual target of the skip link with `tabindex="-1"`, for example a heading (`<h1 id="content" tabindex="-1">`). + Beware that any content prior to such an element will be skipped, including other relevant content within the targeted section, and this creates usability gaps for other assistive tech users. +- **Avoid using skip links where they are not needed:** A set of pages whose only common element is a single link to the Home page does not need a skip link. (Best Practice) +- **Avoid overusing skip links:** A single skip link, or two skip links to cover the primary and secondary sets of common navigation links, is usually sufficient on a single page. Multiple unnecessary skip links add to the number of <kbd>Tab</kbd> stops to a page, which causes frustration for people who use the keyboard alone for navigation. (Best Practice) + +```html +<!-- Good: Skip to main content --> +<a href="#content" class="skip-link">Skip to main content</a> +<main id="content">...</main> +``` + +### Code Example: good use of landmarks, headings, lists, tables, and a skip link + +```html +<!-- Good: Semantic landmarks, heading hierarchy, skip link --> +<header> + <a href="#platform-dashboard" class="skip-link visually-hidden">Skip to content</a> + <nav aria-label="Primary"> + <ul> + <li><a href="/">Home</a></li> + </ul> + </nav> +</header> +<main> + <h1 id="platform-dashboard" tabindex="-1">Platform Dashboard</h1> + <section> + <h2>User Statistics</h2> + <table> + <caption>Monthly active users</caption> + <thead> + <tr> + <th scope="col">Month</th> + <th scope="col">Users</th> + </tr> + </thead> + <tbody> + <tr> + <td>January</td> + <td>12,000</td> + </tr> + </tbody> + </table> + <h3>Notes</h3> + <ul> + <li>Results do not include users without active subscriptions</li> + <li>Results are accurate as of 31 December 2025</li> + </ul> + </section> +</main> +``` + +## 2. Semantic HTML and ARIA + +### Prefer native HTML + +- **Prefer HTML elements and attributes to ARIA:** Avoid `<div role="button">` or `<a role="button">` and use `<button>` instead. Native HTML elements and attributes already have the correct role and behavior, including keyboard support, built-in by default. `<button>` already implies `role="button"`, `required` already implies `aria-required`, and no additional JavaScript is required to ensure the correct role and behavior is supported. (Best Practice) + - **Caveat:** ARIA is required when the related feature is not available in HTML, for example there are no HTML elements explicitly intended for building a tabbed navigation widget, but ARIA includes appropriate role and state properties for this purpose. +- **Don't add redundant ARIA roles or properties:** Avoid `<ul role="list">`, `<nav role="navigation">`, and `<input required aria-required="true">`. (Best Practice) + - **Caveat:** Safari removes list semantics from `<ul>`/`<ol>` outside `<nav>` when `list-style: none` or `display: flex`/`grid` is applied. In that case `role="list"` is required to restore them. +- **Don't apply interactive ARIA roles to structural HTML elements:** Use `<li><span role="button">Button in a list</span></li>`, not `<li role="button">Button in a list</li>`, to avoid breaking the container list semantics. (4.1.2 Name, Role, Value (Level A)) + - **Caveat:** It is acceptable to apply ARIA roles to structural HTML elements that have similar semantics, for example a tree view: `<ul role="tree"><li role="treeitem">…</li>…</ul>`. + +### ARIA roles and states + +- **Match ARIA implementations to actual behavior:** When applying `role="tab"` to an element, it must then behave like a tab — including keyboard interactions. Many ARIA patterns can't be implemented in CSS alone and need JavaScript. (4.1.2 Name, Role, Value (Level A)) +- **Manage toggle states:** Utilize `aria-expanded` and `aria-pressed` to communicate toggle states for custom controls. (4.1.2 Name, Role, Value (Level A)) +- **Be deliberate about `disabled` vs `aria-disabled`:** `disabled` removes the element from the focus order entirely (and `tabindex="0"` won't bring it back), which is often wrong for toolbar buttons or links. `aria-disabled="true"` keeps the element focusable so users can land on it and learn it's disabled, but because it only communicates state to assistive technologies, you must also prevent activation (such as pointer clicks and keyboard triggers) in JavaScript. (4.1.2 Name, Role, Value (Level A)) +- **Mandatory - Don't hide interactive elements from screen readers:** Do not apply `aria-hidden="true"` to interactive elements that receive focus, or to their parent elements. (4.1.2 Name, Role, Value (Level A)) +- **Mandatory - Don't suppress the roles of interactive elements:** Applying `role="presentation"` or `role="none"` to an interactive element strips its semantics. The element will still be exposed to assistive technologies and announced by screen readers, but its role and any state information will not be, which results in confusion and potentially unexpected behavior. (4.1.2 Name, Role, Value (Level A)) +- **Don't assume custom elements have no ARIA:** Custom elements can attach ARIA via `ElementInternals`, which some automated test tools can't see — so the absence of `role`/`aria-*` attributes in markup doesn't prove the element has no semantics. Verify with the browser's accessibility-tree inspector. (Best Practice) + +## 3. Accessible Names and Descriptions + +Every interactive element and some landmarks need an accessible name, and many benefit from an accessible description. Names are short and identify the element; descriptions add context. + +### Naming mechanisms + +- **Prefer native naming mechanisms:** `<label>` for form controls, `<caption>` for `<table>`, `<legend>` for `<fieldset>`, `<figcaption>` for `<figure>`. (Best Practice) +- **Explicitly associate `<label>` with its control via `for`/`id`:** As well as ensuring the control is appropriately labeled by the contents of the `<label>` element, this results in a larger target area for people to activate the label _or_ its related control to move focus to the control, which isn't globally supported with other forms of labeling (such as using the `aria-labelledby` attribute). Additionally, some assistive technologies, particularly voice recognition software, require the `for`/`id` relationship to be explicitly set, even if an equivalent accessibility-supported approach (such as nesting an `<input>` inside a `<label>`) is taken. (4.1.2 Name, Role, Value (Level A)) +- **Prefer `aria-labelledby` over `aria-label` when a visible label exists:** This avoids duplication, improves maintainability, and translates better. (Best Practice) +- **Don't use `title` as a naming mechanism:** The value of this attribute is only displayed on mouse hover, and cannot be displayed or otherwise accessed using the keyboard in any browser. (Best Practice) + - **Caveat:** `<iframe>` is an exception, as the `title` attribute is the standard way of providing an accessible name for this element. +- **Don't use `placeholder` as a naming mechanism:** While the value of this attribute is initially displayed inside an `<input>` or `<textarea>`, it disappears as soon as the user enters text into the field, meaning that there is no longer a visual cue as to what the field represents. The contrast between `placeholder` text and its background in most browsers is very low, and therefore is difficult for people to perceive. (3.3.2 Labels or Instructions (Level A)) +- **Don't put `aria-label`/`aria-labelledby` on elements that shouldn't be named:** Examples include plain `<div>`, `<span>`, and other custom elements without a role. These attributes are only intended for providing accessible names for elements that have explicit or implicit widget (for example, button, link, and tab), landmark, image, or iframe roles. (1.3.1 Info and Relationships (Level A)) + - **Caveat:** Custom elements may have an implicit role set via `ElementInternals`, so the absence of a `role` attribute isn't conclusive. + +### Choosing the name + +- **Avoid inconsistent link text:** If multiple hyperlinks direct users to the same target page, make sure each link has the same accessible name, which should typically be the same as its visible link text. For example, if the target page for a link is `profile.html`, make sure that all links have an accessible name of "My Profile" **or** "Profile"; do not use "My Profile" in one instance, and "Profile" in the other. (Best Practice) +- **Disambiguate controls that look visually identical but which perform different actions by their accessible name:** For example, a page may include multiple "Edit" buttons, each of which triggers functionality to perform a different action, such as edit the user's name, edit the user's password, and edit the user's address. While it may be visually obvious from the surrounding content what the purpose of these buttons might be, screen reader users may only hear "Edit, button". Techniques to resolve this include the following: (4.1.2 Name, Role, Value (Level A)) + - **Use visually hidden text:** The text is included in the DOM but is visually hidden using CSS. For example `<button>Edit <span class="visually-hidden">first name</span></button>`. Refer to the [code example: visually hidden utility](#code-example-visually-hidden-utility) for the full CSS. + - **Apply the `aria-label` attribute to the control:** The value of this attribute should then include the visible text as well as important contextual information, for example `<button aria-label="Edit name">Edit</button>`, `<button aria-label="Edit address">Edit</button>`, and so on. + - **Use the `aria-labelledby` or `aria-describedby` attribute to associate the control with the content it relates to:** Chaining IDs in `aria-labelledby` appends the related text to the control's accessible name (for example, `<p id="firstName">First name</p> <button id="editFirstName" aria-labelledby="editFirstName firstName">Edit</button>`), whereas `aria-describedby` provides the context as an accessible description (`<p id="firstName">First name</p> <button aria-describedby="firstName">Edit</button>`). +- **Do include a control's visible text in its accessible name:** Any visible text used to label a control must be included exactly as written in the control's accessible name. The accessible name can include additional information before or after the visible text, but the visible text itself must not differ. For example, the accessible name for a link containing the link phrase "Read more" can be "Read more about [subject]", because the visible text is included without modification. However, the accessible name cannot be "Read some more about [subject]", because the term "some" appears between the words "Read" and "more", which is not reflected in the visible text. (2.5.3 Label in Name (Level A)) +- **Don't reuse an accessible name across controls with different effects in the same view:** Make sure that each accessible name is disambiguated. (4.1.2 Name, Role, Value (Level A)) + - **Caveat:** Having two separate buttons with the accessible name of "Close" in two separate dialog boxes is fine, because only one dialog box is reachable at a time; however, multiple "Edit" buttons for different effects in the same dialog box is not. +- **Don't reuse an accessible name across hyperlinks that point to different `href` targets:** For example, avoid `<a href="annualreport2025.html">Annual Report</a>` and `<a href="annualreport2026.html">Annual Report</a>`. Use `<a href="annualreport2025.html">Annual Report 2025</a>` and `<a href="annualreport2026.html">Annual Report 2026</a>` (or an equivalent technique for disambiguating controls) instead. +- **Don't pack descriptions, error messages, or instructions into the label:** Verbose labels confuse and frustrate people who use screen readers. Instead, use the `aria-describedby` attribute to programmatically associate any additional content with the field. For example: `<label for="yourComments">Your comments</label><p id="yourCommentsWarning">Please do not include any personal information in this field.</p><textarea id="yourComments" aria-describedby="yourCommentsWarning"></textarea>`. Screen readers will then announce the label first, followed by the control type, and only then announce additional information that a user can skip if they are already aware of it. (Best Practice) +- **Don't repeat state information already exposed by ARIA inside the label:** Examples include information exposed via the `aria-expanded`, `aria-checked`, `aria-selected`, `aria-pressed`, and similar attributes. This creates duplication, redundancy, and ambiguity. (Best Practice) +- **Don't include the role name in the label:** This will lead to duplication. For example, `<nav aria-label="Primary navigation">` will be read by screen readers as "Primary navigation, navigation". (Best Practice) + +### Descriptions + +- **Identify instructions, error messages, and any other important information explicitly related to a control as a description:** Examples of descriptions include: (1.3.1 Info and Relationships (Level A)) + - A list of password requirements when creating an account + - An error message related to a field in a previously submitted form + - Additional instructions, such as "You must agree to the terms and conditions before you can submit the form" +- **Make sure the description is available to assistive technologies when the related element receives focus:** Take one of the following approaches (1.3.1 Info and Relationships (Level A)): + - If the description is already visible on the page, programmatically associate it with the element using the `aria-describedby` attribute. + - If the description is not visible on the page and is explicitly for screen reader users (in other words, when providing a visible description is not appropriate or useful), apply the `aria-description` attribute to the element and use it to provide an appropriate description. +- **Avoid including a description for every element:** if the primary label already provides sufficient information (such as "First name"), any additional description is likely to be redundant. (Best Practice) + - **Caveat:** Inline error messages associated with controls, such as error messages below / above text fields, as well as secondary labels that should be announced when the element receives focus, are exceptions. +- **Avoid including information in the description that is already provided by attributes applied to the element:** For example, including "This is a required field" as a description for an `<input>` element that already includes the `required` attribute will lead to duplicate announcements. (Best Practice) + - **Caveat:** `aria-invalid="true"` is an exception; many screen readers announce "invalid" when they reach an `<input>` that includes this attribute set to `true`, which indicates that the field is in error. However, it does not indicate what the error is. In such cases, a description (the actual human readable error message) is required. This attribute should only be applied and set to true when the error message is present. +- **Avoid including interactive elements in an `aria-describedby` target** unless the content of the element reads sensibly as a description on its own. For example: (Best Practice) + - This is acceptable, as the entire text string makes sense when announced: `<p id="description">I understand the <a href="tandc.html">terms and conditions</a>.</p>` + - This is not appropriate, as the description will be announced as "Please don't include personal information in this field. Learn more.", where "Learn more" will be announced out of its intended context: `<p id="description">Please don't include personal information in this field. <a href="…">Learn more</a></p>`. Instead, only include the plain text in the referenced description: `<p><span id="description">Please don't include personal information in this field.</span> <a href="…">Learn more</a></p>`. + +### Code Example: Visually Hidden Utility + +A `.visually-hidden` utility provides text for screen readers without rendering it visually. It's commonly used for skip links, additional context on icon-only buttons, and supplementary labels. + +```css +/* Hides content visually but keeps it in the accessibility tree. + :focus-within / :active opt elements out — useful for skip links and + any focusable content wrapped in this class. */ +.visually-hidden:where(:not(:focus-within, :active)) { + position: absolute !important; + clip-path: inset(50%) !important; + overflow: hidden !important; + width: 1px !important; + height: 1px !important; + margin: -1px !important; + padding: 0 !important; + border: 0 !important; + white-space: nowrap !important; +} +``` + +When the hidden content is focusable (skip links, focus-receiving wrappers), the `:focus-within`/`:active` exception lets it become visible. Style the visible state per situation, e.g. a skip link to the main content typically wants fixed positioning at the top-left of the viewport so the rest of the page doesn't shift. + +## 4. Document Metadata and Language + +### Language + +- **Do declare the page's human language:** Always apply the `lang` attribute to the page's `<html>` element, and always set it to the primary human language of the page's content. For example, `<html lang="en">`. (3.1.1 Language of Page (Level A)) +- **Do declare changes in the page's human language:** If there is a language change on the page, apply the `lang` attribute to the parent/container element for the affected content, and set the value of this attribute to the shortcode for the language of the affected content. For example: `<p>In Germany, you may be greeted by someone saying <span lang="de">Guten Morgen</span>.</p>`. (3.1.2 Language of Parts (Level AA)) +- **Don't set the value of the `lang` attribute to a value that doesn't match the human language of the content:** This causes screen readers and other text-to-speech software to announce the content in a non-understandable manner. (3.1.1 Language of Page (Level A), 3.1.2 Language of Parts (Level AA)) + +### Titles + +- **Do include the `<title>` element in the `<head>` section for all pages:** This includes pages embedded into a document using an `<iframe>`. (2.4.2 Page Titled (Level A)) +- **Provide unique page titles:** Every page should have its own title that unambiguously reflects the page's purpose or content. (2.4.2 Page Titled (Level A)) +- **Front-load the page's `<title>` with unique content:** For example, `<title>Page Topic | Site Name`. (Best Practice) +- **Update single-page application (SPA) titles during page transitions:** For example, if the page title is `Contact Us` when a contact form is displayed, update the title to `Thank you for contacting us!` when the form is successfully submitted. (2.4.2 Page Titled (Level A)) +- **Don't include an empty `` element in the `<head>` section of a page:** No matter how short or inconsequential a page appears, do not use `<title>`. Page titles appear in the browser tab, so pages are distinguishable when users have multiple tabs open, and are typically announced by screen readers when the page is opened. (2.4.2 Page Titled (Level A)) +- **Avoid using the same title for multiple pages, or using ambiguous page titles:** In such cases, pages are indistinguishable without reviewing the actual page content itself. (2.4.2 Page Titled (Level A)) + +### Iframes + +- **Do provide a descriptive `title` for ` + + +``` + +## 5. Keyboard and Focus Management + +### Tab order and focusability + +- **Provide a logical tab order:** Make sure that the order in which elements receive focus follows an appropriate sequence. (2.4.3 Focus Order (Level A)) + - **Caveat:** In left-to-right reading languages, the tab order is logical when focus moves from top-to-bottom and left-to-right. However, in right-to-left reading languages, the tab order is logical when focus moves from top-to-bottom and right-to-left. +- **Use `tabindex` only when necessary:** Apply `tabindex="0"` to elements requiring focus where an appropriate interactive role has been applied but the element isn't focusable by default; for example, `Print` (2.4.3 Focus Order (Level A)) +- **Manage focus correctly to and from updated content:** For example, if a dialog box appears on the page, make sure that focus immediately moves to the dialog box, is restricted to the contents of the dialog box, and (when dismissed) returns to the element that originally triggered the dialog box. (2.4.3 Focus Order (Level A)) +- **Don't apply `tabindex="0"` to interactive elements that are already focusable by default:** Native elements like the `