diff --git a/assets/js/components/api-scalar.ts b/assets/js/components/api-scalar.ts
deleted file mode 100644
index 62161a214..000000000
--- a/assets/js/components/api-scalar.ts
+++ /dev/null
@@ -1,326 +0,0 @@
-/**
- * Scalar API Documentation Component
- *
- * Initializes the Scalar API reference viewer for OpenAPI documentation.
- * Features:
- * - Dynamic CDN loading of Scalar library
- * - Theme synchronization with site theme
- * - InfluxData brand colors
- * - Error handling and fallback UI
- *
- * Usage:
- *
- */
-
-import { getPreference } from '../services/local-storage.js';
-
-interface ComponentOptions {
- component: HTMLElement;
-}
-
-interface ScalarConfig {
- url: string;
- forceDarkModeState?: 'dark' | 'light';
- layout?: 'classic' | 'modern';
- showSidebar?: boolean;
- hideDarkModeToggle?: boolean;
- hideSearch?: boolean;
- documentDownloadType?: 'none' | 'yaml' | 'json';
- hideModels?: boolean;
- hideTestRequestButton?: boolean;
- withDefaultFonts?: boolean;
- customCss?: string;
-}
-
-type ScalarCreateFn = (
- selector: string | HTMLElement,
- config: ScalarConfig
-) => void;
-
-declare global {
- interface Window {
- Scalar?: {
- createApiReference: ScalarCreateFn;
- };
- }
-}
-
-const SCALAR_CDN = 'https://cdn.jsdelivr.net/npm/@scalar/api-reference@latest';
-
-/**
- * Load script dynamically
- */
-function loadScript(src: string, timeout = 8000): Promise {
- return new Promise((resolve, reject) => {
- // Check if script already exists
- const existing = Array.from(document.scripts).find(
- (s) => s.src && s.src.includes(src)
- );
- if (existing && window.Scalar?.createApiReference) {
- return resolve();
- }
-
- const script = document.createElement('script');
- script.src = src;
- script.defer = true;
- script.onload = () => resolve();
- script.onerror = () => reject(new Error(`Failed to load script: ${src}`));
-
- document.head.appendChild(script);
-
- // Fallback timeout
- setTimeout(() => {
- if (window.Scalar?.createApiReference) {
- resolve();
- } else {
- reject(new Error(`Timeout loading script: ${src}`));
- }
- }, timeout);
- });
-}
-
-/**
- * Get current theme from localStorage (source of truth for Hugo theme system)
- */
-function getTheme(): 'dark' | 'light' {
- const theme = getPreference('theme');
- return theme === 'dark' ? 'dark' : 'light';
-}
-
-/**
- * Poll for Scalar availability
- */
-function waitForScalar(maxAttempts = 50, interval = 100): Promise {
- return new Promise((resolve, reject) => {
- let attempts = 0;
-
- const checkInterval = setInterval(() => {
- attempts++;
-
- if (window.Scalar?.createApiReference) {
- clearInterval(checkInterval);
- resolve();
- } else if (attempts >= maxAttempts) {
- clearInterval(checkInterval);
- reject(
- new Error(`Scalar not available after ${maxAttempts * interval}ms`)
- );
- }
- }, interval);
- });
-}
-
-/**
- * Initialize Scalar API reference
- */
-async function initScalar(
- container: HTMLElement,
- specUrl: string
-): Promise {
- if (!window.Scalar?.createApiReference) {
- throw new Error('Scalar is not available');
- }
-
- // Clean up previous Scalar instance (important for theme switching)
- // Remove any Scalar-injected content and classes
- container.innerHTML = '';
- // Remove Scalar's dark-mode class from body if it exists
- document.body.classList.remove('dark-mode');
-
- const isDark = getTheme() === 'dark';
-
- window.Scalar.createApiReference(container, {
- url: specUrl,
- forceDarkModeState: getTheme(),
- layout: 'classic',
- showSidebar: false,
- hideDarkModeToggle: true,
- hideSearch: true,
- documentDownloadType: 'none',
- hideModels: false,
- hideTestRequestButton: false,
- withDefaultFonts: false,
- customCss: `
- :root {
- /* Typography - match Hugo docs site */
- --scalar-font: 'Proxima Nova', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
- --scalar-font-code: 'IBM Plex Mono', Monaco, Consolas, monospace;
- --scalar-font-size-base: 16px;
- --scalar-line-height: 1.65;
-
- /* InfluxData brand colors */
- --scalar-color-1: #F63C41;
- --scalar-color-2: #d32f34;
- --scalar-color-accent: #F63C41;
-
- /* Border radius */
- --scalar-radius: 4px;
- --scalar-radius-lg: 8px;
-
- /* Background and text colors - theme-aware */
- --scalar-background-1: ${isDark ? '#1a1a2e' : '#ffffff'};
- --scalar-background-2: ${isDark ? '#232338' : '#f7f8fa'};
- --scalar-background-3: ${isDark ? '#2d2d44' : '#f0f2f5'};
- --scalar-text-1: ${isDark ? '#e0e0e0' : '#2b2b2b'};
- --scalar-text-2: ${isDark ? '#a0a0a0' : '#545454'};
- --scalar-text-3: ${isDark ? '#888888' : '#757575'};
- --scalar-border-color: ${isDark ? '#3a3a50' : '#e0e0e0'};
-
- /* Heading colors */
- --scalar-heading-color: ${isDark ? '#ffffff' : '#2b2b2b'};
- }
-
- /* Match Hugo heading styles */
- h1, h2, h3, h4, h5, h6 {
- font-family: var(--scalar-font);
- font-weight: 600;
- color: var(--scalar-heading-color);
- line-height: 1.25;
- }
-
- h1 { font-size: 2rem; }
- h2 { font-size: 1.5rem; margin-top: 2rem; }
- h3 { font-size: 1.25rem; margin-top: 1.5rem; }
- h4 { font-size: 1rem; margin-top: 1rem; }
-
- /* Body text size */
- p, li, td, th {
- font-size: 1rem;
- line-height: var(--scalar-line-height);
- }
-
- /* Code block styling */
- pre, code {
- font-family: var(--scalar-font-code);
- font-size: 0.875rem;
- }
-
- /* Hide section-content div */
- div.section-content {
- display: none !important;
- }
- `,
- });
-
- console.log(
- '[API Docs] Scalar initialized with spec:',
- specUrl,
- 'theme:',
- getTheme()
- );
-}
-
-/**
- * Show error message in container
- */
-function showError(container: HTMLElement, message: string): void {
- container.innerHTML = `${message}
`;
-}
-
-/**
- * Watch for Hugo theme changes via stylesheet manipulation
- * Hugo theme.js enables/disables link[title*="theme"] elements
- */
-function watchThemeChanges(container: HTMLElement, specUrl: string): void {
- // Watch for stylesheet changes in the document
- const observer = new MutationObserver(() => {
- const currentTheme = getTheme();
- console.log('[API Docs] Theme changed to:', currentTheme);
- // Re-initialize Scalar with new theme
- initScalar(container, specUrl).catch((error) => {
- console.error(
- '[API Docs] Failed to re-initialize Scalar on theme change:',
- error
- );
- });
- });
-
- // Watch for changes to stylesheet link elements
- const head = document.querySelector('head');
- if (head) {
- observer.observe(head, {
- attributes: true,
- attributeFilter: ['disabled'],
- subtree: true,
- });
- }
-
- // Also watch for localStorage changes from other tabs
- window.addEventListener('storage', (event) => {
- if (event.key === 'influxdata_docs_preferences' && event.newValue) {
- try {
- const prefs = JSON.parse(event.newValue);
- if (prefs.theme) {
- const currentTheme = getTheme();
- console.log(
- '[API Docs] Theme changed via storage event to:',
- currentTheme
- );
- initScalar(container, specUrl).catch((error) => {
- console.error(
- '[API Docs] Failed to re-initialize Scalar on storage change:',
- error
- );
- });
- }
- } catch (error) {
- console.error(
- '[API Docs] Failed to parse localStorage preferences:',
- error
- );
- }
- }
- });
-}
-
-/**
- * Initialize API Scalar component
- */
-export default async function ApiScalar({
- component,
-}: ComponentOptions): Promise {
- try {
- // Get spec path from data attribute
- const specPath = component.dataset.specPath;
- const cdn = component.dataset.cdn || SCALAR_CDN;
-
- if (!specPath) {
- console.error('[API Docs] No OpenAPI specification path provided');
- showError(
- component,
- 'Error: No API specification configured for this page.'
- );
- return;
- }
-
- // Build full URL for spec (Scalar needs absolute URL)
- const specUrl = window.location.origin + specPath;
-
- // Load Scalar from CDN if not already loaded
- if (!window.Scalar?.createApiReference) {
- try {
- await loadScript(cdn);
- } catch (err) {
- console.error('[API Docs] Failed to load Scalar from CDN', err);
- }
- }
-
- // Wait for Scalar to be ready
- try {
- await waitForScalar();
- } catch (err) {
- console.error('[API Docs] Scalar failed to initialize', err);
- showError(component, 'Error: API viewer failed to load.');
- return;
- }
-
- // Initialize Scalar
- await initScalar(component, specUrl);
-
- // Watch for theme changes and re-initialize Scalar when theme changes
- watchThemeChanges(component, specUrl);
- } catch (err) {
- console.error('[API Docs] ApiScalar component error', err);
- showError(component, 'Error: API viewer failed to initialize.');
- }
-}
diff --git a/assets/js/main.js b/assets/js/main.js
index fb5e79b27..c703d6309 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -47,7 +47,6 @@ import { SidebarToggle } from './sidebar-toggle.js';
import Theme from './theme.js';
import ThemeSwitch from './theme-switch.js';
import ApiRapiDoc from './components/api-rapidoc.ts';
-import ApiScalar from './components/api-scalar.ts';
import ApiTabs from './components/api-tabs.ts';
import ApiToc from './components/api-toc.ts';
import RapiDocMini from './components/rapidoc-mini.ts';
@@ -83,7 +82,6 @@ const componentRegistry = {
theme: Theme,
'theme-switch': ThemeSwitch,
'api-rapidoc': ApiRapiDoc,
- 'api-scalar': ApiScalar,
'api-tabs': ApiTabs,
'api-toc': ApiToc,
'rapidoc-mini': RapiDocMini,
diff --git a/config/_default/hugo.yml b/config/_default/hugo.yml
index 12b0f0ad1..c576413cd 100644
--- a/config/_default/hugo.yml
+++ b/config/_default/hugo.yml
@@ -98,8 +98,6 @@ module:
params:
env: development
environment: development
- # API documentation renderer: "scalar" (default) or "rapidoc"
- apiRenderer: rapidoc
# Configure the server for development
server:
diff --git a/docs/plans/2024-12-12-api-code-review-fixes.md b/docs/plans/2024-12-12-api-code-review-fixes.md
new file mode 100644
index 000000000..6360c9f69
--- /dev/null
+++ b/docs/plans/2024-12-12-api-code-review-fixes.md
@@ -0,0 +1,779 @@
+# API Code Review Fixes Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Fix code review violations by extracting inline JavaScript from rapidoc.html into a TypeScript component and removing unused Scalar renderer code.
+
+**Architecture:** Create a new `api-rapidoc.ts` TypeScript component following the established component pattern (same as `rapidoc-mini.ts`). The component handles theme synchronization, shadow DOM manipulation, and MutationObserver setup. Remove the Scalar renderer, api-tabs component, and associated partials since they're no longer used.
+
+**Tech Stack:** TypeScript, Hugo templates, SCSS, Cypress
+
+***
+
+## Task 1: Create api-rapidoc.ts TypeScript Component
+
+**Files:**
+
+- Create: `assets/js/components/api-rapidoc.ts`
+
+**Step 1: Create the TypeScript component file**
+
+Create `assets/js/components/api-rapidoc.ts` with the following content:
+
+```typescript
+/**
+ * RapiDoc API Documentation Component
+ *
+ * Initializes the full RapiDoc renderer with theme synchronization.
+ * This is the component version of the inline JavaScript from rapidoc.html.
+ *
+ * Features:
+ * - Theme detection from Hugo's stylesheet toggle system
+ * - Automatic theme synchronization when user toggles dark/light mode
+ * - Shadow DOM manipulation to hide unwanted UI elements
+ * - CSS custom property injection for styling
+ *
+ * Usage:
+ *
+ *
+ * The component expects a element to already exist in the container
+ * (created by Hugo template) or will wait for it to be added.
+ */
+
+import { getPreference } from '../services/local-storage.js';
+
+interface ComponentOptions {
+ component: HTMLElement;
+}
+
+interface ThemeColors {
+ theme: 'light' | 'dark';
+ bgColor: string;
+ textColor: string;
+ headerColor: string;
+ primaryColor: string;
+ navBgColor: string;
+ navTextColor: string;
+ navHoverBgColor: string;
+ navHoverTextColor: string;
+ navAccentColor: string;
+ codeTheme: string;
+}
+
+type CleanupFn = () => void;
+
+/**
+ * Get current theme from localStorage (source of truth for Hugo theme system)
+ */
+function getTheme(): 'dark' | 'light' {
+ const theme = getPreference('theme');
+ return theme === 'dark' ? 'dark' : 'light';
+}
+
+/**
+ * Get theme colors matching Hugo SCSS variables
+ */
+function getThemeColors(isDark: boolean): ThemeColors {
+ if (isDark) {
+ return {
+ theme: 'dark',
+ bgColor: '#14141F', // $grey10 ($article-bg in dark theme)
+ textColor: '#D4D7DD', // $g15-platinum
+ headerColor: '#D4D7DD',
+ primaryColor: '#a0a0ff',
+ navBgColor: '#1a1a2a',
+ navTextColor: '#D4D7DD',
+ navHoverBgColor: '#252535',
+ navHoverTextColor: '#ffffff',
+ navAccentColor: '#a0a0ff',
+ codeTheme: 'monokai',
+ };
+ }
+
+ return {
+ theme: 'light',
+ bgColor: '#ffffff', // $g20-white
+ textColor: '#2b2b2b',
+ headerColor: '#020a47', // $br-dark-blue
+ primaryColor: '#020a47',
+ navBgColor: '#f7f8fa',
+ navTextColor: '#2b2b2b',
+ navHoverBgColor: '#e8e8f0',
+ navHoverTextColor: '#020a47',
+ navAccentColor: '#020a47',
+ codeTheme: 'prism',
+ };
+}
+
+/**
+ * Apply theme to RapiDoc element
+ */
+function applyTheme(rapiDoc: HTMLElement): void {
+ const isDark = getTheme() === 'dark';
+ const colors = getThemeColors(isDark);
+
+ rapiDoc.setAttribute('theme', colors.theme);
+ rapiDoc.setAttribute('bg-color', colors.bgColor);
+ rapiDoc.setAttribute('text-color', colors.textColor);
+ rapiDoc.setAttribute('header-color', colors.headerColor);
+ rapiDoc.setAttribute('primary-color', colors.primaryColor);
+ rapiDoc.setAttribute('nav-bg-color', colors.navBgColor);
+ rapiDoc.setAttribute('nav-text-color', colors.navTextColor);
+ rapiDoc.setAttribute('nav-hover-bg-color', colors.navHoverBgColor);
+ rapiDoc.setAttribute('nav-hover-text-color', colors.navHoverTextColor);
+ rapiDoc.setAttribute('nav-accent-color', colors.navAccentColor);
+ rapiDoc.setAttribute('code-theme', colors.codeTheme);
+}
+
+/**
+ * Set custom CSS properties on RapiDoc element
+ */
+function setInputBorderStyles(rapiDoc: HTMLElement): void {
+ rapiDoc.style.setProperty('--border-color', '#00A3FF');
+}
+
+/**
+ * Hide unwanted elements in RapiDoc shadow DOM
+ */
+function hideExpandCollapseControls(rapiDoc: HTMLElement): void {
+ const maxAttempts = 10;
+ let attempts = 0;
+
+ const tryHide = (): void => {
+ attempts++;
+
+ try {
+ const shadowRoot = rapiDoc.shadowRoot;
+ if (!shadowRoot) {
+ if (attempts < maxAttempts) {
+ setTimeout(tryHide, 500);
+ }
+ return;
+ }
+
+ // Find all elements and hide those containing "Expand all" / "Collapse all"
+ const allElements = shadowRoot.querySelectorAll('*');
+ let hiddenCount = 0;
+
+ allElements.forEach((element) => {
+ const text = element.textContent || '';
+
+ if (text.includes('Expand all') || text.includes('Collapse all')) {
+ (element as HTMLElement).style.display = 'none';
+ if (element.parentElement) {
+ element.parentElement.style.display = 'none';
+ }
+ hiddenCount++;
+ }
+ });
+
+ // Hide "Overview" headings
+ const headings = shadowRoot.querySelectorAll('h1, h2, h3, h4');
+ headings.forEach((heading) => {
+ const text = (heading.textContent || '').trim();
+ if (text.includes('Overview')) {
+ (heading as HTMLElement).style.display = 'none';
+ hiddenCount++;
+ }
+ });
+
+ // Inject CSS as backup
+ const style = document.createElement('style');
+ style.textContent = `
+ .section-gap.section-tag,
+ [id*="overview"],
+ .regular-font.section-gap:empty,
+ h1:empty, h2:empty, h3:empty {
+ display: none !important;
+ }
+ `;
+ shadowRoot.appendChild(style);
+
+ if (hiddenCount === 0 && attempts < maxAttempts) {
+ setTimeout(tryHide, 500);
+ }
+ } catch (e) {
+ if (attempts < maxAttempts) {
+ setTimeout(tryHide, 500);
+ }
+ }
+ };
+
+ setTimeout(tryHide, 500);
+}
+
+/**
+ * Watch for theme changes via stylesheet toggle
+ */
+function watchThemeChanges(rapiDoc: HTMLElement): CleanupFn {
+ const handleThemeChange = (): void => {
+ applyTheme(rapiDoc);
+ };
+
+ // Watch stylesheet disabled attribute changes (Hugo theme.js toggles this)
+ const observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ if (
+ mutation.type === 'attributes' &&
+ mutation.target instanceof HTMLLinkElement &&
+ mutation.target.title?.includes('theme')
+ ) {
+ handleThemeChange();
+ break;
+ }
+ // Also watch data-theme changes as fallback
+ if (mutation.attributeName === 'data-theme') {
+ handleThemeChange();
+ }
+ }
+ });
+
+ // Observe head for stylesheet changes
+ observer.observe(document.head, {
+ attributes: true,
+ attributeFilter: ['disabled'],
+ subtree: true,
+ });
+
+ // Observe documentElement for data-theme changes
+ observer.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['data-theme'],
+ });
+
+ return (): void => {
+ observer.disconnect();
+ };
+}
+
+/**
+ * Initialize RapiDoc component
+ */
+export default function ApiRapiDoc({
+ component,
+}: ComponentOptions): CleanupFn | void {
+ // Find the rapi-doc element inside the container
+ const rapiDoc = component.querySelector('rapi-doc') as HTMLElement | null;
+
+ if (!rapiDoc) {
+ console.warn('[API RapiDoc] No rapi-doc element found in container');
+ return;
+ }
+
+ // Apply initial theme
+ applyTheme(rapiDoc);
+
+ // Set custom CSS properties
+ if (customElements && customElements.whenDefined) {
+ customElements.whenDefined('rapi-doc').then(() => {
+ setInputBorderStyles(rapiDoc);
+ setTimeout(() => setInputBorderStyles(rapiDoc), 500);
+ });
+ } else {
+ setInputBorderStyles(rapiDoc);
+ setTimeout(() => setInputBorderStyles(rapiDoc), 500);
+ }
+
+ // Hide unwanted UI elements
+ hideExpandCollapseControls(rapiDoc);
+
+ // Watch for theme changes
+ return watchThemeChanges(rapiDoc);
+}
+```
+
+**Step 2: Verify the file was created correctly**
+
+Run: `head -30 assets/js/components/api-rapidoc.ts`
+Expected: File header and imports visible
+
+**Step 3: Commit**
+
+```bash
+git add assets/js/components/api-rapidoc.ts
+git commit -m "feat(api): Create api-rapidoc TypeScript component
+
+Extract inline JavaScript from rapidoc.html into a proper TypeScript
+component following the established component pattern."
+```
+
+***
+
+## Task 2: Register api-rapidoc Component in main.js
+
+**Files:**
+
+- Modify: `assets/js/main.js:49-88`
+
+**Step 1: Add import for ApiRapiDoc**
+
+Add this import after line 52 (after RapiDocMini import):
+
+```javascript
+import ApiRapiDoc from './components/api-rapidoc.ts';
+```
+
+**Step 2: Register component in componentRegistry**
+
+Add this entry in the componentRegistry object (after line 87, the 'rapidoc-mini' entry):
+
+```javascript
+ 'api-rapidoc': ApiRapiDoc,
+```
+
+**Step 3: Verify changes**
+
+Run: `grep -n "api-rapidoc\|ApiRapiDoc" assets/js/main.js`
+Expected: Both the import and registry entry appear
+
+**Step 4: Commit**
+
+```bash
+git add assets/js/main.js
+git commit -m "feat(api): Register api-rapidoc component in main.js"
+```
+
+***
+
+## Task 3: Update rapidoc.html to Use Component Pattern
+
+**Files:**
+
+- Modify: `layouts/partials/api/rapidoc.html`
+
+**Step 1: Replace inline JavaScript with data-component attribute**
+
+Replace the entire content of `layouts/partials/api/rapidoc.html` with:
+
+```html
+{{/*
+ RapiDoc API Documentation Renderer
+
+ Primary API documentation renderer using RapiDoc with "Mix your own HTML" slots.
+ See: https://rapidocweb.com/examples.html
+
+ Required page params:
+ - staticFilePath: Path to the OpenAPI specification file
+
+ Optional page params:
+ - operationId: Specific operation to display (renders only that operation)
+ - tag: Tag to filter operations by
+
+ RapiDoc slots available for custom content:
+ - slot="header" - Custom header
+ - slot="footer" - Custom footer
+ - slot="overview" - Custom overview content
+ - slot="auth" - Custom authentication section
+ - slot="nav-logo" - Custom navigation logo
+*/}}
+
+{{ $specPath := .Params.staticFilePath }}
+{{ $specPathJSON := replace $specPath ".yaml" ".json" | replace ".yml" ".json" }}
+{{ $operationId := .Params.operationId | default "" }}
+{{ $tag := .Params.tag | default "" }}
+
+{{/* Machine-readable links for AI agent discovery */}}
+{{ if $specPath }}
+
+
+{{ end }}
+
+
+ {{/* RapiDoc component with slot-based customization */}}
+
+ {{/* Custom overview slot - Hugo page content */}}
+ {{ with .Content }}
+
+ {{ . }}
+
+ {{ end }}
+
+ {{/* Custom examples from frontmatter */}}
+ {{ with .Params.examples }}
+
+
Examples
+ {{ range . }}
+
+
{{ .title }}
+ {{ with .description }}
{{ . | markdownify }}
{{ end }}
+
{{ .code }}
+
+ {{ end }}
+
+ {{ end }}
+
+
+
+{{/* Load RapiDoc from CDN */}}
+
+
+
+```
+
+**Step 2: Verify the inline script is removed**
+
+Run: `grep -c "