@hanzo/docs-ui
v17.0.5
Published
The user interface of @hanzo/docs — layouts, sidebar, table of contents, search, Ask AI and the MDX components — composed from @hanzo/ui and @hanzo/gui, styled by @hanzo/design.
Readme
@hanzo/docs-ui
The user interface of @hanzo/docs: layouts, sidebar, table of contents, search,
Ask AI, and the components MDX renders. Composed from @hanzo/ui and @hanzo/gui,
coloured by @hanzo/design. Nothing else: no Tailwind, no Radix, no Base UI, no
shadcn, no cn, no class strings, no generated CSS.
This file is the contract. Read it before writing a component.
What sits under what
@hanzo/design CSS custom properties: --background --foreground --border
↑ read by --muted-foreground --card --primary --ring --text-* --font-*
@hanzo/ui/gui-config the gui config whose theme keys resolve to those variables
↑ mounted by ($background, $color, $borderColor, $color4, $placeholderColor …)
@hanzo/ui gui-backend components: Button, Accordion, Collapsible, Tabs,
Dialog, Popover, Tooltip, ScrollArea, Command, Input, Separator …
↑ composed by
@hanzo/docs-ui (this) DocsLayout, HomeLayout, Sidebar, TOC, SearchDialog, AskAI,
Callout, Card, CodeBlock, Steps, Tabs, TypeTable, Files, …
↑ imported by
a docs site docs.hanzo.ai, docs.lux.network, docs.zoo.ngo, …@hanzo/docs-core (page tree, source loader, search clients, TOC anchors, the
framework abstraction) is untouched by this package and stays UI-free.
Rules
Primitives come from
@hanzo/uifirst,@hanzo/guisecond, nothing third. A component that exists in@hanzo/ui(Accordion, Collapsible, Tabs, Popover, Dialog, Tooltip, ScrollArea, Command, Button, Input, Separator, Select …) is used from there. Layout and text come from@hanzo/gui:YStack,XStack,View,Text,SizableText,Paragraph,H1–H6,Anchor,Sheet,Separator,useMedia,styled,Theme. Semantic elements come from@hanzo/guitoo:Nav,Aside,Main,Article,Header,Footer,Section. Icons come from@hanzogui/lucide-icons-2. Never importreact-native,tamagui,@base-ui/*,@radix-ui/*,class-variance-authority,tailwind-merge,clsx.Style is props and tokens, never class strings. Write
<YStack bg="$background" borderColor="$borderColor" rounded="$4" p="$3">. The token vocabulary is@hanzo/ui/gui-config's:- colour:
$background,$color(foreground),$color12,$color11(secondary text),$color10,$placeholderColor(tertiary),$borderColor,$color4(edge),$color3(raised surface),$color2,$accentBackground,$accentColor,$outlineColor(focus ring — the ONE colour allowed to be loud) - space:
$1…$20on the 4px ramp ($1=4$2=8$3=12$4=16$5=20$6=24$8=32$10=48 …); negative mirrors exist - radius:
$2=6$4=8$6=12$true=8$10=pill - text:
size="$1"=11$2=13$3=14$4=15$6=17$7=21$8=26$10=32 - font:
fontFamily="$mono"for code; body is$bodyWhere design's own vocabulary is needed for something gui has no key for (a hairline, an error tint, an elevation), reach for the CSS variable by name:borderColor="var(--border-hairline)",bg="var(--state-error-bg)". Never a hex, never an rgb. Colour is monochrome; only live/error/warning/success states carry hue, and those come from design's--state-*variables.
- colour:
classNamestays on every public component and is passed through. It is the escape hatch a host uses to reach a component with its own CSS. We never read it, never merge it, never generate one.'use client'on every file that renders a gui component.@hanzo/guiships no client directives, so the boundary is ours. Server-safe files —layouts/*/index.tsx(they compose client parts and pass JSX),mdx.server.tsx,og.tsx,i18n.tsx,components/sidebar/tabs/index.tsx,layouts/shared/index.tsx(types +resolveLinkItems),utils/urls.ts,utils/merge-refs.ts— carry no directive and import no gui. Anything a server layout hands to a client component must be serialisable: JSX, strings, numbers, plain objects, arrays. Never a function across the boundary.Layout is stacks and media props, not a CSS grid. The docs shell is a row:
Aside(sidebar) ·Main(page) ·Aside(toc). Widths are the constants insrc/theme.ts(SIDEBAR_WIDTH,TOC_WIDTH,HEADER_HEIGHT,LAYOUT_WIDTH) and they are also written to the DOM as--fd-sidebar-width,--fd-toc-width,--fd-header-height,--fd-layout-widthon the layout root, so a host's own CSS can still read them. The document scrolls, not a box inside it: the layout root carries aminHand never a height, the row and the page column grow withflexBasis="auto"(flex={1}on the vertical axis is a zero basis, which is a column that collapses to nothing), and only the rail and the contents column pin themselves —position: stickywith thetopandheightrailpublishes intheme.ts, each scrolling its own overflow. Responsive behaviour is$md/$lg/$max-mdprops anduseMedia(), neverwindow.matchMediain render. Belowmdthe sidebar is aSheet(drawer) and the TOC is a popover.The prose that MDX emits is styled by one stylesheet:
css/prose.css. MDX renders rawp,ul,ol,li,blockquote,hr,strong,em, inlinecode,table— elements no component wraps.DocsBodyrenders<Article className="fd-prose">andcss/prose.cssstyles descendants of.fd-prosewith design's variables. It is plain CSS, hand-written, ~150 lines, and it is the ONLY place a selector like.fd-prose pmay appear. Shiki's token colours live incss/shiki.css.css/style.cssimports both plus the design token files. Nothing generates CSS at build time in this package.The gui atomic sheet is a build artefact of the host, produced by our bin.
hanzo-docs-css app/gui.css(bin/css.mjs) bundles@hanzo/ui/gui-configand this package's own dist with esbuild, renders both layouts and every component MDX can put in a page once throughreact-dom/server, then callscss(monochrome)and writes the file. The render is the point: gui writes the rule behindpx="$4"when that component first constructs, so a sheet taken from the config alone carries the themes and almost nothing the pages wear. A component that cannot render in Node is skipped and the sheet is smaller by that component. A host runs it inprebuildandpredev, imports the file once, and mountsRootProviderwhich rendersGuiProviderwithdisableInjectCSS. NevergetCSS(), neveruseServerInsertedHTML.Theme is a class on
<html>, owned bynext-themes,attribute="class", valuesdark/light. Dark is the root.RootProvidermounts next-themes, thenGuiProviderreadingresolvedTheme,disableRootThemeClass.ThemeTogglecallsuseTheme().setTheme. Nothing else touches theme.One search dialog, on
@hanzo/ui'sCommand.SearchDialogand its parts keep their names and props from the previous version; the list is aCommandList, keyboard navigation is the Command's own. The three backends (search-default,search-orama,search-algolia) stay thin: they own the query,SearchDialogowns the rendering.Ask AI is a component of this package, brand-agnostic.
AskAIincomponents/ai/talks to an OpenAI-compatible endpoint (defaulthttps://api.hanzo.ai/v1/chat/completions) with the host's key, sendsX-Retrieval: 1andX-Retrieval-Store: <store>so answers are grounded in the host's index, streams SSE, renders markdown via the same MDX components. The trigger sits in the docs header and the search dialog ("Ask AI about …"). Nothing about Hanzo or Lux is hardcoded; the host passesendpoint,token,store,greeting.API compatibility with 16.x is deliberate. Every export name, prop name and subpath from the 16.x UI package that a host used still exists here, with the same meaning, except:
buttonVariants(was a Tailwind class generator — hosts useButtonfrom@hanzo/ui),cn(wastwMerge), and thefluxandnotebooklayouts (removed;layouts/docsis the docs layout). A host upgrades by bumping the version, deleting its Tailwind setup and importing@hanzo/docs-ui/css/style.css+ its generatedgui.css.Names. Files and exports keep the previous names (hosts import them). New things are named for what they are, one word where one will do:
theme.ts,ask.tsx,transport.ts. NoWrapper, noManager, noHelper.
The file tree
src/
theme.ts layout constants + the --fd-* names they publish
i18n.tsx defineI18nUI (server-safe)
mdx.tsx / mdx.server.tsx defaultMdxComponents / createRelativeLink
og.tsx generateOGImage (server-safe)
page.tsx DocsPage compat shim + re-exports
provider/base.tsx RootProvider: next-themes → GuiProvider → I18n → Search
provider/{next,react-router,tanstack,waku}.tsx
contexts/{i18n,search,tree}.tsx
utils/{ink,merge-refs,urls,link-item,use-copy-button,use-footer-items,use-is-scroll-top}.ts(x)
layouts/shared/index.tsx BaseLayoutProps, NavOptions, resolveLinkItems, useLinkItems
layouts/shared/{theme-toggle,search-toggle,language-toggle}.tsx
layouts/docs/{index,client,sidebar}.tsx, layouts/docs/page/{index,client}.tsx
layouts/home/{index,client,navbar}.tsx
components/{accordion,banner,callout,card,codeblock,dynamic-codeblock,dynamic-codeblock.core,
files,github-info,heading,image-zoom,inline-toc,steps,tabs,type-table}.tsx
components/dialog/{search,search-default,search-orama,search-algolia}.tsx
components/sidebar/{base,page-tree,link-item}.tsx, components/sidebar/tabs/{index,dropdown}.tsx
components/toc/{index,default,clerk}.tsx
components/ai/{ask,transport}.tsx
css/
style.css @import design tokens; @import ./prose.css; @import ./shiki.css
prose.css .fd-prose descendants
shiki.css code token colours
bin/
css.mjs hanzo-docs-css <out>Reference material
- The previous implementation (Tailwind + Base UI) was
packages/base-ui; it is deleted — its names, props and behaviour live on here. - The idiom for gui-backend components:
~/work/hanzo/ui/pkg/ui/src/backends/gui/*(ink,slot,touch, shorthand propsbg px py p m rounded items justify self gap). - A docs UI already written in gui primitives (sidebar, TOC with the SVG active
path, code block, tabs):
~/work/hanzo/gui/apps/gui.hanzo.ai/features/docs/*andfeatures/mdx/MDXComponents.tsx. - Design system rules:
~/work/hanzo/design/skills/design-system/SKILL.md.
