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
@@ -75,7 +75,7 @@ those rights are not affected by anything in this policy.
|
|||||||
|
|
||||||
Questions about this policy, or about how we handle data:
|
Questions about this policy, or about how we handle data:
|
||||||
|
|
||||||
**business@onixbyte.com**
|
**legal@onixbyte.com**
|
||||||
|
|
||||||
Changsha Onix Byte Technology Co., Ltd
|
Changsha Onix Byte Technology Co., Ltd
|
||||||
Hunan Province, China
|
Hunan Province, China
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ that means you accept the revised terms.
|
|||||||
|
|
||||||
## Contact
|
## Contact
|
||||||
|
|
||||||
**business@onixbyte.com**
|
**legal@onixbyte.com**
|
||||||
|
|
||||||
Changsha Onix Byte Technology Co., Ltd
|
Changsha Onix Byte Technology Co., Ltd
|
||||||
Hunan Province, China
|
Hunan Province, China
|
||||||
@@ -40,22 +40,3 @@ sections:
|
|||||||
|
|
||||||
Alongside client work we build tools of our own. We keep the number small: each
|
Alongside client work we build tools of our own. We keep the number small: each
|
||||||
one exists because we needed it ourselves and suspected others did too.
|
one exists because we needed it ourselves and suspected others did too.
|
||||||
|
|
||||||
## Free to use, nothing to install
|
|
||||||
|
|
||||||
Every tool here runs entirely in your browser. There is no account to create and
|
|
||||||
no upload step — whatever you paste in stays on your machine.
|
|
||||||
|
|
||||||
That is a deliberate choice rather than a limitation. We use these tools on real
|
|
||||||
configuration files and real API responses, and a tool that phones home is a
|
|
||||||
tool we would not be able to use.
|
|
||||||
|
|
||||||
## Dev Lab
|
|
||||||
|
|
||||||
A JSON viewer built for data that is too large and too nested to read by hand.
|
|
||||||
Paste a response, query it with JSONPath, and the matching fields are
|
|
||||||
highlighted in place. Matches can be copied out in one click.
|
|
||||||
|
|
||||||
It also carries a few smaller utilities we kept reaching for.
|
|
||||||
|
|
||||||
**[Open Dev Lab →](https://dev-lab.onixbyte.com)**
|
|
||||||
@@ -56,7 +56,7 @@ localStorage 键),但未经律师审阅。正式依赖前请交由法务检
|
|||||||
|
|
||||||
关于本政策,或关于我们如何处理数据:
|
关于本政策,或关于我们如何处理数据:
|
||||||
|
|
||||||
**business@onixbyte.com**
|
**legal@onixbyte.com**
|
||||||
|
|
||||||
长沙曜珀科技有限公司
|
长沙曜珀科技有限公司
|
||||||
中国湖南省
|
中国湖南省
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ OnixByte 名称、标识及本网站的设计归我们所有。我们发布的
|
|||||||
|
|
||||||
## 联系方式
|
## 联系方式
|
||||||
|
|
||||||
**business@onixbyte.com**
|
**legal@onixbyte.com**
|
||||||
|
|
||||||
长沙曜珀科技有限公司
|
长沙曜珀科技有限公司
|
||||||
中国湖南省
|
中国湖南省
|
||||||
@@ -37,17 +37,3 @@ sections:
|
|||||||
---
|
---
|
||||||
|
|
||||||
除了客户项目,我们也做自己的工具。数量刻意保持得很少:每一件都是因为我们自己需要,并且认为别人可能也需要。
|
除了客户项目,我们也做自己的工具。数量刻意保持得很少:每一件都是因为我们自己需要,并且认为别人可能也需要。
|
||||||
|
|
||||||
## 免费使用,无需安装
|
|
||||||
|
|
||||||
这里的每一个工具都完全在您的浏览器中运行。不需要注册账号,也没有任何上传步骤 —— 您粘贴进去的内容始终留在您自己的机器上。
|
|
||||||
|
|
||||||
这是刻意为之,不是能力所限。我们自己在真实的配置文件和真实的接口返回上使用这些工具,而一个会把数据传出去的工具,我们自己就不会用。
|
|
||||||
|
|
||||||
## Dev Lab
|
|
||||||
|
|
||||||
为「大到没法用肉眼看」的嵌套 JSON 而做的查看器。粘贴一段返回数据,用 JSONPath 查询,命中的字段会就地高亮,结果可一键复制。
|
|
||||||
|
|
||||||
除此之外,它还带了一些我们日常反复需要的小工具。
|
|
||||||
|
|
||||||
**[打开 Dev Lab →](https://dev-lab.onixbyte.com)**
|
|
||||||
@@ -24,6 +24,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.4",
|
"@eslint/js": "^9.39.4",
|
||||||
|
"@mdx-js/react": "3.1.1",
|
||||||
"@rspress/plugin-sitemap": "2.0.11",
|
"@rspress/plugin-sitemap": "2.0.11",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
"@types/node": "^22.8.1",
|
"@types/node": "^22.8.1",
|
||||||
|
|||||||
Generated
+3
@@ -173,6 +173,9 @@ importers:
|
|||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.39.4
|
specifier: ^9.39.4
|
||||||
version: 9.39.4
|
version: 9.39.4
|
||||||
|
'@mdx-js/react':
|
||||||
|
specifier: 3.1.1
|
||||||
|
version: 3.1.1(@types/react@19.2.14)(react@19.2.6)
|
||||||
'@rspress/plugin-sitemap':
|
'@rspress/plugin-sitemap':
|
||||||
specifier: 2.0.11
|
specifier: 2.0.11
|
||||||
version: 2.0.11(@rspress/core@2.0.11(@rspack/core@2.0.3(@swc/helpers@0.5.21))(@types/mdast@4.0.4)(@types/react@19.2.14)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))
|
version: 2.0.11(@rspress/core@2.0.11(@rspack/core@2.0.3(@swc/helpers@0.5.21))(@types/mdast@4.0.4)(@types/react@19.2.14)(micromark-util-types@2.0.2)(micromark@4.0.2(supports-color@7.2.0))(supports-color@7.2.0))
|
||||||
|
|||||||
@@ -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 { TechStack } from "./sections/TechStack"
|
||||||
import { Cases } from "./sections/Cases"
|
import { Cases } from "./sections/Cases"
|
||||||
import { CallToAction } from "./sections/CallToAction"
|
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`. */
|
/** Type guard for a single entry of `frontmatter.sections`. */
|
||||||
function isSection(value: unknown): value is Section {
|
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.
|
* Renders `frontmatter.sections` in declaration order.
|
||||||
*
|
*
|
||||||
* Attached to the `Layout` `afterHero` (home) and `afterDoc` (products,
|
* Attached to the `Layout` `afterFeatures` (home) and `afterDoc` (products,
|
||||||
* services) slots. Returns `null` for any page without a `sections` array, so
|
* services) slots, and to the `DocContent` override for hoisted bands. Returns
|
||||||
* it is inert on blog posts, project pages and every other doc page.
|
* `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 { frontmatter } = useFrontmatter()
|
||||||
const raw = frontmatter?.sections
|
const raw = frontmatter?.sections
|
||||||
if (!Array.isArray(raw)) return null
|
if (!Array.isArray(raw)) return null
|
||||||
|
|
||||||
const sections = raw.filter(isSection)
|
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 (
|
return (
|
||||||
<div className="rp-site-sections">
|
<div className="rp-site-sections">
|
||||||
{sections.map((section, index) => (
|
{visible.map(({ section, index }) => (
|
||||||
<SectionBand
|
<SectionBand
|
||||||
key={section.id ?? `${section.type}-${index}`}
|
key={section.id ?? `${section.type}-${index}`}
|
||||||
section={section}
|
section={section}
|
||||||
|
tinted={index % 2 === 1}
|
||||||
hasContent={hasContent(section)}>
|
hasContent={hasContent(section)}>
|
||||||
{renderBody(section)}
|
{renderBody(section)}
|
||||||
</SectionBand>
|
</SectionBand>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ interface SectionBandProps {
|
|||||||
section: Section
|
section: Section
|
||||||
/** Whether the section body produced any content. */
|
/** Whether the section body produced any content. */
|
||||||
hasContent: boolean
|
hasContent: boolean
|
||||||
|
/** Whether this band takes the soft background in the page's zebra stripe. */
|
||||||
|
tinted: boolean
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -13,15 +15,21 @@ interface SectionBandProps {
|
|||||||
* Shell for a marketing band: the `<section>` wrapper, its heading and its
|
* 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
|
* subtitle. Renders nothing at all when the section has neither content nor a
|
||||||
* heading, so an unfilled page degrades to just its hero.
|
* 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 title = str(section.title)
|
||||||
const subtitle = str(section.subtitle)
|
const subtitle = str(section.subtitle)
|
||||||
|
const className = `rp-site-section rp-not-doc${tinted ? " rp-site-section--tinted" : ""}`
|
||||||
|
|
||||||
if (!hasContent && !title && !subtitle) {
|
if (!hasContent && !title && !subtitle) {
|
||||||
if (!SHOW_COPY_PLACEHOLDERS) return null
|
if (!SHOW_COPY_PLACEHOLDERS) return null
|
||||||
return (
|
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="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)">
|
<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}
|
TODO: {section.type}
|
||||||
@@ -32,7 +40,7 @@ export function SectionBand({ section, hasContent, children }: SectionBandProps)
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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="mx-auto max-w-5xl px-6">
|
||||||
{(title || subtitle) && (
|
{(title || subtitle) && (
|
||||||
<header className="mb-10 text-center">
|
<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
|
* Technology matrix: named groups of technologies, each with an optional
|
||||||
* emoji/icon and link.
|
* 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[] }) {
|
export function TechStack({ groups }: { groups?: TechStackGroup[] }) {
|
||||||
const visible = (arr<TechStackGroup>(groups) ?? [])
|
const visible = (arr<TechStackGroup>(groups) ?? [])
|
||||||
|
|||||||
@@ -50,6 +50,16 @@ interface SectionBase {
|
|||||||
id?: string
|
id?: string
|
||||||
title?: string
|
title?: string
|
||||||
subtitle?: 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 =
|
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"
|
||||||
|
}
|
||||||
+7
-2
@@ -19,11 +19,16 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full-bleed marketing sections, driven by `frontmatter.sections`. */
|
/* Marketing sections, driven by `frontmatter.sections`. A band is full-bleed
|
||||||
|
when it renders below the article and article-width when `placement: top`
|
||||||
|
puts it inside the doc column. */
|
||||||
.rp-site-section {
|
.rp-site-section {
|
||||||
padding-block: var(--rp-site-section-gap);
|
padding-block: var(--rp-site-section-gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
.rp-site-section:nth-child(even) {
|
/* Zebra striping. Chosen by `HomeSections` from the band's position in the
|
||||||
|
page's whole `sections` list rather than by `:nth-child`, so that bands
|
||||||
|
split across the top and bottom slots keep one continuous stripe. */
|
||||||
|
.rp-site-section--tinted {
|
||||||
background: var(--rp-c-bg-soft);
|
background: var(--rp-c-bg-soft);
|
||||||
}
|
}
|
||||||
+7
-2
@@ -8,6 +8,7 @@ import { Tags } from "./components/Tags"
|
|||||||
import { Author } from "./components/Author"
|
import { Author } from "./components/Author"
|
||||||
import { SiteFooter } from "./components/SiteFooter"
|
import { SiteFooter } from "./components/SiteFooter"
|
||||||
import { HomeSections } from "./components/HomeSections"
|
import { HomeSections } from "./components/HomeSections"
|
||||||
|
import { DocContent } from "./components/DocContent"
|
||||||
|
|
||||||
function DocLayout(props: DocLayoutProps) {
|
function DocLayout(props: DocLayoutProps) {
|
||||||
return (
|
return (
|
||||||
@@ -44,6 +45,10 @@ function DocLayout(props: DocLayoutProps) {
|
|||||||
* than part of the prose column, so sections placed there stay full-bleed.
|
* than part of the prose column, so sections placed there stay full-bleed.
|
||||||
* `HomeSections` renders nothing without the frontmatter, and the two slots are
|
* `HomeSections` renders nothing without the frontmatter, and the two slots are
|
||||||
* never both live on the same page.
|
* never both live on the same page.
|
||||||
|
*
|
||||||
|
* A band marked `placement: top` is not rendered here at all — `DocContent`
|
||||||
|
* picks it up and slots it between the page title and the article body, and
|
||||||
|
* `afterDoc` renders the remaining bands. See `components/DocContent.tsx`.
|
||||||
*/
|
*/
|
||||||
function Layout(props: LayoutProps) {
|
function Layout(props: LayoutProps) {
|
||||||
return (
|
return (
|
||||||
@@ -64,7 +69,7 @@ function Layout(props: LayoutProps) {
|
|||||||
afterDoc={
|
afterDoc={
|
||||||
<>
|
<>
|
||||||
{props.afterDoc}
|
{props.afterDoc}
|
||||||
<HomeSections />
|
<HomeSections placement="bottom" />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
@@ -72,5 +77,5 @@ function Layout(props: LayoutProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export * from "@rspress/core/theme-original"
|
export * from "@rspress/core/theme-original"
|
||||||
export { DocLayout, Layout }
|
export { DocLayout, Layout, DocContent }
|
||||||
export { Kbd } from "./components/Kbd"
|
export { Kbd } from "./components/Kbd"
|
||||||
Reference in new issue
Block a user