Skip to content

Commit edbbea8

Browse files
authored
Merge pull request #32 from shellui-dev/fix/sidebar-index-pages
fix: sidebar index pages, phone-width previews and tables
2 parents 3d3ebe3 + 1d6c92c commit edbbea8

20 files changed

Lines changed: 222 additions & 22 deletions

‎CHANGELOG.md‎

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,24 @@ All notable changes to ShellDocs land here. Format follows [Keep a Changelog](ht
44

55
## [Unreleased]
66

7+
## [0.1.11-alpha] — 2026-10-03
8+
9+
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.
10+
11+
### Added
12+
13+
- **`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.
14+
15+
### Changed
16+
17+
- 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.
18+
19+
### Fixed
20+
21+
- **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.
22+
- **Collapsed sidebar sections had no `aria-expanded`**: Blazor drops a `false` boolean attribute, so the toggle now writes `"true"` / `"false"`.
23+
- **Wide markdown tables widened the page on phones.** Tables render inside a `.shelldocs-table` box that scrolls horizontally.
24+
725
## [0.1.10-alpha] — 2026-10-02
826

927
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.
@@ -342,7 +360,8 @@ Published to NuGet:
342360
- `<TypeTable>` is hand-authored today; XML-doc auto-generation ships in `ShellDocs.Xml` (Phase 4)
343361
- No `<DocsBreadcrumb>` opt-out — currently hides when the trail has ≤ 1 node, otherwise always renders
344362

345-
[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.10-alpha...HEAD
363+
[Unreleased]: https://github.com/shellui-dev/shelldocs/compare/v0.1.11-alpha...HEAD
364+
[0.1.11-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.11-alpha
346365
[0.1.10-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.10-alpha
347366
[0.1.9-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.9-alpha
348367
[0.1.8-alpha]: https://github.com/shellui-dev/shelldocs/releases/tag/v0.1.8-alpha

‎Directory.Build.props‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
<!-- Package metadata (applies to any project with IsPackable=true) -->
1919
<PropertyGroup>
20-
<Version>0.1.10-alpha</Version>
20+
<Version>0.1.11-alpha</Version>
2121
<Authors>ShellUI</Authors>
2222
<Company>ShellUI</Company>
2323
<Copyright>Copyright © 2026 ShellUI</Copyright>

‎README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ o.AddRedirect("/docs/v0.3.0", "/docs/v0.3"); // also /docs/v0.3.0/x → /docs/
8080
- **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>`.
8181
- **Page typography stays out.** Preview frames are `not-prose`, so prose margins, list padding and link underlines don't reach your components.
8282
- **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.
83-
- **Centred or stretched.** Examples are centred; `razor:preview stretch` (or `Layout="stretch"` on `<DemoPreview>` / `<ComponentPreview>`) lets charts, inputs and tables fill the frame.
83+
- **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.
8484
- **Razor-shaped attribute values work:** `Variant="ButtonVariant.Destructive"`, `@ButtonVariant.Destructive`, `@true`, `@42`, `[Flags]` values as `Bold | Italic`.
8585
- **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.
8686
- **Inline code stays code.** `` `<Button>` `` in prose renders as literal code, not a component.

‎docs/ARCHITECTURE.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ Component child markup is rendered recursively. Inline tags in prose take markdo
130130

131131
**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`.
132132

133-
**`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).
133+
**`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).
134134

135135
**`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.
136136

‎docs/ROADMAP.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -209,14 +209,15 @@ Ships to `ShellDocs.Components` + `ShellDocs.Templates` + `ShellDocs.CLI` + `She
209209
**Authoring fix (`ShellDocs.Markdown`)**
210210
- `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.
211211

212-
### ✅ Hardening releases (`0.1.3`–`0.1.10-alpha`) — shipped
212+
### ✅ Hardening releases (`0.1.3`–`0.1.11-alpha`) — shipped
213213
Driven by building the ShellUI docs on ShellDocs. Highlights (details in the CHANGELOG):
214214

215215
- `feat/build-static-prerender` (0.1.5) — `shelldocs build` prerenders every URL into a static site.
216216
- `feat/auto-typetable-named-slots-sitemap` (0.1.6) — `<AutoTypeTable>`, named `RenderFragment` slots, `--site-url` sitemap / robots / og meta.
217217
- `fix/static-chrome-interactivity` (0.1.7) — sidebar, package selector, TOC and preview frames work on static hosts via `shelldocs.js`.
218218
- `feat/versioned-docs` (0.1.8) — `AddVersion`, `<VersionSelector>`, version-scoped chrome, multi-sibling `razor:preview`, Razor-form attribute values, `<DemoPreview>`, ShellIcons.
219219
- `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.
220+
- `fix/sidebar-index-pages` (0.1.11) — index pages as sidebar section links, `scroll` preview layout and phone-width previews, scrolling tables.
220221
- `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`.
221222

222223
### `feat/animation-polish`

‎examples/ShellDocs.Preview/content/docs/components/component-preview.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Body content that becomes the Callout's ChildContent.
2323

2424
- `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.
2525
- 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.
26-
- `Layout` — `"center"` (default) or `"stretch"`, which lets block-level components fill the frame's width.
26+
- `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.
2727
- 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.
2828

2929
## Notes

‎examples/ShellDocs.Preview/content/docs/markdown-syntax.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ Inline component tags in prose (outside a fence) still take markdown bodies.
6767

6868
### Layout
6969

70-
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"`.
70+
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"`.
7171

7272
```razor:preview stretch
7373
<Callout Variant="info" Text="This callout fills the frame instead of shrinking to its text." />

‎src/ShellDocs.Components/Chrome/DocsSidebar.razor.css‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,10 +117,42 @@
117117
margin-left: 0.25rem;
118118
transition: transform 200ms cubic-bezier(0.16, 1, 0.3, 1);
119119
}
120-
::deep .sidebar-section[data-open="true"] > .sidebar-section-toggle .sidebar-section-chevron {
120+
::deep .sidebar-section[data-open="true"] > .sidebar-section-toggle .sidebar-section-chevron,
121+
::deep .sidebar-section[data-open="true"] > .sidebar-section-row .sidebar-section-chevron {
121122
transform: rotate(90deg);
122123
}
123124

125+
/* A section whose folder has an index.md: the label links to that page; in a
126+
collapsible section the chevron is its own toggle button. */
127+
::deep .sidebar-section-link {
128+
text-decoration: none;
129+
border-radius: calc(var(--radius) - 3px);
130+
transition: background 150ms, color 150ms;
131+
}
132+
::deep .sidebar-section-link:hover { background: var(--muted); }
133+
::deep .sidebar-section-link.active { background: var(--accent); }
134+
::deep .sidebar-section[data-depth="0"] > .sidebar-section-link { padding: 0.3rem 0.5rem; margin: 0.15rem 0 0.35rem 0; }
135+
::deep .sidebar-section > .sidebar-section-label.sidebar-section-row {
136+
padding: 0;
137+
gap: 0;
138+
}
139+
::deep .sidebar-section-row > .sidebar-section-link {
140+
display: inline-flex;
141+
align-items: center;
142+
gap: 0.55rem;
143+
flex: 1;
144+
min-width: 0;
145+
color: inherit;
146+
padding: 0.45rem 0.25rem 0.45rem 0.55rem;
147+
}
148+
::deep .sidebar-section-row > .sidebar-section-toggle {
149+
display: inline-flex;
150+
align-items: center;
151+
padding: 0.45rem 0.5rem;
152+
color: inherit;
153+
}
154+
::deep .sidebar-section-row > .sidebar-section-toggle .sidebar-section-chevron { margin-left: 0; }
155+
124156
/* grid-rows 0fr→1fr animates to natural height without a fixed max-height;
125157
the inner wrapper needs min-height: 0 to collapse fully. */
126158
::deep .sidebar-section-shell {

‎src/ShellDocs.Components/Chrome/DocsSidebarNode.razor‎

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,28 @@ else if (Node.Kind == NodeKind.Section)
1111
<div class="sidebar-section" data-depth="@Depth" data-open="@(_isOpen ? "true" : "false")">
1212
@if (!string.IsNullOrEmpty(Node.Title))
1313
{
14-
@if (_isToggleable)
14+
@* A folder's index.md is the label's link, not a second item with the same title. *@
15+
@if (_index is not null && _isToggleable)
16+
{
17+
<div class="sidebar-section-label sidebar-section-row">
18+
<a class="sidebar-section-link @(IsActive(_index) ? "active" : "")" href="@_index.Url">
19+
@RenderLabelInner()
20+
</a>
21+
<button type="button" class="sidebar-section-toggle" aria-expanded="@(_isOpen ? "true" : "false")" aria-label="@($"Toggle {Node.Title}")">
22+
<ChevronRight Class="sidebar-section-chevron" />
23+
</button>
24+
</div>
25+
}
26+
else if (_index is not null)
27+
{
28+
<a class="sidebar-section-label sidebar-section-link @(IsActive(_index) ? "active" : "")" href="@_index.Url">
29+
@RenderLabelInner()
30+
</a>
31+
}
32+
else if (_isToggleable)
1533
{
1634
@* Toggled by shelldocs.js so it works without a Blazor runtime. *@
17-
<button type="button" class="sidebar-section-label sidebar-section-toggle" aria-expanded="@_isOpen">
35+
<button type="button" class="sidebar-section-label sidebar-section-toggle" aria-expanded="@(_isOpen ? "true" : "false")">
1836
@RenderLabelInner()
1937
<ChevronRight Class="sidebar-section-chevron" />
2038
</button>
@@ -30,7 +48,7 @@ else if (Node.Kind == NodeKind.Section)
3048
{
3149
<div class="sidebar-section-shell" data-open="@(_isOpen ? "true" : "false")">
3250
<div class="sidebar-section-items">
33-
@foreach (var child in Node.Children.Where(c => !Versions.IsHiddenInSidebar(c)))
51+
@foreach (var child in VisibleChildren)
3452
{
3553
<DocsSidebarNode Node="child" Depth="@(Depth + 1)" CurrentPath="@CurrentPath" />
3654
}
@@ -40,7 +58,7 @@ else if (Node.Kind == NodeKind.Section)
4058
else
4159
{
4260
<div class="sidebar-section-items">
43-
@foreach (var child in Node.Children.Where(c => !Versions.IsHiddenInSidebar(c)))
61+
@foreach (var child in VisibleChildren)
4462
{
4563
<DocsSidebarNode Node="child" Depth="@(Depth + 1)" CurrentPath="@CurrentPath" />
4664
}
@@ -50,7 +68,7 @@ else if (Node.Kind == NodeKind.Section)
5068
}
5169
else
5270
{
53-
var isActive = string.Equals(Node.Url.TrimEnd('/'), CurrentPath.TrimEnd('/'), StringComparison.OrdinalIgnoreCase);
71+
var isActive = IsActive(Node);
5472
var itemIcon = Depth <= 1 ? SidebarIcons.Get(Node.Title) : null;
5573
<a class="sidebar-item @(isActive ? "active" : "")" href="@Node.Url">
5674
@if (itemIcon is not null)
@@ -68,11 +86,13 @@ else
6886

6987
private bool _isOpen;
7088
private bool _isToggleable;
89+
private NavigationNode? _index;
7190
private bool _initialized;
7291
private string _lastPath = "";
7392

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

117+
private IEnumerable<NavigationNode> VisibleChildren
118+
=> Node.Children.Where(c => c != _index && !Versions.IsHiddenInSidebar(c));
119+
120+
private bool IsActive(NavigationNode page)
121+
=> string.Equals(page.Url.TrimEnd('/'), CurrentPath.TrimEnd('/'), StringComparison.OrdinalIgnoreCase);
122+
123+
// The page built from the section's own folder/index.md.
124+
private static NavigationNode? FindIndexPage(NavigationNode section)
125+
{
126+
if (string.IsNullOrEmpty(section.Path)) return null;
127+
var folder = System.IO.Path.TrimEndingDirectorySeparator(System.IO.Path.GetFullPath(section.Path));
128+
return section.Children.FirstOrDefault(c =>
129+
c.Kind == NodeKind.Page
130+
&& !string.IsNullOrEmpty(c.Path)
131+
&& string.Equals(System.IO.Path.GetFileName(c.Path), "index.md", StringComparison.OrdinalIgnoreCase)
132+
&& string.Equals(System.IO.Path.GetDirectoryName(System.IO.Path.GetFullPath(c.Path)), folder, StringComparison.OrdinalIgnoreCase));
133+
}
134+
97135
private static bool ContainsPath(NavigationNode node, string path)
98136
{
99137
if (string.IsNullOrEmpty(path)) return false;

‎src/ShellDocs.Components/Content/ComponentPreview.razor‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
[Parameter] public RenderFragment? ChildContent { get; set; }
2020
// Raw child markup from SlotRenderer, used to rebuild the source view.
2121
[Parameter] public string? ChildContentSource { get; set; }
22-
// "center" (default) or "stretch"; see PreviewFrame.
22+
// "center" (default), "stretch" or "scroll"; see PreviewFrame.
2323
[Parameter] public string? Layout { get; set; }
2424
[Parameter(CaptureUnmatchedValues = true)]
2525
public IReadOnlyDictionary<string, object>? ExtraProps { get; set; }

0 commit comments

Comments
 (0)