Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 22 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,26 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht

## [Unreleased]

## [0.1.13-alpha] — 2026-10-10

Code variants for cross-platform libraries: one preview with code tabs per platform (MAUI and Avalonia XAML, say), a page switch that picks the platform site-wide, captions, and live iframe previews. Also fixes one-line component bodies being wrapped in `<p>`.

### Added

- **Code variants on previews.** `<DemoPreview>`, `<ComponentPreview>` and `<IframePreview>` take `<CodeTab Label="…">` children with a code fence inside; the Code panel shows them as tabs instead of the Razor source, for libraries documented in several languages (MAUI and Avalonia XAML, say). The copy button copies the tab that's showing. `SyncKey` works as on `<CodeGroup>`: blocks with the same key switch together across pages and the choice is saved. A block without a tab for the current choice shows "Not available on {label} yet" and keeps its height. From `.razor`, pass `CodeTabs` (`PreviewCodeTab` records). Without code tabs nothing changes. `razor:preview` fences can't take code tabs: their body is Razor, so a fence can't nest in one.
- **Page switch.** `o.AddSyncSwitch("platform", "MAUI", "Avalonia")` adds underlined tabs, like shadcn/ui's library switch, to every page whose blocks use that key: under the page header, or right above the first such block. `<SyncSwitch SyncKey="…" [Options="a, b"] />` places one by hand.
- **Preview caption.** `Caption="…"` puts a short muted note in the preview toolbar; `ShellDocsOptions.PreviewCaption` sets a site-wide default.
- **`<IframePreview Src Height Lazy SyncKey Caption Title Id>`** shows another page (a live Avalonia app compiled to WebAssembly, for instance) in the Preview panel with the same toolbar and code tabs. `shelldocs.js` loads it when scrolled into view (`Lazy="visible"`, the default), on a **Run live preview** button (`click`) or at once (`none`), appends `?theme=light|dark`, and posts `{ type: "shelldocs-theme", theme }` to the page's origin when the theme changes. The Code tab is hidden when there's no code.
- XAML highlighting: `xaml` and `axaml` fences are highlighted as XML (`cs` as C#). The `init` template loads Shiki's `xml` grammar; existing sites add `'xml'` to the `langs` list in `App.razor`.

### Changed

- `SlotRenderer` no longer warns about child content for components that read their raw body (`ChildContentSource`).

### Fixed

- **One-line component bodies in prose were wrapped in `<p>`.** `<Badge>v0.3.2 stable</Badge>` rendered a paragraph inside the badge, so the page's paragraph margin and colour overrode it. As in MDX, a body on the same line as its tags now renders inline; a body on its own lines still becomes paragraphs.

## [0.1.12-alpha] — 2026-10-04

Parser fixes found while rewriting the ShellDocs docs site on 0.1.11: fences and component tags are now read the way CommonMark and Razor read them, so a page that shows its own markdown source no longer breaks what follows it.
Expand Down Expand Up @@ -380,7 +400,8 @@ Published to NuGet:
- `<TypeTable>` is hand-authored today; XML-doc auto-generation ships in `ShellDocs.Xml` (Phase 4)
- No `<DocsBreadcrumb>` opt-out — currently hides when the trail has ≤ 1 node, otherwise always renders

[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.12-alpha...HEAD
[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.13-alpha...HEAD
[0.1.13-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.13-alpha
[0.1.12-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.12-alpha
[0.1.11-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.11-alpha
[0.1.10-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.10-alpha
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

<!-- Package metadata (applies to any project with IsPackable=true) -->
<PropertyGroup>
<Version>0.1.12-alpha</Version>
<Version>0.1.13-alpha</Version>
<Authors>ShellUI</Authors>
<Company>ShellUI</Company>
<Copyright>Copyright © 2026 ShellUI</Copyright>
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ o.AddRedirect("/docs/v0.3.0", "/docs/v0.3"); // also /docs/v0.3.0/x → /docs/
- **Child content is Razor.** Component bodies inside a fence are parsed like the fence itself, so `<Navbar><div>…<ThemeToggle /></div></Navbar>` keeps its structure and nothing is wrapped in `<p>`.
- **Page typography stays out.** Preview frames are `not-prose`, so prose margins, list padding and link underlines don't reach your components.
- **Generic components work.** Set the type argument as Razor does: `<BarChart TItem="SalesRow" />`, `<DataTable TItem="int" />`. Libraries' generic components are registered under their bare name.
- **Code variants.** Give `<DemoPreview>`, `<ComponentPreview>` or `<IframePreview>` `<CodeTab Label="MAUI">` children with a fence inside, and the Code panel shows those tabs instead of the Razor (for a library documented in MAUI and Avalonia XAML, say). `SyncKey="platform"` switches every block together and remembers the choice; `o.AddSyncSwitch("platform", "MAUI", "Avalonia")` adds a shadcn-style tab row to those pages; a block missing the chosen tab says "Not available on Avalonia yet". `Caption="Rendered with Blazor"` adds a note to the toolbar.
- **Live iframe previews.** `<IframePreview Src="/preview/?c=button">` shows another page, such as Avalonia on WebAssembly, loaded lazily with `?theme=` and theme-change messages.
- **Centred, stretched or scrolling.** Examples are centred; `razor:preview stretch` (or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>`) lets charts, inputs and tables fill the frame, and `scroll` lets an example wider than a phone screen (pagination, toolbars, OTP inputs) scroll inside the frame. A scrolling frame clips popovers, so leave it off examples with dropdowns.
- **Razor-shaped attribute values work:** `Variant="ButtonVariant.Destructive"`, `@ButtonVariant.Destructive`, `@true`, `@42`, `[Flags]` values as `Bold | Italic`.
- **Attributes a static preview can't evaluate are skipped, not fatal:** `OnClick="HandleClick"`, `@onclick`, `@bind-*`, `@ref`, non-primitive parameter types, unparseable values. Each logs a warning and the component still renders.
Expand Down
9 changes: 6 additions & 3 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,14 @@ Placeholders plus a slot list keep everything at render time: no generated Razor

**Generic components** register under their bare name (`TypeRegistry.TagNameOf`) as open definitions. `SlotRenderer.Component` closes one per use with `GenericComponents.Close`, reading attributes named after the type parameters (`TItem="int"`, resolved from C# spellings across loaded assemblies) or falling back to `object`; one it can't close renders a `.shelldocs-render-error` element instead. A non-generic component with the same name keeps the tag.

Component child markup is rendered recursively. Inline tags in prose take markdown bodies (`SlotRenderer.FromMarkup`, dedented first, since Markdig treats 4-space indentation as a code block). Inside a `razor:preview` and in `<ComponentPreview>` the body is Razor: `SlotRenderer.FromRazor` parses it with `PreviewParser` and emits real elements, so wrappers around nested components stay intact. Child tags named after a `RenderFragment` parameter (`<Icon>` → `Alert.Icon`) are routed into that slot.
Component child markup is rendered recursively. Inline tags in prose take markdown bodies (`SlotRenderer.FromMarkup`, dedented first, since Markdig treats 4-space indentation as a code block; as in MDX, a body on the tags' own line renders inline, without a wrapping `<p>`). Inside a `razor:preview` and in `<ComponentPreview>` the body is Razor: `SlotRenderer.FromRazor` parses it with `PreviewParser` and emits real elements, so wrappers around nested components stay intact. Child tags named after a `RenderFragment` parameter (`<Icon>` → `Alert.Icon`) are routed into that slot.

**Parameter coercion.** Attribute values are strings. `BuildParameters` matches them to `[Parameter]` properties case-insensitively and coerces string, bool, char, numeric primitives and enums, accepting Razor forms: a leading `@`, `@( … )`, `Type.Member` enum values, `A | B` flags, numeric suffixes, `@null`. It never throws. Anything static markup can't set is skipped with a logged warning: directive attributes, delegate/`EventCallback` parameters, unsupported types, unparseable values, unknown attributes on components without a `CaptureUnmatchedValues` catch-all, and child content on components without a plain `RenderFragment ChildContent`.

**`PreviewFrame`** is the frame for every example. It carries `not-prose` (every `.shelldocs-prose` rule skips `.not-prose` subtrees) and a `Layout` of `center`, `stretch` or `scroll` (the example scrolls horizontally inside the frame; opt-in because a scroll box clips popovers). Centring uses `safe center`, so an example wider than the frame starts at its left edge. It has a toolbar with Preview | Code tabs, copy and a ⋯ menu (*Open in new tab* → the example's anchor; *Report a bug* / *Suggest something* → new-issue links built by `PreviewLinks` from `IssueTrackerUrl` or `GitHubRepo`). `ComponentPreview` and `DemoPreview` feed it a ready-made `Content` fragment and `Code` string instead of a `PreviewSlot`. `DemoPreview` reads its source from the first `{DemoSourceRoot}/**/X.razor` (shallowest path wins, cached, re-read when the file changes).

**Code variants.** `PreviewFrame.CodeTabs` (`PreviewCodeTab(Label, Code, Language)`) replaces the single source with a `[data-tabs data-tabs-missing]` group: a tab bar, one panel per tab stacked in one grid cell (so the height never changes) and a hidden "Not available on {label} yet" note. In markdown, `DemoPreview`, `ComponentPreview` and `IframePreview` get their raw body as `ChildContentSource` and `CodeTabParser` reads `<CodeTab>` blocks out of it with `RazorTagScanner`, taking each tab's code and language from its first fence (`SlotExtractor.FenceBlock`); `ComponentPreview` passes the rest on as the target's children. `Caption` (or `ShellDocsOptions.PreviewCaption`) is a muted note in the toolbar. `IframePreview` renders `[data-iframe-preview data-src data-lazy]` with an empty iframe that `shelldocs.js` fills. `MarkdownContent` adds a `<SyncSwitch>` (a panel-less `[data-tabs data-tabs-switch]` group) for each `AddSyncSwitch` key the page's blocks use, under the page header or above the first such block.

**`DocsPageState`** (scoped) is fed by `MarkdownContent` and recomputes current node, prev/next and breadcrumbs on navigation, so the layout chrome needs no per-page wiring.

---
Expand Down Expand Up @@ -204,7 +206,8 @@ Chrome that must work without a Blazor runtime follows one contract:
| Sidebar sections | `.sidebar-section[data-open]` | toggle on click |
| Package / version selectors, preview ⋯ menu | `.pkg` / `.ver` / `.preview-menu` `[data-open]` | open on trigger; close on outside click, Escape or option |
| Preview tabs | `.preview-frame[data-preview-tab]` | Preview / Code; arrow keys; copy reads the source |
| Tabs, CodeGroup | `[data-tabs]`, `[data-tab-target]`, `[data-tab-panel]` | switch on click / arrows; `[data-tabs-sync]` groups switch together and persist in `localStorage` |
| Tabs, CodeGroup, preview code tabs, page switch | `[data-tabs]`, `[data-tab-target]`, `[data-tab-panel]` | switch on click / arrows; `[data-tabs-sync]` groups switch together and persist in `localStorage`; a `[data-tabs-missing]` group without the chosen value shows its `[data-tab-missing]` note; with nothing saved yet, groups follow a `[data-tabs-switch]` for their key |
| Iframe previews | `[data-iframe-preview][data-src][data-lazy]` | sets the iframe's `src` (plus `?theme=`) when visible (IntersectionObserver), on `[data-iframe-run]` or at once; posts `{ type: "shelldocs-theme", theme }` to loaded frames on theme change |
| Mobile nav | page shell `[data-mobile-open]` | hamburger toggles; backdrop, Escape, link or navigation closes; page scroll locked on `<html>` |
| TOC | `[data-toc-list][data-toc-ids]` | IntersectionObserver scroll-spy, re-attached on `enhancedload` |
| Theme | `<html class="dark">`, `[data-theme-toggle]` | inline head script applies the saved / system theme; the toggle flips the class and CSS picks its icon; a `MutationObserver` saves any change (including another library's toggle) and pushes it to `ThemeState`; re-applied after enhanced navigation |
Expand All @@ -213,7 +216,7 @@ Still Blazor-only: search, the desktop sidebar-collapse button.

`enhancedload` is a Blazor event (`Blazor.addEventListener`), not a DOM event; `shelldocsOnEnhancedLoad(fn)` attaches once `blazor.web.js` has run.

**Syntax highlighting.** Shiki (loaded by the app as `window.__shiki`) never replaces Blazor-owned nodes: the source `<pre>` gets `data-shiki="source"` (hidden by CSS) and the highlighted output goes into a JS-owned `[data-shiki-output]` sibling, re-rendered when the source text changes and removed when its source is gone.
**Syntax highlighting.** Shiki (loaded by the app as `window.__shiki`) never replaces Blazor-owned nodes: the source `<pre>` gets `data-shiki="source"` (hidden by CSS) and the highlighted output goes into a JS-owned `[data-shiki-output]` sibling, re-rendered when the source text changes and removed when its source is gone. `xaml` / `axaml` are highlighted as `xml` and `cs` as `csharp`.

---

Expand Down
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ Ships to `ShellDocs.Components` + `ShellDocs.Templates` + `ShellDocs.CLI` + `She
**Authoring fix (`ShellDocs.Markdown`)**
- `SlotExtractor.ReplaceComponentTags` no longer `.Trim()`s the raw child content of inline component tags. The Trim was stripping the first line's indent and defeating `SlotRenderer.Dedent` — Markdig then interpreted the remaining 4-space-indented lines as an indented code block. Symptom was the same "literal `<pre>` around placeholder divs" bug that had already been fixed for `razor:preview` fences; the inline-tag code path was still hitting it.

### ✅ Hardening releases (`0.1.3`–`0.1.12-alpha`) — shipped
### ✅ Hardening releases (`0.1.3`–`0.1.13-alpha`) — shipped
Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHANGELOG):

- `feat/build-static-prerender` (0.1.5) — `shelldocs build` prerenders every URL into a static site.
Expand All @@ -219,6 +219,7 @@ Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHA
- `feat/preview-toolbar` (0.1.9) — Preview | Code toolbar with ⋯ menu, `<Tabs>`, static-host `<CodeGroup>` and mobile nav, consumer components winning name collisions, code-span masking, highlighting and hydration fixes.
- `fix/sidebar-index-pages` (0.1.11) — index pages as sidebar section links, `scroll` preview layout and phone-width previews, scrolling tables.
- `fix/fence-scanner-inline-attrs` (0.1.12) — CommonMark fences (any length, tildes), inline and named-slot tags read by the Razor tag scanner so `>` in a quoted value is safe, `<TypeTable>` rows that share a name.
- `feat/preview-code-variants` (0.1.13) — code tabs on previews with sync and a missing-tab note, page switch (`AddSyncSwitch`), captions, `<IframePreview>`, XAML highlighting; one-line component bodies stay inline.
- `fix/preview-fidelity`, `feat/generics-redirects-page-title` (0.1.10) — `not-prose` previews, Razor child content, stretch layout, generic components in markdown, redirects (middleware + static pages), `RenderPageTitle`, theme sync and a static-host `ThemeToggle`.

### `feat/animation-polish`
Expand Down
2 changes: 1 addition & 1 deletion examples/ShellDocs.Preview/Components/App.razor
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
import { createHighlighter } from 'https://esm.sh/shiki@1.24.0';
window.__shiki = await createHighlighter({
themes: ['github-light', 'github-dark'],
langs: ['razor', 'csharp', 'html', 'json', 'yaml', 'bash', 'typescript', 'javascript', 'markdown']
langs: ['razor', 'csharp', 'html', 'xml', 'json', 'yaml', 'bash', 'typescript', 'javascript', 'markdown']
});
if (window.shelldocsHighlight) window.shelldocsHighlight();
</script>
Expand Down
3 changes: 3 additions & 0 deletions examples/ShellDocs.Preview/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
o.AddVersion("v2.0", "v2.0.0", "/docs/v2.0", "Current stable", latest: true);
o.AddVersion("v1.9.1", "v1.9.1", "/docs/v1.9.1", "Previous release");

// Page-level MAUI | Avalonia tabs on pages whose blocks use SyncKey="platform".
o.AddSyncSwitch("platform", "MAUI", "Avalonia");

// Built-in primitives (Callout, Card, Steps, …) are registered by AddShellDocs.
o.RegisterComponentsFromAssembly<App>("ShellDocs.Preview.Ui");
o.RegisterComponentsFromAssembly<App>("ShellDocs.Preview.Demos");
Expand Down
Loading
Loading