style(api): remove RapiDoc styles and rename hugo-native to operations

Remove all RapiDoc-specific code from styles and JavaScript:
- Delete rapi-doc::part() CSS selectors from _api-layout.scss
- Delete dead auth modal styles from _api-security-schemes.scss
- Delete api-auth-input.ts component (RapiDoc "Try it" integration)
- Delete static/css/rapidoc-custom.css (unused)
- Remove setupRapiDocNavigation() from api-toc.ts
- Update comments to remove RapiDoc references

Rename _api-hugo-native.scss to _api-operations.scss since
Hugo-native is now the standard (not a POC).
claude/api-code-samples-plan-MEkQO
Jason Stirnaman 2026-02-13 22:35:22 -06:00
parent 242989b7db
commit 3a56561cfa
13 changed files with 761 additions and 1922 deletions

56
PLAN.md Normal file
View File

@ -0,0 +1,56 @@
---
branch: feat-api-uplift
repo: docs-v2
created: 2025-12-02T15:28:32Z
status: in-progress
---
# feat-api-uplift
## Overview
Replace the current API reference documentation implementation (RapiDoc web components) with Hugo-native templates.
## Phase 1: Core Infrastructure (completed)
### Build process
- `yarn build:api` parses OpenAPI specs into Hugo data
- Generates Hugo pages with frontmatter for Algolia search integration
- Static JSON chunks for faster page loads
### OpenAPI tag cleanup
- Removed unused tags from OpenAPI specs
- Updated tags to be consistent and descriptive
### Hugo-native POC
- Implemented Hugo-native templates in `layouts/partials/api/hugo-native/`
- Tested with InfluxDB 3 Core product
## Phase 2: Migration to Hugo-Native (in progress)
**Plan**: @plans/2026-02-13-hugo-native-api-migration.md
### Task Order
1. ✅ **Promote Hugo-native templates** - Move from POC to production
2. ✅ **Remove RapiDoc templates** - Delete templates and partials
3. ✅ **Remove RapiDoc JavaScript** - Delete components
4. ✅ **Remove operation pages** - Delete individual operation page generation
5. ✅ **Update Cypress tests** - Simplify tests for static HTML
6. ✅ **Clean up styles** - Remove RapiDoc CSS and dead auth modal code
7. **Fix generation script cleanup** - Add `--clean` flag (planned)
8. **Apply Cache Data tag split** - Enterprise spec update (planned)
9. **Migrate remaining products** - Apply to all InfluxDB products (planned)
## Related Files
- Branch: `feat-api-uplift`
- Plan: `plans/2026-02-13-hugo-native-api-migration.md`
## Notes
- Use Chrome devtools and Cypress to debug
- No individual operation pages - operations accessed only via tag pages

View File

@ -157,7 +157,7 @@ function generateDataFromOpenAPI(specFile, dataOutPath, articleOutPath) {
* Generate Hugo content pages from article data
*
* Creates markdown files with frontmatter from article metadata.
* Each article becomes a page with type: api that renders via RapiDoc.
* Each article becomes a page with type: api that renders via Hugo-native templates.
*
* @param options - Generation options
*/
@ -278,7 +278,7 @@ ${yaml.dump(frontmatter)}---
* Generate Hugo content pages from tag-based article data
*
* Creates markdown files with frontmatter from article metadata.
* Each article becomes a page with type: api that renders via RapiDoc.
* Each article becomes a page with type: api that renders via Hugo-native templates.
* Includes operation metadata for TOC generation.
*
* @param options - Generation options
@ -449,165 +449,9 @@ ${yaml.dump(frontmatter)}---
}
console.log(`✓ Generated ${data.articles.length} tag-based content pages in ${contentPath}`);
// NOTE: Path page generation is disabled - all operations are now displayed
// inline on tag pages using RapiDoc with hash-based navigation for deep linking.
// The tag pages render all operations in a single scrollable view with a
// server-side generated TOC for quick navigation.
//
// Previously this generated individual pages per API path:
// generatePathPages({ articlesPath, contentPath, pathSpecFiles });
}
/**
* Convert API path to URL-safe slug with normalized version prefix
*
* Transforms an API path to a URL-friendly format:
* - Removes leading "/api" prefix (added by parent directory structure)
* - Ensures all paths have a version prefix (defaults to v1 if none)
* - Removes leading slash
* - Removes curly braces from path parameters (e.g., {db} db)
*
* Examples:
* - "/write" "v1/write"
* - "/api/v3/configure/database" "v3/configure/database"
* - "/api/v3/configure/database/{db}" "v3/configure/database/db"
* - "/api/v2/write" "v2/write"
* - "/health" "v1/health"
*
* @param apiPath - The API path (e.g., "/write", "/api/v3/write_lp")
* @returns URL-safe path slug with version prefix (e.g., "v1/write", "v3/configure/database")
*/
function apiPathToSlug(apiPath) {
// Remove leading "/api" prefix if present
let normalizedPath = apiPath.replace(/^\/api/, '');
// Remove leading slash
normalizedPath = normalizedPath.replace(/^\//, '');
// If path doesn't start with version prefix, add v1/
if (!/^v\d+\//.test(normalizedPath)) {
normalizedPath = `v1/${normalizedPath}`;
}
// Remove curly braces from path parameters (e.g., {db} → db)
// to avoid URL encoding issues in Hugo
normalizedPath = normalizedPath.replace(/[{}]/g, '');
return normalizedPath;
}
/** Method sort order for consistent display */
const METHOD_ORDER = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'];
/**
* Generate standalone Hugo content pages for each API path
*
* Creates individual pages at path-based URLs like /api/v3/configure/database/
* for each unique API path. Each page includes all HTTP methods (operations)
* for that path, rendered using RapiDoc with match-type='includes'.
*
* When pathSpecFiles is provided, uses path-specific specs for isolated rendering.
* Falls back to tag-based specs when pathSpecFiles is not available.
*
* @param options - Generation options
*/
function generatePathPages(options) {
const { articlesPath, contentPath, pathSpecFiles } = options;
const yaml = require('js-yaml');
const articlesFile = path.join(articlesPath, 'articles.yml');
if (!fs.existsSync(articlesFile)) {
console.warn(`⚠️ Articles file not found: ${articlesFile}`);
return;
}
// Read articles data
const articlesContent = fs.readFileSync(articlesFile, 'utf8');
const data = yaml.load(articlesContent);
if (!data.articles || !Array.isArray(data.articles)) {
console.warn(`⚠️ No articles found in ${articlesFile}`);
return;
}
// Collect all operations and group by API path
const pathOperations = new Map();
// Process each article (tag) and collect operations by path
for (const article of data.articles) {
// Skip conceptual articles (they don't have operations)
if (article.fields.isConceptual) {
continue;
}
const operations = article.fields.operations || [];
const tagSpecFile = article.fields.staticFilePath;
const tagName = article.fields.tag || article.fields.name || '';
for (const op of operations) {
const existing = pathOperations.get(op.path);
if (existing) {
// Add operation to existing path group
existing.operations.push(op);
}
else {
// Create new path group
pathOperations.set(op.path, {
operations: [op],
tagSpecFile,
tagName,
});
}
}
}
let pathCount = 0;
// Generate a page for each unique API path
for (const [apiPath, pathData] of pathOperations) {
// Build page path: api/{path}/
// e.g., /api/v3/configure/database -> api/v3/configure/database/
const pathSlug = apiPathToSlug(apiPath);
// Only add 'api/' prefix if the path doesn't already start with 'api/'
const basePath = pathSlug.startsWith('api/') ? pathSlug : `api/${pathSlug}`;
const pathDir = path.join(contentPath, basePath);
const pathFile = path.join(pathDir, '_index.md');
// Create directory if needed
if (!fs.existsSync(pathDir)) {
fs.mkdirSync(pathDir, { recursive: true });
}
// Sort operations by method order
const sortedOperations = [...pathData.operations].sort((a, b) => {
const aIndex = METHOD_ORDER.indexOf(a.method.toUpperCase());
const bIndex = METHOD_ORDER.indexOf(b.method.toUpperCase());
return (aIndex === -1 ? 999 : aIndex) - (bIndex === -1 ? 999 : bIndex);
});
// Use first operation's summary or construct from methods
const methods = sortedOperations.map((op) => op.method.toUpperCase());
const title = sortedOperations.length === 1 && sortedOperations[0].summary
? sortedOperations[0].summary
: `${apiPath}`;
// Determine spec file - use path-specific spec if available
const pathSpecFile = pathSpecFiles?.get(apiPath);
const specFile = pathSpecFile || pathData.tagSpecFile;
const frontmatter = {
title,
description: `API reference for ${apiPath} - ${methods.join(', ')}`,
type: 'api-path',
layout: 'path',
// RapiDoc configuration
specFile,
apiPath,
// Include all operations for TOC generation
operations: sortedOperations.map((op) => ({
operationId: op.operationId,
method: op.method,
path: op.path,
summary: op.summary,
...(op.compatVersion && { compatVersion: op.compatVersion }),
})),
tag: pathData.tagName,
};
// Collect related links from all operations
const relatedLinks = [];
for (const op of sortedOperations) {
if (op.externalDocs?.url && !relatedLinks.includes(op.externalDocs.url)) {
relatedLinks.push(op.externalDocs.url);
}
}
if (relatedLinks.length > 0) {
frontmatter.related = relatedLinks;
}
const pageContent = `---
${yaml.dump(frontmatter)}---
`;
fs.writeFileSync(pathFile, pageContent);
pathCount++;
}
console.log(`✓ Generated ${pathCount} path pages in ${contentPath}/api/`);
// inline on tag pages using Hugo-native templates with hash-based navigation
// for deep linking. The tag pages render all operations in a single scrollable
// view with a server-side generated TOC for quick navigation.
}
/**
* Merge article data from multiple specs into a single articles.yml

File diff suppressed because it is too large Load Diff

View File

@ -1,604 +0,0 @@
/**
* API Auth Input Component (Popover)
*
* Provides a popover-based credential input for API operations.
* Integrates with RapiDoc's auth system via JavaScript API.
*
* Features:
* - Popover UI triggered by button click
* - Filters auth schemes based on operation requirements
* - Session-only credentials (not persisted to storage)
* - Syncs with RapiDoc's "Try it" feature
*
* Usage:
* <button data-component="api-auth-input"
* data-schemes="bearer,token"
* data-popover="true">
* Set credentials
* </button>
* <div class="api-auth-popover" hidden></div>
*/
interface ComponentOptions {
component: HTMLElement;
}
interface AuthCredentials {
bearer?: string;
basic?: { username: string; password: string };
querystring?: string;
}
type CleanupFn = () => void;
// sessionStorage key for credentials
// Persists across page navigations, cleared when tab closes
const CREDENTIALS_KEY = 'influxdata-api-credentials';
/**
* Get credentials from sessionStorage
*/
function getCredentials(): AuthCredentials {
try {
const stored = sessionStorage.getItem(CREDENTIALS_KEY);
return stored ? JSON.parse(stored) : {};
} catch {
return {};
}
}
/**
* Set credentials in sessionStorage
*/
function setCredentials(credentials: AuthCredentials): void {
try {
if (Object.keys(credentials).length === 0) {
sessionStorage.removeItem(CREDENTIALS_KEY);
} else {
sessionStorage.setItem(CREDENTIALS_KEY, JSON.stringify(credentials));
}
} catch (e) {
console.warn('[API Auth] Failed to store credentials:', e);
}
}
/**
* Check if any credentials are set
*/
function hasCredentials(): boolean {
const creds = getCredentials();
return !!(creds.bearer || creds.basic?.password || creds.querystring);
}
/**
* Try to update the visible auth input in RapiDoc's shadow DOM.
* This provides visual feedback but is not essential for authentication.
*/
function updateRapiDocAuthInput(
rapiDoc: HTMLElement,
token: string,
scheme: 'bearer' | 'token'
): void {
try {
const shadowRoot = rapiDoc.shadowRoot;
if (!shadowRoot) return;
const headerValue =
scheme === 'bearer' ? `Bearer ${token}` : `Token ${token}`;
const authInputSelectors = [
'input[data-pname="Authorization"]',
'input[placeholder*="authorization" i]',
'input[placeholder*="token" i]',
'.request-headers input[type="text"]',
];
for (const selector of authInputSelectors) {
const input = shadowRoot.querySelector<HTMLInputElement>(selector);
if (input && !input.value) {
input.value = headerValue;
input.dispatchEvent(new Event('input', { bubbles: true }));
console.log('[API Auth] Updated visible auth input in RapiDoc');
return;
}
}
} catch (e) {
console.debug('[API Auth] Could not update visible input:', e);
}
}
/**
* Apply credentials to a RapiDoc element
* Returns true if credentials were successfully applied
*/
function applyCredentialsToRapiDoc(
rapiDoc: HTMLElement,
credentials: AuthCredentials
): boolean {
let applied = false;
// Clear existing credentials first
if ('removeAllSecurityKeys' in rapiDoc) {
try {
(rapiDoc as any).removeAllSecurityKeys();
} catch (e) {
console.warn('[API Auth] Failed to clear existing credentials:', e);
}
}
// Apply bearer/token credentials using setApiKey() only
// Using both HTML attributes AND setApiKey() causes "2 API keys applied"
if (credentials.bearer) {
try {
if ('setApiKey' in rapiDoc) {
(rapiDoc as any).setApiKey('BearerAuthentication', credentials.bearer);
console.log('[API Auth] Applied bearer via setApiKey()');
applied = true;
}
updateRapiDocAuthInput(rapiDoc, credentials.bearer, 'bearer');
} catch (e) {
console.error('[API Auth] Failed to set API key:', e);
}
}
// Apply basic auth credentials
if ('setHttpUserNameAndPassword' in rapiDoc && credentials.basic?.password) {
try {
(rapiDoc as any).setHttpUserNameAndPassword(
'BasicAuthentication',
credentials.basic.username || '',
credentials.basic.password
);
applied = true;
console.log('[API Auth] Applied basic auth credentials to RapiDoc');
} catch (e) {
console.error('[API Auth] Failed to set basic auth:', e);
}
}
return applied;
}
/**
* Create auth field HTML for a specific scheme
*/
function createAuthField(scheme: string): string {
switch (scheme) {
case 'bearer':
return `
<div class="auth-field" data-scheme="bearer">
<label for="auth-bearer">
<span class="auth-label-text">API Token</span>
<span class="auth-label-hint">(Bearer auth)</span>
</label>
<div class="auth-input-group">
<input type="password"
id="auth-bearer"
placeholder="Enter your API token"
autocomplete="off" />
<button type="button" class="auth-show-toggle" data-target="auth-bearer" aria-label="Show token">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>`;
case 'token':
return `
<div class="auth-field" data-scheme="token">
<label for="auth-token">
<span class="auth-label-text">API Token</span>
<span class="auth-label-hint">(v2 Token scheme)</span>
</label>
<div class="auth-input-group">
<input type="password"
id="auth-token"
placeholder="Enter your API token"
autocomplete="off" />
<button type="button" class="auth-show-toggle" data-target="auth-token" aria-label="Show token">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>`;
case 'basic':
return `
<div class="auth-field-group" data-scheme="basic">
<p class="auth-group-label">Basic Authentication <span class="auth-label-hint">(v1 compatibility)</span></p>
<div class="auth-field">
<label for="auth-username">Username</label>
<input type="text"
id="auth-username"
placeholder="Optional"
autocomplete="username" />
</div>
<div class="auth-field">
<label for="auth-password">Password / Token</label>
<div class="auth-input-group">
<input type="password"
id="auth-password"
placeholder="Enter token"
autocomplete="current-password" />
<button type="button" class="auth-show-toggle" data-target="auth-password" aria-label="Show password">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>
</div>`;
case 'querystring':
return `
<div class="auth-field" data-scheme="querystring">
<label for="auth-querystring">
<span class="auth-label-text">Query Parameter Token</span>
<span class="auth-label-hint">(v1 ?p=TOKEN)</span>
</label>
<div class="auth-input-group">
<input type="password"
id="auth-querystring"
placeholder="Enter token for ?p= parameter"
autocomplete="off" />
<button type="button" class="auth-show-toggle" data-target="auth-querystring" aria-label="Show token">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 3C4.5 3 1.5 5.5 0 8c1.5 2.5 4.5 5 8 5s6.5-2.5 8-5c-1.5-2.5-4.5-5-8-5zm0 8c-1.7 0-3-1.3-3-3s1.3-3 3-3 3 1.3 3 3-1.3 3-3 3z"/>
</svg>
</button>
</div>
</div>`;
default:
return '';
}
}
/**
* Create the popover content HTML
*/
function createPopoverContent(schemes: string[]): string {
// If both bearer and token are supported, show combined field
const hasBearerAndToken =
schemes.includes('bearer') && schemes.includes('token');
const displaySchemes = hasBearerAndToken
? schemes.filter((s) => s !== 'token')
: schemes;
const fields = displaySchemes.map((s) => createAuthField(s)).join('');
// Adjust label if both bearer and token are supported
const bearerLabel = hasBearerAndToken
? '(Bearer / Token auth)'
: '(Bearer auth)';
return `
<div class="api-auth-popover-content">
<div class="popover-header">
<h4>API Credentials</h4>
<button type="button" class="popover-close" aria-label="Close">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M4.646 4.646a.5.5 0 0 1 .708 0L8 7.293l2.646-2.647a.5.5 0 0 1 .708.708L8.707 8l2.647 2.646a.5.5 0 0 1-.708.708L8 8.707l-2.646 2.647a.5.5 0 0 1-.708-.708L7.293 8 4.646 5.354a.5.5 0 0 1 0-.708z"/>
</svg>
</button>
</div>
<p class="auth-description">
Enter credentials for "Try it" requests.
</p>
${fields.replace('(Bearer auth)', bearerLabel)}
<div class="auth-actions">
<button type="button" class="btn btn-sm auth-apply">Apply</button>
<button type="button" class="btn btn-sm btn-secondary auth-clear">Clear</button>
</div>
<p class="auth-feedback" hidden></p>
</div>
`;
}
/**
* Show feedback message
*/
function showFeedback(
container: HTMLElement,
message: string,
type: 'success' | 'error'
): void {
const feedback = container.querySelector<HTMLElement>('.auth-feedback');
if (feedback) {
feedback.textContent = message;
feedback.className = `auth-feedback auth-feedback--${type}`;
feedback.hidden = false;
setTimeout(() => {
feedback.hidden = true;
}, 3000);
}
}
/**
* Update the status indicator on the trigger button
*/
function updateStatusIndicator(trigger: HTMLElement): void {
const indicator = trigger.querySelector<HTMLElement>(
'.auth-status-indicator'
);
const hasCreds = hasCredentials();
if (indicator) {
indicator.hidden = !hasCreds;
}
trigger.classList.toggle('has-credentials', hasCreds);
}
/**
* Initialize the auth input popover component
*/
export default function ApiAuthInput({
component,
}: ComponentOptions): CleanupFn | void {
// Component is the banner container, find the trigger button inside it
const triggerEl =
component.querySelector<HTMLButtonElement>('.api-auth-trigger');
const statusEl = component.querySelector<HTMLElement>('.api-auth-status');
if (!triggerEl) {
console.error('[API Auth] Trigger button not found in banner');
return;
}
// Find the popover element (it's a sibling before the banner)
const popoverEl = document.querySelector<HTMLElement>('.api-auth-popover');
if (!popoverEl) {
console.error('[API Auth] Popover container not found');
return;
}
// Reassign to new consts so TypeScript knows they're not null in closures
const trigger = triggerEl;
const popover = popoverEl;
// Get schemes from the component (banner) dataset
const schemesAttr = component.dataset.schemes || 'bearer';
const schemes = schemesAttr.split(',').map((s) => s.trim().toLowerCase());
// Render popover content
popover.innerHTML = createPopoverContent(schemes);
// Element references
const bearerInput = popover.querySelector<HTMLInputElement>('#auth-bearer');
const tokenInput = popover.querySelector<HTMLInputElement>('#auth-token');
const usernameInput =
popover.querySelector<HTMLInputElement>('#auth-username');
const passwordInput =
popover.querySelector<HTMLInputElement>('#auth-password');
const querystringInput =
popover.querySelector<HTMLInputElement>('#auth-querystring');
const applyBtn = popover.querySelector<HTMLButtonElement>('.auth-apply');
const clearBtn = popover.querySelector<HTMLButtonElement>('.auth-clear');
const closeBtn = popover.querySelector<HTMLButtonElement>('.popover-close');
// Backdrop element for modal overlay
const backdrop = document.querySelector<HTMLElement>('.api-auth-backdrop');
// Restore saved credentials from sessionStorage
const savedCredentials = getCredentials();
if (savedCredentials.bearer && bearerInput) {
bearerInput.value = savedCredentials.bearer;
}
if (savedCredentials.basic) {
if (usernameInput) usernameInput.value = savedCredentials.basic.username;
if (passwordInput) passwordInput.value = savedCredentials.basic.password;
}
if (savedCredentials.querystring && querystringInput) {
querystringInput.value = savedCredentials.querystring;
}
// Update status indicator based on saved credentials
if (hasCredentials() && statusEl) {
statusEl.textContent = 'Credentials set for this session.';
trigger.textContent = 'Update credentials';
}
/**
* Toggle popover visibility
*/
function togglePopover(show?: boolean): void {
const shouldShow = show ?? popover.hidden;
popover.hidden = !shouldShow;
if (backdrop) backdrop.hidden = !shouldShow;
trigger.setAttribute('aria-expanded', String(shouldShow));
if (shouldShow) {
// Focus first input when opening
const firstInput = popover.querySelector<HTMLInputElement>(
'input:not([type="hidden"])'
);
firstInput?.focus();
}
}
/**
* Close popover
*/
function closePopover(): void {
togglePopover(false);
trigger.focus();
}
// Trigger button click
trigger.addEventListener('click', (e) => {
e.stopPropagation();
togglePopover();
});
// Close button
closeBtn?.addEventListener('click', closePopover);
// Close on backdrop click
backdrop?.addEventListener('click', closePopover);
// Close on outside click
function handleOutsideClick(e: MouseEvent): void {
if (
!popover.hidden &&
!popover.contains(e.target as Node) &&
!trigger.contains(e.target as Node)
) {
closePopover();
}
}
document.addEventListener('click', handleOutsideClick);
// Close on Escape
function handleEscape(e: KeyboardEvent): void {
if (e.key === 'Escape' && !popover.hidden) {
closePopover();
}
}
document.addEventListener('keydown', handleEscape);
// Show/hide toggle for password fields
const showToggles =
popover.querySelectorAll<HTMLButtonElement>('.auth-show-toggle');
showToggles.forEach((btn) => {
btn.addEventListener('click', () => {
const targetId = btn.dataset.target;
const input = popover.querySelector<HTMLInputElement>(`#${targetId}`);
if (input) {
const isPassword = input.type === 'password';
input.type = isPassword ? 'text' : 'password';
btn.classList.toggle('showing', !isPassword);
}
});
});
/**
* Apply credentials
*/
function applyCredentials(): void {
const newCredentials: AuthCredentials = {};
// Get token from bearer or token input (they're combined for UX)
const tokenValue = bearerInput?.value || tokenInput?.value;
if (tokenValue) {
newCredentials.bearer = tokenValue;
}
if (usernameInput?.value || passwordInput?.value) {
newCredentials.basic = {
username: usernameInput?.value || '',
password: passwordInput?.value || '',
};
}
if (querystringInput?.value) {
newCredentials.querystring = querystringInput.value;
}
setCredentials(newCredentials);
updateStatusIndicator(trigger);
// Update status text and button
if (hasCredentials()) {
if (statusEl) statusEl.textContent = 'Credentials set for this session.';
trigger.textContent = 'Update credentials';
}
// Apply to RapiDoc
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null;
if (rapiDoc && 'setApiKey' in rapiDoc) {
const applied = applyCredentialsToRapiDoc(rapiDoc, newCredentials);
if (applied) {
showFeedback(popover, 'Credentials saved for this session', 'success');
} else {
showFeedback(popover, 'No credentials to apply', 'error');
}
} else {
showFeedback(popover, 'Credentials saved for this session', 'success');
}
}
/**
* Clear credentials
*/
function clearCredentials(): void {
if (bearerInput) bearerInput.value = '';
if (tokenInput) tokenInput.value = '';
if (usernameInput) usernameInput.value = '';
if (passwordInput) passwordInput.value = '';
if (querystringInput) querystringInput.value = '';
setCredentials({});
updateStatusIndicator(trigger);
// Reset status text and button
if (statusEl)
statusEl.textContent = 'This endpoint requires authentication.';
trigger.textContent = 'Set credentials';
// Clear from RapiDoc using removeAllSecurityKeys()
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null;
if (rapiDoc && 'removeAllSecurityKeys' in rapiDoc) {
try {
(rapiDoc as any).removeAllSecurityKeys();
} catch (e) {
console.debug('[API Auth] Failed to clear RapiDoc credentials:', e);
}
}
showFeedback(popover, 'Credentials cleared', 'success');
}
// Button handlers
applyBtn?.addEventListener('click', applyCredentials);
clearBtn?.addEventListener('click', clearCredentials);
// Listen for RapiDoc spec-loaded event to apply stored credentials
function handleSpecLoaded(event: Event): void {
const rapiDoc = event.target as HTMLElement;
const storedCredentials = getCredentials();
if (
storedCredentials.bearer ||
storedCredentials.basic?.password ||
storedCredentials.querystring
) {
setTimeout(() => {
applyCredentialsToRapiDoc(rapiDoc, storedCredentials);
}, 100);
}
}
// Watch for RapiDoc elements
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
for (const node of mutation.addedNodes) {
if (node instanceof HTMLElement && node.tagName === 'RAPI-DOC') {
node.addEventListener('spec-loaded', handleSpecLoaded);
}
}
}
});
observer.observe(document.body, { childList: true, subtree: true });
// Check if RapiDoc already exists
const existingRapiDoc = document.querySelector('rapi-doc');
if (existingRapiDoc) {
existingRapiDoc.addEventListener('spec-loaded', handleSpecLoaded);
}
// Initialize status indicator
updateStatusIndicator(trigger);
// Cleanup function
return (): void => {
observer.disconnect();
document.removeEventListener('click', handleOutsideClick);
document.removeEventListener('keydown', handleEscape);
existingRapiDoc?.removeEventListener('spec-loaded', handleSpecLoaded);
};
}

View File

@ -3,7 +3,7 @@
*
* Generates "ON THIS PAGE" navigation from content headings or operations data.
* Features:
* - Builds TOC from h2 headings by default (avoids RapiDoc h3 fragment collisions)
* - Builds TOC from h2 headings by default
* - Builds TOC from operations data passed via data-operations attribute (tag-based)
* - Highlights current section on scroll (intersection observer)
* - Smooth scroll to anchors
@ -44,11 +44,7 @@ interface OperationMeta {
/**
* Get headings from the currently visible content
*
* For API pages, only h2 headings are collected to avoid fragment collisions
* from repetitive h3 headings like "Syntax", "Parameters", "Responses" that
* RapiDoc generates for each operation.
*
* @param maxLevel - Maximum heading level to include (default: 2 for API pages)
* @param maxLevel - Maximum heading level to include (default: 2)
*/
function getVisibleHeadings(maxLevel: number = 2): TocEntry[] {
// Find the active tab panel or main content area
@ -147,7 +143,7 @@ function buildOperationsTocHtml(operations: OperationMeta[]): string {
let html = '<ul class="api-toc-list api-toc-list--operations">';
operations.forEach((op) => {
// Generate anchor ID from operationId (RapiDoc uses operationId for anchors)
// Generate anchor ID from operationId
const anchorId = op.operationId;
const methodClass = getMethodClass(op.method);
@ -269,44 +265,6 @@ function setupScrollHighlighting(
return observer;
}
/**
* Set up RapiDoc navigation for TOC links (for tag pages)
* Uses RapiDoc's scrollToPath method instead of native scroll
*/
function setupRapiDocNavigation(container: HTMLElement): void {
container.addEventListener('click', (event) => {
const target = event.target as HTMLElement;
const link = target.closest<HTMLAnchorElement>('.api-toc-link');
if (!link) {
return;
}
const href = link.getAttribute('href');
if (!href?.startsWith('#')) {
return;
}
event.preventDefault();
// Get the path from the hash (e.g., "post-/api/v3/configure/distinct_cache")
const path = href.slice(1);
// Find RapiDoc element and call scrollToPath
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement & {
// eslint-disable-next-line no-unused-vars
scrollToPath?: (path: string) => void;
};
if (rapiDoc && typeof rapiDoc.scrollToPath === 'function') {
rapiDoc.scrollToPath(path);
}
// Update URL hash
history.pushState(null, '', href);
});
}
/**
* Set up smooth scroll for TOC links
*/
@ -348,7 +306,6 @@ function setupSmoothScroll(container: HTMLElement): void {
/**
* Update TOC visibility based on active tab
* Hide TOC for Operations tab (RapiDoc has built-in navigation)
*/
function updateTocVisibility(container: HTMLElement): void {
const operationsPanel = document.querySelector(
@ -417,21 +374,12 @@ export default function ApiToc({ component }: ComponentOptions): void {
}
// Check if TOC was pre-rendered server-side (has existing links)
// For tag pages with RapiDoc, the TOC is rendered by Hugo from operations frontmatter
const hasServerRenderedToc = nav.querySelectorAll('.api-toc-link').length > 0;
if (hasServerRenderedToc) {
// Server-side TOC exists - just show it and set up navigation
component.classList.remove('is-hidden');
// For tag pages with RapiDoc, use RapiDoc's scrollToPath for navigation
// instead of smooth scrolling (which can't access shadow DOM elements)
const rapiDocWrapper = document.querySelector('[data-tag-page="true"]');
if (rapiDocWrapper) {
setupRapiDocNavigation(component);
} else {
setupSmoothScroll(component);
}
setupSmoothScroll(component);
return;
}
@ -439,7 +387,7 @@ export default function ApiToc({ component }: ComponentOptions): void {
const operations = parseOperationsData(component);
let observer: IntersectionObserver | null = null;
// Get max heading level from data attribute (default: 2 to avoid RapiDoc h3 collisions)
// Get max heading level from data attribute (default: 2)
// Use data-toc-depth="3" to include h3 headings if needed
const maxHeadingLevel = parseInt(
component.getAttribute('data-toc-depth') || '2',
@ -467,7 +415,6 @@ export default function ApiToc({ component }: ComponentOptions): void {
}
// Otherwise, fall back to heading-based TOC
// Use configured max heading level to avoid fragment collisions from RapiDoc h3s
const entries = getVisibleHeadings(maxHeadingLevel);
if (nav) {
nav.innerHTML = buildTocHtml(entries);

View File

@ -46,7 +46,6 @@ import SidebarSearch from './components/sidebar-search.js';
import { SidebarToggle } from './sidebar-toggle.js';
import Theme from './theme.js';
import ThemeSwitch from './theme-switch.js';
import ApiAuthInput from './components/api-auth-input.ts';
import ApiToc from './components/api-toc.ts';
/**
@ -79,7 +78,6 @@ const componentRegistry = {
'sidebar-toggle': SidebarToggle,
theme: Theme,
'theme-switch': ThemeSwitch,
'api-auth-input': ApiAuthInput,
'api-toc': ApiToc,
};

View File

@ -35,7 +35,7 @@
padding: 1rem;
border-left: 1px solid $nav-border;
// Hidden state (used when Operations/RapiDoc tab is active)
// Hidden state (used when a tab panel hides the TOC)
&.is-hidden {
display: none;
}
@ -589,43 +589,6 @@
.tab-content:not(:first-of-type) {
display: none;
}
// RapiDoc container styling
rapi-doc {
display: block;
width: 100%;
min-height: 400px;
}
}
////////////////////////////////////////////////////////////////////////////////
////////////////////////////// RapiDoc Overrides ///////////////////////////////
////////////////////////////////////////////////////////////////////////////////
// Hide RapiDoc's internal navigation (we provide our own)
rapi-doc::part(section-navbar) {
display: none !important;
}
// Hide RapiDoc's internal tag headers/titles (we use custom tabs for navigation)
// label-tag-title is the "PROCESSING ENGINE" header with auth badges shown in tag groups
rapi-doc::part(label-tag-title) {
display: none !important;
}
// Hide RapiDoc's authentication section (we have separate Auth tab)
rapi-doc::part(section-auth) {
display: none !important;
}
// Ensure RapiDoc content fills available space
rapi-doc::part(section-main-content) {
padding: 0;
}
// Match RapiDoc's operation section styling to our theme
rapi-doc::part(section-operations) {
padding: 0;
}
////////////////////////////////////////////////////////////////////////////////

View File

@ -1,5 +1,5 @@
// Hugo-Native API Documentation Styles
// Styled after docusaurus-openapi aesthetic with clean, readable layouts
// API Operations Styles
// Renders OpenAPI operations, parameters, schemas, and responses
// Variables
$api-border-radius: 6px;

View File

@ -1,9 +1,8 @@
////////////////////////////////////////////////////////////////////////////////
// API Documentation Style Overrides
//
// Provides loading spinner and reusable API-related styles.
// Note: Legacy Redoc-specific overrides have been removed in favor of
// Scalar/RapiDoc renderers which use CSS custom properties for theming.
// Provides loading spinner and reusable HTTP method badge colors.
// Used by Hugo-native API templates for consistent styling.
////////////////////////////////////////////////////////////////////////////////
@import "tools/color-palette";

View File

@ -3,7 +3,7 @@
//
// Styles for security schemes sections displayed on conceptual API pages
// (like Authentication). These sections are rendered from OpenAPI spec
// securitySchemes using Hugo templates, not RapiDoc.
// securitySchemes using Hugo templates.
////////////////////////////////////////////////////////////////////////////////
.api-security-schemes {
@ -90,296 +90,3 @@ html:has(link[title="dark-theme"]:not([disabled])) {
}
}
////////////////////////////////////////////////////////////////////////////////
// API Auth Modal - Credential input for operation pages
//
// Modal UI triggered by "Set credentials" button in auth info banner.
// Integrates with RapiDoc "Try it" via JavaScript API.
////////////////////////////////////////////////////////////////////////////////
// Backdrop overlay
.api-auth-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
z-index: 1000;
&[hidden] {
display: none;
}
}
// Credentials modal
.api-auth-popover {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1001;
min-width: 320px;
max-width: 400px;
background: $g20-white;
border: 1px solid $g5-pepper;
border-radius: 8px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
&[hidden] {
display: none;
}
}
.api-auth-popover-content {
padding: 1rem;
}
.popover-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.75rem;
h4 {
margin: 0;
font-size: 0.95rem;
font-weight: 600;
color: $article-heading;
}
}
.popover-close {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
background: none;
border: none;
border-radius: 4px;
cursor: pointer;
color: $g9-mountain;
transition: all 0.15s;
&:hover {
background: rgba(0, 0, 0, 0.05);
color: $article-text;
}
}
.auth-description {
margin: 0 0 1rem 0;
font-size: 0.85rem;
color: $g9-mountain;
}
.auth-field {
margin-bottom: 0.75rem;
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 600;
font-size: 0.85rem;
color: $article-text;
}
.auth-label-hint {
font-weight: 400;
color: $g9-mountain;
}
input {
width: 100%;
padding: 0.5rem 0.75rem;
border: 1px solid $g5-pepper;
border-radius: 3px;
font-family: inherit;
font-size: 0.9rem;
background: $g20-white;
color: $article-text;
&:focus {
outline: none;
border-color: $b-pool;
box-shadow: 0 0 0 2px rgba($b-pool, 0.2);
}
&::placeholder {
color: $g9-mountain;
}
}
}
.auth-input-group {
position: relative;
display: flex;
align-items: center;
input {
padding-right: 2.5rem;
}
}
.auth-show-toggle {
position: absolute;
right: 0.5rem;
display: flex;
align-items: center;
justify-content: center;
padding: 0.25rem;
background: none;
border: none;
cursor: pointer;
color: $g9-mountain;
opacity: 0.6;
transition: opacity 0.15s;
&:hover {
opacity: 1;
}
&.showing {
color: $b-pool;
opacity: 1;
}
}
.auth-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
.auth-apply,
.auth-clear {
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
font-weight: 500;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
text-decoration: none;
}
.auth-apply {
background: $b-pool;
color: $g20-white;
border: 1px solid $b-pool;
&:hover {
background: darken($b-pool, 8%);
border-color: darken($b-pool, 8%);
}
}
.auth-clear {
background: transparent;
color: $article-text;
border: 1px solid $g5-pepper;
&:hover {
background: rgba(0, 0, 0, 0.05);
border-color: $g9-mountain;
}
}
}
.auth-feedback {
margin: 0.75rem 0 0 0;
padding: 0.5rem 0.75rem;
font-size: 0.85rem;
border-radius: 3px;
&.auth-feedback--success {
background: rgba($gr-viridian, 0.1);
color: $gr-viridian;
}
&.auth-feedback--error {
background: rgba($r-fire, 0.1);
color: $r-fire;
}
}
// Dark theme for modal
[data-theme="dark"],
html:has(link[title="dark-theme"]:not([disabled])) {
.api-auth-popover {
background: $grey15;
border-color: $grey25;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.4);
}
.popover-header h4 {
color: $g20-white;
}
.popover-close {
color: $g15-platinum;
&:hover {
background: $grey20;
color: $g20-white;
}
}
.auth-description {
color: $g15-platinum;
}
.auth-field {
label {
color: $g20-white;
}
.auth-label-hint {
color: $g15-platinum;
}
input {
background: $grey20;
border-color: $grey25;
color: $g20-white;
&:focus {
border-color: $b-pool;
}
&::placeholder {
color: $g9-mountain;
}
}
}
.auth-show-toggle {
color: $g15-platinum;
&.showing {
color: $b-pool;
}
}
.auth-feedback {
&.auth-feedback--success {
background: rgba($gr-viridian, 0.15);
color: $gr-emerald;
}
&.auth-feedback--error {
background: rgba($r-fire, 0.15);
color: $r-tungsten;
}
}
.auth-actions {
.auth-clear {
color: $g15-platinum;
border-color: $grey25;
&:hover {
background: $grey20;
border-color: $g15-platinum;
color: $g20-white;
}
}
}
}

View File

@ -35,7 +35,7 @@
"layouts/v3-wayfinding",
"layouts/api-layout",
"layouts/api-security-schemes",
"layouts/api-hugo-native";
"layouts/api-operations";
// Import Components
@import "components/influxdb-version-detector",

View File

@ -142,24 +142,37 @@ Simplified Cypress tests now that we use standard HTML instead of shadow DOM.
***
### Task 6: Clean up styles
### Task 6: Clean up styles ✅ COMPLETED
**Priority:** Medium
**Priority:** Medium | **Status:** Completed 2026-02-13
Remove RapiDoc-specific styles and consolidate Hugo-native styles.
Remove RapiDoc-specific styles, JavaScript, and references from the codebase.
**Files to review:**
**Files modified:**
- `assets/styles/layouts/_api-layout.scss`
- `assets/styles/layouts/_api-overrides.scss`
- `assets/styles/layouts/_api-hugo-native.scss`
- `assets/styles/layouts/_api-layout.scss` - Removed \~40 lines of `rapi-doc::part()` CSS selectors
- `assets/styles/layouts/_api-overrides.scss` - Updated comment header
- `assets/styles/layouts/_api-security-schemes.scss` - Removed \~290 lines of dead auth modal styles
- `assets/js/main.js` - Removed dead `api-auth-input` import and registration
- `assets/js/components/api-toc.ts` - Removed RapiDoc-specific code and updated comments
**Files deleted:**
- `static/css/rapidoc-custom.css` - Unused static CSS file
**Changes:**
1. Remove RapiDoc-specific CSS variables and selectors
2. Merge `_api-hugo-native.scss` into `_api-layout.scss`
3. Remove `_api-overrides.scss` if only contains RapiDoc overrides
4. Update SCSS imports in the main stylesheet
1. ✅ Removed `rapi-doc` container styling and `::part()` selectors from `_api-layout.scss`
2. ✅ Removed dead auth modal section from `_api-security-schemes.scss` (was for RapiDoc "Try it" integration)
3. ✅ Removed `api-auth-input` dead import from `main.js` (component file was already deleted)
4. ✅ Removed `setupRapiDocNavigation()` dead function and references from `api-toc.ts`
5. ✅ Updated comments throughout to remove RapiDoc mentions
6. ✅ Rebuilt `api-docs/scripts/dist/` to update compiled JavaScript
**Architecture decision:** Kept operation styles separate from layout styles for cleaner separation of concerns:
- `_api-layout.scss` handles page structure and navigation
- `_api-operations.scss` handles operation/schema component rendering (renamed from `_api-hugo-native.scss`)
***

View File

@ -1,18 +0,0 @@
/* Custom RapiDoc overrides */
/* Reduce parameter table indentation - target the expand column */
.m-table tr > td:first-child {
width: 0 !important;
min-width: 0 !important;
padding: 0 !important;
}
/* Reduce param-name cell indentation */
.param-name {
text-align: left !important;
}
/* Make auth section scrollable on narrow viewports */
.security-info-button {
flex-wrap: wrap;
}