diff --git a/docs/en-gb/legal/privacy.md b/docs/en-gb/legal/privacy.md index fb79a4b..5aeaed9 100644 --- a/docs/en-gb/legal/privacy.md +++ b/docs/en-gb/legal/privacy.md @@ -75,7 +75,7 @@ those rights are not affected by anything in this policy. Questions about this policy, or about how we handle data: -**business@onixbyte.com** +**legal@onixbyte.com** Changsha Onix Byte Technology Co., Ltd Hunan Province, China diff --git a/docs/en-gb/legal/terms.md b/docs/en-gb/legal/terms.md index 0138371..b82a163 100644 --- a/docs/en-gb/legal/terms.md +++ b/docs/en-gb/legal/terms.md @@ -81,7 +81,7 @@ that means you accept the revised terms. ## Contact -**business@onixbyte.com** +**legal@onixbyte.com** Changsha Onix Byte Technology Co., Ltd Hunan Province, China diff --git a/docs/en-gb/products/index.md b/docs/en-gb/products/index.md index 7a86600..341748b 100644 --- a/docs/en-gb/products/index.md +++ b/docs/en-gb/products/index.md @@ -40,22 +40,3 @@ sections: 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. - -## 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)** diff --git a/docs/zh-hans/legal/privacy.md b/docs/zh-hans/legal/privacy.md index 9589c42..2148549 100644 --- a/docs/zh-hans/legal/privacy.md +++ b/docs/zh-hans/legal/privacy.md @@ -56,7 +56,7 @@ localStorage 键),但未经律师审阅。正式依赖前请交由法务检 关于本政策,或关于我们如何处理数据: -**business@onixbyte.com** +**legal@onixbyte.com** 长沙曜珀科技有限公司 中国湖南省 diff --git a/docs/zh-hans/legal/terms.md b/docs/zh-hans/legal/terms.md index f1e6469..7ea4675 100644 --- a/docs/zh-hans/legal/terms.md +++ b/docs/zh-hans/legal/terms.md @@ -57,7 +57,7 @@ OnixByte 名称、标识及本网站的设计归我们所有。我们发布的 ## 联系方式 -**business@onixbyte.com** +**legal@onixbyte.com** 长沙曜珀科技有限公司 中国湖南省 diff --git a/docs/zh-hans/products/index.md b/docs/zh-hans/products/index.md index fb8f1d5..39f1086 100644 --- a/docs/zh-hans/products/index.md +++ b/docs/zh-hans/products/index.md @@ -37,17 +37,3 @@ sections: --- 除了客户项目,我们也做自己的工具。数量刻意保持得很少:每一件都是因为我们自己需要,并且认为别人可能也需要。 - -## 免费使用,无需安装 - -这里的每一个工具都完全在您的浏览器中运行。不需要注册账号,也没有任何上传步骤 —— 您粘贴进去的内容始终留在您自己的机器上。 - -这是刻意为之,不是能力所限。我们自己在真实的配置文件和真实的接口返回上使用这些工具,而一个会把数据传出去的工具,我们自己就不会用。 - -## Dev Lab - -为「大到没法用肉眼看」的嵌套 JSON 而做的查看器。粘贴一段返回数据,用 JSONPath 查询,命中的字段会就地高亮,结果可一键复制。 - -除此之外,它还带了一些我们日常反复需要的小工具。 - -**[打开 Dev Lab →](https://dev-lab.onixbyte.com)** diff --git a/package.json b/package.json index ba98588..860424d 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", + "@mdx-js/react": "3.1.1", "@rspress/plugin-sitemap": "2.0.11", "@tailwindcss/postcss": "^4.3.0", "@types/node": "^22.8.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a80daeb..af9cb47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -173,6 +173,9 @@ importers: '@eslint/js': specifier: ^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': 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)) diff --git a/theme/components/DocContent.tsx b/theme/components/DocContent.tsx new file mode 100644 index 0000000..aa9ba82 --- /dev/null +++ b/theme/components/DocContent.tsx @@ -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 + +/** + * 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 +} + +/** + * 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 ( + + {beforeDocContent} + {!isOverviewPage && } + + + {afterDocContent} + + ) +} diff --git a/theme/components/HomeSections.tsx b/theme/components/HomeSections.tsx index 0f49ae5..091cca6 100644 --- a/theme/components/HomeSections.tsx +++ b/theme/components/HomeSections.tsx @@ -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 (
- {sections.map((section, index) => ( + {visible.map(({ section, index }) => ( {renderBody(section)} diff --git a/theme/components/sections/SectionBand.tsx b/theme/components/sections/SectionBand.tsx index c1cef93..bd2e6ab 100644 --- a/theme/components/sections/SectionBand.tsx +++ b/theme/components/sections/SectionBand.tsx @@ -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 `
` 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 ( -
+
TODO: {section.type} @@ -32,7 +40,7 @@ export function SectionBand({ section, hasContent, children }: SectionBandProps) } return ( -
+
{(title || subtitle) && (
diff --git a/theme/components/sections/TechStack.tsx b/theme/components/sections/TechStack.tsx index dbfa750..09c4ccf 100644 --- a/theme/components/sections/TechStack.tsx +++ b/theme/components/sections/TechStack.tsx @@ -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(groups) ?? []) diff --git a/theme/components/sections/types.ts b/theme/components/sections/types.ts index a42423d..d463380 100644 --- a/theme/components/sections/types.ts +++ b/theme/components/sections/types.ts @@ -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" +} diff --git a/theme/index.css b/theme/index.css index 333c4bb..6aeabeb 100644 --- a/theme/index.css +++ b/theme/index.css @@ -19,11 +19,16 @@ 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 { 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); } diff --git a/theme/index.tsx b/theme/index.tsx index dfa41d6..a1f1037 100644 --- a/theme/index.tsx +++ b/theme/index.tsx @@ -8,6 +8,7 @@ import { Tags } from "./components/Tags" import { Author } from "./components/Author" import { SiteFooter } from "./components/SiteFooter" import { HomeSections } from "./components/HomeSections" +import { DocContent } from "./components/DocContent" function DocLayout(props: DocLayoutProps) { return ( @@ -44,6 +45,10 @@ function DocLayout(props: DocLayoutProps) { * than part of the prose column, so sections placed there stay full-bleed. * `HomeSections` renders nothing without the frontmatter, and the two slots are * 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) { return ( @@ -64,7 +69,7 @@ function Layout(props: LayoutProps) { afterDoc={ <> {props.afterDoc} - + } /> @@ -72,5 +77,5 @@ function Layout(props: LayoutProps) { } export * from "@rspress/core/theme-original" -export { DocLayout, Layout } +export { DocLayout, Layout, DocContent } export { Kbd } from "./components/Kbd"