Skip to content

Commit f6cc334

Browse files
sserrataclaude
andauthored
feat(demo): modernized styles with swappable palettes and runtime theme switcher (#1371)
* feat(demo): modernize styles inspired by glean developer site - Replace default Docusaurus green with Glean blue (#343ced) primary palette - Add Inter font for body text, JetBrains Mono for code - Full dark mode with navy base (#111827) and gray emphasis scale - Modernize HTTP method badges: pill-style with translucent inactive / solid active states, dark mode variants for all methods - Port sidebar behavior: transparent hover, uppercase top-level categories, active item uses primary color (no background fill) - Add schema parameter highlight-flash animation on anchor target - Add custom scrollbar styling for code tab panels - Add border-radius and shadow scale variables - Add grid utility classes - Port admonition border removal + border-radius - Enable prismThemes.github/dracula for code blocks - Add colorMode.respectPrefersColorScheme: true - Remove navbar title text (logo-only nav) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(demo): align API page badge colors with sidebar method labels MethodEndpoint uses .badge--primary/success/danger/info/warning which map to different Infima colors than the sidebar ::before pseudo-elements. Override all badge classes to use the same color scheme: GET → badge--primary → green (#16a34a / #22c55e dark) POST → badge--success → navy (#004c9d / #5eb7ff dark) DELETE → badge--danger → red (#dc2626 / #ef4444 dark) PUT → badge--info → amber (#d97706 / #f59e0b dark) PATCH → badge--warning → amber (#d97706 / #f59e0b dark) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(demo): restore contrast on send button in dark mode --ifm-color-primary-light resolves to #acd9ff in dark mode, making white text nearly invisible. Pin the request button to PANW navy (#004c9d) in dark mode to ensure readable contrast. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * refactor(demo): split CSS into structure + swappable theme files custom.css is now purely structural (layout, animations, utilities). All colors, fonts, and palette-specific values live in a theme file. To try a different palette, swap one line in docusaurus.config.ts: customCss: ["./src/css/custom.css", "./src/css/themes/<theme>.css"] themes/panw.css — Palo Alto Networks brand (pan.dev): - Plus Jakarta Sans + Fira Code fonts - PANW navy primary (#004c9d), orange accent (#fa582d) - Near-black dark mode (#0b1117) with steel-blue gray scale - Method badge colors, .badge--* overrides, send button fix custom.css changes: - Uses --theme-primary-rgb for the highlight-flash animation - Uses --theme-border-radius-sm/md with fallback defaults - Badge content labels separated from colors (colors in theme) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(theme): add violet palette and activate it Adds demo/src/css/themes/violet.css (Space Grotesk + JetBrains Mono, Violet-600 primary, deep purple-tinted dark mode) and switches docusaurus.config.ts to load it instead of panw.css. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(theme): add cyber palette and activate it Adds demo/src/css/themes/cyber.css — neon green (#00ff88) on near-black (#030a06), monospace (JetBrains Mono) everywhere including body and headings, electric cyan POST badges, hot pink DELETE badges in dark mode, and sharp 2-4px border radii throughout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(theme): add midnight palette and activate it Adds demo/src/css/themes/midnight.css — slate-900 (#0f172a) base with sky-blue (#38bdf8) accents, Inter body font, JetBrains Mono code. Active badges use dark text on bright fills for contrast on the slate background. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(theme): add indigo palette and activate it Adds demo/src/css/themes/indigo.css — the modern dev site aesthetic. Zinc-950 (#09090b) dark base, indigo-500 (#6366f1) accent, Geist + Geist Mono font stack. Neutral shadows (no color tint), subtle badge opacity, matches the shadcn/ui + Linear + Clerk visual language. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(theme): add nord palette and activate it Adds demo/src/css/themes/nord.css — faithful to the Nord color palette (Arctic Ice Studio). Polar Night (#2e3440) dark base, Frost blue-cyan (#88c0d0) primary, Aurora semantic colors for method badges throughout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(theme): add evergreen palette and activate it Adds demo/src/css/themes/evergreen.css — a modern riff on the classic Docusaurus green (#2e8555). Keeps the signature primary, upgrades to Plus Jakarta Sans + Fira Code, deepens the dark mode to #18191a, and applies neutral shadows throughout. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(theme/evergreen): increase explorer code font size to 14px Overrides --openapi-explorer-font-size-code from the 12px default to 14px for better readability in the request body example panel. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(theme/evergreen): tie explorer code font size to --ifm-code-font-size Uses var(--ifm-code-font-size) instead of a hardcoded pixel value so the request body panel always matches the code snippet blocks. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(theme/evergreen): unify explorer body and code tab font sizes Sets --openapi-explorer-font-size-code to 13px and explicitly applies it to .openapi-explorer__playground-editor plus its inner textarea and pre elements, which react-simple-code-editor renders outside the reach of the outer font shorthand. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(palette-picker): add runtime theme switcher to navbar Moves all theme CSS to demo/static/themes/ for URL-accessible dynamic loading. Registers a custom-PalettePicker navbar item that injects the selected /themes/<id>.css at runtime, persists the choice in localStorage, and restores it on reload via an inline <head> script. * fix(palette-picker): swizzle NavbarItem/index instead of ComponentTypes ComponentTypes swizzle was silently not loading. Replacing it with a NavbarItem/index wrapper that intercepts custom-PalettePicker before the ComponentTypes lookup, which is more reliable across Docusaurus theme chain configurations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(palette-picker): hide in mobile top bar via media query The component already returns null for mobile={true} (drawer), but Docusaurus also renders right-side navbar items in the collapsed mobile top bar without that prop. Hide .root below 996px to match Docusaurus's own navbar collapse breakpoint. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(palette-picker): mobile-friendly compact button + bottom sheet On mobile (≤996px): - Trigger collapses to a 36px circular swatch-only button (no text/chevron) that fits cleanly in the navbar top bar - Dropdown becomes a fixed bottom sheet with rounded top corners, a drag handle indicator, and larger touch targets (12px padding per row) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(palette-picker): move mobile picker into hamburger drawer Desktop: unchanged pill button + dropdown in the top bar. Mobile: top-bar button hidden; instead renders a 2-column grid of labeled color swatches inside the hamburger drawer (mobile={true}) with active-state border highlight and a section heading. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(palette-picker): make mobile drawer section collapsible Replaces the always-open grid with an accordion toggle button showing the current palette swatch + "Color Palette" label and a chevron. The theme grid only renders when expanded, keeping the drawer compact by default. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(palette-picker): mobile chevron starts right (>) rotates to down (v) Matches Docusaurus's own collapsible nav item caret behavior. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c2301d9 commit f6cc334

File tree

19 files changed

+5145
-77
lines changed

19 files changed

+5145
-77
lines changed

demo/docusaurus.config.ts

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { themes as prismThemes } from "prism-react-renderer";
12
import type * as Preset from "@docusaurus/preset-classic";
23
import type { Config } from "@docusaurus/types";
34
import type * as Plugin from "@docusaurus/types/src/plugin";
@@ -40,7 +41,7 @@ const config: Config = {
4041
},
4142
blog: false,
4243
theme: {
43-
customCss: "./src/css/custom.css",
44+
customCss: ["./src/css/custom.css"],
4445
},
4546
gtag: {
4647
trackingID: "GTM-THVM29S",
@@ -56,10 +57,14 @@ const config: Config = {
5657
hideable: true,
5758
},
5859
},
60+
colorMode: {
61+
defaultMode: "light",
62+
disableSwitch: false,
63+
respectPrefersColorScheme: true,
64+
},
5965
navbar: {
60-
title: "OpenAPI Docs",
6166
logo: {
62-
alt: "Keytar",
67+
alt: "OpenAPI Docs",
6368
src: "img/docusaurus-openapi-docs-logo.svg",
6469
},
6570
items: [
@@ -88,6 +93,7 @@ const config: Config = {
8893
},
8994
],
9095
},
96+
{ type: "custom-PalettePicker", position: "right" },
9197
{
9298
href: "https://medium.com/palo-alto-networks-developer-blog",
9399
position: "right",
@@ -148,6 +154,8 @@ const config: Config = {
148154
copyright: `Copyright © ${new Date().getFullYear()} Palo Alto Networks, Inc. Built with Docusaurus ${DOCUSAURUS_VERSION}.`,
149155
},
150156
prism: {
157+
theme: prismThemes.github,
158+
darkTheme: prismThemes.dracula,
151159
additionalLanguages: [
152160
"ruby",
153161
"csharp",
@@ -372,6 +380,22 @@ const config: Config = {
372380
} satisfies Plugin.PluginOptions,
373381
},
374382
],
383+
// FOUC prevention: restore saved palette before React hydrates
384+
function paletteScript() {
385+
return {
386+
name: "palette-fouc-script",
387+
injectHtmlTags() {
388+
return {
389+
headTags: [
390+
{
391+
tagName: "script",
392+
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){}`,
393+
},
394+
],
395+
};
396+
},
397+
};
398+
},
375399
],
376400
themes: ["docusaurus-theme-openapi-docs"],
377401
stylesheets: [
Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
/* ============================================================================
2+
* PalettePicker — runtime color palette switcher for the navbar.
3+
*
4+
* Desktop: pill button in the top bar that opens a dropdown.
5+
* Mobile: section rendered inside the hamburger drawer (mobile={true}).
6+
*
7+
* Dynamically injects /themes/<id>.css into <head> and persists
8+
* the choice in localStorage under 'openapi-demo-palette'.
9+
* ========================================================================== */
10+
11+
import React, { useEffect, useRef, useState } from "react";
12+
13+
import styles from "./styles.module.css";
14+
15+
const THEMES = [
16+
{ id: "evergreen", label: "Evergreen", color: "#2e8555" },
17+
{ id: "panw", label: "PANW", color: "#004c9d" },
18+
{ id: "violet", label: "Violet", color: "#7c3aed" },
19+
{ id: "midnight", label: "Midnight", color: "#0284c7" },
20+
{ id: "indigo", label: "Indigo", color: "#6366f1" },
21+
{ id: "nord", label: "Nord", color: "#5e81ac" },
22+
{ id: "cyber", label: "Cyber", color: "#059669" },
23+
] as const;
24+
25+
type ThemeId = (typeof THEMES)[number]["id"];
26+
27+
const STORAGE_KEY = "openapi-demo-palette";
28+
const DEFAULT_PALETTE: ThemeId = "evergreen";
29+
30+
function applyPalette(id: ThemeId): void {
31+
let link = document.getElementById(
32+
"openapi-palette-link"
33+
) as HTMLLinkElement | null;
34+
if (!link) {
35+
link = document.createElement("link");
36+
link.id = "openapi-palette-link";
37+
link.rel = "stylesheet";
38+
document.head.appendChild(link);
39+
}
40+
link.href = `/themes/${id}.css`;
41+
localStorage.setItem(STORAGE_KEY, id);
42+
}
43+
44+
function MobileSection({
45+
active,
46+
onSelect,
47+
current,
48+
}: {
49+
active: ThemeId;
50+
onSelect: (id: ThemeId) => void;
51+
current: (typeof THEMES)[number];
52+
}) {
53+
const [expanded, setExpanded] = useState(false);
54+
55+
return (
56+
<div className={styles.mobileRoot}>
57+
<button
58+
className={styles.mobileToggle}
59+
onClick={() => setExpanded((e) => !e)}
60+
aria-expanded={expanded}
61+
>
62+
<span className={styles.mobileToggleLeft}>
63+
<span
64+
className={styles.mobileSwatch}
65+
style={{ background: current.color }}
66+
/>
67+
<span>Color Palette</span>
68+
</span>
69+
<svg
70+
className={`${styles.mobileChevron} ${expanded ? styles.mobileChevronOpen : ""}`}
71+
width="10"
72+
height="10"
73+
viewBox="0 0 10 10"
74+
fill="none"
75+
aria-hidden="true"
76+
>
77+
<path
78+
d="M2 3.5L5 6.5L8 3.5"
79+
stroke="currentColor"
80+
strokeWidth="1.5"
81+
strokeLinecap="round"
82+
strokeLinejoin="round"
83+
/>
84+
</svg>
85+
</button>
86+
87+
{expanded && (
88+
<div className={styles.mobileGrid}>
89+
{THEMES.map((theme) => (
90+
<button
91+
key={theme.id}
92+
className={`${styles.mobileTile} ${theme.id === active ? styles.mobileTileActive : ""}`}
93+
onClick={() => onSelect(theme.id)}
94+
aria-pressed={theme.id === active}
95+
>
96+
<span
97+
className={styles.mobileSwatch}
98+
style={{ background: theme.color }}
99+
/>
100+
<span>{theme.label}</span>
101+
</button>
102+
))}
103+
</div>
104+
)}
105+
</div>
106+
);
107+
}
108+
109+
export default function PalettePicker({
110+
mobile,
111+
}: {
112+
mobile?: boolean;
113+
}): JSX.Element | null {
114+
const [open, setOpen] = useState(false);
115+
const [active, setActive] = useState<ThemeId>(DEFAULT_PALETTE);
116+
const containerRef = useRef<HTMLDivElement>(null);
117+
118+
useEffect(() => {
119+
const saved =
120+
(localStorage.getItem(STORAGE_KEY) as ThemeId) ?? DEFAULT_PALETTE;
121+
setActive(saved);
122+
if (!document.getElementById("openapi-palette-link")) {
123+
applyPalette(saved);
124+
}
125+
}, []);
126+
127+
// Close dropdown on outside pointer-down (desktop only)
128+
useEffect(() => {
129+
if (mobile) return;
130+
function onPointerDown(e: PointerEvent) {
131+
if (
132+
containerRef.current &&
133+
!containerRef.current.contains(e.target as Node)
134+
) {
135+
setOpen(false);
136+
}
137+
}
138+
document.addEventListener("pointerdown", onPointerDown);
139+
return () => document.removeEventListener("pointerdown", onPointerDown);
140+
}, [mobile]);
141+
142+
useEffect(() => {
143+
if (mobile) return;
144+
function onKeyDown(e: KeyboardEvent) {
145+
if (e.key === "Escape") setOpen(false);
146+
}
147+
document.addEventListener("keydown", onKeyDown);
148+
return () => document.removeEventListener("keydown", onKeyDown);
149+
}, [mobile]);
150+
151+
function select(id: ThemeId) {
152+
setActive(id);
153+
applyPalette(id);
154+
setOpen(false);
155+
}
156+
157+
const current = THEMES.find((t) => t.id === active) ?? THEMES[0];
158+
159+
/* ---- Mobile drawer ---------------------------------------------------- */
160+
if (mobile) {
161+
return (
162+
<MobileSection active={active} onSelect={select} current={current} />
163+
);
164+
}
165+
166+
/* ---- Desktop dropdown ------------------------------------------------- */
167+
return (
168+
<div ref={containerRef} className={styles.root}>
169+
<button
170+
className={styles.trigger}
171+
onClick={() => setOpen((o) => !o)}
172+
aria-haspopup="listbox"
173+
aria-expanded={open}
174+
aria-label={`Color palette: ${current.label}`}
175+
>
176+
<span
177+
className={styles.swatch}
178+
style={{ background: current.color }}
179+
aria-hidden="true"
180+
/>
181+
<span className={styles.triggerLabel}>{current.label}</span>
182+
<svg
183+
className={`${styles.chevron} ${open ? styles.chevronOpen : ""}`}
184+
width="10"
185+
height="10"
186+
viewBox="0 0 10 10"
187+
fill="none"
188+
aria-hidden="true"
189+
>
190+
<path
191+
d="M2 3.5L5 6.5L8 3.5"
192+
stroke="currentColor"
193+
strokeWidth="1.5"
194+
strokeLinecap="round"
195+
strokeLinejoin="round"
196+
/>
197+
</svg>
198+
</button>
199+
200+
{open && (
201+
<div
202+
className={styles.dropdown}
203+
role="listbox"
204+
aria-label="Select color palette"
205+
>
206+
{THEMES.map((theme) => (
207+
<button
208+
key={theme.id}
209+
role="option"
210+
aria-selected={theme.id === active}
211+
className={`${styles.option} ${theme.id === active ? styles.optionActive : ""}`}
212+
onClick={() => select(theme.id)}
213+
>
214+
<span
215+
className={styles.swatch}
216+
style={{ background: theme.color }}
217+
aria-hidden="true"
218+
/>
219+
{theme.label}
220+
{theme.id === active && (
221+
<svg
222+
className={styles.check}
223+
width="12"
224+
height="12"
225+
viewBox="0 0 12 12"
226+
fill="none"
227+
aria-hidden="true"
228+
>
229+
<path
230+
d="M2 6L5 9L10 3"
231+
stroke="currentColor"
232+
strokeWidth="1.5"
233+
strokeLinecap="round"
234+
strokeLinejoin="round"
235+
/>
236+
</svg>
237+
)}
238+
</button>
239+
))}
240+
</div>
241+
)}
242+
</div>
243+
);
244+
}

0 commit comments

Comments
 (0)