Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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: 21 additions & 8 deletions .claude/skills/repo-profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,11 +99,22 @@
in nondeterministic order, so a check that reads the first one accepts one project's total as the
whole suite. Confirm all three assemblies reported, or trust the process exit code for the full run
(unpiped). Filter to iterate; run one of these before you claim done.
- **Format/lint apply:** `dotnet format FormCraft.sln`
- **Format/lint verify (the gate):** *CI runs no `dotnet format` check.* The enforcing gate is the
**build itself**: `Directory.Build.props` sets `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`
(with `NoWarn=CS1591;CS8620`), so any analyzer/compiler warning fails `dotnet build -c Release`.
`.editorconfig` severities are capped at `warning` and are IDE/`dotnet format` guidance only.
- **Format/lint apply:** `dotnet format FormCraft.sln` (or `whitespace` / `style` to scope it)
- **Format/lint verify (the gate):** `./build.sh Format` — a Nuke target wrapping
`dotnet format FormCraft.sln --verify-no-changes`, run by `ci.yml` **before** `Test` (#301).
There are now **two** independent gates:
1. **The build** — `Directory.Build.props` sets `<TreatWarningsAsErrors>true</TreatWarningsAsErrors>`
(with `NoWarn=CS1591;CS8620`), so any compiler/package-analyzer warning fails
`dotnet build -c Release`. This does **not** cover `.editorconfig`.
2. **The `Format` target** — the only thing that reads `.editorconfig`. `EnforceCodeStyleInBuild`
is deliberately unset, so `IDE*` analyzers never run during a build; before #301 that meant
nothing anywhere read those severities and 574 violations had accumulated across 201 files.
- ⚠️ **`dotnet format` corrupts multi-targeted files when *applying*.** `FormCraft` is
`net8.0;net10.0`, so a fix can be applied once per TFM and land as a literal
`<<<<<<< TODO: Unmerged change from project 'FormCraft(net10.0)'` conflict block **written into the
`.cs` file** (measured in #301 on `FieldRendererBase.cs`; the result does not compile). Verify mode
never writes, so CI is unaffected. After any *apply* run:
`grep -rl '<<<<<<< TODO' --include='*.cs' .` and hand-resolve before committing.
- **Prerequisites / caveats:**
- `global.json` pins SDK `10.0.302` with `rollForward: latestFeature`.
- Multi-target `net8.0;net10.0` — a build error can be TFM-specific; read which TFM the error names.
Expand All @@ -113,6 +124,8 @@
`Continuous`). `Test` is `DependsOn(Compile)` with `--no-build --no-restore`.

## CI gates (the exact commands CI fails on — satisfy these locally before ready/merge)
- `./build.cmd Format` — `.github/workflows/ci.yml`, run **before** `Test` since #301. Verifies
`dotnet format --verify-no-changes` is clean; fails on any diff.
- `./build.cmd Test` — `.github/workflows/ci.yml` (Compile then Test, Release config)
- `./build.cmd Pack` — `.github/workflows/continuous.yml` (`Pack` is `DependsOn(Test)`). Since #197
this workflow builds/tests/packs only: no `tags:` trigger, no key, and it invokes `Pack` rather
Expand All @@ -121,9 +134,9 @@
*is* the `v3.1.0` commit) and would then hard-fail on `Publish`'s `Requires(NuGetApiKey)`.
- **`pr-title-lint.yml`** — `amannn/action-semantic-pull-request` on `pull_request_target`. The PR
title **must** be a Conventional Commit. Skipped on drafts, and re-evaluated on `ready_for_review`.
- Locally equivalent: `dotnet build -c Release` && `dotnet test -c Release`.
- No format/lint check, and **no branch protection** (`required_status_checks` is
`enforcement_level: off`) — CI is advisory at the API level, so read the run result yourself.
- Locally equivalent: `./build.sh Format` && `dotnet build -c Release` && `dotnet test -c Release`.
- **No branch protection** (`required_status_checks` is `enforcement_level: off`) — CI is advisory at
the API level, so read the run result yourself.
- `release-please.yml` is not a PR gate: it runs on push to `dev` and on `workflow_dispatch`.

## Integration style
Expand Down
11 changes: 11 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ jobs:
- uses: actions/setup-dotnet@v6
with:
dotnet-version: '10.0.x'
# Runs BEFORE Test so a style slip reports in ~30s instead of behind a full build+test.
#
# This is the only thing that reads .editorconfig (#301). Its severities say `warning` and
# Directory.Build.props sets TreatWarningsAsErrors=true, which together look like enforcement
# and are not: IDE* analyzers need EnforceCodeStyleInBuild, which is deliberately NOT set —
# turning it on would make one missing brace break `dotnet build` mid-edit. Delete this step
# and the rules go back to being decorative, which is how 574 violations accumulated.
- run: ./build.cmd Format
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1

- run: ./build.cmd Test
env:
DOTNET_CLI_TELEMETRY_OPTOUT: 1
Expand Down
32 changes: 32 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,38 @@ dotnet build /p:TreatWarningsAsErrors=true
./pack-local.ps1 # Windows
```

### Code style (`.editorconfig`) — enforced in CI, not in the build

```bash
./build.sh Format # the CI gate: verify only, fails on any diff
dotnet format FormCraft.sln # apply the fixes
dotnet format FormCraft.sln whitespace # or scope it: whitespace only
dotnet format FormCraft.sln style # ...or the IDE* code-style rules only
```

- **`EnforceCodeStyleInBuild` is deliberately NOT set**, so `IDE*` analyzers do not run during
`dotnet build`. That is why `.editorconfig`'s `warning` severities never met
`TreatWarningsAsErrors` and 574 violations accumulated across 201 files before #301 — the two
settings *look* like enforcement together and are not. ⛔ Don't "fix" that by switching the
property on: with warnings-as-errors, one missing brace would break `dotnet build` mid-edit and
every incremental build would pay for the analyzer pass. The `Format` CI step is where regression
is caught.
- ⚠️ **`dotnet format` can corrupt multi-targeted files when it *applies* fixes.** `FormCraft` is
`net8.0;net10.0`, and a fix applied once per TFM can land as a literal
`<<<<<<< TODO: Unmerged change from project 'FormCraft(net10.0)'` conflict block **written into
the `.cs` file** — code that does not compile (measured in #301 on `FieldRendererBase.cs`).
Verify mode never writes, so CI is safe. After any apply run:
`grep -rl '<<<<<<< TODO' --include='*.cs' .` and hand-resolve before committing.
- ⚠️ **One `dotnet format` run does not always reach a fixpoint.** Measured in #301: after formatting
a `field`-keyword property, verify still reported three `WHITESPACE` diagnostics on it, and a
second run cleared them. If `./build.sh Format` fails right after you formatted, run the formatter
again before assuming the gate is wrong.
- **`IDE0032` now rewrites a private backing field into C# 14's `field` keyword.** Value-identical,
but it silently deletes the XML docs that lived on the field — which on
`MudBlazorFileUploadComponentBase.RequiredDescriptionId` were documenting a real correctness
invariant (#262: the hint id must be unique per rendered instance). Read those hunks; move the
explanation onto the property rather than losing it.

### Running Tests

The test projects are **Microsoft.Testing.Platform** hosts, not VSTest. That changes how you filter —
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,4 @@ private void Cancel()
{
MudDialog.Cancel();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@ private void Cancel()
{
MudDialog.Cancel();
}
}
}
82 changes: 61 additions & 21 deletions FormCraft.DemoBlazorApp/Components/Layout/FormCraftTheme.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,67 +115,107 @@ public static class FormCraftTheme
// declared parts — so it earns the largest type on the page.
H1 = new H1Typography
{
FontFamily = DisplayFont, FontSize = "3.4rem", FontWeight = "800",
LineHeight = "1.02", LetterSpacing = "-0.03em"
FontFamily = DisplayFont,
FontSize = "3.4rem",
FontWeight = "800",
LineHeight = "1.02",
LetterSpacing = "-0.03em"
},
H2 = new H2Typography
{
FontFamily = DisplayFont, FontSize = "2.6rem", FontWeight = "800",
LineHeight = "1.06", LetterSpacing = "-0.028em"
FontFamily = DisplayFont,
FontSize = "2.6rem",
FontWeight = "800",
LineHeight = "1.06",
LetterSpacing = "-0.028em"
},
H3 = new H3Typography
{
FontFamily = DisplayFont, FontSize = "2.1rem", FontWeight = "700",
LineHeight = "1.1", LetterSpacing = "-0.024em"
FontFamily = DisplayFont,
FontSize = "2.1rem",
FontWeight = "700",
LineHeight = "1.1",
LetterSpacing = "-0.024em"
},
H4 = new H4Typography
{
FontFamily = DisplayFont, FontSize = "1.65rem", FontWeight = "700",
LineHeight = "1.15", LetterSpacing = "-0.02em"
FontFamily = DisplayFont,
FontSize = "1.65rem",
FontWeight = "700",
LineHeight = "1.15",
LetterSpacing = "-0.02em"
},
H5 = new H5Typography
{
FontFamily = DisplayFont, FontSize = "1.3rem", FontWeight = "700",
LineHeight = "1.2", LetterSpacing = "-0.015em"
FontFamily = DisplayFont,
FontSize = "1.3rem",
FontWeight = "700",
LineHeight = "1.2",
LetterSpacing = "-0.015em"
},
H6 = new H6Typography
{
FontFamily = DisplayFont, FontSize = "1.075rem", FontWeight = "600",
LineHeight = "1.3", LetterSpacing = "-0.01em"
FontFamily = DisplayFont,
FontSize = "1.075rem",
FontWeight = "600",
LineHeight = "1.3",
LetterSpacing = "-0.01em"
},
Subtitle1 = new Subtitle1Typography
{
FontFamily = BodyFont, FontSize = "1rem", FontWeight = "500", LineHeight = "1.5"
FontFamily = BodyFont,
FontSize = "1rem",
FontWeight = "500",
LineHeight = "1.5"
},
Subtitle2 = new Subtitle2Typography
{
FontFamily = BodyFont, FontSize = "0.875rem", FontWeight = "600", LineHeight = "1.45"
FontFamily = BodyFont,
FontSize = "0.875rem",
FontWeight = "600",
LineHeight = "1.45"
},
Body1 = new Body1Typography
{
FontFamily = BodyFont, FontSize = "0.9375rem", FontWeight = "400", LineHeight = "1.6"
FontFamily = BodyFont,
FontSize = "0.9375rem",
FontWeight = "400",
LineHeight = "1.6"
},
Body2 = new Body2Typography
{
FontFamily = BodyFont, FontSize = "0.875rem", FontWeight = "400", LineHeight = "1.55"
FontFamily = BodyFont,
FontSize = "0.875rem",
FontWeight = "400",
LineHeight = "1.55"
},
// Buttons keep sentence case: a control is named for what it does, and
// SHOUTING CAPS makes long labels ("Read the documentation") hard to scan.
Button = new ButtonTypography
{
FontFamily = BodyFont, FontSize = "0.875rem", FontWeight = "600",
LineHeight = "1.75", LetterSpacing = "0.01em", TextTransform = "none"
FontFamily = BodyFont,
FontSize = "0.875rem",
FontWeight = "600",
LineHeight = "1.75",
LetterSpacing = "0.01em",
TextTransform = "none"
},
Caption = new CaptionTypography
{
FontFamily = BodyFont, FontSize = "0.78rem", FontWeight = "400", LineHeight = "1.4"
FontFamily = BodyFont,
FontSize = "0.78rem",
FontWeight = "400",
LineHeight = "1.4"
},
// Overline is the eyebrow role, and every eyebrow on this site is mono.
Overline = new OverlineTypography
{
FontFamily = ["IBM Plex Mono", "ui-monospace", "monospace"],
FontSize = "0.72rem", FontWeight = "500", LineHeight = "1.6",
LetterSpacing = "0.16em", TextTransform = "uppercase"
FontSize = "0.72rem",
FontWeight = "500",
LineHeight = "1.6",
LetterSpacing = "0.16em",
TextTransform = "uppercase"
}
}
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ public partial class FormPageLayout
[Parameter] public RenderFragment FormContent { get; set; } = null!;
[Parameter] public RenderFragment SidebarContent { get; set; } = null!;
[Parameter] public RenderFragment? SuccessContent { get; set; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,9 @@ private async Task<bool> ValidatePostalCodeAsync(string postalCode)
await Task.Delay(300);

if (string.IsNullOrEmpty(postalCode))
{
return true;
}

// Simple validation based on country
return _model.Country switch
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
using FormCraft.DemoBlazorApp.Components.Shared;
using FormCraft.DemoBlazorApp.Models;
using FormCraft.DemoBlazorApp.Services;
using MudBlazor;
using Microsoft.AspNetCore.Components;
using MudBlazor;

namespace FormCraft.DemoBlazorApp.Components.Pages;

Expand Down Expand Up @@ -217,4 +217,4 @@ private static string GetFormGenerationCodeStatic()
Configuration=""@_formConfiguration""
OnValidSubmit=""HandleValidSubmit"" />";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ public partial class AutoFormDemo
private bool _isPlainSubmitting;
private IFormConfiguration<AccountSignupModel> _plainConfiguration = null!;

private SpeakerProfileModel _annotatedModel = new();
private readonly SpeakerProfileModel _annotatedModel = new();
private bool _isAnnotatedSubmitted;
private IFormConfiguration<SpeakerProfileModel> _annotatedConfiguration = null!;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public partial class CrossFieldValidationDemo
private IFormConfiguration<BookingModel>? _formConfig;
private bool _submitted;
private bool _isSubmitting;
private List<string> _validationErrors = [];
private readonly List<string> _validationErrors = [];

/// <summary>
/// Structured documentation for this demo page.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -271,4 +271,4 @@ public static FieldBuilder<TModel, string> AsColorPicker<TModel>(
}
""";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -207,4 +207,4 @@ private async Task OpenDialog()
}
""";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,4 +232,4 @@ private static string GetGeneratedCodeStatic()

return code;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -197,4 +197,4 @@ private static string GetGeneratedCodeStatic()
Configuration=""@_formConfiguration""
OnValidSubmit=""@HandleSubmit"" />";
}
}
}
10 changes: 9 additions & 1 deletion FormCraft.DemoBlazorApp/Components/Pages/FluentForm.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -152,18 +152,26 @@ private Task HandleFieldChanged(string fieldName, object? value)
};

if (!string.IsNullOrEmpty(_model.City))
{
items.Add(new() { Label = "City", Value = _model.City });
}

if (_model.ExpectedSalary.HasValue)
{
items.Add(new() { Label = "Expected Salary", Value = $"${_model.ExpectedSalary.Value:N2}" });
}

if (_model.HourlyRate.HasValue)
{
items.Add(new() { Label = "Hourly Rate", Value = $"${_model.HourlyRate.Value:N2}/hr" });
}

items.Add(new() { Label = "Newsletter", Value = _model.SubscribeToNewsletter ? "Subscribed" : "Not Subscribed" });

if (_fieldChanges.Any())
{
items.Add(new() { Label = "Field Changes", Value = $"{_fieldChanges.Count} changes tracked" });
}

return items;
}
Expand Down Expand Up @@ -225,4 +233,4 @@ private static string GetGeneratedCodeStatic()

return code;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -202,4 +202,4 @@ public CustomerValidator()
.GreaterThanOrEqualTo(18).WithMessage("You must be 18 or older");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -283,4 +283,4 @@ public override void Dispose()
_countdownTimer?.Dispose();
base.Dispose();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ private void ResetForm()
};

if (!string.IsNullOrEmpty(_model.City))
{
items.Add(new() { Label = "City", Value = _model.City });
}

items.Add(new() { Label = "Newsletter", Value = _model.SubscribeToNewsletter ? "Yes" : "No" });

Expand Down Expand Up @@ -227,4 +229,4 @@ private static string GetGeneratedCodeStatic()

return code;
}
}
}
Loading
Loading