feat(api): improve auth UX with sessionStorage and fix styling issues

Auth credentials:
- Switch from in-memory to sessionStorage for credentials
- Credentials persist across page navigations within browser tab
- Auto-clear when tab closes (no long-term storage)
- Pre-fill form fields with saved credentials on page load
- Update status text and button based on credential state

Styling fixes:
- Add right padding to code blocks so Copy button doesn't overlap content
- Make long URLs wrap instead of requiring horizontal scroll
- Hide TOC sidebar when no headings exist (e.g., quick-start page)
- Remove "Use RapiDoc's navigation..." message from TOC

API docs:
- Remove x-codeSamples from write_lp endpoints (Core and Enterprise)
- Add schema descriptions for line protocol request body
claude/fix-docs-build-issue-VL3Et
Jason Stirnaman 2025-12-30 15:30:06 -06:00
parent 4fccddc255
commit fa9eda452a
7 changed files with 295 additions and 427 deletions

View File

@ -503,38 +503,6 @@ paths:
description: Request entity too large.
'422':
description: Unprocessable entity.
x-codeSamples:
- label: cURL - Basic write
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01 usage=85.2 1638360000000000000"
- label: cURL - Write with millisecond precision
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ms" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01 usage=85.2 1638360000000"
- label: cURL - Asynchronous write with partial acceptance
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&accept_partial=true&no_sync=true&precision=auto" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01 usage=85.2
memory,host=server01 used=4096"
- label: cURL - Multiple measurements with tags
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ns" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000"
tags:
- Write data
/api/v3/query_sql:
@ -1969,16 +1937,23 @@ components:
text/plain:
schema:
type: string
description: |
Line protocol data. Each line represents a point with a measurement name,
optional tag set, field set, and optional timestamp.
Format: `<measurement>[,<tag_key>=<tag_value>...] <field_key>=<field_value>[,<field_key>=<field_value>...] [<timestamp>]`
examples:
line:
summary: Example line protocol
value: measurement,tag=value field=1 1234567890
multiline:
summary: Example line protocol with UTF-8 characters
single-point:
summary: Write a single point
description: Write one point with tags and fields to a table.
value: cpu,host=server01 usage=85.2,load=0.75 1638360000000000000
multiple-tables:
summary: Write to multiple tables
description: Write points to different tables (measurements) in a single request.
value: |
measurement,tag=value field=1 1234567890
measurement,tag=value field=2 1234567900
measurement,tag=value field=3 1234568000
cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000
queryRequestBody:
required: true
content:

View File

@ -458,38 +458,6 @@ paths:
description: Request entity too large.
'422':
description: Unprocessable entity.
x-codeSamples:
- label: cURL - Basic write
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01 usage=85.2 1638360000000000000"
- label: cURL - Write with millisecond precision
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ms" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01 usage=85.2 1638360000000"
- label: cURL - Asynchronous write with partial acceptance
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&accept_partial=true&no_sync=true&precision=auto" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01 usage=85.2
memory,host=server01 used=4096"
- label: cURL - Multiple measurements with tags
lang: Shell
source: |
curl --request POST "http://localhost:8181/api/v3/write_lp?db=sensors&precision=ns" \
--header "Authorization: Bearer DATABASE_TOKEN" \
--header "Content-Type: text/plain" \
--data-raw "cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000"
tags:
- Write data
/api/v3/query_sql:
@ -2019,16 +1987,23 @@ components:
text/plain:
schema:
type: string
description: |
Line protocol data. Each line represents a point with a measurement name,
optional tag set, field set, and optional timestamp.
Format: `<measurement>[,<tag_key>=<tag_value>...] <field_key>=<field_value>[,<field_key>=<field_value>...] [<timestamp>]`
examples:
line:
summary: Example line protocol
value: measurement,tag=value field=1 1234567890
multiline:
summary: Example line protocol with UTF-8 characters
single-point:
summary: Write a single point
description: Write one point with tags and fields to a table.
value: cpu,host=server01 usage=85.2,load=0.75 1638360000000000000
multiple-tables:
summary: Write to multiple tables
description: Write points to different tables (measurements) in a single request.
value: |
measurement,tag=value field=1 1234567890
measurement,tag=value field=2 1234567900
measurement,tag=value field=3 1234568000
cpu,host=server01,region=us-west usage=85.2,load=0.75 1638360000000000000
memory,host=server01,region=us-west used=4096,free=12288 1638360000000000000
disk,host=server01,region=us-west,device=/dev/sda1 used=50.5,free=49.5 1638360000000000000
queryRequestBody:
required: true
content:

View File

@ -31,32 +31,43 @@ interface AuthCredentials {
type CleanupFn = () => void;
// In-memory credential storage (not persisted)
let currentCredentials: AuthCredentials = {};
// sessionStorage key for credentials
// Persists across page navigations, cleared when tab closes
const CREDENTIALS_KEY = 'influxdata-api-credentials';
/**
* Get current credentials (in-memory only)
* Get credentials from sessionStorage
*/
function getCredentials(): AuthCredentials {
return currentCredentials;
try {
const stored = sessionStorage.getItem(CREDENTIALS_KEY);
return stored ? JSON.parse(stored) : {};
} catch {
return {};
}
}
/**
* Set credentials (in-memory only, not persisted)
* Set credentials in sessionStorage
*/
function setCredentials(credentials: AuthCredentials): void {
currentCredentials = credentials;
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 {
return !!(
currentCredentials.bearer ||
currentCredentials.basic?.password ||
currentCredentials.querystring
);
const creds = getCredentials();
return !!(creds.bearer || creds.basic?.password || creds.querystring);
}
/**
@ -115,23 +126,15 @@ function applyCredentialsToRapiDoc(
}
}
// Apply bearer/token credentials
// Apply bearer/token credentials using setApiKey() only
// Using both HTML attributes AND setApiKey() causes "2 API keys applied"
if (credentials.bearer) {
try {
// Method 1: HTML attributes (most reliable)
rapiDoc.setAttribute('api-key-name', 'Authorization');
rapiDoc.setAttribute('api-key-location', 'header');
rapiDoc.setAttribute('api-key-value', `Bearer ${credentials.bearer}`);
console.log('[API Auth] Set auth via HTML attributes');
// Method 2: JavaScript API for scheme-specific auth
if ('setApiKey' in rapiDoc) {
(rapiDoc as any).setApiKey('BearerAuthentication', credentials.bearer);
(rapiDoc as any).setApiKey('TokenAuthentication', credentials.bearer);
console.log('[API Auth] Applied bearer/token via setApiKey()');
console.log('[API Auth] Applied bearer via setApiKey()');
applied = true;
}
applied = true;
updateRapiDocAuthInput(rapiDoc, credentials.bearer, 'bearer');
} catch (e) {
console.error('[API Auth] Failed to set API key:', e);
@ -336,19 +339,30 @@ function updateStatusIndicator(trigger: HTMLElement): void {
export default function ApiAuthInput({
component,
}: ComponentOptions): CleanupFn | void {
// Component is the trigger button
const trigger = component;
const popoverEl = trigger.nextElementSibling as HTMLElement | null;
// 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 (!popoverEl || !popoverEl.classList.contains('api-auth-popover')) {
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;
}
// Now TypeScript knows popover is not null
// Reassign to new consts so TypeScript knows they're not null in closures
const trigger = triggerEl;
const popover = popoverEl;
const schemesAttr = trigger.dataset.schemes || 'bearer';
// 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
@ -367,12 +381,35 @@ export default function ApiAuthInput({
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) {
@ -401,6 +438,9 @@ export default function ApiAuthInput({
// Close button
closeBtn?.addEventListener('click', closePopover);
// Close on backdrop click
backdrop?.addEventListener('click', closePopover);
// Close on outside click
function handleOutsideClick(e: MouseEvent): void {
if (
@ -462,17 +502,23 @@ export default function ApiAuthInput({
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 applied', 'success');
showFeedback(popover, 'Credentials saved for this session', 'success');
} else {
showFeedback(popover, 'No credentials to apply', 'error');
}
} else {
showFeedback(popover, 'Saved (API viewer loading...)', 'success');
showFeedback(popover, 'Credentials saved for this session', 'success');
}
}
@ -489,19 +535,18 @@ export default function ApiAuthInput({
setCredentials({});
updateStatusIndicator(trigger);
// Clear from RapiDoc
const rapiDoc = document.querySelector('rapi-doc') as HTMLElement | null;
if (rapiDoc) {
rapiDoc.removeAttribute('api-key-name');
rapiDoc.removeAttribute('api-key-location');
rapiDoc.removeAttribute('api-key-value');
// Reset status text and button
if (statusEl)
statusEl.textContent = 'This endpoint requires authentication.';
trigger.textContent = 'Set credentials';
if ('removeAllSecurityKeys' in rapiDoc) {
try {
(rapiDoc as any).removeAllSecurityKeys();
} catch (e) {
console.debug('[API Auth] Failed to clear RapiDoc credentials:', e);
}
// 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);
}
}

View File

@ -37,16 +37,6 @@ interface OperationMeta {
tags: string[];
}
/**
* Check if the active panel contains a RapiDoc component
*/
function isRapiDocActive(): boolean {
const activePanel = document.querySelector(
'.tab-content:not([style*="display: none"]), [data-tab-panel]:not([style*="display: none"])'
);
return activePanel?.querySelector('rapi-doc') !== null;
}
/**
* Get headings from the currently visible content
*/
@ -90,11 +80,8 @@ function getVisibleHeadings(): TocEntry[] {
*/
function buildTocHtml(entries: TocEntry[]): string {
if (entries.length === 0) {
// Check if RapiDoc is active - show helpful message
if (isRapiDocActive()) {
return '<p class="api-toc-empty">Use RapiDoc\'s navigation below to explore this endpoint.</p>';
}
return '<p class="api-toc-empty">No sections on this page.</p>';
// Return empty string - the TOC container can be hidden via CSS when empty
return '';
}
let html = '<ul class="api-toc-list">';
@ -405,8 +392,14 @@ export default function ApiToc({ component }: ComponentOptions): void {
nav.innerHTML = buildTocHtml(entries);
}
// Set up scroll highlighting
observer = setupScrollHighlighting(component, entries);
// Hide TOC if no entries, show if entries exist
if (entries.length === 0) {
component.classList.add('is-hidden');
} else {
component.classList.remove('is-hidden');
// Set up scroll highlighting only when we have entries
observer = setupScrollHighlighting(component, entries);
}
}
// Check initial visibility (hide for Operations tab, only for non-operations pages)

View File

@ -247,10 +247,7 @@ function createRapiDocElement(
// For credential input on operation pages, we need a custom
// component (Task 5).
//
// RECOMMENDATION:
// - Keep render-style="read" for compact operation display
// - Implement custom auth input component above RapiDoc (Task 5)
// - Use sessionStorage to pass credentials to "Try it" feature
// Layout and render style for compact operation display
element.setAttribute('layout', 'column');
element.setAttribute('render-style', 'read');
element.setAttribute('show-header', 'false');
@ -270,10 +267,8 @@ function createRapiDocElement(
element.setAttribute('use-path-in-nav-bar', 'false');
element.setAttribute('show-info', 'false');
// Authentication display - hide RapiDoc's built-in auth section
// We use a custom popover component for credential input instead
// Credentials are applied via HTML attributes (api-key-name, api-key-value)
// and the setApiKey() JavaScript API
// Authentication display - disabled because RapiDoc's auth UI doesn't work
// with match-paths filtering. We show a separate auth info banner instead.
element.setAttribute('allow-authentication', 'false');
element.setAttribute('show-components', 'false');
@ -319,84 +314,63 @@ function injectShadowStyles(element: HTMLElement): void {
padding-top: 0 !important;
}
/* Hide RapiDoc's built-in security section - we show our own */
/* Target the authorization requirements shown near each operation */
.api-key,
.api-key-info,
.security-info-button,
[class*="api-key"],
[class*="security-info"],
.m-markdown-small:has(.lock-icon),
div:has(> .lock-icon),
/* Target the section showing "AUTHORIZATIONS:" or similar */
.req-resp-container > div:first-child:has(svg[style*="lock"]),
/* Target lock icons and their parent containers */
svg.lock-icon,
.lock-icon,
/* Wide selectors for security-related elements */
[part="section-operation-security"],
.expanded-endpoint-body > div:first-child:has([class*="lock"]) {
display: none !important;
/* Fix text cutoff - ensure content flows responsively */
.req-res-title,
.resp-head,
.api-request,
.api-response,
.param-name,
.param-type,
.descr {
word-wrap: break-word;
overflow-wrap: break-word;
}
/* Allow wrapping in tables and flex containers */
table {
table-layout: auto !important;
width: 100% !important;
}
td, th {
word-wrap: break-word;
overflow-wrap: break-word;
white-space: normal !important;
}
/* Prevent horizontal overflow */
.section-gap,
.expanded-req-resp-container {
max-width: 100%;
overflow-x: auto;
}
/* Fix Copy button overlapping code content in Try It section */
/* The Copy button is absolutely positioned, so add padding to prevent overlap */
.curl-request pre,
.curl-request code,
.request-body-container pre,
.response-panel pre {
padding-right: 4.5rem !important; /* Space for Copy button */
}
/* Ensure Copy button stays visible and accessible */
.copy-btn,
button[title="Copy"] {
z-index: 1;
background: rgba(0, 163, 255, 0.9) !important;
border-radius: 4px;
}
/* Make code blocks wrap long URLs instead of requiring horizontal scroll */
.curl-request code,
.request-body-container code {
white-space: pre-wrap !important;
word-break: break-all;
}
`;
shadowRoot.appendChild(style);
// Hide security badge elements by examining content
const hideSecurityBadge = () => {
// Find elements containing security-related text and hide their container
const allElements = shadowRoot.querySelectorAll('span, div');
allElements.forEach((el) => {
const text = el.textContent?.trim();
// Find leaf elements that contain authorization-related text
if (
el.children.length === 0 &&
(text === 'HTTP Bearer' ||
text === 'Bearer' ||
text === 'AUTHORIZATIONS:' ||
text === 'Authorization' ||
text === 'api_token' ||
text === 'BearerAuthentication')
) {
// Walk up the DOM to find a suitable container to hide
// This hides both the text AND any sibling icons (like lock)
let target: HTMLElement = el as HTMLElement;
let parent: HTMLElement | null = el.parentElement;
let depth = 0;
while (parent && depth < 4) {
// Stop at reasonable container boundaries
if (
parent.classList.contains('expanded-endpoint-body') ||
parent.classList.contains('req-resp-container') ||
parent.tagName === 'SECTION'
) {
break;
}
target = parent;
parent = parent.parentElement;
depth++;
}
target.style.display = 'none';
}
});
};
// Run immediately and after delays for dynamic content
hideSecurityBadge();
setTimeout(hideSecurityBadge, 300);
setTimeout(hideSecurityBadge, 800);
// Watch for dynamically added security elements
const observer = new MutationObserver(() => {
hideSecurityBadge();
});
observer.observe(shadowRoot, {
childList: true,
subtree: true,
});
// Disconnect after 5 seconds to avoid performance issues
setTimeout(() => observer.disconnect(), 5000);
return true;
};

View File

@ -72,92 +72,61 @@
}
}
// Dark theme overrides for security schemes
[data-theme="dark"],
html:has(link[title="dark-theme"]:not([disabled])) {
.api-security-schemes {
border-top-color: $grey25;
.security-scheme {
background: $grey15;
border-color: $grey25;
}
.scheme-details {
dt {
color: $g15-platinum;
}
}
.scheme-description {
border-top-color: $grey25;
}
}
}
////////////////////////////////////////////////////////////////////////////////
// API Auth Popover - Credential input for operation pages
// API Auth Modal - Credential input for operation pages
//
// Popover-based UI triggered by "Set credentials" button.
// Positioned above RapiDoc, integrates with "Try it" via JavaScript API.
// Modal UI triggered by "Set credentials" button in auth info banner.
// Integrates with RapiDoc "Try it" via JavaScript API.
////////////////////////////////////////////////////////////////////////////////
.api-auth-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.5rem;
}
.api-auth-trigger-wrapper {
position: relative;
display: inline-block;
}
.api-auth-schemes {
font-size: 0.85rem;
color: $g9-mountain;
.api-auth-label {
font-weight: 400;
opacity: 0.8;
}
}
.api-auth-trigger {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.4rem 0.75rem;
font-size: 0.85rem;
font-weight: 500;
color: $article-text;
background: $g20-white;
border: 1px solid $g5-pepper;
border-radius: 4px;
cursor: pointer;
transition: all 0.15s ease;
&:hover {
background: rgba($b-pool, 0.08);
border-color: $b-pool;
}
&:focus {
outline: 2px solid $b-pool;
outline-offset: 2px;
}
&.has-credentials {
border-color: $gr-viridian;
background: rgba($gr-viridian, 0.08);
}
.auth-icon {
color: $g9-mountain;
}
&.has-credentials .auth-icon {
color: $gr-viridian;
}
}
.auth-status-indicator {
width: 8px;
height: 8px;
background: $gr-viridian;
border-radius: 50%;
margin-left: 0.25rem;
}
.api-auth-popover {
position: absolute;
top: calc(100% + 8px);
left: 0;
// 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: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
border-radius: 8px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.25);
&[hidden] {
display: none;
@ -219,10 +188,6 @@
color: $article-text;
}
.auth-label-text {
margin-right: 0.25rem;
}
.auth-label-hint {
font-weight: 400;
color: $g9-mountain;
@ -284,25 +249,11 @@
}
}
.auth-field-group {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid $g5-pepper;
.auth-group-label {
margin: 0 0 0.75rem 0;
font-weight: 600;
font-size: 0.85rem;
color: $article-text;
}
}
.auth-actions {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
// Explicit button styling to avoid link-like appearance
.auth-apply,
.auth-clear {
padding: 0.4rem 0.75rem;
@ -354,56 +305,9 @@
}
}
// Dark theme overrides
// Dark theme for modal
[data-theme="dark"],
html:has(link[title="dark-theme"]:not([disabled])) {
.api-auth-schemes {
color: $g15-platinum;
}
.api-security-schemes {
border-top-color: $grey25;
.security-scheme {
background: $grey15;
border-color: $grey25;
}
.scheme-details {
dt {
color: $g15-platinum;
}
}
.scheme-description {
border-top-color: $grey25;
}
}
.api-auth-trigger {
background: $grey15;
border-color: $grey25;
color: $g20-white;
&:hover {
background: rgba($b-pool, 0.1);
border-color: $b-pool;
}
&.has-credentials {
border-color: $gr-viridian;
background: rgba($gr-viridian, 0.1);
}
.auth-icon {
color: $g15-platinum;
}
&.has-credentials .auth-icon {
color: $gr-emerald;
}
}
.api-auth-popover {
background: $grey15;
border-color: $grey25;
@ -459,14 +363,6 @@ html:has(link[title="dark-theme"]:not([disabled])) {
}
}
.auth-field-group {
border-top-color: $grey25;
.auth-group-label {
color: $g20-white;
}
}
.auth-feedback {
&.auth-feedback--success {
background: rgba($gr-viridian, 0.15);
@ -480,19 +376,7 @@ html:has(link[title="dark-theme"]:not([disabled])) {
}
.auth-actions {
.auth-apply {
background: $b-pool;
color: $g20-white;
border-color: $b-pool;
&:hover {
background: lighten($b-pool, 5%);
border-color: lighten($b-pool, 5%);
}
}
.auth-clear {
background: transparent;
color: $g15-platinum;
border-color: $grey25;

View File

@ -41,41 +41,21 @@
<link rel="alternate" type="application/json" href="{{ $specPathJSON | absURL }}" title="OpenAPI Specification (JSON)" />
{{ end }}
{{/* Auth credentials popover trigger with scheme indicator */}}
{{/* Map scheme codes to display names */}}
{{ $schemeNames := dict "bearer" "HTTP Bearer" "token" "API Token" "basic" "HTTP Basic" "querystring" "Query String" }}
{{ $schemeList := split $authSchemes "," }}
{{ $displaySchemes := slice }}
{{ range $schemeList }}
{{ $name := index $schemeNames . }}
{{ if $name }}
{{ $displaySchemes = $displaySchemes | append $name }}
{{ end }}
{{ end }}
{{/* Auth credentials modal */}}
<div class="api-auth-backdrop" hidden></div>
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
{{/* Content rendered by TypeScript component */}}
</div>
<div class="api-auth-row">
<div class="api-auth-trigger-wrapper">
<button type="button"
class="api-auth-trigger btn btn-sm"
data-component="api-auth-input"
data-schemes="{{ $authSchemes }}"
data-popover="true"
aria-expanded="false"
aria-haspopup="dialog">
<svg class="auth-icon" width="14" height="14" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true">
<path d="M8 1a3.5 3.5 0 0 0-3.5 3.5V6H3a1 1 0 0 0-1 1v7a1 1 0 0 0 1 1h10a1 1 0 0 0 1-1V7a1 1 0 0 0-1-1h-1.5V4.5A3.5 3.5 0 0 0 8 1zm2 5H6V4.5a2 2 0 1 1 4 0V6z"/>
</svg>
<span class="auth-trigger-text">Set credentials</span>
<span class="auth-status-indicator" hidden aria-label="Credentials configured"></span>
</button>
{{/* Popover container - positioned by CSS */}}
<div class="api-auth-popover" role="dialog" aria-label="API credentials" hidden>
{{/* Content rendered by TypeScript component */}}
</div>
</div>
<span class="api-auth-schemes" title="Supported authentication methods">
<span class="api-auth-label">Auth:</span> {{ delimit $displaySchemes " or " }}
</span>
{{/* Authentication info banner with trigger for credentials modal */}}
<div class="api-auth-info"
data-component="api-auth-input"
data-schemes="{{ $authSchemes }}">
<svg class="api-auth-icon" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>
<span class="api-auth-status">This endpoint requires authentication.</span>
<button type="button" class="api-auth-trigger">Set credentials</button>
</div>
{{/* Component container - TypeScript handles initialization */}}
@ -155,15 +135,57 @@ rapi-doc::part(section-tag) {
display: none;
}
/* Fix auth schemes at narrow widths - ensure content is scrollable */
@media (max-width: 1280px) {
.api-reference-mini {
overflow-x: auto;
}
/* Responsive layout - allow content to flow naturally */
.api-reference-mini {
overflow-x: auto;
max-width: 100%;
}
rapi-doc-mini, rapi-doc {
min-width: 800px; /* Prevent auth elements from collapsing/overlapping */
}
/* Authentication info banner */
.api-auth-info {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
background: #F0F4FF;
border: 1px solid rgba(0, 163, 255, 0.3);
border-radius: 4px;
font-size: 0.9rem;
color: #545667;
}
.api-auth-info .api-auth-icon {
flex-shrink: 0;
color: #00A3FF;
}
.api-auth-info .api-auth-status {
flex: 1;
}
.api-auth-info .api-auth-trigger {
background: none;
border: 1px solid #00A3FF;
color: #00A3FF;
padding: 0.25rem 0.75rem;
border-radius: 4px;
font-size: 0.85rem;
cursor: pointer;
transition: all 0.15s;
}
.api-auth-info .api-auth-trigger:hover {
background: #00A3FF;
color: #fff;
}
/* Dark mode */
[data-theme="dark"] .api-auth-info,
html:has(link[title="dark-theme"]:not([disabled])) .api-auth-info {
background: #1a1a2e;
border-color: rgba(0, 163, 255, 0.2);
color: #D4D7DD;
}