Skip to content

Commit

Permalink
fix: <MenuModal> column layout (#19)
Browse files Browse the repository at this point in the history
  • Loading branch information
brillout authored Oct 30, 2024
1 parent 8735857 commit 76fc060
Show file tree
Hide file tree
Showing 6 changed files with 224 additions and 292 deletions.
2 changes: 2 additions & 0 deletions src/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export { Layout }
export { containerQueryMobile }
export { navWidthMin }
export { navWidthMax }

import React from 'react'
import { NavigationContent } from './navigation/Navigation'
Expand Down
8 changes: 2 additions & 6 deletions src/config/resolveHeadingsData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import type { LinkData } from '../components'
import type { Exports, PageContextOriginal } from './resolvePageContext'
import pc from '@brillout/picocolors'
import { parseTitle } from '../parseTitle'
import { determineColumnLayoutEntries } from '../renderer/getStyleColumnLayout'
import { determineColumnEntries } from '../renderer/determineColumnEntries'
assert(!isBrowser())

type PageSectionResolved = {
Expand Down Expand Up @@ -56,14 +56,12 @@ function resolveHeadingsData(pageContext: PageContextOriginal) {
// TODO/refactor: remove navItems
let navItems: NavItem[]
let navItemsAll: NavItemAll[]
let columnLayouts: number[][]
{
const navItemsPageSections = pageSectionsResolved
.filter((pageSection) => pageSection.pageSectionLevel === 2)
.map(pageSectionToNavItem)
navItemsAll = headingsResolved.map(headingToNavItem)
const res = determineColumnLayoutEntries(navItemsAll)
columnLayouts = res.columnLayouts
determineColumnEntries(navItemsAll)
if (isDetachedPage) {
navItems = [headingToNavItem(activeHeading), ...navItemsPageSections]
} else {
Expand All @@ -85,8 +83,6 @@ function resolveHeadingsData(pageContext: PageContextOriginal) {
pageTitle,
documentTitle,
// TODO: don't pass to client-side
columnLayouts,
// TODO: don't pass to client-side
activeCategory,
}
return pageContextAddendum
Expand Down
117 changes: 72 additions & 45 deletions src/navigation/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { usePageContext } from '../renderer/usePageContext'
import '@docsearch/css'
import '../global.d.ts'
import { getViewportWidth } from '../utils/getViewportWidth'
import { navWidthMax, navWidthMin } from '../Layout'

type NavItem = {
level: number
Expand All @@ -22,7 +23,7 @@ type NavItem = {
menuModalFullWidth?: true
}
type NavItemAll = NavItem & {
isColumnLayoutElement?: true
isColumnEntry?: ColumnMap
}
function NavigationContent(props: {
navItems: NavItem[]
Expand Down Expand Up @@ -50,49 +51,51 @@ function NavigationContent(props: {
}

function NavigationColumnLayout(props: { navItemsWithComputed: NavItemComputed[] }) {
const navItemsColumnLayout = groupByColumnLayout(props.navItemsWithComputed)
const paddingBottom = 40
let [viewportWidth, setViewportWidth] = useState<number | undefined>()
const updateviewportwidth = () => setViewportWidth(getViewportWidth())
useEffect(() => {
updateviewportwidth()
window.addEventListener('resize', updateviewportwidth, { passive: true })
})

const navItemsByColumnLayouts = getNavItemsByColumnLayouts(props.navItemsWithComputed, viewportWidth)

return (
<>
{navItemsColumnLayout.map(({ navItemsColumnEntries, isFullWidth }, i) => (
{navItemsByColumnLayouts.map(({ columns, isFullWidth }, i) => (
<div
key={i}
style={{
display: 'flex',
justifyContent: 'center',
width: columns.length * (navWidthMax + 20),
justifyContent: 'space-between',
maxWidth: '100%',
margin: 'auto',
marginBottom: 40,
}}
>
<div
className={`column-layout-${i}`}
style={{
flexGrow: 1,
columnGap: 20,
paddingBottom: isFullWidth ? paddingBottom : undefined,
}}
>
{navItemsColumnEntries.map((navItemColumnEntry, j) => (
<div
key={j}
className="column-layout-entry"
style={{
breakInside: 'avoid',
paddingBottom: !isFullWidth ? paddingBottom : undefined,
paddingTop: isFullWidth ? undefined : 0,
width: '100%',
}}
>
<div>
<NavItemComponent navItem={navItemColumnEntry} />
{navItemColumnEntry.navItemChilds.map((navItem, k) => (
<NavItemComponent navItem={navItem} key={k} />
{columns.map((columnEntry, j) => (
<div
key={j}
style={{
flexGrow: 1,
maxWidth: navWidthMax,
display: 'flex',
flexDirection: 'column',
paddingTop: isFullWidth && j !== 0 ? 36 : undefined,
}}
>
{columnEntry.map((navItems, k) => (
<div key={k}>
{navItems.map((navItem, l) => (
<NavItemComponent navItem={navItem} key={l} />
))}
<CategoryBorder navItemLevel1={isFullWidth ? undefined : navItemColumnEntry!} />
<CategoryBorder navItemLevel1={isFullWidth ? undefined : navItems[0]!} />
</div>
</div>
))}
<CategoryBorder navItemLevel1={!isFullWidth ? undefined : navItemsColumnEntries[0]!} />
</div>
))}
</div>
))}
<CategoryBorder navItemLevel1={!isFullWidth ? undefined : columns[0][0][0]!} />
</div>
))}
</>
Expand Down Expand Up @@ -164,33 +167,57 @@ function NavItemComponent({
}
}

type NavItemsColumnEntry = NavItemComputed & { navItemChilds: NavItemComputed[] }
function groupByColumnLayout(navItems: NavItemComputed[]) {
const navItemsColumnLayout: { navItemsColumnEntries: NavItemsColumnEntry[]; isFullWidth: boolean }[] = []
let navItemsColumnEntries: NavItemsColumnEntry[] = []
type NavItemsByColumnLayout = { columns: NavItemComputed[][][]; isFullWidth: boolean }
function getNavItemsByColumnLayouts(navItems: NavItemComputed[], viewportWidth: number = 0): NavItemsByColumnLayout[] {
const navItemsByColumnEntries = getNavItemsByColumnEntries(navItems)
const numberOfColumnsMax = Math.floor(viewportWidth / navWidthMin) || 1
const navItemsByColumnLayouts: NavItemsByColumnLayout[] = navItemsByColumnEntries.map(
({ columnEntries, isFullWidth }) => {
const numberOfColumns = Math.min(numberOfColumnsMax, columnEntries.length)
const columns: NavItemComputed[][][] = []
columnEntries.forEach((columnEntry) => {
const idx = numberOfColumns === 1 ? 0 : columnEntry.columnMap[numberOfColumns]!
assert(idx >= 0)
columns[idx] ??= []
columns[idx].push(columnEntry.navItems)
})
const navItemsByColumnLayout: NavItemsByColumnLayout = { columns, isFullWidth }
return navItemsByColumnLayout
},
)
return navItemsByColumnLayouts
}

type NavItemsByColumnEntries = { columnEntries: ColumnEntry[]; isFullWidth: boolean }[]
type ColumnEntry = { navItems: NavItemComputed[]; columnMap: ColumnMap }
type ColumnMap = Record<number, number>
function getNavItemsByColumnEntries(navItems: NavItemComputed[]): NavItemsByColumnEntries {
const navItemsByColumnEntries: NavItemsByColumnEntries = []
let columnEntries: ColumnEntry[] = []
let columnEntry: ColumnEntry
let isFullWidth: boolean | undefined
navItems.forEach((navItem) => {
if (navItem.level === 1) {
const isFullWidthPrevious = isFullWidth
isFullWidth = !!navItem.menuModalFullWidth
if (isFullWidthPrevious !== undefined && isFullWidthPrevious !== isFullWidth) {
navItemsColumnLayout.push({ navItemsColumnEntries, isFullWidth: isFullWidthPrevious })
navItemsColumnEntries = []
navItemsByColumnEntries.push({ columnEntries, isFullWidth: isFullWidthPrevious })
columnEntries = []
}
}
assert(isFullWidth !== undefined)
if (navItem.isColumnLayoutElement) {
if (navItem.isColumnEntry) {
assert(navItem.level === 1 || navItem.level === 4)
const navItemColumnEntry = { ...navItem, navItemChilds: [] }
navItemsColumnEntries.push(navItemColumnEntry)
columnEntry = { navItems: [navItem], columnMap: navItem.isColumnEntry }
columnEntries.push(columnEntry)
} else {
assert(navItem.level !== 1)
navItemsColumnEntries[navItemsColumnEntries.length - 1].navItemChilds.push(navItem)
columnEntry.navItems.push(navItem)
}
})
assert(isFullWidth !== undefined)
navItemsColumnLayout.push({ navItemsColumnEntries, isFullWidth })
return navItemsColumnLayout
navItemsByColumnEntries.push({ columnEntries, isFullWidth })
return navItemsByColumnEntries
}

type NavItemComputed = ReturnType<typeof getNavItemsWithComputed>[number]
Expand Down
148 changes: 148 additions & 0 deletions src/renderer/determineColumnEntries.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
export { determineColumnEntries }

// A CSS-only solution doesn't seem to exist.
// - https://github.com/brillout/docpress/blob/2e41d8b9df098ff8312b02f7e9d41a202548e2b9/src/renderer/getStyleColumnLayout.ts#L4-L26

import { type NavItemAll } from '../navigation/Navigation'
import { assert, assertUsage, isBrowser } from '../utils/server'
assert(!isBrowser())

type NavItemWithLength = NavItemAll & { numberOfHeadings: number | null }
function determineColumnEntries(navItems: NavItemAll[]): undefined {
const navItemsWithLength: NavItemWithLength[] = navItems.map((navItem) => ({
...navItem,
numberOfHeadings: navItem.level === 1 || navItem.level === 4 ? 0 : null,
}))
let navItemLevel1: NavItemWithLength | undefined
let navItemLevel4: NavItemWithLength | undefined
navItemsWithLength.forEach((navItem) => {
if (navItem.level === 1) {
navItemLevel1 = navItem
navItemLevel4 = undefined
return
}
if (navItem.level === 4) {
navItemLevel4 = navItem
return
}
const bumpNavItemLength = (navItem: NavItemWithLength) => {
assert(navItem.numberOfHeadings !== null)
navItem.numberOfHeadings++
}
assert(navItemLevel1)
bumpNavItemLength(navItemLevel1)
if (navItemLevel4) {
bumpNavItemLength(navItemLevel4)
}
})

type ColumnEntry = { navItemLeader: NavItemAll; numberOfEntries: number }
const columnLayouts: ColumnEntry[][] = []
let columnEntries: ColumnEntry[] = []
let isFullWidth: boolean | undefined
navItemsWithLength.forEach((navItem, i) => {
let isFullWidthBegin = false
if (navItem.level === 1) {
const isFullWidthPrevious = isFullWidth
isFullWidth = !!navItem.menuModalFullWidth
if (isFullWidth) isFullWidthBegin = true
if (isFullWidthPrevious !== undefined && isFullWidthPrevious !== isFullWidth) {
columnLayouts.push(columnEntries)
columnEntries = []
}
}
const navItemPrevious = navItemsWithLength[i - 1]
const navItemNext = navItemsWithLength[i + 1]
if (
!isFullWidth ? navItem.level === 1 : (navItem.level === 4 && navItemPrevious!.level !== 1) || isFullWidthBegin
) {
if (isFullWidth) {
assert(navItem.level === 4 || (navItem.level === 1 && isFullWidthBegin))
} else {
assert(navItem.level === 1)
}
let { numberOfHeadings } = navItem
assert(numberOfHeadings !== null)
if (isFullWidthBegin) {
assert(navItem.level === 1)
assertUsage(
navItemNext && navItemNext.level === 4,
// We can lift this requirement, but it isn't trivial to implement.
'level-1 headings with menuModalFullWidth need to be followed by a level-4 heading',
)
assert(navItemNext.numberOfHeadings)
numberOfHeadings = navItemNext.numberOfHeadings
}
columnEntries.push({ navItemLeader: navItems[i], numberOfEntries: numberOfHeadings })
}
})
assert(columnEntries!)
columnLayouts.push(columnEntries)

columnLayouts.forEach((columnEntries) => {
for (let numberOfColumns = columnEntries.length; numberOfColumns >= 1; numberOfColumns--) {
const columnsIdMap = determineColumns(
columnEntries.map((columnEntry) => columnEntry.numberOfEntries),
numberOfColumns,
)
columnEntries.forEach((columnEntry, i) => {
columnEntry.navItemLeader.isColumnEntry ??= {}
columnEntry.navItemLeader.isColumnEntry[numberOfColumns] = columnsIdMap[i]
})
}
})
}

function determineColumns(columnsUnmerged: number[], numberOfColumns: number): number[] {
assert(numberOfColumns <= columnsUnmerged.length)
const columnsMergingInit: ColumnMerging[] = columnsUnmerged.map((columnHeight, i) => ({
columnIdsMerged: [i],
heightTotal: columnHeight,
}))
const columnsMerged = mergeColumns(columnsMergingInit, numberOfColumns)
const columnsIdMap: number[] = new Array(columnsUnmerged.length)
assert(columnsMerged.length === numberOfColumns)
columnsMerged.forEach((columnMerged, columnMergedId) => {
columnMerged.columnIdsMerged.forEach((columnId) => {
columnsIdMap[columnId] = columnMergedId
})
})
assert(columnsIdMap.length === columnsUnmerged.length)

return columnsIdMap
}
type ColumnMerging = { columnIdsMerged: number[]; heightTotal: number }
function mergeColumns(columnsMerging: ColumnMerging[], numberOfColumns: number): ColumnMerging[] {
if (columnsMerging.length <= numberOfColumns) return columnsMerging

let mergeCandidate: null | (ColumnMerging & { i: number }) = null
for (let i = 0; i <= columnsMerging.length - 2; i++) {
const column1 = columnsMerging[i + 0]
const column2 = columnsMerging[i + 1]
const heightTotal = column1.heightTotal + column2.heightTotal
if (!mergeCandidate || mergeCandidate.heightTotal > heightTotal) {
mergeCandidate = {
i,
columnIdsMerged: [
//
...column1.columnIdsMerged,
...column2.columnIdsMerged,
],
heightTotal,
}
}
}
assert(mergeCandidate)

const { i } = mergeCandidate
assert(-1 < i && i < columnsMerging.length - 1)
const columnsMergingMod = [
//
...columnsMerging.slice(0, i),
mergeCandidate,
...columnsMerging.slice(i + 2),
]

assert(columnsMergingMod.length === columnsMerging.length - 1)
return mergeColumns(columnsMergingMod, numberOfColumns)
}
Loading

0 comments on commit 76fc060

Please sign in to comment.