Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8e8f807
feat(demo): modernize styles inspired by glean developer site
sserrata Mar 30, 2026
7250b68
fix(demo): align API page badge colors with sidebar method labels
sserrata Mar 30, 2026
71aae6c
fix(demo): restore contrast on send button in dark mode
sserrata Mar 30, 2026
4a7f9b2
refactor(demo): split CSS into structure + swappable theme files
sserrata Mar 30, 2026
991b4de
feat(theme): add violet palette and activate it
sserrata Mar 30, 2026
a582590
feat(theme): add cyber palette and activate it
sserrata Mar 30, 2026
7cd1e99
feat(theme): add midnight palette and activate it
sserrata Mar 30, 2026
dd7883d
feat(theme): add indigo palette and activate it
sserrata Mar 30, 2026
7d9f9e3
feat(theme): add nord palette and activate it
sserrata Mar 30, 2026
73c51f0
feat(theme): add evergreen palette and activate it
sserrata Mar 30, 2026
6c26bd1
fix(theme/evergreen): increase explorer code font size to 14px
sserrata Mar 30, 2026
4a08874
fix(theme/evergreen): tie explorer code font size to --ifm-code-font-…
sserrata Mar 30, 2026
0fd4b2f
fix(theme/evergreen): unify explorer body and code tab font sizes
sserrata Mar 30, 2026
47e8e91
feat(palette-picker): add runtime theme switcher to navbar
sserrata Mar 30, 2026
09c48c6
fix(palette-picker): swizzle NavbarItem/index instead of ComponentTypes
sserrata Mar 30, 2026
f7d5f65
fix(palette-picker): hide in mobile top bar via media query
sserrata Mar 31, 2026
5bc9d0d
fix(palette-picker): mobile-friendly compact button + bottom sheet
sserrata Mar 31, 2026
73a1008
fix(palette-picker): move mobile picker into hamburger drawer
sserrata Mar 31, 2026
62f1393
fix(palette-picker): make mobile drawer section collapsible
sserrata Mar 31, 2026
9c99d53
fix(palette-picker): mobile chevron starts right (>) rotates to down (v)
sserrata Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions demo/docusaurus.config.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { themes as prismThemes } from "prism-react-renderer";
import type * as Preset from "@docusaurus/preset-classic";
import type { Config } from "@docusaurus/types";
import type * as Plugin from "@docusaurus/types/src/plugin";
Expand Down Expand Up @@ -40,7 +41,7 @@ const config: Config = {
},
blog: false,
theme: {
customCss: "./src/css/custom.css",
customCss: ["./src/css/custom.css"],
},
gtag: {
trackingID: "GTM-THVM29S",
Expand All @@ -56,10 +57,14 @@ const config: Config = {
hideable: true,
},
},
colorMode: {
defaultMode: "light",
disableSwitch: false,
respectPrefersColorScheme: true,
},
navbar: {
title: "OpenAPI Docs",
logo: {
alt: "Keytar",
alt: "OpenAPI Docs",
src: "img/docusaurus-openapi-docs-logo.svg",
},
items: [
Expand Down Expand Up @@ -88,6 +93,7 @@ const config: Config = {
},
],
},
{ type: "custom-PalettePicker", position: "right" },
{
href: "https://medium.com/palo-alto-networks-developer-blog",
position: "right",
Expand Down Expand Up @@ -148,6 +154,8 @@ const config: Config = {
copyright: `Copyright © ${new Date().getFullYear()} Palo Alto Networks, Inc. Built with Docusaurus ${DOCUSAURUS_VERSION}.`,
},
prism: {
theme: prismThemes.github,
darkTheme: prismThemes.dracula,
additionalLanguages: [
"ruby",
"csharp",
Expand Down Expand Up @@ -372,6 +380,22 @@ const config: Config = {
} satisfies Plugin.PluginOptions,
},
],
// FOUC prevention: restore saved palette before React hydrates
function paletteScript() {
return {
name: "palette-fouc-script",
injectHtmlTags() {
return {
headTags: [
{
tagName: "script",
innerHTML: `try{var p=localStorage.getItem('openapi-demo-palette');if(p){var l=document.createElement('link');l.id='openapi-palette-link';l.rel='stylesheet';l.href='/themes/'+p+'.css';document.head.appendChild(l);}}catch(e){}`,
},
],
};
},
};
},
],
themes: ["docusaurus-theme-openapi-docs"],
stylesheets: [
Expand Down
244 changes: 244 additions & 0 deletions demo/src/components/PalettePicker/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
/* ============================================================================
* PalettePicker — runtime color palette switcher for the navbar.
*
* Desktop: pill button in the top bar that opens a dropdown.
* Mobile: section rendered inside the hamburger drawer (mobile={true}).
*
* Dynamically injects /themes/<id>.css into <head> and persists
* the choice in localStorage under 'openapi-demo-palette'.
* ========================================================================== */

import React, { useEffect, useRef, useState } from "react";

import styles from "./styles.module.css";

const THEMES = [
{ id: "evergreen", label: "Evergreen", color: "#2e8555" },
{ id: "panw", label: "PANW", color: "#004c9d" },
{ id: "violet", label: "Violet", color: "#7c3aed" },
{ id: "midnight", label: "Midnight", color: "#0284c7" },
{ id: "indigo", label: "Indigo", color: "#6366f1" },
{ id: "nord", label: "Nord", color: "#5e81ac" },
{ id: "cyber", label: "Cyber", color: "#059669" },
] as const;

type ThemeId = (typeof THEMES)[number]["id"];

const STORAGE_KEY = "openapi-demo-palette";
const DEFAULT_PALETTE: ThemeId = "evergreen";

function applyPalette(id: ThemeId): void {
let link = document.getElementById(
"openapi-palette-link"
) as HTMLLinkElement | null;
if (!link) {
link = document.createElement("link");
link.id = "openapi-palette-link";
link.rel = "stylesheet";
document.head.appendChild(link);
}
link.href = `/themes/${id}.css`;
localStorage.setItem(STORAGE_KEY, id);
}

function MobileSection({
active,
onSelect,
current,
}: {
active: ThemeId;
onSelect: (id: ThemeId) => void;
current: (typeof THEMES)[number];
}) {
const [expanded, setExpanded] = useState(false);

return (
<div className={styles.mobileRoot}>
<button
className={styles.mobileToggle}
onClick={() => setExpanded((e) => !e)}
aria-expanded={expanded}
>
<span className={styles.mobileToggleLeft}>
<span
className={styles.mobileSwatch}
style={{ background: current.color }}
/>
<span>Color Palette</span>
</span>
<svg
className={`${styles.mobileChevron} ${expanded ? styles.mobileChevronOpen : ""}`}
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
aria-hidden="true"
>
<path
d="M2 3.5L5 6.5L8 3.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>

{expanded && (
<div className={styles.mobileGrid}>
{THEMES.map((theme) => (
<button
key={theme.id}
className={`${styles.mobileTile} ${theme.id === active ? styles.mobileTileActive : ""}`}
onClick={() => onSelect(theme.id)}
aria-pressed={theme.id === active}
>
<span
className={styles.mobileSwatch}
style={{ background: theme.color }}
/>
<span>{theme.label}</span>
</button>
))}
</div>
)}
</div>
);
}

export default function PalettePicker({
mobile,
}: {
mobile?: boolean;
}): JSX.Element | null {
const [open, setOpen] = useState(false);
const [active, setActive] = useState<ThemeId>(DEFAULT_PALETTE);
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
const saved =
(localStorage.getItem(STORAGE_KEY) as ThemeId) ?? DEFAULT_PALETTE;
setActive(saved);
if (!document.getElementById("openapi-palette-link")) {
applyPalette(saved);
}
}, []);

// Close dropdown on outside pointer-down (desktop only)
useEffect(() => {
if (mobile) return;
function onPointerDown(e: PointerEvent) {
if (
containerRef.current &&
!containerRef.current.contains(e.target as Node)
) {
setOpen(false);
}
}
document.addEventListener("pointerdown", onPointerDown);
return () => document.removeEventListener("pointerdown", onPointerDown);
}, [mobile]);

useEffect(() => {
if (mobile) return;
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("keydown", onKeyDown);
return () => document.removeEventListener("keydown", onKeyDown);
}, [mobile]);

function select(id: ThemeId) {
setActive(id);
applyPalette(id);
setOpen(false);
}

const current = THEMES.find((t) => t.id === active) ?? THEMES[0];

/* ---- Mobile drawer ---------------------------------------------------- */
if (mobile) {
return (
<MobileSection active={active} onSelect={select} current={current} />
);
}

/* ---- Desktop dropdown ------------------------------------------------- */
return (
<div ref={containerRef} className={styles.root}>
<button
className={styles.trigger}
onClick={() => setOpen((o) => !o)}
aria-haspopup="listbox"
aria-expanded={open}
aria-label={`Color palette: ${current.label}`}
>
<span
className={styles.swatch}
style={{ background: current.color }}
aria-hidden="true"
/>
<span className={styles.triggerLabel}>{current.label}</span>
<svg
className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`}
width="10"
height="10"
viewBox="0 0 10 10"
fill="none"
aria-hidden="true"
>
<path
d="M2 3.5L5 6.5L8 3.5"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>

{open && (
<div
className={styles.dropdown}
role="listbox"
aria-label="Select color palette"
>
{THEMES.map((theme) => (
<button
key={theme.id}
role="option"
aria-selected={theme.id === active}
className={`${styles.option} ${theme.id === active ? styles.optionActive : ""}`}
onClick={() => select(theme.id)}
>
<span
className={styles.swatch}
style={{ background: theme.color }}
aria-hidden="true"
/>
{theme.label}
{theme.id === active && (
<svg
className={styles.check}
width="12"
height="12"
viewBox="0 0 12 12"
fill="none"
aria-hidden="true"
>
<path
d="M2 6L5 9L10 3"
stroke="currentColor"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</button>
))}
</div>
)}
</div>
);
}
Loading
Loading