feat(docs): polish docs rendering with desert-themed components

Add custom Markdoc renderers for images, links, paragraphs, code blocks,
inline code, and horizontal rules. Restyle existing heading, table, and
list components to match the desert tactical color palette. Add 8
screenshots to docs with polished image presentation (rounded corners,
shadow, captions). Constrain content width for readability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Chris Sherwood 2026-02-06 09:38:02 -08:00 committed by Jake Turner
parent 8b8e00de8b
commit 1a95b84a8c
19 changed files with 184 additions and 34 deletions

View File

@ -155,6 +155,40 @@ export class DocsService {
td: {
render: 'TableCell',
},
paragraph: {
render: 'Paragraph',
},
image: {
render: 'Image',
attributes: {
src: { type: String, required: true },
alt: { type: String },
title: { type: String },
},
},
link: {
render: 'Link',
attributes: {
href: { type: String, required: true },
title: { type: String },
},
},
fence: {
render: 'CodeBlock',
attributes: {
content: { type: String },
language: { type: String },
},
},
code: {
render: 'InlineCode',
attributes: {
content: { type: String },
},
},
hr: {
render: 'HorizontalRule',
},
},
}
}

View File

@ -10,10 +10,14 @@ If this is your first time using N.O.M.A.D., the Easy Setup wizard will help you
**[Launch Easy Setup →](/easy-setup)**
![Easy Setup Wizard — Step 1: Choose your capabilities](/docs/easy-setup-step1.png)
The wizard walks you through four simple steps:
1. **Capabilities** — Choose what to enable: Information Library, AI Assistant, Education Platform, Maps, Data Tools, and Notes
2. **Maps** — Select geographic regions for offline maps
3. **Content** — Choose curated content collections with Essential, Standard, or Comprehensive tiers
![Content tiers — Essential, Standard, and Comprehensive](/docs/easy-setup-tiers.png)
4. **Review** — Confirm your selections and start downloading
Depending on what you selected, downloads may take a while. You can monitor progress in the Settings area, continue using features that are already installed, or leave your server running overnight for large downloads.
@ -60,6 +64,8 @@ The Education Platform provides complete educational courses that work offline.
### AI Assistant — Built-in Chat
![AI Chat interface](/docs/ai-chat.png)
N.O.M.A.D. includes a built-in AI chat interface powered by Ollama. It runs entirely on your server — no internet needed, no data sent anywhere.
**What can it do:**
@ -82,6 +88,8 @@ N.O.M.A.D. includes a built-in AI chat interface powered by Ollama. It runs enti
### Knowledge Base — Document-Aware AI
![Knowledge Base upload interface](/docs/knowledge-base.png)
The Knowledge Base lets you upload documents so the AI can reference them when answering your questions. It uses semantic search (RAG via Qdrant) to find relevant information from your uploaded files.
**Supported file types:**
@ -104,6 +112,8 @@ The Knowledge Base lets you upload documents so the AI can reference them when a
### Maps — Offline Navigation
![Offline maps viewer](/docs/maps.png)
View maps without internet. Download the regions you need before going offline.
**How to use it:**
@ -135,6 +145,8 @@ As your needs change, you can add more content anytime:
### Wikipedia Selector
![Content Explorer — browse and download Wikipedia packages and curated collections](/docs/content-explorer.png)
N.O.M.A.D. includes a dedicated Wikipedia content management tool for browsing and downloading Wikipedia packages.
**How to use it:**
@ -146,6 +158,8 @@ N.O.M.A.D. includes a dedicated Wikipedia content management tool for browsing a
### System Benchmark
![System Benchmark with NOMAD Score and Builder Tag](/docs/benchmark.png)
Test your hardware performance and see how your NOMAD build stacks up against the community.
**How to use it:**

View File

@ -8,6 +8,8 @@ Your personal offline knowledge server is ready to use.
Think of it as having Wikipedia, Khan Academy, an AI assistant, and offline maps all in one place, running on hardware you control.
![Command Center Dashboard](/docs/dashboard.png)
## What Can You Do?
### Browse Offline Knowledge

View File

@ -3,9 +3,81 @@ import Markdoc from '@markdoc/markdoc'
import { Heading } from './markdoc/Heading'
import { List } from './markdoc/List'
import { ListItem } from './markdoc/ListItem'
import { Image } from './markdoc/Image'
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from './markdoc/Table'
// Custom components for Markdoc tags
// Paragraph component
const Paragraph = ({ children }: { children: React.ReactNode }) => {
return <p className="mb-4 leading-relaxed text-desert-green-darker/85">{children}</p>
}
// Link component
const Link = ({
href,
title,
children,
}: {
href: string
title?: string
children: React.ReactNode
}) => {
const isExternal = href?.startsWith('http')
return (
<a
href={href}
title={title}
className="text-desert-orange font-medium hover:text-desert-orange-dark underline decoration-desert-orange-lighter/50 underline-offset-2 hover:decoration-desert-orange transition-colors"
{...(isExternal ? { target: '_blank', rel: 'noopener noreferrer' } : {})}
>
{children}
</a>
)
}
// Inline code component
const InlineCode = ({ content, children }: { content?: string; children?: React.ReactNode }) => {
return (
<code className="bg-desert-green-lighter/30 text-desert-green-darker border border-desert-green-lighter/50 px-1.5 py-0.5 rounded text-[0.875em] font-mono">
{content || children}
</code>
)
}
// Code block component
const CodeBlock = ({
content,
language,
children,
}: {
content?: string
language?: string
children?: React.ReactNode
}) => {
const code = content || (typeof children === 'string' ? children : '')
return (
<div className="my-6 overflow-hidden rounded-lg border border-desert-green-dark/20">
{language && (
<div className="bg-desert-green-dark px-4 py-1.5 text-xs font-mono text-desert-green-lighter uppercase tracking-wider">
{language}
</div>
)}
<pre className="bg-desert-green-darker overflow-x-auto p-4">
<code className="text-sm font-mono text-desert-green-lighter leading-relaxed whitespace-pre">
{code}
</code>
</pre>
</div>
)
}
// Horizontal rule component
const HorizontalRule = () => {
return (
<hr className="my-10 border-0 h-px bg-gradient-to-r from-transparent via-desert-tan-lighter to-transparent" />
)
}
// Callout component
const Callout = ({
type = 'info',
title,
@ -15,24 +87,29 @@ const Callout = ({
title?: string
children: React.ReactNode
}) => {
const styles = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
error: 'bg-red-50 border-red-200 text-red-800',
success: 'bg-green-50 border-green-200 text-green-800',
const styles: Record<string, string> = {
info: 'bg-desert-sand/60 border-desert-olive text-desert-green-darker',
warning: 'bg-desert-orange-lighter/15 border-desert-orange text-desert-green-darker',
error: 'bg-desert-red-lighter/15 border-desert-red text-desert-green-darker',
success: 'bg-desert-olive-lighter/15 border-desert-olive text-desert-green-darker',
}
return (
// @ts-ignore
<div className={`border-l-4 p-4 mb-4 ${styles[type]}`}>
<div className={`border-l-4 rounded-r-lg p-5 mb-6 ${styles[type] || styles.info}`}>
{title && <h4 className="font-semibold mb-2">{title}</h4>}
{children}
<div className="[&>p:last-child]:mb-0">{children}</div>
</div>
)
}
// Component mapping for Markdoc
const components = {
Paragraph,
Image,
Link,
InlineCode,
CodeBlock,
HorizontalRule,
Callout,
Heading,
List,
@ -50,7 +127,9 @@ interface MarkdocRendererProps {
}
const MarkdocRenderer: React.FC<MarkdocRendererProps> = ({ content }) => {
return <div className="tracking-wide">{Markdoc.renderers.react(content, React, { components })}</div>
return (
<div className="text-base tracking-wide">{Markdoc.renderers.react(content, React, { components })}</div>
)
}
export default MarkdocRenderer

View File

@ -10,18 +10,18 @@ export function Heading({
children: React.ReactNode
}) {
const Component = `h${level}` as keyof JSX.IntrinsicElements
const sizes = {
1: 'text-3xl font-bold',
2: 'text-2xl font-semibold',
3: 'text-xl font-semibold',
4: 'text-lg font-semibold',
5: 'text-base font-semibold',
6: 'text-sm font-semibold',
const styles: Record<number, string> = {
1: 'text-3xl font-bold text-desert-green-darker pb-3 mb-6 mt-2 border-b-2 border-desert-orange',
2: 'text-2xl font-bold text-desert-green-dark pb-2 mb-5 mt-10 border-b border-desert-tan-lighter',
3: 'text-xl font-semibold text-desert-green-dark mb-3 mt-8',
4: 'text-lg font-semibold text-desert-green mb-2 mt-6',
5: 'text-base font-semibold text-desert-green mb-2 mt-5',
6: 'text-sm font-semibold text-desert-green mb-2 mt-4',
}
return (
// @ts-ignore
<Component id={id} className={`${sizes[level]} mb-2 mt-6`}>
<Component id={id} className={styles[level]}>
{children}
</Component>
)

View File

@ -0,0 +1,20 @@
export function Image({ src, alt, title }: { src: string; alt?: string; title?: string }) {
return (
<figure className="my-8">
<div className="overflow-hidden rounded-lg border border-desert-tan-lighter shadow-md">
<img
src={src}
alt={alt || ''}
title={title}
className="w-full h-auto"
loading="lazy"
/>
</div>
{alt && (
<figcaption className="mt-3 text-center text-sm text-desert-stone italic">
{alt}
</figcaption>
)}
</figure>
)
}

View File

@ -8,8 +8,8 @@ export function List({
children: React.ReactNode
}) {
const className = ordered
? 'list-decimal list-outside !ml-12 mb-4 space-y-1'
: 'list-disc list-outside !ml-12 mb-4 space-y-1'
? 'list-decimal list-outside ml-6 mb-5 space-y-2 marker:text-desert-orange marker:font-semibold'
: 'list-disc list-outside ml-6 mb-5 space-y-2 marker:text-desert-orange'
const Tag = ordered ? 'ol' : 'ul'
return (
// @ts-ignore

View File

@ -1,4 +1,3 @@
export function ListItem({ children }: { children: React.ReactNode }) {
return <li className="ml-0 !pl-4">{children}</li>
return <li className="pl-2 text-desert-green-darker/85 leading-relaxed">{children}</li>
}

View File

@ -1,7 +1,7 @@
export function Table({ children }: { children: React.ReactNode }) {
return (
<div className="overflow-x-auto my-6">
<table className="min-w-full divide-y divide-gray-300 border border-gray-300">
<div className="overflow-x-auto my-6 rounded-lg border border-desert-tan-lighter shadow-sm">
<table className="min-w-full divide-y divide-desert-tan-lighter">
{children}
</table>
</div>
@ -9,20 +9,20 @@ export function Table({ children }: { children: React.ReactNode }) {
}
export function TableHead({ children }: { children: React.ReactNode }) {
return <thead className="bg-gray-50">{children}</thead>
return <thead className="bg-desert-green-dark">{children}</thead>
}
export function TableBody({ children }: { children: React.ReactNode }) {
return <tbody className="divide-y divide-gray-200 bg-white">{children}</tbody>
return <tbody className="divide-y divide-desert-tan-lighter/50 bg-white">{children}</tbody>
}
export function TableRow({ children }: { children: React.ReactNode }) {
return <tr>{children}</tr>
return <tr className="hover:bg-desert-sand/40 transition-colors">{children}</tr>
}
export function TableHeader({ children }: { children: React.ReactNode }) {
return (
<th className="px-6 py-3 text-left text-sm font-semibold text-gray-900 border-r border-gray-300 last:border-r-0">
<th className="px-5 py-3 text-left text-sm font-semibold text-desert-white tracking-wide">
{children}
</th>
)
@ -30,7 +30,7 @@ export function TableHeader({ children }: { children: React.ReactNode }) {
export function TableCell({ children }: { children: React.ReactNode }) {
return (
<td className="px-6 py-4 text-sm text-gray-700 border-r border-gray-200 last:border-r-0">
<td className="px-5 py-3.5 text-sm text-desert-green-darker">
{children}
</td>
)

View File

@ -22,7 +22,7 @@ export default function DocsLayout({ children }: { children: React.ReactNode })
}, [data, isLoading])
return (
<div className="min-h-screen flex flex-row bg-stone-50/90">
<div className="min-h-screen flex flex-row bg-desert-white">
<StyledSidebar title="Documentation" items={items} />
{children}
</div>

View File

@ -6,9 +6,11 @@ export default function Show({ content }: { content: any; }) {
return (
<DocsLayout>
<Head title={'Documentation'} />
<div className="xl:pl-80 py-6">
<div className="xl:pl-80 pt-14 xl:pt-8 pb-8 px-6 sm:px-8 lg:px-12">
<div className="max-w-4xl">
<MarkdocRenderer content={content} />
</div>
</div>
</DocsLayout>
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 214 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
admin/public/docs/maps.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 400 KiB