UNPKG

rapidoc

Version:

RapiDoc - Open API spec viewer with built in console

583 lines (528 loc) • 19.6 kB
import { css, unsafeCSS } from "lit"; export const DEFAULT_THEME = "ember"; /** * Brand theme presets (the `theme` attribute), as CSS declaration lists. Each one sets the accent and, where the theme tints * its surfaces, the background and foreground for the dark and the light color scheme; every other * token (card, muted, border, input, ...) is derived from those two in section 3. A theme rule outranks the * shared color-scheme rules, so a theme that sets a surface for one scheme must set it for the other too. * * - 'ember' (default): orange-red accent over neutral charcoal * - 'carbon': monochrome, near-black surfaces and a white accent * - 'cobalt': matte steel-blue accent over grey-blue slate, with cards darker than the page * - 'jade': daisyUI-style green accent over slate-navy, with cards darker than the page * - 'indigo': indigo accent over indigo-black */ export const THEMES = { ember: { dark: ` --primary: #f76b39; --primary-foreground: #000000; `, light: ` --primary: #f76b39; --primary-foreground: #000000; `, }, carbon: { dark: ` --primary: #fafafa; --primary-foreground: #18181b; --ring: #a1a1aa; --background: #121214; --foreground: #e4e4e7; `, light: ` --primary: #18181b; --primary-foreground: #fafafa; --ring: #52525b; --background: #ffffff; --foreground: #18181b; `, }, cobalt: { dark: ` --primary: #7aa2d6; --primary-foreground: #0f1a2a; --background: #1e2530; --foreground: #b4bcc8; --card: #181e27; --card-foreground: #ced5df; `, light: ` --primary: #3a6cb0; --primary-foreground: #ffffff; --background: #ffffff; --foreground: #243244; --card: color-mix(in srgb, var(--background) 94%, var(--foreground)); --card-foreground: color-mix(in srgb, var(--foreground) 90%, #000000); `, }, jade: { dark: ` --primary: #00d390; --primary-foreground: #0b1f17; --background: #1d232a; --foreground: #a6adbb; --card: #181e25; --card-foreground: #c8cfd9; `, light: ` --primary: #047857; --primary-foreground: #ffffff; --background: #ffffff; --foreground: #1a2e26; --card: color-mix(in srgb, var(--background) 94%, var(--foreground)); --card-foreground: color-mix(in srgb, var(--foreground) 90%, #000000); `, }, indigo: { dark: ` --primary: #818cf8; --primary-foreground: #1e1b4b; --background: #0f0e1c; --foreground: #e4e2f5; `, light: ` --primary: #4f46e5; --primary-foreground: #ffffff; --background: #ffffff; --foreground: #1e1b3a; `, }, }; export const THEME_PRESETS = Object.keys(THEMES); /** * The :host rules for one preset: dark (the default scheme), light, and light again for * color-scheme="system" when the OS prefers light. The default preset also applies without a theme attribute. * The selector shapes are what getThemeTokensCss (css-bundle.js) rewrites for the portal's light DOM. */ function presetCss(name, { dark, light }) { const selector = (scheme) => { const suffix = scheme ? `[color-scheme='${scheme}']` : ""; const hosts = [`:host([theme='${name}']${suffix})`]; if (name === DEFAULT_THEME) hosts.push(`:host(:not([theme])${suffix})`); return hosts.join(",\n "); }; return ` /* --------- ${name.toUpperCase()}${name === DEFAULT_THEME ? " (default)" : ""} --------- */ ${selector()} {${dark}} ${selector("light")} {${light}} @media (prefers-color-scheme: light) { ${selector("system")} {${light}} } `; } const presetsCss = unsafeCSS( Object.entries(THEMES) .map(([name, schemes]) => presetCss(name, schemes)) .join(""), ); /** * Checks if a string is a valid CSS hex color code (#rgb, #rrggbb, #rrggbbaa). * @param {string} color - Color string to validate. * @returns {boolean} True if the string is a valid hex color code. */ export function isValidHexColor(color) { if (!color || typeof color !== "string") return false; return /^#([0-9a-f]{3}|[0-9a-f]{6}|[0-9a-f]{8})$/i.test(color.trim()); } /** * Checks if a theme string is valid: must match an allowed preset name or be a valid hex color code. * @param {string} theme - Theme name or hex string to validate. * @returns {boolean} True if theme is supported. */ export function isValidTheme(theme) { if (!theme || typeof theme !== "string") return false; const trimmed = theme.trim().toLowerCase(); return THEME_PRESETS.includes(trimmed) || isValidHexColor(theme); } /** * Normalizes and validates the theme attribute value. * - Allowed presets are normalized to lowercase. * - Valid hex color codes are preserved as-is. * - Any invalid, unrecognized, or missing value defaults to DEFAULT_THEME. * * @param {string} theme - The raw theme attribute value. * @returns {{ theme: string }} */ export function normalizeTheme(theme) { if (!theme || typeof theme !== "string") { return { theme: DEFAULT_THEME }; } const trimmed = theme.trim(); const lower = trimmed.toLowerCase(); if (THEME_PRESETS.includes(lower)) { return { theme: lower }; } if (isValidHexColor(trimmed)) { return { theme: trimmed }; } return { theme: DEFAULT_THEME }; } /** * Determines whether a color parameter is a custom brand hex code rather than a preset name. * @param {string} color - Theme identifier or color code. * @returns {boolean} */ export function isCustomColor(color) { if (!color || typeof color !== "string") return false; const trimmed = color.trim().toLowerCase(); if (THEME_PRESETS.includes(trimmed)) return false; return isValidHexColor(color); } /** * Calculates whether a hex color is perceived as light using standard YIQ luminance formula. * Used to ensure accessible high-contrast text foreground (pure black on light vs white on dark). * @param {string} color - Hex color code (#rgb, #rrggbb, #rrggbbaa). * @returns {boolean} True if color luminance >= 128 (light). */ export function isLightColor(color) { if (!color || typeof color !== "string") return false; let hex = color.trim().replace(/^#/, ""); if (hex.length === 3) { hex = hex .split("") .map((c) => c + c) .join(""); } if (hex.length === 6 || hex.length === 8) { const r = parseInt(hex.slice(0, 2), 16); const g = parseInt(hex.slice(2, 4), 16); const b = parseInt(hex.slice(4, 6), 16); const yiq = (r * 299 + g * 587 + b * 114) / 1000; return yiq >= 128; } return false; } /** * Dynamically applies custom corporate brand hex colors to the host element. * Calculates luminance and injects `--primary` and `--primary-foreground` (`--ring` follows `--primary`). * @param {HTMLElement} element - Custom element host target. * @param {string} themeVal - User-supplied theme attribute value. */ export function applyCustomTheme(element, themeVal) { if (!element || !element.style) return; if ( typeof themeVal !== "string" || THEME_PRESETS.includes(themeVal.trim().toLowerCase()) || !isValidHexColor(themeVal) ) { element.style.removeProperty("--primary"); element.style.removeProperty("--primary-foreground"); return; } const hex = themeVal.trim(); element.style.setProperty("--primary", hex); const fg = isLightColor(hex) ? "#000000" : "#ffffff"; element.style.setProperty("--primary-foreground", fg); } export default css` /* ========================================================================= 1. ROOT DESIGN TOKEN DEFAULTS (:host) ========================================================================= */ :host { /* --- Fonts (shadcn/ui) --- */ --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Roboto Mono', monospace; /* --- Headings & Prose (RapiDoc headings and markdown descriptions) --- */ --heading-font: var(--font-sans); --heading-weight: 600; --heading-line-height: 1.25; --heading-letter-spacing: normal; --heading-color: var(--foreground); --heading-margin-top: 1.5em; --heading-margin-bottom: 0.5em; --h1-font-size: 2rem; --h2-font-size: 1.5rem; --h3-font-size: 1.25rem; --h4-font-size: 1rem; --h5-font-size: 0.875rem; --h6-font-size: 0.8125rem; --label-heading-font-size: var(--font-size-small); --line-height: 1.6; --paragraph-spacing: 1em; /* --- Corner Radius (shadcn/ui) --- */ --radius: 0.25rem; /* --- Sizes & Layout --- */ /* xs is the floor: badges, chips and tiny buttons never go below it. description is for short prose next to data (parameter and property descriptions). */ --font-size-xs: 11px; --font-size-small: 12px; --font-size-description: 13px; --font-size-mono: 13px; --font-size-regular: 14px; --resp-area-height: 400px; --table-schema-key-width: 240px; --table-schema-key-text-overflow: ellipsis; --table-schema-key-whitespace: nowrap; --scroll-margin-top: 120px; /* --- Parameter lists (request parameters, form fields, response headers) --- */ --param-label-width: 12rem; --param-gap: 0.5rem; --param-row-gap: 0.25rem; --param-padding: 0.5rem; --auth-scheme-padding: 0.75rem; --value-list-gap: 0.375rem; --value-list-row-gap: 0.125rem; --value-list-label-gap: 0.25rem; /* --- Focus Ring & Selection --- */ --ring: var(--primary); --focus-shadow: 0 0 0 1px transparent, 0 0 0 2px color-mix(in srgb, var(--ring) 40%, transparent); --selection-bg: color-mix(in srgb, var(--primary) 25%, transparent); --selection-fg: var(--foreground); /* --- Navigation Sidebar (shadcn/ui sidebar tokens) --- */ --sidebar-width: 18.75rem; --sidebar-item-padding: 0.4375rem 1rem 0.4375rem 0.625rem; --sidebar-logo-max-height: 60px; --sidebar: var(--background); --sidebar-foreground: var(--foreground); --sidebar-primary: var(--primary); --sidebar-accent-foreground: var(--foreground); --sidebar-border: var(--border); --sidebar-ring: var(--ring); } /* ========================================================================= 2. SCALE & GEOMETRY TOKENS (radius, scale, nav-item-spacing) ========================================================================= */ /* --- 2A. Corner Radius Scale (radius) --- */ :host([radius='none']) { --radius: 0px; } :host([radius='sm']), :host(:not([radius])) { --radius: 0.25rem; } :host([radius='md']) { --radius: 0.375rem; } :host([radius='lg']) { --radius: 0.5rem; } :host([radius='full']) { --radius: 9999px; } /* --- 2B. UI Scale & Typography (scale) --- */ :host([scale='sm']) { --font-size-xs: 11px; --font-size-small: 11px; --font-size-description: 12px; --font-size-mono: 12px; --font-size-regular: 13px; --sidebar-width: 17rem; } :host([scale='default']), :host(:not([scale])) { --font-size-xs: 11px; --font-size-small: 12px; --font-size-description: 13px; --font-size-mono: 13px; --font-size-regular: 14px; --sidebar-width: 18.75rem; } :host([scale='lg']) { --font-size-xs: 12px; --font-size-small: 14px; --font-size-description: 15px; --font-size-mono: 15px; --font-size-regular: 16px; --sidebar-width: 21rem; } /* --- 2C. Navigation Density (nav-item-spacing) --- */ :host([nav-item-spacing='compact']) { --sidebar-item-padding: 0.3125rem 1rem 0.3125rem 0.625rem; } :host([nav-item-spacing='default']), :host(:not([nav-item-spacing])) { --sidebar-item-padding: 0.4375rem 1rem 0.4375rem 0.625rem; } :host([nav-item-spacing='relaxed']) { --sidebar-item-padding: 0.625rem 1rem 0.625rem 0.625rem; } /* ========================================================================= 3. SURFACE & COLOR SCHEME (color-scheme) ========================================================================= */ /* --------- 3A. DARK (Default) --------- */ :host, :host([color-scheme='dark']), :host([color-scheme='system']), :host(:not([color-scheme])) { /* Surfaces: neutral charcoal. Tinted themes override these two; everything below is derived from them. */ --background: #1f1f23; --foreground: #d4d4d8; /* Card Vars: card (bg) : dialog/modal popups, table headers (th), floating tooltips, and catalog cards card-foreground : Text & icon color on elevated surfaces (dialog content, table header labels, tooltip text, schema type badges) */ --card: color-mix(in srgb, var(--background) 92%, var(--foreground)); --card-foreground: color-mix(in srgb, var(--foreground) 90%, #ffffff); /* Muted Vars: muted (bg) : input tags, secondary buttons, table footers, and code backgrounds muted-foreground : inactive tabs, parameter descriptions, schema constraints, and timestamps */ --muted: color-mix(in srgb, var(--background) 86%, var(--foreground)); --muted-foreground: color-mix(in srgb, var(--foreground) 65%, var(--background)); /* Border Vars: border: dividing lines for tables, cards, sections, and panels */ --border: color-mix(in srgb, var(--background) 85%, var(--foreground)); --sidebar-accent: color-mix(in srgb, var(--background) 88%, var(--foreground)); --input: oklch(from var(--border) calc(l + 0.05) c h); --input-background: color-mix(in srgb, var(--background) 80%, #000000); /* HTTP Method Badges (Dark Mode: High Vibrancy) */ --method-get: #5c98f9ff; --method-post: #68cc97; --method-put: #f59e0b; --method-delete: #f75252ff; --method-patch: #a78bfa; /* violet-400: 4.5:1 or more as text on the dark background */ --method-head: #eab308; --method-options: #06b6d4; --method-trace: #ec4899; --method-query: #818cf8; --method-default: #64748b; --method-badge-fg: #000000; --destructive: #ef4444; /* Named colours (also used by x-badges: color: blue | green | ...) */ --blue: #3b82f6; --green: #10b981; --orange: #f59e0b; --red: #ef4444; --purple: #8b5cf6; --yellow: #eab308; --pink: #ec4899; --brown: #d97706; /* Syntax Highlighting */ --syntax-comment: #8b949e; --syntax-keyword: #ff7b72; --syntax-operator: #c9d1d9; --syntax-string: #a5d6ff; --syntax-constant: #79c0ff; --syntax-function: #d2a8ff; --syntax-type: #d2a8ff; --syntax-variable: #ffa657; --syntax-property: #79c0ff; --syntax-tag: #7ee787; --syntax-selector: #d2a8ff; --syntax-inserted: #7ee787; --syntax-deleted: #ff7b72; /* Code Blocks */ --code-bg: var(--input-background); } /* --------- 3B. LIGHT --------- */ :host([color-scheme='light']) { /* Surfaces */ --background: #ffffff; --foreground: #27272a; /* Card Vars: card (bg) : dialog/modal popups, table headers (th), floating tooltips, and catalog cards card-foreground : Text & icon color on elevated surfaces */ --card: color-mix(in srgb, var(--background) 94%, var(--foreground)); --card-foreground: color-mix(in srgb, var(--foreground) 90%, #000000); /* Muted Vars: muted (bg) : input tags, secondary buttons, table footers, and code backgrounds muted-foreground : inactive tabs, parameter descriptions, schema constraints, and timestamps */ --muted: color-mix(in srgb, var(--background) 94%, var(--foreground)); --muted-foreground: color-mix(in srgb, var(--foreground) 60%, var(--background)); /* Border Vars: border: dividing lines for tables, cards, sections, and panels */ --border: color-mix(in srgb, var(--background) 85%, var(--foreground)); --sidebar-accent: color-mix(in srgb, var(--background) 92%, var(--foreground)); --input: color-mix(in srgb, var(--border) 75%, var(--foreground)); /* Light: fields are white, so they stand out on the tinted card surface */ --input-background: var(--background); /* HTTP Method Badges (Light Mode: High Contrast). White badge text, and the colors used as text on the tinted card surface, keep WCAG AA contrast (4.5:1). */ --method-get: #2563eb; --method-post: #047857; --method-put: #c2410c; --method-delete: #b91c1c; --method-patch: #7c3aed; --method-head: #b45309; --method-options: #0e7490; --method-trace: #be185d; --method-query: #4f46e5; --method-default: #475569; --method-badge-fg: #ffffff; --destructive: #dc2626; /* Named colours (also used by x-badges: color: blue | green | ...) */ --blue: #2768f4ff; --green: #059669; --orange: #d97706; --red: #dc2626; --purple: #7c3aed; --yellow: #b45309; --pink: #db2777; --brown: #92400e; /* Syntax Highlighting */ --syntax-comment: #6e7781; --syntax-keyword: #cf222e; --syntax-operator: #24292f; --syntax-string: #0a3069; --syntax-constant: #0550ae; --syntax-function: #8250df; --syntax-type: #8250df; --syntax-variable: #953800; --syntax-property: #0550ae; --syntax-tag: #116329; --syntax-selector: #8250df; --syntax-inserted: #116329; --syntax-deleted: #cf222e; /* Code Blocks */ --code-bg: color-mix(in srgb, var(--background) 96%, var(--foreground)); } /* --------- 3C. SYSTEM --------- */ @media (prefers-color-scheme: light) { :host([color-scheme='system']) { --background: #ffffff; --foreground: #27272a; --card: color-mix(in srgb, var(--background) 94%, var(--foreground)); --card-foreground: color-mix(in srgb, var(--foreground) 90%, #000000); --muted: color-mix(in srgb, var(--background) 94%, var(--foreground)); --muted-foreground: color-mix(in srgb, var(--foreground) 60%, var(--background)); --border: color-mix(in srgb, var(--background) 85%, var(--foreground)); --sidebar-accent: color-mix(in srgb, var(--background) 92%, var(--foreground)); --input: color-mix(in srgb, var(--border) 75%, var(--foreground)); --input-background: var(--background); --method-get: #2563eb; --method-post: #047857; --method-put: #c2410c; --method-delete: #b91c1c; --method-patch: #7c3aed; --method-head: #b45309; --method-options: #0e7490; --method-trace: #be185d; --method-query: #4f46e5; --method-default: #475569; --method-badge-fg: #ffffff; --destructive: #dc2626; --blue: #2768f4ff; --green: #059669; --orange: #d97706; --red: #dc2626; --purple: #7c3aed; --yellow: #b45309; --pink: #db2777; --brown: #92400e; --syntax-comment: #6e7781; --syntax-keyword: #cf222e; --syntax-operator: #24292f; --syntax-string: #0a3069; --syntax-constant: #0550ae; --syntax-function: #8250df; --syntax-type: #8250df; --syntax-variable: #953800; --syntax-property: #0550ae; --syntax-tag: #116329; --syntax-selector: #8250df; --syntax-inserted: #116329; --syntax-deleted: #cf222e; --code-bg: color-mix(in srgb, var(--background) 96%, var(--foreground)); } } /* ========================================================================= 4. BRAND THEME PRESETS (theme), generated from THEMES above ========================================================================= */ ${presetsCss} `;