mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-21 21:15:56 +08:00
* improvement(docs): clear leftovers from the reverted revisions
A cleanup pass over the final state. Every finding was residue from an approach
this PR tried and abandoned, or a claim that stopped being true when it did.
- Delete the copy-button svg sizing rule: a later rule sets `display: none` on
that same element ungated, so sizing it was never observable. Superseded by
the mask approach.
- Drop the paragraph in page.tsx arguing about a custom Shiki factory. The
factory was deleted; nothing configures one now.
- Correct shiki-curl-json.ts, which still claimed the grammar "reaches the
client path too". It does not — that was the justification for choosing a
grammar over a transformer, so leaving it stated the opposite of the truth.
Now records where it applies, where it does not, and why not to retry.
- Correct the global.css section header, which claimed the component owns the
shell while the next rule defines it here.
- Qualify the `--copy-glyph` declarations with `:has(> svg[class*="lucide"])`,
which the group's own comment asserts of every rule in it.
- Correct `getCode`'s TSDoc: the gutter is a `::before`, and pseudo-element
content never reaches `textContent`, so line numbers were never what the
clone guards. It guards transformer-emitted `.nd-copy-ignore` nodes.
- Compose `chipGeometryClass` and emcn's `ChipChevronDown` in the API example
selector instead of restating their literals.
- Merge the duplicated `div[role="region"]` rule. The tablist pair stays split:
biome's `noDuplicateProperties` reads a nested `@variant` setting the same
property as a duplicate and fails the build — recorded so it is not remerged.
- Note that fumadocs ships its own gutter for `lines`-meta fences, which cannot
be suppressed from here and would paint a second column.
* fix(docs): drop a highlighter registration that can never fire
fumadocs-openapi calls `renderCodeBlock` with a hard-coded `"json"` from both of
its call sites (`request-tabs.js:76`, `response-tabs.js:48`), so the docs
`CodeBlock` it routes through never receives a shell language. The
`getHighlighter('js', { langs: [curlJsonBodyGrammar] })` registering the
shell-scoped JSON-body injection therefore did nothing but await on every API
sample render, and the docblock claiming the grammar covers those samples was
wrong.
- Delete the call and its imports.
- State the grammar's real coverage: prose fences only, via `langs`. Both API
reference paths are unreachable — samples are JSON, and the cURL usage tabs
highlight client-side off fumadocs' own factory.
- Correct `code-block.tsx`'s TSDoc, which still said API samples come from
fumadocs' own renderer. They come through this component; `UsageTab` is the
renderer that bypasses it.
- Re-home a comment orphaned when two CSS rules merged — it had drifted onto
the rule below and read as documenting it.
- Drop a `.nd-copy-ignore` claim about transformers emitting those nodes;
nothing here does, and upstream parity is the reason the clone exists.
2019 lines
70 KiB
CSS
2019 lines
70 KiB
CSS
@import "tailwindcss";
|
|
@import "fumadocs-ui/css/neutral.css";
|
|
@import "fumadocs-ui/css/preset.css";
|
|
@import "fumadocs-openapi/css/preset.css";
|
|
@source "../../../packages/emcn/src";
|
|
@source "../../../packages/workflow-renderer/src";
|
|
|
|
/* Every @sim/emcn component expresses hover through `hover-hover:` so touch
|
|
devices never latch a sticky hover state. The app registers it as a plugin
|
|
variant in apps/sim/tailwind.config.ts; docs is CSS-first Tailwind v4 with no
|
|
config, so without this declaration the variant compiles to nothing and every
|
|
emcn hover state silently no-ops here. */
|
|
@custom-variant hover-hover {
|
|
@media (hover: hover) and (pointer: fine) {
|
|
&:hover {
|
|
@slot;
|
|
}
|
|
}
|
|
}
|
|
|
|
/* Prevent overscroll bounce effect on the page */
|
|
html,
|
|
body {
|
|
overscroll-behavior: none;
|
|
}
|
|
|
|
/* Prevent modals/dialogs from shifting layout via scroll-lock compensation */
|
|
html,
|
|
body {
|
|
padding-right: 0 !important;
|
|
margin-right: 0 !important;
|
|
}
|
|
|
|
@theme {
|
|
--color-fd-primary: var(--color-fd-foreground);
|
|
/* Sim's custom type scale — emcn components (Label, Badge, the shared block
|
|
views) use these names, so they must resolve here or text falls back to the
|
|
inherited size. Mirrors apps/sim/tailwind.config.ts. */
|
|
--text-micro: 10px;
|
|
--text-xs: 11px;
|
|
--text-caption: 12px;
|
|
--text-small: 13px;
|
|
--text-base: 15px;
|
|
--text-md: 16px;
|
|
|
|
/* Code-token size for the API reference — a deliberate sixth step, between
|
|
--text-caption and --text-small, because the mono face reads small at 12px. */
|
|
--text-code: 0.78125rem;
|
|
|
|
/* The platform's code face. `apps/sim/tailwind.config.ts` lists `var(--font-martian-mono)`
|
|
first, but nothing in the app ever defines that variable — `apps/sim/app/layout.tsx`
|
|
applies only `season.variable` — so every code surface in the product resolves to the
|
|
system stack below. Docs match what the app actually renders rather than the token it
|
|
nominally references; loading a webfont here would make docs the odd one out, not the
|
|
aligned one. If the app ever wires that font up for real, add the var back in both
|
|
places at once. */
|
|
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
|
|
"Courier New", monospace;
|
|
}
|
|
|
|
/* Pure white light mode background */
|
|
:root:not(.dark) {
|
|
--color-fd-background: hsl(0, 0%, 100%) !important;
|
|
}
|
|
|
|
/* Match landing page dark background (#1b1b1b) */
|
|
.dark {
|
|
--color-fd-background: hsl(0, 0%, 10.6%) !important;
|
|
--color-fd-card: hsl(0, 0%, 13%) !important;
|
|
--color-fd-popover: hsl(0, 0%, 14%) !important;
|
|
--color-fd-secondary: hsl(0, 0%, 15.5%) !important;
|
|
--color-fd-muted: hsl(0, 0%, 18%) !important;
|
|
}
|
|
|
|
/*
|
|
* emcn workspace tokens — mirrored from apps/sim/app/_styles/globals.css so the
|
|
* docs chip chrome (components/ui/chip.tsx, dropdown-menu.tsx, badges) renders
|
|
* identically to the platform. Keep values in sync with the main app.
|
|
*/
|
|
:root {
|
|
--bg: #fefefe;
|
|
--workflow-edge: #e0e0e0;
|
|
--surface-1: #fbfbfb;
|
|
--surface-2: #ffffff;
|
|
--surface-3: #f7f7f7;
|
|
--surface-4: #f5f5f5;
|
|
--surface-5: #f3f3f3;
|
|
--surface-6: #e5e5e5;
|
|
--surface-7: #d9d9d9;
|
|
--surface-active: #ececec;
|
|
--surface-hover: #f2f2f2;
|
|
--border: #d8d8d8;
|
|
/** Legacy neutral-border aliases. New work should use --border. */
|
|
--border-1: var(--border);
|
|
--text-primary: #1a1a1a;
|
|
--text-secondary: #525252;
|
|
--text-tertiary: #5c5c5c;
|
|
--text-body: #434343;
|
|
--text-muted: #7a7a7a;
|
|
--text-subtle: #8c8c8c;
|
|
--text-icon: #5a5a5a;
|
|
--text-icon-muted: #5c5c5c;
|
|
--text-inverse: #ffffff;
|
|
--text-error: #ef4444;
|
|
--text-muted-inverse: #a0a0a0;
|
|
--brand-accent: #33c482;
|
|
--brand-accent-hover: #2dac72;
|
|
--brand-secondary: #33b4ff;
|
|
--surface-inverted: #1b1b1b;
|
|
--surface-inverted-hover: #363636;
|
|
--border-inverted: #363636;
|
|
--border-muted: var(--border);
|
|
--badge-success-bg: #bbf7d0;
|
|
--badge-success-text: #15803d;
|
|
/* Shared-component tokens: referenced by @sim/emcn and @sim/workflow-renderer.
|
|
Undefined here, a var() silently falls back to currentColor. Values mirror
|
|
apps/sim/app/_styles/globals.css. */
|
|
--border-success: #e0e0e0;
|
|
--success: #22c55e;
|
|
--error: #dc2626;
|
|
--warning: #ea580c;
|
|
--caution: #f59e0b;
|
|
--text-placeholder: #8d8d8d;
|
|
--badge-teal-bg: #99f6e4;
|
|
--badge-teal-text: #0f766e;
|
|
--badge-cyan-bg: #cffafe;
|
|
--badge-cyan-text: #0891b2;
|
|
--badge-pink-bg: #fbcfe8;
|
|
--badge-pink-text: #be185d;
|
|
--badge-blue-secondary-bg: #bae6fd;
|
|
--badge-blue-secondary-text: #0369a1;
|
|
--badge-blue-bg: #bfdbfe;
|
|
--badge-blue-text: #1d4ed8;
|
|
--badge-purple-bg: #e9d5ff;
|
|
--badge-purple-text: #7c3aed;
|
|
--badge-amber-bg: #fde68a;
|
|
--badge-amber-text: #a16207;
|
|
--badge-orange-bg: #fed7aa;
|
|
--badge-orange-text: #c2410c;
|
|
--badge-error-bg: #fecaca;
|
|
--badge-error-text: #dc2626;
|
|
--badge-gray-bg: #e7e5e4;
|
|
--badge-gray-text: #57534e;
|
|
--code-bg: #f5f5f5;
|
|
--code-foreground: #1a1a1a;
|
|
--code-line-number: #737373;
|
|
--selection-bg: #add6ff;
|
|
--selection-dark: #264f78;
|
|
--highlight-search-active: #f6ad55;
|
|
--scrollbar-thumb-color: #c0c0c0;
|
|
--shadow-subtle: 0 2px 4px 0 rgba(0, 0, 0, 0.08);
|
|
--shadow-medium: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
--shadow-overlay: 0 10px 30px rgba(0, 0, 0, 0.11);
|
|
--shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04);
|
|
/* z-index scale — poppers sit above the modal so menus stay clickable over
|
|
the modal overlay. Mirrors apps/sim/app/_styles/globals.css. */
|
|
--z-toast: 150;
|
|
--z-modal: 200;
|
|
--z-popover: 300;
|
|
--z-tooltip: 400;
|
|
}
|
|
|
|
.dark {
|
|
--bg: #1b1b1b;
|
|
--workflow-edge: #454545;
|
|
--surface-1: #1e1e1e;
|
|
--surface-2: #232323;
|
|
--surface-3: #242424;
|
|
--surface-4: #292929;
|
|
--surface-5: #363636;
|
|
--surface-6: #454545;
|
|
--surface-7: #505050;
|
|
--surface-active: #2c2c2c;
|
|
--surface-hover: #262626;
|
|
--border: #444444;
|
|
/** Legacy neutral-border aliases. New work should use --border. */
|
|
--border-1: var(--border);
|
|
--text-primary: #e6e6e6;
|
|
--text-secondary: #cccccc;
|
|
--text-tertiary: #b3b3b3;
|
|
--text-body: #c1c1c1;
|
|
--text-muted: #6e6e6e;
|
|
--text-subtle: #7d7d7d;
|
|
--text-icon: #969696;
|
|
--text-icon-muted: #949494;
|
|
--text-inverse: #1b1b1b;
|
|
--text-error: #ef4444;
|
|
--text-muted-inverse: #b3b3b3;
|
|
--brand-accent: #33c482;
|
|
--brand-accent-hover: #2dac72;
|
|
--brand-secondary: #33b4ff;
|
|
--surface-inverted: #242424;
|
|
--surface-inverted-hover: #363636;
|
|
--border-inverted: #3d3d3d;
|
|
--border-muted: var(--border);
|
|
--badge-success-bg: rgba(34, 197, 94, 0.2);
|
|
--badge-success-text: #86efac;
|
|
/* Shared-component tokens: referenced by @sim/emcn and @sim/workflow-renderer.
|
|
Undefined here, a var() silently falls back to currentColor. Values mirror
|
|
apps/sim/app/_styles/globals.css. */
|
|
--border-success: #575757;
|
|
--success: #22c55e;
|
|
--error: #f87171;
|
|
--warning: #ff6600;
|
|
--caution: #f59e0b;
|
|
--text-placeholder: #8d8d8d;
|
|
--badge-teal-bg: rgba(20, 184, 166, 0.2);
|
|
--badge-teal-text: #5eead4;
|
|
--badge-cyan-bg: rgba(14, 165, 233, 0.2);
|
|
--badge-cyan-text: #7dd3fc;
|
|
--badge-pink-bg: rgba(236, 72, 153, 0.2);
|
|
--badge-pink-text: #f9a8d4;
|
|
--badge-blue-secondary-bg: rgba(51, 180, 255, 0.2);
|
|
--badge-blue-secondary-text: #7dd3fc;
|
|
--badge-blue-bg: rgba(59, 130, 246, 0.2);
|
|
--badge-blue-text: #93c5fd;
|
|
--badge-purple-bg: rgba(168, 85, 247, 0.2);
|
|
--badge-purple-text: #d8b4fe;
|
|
--badge-amber-bg: rgba(245, 158, 11, 0.2);
|
|
--badge-amber-text: #fcd34d;
|
|
--badge-orange-bg: rgba(249, 115, 22, 0.2);
|
|
--badge-orange-text: #fdba74;
|
|
--badge-error-bg: #551a1a;
|
|
--badge-error-text: #fca5a5;
|
|
--badge-gray-bg: #3a3a3a;
|
|
--badge-gray-text: #a8a8a8;
|
|
--code-bg: #1f1f1f;
|
|
--code-foreground: #eeeeee;
|
|
--code-line-number: #a8a8a8;
|
|
--selection-bg: #264f78;
|
|
--scrollbar-thumb-color: #5a5a5a;
|
|
--shadow-overlay: 0 10px 30px rgba(0, 0, 0, 0.3);
|
|
}
|
|
|
|
/* Scrollbars — platform thumb tokens, transparent track. A non-auto
|
|
`scrollbar-width`/`scrollbar-color` makes Chromium ignore every
|
|
`::-webkit-scrollbar*` rule on the element, so no webkit block here. Hover
|
|
shading is not expressible through the standard properties. */
|
|
* {
|
|
scrollbar-width: thin;
|
|
scrollbar-color: var(--scrollbar-thumb-color) transparent;
|
|
}
|
|
|
|
/* Font family utilities */
|
|
.font-sans {
|
|
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
}
|
|
|
|
/* Platform UI font — Season Sans, used by the chip chrome to match the main app */
|
|
.font-season {
|
|
font-family: var(--font-season), system-ui, "Segoe UI", Roboto, "Helvetica Neue", Arial,
|
|
"Noto Sans", sans-serif;
|
|
}
|
|
|
|
:root {
|
|
--fd-border-sidebar: transparent !important;
|
|
--fd-nav-height: 92px; /* Custom navbar height (52px top + 40px tabs) */
|
|
/* Content container width used to center main content */
|
|
--spacing-fd-container: 1400px;
|
|
/* Edge gutter = leftover space on each side of centered container */
|
|
--edge-gutter: max(1rem, calc((100vw - var(--spacing-fd-container)) / 2));
|
|
/* How far sidebar/TOC shift inward from the edge gutter */
|
|
--sidebar-shift: 50px;
|
|
--sidebar-offset: max(0px, calc(var(--edge-gutter) - var(--sidebar-shift)));
|
|
--toc-shift: 50px;
|
|
--toc-offset: max(0px, calc(var(--edge-gutter) - var(--toc-shift)));
|
|
/* Inner padding for navbar and sidebar — single source of truth */
|
|
--nav-inset: 44px;
|
|
/* Extra gap between sidebar/TOC and the main text content */
|
|
--content-gap: 2.25rem;
|
|
}
|
|
|
|
/* Light mode navbar background */
|
|
:root:not(.dark) nav {
|
|
background-color: hsla(0, 0%, 100%, 0.85) !important;
|
|
}
|
|
|
|
/* Dark mode navbar background */
|
|
:root.dark nav {
|
|
background-color: hsla(0, 0%, 10.6%, 0.92) !important;
|
|
}
|
|
|
|
/* Floating sidebar appearance - remove background */
|
|
[data-sidebar-container],
|
|
#nd-sidebar {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
--color-fd-muted: transparent !important;
|
|
--color-fd-card: transparent !important;
|
|
--color-fd-secondary: transparent !important;
|
|
}
|
|
|
|
aside[data-sidebar],
|
|
aside#nd-sidebar {
|
|
background: transparent !important;
|
|
border: none !important;
|
|
}
|
|
|
|
/* Remove the fade-out mask on sidebar scroll viewport (Fumadocs applies inline maskImage) */
|
|
[data-sidebar-placeholder] [data-radix-scroll-area-viewport],
|
|
#nd-sidebar [data-radix-scroll-area-viewport],
|
|
#nd-sidebar [data-radix-scroll-area-viewport] > div,
|
|
aside#nd-sidebar [data-radix-scroll-area-viewport] {
|
|
mask: none !important;
|
|
mask-image: none !important;
|
|
-webkit-mask: none !important;
|
|
-webkit-mask-image: none !important;
|
|
}
|
|
|
|
/* Fumadocs v16: Add sidebar placeholder styling for grid area */
|
|
[data-sidebar-placeholder] {
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* Fumadocs v16: Hide sidebar panel (floating collapse button) */
|
|
[data-sidebar-panel] {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Mobile only: Reduce gap between navbar and content (custom navbar hidden on mobile) */
|
|
@media (max-width: 1023px) {
|
|
#nd-docs-layout {
|
|
margin-top: -25px;
|
|
}
|
|
}
|
|
|
|
/* Hide TOC popover on tablet/medium screens (768px - 1279px) */
|
|
/* Keeps it visible on mobile (<768px) for easy navigation */
|
|
/* Desktop (>=1280px) already hides it via fumadocs xl:hidden */
|
|
@media (min-width: 768px) and (max-width: 1279px) {
|
|
#nd-docs-layout {
|
|
--fd-toc-popover-height: 0px !important;
|
|
}
|
|
|
|
[data-toc-popover] {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
/* Desktop only: Apply custom navbar offset, sidebar width and margin offsets */
|
|
/* On mobile, let fumadocs handle the layout natively */
|
|
@media (min-width: 1024px) {
|
|
:root {
|
|
--fd-banner-height: 92px !important; /* 52px top + 40px tabs */
|
|
}
|
|
|
|
#nd-docs-layout {
|
|
/* Use 100dvh so sidebar height = 100dvh - 92px (banner) = full viewport below navbar */
|
|
--fd-docs-height: 100dvh !important;
|
|
--fd-sidebar-width: 300px !important;
|
|
margin-left: var(--sidebar-offset) !important;
|
|
margin-right: var(--toc-offset) !important;
|
|
min-height: var(--fd-docs-height) !important;
|
|
}
|
|
|
|
/* Pin the sidebar to the viewport instead of letting fumadocs' `sticky` do it.
|
|
A sticky box is bottom-limited by its containing block, and #nd-docs-layout
|
|
ends ~660px above the document bottom because the site footer is a sibling
|
|
of the layout, not a grid child. So across the whole footer the sidebar gets
|
|
pushed upward — and any content-height change while the reader is in that
|
|
zone (expanding an FAQ row, say) makes it visibly jump. A fixed box ignores
|
|
both the container's end and the document's height, so neither happens.
|
|
|
|
Safe because the grid columns are explicit (`0px 300px 1fr 268px 0px`), so
|
|
removing the placeholder from flow leaves its track intact. `left`/`width`
|
|
are restated because a fixed box no longer derives them from its grid cell,
|
|
and `height` already comes from fumadocs' own utility classes.
|
|
|
|
Anchoring to `bottom` rather than `top` is what keeps the footer off it: the
|
|
offset is how far the footer currently reaches into the viewport (published
|
|
by `FooterOverlapProbe`), so the sidebar keeps its full height and slides up
|
|
out of view as the footer arrives, the way it did before it was pinned. With
|
|
no footer on screen the offset is 0 and this resolves back to top: 92px. */
|
|
[data-sidebar-placeholder] {
|
|
position: fixed !important;
|
|
left: var(--sidebar-offset);
|
|
width: var(--fd-sidebar-width);
|
|
top: auto !important;
|
|
bottom: var(--docs-footer-overlap, 0px) !important;
|
|
}
|
|
|
|
/* Sidebar divider line — pinned for the same reason, and so it stays glued to
|
|
the sidebar's right edge. Being fixed takes it out of #nd-docs-layout's grid
|
|
entirely, so it needs no grid placement and cannot skew a content cell; its
|
|
position comes from `left`/`top`/`bottom` alone. Unlike the sidebar it is
|
|
shortened rather than slid, so it runs from the navbar down to the footer's
|
|
top border and the two meet instead of the line stopping short. */
|
|
#nd-docs-layout::before {
|
|
content: "";
|
|
display: block;
|
|
position: fixed;
|
|
top: 92px; /* below navbar */
|
|
bottom: var(--docs-footer-overlap, 0px);
|
|
left: calc(var(--sidebar-offset) + var(--fd-sidebar-width));
|
|
width: 1px;
|
|
background-color: var(--surface-active);
|
|
pointer-events: none;
|
|
z-index: 21;
|
|
}
|
|
|
|
/* Hide fumadocs nav on desktop - we use custom navbar there */
|
|
#nd-docs-layout > header {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
/* Sidebar spacing — left padding aligns item text with navbar content (item has 8px internal padding) */
|
|
[data-sidebar-viewport],
|
|
#nd-sidebar > div {
|
|
padding: 0 14px 0 calc(var(--nav-inset) - 8px) !important;
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* Override Fumadocs p-4 on scroll viewport — must beat Tailwind v4 utility layer specificity */
|
|
html #nd-sidebar [data-radix-scroll-area-viewport],
|
|
html #nd-sidebar [data-radix-scroll-area-viewport].p-4,
|
|
html aside#nd-sidebar [data-radix-scroll-area-viewport] {
|
|
padding: 0 !important;
|
|
padding-top: 0 !important;
|
|
padding-bottom: 0 !important;
|
|
padding-left: 0 !important;
|
|
padding-right: 0 !important;
|
|
}
|
|
|
|
/* Top/bottom breathing room inside the scrollable sidebar content — scrollable, not clipped */
|
|
html #nd-sidebar [data-radix-scroll-area-viewport] > div {
|
|
padding-top: 20px !important;
|
|
padding-bottom: 16px !important;
|
|
}
|
|
|
|
/* Override sidebar item styling to match Raindrop */
|
|
/* Target Link and button elements in sidebar - override Fumadocs itemVariants */
|
|
/* Exclude the small chevron-only toggle buttons */
|
|
/* Using html prefix for higher specificity over Tailwind v4 utilities */
|
|
/* NEVER set `display` here. This selector scores (1,2,2) and carries
|
|
`!important`, so it out-specifies every rule below that hides the language
|
|
selector, theme toggle, and search button — a `display` declaration here
|
|
forces all of them back into the sidebar. The 30px chip height therefore
|
|
comes from line-height + padding (20 + 5 + 5), not from flex centering. */
|
|
html #nd-sidebar a:not(:has(span.font-mono)),
|
|
html #nd-sidebar button:not([aria-label*="ollapse"]):not([aria-label*="xpand"]) {
|
|
font-size: 0.875rem !important; /* 14px to match navbar items */
|
|
line-height: 20px !important;
|
|
padding: 5px 0.5rem !important; /* 30px tall overall — the app's chip pill, at its px-2 */
|
|
font-weight: 400 !important;
|
|
border-radius: 0.5rem !important; /* platform rounded-lg */
|
|
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif !important;
|
|
}
|
|
|
|
/* Sidebar text — platform --text-body */
|
|
html #nd-sidebar a:not(:has(span.font-mono)),
|
|
html #nd-sidebar button:not([aria-label*="ollapse"]):not([aria-label*="xpand"]) {
|
|
color: var(--text-body) !important;
|
|
}
|
|
|
|
/* Chevron icons — platform --text-icon */
|
|
#nd-sidebar svg {
|
|
display: inline-block !important;
|
|
opacity: 1 !important;
|
|
color: var(--text-icon) !important;
|
|
flex-shrink: 0 !important;
|
|
}
|
|
|
|
/* Ensure the small chevron toggle buttons are visible */
|
|
#nd-sidebar button[aria-label*="ollapse"],
|
|
#nd-sidebar button[aria-label*="xpand"] {
|
|
display: flex !important;
|
|
opacity: 1 !important;
|
|
padding: 0.25rem !important;
|
|
}
|
|
|
|
/* Add tiny gap between nested items */
|
|
#nd-sidebar ul li {
|
|
margin-bottom: 0.0625rem !important;
|
|
}
|
|
|
|
#nd-sidebar ul li:last-child {
|
|
margin-bottom: 0 !important;
|
|
}
|
|
|
|
/* Section separator headers — match the app sidebar's group label: 12px,
|
|
sentence case, normal weight. Only the fumadocs defaults are overridden here;
|
|
size and color stay on the component. */
|
|
[data-sidebar-viewport] [data-separator] p,
|
|
#nd-sidebar [data-separator] p {
|
|
font-weight: 400 !important;
|
|
text-transform: none !important;
|
|
letter-spacing: normal !important;
|
|
margin: 0 !important;
|
|
padding: 0 !important;
|
|
}
|
|
|
|
/* Active state — aligned with platform --surface-active */
|
|
#nd-sidebar a[data-active="true"]:not(:has(span.font-mono)),
|
|
#nd-sidebar button[data-active="true"] {
|
|
background-image: none !important;
|
|
}
|
|
|
|
html #nd-sidebar a[data-active="true"]:not(:has(span.font-mono)),
|
|
html #nd-sidebar button[data-active="true"] {
|
|
background-color: var(--surface-active) !important;
|
|
color: var(--text-body) !important;
|
|
}
|
|
|
|
/* Hover state — emcn's two-surface row model (see `chipHoverSurfaceClass`).
|
|
These rules must carry !important to beat fumadocs' own sidebar styles, which
|
|
also means they override the Tailwind hover utilities on the items; keep the
|
|
two in step. */
|
|
html #nd-sidebar a:not(:has(span.font-mono)):hover:not([data-active="true"]),
|
|
html #nd-sidebar button:hover:not([data-active="true"]) {
|
|
background-color: var(--surface-hover) !important;
|
|
}
|
|
|
|
/* Hide search, platform, and collapse button from sidebar completely */
|
|
[data-sidebar] [data-search],
|
|
[data-sidebar] .search-toggle,
|
|
#nd-sidebar [data-search],
|
|
#nd-sidebar .search-toggle,
|
|
[data-sidebar-viewport] [data-search],
|
|
[data-sidebar-viewport] button[data-search],
|
|
aside[data-sidebar] [role="button"]:has([data-search]),
|
|
aside[data-sidebar] > div > button:first-child,
|
|
#nd-sidebar > div > button:first-child,
|
|
[data-sidebar] a[href*="sim.ai"],
|
|
#nd-sidebar a[href*="sim.ai"],
|
|
[data-sidebar-viewport] a[href*="sim.ai"],
|
|
/* Hide search buttons (but NOT folder chevron buttons) */
|
|
aside[data-sidebar] > div:first-child
|
|
> button:not([aria-label="Collapse"]):not([aria-label="Expand"]),
|
|
#nd-sidebar > div:first-child > button:not([aria-label="Collapse"]):not([aria-label="Expand"]),
|
|
/* Hide sidebar collapse button (panel icon) - direct children only */
|
|
aside[data-sidebar] > button:first-of-type:not([aria-label="Collapse"]):not([aria-label="Expand"]),
|
|
[data-sidebar]
|
|
> button[type="button"]:first-of-type:not([aria-label="Collapse"]):not([aria-label="Expand"]),
|
|
button[data-collapse]:not([aria-label="Collapse"]):not([aria-label="Expand"]),
|
|
[data-sidebar-header] button,
|
|
/* Hide theme toggle from sidebar footer */
|
|
aside[data-sidebar] [data-theme-toggle],
|
|
[data-sidebar-footer],
|
|
[data-sidebar] footer,
|
|
footer button[aria-label*="heme"],
|
|
aside[data-sidebar] > div:last-child:has(button[aria-label*="heme"]),
|
|
aside[data-sidebar] button[aria-label*="heme"],
|
|
[data-sidebar] button[aria-label*="Theme"],
|
|
/* Additional theme toggle selectors */
|
|
aside[data-sidebar] > *:last-child
|
|
button,
|
|
[data-sidebar-viewport] ~ *,
|
|
aside[data-sidebar] > div:not([data-sidebar-viewport]),
|
|
/* Aggressive theme toggle hiding */
|
|
aside[data-sidebar] svg[class*="sun"],
|
|
aside[data-sidebar] svg[class*="moon"],
|
|
aside[data-sidebar] button[type="button"]:last-child,
|
|
aside button:has(svg:only-child),
|
|
[data-sidebar] div:has(> button[type="button"]:only-child:last-child),
|
|
/* Hide theme toggle and other non-content elements */
|
|
aside[data-sidebar] > *:not([data-sidebar-viewport]) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Desktop only: Hide sidebar toggle buttons and nav title/logo (keep visible on mobile) */
|
|
@media (min-width: 1025px) {
|
|
[data-sidebar-container] > button,
|
|
[data-sidebar-container] [data-toggle],
|
|
aside[data-sidebar] [data-sidebar-toggle],
|
|
button[data-sidebar-toggle],
|
|
nav button[data-sidebar-toggle],
|
|
button[aria-label="Toggle Sidebar"],
|
|
button[aria-label="Collapse Sidebar"],
|
|
/* Hide nav title/logo in sidebar on desktop - target all possible locations */
|
|
/* Lower specificity selectors first (attribute selectors) */
|
|
[data-sidebar-header],
|
|
[data-sidebar] [data-title],
|
|
aside[data-sidebar] a[href="/"],
|
|
aside[data-sidebar] a[href="/"] img,
|
|
aside[data-sidebar] > a:first-child,
|
|
aside[data-sidebar] > div > a:first-child,
|
|
aside[data-sidebar] img[alt="Sim"],
|
|
aside[data-sidebar] svg[aria-label="Sim"],
|
|
/* Higher specificity selectors (ID selectors) */
|
|
#nd-sidebar
|
|
a[href="/"],
|
|
#nd-sidebar a[href="/"] img,
|
|
#nd-sidebar a[href="/"] svg,
|
|
#nd-sidebar > a:first-child,
|
|
#nd-sidebar > div:first-child > a:first-child,
|
|
#nd-sidebar img[alt="Sim"],
|
|
#nd-sidebar svg[aria-label="Sim"],
|
|
/* Hide theme toggle at bottom of sidebar on desktop */
|
|
#nd-sidebar
|
|
> footer,
|
|
#nd-sidebar footer,
|
|
aside#nd-sidebar > *:last-child:not(div),
|
|
#nd-sidebar > button:last-child,
|
|
#nd-sidebar button[aria-label*="theme" i],
|
|
#nd-sidebar button[aria-label*="Theme"],
|
|
#nd-sidebar > div:last-child > button {
|
|
display: none !important;
|
|
}
|
|
}
|
|
|
|
/* Extra aggressive - hide everything after the viewport */
|
|
aside[data-sidebar] [data-sidebar-viewport] ~ * {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Tighter spacing for sidebar content */
|
|
[data-sidebar-viewport] > * {
|
|
margin-bottom: 0.0625rem;
|
|
}
|
|
|
|
[data-sidebar-viewport] > *:last-child {
|
|
margin-bottom: 0;
|
|
}
|
|
|
|
[data-sidebar-viewport] ul {
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Ensure sidebar starts with content immediately */
|
|
aside[data-sidebar] > div:first-child {
|
|
padding-top: 0;
|
|
}
|
|
|
|
/* Remove all sidebar borders and backgrounds (except the right divider) */
|
|
[data-sidebar-container],
|
|
aside[data-sidebar],
|
|
[data-sidebar],
|
|
[data-sidebar] *,
|
|
#nd-sidebar,
|
|
#nd-sidebar * {
|
|
border: none !important;
|
|
}
|
|
|
|
/* Sidebar border is handled via #nd-docs-layout::before pseudo-element (see desktop section below) */
|
|
|
|
/* Override fumadocs background colors for sidebar */
|
|
.dark #nd-sidebar,
|
|
.dark [data-sidebar-container],
|
|
.dark aside[data-sidebar] {
|
|
--color-fd-muted: transparent !important;
|
|
--color-fd-secondary: transparent !important;
|
|
background: transparent !important;
|
|
}
|
|
|
|
/* Force normal text flow in sidebar */
|
|
[data-sidebar],
|
|
[data-sidebar] *,
|
|
[data-sidebar-viewport],
|
|
[data-sidebar-viewport] * {
|
|
writing-mode: horizontal-tb !important;
|
|
}
|
|
|
|
code,
|
|
pre,
|
|
pre code {
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Inline code chip, matching the one the app renders for markdown
|
|
(`apps/sim/lib/content/mdx.tsx`).
|
|
|
|
Two things here are deliberate rather than oversights. The border is gone because the app's
|
|
chip has none, and at inline size a hairline reads as a boxed-in label rather than a
|
|
highlight. The color is unset so the chip composites over whatever encloses it — inline
|
|
code inside a link picks up the link color instead of sitting on it as a grey island, which
|
|
is the behavior the app's chip was written for. */
|
|
:not(pre) > code {
|
|
padding: 0.125rem 0.375rem;
|
|
border-radius: 4px;
|
|
font-size: 0.875em;
|
|
font-weight: 400;
|
|
background-color: var(--surface-5);
|
|
}
|
|
|
|
/* Search dialog — lighter shadow + constrained size */
|
|
[data-radix-popper-content-wrapper] > div,
|
|
[role="dialog"][data-state] {
|
|
box-shadow: var(--shadow-medium) !important;
|
|
}
|
|
|
|
[role="dialog"][data-state] {
|
|
max-width: 480px !important;
|
|
}
|
|
|
|
/* Search dialog overlay + panel must cover the sticky navbar — both default to z-50,
|
|
and the navbar wins the tie by DOM order, leaving it unblurred above the overlay */
|
|
.bg-fd-overlay,
|
|
[role="dialog"][data-state] {
|
|
z-index: 60 !important;
|
|
}
|
|
|
|
/* Code metrics — the platform's `Code.Viewer` runs 13px on a 21px line box
|
|
(`text-small` / `leading-[21px]`); identical snippets should not read larger here. */
|
|
pre {
|
|
font-size: var(--text-small);
|
|
line-height: 21px;
|
|
tab-size: 2;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
pre code {
|
|
display: block;
|
|
width: fit-content;
|
|
min-width: 100%;
|
|
}
|
|
|
|
/* Syntax highlighting adjustments for better readability */
|
|
pre code .line {
|
|
padding-left: 0;
|
|
padding-right: 0;
|
|
}
|
|
|
|
/* Content typography — align heading sizes and weights with platform */
|
|
main article h1,
|
|
[data-content] h1,
|
|
#nd-page h1 {
|
|
font-size: 1.5rem !important;
|
|
font-weight: 550 !important;
|
|
letter-spacing: -0.02em;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
main article h2,
|
|
[data-content] h2,
|
|
#nd-page h2 {
|
|
font-size: 1.25rem !important;
|
|
font-weight: 500 !important;
|
|
letter-spacing: -0.015em;
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
main article h3,
|
|
main article h4,
|
|
[data-content] h3,
|
|
[data-content] h4,
|
|
#nd-page h3,
|
|
#nd-page h4 {
|
|
font-size: var(--text-md) !important;
|
|
font-weight: 470 !important;
|
|
letter-spacing: -0.01em;
|
|
color: var(--text-body);
|
|
}
|
|
|
|
/* Body text — platform --text-secondary */
|
|
main article p,
|
|
[data-content] p,
|
|
[data-docs-page] p {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Muted text — platform --text-muted */
|
|
main article .text-muted-foreground,
|
|
[data-content] .text-muted-foreground,
|
|
[data-docs-page] .text-muted-foreground {
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* List items — inherit body text color */
|
|
main article li,
|
|
[data-content] li,
|
|
[data-docs-page] li {
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* Custom text highlighting styles */
|
|
.text-highlight {
|
|
color: var(--color-fd-primary);
|
|
}
|
|
|
|
/* Override marker color for highlighted lists */
|
|
.highlight-markers li::marker {
|
|
color: var(--color-fd-primary);
|
|
}
|
|
|
|
/* Remove the thin border-left on nested TOC items (keeps main indicator only) */
|
|
#nd-toc a[style*="padding-inline-start"] {
|
|
border-left: none !important;
|
|
}
|
|
|
|
/* The clerk TOC draws its active range twice: a track segment, plus a dot that
|
|
animates along the track to the current heading. The dot is redundant with the
|
|
segment and reads as a stray artifact, so hide it and keep the segment. It is
|
|
the only node fumadocs gives an inline offset-path, which is what anchors it. */
|
|
#nd-toc [style*="offset-path"] {
|
|
display: none !important;
|
|
}
|
|
|
|
/* TOC heading — align weight with landing */
|
|
#nd-toc h3,
|
|
#nd-toc [class*="title"] {
|
|
font-weight: 480 !important;
|
|
font-size: var(--text-small) !important;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* TOC links — softer colors aligned with landing muted text */
|
|
#nd-toc a {
|
|
font-weight: 430 !important;
|
|
font-size: var(--text-small) !important;
|
|
color: var(--text-muted);
|
|
transition: color 0.2s;
|
|
}
|
|
|
|
#nd-toc a:hover {
|
|
color: var(--text-body);
|
|
}
|
|
|
|
#nd-toc a[data-active="true"] {
|
|
color: var(--text-primary) !important;
|
|
font-weight: 470 !important;
|
|
}
|
|
|
|
/* Add bottom spacing to prevent abrupt page endings */
|
|
[data-content] {
|
|
padding-top: 1.5rem !important;
|
|
padding-bottom: 4rem;
|
|
}
|
|
|
|
/* Alternative fallback for different Fumadocs versions */
|
|
main article,
|
|
.docs-page main {
|
|
padding-top: 1.5rem !important;
|
|
padding-bottom: 4rem;
|
|
}
|
|
|
|
/* Main content area - center and constrain like turborepo/raindrop */
|
|
/* Note: --sidebar-offset and --toc-offset are now applied at #nd-docs-layout level */
|
|
main[data-main] {
|
|
max-width: var(--spacing-fd-container, 1400px);
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
padding-top: 1rem;
|
|
padding-left: var(--content-gap);
|
|
padding-right: var(--content-gap);
|
|
order: 1 !important;
|
|
}
|
|
|
|
/* Adjust for smaller screens */
|
|
@media (max-width: 768px) {
|
|
main[data-main] {
|
|
padding-left: 1rem;
|
|
padding-right: 1rem;
|
|
}
|
|
#nd-page:has(.api-page-header) {
|
|
padding-left: 1rem;
|
|
padding-right: 1rem;
|
|
}
|
|
}
|
|
|
|
/* Ensure docs page content is properly constrained */
|
|
[data-docs-page] {
|
|
max-width: 1400px;
|
|
margin-left: auto;
|
|
margin-right: auto;
|
|
padding-top: 1.5rem !important;
|
|
}
|
|
|
|
/* Remove any unwanted outlines from video elements */
|
|
video {
|
|
outline: none !important;
|
|
}
|
|
|
|
/* API Reference Pages — Mintlify-style overrides */
|
|
|
|
/* OpenAPI pages: span main + TOC grid columns for wide two-column layout.
|
|
Use named grid lines from grid-template-areas so this works regardless
|
|
of whether the grid has 3 columns (production) or 5 columns (local dev). */
|
|
#nd-page:has(.api-page-header) {
|
|
grid-column: main-start / toc-end !important;
|
|
max-width: 1400px !important;
|
|
padding-left: calc(var(--content-gap) + 1rem);
|
|
padding-right: calc(var(--content-gap) + 1rem);
|
|
}
|
|
|
|
/* Hide the empty TOC aside on OpenAPI pages so it doesn't overlay content */
|
|
#nd-docs-layout:has(#nd-page:has(.api-page-header)) #nd-toc {
|
|
display: none;
|
|
}
|
|
|
|
/* Hide the default "Response Body" heading rendered by fumadocs-openapi */
|
|
.response-section-wrapper > .response-section-content > h2,
|
|
.response-section-wrapper > .response-section-content > h3 {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Hide default accordion triggers (status code rows) — we show our own dropdown */
|
|
.response-section-wrapper [data-orientation="vertical"] > [data-state] > h3 {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Ensure API reference pages use the same font as the rest of the docs.
|
|
`.font-mono` is excluded: this selector (id + element) outranks the
|
|
`.font-mono` class rule, so without it every code identifier renders sans. */
|
|
#nd-page:has(.api-page-header),
|
|
#nd-page:has(.api-page-header) h2,
|
|
#nd-page:has(.api-page-header) h3,
|
|
#nd-page:has(.api-page-header) h4,
|
|
#nd-page:has(.api-page-header) p:not(.font-mono),
|
|
#nd-page:has(.api-page-header) span:not(.font-mono),
|
|
#nd-page:has(.api-page-header) div:not(.font-mono),
|
|
#nd-page:has(.api-page-header) label:not(.font-mono),
|
|
#nd-page:has(.api-page-header) button:not(.font-mono) {
|
|
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
|
|
"Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
}
|
|
|
|
/* Method badge pills — shared background colors (page + sidebar) */
|
|
span.font-mono.font-medium[data-method="get"],
|
|
span.font-mono.font-medium[data-method="head"],
|
|
span.font-mono.font-medium[data-method="options"] {
|
|
background-color: var(--badge-success-bg);
|
|
}
|
|
span.font-mono.font-medium[data-method="post"] {
|
|
background-color: var(--badge-blue-bg);
|
|
}
|
|
span.font-mono.font-medium[data-method="put"] {
|
|
background-color: var(--badge-amber-bg);
|
|
}
|
|
span.font-mono.font-medium[data-method="patch"] {
|
|
background-color: var(--badge-orange-bg);
|
|
}
|
|
span.font-mono.font-medium[data-method="delete"] {
|
|
background-color: var(--badge-error-bg);
|
|
}
|
|
|
|
/* Sidebar links with method badges — match regular item padding. Radius follows the
|
|
platform's two-tier convention (documented on emcn's `DropdownMenu`): floating surfaces
|
|
take 12px, rows inside one take 8px. These are rows. */
|
|
html #nd-sidebar a:has(span.font-mono.font-medium) {
|
|
display: flex !important;
|
|
align-items: center !important;
|
|
gap: 0.375rem;
|
|
padding: 0.375rem 0.875rem !important;
|
|
font-size: 0.875rem !important;
|
|
line-height: 1.4 !important;
|
|
font-weight: 450 !important;
|
|
border-radius: 0.5rem !important;
|
|
}
|
|
|
|
/* Sidebar method badges — fixed-width for right-aligned labels */
|
|
#nd-sidebar a span.font-mono.font-medium {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 2.625rem;
|
|
font-size: var(--text-micro) !important;
|
|
line-height: 1 !important;
|
|
padding: 0.15625rem 0.25rem;
|
|
border-radius: 0.375rem;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
/* Footer navigation method badges — pill styling to match sidebar */
|
|
#nd-page span.font-mono.font-medium[data-method] {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: var(--text-micro) !important;
|
|
line-height: 1 !important;
|
|
padding: 0.15625rem 0.375rem;
|
|
border-radius: 0.375rem;
|
|
}
|
|
|
|
/* API page code/card containers — transparent background */
|
|
#nd-page:has(.api-page-header) {
|
|
--color-fd-card: transparent !important;
|
|
}
|
|
|
|
/* API page code example panels — transparent */
|
|
#nd-page:has(.api-page-header) .rounded-xl.border.bg-fd-card {
|
|
background-color: transparent !important;
|
|
}
|
|
|
|
#nd-page:has(.api-page-header) .rounded-xl.border.shadow-md.bg-fd-card {
|
|
background-color: transparent !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
/* Hide "Filter Properties" search bar everywhere — main page and popovers */
|
|
input[placeholder="Filter Properties"] {
|
|
display: none !important;
|
|
}
|
|
div:has(> input[placeholder="Filter Properties"]) {
|
|
display: none !important;
|
|
}
|
|
/* Remove top border on first visible property after hidden Filter Properties */
|
|
div:has(> input[placeholder="Filter Properties"]) + .text-sm.border-t {
|
|
border-top: none !important;
|
|
}
|
|
|
|
/* Hide "TypeScript Definitions" copy panel on API pages */
|
|
#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3.mb-4 {
|
|
display: none !important;
|
|
}
|
|
#nd-page:has(.api-page-header) div.not-prose.rounded-xl.border.p-3:has(> div > p.font-medium) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Hide info tags (Format, Default, etc.) everywhere — main page and popovers */
|
|
div.flex.flex-row.gap-2.flex-wrap.not-prose:has(> div.bg-fd-secondary) {
|
|
display: none !important;
|
|
}
|
|
div.flex.flex-row.items-start.bg-fd-secondary.border.rounded-lg.text-xs {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Method+path bar — cleaner, lighter styling like Gumloop.
|
|
Override bg-fd-card CSS variable directly for reliability. */
|
|
#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose {
|
|
--color-fd-card: var(--surface-3) !important;
|
|
background-color: var(--surface-3) !important;
|
|
border-color: var(--border-1) !important;
|
|
}
|
|
/* Method badge inside path bar — cleaner sans-serif, softer colors */
|
|
#nd-page:has(.api-page-header)
|
|
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
|
span.font-mono.font-medium {
|
|
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif !important;
|
|
font-weight: 600 !important;
|
|
font-size: var(--text-xs) !important;
|
|
letter-spacing: 0.025em;
|
|
text-transform: uppercase;
|
|
padding: 0.125rem 0.5rem !important;
|
|
border-radius: 0.375rem !important;
|
|
}
|
|
/* Path bar per-method colors (fumadocs renders these, so we match by class) */
|
|
/* GET */
|
|
#nd-page:has(.api-page-header)
|
|
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
|
span.font-mono.font-medium[class*="text-green"] {
|
|
color: var(--badge-success-text) !important;
|
|
background-color: var(--badge-success-bg) !important;
|
|
}
|
|
/* POST */
|
|
#nd-page:has(.api-page-header)
|
|
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
|
span.font-mono.font-medium[class*="text-blue"] {
|
|
color: var(--badge-blue-text) !important;
|
|
background-color: var(--badge-blue-bg) !important;
|
|
}
|
|
/* PUT */
|
|
#nd-page:has(.api-page-header)
|
|
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
|
span.font-mono.font-medium[class*="text-yellow"] {
|
|
color: var(--badge-amber-text) !important;
|
|
background-color: var(--badge-amber-bg) !important;
|
|
}
|
|
/* PATCH */
|
|
#nd-page:has(.api-page-header)
|
|
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
|
span.font-mono.font-medium[class*="text-orange"] {
|
|
color: var(--badge-orange-text) !important;
|
|
background-color: var(--badge-orange-bg) !important;
|
|
}
|
|
/* DELETE */
|
|
#nd-page:has(.api-page-header)
|
|
div.flex.flex-row.items-center.rounded-xl.border.not-prose
|
|
span.font-mono.font-medium[class*="text-red"] {
|
|
color: var(--badge-error-text) !important;
|
|
background-color: var(--badge-error-bg) !important;
|
|
}
|
|
|
|
/* The path inside the method+path bar is a heading, not a chip — strip the inline-code
|
|
surface so it reads as the endpoint itself. */
|
|
#nd-page:has(.api-page-header) div.flex.flex-row.items-center.rounded-xl.border.not-prose code {
|
|
color: var(--text-body) !important;
|
|
background: none !important;
|
|
padding: 0 !important;
|
|
font-size: var(--text-small) !important;
|
|
}
|
|
|
|
/* Response Section — custom dropdown-based rendering (Mintlify style) */
|
|
|
|
/* Hide divider lines between accordion items */
|
|
.response-section-wrapper [data-orientation="vertical"].divide-y > * {
|
|
border-top-width: 0 !important;
|
|
border-bottom-width: 0 !important;
|
|
}
|
|
.response-section-wrapper [data-orientation="vertical"].divide-y {
|
|
border-top: none !important;
|
|
}
|
|
|
|
/* Remove content type labels inside accordion items (we show one in the header) */
|
|
.response-section-wrapper [data-orientation="vertical"] p.not-prose:has(code.text-xs) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Hide the top-level response description (e.g. "Execution was successfully cancelled.")
|
|
but NOT field descriptions inside Schema which also use prose-no-margin.
|
|
The response description is a direct child of AccordionContent (role=region) with mb-2. */
|
|
.response-section-wrapper [data-orientation="vertical"] [role="region"] > .prose-no-margin.mb-2,
|
|
.response-section-wrapper
|
|
[data-orientation="vertical"]
|
|
[role="region"]
|
|
> div
|
|
> .prose-no-margin.mb-2 {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Remove left padding on accordion content so it aligns with Path Parameters */
|
|
.response-section-wrapper [data-orientation="vertical"] [role="region"] {
|
|
padding-inline-start: 0 !important;
|
|
}
|
|
|
|
/* Response section header */
|
|
.response-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1rem;
|
|
margin-top: 1.75rem;
|
|
margin-bottom: 0.25rem;
|
|
}
|
|
|
|
.response-section-title {
|
|
font-size: 1.25rem;
|
|
font-weight: 500;
|
|
letter-spacing: -0.015em;
|
|
margin: 0;
|
|
color: var(--text-primary);
|
|
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, -apple-system, sans-serif;
|
|
}
|
|
|
|
.response-section-meta {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.75rem;
|
|
margin-left: auto;
|
|
}
|
|
|
|
/* Status code dropdown */
|
|
.response-section-dropdown-wrapper {
|
|
position: relative;
|
|
}
|
|
|
|
/* API-reference metadata face — the status trigger, the content-type label, the
|
|
`required` / `header` markers, and the status-code tabs. Defined once; each
|
|
consumer below adds only its own colour, content, and order. The `code.text-xs`
|
|
label further down needs `!important` to beat fumadocs and stays separate. */
|
|
#nd-page:has(.api-page-header) button.response-section-dropdown-trigger,
|
|
.response-section-dropdown-trigger,
|
|
#nd-page:has(.api-page-header) span.response-section-content-type,
|
|
.response-section-content-type,
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after,
|
|
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::before,
|
|
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::after,
|
|
#nd-page:has(.api-page-header) .flex.gap-3\.5.overflow-x-auto.not-prose > button {
|
|
font-size: var(--text-code);
|
|
line-height: 1.25rem;
|
|
font-weight: 400;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Status-code trigger — matches the content-type label beside it. */
|
|
#nd-page:has(.api-page-header) button.response-section-dropdown-trigger,
|
|
.response-section-dropdown-trigger {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
height: 1.25rem;
|
|
padding: 0 0.25rem;
|
|
color: var(--text-secondary);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
border-radius: 0.375rem;
|
|
transition: color 0.15s;
|
|
}
|
|
/* Carries the same id-qualified prefix as the base rule above; without it the
|
|
base rule outranks this one and the trigger never changes colour on hover. */
|
|
#nd-page:has(.api-page-header) button.response-section-dropdown-trigger:hover,
|
|
.response-section-dropdown-trigger:hover {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.response-section-chevron {
|
|
width: 0.75rem;
|
|
height: 0.75rem;
|
|
transition: transform 0.15s;
|
|
}
|
|
.response-section-chevron-open {
|
|
transform: rotate(180deg);
|
|
}
|
|
|
|
.response-section-dropdown-menu {
|
|
position: absolute;
|
|
top: calc(100% + 0.25rem);
|
|
left: 0;
|
|
z-index: 50;
|
|
min-width: 5rem;
|
|
background-color: var(--bg);
|
|
border: 1px solid var(--border);
|
|
border-radius: 0.75rem;
|
|
box-shadow: var(--shadow-card);
|
|
padding: 0.375rem;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.response-section-dropdown-item {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
width: 100%;
|
|
padding: 0.375rem 0.5rem;
|
|
font-size: 0.875rem;
|
|
color: var(--text-body);
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
border-radius: 0.5rem;
|
|
transition:
|
|
background-color 0.1s,
|
|
color 0.1s;
|
|
font-family: var(--font-geist-sans), ui-sans-serif, system-ui, sans-serif;
|
|
}
|
|
.response-section-dropdown-item:hover {
|
|
background-color: var(--surface-active);
|
|
color: var(--text-primary);
|
|
}
|
|
.response-section-dropdown-item-selected {
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
.response-section-check {
|
|
width: 0.875rem;
|
|
height: 0.875rem;
|
|
}
|
|
|
|
/* Content-type label. The Response header renders this class; the Request Body
|
|
header renders a fumadocs `code.text-xs`. Keep the two in sync — the same
|
|
string at different weights reads as one being lighter than the other. */
|
|
#nd-page:has(.api-page-header) span.response-section-content-type,
|
|
.response-section-content-type {
|
|
color: var(--text-secondary);
|
|
background: none;
|
|
border: none;
|
|
padding: 0;
|
|
}
|
|
|
|
/* Response schema container — remove border to match Path Parameters style */
|
|
.response-section-wrapper [data-orientation="vertical"] .border.px-3.py-2.rounded-lg {
|
|
border: none !important;
|
|
padding: 0 !important;
|
|
border-radius: 0 !important;
|
|
background-color: transparent;
|
|
}
|
|
|
|
/* Property row — reorder: name (1) → type badge (2) → required badge (3) */
|
|
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
align-items: center;
|
|
}
|
|
|
|
/* Name span — order 1 */
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.font-medium.font-mono.text-fd-primary {
|
|
order: 1;
|
|
}
|
|
|
|
/* Type token — order 2. Covers every shape the slot takes: scalar span, union
|
|
wrapper, schema-reference button, and the auth row's `::after` label. Reuses
|
|
the docs inline-code recipe, so a type reads as code wherever it appears; the
|
|
explicit 20px height keeps a union level with a scalar, which its nested
|
|
links would otherwise push to 26px. */
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground,
|
|
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > button,
|
|
#nd-page:has(.api-page-header) .flex.flex-wrap.items-center.gap-3.not-prose > span:has(> button),
|
|
#nd-page:has(.api-page-header)
|
|
div.my-4
|
|
> .flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground::after {
|
|
order: 2;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
height: 1.25rem;
|
|
/* No gap: an `array<T>` slot holds its brackets as bare text nodes, which
|
|
become anonymous flex items, so any gap here would prise `array<` and `>`
|
|
away from the type they wrap. The union separator spaces itself instead. */
|
|
gap: 0;
|
|
background-color: var(--surface-4);
|
|
border: 1px solid var(--border-1);
|
|
color: var(--text-body);
|
|
padding: 0 0.3125rem;
|
|
border-radius: 0.375rem;
|
|
font-size: var(--text-code);
|
|
line-height: 1;
|
|
font-weight: 400;
|
|
font-family: var(--font-mono);
|
|
}
|
|
|
|
/* Everything inside a type token inherits the token's own face, size, and ink.
|
|
Applied to every descendant, not just the links: a union's `|` separator is a
|
|
classless `span`, so the page-wide `span:not(.font-mono)` rule assigned it the
|
|
body sans face and one chip rendered in two faces. Anything fumadocs nests in
|
|
here later is covered by the same reset.
|
|
Underline is deferred to hover so links don't read heavier than a plain scalar
|
|
in the same box. The button that *is* the slot needs its own rule below: it
|
|
cannot `inherit`, which would pull the row's 14px sans back in. */
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground
|
|
* {
|
|
text-decoration: none;
|
|
color: inherit;
|
|
font-size: inherit;
|
|
font-family: inherit;
|
|
}
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> button.text-sm.font-mono.text-fd-muted-foreground {
|
|
text-decoration: none;
|
|
}
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground
|
|
:is(a, button):hover,
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> button.text-sm.font-mono.text-fd-muted-foreground:hover {
|
|
text-decoration: underline;
|
|
text-underline-offset: 2px;
|
|
}
|
|
|
|
/* Union separator — dimmed one step, no further: `string | null` started
|
|
reading as `string null` on the chip fill. Own margin; the slot has no gap. */
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground
|
|
> span {
|
|
margin: 0 0.375rem;
|
|
color: var(--text-muted);
|
|
}
|
|
|
|
/* Hide the "*" inside the name span — we'll add "required" as a ::after on the flex row */
|
|
#nd-page:has(.api-page-header) span.font-medium.font-mono.text-fd-primary > span.text-red-400 {
|
|
display: none;
|
|
}
|
|
|
|
/* Required marker — order 3. Error text colour but no fill: eight required
|
|
params on one page should not read as eight alarms. */
|
|
#nd-page:has(.api-page-header)
|
|
.flex.flex-wrap.items-center.gap-3.not-prose:has(span.text-red-400)::after {
|
|
content: "required";
|
|
order: 3;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
color: var(--badge-error-text);
|
|
}
|
|
/* Optional "?" indicator — hide it */
|
|
#nd-page:has(.api-page-header)
|
|
span.font-medium.font-mono.text-fd-primary
|
|
> span.text-fd-muted-foreground {
|
|
display: none;
|
|
}
|
|
|
|
/* Hide the auth scheme type label (e.g. "apiKey") next to Authorization heading */
|
|
#nd-page:has(.api-page-header) .flex.items-start.justify-between.gap-2 > div.not-prose {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Auth property — replace "<token>" with "string" badge, add "header" and "required" badges.
|
|
Auth properties use my-4 (vs py-4 for regular properties). */
|
|
|
|
/* Auth property flex row — name: order 1, type: order 2, ::before "header": order 3, ::after "required": order 4 */
|
|
#nd-page:has(.api-page-header)
|
|
div.my-4
|
|
> .flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.font-medium.font-mono.text-fd-primary {
|
|
order: 1;
|
|
}
|
|
/* Auth rows collapse the real `<token>` text to zero and draw the chip in the
|
|
`::after` below, so this span is a bare wrapper: it must drop the type-token
|
|
box it matches, or the chip renders inside a second, empty bordered box. */
|
|
#nd-page:has(.api-page-header)
|
|
div.my-4
|
|
> .flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground {
|
|
order: 2;
|
|
font-size: 0;
|
|
padding: 0;
|
|
background: none;
|
|
border: none;
|
|
height: auto;
|
|
line-height: 0;
|
|
}
|
|
/* Only the label — the box comes from the shared type-token rule above, which
|
|
this pseudo-element is a member of. */
|
|
#nd-page:has(.api-page-header)
|
|
div.my-4
|
|
> .flex.flex-wrap.items-center.gap-3.not-prose
|
|
> span.text-sm.font-mono.text-fd-muted-foreground::after {
|
|
content: "string";
|
|
}
|
|
|
|
/* "header" location via ::before on the auth flex row — uncontained metadata,
|
|
matching the `required` marker rather than the type token. */
|
|
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::before {
|
|
content: "header";
|
|
order: 3;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
color: var(--text-secondary);
|
|
}
|
|
|
|
/* "required" marker via ::after on the auth flex row */
|
|
#nd-page:has(.api-page-header) div.my-4 > .flex.flex-wrap.items-center.gap-3.not-prose::after {
|
|
content: "required";
|
|
order: 4;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
color: var(--badge-error-text);
|
|
}
|
|
/* Hide the trailing "In: header" line — redundant with the header marker.
|
|
Matched by position, not shape: descriptions contain a `code` too (status
|
|
codes), so a bare `p:has(> code)` also hid the API-key description. */
|
|
#nd-page:has(.api-page-header) div.my-4 .prose-no-margin > p:last-child:has(> code) {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Section dividers — bottom border after Authorization and Body sections. */
|
|
.api-section-divider {
|
|
padding-bottom: 0.5rem;
|
|
border-bottom: 1px solid var(--surface-active);
|
|
}
|
|
|
|
/* Property rows — breathing room like Mintlify.
|
|
Regular properties use border-t py-4; auth properties use border-t my-4. */
|
|
#nd-page:has(.api-page-header) .text-sm.border-t.py-4 {
|
|
padding-top: 1.25rem !important;
|
|
padding-bottom: 1.25rem !important;
|
|
}
|
|
#nd-page:has(.api-page-header) .text-sm.border-t.my-4 {
|
|
margin-top: 1.25rem !important;
|
|
margin-bottom: 1.25rem !important;
|
|
padding-top: 1.25rem;
|
|
}
|
|
|
|
/* Divider lines between fields — very subtle like Mintlify */
|
|
#nd-page:has(.api-page-header) .text-sm.border-t {
|
|
border-color: var(--surface-active);
|
|
}
|
|
|
|
/* Body/Callback "application/json" label — strip inline-code chrome and keep in
|
|
sync with `.response-section-content-type`; same string, two headers. */
|
|
#nd-page:has(.api-page-header) .flex.gap-2.items-center.justify-between p.not-prose code.text-xs,
|
|
#nd-page:has(.api-page-header) .flex.justify-between.gap-2.items-end p.not-prose code.text-xs {
|
|
background: none !important;
|
|
border: none !important;
|
|
padding: 0 !important;
|
|
color: var(--text-secondary) !important;
|
|
font-size: var(--text-code) !important;
|
|
line-height: 1.25rem !important;
|
|
font-weight: 400 !important;
|
|
font-family: var(--font-mono) !important;
|
|
}
|
|
|
|
/* Section headings (Authorization, Path Parameters, etc.) — consistent top spacing */
|
|
#nd-page:has(.api-page-header) .min-w-0.flex-1 h2 {
|
|
margin-top: 1.75rem !important;
|
|
margin-bottom: 0.25rem !important;
|
|
}
|
|
|
|
/* Example-panel code overflows rather than wraps: a wrapped line restarts at
|
|
column zero and misreports the JSON nesting depth. */
|
|
|
|
/* fumadocs' own lucide glyphs (heading anchor, copy button) ship at stroke-width
|
|
2 while emcn strokes at 1.55, so they read heavier than everything near them.
|
|
Layout-wide on purpose: one icon weight across the docs. Retired once
|
|
createAPIPage is given renderHeading/renderCodeBlock. */
|
|
#nd-docs-layout svg[class*="lucide"] {
|
|
stroke-width: 1.55;
|
|
}
|
|
|
|
/* Callout/alert — transparent background, no shadow, hide colored bar, add padding */
|
|
div.rounded-xl.border.bg-fd-card.shadow-md:has(> [role="none"]) {
|
|
background-color: transparent !important;
|
|
box-shadow: none !important;
|
|
border-color: var(--border-1) !important;
|
|
padding-left: 0.875rem !important;
|
|
align-items: center !important;
|
|
}
|
|
|
|
div.rounded-xl.border.bg-fd-card.shadow-md > [role="none"] {
|
|
display: none !important;
|
|
}
|
|
|
|
/* Files component — transparent background */
|
|
div.not-prose.rounded-md.border.bg-fd-card.p-2 {
|
|
background-color: transparent !important;
|
|
border-color: var(--border-1) !important;
|
|
}
|
|
|
|
/* Callout icon — outline style, text color, vertically centered */
|
|
div.rounded-xl.border.bg-fd-card.shadow-md:has(> [role="none"]) > svg {
|
|
fill: none !important;
|
|
color: var(--color-fd-foreground) !important;
|
|
stroke-width: 1.55 !important;
|
|
flex-shrink: 0;
|
|
width: 1rem !important;
|
|
height: 1rem !important;
|
|
}
|
|
|
|
/* API page header — constrain title/copy-page to left content column, not full width.
|
|
Only applies on OpenAPI pages (which have the two-column layout). */
|
|
@media (min-width: 1280px) {
|
|
.api-page-header {
|
|
max-width: calc(100% - 400px - 1.5rem);
|
|
}
|
|
}
|
|
|
|
/* Footer navigation — constrain to left content column on OpenAPI pages only.
|
|
Target pages that contain the two-column layout via :has() selector. */
|
|
#nd-page:has(.api-page-header) > div:last-child {
|
|
max-width: calc(100% - 400px - 1.5rem);
|
|
}
|
|
@media (max-width: 1024px) {
|
|
#nd-page:has(.api-page-header) > div:last-child {
|
|
max-width: 100%;
|
|
}
|
|
}
|
|
|
|
/* ─── Content Components — aligned with platform design system ─── */
|
|
|
|
/* Heading anchor links — no underline, inherit color */
|
|
#nd-page h1 a,
|
|
#nd-page h2 a,
|
|
#nd-page h3 a,
|
|
#nd-page h4 a,
|
|
#nd-page h5 a,
|
|
#nd-page h6 a,
|
|
.prose h1 a,
|
|
.prose h2 a,
|
|
.prose h3 a,
|
|
.prose h4 a,
|
|
.prose h5 a,
|
|
.prose h6 a {
|
|
text-decoration: none !important;
|
|
color: inherit !important;
|
|
border: none !important;
|
|
}
|
|
|
|
/* Tables — clean divider-based style matching chat markdown renderer */
|
|
.prose table,
|
|
[data-docs-page] table,
|
|
[data-content] table,
|
|
main article table {
|
|
border-collapse: collapse !important;
|
|
border-spacing: 0 !important;
|
|
background: none !important;
|
|
border: none !important;
|
|
border-radius: 0 !important;
|
|
overflow: visible !important;
|
|
font-size: 0.875rem !important;
|
|
}
|
|
|
|
.prose thead th,
|
|
[data-docs-page] thead th,
|
|
[data-content] thead th,
|
|
main article thead th {
|
|
background: none !important;
|
|
font-weight: 600 !important;
|
|
padding: 0.5rem 0.75rem !important;
|
|
border-bottom: 1px solid var(--border) !important;
|
|
border-left: none !important;
|
|
border-right: none !important;
|
|
border-top: none !important;
|
|
border-inline-start: none !important;
|
|
color: var(--text-primary);
|
|
text-align: left;
|
|
white-space: nowrap;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.prose td,
|
|
[data-docs-page] td,
|
|
[data-content] td,
|
|
main article td {
|
|
padding: 0.5rem 0.75rem !important;
|
|
border-bottom: 1px solid var(--surface-active) !important;
|
|
border-left: none !important;
|
|
border-right: none !important;
|
|
border-top: none !important;
|
|
border-inline-start: none !important;
|
|
color: var(--text-secondary);
|
|
line-height: 1.5;
|
|
}
|
|
|
|
/* Remove bottom border on last row */
|
|
.prose tbody tr:last-child td,
|
|
[data-docs-page] tbody tr:last-child td,
|
|
[data-content] tbody tr:last-child td,
|
|
main article tbody tr:last-child td {
|
|
border-bottom: none !important;
|
|
}
|
|
|
|
/* Code blocks — platform field chrome.
|
|
|
|
Three renderers emit these figures and only two go through components/ui/code-block.tsx, so
|
|
the shell and the fumadocs internals that component cannot reach — the title row, the scroll
|
|
viewport, the tab strip `<Tabs>` puts above a fence — are all defined here. The component
|
|
keeps only the copy control and the prose margin. */
|
|
|
|
/* Local aliases, not new design values: each names an existing platform token pair whose light
|
|
and dark halves differ (`--surface-5`/`--code-bg`, `--text-muted`/`--code-line-number`).
|
|
|
|
`--code-surface` has three consumers. `--code-gutter` has one, and is an alias anyway because
|
|
a `.dark` twin would have to restate the sibling-combinator `:has()` its consumer is matched
|
|
by — the expensive form, which re-checks on every line insertion. */
|
|
:root {
|
|
--code-surface: var(--surface-5);
|
|
--code-gutter: var(--text-muted);
|
|
}
|
|
|
|
.dark {
|
|
--code-surface: var(--code-bg);
|
|
--code-gutter: var(--code-line-number);
|
|
}
|
|
|
|
/* Code block shell — `chipFieldSurfaceClass`'s `rounded-lg` and `--border-1` hairline, over
|
|
the `--surface-5` / `--code-bg` fill. (`--surface-5` is `chipFilledFillTokens`' light fill;
|
|
the dark half is the code surface emcn's `Code` container paints. The chip field uses
|
|
`--surface-4` on dark, which is not a code surface.)
|
|
|
|
Three renderers emit these figures: the MDX `pre` mapping and fumadocs-openapi's
|
|
`renderCodeBlock` both go through `components/ui/code-block.tsx`, but its `UsageTab` renders
|
|
`ClientCodeBlock` directly and takes no override. The shell could be a class on the
|
|
component for the first two, at the cost of defining it twice — once there and once here for
|
|
the third. One definition covering all three is the simpler seam.
|
|
|
|
Vertical margin is deliberately not set here: prose fences want `my-4` (the component
|
|
supplies it) and API samples sit flush inside their panel with `my-0`.
|
|
|
|
@see packages/emcn/src/components/chip/chip-chrome.ts — `chipFieldSurfaceClass`, the constant
|
|
this mirrors. Keep the two in step by hand. */
|
|
figure.shiki,
|
|
div:has(> [role="tablist"]):has(> div > figure.shiki) {
|
|
border-radius: 0.5rem;
|
|
border: 1px solid var(--border-1);
|
|
background-color: var(--code-surface);
|
|
overflow: hidden;
|
|
box-shadow: none;
|
|
}
|
|
|
|
/* Title row — the platform's `--surface-4`-over-`--surface-5` pairing, the same split
|
|
ChipModal uses between its outer frame and inner panel. */
|
|
figure.shiki > div:first-child:has(figcaption),
|
|
div:has(> div > figure.shiki) > [role="tablist"] {
|
|
height: 34px;
|
|
padding-inline: 0.625rem;
|
|
background-color: var(--surface-4);
|
|
border-bottom: 1px solid var(--border-1);
|
|
}
|
|
|
|
figure.shiki figcaption {
|
|
color: var(--text-muted);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-caption);
|
|
}
|
|
|
|
/* fumadocs derives a file-type icon from the title's extension and leaves it at
|
|
`currentColor`; the platform paints row icons `--text-icon`. */
|
|
figure.shiki > div:first-child:has(figcaption) svg {
|
|
color: var(--text-icon);
|
|
}
|
|
|
|
/* Line-number gutter, mirroring the one `Code.Viewer` draws in the app.
|
|
|
|
Only multi-line fences get one, column and digit together. A line number on a one-line shell
|
|
command references nothing, and these docs are mostly one-line commands. Reserving the column
|
|
but leaving it blank was tried and is worse: the code picks up a 44px indent with nothing in
|
|
it, which reads as a rendering fault rather than as alignment. Single-line fences therefore
|
|
keep fumadocs' default padding, so adjacent blocks can start their text 28px apart —
|
|
acceptable, because each fence is its own bordered card, and differing padding between cards
|
|
is far less noticeable than an unexplained indent inside one.
|
|
|
|
The column is opened through fumadocs' own `--padding-left`, not by overriding its `.line`
|
|
rule: that rule is `.shiki:not(.not-fumadocs-codeblock *) .line`, and `:not()` carries its
|
|
argument's specificity, so it lands at (0,3,0) — above anything reasonable to write here.
|
|
|
|
Declared at every level the variable could be shadowed at, because it is inherited and the
|
|
nearest declaration wins: fumadocs sets it on `.shiki`, which is on the `figure` for prose
|
|
fences but on the `figure` AND the inner `pre` for API samples.
|
|
|
|
The `!important` and the viewport selector are belt-and-braces, not strictly required —
|
|
fumadocs' declaration is (0,2,0) and these selectors are (0,3,1) and (0,4,1), so they
|
|
already win, and nothing declares this property on the viewport. They are kept because the
|
|
specificity of `:has()` and `:not()` is easy to miscount in the direction that puts the line
|
|
numbers on top of the code. Remove them only alongside a visual check.
|
|
*/
|
|
figure.shiki:has(.line ~ .line),
|
|
figure.shiki:has(.line ~ .line) > div[role="region"],
|
|
figure.shiki:has(.line ~ .line) .shiki {
|
|
--padding-left: 2.75rem !important;
|
|
}
|
|
|
|
figure.shiki code {
|
|
counter-reset: line;
|
|
}
|
|
|
|
/* Gated on the same condition as the digit these anchor: without it, every line of every
|
|
single-line fence becomes a positioned containing block for a pseudo-element that never
|
|
renders. */
|
|
figure.shiki code:has(.line ~ .line) .line {
|
|
position: relative;
|
|
counter-increment: line;
|
|
}
|
|
|
|
figure.shiki code:has(.line ~ .line) .line::before {
|
|
content: counter(line);
|
|
position: absolute;
|
|
left: 0;
|
|
width: 1.75rem;
|
|
text-align: right;
|
|
color: var(--code-gutter);
|
|
font-size: var(--text-xs);
|
|
font-variant-numeric: tabular-nums;
|
|
user-select: none;
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* The code fill is painted here, on the scroll viewport, rather than left to show through from
|
|
the figure or the tab group. Those diverge per renderer: fumadocs gives a standalone figure
|
|
`bg-fd-card` but an in-tab figure `bg-fd-secondary`, and this app forces
|
|
`--color-fd-card: transparent` on API reference pages. The result was measurable — a request
|
|
panel showing the page background (#fff light, `--bg` dark) beside a response panel showing
|
|
the code surface. The viewport is the one box all three renderers agree on. */
|
|
figure.shiki > div[role="region"] {
|
|
background-color: var(--code-surface);
|
|
/* fumadocs ships 14px of vertical padding, the platform's viewer 8px; 10px splits them and
|
|
keeps a single-line fence from looking hollow at the tighter 21px line box. */
|
|
padding-block: 10px;
|
|
}
|
|
|
|
/* Shiki emits `--shiki-*-bg` custom properties under `defaultColor: false`; keep the `pre` clear
|
|
so nothing paints over the viewport fill. */
|
|
figure.shiki pre {
|
|
background-color: transparent;
|
|
}
|
|
|
|
/* Untitled blocks float the copy control over the code, so the last column has to clear it:
|
|
an 8px offset plus emcn's 20px icon button, with room to breathe. fumadocs reserves 32px,
|
|
which the glyphs run into.
|
|
|
|
`!important` is load-bearing: fumadocs writes this same custom property as an inline style
|
|
on the viewport, and an inline declaration outranks any stylesheet rule without it. */
|
|
figure.shiki:not(:has(figcaption)),
|
|
figure.shiki:not(:has(figcaption)) > div[role="region"],
|
|
figure.shiki:not(:has(figcaption)) .shiki {
|
|
--padding-right: 2.75rem !important;
|
|
}
|
|
|
|
/* Copy control on the one block this app does not render.
|
|
|
|
Prose fences and the API reference's request/response samples both render
|
|
`components/ui/code-block.tsx`, which uses an emcn `Button`. Only fumadocs-openapi's
|
|
`UsageTab` is left — it renders `ClientCodeBlock` directly and takes no override — so these
|
|
rules bring its button to the same geometry and color as emcn's `variant='quiet' size='icon'`.
|
|
|
|
Every rule here is qualified with `:has(> svg[class*="lucide"])`. Without it they also match
|
|
the emcn `Button`, whose `aria-label` is identical, and re-declare chrome that component
|
|
owns — silently pinning docs to today's `buttonVariants` values.
|
|
|
|
The container carries an opaque `--code-surface` pad rather than fumadocs' `backdrop-blur-lg`.
|
|
The blur went milky over a filled block, but something has to sit behind the button: these
|
|
fences scroll horizontally and a long line runs straight under a transparent one. Matching
|
|
the block's own fill hides the code cleanly instead of frosting it. */
|
|
figure.shiki > div:has(> button[aria-label$="Text"] > svg[class*="lucide"]) {
|
|
top: 0.5rem;
|
|
right: 0.5rem;
|
|
padding-left: 0.25rem;
|
|
border-radius: 0.25rem;
|
|
background-color: var(--code-surface);
|
|
backdrop-filter: none;
|
|
}
|
|
|
|
figure.shiki button[aria-label="Copy Text"]:has(> svg[class*="lucide"]),
|
|
figure.shiki button[aria-label="Copied Text"]:has(> svg[class*="lucide"]) {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
border: none;
|
|
border-radius: 0.25rem;
|
|
width: 20px;
|
|
height: 20px;
|
|
color: var(--text-icon-muted);
|
|
background: none;
|
|
box-shadow: none;
|
|
transition:
|
|
background-color 150ms,
|
|
color 150ms;
|
|
}
|
|
|
|
figure.shiki button[aria-label="Copy Text"]:has(> svg[class*="lucide"]) {
|
|
@variant hover-hover {
|
|
background-color: var(--surface-active);
|
|
}
|
|
}
|
|
|
|
figure.shiki button[aria-label="Copied Text"]:has(> svg[class*="lucide"]) {
|
|
color: var(--brand-accent);
|
|
}
|
|
|
|
/* One block per API reference page — the request "usage tabs" (cURL / JavaScript / Go / …) —
|
|
is rendered by fumadocs-openapi's `UsageTab`, which hardcodes its own `ClientCodeBlock`.
|
|
`renderCodeBlock` covers every other sample, and `OperationClientOptions` exposes only
|
|
`APIExampleSelector`, so there is no supported prop that reaches this one. Its lucide
|
|
clipboard is masked over with emcn's `Duplicate` / `Check` artwork so the copy glyph is the
|
|
same everywhere.
|
|
|
|
Scoped to `svg.lucide-*`: any block that already renders the real emcn icon is untouched,
|
|
and if fumadocs ever adds an override for these tabs, these rules simply stop matching.
|
|
The paths are copied from `packages/emcn/src/icons/{duplicate,check}.tsx` — a mask cannot
|
|
reference a component — so they must be updated together if that artwork changes. */
|
|
figure.shiki button[aria-label$="Text"] > svg[class*="lucide"] {
|
|
display: none;
|
|
}
|
|
|
|
figure.shiki button[aria-label$="Text"]:has(> svg[class*="lucide"])::before {
|
|
content: "";
|
|
display: block;
|
|
width: 14px;
|
|
height: 14px;
|
|
background-color: currentColor;
|
|
mask-image: var(--copy-glyph);
|
|
mask-repeat: no-repeat;
|
|
mask-position: center;
|
|
mask-size: contain;
|
|
-webkit-mask-image: var(--copy-glyph);
|
|
-webkit-mask-repeat: no-repeat;
|
|
-webkit-mask-position: center;
|
|
-webkit-mask-size: contain;
|
|
}
|
|
|
|
figure.shiki button[aria-label="Copy Text"]:has(> svg[class*="lucide"]) {
|
|
--copy-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-1 -2 24 24' fill='none' stroke='%23000' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14.25 0.75H2.75C1.64543 0.75 0.75 1.64543 0.75 2.75V14.25'/%3E%3Crect x='5.25' y='5.25' width='14' height='14' rx='2'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
figure.shiki button[aria-label="Copied Text"]:has(> svg[class*="lucide"]) {
|
|
--copy-glyph: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='-1 -2 24 24' fill='none' stroke='%23000' stroke-width='1.25' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M18.25 2.75L7.25 15.75L1.75 10.25'/%3E%3C/svg%3E");
|
|
}
|
|
|
|
/* Tabbed fences (`<Tabs>` around code) — collapse the strip and the figure into one box so
|
|
the group reads as a single control rather than a bar stacked on a card.
|
|
|
|
Every rule in this group carries the full `:has(> div > figure.shiki)` qualifier. Dropping
|
|
it on the panel rules would match prose `<Tabs>` too — nine pages use those — and strip the
|
|
padding they rely on. */
|
|
/* The strip becomes the group's title row; it and a titled fence's caption row are the same
|
|
object, so they share the rule above rather than restating its body.
|
|
|
|
Its scrollbar is hidden, matching what the platform does for a scrolling tab strip
|
|
(`overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden` on emcn's `TabStrip`).
|
|
fumadocs makes the strip `overflow-x-auto`, and an endpoint with ten status codes overflows it
|
|
in the API reference's narrow rail — leaving a scrollbar across the bottom of a 34px header,
|
|
which reads as the header being clipped rather than as something scrollable. The code viewport
|
|
below keeps its scrollbar: there the overflow is content, and hiding it would hide that a line
|
|
continues. */
|
|
div:has(> div > figure.shiki) > [role="tablist"] {
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
padding-inline: 0.375rem;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
div:has(> div > figure.shiki) > [role="tablist"]::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
/* Triggers read as chips — a `--surface-active` pad on the selected tab, at `ChipTag`'s 6px
|
|
inline-tag corner — rather than fumadocs' underline, which drew a hard black rule the
|
|
platform uses nowhere. */
|
|
div:has(> div > figure.shiki) > [role="tablist"] button {
|
|
height: 22px;
|
|
padding-inline: 0.5rem;
|
|
border: none;
|
|
border-radius: 0.375rem;
|
|
color: var(--text-muted);
|
|
font-family: var(--font-mono);
|
|
font-size: var(--text-caption);
|
|
font-weight: 470;
|
|
transition:
|
|
background-color 150ms,
|
|
color 150ms;
|
|
}
|
|
|
|
/* Kept as its own rule rather than nested into the block above: biome's
|
|
`noDuplicateProperties` reads a nested `@variant` setting the same property as a duplicate
|
|
declaration and fails the build. */
|
|
div:has(> div > figure.shiki) > [role="tablist"] button {
|
|
@variant hover-hover {
|
|
color: var(--text-body);
|
|
}
|
|
}
|
|
|
|
/* The API reference's request-sample tabs draw their active indicator as an absolutely
|
|
positioned hairline child rather than a border, so the chip pad below would sit under a
|
|
second, redundant underline. Matched structurally — it is the only element fumadocs renders
|
|
before the trigger's label — rather than by its utility classes, which are upstream's to
|
|
change. */
|
|
div:has(> div > figure.shiki) > [role="tablist"] button > div:first-child {
|
|
display: none;
|
|
}
|
|
|
|
div:has(> div > figure.shiki) > [role="tablist"] button[data-state="active"] {
|
|
background-color: var(--surface-active);
|
|
color: var(--text-primary);
|
|
}
|
|
|
|
/* The panel is scaffolding, not chrome. fumadocs gives it `p-4`, its own `rounded-xl`, and a
|
|
`bg-fd-background` fill that would paint white inside the group's surface; strip all three
|
|
so the fence sits flush and the group reads as one box. */
|
|
div:has(> [role="tablist"]):has(> div > figure.shiki) > [role="tabpanel"] {
|
|
padding: 0;
|
|
border-radius: 0;
|
|
background-color: transparent;
|
|
}
|
|
|
|
/* A fence inside a panel is already inside the group's box — drop its own shell. Its
|
|
viewport padding supplies the inset the panel's `p-4` used to. */
|
|
div:has(> [role="tablist"]):has(> div > figure.shiki) > [role="tabpanel"] > figure.shiki {
|
|
margin: 0;
|
|
border: none;
|
|
border-radius: 0;
|
|
}
|
|
|
|
/* Cards container — vertical list with line dividers instead of grid boxes */
|
|
.prose .grid:has(> a[data-card]),
|
|
#nd-page .grid:has(> a[data-card]) {
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
gap: 0 !important;
|
|
border-top: 1px solid var(--border) !important;
|
|
border-bottom: 1px solid var(--border) !important;
|
|
}
|
|
|
|
/* Card items — line-separated rows with hover bg (scoped to grid container only) */
|
|
.grid > a[data-card] {
|
|
display: flex !important;
|
|
flex-direction: column !important;
|
|
gap: 0.125rem !important;
|
|
background: none !important;
|
|
border: none !important;
|
|
border-bottom: 1px solid var(--border) !important;
|
|
border-radius: 0 !important;
|
|
padding: 0.875rem 1rem !important;
|
|
transition: background-color 0.15s !important;
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
.grid > a[data-card]:last-child {
|
|
border-bottom: none !important;
|
|
}
|
|
|
|
.grid > a[data-card]:hover {
|
|
background-color: var(--surface-3) !important;
|
|
}
|
|
|
|
/* Card title */
|
|
.grid > a[data-card] h3 {
|
|
font-weight: 470 !important;
|
|
font-size: 0.875rem !important;
|
|
color: var(--text-primary) !important;
|
|
}
|
|
|
|
/* Card description */
|
|
.grid > a[data-card] p,
|
|
.grid > a[data-card] div {
|
|
color: var(--text-muted) !important;
|
|
font-size: var(--text-small) !important;
|
|
}
|
|
|
|
/* Steps — lighter styling aligned with platform */
|
|
.fd-steps {
|
|
border-left-color: var(--surface-active) !important;
|
|
}
|
|
|
|
.fd-step::before {
|
|
background-color: var(--surface-4) !important;
|
|
color: var(--text-muted) !important;
|
|
font-weight: 500 !important;
|
|
}
|
|
|
|
/* Blockquotes — subtle left border, matching platform divider */
|
|
.prose blockquote,
|
|
[data-docs-page] blockquote,
|
|
[data-content] blockquote,
|
|
main article blockquote {
|
|
border-left: 3px solid var(--border-1) !important;
|
|
padding: 0.25rem 0 0.25rem 1rem !important;
|
|
color: var(--text-muted) !important;
|
|
font-style: italic;
|
|
background: none !important;
|
|
margin: 1.5rem 0 !important;
|
|
}
|
|
|
|
/* Remove all remaining box-shadows on content components */
|
|
.prose > *,
|
|
[data-docs-page] figure,
|
|
[data-docs-page] table,
|
|
[data-docs-page] [data-card] {
|
|
box-shadow: none !important;
|
|
}
|
|
|
|
/* Tailwind v4 content sources */
|
|
@source '../app/**/*.{js,ts,jsx,tsx,mdx}';
|
|
@source '../components/**/*.{js,ts,jsx,tsx,mdx}';
|
|
@source '../content/**/*.{js,ts,jsx,tsx,mdx}';
|
|
@source '../mdx-components.tsx';
|
|
@source '../node_modules/fumadocs-ui/dist/**/*.js';
|