From ded2f560e326dbe983eb49f8c230bb958cf14de2 Mon Sep 17 00:00:00 2001 From: Zeno Kapitein Date: Tue, 25 Aug 2026 17:14:58 +0200 Subject: [PATCH] Show loose sections in a section group dropdown as secondary links unless the group starts with one (#4545) Co-authored-by: Claude Opus 5 --- .../section-tabs-loose-section-order.md | 5 + .../SiteSections/SiteSectionTabs.tsx | 136 ++++++++++-------- 2 files changed, 84 insertions(+), 57 deletions(-) create mode 100644 .changeset/section-tabs-loose-section-order.md diff --git a/.changeset/section-tabs-loose-section-order.md b/.changeset/section-tabs-loose-section-order.md new file mode 100644 index 000000000..c1587cf11 --- /dev/null +++ b/.changeset/section-tabs-loose-section-order.md @@ -0,0 +1,5 @@ +--- +"gitbook": patch +--- + +Show loose sections in a section group dropdown as secondary links unless the group starts with one diff --git a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx index c864e921c..1a9c7d83d 100644 --- a/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx +++ b/packages/gitbook/src/components/SiteSections/SiteSectionTabs.tsx @@ -225,6 +225,8 @@ function SectionGroupTileList(props: { const hasSections = sections.length > 0; const hasGroups = groups.length > 0; + // Loose sections only lead when the structure opens with one, otherwise they read as secondary links and trail the groups. + const sectionsLead = items[0]?.object === 'site-section'; const isMasonryLayout = groups.length > GROUP_MASONRY_THRESHOLD; const masonryRows = groups.reduce((total, group) => total + 1 + group.children.length, 0); // title + sections const masonryColumnCount = Math.min( @@ -232,65 +234,85 @@ function SectionGroupTileList(props: { MAX_MASONRY_COLUMNS ); + // Whichever panel comes second is recessed: it carries the divider, the background and inverted tile icons. + const sectionsRecessed = hasGroups && !sectionsLead; + const groupsRecessed = hasSections && sectionsLead; + const RECESSED_PANEL = 'border-tint-subtle bg-tint-subtle max-md:border-t md:border-l'; + + // Non-grouped sections. The wrapper spans the dropdown's height, so the list itself can stay content-sized. + const sectionsPanel = hasSections ? ( +
+
    + {sections.map((section) => ( + + ))} +
+
+ ) : null; + + // Grouped sections + const groupsPanel = hasGroups ? ( +
+
    li]:mb-4' + : 'flex w-full flex-col justify-start space-y-8 md:w-max md:flex-row md:items-start md:gap-[var(--site-section-column-gap)] md:space-y-0' + )} + style={ + isMasonryLayout + ? ({ + '--masonry-columns': String(masonryColumnCount), + } as React.CSSProperties) + : undefined + } + > + {groups.map((group) => ( + + ))} +
+
+ ) : null; + return (
- {/* Non-grouped sections */} - {hasSections && ( -
    - {sections.map((section) => ( - - ))} -
- )} - - {/* Grouped sections */} - {hasGroups && ( -
-
    li]:mb-4' - : 'flex w-full flex-col justify-start space-y-8 md:w-max md:flex-row md:items-start md:gap-[var(--site-section-column-gap)] md:space-y-0' - )} - style={ - isMasonryLayout - ? ({ - '--masonry-columns': String(masonryColumnCount), - } as React.CSSProperties) - : undefined - } - > - {groups.map((group) => ( - - ))} -
-
+ {sectionsLead ? ( + <> + {sectionsPanel} + {groupsPanel} + + ) : ( + <> + {groupsPanel} + {sectionsPanel} + )}
);