Repository navigation
fix: the ten views whose literal root dropped Style and Class (#1297, part 2) - #1327
Conversation
Eight control views write Style="@Style" onto a real root element of their own and never wrote the author's Class beside it. Unlike the wrapper cases in #1288 there is no "where does the box go" question here: the element already carrying the Style is by definition the element that must carry the Class. Two shapes, chosen per view by what its root already has: * root with NO class of its own -> class="@Class" (Blazor omits a null attribute) * root with a FIXED class list -> "fixed@(ClassSuffix)" BlazorView.ClassSuffix renders " " + Class, or the empty string when nothing was declared, so an unstyled control's attribute stays byte-identical. Appending " @Class" literally would leave a trailing space in every such attribute on every page. LayoutAreaView and ThreadMessageBubbleView compose their class in code instead: Razor cannot parse two adjacent explicit expressions in one attribute value, and both roots end in a conditional expression. Nothing here is gated on the declaration (ViewModel.Class) as HtmlView is, because no element is created or removed — a JsonPointerReference that lands a frame later just updates an attribute; it cannot restructure the DOM mid-flight. Tests: five render through the real HtmlRenderer and were verified by falsification — reverting each view makes exactly its own test fail (5 failed / 17 passed). The four backwards-compatibility guards pass with AND without the fix, asserting the exact attribute (class="markdown-body", not Contain) so a stray trailing space or empty class="" fails. NumberFieldView, LayoutAreaView and ThreadMessageBubbleView are fixed the same way but need a live pointer/area/thread stream to render; that is stated in the test class doc rather than left implicit. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Ten control views render a panel / page / editor with ONE root element of their own, carrying only hard-coded declarations (`style="max-width: 720px;"`, `class="node-export-view"`) and no author input at all. That root IS the control's box, so it is where the author's Style and Class belong — no wrapper is introduced anywhere here, which is the point: #1288 showed a blanket wrapper breaks call sites that depend on a fragment's own root being the direct flex child. Placement rule, applied per view rather than mechanically: * the author's Style goes LAST (BlazorView.StyleSuffix) so a declared width/height OVERRIDES the view's default. Dropping it was the bug; losing the cascade would be the same bug with extra steps. * the author's Class joins the fixed class list (ClassSuffix), never replaces it. Three compose in code because Razor cannot parse two adjacent explicit expressions in one attribute value: CatalogView (its root style was already a code-behind literal), CollaborativeMarkdownView, and DiffEditorView — whose host box read ViewModel.Height directly and now keeps it as the default with the declared Style after it. Backwards compatibility: with nothing declared both suffixes are empty, so each root is byte-identical — asserted as exact attributes, not Contain. One deliberate exception: CollaborativeMarkdownView's container previously emitted a trailing space when its view-mode class was empty (the default mode); the tokens are now joined, so that space is gone. A class attribute is a whitespace-separated token list, so it never participated in a selector match. Tests: nine render through the real HtmlRenderer, verified by falsification — reverting the views fails exactly those nine (9 failed / 26 passed) while the four backwards-compatibility guards pass either way. MeshNodeContentEditorView is fixed the same way but not rendered: its first render is a progress ring and the styled root appears only after the node's fields load. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Re-targeted to |
…oc example Two of Copilot's three findings on #1323, both real: * LayoutAreaView produced a DOUBLE space (and an empty class token) when the area is neither Top nor Fill and a class was declared — the interpolated literal already ended in a space before the empty conditional, and ClassSuffix adds its own. Joined as a token list instead, which also drops the pre-existing trailing space in that case. Same treatment CollaborativeMarkdownView gets in part 2, same justification: a class attribute is a whitespace-separated token list, so neither the old space nor its removal can change a selector match. * The ClassSuffix XML doc used 'markdown-body@ClassSuffix' as its example — the exact bare form that Razor's email-address heuristic emits as LITERAL TEXT with no error. Copying the doc would have reintroduced the bug; it now shows @(ClassSuffix) and says why. The third finding (that Contain("class=\"markdown-body\"") would still pass on a trailing space) does not hold: the needle includes the CLOSING QUOTE, so class="markdown-body " and class="markdown-body x" both fail to match. Answered on the thread rather than changed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes a set of Blazor control views that were silently ignoring the public UiControl.Style / UiControl.Class API by ensuring author-declared styling reaches the correct DOM “box” element (particularly for views with a single literal root element).
Changes:
- Adds
BlazorView.StyleSuffix/BlazorView.ClassSuffixhelpers to append author styling to roots that already have fixed style/class tokens (preserving no-op output when unset). - Updates multiple component views to apply
Style/Classon the intended root element (including literal-root panels/editors and previously class-only omissions). - Extends
ControlStyleRenderingTestwith render-based assertions and adds corresponding “What’s New” entries.
Reviewed changes
Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| test/MeshWeaver.Hosting.Blazor.Test/ControlStyleRenderingTest.cs | Adds render-based tests covering Style/Class propagation for the affected views and registers missing Blazor services needed by the harness. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-13-styling-hooks-now-reach-eight-more-parts-of-the-page.md | Documents the “class-only drop” fixes in release notes. |
| src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-13-panels-and-editors-now-accept-the-styling-you-give-them.md | Documents the literal-root Style/Class fixes in release notes. |
| src/MeshWeaver.Blazor/Components/ThreadMessageBubbleView.razor.cs | Introduces composed bubble class string that preserves fixed tokens while appending author class. |
| src/MeshWeaver.Blazor/Components/ThreadMessageBubbleView.razor | Applies the composed bubble class to the root element. |
| src/MeshWeaver.Blazor/Components/TabsView.razor | Forwards Class into the Fluent tabs component so authored class is not dropped. |
| src/MeshWeaver.Blazor/Components/NumberFieldView.razor | Adds class="@Class" to the wrapper so authored class is not dropped. |
| src/MeshWeaver.Blazor/Components/NodeImportView.razor | Appends ClassSuffix / StyleSuffix to the literal root element. |
| src/MeshWeaver.Blazor/Components/NodeExportView.razor | Appends ClassSuffix / StyleSuffix to the literal root element. |
| src/MeshWeaver.Blazor/Components/Monaco/DiffEditorView.razor | Moves host sizing into a composed style and appends authored style/class to the host box. |
| src/MeshWeaver.Blazor/Components/MeshNodeContentEditorView.razor | Appends ClassSuffix / StyleSuffix to the editor’s styled root element. |
| src/MeshWeaver.Blazor/Components/MeshNodeCollectionView.razor | Appends ClassSuffix so authored class is preserved alongside the fixed class. |
| src/MeshWeaver.Blazor/Components/MarkdownView.razor | Appends ClassSuffix so authored class joins markdown-body. |
| src/MeshWeaver.Blazor/Components/MarkdownEditorView.razor | Appends ClassSuffix and applies authored Style to the container element. |
| src/MeshWeaver.Blazor/Components/LayoutAreaView.razor | Composes container class tokens into a single class string that can include authored class. |
| src/MeshWeaver.Blazor/Components/Label.razor | Forwards Class into FluentLabel in both branches so authored class is not dropped. |
| src/MeshWeaver.Blazor/Components/ExportDocumentView.razor | Appends ClassSuffix / StyleSuffix to the literal root element. |
| src/MeshWeaver.Blazor/Components/EditorView.razor | Adds class="@Class" to the wrapper so authored class is not dropped. |
| src/MeshWeaver.Blazor/Components/CommentableView.razor | Appends ClassSuffix and applies authored Style on the wrapper element. |
| src/MeshWeaver.Blazor/Components/CollaborativeMarkdownView.razor.cs | Composes container class list (including authored class) as a token-joined string. |
| src/MeshWeaver.Blazor/Components/CollaborativeMarkdownView.razor | Uses composed container class and applies authored Style to the container element. |
| src/MeshWeaver.Blazor/Components/CatalogView.razor.cs | Composes container style to append authored style after defaults. |
| src/MeshWeaver.Blazor/Components/CatalogView.razor | Appends ClassSuffix to catalog container class list. |
| src/MeshWeaver.Blazor/Components/AppearanceView.razor | Applies authored class and appends authored style after the fixed max-width declaration. |
| src/MeshWeaver.Blazor/BlazorView.razor.cs | Adds StyleSuffix / ClassSuffix helpers for safe concatenation onto fixed root attributes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| @inject AccessService Access | ||
|
|
||
| <div style="max-width: 500px;"> | ||
| <div class="@Class" style="max-width: 500px;@StyleSuffix"> |
Part 2 of the #1297 sweep — 10 of the 17 views that drop both
StyleandClass: the ones whosebox is a single literal root element.
The judgement, made per view rather than mechanically
#1288 established that a blanket wrapper is wrong, with a case that would have shipped undetected
(
TodoLayoutAreas.cs:117putsflex: 1on its fragment's own root because that root is the directflex child — in-mesh source no build type-checks). So the question for each of the 17 is where does
the box go, not what transformation do I apply.
For these ten the answer is the same and it is not a wrapper: each renders a panel / page / editor
with one root element of its own, carrying only hard-coded declarations and no author input at all.
That root IS the control's box. Nothing is created, nothing is nested, nothing moves.
NodeExportViewclass="node-export-view" style="max-width: 800px;"NodeImportViewclass="node-import-view" style="max-width: 800px;"ExportDocumentViewclass="export-document-view" style="max-width: 720px;"AppearanceViewstyle="max-width: 500px;", no class attributeclass="@Class")CatalogViewclass="catalog-container" style="@ContainerStyle"— a code-behind literalContainerStylecomposition + class listCommentableViewclass="commentable-wrapper", no style attributeMarkdownEditorViewclass="collaborative-editor-container", no styleCollaborativeMarkdownViewclass="collab-md-container @ViewModeClass"MeshNodeContentEditorViewclass="mesh-node-content-editor" style="display:flex;…"Monaco/DiffEditorViewstyle="height: @(ViewModel.Height ?? "500px"); width:100%"Two placement decisions, both load-bearing
The author's Style goes LAST.
BlazorView.StyleSuffixappends after the view's own declarations, soa declared
max-width: 400pxoverrides the built-inmax-width: 800px. Dropping the declaration wasthe bug; putting it where the default wins would be the same bug with extra steps. The tests assert the
order (
"max-width: 800px; max-width: 400px"), not just presence.DiffEditorViewis the sharpest case: it readViewModel.Heightstraight into its host box and ignoredStyleentirely, soWithStyle("height: 200px")was a no-op. The control's ownHeightstays thedefault and the declared style now follows it.
The author's Class joins the fixed list, never replaces it.
.node-export-view,.catalog-containeretc. are the hooks their own stylesheets match on.
Backwards compatibility
Both suffixes are the empty string when nothing was declared, so every root is byte-identical. The
guards assert the exact attribute —
class="node-export-view" style="max-width: 800px;"— notContain, so a trailing space or an emptyclass=""fails.CollaborativeMarkdownView's container previously emittedclass="collab-md-container "— a trailingspace — whenever its view-mode class was empty, which is the default mode. Preserving that byte-for-byte
would have produced
"collab-md-container probe-class"(double space) once a class was declared, sothe tokens are now joined and the stray space is gone. A class attribute is a whitespace-separated token
list; that space never participated in any selector match.
Three views compose their attribute in code rather than markup, for a mechanical reason worth recording
so nobody "simplifies" it back: Razor cannot parse two adjacent explicit expressions in one attribute
value (
@(a)@(b)→CS1073: Unexpected token).Tests — verified by falsification
Thirteen tests added to
ControlStyleRenderingTest, rendering through the realHtmlRendereragainst a real monolith mesh. Reverting all ten views:
MeshNodeContentEditorViewis the one not rendered, and the test class doc says why rather thanleaving it implicit: its first render is a progress ring, and the styled root appears only once the
node's editable fields have loaded from the mesh. Its change is the same one-line edit to a root it
already styles.
Verification
dotnet build src/MeshWeaver.Blazor -c Release -warnaserrordotnet build test/MeshWeaver.Hosting.Blazor.Test -c Release -warnaserrorControlStyleRenderingTest(35)WhatsNewEntryIntegrityTest+DocumentationLinkIntegrityTestWhat's New entry added (
Category: Fix).Left for part 3
The remaining 7 of the 17, which are the ones with a genuine "which element" question rather than a
single obvious root:
MeshNodeCardView(three branches, one of which delegates to another control),MeshNodeThumbnailViewandLayoutAreaDefinitionView(hard-codedClass/Styleparameters shadowingthe bound values),
NavLink(itsClassis overwritten by the active-state token),DialogView(itsstyle slot composes CSS variables from
Size),MeshNodePickerView(aFormComponentBasethat ignoresComputedStyle, hence also droppingWidth/Height), andMeshNodeRoleEditorView(four branch roots).🤖 Generated with Claude Code