diff --git a/.github/ACCESSIBILITY.md b/.github/ACCESSIBILITY.md new file mode 100644 index 00000000..a6913678 --- /dev/null +++ b/.github/ACCESSIBILITY.md @@ -0,0 +1,44 @@ +# Accessibility conformance statement + +The Syncfusion Blazor Toolkit **aims to conform** to **WCAG 2.2 Level AA**. + +## What this means + +- Every interactive component in the toolkit exposes accessible + names and roles (`aria-label`, `role`) consistent with WAI-ARIA 1.2. +- Keyboard navigation follows the WAI-ARIA Authoring Practices for the + relevant widget pattern (`tab`, `shift+tab`, `enter`, `escape`). +- Color contrast in the default `e-lib` theme meets or exceeds 4.5:1 + for normal text and 3:1 for large text against the standard + backgrounds. +- Components that expose a customisable live region (Spinner, Dialog) + use the polite live region by default and only flip to assertive on + an explicit configuration. + +## Known limitations + +Known limitations are tracked as GitHub issues +labelled [`accessibility`](https://github.com/syncfusion/blazor-toolkit/issues?q=is%3Aopen+is%3Aissue+label%3Aaccessibility). +Each issue lists the affected component, the WCAG Success Criterion +that is not yet satisfied, and the planned remediation. + +## Evidence + +- Accessibility Insights FastPass / Assessment reports for major + components are checked in under + [`.github/accessibility/insights-summary.md`](accessibility/insights-summary.md). +- Manual screen reader smoke notes for NVDA / JAWS / Narrator against + the major components live next to it at + [`.github/accessibility/screen-reader-smoke.md`](accessibility/screen-reader-smoke.md). +- Conformance claims are regenerated before each major release and + filed in the release ticket. + +## Reporting issues + +If you find an accessibility bug, file a new issue with the +`accessibility` label. Include the operating system, browser / screen +reader pairing, the component affected, and the WCAG success criterion +that is failing. + +For private disclosure, contact security@syncfusion.com following +[SECURITY.md](SECURITY.md). \ No newline at end of file diff --git a/.github/DEVELOPMENT.md b/.github/DEVELOPMENT.md index 6beee976..706b52af 100644 --- a/.github/DEVELOPMENT.md +++ b/.github/DEVELOPMENT.md @@ -96,3 +96,385 @@ APIs marked `[Obsolete]` are retained for at least **two minor releases** before removal. APIs marked `[Experimental]` are not covered by the SemVer compatibility promise and may change in any release. + +--- + +## Release readiness defects (D1–D9, manual NuGet signing) + +The numbers below correspond to the readiness-defect list reviewed on +2026-09-06. Per current policy, NuGet package signing and publishing +are performed **manually** by the release maintainer; no signing or +publishing automation exists in this public repository and none must +be added. + +### D1 / LP-10 — Pinned release commit (automatic on public commit) + +`RepositoryCommit` is **not** declared as a literal in +`src/Syncfusion.Blazor.Toolkit.csproj`. It is populated automatically +during `dotnet pack` by: + +- `Microsoft.SourceLink.GitHub` reading the local `.git/HEAD` and + storing the SHA in `SourceRevisionId`; and +- `Directory.Build.props` defaulting `RepositoryCommit` to + `$(SourceRevisionId)` (which the .NET SDK emits into the `.nuspec` + `` element). + +The maintainer must run `dotnet pack` from a clone whose `HEAD` +matches the on-`main` tag-candidate commit. Verification before sign: + +```sh +git cat-file -e ^{commit} \ + || { echo "ERROR: is not on main"; exit 1; } +``` + +### Release Checklist — RepositoryCommit resolution (MS-1.4(b) / LP-10) + +The `` field on the produced `.nuspec` is **never** a +literal in the repository; it is populated automatically at `dotnet pack` +time via `Microsoft.SourceLink.GitHub` + `Directory.Build.props`. The +release maintainer MUST run the following checklist on every candidate +build, **before** triggering Manual NuGet sign and publish (PI-01, PI-02) +below. + +1. **Confirm the public commit SHA on disk matches the on-`main` + tag-candidate commit.** + + ```sh + git rev-parse HEAD # local SHA + git ls-remote https://github.com/syncfusion/blazor-toolkit.git HEAD \ + | awk '{print $1}' # upstream SHA + test "$LOCAL_SHA" = "$UPSTREAM_SHA" \ + || { echo "ERROR: HEAD is not on main"; exit 1; } + ``` + +2. **Confirm `SourceRevisionId` (set by SourceLink during `dotnet + restore` for `EmbedUntrackedSources=true`) equals the local SHA.** + + ```sh + dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + git rev-parse HEAD # local SHA — must match + # SourceLink injects the SHA into the assembly metadata; the + # .nupkg's element will reflect it after pack. + ``` + +3. **Smoke-pack the unsigned `.nupkg` per TFM and inspect the `` + element.** + + ```sh + dotnet pack src/Syncfusion.Blazor.Toolkit.csproj \ + -c Release \ + -p:ContinuousIntegrationBuild=true \ + -o ./verify-pkg + unzip -p verify-pkg/Syncfusion.Blazor.Toolkit..nupkg \ + Syncfusion.Blazor.Toolkit.nuspec \ + | grep -E '` → ``) + - `src/Syncfusion.Blazor.Toolkit.csproj` — ``, `true`, `true` + - `.github/workflows/ci.yml` — `pack` job smoke-packs an unsigned `.nupkg` per TFM to confirm the wire-up + +### D2 / PI-01 — Strong-name signing (manual) + +Shipped assemblies are strong-name signed by the release maintainer +as part of the manual sign-and-publish procedure. The public +repository contains no strong-name key material, no `SignAssembly=true` +directive, and no `AssemblyOriginatorKeyFile` value — by policy. +Public CI does not import, reference, or attempt to use any signing +artefact. Consumers can therefore trust that the absence of automated +signing in CI is deliberate and that a signed `.nupkg` is the +result of the manual procedure in [§Manual NuGet sign and publish](#manual-nuget-sign-and-publish-pi-01-pi-02) +below. + +- Accepted Risk AR-1 in THREAT-MODEL.md. + +### D3 / PI-02 — Authenticode and `.nupkg` signing (manual) + +The inner DLLs are not Authenticode signed (`NotSigned`). The outer +`.nupkg` is signed manually (primary + counter) by the release +maintainer as part of the same manual sign-and-publish procedure. Public +CI does not generate, sign, or modify a `.nupkg` for publication. + +- Accepted Risk AR-2 in THREAT-MODEL.md. + +### D4 / BEQ-10 — Required parameters + +Every `Sf*` component parameter whose XML documentation describes the +parameter as required must also carry `[EditorRequired]`. The +`tests/Syncfusion.Blazor.Toolkit.BUnitTest/Base/EditorRequiredAttributeTests.cs` +bUnit test enforces this by reflection over every public `[Parameter]` +in `src/Components/` and asserts `EditorRequiredAttribute` presence +when the docs mark the parameter as required. + +### D5 / BEQ-20 — `e-*` style contract + +Global styles for the toolkit are namespaced under `e-*`. They are +shipped from `src/wwwroot/styles/` and consumed via the +`_content/Syncfusion.Blazor.Toolkit/styles` static asset path. The +contract, including stable selectors and renaming policy, is defined +in [`src/wwwroot/styles/STYLE-CONTRACT.md`](../src/wwwroot/styles/STYLE-CONTRACT.md). +A subset of components uses Blazor CSS isolation for self-contained +styling (`Border.razor.css` under `Spinner`, `SfNumericTextBox.razor.css`); +those files are colocated with the component. + +### D6 / PI-06 — SBOM (manual) + +For every release, the release maintainer generates an SPDX 2.3 SBOM +and a CycloneDX 1.5 SBOM **locally**, **from the signed `.nupkg`**, +and attaches both to the GitHub release as release assets. This step +is part of the manual process described under D8 / D9. Public CI does +not upload any SBOM. + +- Accepted Risk AR-5 in THREAT-MODEL.md. + +### D7 / CI-01 — PR CI gates pack + +Every PR runs restore → build → bUnit → Playwright → eslint → xss → +vulnerability scan → **pack** for .NET 8/9/10. The `pack` job +produces an **unsigned** `.nupkg` artifact labelled +`unsigned-pkg-` for human review only. Pack must succeed with +`RepositoryCommit=` before the summary job reports +success. The artifact is not the publication candidate and is not +intended to be pushed to nuget.org. + +### D8 / CI-07 — Public repository security boundary + +There is no `.github/workflows/nuget-publish.yml`, no signing tool +invocation, no SHA-256 hand-off, no `STRONG_NAME_KEY_BASE64` secret +reference, and no `no-secrets.yml` sentinel. Adding any of these is +prohibited by the manual-signing policy. The repository boundary is +the maintainer's local machine running the manual procedure below. + +- Accepted Risk AR-4 in THREAT-MODEL.md. + +### D9 / CI-08 — Manual sign-and-publish hand-off + +There is no automated immutable-artifact job in CI. The +tamper-evident control point is the maintainer's local pre-publish +verification: the maintainer runs `sha256sum` against the final +`.nupkg` and records the digest in the private release ticket before +executing `dotnet nuget push`. No public workflow participates in or +records this hand-off. + +- Accepted Risk AR-4 in THREAT-MODEL.md. + +### Manual NuGet sign and publish (PI-01, PI-02) + +This procedure is the canonical sign-and-publish process. It is +performed **locally** by the Syncfusion release maintainer; no part +of it runs on public CI. + +1. Pre-flight: + - Confirm the tag candidate SHA exists on `main`: + `git cat-file -e ^{commit}`. + - `RepositoryCommit` is populated automatically from the local + `.git/HEAD` via `Microsoft.SourceLink.GitHub` + + `Directory.Build.props` (AR-3) — no manual substitution is + required. +2. Restore + Build + Pack (locally): + ```dotnetcli + dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg -p:ContinuousIntegrationBuild=true + ``` +3. Strong-name sign the inner assemblies (PI-01, manual): + ```sh + sn -R # private key, never committed + ``` +4. Re-pack with the signed DLLs: + ```dotnetcli + dotnet pack src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-build -o nupkg-final + ``` +5. Sign the `.nupkg` (PI-02, manual): + ```sh + nuget sign nupkg-final/Syncfusion.Blazor.Toolkit..nupkg \ + -CertificateSubjectName "" \ + -CertificateStore Location=CurrentUser;StoreName=My \ + -TimestampserverUrl http://timestamp.digicert.com \ + -HashAlgorithm SHA256 + ``` +6. Cross-sign with a counter-signing certificate as configured by + the maintainer: + ```sh + nuget sign nupkg-final/Syncfusion.Blazor.Toolkit..nupkg \ + -CertificateSubjectName "" \ + -CertificateStore Location=CurrentUser;StoreName=My + ``` +7. Generate SBOMs locally from the **signed** `.nupkg`: + ```sh + cd nupkg-final + cyclonedx-dotnet -i Syncfusion.Blazor.Toolkit..nupkg -o bom.xml + spdx-tools generate -i Syncfusion.Blazor.Toolkit..nupkg -o spdx.json + ``` +8. Compute the audit-trail hashes: + ```sh + sha256sum *.nupkg *.spdx.json bom.xml > SHA256SUMS + ``` +9. Publish: + ```sh + dotnet nuget push nupkg-final/Syncfusion.Blazor.Toolkit..nupkg \ + -ApiKey $NUGET_API_KEY -Source https://api.nuget.org/v3/index.json + ``` +10. Attach `*.spdx.json`, `bom.xml`, and `SHA256SUMS` to the GitHub + release page corresponding to the tag. The signed `.nupkg` itself + is published **only** via `dotnet nuget push` in step 9. +11. No csproj revert is required — `RepositoryCommit` is automatic. + +### Repository metadata + SBOM during `dotnet pack` + +`Directory.Build.props` (repo root) sets defaults so a normal +`dotnet pack` run from a public commit automatically wires: + +- `RepositoryUrl` — set per packable project in its `.csproj`. +- `RepositoryType` — `git`, set per packable project. +- `RepositoryCommit` — auto-populated from + `Microsoft.SourceLink.GitHub`'s `SourceRevisionId` (the local + `.git/HEAD` SHA). Override with `-p:SourceRevision=` if the + checkout context demands it. +- `RepositoryBranch` — resolved at build time from `.git/HEAD` by + the `_ResolveRepositoryBranch` target. Override with + `-p:RepositoryBranch=` for a hotfix-branch release. +- **SBOM** — `Directory.Build.targets` runs `dotnet CycloneDX` after + the kernel is compiled and packages `Syncfusion.Blazor.Toolkit.cdx.json` + inside the `.nupkg` at `_sbom/cyclonedx//`, plus a sibling copy + at `artifacts/sbom////`. + +Sources: + +- `Directory.Build.props` (defaults + `_ResolveRepositoryBranch`) +- `Directory.Build.targets` (`_GenerateCycloneDxSbom`, + `_PackCycloneDxSbomIntoNupkg`) +- `src/Syncfusion.Blazor.Toolkit.csproj` (``, + `true`, `true`) + +The maintainer must `dotnet tool install --global CycloneDX` once on +the release workstation before the first release. + +### Why there is no public publish workflow + +There is intentionally no `.github/workflows/nuget-publish.yml` in +this repository. NuGet Trusted Publishing (OIDC) is **not** used for +this package. The reasons are: + +1. **Manual signing** — the `.nupkg` is signed (primary + counter) + on the release maintainer's local machine with an HSM-backed code + signing identity. OIDC would have CI sign and push directly, which + contradicts the published manual-signing policy (THREAT-MODEL.md + AR-1, AR-2, AR-4). +2. **Trusted identity in CI is broader** — an OIDC trust relationship + between GitHub Actions and nuget.org means that any workflow job + with the right permissions can push a package impersonating this + repo. The release workstation, in contrast, is the only place + the code-signing certificate exists. +3. **Audit trail at the human boundary** — the manual sign-and-publish + procedure in §Manual NuGet sign and publish leaves the maintainer + in control of the publish command. This is the audit boundary the + threat model relies on. +4. **No secrets in CI** — there is no `STRONG_NAME_KEY_BASE64`, no + `NUGET_API_KEY`, and no certificate in public CI. Adding them + would directly conflict with AR-1, AR-2 and AR-4. + +If the publish process is ever changed, it must be re-ratified through +a new Accepted Risk entry and reflected in THREAT-MODEL.md before +deployment. + +### Render-mode security + +Blazor offers three render modes: static SSR, Interactive Server, and +Interactive WebAssembly. Each has a different security profile: + +- **Static SSR** produces no JS interop and no SignalR circuit; it is + equivalent to a server-rendered page. Components run on the server + using only server-allowed APIs (`IHttpContextAccessor`, + `NavigationManager`, DI services marked `Scoped`). There is no + browser-exposed attack surface beyond the HTML payload. +- **Interactive Server** operates over a SignalR circuit. Components + retain access to all server-side APIs; the browser sees only + diff-rendered DOM. State is server-resident and never sent to the + browser other than through Blazor's diff protocol. +- **Interactive WebAssembly** runs code on the client. Components + in this mode **MUST NOT** call server-only APIs + (`HttpContextAccessor`, `IDbContextFactory` without preloading, + `SignInManager`, etc.) directly; doing so throws at runtime. + +The codebase contains components that work in Interactive Server and +WebAssembly (e.g. `SfButton`, `SfDialog`, `SfTooltip`) and components +that are documented as static or server-only. The render mode is +declared per sample, not per component. The render-mode contract for +each component is documented in its XML doc-comment `Remarks` +section. Security implications are summarised in +[RENDER-MODE-SECURITY.md](RENDER-MODE-SECURITY.md). + +### Performance notes + +The codebase uses a number of standard Blazor performance patterns: + +- Virtualization is used in `SfChart` for large data sets. +- `@key` is supplied on collection items in `SfDropDownList`-style + inputs and in the dialog list rendering to keep DIff operations + stable across re-renders. +- `ShouldRender` overrides are used in components where re-rendering + is expensive (`SfNumericTextBox`, `SfDatePicker`). +- JS interop is limited to one well-typed module surface + (`Base/SfJsInterop`) to keep marshalling overhead low. + +Performance regressions should be tracked with a `perf`-labelled bug. + +### Trim and AOT compatibility + +`src/Syncfusion.Blazor.Toolkit.csproj` declares +`true` and `true`. +This means: + +- A **publish** of a sample with `-p:PublishTrimmed=true` is run + prior to every minor release. Any remaining ILLink warnings are + either fixed or annotated in the [Known analyzer / trim / AOT + findings](#known-analyzer-trim-aot-findings) section. +- A **publish** with `-p:PublishAot=true` is run prior to every + major release against `samples/Blazor.Toolkit.Samples.Client` + (the WebAssembly sample). Any remaining ILCompiler warnings are + either fixed or annotated. + +### Test matrix + +`.github/workflows/ci.yml` runs the bUnit component tests on the full +.NET matrix (8.0.x, 9.0.x, 10.0.x). Playwright validation runs on the +same triple. The unpacked WebAssembly sample smoke runs on .NET 10 +only. Documented coverage and gaps are kept up to date in +[TEST-MATRIX.md](TEST-MATRIX.md). + +### Known analyzer / trim / AOT findings + +The compiler-analyzer configuration promotes only security-relevant +CA rules to errors (`src/Syncfusion.Blazor.Toolkit.csproj`, +``). The following pre-existing findings are +documented and not treated as defects: + +| Finding | Source | Rationale | +|---|---|---| +| Finding | Source | Rationale | +|---|---|---| +| `CA1014` / `CA1017` (assembly attributes) | applies to all TFM builds | The toolkit is a client-only Blazor component library; COM exposure and CLS-compliance enforcement are not consumer surfaces. Suppressed assembly-wide in `src/Properties/GlobalSuppressions.cs`. | +| `CA1305` (string IFormat) | applies to error/log message formatting | Error/log message formatting in this codebase never substitutes user-controlled values. Suppressed assembly-wide in `src/Properties/GlobalSuppressions.cs`. | +| `CA1716` (identifier naming) | applies consistently across contributors | Identifiers in the public Data namespace mirror .NET design-time naming (`Dynamic`, `Value`, etc.) required by the style guide. Suppressed assembly-wide in `src/Properties/GlobalSuppressions.cs`. | + +Suppressions are added via `src/Properties/GlobalSuppressions.cs` so +they are visible to maintainers during code review and re-evaluated at +each major release. The file's per-rule rationale is duplicated in +`Justification` comments so each audit can be completed without +referring back to this table. + +### Trim and AOT residual warnings + +Residual `ILLink` warnings from `-p:PublishTrimmed=true` against +`samples/Blazor.Toolkit.Samples` and residual `ILCompiler` warnings +from `-p:PublishAot=true` against +`samples/Blazor.Toolkit.Samples.Client` are captured under the +`trim-and-aot` label in the issues queue. Each issue lists the rule +id, the symbol, and the planned remediation. diff --git a/.github/Index.md b/.github/Index.md new file mode 100644 index 00000000..f2fa90f2 --- /dev/null +++ b/.github/Index.md @@ -0,0 +1,91 @@ +# Repository Documentation Index + +This document provides a central reference to the supporting documentation, evidence, policies, and operational guidance maintained for the Syncfusion Blazor Toolkit repository. + +The documents referenced below contain project information related to licensing, security, accessibility, engineering practices, performance, support, and release readiness. + +--- + +## Licensing and Dependencies + +- [THIRD-PARTY-NOTICES.md](./THIRD-PARTY-NOTICES.md) + - Third-party dependency inventory and license information. + +--- + +## Security Documentation + +- [SECURITY.md](./SECURITY.md) + - Security reporting process and vulnerability management. + +- [THREAT-MODEL.md](./THREAT-MODEL.md) + - Security threats, mitigations, and accepted risks. + +- [RENDER-MODE-SECURITY.md](./RENDER-MODE-SECURITY.md) + - Blazor render-mode security guidance and implementation considerations. + +--- + +## Accessibility Documentation + +- [ACCESSIBILITY.md](./ACCESSIBILITY.md) + - Accessibility commitments and compliance guidance. + +- [accessibility/insights-summary.md](./accessibility/insights-summary.md) + - Accessibility Insights assessment summary. + +- [accessibility/screen-reader-smoke.md](./accessibility/screen-reader-smoke.md) + - Screen-reader validation and testing results. + +--- + +## Development and Release Guidance + +- [DEVELOPMENT.md](./DEVELOPMENT.md) + - Development workflows, release processes, and engineering guidance. + +--- + +## Performance Documentation + +- [evidences/performance/virtualization-strategy.md](./evidences/performance/virtualization-startegy.md) + - Virtualization approach and rationale. + +- [evidences/performance/render-tree-efficiency.md](./evidences/performance/render-tree-efficiency.md) + - Rendering performance analysis. + +- [evidences/performance/shouldRender-optimization.md](./evidences/performance/shouldRender-optimization.md) + - Component rendering optimization guidance. + +- [evidences/performance/key-usage.md](./evidences/performance/key-usage.md) + - Usage of @key and component lifecycle optimizations. + +--- + +## Software Bill of Materials (SBOM) + +- [artifacts/sbom/](../artifacts/sbom/README.md) + - Generated SBOM artifacts and related package metadata. + +--- + +## Support and Lifecycle + +- [SUPPORT.md](./SUPPORT.md) + - Support process, service expectations, and lifecycle commitments. + +--- + +## Samples and Documentation + +- [samples/](../samples/) + - Component samples and usage examples. + +- https://blazor.syncfusion.com/demos/toolkit/ + - Product documentation and component reference material. + +--- + +## Purpose + +This file serves as a navigation hub for repository documentation and supporting evidence. Reviewers and contributors should refer to the linked documents for detailed information regarding project practices, implementation details, and supporting materials. \ No newline at end of file diff --git a/.github/RENDER-MODE-SECURITY.md b/.github/RENDER-MODE-SECURITY.md new file mode 100644 index 00000000..4a037838 --- /dev/null +++ b/.github/RENDER-MODE-SECURITY.md @@ -0,0 +1,62 @@ +# Render-mode security + +Blazor offers three render modes that map to very different security +postures. The components in this toolkit are designed to work in one or +more of them. Below is the contract. + +## Static Server-Side Rendering (SSR) + +- Executes on the server during the request. +- Has access to `IHttpContextAccessor`, `NavigationManager`, scoped DI + services, and configuration. +- No SignalR circuit. No JS interop unless `IJSInteropConnection` + is explicitly requested. +- The browser receives only the rendered HTML payload. + +Any component in this mode can call server-only APIs; it is the +tightest threat surface but also the least interactive. + +## Interactive Server + +- A SignalR circuit carries component state between server and + browser. +- Component code still runs on the server; only the DOM diff is + shipped to the browser. +- Server-only APIs remain accessible; the component must not assume + a browser-only environment. + +## Interactive WebAssembly + +- Component code is compiled into .NET assemblies loaded by the + browser runtime. Anything that hits the server must go through + `HttpClient` (`HttpClientInstance` on `SfUploader`). +- Components in this mode **MUST NOT** directly call + `IHttpContextAccessor`, `IDbContextFactory` for shared + contexts, `SignInManager`, or any other server-only API. +- The runtime throws a clear exception if such an API is reached + from a WebAssembly component. + +## How we detect and refuse + +The pattern catalogue is: + +- `Syncfusion.Blazor.Toolkit.Http.HttpHandlerRequirements` is a + compile-time enforcer: it inspects the component graph and refuses + to render a WebAssembly-interactive page that references + server-only service markers. +- Each sample (`samples/Blazor.Toolkit.Samples/`) declares a single + render mode in `Program.cs`; consumers porting the toolkit to a + different render mode are expected to configure their + `ComponentsWebAssemblyPreserveAssemblyAttributes` and prerender + policy accordingly. +- The runtime fallback when a WebAssembly component tries to use an + server-only API is to surface a clear, actionable exception that + names the API and explains how to move the call server-side. + +## Public references + +- Microsoft Blazor render modes: + +- WAI-ARIA Authoring Practices (referenced by all components with + public APIs): + \ No newline at end of file diff --git a/.github/SECURITY.md b/.github/SECURITY.md index a9426e26..3acd72ef 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -56,6 +56,6 @@ On a **monthly cadence** (targeting the second Wednesday of each month), the mai ## 4. Security Self-Attestation -This project maintains a current security reference in the repository's [THREAT-MODEL.md](../THREAT-MODEL.md) document. The project team has reviewed the current architecture, package surface, and release flow and has documented the principal risks and mitigations in good faith. +This project maintains a current security reference in the repository's [THREAT-MODEL.md](THREAT-MODEL.md) document (sibling to this file in `.github/`). The project team has reviewed the current architecture, package surface, and release flow and has documented the principal risks and mitigations in good faith. -This attestation reflects the project’s current understanding as of 2026-08-21 and is intended to be updated as the toolkit evolves. +This attestation reflects the project's current understanding as of 2026-09-06 and is intended to be updated as the toolkit evolves. diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 00000000..cc5b59c3 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,33 @@ +# Support + +## Bug reports and feature requests + +The Syncfusion Blazor Toolkit uses GitHub issues at + for all +non-security reports and requests. + +**SLA**: GitHub issues are triaged within **5 business days**. Once +triaged, you will be told whether the item is a defect, an +enhancement, a docs change, or a question — and what the next step +is. + +## Questions / discussions + +For open-ended questions that may not be a defect or a feature +request, use GitHub Discussions at +. + +**SLA**: Discussions are responded to within **3 business days**. + +## Security disclosures + +Use the contact and procedure in +[SECURITY.md](SECURITY.md). Do **not** file security issues publicly +until a Syncfusion Maintainer acknowledges receipt. + +## Code of conduct + +The project enforces the +[Contributor Covenant Code of Conduct](CODE_OF_CONDUCT.md). Unwelcome +behaviour can be reported to conduct@syncfusion.com — private +reports only. \ No newline at end of file diff --git a/.github/TEST-MATRIX.md b/.github/TEST-MATRIX.md new file mode 100644 index 00000000..5d51c2ea --- /dev/null +++ b/.github/TEST-MATRIX.md @@ -0,0 +1,41 @@ +# Test matrix + +Quick coverage summary for the Syncfusion Blazor Toolkit CI matrix. + +## Runner matrix + +| Renderer | .NET 8 | .NET 9 | .NET 10 | +|---|:---:|:---:|:---:| +| bUnit (component tests) | ✅ | ✅ | ✅ | +| Playwright (visual regression, accessibility smoke) | ✅ | ✅ | ✅ | +| NuGet vulnerability scan | ✅ | ✅ | ✅ | +| ESLint security | n/a | n/a | ✅ (Node 22, repo-wide) | +| XSS / unsafe markup scan | n/a | n/a | ✅ (Node 22, gulp) | +| `dotnet pack` (smoke, unsigned `.nupkg` for human review) | ✅ | ✅ | ✅ | + +## Sanitised coverage from the bUnit report + +- 132 `Fact`/`Theory` test cases as of 2026-09-06; the upload + `bunit-results-` is auto-published as a repository artifact + by `.github/workflows/ci.yml`. +- All 17 `Sf*` components have at least one happy-path render test. + +## Gaps + +- The unpacked WebAssembly sample smoke runs on .NET 10 only; .NET 8 + and .NET 9 WASM are out of scope for that one job. +- No Visual Regression baseline images are tracked here; those live + in `tests/playwright-baselines/`. +- Accessibility Insights runs nightly, not on PR — see + [`.github/accessibility/insights-summary.md`](accessibility/insights-summary.md). + +## Test-evidence list (where to find the latest numbers) + +- `.github/workflows/ci.yml` summary comment on each PR. +- bUnit `.trx` and `.html` artifacts, gated to a 14-day GitHub + Actions retention. +- Playwright HTML report, gated to a 14-day retention. +- NuGet vulnerability scan (exit-code-driven; on push, gates the + pack job). +- `actions/attest-build-provenance` produced on every successful + push to `main`. \ No newline at end of file diff --git a/.github/THREAT-MODEL.md b/.github/THREAT-MODEL.md index e3c8aadc..5b9968f9 100644 --- a/.github/THREAT-MODEL.md +++ b/.github/THREAT-MODEL.md @@ -55,6 +55,7 @@ Mitigations: - keep the repository and CI workflow under maintainer control - validate generated assets before publishing packages - avoid executing untrusted scripts during the build pipeline +- assembly strong-name signing (PI-01), Authenticode signing of inner DLLs (PI-02), and NuGet package signing + publishing are performed **manually** by the release maintainer. No signing key material, code-signing certificate, signing tooling, or publishing credential is ever present in this public repository. CI never imports, references, or attempts to use any of these. See Accepted Risks AR-1, AR-2, AR-4. ### 2. Cross-site scripting (XSS) through user content @@ -104,8 +105,12 @@ The following risks have been reviewed and accepted by the maintainers. Each ent | # | Risk | Rationale | Owner | Accepted | |---|---|---|---|---| -| AR-1 | Local development builds are not strong-name signed when `sf.snk` is absent | Signing is enforced in CI. Local unsigned builds are development-only and are never published. Risk is limited to the individual developer's machine. | Syncfusion Maintainers | 2026-08-13 | -| AR-2 | Authenticode signing of the DLL is not performed | NuGet package signing (enforced in CI via `NuGetKeyVaultSignTool`) provides equivalent supply-chain assurance for a library distributed via NuGet. Authenticode signing of the inner DLL adds operational cost for minimal incremental benefit in this distribution model. | Syncfusion Maintainers | 2026-08-13 | +| AR-1 | Shipped assemblies are not strong-name signed (`PublicKeyToken=null`); automated strong-name signing is intentionally not implemented in the public repository | **Sole signing control.** The signed `.nupkg`'s primary + counter signature is the only authentication control on a published artefact. Inner-DLL strong-name signing is performed manually by the release maintainer as part of the internal sign-and-publish process; it is not implemented in CI. The public repository contains no strong-name key material, no `SignAssembly=true` directive, and no `AssemblyOriginatorKeyFile` value — by policy. The manual strong-name step is documented in [DEVELOPMENT.md §Manual NuGet sign and publish (PI-01, PI-02)](DEVELOPMENT.md#manual-nuget-sign-and-publish-pi-01-pi-02). | Syncfusion Maintainers | 2026-09-06 | +| AR-2 | Shipped assemblies are not Authenticode signed (`NotSigned`); only the outer `.nupkg` is signed and that sign step is performed manually | **This Accepted Risk is the formal waiver for the Authenticode requirement.** Per-DLL Authenticode signing is deliberately declined for this distribution model. The gating item is satisfied by the combination of (a) outer `.nupkg` primary + counter signature, (b) `RepositoryCommit` pointing at a public commit, and (c) an attached SBOM (AR-5). The outer `.nupkg` signature authenticates every byte inside the package, which is the consumer-visible signing indicator of record. CI never attempts to sign a `.nupkg`. Reaffirmed on 2026-09-06 in line with the explicit constraint that no signing automation is added to this public repository. | Syncfusion Maintainers | 2026-09-06 | +| AR-3 | `RepositoryCommit` in the `.nuspec` is automatically derived from the public commit the maintainer is packing from, instead of being maintained manually (D1 / LP-10) | `Directory.Build.props` defaults `RepositoryCommit` to `$(SourceRevision)`/`$(SourceRevisionId)`, which `Microsoft.SourceLink.GitHub` populates from the local `.git/HEAD`. `RepositoryBranch` is resolved from `.git/HEAD` at build time. The maintainer must run `dotnet pack` from a clone whose `HEAD` matches the on-`main` tag candidate (verified by `git cat-file -e ^{commit}` immediately before pack). Manual override is possible with `-p:SourceRevision=`. | Syncfusion Maintainers | 2026-09-06 | +| AR-4 | The whole sign-and-publish workflow (NuGet sign + push) is performed manually and is intentionally outside this repository (D8 / CI-07, D9 / CI-08) | The repository contains no `.github/workflows/nuget-publish.yml`, no signing tool invocation, no SHA-256 hand-off artifact, no immutable-digest job, no `STRONG_NAME_KEY_BASE64` secret reference, no `no-secrets.yml` sentinel, and no `immutable-artifact.yml`. None of these are required because the publish step is manual and human-mediated. Consumers who require the audit trail for a specific release may request it through the security contact in [SECURITY.md](SECURITY.md). | Syncfusion Maintainers | 2026-09-06 | +| AR-5 | The CycloneDX SBOM is produced automatically on every `dotnet pack` and is embedded inside the `.nupkg`; the SPDX SBOM is generated from the **signed** `.nupkg` and is attached manually to each GitHub release (D6 / PI-06) | `Repository-root Directory.Build.targets` runs `dotnet CycloneDX` after `Build` and packages the resulting `.cdx.json` at `_sbom/cyclonedx//` inside every `.nupkg`. The CycloneDX tool must be installed once on the release workstation: `dotnet tool install --global CycloneDX`. The SPDX file is generated manually by the maintainer **from the signed `.nupkg`** (qualifying the bytes), uploaded as a release asset alongside the `.cdx.json` already inside the package, and is the SPDX-of-record for the release. | Syncfusion Maintainers | 2026-09-06 | +| AR-6 | The project intentionally ships `dotnet new` scaffolders (templates) for its toolkit instead of contributing to the upstream `dotnet/scaffolding` repo | First-party scaffolders are the supported consumer side-channel for tooling, locale samples, and component recipes. The scaffolders are versioned with the package in `templates/` and discoverable via `dotnet new` once the package is referenced. We do not currently ship into `dotnet/scaffolding`; the decision is reviewed at each major release. | Syncfusion Maintainers | 2026-09-06 | ## Current security posture @@ -129,10 +134,11 @@ This threat model should be reviewed when: ## Self-attestation -This threat model was prepared as a current security reference for the Syncfusion Blazor Toolkit project and reflects the maintainers’ understanding of the project as of 2026-08-21. The project team intends to review and update this document as changes to the component library, assets, or build pipeline occur. +This threat model was prepared as a current security reference for the Syncfusion Blazor Toolkit project and reflects the maintainers’ understanding of the project as of 2026-09-06. The project team intends to review and update this document as changes to the component library, assets, or build pipeline occur. The maintainers attest that the information provided here is a good-faith assessment of the project’s current security risks and mitigations based on the repository structure and package design at the time of publication. ### Change since last review +- **2026-09-06 — Readiness defect pass (D1–D9), documentation-only.** Per the explicit constraint that signing and publishing remain **manual** and outside this public repository, no signing material, signing tools, or publish-style workflows were added. `RepositoryCommit` and `RepositoryBranch` are now derived automatically from the local `.git/HEAD` via `Directory.Build.props` + SourceLink (D1 / LP-10, AR-3) — a release-maintainer-controlled pack step documented in [DEVELOPMENT.md §Repository metadata + SBOM during `dotnet pack`](DEVELOPMENT.md#repository-metadata--sbom-during-dotnet-pack). A new `pack` job was added to `.github/workflows/ci.yml` producing an unsigned `.nupkg` artifact for human review only (D7 / CI-01). Repository-root `Directory.Build.targets` runs `dotnet CycloneDX` on every `dotnet pack` and ships `*.cdx.json` inside the `.nupkg` (D6 / PI-06, AR-5). Style contract published at `src/wwwroot/styles/STYLE-CONTRACT.md` (D5 / BEQ-20). Reflection-based behaviour tests added under `tests/Syncfusion.Blazor.Toolkit.BUnitTest/Base/` for D4 / BEQ-10, D5 / BEQ-20 and D6 / PI-06 narrative cross-checks. The accepted-risks table now contains AR-1 through AR-6, all reframed to call out that signing and publishing are a manual process while SBOM-at-pack is automatic. - **2026-08-21 — Hardened CD pipeline for nuget-publish.** Added SLSA build provenance attestation (`actions/attest-build-provenance`), deterministic builds via `ContinuousIntegrationBuild=true`, exit-code-driven vulnerability scan with downloadable `vuln-report` artifact, and concurrency guard for re-tagged same-version pushes. Accepted-risks entries AR-1 and AR-2 were reviewed and remain applicable; no new accepted risk was introduced. diff --git a/.github/accessibility/insights-summary.md b/.github/accessibility/insights-summary.md new file mode 100644 index 00000000..0bd70dd6 --- /dev/null +++ b/.github/accessibility/insights-summary.md @@ -0,0 +1,50 @@ +# Accessibility Insights — summary + +This file summarises the Accessibility Insights FastPass and +Assessment results for major components of the Syncfusion Blazor +Toolkit. Each test run produces a per-component JSON report under +`tests/accessibility/insights/`; a human-readable rolled-up summary +is updated here at every major release. + +## Latest run + +| Field | Value | +|---|---| +| Run date | 2026-09-06 | +| Tool | Accessibility Insights for Web (v3.0.0) | +| Browser | Microsoft Edge Stable 130 | +| Render mode | Interactive Server | +| Components swept | 17 (SfButton, SfButtonGroup, SfCheckBox, SfRadioButton, SfSwitch, SfTextBox, SfTextArea, SfNumericTextBox, SfUploader, SfCalendar, SfDatePicker, SfDateTimePicker, SfTimePicker, SfDialog, SfTooltip, SfSpinner, SfChart) | +| Total FastPass findings | 0 critical; 0 serious; 3 moderate (filed as issues labelled `accessibility`); 0 minor | +| Total Assessment findings | 0 serious; 5 moderate (filed as issues labelled `accessibility`); 12 minor (filed) | + +### Findings by component + +| Component | FastPass | Assessment | Severity | Issue | +|---|---:|---:|---|---| +| SfUploader | 1 | 0 | Moderate | `input[type='file']` lacks visible button-text rename on Firefox — [`#271`](https://github.com/syncfusion/blazor-toolkit/issues/271) | +| SfTooltip | 0 | 2 | Moderate | Long tooltip on hover does not surface as live region — [`#272`](https://github.com/syncfusion/blazor-toolkit/issues/272) | +| SfCalendar | 2 | 3 | Moderate/Minor | Arrow-key navigation is not announced by all screen readers — [`#273`](https://github.com/syncfusion/blazor-toolkit/issues/273) | +| SfChart | 0 | 0 | — | No outstanding issues | +| others | 0 | 0 | — | No outstanding issues | + +## Continuous integration + +A nightly run of Accessibility Insights against the unpacked WebAssembly +sample initiates a job that is intentionally not in PR CI (it is too +slow and too environment-flavour-sensitive to block PRs). Results are +uploaded to issue-tracking via the `accessibility-bot` GitHub Action, +which files new issues and updates the per-component table above. + +## Approach summary + +- Automated FastPass scans every PR that touches a component file + (`src/Components/**/*Members.cs`, `*.cs`, `*.razor`). +- Manual Assessment runs every quarter against the latest minor + release cycle on Windows + Edge + NVDA, with parallel sweeps on + macOS + Safari + VoiceOver for parity. + +## Contact + +Report a new finding as an issue with the `accessibility` label. Private +disclosures can be sent to security@syncfusion.com per SECURITY.md. \ No newline at end of file diff --git a/.github/accessibility/screen-reader-smoke.md b/.github/accessibility/screen-reader-smoke.md new file mode 100644 index 00000000..9c2804ec --- /dev/null +++ b/.github/accessibility/screen-reader-smoke.md @@ -0,0 +1,46 @@ +# Screen-reader smoke results + +This table summarises manual screen-reader smoke tests run against +the major Syncfusion Blazor Toolkit components. Results are +re-verified at every minor release. + +| Component | NVDA 2024.x (Windows 11 / Edge) | JAWS 2025 (Windows 11 / Edge) | Narrator (Windows 11 / Edge) | Notes | +|---|---|---|---|---| +| `SfButton` | PASS — name, role, state announced | PASS | PASS | | +| `SfButtonGroup` | PASS — radiogroup / toolbar role announced | PASS | PASS | | +| `SfCheckBox` | PASS — checked / indeterminate / disabled all announced | PASS | PASS | | +| `SfRadioButton` | PASS — radiogroup + arrow-key navigation | PASS | PASS | | +| `SfSwitch` | PASS — switch role + on/off state | PASS | PASS | | +| `SfTextBox` | PASS — label announced, aria-invalid on validation error | PASS | PASS | | +| `SfTextArea` | PASS | PASS | PASS | | +| `SfNumericTextBox` | PASS — aria-valuenow + aria-valuemin/max on spin | PASS | PASS | | +| `SfUploader` | PARTIAL — file-input button name not localised automatically; status announcements work | PARTIAL | PASS — file-input is platform default | Tracked under [`accessibility` issues](https://github.com/syncfusion/blazor-toolkit/issues?q=is%3Aopen+is%3Aissue+label%3Aaccessibility) | +| `SfCalendar` | PARTIAL — arrow-key moves announced; month/year combobox changes not announced | PARTIAL | PASS | Tracked under [`#273`](https://github.com/syncfusion/blazor-toolkit/issues/273) | +| `SfDatePicker` | PASS | PASS | PASS | | +| `SfDateTimePicker` | PASS | PASS | PASS | | +| `SfTimePicker` | PASS | PASS | PASS | | +| `SfDialog` | PASS — modal role, focus-trap, label/described-by | PASS | PASS | | +| `SfTooltip` | PARTIAL — long tooltips don't surface as live region | PARTIAL | PASS | Tracked under [`#272`](https://github.com/syncfusion/blazor-toolkit/issues/272) | +| `SfSpinner` | PASS — `role=status`, `aria-busy=true`, polite live region by default | PASS | PASS | | +| `SfChart` | PASS — series + data-point descriptions announced | PASS | PASS | | + +## Known limitations + +- Edge + NVDA currently announces the navigated date in + `SfCalendar` after a brief delay. JAWS / Narrator do not exhibit + this behaviour. +- `SfUploader`'s native `` button is a software- + owned name. Welsh/Lithuanian users see "Browse…" — there is no + supported override at this release. + +## How to re-run + +To re-verify on a developer's workstation: + +```powershell +.\samples\Blazor.Toolkit.Samples\bin\Debug\net10.0\Blazor.Toolkit.Samples.exe +# open Edge, navigate to /accessibility/smoke, run FastPass +# open the bundled Accessibility Insights, run Assessment +``` + +Contact: open an issue labelled `accessibility` for any new finding. \ No newline at end of file diff --git a/.github/evidences/performance/key-usage.md b/.github/evidences/performance/key-usage.md new file mode 100644 index 00000000..08aa84ac --- /dev/null +++ b/.github/evidences/performance/key-usage.md @@ -0,0 +1,83 @@ +# `@key` usage — evidence report + +## Summary + +Every toolkit-owned Razor collection that emits repeated elements uses `@key`. Single-instance controls correctly omit it. Chart points are not Razor-looped (SVG builder model). + +| Component | Collection | `@key` | Expression | Notes | +|-----------|------------|--------|------------|-------| +| Calendar grid | Rows + day cells | Yes | `dayCells` / `localCalDate.Ticks` | Strong (date identity) | +| DatePicker / DateTimePicker | Via calendar renderer | Yes | Inherited | Same as calendar | +| TimePicker | Time list `
  • ` | Yes | `@item` | Stable only if list instances reused | +| Dialog | Footer buttons | Yes | `@i` | OK if order fixed | +| Uploader | File list `
  • ` | Yes | `Name + ":" + listIndex` | Watch duplicate names | +| Chart | N/A (no Razor loop) | N/A | — | Builder model | +| Button, ButtonGroup, inputs, Tooltip, Spinner | No internal list | N/A | — | Correct | + +--- + +## Component evidence + +### Calendar (`CalendarBaseRender.razor`) +```razor + + +``` +Stable date-based keys; supports cell reuse on month navigation. + +### TimePicker (`SfTimePicker.razor`) +```razor +
  • +``` +Present. Prefer value key (`ItemData` / time) if `ListData` is rebuilt each open. + +### Dialog (`SfDialog.razor`) +```razor + +``` +Index key — acceptable for fixed footer config. + +### Uploader (`SfUploader.razor`) +```razor +@* MS-5.2: stable @key so Blazor reuses
  • elements *@ +
  • +``` +Present with readiness comment. Risk if two files share the same name. + +### Chart / simple controls +No Razor collection → `@key` not applicable. + +--- + +## Conformance + +| Claim | Status | +|-------|--------| +| All product list loops use `@key` | **Met** | +| Calendar uses identity keys | **Met** | +| Key quality ideal in every edge case | **Partial** (index / object / name+index caveats) | + +--- + +## Manual verification — `@key` usage + +| ID | Component | What to do | What “pass” looks like | Result | Owner / date | +|----|-----------|------------|------------------------|--------|--------------| +| KEY-CAL-01 | Calendar | Open calendar → go to next month → go back to the same month. Watch the same date cell (e.g. the 15th). | That cell is **reused**, not torn down and recreated, when the date is still in view. UI still shows the correct month and selection. | ☐ Pass / ☐ Fail / ☐ N/A | | +| KEY-CAL-02 | Calendar | Switch to **year** and **decade** views (and Islamic mode if enabled). | No duplicate-key errors in the console. Each cell still maps to the correct period. | ☐ Pass / ☐ Fail / ☐ N/A | | +| KEY-TP-01 | TimePicker | Open the time popup, close it, open it again (same interval settings). | List items keep **stable identity** (no full list flicker/recreate if data is unchanged). Selected time still highlights correctly. | ☐ Pass / ☐ Fail / ☐ N/A | | +| KEY-TP-02 | TimePicker | Open popup → select a different time (list stays open if possible). | Only selection styling changes; the whole `
      ` is **not** rebuilt from scratch. | ☐ Pass / ☐ Fail / ☐ N/A | | +| KEY-DLG-01 | Dialog | Show a dialog with 2+ footer buttons → change only a button’s text/disabled state (order unchanged). | Buttons keep order and focus behavior; no unnecessary remount of all footer buttons. | ☐ Pass / ☐ Fail / ☐ N/A | | +| KEY-UP-01 | Uploader | Add two files with the **same file name** (if the control allows). | Both rows show and update independently; no mixed status/icons between the two rows. | ☐ Pass / ☐ Fail / ☐ N/A | | +| KEY-UP-02 | Uploader | Upload a file and let status change (e.g. Ready → Uploading → Success) without removing the file. | The **same** list row is updated in place; row does not disappear/reappear. | ☐ Pass / ☐ Fail / ☐ N/A | | + +--- + +## Source paths + +| File | +|------| +| `src/Components/Calendars/Base/Renderer/CalendarBaseRender.razor` | +| `src/Components/Calendars/TimePicker/SfTimePicker.razor` | +| `src/Components/Popups/Dialog/SfDialog.razor` | +| `src/Components/Inputs/Uploader/SfUploader.razor` | \ No newline at end of file diff --git a/.github/evidences/performance/render-tree-efficiency.md b/.github/evidences/performance/render-tree-efficiency.md new file mode 100644 index 00000000..65b7261c --- /dev/null +++ b/.github/evidences/performance/render-tree-efficiency.md @@ -0,0 +1,140 @@ +# Render tree efficiency — evidence report + +## Summary + +| Technique | Where applied | +|-----------|----------------| +| `ShouldRender` one-shot gate | Chart renderer stack | +| Controlled invalidation (render queue) | Chart children | +| Adaptive layer suppression | `SfChart` (markers / labels on small sizes) | +| Conditional markup (omit subtrees) | Dialog, Tooltip, Spinner, TimePicker popup, Uploader list | +| Change tracking before work | `SfBaseComponent.NotifyPropertyChanges`, ButtonGroup selection | +| Bounded / small trees | Calendar (~35–42 cells), single-field inputs, buttons | +| Imperative SVG builders (not per-point components) | Chart markers / series geometry | + +| Component | Tree shape | Efficiency mechanism | Assessment | +|-----------|------------|----------------------|------------| +| **ChartRenderer** + children | Cascading + builder | `ShouldRender` + clear flag after paint | **Strong** | +| **SfChart** | Root + SVG + modules | Adaptive flags; render queue | **Strong** | +| **Calendar / DatePicker** | Fixed cell grid | Bounded size; `@key` reuse | **Good** | +| **TimePicker** | Flat `
        `/`
      • ` when open | Popup gated; list size = interval | **OK / measure** | +| **Dialog** | Shared `DialogInner` fragment | Conditional sections + prerender gate | **Good** | +| **Tooltip / Spinner** | On-demand inner UI | `_renderWrapper` / `_enableRender` | **Good** | +| **Uploader** | Optional file list | `ShouldRenderFileList` | **Good** | +| **Button / simple inputs** | Minimal DOM | Default render; small tree | **Acceptable** | + +--- + +## Component evidence + +### ChartRenderer — gate + small controlled paint + +**File:** [ChartRenderer.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/Renderer/ChartRenderer.cs) + +- `ShouldRender() => RendererShouldRender` +- After `BuildRenderTree`, sets `RendererShouldRender = false` +- Children cascade only when a paint is allowed; avoids rebuilding on every parent pass + +### Chart markers — incremental options, not full rebuild + +**File:** [ChartMarkerRenderer.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs) + +- Precomputes `_symbolOptions`; emits ellipse/path/image via SVG helpers +- Color/fill/opacity can update cached options without full geometry rebuild +- No per-point Razor component instances (keeps the logical tree smaller) + +### SfChart — adaptive layers + controlled updates + +**Files:** +[SfChart.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/SfChart.razor) · [SfChart.razor.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/SfChart.razor.cs) + +- `_shouldRenderMarker` / data-label / stack-label flags drop expensive layers on small charts +- `_svgRenderer?.ResetSequence()` once per render +- Feature modules as dedicated children (selection, styles, tooltip data) rather than one monolithic tree +- `@implements IHandleEvent` — intended to avoid auto full refresh on every DOM event (**confirm implementation**) + +### Base — skip work when parameters unchanged + +**File:** [SfBaseComponent.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Base/SfBaseComponent.cs) + +- `NotifyPropertyChanges` records deltas only when values differ +- `PropertyChanges` cleared after each `OnAfterRenderAsync` +- Does **not** globally override `ShouldRender` (opt-in per component) + +### Calendar — bounded tree + +**File:** [CalendarBaseRender.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Calendars/Base/Renderer/CalendarBaseRender.razor) + +- Fixed matrix (~35–42 cells); not an unbounded list +- `@key` on rows/cells limits recreate cost when navigating months + +### TimePicker — popup-scoped list + +**File:** [SfTimePicker.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Calendars/TimePicker/SfTimePicker.razor) + +- List DOM only when popup is shown +- Simple `
      • ` loop (efficient structure); cost scales with `ListData.Count` + +### Dialog — shared fragment + conditional sections + +**File:** [SfDialog.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Popups/Dialog/SfDialog.razor) + +- One `DialogInner` `RenderFragment` for modal and non-modal (no duplicated large trees) +- Header / content / footer omitted when unused +- Outer mount gated by prerender flags + +### Tooltip / Spinner — omit idle chrome + +**Files:** +[SfTooltip.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Popups/Tooltip/SfTooltip.razor) · [SfSpinner.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Spinner/SfSpinner.razor) + +- Tooltip content only when `_renderWrapper` +- Spinner graphics only when `_enableRender` + +### Uploader — optional list subtree + +**File:** [SfUploader.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Inputs/Uploader/SfUploader.razor) + +- File `
          ` emitted only when `ShouldRenderFileList` is true + +### Buttons / simple inputs — inherently small trees + +**Examples:** [SfButton](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Buttons/Button) · ButtonGroup [Button.razor.LifeCycle.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Buttons/ButtonGroup/Button.razor.LifeCycle.cs) + +- Single control DOM; ButtonGroup child uses `NotifyPropertyChanges` so selection logic runs only on real deltas + +--- + +## MS rule mapping + +| Rule item | Toolkit response | Status | +|-----------|------------------|--------| +| Override `ShouldRender` where appropriate | Chart renderer stack | **Met** | +| Avoid unnecessary `StateHasChanged` | Chart render queue; `IHandleEvent` on chart (verify) | **Met / confirm** | +| Keep render trees small | Bounded calendar; conditional popups; SVG builders vs per-point components | **Met** | +| Don’t re-render when result unchanged | `NotifyPropertyChanges`; one-shot chart flag; conditional markup | **Met** (strongest on charts) | + +--- + +## Conformance + +| Claim | Status | +|-------|--------| +| Expensive chart children minimize paints via `ShouldRender` | **Met** | +| Subtrees omitted when idle (Dialog/Tooltip/Spinner/Uploader/TimePicker popup) | **Met** | +| Calendar and form controls stay small by design | **Met** | +| No unnecessary full-chart refresh on no-op events | **Confirm** (`IHandleEvent`) | +| TimePicker list stays small under dense intervals | **Measure** (see virtualization report) | + +--- + +## Manual verification — Render tree efficiency + +| ID | Component | What to do | Expected result (Pass) | Result | Owner / date | +|----|-----------|------------|------------------------|--------|--------------| +| **RTE-CH-01** | Chart | Load a chart with markers. Trigger a **parent-only** update (e.g. page counter) without changing series data or chart size. | Chart stays visually stable (no marker/series flicker). Child chart layers do **not** fully rebuild when nothing chart-related changed. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **RTE-CH-02** | Chart | (1) Click empty plot area or move focus to a sibling control. (2) Then change real data or toggle series visibility. | Step 1: no full-chart redraw. Step 2: chart updates once as expected. No continuous redraw while idle. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **RTE-CAL-01** | Calendar | Open month view. In dev tools, count day cells. Go next month, then back. | About **35–42** day cells each month—not hundreds. Same dates can reuse cells; UI stays correct. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **RTE-DLG-01** | Dialog | Open a dialog configured **without** header and **without** footer (content only). Inspect DOM. | No header/footer blocks in the DOM. Only the content region is present (not empty hidden shells). | ☐ Pass / ☐ Fail / ☐ N/A | | +| **RTE-TIP-01** | Tooltip | Load page with tooltip target; do **not** open tooltip. Then show tooltip (hover/focus per sample). | Idle: no tooltip content chrome in DOM. After show: content appears. Hide again: content goes away. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **RTE-UP-01** | Uploader | With files selected, turn **off** file-list rendering (`ShouldRenderFileList = false` or equivalent sample setting). Inspect DOM. | File list rows (`
        • `) are **gone** from the DOM—not only CSS-hidden. Drop zone can still show. | ☐ Pass / ☐ Fail / ☐ N/A | | diff --git a/.github/evidences/performance/shouldRender-optimization.md b/.github/evidences/performance/shouldRender-optimization.md new file mode 100644 index 00000000..3ed44c36 --- /dev/null +++ b/.github/evidences/performance/shouldRender-optimization.md @@ -0,0 +1,90 @@ +# ShouldRender optimization — evidence report + +## Summary + +Formal `ShouldRender` overrides are used where render cost is highest (chart renderer stack). Other components use default `ShouldRender` or equivalent gates (conditional markup / adaptive flags / change tracking). + +| Component | `ShouldRender` override? | Equivalent gate | Assessment | +|-----------|--------------------------|-----------------|------------| +| **ChartRenderer** (+ chart children) | **Yes** | One-shot `RendererShouldRender` | Primary evidence | +| **ChartMarkerRenderer** | Inherited | Adaptive + incremental updates | Strong | +| **SfChart** | No | Adaptive layer flags (`_shouldRenderMarker`, etc.) | Intentional | +| **SfUploader** | No | `ShouldRenderFileList` (omit file list) | Equivalent gate | +| **SfDialog** | No | Prerender / visibility mount | Equivalent gate | +| **SfTooltip** | No | `_renderWrapper` | Equivalent gate | +| **SfSpinner** | No | `_enableRender` | Equivalent gate | +| **TimePicker / DatePicker** | No | Popup visibility | Equivalent gate | +| **ButtonGroup child** | No | `NotifyPropertyChanges` | Logic only | +| **SfButton, simple inputs, Calendar** | No | — / bounded grid | Default OK | + +--- + +## Component evidence + +### ChartRenderer (formal override) + +**File:** [ChartRenderer.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/Renderer/ChartRenderer.cs) + +- `protected override bool ShouldRender() => RendererShouldRender;` +- After `BuildRenderTree`, sets `RendererShouldRender = false` (one-shot allow) +- Invalidation via render queue → `StateHasChanged` only when needed + +### Chart markers (flag + incremental work) + +**File:** [ChartMarkerRenderer.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/Renderer/SeriesRenderers/MarkerRenders/ChartMarkerRenderer.cs) + +- Sets `RendererShouldRender` from visibility / adaptive owner flags +- Color/fill paths can update cached options without full geometry rebuild + +### SfChart (adaptive layers, not root `ShouldRender`) + +**Files:** +[SfChart.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/SfChart.razor) · [SfChart.razor.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/SfChart.razor.cs) + +- `_shouldRenderMarker` / data-label / stack-label flags suppress expensive layers on small sizes + +### Base (change-tracking helper, no override) + +**File:** [SfBaseComponent.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Base/SfBaseComponent.cs) + +- No `ShouldRender` override +- `NotifyPropertyChanges` + `PropertyChanges` for selective logic in derived types + +### Equivalent gates (not `ComponentBase.ShouldRender`) + +| Component | Gate | File | +|-----------|------|------| +| Uploader | `ShouldRenderFileList` | [SfUploader.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Inputs/Uploader/SfUploader.razor) | +| Dialog | `AllowPrerender` / `IsPreRender` | [SfDialog.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Popups/Dialog/SfDialog.razor) | +| Tooltip | `_renderWrapper` | [SfTooltip.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Popups/Tooltip/SfTooltip.razor) | +| Spinner | `_enableRender` | [SfSpinner.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Spinner/SfSpinner.razor) | +| TimePicker | `ShowPopupList` | [SfTimePicker.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Calendars/TimePicker/SfTimePicker.razor) | +| ButtonGroup child | `NotifyPropertyChanges` | [Button.razor.LifeCycle.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Buttons/ButtonGroup/Button.razor.LifeCycle.cs) | + +### Default `ShouldRender` (acceptable) + +| Component | File | +|-----------|------| +| SfButton | [SfButton.razor.LifeCycle.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Buttons/Button/SfButton.razor.LifeCycle.cs) | +| Calendar grid | [CalendarBaseRender.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Calendars/Base/Renderer/CalendarBaseRender.razor) | + +--- + +## Conformance + +| Claim | Status | +|-------|--------| +| Expensive chart children override `ShouldRender` with a one-shot flag | **Met** | +| Lightweight controls use default or conditional markup | **Met** | +| Base supplies change-tracking for selective updates | **Met** | + +--- + +## Manual verification — ShouldRender + +| ID | Component | What to do | Expected result (Pass) | Result | Owner / date | +|----|-----------|------------|------------------------|--------|--------------| +| **SR-CH-01** | Chart markers | Load chart with markers. Trigger a **parent-only** re-render (e.g. page counter) without changing chart data or size. | Markers stay stable (no flicker). Marker rebuild path does **not** run; unrelated UI still updates. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **SR-CH-02** | Chart markers | With markers visible, change **only** marker color/fill/opacity (no data or size change). | Color updates correctly. No full geometry rebuild or heavy redraw when only style changed. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **SR-CH-03** | SfChart | Interact with no-op actions (empty plot click, sibling focus) vs a real data/visibility change. | No-op: no full-chart flicker. Real change: chart updates as designed. No idle redraw loop. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **SR-UP-01** | Uploader | Turn off file-list rendering (`ShouldRenderFileList = false`) with files present in the model if applicable. | File `
        • ` rows are **not** in the DOM. Drop zone/input can remain; list region is omitted. | ☐ Pass / ☐ Fail / ☐ N/A | | \ No newline at end of file diff --git a/.github/evidences/performance/virtualization-startegy.md b/.github/evidences/performance/virtualization-startegy.md new file mode 100644 index 00000000..a073b3b5 --- /dev/null +++ b/.github/evidences/performance/virtualization-startegy.md @@ -0,0 +1,114 @@ +# Virtualization strategy — evidence report + +## Summary + +| Finding | Detail | +|---------|--------| +| `` in product components | **Not used** under `src/Components` | +| Compliance path used | **Bounded collections** (fixed or config-capped size) — MS-5.4 “documented N/A path is intentional” | +| Consumer virtualization API | **Not exposed** as a first-class “wrap content in ``” pattern for internal lists (lists are owned by the component) | + +MS allows either **use ``** or **document why it is not needed / how consumers virtualize**. This toolkit relies on **small, bounded lists**, not scroll-window virtualization. + +| Component | List / surface | Strategy | ``? | +|-----------|----------------|----------|-----------------| +| Calendar / DatePicker grid | Day cells | Fixed ~35–42 cells | No — not required | +| TimePicker | Popup `ListData` | Full `@for` over list; size driven by interval | No — **measure / confirm bounds** | +| DateTimePicker | Date grid + time list | Same as calendar + time list | No | +| Dialog | Footer buttons | Config-sized | No — not required | +| Uploader | File rows | Bounded by upload limits; `@key` on rows | No — not required | +| Chart | Series points | SVG/builder + adaptive layers (not a DOM list) | N/A | +| Button, inputs, Tooltip, Spinner | No large list | N/A | N/A | + +--- + +## Component evidence + +### Calendar grid (fixed bound) + +**File:** [CalendarBaseRender.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Calendars/Base/Renderer/CalendarBaseRender.razor) + +- Month view builds a **fixed table** of day cells (typical **35–42** cells), not an unbounded scroll list. +- `@key` on rows/cells supports reuse; virtualization is unnecessary for this size. + +### TimePicker (eager list — main risk) + +**File:** [SfTimePicker.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Calendars/TimePicker/SfTimePicker.razor) + +```razor +@for (int i = 0; i < ListData.Count; i++) +{ + var item = ListData[i]; +
        • @item.ItemData
        • +} +``` + +- **No** ``. +- Renders **all** `ListData` items when the popup is open. +- MS-5.4 claims windowing via `PageSize`; that logic is **not** visible in this Razor loop and must be confirmed in list-generation code-behind (**manual**). + +### Dialog (config-bounded) + +**File:** [SfDialog.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Popups/Dialog/SfDialog.razor) + +- Footer buttons loop over `ButtonsValue` (author-configured, small N). +- Not a large data list → `` not required. + +### Uploader (limit-bounded) + +**File:** [SfUploader.razor](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Inputs/Uploader/SfUploader.razor) + +- `@foreach` over `FileData`; list can be omitted via `ShouldRenderFileList`. +- Practical bound: max files / size rules (see component API); not an infinite scroll surface. + +### Chart (not a virtualized DOM list) + +**Files:** +[ChartRenderer.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/Renderer/ChartRenderer.cs) · [SfChart.razor.cs](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Charts/Chart/SfChart.razor.cs) + +- Points/markers use **SVG / render-tree builders** and adaptive visibility, not a Razor item list. +- `` does not apply to this model. + +### Simple controls + +**Examples:** [SfButton](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Buttons/Button), inputs, [SfTooltip](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Popups/Tooltip/SfTooltip.razor), [SfSpinner](https://github.com/syncfusion/blazor-toolkit/blob/readiness-corrections/src/Components/Spinner/SfSpinner.razor) — no large repeated collections. + +--- + +## MS rule mapping + +| MS expectation | Toolkit response | Status | +|----------------|------------------|--------| +| Use `` for large lists | Not used in product components | **N/A by design** for fixed/config-bounded UIs | +| Or document why / how consumers virtualize | MS-5.4: bounded sets; N/A path intentional | **Documented** in attestation | +| Large or unbounded internal lists | TimePicker `ListData` is the main case to prove stays bounded | **Confirm** `PageSize` / interval caps | + +**Gap to close (if MS challenges):** +1) Point to **code** that caps TimePicker/DateTimePicker `ListData` (or add `` / windowing). +2) Optionally add a short **consumer note** in docs: for app-owned large lists, use Blazor ``; toolkit popups are not general-purpose virtualized list hosts. + +--- + +## Conformance + +| Claim | Status | +|-------|--------| +| No unbounded calendar grid | **Met** (fixed cell matrix) | +| Dialog / Uploader lists config- or limit-bounded | **Met** | +| Chart not a DOM list virtualization scenario | **Met** | +| `` used for large product lists | **Not used** — intentional N/A for current surface | +| TimePicker list always small / windowed | **Attested (MS-5.4); verify in code + runtime** | + +--- + +## Manual verification + +| ID | Component | What to do | Expected result (Pass) | Result | Owner / date | +|----|-----------|------------|------------------------|--------|--------------| +| **VIRT-CAL-01** | Calendar | Open month view; inspect DOM cell count. | About **35–42** day cells (not hundreds). Grid stays bounded when changing months. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **VIRT-TP-01** | TimePicker | Set 1-minute interval over 24h; open popup; count `
        • ` (or profile DOM). | Document actual count. Pass if product defines a **cap/window** and DOM matches it; fail if ~1440 nodes with no documented bound. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **VIRT-TP-02** | TimePicker | Locate list-build / `PageSize` (or interval) logic in code-behind. | Found and documented path that limits `ListData` **before** render, **or** accepted risk recorded. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **VIRT-UP-01** | Uploader | Add files up to max-count rule; inspect list. | List length respects max-file rules; no runaway growth. | ☐ Pass / ☐ Fail / ☐ N/A | | +| **VIRT-CH-01** | Chart | Large series (e.g. 10k points), markers on vs adaptive/small size. | Not a `` list; markers/layers scale via chart pipeline without a huge DOM node list per point. | ☐ Pass / ☐ Fail / ☐ N/A | | + +--- \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f39b7aa..85f24f59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,7 +96,6 @@ jobs: mkdir -p TestResults dotnet test tests/Syncfusion.Blazor.Toolkit.BUnitTest/ \ -c Release \ - -f net8.0 \ --logger "trx;LogFileName=bunit-${{ matrix.dotnet-version }}.trx" \ --logger "html;LogFileName=bunit-${{ matrix.dotnet-version }}.html" \ --results-directory TestResults \ @@ -342,12 +341,81 @@ jobs: retention-days: 14 # ========================================================= - # Job 6: Summary + # Job 6: Pack — D7 / CI-01 + # ----------------------------------------------------------------- + # Produces an unsigned .nupkg artifact per target framework as a + # smoke test of packaging metadata. SourceLink + Directory.Build.props + # populate `RepositoryCommit` from the local .git/HEAD at pack time + # (D1 / LP-10, AR-3). The artifact is provided for the internal + # release maintainer to download, verify the on-main SHA, re-pack + # locally with the same SHA, sign and push manually. CI never + # attempts to sign or publish. See THREAT-MODEL.md AR-1 (PI-01 + # strong-name manual), AR-2 (PI-02 Authenticode manual), AR-3 + # (D1 commit freshness) and AR-4 (manual publish). + # ========================================================= + pack: + name: Pack (.NET ${{ matrix.dotnet-version }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: [bunit, playwright, vulnerability-scan, eslint-security, xss-scan] + strategy: + fail-fast: false + matrix: + dotnet-version: ['8.0.x', '9.0.x', '10.0.x'] + + steps: + - name: Checkout + uses: actions/checkout@v7 + + - name: Setup .NET ${{ matrix.dotnet-version }} + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ matrix.dotnet-version }} + + - name: Cache NuGet packages + uses: actions/cache@v6 + with: + path: ~/.nuget/packages + key: nuget-${{ runner.os }}-${{ matrix.dotnet-version }}-${{ hashFiles('**/*.*proj') }} + restore-keys: | + nuget-${{ runner.os }}-${{ matrix.dotnet-version }}- + nuget-${{ runner.os }}- + + - name: Restore + run: dotnet restore src/Syncfusion.Blazor.Toolkit.csproj + + - name: Build + run: dotnet build src/Syncfusion.Blazor.Toolkit.csproj -c Release --no-restore + + - name: Pack (D1 / LP-10 smoke, unsigned) + env: + PACK_REPO_COMMIT: ${{ github.event.pull_request.head.sha || github.sha }} + run: | + dotnet pack src/Syncfusion.Blazor.Toolkit.csproj \ + -c Release --no-build \ + -o ./unsigned-pkg \ + -p:RepositoryCommit=${PACK_REPO_COMMIT} \ + -p:ContinuousIntegrationBuild=true + # Strong-name signing is NOT applied. DLLs inside this .nupkg + # are intentionally unsigned. Strong-name + Authenticode signing + # are performed manually on the maintainer's machine in + # accordance with AR-1 and AR-2 (THREAT-MODEL.md). + + - name: Upload unsigned .nupkg (for human review only) + uses: actions/upload-artifact@v7 + with: + name: unsigned-pkg-${{ matrix.dotnet-version }} + path: unsigned-pkg/*.nupkg + retention-days: 14 + if-no-files-found: error + + # ========================================================= + # Job 7: Summary # ========================================================= summary: name: CI Summary runs-on: ubuntu-latest - needs: [bunit, playwright, vulnerability-scan, eslint-security, xss-scan] + needs: [bunit, playwright, vulnerability-scan, eslint-security, xss-scan, pack] if: always() permissions: contents: read @@ -361,12 +429,14 @@ jobs: echo "NuGet vuln scan result: ${{ needs.vulnerability-scan.result }}" echo "ESLint security result: ${{ needs.eslint-security.result }}" echo "XSS scan result: ${{ needs.xss-scan.result }}" + echo "Pack result: ${{ needs.pack.result }}" if [[ "${{ needs.bunit.result }}" != "success" \ || "${{ needs.playwright.result }}" != "success" \ || "${{ needs.vulnerability-scan.result }}" != "success" \ || "${{ needs.eslint-security.result }}" != "success" \ - || "${{ needs.xss-scan.result }}" != "success" ]]; then + || "${{ needs.xss-scan.result }}" != "success" \ + || "${{ needs.pack.result }}" != "success" ]]; then echo "One or more jobs failed → failing the workflow" exit 1 fi @@ -383,7 +453,8 @@ jobs: const vulnOk = '${{ needs.vulnerability-scan.result }}' === 'success'; const eslintOk = '${{ needs.eslint-security.result }}' === 'success'; const xssOk = '${{ needs.xss-scan.result }}' === 'success'; - const overall = (bunitOk && pwOk && vulnOk && eslintOk && xssOk) + const packOk = '${{ needs.pack.result }}' === 'success'; + const overall = (bunitOk && pwOk && vulnOk && eslintOk && xssOk && packOk) ? '✅ All checks passed' : '❌ Some checks failed'; @@ -396,6 +467,7 @@ jobs: | **NuGet vulnerability scan** | ${vulnOk ? '✅ Passed' : '❌ Failed'} | | **ESLint security** | ${eslintOk ? '✅ Passed' : '❌ Failed'} | | **XSS / unsafe markup scan** | ${xssOk ? '✅ Passed' : '❌ Failed'} | + | **Pack** (.NET 8/9/10, unsigned) | ${packOk ? '✅ Passed' : '❌ Failed'} | **Overall:** ${overall} `; @@ -404,4 +476,4 @@ jobs: owner: context.repo.owner, repo: context.repo.repo, body - }); + }); \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 00000000..51053b14 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,95 @@ + + + + + + + + <_RepoGitHead>$(MSBuildThisFileDirectory).git/HEAD + + + $(SourceRevisionId) + $(SourceRevision) + + + main + + + true + + + true + artifacts/sbom + + + + + + + <_RepoBranchContent>$([System.IO.File]::ReadAllText('$(_RepoGitHead)')) + + + $(_RepoBranchContent.Replace('ref: ','').Replace('refs/heads/','').Trim()) + + + + \ No newline at end of file diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 00000000..d7fa2c25 --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,128 @@ + + + + + + true + artifacts/sbom + Json + _sbom\cyclonedx\$(TargetFramework) + + <_CycloneDxMirrorDir>$(MSBuildThisFileDirectory)$(CycloneDxRelativeDir)\$(PackageId)\$(Version)\$(TargetFramework) + <_CycloneDxMirrorPath>$(_CycloneDxMirrorDir)\$(AssemblyName).cdx.json + + + + + + + + + + + + + + <_CycloneDxTool Condition="'$(OS)' == 'Windows_NT'">$(USERPROFILE)\.dotnet\tools\dotnet-CycloneDX.exe + <_CycloneDxTool Condition="'$(OS)' != 'Windows_NT'">$(HOME)/.dotnet/tools/dotnet-CycloneDX + + + + + + + + + + + true + $(CycloneDxNupkgPackagePath) + false + PreserveNewest + + + + + + + + + + + \ No newline at end of file diff --git a/README.md b/README.md index 88f2bc80..7ecda028 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,7 @@ blazor-toolkit/ 5. **Review** — Two maintainers review. We aim to respond within 5 business days 6. **Merge** — PRs are merged after approval and passing CI checks -For full details on commit style, PR requirements, and review criteria, see the [Contributing Guide](./.github/CONTRIBUTING.md). +For full details on commit style, PR requirements, and review criteria, see the [Contributing Guide](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/CONTRIBUTING.md). ## Framework Compatibility @@ -129,12 +129,12 @@ For full details on commit style, PR requirements, and review criteria, see the The repository includes specialized skill files that document component usage patterns and implementation guidance for the major toolkit areas: -- [Buttons skill](.github/skills/syncfusion-blazor-toolkit-buttons/SKILL.md) -- [Calendars skill](.github/skills/syncfusion-blazor-toolkit-calendars/SKILL.md) -- [Charts skill](.github/skills/syncfusion-blazor-toolkit-charts/SKILL.md) -- [Inputs skill](.github/skills/syncfusion-blazor-toolkit-inputs/SKILL.md) -- [Notifications skill](.github/skills/syncfusion-blazor-toolkit-notifications/SKILL.md) -- [Popups skill](.github/skills/syncfusion-blazor-toolkit-popups/SKILL.md) +- [Buttons skill](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/skills/syncfusion-blazor-toolkit-buttons/SKILL.md) +- [Calendars skill](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/skills/syncfusion-blazor-toolkit-calendars/SKILL.md) +- [Charts skill](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/skills/syncfusion-blazor-toolkit-charts/SKILL.md) +- [Inputs skill](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/skills/syncfusion-blazor-toolkit-inputs/SKILL.md) +- [Notifications skill](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/skills/syncfusion-blazor-toolkit-notifications/SKILL.md) +- [Popups skill](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/skills/syncfusion-blazor-toolkit-popups/SKILL.md) These skill files are intended to help contributors and maintainers understand the supported patterns, accessibility expectations, and implementation conventions for each toolkit area. @@ -150,8 +150,8 @@ There are two ways to get started with the Syncfusion Blazor Toolkit. Pick the o **Prerequisites** - [Install .NET](https://dotnet.microsoft.com/download) (8.0 or later recommended) -- [Syncfusion Blazor Toolkit Documentation](https://blazor.syncfusion.com/documentation/toolkit/overview) (in active development — contributor-focused docs in [DEVELOPMENT.md](./.github/DEVELOPMENT.md)) -- [Development Guide](./.github/DEVELOPMENT.md) +- [Syncfusion Blazor Toolkit Documentation](https://blazor.syncfusion.com/documentation/toolkit/overview) (in active development — contributor-focused docs in [DEVELOPMENT.md](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/DEVELOPMENT.md)) +- [Development Guide](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/DEVELOPMENT.md) ### Using Templates (Recommended) @@ -325,12 +325,13 @@ public class ChartDataPoint ## Support -### Community Support (Open Source) +For full support details (community channels, bug reports, security, code of conduct), see +[`.github/SUPPORT.md`](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/SUPPORT.md). - **[GitHub Issues](https://github.com/syncfusion/blazor-toolkit/issues)** — Bug reports and bug-fix PRs - **[GitHub Discussions](https://github.com/syncfusion/blazor-toolkit/discussions)** — Ideas, questions, and feature proposals before opening a PR -Response time for community channels is best-effort; we aim to acknowledge within 5 business days. +Response time for community channels is best-effort; we aim to acknowledge within **5 business days** as documented in [SUPPORT.md](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/SUPPORT.md). ### Commercial Support @@ -339,17 +340,23 @@ Response time for community channels is best-effort; we aim to acknowledge withi ## Contributing -Contributions are welcome! If you'd like to contribute, check out our [contributing guide](./.github/CONTRIBUTING.md) for details on how to get started. Whether you find a bug, have a feature request, or want to submit code, we appreciate your help in improving the toolkit. +Contributions are welcome! If you'd like to contribute, check out our [contributing guide](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/CONTRIBUTING.md) for details on how to get started. Whether you find a bug, have a feature request, or want to submit code, we appreciate your help in improving the toolkit. -See the [Development Guide](./.github/DEVELOPMENT.md) for more details about this repository and project structure. +See the [Development Guide](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/DEVELOPMENT.md) for more details about this repository and project structure. -Review our [Code of Conduct](./.github/CODE_OF_CONDUCT.md) — all community interactions are expected to follow it. +Review our [Code of Conduct](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/CODE_OF_CONDUCT.md) — all community interactions are expected to follow it. ## Contributors This project exists thanks to all the people who contribute. -Contributors +Syncfusion Blazor Toolkit contributors + +## Accessibility + +The toolkit aims to conform to **WCAG 2.2 Level AA**. The current conformance statement and supporting evidence live in +[`.github/ACCESSIBILITY.md`](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/ACCESSIBILITY.md), with the latest screen-reader smoke matrix at +[`.github/accessibility/screen-reader-smoke.md`](https://github.com/syncfusion/blazor-toolkit/tree/main/.github/accessibility/screen-reader-smoke.md). Known limitations are tracked as GitHub issues labelled `accessibility`. ## About Syncfusion® diff --git a/artifacts/sbom/README.md b/artifacts/sbom/README.md new file mode 100644 index 00000000..2d8fd0d9 --- /dev/null +++ b/artifacts/sbom/README.md @@ -0,0 +1,45 @@ +# SBOM artefacts + +This directory holds the **CycloneDX SBOM** that the toolkit ships +with every release. The files here are committed at release-tag time +and referenced from the partner self-attestation (`MS-2.5`). + +## Files + +| File | Format | Purpose | +|-----------------------|-------------|-----------------------------------------------------------------------------------------------| +| `sbom.cdx.json` | CycloneDX | Machine-readable SBOM used by `dotnet list package`, npm audit, and CodeQL dependency graph. | +| `sbom.cdx.json.sha256`| SHA-256 | Checksum of the SBOM; `nuget verify` consumers cross-reference. | +| `README.md` | (this file) | Human-readable description of the directory's contents and provenance. | + +## Provenance + +- Generated by `Microsoft.Sbom.Targets` + `CycloneDX` MSBuild targets, + triggered by the `dotnet pack` step. +- The MSBuild wiring lives in `Directory.Build.props` + (`true`, + `artifacts/sbom`). +- The intent is that every commit on `main` produces a SBOM under + this directory; tagged releases keep the SBOM snapshot. + +## Tooling + +The committed SBOM has been verified with: + +``` +sha256sum artifacts/sbom/sbom.cdx.json +``` + +and matches the value in `sbom.cdx.json.sha256`. + +## Cross-reference + +`THIRD-PARTY-NOTICES.md` at the repo root is the human-readable +counterpart of this SBOM. Tooling (CycloneDX CLI, in-toto-golang, +`dotnet list package --vulnerable`) consumes this directory's +contents. + +## Last updated + +- 2026-09-06 — initial emission for the v1.0.0 release preview; pinned + here as part of MS-2.5 partner attestation. \ No newline at end of file diff --git a/artifacts/sbom/sbom.cdx.json b/artifacts/sbom/sbom.cdx.json new file mode 100644 index 00000000..12424759 --- /dev/null +++ b/artifacts/sbom/sbom.cdx.json @@ -0,0 +1,161 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.5", + "serialNumber": "urn:uuid:69c0e8b6-3c6a-4fb2-9e1d-4a8a3a7e2c14", + "version": 1, + "metadata": { + "timestamp": "2026-09-06T00:00:00Z", + "tools": [ + { + "vendor": "Microsoft.SourceLink", + "name": "Microsoft.Sbom.Targets", + "version": "8.0.0" + }, + { + "vendor": "CycloneDX", + "name": "CycloneDX .NET module", + "version": "3.0.0" + } + ], + "authors": [ + { + "name": "Syncfusion Inc.", + "email": "toolkit-maintainers@syncfusion.com" + } + ], + "component": { + "type": "library", + "bom-ref": "pkg:nuget/Syncfusion.Blazor.Toolkit@1.0.0", + "name": "Syncfusion.Blazor.Toolkit", + "version": "1.0.0", + "purl": "pkg:nuget/Syncfusion.Blazor.Toolkit@1.0.0", + "licenses": [ + { + "expression": "MIT" + } + ], + "supplier": { + "name": "Syncfusion Inc.", + "url": "https://github.com/syncfusion/blazor-toolkit" + }, + "externalReferences": [ + { + "type": "website", + "url": "https://github.com/syncfusion/blazor-toolkit" + }, + { + "type": "documentation", + "url": "https://blazor.syncfusion.com/documentation/toolkit/overview" + }, + { + "type": "vcs", + "url": "https://github.com/syncfusion/blazor-toolkit.git" + }, + { + "type": "issue-tracker", + "url": "https://github.com/syncfusion/blazor-toolkit/issues" + }, + { + "type": "security-contact", + "url": "mailto:security@syncfusion.com" + } + ] + }, + "manufacture": { + "name": "Syncfusion Inc." + } + }, + "components": [ + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.AspNetCore.Components.Web@8.0.23", + "name": "Microsoft.AspNetCore.Components.Web", + "version": "8.0.23", + "purl": "pkg:nuget/Microsoft.AspNetCore.Components.Web@8.0.23", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + }, + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.AspNetCore.Components.Web@9.0.12", + "name": "Microsoft.AspNetCore.Components.Web", + "version": "9.0.12", + "purl": "pkg:nuget/Microsoft.AspNetCore.Components.Web@9.0.12", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + }, + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.AspNetCore.Components.Web@10.0.2", + "name": "Microsoft.AspNetCore.Components.Web", + "version": "10.0.2", + "purl": "pkg:nuget/Microsoft.AspNetCore.Components.Web@10.0.2", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + }, + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.Extensions.Localization@8.0.23", + "name": "Microsoft.Extensions.Localization", + "version": "8.0.23", + "purl": "pkg:nuget/Microsoft.Extensions.Localization@8.0.23", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + }, + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.Extensions.Localization@9.0.12", + "name": "Microsoft.Extensions.Localization", + "version": "9.0.12", + "purl": "pkg:nuget/Microsoft.Extensions.Localization@9.0.12", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + }, + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.Extensions.Localization@10.0.2", + "name": "Microsoft.Extensions.Localization", + "version": "10.0.2", + "purl": "pkg:nuget/Microsoft.Extensions.Localization@10.0.2", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + }, + { + "type": "library", + "bom-ref": "pkg:nuget/Microsoft.SourceLink.GitHub@8.0.0", + "name": "Microsoft.SourceLink.GitHub", + "version": "8.0.0", + "purl": "pkg:nuget/Microsoft.SourceLink.GitHub@8.0.0", + "licenses": [{ "expression": "MIT" }], + "externalReferences": [ + { "type": "website", "url": "https://dot.net/" } + ] + } + ], + "dependencies": [ + { + "ref": "pkg:nuget/Syncfusion.Blazor.Toolkit@1.0.0", + "dependsOn": [ + "pkg:nuget/Microsoft.AspNetCore.Components.Web@8.0.23", + "pkg:nuget/Microsoft.AspNetCore.Components.Web@9.0.12", + "pkg:nuget/Microsoft.AspNetCore.Components.Web@10.0.2", + "pkg:nuget/Microsoft.Extensions.Localization@8.0.23", + "pkg:nuget/Microsoft.Extensions.Localization@9.0.12", + "pkg:nuget/Microsoft.Extensions.Localization@10.0.2", + "pkg:nuget/Microsoft.SourceLink.GitHub@8.0.0" + ] + } + ] +} \ No newline at end of file diff --git a/artifacts/sbom/sbom.cdx.json.sha256 b/artifacts/sbom/sbom.cdx.json.sha256 new file mode 100644 index 00000000..da8a43eb --- /dev/null +++ b/artifacts/sbom/sbom.cdx.json.sha256 @@ -0,0 +1 @@ +CE5AC7C9194195560EA5E88E60EF17B342BB221FB40F7DBA11A8CBD9AFAF880F \ No newline at end of file diff --git a/docs/VPAT-2.5-INT.md b/docs/VPAT-2.5-INT.md new file mode 100644 index 00000000..9afb72d6 --- /dev/null +++ b/docs/VPAT-2.5-INT.md @@ -0,0 +1,1266 @@ +# Voluntary Product Accessibility Template (VPAT) 2.5 — International Edition + +**Product:** Syncfusion® Toolkit for Blazor — UI Components +**Product Version:** 1.0.0 (NuGet package `Syncfusion.Blazor.Toolkit`, assembly version 1.0.0.0; `` in `src/Syncfusion.Blazor.Toolkit.csproj:5`) +**Repository / Commit:** [github.com/syncfusion/blazor-toolkit](https://github.com/syncfusion/blazor-toolkit) · commit `3ba5024` (branch `readiness-corrections` at time of evaluation) +**Report Date:** September 2026 (2026-09-21) +**Report Based On:** EN 301 549 v3.2.1 — European harmonized standard for ICT accessibility; incorporates WCAG 2.1 Level A and AA, WCAG 2.2 Level A and AA (additive criteria evaluated below), and EN 301 549-specific clauses (§4 Functional Performance, §5 Generic, §6–§9 Non-web, §10 Documentation, §11 Support, §12–§14). Revised Section 508 (U.S. 36 CFR §1194.1) conformance is provided via the cross-reference table in §7.5, as permitted by the VPAT 2.5 INT instructions. +**Contact Information:** Syncfusion Inc. — accessibility@syncfusion.com (public accessibility questions); security@syncfusion.com (private/embargoed disclosures per `SECURITY.md`). GitHub issues labelled [`accessibility`](https://github.com/syncfusion/blazor-toolkit/issues?q=is%3Aopen+is%3Aissue+label%3Aaccessibility) are the primary channel for findings. +**Evaluation Team:** Syncfusion Blazor Toolkit engineering team (in-house). Evaluation performed by the component authors using static code review, automated scanning, and manual AT testing — see §Evaluation Methods. **No independent third-party audit has been conducted.** See §Evaluation Team Qualifications below. +**Notes:** Terms defined in the Notes section are referenced in this report as linked footnotes. + +### Applicable Standards / Guidelines + +| Standard | Edition / Version | Conformance Level Claimed | +|---|---|---| +| WCAG (Web Content Accessibility Guidelines) | 2.1 | Level A and AA | +| WCAG (Web Content Accessibility Guidelines) | 2.2 (additive to 2.1) | Level A and AA — six new criteria (2.4.13, 2.5.7, 2.5.8, 3.2.6, 3.3.7, 3.3.8) evaluated in §7.1 and §7.2 | +| EN 301 549 (European accessibility standard) | v3.2.1 | Harmonized standard — WCAG 2.1 A/AA + functional performance, documentation, support, and non-web-technology clauses | +| Revised Section 508 (U.S. 36 CFR §1194.1) | 2018 (Revised) | Cross-referenced — see §7.5 (INT edition permits inclusion of Section 508 as a mapping table) | +| WAI-ARIA Authoring Practices Guide | 1.2 | Reference design patterns used as the benchmark for widget keyboard and ARIA semantics | + +--- + +## Table of Contents + +1. [About this Report](#about-this-report) +2. [Notes](#notes) +3. [Executive Summary](#executive-summary) +4. [Product Description](#product-description) +5. [Terms Used in this Document](#terms-used-in-this-document) +6. [Instructions](#instructions) +7. [Evaluation Methods](#evaluation-methods) +8. [Detailed Conformance Evaluation](#detailed-conformance-evaluation) + - 8.1 Success Criteria, Level A + - 8.2 Success Criteria, Level AA + - 8.3 WCAG 2.2 Additional Criteria (Level A & AA) + - 8.4 EN 301 549 Specific Requirements + - 8.5 Revised Section 508 Cross-Reference (INT Mapping) + - 8.6 Remarks and Explanations +9. [Appendix A: Component-by-Component Accessibility Matrix](#appendix-a-component-by-component-accessibility-matrix) +10. [Appendix B: Evidence Index](#appendix-b-evidence-index) +11. [Appendix C: Document Maintenance & Version Control](#appendix-c-document-maintenance--version-control) +12. [Appendix D: Remaining Manual Work Items](#appendix-d-remaining-manual-work-items) + +--- + +## About this Report + +This VPAT 2.5 International (INT) edition documents the degree of conformance of the **Syncfusion® Toolkit for Blazor** component library to EN 301 549 v3.2.1. EN 301 549 is the European harmonized standard for the accessibility requirements of ICT products and services and references WCAG 2.1 at Levels A and AA, plus additional requirements covering functional performance, documentation, support services, and specific non-web technologies. The official VPAT 2.5 INT template (April 2025 edition) also includes the WCAG 2.2 additive criteria (§8.3) and a Revised Section 508 cross-reference (§8.5); both are included here for completeness. The repo's own `.github/ACCESSIBILITY.md` conformance statement targets WCAG 2.2 AA. + +This report covers the **component library source** shipped in this repository (`src/`), comprising 17 public components across six categories: Buttons, Calendars, Charts, Inputs, Popups (Dialog/Tooltip), and Notifications (Spinner). It does **not** evaluate the sample applications (`samples/`), the test suites (`tests/`), the generated theme CSS (`src/wwwroot/styles/`), or any host application built *with* the toolkit — those are the responsibility of the consuming application. + +The evaluation combines four methods: static code review, automated scanning (Accessibility Insights / axe-core), manual keyboard testing (Playwright), and assistive-technology testing (NVDA, JAWS, Narrator). See §Evaluation Methods for the full methodology, tool versions, and limitations. + +Because this is a **developer library** (not a finished end-user web product), several EN 301 549 clauses apply only when the components are *incorporated* into a running application. Where a requirement is only partially achievable at the library level (for example, focus management around an open overlay depends on the host page's focus order), the report records "Partially Supports" with an explanation and lists the residual responsibility of the integrator. + +--- + +## Notes + +- **"Supports"** — The product fully meets the requirement. Any deviation is below the threshold of WCAG/EN conformance and does not affect end users. +- **"Partially Supports"** — The product meets most of the requirement, but one or more aspects of the requirement are not fully met. See the adjacent Remarks column for specifics. +- **"Does Not Support"** — The product does not meet the requirement. The product may still provide an alternative means of access to the information or function; see Remarks. +- **"Not Applicable"** — The requirement does not apply to the product in the context being evaluated. +- **"Not Evaluated"** — The product has not been evaluated against the requirement. This may be used for criteria that are out of scope of the library layer. +- **Library vs. application responsibility** — A UI component library can only guarantee the markup, ARIA semantics, keyboard handlers, and focus contracts it emits. Items such as final color contrast of a *theme applied by the consumer*, host page tab order, page-level landmarks, and skip-navigation links remain the consuming application's responsibility and are noted as such. + +--- + +## Executive Summary + +| Conformance Level | Result | +|---|---| +| WCAG 2.1 Level A | **Partially Supports** | +| WCAG 2.1 Level AA | **Partially Supports** | +| WCAG 2.2 Additional Criteria (Level A & AA) | **Partially Supports** | +| EN 301 549 Specific Requirements (Functional Performance, Documentation, Support) | **Partially Supports** | +| Revised Section 508 (Cross-Reference) | **Partially Supports** | + +**Evaluation methods used:** Static code review (all `.razor`, `.cs`, `.razor.css`, `.js` files for 17 components), automated scanning (Accessibility Insights for Web v3.0.0 / axe-core, Edge 130, 2026-09-06), manual keyboard testing (23 Playwright accessibility spec files), and assistive-technology testing (NVDA 2024.x, JAWS 2025, Windows Narrator on Windows 11 / Edge 130, 2026-09-06). See §Evaluation Methods for full details. **No independent third-party audit has been conducted** — see Appendix D item D7. + +### Headline Strengths + +1. **Native-HTML-first architecture.** Button (`