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
29 changes: 29 additions & 0 deletions src/MeshWeaver.Blazor/BlazorView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,19 @@ public class BlazorView<TViewModel, TView> : ComponentBase, IAsyncDisposable
/// <summary>Inline CSS style string bound from <c>ViewModel.Style</c>; applied to the root element.</summary>
protected string? Style { get; set; }

/// <summary>
/// <see cref="Style"/> rendered as a space-prefixed suffix, for views whose root element already
/// carries FIXED declarations of its own (<c>style="max-width: 720px;@StyleSuffix"</c>). It goes
/// LAST so the author's declarations win the cascade over the view's defaults, and it is the empty
/// string when nothing was declared, so an unstyled control's markup stays byte-identical.
/// </summary>
/// <remarks>
/// Views whose root has no style of its own use <c>style="@Style"</c> directly, and views that
/// forward to a Fluent component pass <c>Style="@Style"</c>. Form views fold Style with Width and
/// Height through <c>FormComponentBase.ComputedStyle</c> instead.
/// </remarks>
protected string StyleSuffix => string.IsNullOrWhiteSpace(Style) ? string.Empty : " " + Style;

// Snapshot of the binding-relevant parameters at the last actual bind β€” OnParametersSet compares
// against these to skip redundant re-binds (see the guard there).
private TViewModel? _boundViewModel;
Expand Down Expand Up @@ -141,6 +154,22 @@ protected virtual void BindDataAfterParameterReset()

/// <summary>CSS class string bound from <c>ViewModel.Class</c>; applied to the root element.</summary>
protected string? Class { get; set; }

/// <summary>
/// <see cref="Class"/> rendered as a space-prefixed suffix, for views whose root element already
/// carries a FIXED class list of its own. 🚨 The Razor form is the PARENTHESISED one β€”
/// <c>class="markdown-body@(ClassSuffix)"</c>: written bare as <c>markdown-body@ClassSuffix</c> it
/// hits Razor's email-address heuristic (<c>word@word</c>) and is emitted as LITERAL TEXT, with no
/// error. Emits the empty
/// string when the author declared no class, so an unstyled control's markup stays byte-identical to
/// what it was before the class was honoured β€” appending <c> @Class</c> literally instead would leave
/// a trailing space in every such attribute.
/// </summary>
/// <remarks>
/// Views whose root has NO class of its own use <c>class="@Class"</c> directly (Blazor omits a null
/// attribute entirely), and views that forward to a Fluent component pass <c>Class="@Class"</c>.
/// </remarks>
protected string ClassSuffix => string.IsNullOrWhiteSpace(Class) ? string.Empty : " " + Class;
/// <summary>HTML element id bound from <c>ViewModel.Id</c>.</summary>
protected string? Id { get; set; }

Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/AppearanceView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@inherits BlazorView<AppearanceControl, AppearanceView>
@inject AccessService Access

<div style="max-width: 500px;">
<div class="@Class" style="max-width: 500px;@StyleSuffix">
<FluentDesignTheme @ref="theme"
@bind-Mode="@ThemeMode"
@bind-OfficeColor="@OfficeColor"
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/CatalogView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@using MeshWeaver.Layout.Catalog
@inherits BlazorView<CatalogControl, CatalogView>

<div class="catalog-container" style="@ContainerStyle">
<div class="catalog-container@(ClassSuffix)" style="@ContainerStyle">
@foreach (var group in Groups)
{
var isExpanded = expandedGroups.GetValueOrDefault(group.Key, group.IsExpanded);
Expand Down
3 changes: 2 additions & 1 deletion src/MeshWeaver.Blazor/Components/CatalogView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ public partial class CatalogView

private Dictionary<string, bool> expandedGroups = new();

private string ContainerStyle => "display: flex; flex-direction: column;";
// The author's Style goes LAST so a declared flex-direction / gap overrides the container's own.
private string ContainerStyle => $"display: flex; flex-direction: column;{StyleSuffix}";
private string CardStyle => $"height: {CardHeight}px; min-height: {CardHeight}px; max-height: {CardHeight}px; overflow: hidden; border: 1px solid var(--neutral-stroke-rest); border-radius: 4px; background: var(--neutral-layer-card-container);";

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
@inherits BlazorView<CollaborativeMarkdownControl, CollaborativeMarkdownView>
@inject AccessService Access

<div class="collab-md-container @ViewModeClass" data-theme="@Mode.ToString().ToLower()" @ref="containerRef">
<div class="@ContainerClass" data-theme="@Mode.ToString().ToLower()" style="@Style" @ref="containerRef">
@* Toolbar β€” comment filter. Tracked changes are already IN the document (they are projected from
the version history), so there is nothing to "accept": the only action is Revert, per card. *@
@if (HasCommentAnnotations)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,18 @@ private Address ResolveActivityAddress()
_ => ""
};

/// <summary>
/// The container's own classes plus the author-declared <c>Class</c>, joined as a token list.
/// Composed here because Razor cannot parse two adjacent expressions in one attribute value.
/// <para>The tokens are exactly the ones emitted before; the only difference is that the trailing
/// space left behind when <see cref="ViewModeClass"/> is empty (the default mode) is no longer
/// emitted. A class attribute is a whitespace-separated token list, so that space never
/// participated in any selector match.</para>
/// </summary>
private string ContainerClass =>
string.Join(" ", new[] { "collab-md-container", ViewModeClass, Class }
.Where(token => !string.IsNullOrWhiteSpace(token)));

/// <summary>
/// Binds the node path, annotation mode, comment list, and tracked-change state
/// from the layout-area stream, and subscribes to reactive comment and change feeds.
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/CommentableView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

@* The wrapper is the positioning context for the floating "Comment" button (position: relative
in the .css), so the button can be placed over the selection. *@
<div class="commentable-wrapper" @ref="containerRef">
<div class="commentable-wrapper@(ClassSuffix)" style="@Style" @ref="containerRef">
<div class="commentable-content">
@foreach (var area in ViewModel.Areas)
{
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/DialogView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@using FluentHorizontalAlignment = Microsoft.FluentUI.AspNetCore.Components.HorizontalAlignment
@inherits BlazorView<DialogControl, DialogView>

<FluentDialog @bind-Hidden="isHidden" Modal="true" OnDialogResult="HandleDialogResult" style="@GetDialogStyle()">
<FluentDialog @bind-Hidden="isHidden" Modal="true" OnDialogResult="HandleDialogResult" Class="@Class" style="@GetDialogStyle()">
<FluentDialogHeader>
<FluentStack HorizontalAlignment="FluentHorizontalAlignment.Left">
<FluentLabel Typography="Typography.PaneHeader">@Title</FluentLabel>
Expand Down
4 changes: 3 additions & 1 deletion src/MeshWeaver.Blazor/Components/DialogView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@ private string GetDialogStyle()
style += $" --dialog-height: {height};";
}

return style;
// The author's declaration goes LAST so it overrides the Size-derived variables rather than
// being dropped; StyleSuffix is empty when nothing was declared.
return style + StyleSuffix;
}

private string GetDialogWidth(string size) => size switch
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/EditorView.razor
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ο»Ώ@using Appearance = Microsoft.FluentUI.AspNetCore.Components.Appearance
@inherits MeshWeaver.Blazor.Components.SkinnedView<MeshWeaver.Layout.EditorControl, MeshWeaver.Layout.EditorSkin, EditorView>

<div style="@Style">
<div class="@Class" style="@Style">
<dl class="form-group" style="width: 100%;">
<DispatchView ViewModel="@ViewModel" Area="@Area" Stream="@Stream"></DispatchView>
</dl>
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/ExportDocumentView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
@inherits BlazorView<ExportDocumentControl, ExportDocumentView>
@inject AccessService Access

<div class="export-document-view" style="max-width: 720px;">
<div class="export-document-view@(ClassSuffix)" style="max-width: 720px;@StyleSuffix">
<h2 style="margin-top: 0;">Export @(_title)</h2>
<p style="color: var(--neutral-foreground-hint); margin-bottom: 24px;">
Export <strong>@(NodeName ?? SourcePath)</strong> as a @_formatLabel document.
Expand Down
2 changes: 2 additions & 0 deletions src/MeshWeaver.Blazor/Components/Label.razor
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
Disabled="@Disabled"
Typo="@Typo"
Weight="@Weight"
Class="@Class"
Style="@Style"
title="@InternalTooltip"
@onclick="OnClick">
Expand All @@ -23,6 +24,7 @@ else
Disabled="@Disabled"
Typo="@Typo"
Weight="@Weight"
Class="@Class"
Style="@Style"
title="@InternalTooltip">
@InternalData
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
ο»Ώ@inherits BlazorView<LayoutAreaDefinitionControl, LayoutAreaDefinitionView>


<a href="@ViewModel.Definition.Url" class="card layout-area-card text-decoration-none">
<a href="@ViewModel.Definition.Url" class="card layout-area-card text-decoration-none@(ClassSuffix)" style="@Style">
<div class="layout-area-card-header">
<h5 class="layout-area-card-title">@ViewModel.Definition.Title</h5>
</div>
Expand Down
21 changes: 20 additions & 1 deletion src/MeshWeaver.Blazor/Components/LayoutAreaView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
@inherits BlazorView<LayoutAreaControl, LayoutAreaView>
@inject AccessService Access

<div class="layout-area-container @(IsContentLoaded ? "content-loaded" : "content-loading") @((Top || Fill) ? "fill-area" : "")" style="@Style">
<div class="@ContainerClass" style="@Style">
@* Surface NodeType compile progress when the stream hasn't emitted yet so the
user sees "Compiling <path> (Ns)…" instead of an indefinite spinner. *@
@if (!IsContentLoaded)
Expand Down Expand Up @@ -77,6 +77,25 @@

@code
{
/// <summary>
/// The container's own state classes plus the author-declared <c>Class</c>, joined as a token list.
/// Composed here rather than inline because Razor cannot parse two adjacent explicit expressions in
/// one attribute value.
/// <para>The tokens are exactly the ones emitted before. The only difference is that the trailing
/// space left behind when the area is neither <c>Top</c> nor <c>Fill</c> is no longer emitted β€”
/// interpolating the empty conditional plus a space-prefixed class would otherwise produce a double
/// space and an empty token. A class attribute is a whitespace-separated token list, so neither the
/// old trailing space nor its removal participates in any selector match.</para>
/// </summary>
private string ContainerClass =>
string.Join(" ", new[]
{
"layout-area-container",
IsContentLoaded ? "content-loaded" : "content-loading",
(Top || Fill) ? "fill-area" : null,
Class
}.Where(token => !string.IsNullOrWhiteSpace(token)));

[Parameter] public bool Top { get; set; }

/// <summary>
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/MarkdownEditorView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
@inherits BlazorView<MarkdownEditorControl, MarkdownEditorView>
@inject AccessService Access

<div class="collaborative-editor-container">
<div class="collaborative-editor-container@(ClassSuffix)" style="@Style">
@* Suggest mode indicator stripe *@
@if (TrackChangesActive)
{
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/MarkdownView.razor
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
@inherits BlazorView<MarkdownControl, MarkdownView>

<article class="markdown-body" data-theme="@Mode.ToString().ToLower()" style="background-color: var(--neutral-layer-1); @Style">
<article class="markdown-body@(ClassSuffix)" data-theme="@Mode.ToString().ToLower()" style="background-color: var(--neutral-layer-1); @Style">
@((RenderFragment)RenderHtml)
</article>
26 changes: 23 additions & 3 deletions src/MeshWeaver.Blazor/Components/MeshNodeCardView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ else

private string Initial => !string.IsNullOrEmpty(Title) ? Title[0].ToString().ToUpper() : "?";

// The card IS the control's box, so the author's Class/Style land here rather than on the <a> that
// wraps it for navigation (a transparent display:block pass-through). Composed as single
// expressions because a COMPONENT attribute β€” unlike a plain HTML one β€” cannot mix markup and C#.
private string CardClass => $"mesh-node-card{ClassSuffix}";
private string CardStyle => $"cursor: pointer; width: 100%; display: block; box-sizing: border-box;{StyleSuffix}";

/// <summary>
/// Hard cap on the title so a very long name can never overflow the card (a backstop to the
/// CSS single-line ellipsis, which can't kick in when a parent doesn't constrain width).
Expand All @@ -45,9 +51,23 @@ else
? Description[..97] + "..."
: Description ?? "";

private LayoutAreaControl GetLayoutArea() =>
new LayoutAreaControl(NodePath, new LayoutAreaReference(ItemArea!))
/// <summary>
/// The embedded-area variant of the card. This branch renders NO element of its own β€” the area is a
/// different control that applies Style/Class to its own container β€” so the card's declaration is
/// FORWARDED to it rather than wrapped around it, the same way <c>PropertyView</c>/<c>EditFormView</c>
/// let the terminal view apply them. Forwarded only when actually declared, so an undeclared card
/// produces the identical control record as before.
/// </summary>
private LayoutAreaControl GetLayoutArea()
{
var area = new LayoutAreaControl(NodePath, new LayoutAreaReference(ItemArea!))
.WithShowProgress(false);
if (!string.IsNullOrWhiteSpace(Style))
area = area.WithStyle(Style);
if (!string.IsNullOrWhiteSpace(Class))
area = area.WithClass(Class);
return area;
}

private RenderFragment CardContent => __builder =>
{
Expand All @@ -57,7 +77,7 @@ else
}
else
{
<FluentCard Class="mesh-node-card" Style="cursor: pointer; width: 100%; display: block; box-sizing: border-box;">
<FluentCard Class="@CardClass" Style="@CardStyle">
<div class="mesh-node-card-content">
@if (!string.IsNullOrEmpty(ImageUrl))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@inject MeshWeaver.Mesh.Services.IMeshService MeshQuery
@inject AccessService Access

<div class="mesh-node-collection" style="@Style">
<div class="mesh-node-collection@(ClassSuffix)" style="@Style">
@if (_isLoading)
{
<FluentProgressRing Style="margin: 8px;" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ else if (Fields.Count == 0)
}
else
{
<div class="mesh-node-content-editor" style="display: flex; flex-direction: column; gap: 14px; width: 100%;">
<div class="mesh-node-content-editor@(ClassSuffix)" style="display: flex; flex-direction: column; gap: 14px; width: 100%;@StyleSuffix">
@foreach (var f in Fields)
{
<div class="field" style="display: flex; flex-direction: column; gap: 4px; width: 100%;">
Expand Down
2 changes: 1 addition & 1 deletion src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
@inherits FormComponentBase<MeshNodePickerControl, MeshNodePickerView, string>
@inject AccessService Access

<div class="mesh-node-picker@(IsThin ? " mesh-node-picker--thin" : "")">
<div class="@PickerClass" style="@ComputedStyle">
@if (Label != null)
{
<FluentLabel Weight="Microsoft.FluentUI.AspNetCore.Components.FontWeight.Bold" Style="margin-bottom: 4px;">@Label</FluentLabel>
Expand Down
6 changes: 6 additions & 0 deletions src/MeshWeaver.Blazor/Components/MeshNodePickerView.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ private bool CanServeFromCache(string searchText)

// Compact "thin" rendering + open-upward dropdown, driven by the control's Layout/Open.
private bool IsThin => ViewModel?.Layout == MeshNodePickerLayout.Thin;

/// <summary>
/// The picker's own layout classes plus the author-declared <c>Class</c>. Composed here because
/// Razor cannot parse two adjacent explicit expressions in one attribute value.
/// </summary>
private string PickerClass => $"mesh-node-picker{(IsThin ? " mesh-node-picker--thin" : "")}{ClassSuffix}";
private bool OpensUp => ViewModel?.Open == MeshNodePickerOpenDirection.Up;

/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions src/MeshWeaver.Blazor/Components/MeshNodeRoleEditorView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ else if (!_hasRole)
}
else if (CanEdit)
{
<div style="display: flex; align-items: center; gap: 8px;">
<div class="@Class" style="display: flex; align-items: center; gap: 8px;@StyleSuffix">
<FluentSelect TOption="string"
Items="@_options"
OptionValue="@(o => o)"
Expand All @@ -26,7 +26,8 @@ else if (CanEdit)
else
{
<FluentLabel Typo="Microsoft.FluentUI.AspNetCore.Components.Typography.Body"
Style="@(_denied ? "text-decoration: line-through; color: var(--error-foreground);" : "")">
Class="@Class"
Style="@((_denied ? "text-decoration: line-through; color: var(--error-foreground);" : "") + StyleSuffix)">
@_role@(_denied ? " (denied)" : "")
</FluentLabel>
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@inherits BlazorView<MeshNodeThumbnailControl, MeshNodeThumbnailView>
@using MeshWeaver.Graph

<FluentCard Class="mesh-node-thumbnail" Style="min-width: 320px; cursor: pointer; margin: 8px;" @onclick="Navigate">
<FluentCard Class="@CardClass" Style="@CardStyle" @onclick="Navigate">
<div class="mesh-node-thumbnail-content">
@if (!string.IsNullOrEmpty(ImageUrl))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public partial class MeshNodeThumbnailView

private string Initial => !string.IsNullOrEmpty(Title) ? Title[0].ToString().ToUpper() : "?";

// The card's Class/Style parameters were hard-coded, which SHADOWED the bound values entirely.
// The author's tokens now join / follow them. Composed as single expressions because a COMPONENT
// attribute β€” unlike a plain HTML one β€” cannot mix markup and C#.
private string CardClass => $"mesh-node-thumbnail{ClassSuffix}";
private string CardStyle => $"min-width: 320px; cursor: pointer; margin: 8px;{StyleSuffix}";

private string TruncatedDescription => Description?.Length > 120
? Description.Substring(0, 117) + "..."
: Description ?? string.Empty;
Expand Down
10 changes: 9 additions & 1 deletion src/MeshWeaver.Blazor/Components/Monaco/DiffEditorView.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
@using System.Text.Json
@inherits BlazorView<DiffEditorControl, DiffEditorView>

<div style="height: @(ViewModel?.Height ?? "500px"); width: 100%;">
<div class="@Class" style="@HostStyle">
<StandaloneDiffEditor @ref="diffEditor"
ConstructionOptions="GetDiffEditorOptions"
OnDidInit="OnEditorInit"
Expand All @@ -16,6 +16,14 @@
private StandaloneDiffEditor? diffEditor;
private bool isDarkMode = false;

/// <summary>
/// The host box's own sizing plus the author-declared <c>Style</c> LAST, so a declared height
/// overrides <c>DiffEditorControl.Height</c> rather than being dropped. Composed here because
/// Razor cannot parse two adjacent explicit expressions in one attribute value; the literal text
/// is unchanged and <c>StyleSuffix</c> is empty when nothing was declared.
/// </summary>
private string HostStyle => $"height: {ViewModel?.Height ?? "500px"}; width: 100%;{StyleSuffix}";

private string GetMonacoTheme() => isDarkMode ? "vs-dark" : "vs";

private StandaloneDiffEditorConstructionOptions GetDiffEditorOptions(StandaloneDiffEditor editor)
Expand Down
Loading
Loading