Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
20c7159
feat(site): CAVE theme + per-page Copy/Open-in-LLM dropdown
lliWcWill Apr 29, 2026
05a3d51
Docs: keystone-emphasis chain diagram, native-vs-fsuite block, drone …
lliWcWill Apr 29, 2026
aa17ce4
Docs: round 3 — monokai terminal blocks, full cheat sheet, MCP-sequen…
lliWcWill Apr 29, 2026
9e492d6
Docs: round 4 — per-command drone profiles + canonical chains + termi…
lliWcWill Apr 29, 2026
c4d7128
Docs: round 5 — architecture trio rebuild + first-contact polish + de…
lliWcWill Apr 29, 2026
36053d5
Docs: round 6 — story-page rebuild (Episodes 1/2/3 + Lightbulb) with …
lliWcWill Apr 29, 2026
a5a25ca
Address Codex P1/P2 review on PR #39
lliWcWill Apr 29, 2026
bc2afd3
docs(internal): add PR #39 handoff for next agent
lliWcWill Apr 29, 2026
e566679
fix(gen-commands): preserve handcrafted preambles above '## Help output'
lliWcWill Apr 29, 2026
42ad05f
docs(handoff): record gen-commands fix + 2 newly-reported visual bugs
lliWcWill Apr 29, 2026
f244d02
fix(theme): complete light-theme Starlight token overrides
lliWcWill Apr 29, 2026
dca597a
docs(handoff): light theme fixed in f244d02 — strike from open-bugs list
lliWcWill Apr 29, 2026
7070184
docs(handoff): refresh after dev-server rebuild on dca597a
lliWcWill Apr 29, 2026
c9c6f5c
feat(theme): force dark + harden gen-commands against custom-page wipes
lliWcWill Apr 29, 2026
9ffd39f
docs(handoff): record force-dark + defensive gen-commands
lliWcWill Apr 29, 2026
fe98a07
docs(handoff): remove duplicate cache-cleared line in dev section
lliWcWill Apr 29, 2026
d9ac5c6
fix(pr39-codex): popup blocker, ghSourcePath for index pages, draft f…
lliWcWill Apr 29, 2026
1325843
Merge branch 'master' into docs/site-revamp
lliWcWill Apr 30, 2026
f81f3e6
fix(a11y,cleanup): strip ARIA menu roles + delete custom_backup.css
lliWcWill Apr 30, 2026
ab7fbc4
fix(css): reconnect dropdown styling after ARIA role removal
lliWcWill Apr 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
293 changes: 293 additions & 0 deletions docs/internal/specs/2026-04-29-pr39-handoff.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export default defineConfig({
// Override the page H1 so we can render the page-actions dropdown
// (Copy / View / Open in LLM) inline with the title.
PageTitle: './src/components/PageTitle.astro',
// Force-dark theme: hide the toggle UI + pin data-theme="dark" on load.
// CAVE theme is dark-only by design — bloom/scanline effects don't
// translate to a light canvas.
ThemeSelect: './src/components/HiddenThemeSelect.astro',
ThemeProvider: './src/components/ForceDarkThemeProvider.astro',
},
social: [
{ icon: 'github', label: 'GitHub', href: 'https://github.com/lliWcWill/fsuite' },
Expand All @@ -26,7 +31,7 @@ export default defineConfig({
'./src/styles/custom.css',
],
expressiveCode: {
themes: ['monokai', 'github-light'],
themes: ['monokai'],
styleOverrides: {
codeFontFamily: '"JetBrainsMono Nerd Font", "Fira Code", ui-monospace, SFMono-Regular, Menlo, Consolas, monospace',
},
Expand Down
63 changes: 45 additions & 18 deletions site/scripts/gen-commands.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
*/

import { execSync } from 'node:child_process';
import { mkdirSync, writeFileSync, existsSync } from 'node:fs';
import { mkdirSync, writeFileSync, existsSync, readFileSync } from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';

Expand Down Expand Up @@ -65,24 +65,17 @@ function captureHelp(toolName) {

/**
* Build a markdown page for one tool.
*
* Preserves any handcrafted preamble above the `## Help output` heading
* (e.g. the round-4 drone-profile cards + canonical chains + monokai
* terminal samples). The auto-generated `## Help output` section + `## See
* also` are regenerated on every build so the help text stays in sync with
* the binary; everything above is treated as durable hand-edited content.
*/
function buildPage(tool) {
function buildPage(tool, outPath) {
const helpText = captureHelp(tool.name);
const frontmatter = [
'---',
`title: ${tool.emoji} ${tool.title}`,
`description: ${tool.tagline}`,
`sidebar:`,
` order: ${tool.order}`,
'---',
].join('\n');

const body = `
## ${tool.tagline}

\`${tool.name}\` is part of the fsuite toolkit — a set of fourteen CLI tools built for AI coding agents.

## Help output
const helpSection = `## Help output

The content below is the **live** \`--help\` output of \`${tool.name}\`, captured at build time from the tool binary itself. It cannot drift from the source — regenerating the docs regenerates this section.

Expand All @@ -97,7 +90,41 @@ ${helpText}
- [View source on GitHub](https://github.com/lliWcWill/fsuite/blob/master/${tool.name})
`;

return frontmatter + '\n' + body;
// If a page already exists, preserve everything above `## Help output`
// (frontmatter + tagline H2 + intro paragraph + any round-4 preamble) and
// splice the regenerated help + see-also onto the bottom.
//
// Defensive case: if the file exists but the marker is missing, the page
// has been hand-customized (e.g. `fwrite.md` uses `## Usage notes` since
// its surface is MCP-only). Leave it untouched rather than nuking the
// custom content with the default first-time-generation template.
if (existsSync(outPath)) {
const existing = readFileSync(outPath, 'utf8');
const helpIdx = existing.indexOf('## Help output');
if (helpIdx > 0) {
return existing.slice(0, helpIdx) + helpSection;
}
return existing;
}

// First-time generation: build the default frontmatter + tagline H2 + intro
const frontmatter = [
'---',
`title: ${tool.emoji} ${tool.title}`,
`description: ${tool.tagline}`,
`sidebar:`,
` order: ${tool.order}`,
'---',
].join('\n');

const intro = `
## ${tool.tagline}

\`${tool.name}\` is part of the fsuite toolkit — a set of fourteen CLI tools built for AI coding agents.

`;

return frontmatter + intro + helpSection;
}

/**
Expand All @@ -110,8 +137,8 @@ function main() {

let written = 0;
for (const tool of TOOLS) {
const page = buildPage(tool);
const outPath = join(OUT_DIR, `${tool.name}.md`);
const page = buildPage(tool, outPath);
writeFileSync(outPath, page, 'utf8');
written++;
console.log(` ✓ ${tool.name.padEnd(10)} → src/content/docs/commands/${tool.name}.md`);
Expand Down
23 changes: 23 additions & 0 deletions site/src/components/ForceDarkThemeProvider.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
/**
* ForceDarkThemeProvider.astro — Starlight `ThemeProvider` override that pins
* the site to dark mode permanently.
*
* Replaces Starlight's default theme-detection script (which respects
* `localStorage.starlight-theme` + `prefers-color-scheme`). Since the toggle
* UI is also hidden via `HiddenThemeSelect.astro`, no code path can flip the
* site to light. The `[data-theme='light']` block in custom.css remains as
* dormant code in case we ever re-enable the toggle.
*
* Runs `is:inline` so it executes before any visible paint — no theme flash.
*/
---
<script is:inline>
// Force dark + overwrite any prior preference.
document.documentElement.dataset.theme = 'dark';
try {
window.localStorage.setItem('starlight-theme', 'dark');
} catch (_) {
/* localStorage not available (private mode, etc.) — dataset is enough. */
}
</script>
11 changes: 11 additions & 0 deletions site/src/components/HiddenThemeSelect.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
/**
* HiddenThemeSelect.astro — Starlight `ThemeSelect` override that renders nothing.
*
* The fsuite docs site is dark-only by design (CAVE theme: bioluminescent green
* accents on void-black surfaces; the bloom + scanline effects only read on a
* dark canvas). The toggle is therefore disabled — paired with
* `ForceDarkThemeProvider.astro` which pins `data-theme="dark"` regardless of
* any prior user preference.
*/
---
110 changes: 69 additions & 41 deletions site/src/components/PageActions.astro
Original file line number Diff line number Diff line change
Expand Up @@ -18,34 +18,40 @@
* · View source on GitHub
*
* Accessibility:
* - Real <button> elements + ARIA roles (menu, menuitem)
* - Real <button> elements with aria-expanded for the toggle state
* - Keyboard: Enter/Space toggles, Escape closes, click-outside closes
*/

const route = Astro.locals.starlightRoute;
const entry = route?.entry;
const entryId = entry?.id ?? '';
const slug = entryId.replace(/\.(md|mdx)$/i, '');
const slug = entry?.id?.replace(/\.(md|mdx)$/i, '') ?? '';
const title = entry?.data?.title ?? '';
const basePath = import.meta.env.BASE_URL;

// Build URLs using the runtime origin so it works on lliwcwill.github.io,
// localhost:4321, or any preview deployment.
// Keep the path relative to Astro's configured base (for GitHub Pages /fsuite).
const mdRelative = slug ? `${slug}.md` : 'index.md';
// Build URLs using Astro's BASE_URL (e.g., "/fsuite/") so they survive on
// GitHub Pages-style deployments under a subpath. We emit the full path-with-base
// so the script just needs to prepend window.location.origin.
const baseUrl = (import.meta.env.BASE_URL ?? '/').replace(/\/$/, ''); // "/fsuite" or ""
const mdRelative = slug ? `${baseUrl}/${slug}.md` : '';

// GitHub source URL — assumes master branch + the canonical path layout.
// Preserve the original content extension so .md pages link to .md sources.
// GitHub source URL — preserve the actual file extension since the docs tree
// mixes .md and .mdx and a hardcoded .mdx 404s for .md files. Astro's
// entry.filePath keeps the extension; entry.id has it stripped by Starlight.
const ghBase = 'https://github.com/lliWcWill/fsuite/blob/master/site/src/content/docs';
const ghSourcePath = entryId || 'index.mdx';
const filePath = (entry as any)?.filePath as string | undefined;
// Prefer entry.filePath — it preserves the actual on-disk path with extension
// and handles section index pages (e.g. architecture/index.mdx) that Starlight
// collapses to bare "architecture" in entry.id.
const filePathRel = filePath?.replace(/^.*[\/\\]content[\/\\]docs[\/\\]/, '');
const fileExtMatch = filePath?.match(/\.(md|mdx)$/i);
const fileExt = fileExtMatch ? fileExtMatch[1].toLowerCase() : 'mdx';
const ghSourcePath = filePathRel ?? (slug ? `${slug}.${fileExt}` : `index.${fileExt}`);
const ghSourceUrl = `${ghBase}/${ghSourcePath}`;
---

<div
class="page-actions"
data-page-actions
data-md-relative={mdRelative}
data-base-path={basePath}
data-page-title={title}
data-gh-source={ghSourceUrl}
>
Expand Down Expand Up @@ -150,7 +156,6 @@ const ghSourceUrl = `${ghBase}/${ghSourcePath}`;

function init(root: HTMLElement) {
const mdRelative = root.dataset.mdRelative ?? '';
const basePath = root.dataset.basePath ?? '/';
const pageTitle = root.dataset.pageTitle ?? 'this page';
const menu = root.querySelector<HTMLElement>('.page-actions__menu')!;
const caret = root.querySelector<HTMLButtonElement>('[data-action="toggle-menu"]')!;
Expand Down Expand Up @@ -182,11 +187,13 @@ const ghSourceUrl = `${ghBase}/${ghSourcePath}`;
};

const buildMdUrl = (): string => {
// mdRelative already includes Astro's BASE_URL (e.g., "/fsuite/foo.md"),
// so we only need to prepend the runtime origin. Avoids the new-URL()
// base-path-stripping pitfall when mdRelative starts with "/".
if (mdRelative) {
const normalizedBase = basePath.endsWith('/') ? basePath : `${basePath}/`;
return new URL(mdRelative.replace(/^\/+/, ''), new URL(normalizedBase, window.location.origin)).toString();
return `${window.location.origin}${mdRelative}`;
}
// Fallback: derive from current pathname.
// Fallback: derive from current pathname (already includes base).
const path = window.location.pathname.replace(/\/$/, '');
return `${window.location.origin}${path}.md`;
};
Expand Down Expand Up @@ -263,34 +270,55 @@ const ghSourceUrl = `${ghBase}/${ghSourcePath}`;
return;
}

case 'open-claude': {
try {
const md = await fetchMarkdown();
const q = encodeURIComponent(promptForLLM(md));
window.open(`https://claude.ai/new?q=${q}`, '_blank', 'noopener,noreferrer');
} catch {
// Fallback: just hand Claude the URL.
const q = encodeURIComponent(`Please read ${buildMdUrl()} and help me understand it.`);
window.open(`https://claude.ai/new?q=${q}`, '_blank', 'noopener,noreferrer');
} finally {
closeMenu();
case 'open-claude': {
// Open the popup synchronously inside the user gesture — browsers veto
// window.open() that runs after an await, so we open about:blank first
// and navigate after fetchMarkdown resolves. `noopener` is dropped because
// we need the popup ref to set location.href; opener is severed manually
// below as the security mitigation.
const popup = window.open('about:blank', '_blank');
if (!popup) {
showToast('Popup blocked — allow popups for this site', false);
closeMenu();
return;
}
try {
const md = await fetchMarkdown();
const q = encodeURIComponent(promptForLLM(md));
popup.opener = null;
popup.location.href = `https://claude.ai/new?q=${q}`;
} catch {
// Fallback: just hand Claude the URL.
const q = encodeURIComponent(`Please read ${buildMdUrl()} and help me understand it.`);
popup.opener = null;
popup.location.href = `https://claude.ai/new?q=${q}`;
} finally {
closeMenu();
}
return;
}
return;
}

case 'open-chatgpt': {
try {
const md = await fetchMarkdown();
const q = encodeURIComponent(promptForLLM(md));
window.open(`https://chatgpt.com/?hints=search&q=${q}`, '_blank', 'noopener,noreferrer');
} catch {
const q = encodeURIComponent(`Please read ${buildMdUrl()} and help me understand it.`);
window.open(`https://chatgpt.com/?hints=search&q=${q}`, '_blank', 'noopener,noreferrer');
} finally {
closeMenu();
case 'open-chatgpt': {
const popup = window.open('about:blank', '_blank');
if (!popup) {
showToast('Popup blocked — allow popups for this site', false);
closeMenu();
return;
}
try {
const md = await fetchMarkdown();
const q = encodeURIComponent(promptForLLM(md));
popup.opener = null;
popup.location.href = `https://chatgpt.com/?hints=search&q=${q}`;
} catch {
const q = encodeURIComponent(`Please read ${buildMdUrl()} and help me understand it.`);
popup.opener = null;
popup.location.href = `https://chatgpt.com/?hints=search&q=${q}`;
} finally {
closeMenu();
}
return;
}
return;
}
}
};

Expand Down
Loading