s; reset UA chrome and keep only the
+ // underline indicator so they render identically in light and dark.
+ border-bottom: 2px solid transparent;
font-family: var(--strapi-font-family-mono);
font-size: 11px;
font-weight: 500;
padding: 9px 12px;
color: var(--strapi-fg-4);
cursor: pointer;
- border-bottom: 2px solid transparent;
- transition: all 0.15s;
+ transition: color 0.15s, border-color 0.15s;
user-select: none;
&:hover {
color: var(--strapi-fg-2);
}
+ &:focus-visible {
+ outline: 2px solid var(--ifm-link-color);
+ outline-offset: -2px;
+ }
+
&--active {
color: var(--strapi-fg-1);
border-bottom-color: var(--strapi-accent);
@@ -442,7 +464,7 @@
display: flex;
align-items: center;
gap: 10px;
- padding: 10px 14px;
+ padding: 6px 8px 6px 14px;
border-bottom: 1px solid var(--strapi-border);
font-family: var(--strapi-font-family-mono);
font-size: 12px;
@@ -481,14 +503,13 @@
color: var(--strapi-fg-2);
}
- // Wraps the so the copy button can anchor to its top-right corner.
- // Reveals the copy button on hover.
&__codeWrap {
position: relative;
- &:hover .copyCodeBtn,
- .copyCodeBtn:focus-visible {
- opacity: 1;
+ // Collapsed state: clip to ~10 lines of code.
+ &--collapsed {
+ max-height: 240px;
+ overflow: hidden;
}
}
@@ -497,24 +518,30 @@
margin: 0;
overflow-x: auto;
background: transparent;
+ font-family: var(--strapi-font-family-mono);
+ font-size: 12.5px;
+ line-height: 1.65;
+ color: var(--strapi-fg-2);
code {
- font-family: var(--strapi-font-family-mono);
- font-size: 12.5px;
- line-height: 1.65;
- color: var(--strapi-fg-2);
+ font-family: inherit;
// Prism/Infima give the inner a solid (white) background; strip it
// so the code block blends with the now-transparent panel.
background: transparent !important;
}
- // Collapsed state: clip to ~10 lines of code.
- &--collapsed {
- max-height: 220px;
- overflow: hidden;
+ // Soft-wrap toggle (the "wrap" action button).
+ &--wrap code {
+ white-space: pre-wrap;
+ word-break: break-word;
}
}
+ // Each highlighted line is its own block so line breaks are preserved.
+ &__line {
+ display: block;
+ }
+
// Fade-out gradient over the bottom of a collapsed response.
&__fade {
position: absolute;
@@ -554,39 +581,73 @@
color: var(--strapi-accent-light);
}
-/* ═══ COPY BUTTON (self-contained, used by CodePanel + ResponsePanel) ═══ */
-.copyCodeBtn {
- position: absolute;
- top: 8px;
- right: 8px;
- z-index: 2;
+/* ═══ CODE ACTIONS (Ask AI · wrap · copy) ═══ */
+// Inline action group sitting in the panel's top bar (URL bar / status header),
+// pushed to the right — the same placement and look as the title-bar buttons on
+// regular code blocks (see src/scss/code-block.scss .code-title-bar).
+.codeActions {
display: flex;
align-items: center;
- justify-content: center;
- width: 30px;
- height: 30px;
- padding: 0;
- border: 1px solid var(--strapi-border);
- border-radius: 6px;
- background: var(--strapi-surface-2);
- color: var(--strapi-fg-2);
+ gap: 4px;
+ margin-left: auto;
+}
+
+// Shared button base for Ask AI / wrap / copy: transparent, low-opacity at
+// rest, solid on hover — matching .code-title-bar .clean-btn / .ai-button.
+%codeActionBase {
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ padding: 4px 8px;
+ border: none;
+ border-radius: var(--strapi-radius-xs, 4px);
+ background: transparent;
+ color: var(--strapi-fg-3);
+ font-family: var(--strapi-font-family-technical);
+ font-size: 11px;
+ font-weight: 600;
cursor: pointer;
- opacity: 0;
- transition: opacity 0.15s ease-in-out, color 0.15s, border-color 0.15s;
+ opacity: 0.6;
+ transition: opacity 0.15s, background 0.15s;
&:hover {
- color: var(--strapi-fg-1);
- border-color: var(--strapi-border-strong);
+ opacity: 1;
+ background: var(--strapi-surface-3);
}
- &--copied {
+ &:focus-visible {
opacity: 1;
- color: var(--strapi-green);
- border-color: var(--strapi-green);
+ outline: 2px solid var(--ifm-link-color);
+ outline-offset: 1px;
}
- svg {
- display: block;
+ svg { display: block; }
+ i { font-size: 13px; }
+}
+
+.codeActionBtn {
+ @extend %codeActionBase;
+
+ // The Ask AI star uses the primary accent, like elsewhere in the docs.
+ // `.ph-sparkle` is a global (Phosphor) class, so it must be escaped from the
+ // CSS-module local-scope hashing or the rule never matches.
+ :global(.ph-sparkle) {
+ color: var(--strapi-primary-500);
+ }
+
+ &--active {
+ opacity: 1;
+ color: var(--strapi-accent);
+ }
+}
+
+/* ═══ COPY BUTTON (self-contained, used inside .codeActions) ═══ */
+.copyCodeBtn {
+ @extend %codeActionBase;
+
+ &--copied {
+ opacity: 1;
+ color: var(--strapi-green);
}
}
@@ -595,49 +656,81 @@
margin-top: 4px;
}
-.responseTabs {
- display: flex;
+/* ═══ STATUS TABS (accessible segmented control — fixes clickability) ═══ */
+// Rendered as a real role="tablist" of . Styled as a
+// segmented control so the tabs read as clickable at rest, unlike the old
+// pills which looked like decorative badges.
+.statusTabs {
+ display: inline-flex;
gap: 2px;
margin-bottom: 8px;
+ padding: 3px;
+ border: 1px solid var(--strapi-border);
+ border-radius: 8px;
+ background: var(--strapi-surface-2);
}
-.responseTab {
+.statusTab {
+ appearance: none;
+ // Per-status accent carried on a CSS var so the active-state rule doesn't
+ // depend on stacking two CSS-module classes (which hash independently and
+ // don't reliably match a `&.otherClass` selector).
+ --tab-color: var(--strapi-fg-3);
font-family: var(--strapi-font-family-technical);
- font-size: 10px;
+ font-size: 11px;
font-weight: 600;
- padding: 3px 8px;
- border-radius: 5px;
- cursor: pointer;
- transition: all 0.15s;
- text-transform: uppercase;
- letter-spacing: 0.04em;
+ padding: 4px 12px;
+ border-radius: 6px;
border: 1px solid transparent;
- user-select: none;
+ background: transparent;
+ color: var(--strapi-fg-3);
+ cursor: pointer;
+ transition: color 0.15s, background 0.15s, border-color 0.15s, box-shadow 0.15s;
+ letter-spacing: 0.03em;
+ line-height: 1.4;
- &--2xx {
- background: rgba(52, 211, 153, 0.1);
- color: var(--strapi-green);
- }
- &--3xx {
- background: rgba(96, 165, 250, 0.1);
- color: #60a5fa;
- }
- &--4xx {
- background: rgba(248, 113, 113, 0.1);
- color: var(--strapi-red);
+ &--2xx { --tab-color: var(--strapi-green); }
+ &--3xx { --tab-color: #60a5fa; }
+ &--4xx { --tab-color: var(--strapi-red); }
+ &--5xx { --tab-color: var(--strapi-amber); }
+
+ &:hover {
+ background: var(--strapi-surface-3);
+ color: var(--strapi-fg-1);
}
- &--5xx {
- background: rgba(251, 191, 36, 0.1);
- color: var(--strapi-amber);
+
+ &:focus-visible {
+ outline: 2px solid var(--ifm-link-color);
+ outline-offset: 1px;
}
+ // Selected tab lifts onto a solid surface and takes its status accent for
+ // BOTH text and border, so light/dark stay consistent.
&--active {
- &.responseTab--2xx { border-color: var(--strapi-green); }
- &.responseTab--4xx { border-color: var(--strapi-red); }
- &.responseTab--5xx { border-color: var(--strapi-amber); }
+ background: var(--strapi-surface-0);
+ color: var(--tab-color);
+ border-color: var(--tab-color);
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
}
}
+// The global design-system rule `html[data-theme="dark"] [aria-selected="true"]`
+// forces the label to the primary (blue) color and out-specifies a plain class.
+// Re-assert the status accent with matching-or-higher specificity in both themes
+// so the active status tab's text keeps its own color (no blue-text bug).
+:global(html[data-theme='light']),
+:global(html[data-theme='dark']) {
+ .statusTab--active[aria-selected='true'] {
+ color: var(--tab-color);
+ }
+}
+
+// Wraps the response status tabs + code. The status header sits inside each
+// tab, above the (standard) code block.
+.responses {
+ margin-top: 4px;
+}
+
.responseHeader {
display: flex;
align-items: center;
@@ -647,6 +740,15 @@
font-size: 11px;
}
+// Single-status responses (no 200/404 tabs) render the header as a direct child
+// of .responses, flush at 0px while the JSON code sits at 24px. Inset the header
+// so the pulsing dot + "200 OK" line up with the code's opening `{`. In the
+// multi-status case the header lives inside a tab panel (not a direct child),
+// so this rule does not touch it — that alignment is handled in tabs.scss.
+.responses > .responseHeader {
+ padding-left: 24px;
+}
+
.responseDot {
width: 6px;
height: 6px;
@@ -670,11 +772,6 @@
&--err { color: var(--strapi-red); }
}
-.responseTime {
- color: var(--strapi-fg-5);
- margin-left: auto;
-}
-
@keyframes apiPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
diff --git a/docusaurus/src/components/ApiReference/index.js b/docusaurus/src/components/ApiReference/index.js
index c0e7a8b4e6..7220997943 100644
--- a/docusaurus/src/components/ApiReference/index.js
+++ b/docusaurus/src/components/ApiReference/index.js
@@ -2,7 +2,6 @@ export { default as ApiReferencePage } from './ApiReferencePage';
export { default as ApiHeader } from './ApiHeader';
export { default as ApiSidebar } from './ApiSidebar';
export { default as Endpoint } from './Endpoint';
+export { default as Responses, ResponseTab } from './Responses';
export { default as MethodPill } from './MethodPill';
export { default as ParamTable } from './ParamTable';
-export { default as CodePanel } from './CodePanel';
-export { default as ResponsePanel } from './ResponsePanel';
diff --git a/docusaurus/src/scss/_base.scss b/docusaurus/src/scss/_base.scss
index 912c61fb3d..f03b032547 100644
--- a/docusaurus/src/scss/_base.scss
+++ b/docusaurus/src/scss/_base.scss
@@ -359,7 +359,7 @@ main:has(.api-endpoint-block) {
}
.api-endpoint-block--columns [class*="endpoint__desc"] {
border-right: none;
- border-bottom: 1px solid var(--strapi-border);
+ border-bottom: none;
}
[data-content-width="wide"]:not([data-view-mode="ai"]) .api-endpoint-block--columns [class*="endpoint__columns"],
@@ -368,7 +368,7 @@ main:has(.api-endpoint-block) {
}
[data-content-width="wide"]:not([data-view-mode="ai"]) .api-endpoint-block--columns [class*="endpoint__desc"],
[data-content-width="max"]:not([data-view-mode="ai"]) .api-endpoint-block--columns [class*="endpoint__desc"] {
- border-right: 1px solid var(--strapi-border);
+ border-right: none;
border-bottom: none;
}
diff --git a/docusaurus/src/scss/code-block.scss b/docusaurus/src/scss/code-block.scss
index f14c19edf9..a31e1f7a00 100644
--- a/docusaurus/src/scss/code-block.scss
+++ b/docusaurus/src/scss/code-block.scss
@@ -249,39 +249,62 @@ pre.prism-code {
background: var(--code-title-bar-lang-bg) !important;
}
- svg, i::before, span {
+ // Do not force opacity here: the more specific rules inside
+ // .code-block-enhanced handle the rest/hover dimming. Forcing opacity:1 on
+ // every svg also lit up the copy button's stacked success (check) icon,
+ // which Docusaurus keeps at opacity 0 until a copy succeeds — leaving a
+ // ghost icon behind the visible one. Only set color, and never touch the
+ // success icon's opacity.
+ svg:not([class*="copyButtonSuccessIcon"]), i::before, span {
color: var(--code-title-bar-lang-color) !important;
- opacity: 1 !important;
}
}
- // AI button inside the title bar
+ // AI button inside the title bar — same white pill as the copy/wrap buttons
+ // and the Ask AI button on regular code blocks, so all actions match.
.ai-button {
- padding: 4px 8px;
- display: flex;
+ display: inline-flex;
align-items: center;
- gap: 4px;
+ gap: 6px;
+ height: 34px;
+ padding: 0 10px;
font-size: 11px;
font-weight: 600;
- border-radius: var(--strapi-radius-xs);
+ border-radius: var(--strapi-radius-md) !important;
cursor: pointer;
transition: all var(--strapi-duration-fast) var(--strapi-ease);
- min-height: auto;
- opacity: 0.6;
- background: transparent;
+ // Stays opaque; the actions row handles the hover fade.
+ opacity: 1 !important;
+ background: var(--strapi-neutral-0) !important;
+ border: 1px solid var(--strapi-neutral-200) !important;
- &:hover {
- opacity: 1;
- background: var(--code-title-bar-lang-bg);
+ // Same color + opacity as the copy/wrap buttons (which use --strapi-fg-1
+ // at 0.6 opacity), so all three action buttons match at rest. !important
+ // beats the legacy .clean-btn rules (this button also carries clean-btn).
+ // The sparkle is a Phosphor fill glyph rendered via ::before, which does
+ // not inherit the 's color (it defaults to neutral-600, a purple-grey),
+ // so color the ::before explicitly to match the neutral copy/wrap icons.
+ i, span {
+ color: var(--strapi-fg-1) !important;
+ opacity: 0.6 !important;
+ }
+
+ i,
+ i::before {
+ color: var(--strapi-fg-1) !important;
}
i {
font-size: 12px;
- color: var(--code-title-bar-lang-color);
}
- span {
- color: var(--code-title-bar-lang-color);
+ // On hover: full opacity, matching the copy/wrap icons.
+ &:hover {
+ background: var(--strapi-neutral-0) !important;
+
+ i, span {
+ opacity: 1 !important;
+ }
}
}
}
@@ -313,9 +336,11 @@ pre.prism-code {
────────────────────────────────────────────────────── */
.code-block-enhanced {
- // The wrapper provides the border, radius, and background
+ // The wrapper provides the border, radius, and background. Use the same
+ // corner radius as admonitions (--strapi-radius-xs) for all terminal blocks.
+ position: relative;
border: solid 1px var(--custom-code-block-border-color);
- border-radius: var(--strapi-radius-md);
+ border-radius: var(--strapi-radius-xs);
overflow: hidden;
transition: border-color var(--strapi-duration-fast) var(--strapi-ease);
margin-bottom: var(--ifm-leading);
@@ -325,6 +350,23 @@ pre.prism-code {
border-color: var(--strapi-border-accent);
}
+ // Ask AI + the relocated copy/wrap group share this one flex row, so they
+ // align naturally with no absolute positioning (the buttons are moved into
+ // here by CodeBlockWrapper's relocateButtons effect). Hidden at rest, shown
+ // when the whole block is hovered/focused — same as regular code blocks.
+ .code-title-bar__actions {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ opacity: 0;
+ transition: opacity var(--strapi-duration-fast) var(--strapi-ease);
+ }
+
+ &:hover .code-title-bar__actions,
+ &:focus-within .code-title-bar__actions {
+ opacity: 1;
+ }
+
// The inner .theme-code-block should not have its own border
.theme-code-block {
border: none !important;
@@ -338,10 +380,15 @@ pre.prism-code {
}
// Code block container inside enhanced wrapper
- // All backgrounds are transparent — the wrapper controls the bg
+ // All backgrounds are transparent — the wrapper controls the bg.
+ // The :not(:has(codeBlockTitle)) variants raise specificity to beat the
+ // generic `.theme-code-block[codeBlockContainer]:not(:has(title)) { padding:24px }`
+ // rule, so terminal code isn't pushed in by that 24px padding.
.theme-code-block,
.theme-code-block[class*="codeBlockContainer"],
- .theme-code-block [class*="codeBlockContainer"] {
+ .theme-code-block[class*="codeBlockContainer"]:not(:has([class*="codeBlockTitle"])),
+ .theme-code-block [class*="codeBlockContainer"],
+ .theme-code-block [class*="codeBlockContainer"]:not(:has([class*="codeBlockTitle"])) {
padding: 0 !important;
box-shadow: none !important;
background: transparent !important;
@@ -354,7 +401,9 @@ pre.prism-code {
background-color: transparent !important;
code {
- padding: 16px 24px;
+ // Left inset is handled on the lines container below (18px); keep the
+ // code's own left padding at 0 to avoid stacking.
+ padding: 16px 24px 16px 0;
background: transparent !important;
background-color: transparent !important;
@@ -363,29 +412,80 @@ pre.prism-code {
background-color: var(--strapi-primary-500) !important;
}
}
+
+ // The default lines-container padding (for line numbers) is replaced by
+ // an 18px inset so the code lines up under the title bar's first dot.
+ [class*="codeBlockLines"] {
+ padding-left: 18px !important;
+ }
}
}
- // Docusaurus copy/wrap buttons — show on hover, positioned in code area
+ // The copy/wrap button group is moved into .code-title-bar__actions (see
+ // CodeBlockWrapper). It flows inline there — no absolute positioning.
[class*="buttonGroup"] {
- opacity: 0;
- transition: opacity var(--strapi-duration-fast) var(--strapi-ease);
+ position: static !important;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ opacity: 1;
+ // Match the regular (non-terminal) code block buttons exactly: white pill
+ // with a neutral border, so terminal blocks are visually identical to the
+ // rest of the site. Leave the icon color to the default so it inverts
+ // correctly in dark mode.
.clean-btn {
- background: var(--code-title-bar-lang-bg) !important;
position: static !important;
top: auto !important;
- border-radius: var(--strapi-radius-xs);
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 34px;
+ height: 34px;
+ padding: 6.4px;
+ background: var(--strapi-neutral-0) !important;
+ border: 1px solid var(--strapi-neutral-200) !important;
+ border-radius: var(--strapi-radius-md) !important;
+
+ // The button (and its border) stays fully opaque; only the ICON is dimmed
+ // to 0.6 at rest and goes to 1 when the button itself is hovered — the
+ // exact treatment of the Ask AI button, so all three match in light/dark.
+ opacity: 1 !important;
+ // Color the icons so they invert correctly in dark mode.
svg, i::before, span {
- color: var(--code-title-bar-lang-color) !important;
- opacity: 1 !important;
+ color: var(--strapi-fg-1) !important;
}
- }
- }
- &:hover [class*="buttonGroup"] {
- opacity: 1;
+ // Dim the ICON only (never the border). The copy button stacks two SVGs
+ // (copy icon + success check) inside a [class*="copyButtonIcons"] wrapper
+ // and toggles which is visible via each svg's own opacity, so dim the
+ // WRAPPER (0.6 × the inner 1/0 toggle preserves the toggle, no ghost)
+ // rather than the raw svgs. The wrap button has a plain svg, and Ask AI a
+ // glyph in i::before; dim those too.
+ [class*="copyButtonIcons"],
+ > svg,
+ i::before {
+ opacity: 0.6 !important;
+ transition: opacity 0.2s ease;
+ }
+
+ // The copy glyph is two thin overlapping outlined rectangles: far fewer
+ // inked pixels than Ask AI's solid star or the wrap button's thick bars,
+ // so at the same 0.6 opacity it READS lighter. Nudge just the copy icon
+ // up to ~0.78 so its perceived weight matches the other two.
+ [class*="copyButtonIcons"] {
+ opacity: 0.78 !important;
+ }
+
+ &:hover {
+ [class*="copyButtonIcons"],
+ > svg,
+ i::before {
+ opacity: 1 !important;
+ }
+ }
+ }
}
}
@@ -573,24 +673,19 @@ pre.prism-code {
border: none;
transition: all 0.2s ease;
+ // Match the copy/wrap buttons exactly: same neutral icon+label color and the
+ // same 0.6→1 opacity fade (applied by the shared .clean-btn rules below).
+ // This button also carries .clean-btn, so we only need to size the icon here
+ // and NOT recolor it — a colored (purple) icon made Ask AI stand out from the
+ // neutral copy/wrap icons sitting right next to it on hover.
i {
font-size: 13px;
- color: var(--strapi-primary-500);
}
span {
- color: var(--strapi-neutral-600);
font-size: 11px;
font-weight: 600;
}
-
- &:hover {
- background: var(--strapi-primary-100);
-
- span {
- color: var(--strapi-primary-600);
- }
- }
}
// Desktop: Button opacity behavior
@@ -608,7 +703,10 @@ pre.prism-code {
.clean-btn {
opacity: 1 !important;
- svg,
+ // Exclude the copy button's success (check) icon: Docusaurus keeps it at
+ // opacity 0 until a copy succeeds, so forcing an opacity here would make
+ // the check bleed through as a ghost behind the copy icon.
+ svg:not([class*="copyButtonSuccessIcon"]),
i::before,
span {
opacity: 0.6;
@@ -619,7 +717,7 @@ pre.prism-code {
}
.clean-btn:hover {
- svg,
+ svg:not([class*="copyButtonSuccessIcon"]),
i::before,
span {
opacity: 1 !important;
@@ -665,6 +763,18 @@ pre.prism-code {
padding-top: 48px;
}
}
+ }
+
+ // Terminal blocks keep their action buttons in the title bar even on mobile,
+ // so the 48px top padding (meant to clear floating buttons) just leaves a big
+ // empty gap above the code. Remove it here.
+ .code-block-enhanced .theme-code-block {
+ &[class*="codeBlockContainer"],
+ [class*="codeBlockContainer"] {
+ [class*="codeBlockContent"] {
+ padding-top: 0;
+ }
+ }
// Reset button position on mobile (don't put them in title bar)
&:has([class*="codeBlockTitle"]) {
@@ -782,3 +892,19 @@ pre.prism-code {
}
}
}
+
+/* ──────────────────────────────────────────────────────
+ API endpoint code blocks: match the admonition corner radius
+ ────────────────────────────────────────────────────── */
+// The request (tabs), response, and terminal blocks used a 10px radius while
+// admonitions use 4px. Align them to the admonition radius (--strapi-radius-xs)
+// so all boxed content on the page shares the same corners. Scoped to endpoints.
+.api-endpoint-block {
+ .tabs-container,
+ [class*="responses"] [class*="codeBlockContainer"],
+ .code-block-enhanced {
+ border-radius: var(--strapi-radius-xs) !important;
+ }
+ // The terminal window's inner corners (title bar top / code block bottom)
+ // are set to --strapi-radius-xs directly in tabs.scss.
+}
diff --git a/docusaurus/src/scss/details.scss b/docusaurus/src/scss/details.scss
index 7987c35c54..07e94a703b 100644
--- a/docusaurus/src/scss/details.scss
+++ b/docusaurus/src/scss/details.scss
@@ -80,15 +80,35 @@ details.alert {
padding: var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal);
}
+ // A terminal code block (.code-block-enhanced) sitting as a direct child of a
+ // details/alert must keep its own zero padding: the rule above would push its
+ // title bar and code 20px inward, which detaches them from the terminal frame
+ // and draws a full-width divider line (the bug seen in Quick Start Step 2).
+ // Inside a TabItem the wrapper already has padding:0, so this restores parity.
+ > .code-block-enhanced {
+ padding: 0;
+ }
+
> *:last-child {
- padding-bottom: 0 !important;
margin-bottom: 0 !important;
}
+ // Strip the bottom padding of a plain (unframed) last child so its text does
+ // not add space on top of the block's own bottom padding. Framed blocks
+ // (admonitions, code blocks, tab containers) are excluded: their internal
+ // padding is part of their box, and zeroing it collapses their content
+ // against their own border.
+ > *:last-child:not(.theme-admonition):not([class*="admonition"]):not([class*="codeBlock"]):not(.tabs-container):not(.code-block-enhanced) {
+ padding-bottom: 0 !important;
+ }
+
// Only neutralize the top spacing of the first inner element, NOT the
// content wrapper itself: the wrapper keeps its padding-top so the content
// is separated from the summary (and its bottom border when open).
- > div > *:first-child {
+ // Exclude a terminal's title bar (.code-title-bar): it is the first child of
+ // the .code-block-enhanced wrapper and needs its own 10px top padding to keep
+ // the dots, "terminal" label, and copy button vertically centered.
+ > div > *:first-child:not(.code-title-bar) {
padding-top: 0 !important;
margin-top: 0 !important;
}
diff --git a/docusaurus/src/scss/tabs.scss b/docusaurus/src/scss/tabs.scss
index 5aa7b92a58..ba8005dceb 100644
--- a/docusaurus/src/scss/tabs.scss
+++ b/docusaurus/src/scss/tabs.scss
@@ -22,7 +22,11 @@
&__item {
border-top: 2px solid transparent;
border-bottom-width: 2px;
- padding-bottom: 18px !important;
+ // Center the label vertically: the fixed 34px item height plus the
+ // symmetric --tabs-item-padding already center the text. (A previous
+ // padding-bottom:18px pushed the label upward inside the active pill.)
+ display: inline-flex;
+ align-items: center;
font-family: var(--strapi-font-family-technical);
font-weight: 600;
font-size: 13px;
@@ -44,6 +48,15 @@
border-radius: var(--strapi-radius-md, 10px);
border: solid 1px var(--strapi-neutral-150);
overflow: hidden; // Prevents children from overflowing the border-radius
+
+ // In API endpoint blocks, match the response block's border color so the
+ // request (tabs) and response outer borders look identical in both light
+ // and dark. The response uses --custom-code-block-border-color, so reuse
+ // that exact variable here rather than --strapi-border-strong (which
+ // resolves to a different value in dark mode).
+ .api-endpoint-block & {
+ border-color: var(--custom-code-block-border-color);
+ }
.margin-top--md {
margin-top: 24px !important;
@@ -59,6 +72,38 @@
padding: 16px 20px;
}
+ // In API endpoint blocks, use a symmetric 21px top and bottom padding: the
+ // top matches the gap above the active tab pill, and the bottom matches the
+ // top so the code sits evenly between the tab bar and the block's bottom
+ // border (the container's own 24px bottom padding is zeroed out below).
+ .api-endpoint-block & [role="tabpanel"] {
+ padding-top: 21px;
+ padding-bottom: 21px;
+ }
+
+ // Exception: when the tab panel holds a terminal block, drop the top
+ // padding to 0 — the terminal's own title bar already provides the spacing,
+ // so the 21px just left an empty gap between the tab bar and the window.
+ .api-endpoint-block & [role="tabpanel"]:has(.code-block-enhanced) {
+ padding-top: 0;
+ }
+
+ // Response blocks (200/404 status tabs): align the status header
+ // (● 200 OK) and the JSON code with the left edge of the active status
+ // pill's TEXT — the "200" label, not the pill box. The pill box sits at
+ // 24px and its inner padding is 14px, so the label starts at ~38px. Give
+ // the panel a 38px left padding so the header (a direct child) lines up
+ // with the label; the code container's own padding is neutralized below so
+ // it inherits the same 38px inset.
+ .api-endpoint-block [class*="responses"] & [role="tabpanel"] {
+ padding-left: 38px;
+
+ [class*="codeBlockContainer"]:not(:has([class*="codeBlockTitle"])) {
+ padding-left: 0 !important;
+ }
+ }
+
+
// Remove border for nested tabs container
[role="tabpanel"] .tabs-container {
border: none;
@@ -86,6 +131,17 @@
}
}
+ // In API endpoint blocks, align the tab pills with the terminal window's
+ // left edge (24px) and give the tab bar a vertical padding of half that
+ // (12px) so the active pill isn't cramped against the block's top border.
+ .api-endpoint-block & {
+ padding: 12px 0;
+
+ [role="tab"]:nth-child(1) {
+ margin-left: 24px !important;
+ }
+ }
+
// Active tab styles
[aria-selected="true"] {
border-bottom: none;
@@ -120,32 +176,67 @@
// Remove borders from code blocks inside tabs
[class*="codeBlockContainer"]:not(:has([class*="codeBlockTitle"])) {
border: none !important;
+ // Drop the default bottom margin: it left more empty space below the
+ // code than above it, unbalancing the block inside the tab panel.
+ margin-bottom: 0 !important;
+ // Inset the code so its first character lines up with the left edge of
+ // the tab labels' TEXT (the "cURL" word), not the tab button box. The
+ // panel already provides 20px; add 18px so the code starts at ~38px,
+ // matching the tab label (button left 24px + its 14px inner padding).
+ padding-left: 18px !important;
+ // Drop the container's top AND bottom padding: the tab panel provides
+ // the space above and below the code. Without this the container's own
+ // 24px bottom padding stacked on the panel's bottom padding, leaving far
+ // more empty space below the code than above it.
+ padding-top: 0 !important;
+ padding-bottom: 0 !important;
+
+ // A titleless code block inside a tab panel has no bar to host the
+ // action buttons. Sit the button group just below the tab bar's bottom
+ // border (in the gap above the code) rather than straddling that border.
+ // The group is ~33px tall and the gap is ~23px, so it necessarily
+ // overlaps something: we let it overlap the code's right margin (bottom)
+ // instead of the tab bar (top), which reads much cleaner.
+ [class*="buttonGroup"] {
+ top: 9px;
+ }
}
+
[class*="codeBlockContainer"]:has([class*="codeBlockTitle"]) {
margin: 22px 20px;
}
- // Enhanced terminal blocks inside tabs: remove double border, add spacing
+ // Enhanced terminal blocks inside tabs: remove double border, add spacing.
+ // The tab panel adds 20px left padding; the terminal window (title bar +
+ // code) is inset another 4px via its margin so its left border sits at
+ // 24px from the code column — lined up with the response block's `{`.
.code-block-enhanced {
border: none !important;
border-radius: 0 !important;
margin: 0 !important;
background: transparent !important;
+ // Undo the 18px left inset applied to titleless code containers above:
+ // a terminal aligns its code under the first title-bar dot via its own
+ // inner padding, so the extra 18px pushed the command too far right.
+ [class*="codeBlockContainer"]:not(:has([class*="codeBlockTitle"])) {
+ padding-left: 0 !important;
+ }
+
.code-title-bar {
border-radius: 0;
- margin: 0 20px;
+ margin: 0 4px;
border: 1px solid var(--custom-code-block-border-color);
border-bottom: 1px solid var(--code-title-bar-border);
- border-radius: var(--strapi-radius-md) var(--strapi-radius-md) 0 0;
+ border-radius: var(--strapi-radius-xs) var(--strapi-radius-xs) 0 0;
}
.theme-code-block {
- margin: 0 20px 20px !important;
+ margin: 0 4px !important;
border: 1px solid var(--custom-code-block-border-color) !important;
border-top: none !important;
- border-radius: 0 0 var(--strapi-radius-md) var(--strapi-radius-md) !important;
+ border-radius: 0 0 var(--strapi-radius-xs) var(--strapi-radius-xs) !important;
}
}
}
@@ -201,7 +292,13 @@ details {
:root body {
--tabs-active-bg: rgba(73, 69, 255, 0.2); // Slightly different background in dark mode
}
-
+
+ // Match the response block's dark-mode border (which uses --strapi-neutral-200)
+ // so the request (tabs) and response outer borders stay identical in dark too.
+ .api-endpoint-block .tabs-container {
+ border-color: var(--strapi-neutral-200);
+ }
+
.tabs__item:not(.tabs__item--active) {
color: var(--strapi-neutral-400);
}
diff --git a/docusaurus/src/theme/CodeBlock/index.js b/docusaurus/src/theme/CodeBlock/index.js
index 67f491ad63..a1b2de1922 100644
--- a/docusaurus/src/theme/CodeBlock/index.js
+++ b/docusaurus/src/theme/CodeBlock/index.js
@@ -69,7 +69,7 @@ function useInjectAIButton(ref, language, codeContent, showAI) {
btn.className = 'clean-btn ai-button-injected';
btn.title = 'Ask AI to explain this code';
btn.setAttribute('aria-label', 'Ask AI to explain this code example');
- btn.innerHTML = 'Ask AI ';
+ btn.innerHTML = 'Ask AI ';
btn.addEventListener('click', () => handleAskAI(language, codeContent));
container.prepend(btn);
@@ -82,7 +82,6 @@ function useInjectAIButton(ref, language, codeContent, showAI) {
// macOS-style title bar for terminal blocks
function TerminalTitleBar({ language, title, showAI, codeContent }) {
- const langLabel = LANG_LABELS[language] || (language ? language.toUpperCase() : '');
const labelText = title || 'terminal';
return (
@@ -105,13 +104,10 @@ function TerminalTitleBar({ language, title, showAI, codeContent }) {
aria-label="Ask AI to explain this code example"
onClick={() => handleAskAI(language, codeContent)}
>
-
+
Ask AI
)}
- {langLabel && (
- {langLabel}
- )}
);
@@ -140,41 +136,51 @@ export default function CodeBlockWrapper(props) {
// Inject a real DOM cursor element into terminal blocks
// (CSS ::after pseudo-elements get their background overridden by parent rules)
const terminalRef = useRef(null);
- const injectCursor = useCallback((node) => {
- terminalRef.current = node;
- if (!node) return;
-
- // Find the last token line and append a real cursor span
- const injectCursorElement = () => {
- // Remove any previously injected cursor
- const existing = node.querySelector('.terminal-cursor');
- if (existing) existing.remove();
-
- const codeEl = node.querySelector('code');
- if (!codeEl) return;
-
- const lastLine = codeEl.querySelector('.token-line:last-child');
- if (!lastLine) return;
-
- const cursor = document.createElement('span');
- cursor.className = 'terminal-cursor';
-
- // Insert before the trailing so the cursor stays on the same line
- const lastBr = lastLine.querySelector('br:last-child');
- if (lastBr) {
- lastLine.insertBefore(cursor, lastBr);
- lastBr.remove();
- } else {
- lastLine.appendChild(cursor);
- }
- };
- // Run after Docusaurus finishes rendering the code block
- requestAnimationFrame(() => {
- requestAnimationFrame(injectCursorElement);
- });
+ // Terminal blocks render their own title bar (with the Ask AI button) as a
+ // sibling of the Docusaurus code block, while the native copy/wrap buttons
+ // live deep inside the code block. Rather than align the two across separate
+ // DOM subtrees with brittle absolute positioning, physically MOVE the native
+ // button group into the title bar's actions container so Ask AI + wrap + copy
+ // sit in one flex row and align naturally.
+ const relocateButtons = useCallback((wrapper) => {
+ if (!wrapper) return;
+ const move = () => {
+ const actions = wrapper.querySelector('.code-title-bar__actions');
+ if (!actions) return;
+ // Relocate the native copy/wrap group into the title bar. React may
+ // re-render a FRESH group inside the code block after we move one; that
+ // fresh group is the live one (its click handlers are wired), while the
+ // one we moved earlier becomes a detached, dead clone. So whenever a group
+ // appears outside the bar, treat it as the live one: drop whatever stale
+ // group we previously parked in the bar and move the fresh one in. This
+ // keeps the working button (and fixes copy being dead inside StepDetails).
+ const groups = Array.from(wrapper.querySelectorAll('[class*="buttonGroup"]'));
+ const outside = groups.filter((g) => !g.closest('.code-title-bar__actions'));
+ if (outside.length === 0) return; // nothing new to relocate
+ // The last one in document order is the most recently rendered (live) one.
+ const live = outside[outside.length - 1];
+ // Remove any other groups (stale relocated one in the bar + extra dupes).
+ groups.forEach((g) => {
+ if (g !== live) g.remove();
+ });
+ actions.appendChild(live);
+ };
+ // Run now and keep watching: React may re-insert a group after our move.
+ requestAnimationFrame(() => requestAnimationFrame(move));
+ const observer = new MutationObserver(move);
+ observer.observe(wrapper, { childList: true, subtree: true });
+ // Stop observing shortly after mount once the DOM has settled.
+ setTimeout(() => observer.disconnect(), 2000);
}, []);
+ // Ref callback for the terminal wrapper. Only relocates the action buttons
+ // into the title bar; the blinking end-of-line cursor was removed.
+ const injectCursor = useCallback((node) => {
+ terminalRef.current = node;
+ if (node) relocateButtons(node);
+ }, [relocateButtons]);
+
// Terminal blocks: macOS-style wrapper with title bar + blinking cursor
if (isTerminal) {
return (
diff --git a/docusaurus/src/theme/MDXComponents.js b/docusaurus/src/theme/MDXComponents.js
index a6fb64fe10..c232c986fd 100644
--- a/docusaurus/src/theme/MDXComponents.js
+++ b/docusaurus/src/theme/MDXComponents.js
@@ -43,6 +43,7 @@ import Checklist, { ChecklistItem } from '../components/Checklist';
import DebugComponent from '../components/DebugComponent';
// V3: API 2-column layout
import Endpoint from '../components/ApiReference/Endpoint';
+import Responses, { ResponseTab } from '../components/ApiReference/Responses';
import StepDetails from '../components/StepDetails/StepDetails';
import NextSteps from '../components/NextSteps/NextSteps';
@@ -111,6 +112,8 @@ export default {
Codemods,
NamingConventions,
Endpoint,
+ Responses,
+ ResponseTab,
StepDetails,
NextSteps,
};