feat(theme): add top placement for sections
- Add `placement: top|bottom` to the section frontmatter - Override `DocContent` to render hoisted bands under the page title - Derive the zebra stripe from list position so split bands stay continuous - Opt marketing bands out of doc typography with `rp-not-doc` - Point the legal and privacy contact address at legal@onixbyte.com - Condense the products pages' body copy into their section frontmatter
This commit is contained in:
15 files changed
+145
-54
No files matched your search
@@ -0,0 +1,69 @@
|
||||
import type { ComponentProps } from "react"
|
||||
import { MDXProvider } from "@mdx-js/react"
|
||||
import { Content, usePage, useSite } from "@rspress/core/runtime"
|
||||
import {
|
||||
Callout,
|
||||
DocContent as OriginalDocContent,
|
||||
FallbackHeading,
|
||||
getCustomMDXComponent,
|
||||
useScrollAfterNav,
|
||||
} from "@rspress/core/theme-original"
|
||||
import { HomeSections } from "./HomeSections"
|
||||
|
||||
/** Taken from upstream so the two cannot drift apart on an Rspress upgrade. */
|
||||
type DocContentProps = ComponentProps<typeof OriginalDocContent>
|
||||
|
||||
/**
|
||||
* The page title, rendered when the page does not supply an `# h1` of its own.
|
||||
* Mirrors the private `FallbackTitle` inside `@rspress/core`'s `DocContent`.
|
||||
*/
|
||||
function FallbackTitle() {
|
||||
const { site } = useSite()
|
||||
const { page } = usePage()
|
||||
const { headingTitle, title } = page
|
||||
|
||||
if (site.themeConfig.fallbackHeadingTitle === false || headingTitle) return null
|
||||
return <FallbackHeading level={1} title={title} />
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors `@rspress/core`'s `DocContent`, with one addition: the bands a page
|
||||
* marks `placement: top` render between the page title and the article body.
|
||||
*
|
||||
* Rspress has no slot for that position. `beforeDocContent` sits above the
|
||||
* title, and `afterDocContent` and the `Layout`'s `afterDoc` both sit below the
|
||||
* whole article — which is why sections used to land at the foot of the
|
||||
* products page. Replacing this component is the only way in, so it is a
|
||||
* deliberate copy of upstream and is worth re-diffing whenever Rspress is
|
||||
* upgraded. Everything below comes straight from upstream: `useScrollAfterNav`
|
||||
* restores the scroll position after a client-side navigation, and the MDX
|
||||
* component map is layered as defaults, then the caller's overrides, then the
|
||||
* placeholder the compiled MDX uses for callouts.
|
||||
*
|
||||
* Every page rendering through `DocLayout` goes through here, so be careful:
|
||||
* a mistake affects blogs and project pages as much as the marketing ones.
|
||||
*/
|
||||
export function DocContent({
|
||||
components,
|
||||
isOverviewPage = false,
|
||||
beforeDocContent,
|
||||
afterDocContent,
|
||||
}: DocContentProps) {
|
||||
useScrollAfterNav()
|
||||
|
||||
const mdxComponents = {
|
||||
...getCustomMDXComponent(),
|
||||
...components,
|
||||
$$$callout$$$: Callout,
|
||||
}
|
||||
|
||||
return (
|
||||
<MDXProvider components={mdxComponents}>
|
||||
{beforeDocContent}
|
||||
{!isOverviewPage && <FallbackTitle />}
|
||||
<HomeSections placement="top" />
|
||||
<Content />
|
||||
{afterDocContent}
|
||||
</MDXProvider>
|
||||
)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import { FeatureGrid } from "./sections/FeatureGrid"
|
||||
import { TechStack } from "./sections/TechStack"
|
||||
import { Cases } from "./sections/Cases"
|
||||
import { CallToAction } from "./sections/CallToAction"
|
||||
import { hasContent, type Section } from "./sections/types"
|
||||
import { hasContent, isHoisted, type Section } from "./sections/types"
|
||||
|
||||
/** Type guard for a single entry of `frontmatter.sections`. */
|
||||
function isSection(value: unknown): value is Section {
|
||||
@@ -33,27 +33,48 @@ function renderBody(section: Section) {
|
||||
}
|
||||
}
|
||||
|
||||
interface HomeSectionsProps {
|
||||
/**
|
||||
* Which bands this instance renders. Omit it to render all of them in
|
||||
* declaration order, which is what the home page wants — it has no article
|
||||
* for a band to sit on either side of.
|
||||
*/
|
||||
placement?: "top" | "bottom"
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders `frontmatter.sections` in declaration order.
|
||||
*
|
||||
* Attached to the `Layout` `afterHero` (home) and `afterDoc` (products,
|
||||
* services) slots. Returns `null` for any page without a `sections` array, so
|
||||
* it is inert on blog posts, project pages and every other doc page.
|
||||
* Attached to the `Layout` `afterFeatures` (home) and `afterDoc` (products,
|
||||
* services) slots, and to the `DocContent` override for hoisted bands. Returns
|
||||
* `null` for any page without a `sections` array, so it is inert on blog posts,
|
||||
* project pages and every other doc page.
|
||||
*/
|
||||
export function HomeSections() {
|
||||
export function HomeSections({ placement }: HomeSectionsProps = {}) {
|
||||
const { frontmatter } = useFrontmatter()
|
||||
const raw = frontmatter?.sections
|
||||
if (!Array.isArray(raw)) return null
|
||||
|
||||
const sections = raw.filter(isSection)
|
||||
if (sections.length === 0) return null
|
||||
// The stripe alternates over the page's whole `sections` list rather than
|
||||
// over whatever this instance happens to render: hoisting a band must not
|
||||
// flip the background of every band below it.
|
||||
const visible = sections
|
||||
.map((section, index) => ({ section, index }))
|
||||
.filter(({ section }) => {
|
||||
if (placement === undefined) return true
|
||||
return placement === "top" ? isHoisted(section) : !isHoisted(section)
|
||||
})
|
||||
|
||||
if (visible.length === 0) return null
|
||||
|
||||
return (
|
||||
<div className="rp-site-sections">
|
||||
{sections.map((section, index) => (
|
||||
{visible.map(({ section, index }) => (
|
||||
<SectionBand
|
||||
key={section.id ?? `${section.type}-${index}`}
|
||||
section={section}
|
||||
tinted={index % 2 === 1}
|
||||
hasContent={hasContent(section)}>
|
||||
{renderBody(section)}
|
||||
</SectionBand>
|
||||
|
||||
@@ -6,6 +6,8 @@ interface SectionBandProps {
|
||||
section: Section
|
||||
/** Whether the section body produced any content. */
|
||||
hasContent: boolean
|
||||
/** Whether this band takes the soft background in the page's zebra stripe. */
|
||||
tinted: boolean
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
@@ -13,15 +15,21 @@ interface SectionBandProps {
|
||||
* Shell for a marketing band: the `<section>` wrapper, its heading and its
|
||||
* subtitle. Renders nothing at all when the section has neither content nor a
|
||||
* heading, so an unfilled page degrades to just its hero.
|
||||
*
|
||||
* `rp-not-doc` opts the band out of the doc typography, which applies to
|
||||
* everything inside `.rp-doc` and would otherwise restyle our headings and
|
||||
* paragraphs. It is inert for bands rendered outside the article, as on the
|
||||
* home page.
|
||||
*/
|
||||
export function SectionBand({ section, hasContent, children }: SectionBandProps) {
|
||||
export function SectionBand({ section, hasContent, tinted, children }: SectionBandProps) {
|
||||
const title = str(section.title)
|
||||
const subtitle = str(section.subtitle)
|
||||
const className = `rp-site-section rp-not-doc${tinted ? " rp-site-section--tinted" : ""}`
|
||||
|
||||
if (!hasContent && !title && !subtitle) {
|
||||
if (!SHOW_COPY_PLACEHOLDERS) return null
|
||||
return (
|
||||
<section id={section.id} className="rp-site-section">
|
||||
<section id={section.id} className={className}>
|
||||
<div className="mx-auto max-w-5xl px-6">
|
||||
<div className="rounded-lg border border-dashed border-(--rp-c-divider) p-8 text-center text-sm text-(--rp-c-text-3)">
|
||||
TODO: {section.type}
|
||||
@@ -32,7 +40,7 @@ export function SectionBand({ section, hasContent, children }: SectionBandProps)
|
||||
}
|
||||
|
||||
return (
|
||||
<section id={section.id} className="rp-site-section">
|
||||
<section id={section.id} className={className}>
|
||||
<div className="mx-auto max-w-5xl px-6">
|
||||
{(title || subtitle) && (
|
||||
<header className="mb-10 text-center">
|
||||
|
||||
@@ -3,9 +3,6 @@ import { arr, str, type TechStackGroup, type TechStackItem } from "./types"
|
||||
/**
|
||||
* Technology matrix: named groups of technologies, each with an optional
|
||||
* emoji/icon and link.
|
||||
*
|
||||
* TODO(copy): populate `frontmatter.sections` with real entries. Groups with no
|
||||
* items are skipped, so a half-filled list still renders sensibly.
|
||||
*/
|
||||
export function TechStack({ groups }: { groups?: TechStackGroup[] }) {
|
||||
const visible = (arr<TechStackGroup>(groups) ?? [])
|
||||
|
||||
@@ -50,6 +50,16 @@ interface SectionBase {
|
||||
id?: string
|
||||
title?: string
|
||||
subtitle?: string
|
||||
/**
|
||||
* Where the band sits relative to the article body. `"top"` hoists it above
|
||||
* the body, directly under the page title; `"bottom"` — the default — leaves
|
||||
* it below the article.
|
||||
*
|
||||
* Only doc pages can honour this, because only they have an article to sit
|
||||
* between. The home page renders every band below the features grid, in
|
||||
* declaration order, whatever this says.
|
||||
*/
|
||||
placement?: "top" | "bottom"
|
||||
}
|
||||
|
||||
export type Section =
|
||||
@@ -100,3 +110,8 @@ export function hasContent(section: Section): boolean {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Whether a section asked to be hoisted above the article body. */
|
||||
export function isHoisted(section: Section): boolean {
|
||||
return section.placement === "top"
|
||||
}
|
||||
Reference in new issue
Block a user