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
21 changes: 20 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht

## [Unreleased]

## [0.1.11-alpha] — 2026-10-03

Polish from moving the ShellUI docs onto 0.1.10: the sidebar no longer repeats a folder's index page, and previews and tables behave on phones.

### Added

- **`scroll` preview layout.** `razor:preview scroll`, or `Layout="scroll"` on `<DemoPreview>` / `<ComponentPreview>`, lets an example wider than the frame (pagination, toolbars, OTP inputs) scroll inside it on small screens instead of spilling past the frame. Opt-in, since a scroll box clips popovers.

### Changed

- On phones the preview frame's side padding drops from 1.5rem to 0.75rem, and centring uses `safe center`, so an example that's still too wide starts at the left edge instead of losing both sides.

### Fixed

- **Sidebar listed a folder's `index.md` twice**: once as the section label and again as a page with the same title. The label now links to the index page (highlighted when it's the current page) and the page isn't repeated; in collapsible sections the chevron is its own toggle button.
- **Collapsed sidebar sections had no `aria-expanded`**: Blazor drops a `false` boolean attribute, so the toggle now writes `"true"` / `"false"`.
- **Wide markdown tables widened the page on phones.** Tables render inside a `.shelldocs-table` box that scrolls horizontally.

## [0.1.10-alpha] — 2026-10-02

Previews that look like the real thing (no page typography leaking in, Razor child content, stretch layout, generic components), redirects for folders, version roots and moved URLs, an optional frontmatter page header, and a theme that stays in sync with whatever flips it.
Expand Down Expand Up @@ -342,7 +360,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.10-alpha...HEAD
[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.11-alpha...HEAD
[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
[0.1.9-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.9-alpha
[0.1.8-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.8-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.10-alpha</Version>
<Version>0.1.11-alpha</Version>
<Authors>ShellUI</Authors>
<Company>ShellUI</Company>
<Copyright>Copyright © 2026 ShellUI</Copyright>
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ 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.
- **Centred or stretched.** Examples are centred; `razor:preview stretch` (or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>`) lets charts, inputs and tables fill the frame.
- **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.
- **Inline code stays code.** `` `<Button>` `` in prose renders as literal code, not a component.
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ Component child markup is rendered recursively. Inline tags in prose take markdo

**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` or `stretch`. 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).
**`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).

**`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
3 changes: 2 additions & 1 deletion docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,15 @@ 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.10-alpha`) — shipped
### ✅ Hardening releases (`0.1.3`–`0.1.11-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.
- `feat/auto-typetable-named-slots-sitemap` (0.1.6) — `<AutoTypeTable>`, named `RenderFragment` slots, `--site-url` sitemap / robots / og meta.
- `fix/static-chrome-interactivity` (0.1.7) — sidebar, package selector, TOC and preview frames work on static hosts via `shelldocs.js`.
- `feat/versioned-docs` (0.1.8) — `AddVersion`, `<VersionSelector>`, version-scoped chrome, multi-sibling `razor:preview`, Razor-form attribute values, `<DemoPreview>`, ShellIcons.
- `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/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
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Body content that becomes the Callout's ChildContent.

- `Component` — required. The registered tag name (`"Callout"`, `"Card"`, `"LinkCard"`, …). Resolved through the same registry as `razor:preview`, so anything `AddShellDocs` or your `RegisterComponent*` calls register works.
- Any other attribute — forwarded to the target. Values are strings in markdown and are coerced to each parameter's type (`bool`, numbers, enums including `Type.Member` and `A | B` flags). Attributes that can't be set this way are skipped with a logged warning.
- `Layout` — `"center"` (default) or `"stretch"`, which lets block-level components fill the frame's width.
- `Layout` — `"center"` (default), `"stretch"`, which lets block-level components fill the frame's width, or `"scroll"`, which lets a wide example scroll inside the frame.
- The tag body is parsed as Razor (HTML and registered components, no markdown) and becomes the target's `ChildContent`. Child tags named after one of the target's `RenderFragment` parameters fill that slot.

## Notes
Expand Down
2 changes: 1 addition & 1 deletion examples/ShellDocs.Preview/content/docs/markdown-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Inline component tags in prose (outside a fence) still take markdown bodies.

### Layout

Examples are centred. Add `stretch` to the info string (`razor:preview stretch`) so block-level components such as charts, inputs and tables fill the frame's width. `<DemoPreview>` and `<ComponentPreview>` take `Layout="stretch"`.
Examples are centred. Add `stretch` to the info string (`razor:preview stretch`) so block-level components such as charts, inputs and tables fill the frame's width, or `scroll` so an example wider than a phone screen scrolls inside the frame instead of spilling past it. A scrolling frame clips popovers, so leave `scroll` off examples that open dropdowns. `<DemoPreview>` and `<ComponentPreview>` take `Layout="stretch"`.

```razor:preview stretch
<Callout Variant="info" Text="This callout fills the frame instead of shrinking to its text." />
Expand Down
34 changes: 33 additions & 1 deletion src/ShellDocs.Components/Chrome/DocsSidebar.razor.css
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,42 @@
margin-left: 0.25rem;
transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
}
::deep .sidebar-section[data-open="true"] > .sidebar-section-toggle .sidebar-section-chevron {
::deep .sidebar-section[data-open="true"] > .sidebar-section-toggle .sidebar-section-chevron,
::deep .sidebar-section[data-open="true"] > .sidebar-section-row .sidebar-section-chevron {
transform: rotate(90deg);
}

/* A section whose folder has an index.md: the label links to that page; in a
collapsible section the chevron is its own toggle button. */
::deep .sidebar-section-link {
text-decoration: none;
border-radius: calc(var(--radius) - 3px);
transition: background 150ms, color 150ms;
}
::deep .sidebar-section-link:hover { background: var(--muted); }
::deep .sidebar-section-link.active { background: var(--accent); }
::deep .sidebar-section[data-depth="0"] > .sidebar-section-link { padding: 0.3rem 0.5rem; margin: 0.15rem 0 0.35rem 0; }
::deep .sidebar-section > .sidebar-section-label.sidebar-section-row {
padding: 0;
gap: 0;
}
::deep .sidebar-section-row > .sidebar-section-link {
display: inline-flex;
align-items: center;
gap: 0.55rem;
flex: 1;
min-width: 0;
color: inherit;
padding: 0.45rem 0.25rem 0.45rem 0.55rem;
}
::deep .sidebar-section-row > .sidebar-section-toggle {
display: inline-flex;
align-items: center;
padding: 0.45rem 0.5rem;
color: inherit;
}
::deep .sidebar-section-row > .sidebar-section-toggle .sidebar-section-chevron { margin-left: 0; }

/* grid-rows 0fr→1fr animates to natural height without a fixed max-height;
the inner wrapper needs min-height: 0 to collapse fully. */
::deep .sidebar-section-shell {
Expand Down
48 changes: 43 additions & 5 deletions src/ShellDocs.Components/Chrome/DocsSidebarNode.razor
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,28 @@ else if (Node.Kind == NodeKind.Section)
<div class="sidebar-section" data-depth="@Depth" data-open="@(_isOpen ? "true" : "false")">
@if (!string.IsNullOrEmpty(Node.Title))
{
@if (_isToggleable)
@* A folder's index.md is the label's link, not a second item with the same title. *@
@if (_index is not null && _isToggleable)
{
<div class="sidebar-section-label sidebar-section-row">
<a class="sidebar-section-link @(IsActive(_index) ? "active" : "")" href="@_index.Url">
@RenderLabelInner()
</a>
<button type="button" class="sidebar-section-toggle" aria-expanded="@(_isOpen ? "true" : "false")" aria-label="@($"Toggle {Node.Title}")">
<ChevronRight Class="sidebar-section-chevron" />
</button>
</div>
}
else if (_index is not null)
{
<a class="sidebar-section-label sidebar-section-link @(IsActive(_index) ? "active" : "")" href="@_index.Url">
@RenderLabelInner()
</a>
}
else if (_isToggleable)
{
@* Toggled by shelldocs.js so it works without a Blazor runtime. *@
<button type="button" class="sidebar-section-label sidebar-section-toggle" aria-expanded="@_isOpen">
<button type="button" class="sidebar-section-label sidebar-section-toggle" aria-expanded="@(_isOpen ? "true" : "false")">
@RenderLabelInner()
<ChevronRight Class="sidebar-section-chevron" />
</button>
Expand All @@ -30,7 +48,7 @@ else if (Node.Kind == NodeKind.Section)
{
<div class="sidebar-section-shell" data-open="@(_isOpen ? "true" : "false")">
<div class="sidebar-section-items">
@foreach (var child in Node.Children.Where(c => !Versions.IsHiddenInSidebar(c)))
@foreach (var child in VisibleChildren)
{
<DocsSidebarNode Node="child" Depth="@(Depth + 1)" CurrentPath="@CurrentPath" />
}
Expand All @@ -40,7 +58,7 @@ else if (Node.Kind == NodeKind.Section)
else
{
<div class="sidebar-section-items">
@foreach (var child in Node.Children.Where(c => !Versions.IsHiddenInSidebar(c)))
@foreach (var child in VisibleChildren)
{
<DocsSidebarNode Node="child" Depth="@(Depth + 1)" CurrentPath="@CurrentPath" />
}
Expand All @@ -50,7 +68,7 @@ else if (Node.Kind == NodeKind.Section)
}
else
{
var isActive = string.Equals(Node.Url.TrimEnd('/'), CurrentPath.TrimEnd('/'), StringComparison.OrdinalIgnoreCase);
var isActive = IsActive(Node);
var itemIcon = Depth <= 1 ? SidebarIcons.Get(Node.Title) : null;
<a class="sidebar-item @(isActive ? "active" : "")" href="@Node.Url">
@if (itemIcon is not null)
Expand All @@ -68,11 +86,13 @@ else

private bool _isOpen;
private bool _isToggleable;
private NavigationNode? _index;
private bool _initialized;
private string _lastPath = "";

protected override void OnParametersSet()
{
_index = Node.Kind == NodeKind.Section && !string.IsNullOrEmpty(Node.Title) ? FindIndexPage(Node) : null;
_isToggleable = Node.Kind == NodeKind.Section
&& Depth >= 1
&& Node.Children.Any(c => c.Kind == NodeKind.Page);
Expand All @@ -94,6 +114,24 @@ else
_lastPath = CurrentPath;
}

private IEnumerable<NavigationNode> VisibleChildren
=> Node.Children.Where(c => c != _index && !Versions.IsHiddenInSidebar(c));

private bool IsActive(NavigationNode page)
=> string.Equals(page.Url.TrimEnd('/'), CurrentPath.TrimEnd('/'), StringComparison.OrdinalIgnoreCase);

// The page built from the section's own folder/index.md.
private static NavigationNode? FindIndexPage(NavigationNode section)
{
if (string.IsNullOrEmpty(section.Path)) return null;
var folder = System.IO.Path.TrimEndingDirectorySeparator(System.IO.Path.GetFullPath(section.Path));
return section.Children.FirstOrDefault(c =>
c.Kind == NodeKind.Page
&& !string.IsNullOrEmpty(c.Path)
&& string.Equals(System.IO.Path.GetFileName(c.Path), "index.md", StringComparison.OrdinalIgnoreCase)
&& string.Equals(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(c.Path)), folder, StringComparison.OrdinalIgnoreCase));
}

private static bool ContainsPath(NavigationNode node, string path)
{
if (string.IsNullOrEmpty(path)) return false;
Expand Down
2 changes: 1 addition & 1 deletion src/ShellDocs.Components/Content/ComponentPreview.razor
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
[Parameter] public RenderFragment? ChildContent { get; set; }
// Raw child markup from SlotRenderer, used to rebuild the source view.
[Parameter] public string? ChildContentSource { get; set; }
// "center" (default) or "stretch"; see PreviewFrame.
// "center" (default), "stretch" or "scroll"; see PreviewFrame.
[Parameter] public string? Layout { get; set; }
[Parameter(CaptureUnmatchedValues = true)]
public IReadOnlyDictionary<string, object>? ExtraProps { get; set; }
Expand Down
2 changes: 1 addition & 1 deletion src/ShellDocs.Components/Content/DemoPreview.razor
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
[Parameter] public string? Title { get; set; }
// Anchor id; defaults to demo-{component-slug}. Set it when a page shows the same demo twice.
[Parameter] public string? Id { get; set; }
// "center" (default) or "stretch"; see PreviewFrame.
// "center" (default), "stretch" or "scroll"; see PreviewFrame.
[Parameter] public string? Layout { get; set; }

private RenderFragment? _content;
Expand Down
10 changes: 8 additions & 2 deletions src/ShellDocs.Components/Content/PreviewFrame.razor
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@
[Parameter] public string? Error { get; set; }
[Parameter] public string ErrorTitle { get; set; } = "razor:preview error";
// "center" (default) centres the example; "stretch" lets block-level
// components (charts, inputs, tables) fill the frame's width.
// components (charts, inputs, tables) fill the frame's width; "scroll" lets an
// example wider than the frame (pagination, toolbars) scroll inside it.
[Parameter] public string? Layout { get; set; }

// Must be deterministic: it's the anchor, and how shelldocs.js restores the
Expand All @@ -121,7 +122,12 @@

protected override void OnParametersSet()
{
_layout = string.Equals(Layout ?? Preview?.Layout, "stretch", StringComparison.OrdinalIgnoreCase) ? "stretch" : "center";
_layout = (Layout ?? Preview?.Layout)?.ToLowerInvariant() switch
{
"stretch" => "stretch",
"scroll" => "scroll",
_ => "center",
};
var path = DocsVersionResolver.PathOf(Nav);
_openHref = string.IsNullOrEmpty(Id) ? path : $"{path}#{Id}";

Expand Down
Loading
Loading