Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
45 changes: 35 additions & 10 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ See [DESIGN.md](DESIGN.md) for the high-level design and [ARCHITECTURE.md](ARCHI
| `ShellDocs.Core` | Navigation graph, search index model, routing helpers |
| `ShellDocs.Markdown` | Markdig pipeline + frontmatter + Razor component embedding |
| `ShellDocs.Components` | RCL — UI primitives (DocsLayout, CodeBlock, SearchDialog, etc.) |
| `ShellDocs.Tokens` | Shared CSS variable definitions — the palette + spacing scale that both ShellDocs and ShellUI-in-docs consume. Single source of truth for `--background`, `--foreground`, `--primary`, `--radius`, etc. |
| `ShellDocs.CLI` | `shelldocs init`, `shelldocs new`, `shelldocs dev`, `shelldocs build` |
| `ShellDocs.Templates` | Content for CLI scaffolding — starter markdown, meta.json, .csproj patches |
| `ShellDocs.Xml` | v2 — extract `<TypeTable>` from XML doc comments |
Expand All @@ -38,17 +39,17 @@ Each branch below annotates which package it touches. Multi-package branches cal

Goal: a consumer can `shelldocs init` an empty Blazor WASM project and get a working docs site with sidebar, header, code blocks, and one theme.

### `chore/repo-scaffolding`
### `chore/repo-scaffolding` — shipped
Bootstrap the monorepo.

- `.sln` with the six package projects (`ShellDocs.Core`, `ShellDocs.Markdown`, `ShellDocs.Components`, `ShellDocs.CLI`, `ShellDocs.Templates`, plus `ShellDocs.Tests`)
- `.slnx` (.NET 10 XML solution format) with all package projects + tests
- `.csproj` files with correct `TargetFramework`, `IsPackable`, `PackageId`
- `.gitignore`, `Directory.Build.props`, `Directory.Packages.props` for central package management
- `.github/workflows/ci.yml` — build + test on every push
- `.github/workflows/release.yml` — pack + push to NuGet on tag
- Nothing shipped; groundwork only

### `feat/core-navigation-graph`
### `feat/core-navigation-graph` — shipped
Ships to `ShellDocs.Core`.

- `NavigationGraph` — tree of `NavigationNode { Url, Title, Description, Category, Order, Headings, Path }`
Expand All @@ -57,7 +58,7 @@ Ships to `ShellDocs.Core`.
- Route resolution — `NavigationGraph.ResolveByUrl("/docs/button")` → node
- Unit tests: markdown → tree, meta.json ordering, deep-nested folders

### `feat/markdown-pipeline`
### `feat/markdown-pipeline` — shipped
Ships to `ShellDocs.Markdown`.

- Markdig extension for YAML frontmatter (YamlDotNet)
Expand All @@ -67,15 +68,31 @@ Ships to `ShellDocs.Markdown`.
- Type registry — `RegisterComponent<T>()` API for consumer to expose their components to inline tags
- Unit tests: frontmatter parsing, fenced block replacement, tag resolution, unknown-tag graceful degradation

### `feat/components-shell`
### `feat/components-shell` — shipped
Ships to `ShellDocs.Components`.

- `DocsLayout` — full-page grid: header + sidebar + main + TOC placeholder + footer
- `DocsHeader` — logo, primary nav, search-button placeholder, theme toggle, GitHub link
- `DocsSidebar` — grouped nav from navigation graph, collapsible sections, active highlighting
- `DocsLayout` — full-page grid: header + sidebar + main + TOC + footer, fumadocs-shaped
- `DocsHeader` — logo, primary nav with hover mega-menu (icon cards), search-button placeholder, theme toggle, GitHub link, hamburger for mobile
- `DocsSidebar` — grouped nav from navigation graph, collapsible sections (fumadocs pattern — closed by default, active-path auto-open), lucide-style icons per section/page, package selector (ShellDocs · Markdown · Core · CLI · Components), footer bar with GitHub + theme toggle
- `MarkdownContent` — renders a doc page from a `.md` path via `MarkdownRenderer`
- One theme preset baked in (`Shadcn`)
- No search, no TOC, no code highlighting yet — those come in later branches
- `TableOfContents` — right-rail nav (h2/h3), scroll-tracked via multi-active headings so the thumb slides smoothly, SVG-mask + coloured thumb pattern lifted from fumadocs' `ClerkTOCItems`
- `PrevNextNav` — bordered cards, arrow icon square, translate-on-hover
- `MobileNavState` service + fixed drawer + backdrop + auto-close on route change
- Prism.js syntax highlighting (via CDN for now — Shiki lands in the next branch)
- Neutral fumadocs-shaped palette (`--background`, `--foreground`, `--primary`, `--muted`, `--accent`, `--border`) — will move to `ShellDocs.Tokens` in the next branch

### `feat/design-tokens` — **next**
Ships to new package `ShellDocs.Tokens`.

Extracts the palette + scale from `ShellDocs.Components/wwwroot/shelldocs-theme.css` into its own package so ShellUI (and any third-party consumer) can depend on the *tokens* without pulling in the whole components RCL.

- New `ShellDocs.Tokens` project — RCL that ships a single `wwwroot/tokens.css` with `:root` + `:root.dark` variable definitions
- `ShellDocs.Components` and `ShellDocs.Preview` update their `App.razor` link to `_content/ShellDocs.Tokens/tokens.css` and remove the inline theme file
- Add a `tokens-full.css` variant for consumers who want the extended set (semantic + chart colors), and a `tokens-base.css` for consumers who only want the core palette
- Document the token contract in `docs/TOKENS.md`: which names are stable, which are internal, and how to override
- **ShellUI integration path (Tailwind install):** ShellUI's Tailwind config reads the same `--primary`, `--background`, `--border` etc. — nothing changes on their side. Consumer just references `tokens.css` and both design systems light up together.
- **ShellUI integration path (NuGet install):** ShellUI's RCL detects `ShellDocs.Tokens` at runtime and skips emitting its own token file. Deferred to Phase 3 — needs a small opt-in flag on `AddShellUI()`.
- Unit tests: token file emits, dark-mode class toggling, no duplicate declarations across bundles

### `feat/codeblock-shiki`
Ships to `ShellDocs.Components`.
Expand Down Expand Up @@ -303,6 +320,14 @@ ShellDocs depends on `ShellUI.Components` for base primitives:

**Locked ShellUI version:** ShellDocs targets `ShellUI.Components >= 0.5.0` (the version that ships `feat/data-selection-suite` — CommandPalette is required). Bumps require a ShellDocs major/minor.

### Install path — Tailwind-first, NuGet later

Mirrors how shadcn interops with fumadocs: shared CSS variables on `:root`, both design systems read them, one visual language.

- **Phase 1–2 (now):** ShellUI ships as a Tailwind consumer. Doc site owners install ShellUI the same way they would in any Blazor app — via `shellui add card` etc. — and the components read the same `--primary`, `--muted`, `--border` tokens that ShellDocs emits. Zero interop work; a `<Card>` written mid-markdown just picks up the ShellDocs palette. `ShellDocs.Tokens` (next branch) formalizes the contract so both packages point at the same source of truth.
- **Phase 3+ (`feat/shellui-nuget-interop`):** teach the ShellUI NuGet RCL to defer to `ShellDocs.Tokens` when it's on the classpath, so shipping both packages doesn't double-emit `:root` blocks. Small change — an `AddShellUI(o => o.UseSharedTokens())` opt-in. Deferred because it's not blocking for real docs sites — Tailwind consumers get 90% of the value today, and the NuGet story only matters for pure-server projects that don't run Tailwind.
- **Not doing:** shipping a "ShellUI-NuGet-only" install story for now. It'd double the QA surface for zero customer wins on day one. Revisit when a real consumer asks.

---

## Effort estimates (rough)
Expand Down
37 changes: 37 additions & 0 deletions examples/ShellDocs.Preview/Components/App.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
@using ShellDocs.Preview.Components
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ShellDocs — the docs framework for .NET</title>
<base href="/" />
<link rel="stylesheet" href="_content/ShellDocs.Components/shelldocs-theme.css" />
<link rel="stylesheet" href="ShellDocs.Preview.styles.css" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism-tomorrow.min.css" media="(prefers-color-scheme: dark)" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/themes/prism.min.css" media="(prefers-color-scheme: light)" />
<script>
(function () {
var q = new URLSearchParams(location.search).get('theme');
var saved = null;
try { saved = localStorage.getItem('shelldocs-theme'); } catch (e) {}
var systemDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var mode = q || saved || (systemDark ? 'dark' : 'light');
if (mode === 'dark') document.documentElement.classList.add('dark');
})();
</script>
<HeadOutlet @rendermode="RenderMode.InteractiveServer" />
</head>
<body>
<Routes @rendermode="RenderMode.InteractiveServer" />
<script src="_content/ShellDocs.Components/shelldocs.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/prism.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-csharp.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-json.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-yaml.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-bash.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-markup-templating.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.29.0/components/prism-typescript.min.js"></script>
<script src="_framework/blazor.web.js"></script>
</body>
</html>
68 changes: 68 additions & 0 deletions examples/ShellDocs.Preview/Components/Demos/Callout.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
@* Demo Callout — a temporary implementation local to the preview app.
The production <Callout> ships in feat/content-primitives (Phase 2). *@

<div class="demo-callout demo-callout-@Variant.ToLowerInvariant()">
<span class="demo-callout-icon" aria-hidden="true">
@switch (Variant.ToLowerInvariant())
{
case "warning":
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>
break;
case "danger":
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>
break;
case "tip":
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4M12 2a7 7 0 0 0-4 12.7c.5.5 1 1.3 1 2.3v1h6v-1c0-1 .5-1.8 1-2.3A7 7 0 0 0 12 2z"/></svg>
break;
default:
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>
break;
}
</span>
<div class="demo-callout-body">
@if (!string.IsNullOrEmpty(Title))
{
<div class="demo-callout-title">@Title</div>
}
<div class="demo-callout-content">
@if (ChildContent is not null) { @ChildContent }
else { @Text }
</div>
</div>
</div>

@code {
[Parameter] public string Variant { get; set; } = "info";
[Parameter] public string? Title { get; set; }
[Parameter] public string? Text { get; set; }
[Parameter] public RenderFragment? ChildContent { get; set; }
}

<style>
.demo-callout {
display: flex;
gap: 0.85rem;
padding: 0.9rem 1.1rem;
border: 1px solid var(--border);
border-radius: calc(var(--radius) + 2px);
background: color-mix(in oklch, var(--card) 70%, var(--background));
line-height: 1.6;
font-size: 0.9rem;
}
.demo-callout-icon { flex-shrink: 0; display: inline-flex; align-items: flex-start; padding-top: 0.05rem; }
.demo-callout-icon svg { width: 1.05rem; height: 1.05rem; }
.demo-callout-body { flex: 1; min-width: 0; }
.demo-callout-title { font-weight: 600; margin-bottom: 0.2rem; letter-spacing: -0.005em; }
.demo-callout-content { color: var(--muted-foreground); }
.demo-callout-content > *:first-child { margin-top: 0; }
.demo-callout-content > *:last-child { margin-bottom: 0; }

.demo-callout-info { border-color: color-mix(in oklch, var(--info) 30%, var(--border)); }
.demo-callout-info .demo-callout-icon { color: var(--info); }
.demo-callout-warning { border-color: color-mix(in oklch, var(--warning) 35%, var(--border)); }
.demo-callout-warning .demo-callout-icon { color: var(--warning); }
.demo-callout-danger { border-color: color-mix(in oklch, var(--error) 35%, var(--border)); }
.demo-callout-danger .demo-callout-icon { color: var(--error); }
.demo-callout-tip { border-color: color-mix(in oklch, var(--success) 30%, var(--border)); }
.demo-callout-tip .demo-callout-icon { color: var(--success); }
</style>
120 changes: 120 additions & 0 deletions examples/ShellDocs.Preview/Components/Pages/Blog.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
@page "/blog"
@layout HomeLayout

<PageTitle>Blog — ShellDocs</PageTitle>

<div class="blog-wrap">
<header class="blog-hero">
<span class="eyebrow">Blog</span>
<h1>Notes from the ShellDocs team</h1>
<p>Release notes, deep-dives, and the occasional postmortem. Written by the same people building the framework.</p>
</header>

<div class="posts">
@foreach (var post in _posts)
{
<a class="post" href="@post.Href">
<div class="post-meta">
<span class="post-tag">@post.Tag</span>
<time>@post.Date</time>
</div>
<h2>@post.Title</h2>
<p>@post.Excerpt</p>
<div class="post-foot">
<div class="post-author">
<span class="post-avatar" style="background:@post.AvatarBg">@post.AuthorInitials</span>
<span>@post.Author</span>
</div>
<span class="post-read">@post.ReadTime</span>
</div>
</a>
}
</div>
</div>

@code {
private record Post(string Title, string Excerpt, string Tag, string Date, string Author, string AuthorInitials, string AvatarBg, string ReadTime, string Href);

private static readonly List<Post> _posts = new()
{
new("Announcing ShellDocs 0.1.0-alpha",
"The docs framework we wanted for .NET. Markdown-first, Razor-native, Cmd+K searchable, deploys anywhere. Here's what shipped and what's next on the roadmap.",
"Release", "Jul 12 · 2026", "Shephard Tseisi", "ST", "oklch(0.65 0.18 260)", "6 min read", "#"),

new("Why we built the TOC three times",
"The story of every fumadocs-inspired scroll spy we shipped, why they broke on Blazor Server, and the 50-line version we ended up with. A lesson in doing less.",
"Engineering", "Jul 9 · 2026", "Shephard Tseisi", "ST", "oklch(0.7 0.18 200)", "9 min read", "#"),

new("Frontmatter, not folders",
"How ShellDocs decides sidebar ordering, breadcrumbs, and prev/next navigation from a single YAML block. Plus: the meta.json escape hatch for when you need it.",
"Design", "Jul 3 · 2026", "Shephard Tseisi", "ST", "oklch(0.72 0.19 320)", "5 min read", "#"),

new("Razor components mid-markdown, live",
"A short walkthrough of how <Button /> mid-markdown becomes a real Blazor component at render time. Includes the parsing rules and why we chose them.",
"Engineering", "Jun 27 · 2026", "Shephard Tseisi", "ST", "oklch(0.7 0.18 200)", "7 min read", "#"),

new("The Blazor Server signalR trap",
"Why round-tripping every scroll event through SignalR is a bad idea, and the pattern we use to keep Blazor out of hot animation loops. Applies to any Blazor Server app.",
"Engineering", "Jun 20 · 2026", "Shephard Tseisi", "ST", "oklch(0.7 0.18 200)", "8 min read", "#"),

new("How shellui.dev is being rebuilt on ShellDocs",
"A dogfood tour: the ShellUI docs site is moving from custom Blazor pages to ShellDocs' primitives. Screenshots, migration notes, and every rough edge we hit.",
"Case study", "Jun 14 · 2026", "Shephard Tseisi", "ST", "oklch(0.68 0.2 30)", "10 min read", "#")
};
}

<style>
.blog-wrap { max-width: 60rem; margin: 0 auto; padding: 5rem 1.5rem 6rem; }

.blog-hero { text-align: center; margin-bottom: 3.5rem; }
.blog-hero .eyebrow {
display: inline-block;
font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--muted-foreground); font-weight: 600;
padding: 0.2rem 0.6rem; border: 1px solid var(--border); border-radius: 9999px;
margin-bottom: 1rem;
}
.blog-hero h1 { font-size: 2.5rem; font-weight: 700; letter-spacing: -0.03em; margin: 0 0 0.75rem; line-height: 1.1; }
.blog-hero p { margin: 0 auto; max-width: 34rem; color: var(--muted-foreground); font-size: 1rem; line-height: 1.6; }
@@media (min-width: 768px) { .blog-hero h1 { font-size: 3rem; } }

.posts { display: flex; flex-direction: column; gap: 1rem; }
.post {
display: flex; flex-direction: column; gap: 0.5rem;
padding: 1.5rem;
border: 1px solid var(--border);
border-radius: calc(var(--radius) + 2px);
background: var(--card);
color: var(--foreground);
text-decoration: none;
transition: border-color 200ms, background 200ms, transform 200ms;
}
.post:hover { border-color: var(--border-strong); background: var(--muted); transform: translateY(-2px); }
.post-meta {
display: flex; align-items: center; gap: 0.75rem;
font-size: 0.75rem; color: var(--muted-foreground);
}
.post-tag {
display: inline-flex; align-items: center;
padding: 0.15rem 0.55rem;
background: var(--muted); border: 1px solid var(--border); border-radius: 9999px;
font-size: 0.7rem; font-weight: 500; color: var(--foreground);
letter-spacing: -0.005em;
}
.post h2 { margin: 0; font-size: 1.25rem; font-weight: 600; letter-spacing: -0.015em; line-height: 1.3; }
.post p { margin: 0; color: var(--muted-foreground); font-size: 0.9rem; line-height: 1.55; }
.post-foot {
display: flex; align-items: center; justify-content: space-between;
margin-top: 0.5rem; padding-top: 0.9rem;
border-top: 1px solid var(--border);
font-size: 0.75rem; color: var(--muted-foreground);
}
.post-author { display: inline-flex; align-items: center; gap: 0.5rem; }
.post-avatar {
display: inline-flex; align-items: center; justify-content: center;
width: 1.4rem; height: 1.4rem;
border-radius: 9999px;
font-size: 0.6rem; font-weight: 700; color: white;
letter-spacing: 0;
}
</style>
Loading
Loading