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>
|
|
@ -155,6 +155,40 @@ export class DocsService {
|
||||||
td: {
|
td: {
|
||||||
render: 'TableCell',
|
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',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)**
|
**[Launch Easy Setup →](/easy-setup)**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
The wizard walks you through four simple steps:
|
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
|
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
|
2. **Maps** — Select geographic regions for offline maps
|
||||||
3. **Content** — Choose curated content collections with Essential, Standard, or Comprehensive tiers
|
3. **Content** — Choose curated content collections with Essential, Standard, or Comprehensive tiers
|
||||||
|
|
||||||
|

|
||||||
4. **Review** — Confirm your selections and start downloading
|
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.
|
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 Assistant — Built-in Chat
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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.
|
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:**
|
**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 — Document-Aware AI
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
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.
|
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:**
|
**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
|
### Maps — Offline Navigation
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
View maps without internet. Download the regions you need before going offline.
|
View maps without internet. Download the regions you need before going offline.
|
||||||
|
|
||||||
**How to use it:**
|
**How to use it:**
|
||||||
|
|
@ -135,6 +145,8 @@ As your needs change, you can add more content anytime:
|
||||||
|
|
||||||
### Wikipedia Selector
|
### Wikipedia Selector
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
N.O.M.A.D. includes a dedicated Wikipedia content management tool for browsing and downloading Wikipedia packages.
|
N.O.M.A.D. includes a dedicated Wikipedia content management tool for browsing and downloading Wikipedia packages.
|
||||||
|
|
||||||
**How to use it:**
|
**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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
Test your hardware performance and see how your NOMAD build stacks up against the community.
|
Test your hardware performance and see how your NOMAD build stacks up against the community.
|
||||||
|
|
||||||
**How to use it:**
|
**How to use it:**
|
||||||
|
|
|
||||||
|
|
@ -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.
|
Think of it as having Wikipedia, Khan Academy, an AI assistant, and offline maps all in one place, running on hardware you control.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## What Can You Do?
|
## What Can You Do?
|
||||||
|
|
||||||
### Browse Offline Knowledge
|
### Browse Offline Knowledge
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,81 @@ import Markdoc from '@markdoc/markdoc'
|
||||||
import { Heading } from './markdoc/Heading'
|
import { Heading } from './markdoc/Heading'
|
||||||
import { List } from './markdoc/List'
|
import { List } from './markdoc/List'
|
||||||
import { ListItem } from './markdoc/ListItem'
|
import { ListItem } from './markdoc/ListItem'
|
||||||
|
import { Image } from './markdoc/Image'
|
||||||
import { Table, TableHead, TableBody, TableRow, TableHeader, TableCell } from './markdoc/Table'
|
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 = ({
|
const Callout = ({
|
||||||
type = 'info',
|
type = 'info',
|
||||||
title,
|
title,
|
||||||
|
|
@ -15,24 +87,29 @@ const Callout = ({
|
||||||
title?: string
|
title?: string
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) => {
|
}) => {
|
||||||
const styles = {
|
const styles: Record<string, string> = {
|
||||||
info: 'bg-blue-50 border-blue-200 text-blue-800',
|
info: 'bg-desert-sand/60 border-desert-olive text-desert-green-darker',
|
||||||
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
|
warning: 'bg-desert-orange-lighter/15 border-desert-orange text-desert-green-darker',
|
||||||
error: 'bg-red-50 border-red-200 text-red-800',
|
error: 'bg-desert-red-lighter/15 border-desert-red text-desert-green-darker',
|
||||||
success: 'bg-green-50 border-green-200 text-green-800',
|
success: 'bg-desert-olive-lighter/15 border-desert-olive text-desert-green-darker',
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
<div className={`border-l-4 rounded-r-lg p-5 mb-6 ${styles[type] || styles.info}`}>
|
||||||
<div className={`border-l-4 p-4 mb-4 ${styles[type]}`}>
|
|
||||||
{title && <h4 className="font-semibold mb-2">{title}</h4>}
|
{title && <h4 className="font-semibold mb-2">{title}</h4>}
|
||||||
{children}
|
<div className="[&>p:last-child]:mb-0">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Component mapping for Markdoc
|
// Component mapping for Markdoc
|
||||||
const components = {
|
const components = {
|
||||||
|
Paragraph,
|
||||||
|
Image,
|
||||||
|
Link,
|
||||||
|
InlineCode,
|
||||||
|
CodeBlock,
|
||||||
|
HorizontalRule,
|
||||||
Callout,
|
Callout,
|
||||||
Heading,
|
Heading,
|
||||||
List,
|
List,
|
||||||
|
|
@ -50,7 +127,9 @@ interface MarkdocRendererProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const MarkdocRenderer: React.FC<MarkdocRendererProps> = ({ content }) => {
|
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
|
export default MarkdocRenderer
|
||||||
|
|
|
||||||
|
|
@ -10,18 +10,18 @@ export function Heading({
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const Component = `h${level}` as keyof JSX.IntrinsicElements
|
const Component = `h${level}` as keyof JSX.IntrinsicElements
|
||||||
const sizes = {
|
const styles: Record<number, string> = {
|
||||||
1: 'text-3xl font-bold',
|
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-semibold',
|
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',
|
3: 'text-xl font-semibold text-desert-green-dark mb-3 mt-8',
|
||||||
4: 'text-lg font-semibold',
|
4: 'text-lg font-semibold text-desert-green mb-2 mt-6',
|
||||||
5: 'text-base font-semibold',
|
5: 'text-base font-semibold text-desert-green mb-2 mt-5',
|
||||||
6: 'text-sm font-semibold',
|
6: 'text-sm font-semibold text-desert-green mb-2 mt-4',
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
<Component id={id} className={`${sizes[level]} mb-2 mt-6`}>
|
<Component id={id} className={styles[level]}>
|
||||||
{children}
|
{children}
|
||||||
</Component>
|
</Component>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
20
admin/inertia/components/markdoc/Image.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -7,9 +7,9 @@ export function List({
|
||||||
start?: number
|
start?: number
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const className = ordered
|
const className = ordered
|
||||||
? 'list-decimal 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-12 mb-4 space-y-1'
|
: 'list-disc list-outside ml-6 mb-5 space-y-2 marker:text-desert-orange'
|
||||||
const Tag = ordered ? 'ol' : 'ul'
|
const Tag = ordered ? 'ol' : 'ul'
|
||||||
return (
|
return (
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
export function ListItem({ children }: { children: React.ReactNode }) {
|
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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export function Table({ children }: { children: React.ReactNode }) {
|
export function Table({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<div className="overflow-x-auto my-6">
|
<div className="overflow-x-auto my-6 rounded-lg border border-desert-tan-lighter shadow-sm">
|
||||||
<table className="min-w-full divide-y divide-gray-300 border border-gray-300">
|
<table className="min-w-full divide-y divide-desert-tan-lighter">
|
||||||
{children}
|
{children}
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -9,20 +9,20 @@ export function Table({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function TableHead({ 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 }) {
|
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 }) {
|
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 }) {
|
export function TableHeader({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</th>
|
</th>
|
||||||
)
|
)
|
||||||
|
|
@ -30,7 +30,7 @@ export function TableHeader({ children }: { children: React.ReactNode }) {
|
||||||
|
|
||||||
export function TableCell({ children }: { children: React.ReactNode }) {
|
export function TableCell({ children }: { children: React.ReactNode }) {
|
||||||
return (
|
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}
|
{children}
|
||||||
</td>
|
</td>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ export default function DocsLayout({ children }: { children: React.ReactNode })
|
||||||
}, [data, isLoading])
|
}, [data, isLoading])
|
||||||
|
|
||||||
return (
|
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} />
|
<StyledSidebar title="Documentation" items={items} />
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,10 @@ export default function Show({ content }: { content: any; }) {
|
||||||
return (
|
return (
|
||||||
<DocsLayout>
|
<DocsLayout>
|
||||||
<Head title={'Documentation'} />
|
<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">
|
||||||
<MarkdocRenderer content={content} />
|
<div className="max-w-4xl">
|
||||||
|
<MarkdocRenderer content={content} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DocsLayout>
|
</DocsLayout>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
BIN
admin/public/docs/ai-chat.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
admin/public/docs/benchmark.png
Normal file
|
After Width: | Height: | Size: 140 KiB |
BIN
admin/public/docs/content-explorer.png
Normal file
|
After Width: | Height: | Size: 163 KiB |
BIN
admin/public/docs/dashboard.png
Normal file
|
After Width: | Height: | Size: 139 KiB |
BIN
admin/public/docs/easy-setup-step1.png
Normal file
|
After Width: | Height: | Size: 214 KiB |
BIN
admin/public/docs/easy-setup-tiers.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
admin/public/docs/knowledge-base.png
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
admin/public/docs/maps.png
Normal file
|
After Width: | Height: | Size: 400 KiB |