Skip to content

Commit 6af5083

Browse files
committed
docs: align content with real behavior and add services page
1 parent a60ec67 commit 6af5083

21 files changed

Lines changed: 913 additions & 539 deletions

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"lint": "echo \"no lint configured\""
1010
},
1111
"dependencies": {
12+
"highlight.js": "^11.11.1",
1213
"react": "^18.3.1",
1314
"react-dom": "^18.3.1",
1415
"react-router-dom": "^6.22.3"

src/components/DocsBlocks.tsx

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import React from 'react';
2+
3+
type CalloutVariant = 'note' | 'tip' | 'warning';
4+
5+
const calloutStyles: Record<CalloutVariant, string> = {
6+
note: 'border-sky-500/30 bg-sky-500/10 text-sky-100',
7+
tip: 'border-emerald-500/30 bg-emerald-500/10 text-emerald-100',
8+
warning: 'border-amber-500/30 bg-amber-500/10 text-amber-100',
9+
};
10+
11+
const labelStyles: Record<CalloutVariant, string> = {
12+
note: 'text-sky-300',
13+
tip: 'text-emerald-300',
14+
warning: 'text-amber-300',
15+
};
16+
17+
export const TldrBlock: React.FC<{ items: string[] }> = ({ items }) => (
18+
<section className="rounded-xl border border-cyan-500/25 bg-cyan-500/10 p-4">
19+
<p className="text-xs font-semibold uppercase tracking-wider text-cyan-300">TL;DR</p>
20+
<ul className="mt-2 space-y-1 text-sm text-cyan-100">
21+
{items.map((item) => (
22+
<li key={item}>- {item}</li>
23+
))}
24+
</ul>
25+
</section>
26+
);
27+
28+
export const Callout: React.FC<{
29+
variant: CalloutVariant;
30+
title: string;
31+
children: React.ReactNode;
32+
}> = ({ variant, title, children }) => (
33+
<section className={`rounded-xl border p-4 ${calloutStyles[variant]}`}>
34+
<p className={`text-xs font-semibold uppercase tracking-wider ${labelStyles[variant]}`}>{title}</p>
35+
<div className="mt-2 text-sm leading-relaxed">{children}</div>
36+
</section>
37+
);
38+
39+
export const StepList: React.FC<{ steps: Array<{ title: string; detail: string }> }> = ({ steps }) => (
40+
<ol className="space-y-3">
41+
{steps.map((step, index) => (
42+
<li key={step.title} className="flex gap-3 rounded-lg border border-slate-800/80 bg-slate-900/40 p-3">
43+
<span className="mt-0.5 inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-full border border-cyan-500/30 bg-cyan-500/10 text-xs font-semibold text-cyan-300">
44+
{index + 1}
45+
</span>
46+
<div>
47+
<p className="text-sm font-medium text-white">{step.title}</p>
48+
<p className="mt-0.5 text-sm text-slate-400">{step.detail}</p>
49+
</div>
50+
</li>
51+
))}
52+
</ol>
53+
);

src/components/DocsLayout.tsx

Lines changed: 147 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,27 @@
1-
import React, { useState } from 'react';
1+
import React, { useEffect, useState } from 'react';
22
import { NavLink, useLocation } from 'react-router-dom';
3+
import hljs from 'highlight.js/lib/core';
4+
import bash from 'highlight.js/lib/languages/bash';
5+
import dockerfile from 'highlight.js/lib/languages/dockerfile';
6+
import ini from 'highlight.js/lib/languages/ini';
7+
import javascript from 'highlight.js/lib/languages/javascript';
8+
import json from 'highlight.js/lib/languages/json';
9+
import powershell from 'highlight.js/lib/languages/powershell';
10+
import python from 'highlight.js/lib/languages/python';
11+
import typescript from 'highlight.js/lib/languages/typescript';
12+
import xml from 'highlight.js/lib/languages/xml';
13+
import yaml from 'highlight.js/lib/languages/yaml';
14+
15+
hljs.registerLanguage('bash', bash);
16+
hljs.registerLanguage('dockerfile', dockerfile);
17+
hljs.registerLanguage('ini', ini);
18+
hljs.registerLanguage('javascript', javascript);
19+
hljs.registerLanguage('json', json);
20+
hljs.registerLanguage('powershell', powershell);
21+
hljs.registerLanguage('python', python);
22+
hljs.registerLanguage('typescript', typescript);
23+
hljs.registerLanguage('xml', xml);
24+
hljs.registerLanguage('yaml', yaml);
325

426
interface SidebarSection {
527
title: string;
@@ -9,7 +31,7 @@ interface SidebarSection {
931

1032
const sections: SidebarSection[] = [
1133
{
12-
title: 'Getting Started',
34+
title: 'Nihil Wrapper',
1335
icon: (
1436
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1537
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M15.59 14.37a6 6 0 01-5.84 7.38v-4.8m5.84-2.58a14.98 14.98 0 006.16-12.12A14.98 14.98 0 009.63 8.41m5.96 5.96a14.926 14.926 0 01-5.84 2.58m0 0a14.926 14.926 0 01-5.84-2.58" />
@@ -19,33 +41,27 @@ const sections: SidebarSection[] = [
1941
{ label: 'Linux', to: '/docs/installation/linux' },
2042
{ label: 'macOS', to: '/docs/installation/macos' },
2143
{ label: 'Windows', to: '/docs/installation/windows' },
22-
],
23-
},
24-
{
25-
title: 'Usage',
26-
icon: (
27-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
29-
</svg>
30-
),
31-
items: [
3244
{ label: 'CLI Commands', to: '/docs/usage' },
3345
{ label: 'Configuration', to: '/docs/configuration' },
46+
{ label: 'Services', to: '/docs/service' },
47+
{ label: 'Shell Completion', to: '/docs/completion' },
48+
{ label: 'Command History', to: '/docs/history' },
3449
],
3550
},
3651
{
37-
title: 'Images',
52+
title: 'Nihil Images',
3853
icon: (
3954
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
40-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M21 7.5l-2.25-1.313M21 7.5v2.25m0-2.25l-2.25 1.313M3 7.5l2.25-1.313M3 7.5l2.25 1.313M3 7.5v2.25m9 3l2.25-1.313M12 12.75l-2.25-1.313M12 12.75V15m0 6.75l2.25-1.313M12 21.75V19.5m0 2.25l-2.25-1.313m0-16.875L12 2.25l2.25 1.313M21 14.25v2.25l-2.25 1.313m-13.5 0L3 16.5v-2.25" />
55+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M6.75 7.5l3 2.25-3 2.25m4.5 0h3m-9 8.25h13.5A2.25 2.25 0 0021 18V6a2.25 2.25 0 00-2.25-2.25H5.25A2.25 2.25 0 003 6v12a2.25 2.25 0 002.25 2.25z" />
4156
</svg>
4257
),
4358
items: [
4459
{ label: 'Available Images', to: '/docs/images' },
60+
{ label: 'Architecture', to: '/docs/architecture' },
4561
],
4662
},
4763
{
48-
title: 'nihil-history',
64+
title: 'Nihil History',
4965
icon: (
5066
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
5167
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
@@ -56,39 +72,16 @@ const sections: SidebarSection[] = [
5672
],
5773
},
5874
{
59-
title: 'Features',
60-
icon: (
61-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
62-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9.813 15.904L9 18.75l-.813-2.846a4.5 4.5 0 00-3.09-3.09L2.25 12l2.846-.813a4.5 4.5 0 003.09-3.09L9 5.25l.813 2.846a4.5 4.5 0 003.09 3.09L15.75 12l-2.846.813a4.5 4.5 0 00-3.09 3.09z" />
63-
</svg>
64-
),
65-
items: [
66-
{ label: 'Shell Completion', to: '/docs/completion' },
67-
{ label: 'Command History', to: '/docs/history' },
68-
],
69-
},
70-
{
71-
title: 'Project',
75+
title: 'About',
7276
icon: (
7377
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
7478
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
7579
</svg>
7680
),
7781
items: [
78-
{ label: 'Architecture', to: '/docs/architecture' },
79-
{ label: 'Contributing', to: '/docs/contributing' },
80-
],
81-
},
82-
{
83-
title: 'More',
84-
icon: (
85-
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
86-
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
87-
</svg>
88-
),
89-
items: [
82+
{ label: 'About Nihil', to: '/docs/about' },
9083
{ label: 'FAQ', to: '/docs/faq' },
91-
{ label: 'About', to: '/docs/about' },
84+
{ label: 'Contributing', to: '/docs/contributing' },
9285
],
9386
},
9487
];
@@ -154,6 +147,118 @@ export const DocsLayout: React.FC<{ children: React.ReactNode }> = ({ children }
154147
const isInSection = (section: SidebarSection) =>
155148
section.items.some((item) => location.pathname === item.to);
156149

150+
useEffect(() => {
151+
const candidates = ['bash', 'powershell', 'json', 'yaml', 'python', 'xml', 'javascript', 'typescript', 'ini', 'dockerfile'];
152+
const commandLineRe = /^\s*(\$|#)?\s*[a-zA-Z0-9_.-]+(?:\s+[-][-\w]+|\s+[A-Za-z0-9_./~:@'"=+-]+)*\s*$/;
153+
const terminalPrefixes = ['nihil ', 'nhi ', 'nxh ', 'docker ', 'git ', 'pip ', 'pipx ', 'python ', 'npm ', 'pnpm ', 'yarn ', 'curl ', 'wget ', 'sudo '];
154+
const looksLikeTerminal = (text: string) => {
155+
const lines = text
156+
.split('\n')
157+
.map((line) => line.trim())
158+
.filter(Boolean);
159+
if (!lines.length) return false;
160+
const matches = lines.filter((line) => {
161+
const lower = line.toLowerCase();
162+
if (lower.startsWith('#')) return true;
163+
if (terminalPrefixes.some((prefix) => lower.startsWith(prefix))) return true;
164+
return commandLineRe.test(line);
165+
}).length;
166+
return matches / lines.length >= 0.6;
167+
};
168+
const escapeHtml = (value: string) =>
169+
value
170+
.replaceAll('&', '&amp;')
171+
.replaceAll('<', '&lt;')
172+
.replaceAll('>', '&gt;');
173+
const renderTerminalHtml = (text: string) => {
174+
const lines = text.split('\n');
175+
return lines
176+
.map((rawLine) => {
177+
const line = rawLine.trim();
178+
if (!line) return '<span class="cmd-line"></span>';
179+
if (line.startsWith('#')) {
180+
return `<span class="cmd-comment">${escapeHtml(rawLine)}</span>`;
181+
}
182+
const tokens = line.split(/\s+/);
183+
let prompt = '';
184+
let command = tokens[0] ?? '';
185+
let rest = tokens.slice(1);
186+
if ((command === '$' || command === '#') && tokens.length > 1) {
187+
prompt = command;
188+
command = tokens[1];
189+
rest = tokens.slice(2);
190+
}
191+
const promptPart = prompt
192+
? `<span class="cmd-prompt">${escapeHtml(prompt)}</span> `
193+
: '';
194+
const commandPart = `<span class="cmd-bin">${escapeHtml(command)}</span>`;
195+
const argsPart = rest.length
196+
? ` <span class="cmd-args">${escapeHtml(rest.join(' '))}</span>`
197+
: '';
198+
return `<span class="cmd-line">${promptPart}${commandPart}${argsPart}</span>`;
199+
})
200+
.join('');
201+
};
202+
203+
const blocks = document.querySelectorAll('.docs-content pre');
204+
blocks.forEach((pre) => {
205+
if (pre.dataset.enhanced === '1') return;
206+
pre.dataset.enhanced = '1';
207+
208+
let code = pre.querySelector('code');
209+
if (!code) {
210+
code = document.createElement('code');
211+
code.textContent = pre.textContent ?? '';
212+
pre.textContent = '';
213+
pre.appendChild(code);
214+
}
215+
216+
const source = code.textContent ?? '';
217+
const isTerminal = looksLikeTerminal(source);
218+
const highlighted = isTerminal
219+
? { value: renderTerminalHtml(source) }
220+
: hljs.highlightAuto(source, candidates);
221+
const language = isTerminal ? 'shell' : (highlighted.language ?? 'text');
222+
code.innerHTML = highlighted.value;
223+
code.classList.add('hljs', `language-${language}`);
224+
225+
const wrapper = document.createElement('div');
226+
wrapper.className = 'code-shell';
227+
pre.parentNode?.insertBefore(wrapper, pre);
228+
wrapper.appendChild(pre);
229+
230+
const header = document.createElement('div');
231+
header.className = 'code-shell-head';
232+
233+
const label = document.createElement('span');
234+
label.className = 'code-shell-lang';
235+
label.textContent = language;
236+
237+
const button = document.createElement('button');
238+
button.type = 'button';
239+
button.className = 'code-shell-copy';
240+
button.textContent = 'Copy';
241+
button.onclick = async () => {
242+
try {
243+
await navigator.clipboard.writeText(source);
244+
button.textContent = 'Copied';
245+
window.setTimeout(() => {
246+
button.textContent = 'Copy';
247+
}, 1200);
248+
} catch {
249+
button.textContent = 'Error';
250+
window.setTimeout(() => {
251+
button.textContent = 'Copy';
252+
}, 1200);
253+
}
254+
};
255+
256+
header.append(label, button);
257+
wrapper.prepend(header);
258+
pre.classList.add('code-shell-pre');
259+
});
260+
}, [location.pathname]);
261+
157262
return (
158263
<div className="flex gap-12 items-start">
159264
<aside className="w-60 shrink-0 sticky top-24 hidden md:block">
@@ -170,7 +275,7 @@ export const DocsLayout: React.FC<{ children: React.ReactNode }> = ({ children }
170275
))}
171276
</div>
172277
</aside>
173-
<div className="flex-1 min-w-0">{children}</div>
278+
<div className="flex-1 min-w-0 docs-content">{children}</div>
174279
</div>
175280
);
176281
};

src/components/SeoMeta.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const ROUTE_META: Array<{ match: (p: string) => boolean; meta: MetaConfig }> = [
1515
{ match: (p) => p === '/docs/nihil-history', meta: { title: 'TheNullPigeons - nihil-history', description: 'Track credentials, hosts, and access links across engagements.' } },
1616
{ match: (p) => p === '/docs/architecture', meta: { title: 'TheNullPigeons - Architecture', description: 'Understand how nihil CLI, Docker manager, and image pipeline fit together.' } },
1717
{ match: (p) => p === '/docs/configuration', meta: { title: 'TheNullPigeons - Configuration', description: 'Configure nihil paths, resources, env variables, and command history.' } },
18+
{ match: (p) => p === '/docs/service', meta: { title: 'TheNullPigeons - Services', description: 'Understand session services in nihil, including Desktop Browser UI and port behavior.' } },
1819
{ match: (p) => p === '/docs/contributing', meta: { title: 'TheNullPigeons - Contributing', description: 'Report bugs, request tools, and contribute to the nihil ecosystem.' } },
1920
{ match: (p) => p === '/docs/faq', meta: { title: 'TheNullPigeons - FAQ', description: 'Answers to frequent questions about nihil setup and usage.' } },
2021
{ match: (p) => p === '/docs/about', meta: { title: 'TheNullPigeons - About', description: 'Why nihil was built and how the project is structured.' } },

src/pages/App.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { NihilHistoryPage } from './docs/NihilHistoryPage';
2020
import { ArchitecturePage } from './docs/ArchitecturePage';
2121
import { ConfigurationPage } from './docs/ConfigurationPage';
2222
import { ContributingPage } from './docs/ContributingPage';
23+
import { ServicePage } from './docs/ServicePage';
2324
import { BlogPage } from './BlogPage';
2425
import { BlogPostPage } from './BlogPostPage';
2526
import { SourceCodePage } from './SourceCodePage';
@@ -83,6 +84,10 @@ export const App: React.FC = () => {
8384
path="/docs/configuration"
8485
element={<DocsLayout><ConfigurationPage /></DocsLayout>}
8586
/>
87+
<Route
88+
path="/docs/service"
89+
element={<DocsLayout><ServicePage /></DocsLayout>}
90+
/>
8691
<Route
8792
path="/docs/contributing"
8893
element={<DocsLayout><ContributingPage /></DocsLayout>}

0 commit comments

Comments
 (0)